Compare commits
3
Commits
main
..
41aeb2b89a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41aeb2b89a | ||
|
|
2569e0f836 | ||
|
|
81b5850b62 |
-1
@@ -163,7 +163,6 @@ public class CommandSourceService {
|
||||
|| "CURRENTPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
||||
|| "SHAREPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
||||
|| "FIXEDDEPOSITPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
||||
|| "RECURRINGDEPOSITPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
||||
|| "USER".equalsIgnoreCase(commandSource.getEntityName())
|
||||
|| "ROLE".equalsIgnoreCase(commandSource.getEntityName())) {
|
||||
return false;
|
||||
|
||||
+56
-9
@@ -95,6 +95,62 @@ public class CommandWrapperBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder createInvestmentParty() {
|
||||
this.actionName = "CREATE";
|
||||
this.entityName = "INVESTMENTPARTY";
|
||||
this.entityId = null;
|
||||
this.href = "/investment/parties";
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder updateInvestmentParty(final Long partyId) {
|
||||
this.actionName = "UPDATE";
|
||||
this.entityName = "INVESTMENTPARTY";
|
||||
this.entityId = partyId;
|
||||
this.href = "/investment/parties/" + partyId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder deleteInvestmentParty(final Long partyId) {
|
||||
this.actionName = "DELETE";
|
||||
this.entityName = "INVESTMENTPARTY";
|
||||
this.entityId = partyId;
|
||||
this.href = "/investment/parties/" + partyId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder createInvestmentScript() {
|
||||
this.actionName = "CREATE";
|
||||
this.entityName = "INVESTMENTSCRIPT";
|
||||
this.entityId = null;
|
||||
this.href = "/investment/scripts";
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder updateInvestmentScript(final Long scriptId) {
|
||||
this.actionName = "UPDATE";
|
||||
this.entityName = "INVESTMENTSCRIPT";
|
||||
this.entityId = scriptId;
|
||||
this.href = "/investment/scripts/" + scriptId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder createInvestmentPurchase() {
|
||||
this.actionName = "CREATE";
|
||||
this.entityName = "INVESTMENTPURCHASE";
|
||||
this.entityId = null;
|
||||
this.href = "/investment/purchases";
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder deleteInvestmentPurchase(final Long purchaseId) {
|
||||
this.actionName = "DELETE";
|
||||
this.entityName = "INVESTMENTPURCHASE";
|
||||
this.entityId = purchaseId;
|
||||
this.href = "/investment/purchases/" + purchaseId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder createChequeStockLot() {
|
||||
this.actionName = "CREATE";
|
||||
this.entityName = "CHEQUESTOCKLOT";
|
||||
@@ -3157,15 +3213,6 @@ public class CommandWrapperBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder extendTenureRecurringDepositAccount(final Long accountId) {
|
||||
this.actionName = "EXTENDTENURE";
|
||||
this.entityName = "RECURRINGDEPOSITACCOUNT";
|
||||
this.entityId = accountId;
|
||||
this.savingsId = accountId;
|
||||
this.href = "/recurringdepositaccounts/" + accountId + "?command=extendTenure";
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder prematureCloseRecurringDepositAccount(final Long accountId) {
|
||||
this.actionName = "PREMATURECLOSE";
|
||||
this.entityName = "RECURRINGDEPOSITACCOUNT";
|
||||
|
||||
-2
@@ -75,8 +75,6 @@ public enum JobName {
|
||||
AML_STR_WEEKLY_DIGEST("AML STR Weekly Digest"), //
|
||||
CLIENT_BEHAVIOUR_WINDOW("Client Behaviour Window Computation"), //
|
||||
SAVE_RBI_FORM_A_RETURN("Save RBI Form A Return"), //
|
||||
EXECUTE_RD_AUTO_DEBIT("Execute RD Auto Debit"), //
|
||||
APPLY_RD_MISSED_INSTALLMENT_PENALTY("Apply RD Missed Installment Penalty"), //
|
||||
; //
|
||||
|
||||
private final String name;
|
||||
|
||||
-10
@@ -432,10 +432,6 @@ public final class DepositsApiConstants {
|
||||
recurringDepositRequestData.add("prematureWithdrawalOption");
|
||||
recurringDepositRequestData.add("tenureFlexibility");
|
||||
recurringDepositRequestData.add("lienMarked");
|
||||
recurringDepositRequestData.add("lienPercentage");
|
||||
recurringDepositRequestData.add("missedInstallmentGraceDays");
|
||||
recurringDepositRequestData.add("missedInstallmentPenaltyType");
|
||||
recurringDepositRequestData.add("missedInstallmentPenaltyAmount");
|
||||
recurringDepositRequestData.add("withdrawalFeeForTransfers");
|
||||
recurringDepositRequestData.add("enableLockinPeriod");
|
||||
recurringDepositRequestData.add("maturityInstructions");
|
||||
@@ -495,10 +491,6 @@ public final class DepositsApiConstants {
|
||||
recurringDepositRequestData.add("prematureWithdrawalOption");
|
||||
recurringDepositRequestData.add("tenureFlexibility");
|
||||
recurringDepositRequestData.add("lienMarked");
|
||||
recurringDepositRequestData.add("lienPercentage");
|
||||
recurringDepositRequestData.add("missedInstallmentGraceDays");
|
||||
recurringDepositRequestData.add("missedInstallmentPenaltyType");
|
||||
recurringDepositRequestData.add("missedInstallmentPenaltyAmount");
|
||||
recurringDepositRequestData.add("maturityInstructions");
|
||||
recurringDepositRequestData.add("creditMaturityProceeds");
|
||||
recurringDepositRequestData.add("penaltyGracePeriodMissedInstallments");
|
||||
@@ -656,8 +648,6 @@ public final class DepositsApiConstants {
|
||||
recurringDepositRequestData.add(SavingsApiConstants.balanceSwapOptionParamName);
|
||||
recurringDepositRequestData.add(SavingsApiConstants.msmeParamName);
|
||||
recurringDepositRequestData.add(SavingsApiConstants.autoDebitFacilityParamName);
|
||||
// Lien Marked: how much of this deposit may be given as loan, capped by its product's percentage.
|
||||
recurringDepositRequestData.add("lienPercentage");
|
||||
recurringDepositRequestData.add(isSeniorCitizenParamName);
|
||||
recurringDepositRequestData.add(seniorCitizenInterestRateParamName);
|
||||
return recurringDepositRequestData;
|
||||
|
||||
-14
@@ -234,20 +234,6 @@ public final class SavingsAccountData implements Serializable {
|
||||
return this.transferInterestToLinkedAccount;
|
||||
}
|
||||
|
||||
/**
|
||||
* True for a recurring deposit opened under a product whose Interest Calculation Type is "Simple": its interest
|
||||
* never compounds. Transient because it is only read by the batch interest-posting engine.
|
||||
*/
|
||||
private transient boolean simpleInterest;
|
||||
|
||||
public void setSimpleInterest(final boolean simpleInterest) {
|
||||
this.simpleInterest = simpleInterest;
|
||||
}
|
||||
|
||||
public boolean isSimpleInterest() {
|
||||
return this.simpleInterest;
|
||||
}
|
||||
|
||||
public static SavingsAccountData importInstanceIndividual(Long clientId, Long productId, Long fieldOfficerId, LocalDate submittedOnDate,
|
||||
BigDecimal nominalAnnualInterestRate, EnumOptionData interestCompoundingPeriodTypeEnum,
|
||||
EnumOptionData interestPostingPeriodTypeEnum, EnumOptionData interestCalculationTypeEnum,
|
||||
|
||||
-6
@@ -180,12 +180,6 @@ public final class SavingsHelper {
|
||||
immediateWithdrawalOfInterest);
|
||||
}
|
||||
|
||||
public Money calculateInterestForAllPostingPeriods(final MonetaryCurrency currency, final List<PostingPeriod> allPeriods,
|
||||
LocalDate accountLockedUntil, Boolean immediateWithdrawalOfInterest, final boolean simpleInterest) {
|
||||
return COMPOUND_INTEREST_HELPER.calculateInterestForAllPostingPeriods(currency, allPeriods, accountLockedUntil,
|
||||
immediateWithdrawalOfInterest, simpleInterest);
|
||||
}
|
||||
|
||||
public Collection<Long> fetchPostInterestTransactionIds(Long accountId) {
|
||||
return this.accountTransfersReadPlatformService.fetchPostInterestTransactionIds(accountId);
|
||||
}
|
||||
|
||||
+1
-35
@@ -40,18 +40,6 @@ public class CompoundInterestHelper {
|
||||
*/
|
||||
public Money calculateInterestForAllPostingPeriods(final MonetaryCurrency currency, final List<PostingPeriod> allPeriods,
|
||||
LocalDate lockUntil, Boolean interestTransferEnabled) {
|
||||
return calculateInterestForAllPostingPeriods(currency, allPeriods, lockUntil, interestTransferEnabled, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param simpleInterest
|
||||
* - when true, earned interest is never added back to the balance it is earned on, so every period
|
||||
* earns on the principal alone (a recurring deposit whose product's Interest Calculation Type is
|
||||
* "Simple"). Compounding and posting periods still decide when interest is recognised and posted;
|
||||
* they just stop changing how much is earned.
|
||||
*/
|
||||
public Money calculateInterestForAllPostingPeriods(final MonetaryCurrency currency, final List<PostingPeriod> allPeriods,
|
||||
LocalDate lockUntil, Boolean interestTransferEnabled, final boolean simpleInterest) {
|
||||
|
||||
// sum up the 'rounded' values that are posted each posting period
|
||||
Money interestEarned = Money.zero(currency);
|
||||
@@ -59,8 +47,7 @@ public class CompoundInterestHelper {
|
||||
// total interest earned in previous periods but not yet recognised
|
||||
BigDecimal compoundedInterest = BigDecimal.ZERO;
|
||||
BigDecimal unCompoundedInterest = BigDecimal.ZERO;
|
||||
final CompoundInterestValues compoundInterestValues = simpleInterest ? new SimpleInterestValues()
|
||||
: new CompoundInterestValues(compoundedInterest, unCompoundedInterest);
|
||||
final CompoundInterestValues compoundInterestValues = new CompoundInterestValues(compoundedInterest, unCompoundedInterest);
|
||||
for (final PostingPeriod postingPeriod : allPeriods) {
|
||||
|
||||
final BigDecimal interestEarnedThisPeriod = postingPeriod.calculateInterest(compoundInterestValues);
|
||||
@@ -80,25 +67,4 @@ public class CompoundInterestHelper {
|
||||
|
||||
return interestEarned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interest values that refuse to compound: whatever a compounding period hands over to be compounded is dropped,
|
||||
* so the balance interest is earned on never includes interest already earned.
|
||||
*/
|
||||
private static final class SimpleInterestValues extends CompoundInterestValues {
|
||||
|
||||
SimpleInterestValues() {
|
||||
super(BigDecimal.ZERO, BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCompoundedInterest(final BigDecimal interestToBeCompounded) {
|
||||
super.setCompoundedInterest(BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSimpleInterest() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-5
@@ -50,9 +50,4 @@ public class CompoundInterestValues {
|
||||
this.uncompoundedInterest = BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
/** True when earned interest must never earn interest itself - not across periods, and not within one either. */
|
||||
public boolean isSimpleInterest() {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-12
@@ -94,18 +94,6 @@ public final class DailyCompoundingPeriod implements CompoundingPeriod {
|
||||
return interestEarned;
|
||||
}
|
||||
|
||||
/** Interest on each day's balance alone, none of it added to the balance - for simple interest. */
|
||||
public BigDecimal calculateSimpleInterest(final BigDecimal interestRateAsFraction, final long daysInYear,
|
||||
final BigDecimal minBalanceForInterestCalculation, final BigDecimal overdraftInterestRateAsFraction,
|
||||
final BigDecimal minOverdraftForInterestCalculation) {
|
||||
BigDecimal interestEarned = BigDecimal.ZERO;
|
||||
for (final EndOfDayBalance balance : this.endOfDayBalances) {
|
||||
interestEarned = interestEarned.add(balance.calculateInterestOnBalance(BigDecimal.ZERO, interestRateAsFraction, daysInYear,
|
||||
minBalanceForInterestCalculation, overdraftInterestRateAsFraction, minOverdraftForInterestCalculation));
|
||||
}
|
||||
return interestEarned;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalDateInterval getPeriodInterval() {
|
||||
return this.periodInterval;
|
||||
|
||||
+4
-11
@@ -301,17 +301,10 @@ public final class PostingPeriod {
|
||||
// to be applied to the balanced for interest calculation
|
||||
for (final CompoundingPeriod compoundingPeriod : this.compoundingPeriods) {
|
||||
|
||||
// A daily compounding period compounds day by day inside itself, so simple interest has to bypass it - zeroing
|
||||
// what is carried between periods alone still left a month's interest earning interest within the month.
|
||||
final BigDecimal interestUnrounded = compoundInterestValues.isSimpleInterest()
|
||||
&& compoundingPeriod instanceof DailyCompoundingPeriod dailyPeriod
|
||||
? dailyPeriod.calculateSimpleInterest(this.interestRateAsFraction, this.daysInYear,
|
||||
this.minBalanceForInterestCalculation.getAmount(), this.overdraftInterestRateAsFraction,
|
||||
this.minOverdraftForInterestCalculation.getAmount())
|
||||
: compoundingPeriod.calculateInterest(this.interestCompoundingType, this.interestCalculationType,
|
||||
compoundInterestValues.getcompoundedInterest(), this.interestRateAsFraction, this.daysInYear,
|
||||
this.minBalanceForInterestCalculation.getAmount(), this.overdraftInterestRateAsFraction,
|
||||
this.minOverdraftForInterestCalculation.getAmount());
|
||||
final BigDecimal interestUnrounded = compoundingPeriod.calculateInterest(this.interestCompoundingType,
|
||||
this.interestCalculationType, compoundInterestValues.getcompoundedInterest(), this.interestRateAsFraction,
|
||||
this.daysInYear, this.minBalanceForInterestCalculation.getAmount(), this.overdraftInterestRateAsFraction,
|
||||
this.minOverdraftForInterestCalculation.getAmount());
|
||||
BigDecimal unCompoundedInterest = compoundInterestValues.getuncompoundedInterest().add(interestUnrounded);
|
||||
compoundInterestValues.setuncompoundedInterest(unCompoundedInterest);
|
||||
LocalDate compoundingPeriodEndDate = compoundingPeriod.getPeriodInterval().endDate();
|
||||
|
||||
+3
-28
@@ -650,7 +650,7 @@ public class AccountingProcessorHelper {
|
||||
*/
|
||||
public void createInterestPostingJournalEntriesForSavings(final Office office, final String currencyCode,
|
||||
final Long savingsProductId, final Long paymentTypeId, final Long savingsId, final String transactionId,
|
||||
final LocalDate transactionDate, final BigDecimal amount, final boolean isReversal, final boolean payableCarriesAccrual) {
|
||||
final LocalDate transactionDate, final BigDecimal amount, final boolean isReversal) {
|
||||
|
||||
final GLAccount controlAccount = getLinkedGLAccountForSavingsProduct(savingsProductId,
|
||||
CashAccountsForSavings.SAVINGS_CONTROL.getValue(), paymentTypeId);
|
||||
@@ -665,14 +665,10 @@ public class AccountingProcessorHelper {
|
||||
.ofNullable(optionalLinkedGLAccountForSavingsProduct(savingsProductId,
|
||||
AccrualAccountsForSavings.INTEREST_PAYABLE.getValue(), paymentTypeId))
|
||||
.or(() -> Optional.ofNullable(namedSavingsGlAccount(savingsProductId,
|
||||
SAVINGS_INTEREST_PAYABLE_MAPPING, "Fixed Deposit Interest Payable", "Recurring Deposit Interest Payable",
|
||||
"Interest Payable", "Interest Payable Account")))
|
||||
SAVINGS_INTEREST_PAYABLE_MAPPING, "Fixed Deposit Interest Payable", "Interest Payable", "Interest Payable Account")))
|
||||
.orElse(controlAccount);
|
||||
// The RD product screen saves its paid account as "Recurring Deposit Interest Paid"; without that name here an
|
||||
// RD's interest skipped the paid leg even when the product had one mapped.
|
||||
final GLAccount paidAccount = Optional.ofNullable(namedSavingsGlAccount(savingsProductId,
|
||||
SAVINGS_INTEREST_PAID_MAPPING, "Fixed Deposit Interest Paid", "Recurring Deposit Interest Paid", "Interest Paid",
|
||||
"Interest Paid Account"))
|
||||
SAVINGS_INTEREST_PAID_MAPPING, "Fixed Deposit Interest Paid", "Interest Paid", "Interest Paid Account"))
|
||||
.orElse(payableAccount);
|
||||
|
||||
log.info("createInterestPostingJournalEntriesForSavings: savingsProductId={}, payableAccount=[{} - {}], paidAccount=[{} - {}], controlAccount=[{} - {}], interestExpense=[{} - {}], amount={}, isReversal={}",
|
||||
@@ -710,19 +706,6 @@ public class AccountingProcessorHelper {
|
||||
// debit balance would come back negative and every posting would raise a shortfall larger
|
||||
// than the interest itself.
|
||||
final boolean payableIsLiability = GLAccountType.LIABILITY.getValue().equals(payableAccount.getType());
|
||||
if (!payableCarriesAccrual) {
|
||||
// Cash basis: nothing ever accrues into the payable, so whatever already sits on it - an opening balance
|
||||
// brought over at migration, or another product's interest sharing the GL - is not this interest. Reading
|
||||
// it as "already accrued" skipped the expense and drew that balance down instead, leaving the cost off the
|
||||
// P&L. The whole amount is charged and raised, and a reversal or clawback takes it back the same way.
|
||||
if (payableIsLiability) {
|
||||
postSavingsLeg(office, currencyCode, interestOnSavingsAccount, payableAccount, savingsId, transactionId, transactionDate,
|
||||
amount, isReversal);
|
||||
}
|
||||
postPayableToMember(office, currencyCode, payableAccount, paidAccount, controlAccount, savingsId, transactionId,
|
||||
transactionDate, amount, isReversal);
|
||||
return;
|
||||
}
|
||||
final BigDecimal alreadyAccrued = payableBalance(office, payableAccount, currencyCode);
|
||||
final BigDecimal shortfall = amount.subtract(alreadyAccrued).max(BigDecimal.ZERO);
|
||||
log.info("createInterestPostingJournalEntriesForSavings: alreadyAccrued={}, shortfall={}, payableIsLiability={}, isReversal={}",
|
||||
@@ -735,14 +718,6 @@ public class AccountingProcessorHelper {
|
||||
shortfall, false);
|
||||
}
|
||||
|
||||
postPayableToMember(office, currencyCode, payableAccount, paidAccount, controlAccount, savingsId, transactionId, transactionDate,
|
||||
amount, isReversal);
|
||||
}
|
||||
|
||||
/** Stages 2 and 3: out of the payable, through the paid account when the product names one, into the member. */
|
||||
private void postPayableToMember(final Office office, final String currencyCode, final GLAccount payableAccount,
|
||||
final GLAccount paidAccount, final GLAccount controlAccount, final Long savingsId, final String transactionId,
|
||||
final LocalDate transactionDate, final BigDecimal amount, final boolean isReversal) {
|
||||
final boolean hasSeparatePaidAccount = !payableAccount.getId().equals(paidAccount.getId());
|
||||
if (hasSeparatePaidAccount) {
|
||||
log.info("createInterestPostingJournalEntriesForSavings: posting Stage 2 [Dr {} / Cr {} = {}] and Stage 3 [Dr {} / Cr {} = {}]",
|
||||
|
||||
+1
-1
@@ -194,7 +194,7 @@ public class AccrualBasedAccountingProcessorForSavings implements AccountingProc
|
||||
// payable-to-control pair that used to be here, which skipped interest-paid
|
||||
// entirely and left the expense off the P&L.
|
||||
this.helper.createInterestPostingJournalEntriesForSavings(office, currencyCode, savingsProductId, paymentTypeId,
|
||||
savingsId, transactionId, transactionDate, postingAmount, effectiveIsReversal, true);
|
||||
savingsId, transactionId, transactionDate, postingAmount, effectiveIsReversal);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -186,7 +186,7 @@ public class CashBasedAccountingProcessorForSavings implements AccountingProcess
|
||||
// to the plain Dr Interest On Savings / Cr Savings Control pair for a product
|
||||
// that names neither.
|
||||
this.helper.createInterestPostingJournalEntriesForSavings(office, currencyCode, savingsProductId, paymentTypeId,
|
||||
savingsId, transactionId, transactionDate, postingAmount, effectiveIsReversal, false);
|
||||
savingsId, transactionId, transactionDate, postingAmount, effectiveIsReversal);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-7
@@ -294,14 +294,8 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
|
||||
SavingProductAccountingParams.TRANSFERS_SUSPENSE.getValue(), savingProductId,
|
||||
CashAccountsForSavings.TRANSFERS_SUSPENSE.getValue());
|
||||
|
||||
// FD and RD products switch dormancy on with "dormancyAlert" (DepositProductAssembler derives the product's
|
||||
// tracking flag from it) and never send isDormancyTrackingActive, so reading only that dropped the escheat
|
||||
// account the screen had asked for - the product then tracked dormancy with no escheat account mapped.
|
||||
final Boolean isDormancyTrackingActive = this.fromApiJsonHelper.extractBooleanNamed(isDormancyTrackingActiveParamName, element);
|
||||
final Boolean dormancyAlert = this.fromApiJsonHelper.parameterExists("dormancyAlert", element)
|
||||
? this.fromApiJsonHelper.extractBooleanNamed("dormancyAlert", element)
|
||||
: null;
|
||||
if (Boolean.TRUE.equals(isDormancyTrackingActive) || Boolean.TRUE.equals(dormancyAlert)) {
|
||||
if (null != isDormancyTrackingActive && isDormancyTrackingActive) {
|
||||
this.savingsProductToGLAccountMappingHelper.saveSavingsToLiabilityAccountMapping(element,
|
||||
SavingProductAccountingParams.ESCHEAT_LIABILITY.getValue(), savingProductId,
|
||||
CashAccountsForSavings.ESCHEAT_LIABILITY.getValue());
|
||||
|
||||
-29
@@ -27,7 +27,6 @@ import static org.apache.fineract.portfolio.account.api.AccountTransfersApiConst
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
@@ -45,7 +44,6 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
@@ -76,7 +74,6 @@ import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
|
||||
import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues;
|
||||
import org.apache.fineract.portfolio.savings.domain.GSIMRepositoy;
|
||||
import org.apache.fineract.portfolio.savings.domain.GroupSavingsIndividualMonitoring;
|
||||
import org.apache.fineract.portfolio.savings.domain.RecurringDepositAccount;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction;
|
||||
@@ -152,7 +149,6 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
||||
|
||||
final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount, fmt, transactionDate,
|
||||
transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill);
|
||||
applyToRecurringDepositSchedule(toSavingsAccount, deposit);
|
||||
|
||||
if (!fromSavingsAccount.getCurrency().getCode().equals(toSavingsAccount.getCurrency().getCode())) {
|
||||
throw new DifferentCurrenciesException(fromSavingsAccount.getCurrency().getCode(),
|
||||
@@ -208,7 +204,6 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
||||
|
||||
final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount, fmt, transactionDate,
|
||||
transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill);
|
||||
applyToRecurringDepositSchedule(toSavingsAccount, deposit);
|
||||
|
||||
final AccountTransferDetails accountTransferDetails = this.accountTransferAssembler.assembleLoanToSavingsTransfer(command,
|
||||
fromLoanAccount, toSavingsAccount, deposit, loanRefundTransaction);
|
||||
@@ -414,7 +409,6 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
||||
final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount,
|
||||
accountTransferDTO.getFmt(), transactionDate, accountTransferDTO.getTransactionAmount(),
|
||||
accountTransferDTO.getPaymentDetail(), isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill);
|
||||
applyToRecurringDepositSchedule(toSavingsAccount, deposit);
|
||||
|
||||
accountTransferDetails = this.accountTransferAssembler.assembleSavingsToSavingsTransfer(accountTransferDTO, fromSavingsAccount,
|
||||
toSavingsAccount, withdrawal, deposit);
|
||||
@@ -460,7 +454,6 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
||||
final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount,
|
||||
accountTransferDTO.getFmt(), accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(),
|
||||
accountTransferDTO.getPaymentDetail(), isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill);
|
||||
applyToRecurringDepositSchedule(toSavingsAccount, deposit);
|
||||
accountTransferDetails = this.accountTransferAssembler.assembleLoanToSavingsTransfer(accountTransferDTO, fromLoanAccount,
|
||||
toSavingsAccount, deposit, loanTransaction);
|
||||
this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails);
|
||||
@@ -495,7 +488,6 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
||||
accountTransferDTO.getFmt(), accountTransferDTO.getTransactionDate(),
|
||||
accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(),
|
||||
isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill, true);
|
||||
applyToRecurringDepositSchedule(toSavingsAccount, deposit);
|
||||
|
||||
if (accountTransferDetails == null) {
|
||||
accountTransferDetails = AccountTransferDetails.currentToSavingsTransfer(
|
||||
@@ -657,26 +649,6 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
||||
return accountTransferDetails;
|
||||
}
|
||||
|
||||
/**
|
||||
* Money transferred into a recurring deposit is an installment payment, exactly like a counter deposit through
|
||||
* DepositAccountDomainServiceJpa#handleRDDeposit. The generic deposit above only credits the balance, so without
|
||||
* this the deposit's schedule kept showing the installment as unpaid and overdue, and its projected maturity
|
||||
* amount never moved - for manual transfers, standing instructions and RD Auto Debit alike. Any other destination
|
||||
* is left untouched.
|
||||
*/
|
||||
private void applyToRecurringDepositSchedule(final SavingsAccount toSavingsAccount, final SavingsAccountTransaction deposit) {
|
||||
if (!(toSavingsAccount instanceof RecurringDepositAccount recurringDeposit)) {
|
||||
return;
|
||||
}
|
||||
final boolean isPreMatureClosure = false;
|
||||
recurringDeposit.updateDepositAmount(deposit.getAmount());
|
||||
recurringDeposit.handleScheduleInstallments(deposit);
|
||||
recurringDeposit.updateMaturityDateAndAmount(MathContext.DECIMAL64, isPreMatureClosure,
|
||||
this.configurationDomainService.isSavingsInterestPostingAtCurrentPeriodEnd(),
|
||||
this.configurationDomainService.retrieveFinancialYearBeginningMonth());
|
||||
recurringDeposit.updateOverduePayments(DateUtils.getBusinessLocalDate());
|
||||
}
|
||||
|
||||
private boolean isLoanToSavingsAccountTransfer(final PortfolioAccountType fromAccountType, final PortfolioAccountType toAccountType) {
|
||||
return fromAccountType.isLoanAccount() && toAccountType.isSavingsAccount();
|
||||
}
|
||||
@@ -885,7 +857,6 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
||||
|
||||
final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount, fmt, transactionDate,
|
||||
transactionAmount, paymentDetail, true, true, backdatedTxnsAllowedTill);
|
||||
applyToRecurringDepositSchedule(toSavingsAccount, deposit);
|
||||
|
||||
final AccountTransferDetails accountTransferDetails = this.accountTransferAssembler.assembleLoanToSavingsTransfer(command,
|
||||
fromLoanAccount, toSavingsAccount, deposit, loanRefundTransaction);
|
||||
|
||||
-34
@@ -789,13 +789,11 @@ public class BranchDayRegisterApiResource {
|
||||
final Long registerId = ((Number) found.get(0).get("id")).longValue();
|
||||
saveDenominationDetail(registerId, "CLOSING", denominations);
|
||||
snapshotVaultDenominations(officeId, registerId, businessDate);
|
||||
final int depositsClosed = closeMaturedZeroBalanceRecurringDeposits(officeId, businessDate, currentUser);
|
||||
|
||||
final Map<String, Object> res = new LinkedHashMap<>();
|
||||
res.put("officeId", officeId);
|
||||
res.put("businessDate", businessDate.toString());
|
||||
res.put("status", "CLOSED");
|
||||
res.put("zeroBalanceDepositsClosed", depositsClosed);
|
||||
res.put("closingCash", closingCash);
|
||||
res.put("bookCash", bookCash);
|
||||
res.put("cashDifference", difference);
|
||||
@@ -803,38 +801,6 @@ public class BranchDayRegisterApiResource {
|
||||
return gson.toJson(res);
|
||||
}
|
||||
|
||||
/**
|
||||
* "Account Closed with Zero balance in Day Closing" on a recurring deposit product: at the branch's day close, each
|
||||
* of its recurring deposits that has MATURED with nothing left in it - no balance, and no loan holding a lien on it
|
||||
* - is marked Closed on that business date, so it no longer waits in the matured list.
|
||||
*
|
||||
* Only matured deposits: a running deposit sits at zero between opening and its first installment, and must never
|
||||
* be closed for that. Nothing is paid out (there is nothing to pay), so no transaction or journal entry is made.
|
||||
*
|
||||
* @return how many deposits were closed
|
||||
*/
|
||||
private int closeMaturedZeroBalanceRecurringDeposits(final Long officeId, final LocalDate businessDate, final AppUser currentUser) {
|
||||
final int closed = jdbcTemplate.update("""
|
||||
UPDATE m_savings_account sa
|
||||
SET status_enum = 600, closedon_date = ?, closedon_userid = ?
|
||||
FROM m_savings_product sp, m_client c
|
||||
WHERE sp.id = sa.product_id AND c.id = sa.client_id AND c.office_id = ?
|
||||
AND sa.deposit_type_enum = 300 AND sa.status_enum = 800
|
||||
AND COALESCE(sp.account_closed_zero_balance_in_day_closing, false) = true
|
||||
AND COALESCE(sa.account_balance_derived, 0) = 0
|
||||
AND NOT EXISTS (SELECT 1 FROM m_portfolio_account_associations paa
|
||||
JOIN m_guarantor_funding_details gfd ON gfd.account_associations_id = paa.id
|
||||
JOIN m_loan l ON l.id = paa.loan_account_id
|
||||
WHERE paa.linked_savings_account_id = sa.id AND gfd.status_enum = 100
|
||||
AND l.loan_status_id IN (100, 200, 300)
|
||||
AND COALESCE(gfd.amount_remaining_derived, gfd.amount) > 0)
|
||||
""", Date.valueOf(businessDate), currentUser.getId(), officeId);
|
||||
if (closed > 0) {
|
||||
log.info("Day close {}: office {} closed {} matured recurring deposit(s) with zero balance", businessDate, officeId, closed);
|
||||
}
|
||||
return closed;
|
||||
}
|
||||
|
||||
// ── GET /v1/branchday/denominations ── the note/coin breakdown behind a day's lump sum ────
|
||||
|
||||
/**
|
||||
|
||||
-30
@@ -176,7 +176,6 @@ public class ClerkDisbursementRequestApiResource {
|
||||
// A GL leg carries no savingsAccountId and the guard ignores it.
|
||||
// A CURRENT leg carries the current account's id in savingsAccountId, so it is checked against
|
||||
// m_current_account -- checking it as a savings id would test some unrelated savings account.
|
||||
rejectRecurringDepositLeg(entry, entryAccountType);
|
||||
if (entry.has("savingsAccountId") && !entry.get("savingsAccountId").isJsonNull()) {
|
||||
final KycRestrictionGuard.Operation operation = "Dr".equals(dbCr) ? KycRestrictionGuard.Operation.TRANSFER
|
||||
: KycRestrictionGuard.Operation.CREDIT;
|
||||
@@ -487,8 +486,6 @@ public class ClerkDisbursementRequestApiResource {
|
||||
throw apiError(400, "Account ID missing for entry: " + acNo
|
||||
+ ". Clerk must re-submit with account details loaded.");
|
||||
}
|
||||
// Also refuses requests raised before RD legs were blocked at submit.
|
||||
rejectRecurringDepositLeg(entry, entryType);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -935,7 +932,6 @@ public class ClerkDisbursementRequestApiResource {
|
||||
if (amt.compareTo(BigDecimal.ZERO) <= 0)
|
||||
throw apiError(400, "Amount must be greater than zero. Account: "
|
||||
+ (e.has("accountNo") ? e.get("accountNo").getAsString() : "?"));
|
||||
rejectRecurringDepositLeg(e, acTyp);
|
||||
drAccountIds.add(e.get("savingsAccountId").getAsLong());
|
||||
}
|
||||
for (final JsonObject e : crEntries) {
|
||||
@@ -948,7 +944,6 @@ public class ClerkDisbursementRequestApiResource {
|
||||
if (amt.compareTo(BigDecimal.ZERO) <= 0)
|
||||
throw apiError(400, "Amount must be greater than zero. Account: "
|
||||
+ (e.has("accountNo") ? e.get("accountNo").getAsString() : "?"));
|
||||
rejectRecurringDepositLeg(e, acTyp);
|
||||
final Long crId = e.get("savingsAccountId").getAsLong();
|
||||
if (drAccountIds.contains(crId)) throw apiError(400,
|
||||
"Account " + (e.has("accountNo") ? e.get("accountNo").getAsString() : crId)
|
||||
@@ -1198,31 +1193,6 @@ public class ClerkDisbursementRequestApiResource {
|
||||
.findFirst().orElse(null);
|
||||
}
|
||||
|
||||
/** m_savings_account.deposit_type_enum of a recurring deposit (Fineract's DepositAccountType.RECURRING_DEPOSIT). */
|
||||
private static final int DEPOSIT_TYPE_RECURRING = 300;
|
||||
|
||||
/**
|
||||
* A recurring deposit cannot be a leg of any clerk request. Every request type here posts straight into the
|
||||
* account with a direct insert, which never reaches the RD's installment schedule: the installments stay unpaid,
|
||||
* Auto Debit collects them a second time and the maturity amount is computed on the wrong schedule. RD
|
||||
* installments come in through the cashier's Receipt (the RD deposit command) or the RD's own Auto Debit.
|
||||
* Looked up by id, not trusted from the entry's label; a CURRENT leg's id belongs to m_current_account.
|
||||
*/
|
||||
private void rejectRecurringDepositLeg(final JsonObject entry, final String accountType) {
|
||||
if ("GL".equals(accountType) || "CURRENT".equals(accountType) || !entry.has("savingsAccountId")
|
||||
|| entry.get("savingsAccountId").isJsonNull()) {
|
||||
return;
|
||||
}
|
||||
final List<Map<String, Object>> rows = jdbcTemplate.queryForList(
|
||||
"SELECT account_no FROM m_savings_account WHERE id = ? AND deposit_type_enum = ?",
|
||||
entry.get("savingsAccountId").getAsLong(), DEPOSIT_TYPE_RECURRING);
|
||||
if (!rows.isEmpty()) {
|
||||
throw apiError(422, "Recurring deposit " + rows.get(0).get("account_no")
|
||||
+ " cannot be used in a clerk Disbursement, Collection, Fund Transfer or Bank Transfer."
|
||||
+ " Pay RD installments through a cashier Receipt or Auto Debit.");
|
||||
}
|
||||
}
|
||||
|
||||
/** Savings status_enum for an account still open for business (Fineract's SavingsAccountStatusType.ACTIVE). */
|
||||
private static final int SAVINGS_STATUS_ACTIVE = 300;
|
||||
|
||||
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* 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.portfolio.investment.api;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Constants for the Treasury Investment masters (Bank / Party Master and Script Master), modelled on the bank's existing
|
||||
* CBS Investment screens.
|
||||
*/
|
||||
public final class InvestmentApiConstants {
|
||||
|
||||
private InvestmentApiConstants() {}
|
||||
|
||||
public static final String ENTITY_PARTY = "INVESTMENTPARTY";
|
||||
public static final String ENTITY_SCRIPT = "INVESTMENTSCRIPT";
|
||||
public static final String RESOURCE_PARTY = "investmentparty";
|
||||
public static final String RESOURCE_SCRIPT = "investmentscript";
|
||||
|
||||
/** Instrument ("Bond Type") an investment party / script belongs to. */
|
||||
public static final Set<String> BOND_TYPES = Set.of("GOVT_SECURITY", "NON_SLR", "BOND", "MUTUAL_FUND", "FD", "SHARE");
|
||||
|
||||
/** Bond types that carry coupon terms (ISIN, interest rate, issue date are mandatory for these). */
|
||||
public static final Set<String> DEBT_BOND_TYPES = Set.of("GOVT_SECURITY", "NON_SLR", "BOND");
|
||||
|
||||
/** SLR classification on the Script Master ("GOI" = Central Government). */
|
||||
public static final Set<String> SLR_TYPES = Set.of("GOI", "STATE_GOVT", "TREASURY", "NON_SLR");
|
||||
|
||||
/** Script Code prefix per bond type, e.g. G00001 for the first Govt Security. */
|
||||
public static final Map<String, String> SCRIPT_CODE_PREFIX = Map.of("GOVT_SECURITY", "G", "NON_SLR", "N", "BOND", "B", "MUTUAL_FUND",
|
||||
"M", "FD", "F", "SHARE", "S");
|
||||
|
||||
public static final String PARTY_CODE_PREFIX = "PTY-";
|
||||
|
||||
// Common parameters
|
||||
public static final String BOND_TYPE = "bondType";
|
||||
public static final String GL_ACCOUNT_ID = "glAccountId";
|
||||
public static final String ADDRESS1 = "address1";
|
||||
public static final String ADDRESS2 = "address2";
|
||||
public static final String ADDRESS3 = "address3";
|
||||
public static final String CITY = "city";
|
||||
public static final String PINCODE = "pincode";
|
||||
public static final String PHONE = "phone";
|
||||
public static final String FAX = "fax";
|
||||
public static final String ACTIVE = "active";
|
||||
public static final String LOCALE = "locale";
|
||||
public static final String DATE_FORMAT = "dateFormat";
|
||||
|
||||
// Party parameters
|
||||
public static final String PARTY_NAME = "partyName";
|
||||
public static final String BANK_TYPE = "bankType";
|
||||
public static final String RBI_CODE = "rbiCode";
|
||||
public static final String EXPOSURE_LIMIT = "limit";
|
||||
public static final String LEI_NO = "leiNo";
|
||||
public static final String CP_BANK_NAME = "cpBankName";
|
||||
public static final String CP_BRANCH_NAME = "cpBranchName";
|
||||
public static final String CP_ACCOUNT_NO = "cpAccountNo";
|
||||
public static final String CP_IFSC = "cpIfsc";
|
||||
|
||||
// Script parameters
|
||||
public static final String SLR_TYPE = "slrType";
|
||||
public static final String SCRIPT_DESC = "scriptDesc";
|
||||
public static final String ISIN = "isin";
|
||||
public static final String FACE_VALUE_PER_UNIT = "faceValuePerUnit";
|
||||
public static final String INT_RATE = "intRate";
|
||||
public static final String ISSUE_DATE = "issueDate";
|
||||
public static final String MATURITY_DATE = "maturityDate";
|
||||
public static final String COMPANY_NAME = "companyName";
|
||||
public static final String CORP_CODE = "corpCode";
|
||||
|
||||
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, RBI_CODE, EXPOSURE_LIMIT, LEI_NO, CP_BANK_NAME, CP_BRANCH_NAME, CP_ACCOUNT_NO, CP_IFSC, ACTIVE,
|
||||
LOCALE);
|
||||
|
||||
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,
|
||||
ACTIVE, LOCALE, DATE_FORMAT);
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* 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.portfolio.investment.api;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.DELETE;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.PUT;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||
import org.apache.fineract.commands.service.CommandWrapperBuilder;
|
||||
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.investment.data.InvestmentPartyData;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentMasterReadPlatformService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Path("/v1/investment/parties")
|
||||
@Component
|
||||
@Tag(name = "Investment Bank / Party Master", description = "Counterparties an investment is bought from or placed with.")
|
||||
@RequiredArgsConstructor
|
||||
public class InvestmentPartyApiResource {
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final InvestmentMasterReadPlatformService readPlatformService;
|
||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||
|
||||
@GET
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "List investment parties")
|
||||
public List<InvestmentPartyData> retrieveAll() {
|
||||
this.context.authenticatedUser().validateHasReadPermission(InvestmentApiConstants.ENTITY_PARTY);
|
||||
return this.readPlatformService.retrieveAllParties();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{partyId}")
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Retrieve an investment party")
|
||||
public InvestmentPartyData retrieveOne(@PathParam("partyId") @Parameter(description = "partyId") final Long partyId) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(InvestmentApiConstants.ENTITY_PARTY);
|
||||
return this.readPlatformService.retrieveParty(partyId);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Create an investment party")
|
||||
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
final CommandWrapper request = new CommandWrapperBuilder().createInvestmentParty().withJson(apiRequestBodyAsJson).build();
|
||||
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("{partyId}")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Update an investment party")
|
||||
public CommandProcessingResult update(@PathParam("partyId") @Parameter(description = "partyId") final Long partyId,
|
||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
final CommandWrapper request = new CommandWrapperBuilder().updateInvestmentParty(partyId).withJson(apiRequestBodyAsJson).build();
|
||||
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("{partyId}")
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Drop an investment party")
|
||||
public CommandProcessingResult delete(@PathParam("partyId") @Parameter(description = "partyId") final Long partyId) {
|
||||
final CommandWrapper request = new CommandWrapperBuilder().deleteInvestmentParty(partyId).build();
|
||||
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 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.portfolio.investment.api;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/** Constants for the Investment Purchase Entry (a deal that creates a holding). */
|
||||
public final class InvestmentPurchaseApiConstants {
|
||||
|
||||
private InvestmentPurchaseApiConstants() {}
|
||||
|
||||
public static final String ENTITY = "INVESTMENTPURCHASE";
|
||||
public static final String RESOURCE = "investmentpurchase";
|
||||
|
||||
public static final Set<String> SETTLEMENT_TYPES = Set.of("T+0", "T+1", "T+2");
|
||||
public static final Set<String> CATEGORIES = Set.of("HTM (Held To Maturity)", "AFS (Available For Sale)", "HFT (Held For Trading)");
|
||||
|
||||
public static final String STATUS_ACTIVE = "ACTIVE";
|
||||
|
||||
public static final String BOND_TYPE = "bondType";
|
||||
public static final String SCRIPT_ID = "scriptId";
|
||||
public static final String PARTY_ID = "partyId";
|
||||
public static final String DEAL_DATE = "dealDate";
|
||||
public static final String DEAL_TIME = "dealTime";
|
||||
public static final String SETTLEMENT_TYPE = "settlementType";
|
||||
public static final String FACE_VALUE = "faceValue";
|
||||
public static final String PURCHASE_RATE = "purchaseRate";
|
||||
public static final String CONSIDER_AMT = "considerAmt";
|
||||
public static final String NAV = "nav";
|
||||
public static final String STAMP_DUTY = "stampDuty";
|
||||
public static final String BROKERAGE = "brokerage";
|
||||
public static final String CATEGORY = "category";
|
||||
public static final String LAST_INT_DATE = "lastIntDate";
|
||||
public static final String CR_GL_ACCOUNT_ID = "crGlAccountId";
|
||||
public static final String SUB_TYPE = "subType";
|
||||
/** Optional legacy fields (trade no, authorities, lien, delivery mode, …) kept as a JSON object. */
|
||||
public static final String OTHER_DETAILS = "otherDetails";
|
||||
public static final String LOCALE = "locale";
|
||||
public static final String DATE_FORMAT = "dateFormat";
|
||||
|
||||
public static final Set<String> PARAMS = Set.of(BOND_TYPE, SCRIPT_ID, PARTY_ID, DEAL_DATE, DEAL_TIME, SETTLEMENT_TYPE, FACE_VALUE,
|
||||
PURCHASE_RATE, CONSIDER_AMT, NAV, STAMP_DUTY, BROKERAGE, CATEGORY, LAST_INT_DATE, CR_GL_ACCOUNT_ID, SUB_TYPE, OTHER_DETAILS,
|
||||
LOCALE, DATE_FORMAT);
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* 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.portfolio.investment.api;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.DELETE;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||
import org.apache.fineract.commands.service.CommandWrapperBuilder;
|
||||
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.investment.data.InvestmentInterestScheduleData;
|
||||
import org.apache.fineract.portfolio.investment.data.InvestmentPurchaseData;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentPurchaseReadPlatformService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Path("/v1/investment/purchases")
|
||||
@Component
|
||||
@Tag(name = "Investment Purchase Entry", description = "Investment deals (Govt Security, Bond, Mutual Fund, …) — each becomes a holding.")
|
||||
@RequiredArgsConstructor
|
||||
public class InvestmentPurchaseApiResource {
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final InvestmentPurchaseReadPlatformService readPlatformService;
|
||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||
|
||||
@GET
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "List investment purchases (holdings)")
|
||||
public List<InvestmentPurchaseData> retrieveAll() {
|
||||
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
|
||||
return this.readPlatformService.retrieveAll();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{purchaseId}")
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Retrieve an investment purchase")
|
||||
public InvestmentPurchaseData retrieveOne(@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
|
||||
return this.readPlatformService.retrieveOne(purchaseId);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{purchaseId}/interest-schedule")
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Interest schedule of a holding", description = "Half-yearly coupons and 31 Mar / 30 Sep closing provisions "
|
||||
+ "from purchase to maturity. Calculated only — nothing is posted. asOnDate (dd-MM-yyyy) defaults to the business date.")
|
||||
public InvestmentInterestScheduleData retrieveInterestSchedule(
|
||||
@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId,
|
||||
@QueryParam("asOnDate") @Parameter(description = "dd-MM-yyyy") final String asOnDate) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
|
||||
final LocalDate asOn = StringUtils.isBlank(asOnDate) ? null
|
||||
: LocalDate.parse(asOnDate.trim(), DateTimeFormatter.ofPattern("dd-MM-yyyy"));
|
||||
return this.readPlatformService.retrieveInterestSchedule(purchaseId, asOn);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Save a purchase entry")
|
||||
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
final CommandWrapper request = new CommandWrapperBuilder().createInvestmentPurchase().withJson(apiRequestBodyAsJson).build();
|
||||
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("{purchaseId}")
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Drop a purchase entry")
|
||||
public CommandProcessingResult delete(@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId) {
|
||||
final CommandWrapper request = new CommandWrapperBuilder().deleteInvestmentPurchase(purchaseId).build();
|
||||
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* 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.portfolio.investment.api;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.PUT;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||
import org.apache.fineract.commands.service.CommandWrapperBuilder;
|
||||
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.investment.data.InvestmentScriptData;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentMasterReadPlatformService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Path("/v1/investment/scripts")
|
||||
@Component
|
||||
@Tag(name = "Investment Script Master", description = "Securities / instruments the bank invests in (Govt Security, Bond, Mutual Fund, …).")
|
||||
@RequiredArgsConstructor
|
||||
public class InvestmentScriptApiResource {
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final InvestmentMasterReadPlatformService readPlatformService;
|
||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||
|
||||
@GET
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "List investment scripts")
|
||||
public List<InvestmentScriptData> retrieveAll() {
|
||||
this.context.authenticatedUser().validateHasReadPermission(InvestmentApiConstants.ENTITY_SCRIPT);
|
||||
return this.readPlatformService.retrieveAllScripts();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{scriptId}")
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Retrieve an investment script")
|
||||
public InvestmentScriptData retrieveOne(@PathParam("scriptId") @Parameter(description = "scriptId") final Long scriptId) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(InvestmentApiConstants.ENTITY_SCRIPT);
|
||||
return this.readPlatformService.retrieveScript(scriptId);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Create an investment script")
|
||||
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
final CommandWrapper request = new CommandWrapperBuilder().createInvestmentScript().withJson(apiRequestBodyAsJson).build();
|
||||
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("{scriptId}")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Update an investment script")
|
||||
public CommandProcessingResult update(@PathParam("scriptId") @Parameter(description = "scriptId") final Long scriptId,
|
||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
final CommandWrapper request = new CommandWrapperBuilder().updateInvestmentScript(scriptId).withJson(apiRequestBodyAsJson).build();
|
||||
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* 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.portfolio.investment.data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* Interest schedule of one holding: every half-yearly coupon and every 31 March / 30 September closing provision from
|
||||
* purchase to maturity. Calculated, not yet posted.
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
public class InvestmentInterestScheduleData {
|
||||
|
||||
private final Long purchaseId;
|
||||
private final String dealNo;
|
||||
private final String certNo;
|
||||
private final String scriptCode;
|
||||
private final String scriptDesc;
|
||||
private final BigDecimal faceValue;
|
||||
private final BigDecimal unitValue;
|
||||
private final BigDecimal balanceUnits;
|
||||
private final BigDecimal purchaseRate;
|
||||
private final BigDecimal bookValue;
|
||||
private final BigDecimal intRate;
|
||||
private final String dealDate;
|
||||
private final String lastIntDate;
|
||||
private final String maturityDate;
|
||||
/** Interest paid to the seller at purchase — already in Interest Receivable. */
|
||||
private final BigDecimal purchasedAccruedInterest;
|
||||
private final BigDecimal couponPerHalfYear;
|
||||
private final String asOnDate;
|
||||
private final String interestGlCode;
|
||||
private final String interestGlName;
|
||||
private final List<Row> rows;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
public static class Row {
|
||||
|
||||
/** COUPON (cash received) or PROVISION (closing accrual). */
|
||||
private final String type;
|
||||
private final String intDate;
|
||||
private final String fromDate;
|
||||
private final String toDate;
|
||||
private final int days;
|
||||
/** Coupon received, or interest earned since the last coupon up to the closing. */
|
||||
private final BigDecimal grossInterest;
|
||||
/** Interest Receivable balance before this line (purchased accrued interest / earlier provision). */
|
||||
private final BigDecimal alreadyBooked;
|
||||
/** Provision to book now, or the part of the coupon that is new income. */
|
||||
private final BigDecimal amount;
|
||||
/** True when the date is on or before the as-on date. */
|
||||
private final boolean due;
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* 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.portfolio.investment.data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentParty;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
public class InvestmentPartyData {
|
||||
|
||||
private final Long id;
|
||||
private final String partyCode;
|
||||
private final String bondType;
|
||||
private final String partyName;
|
||||
private final String address1;
|
||||
private final String address2;
|
||||
private final String address3;
|
||||
private final String city;
|
||||
private final String pincode;
|
||||
private final String phone;
|
||||
private final String fax;
|
||||
private final String bankType;
|
||||
private final Long glAccountId;
|
||||
private final String glCode;
|
||||
private final String glName;
|
||||
private final String rbiCode;
|
||||
private final BigDecimal limit;
|
||||
private final String leiNo;
|
||||
private final String cpBankName;
|
||||
private final String cpBranchName;
|
||||
private final String cpAccountNo;
|
||||
private final String cpIfsc;
|
||||
private final boolean active;
|
||||
|
||||
public static InvestmentPartyData from(final InvestmentParty p) {
|
||||
return InvestmentPartyData.builder().id(p.getId()).partyCode(p.getPartyCode()).bondType(p.getBondType())
|
||||
.partyName(p.getPartyName()).address1(p.getAddress1()).address2(p.getAddress2()).address3(p.getAddress3())
|
||||
.city(p.getCity()).pincode(p.getPincode()).phone(p.getPhone()).fax(p.getFax()).bankType(p.getBankType())
|
||||
.glAccountId(p.getGlAccount().getId()).glCode(p.getGlAccount().getGlCode()).glName(p.getGlAccount().getName())
|
||||
.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();
|
||||
}
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* 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.portfolio.investment.data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchase;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
public class InvestmentPurchaseData {
|
||||
|
||||
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
||||
|
||||
private final Long id;
|
||||
private final String dealNo;
|
||||
private final String certNo;
|
||||
private final String bondType;
|
||||
private final Long scriptId;
|
||||
private final String scriptCode;
|
||||
private final String scriptDesc;
|
||||
private final String isin;
|
||||
private final Long partyId;
|
||||
private final String partyCode;
|
||||
private final String partyName;
|
||||
private final String dealDate;
|
||||
private final String dealTime;
|
||||
private final String settlementType;
|
||||
private final String settlementDate;
|
||||
private final BigDecimal faceValue;
|
||||
private final BigDecimal purchaseRate;
|
||||
private final BigDecimal units;
|
||||
private final BigDecimal balanceUnits;
|
||||
private final BigDecimal brokerage;
|
||||
private final BigDecimal stampDuty;
|
||||
private final BigDecimal bookValue;
|
||||
private final BigDecimal premiumDiscount;
|
||||
private final BigDecimal intRate;
|
||||
private final String issueDate;
|
||||
private final String maturityDate;
|
||||
private final String lastIntDate;
|
||||
private final Integer accruedDays;
|
||||
private final BigDecimal accruedInterest;
|
||||
private final BigDecimal settlementAmount;
|
||||
private final BigDecimal ytm;
|
||||
private final String category;
|
||||
private final String subType;
|
||||
private final Long investmentGlAccountId;
|
||||
private final String investmentGlCode;
|
||||
private final String investmentGlName;
|
||||
private final Long crGlAccountId;
|
||||
private final String crGlCode;
|
||||
private final String crGlName;
|
||||
private final String otherDetails;
|
||||
private final String status;
|
||||
|
||||
public static InvestmentPurchaseData from(final InvestmentPurchase p) {
|
||||
return InvestmentPurchaseData.builder().id(p.getId()).dealNo(p.getDealNo()).certNo(p.getCertNo()).bondType(p.getBondType())
|
||||
.scriptId(p.getScript().getId()).scriptCode(p.getScript().getScriptCode()).scriptDesc(p.getScript().getScriptDesc())
|
||||
.isin(p.getScript().getIsin()).partyId(p.getParty().getId()).partyCode(p.getParty().getPartyCode())
|
||||
.partyName(p.getParty().getPartyName()).dealDate(format(p.getDealDate())).dealTime(p.getDealTime())
|
||||
.settlementType(p.getSettlementType()).settlementDate(format(p.getSettlementDate())).faceValue(p.getFaceValue())
|
||||
.purchaseRate(p.getPurchaseRate()).units(p.getUnits()).balanceUnits(p.getBalanceUnits()).brokerage(p.getBrokerage())
|
||||
.stampDuty(p.getStampDuty()).bookValue(p.getBookValue()).premiumDiscount(p.getPremiumDiscount()).intRate(p.getIntRate())
|
||||
.issueDate(format(p.getIssueDate())).maturityDate(format(p.getMaturityDate())).lastIntDate(format(p.getLastIntDate()))
|
||||
.accruedDays(p.getAccruedDays()).accruedInterest(p.getAccruedInterest()).settlementAmount(p.getSettlementAmount())
|
||||
.ytm(p.getYtm()).category(p.getCategory()).subType(p.getSubType())
|
||||
.investmentGlAccountId(p.getInvestmentGlAccount().getId()).investmentGlCode(p.getInvestmentGlAccount().getGlCode())
|
||||
.investmentGlName(p.getInvestmentGlAccount().getName()).crGlAccountId(p.getCrGlAccount().getId())
|
||||
.crGlCode(p.getCrGlAccount().getGlCode()).crGlName(p.getCrGlAccount().getName()).otherDetails(p.getOtherDetails())
|
||||
.status(p.getStatus()).build();
|
||||
}
|
||||
|
||||
private static String format(final LocalDate date) {
|
||||
return date == null ? null : date.format(DATE_FORMAT);
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* 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.portfolio.investment.data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentScript;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
public class InvestmentScriptData {
|
||||
|
||||
/** Dates go out as DD-MM-YYYY, the format the Investment screens use throughout. */
|
||||
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
||||
|
||||
private final Long id;
|
||||
private final String scriptCode;
|
||||
private final String bondType;
|
||||
private final String slrType;
|
||||
private final String scriptDesc;
|
||||
private final Long glAccountId;
|
||||
private final String glCode;
|
||||
private final String glName;
|
||||
private final String isin;
|
||||
private final BigDecimal faceValuePerUnit;
|
||||
private final BigDecimal intRate;
|
||||
private final String issueDate;
|
||||
private final String maturityDate;
|
||||
private final String companyName;
|
||||
private final String address1;
|
||||
private final String address2;
|
||||
private final String address3;
|
||||
private final String city;
|
||||
private final String pincode;
|
||||
private final String phone;
|
||||
private final String fax;
|
||||
private final String corpCode;
|
||||
private final boolean active;
|
||||
|
||||
public static InvestmentScriptData from(final InvestmentScript s) {
|
||||
return InvestmentScriptData.builder().id(s.getId()).scriptCode(s.getScriptCode()).bondType(s.getBondType())
|
||||
.slrType(s.getSlrType()).scriptDesc(s.getScriptDesc()).glAccountId(s.getGlAccount().getId())
|
||||
.glCode(s.getGlAccount().getGlCode()).glName(s.getGlAccount().getName()).isin(s.getIsin())
|
||||
.faceValuePerUnit(s.getFaceValuePerUnit()).intRate(s.getIntRate()).issueDate(format(s.getIssueDate()))
|
||||
.maturityDate(format(s.getMaturityDate())).companyName(s.getCompanyName()).address1(s.getAddress1())
|
||||
.address2(s.getAddress2()).address3(s.getAddress3()).city(s.getCity()).pincode(s.getPincode()).phone(s.getPhone())
|
||||
.fax(s.getFax()).corpCode(s.getCorpCode()).active(s.isActive()).build();
|
||||
}
|
||||
|
||||
private static String format(final LocalDate date) {
|
||||
return date == null ? null : date.format(DATE_FORMAT);
|
||||
}
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* 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.portfolio.investment.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.math.BigDecimal;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
|
||||
/**
|
||||
* Bank / Party Master — the counterparty an investment is bought from or placed with (bank, RBI, issuer, AMC, broker),
|
||||
* following the bank's existing CBS Bank / Party Master.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "m_investment_party")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class InvestmentParty extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
|
||||
@Column(name = "party_code", nullable = false, length = 20, unique = true)
|
||||
private String partyCode;
|
||||
|
||||
@Column(name = "bond_type", nullable = false, length = 20)
|
||||
private String bondType;
|
||||
|
||||
@Column(name = "party_name", nullable = false, length = 150)
|
||||
private String partyName;
|
||||
|
||||
@Column(name = "address1", length = 150)
|
||||
private String address1;
|
||||
|
||||
@Column(name = "address2", length = 150)
|
||||
private String address2;
|
||||
|
||||
@Column(name = "address3", length = 150)
|
||||
private String address3;
|
||||
|
||||
@Column(name = "city", length = 100)
|
||||
private String city;
|
||||
|
||||
@Column(name = "pincode", length = 10)
|
||||
private String pincode;
|
||||
|
||||
@Column(name = "phone", length = 20)
|
||||
private String phone;
|
||||
|
||||
@Column(name = "fax", length = 20)
|
||||
private String fax;
|
||||
|
||||
@Column(name = "bank_type", length = 60)
|
||||
private String bankType;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "gl_account_id", nullable = false)
|
||||
private GLAccount glAccount;
|
||||
|
||||
@Column(name = "rbi_code", length = 30)
|
||||
private String rbiCode;
|
||||
|
||||
@Column(name = "exposure_limit", scale = 6, precision = 19)
|
||||
private BigDecimal exposureLimit;
|
||||
|
||||
@Column(name = "lei_no", length = 30)
|
||||
private String leiNo;
|
||||
|
||||
@Column(name = "cp_bank_name", length = 150)
|
||||
private String cpBankName;
|
||||
|
||||
@Column(name = "cp_branch_name", length = 150)
|
||||
private String cpBranchName;
|
||||
|
||||
@Column(name = "cp_account_no", length = 40)
|
||||
private String cpAccountNo;
|
||||
|
||||
@Column(name = "cp_ifsc", length = 11)
|
||||
private String cpIfsc;
|
||||
|
||||
/** An inactive party is kept for history but not offered for new deals. */
|
||||
@Column(name = "is_active", nullable = false)
|
||||
private boolean active = true;
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 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.portfolio.investment.domain;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface InvestmentPartyRepository extends JpaRepository<InvestmentParty, Long> {}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* 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.portfolio.investment.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
|
||||
/**
|
||||
* Investment Purchase Entry — one deal (as in the bank's existing CBS "Govt.Security/Bond Entry"). Once saved it is the
|
||||
* holding that interest, sale and redemption later work against; {@link #balanceUnits} tracks what is still held.
|
||||
* Every derived figure (units, book value, accrued interest, settlement amount) is computed by the backend.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "m_investment_purchase")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class InvestmentPurchase extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
|
||||
/** Financial-year deal number, e.g. 202627000001 for the first deal of FY 2026-27. */
|
||||
@Column(name = "deal_no", nullable = false, length = 20, unique = true)
|
||||
private String dealNo;
|
||||
|
||||
/** Certificate number — system-generated running number. */
|
||||
@Column(name = "cert_no", nullable = false, length = 20, unique = true)
|
||||
private String certNo;
|
||||
|
||||
@Column(name = "bond_type", nullable = false, length = 20)
|
||||
private String bondType;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "script_id", nullable = false)
|
||||
private InvestmentScript script;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "party_id", nullable = false)
|
||||
private InvestmentParty party;
|
||||
|
||||
@Column(name = "deal_date", nullable = false)
|
||||
private LocalDate dealDate;
|
||||
|
||||
@Column(name = "deal_time", length = 8)
|
||||
private String dealTime;
|
||||
|
||||
@Column(name = "settlement_type", nullable = false, length = 5)
|
||||
private String settlementType;
|
||||
|
||||
@Column(name = "settlement_date", nullable = false)
|
||||
private LocalDate settlementDate;
|
||||
|
||||
@Column(name = "face_value", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal faceValue;
|
||||
|
||||
/** Price per ₹100 face value (Govt Security / Bond); NAV for a mutual fund. */
|
||||
@Column(name = "purchase_rate", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal purchaseRate;
|
||||
|
||||
@Column(name = "units", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal units;
|
||||
|
||||
@Column(name = "balance_units", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal balanceUnits;
|
||||
|
||||
@Column(name = "brokerage", scale = 6, precision = 19)
|
||||
private BigDecimal brokerage;
|
||||
|
||||
@Column(name = "stamp_duty", scale = 6, precision = 19)
|
||||
private BigDecimal stampDuty;
|
||||
|
||||
@Column(name = "book_value", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal bookValue;
|
||||
|
||||
@Column(name = "premium_discount", scale = 6, precision = 19)
|
||||
private BigDecimal premiumDiscount;
|
||||
|
||||
@Column(name = "int_rate", scale = 4, precision = 9)
|
||||
private BigDecimal intRate;
|
||||
|
||||
@Column(name = "issue_date")
|
||||
private LocalDate issueDate;
|
||||
|
||||
@Column(name = "maturity_date")
|
||||
private LocalDate maturityDate;
|
||||
|
||||
@Column(name = "last_int_date")
|
||||
private LocalDate lastIntDate;
|
||||
|
||||
@Column(name = "accrued_days")
|
||||
private Integer accruedDays;
|
||||
|
||||
/** Interest paid to the seller from the last coupon date up to the deal date (30/360). */
|
||||
@Column(name = "accrued_interest", scale = 6, precision = 19)
|
||||
private BigDecimal accruedInterest;
|
||||
|
||||
@Column(name = "settlement_amount", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal settlementAmount;
|
||||
|
||||
@Column(name = "ytm", scale = 4, precision = 9)
|
||||
private BigDecimal ytm;
|
||||
|
||||
@Column(name = "category", nullable = false, length = 40)
|
||||
private String category;
|
||||
|
||||
@Column(name = "sub_type", length = 40)
|
||||
private String subType;
|
||||
|
||||
/** Investment GL the holding is booked to (from the script). */
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "investment_gl_account_id", nullable = false)
|
||||
private GLAccount investmentGlAccount;
|
||||
|
||||
/** Bank GL the purchase is paid from ("CR. To A/c"). */
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "cr_gl_account_id", nullable = false)
|
||||
private GLAccount crGlAccount;
|
||||
|
||||
@Column(name = "other_details", length = 4000)
|
||||
private String otherDetails;
|
||||
|
||||
@Column(name = "status", nullable = false, length = 20)
|
||||
private String status;
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 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.portfolio.investment.domain;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface InvestmentPurchaseRepository extends JpaRepository<InvestmentPurchase, Long> {}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* 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.portfolio.investment.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
|
||||
/**
|
||||
* Script Master — what is being bought (a Govt Security, bond, mutual fund scheme, share, …), following the bank's
|
||||
* existing CBS Script Master. A purchase picks these terms up instead of the dealer re-typing them.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "m_investment_script")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class InvestmentScript extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
|
||||
/** System-generated: bond-type letter + running number, e.g. G00001. */
|
||||
@Column(name = "script_code", nullable = false, length = 20, unique = true)
|
||||
private String scriptCode;
|
||||
|
||||
@Column(name = "bond_type", nullable = false, length = 20)
|
||||
private String bondType;
|
||||
|
||||
/** GOI / STATE_GOVT / TREASURY / NON_SLR. */
|
||||
@Column(name = "slr_type", length = 20)
|
||||
private String slrType;
|
||||
|
||||
@Column(name = "script_desc", nullable = false, length = 150)
|
||||
private String scriptDesc;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "gl_account_id", nullable = false)
|
||||
private GLAccount glAccount;
|
||||
|
||||
@Column(name = "isin", length = 20)
|
||||
private String isin;
|
||||
|
||||
@Column(name = "face_value_per_unit", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal faceValuePerUnit;
|
||||
|
||||
@Column(name = "int_rate", scale = 4, precision = 9)
|
||||
private BigDecimal intRate;
|
||||
|
||||
@Column(name = "issue_date")
|
||||
private LocalDate issueDate;
|
||||
|
||||
@Column(name = "maturity_date")
|
||||
private LocalDate maturityDate;
|
||||
|
||||
@Column(name = "company_name", length = 150)
|
||||
private String companyName;
|
||||
|
||||
@Column(name = "address1", length = 150)
|
||||
private String address1;
|
||||
|
||||
@Column(name = "address2", length = 150)
|
||||
private String address2;
|
||||
|
||||
@Column(name = "address3", length = 150)
|
||||
private String address3;
|
||||
|
||||
@Column(name = "city", length = 100)
|
||||
private String city;
|
||||
|
||||
@Column(name = "pincode", length = 10)
|
||||
private String pincode;
|
||||
|
||||
@Column(name = "phone", length = 20)
|
||||
private String phone;
|
||||
|
||||
@Column(name = "fax", length = 20)
|
||||
private String fax;
|
||||
|
||||
@Column(name = "corp_code", length = 30)
|
||||
private String corpCode;
|
||||
|
||||
@Column(name = "is_active", nullable = false)
|
||||
private boolean active = true;
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 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.portfolio.investment.domain;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface InvestmentScriptRepository extends JpaRepository<InvestmentScript, Long> {}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 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.portfolio.investment.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
|
||||
|
||||
public class InvestmentPartyNotFoundException extends AbstractPlatformResourceNotFoundException {
|
||||
|
||||
public InvestmentPartyNotFoundException(final Long id) {
|
||||
super("error.msg.investmentparty.id.invalid", "Investment party with identifier " + id + " does not exist", id);
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 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.portfolio.investment.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
|
||||
|
||||
public class InvestmentPurchaseNotFoundException extends AbstractPlatformResourceNotFoundException {
|
||||
|
||||
public InvestmentPurchaseNotFoundException(final Long id) {
|
||||
super("error.msg.investmentpurchase.id.invalid", "Investment purchase with identifier " + id + " does not exist", id);
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 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.portfolio.investment.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
|
||||
|
||||
public class InvestmentScriptNotFoundException extends AbstractPlatformResourceNotFoundException {
|
||||
|
||||
public InvestmentScriptNotFoundException(final Long id) {
|
||||
super("error.msg.investmentscript.id.invalid", "Investment script with identifier " + id + " does not exist", id);
|
||||
}
|
||||
}
|
||||
+6
-7
@@ -16,28 +16,27 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.savings.handler;
|
||||
package org.apache.fineract.portfolio.investment.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.savings.service.DepositAccountWritePlatformService;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentMasterWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/** Tenure Flexibility: lengthens a running recurring deposit's term. */
|
||||
@Service
|
||||
@CommandType(entity = "INVESTMENTPARTY", action = "CREATE")
|
||||
@RequiredArgsConstructor
|
||||
@CommandType(entity = "RECURRINGDEPOSITACCOUNT", action = "EXTENDTENURE")
|
||||
public class ExtendTenureRecurringDepositAccountCommandHandler implements NewCommandSourceHandler {
|
||||
public class CreateInvestmentPartyCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final DepositAccountWritePlatformService depositAccountWritePlatformService;
|
||||
private final InvestmentMasterWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.depositAccountWritePlatformService.extendRecurringDepositTenure(command.entityId(), command);
|
||||
return this.writePlatformService.createParty(command);
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 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.portfolio.investment.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentPurchaseWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "INVESTMENTPURCHASE", action = "CREATE")
|
||||
@RequiredArgsConstructor
|
||||
public class CreateInvestmentPurchaseCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final InvestmentPurchaseWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.createPurchase(command);
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 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.portfolio.investment.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentMasterWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "INVESTMENTSCRIPT", action = "CREATE")
|
||||
@RequiredArgsConstructor
|
||||
public class CreateInvestmentScriptCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final InvestmentMasterWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.createScript(command);
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 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.portfolio.investment.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentMasterWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "INVESTMENTPARTY", action = "DELETE")
|
||||
@RequiredArgsConstructor
|
||||
public class DeleteInvestmentPartyCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final InvestmentMasterWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.deleteParty(command.entityId());
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 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.portfolio.investment.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentPurchaseWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "INVESTMENTPURCHASE", action = "DELETE")
|
||||
@RequiredArgsConstructor
|
||||
public class DeleteInvestmentPurchaseCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final InvestmentPurchaseWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.deletePurchase(command.entityId());
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 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.portfolio.investment.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentMasterWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "INVESTMENTPARTY", action = "UPDATE")
|
||||
@RequiredArgsConstructor
|
||||
public class UpdateInvestmentPartyCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final InvestmentMasterWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.updateParty(command.entityId(), command);
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 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.portfolio.investment.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentMasterWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "INVESTMENTSCRIPT", action = "UPDATE")
|
||||
@RequiredArgsConstructor
|
||||
public class UpdateInvestmentScriptCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final InvestmentMasterWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.updateScript(command.entityId(), command);
|
||||
}
|
||||
}
|
||||
+254
@@ -0,0 +1,254 @@
|
||||
/**
|
||||
* 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.portfolio.investment.serialization;
|
||||
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ADDRESS1;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ADDRESS2;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ADDRESS3;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.BANK_TYPE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.BOND_TYPE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.BOND_TYPES;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CITY;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.COMPANY_NAME;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CORP_CODE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CP_ACCOUNT_NO;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CP_BANK_NAME;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CP_BRANCH_NAME;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CP_IFSC;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.DEBT_BOND_TYPES;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.EXPOSURE_LIMIT;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.FACE_VALUE_PER_UNIT;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.FAX;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.GL_ACCOUNT_ID;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.INT_RATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ISIN;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ISSUE_DATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.LEI_NO;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.MATURITY_DATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PARTY_NAME;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PARTY_PARAMS;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PHONE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PINCODE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.RBI_CODE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.RESOURCE_PARTY;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.RESOURCE_SCRIPT;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SCRIPT_DESC;
|
||||
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_TYPES;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import java.lang.reflect.Type;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/** Validates Bank / Party Master and Script Master requests before anything is written. */
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public final class InvestmentMasterCommandFromApiJsonDeserializer {
|
||||
|
||||
private static final String IFSC_PATTERN = "^[A-Z]{4}0[A-Z0-9]{6}$";
|
||||
private static final String PINCODE_PATTERN = "^[0-9]{6}$";
|
||||
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
|
||||
// -------------------- Bank / Party Master --------------------
|
||||
|
||||
public void validateForCreateParty(final String json) {
|
||||
validateParty(json, true);
|
||||
}
|
||||
|
||||
public void validateForUpdateParty(final String json) {
|
||||
validateParty(json, false);
|
||||
}
|
||||
|
||||
private void validateParty(final String json, final boolean create) {
|
||||
final JsonElement element = parse(json, PARTY_PARAMS);
|
||||
final List<ApiParameterError> errors = new ArrayList<>();
|
||||
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(RESOURCE_PARTY);
|
||||
|
||||
validateBondType(element, v, create);
|
||||
validateRequiredString(element, v, PARTY_NAME, 150, create);
|
||||
validateGlAccountId(element, v, create);
|
||||
validateAddress(element, v);
|
||||
validateOptionalString(element, v, BANK_TYPE, 60);
|
||||
validateOptionalString(element, v, RBI_CODE, 30);
|
||||
validateOptionalString(element, v, LEI_NO, 30);
|
||||
validateOptionalString(element, v, CP_BANK_NAME, 150);
|
||||
validateOptionalString(element, v, CP_BRANCH_NAME, 150);
|
||||
validateOptionalString(element, v, CP_ACCOUNT_NO, 40);
|
||||
|
||||
final String ifsc = this.fromApiJsonHelper.extractStringNamed(CP_IFSC, element);
|
||||
if (StringUtils.isNotBlank(ifsc)) {
|
||||
v.reset().parameter(CP_IFSC).value(ifsc.trim().toUpperCase()).matchesRegularExpression(IFSC_PATTERN,
|
||||
"IFSC must be 11 characters: 4 letters, 0, then 6 letters/digits");
|
||||
}
|
||||
|
||||
if (this.fromApiJsonHelper.parameterExists(EXPOSURE_LIMIT, element)) {
|
||||
final BigDecimal limit = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(EXPOSURE_LIMIT, element);
|
||||
v.reset().parameter(EXPOSURE_LIMIT).value(limit).ignoreIfNull().zeroOrPositiveAmount();
|
||||
}
|
||||
throwIfErrors(errors);
|
||||
}
|
||||
|
||||
// -------------------- Script Master --------------------
|
||||
|
||||
public void validateForCreateScript(final String json) {
|
||||
validateScript(json, true);
|
||||
}
|
||||
|
||||
public void validateForUpdateScript(final String json, final String existingBondType) {
|
||||
validateScript(json, false, existingBondType);
|
||||
}
|
||||
|
||||
private void validateScript(final String json, final boolean create) {
|
||||
validateScript(json, create, null);
|
||||
}
|
||||
|
||||
private void validateScript(final String json, final boolean create, final String existingBondType) {
|
||||
final JsonElement element = parse(json, SCRIPT_PARAMS);
|
||||
final List<ApiParameterError> errors = new ArrayList<>();
|
||||
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(RESOURCE_SCRIPT);
|
||||
|
||||
validateBondType(element, v, create);
|
||||
validateRequiredString(element, v, SCRIPT_DESC, 150, create);
|
||||
validateGlAccountId(element, v, create);
|
||||
|
||||
final String slrType = this.fromApiJsonHelper.extractStringNamed(SLR_TYPE, element);
|
||||
if (StringUtils.isNotBlank(slrType)) {
|
||||
v.reset().parameter(SLR_TYPE).value(slrType).isOneOfTheseValues(SLR_TYPES.toArray());
|
||||
}
|
||||
|
||||
final String bondType = create ? this.fromApiJsonHelper.extractStringNamed(BOND_TYPE, element) : existingBondType;
|
||||
final boolean debt = bondType != null && DEBT_BOND_TYPES.contains(bondType);
|
||||
|
||||
final String isin = this.fromApiJsonHelper.extractStringNamed(ISIN, element);
|
||||
if (create && debt) {
|
||||
v.reset().parameter(ISIN).value(isin).notBlank();
|
||||
}
|
||||
v.reset().parameter(ISIN).value(isin).ignoreIfNull().notExceedingLengthOf(20);
|
||||
|
||||
if (this.fromApiJsonHelper.parameterExists(INT_RATE, element) || (create && debt)) {
|
||||
final BigDecimal intRate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(INT_RATE, element);
|
||||
final DataValidatorBuilder rate = v.reset().parameter(INT_RATE).value(intRate);
|
||||
if (create && debt) {
|
||||
rate.notNull();
|
||||
}
|
||||
rate.ignoreIfNull().zeroOrPositiveAmount();
|
||||
}
|
||||
|
||||
if (this.fromApiJsonHelper.parameterExists(FACE_VALUE_PER_UNIT, element)) {
|
||||
final BigDecimal fv = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(FACE_VALUE_PER_UNIT, element);
|
||||
v.reset().parameter(FACE_VALUE_PER_UNIT).value(fv).ignoreIfNull().positiveAmount();
|
||||
}
|
||||
|
||||
LocalDate issueDate = null;
|
||||
if (this.fromApiJsonHelper.parameterExists(ISSUE_DATE, element) || (create && debt)) {
|
||||
issueDate = this.fromApiJsonHelper.extractLocalDateNamed(ISSUE_DATE, element);
|
||||
if (create && debt) {
|
||||
v.reset().parameter(ISSUE_DATE).value(issueDate).notNull();
|
||||
}
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists(MATURITY_DATE, element)) {
|
||||
final LocalDate maturityDate = this.fromApiJsonHelper.extractLocalDateNamed(MATURITY_DATE, element);
|
||||
v.reset().parameter(MATURITY_DATE).value(maturityDate).ignoreIfNull().validateDateAfter(issueDate);
|
||||
}
|
||||
|
||||
validateOptionalString(element, v, COMPANY_NAME, 150);
|
||||
validateOptionalString(element, v, CORP_CODE, 30);
|
||||
validateAddress(element, v);
|
||||
throwIfErrors(errors);
|
||||
}
|
||||
|
||||
// -------------------- shared --------------------
|
||||
|
||||
private void validateBondType(final JsonElement element, final DataValidatorBuilder v, final boolean required) {
|
||||
final String bondType = this.fromApiJsonHelper.extractStringNamed(BOND_TYPE, element);
|
||||
if (required) {
|
||||
v.reset().parameter(BOND_TYPE).value(bondType).notBlank();
|
||||
}
|
||||
if (StringUtils.isNotBlank(bondType)) {
|
||||
v.reset().parameter(BOND_TYPE).value(bondType).isOneOfTheseValues(BOND_TYPES.toArray());
|
||||
}
|
||||
}
|
||||
|
||||
private void validateGlAccountId(final JsonElement element, final DataValidatorBuilder v, final boolean required) {
|
||||
if (!required && !this.fromApiJsonHelper.parameterExists(GL_ACCOUNT_ID, element)) {
|
||||
return;
|
||||
}
|
||||
final Long glAccountId = this.fromApiJsonHelper.extractLongNamed(GL_ACCOUNT_ID, element);
|
||||
v.reset().parameter(GL_ACCOUNT_ID).value(glAccountId).notNull().longGreaterThanZero();
|
||||
}
|
||||
|
||||
private void validateRequiredString(final JsonElement element, final DataValidatorBuilder v, final String param, final int maxLength,
|
||||
final boolean required) {
|
||||
if (!required && !this.fromApiJsonHelper.parameterExists(param, element)) {
|
||||
return;
|
||||
}
|
||||
final String value = this.fromApiJsonHelper.extractStringNamed(param, element);
|
||||
v.reset().parameter(param).value(value).notBlank().notExceedingLengthOf(maxLength);
|
||||
}
|
||||
|
||||
private void validateOptionalString(final JsonElement element, final DataValidatorBuilder v, final String param,
|
||||
final int maxLength) {
|
||||
final String value = this.fromApiJsonHelper.extractStringNamed(param, element);
|
||||
v.reset().parameter(param).value(value).ignoreIfNull().notExceedingLengthOf(maxLength);
|
||||
}
|
||||
|
||||
private void validateAddress(final JsonElement element, final DataValidatorBuilder v) {
|
||||
validateOptionalString(element, v, ADDRESS1, 150);
|
||||
validateOptionalString(element, v, ADDRESS2, 150);
|
||||
validateOptionalString(element, v, ADDRESS3, 150);
|
||||
validateOptionalString(element, v, CITY, 100);
|
||||
validateOptionalString(element, v, PHONE, 20);
|
||||
validateOptionalString(element, v, FAX, 20);
|
||||
final String pincode = this.fromApiJsonHelper.extractStringNamed(PINCODE, element);
|
||||
if (StringUtils.isNotBlank(pincode)) {
|
||||
v.reset().parameter(PINCODE).value(pincode.trim()).matchesRegularExpression(PINCODE_PATTERN, "Pincode must be 6 digits");
|
||||
}
|
||||
}
|
||||
|
||||
private JsonElement parse(final String json, final Set<String> supportedParams) {
|
||||
if (StringUtils.isBlank(json)) {
|
||||
throw new InvalidJsonException();
|
||||
}
|
||||
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
||||
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, supportedParams);
|
||||
return this.fromApiJsonHelper.parse(json);
|
||||
}
|
||||
|
||||
private void throwIfErrors(final List<ApiParameterError> errors) {
|
||||
if (!errors.isEmpty()) {
|
||||
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.serialization;
|
||||
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.BOND_TYPE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.BROKERAGE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.CATEGORIES;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.CATEGORY;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.CONSIDER_AMT;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.CR_GL_ACCOUNT_ID;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.DEAL_DATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.DEAL_TIME;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.FACE_VALUE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.LAST_INT_DATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.NAV;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.OTHER_DETAILS;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.PARAMS;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.PARTY_ID;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.PURCHASE_RATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.RESOURCE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SCRIPT_ID;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SETTLEMENT_TYPE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SETTLEMENT_TYPES;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.STAMP_DUTY;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SUB_TYPE;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import java.lang.reflect.Type;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.portfolio.investment.api.InvestmentApiConstants;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/** Validates a Purchase Entry before anything is computed or written. */
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public final class InvestmentPurchaseCommandFromApiJsonDeserializer {
|
||||
|
||||
private static final String TIME_PATTERN = "^([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$";
|
||||
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
|
||||
public void validateForCreate(final String json) {
|
||||
if (StringUtils.isBlank(json)) {
|
||||
throw new InvalidJsonException();
|
||||
}
|
||||
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
||||
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, PARAMS);
|
||||
final JsonElement element = this.fromApiJsonHelper.parse(json);
|
||||
|
||||
final List<ApiParameterError> errors = new ArrayList<>();
|
||||
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(RESOURCE);
|
||||
|
||||
final String bondType = this.fromApiJsonHelper.extractStringNamed(BOND_TYPE, element);
|
||||
v.reset().parameter(BOND_TYPE).value(bondType).notBlank();
|
||||
if (StringUtils.isNotBlank(bondType)) {
|
||||
v.reset().parameter(BOND_TYPE).value(bondType).isOneOfTheseValues(InvestmentApiConstants.BOND_TYPES.toArray());
|
||||
}
|
||||
v.reset().parameter(SCRIPT_ID).value(this.fromApiJsonHelper.extractLongNamed(SCRIPT_ID, element)).notNull().longGreaterThanZero();
|
||||
v.reset().parameter(PARTY_ID).value(this.fromApiJsonHelper.extractLongNamed(PARTY_ID, element)).notNull().longGreaterThanZero();
|
||||
v.reset().parameter(CR_GL_ACCOUNT_ID).value(this.fromApiJsonHelper.extractLongNamed(CR_GL_ACCOUNT_ID, element)).notNull()
|
||||
.longGreaterThanZero();
|
||||
v.reset().parameter(DEAL_DATE).value(this.fromApiJsonHelper.extractLocalDateNamed(DEAL_DATE, element)).notNull();
|
||||
|
||||
final String settlementType = this.fromApiJsonHelper.extractStringNamed(SETTLEMENT_TYPE, element);
|
||||
v.reset().parameter(SETTLEMENT_TYPE).value(settlementType).notBlank();
|
||||
if (StringUtils.isNotBlank(settlementType)) {
|
||||
v.reset().parameter(SETTLEMENT_TYPE).value(settlementType).isOneOfTheseValues(SETTLEMENT_TYPES.toArray());
|
||||
}
|
||||
|
||||
final String category = this.fromApiJsonHelper.extractStringNamed(CATEGORY, element);
|
||||
v.reset().parameter(CATEGORY).value(category).notBlank();
|
||||
if (StringUtils.isNotBlank(category)) {
|
||||
v.reset().parameter(CATEGORY).value(category).isOneOfTheseValues(CATEGORIES.toArray());
|
||||
}
|
||||
|
||||
if ("MUTUAL_FUND".equals(bondType)) {
|
||||
v.reset().parameter(CONSIDER_AMT).value(amount(CONSIDER_AMT, element)).notNull().positiveAmount();
|
||||
v.reset().parameter(NAV).value(amount(NAV, element)).notNull().positiveAmount();
|
||||
} else {
|
||||
v.reset().parameter(FACE_VALUE).value(amount(FACE_VALUE, element)).notNull().positiveAmount();
|
||||
v.reset().parameter(PURCHASE_RATE).value(amount(PURCHASE_RATE, element)).notNull().positiveAmount();
|
||||
v.reset().parameter(LAST_INT_DATE).value(this.fromApiJsonHelper.extractLocalDateNamed(LAST_INT_DATE, element)).notNull();
|
||||
}
|
||||
v.reset().parameter(BROKERAGE).value(amount(BROKERAGE, element)).ignoreIfNull().zeroOrPositiveAmount();
|
||||
v.reset().parameter(STAMP_DUTY).value(amount(STAMP_DUTY, element)).ignoreIfNull().zeroOrPositiveAmount();
|
||||
|
||||
final String dealTime = this.fromApiJsonHelper.extractStringNamed(DEAL_TIME, element);
|
||||
if (StringUtils.isNotBlank(dealTime)) {
|
||||
v.reset().parameter(DEAL_TIME).value(dealTime).matchesRegularExpression(TIME_PATTERN, "Deal Time must be HH:mm:ss (24 hr)");
|
||||
}
|
||||
v.reset().parameter(SUB_TYPE).value(this.fromApiJsonHelper.extractStringNamed(SUB_TYPE, element)).ignoreIfNull()
|
||||
.notExceedingLengthOf(40);
|
||||
|
||||
if (this.fromApiJsonHelper.parameterExists(OTHER_DETAILS, element)) {
|
||||
final JsonElement other = element.getAsJsonObject().get(OTHER_DETAILS);
|
||||
if (!other.isJsonNull() && !other.isJsonObject()) {
|
||||
v.reset().parameter(OTHER_DETAILS).failWithCode("must.be.an.object");
|
||||
} else if (other.isJsonObject() && other.toString().length() > 4000) {
|
||||
v.reset().parameter(OTHER_DETAILS).failWithCode("too.long");
|
||||
}
|
||||
}
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors);
|
||||
}
|
||||
}
|
||||
|
||||
private BigDecimal amount(final String param, final JsonElement element) {
|
||||
return this.fromApiJsonHelper.parameterExists(param, element)
|
||||
? this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(param, element)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* 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.portfolio.investment.service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.time.Month;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* Interest arithmetic for half-yearly coupon securities, as the bank's existing CBS does it (Jitendra Sir):
|
||||
* <ul>
|
||||
* <li>days are counted 30/360 — every month is 30 days, a half-year is 180, a year 360;</li>
|
||||
* <li>the coupon is fixed: face value × rate ÷ 2, paid on the maturity day/month and six months either side;</li>
|
||||
* <li>at each closing (31 March, 30 September) the interest earned since the last coupon is booked as a provision (Dr
|
||||
* Interest Receivable / Cr Interest Income), even though the cash arrives at the next coupon;</li>
|
||||
* <li>when the coupon arrives, the provision already booked is cleared and only the rest is new income.</li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class InvestmentInterestCalculator {
|
||||
|
||||
private static final BigDecimal THIRTY_SIX_THOUSAND = BigDecimal.valueOf(36000);
|
||||
private static final BigDecimal TWO_HUNDRED = BigDecimal.valueOf(200);
|
||||
|
||||
public static final String TYPE_PROVISION = "PROVISION";
|
||||
public static final String TYPE_COUPON = "COUPON";
|
||||
|
||||
private InvestmentInterestCalculator() {}
|
||||
|
||||
/** One line of the interest schedule. */
|
||||
public record ScheduleRow(String type, LocalDate date, LocalDate fromDate, LocalDate toDate, int days, BigDecimal grossInterest,
|
||||
BigDecimal alreadyBooked, BigDecimal amount) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Days between two dates on a 30/360 basis. Counting runs from the day after {@code from} up to and including
|
||||
* {@code to}: 10-08 → 30-09 is 50 days, 10-08 → 08-10 is 58 days, 30-06 → 30-09 is 90 days.
|
||||
*/
|
||||
public static int days360(final LocalDate from, final LocalDate to) {
|
||||
if (from == null || to == null || !to.isAfter(from)) {
|
||||
return 0;
|
||||
}
|
||||
final int d1 = Math.min(from.getDayOfMonth(), 30);
|
||||
final int d2 = Math.min(to.getDayOfMonth(), 30);
|
||||
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. */
|
||||
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);
|
||||
}
|
||||
|
||||
/** The fixed half-yearly coupon: face value × rate ÷ 2. */
|
||||
public static BigDecimal halfYearCoupon(final BigDecimal faceValue, final BigDecimal ratePercent) {
|
||||
return faceValue.multiply(ratePercent).divide(TWO_HUNDRED, 0, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
/** k-th coupon date counting back from maturity — measured from maturity each time so month-end days survive. */
|
||||
private static LocalDate couponBack(final LocalDate maturity, final int k) {
|
||||
return maturity.minusMonths(6L * k);
|
||||
}
|
||||
|
||||
/** The most recent coupon date on or before {@code on}. */
|
||||
public static LocalDate lastCouponOnOrBefore(final LocalDate maturity, final LocalDate on) {
|
||||
int k = 0;
|
||||
LocalDate d = maturity;
|
||||
while (d.isAfter(on)) {
|
||||
k++;
|
||||
d = couponBack(maturity, k);
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
/** Coupon dates strictly after {@code from}, up to and including maturity, in date order. */
|
||||
public static List<LocalDate> couponDatesAfter(final LocalDate maturity, final LocalDate from) {
|
||||
final List<LocalDate> dates = new ArrayList<>();
|
||||
int k = 0;
|
||||
LocalDate d = maturity;
|
||||
while (d.isAfter(from)) {
|
||||
dates.add(0, d);
|
||||
k++;
|
||||
d = couponBack(maturity, k);
|
||||
}
|
||||
return dates;
|
||||
}
|
||||
|
||||
/** Closing dates (31 March, 30 September) from {@code from} up to {@code to}, inclusive. */
|
||||
public static List<LocalDate> closingDates(final LocalDate from, final LocalDate to) {
|
||||
final List<LocalDate> dates = new ArrayList<>();
|
||||
for (int year = from.getYear(); year <= to.getYear(); year++) {
|
||||
for (final LocalDate closing : List.of(LocalDate.of(year, Month.MARCH, 31), LocalDate.of(year, Month.SEPTEMBER, 30))) {
|
||||
if (!closing.isBefore(from) && !closing.isAfter(to)) {
|
||||
dates.add(closing);
|
||||
}
|
||||
}
|
||||
}
|
||||
return dates;
|
||||
}
|
||||
|
||||
/**
|
||||
* The full interest schedule of a holding, from purchase to maturity.
|
||||
*
|
||||
* @param faceValue
|
||||
* face value currently held
|
||||
* @param ratePercent
|
||||
* coupon rate % p.a.
|
||||
* @param maturity
|
||||
* maturity date (coupons fall on its day/month and six months either side)
|
||||
* @param lastIntDate
|
||||
* coupon date immediately before the purchase
|
||||
* @param dealDate
|
||||
* purchase deal date — closings before it belong to the seller
|
||||
* @param purchasedAccrued
|
||||
* interest paid to the seller at purchase; it already sits in Interest Receivable
|
||||
*/
|
||||
public static List<ScheduleRow> schedule(final BigDecimal faceValue, final BigDecimal ratePercent, final LocalDate maturity,
|
||||
final LocalDate lastIntDate, final LocalDate dealDate, final BigDecimal purchasedAccrued) {
|
||||
final List<ScheduleRow> rows = new ArrayList<>();
|
||||
if (faceValue == null || ratePercent == null || ratePercent.signum() <= 0 || maturity == null || lastIntDate == null
|
||||
|| dealDate == null || !maturity.isAfter(lastIntDate)) {
|
||||
return rows;
|
||||
}
|
||||
final List<LocalDate> coupons = couponDatesAfter(maturity, lastIntDate);
|
||||
final TreeSet<LocalDate> events = new TreeSet<>(coupons);
|
||||
for (final LocalDate closing : closingDates(dealDate, maturity)) {
|
||||
if (closing.isAfter(lastIntDate) && !events.contains(closing)) {
|
||||
events.add(closing);
|
||||
}
|
||||
}
|
||||
|
||||
final BigDecimal coupon = halfYearCoupon(faceValue, ratePercent);
|
||||
BigDecimal receivable = purchasedAccrued == null ? BigDecimal.ZERO : purchasedAccrued;
|
||||
LocalDate previousCoupon = lastIntDate;
|
||||
for (final LocalDate date : events) {
|
||||
final int days = days360(previousCoupon, date);
|
||||
if (coupons.contains(date)) {
|
||||
rows.add(new ScheduleRow(TYPE_COUPON, date, previousCoupon.plusDays(1), date, days, coupon, receivable,
|
||||
coupon.subtract(receivable)));
|
||||
receivable = BigDecimal.ZERO;
|
||||
previousCoupon = date;
|
||||
} else {
|
||||
final BigDecimal earned = interestForDays(faceValue, ratePercent, days);
|
||||
rows.add(new ScheduleRow(TYPE_PROVISION, date, previousCoupon.plusDays(1), date, days, earned, receivable,
|
||||
earned.subtract(receivable)));
|
||||
receivable = earned;
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.service;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.fineract.portfolio.investment.data.InvestmentPartyData;
|
||||
import org.apache.fineract.portfolio.investment.data.InvestmentScriptData;
|
||||
|
||||
public interface InvestmentMasterReadPlatformService {
|
||||
|
||||
List<InvestmentPartyData> retrieveAllParties();
|
||||
|
||||
InvestmentPartyData retrieveParty(Long partyId);
|
||||
|
||||
List<InvestmentScriptData> retrieveAllScripts();
|
||||
|
||||
InvestmentScriptData retrieveScript(Long scriptId);
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* 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.portfolio.investment.service;
|
||||
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.portfolio.investment.data.InvestmentPartyData;
|
||||
import org.apache.fineract.portfolio.investment.data.InvestmentScriptData;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPartyRepository;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentScriptRepository;
|
||||
import org.apache.fineract.portfolio.investment.exception.InvestmentPartyNotFoundException;
|
||||
import org.apache.fineract.portfolio.investment.exception.InvestmentScriptNotFoundException;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/** Masters are small (tens of rows), so they are listed whole, newest first, the way the screens show them. */
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional(readOnly = true)
|
||||
public class InvestmentMasterReadPlatformServiceImpl implements InvestmentMasterReadPlatformService {
|
||||
|
||||
private static final Sort NEWEST_FIRST = Sort.by(Sort.Direction.DESC, "id");
|
||||
|
||||
private final InvestmentPartyRepository partyRepository;
|
||||
private final InvestmentScriptRepository scriptRepository;
|
||||
|
||||
@Override
|
||||
public List<InvestmentPartyData> retrieveAllParties() {
|
||||
return this.partyRepository.findAll(NEWEST_FIRST).stream().map(InvestmentPartyData::from).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InvestmentPartyData retrieveParty(final Long partyId) {
|
||||
return this.partyRepository.findById(partyId).map(InvestmentPartyData::from)
|
||||
.orElseThrow(() -> new InvestmentPartyNotFoundException(partyId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<InvestmentScriptData> retrieveAllScripts() {
|
||||
return this.scriptRepository.findAll(NEWEST_FIRST).stream().map(InvestmentScriptData::from).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InvestmentScriptData retrieveScript(final Long scriptId) {
|
||||
return this.scriptRepository.findById(scriptId).map(InvestmentScriptData::from)
|
||||
.orElseThrow(() -> new InvestmentScriptNotFoundException(scriptId));
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.service;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
|
||||
public interface InvestmentMasterWritePlatformService {
|
||||
|
||||
CommandProcessingResult createParty(JsonCommand command);
|
||||
|
||||
CommandProcessingResult updateParty(Long partyId, JsonCommand command);
|
||||
|
||||
CommandProcessingResult deleteParty(Long partyId);
|
||||
|
||||
CommandProcessingResult createScript(JsonCommand command);
|
||||
|
||||
CommandProcessingResult updateScript(Long scriptId, JsonCommand command);
|
||||
}
|
||||
+298
@@ -0,0 +1,298 @@
|
||||
/**
|
||||
* 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.portfolio.investment.service;
|
||||
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ACTIVE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ADDRESS1;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ADDRESS2;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ADDRESS3;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.BANK_TYPE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.BOND_TYPE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CITY;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.COMPANY_NAME;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CORP_CODE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CP_ACCOUNT_NO;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CP_BANK_NAME;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CP_BRANCH_NAME;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CP_IFSC;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.EXPOSURE_LIMIT;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.FACE_VALUE_PER_UNIT;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.FAX;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.GL_ACCOUNT_ID;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.INT_RATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ISIN;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ISSUE_DATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.LEI_NO;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.MATURITY_DATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PARTY_CODE_PREFIX;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PARTY_NAME;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PHONE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PINCODE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.RBI_CODE;
|
||||
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.SLR_TYPE;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.function.Consumer;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
|
||||
import org.apache.fineract.accounting.glaccount.domain.GLAccountRepositoryWrapper;
|
||||
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentParty;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPartyRepository;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentScript;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentScriptRepository;
|
||||
import org.apache.fineract.portfolio.investment.exception.InvestmentPartyNotFoundException;
|
||||
import org.apache.fineract.portfolio.investment.exception.InvestmentScriptNotFoundException;
|
||||
import org.apache.fineract.portfolio.investment.serialization.InvestmentMasterCommandFromApiJsonDeserializer;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class InvestmentMasterWritePlatformServiceImpl implements InvestmentMasterWritePlatformService {
|
||||
|
||||
private final InvestmentMasterCommandFromApiJsonDeserializer validator;
|
||||
private final InvestmentPartyRepository partyRepository;
|
||||
private final InvestmentScriptRepository scriptRepository;
|
||||
private final GLAccountRepositoryWrapper glAccountRepository;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
// -------------------- Bank / Party Master --------------------
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public CommandProcessingResult createParty(final JsonCommand command) {
|
||||
this.validator.validateForCreateParty(command.json());
|
||||
final InvestmentParty party = new InvestmentParty();
|
||||
party.setPartyCode(nextPartyCode());
|
||||
applyParty(party, command);
|
||||
if (command.parameterExists(ACTIVE)) {
|
||||
party.setActive(command.booleanPrimitiveValueOfParameterNamed(ACTIVE));
|
||||
}
|
||||
final InvestmentParty saved = saveParty(party);
|
||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(saved.getId())
|
||||
.withResourceIdAsString(saved.getPartyCode()).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public CommandProcessingResult updateParty(final Long partyId, final JsonCommand command) {
|
||||
this.validator.validateForUpdateParty(command.json());
|
||||
final InvestmentParty party = this.partyRepository.findById(partyId).orElseThrow(() -> new InvestmentPartyNotFoundException(partyId));
|
||||
applyParty(party, command);
|
||||
if (command.parameterExists(ACTIVE)) {
|
||||
party.setActive(command.booleanPrimitiveValueOfParameterNamed(ACTIVE));
|
||||
}
|
||||
saveParty(party);
|
||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(partyId)
|
||||
.withResourceIdAsString(party.getPartyCode()).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public CommandProcessingResult deleteParty(final Long partyId) {
|
||||
final InvestmentParty party = this.partyRepository.findById(partyId).orElseThrow(() -> new InvestmentPartyNotFoundException(partyId));
|
||||
this.partyRepository.delete(party);
|
||||
this.partyRepository.flush();
|
||||
return new CommandProcessingResultBuilder().withEntityId(partyId).withResourceIdAsString(party.getPartyCode()).build();
|
||||
}
|
||||
|
||||
/** Copies every party field present in the request onto the entity (absent fields are left unchanged). */
|
||||
private void applyParty(final InvestmentParty p, final JsonCommand command) {
|
||||
setIfPresent(command, BOND_TYPE, p::setBondType);
|
||||
setIfPresent(command, PARTY_NAME, p::setPartyName);
|
||||
setIfPresent(command, ADDRESS1, p::setAddress1);
|
||||
setIfPresent(command, ADDRESS2, p::setAddress2);
|
||||
setIfPresent(command, ADDRESS3, p::setAddress3);
|
||||
setIfPresent(command, CITY, p::setCity);
|
||||
setIfPresent(command, PINCODE, p::setPincode);
|
||||
setIfPresent(command, PHONE, p::setPhone);
|
||||
setIfPresent(command, FAX, p::setFax);
|
||||
setIfPresent(command, BANK_TYPE, p::setBankType);
|
||||
setIfPresent(command, RBI_CODE, p::setRbiCode);
|
||||
setIfPresent(command, LEI_NO, p::setLeiNo);
|
||||
setIfPresent(command, CP_BANK_NAME, p::setCpBankName);
|
||||
setIfPresent(command, CP_BRANCH_NAME, p::setCpBranchName);
|
||||
setIfPresent(command, CP_ACCOUNT_NO, p::setCpAccountNo);
|
||||
if (command.parameterExists(CP_IFSC)) {
|
||||
final String ifsc = command.stringValueOfParameterNamedAllowingNull(CP_IFSC);
|
||||
p.setCpIfsc(StringUtils.isBlank(ifsc) ? null : ifsc.trim().toUpperCase());
|
||||
}
|
||||
if (command.parameterExists(EXPOSURE_LIMIT)) {
|
||||
p.setExposureLimit(command.bigDecimalValueOfParameterNamed(EXPOSURE_LIMIT));
|
||||
}
|
||||
if (command.parameterExists(GL_ACCOUNT_ID)) {
|
||||
p.setGlAccount(postableAssetGl(command.longValueOfParameterNamed(GL_ACCOUNT_ID)));
|
||||
}
|
||||
}
|
||||
|
||||
private InvestmentParty saveParty(final InvestmentParty party) {
|
||||
try {
|
||||
return this.partyRepository.saveAndFlush(party);
|
||||
} catch (final DataIntegrityViolationException e) {
|
||||
throw new PlatformDataIntegrityException("error.msg.investmentparty.duplicate", "A party with this code already exists",
|
||||
"partyCode", party.getPartyCode(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Next code in the PTY-001, PTY-002 … series. */
|
||||
private String nextPartyCode() {
|
||||
final Integer max = this.jdbcTemplate.queryForObject(
|
||||
"SELECT MAX(CAST(SUBSTRING(party_code FROM 5) AS INTEGER)) FROM m_investment_party WHERE party_code ~ '^PTY-[0-9]+$'",
|
||||
Integer.class);
|
||||
return PARTY_CODE_PREFIX + String.format("%03d", (max == null ? 0 : max) + 1);
|
||||
}
|
||||
|
||||
// -------------------- Script Master --------------------
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public CommandProcessingResult createScript(final JsonCommand command) {
|
||||
this.validator.validateForCreateScript(command.json());
|
||||
final InvestmentScript script = new InvestmentScript();
|
||||
script.setBondType(command.stringValueOfParameterNamed(BOND_TYPE));
|
||||
script.setScriptCode(nextScriptCode(script.getBondType()));
|
||||
script.setFaceValuePerUnit(defaultFaceValue(script.getBondType()));
|
||||
applyScript(script, command);
|
||||
if (command.parameterExists(ACTIVE)) {
|
||||
script.setActive(command.booleanPrimitiveValueOfParameterNamed(ACTIVE));
|
||||
}
|
||||
final InvestmentScript saved = saveScript(script);
|
||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(saved.getId())
|
||||
.withResourceIdAsString(saved.getScriptCode()).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public CommandProcessingResult updateScript(final Long scriptId, final JsonCommand command) {
|
||||
final InvestmentScript script = this.scriptRepository.findById(scriptId)
|
||||
.orElseThrow(() -> new InvestmentScriptNotFoundException(scriptId));
|
||||
this.validator.validateForUpdateScript(command.json(), script.getBondType());
|
||||
if (command.parameterExists(BOND_TYPE) && !script.getBondType().equals(command.stringValueOfParameterNamed(BOND_TYPE))) {
|
||||
// The script code carries the bond-type letter (G00001), so the type cannot change once created.
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.investmentscript.bondtype.immutable",
|
||||
"Bond Type of an existing script cannot be changed", script.getScriptCode());
|
||||
}
|
||||
applyScript(script, command);
|
||||
if (command.parameterExists(ACTIVE)) {
|
||||
script.setActive(command.booleanPrimitiveValueOfParameterNamed(ACTIVE));
|
||||
}
|
||||
saveScript(script);
|
||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(scriptId)
|
||||
.withResourceIdAsString(script.getScriptCode()).build();
|
||||
}
|
||||
|
||||
private void applyScript(final InvestmentScript s, final JsonCommand command) {
|
||||
setIfPresent(command, SLR_TYPE, s::setSlrType);
|
||||
setIfPresent(command, SCRIPT_DESC, s::setScriptDesc);
|
||||
setIfPresent(command, ISIN, s::setIsin);
|
||||
setIfPresent(command, COMPANY_NAME, s::setCompanyName);
|
||||
setIfPresent(command, ADDRESS1, s::setAddress1);
|
||||
setIfPresent(command, ADDRESS2, s::setAddress2);
|
||||
setIfPresent(command, ADDRESS3, s::setAddress3);
|
||||
setIfPresent(command, CITY, s::setCity);
|
||||
setIfPresent(command, PINCODE, s::setPincode);
|
||||
setIfPresent(command, PHONE, s::setPhone);
|
||||
setIfPresent(command, FAX, s::setFax);
|
||||
setIfPresent(command, CORP_CODE, s::setCorpCode);
|
||||
if (command.parameterExists(FACE_VALUE_PER_UNIT) && command.bigDecimalValueOfParameterNamed(FACE_VALUE_PER_UNIT) != null) {
|
||||
s.setFaceValuePerUnit(command.bigDecimalValueOfParameterNamed(FACE_VALUE_PER_UNIT));
|
||||
}
|
||||
if (command.parameterExists(INT_RATE)) {
|
||||
s.setIntRate(command.bigDecimalValueOfParameterNamed(INT_RATE));
|
||||
}
|
||||
if (command.parameterExists(ISSUE_DATE)) {
|
||||
s.setIssueDate(command.localDateValueOfParameterNamed(ISSUE_DATE));
|
||||
}
|
||||
if (command.parameterExists(MATURITY_DATE)) {
|
||||
s.setMaturityDate(command.localDateValueOfParameterNamed(MATURITY_DATE));
|
||||
}
|
||||
if (s.getIssueDate() != null && s.getMaturityDate() != null && s.getMaturityDate().isBefore(s.getIssueDate())) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.investmentscript.maturity.before.issue",
|
||||
"Maturity date cannot be before the issue date");
|
||||
}
|
||||
if (command.parameterExists(GL_ACCOUNT_ID)) {
|
||||
s.setGlAccount(postableAssetGl(command.longValueOfParameterNamed(GL_ACCOUNT_ID)));
|
||||
}
|
||||
}
|
||||
|
||||
private InvestmentScript saveScript(final InvestmentScript script) {
|
||||
try {
|
||||
return this.scriptRepository.saveAndFlush(script);
|
||||
} catch (final DataIntegrityViolationException e) {
|
||||
throw new PlatformDataIntegrityException("error.msg.investmentscript.duplicate", "A script with this code already exists",
|
||||
"scriptCode", script.getScriptCode(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Next code for the bond type — its letter plus a five-digit running number, e.g. G00001. */
|
||||
private String nextScriptCode(final String bondType) {
|
||||
final String prefix = SCRIPT_CODE_PREFIX.get(bondType);
|
||||
final Integer max = this.jdbcTemplate.queryForObject(
|
||||
"SELECT MAX(CAST(SUBSTRING(script_code FROM 2) AS INTEGER)) FROM m_investment_script WHERE script_code ~ ?",
|
||||
Integer.class, "^" + prefix + "[0-9]{5}$");
|
||||
return prefix + String.format("%05d", (max == null ? 0 : max) + 1);
|
||||
}
|
||||
|
||||
/** Face value of one unit: ₹100 for securities and bonds, as the existing CBS defaults it. */
|
||||
private BigDecimal defaultFaceValue(final String bondType) {
|
||||
return switch (bondType) {
|
||||
case "MUTUAL_FUND" -> BigDecimal.valueOf(1000);
|
||||
case "SHARE", "FD" -> BigDecimal.ONE;
|
||||
default -> BigDecimal.valueOf(100);
|
||||
};
|
||||
}
|
||||
|
||||
// -------------------- shared --------------------
|
||||
|
||||
/** An investment is an asset, so the mapped GL must be an enabled ASSET detail account. */
|
||||
private GLAccount postableAssetGl(final Long glAccountId) {
|
||||
final GLAccount gl = this.glAccountRepository.findOneWithNotFoundDetection(glAccountId);
|
||||
if (gl.isHeaderAccount()) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.investment.glaccount.header",
|
||||
"GL " + gl.getGlCode() + " is a header account; choose a detail account", glAccountId);
|
||||
}
|
||||
if (gl.isDisabled()) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.investment.glaccount.disabled", "GL " + gl.getGlCode() + " is disabled",
|
||||
glAccountId);
|
||||
}
|
||||
if (!GLAccountType.ASSET.getValue().equals(gl.getType())) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.investment.glaccount.not.asset",
|
||||
"GL " + gl.getGlCode() + " is not an Asset account", glAccountId);
|
||||
}
|
||||
return gl;
|
||||
}
|
||||
|
||||
private static void setIfPresent(final JsonCommand command, final String param, final Consumer<String> setter) {
|
||||
if (command.parameterExists(param)) {
|
||||
final String value = command.stringValueOfParameterNamedAllowingNull(param);
|
||||
setter.accept(StringUtils.isBlank(value) ? null : value.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 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.portfolio.investment.service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.portfolio.investment.data.InvestmentInterestScheduleData;
|
||||
import org.apache.fineract.portfolio.investment.data.InvestmentPurchaseData;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchase;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchaseRepository;
|
||||
import org.apache.fineract.portfolio.investment.exception.InvestmentPurchaseNotFoundException;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional(readOnly = true)
|
||||
public class InvestmentPurchaseReadPlatformService {
|
||||
|
||||
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
||||
/** Income head for coupon and provision interest (GL 400401 under 400400 INCOME FROM INVESTMENTS). */
|
||||
private static final String INTEREST_GL_CODE = "400401";
|
||||
private static final String INTEREST_GL_NAME = "INTEREST ON GOVT SECURITIES";
|
||||
|
||||
private final InvestmentPurchaseRepository purchaseRepository;
|
||||
|
||||
/** All deals, newest first — the Holdings register. */
|
||||
public List<InvestmentPurchaseData> retrieveAll() {
|
||||
return this.purchaseRepository.findAll(Sort.by(Sort.Direction.DESC, "id")).stream().map(InvestmentPurchaseData::from).toList();
|
||||
}
|
||||
|
||||
public InvestmentPurchaseData retrieveOne(final Long purchaseId) {
|
||||
return InvestmentPurchaseData.from(find(purchaseId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Coupon and closing-provision lines of a holding from purchase to maturity, worked out on the face value still
|
||||
* held. Lines on or before {@code asOnDate} (default: business date) are marked due.
|
||||
*/
|
||||
public InvestmentInterestScheduleData retrieveInterestSchedule(final Long purchaseId, final LocalDate asOnDate) {
|
||||
final InvestmentPurchase p = find(purchaseId);
|
||||
final LocalDate asOn = asOnDate == null ? DateUtils.getBusinessLocalDate() : asOnDate;
|
||||
final BigDecimal unitValue = p.getScript().getFaceValuePerUnit();
|
||||
final BigDecimal faceHeld = p.getBalanceUnits().multiply(unitValue);
|
||||
final BigDecimal rate = p.getIntRate() == null ? BigDecimal.ZERO : p.getIntRate();
|
||||
|
||||
final List<InvestmentInterestScheduleData.Row> rows = InvestmentInterestCalculator
|
||||
.schedule(faceHeld, rate, p.getMaturityDate(), p.getLastIntDate(), p.getDealDate(), p.getAccruedInterest()).stream()
|
||||
.map(r -> InvestmentInterestScheduleData.Row.builder().type(r.type()).intDate(format(r.date()))
|
||||
.fromDate(format(r.fromDate())).toDate(format(r.toDate())).days(r.days()).grossInterest(r.grossInterest())
|
||||
.alreadyBooked(r.alreadyBooked()).amount(r.amount()).due(!r.date().isAfter(asOn)).build())
|
||||
.toList();
|
||||
|
||||
return InvestmentInterestScheduleData.builder().purchaseId(p.getId()).dealNo(p.getDealNo()).certNo(p.getCertNo())
|
||||
.scriptCode(p.getScript().getScriptCode()).scriptDesc(p.getScript().getScriptDesc()).faceValue(faceHeld)
|
||||
.unitValue(unitValue).balanceUnits(p.getBalanceUnits()).purchaseRate(p.getPurchaseRate()).bookValue(p.getBookValue())
|
||||
.intRate(p.getIntRate()).dealDate(format(p.getDealDate())).lastIntDate(format(p.getLastIntDate()))
|
||||
.maturityDate(format(p.getMaturityDate())).purchasedAccruedInterest(p.getAccruedInterest())
|
||||
.couponPerHalfYear(rate.signum() > 0 ? InvestmentInterestCalculator.halfYearCoupon(faceHeld, rate) : BigDecimal.ZERO)
|
||||
.asOnDate(format(asOn)).interestGlCode(INTEREST_GL_CODE).interestGlName(INTEREST_GL_NAME).rows(rows).build();
|
||||
}
|
||||
|
||||
private InvestmentPurchase find(final Long purchaseId) {
|
||||
return this.purchaseRepository.findById(purchaseId).orElseThrow(() -> new InvestmentPurchaseNotFoundException(purchaseId));
|
||||
}
|
||||
|
||||
private static String format(final LocalDate date) {
|
||||
return date == null ? null : date.format(DATE_FORMAT);
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.service;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
|
||||
public interface InvestmentPurchaseWritePlatformService {
|
||||
|
||||
CommandProcessingResult createPurchase(JsonCommand command);
|
||||
|
||||
CommandProcessingResult deletePurchase(Long purchaseId);
|
||||
}
|
||||
+262
@@ -0,0 +1,262 @@
|
||||
/**
|
||||
* 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.portfolio.investment.service;
|
||||
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.BOND_TYPE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.BROKERAGE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.CATEGORY;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.CONSIDER_AMT;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.CR_GL_ACCOUNT_ID;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.DEAL_DATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.DEAL_TIME;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.FACE_VALUE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.LAST_INT_DATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.NAV;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.OTHER_DETAILS;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.PARTY_ID;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.PURCHASE_RATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SCRIPT_ID;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SETTLEMENT_TYPE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.STAMP_DUTY;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.STATUS_ACTIVE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SUB_TYPE;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
|
||||
import org.apache.fineract.accounting.glaccount.domain.GLAccountRepositoryWrapper;
|
||||
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentParty;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPartyRepository;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchase;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchaseRepository;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentScript;
|
||||
import org.apache.fineract.portfolio.investment.domain.InvestmentScriptRepository;
|
||||
import org.apache.fineract.portfolio.investment.exception.InvestmentPartyNotFoundException;
|
||||
import org.apache.fineract.portfolio.investment.exception.InvestmentPurchaseNotFoundException;
|
||||
import org.apache.fineract.portfolio.investment.exception.InvestmentScriptNotFoundException;
|
||||
import org.apache.fineract.portfolio.investment.serialization.InvestmentPurchaseCommandFromApiJsonDeserializer;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* Saves a Purchase Entry as a holding. Every derived figure is computed here from the script master and the deal
|
||||
* inputs, so the stored deal does not depend on what the screen calculated. No GL entry is posted yet.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class InvestmentPurchaseWritePlatformServiceImpl implements InvestmentPurchaseWritePlatformService {
|
||||
|
||||
private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);
|
||||
|
||||
private final InvestmentPurchaseCommandFromApiJsonDeserializer validator;
|
||||
private final InvestmentPurchaseRepository purchaseRepository;
|
||||
private final InvestmentScriptRepository scriptRepository;
|
||||
private final InvestmentPartyRepository partyRepository;
|
||||
private final GLAccountRepositoryWrapper glAccountRepository;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public CommandProcessingResult createPurchase(final JsonCommand command) {
|
||||
this.validator.validateForCreate(command.json());
|
||||
|
||||
final String bondType = command.stringValueOfParameterNamed(BOND_TYPE);
|
||||
final Long scriptId = command.longValueOfParameterNamed(SCRIPT_ID);
|
||||
final InvestmentScript script = this.scriptRepository.findById(scriptId)
|
||||
.orElseThrow(() -> new InvestmentScriptNotFoundException(scriptId));
|
||||
final Long partyId = command.longValueOfParameterNamed(PARTY_ID);
|
||||
final InvestmentParty party = this.partyRepository.findById(partyId)
|
||||
.orElseThrow(() -> new InvestmentPartyNotFoundException(partyId));
|
||||
if (!script.isActive()) {
|
||||
throw rule("script.inactive", "Script " + script.getScriptCode() + " is inactive");
|
||||
}
|
||||
if (!script.getBondType().equals(bondType)) {
|
||||
throw rule("script.bondtype.mismatch",
|
||||
"Script " + script.getScriptCode() + " is a " + script.getBondType() + ", not " + bondType);
|
||||
}
|
||||
if (!party.isActive()) {
|
||||
throw rule("party.inactive", "Party " + party.getPartyCode() + " is inactive");
|
||||
}
|
||||
|
||||
final LocalDate dealDate = command.localDateValueOfParameterNamed(DEAL_DATE);
|
||||
if (script.getMaturityDate() != null && dealDate.isAfter(script.getMaturityDate())) {
|
||||
throw rule("deal.after.maturity", "Deal date is after the security's maturity date " + script.getMaturityDate());
|
||||
}
|
||||
final String settlementType = command.stringValueOfParameterNamed(SETTLEMENT_TYPE);
|
||||
|
||||
final InvestmentPurchase p = new InvestmentPurchase();
|
||||
p.setDealNo(nextDealNo(dealDate));
|
||||
p.setCertNo(nextCertNo());
|
||||
p.setBondType(bondType);
|
||||
p.setScript(script);
|
||||
p.setParty(party);
|
||||
p.setDealDate(dealDate);
|
||||
p.setDealTime(StringUtils.trimToNull(command.stringValueOfParameterNamedAllowingNull(DEAL_TIME)));
|
||||
p.setSettlementType(settlementType);
|
||||
p.setSettlementDate(dealDate.plusDays(Long.parseLong(settlementType.substring(2))));
|
||||
p.setCategory(command.stringValueOfParameterNamed(CATEGORY));
|
||||
p.setSubType(StringUtils.trimToNull(command.stringValueOfParameterNamedAllowingNull(SUB_TYPE)));
|
||||
p.setIntRate(script.getIntRate());
|
||||
p.setIssueDate(script.getIssueDate());
|
||||
p.setMaturityDate(script.getMaturityDate());
|
||||
p.setInvestmentGlAccount(script.getGlAccount());
|
||||
p.setCrGlAccount(postableAssetGl(command.longValueOfParameterNamed(CR_GL_ACCOUNT_ID)));
|
||||
p.setOtherDetails(otherDetails(command));
|
||||
p.setStatus(STATUS_ACTIVE);
|
||||
|
||||
final BigDecimal brokerage = zeroIfNull(command.bigDecimalValueOfParameterNamed(BROKERAGE));
|
||||
p.setBrokerage(brokerage);
|
||||
if ("MUTUAL_FUND".equals(bondType)) {
|
||||
computeMutualFund(p, command, brokerage);
|
||||
} else {
|
||||
computeSecurity(p, command, script, brokerage);
|
||||
}
|
||||
|
||||
final InvestmentPurchase saved = this.purchaseRepository.saveAndFlush(p);
|
||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(saved.getId())
|
||||
.withResourceIdAsString(saved.getDealNo()).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public CommandProcessingResult deletePurchase(final Long purchaseId) {
|
||||
final InvestmentPurchase p = this.purchaseRepository.findById(purchaseId)
|
||||
.orElseThrow(() -> new InvestmentPurchaseNotFoundException(purchaseId));
|
||||
if (p.getBalanceUnits().compareTo(p.getUnits()) != 0) {
|
||||
throw rule("purchase.partly.sold", "Deal " + p.getDealNo() + " has been partly sold and cannot be dropped");
|
||||
}
|
||||
this.purchaseRepository.delete(p);
|
||||
this.purchaseRepository.flush();
|
||||
return new CommandProcessingResultBuilder().withEntityId(purchaseId).withResourceIdAsString(p.getDealNo()).build();
|
||||
}
|
||||
|
||||
/** Govt Security / Bond: face value at a price per ₹100, plus interest owed to the seller since the last coupon. */
|
||||
private void computeSecurity(final InvestmentPurchase p, final JsonCommand command, final InvestmentScript script,
|
||||
final BigDecimal brokerage) {
|
||||
final BigDecimal faceValue = command.bigDecimalValueOfParameterNamed(FACE_VALUE);
|
||||
final BigDecimal rate = command.bigDecimalValueOfParameterNamed(PURCHASE_RATE);
|
||||
final LocalDate lastIntDate = command.localDateValueOfParameterNamed(LAST_INT_DATE);
|
||||
if (lastIntDate.isAfter(p.getDealDate())) {
|
||||
throw rule("lastint.after.deal", "Last interest date cannot be after the deal date");
|
||||
}
|
||||
final BigDecimal units = faceValue.divide(script.getFaceValuePerUnit(), 6, RoundingMode.HALF_UP);
|
||||
final BigDecimal cost = faceValue.multiply(rate).divide(HUNDRED, 2, RoundingMode.HALF_UP);
|
||||
final int days = InvestmentInterestCalculator.days360(lastIntDate, p.getDealDate());
|
||||
final BigDecimal intRate = zeroIfNull(script.getIntRate());
|
||||
// Interest is counted up to the deal date (the event), not the settlement date — Jitendra Sir.
|
||||
final BigDecimal accrued = InvestmentInterestCalculator.interestForDays(faceValue, intRate, days);
|
||||
|
||||
p.setFaceValue(faceValue);
|
||||
p.setPurchaseRate(rate);
|
||||
p.setUnits(units);
|
||||
p.setBalanceUnits(units);
|
||||
p.setBookValue(cost.add(brokerage));
|
||||
p.setPremiumDiscount(faceValue.multiply(rate.subtract(HUNDRED)).divide(HUNDRED, 2, RoundingMode.HALF_UP));
|
||||
p.setLastIntDate(lastIntDate);
|
||||
p.setAccruedDays(days);
|
||||
p.setAccruedInterest(accrued);
|
||||
p.setSettlementAmount(p.getBookValue().add(accrued));
|
||||
p.setYtm(approxYtm(intRate, rate, script.getIssueDate(), script.getMaturityDate()));
|
||||
}
|
||||
|
||||
/** Mutual fund: the amount invested buys units at the NAV; stamp duty and brokerage add to the cost. */
|
||||
private void computeMutualFund(final InvestmentPurchase p, final JsonCommand command, final BigDecimal brokerage) {
|
||||
final BigDecimal amount = command.bigDecimalValueOfParameterNamed(CONSIDER_AMT);
|
||||
final BigDecimal nav = command.bigDecimalValueOfParameterNamed(NAV);
|
||||
final BigDecimal stampDuty = zeroIfNull(command.bigDecimalValueOfParameterNamed(STAMP_DUTY));
|
||||
final BigDecimal units = amount.divide(nav, 3, RoundingMode.HALF_UP);
|
||||
|
||||
p.setFaceValue(amount);
|
||||
p.setPurchaseRate(nav);
|
||||
p.setUnits(units);
|
||||
p.setBalanceUnits(units);
|
||||
p.setStampDuty(stampDuty);
|
||||
p.setBookValue(amount.add(stampDuty).add(brokerage));
|
||||
p.setAccruedInterest(BigDecimal.ZERO);
|
||||
p.setAccruedDays(0);
|
||||
p.setSettlementAmount(p.getBookValue());
|
||||
}
|
||||
|
||||
/** Approximate yield: coupon adjusted for the discount/premium spread over the security's life, over the price. */
|
||||
static BigDecimal approxYtm(final BigDecimal coupon, final BigDecimal price, final LocalDate issue, final LocalDate maturity) {
|
||||
if (coupon.signum() == 0 || issue == null || maturity == null || !maturity.isAfter(issue)) {
|
||||
return coupon;
|
||||
}
|
||||
final double years = ChronoUnit.DAYS.between(issue, maturity) / 365.25;
|
||||
final double ytm = (coupon.doubleValue() + (100 - price.doubleValue()) / years) / price.doubleValue() * 100;
|
||||
return BigDecimal.valueOf(ytm).setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
/** Financial-year deal number: FY start year + FY end (2 digits) + 6-digit running number, e.g. 202627000001. */
|
||||
private String nextDealNo(final LocalDate dealDate) {
|
||||
final String prefix = dealNoPrefix(dealDate);
|
||||
final Integer max = this.jdbcTemplate.queryForObject(
|
||||
"SELECT MAX(CAST(SUBSTRING(deal_no FROM 7) AS INTEGER)) FROM m_investment_purchase WHERE deal_no LIKE ?", Integer.class,
|
||||
prefix + "%");
|
||||
return prefix + String.format("%06d", (max == null ? 0 : max) + 1);
|
||||
}
|
||||
|
||||
/** Financial year (April–March) of the deal date: 2026-04-01 … 2027-03-31 → "202627". */
|
||||
static String dealNoPrefix(final LocalDate dealDate) {
|
||||
final int fyStart = dealDate.getMonthValue() >= 4 ? dealDate.getYear() : dealDate.getYear() - 1;
|
||||
return fyStart + String.format("%02d", (fyStart + 1) % 100);
|
||||
}
|
||||
|
||||
private String nextCertNo() {
|
||||
final Integer max = this.jdbcTemplate.queryForObject(
|
||||
"SELECT MAX(CAST(cert_no AS INTEGER)) FROM m_investment_purchase WHERE cert_no ~ '^[0-9]+$'", Integer.class);
|
||||
return String.valueOf((max == null ? 0 : max) + 1);
|
||||
}
|
||||
|
||||
private static String otherDetails(final JsonCommand command) {
|
||||
if (!command.parameterExists(OTHER_DETAILS)) {
|
||||
return null;
|
||||
}
|
||||
final JsonElement other = command.parsedJson().getAsJsonObject().get(OTHER_DETAILS);
|
||||
return other == null || other.isJsonNull() ? null : other.toString();
|
||||
}
|
||||
|
||||
/** The paying bank account must be an enabled ASSET detail GL. */
|
||||
private GLAccount postableAssetGl(final Long glAccountId) {
|
||||
final GLAccount gl = this.glAccountRepository.findOneWithNotFoundDetection(glAccountId);
|
||||
if (gl.isHeaderAccount() || gl.isDisabled() || !GLAccountType.ASSET.getValue().equals(gl.getType())) {
|
||||
throw rule("cr.glaccount.invalid", "CR. To A/c " + gl.getGlCode() + " must be an enabled Asset detail account");
|
||||
}
|
||||
return gl;
|
||||
}
|
||||
|
||||
private static BigDecimal zeroIfNull(final BigDecimal value) {
|
||||
return value == null ? BigDecimal.ZERO : value;
|
||||
}
|
||||
|
||||
private static GeneralPlatformDomainRuleException rule(final String code, final String message) {
|
||||
return new GeneralPlatformDomainRuleException("error.msg.investmentpurchase." + code, message);
|
||||
}
|
||||
}
|
||||
+1
-8
@@ -55,7 +55,6 @@ import org.apache.fineract.portfolio.loanaccount.guarantor.exception.InvalidGuar
|
||||
import org.apache.fineract.portfolio.loanaccount.guarantor.serialization.GuarantorCommandFromApiJsonDeserializer;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler;
|
||||
import org.apache.fineract.portfolio.savings.service.RecurringDepositLienService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -80,7 +79,6 @@ public class GuarantorWritePlatformServiceJpaRepositoryIImpl implements Guaranto
|
||||
private final SavingsAccountAssembler savingsAccountAssembler;
|
||||
private final AccountAssociationsRepository accountAssociationsRepository;
|
||||
private final GuarantorDomainService guarantorDomainService;
|
||||
private final RecurringDepositLienService recurringDepositLienService;
|
||||
|
||||
@Autowired
|
||||
public GuarantorWritePlatformServiceJpaRepositoryIImpl(final LoanRepositoryWrapper loanRepositoryWrapper,
|
||||
@@ -88,9 +86,7 @@ public class GuarantorWritePlatformServiceJpaRepositoryIImpl implements Guaranto
|
||||
final StaffRepositoryWrapper staffRepositoryWrapper, final GroupRepositoryWrapper groupRepositoryWrapper,
|
||||
final GuarantorCommandFromApiJsonDeserializer fromApiJsonDeserializer,
|
||||
final CodeValueRepositoryWrapper codeValueRepositoryWrapper, final SavingsAccountAssembler savingsAccountAssembler,
|
||||
final AccountAssociationsRepository accountAssociationsRepository, final GuarantorDomainService guarantorDomainService,
|
||||
final RecurringDepositLienService recurringDepositLienService) {
|
||||
this.recurringDepositLienService = recurringDepositLienService;
|
||||
final AccountAssociationsRepository accountAssociationsRepository, final GuarantorDomainService guarantorDomainService) {
|
||||
this.loanRepositoryWrapper = loanRepositoryWrapper;
|
||||
this.clientRepositoryWrapper = clientRepositoryWrapper;
|
||||
this.groupRepositoryWrapper = groupRepositoryWrapper;
|
||||
@@ -124,9 +120,6 @@ public class GuarantorWritePlatformServiceJpaRepositoryIImpl implements Guaranto
|
||||
final SavingsAccount savingsAccount = this.savingsAccountAssembler.assembleFrom(guarantorCommand.getSavingsId(),
|
||||
backdatedTxnsAllowedTill);
|
||||
validateGuarantorSavingsAccountActivationDateWithLoanSubmittedOnDate(loan, savingsAccount);
|
||||
// A recurring deposit pledged here is a lien on it: its product must allow that, and it can
|
||||
// never secure more than the balance it has not already pledged.
|
||||
this.recurringDepositLienService.validatePledge(savingsAccount, guarantorCommand.getAmount(), loan.getId());
|
||||
accountAssociations = AccountAssociations.associateSavingsAccount(loan, savingsAccount,
|
||||
AccountAssociationType.GUARANTOR_ACCOUNT_ASSOCIATION.getValue(), backdatedTxnsAllowedTill);
|
||||
|
||||
|
||||
+3
-5
@@ -71,11 +71,9 @@ public class RbiReserveConfigApiResource {
|
||||
static {
|
||||
ROLE_DESCRIPTIONS.put("CASH_IN_HAND",
|
||||
"Cash the bank itself holds - vault and teller balances. Counts toward CRR maintained.");
|
||||
// BALANCE_WITH_OTHER_BANKS is deliberately not offered. It belongs to the older working
|
||||
// report, and the statutory Form 1 this bank files has no line for it: balances with other
|
||||
// banks are reported there under III(a), III(b)(i) or VI, depending on which bank holds
|
||||
// them. Left on the screen it invited a mapping that would raise item X without appearing
|
||||
// anywhere on the form to say where the figure came from.
|
||||
ROLE_DESCRIPTIONS.put("BALANCE_WITH_OTHER_BANKS",
|
||||
"Current account balances the bank holds with other banks. Counts toward CRR maintained. "
|
||||
+ "Pin every such account - a bank typically banks with several.");
|
||||
ROLE_DESCRIPTIONS.put("DEMAND_TIME_LIABILITIES",
|
||||
"Deposit liabilities forming the DTL base that CRR and SLR are charged on. "
|
||||
+ "Pin the deposit accounts and any interest accrued on them.");
|
||||
|
||||
+45
-63
@@ -1286,22 +1286,16 @@ public class RbiReserveReturnApiResource {
|
||||
final Rate slr) {
|
||||
final List<Map<String, Object>> lines = new ArrayList<>();
|
||||
|
||||
// The labels below are the printed form's own wording, kept word for word. A shortened line
|
||||
// reads more easily on screen but this form is filed and signed, and a reader checking it
|
||||
// against the paper return has to find each line by its text - a paraphrase makes them stop
|
||||
// and work out whether it is the same line.
|
||||
lines.add(formOneHeading("I. LIABILITIES IN INDIA TO THE BANKING SYSTEM :"));
|
||||
lines.add(formOneHeading("(a) Demand Liabilities:"));
|
||||
final List<BigDecimal> iai = formOneLine(lines,
|
||||
"(i) Total of credit balances in current accounts maintained with the co-operative bank by State Bank of India,"
|
||||
+ " subsidiary banks and corresponding new banks.",
|
||||
lines.add(formOneHeading("I. LIABILITIES IN INDIA TO THE BANKING SYSTEM"));
|
||||
lines.add(formOneHeading("(a) Demand Liabilities"));
|
||||
final List<BigDecimal> iai = formOneLine(lines, "(i) Current account balances of SBI, its subsidiaries and the corresponding new banks",
|
||||
"FORM1_I_A_I_SBI_CURRENT", dates, officeId, 2);
|
||||
final List<BigDecimal> iaii = formOneLine(lines, "(ii) Total of other demand liabilites to the banking system",
|
||||
final List<BigDecimal> iaii = formOneLine(lines, "(ii) Other demand liabilities to the banking system",
|
||||
"FORM1_I_A_II_OTHER_DEMAND_BS", dates, officeId, 2);
|
||||
final List<BigDecimal> ib = formOneLine(lines, "(b) Time Liabilities to the banking system", "FORM1_I_B_TIME_BS",
|
||||
final List<BigDecimal> ib = formOneLine(lines, "(b) Time liabilities to the banking system", "FORM1_I_B_TIME_BS",
|
||||
dates, officeId, 1);
|
||||
final List<BigDecimal> totalI = sumOf(iai, iaii, ib);
|
||||
lines.add(formOneTotal("TOTAL OF I : >>>>>>>>>>>", totalI));
|
||||
lines.add(formOneTotal("TOTAL OF I", totalI));
|
||||
|
||||
lines.add(formOneHeading("II. LIABILITIES IN INDIA TO OTHERS"));
|
||||
// Demand and time come from the one DTL head, split by the class each mapped account
|
||||
@@ -1313,25 +1307,23 @@ public class RbiReserveReturnApiResource {
|
||||
iiA.add(dtl.demandAmount);
|
||||
iiB.add(dtl.timeAmount);
|
||||
}
|
||||
lines.add(formOneRow("(A) DEMAND LIABILITIES :", "DEMAND_TIME_LIABILITIES", iiA, 1));
|
||||
lines.add(formOneRow("(B) TIME LIABILITIES :", "DEMAND_TIME_LIABILITIES", iiB, 1));
|
||||
lines.add(formOneRow("(A) Demand liabilities", "DEMAND_TIME_LIABILITIES", iiA, 1));
|
||||
lines.add(formOneRow("(B) Time liabilities", "DEMAND_TIME_LIABILITIES", iiB, 1));
|
||||
final List<BigDecimal> totalII = sumOf(iiA, iiB);
|
||||
lines.add(formOneTotal("TOTAL OF II : >>>>>>>>>>>", totalII));
|
||||
lines.add(formOneTotal("TOTAL OF II", totalII));
|
||||
|
||||
lines.add(formOneHeading("III. ASSETS IN INDIA WITH THE BANKING SYSTEM"));
|
||||
final List<BigDecimal> iiiA = formOneLine(lines,
|
||||
"(A) TOTAL OF CREDIT BALANCES IN CURRENT ACCOUNT maintained with S.B.I, Subsidiary Banks & corresponding new banks",
|
||||
final List<BigDecimal> iiiA = formOneLine(lines, "(A) Current account balances with SBI, its subsidiaries and the corresponding new banks",
|
||||
"FORM1_III_A_SBI_CURRENT", dates, officeId, 1);
|
||||
lines.add(formOneHeading("(B) TOTAL OF OTHER ASSETS WITH THE BANKING SYSTEM, VIZ."));
|
||||
final List<BigDecimal> iiiBi = formOneLine(lines,
|
||||
"(i) Balances in all Accounts other than those included in item III (a) :",
|
||||
lines.add(formOneHeading("(B) Other assets with the banking system"));
|
||||
final List<BigDecimal> iiiBi = formOneLine(lines, "(i) Balances in all accounts other than III(A)",
|
||||
"FORM1_III_B_I_OTHER_BALANCES", dates, officeId, 2);
|
||||
final List<BigDecimal> iiiBii = formOneLine(lines, "(ii) Money at Call and Short notice :",
|
||||
final List<BigDecimal> iiiBii = formOneLine(lines, "(ii) Money at call and short notice",
|
||||
"FORM1_III_B_II_MONEY_AT_CALL_BS", dates, officeId, 2);
|
||||
final List<BigDecimal> iiiBiv = formOneLine(lines, "(iv) Advances & any other Assets :",
|
||||
final List<BigDecimal> iiiBiv = formOneLine(lines, "(iv) Advances and any other assets",
|
||||
"FORM1_III_B_IV_ADVANCES_BS", dates, officeId, 2);
|
||||
final List<BigDecimal> totalIII = sumOf(iiiA, iiiBi, iiiBii, iiiBiv);
|
||||
lines.add(formOneTotal("TOTAL OF III : >>>>>>>>>>", totalIII));
|
||||
lines.add(formOneTotal("TOTAL OF III", totalIII));
|
||||
|
||||
// IV: the banking system nets out, and what is left is added to the liabilities to others.
|
||||
// Floored at zero per column - the form's own wording, "(I - III) + II if minus figure",
|
||||
@@ -1346,9 +1338,7 @@ public class RbiReserveReturnApiResource {
|
||||
// requirement is actually charged on. A form showing one IV reads as though the bank had to
|
||||
// hold a ratio of today's liabilities, which is not what either section says.
|
||||
lines.add(formOneTotal(
|
||||
"IV. a) TOTAL (NET) DEMAND & TIME LIABILITIES FOR THE PURPOSE OF SEC.18 & 24 OF THE ACT = (I-III)+II IF MINUS"
|
||||
+ " FIGURE [CURRENT FORTNIGHT]",
|
||||
iv));
|
||||
"IV. a) TOTAL (NET) DEMAND AND TIME LIABILITIES FOR THE PURPOSE OF SECTIONS 18 AND 24 [CURRENT FORTNIGHT]", iv));
|
||||
final List<BigDecimal> ivBase = new ArrayList<>();
|
||||
final List<String> ivBaseDates = new ArrayList<>();
|
||||
for (int i = 0; i < dates.size(); i++) {
|
||||
@@ -1356,38 +1346,33 @@ public class RbiReserveReturnApiResource {
|
||||
ivBaseDates.add(base.toString());
|
||||
ivBase.add(formOneNetLiabilities(base, officeId));
|
||||
}
|
||||
// The printed form words this "AS ON THE LAST FRIDAY OF THE SECOND PRECEDING FORTNIGHT",
|
||||
// which is the Act's rule. This bank does not file on that rule - its base is the matching
|
||||
// fortnight close one month earlier, which formOneBaseDate computes - so the line says BASE
|
||||
// DATE and leaves the dates themselves to the column headings. A heading that named a
|
||||
// Friday the return never used would be a statement nobody could check.
|
||||
final Map<String, Object> ivBaseRow = formOneTotal(
|
||||
"IV. b) DEMAND AND TIME LIABILITY OF BASE DATE FOR THE PURPOSE OF SEC.18 & 24",
|
||||
"IV. b) TOTAL (NET) DEMAND AND TIME LIABILITIES FOR THE PURPOSE OF SECTIONS 18 AND 24 [SECOND PRECEDING FORTNIGHT]",
|
||||
ivBase);
|
||||
ivBaseRow.put("baseDates", ivBaseDates);
|
||||
lines.add(ivBaseRow);
|
||||
|
||||
final List<BigDecimal> v = formOneLine(lines, "V. CASH ON HAND :", "CASH_IN_HAND", dates, officeId, 0);
|
||||
final List<BigDecimal> v = formOneLine(lines, "V. CASH ON HAND", "CASH_IN_HAND", dates, officeId, 0);
|
||||
|
||||
lines.add(formOneHeading("VI. BALANCE IN CURRENT ACCOUNTS WITH :"));
|
||||
final List<BigDecimal> viA = formOneLine(lines, "(a) Reserve Bank of India :", "FORM1_VI_A_RBI_CURRENT", dates,
|
||||
lines.add(formOneHeading("VI. BALANCE IN CURRENT ACCOUNTS WITH"));
|
||||
final List<BigDecimal> viA = formOneLine(lines, "(a) Reserve Bank of India", "FORM1_VI_A_RBI_CURRENT", dates,
|
||||
officeId, 1);
|
||||
final List<BigDecimal> viB = formOneLine(lines, "(b) State Co.Op.Bank of the State Concerned :",
|
||||
final List<BigDecimal> viB = formOneLine(lines, "(b) State Co-operative Bank of the state concerned",
|
||||
"FORM1_VI_B_STATE_COOP_CURRENT", dates, officeId, 1);
|
||||
final List<BigDecimal> viC = formOneLine(lines, "(c) Central Co.Op.Bank of the Dist.Concerned :",
|
||||
final List<BigDecimal> viC = formOneLine(lines, "(c) District Central Co-operative Bank of the district concerned",
|
||||
"FORM1_VI_C_DCCB_CURRENT", dates, officeId, 1);
|
||||
final List<BigDecimal> totalVI = sumOf(viA, viB, viC);
|
||||
lines.add(formOneTotal("TOTAL OF VI : >>>>>>>>>>", totalVI));
|
||||
lines.add(formOneTotal("TOTAL OF VI", totalVI));
|
||||
|
||||
lines.add(formOneHeading("VII. Balance Of all Other types With :"));
|
||||
final List<BigDecimal> viiA = formOneLine(lines, "(a) State Co.Op.Bank of the State Concerned :",
|
||||
lines.add(formOneHeading("VII. BALANCES OF ALL OTHER TYPES WITH"));
|
||||
final List<BigDecimal> viiA = formOneLine(lines, "(a) State Co-operative Bank of the state concerned",
|
||||
"FORM1_VII_A_STATE_COOP_OTHER", dates, officeId, 1);
|
||||
final List<BigDecimal> viiB = formOneLine(lines, "(B) CENTRAL CO.OP.BANK OF THE DIST.CONCERNED",
|
||||
final List<BigDecimal> viiB = formOneLine(lines, "(b) District Central Co-operative Bank",
|
||||
"FORM1_VII_B_DCCB_OTHER", dates, officeId, 1);
|
||||
final List<BigDecimal> viiC = formOneLine(lines, "(C) INVESTMENT OF GOVT. SECURITY",
|
||||
final List<BigDecimal> viiC = formOneLine(lines, "(c) Investment in Government securities",
|
||||
"FORM1_VII_C_GOVT_SECURITIES", dates, officeId, 1);
|
||||
final List<BigDecimal> totalVII = sumOf(viiA, viiB, viiC);
|
||||
lines.add(formOneTotal("TOTAL OF VII : >>>>>>>>>>", totalVII));
|
||||
lines.add(formOneTotal("TOTAL OF VII", totalVII));
|
||||
|
||||
// VIII: only the excess counts. Where the banking system's claim on this bank exceeds the
|
||||
// bank's own balance there is nothing to carry into the cash reserve, so it floors at zero.
|
||||
@@ -1395,7 +1380,7 @@ public class RbiReserveReturnApiResource {
|
||||
for (int i = 0; i < dates.size(); i++) {
|
||||
viii.add(iiiA.get(i).subtract(iai.get(i)).max(BigDecimal.ZERO));
|
||||
}
|
||||
lines.add(formOneTotal("VIII. Net balance in current accounts i.e.excess of III (a) over I (a) (i) :", viii));
|
||||
lines.add(formOneTotal("VIII. NET BALANCE IN CURRENT ACCOUNTS — excess of III(A) over I(a)(i)", viii));
|
||||
|
||||
// The base liabilities are IV(b) above - it used to be repeated as a row of its own at the
|
||||
// foot of the part, which put the same figure on the form twice under two different names.
|
||||
@@ -1432,12 +1417,12 @@ public class RbiReserveReturnApiResource {
|
||||
// template's own wording says "Last Friday of the Second Preceding fortnight"; this bank's
|
||||
// base rule is the matching close of the previous month, so the line says IV(b) and leaves
|
||||
// the base date itself to the column heading rather than claiming a Friday it did not use.
|
||||
lines.add(formOneTotal("IX. " + (crr == null ? "CRR rate not set" : crr.percentage + " PERCENT") + " OF IV AS ON THE BASE DATE",
|
||||
lines.add(formOneTotal("IX. CRR REQUIRED TO BE MAINTAINED — " + (crr == null ? "rate not set" : crr.percentage + "% of IV(b)"),
|
||||
required));
|
||||
lines.add(formOneTotal("X. CASH RESERVE ACTUALLY MAINTAINED := V+VI+VIII", maintained));
|
||||
lines.add(formOneTotal("X. CASH RESERVE ACTUALLY MAINTAINED (V + VI + VIII)", maintained));
|
||||
|
||||
final Map<String, Object> part = new LinkedHashMap<>();
|
||||
part.put("title", "PART B : COMPLIANCE WITH SECTION 18. (Not applicable to scheduled state co-op. Bank)");
|
||||
part.put("title", "PART B — Compliance with Section 18");
|
||||
part.put("lines", lines);
|
||||
part.put("required", required);
|
||||
part.put("maintained", maintained);
|
||||
@@ -1460,35 +1445,32 @@ public class RbiReserveReturnApiResource {
|
||||
: base.multiply(slr.percentage).divide(HUNDRED, 2, RoundingMode.HALF_UP);
|
||||
required.add(slrRequired);
|
||||
|
||||
// XII(a) is the cash reserve held OVER the section 18 requirement, plus the government
|
||||
// securities of VII(c). Only the excess cash counts: the part of the cash reserve that
|
||||
// meets section 18 is already committed and cannot also meet section 24.
|
||||
//
|
||||
// VII(c) alone, not the whole of VII. VII(a) and VII(b) are balances with the State
|
||||
// Co-operative Bank and the District Central Co-operative Bank - deposits placed with
|
||||
// other co-operative banks, which section 24 does not admit as liquid assets. Counting
|
||||
// them here overstated what the bank held against SLR by the whole of those deposits.
|
||||
// XII(a) is the cash reserve held OVER the section 18 requirement, plus the other
|
||||
// balances of part VII. Only the excess counts: the part of the cash reserve that meets
|
||||
// section 18 is already committed and cannot also meet section 24.
|
||||
final BigDecimal crrRequired = formOnePartBRequired(date, officeId);
|
||||
final BigDecimal crrMaintained = formOnePartBMaintained(date, officeId);
|
||||
final BigDecimal excessCash = crrMaintained.subtract(crrRequired).max(BigDecimal.ZERO);
|
||||
final BigDecimal governmentSecurities = figureFor("FORM1_VII_C_GOVT_SECURITIES", date, officeId).amount;
|
||||
cashAndOther.add(excessCash.add(governmentSecurities));
|
||||
final BigDecimal otherBalances = figureFor("FORM1_VII_A_STATE_COOP_OTHER", date, officeId).amount
|
||||
.add(figureFor("FORM1_VII_B_DCCB_OTHER", date, officeId).amount)
|
||||
.add(figureFor("FORM1_VII_C_GOVT_SECURITIES", date, officeId).amount);
|
||||
cashAndOther.add(excessCash.add(otherBalances));
|
||||
gold.add(figureFor("FORMA_SLR_GOLD", date, officeId).amount);
|
||||
securities.add(figureFor("SLR_INVESTMENTS", date, officeId).amount);
|
||||
}
|
||||
|
||||
// Charged on IV(b) like the cash reserve above, and named the same way.
|
||||
lines.add(formOneTotal("XI. " + (slr == null ? "SLR rate not set" : slr.percentage + " percent") + " (or a higher specified percentage) of IV as on the base date",
|
||||
lines.add(formOneTotal("XI. SLR REQUIRED TO BE MAINTAINED — " + (slr == null ? "rate not set" : slr.percentage + "% of IV(b)"),
|
||||
required));
|
||||
lines.add(formOneHeading("XII. ASSETS ACTUALLY MAINTAINED :"));
|
||||
lines.add(formOneRow("(a) Cash and other balances maintained in India i.e. X-IX+VII :", null, cashAndOther, 1));
|
||||
lines.add(formOneHeading("XII. ASSETS ACTUALLY MAINTAINED"));
|
||||
lines.add(formOneRow("(a) Cash and other balances maintained in India (X − IX + VII)", null, cashAndOther, 1));
|
||||
lines.add(formOneRow("(b) Gold", "FORMA_SLR_GOLD", gold, 1));
|
||||
lines.add(formOneRow("(c) Unencombered approved Securites $$", "SLR_INVESTMENTS", securities, 1));
|
||||
lines.add(formOneRow("(c) Unencumbered approved securities", "SLR_INVESTMENTS", securities, 1));
|
||||
final List<BigDecimal> totalXII = sumOf(cashAndOther, gold, securities);
|
||||
lines.add(formOneTotal("Total of XII.....................................", totalXII));
|
||||
lines.add(formOneTotal("TOTAL OF XII", totalXII));
|
||||
|
||||
final Map<String, Object> part = new LinkedHashMap<>();
|
||||
part.put("title", "PART C : COMPLIANCE WITH SECTION 24 (Not applicable to scheduled state co-op. banks)");
|
||||
part.put("title", "PART C — Compliance with Section 24");
|
||||
part.put("lines", lines);
|
||||
part.put("required", required);
|
||||
part.put("maintained", totalXII);
|
||||
|
||||
+1
-4
@@ -361,9 +361,6 @@ public class RecurringDepositAccountsApiResource {
|
||||
} else if (is(commandParam, "close")) {
|
||||
final CommandWrapper commandRequest = builder.closeRecurringDepositAccount(accountId).build();
|
||||
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||
} else if (is(commandParam, "extendTenure")) {
|
||||
final CommandWrapper commandRequest = builder.extendTenureRecurringDepositAccount(accountId).build();
|
||||
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||
} else if (is(commandParam, "prematureClose")) {
|
||||
final CommandWrapper commandRequest = builder.prematureCloseRecurringDepositAccount(accountId).build();
|
||||
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||
@@ -380,7 +377,7 @@ public class RecurringDepositAccountsApiResource {
|
||||
if (result == null) {
|
||||
throw new UnrecognizedQueryParamException("command", commandParam,
|
||||
new Object[] { "reject", "withdrawnByApplicant", "approve", "undoapproval", "activate", "calculateInterest",
|
||||
"postInterest", "close", "prematureClose", "calculatePrematureAmount", "extendTenure" });
|
||||
"postInterest", "close", "prematureClose", "calculatePrematureAmount" });
|
||||
}
|
||||
|
||||
return this.toApiJsonSerializer.serialize(result);
|
||||
|
||||
-205
@@ -1,205 +0,0 @@
|
||||
/**
|
||||
* 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.portfolio.savings.api;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import java.time.LocalDate;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.savings.DepositsApiConstants;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* RD Dashboard's data source: every Recurring Deposit still running (ACTIVE, or MATURED and waiting to be closed) in
|
||||
* the user's office hierarchy, each with its installment position worked out as of the business date, plus the
|
||||
* month's closures and penalty income for the summary cards.
|
||||
*
|
||||
* <p>
|
||||
* An installment counts as paid by the deposit's cumulative deposits, not by the schedule's own completed flag - the
|
||||
* same rule {@code RecurringDepositMissedInstallmentPenaltyService} uses, since money paid in advance at the counter is
|
||||
* not always allocated to the installment it covers. The dashboard and the penalty job therefore agree on which
|
||||
* installments are overdue.
|
||||
*/
|
||||
@Path("/v1/recurringdepositaccounts/dashboard")
|
||||
@Component
|
||||
@Tag(name = "Recurring Deposit Accounts", description = "RD Dashboard: running RDs with their installment position.")
|
||||
@RequiredArgsConstructor
|
||||
public class RecurringDepositDashboardApiResource {
|
||||
|
||||
/** DepositAccountType.RECURRING_DEPOSIT. */
|
||||
private static final int RECURRING_DEPOSIT = 300;
|
||||
/** SavingsAccountStatusType values. */
|
||||
private static final int ACTIVE = 300;
|
||||
private static final int CLOSED = 600;
|
||||
private static final int PRE_MATURE_CLOSURE = 700;
|
||||
private static final int MATURED = 800;
|
||||
/** SavingsAccountTransactionType.DEPOSIT. */
|
||||
private static final int DEPOSIT = 1;
|
||||
|
||||
private static final String ROWS_SQL = """
|
||||
WITH rd AS (
|
||||
SELECT sa.id, sa.account_no, sa.client_id, sa.product_id, sa.status_enum, sa.account_balance_derived,
|
||||
sa.auto_debit_facility, sa.linked_account_no, c.display_name AS client_name, o.id AS office_id,
|
||||
o.name AS office_name
|
||||
FROM m_savings_account sa
|
||||
JOIN m_client c ON c.id = sa.client_id
|
||||
JOIN m_office o ON o.id = c.office_id
|
||||
WHERE sa.deposit_type_enum = ? AND sa.status_enum IN (?, ?) AND o.hierarchy LIKE ?
|
||||
),
|
||||
paid AS (
|
||||
SELECT t.savings_account_id AS id, SUM(t.amount) AS total_paid,
|
||||
SUM(t.amount) FILTER (WHERE t.transaction_date BETWEEN ? AND ?) AS paid_this_month
|
||||
FROM m_savings_account_transaction t
|
||||
JOIN rd ON rd.id = t.savings_account_id
|
||||
WHERE t.transaction_type_enum = ? AND t.is_reversed = false
|
||||
GROUP BY t.savings_account_id
|
||||
),
|
||||
sched AS (
|
||||
SELECT s.savings_account_id AS id, s.duedate, s.deposit_amount,
|
||||
SUM(s.deposit_amount) OVER (PARTITION BY s.savings_account_id ORDER BY s.duedate, s.installment) AS cum_due
|
||||
FROM m_mandatory_savings_schedule s
|
||||
JOIN rd ON rd.id = s.savings_account_id
|
||||
),
|
||||
pos AS (
|
||||
SELECT sc.id,
|
||||
COUNT(*) AS total_installments,
|
||||
COUNT(*) FILTER (WHERE sc.cum_due <= COALESCE(p.total_paid, 0)) AS paid_installments,
|
||||
MIN(sc.duedate) FILTER (WHERE sc.cum_due > COALESCE(p.total_paid, 0)) AS next_unpaid_due,
|
||||
COUNT(*) FILTER (WHERE sc.cum_due > COALESCE(p.total_paid, 0) AND sc.duedate < ?) AS overdue_installments,
|
||||
GREATEST(COALESCE(SUM(sc.deposit_amount) FILTER (WHERE sc.duedate < ?), 0) - COALESCE(MAX(p.total_paid), 0), 0)
|
||||
AS overdue_amount,
|
||||
COUNT(*) FILTER (WHERE sc.duedate = ?) AS due_today_count,
|
||||
COUNT(*) FILTER (WHERE sc.duedate = ? AND sc.cum_due <= COALESCE(p.total_paid, 0)) AS due_today_paid,
|
||||
COALESCE(SUM(sc.deposit_amount) FILTER (WHERE sc.duedate BETWEEN ? AND ?), 0) AS due_this_month,
|
||||
COALESCE(SUM(sc.deposit_amount) FILTER (WHERE sc.duedate BETWEEN ? AND ?
|
||||
AND sc.cum_due <= COALESCE(p.total_paid, 0)), 0) AS received_this_month
|
||||
FROM sched sc
|
||||
LEFT JOIN paid p ON p.id = sc.id
|
||||
GROUP BY sc.id
|
||||
),
|
||||
pen AS (
|
||||
SELECT pe.savings_account_id AS id, COUNT(*) AS penalty_count, SUM(pe.penalty_amount) AS penalty_amount
|
||||
FROM m_rd_installment_penalty pe
|
||||
JOIN rd ON rd.id = pe.savings_account_id
|
||||
GROUP BY pe.savings_account_id
|
||||
),
|
||||
lien AS (
|
||||
SELECT paa.linked_savings_account_id AS id, SUM(COALESCE(gfd.amount_remaining_derived, gfd.amount)) AS lien_amount
|
||||
FROM m_guarantor_funding_details gfd
|
||||
JOIN m_portfolio_account_associations paa ON paa.id = gfd.account_associations_id
|
||||
JOIN m_loan l ON l.id = paa.loan_account_id
|
||||
JOIN rd ON rd.id = paa.linked_savings_account_id
|
||||
WHERE gfd.status_enum = 100 AND l.loan_status_id IN (100, 200, 300)
|
||||
AND COALESCE(gfd.amount_remaining_derived, gfd.amount) > 0
|
||||
GROUP BY paa.linked_savings_account_id
|
||||
)
|
||||
SELECT rd.id AS "id", rd.account_no AS "accountNo", rd.client_id AS "clientId", rd.client_name AS "clientName",
|
||||
rd.office_id AS "officeId", rd.office_name AS "officeName", sp.id AS "productId", sp.name AS "productName",
|
||||
rd.status_enum AS "status", rd.account_balance_derived AS "balance",
|
||||
rdd.mandatory_recommended_deposit_amount AS "installmentAmount",
|
||||
TO_CHAR(datp.maturity_date, 'YYYY-MM-DD') AS "maturityDate", datp.maturity_amount AS "maturityAmount",
|
||||
COALESCE(rd.auto_debit_facility, false) AS "autoDebit", rd.linked_account_no AS "linkedAccountNo",
|
||||
COALESCE(sp.missed_installment_grace_days, 0) AS "graceDays",
|
||||
COALESCE(sp.penalty_grace_period_missed_installments, false) AS "penaltyEnabled",
|
||||
COALESCE(pos.total_installments, 0) AS "totalInstallments",
|
||||
COALESCE(pos.paid_installments, 0) AS "paidInstallments",
|
||||
TO_CHAR(pos.next_unpaid_due, 'YYYY-MM-DD') AS "nextDueDate",
|
||||
COALESCE(pos.overdue_installments, 0) AS "overdueInstallments",
|
||||
COALESCE(pos.overdue_amount, 0) AS "overdueAmount",
|
||||
COALESCE(pos.due_today_count, 0) AS "dueTodayCount",
|
||||
COALESCE(pos.due_today_paid, 0) AS "dueTodayPaid",
|
||||
COALESCE(pos.due_this_month, 0) AS "dueThisMonth",
|
||||
COALESCE(pos.received_this_month, 0) AS "receivedThisMonth",
|
||||
COALESCE(p.paid_this_month, 0) AS "paidThisMonth",
|
||||
COALESCE(pen.penalty_count, 0) AS "penaltyCount",
|
||||
COALESCE(pen.penalty_amount, 0) AS "penaltyAmount",
|
||||
COALESCE(lien.lien_amount, 0) AS "lienAmount"
|
||||
FROM rd
|
||||
JOIN m_savings_product sp ON sp.id = rd.product_id
|
||||
LEFT JOIN m_deposit_account_term_and_preclosure datp ON datp.savings_account_id = rd.id
|
||||
LEFT JOIN m_deposit_account_recurring_detail rdd ON rdd.savings_account_id = rd.id
|
||||
LEFT JOIN pos ON pos.id = rd.id
|
||||
LEFT JOIN paid p ON p.id = rd.id
|
||||
LEFT JOIN pen ON pen.id = rd.id
|
||||
LEFT JOIN lien ON lien.id = rd.id
|
||||
ORDER BY pos.next_unpaid_due NULLS LAST, rd.account_no
|
||||
""";
|
||||
|
||||
/** RDs closed this month, split by how they ended. */
|
||||
private static final String CLOSED_THIS_MONTH_SQL = """
|
||||
SELECT COUNT(*) FILTER (WHERE sa.status_enum = ?) AS matured_closed,
|
||||
COUNT(*) FILTER (WHERE sa.status_enum = ?) AS premature_closed
|
||||
FROM m_savings_account sa
|
||||
JOIN m_client c ON c.id = sa.client_id
|
||||
JOIN m_office o ON o.id = c.office_id
|
||||
WHERE sa.deposit_type_enum = ? AND sa.closedon_date BETWEEN ? AND ? AND o.hierarchy LIKE ?
|
||||
""";
|
||||
|
||||
/** Missed-installment penalties charged, this month and in all, on RDs in the hierarchy (closed ones included). */
|
||||
private static final String PENALTY_INCOME_SQL = """
|
||||
SELECT COALESCE(SUM(pe.penalty_amount) FILTER (WHERE pe.applied_on_date BETWEEN ? AND ?), 0) AS this_month,
|
||||
COALESCE(SUM(pe.penalty_amount), 0) AS total
|
||||
FROM m_rd_installment_penalty pe
|
||||
JOIN m_savings_account sa ON sa.id = pe.savings_account_id
|
||||
JOIN m_client c ON c.id = sa.client_id
|
||||
JOIN m_office o ON o.id = c.office_id
|
||||
WHERE o.hierarchy LIKE ?
|
||||
""";
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final Gson gson = new Gson();
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public String retrieve() {
|
||||
this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME);
|
||||
final String hierarchy = this.context.authenticatedUser().getOffice().getHierarchy() + "%";
|
||||
|
||||
// "Today" is the business date - the branch's working day, not the server clock.
|
||||
final LocalDate today = DateUtils.getBusinessLocalDate();
|
||||
final LocalDate monthStart = today.withDayOfMonth(1);
|
||||
final LocalDate monthEnd = today.withDayOfMonth(today.lengthOfMonth());
|
||||
|
||||
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(ROWS_SQL, RECURRING_DEPOSIT, ACTIVE, MATURED, hierarchy,
|
||||
monthStart, monthEnd, DEPOSIT, today, today, today, today, monthStart, monthEnd, monthStart, monthEnd);
|
||||
final Map<String, Object> closed = this.jdbcTemplate.queryForMap(CLOSED_THIS_MONTH_SQL, CLOSED, PRE_MATURE_CLOSURE,
|
||||
RECURRING_DEPOSIT, monthStart, monthEnd, hierarchy);
|
||||
final Map<String, Object> penalty = this.jdbcTemplate.queryForMap(PENALTY_INCOME_SQL, monthStart, monthEnd, hierarchy);
|
||||
|
||||
final Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("businessDate", today.toString());
|
||||
result.put("maturedClosedThisMonth", closed.get("matured_closed"));
|
||||
result.put("prematureClosedThisMonth", closed.get("premature_closed"));
|
||||
result.put("penaltyIncomeThisMonth", penalty.get("this_month"));
|
||||
result.put("penaltyIncomeTotal", penalty.get("total"));
|
||||
result.put("accounts", rows);
|
||||
return this.gson.toJson(result);
|
||||
}
|
||||
}
|
||||
-80
@@ -1,80 +0,0 @@
|
||||
/**
|
||||
* 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.portfolio.savings.api;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.savings.DepositsApiConstants;
|
||||
import org.apache.fineract.portfolio.savings.service.RecurringDepositLienService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Read-only: the loans a recurring deposit currently secures and how much of it each one holds, plus how much more
|
||||
* could be given as loan against it today, so the RD screens can show the lien, its limit, and why the deposit cannot
|
||||
* be closed.
|
||||
*/
|
||||
@Path("/v1/recurringdepositaccounts/{accountId}/liens")
|
||||
@Component
|
||||
@Tag(name = "Recurring Deposit Account", description = "Liens on a recurring deposit pledged as loan security.")
|
||||
@RequiredArgsConstructor
|
||||
public class RecurringDepositLienApiResource {
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final RecurringDepositLienService recurringDepositLienService;
|
||||
private final Gson gson = new Gson();
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public String retrieve(@PathParam("accountId") final Long accountId) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME);
|
||||
final List<RecurringDepositLienService.Lien> liens = this.recurringDepositLienService.activeLiens(accountId);
|
||||
final Map<String, Object> result = new LinkedHashMap<>();
|
||||
final BigDecimal totalLien = liens.stream().map(RecurringDepositLienService.Lien::amount).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
result.put("totalLienAmount", totalLien);
|
||||
final RecurringDepositLienService.LienLimit limit = this.recurringDepositLienService.lienLimit(accountId);
|
||||
if (limit != null) {
|
||||
result.put("lienAllowed", limit.lienAllowed());
|
||||
result.put("lienPercentage", limit.accountPercentage());
|
||||
result.put("productLienPercentage", limit.productPercentage());
|
||||
result.put("effectiveLienPercentage", limit.effectivePercentage());
|
||||
result.put("maxLoanAmount", limit.maxLoanAmount());
|
||||
result.put("availableForLoan", limit.maxLoanAmount().subtract(totalLien).max(BigDecimal.ZERO));
|
||||
}
|
||||
result.put("liens", liens.stream().map(lien -> {
|
||||
final Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("loanId", lien.loanId());
|
||||
row.put("loanAccountNo", lien.loanAccountNo());
|
||||
row.put("loanStatus", lien.loanStatus());
|
||||
row.put("amount", lien.amount());
|
||||
return row;
|
||||
}).toList());
|
||||
return this.gson.toJson(result);
|
||||
}
|
||||
}
|
||||
-36
@@ -180,7 +180,6 @@ public class DepositProductDataValidator {
|
||||
}
|
||||
|
||||
validateRecurringDetailForCreate(element, baseDataValidator);
|
||||
validateLienPercentage(element, baseDataValidator);
|
||||
|
||||
if (!hasRdSlabs) {
|
||||
validateChartsData(element, baseDataValidator);
|
||||
@@ -216,7 +215,6 @@ public class DepositProductDataValidator {
|
||||
}
|
||||
|
||||
validateRecurringDepositUpdate(element, baseDataValidator);
|
||||
validateLienPercentage(element, baseDataValidator);
|
||||
|
||||
// RD products now use the CBS base chart as a read-only template, so
|
||||
// product updates no longer require a charts payload.
|
||||
@@ -227,40 +225,6 @@ public class DepositProductDataValidator {
|
||||
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
||||
}
|
||||
|
||||
/** Lien Marked's loan limit: a share of the deposit's balance, so above 0 and at most 100. Optional. */
|
||||
private void validateLienPercentage(final JsonElement element, final DataValidatorBuilder baseDataValidator) {
|
||||
if (!this.fromApiJsonHelper.parameterExists("lienPercentage", element)) {
|
||||
return;
|
||||
}
|
||||
final BigDecimal lienPercentage = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("lienPercentage", element);
|
||||
baseDataValidator.reset().parameter("lienPercentage").value(lienPercentage).ignoreIfNull().positiveAmount()
|
||||
.notGreaterThanMax(BigDecimal.valueOf(100));
|
||||
validateMissedInstallmentPenalty(element, baseDataValidator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Missed-installment penalty: grace days are whole days from 0, the type is 1 (flat amount) or 2 (% of the
|
||||
* installment), and the amount is positive - at most 100 when it is a percentage. All optional.
|
||||
*/
|
||||
private void validateMissedInstallmentPenalty(final JsonElement element, final DataValidatorBuilder baseDataValidator) {
|
||||
if (this.fromApiJsonHelper.parameterExists("missedInstallmentGraceDays", element)) {
|
||||
final Integer graceDays = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("missedInstallmentGraceDays", element);
|
||||
baseDataValidator.reset().parameter("missedInstallmentGraceDays").value(graceDays).ignoreIfNull().integerZeroOrGreater();
|
||||
}
|
||||
Integer penaltyType = null;
|
||||
if (this.fromApiJsonHelper.parameterExists("missedInstallmentPenaltyType", element)) {
|
||||
penaltyType = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("missedInstallmentPenaltyType", element);
|
||||
baseDataValidator.reset().parameter("missedInstallmentPenaltyType").value(penaltyType).ignoreIfNull().isOneOfTheseValues(1, 2);
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists("missedInstallmentPenaltyAmount", element)) {
|
||||
final BigDecimal amount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("missedInstallmentPenaltyAmount", element);
|
||||
baseDataValidator.reset().parameter("missedInstallmentPenaltyAmount").value(amount).ignoreIfNull().positiveAmount();
|
||||
if (amount != null && Integer.valueOf(2).equals(penaltyType)) {
|
||||
baseDataValidator.reset().parameter("missedInstallmentPenaltyAmount").value(amount).notGreaterThanMax(BigDecimal.valueOf(100));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateDepositDetailForCreate(final JsonElement element, final FromJsonHelper fromApiJsonHelper,
|
||||
final DataValidatorBuilder baseDataValidator, final DepositAccountType accountType) {
|
||||
final String name = fromApiJsonHelper.extractStringNamed(nameParamName, element);
|
||||
|
||||
-3
@@ -67,7 +67,4 @@ public interface DepositAccountDomainService {
|
||||
|
||||
Long handleFDAccountPayment(FixedDepositAccount account, PaymentDetail paymentDetail, AppUser user, JsonCommand command,
|
||||
Map<String, Object> changes);
|
||||
|
||||
/** Names the journal entries of the recurring deposit's interest credits that have no narration yet. */
|
||||
void narrateRdInterestPostings(RecurringDepositAccount account);
|
||||
}
|
||||
|
||||
+15
-164
@@ -72,7 +72,6 @@ import org.apache.fineract.portfolio.savings.DepositAccountOnClosureType;
|
||||
import org.apache.fineract.portfolio.savings.DepositAccountType;
|
||||
import org.apache.fineract.portfolio.savings.DepositsApiConstants;
|
||||
import org.apache.fineract.portfolio.savings.PrematureClosureReasonType;
|
||||
import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType;
|
||||
import org.apache.fineract.portfolio.savings.SavingsApiConstants;
|
||||
import org.apache.fineract.portfolio.savings.SavingsPeriodFrequencyType;
|
||||
import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues;
|
||||
@@ -448,47 +447,14 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi
|
||||
final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale);
|
||||
final LocalDate closedDate = command.localDateValueOfParameterNamed(SavingsApiConstants.closedOnDateParamName);
|
||||
Long savingsTransactionId = null;
|
||||
// Checked before anything is posted or paid out, so a wrong date is reported as such.
|
||||
account.validateMaturityClosureDate(closedDate);
|
||||
account.postMaturityInterest(isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, closedDate, postReversals);
|
||||
// Paid out late: the days between maturity and today earn the product's post-maturity rate, so every closure
|
||||
// branch below - reinvest, transfer or withdraw - carries it.
|
||||
account.postOverdueMaturityInterest(closedDate, postReversals);
|
||||
final BigDecimal transactionAmount = account.getAccountBalance();
|
||||
final Integer onAccountClosureId = command.integerValueOfParameterNamed(onAccountClosureIdParamName);
|
||||
final DepositAccountOnClosureType onClosureType = DepositAccountOnClosureType.fromInt(onAccountClosureId);
|
||||
if (onClosureType.isReinvest()) {
|
||||
BigDecimal reInvestAmount;
|
||||
if (onClosureType.isReinvestPrincipal()) {
|
||||
// getDepositAmount() is the deposit's contracted total (every installment it was meant to receive),
|
||||
// not what actually came in. Capped at the balance, or a deposit with missed installments - or none -
|
||||
// would open its successor with money that was never paid.
|
||||
reInvestAmount = account.getDepositAmount() == null ? account.getAccountBalance()
|
||||
: account.getDepositAmount().min(account.getAccountBalance());
|
||||
// Principal only: the interest part does not go into the new deposit, so it has to be paid
|
||||
// somewhere real first - the savings account named on the request, else the one chosen when
|
||||
// the deposit was opened, else its linked savings account. Previously the full balance was
|
||||
// withdrawn while only the principal was redeposited, so the interest simply vanished.
|
||||
final BigDecimal interestPart = account.getAccountBalance().subtract(reInvestAmount);
|
||||
if (interestPart.signum() > 0) {
|
||||
final Long interestTargetId = resolveRdPayoutSavingsId(account,
|
||||
command.longValueOfParameterNamed(toSavingsAccountIdParamName));
|
||||
if (interestTargetId == null) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.recurringdeposit.reinvest.principal.needs.savings",
|
||||
"Reinvest Principal Only pays the interest out to a savings account - choose one, or link a Savings account to this deposit.");
|
||||
}
|
||||
final SavingsAccount interestTarget = this.depositAccountAssembler.assembleFrom(interestTargetId,
|
||||
DepositAccountType.SAVINGS_DEPOSIT);
|
||||
final boolean isExceptionForBalanceCheck = false;
|
||||
final String description = "RD " + account.getAccountNumber() + " interest to savings "
|
||||
+ interestTarget.getAccountNumber() + " (maturity, principal reinvested)";
|
||||
final AccountTransferDTO interestTransfer = new AccountTransferDTO(closedDate, interestPart, PortfolioAccountType.SAVINGS,
|
||||
PortfolioAccountType.SAVINGS, null, null, description, locale, fmt, null, null, null, null, null,
|
||||
AccountTransferType.INTEREST_TRANSFER.getValue(), null, null, ExternalId.empty(), null, interestTarget, account,
|
||||
isRegularTransaction, isExceptionForBalanceCheck);
|
||||
narrateTransfer(this.accountTransfersWritePlatformService.transferFunds(interestTransfer), description);
|
||||
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
|
||||
}
|
||||
reInvestAmount = account.getDepositAmount();
|
||||
} else {
|
||||
reInvestAmount = account.getAccountBalance();
|
||||
}
|
||||
@@ -504,36 +470,26 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi
|
||||
frequency = frequency == -1 ? 1 : frequency;
|
||||
reinvestedDeposit.generateSchedule(frequencyType, frequency, calendar);
|
||||
reinvestedDeposit.processAccountUponActivation(fmt, postReversals, relaxingDaysConfigForPivotDate);
|
||||
// The opening credit is on the new deposit but its stored balance was never recomputed, so it read 0
|
||||
// until something else touched the account.
|
||||
reinvestedDeposit.updateSavingsAccountSummary(reinvestedDeposit.getTransactions());
|
||||
reinvestedDeposit.updateMaturityDateAndAmount(mc, isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd,
|
||||
financialYearBeginningMonth);
|
||||
this.savingsAccountRepository.saveAndFlush(reinvestedDeposit);
|
||||
this.savingsAccountRepository.save(reinvestedDeposit);
|
||||
autoGenerateAccountNumber(reinvestedDeposit);
|
||||
// The money moves deposit-to-deposit and never touches cash: the new deposit's opening credit and the
|
||||
// old deposit's withdrawal below both post against transfers-in-suspense, which nets to nil. Before
|
||||
// this the opening credit posted no journal entry at all and the withdrawal posted as cash paid out,
|
||||
// leaving the RD control GL short and cash overstated by the whole reinvested amount.
|
||||
final boolean isInternalTransferLeg = true;
|
||||
postJournalEntries(reinvestedDeposit, new HashSet<>(), new HashSet<>(), isInternalTransferLeg);
|
||||
|
||||
final boolean applyWithdrawFee = false;
|
||||
final SavingsAccountTransaction withdrawal = this.handleWithdrawal(account, fmt, closedDate, account.getAccountBalance(),
|
||||
paymentDetail, applyWithdrawFee, isRegularTransaction, isInternalTransferLeg);
|
||||
paymentDetail, false, isRegularTransaction);
|
||||
savingsTransactionId = withdrawal.getId();
|
||||
// The withdrawal posted journal entries for everything new on the account (the maturity interest
|
||||
// included), so every transaction now on it is accounted for.
|
||||
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
|
||||
changes.put("reinvestedDepositId", reinvestedDeposit.getId());
|
||||
|
||||
} else if (onClosureType.isTransferToSavings()) {
|
||||
final Long toSavingsId = command.longValueOfParameterNamed(toSavingsAccountIdParamName);
|
||||
final String transferDescription = command.stringValueOfParameterNamed(transferDescriptionParamName);
|
||||
final SavingsAccount toSavingsAccount = this.depositAccountAssembler.assembleFrom(toSavingsId,
|
||||
DepositAccountType.SAVINGS_DEPOSIT);
|
||||
transferRdPayoutToSavings(account, toSavingsAccount, closedDate, "maturity", transferDescription, locale, fmt,
|
||||
isRegularTransaction);
|
||||
final boolean isExceptionForBalanceCheck = false;
|
||||
final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(closedDate, transactionAmount,
|
||||
PortfolioAccountType.SAVINGS, PortfolioAccountType.SAVINGS, null, null, transferDescription, locale, fmt, null, null,
|
||||
null, null, null, AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, ExternalId.empty(), null,
|
||||
toSavingsAccount, account, isRegularTransaction, isExceptionForBalanceCheck);
|
||||
this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
|
||||
updateAlreadyPostedTransactions(existingTransactionIds, account);
|
||||
} else {
|
||||
final SavingsInterestTdsService.DirectPayout tds = this.savingsInterestTdsService.deductOnDirectPayout(account, closedDate,
|
||||
@@ -549,28 +505,10 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi
|
||||
this.savingsAccountRepository.save(account);
|
||||
|
||||
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer);
|
||||
narrateRdInterestPostings(account);
|
||||
|
||||
return savingsTransactionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Where a recurring deposit's proceeds may be paid when no account was named for this closure: the savings
|
||||
* account chosen when it was opened, else its own linked Savings account (Deposit Account Link).
|
||||
*/
|
||||
private Long resolveRdPayoutSavingsId(final RecurringDepositAccount account, final Long requestedSavingsId) {
|
||||
if (requestedSavingsId != null) {
|
||||
return requestedSavingsId;
|
||||
}
|
||||
if (account.getTransferToSavingsAccountId() != null) {
|
||||
return account.getTransferToSavingsAccountId();
|
||||
}
|
||||
if (account.getLinkedAccountId() != null && "SAVINGS".equalsIgnoreCase(account.getLinkedAccountType())) {
|
||||
return account.getLinkedAccountId();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private CalendarInstance getCalendarInstance(final RecurringDepositAccount account, RecurringDepositAccount reinvestedDeposit) {
|
||||
CalendarInstance calendarInstance = null;
|
||||
CalendarInstance parentCalendarInstance = this.calendarInstanceRepository.findByEntityIdAndEntityTypeIdAndCalendarTypeId(
|
||||
@@ -1259,9 +1197,6 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi
|
||||
final Locale locale = command.extractLocale();
|
||||
final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale);
|
||||
Long savingsTransactionId = null;
|
||||
// The reason decides the penalty (and, for a deceased holder, lock-in) before any interest is worked out.
|
||||
account.applyPrematureClosureReason(
|
||||
PrematureClosureReasonType.fromInt(command.integerValueOfParameterNamed(prematureClosureReasonIdParamName)));
|
||||
// post interest
|
||||
account.postPreMaturityInterest(closedDate, isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd,
|
||||
financialYearBeginningMonth, postReversals);
|
||||
@@ -1270,12 +1205,16 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi
|
||||
DepositAccountOnClosureType closureType = DepositAccountOnClosureType.fromInt(closureTypeValue);
|
||||
|
||||
if (closureType.isTransferToSavings()) {
|
||||
final boolean isExceptionForBalanceCheck = false;
|
||||
final Long toSavingsId = command.longValueOfParameterNamed(toSavingsAccountIdParamName);
|
||||
final String transferDescription = command.stringValueOfParameterNamed(transferDescriptionParamName);
|
||||
final SavingsAccount toSavingsAccount = this.depositAccountAssembler.assembleFrom(toSavingsId,
|
||||
DepositAccountType.SAVINGS_DEPOSIT);
|
||||
transferRdPayoutToSavings(account, toSavingsAccount, closedDate, "premature closure", transferDescription, locale, fmt,
|
||||
isRegularTransaction);
|
||||
final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(closedDate, account.getAccountBalance(),
|
||||
PortfolioAccountType.SAVINGS, PortfolioAccountType.SAVINGS, null, null, transferDescription, locale, fmt, null, null,
|
||||
null, null, null, AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, ExternalId.empty(), null,
|
||||
toSavingsAccount, account, isRegularTransaction, isExceptionForBalanceCheck);
|
||||
this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
|
||||
updateAlreadyPostedTransactions(existingTransactionIds, account);
|
||||
} else {
|
||||
final SavingsInterestTdsService.DirectPayout tds = this.savingsInterestTdsService.deductOnDirectPayout(account, closedDate,
|
||||
@@ -1289,32 +1228,9 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi
|
||||
account.prematureClosure(user, command, changes);
|
||||
this.savingsAccountRepository.save(account);
|
||||
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer);
|
||||
narrateRdInterestPostings(account);
|
||||
return savingsTransactionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Names the journal entries of every interest credit on the recurring deposit that has none yet - the term's
|
||||
* interest posted at maturity or premature closure, and any post-maturity interest - so the ledger shows whose
|
||||
* interest it is. Called once the closure's own entries are posted, which is the last of them to be written.
|
||||
*/
|
||||
@Override
|
||||
public void narrateRdInterestPostings(final RecurringDepositAccount account) {
|
||||
final String narration = "RD " + account.getAccountNumber() + " interest credited";
|
||||
narrateJournalEntries("""
|
||||
SELECT id FROM m_savings_account_transaction
|
||||
WHERE savings_account_id = ? AND transaction_type_enum = ? AND is_reversed = false
|
||||
""", narration, account.getId(), SavingsAccountTransactionType.INTEREST_POSTING.getValue());
|
||||
// An interest credit taken back (a premature closure, or a maturity whose final rate came out lower, credits
|
||||
// the recalculated interest instead): its original entries are already named, so this names only the reversing
|
||||
// entries.
|
||||
narrateJournalEntries("""
|
||||
SELECT id FROM m_savings_account_transaction
|
||||
WHERE savings_account_id = ? AND transaction_type_enum = ? AND is_reversed = true
|
||||
""", "RD " + account.getAccountNumber() + " interest reversed - recalculated on closure", account.getId(),
|
||||
SavingsAccountTransactionType.INTEREST_POSTING.getValue());
|
||||
}
|
||||
|
||||
private void updateExistingTransactionsDetails(SavingsAccount account, Set<Long> existingTransactionIds,
|
||||
Set<Long> existingReversedTransactionIds) {
|
||||
existingTransactionIds.addAll(account.findExistingTransactionIds());
|
||||
@@ -1331,71 +1247,6 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi
|
||||
this.journalEntryWritePlatformService.createJournalEntriesForSavings(accountingBridgeData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pays a closing recurring deposit into a savings account as two credits - its interest, then the principal -
|
||||
* each described with the deposit's account number, so the savings statement shows which RD the money came from
|
||||
* and how much of it is interest.
|
||||
*
|
||||
* <p>
|
||||
* The interest goes first and as an interest transfer: TDS counts as interest only what a payout carries beyond
|
||||
* the interest already swept out of the deposit, so with the interest swept first the principal transfer after it
|
||||
* is never mistaken for interest, and the interest is tracked for TDS on its own credit.
|
||||
*/
|
||||
private void transferRdPayoutToSavings(final RecurringDepositAccount account, final SavingsAccount toSavingsAccount,
|
||||
final LocalDate closedDate, final String closureLabel, final String clerkNote, final Locale locale,
|
||||
final DateTimeFormatter fmt, final boolean isRegularTransaction) {
|
||||
final boolean isExceptionForBalanceCheck = false;
|
||||
final String note = clerkNote == null || clerkNote.isBlank() ? "" : " - " + clerkNote.trim();
|
||||
final String rd = "RD " + account.getAccountNumber();
|
||||
final BigDecimal interest = account.getInterestInBalance();
|
||||
final BigDecimal principal = account.getAccountBalance().subtract(interest);
|
||||
|
||||
if (interest.signum() > 0) {
|
||||
final String description = rd + " interest to savings " + toSavingsAccount.getAccountNumber() + " (" + closureLabel + ")" + note;
|
||||
final Long transferId = this.accountTransfersWritePlatformService.transferFunds(new AccountTransferDTO(closedDate, interest,
|
||||
PortfolioAccountType.SAVINGS, PortfolioAccountType.SAVINGS, null, null, description, locale, fmt, null, null, null, null,
|
||||
null, AccountTransferType.INTEREST_TRANSFER.getValue(), null, null, ExternalId.empty(), null, toSavingsAccount, account,
|
||||
isRegularTransaction, isExceptionForBalanceCheck));
|
||||
narrateTransfer(transferId, description);
|
||||
}
|
||||
if (principal.signum() > 0) {
|
||||
final String description = rd + " principal to savings " + toSavingsAccount.getAccountNumber() + " (" + closureLabel + ")"
|
||||
+ note;
|
||||
final Long transferId = this.accountTransfersWritePlatformService.transferFunds(new AccountTransferDTO(closedDate, principal,
|
||||
PortfolioAccountType.SAVINGS, PortfolioAccountType.SAVINGS, null, null, description, locale, fmt, null, null, null, null,
|
||||
null, AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, ExternalId.empty(), null, toSavingsAccount, account,
|
||||
isRegularTransaction, isExceptionForBalanceCheck));
|
||||
narrateTransfer(transferId, description);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a narration onto the journal entries of both sides of a transfer. The entries are posted while the
|
||||
* transfer is still being made, before its own record - and so its description - exists, so they are named here
|
||||
* once it does.
|
||||
*/
|
||||
private void narrateTransfer(final Long transferDetailsId, final String narration) {
|
||||
if (transferDetailsId == null) {
|
||||
return;
|
||||
}
|
||||
narrateJournalEntries("""
|
||||
SELECT from_savings_transaction_id FROM m_account_transfer_transaction WHERE account_transfer_details_id = ?
|
||||
UNION SELECT to_savings_transaction_id FROM m_account_transfer_transaction WHERE account_transfer_details_id = ?
|
||||
""", narration, transferDetailsId, transferDetailsId);
|
||||
}
|
||||
|
||||
/** Sets the narration of the not-yet-narrated journal entries of the savings transactions the query returns. */
|
||||
private void narrateJournalEntries(final String transactionIdsSql, final String narration, final Object... args) {
|
||||
final List<Long> transactionIds = this.jdbcTemplate.queryForList(transactionIdsSql, Long.class, args);
|
||||
for (final Long transactionId : transactionIds) {
|
||||
if (transactionId != null) {
|
||||
this.jdbcTemplate.update(
|
||||
"UPDATE acc_gl_journal_entry SET description = ? WHERE savings_transaction_id = ? AND (description IS NULL OR description = '')",
|
||||
narration, transactionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** How a closure that does not go to savings paid out, for the TDS tracking record. */
|
||||
private static String directPayoutKind(final DepositAccountOnClosureType closureType) {
|
||||
return closureType != null && closureType.isTransferToGl() ? "GL" : "CASH";
|
||||
|
||||
+1293
-1569
File diff suppressed because it is too large
Load Diff
-62
@@ -1,62 +0,0 @@
|
||||
/**
|
||||
* 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.portfolio.savings.jobs.applyrdmissedinstallmentpenalty;
|
||||
|
||||
import org.apache.fineract.infrastructure.jobs.service.JobName;
|
||||
import org.apache.fineract.portfolio.savings.service.RecurringDepositMissedInstallmentPenaltyService;
|
||||
import org.springframework.batch.core.Job;
|
||||
import org.springframework.batch.core.Step;
|
||||
import org.springframework.batch.core.job.builder.JobBuilder;
|
||||
import org.springframework.batch.core.launch.support.RunIdIncrementer;
|
||||
import org.springframework.batch.core.repository.JobRepository;
|
||||
import org.springframework.batch.core.step.builder.StepBuilder;
|
||||
import org.springframework.batch.repeat.RepeatStatus;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
|
||||
/**
|
||||
* Runs the RD missed-installment penalty. The service decides what is due and isolates each installment, so the step
|
||||
* is a thin trigger and the rules live in one place.
|
||||
*/
|
||||
@Configuration
|
||||
public class ApplyRdMissedInstallmentPenaltyJobConfig {
|
||||
|
||||
@Autowired
|
||||
private JobRepository jobRepository;
|
||||
@Autowired
|
||||
private PlatformTransactionManager transactionManager;
|
||||
@Autowired
|
||||
private RecurringDepositMissedInstallmentPenaltyService recurringDepositMissedInstallmentPenaltyService;
|
||||
|
||||
@Bean
|
||||
protected Step applyRdMissedInstallmentPenaltyStep() {
|
||||
return new StepBuilder(JobName.APPLY_RD_MISSED_INSTALLMENT_PENALTY.name(), jobRepository).tasklet((contribution, chunkContext) -> {
|
||||
recurringDepositMissedInstallmentPenaltyService.applyPenalties();
|
||||
return RepeatStatus.FINISHED;
|
||||
}, transactionManager).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Job applyRdMissedInstallmentPenaltyJob() {
|
||||
return new JobBuilder(JobName.APPLY_RD_MISSED_INSTALLMENT_PENALTY.name(), jobRepository).start(applyRdMissedInstallmentPenaltyStep())
|
||||
.incrementer(new RunIdIncrementer()).build();
|
||||
}
|
||||
}
|
||||
-60
@@ -1,60 +0,0 @@
|
||||
/**
|
||||
* 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.portfolio.savings.jobs.executerdautodebit;
|
||||
|
||||
import org.apache.fineract.infrastructure.jobs.service.JobName;
|
||||
import org.apache.fineract.portfolio.savings.service.RecurringDepositAutoDebitService;
|
||||
import org.springframework.batch.core.Job;
|
||||
import org.springframework.batch.core.Step;
|
||||
import org.springframework.batch.core.job.builder.JobBuilder;
|
||||
import org.springframework.batch.core.launch.support.RunIdIncrementer;
|
||||
import org.springframework.batch.core.repository.JobRepository;
|
||||
import org.springframework.batch.core.step.builder.StepBuilder;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
|
||||
@Configuration
|
||||
public class ExecuteRdAutoDebitJobConfig {
|
||||
|
||||
@Autowired
|
||||
private JobRepository jobRepository;
|
||||
@Autowired
|
||||
private PlatformTransactionManager transactionManager;
|
||||
@Autowired
|
||||
private RecurringDepositAutoDebitService recurringDepositAutoDebitService;
|
||||
|
||||
@Bean
|
||||
public ExecuteRdAutoDebitTasklet executeRdAutoDebitTasklet() {
|
||||
return new ExecuteRdAutoDebitTasklet(recurringDepositAutoDebitService);
|
||||
}
|
||||
|
||||
@Bean
|
||||
protected Step executeRdAutoDebitStep() {
|
||||
return new StepBuilder(JobName.EXECUTE_RD_AUTO_DEBIT.name(), jobRepository).tasklet(executeRdAutoDebitTasklet(), transactionManager)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Job executeRdAutoDebitJob() {
|
||||
return new JobBuilder(JobName.EXECUTE_RD_AUTO_DEBIT.name(), jobRepository).start(executeRdAutoDebitStep())
|
||||
.incrementer(new RunIdIncrementer()).build();
|
||||
}
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* 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.portfolio.savings.jobs.executerdautodebit;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.portfolio.savings.service.RecurringDepositAutoDebitService;
|
||||
import org.springframework.batch.core.StepContribution;
|
||||
import org.springframework.batch.core.scope.context.ChunkContext;
|
||||
import org.springframework.batch.core.step.tasklet.Tasklet;
|
||||
import org.springframework.batch.repeat.RepeatStatus;
|
||||
|
||||
/**
|
||||
* Runs RD Auto Debit. The service decides what is due and isolates each installment, so the tasklet stays a thin
|
||||
* trigger and the rules live in one place.
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class ExecuteRdAutoDebitTasklet implements Tasklet {
|
||||
|
||||
private final RecurringDepositAutoDebitService service;
|
||||
|
||||
@Override
|
||||
public RepeatStatus execute(final StepContribution contribution, final ChunkContext chunkContext) {
|
||||
service.collectDueInstallments();
|
||||
return RepeatStatus.FINISHED;
|
||||
}
|
||||
}
|
||||
-10
@@ -61,16 +61,6 @@ public class UpdateDepositsAccountMaturityDetailsTasklet implements Tasklet {
|
||||
}
|
||||
}
|
||||
|
||||
// Recurring deposits still running: credit any posting period (monthly, quarterly... per product) that ended
|
||||
// before today. The query above only returns deposits that have reached maturity.
|
||||
for (final Long rdId : depositAccountReadPlatformService.retrieveRunningRecurringDepositIds()) {
|
||||
try {
|
||||
depositAccountWritePlatformService.postRecurringDepositPeriodicInterest(rdId);
|
||||
} catch (final Exception ex) {
|
||||
log.error("Periodic interest posting failed for recurring deposit id {}", rdId, ex);
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("{}: Records affected by updateMaturityDetailsOfDepositAccounts: {}", ThreadLocalContextUtil.getTenant().getName(),
|
||||
depositAccounts.size());
|
||||
return RepeatStatus.FINISHED;
|
||||
|
||||
-8
@@ -46,7 +46,6 @@ import org.apache.fineract.portfolio.savings.domain.FixedDepositAccount;
|
||||
import org.apache.fineract.portfolio.savings.domain.RecurringDepositAccount;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.interceptor.TransactionAspectSupport;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class DepositAccountPreMatureCalculationPlatformServiceImpl implements DepositAccountPreMatureCalculationPlatformService {
|
||||
@@ -118,19 +117,12 @@ public class DepositAccountPreMatureCalculationPlatformServiceImpl implements De
|
||||
paymentTypeOptions, savingsAccountDatas, fd.getNominalAnnualInterestRate());
|
||||
} else if (depositAccountType.isRecurringDeposit()) {
|
||||
final RecurringDepositAccount rd = (RecurringDepositAccount) account;
|
||||
// Same reason rules the real closure applies, so the preview pays exactly what the closure will.
|
||||
rd.applyPrematureClosureReason(prematureClosureReason);
|
||||
accountData = RecurringDepositAccountData.preClosureDetails(
|
||||
account.getId(), rd.calculatePreMatureAmount(interestCalculatedToDate, isPreMatureClosure,
|
||||
isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth),
|
||||
onAccountClosureOptions, paymentTypeOptions, savingsAccountDatas);
|
||||
}
|
||||
|
||||
// A preview, not a closure: working the payout out resets the account's own interest rate to the premature
|
||||
// rate and refreshes its interest-earned summary on the managed entity, and committing that left an RD that
|
||||
// was only previewed showing the penalty rate (4.5% instead of 6.5%) as if it had been broken. Nothing the
|
||||
// calculation touched may be saved.
|
||||
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
|
||||
return accountData;
|
||||
}
|
||||
}
|
||||
|
||||
-3
@@ -61,9 +61,6 @@ public interface DepositAccountReadPlatformService {
|
||||
|
||||
Collection<DepositAccountData> retrieveForMaturityUpdate();
|
||||
|
||||
/** Active recurring deposits that have not reached maturity yet - their posting periods are credited nightly. */
|
||||
Collection<Long> retrieveRunningRecurringDepositIds();
|
||||
|
||||
SavingsAccountTransactionData retrieveRecurringAccountDepositTransactionTemplate(Long accountId);
|
||||
|
||||
Collection<AccountTransferDTO> retrieveDataForInterestTransfer();
|
||||
|
||||
-15
@@ -283,18 +283,6 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
|
||||
return this.jdbcTemplate.query(sqlBuilder.toString(), DEPOSIT_ACCOUNT_LOOKUP_MAPPER, depositAccountType.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Long> retrieveRunningRecurringDepositIds() {
|
||||
return this.jdbcTemplate.queryForList("""
|
||||
SELECT da.id FROM m_savings_account da
|
||||
JOIN m_deposit_account_term_and_preclosure dat ON dat.savings_account_id = da.id
|
||||
WHERE da.deposit_type_enum = ? AND da.status_enum = ?
|
||||
AND (dat.maturity_date IS NULL OR dat.maturity_date > ?)
|
||||
ORDER BY da.id
|
||||
""", Long.class, DepositAccountType.RECURRING_DEPOSIT.getValue(), SavingsAccountStatusType.ACTIVE.getValue(),
|
||||
DateUtils.getBusinessLocalDate());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<DepositAccountData> retrieveForMaturityUpdate() {
|
||||
|
||||
@@ -2069,8 +2057,6 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
|
||||
sqlBuilder.append("dptp.max_deposit_term_type_enum as maxDepositTermTypeId, ");
|
||||
sqlBuilder.append("dptp.in_multiples_of_deposit_term as inMultiplesOfDepositTerm, ");
|
||||
sqlBuilder.append("dptp.in_multiples_of_deposit_term_type_enum as inMultiplesOfDepositTermTypeId, ");
|
||||
// Lien Marked's loan limit, so the opening screen can default to it and refuse anything above it.
|
||||
sqlBuilder.append("sa.lien_percentage as lienPercentage, ");
|
||||
sqlBuilder.append("sa.account_type as accountType, sa.customer_segment as customerSegment, ");
|
||||
sqlBuilder.append("sa.product_code as productCode, ");
|
||||
sqlBuilder.append("sa.effective_date as effectiveDate, sa.effective_to as effectiveTo, sa.end_date as endDate, ");
|
||||
@@ -2197,7 +2183,6 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
|
||||
rs.getBoolean("deceasedSettlement"),
|
||||
rs.getBoolean("allowDebitTxnAfterDueDate")
|
||||
);
|
||||
result.setLienPercentage(rs.getBigDecimal("lienPercentage"));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
+3
-138
@@ -24,7 +24,6 @@ import static org.apache.fineract.portfolio.savings.SavingsApiConstants.amountPa
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.chargeIdParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.dueAsOfDateParamName;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.time.LocalDate;
|
||||
@@ -45,7 +44,6 @@ import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
|
||||
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||
@@ -149,8 +147,6 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
|
||||
private final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository;
|
||||
private final DenominationBreakdownWriter denominationBreakdownWriter;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final RecurringDepositLienService recurringDepositLienService;
|
||||
private final RecurringDepositProductRules recurringDepositProductRules;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
@@ -643,12 +639,7 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
|
||||
|
||||
final SavingsAccount account = this.depositAccountAssembler.assembleFrom(savingsId, depositAccountType);
|
||||
checkClientOrGroupActive(account);
|
||||
if (account instanceof RecurringDepositAccount rdAccount) {
|
||||
// A recurring deposit is credited by its posting periods only - the same credits the nightly job makes.
|
||||
postRecurringDepositPeriodicInterest(rdAccount);
|
||||
} else {
|
||||
postInterest(account);
|
||||
}
|
||||
postInterest(account);
|
||||
return new CommandProcessingResultBuilder() //
|
||||
.withEntityId(savingsId) //
|
||||
.withOfficeId(account.officeId()) //
|
||||
@@ -659,28 +650,6 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public void postRecurringDepositPeriodicInterest(final Long recurringDepositId) {
|
||||
final RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler.assembleFrom(recurringDepositId,
|
||||
DepositAccountType.RECURRING_DEPOSIT);
|
||||
postRecurringDepositPeriodicInterest(account);
|
||||
}
|
||||
|
||||
/**
|
||||
* Credits a recurring deposit's completed posting periods (monthly, quarterly... per its product) with their own
|
||||
* journal entries, named after the deposit.
|
||||
*/
|
||||
private void postRecurringDepositPeriodicInterest(final RecurringDepositAccount rdAccount) {
|
||||
final Set<Long> existingTransactionIds = new HashSet<>();
|
||||
final Set<Long> existingReversedTransactionIds = new HashSet<>();
|
||||
updateExistingTransactionsDetails(rdAccount, existingTransactionIds, existingReversedTransactionIds);
|
||||
rdAccount.postPeriodicInterest(this.configurationDomainService.isSavingsInterestPostingAtCurrentPeriodEnd(),
|
||||
this.configurationDomainService.retrieveFinancialYearBeginningMonth(), false);
|
||||
this.savingAccountRepositoryWrapper.saveAndFlush(rdAccount);
|
||||
postJournalEntries(rdAccount, existingTransactionIds, existingReversedTransactionIds);
|
||||
this.depositAccountDomainService.narrateRdInterestPostings(rdAccount);
|
||||
}
|
||||
|
||||
private void postInterest(final SavingsAccount account) {
|
||||
|
||||
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
|
||||
@@ -962,7 +931,6 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
|
||||
final RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId,
|
||||
DepositAccountType.RECURRING_DEPOSIT);
|
||||
checkClientOrGroupActive(account);
|
||||
this.recurringDepositLienService.assertNoActiveLien(savingsId, "closed");
|
||||
|
||||
this.depositAccountDomainService.handleRDAccountClosure(account, paymentDetail, user, command, changes);
|
||||
|
||||
@@ -1079,43 +1047,6 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
|
||||
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult extendRecurringDepositTenure(final Long savingsId, final JsonCommand command) {
|
||||
this.context.authenticatedUser();
|
||||
final Integer newDepositPeriod = command.integerValueOfParameterNamed(DepositsApiConstants.depositPeriodParamName);
|
||||
final RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId,
|
||||
DepositAccountType.RECURRING_DEPOSIT);
|
||||
checkClientOrGroupActive(account);
|
||||
|
||||
// The added installments follow the deposit's own collection calendar (monthly unless it was opened otherwise).
|
||||
PeriodFrequencyType frequencyType = PeriodFrequencyType.MONTHS;
|
||||
Integer frequency = 1;
|
||||
final CalendarInstance calendarInstance = this.calendarInstanceRepository.findByEntityIdAndEntityTypeIdAndCalendarTypeId(savingsId,
|
||||
CalendarEntityType.SAVINGS.getValue(), CalendarType.COLLECTION.getValue());
|
||||
if (calendarInstance != null && calendarInstance.getCalendar() != null) {
|
||||
final String recurrence = calendarInstance.getCalendar().getRecurrence();
|
||||
frequencyType = CalendarFrequencyType.from(CalendarUtils.getFrequency(recurrence));
|
||||
frequency = CalendarUtils.getInterval(recurrence);
|
||||
frequency = frequency == -1 ? 1 : frequency;
|
||||
}
|
||||
|
||||
final Map<String, Object> changes = account.extendTenure(newDepositPeriod, frequencyType, frequency,
|
||||
this.configurationDomainService.isSavingsInterestPostingAtCurrentPeriodEnd(),
|
||||
this.configurationDomainService.retrieveFinancialYearBeginningMonth());
|
||||
this.recurringDepositProductRules.validateMaturityWithinProductEndDate(account);
|
||||
this.savingAccountRepositoryWrapper.saveAndFlush(account);
|
||||
|
||||
return new CommandProcessingResultBuilder() //
|
||||
.withEntityId(savingsId) //
|
||||
.withOfficeId(account.officeId()) //
|
||||
.withClientId(account.clientId()) //
|
||||
.withGroupId(account.groupId()) //
|
||||
.withSavingsId(savingsId) //
|
||||
.with(changes) //
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandProcessingResult prematureCloseRDAccount(final Long savingsId, final JsonCommand command) {
|
||||
final AppUser user = this.context.authenticatedUser();
|
||||
@@ -1128,7 +1059,6 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
|
||||
final RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId,
|
||||
DepositAccountType.RECURRING_DEPOSIT);
|
||||
checkClientOrGroupActive(account);
|
||||
this.recurringDepositLienService.assertNoActiveLien(savingsId, "closed prematurely");
|
||||
if (account.maturityDate() == null) {
|
||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
|
||||
@@ -1659,76 +1589,11 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
|
||||
}
|
||||
}
|
||||
} else if (depositAccountType.isRecurringDeposit()) {
|
||||
final RecurringDepositAccount rdAccount = (RecurringDepositAccount) account;
|
||||
// Every night: credit any posting period (the product's Interest Posting Period) that ended before today.
|
||||
rdAccount.postPeriodicInterest(isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postReversals);
|
||||
rdAccount.updateMaturityStatus(isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postReversals);
|
||||
final JsonCommand maturityClosure = rdMaturityInstructionCommand(rdAccount);
|
||||
if (maturityClosure != null) {
|
||||
// Same order as the FD branch above: give the maturity interest its own journal entry first and
|
||||
// mark it posted, so neither the closure's withdrawal nor the final call below posts it again.
|
||||
this.savingAccountRepositoryWrapper.saveAndFlush(account);
|
||||
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds);
|
||||
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
|
||||
this.depositAccountDomainService.handleRDAccountClosure(rdAccount, null, this.context.authenticatedUser(),
|
||||
maturityClosure, new HashMap<>());
|
||||
// The closure posted its own entries for everything it created.
|
||||
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
|
||||
}
|
||||
((RecurringDepositAccount) account).updateMaturityStatus(isSavingsInterestPostingAtCurrentPeriodEnd,
|
||||
financialYearBeginningMonth, postReversals);
|
||||
}
|
||||
this.savingAccountRepositoryWrapper.saveAndFlush(account);
|
||||
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds);
|
||||
if (account instanceof RecurringDepositAccount rdAccount) {
|
||||
// The term's interest credited on the maturity date, named in the ledger as soon as it is posted.
|
||||
this.depositAccountDomainService.narrateRdInterestPostings(rdAccount);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The close command that carries out a recurring deposit's maturity instruction tonight, or null when it must
|
||||
* wait for staff instead. It runs only when the product has Maturity Instructions on, the deposit has just
|
||||
* matured, it was opened with Transfer to Savings or a Reinvest option, nothing holds a lien on it, and - for a
|
||||
* transfer - there is a savings account to pay into (the one chosen at opening, else the linked Savings account).
|
||||
* Anything else stays Matured, earning the post-maturity rate until someone closes it.
|
||||
*/
|
||||
private JsonCommand rdMaturityInstructionCommand(final RecurringDepositAccount rdAccount) {
|
||||
if (!rdAccount.isMatured() || rdAccount.savingsProduct() == null || !rdAccount.savingsProduct().isMaturityInstructions()) {
|
||||
return null;
|
||||
}
|
||||
final DepositAccountOnClosureType instruction = DepositAccountOnClosureType.fromInt(rdAccount.getOnAccountClosureId());
|
||||
if (!(instruction.isTransferToSavings() || instruction.isReinvest()) || instruction.isPartialReinvest()) {
|
||||
return null;
|
||||
}
|
||||
if (rdAccount.getAccountBalance() == null || rdAccount.getAccountBalance().signum() <= 0) {
|
||||
// Nothing was ever paid in: there is nothing to transfer or reinvest, so a person has to look at it.
|
||||
log.info("RD {} matured with no balance; maturity instruction left for staff", rdAccount.getAccountNumber());
|
||||
return null;
|
||||
}
|
||||
if (!this.recurringDepositLienService.activeLiens(rdAccount.getId()).isEmpty()) {
|
||||
log.info("RD {} matured with a lien on it; maturity instruction left for staff", rdAccount.getAccountNumber());
|
||||
return null;
|
||||
}
|
||||
Long payoutSavingsId = rdAccount.getTransferToSavingsAccountId();
|
||||
if (payoutSavingsId == null && rdAccount.getLinkedAccountId() != null
|
||||
&& "SAVINGS".equalsIgnoreCase(rdAccount.getLinkedAccountType())) {
|
||||
payoutSavingsId = rdAccount.getLinkedAccountId();
|
||||
}
|
||||
// Transfer always needs a destination; Reinvest Principal Only needs one for the interest part.
|
||||
if (payoutSavingsId == null && (instruction.isTransferToSavings() || instruction.isReinvestPrincipal())) {
|
||||
log.warn("RD {} matured with instruction {} but no savings account to pay into; left for staff", rdAccount.getAccountNumber(),
|
||||
instruction.getCode());
|
||||
return null;
|
||||
}
|
||||
final JsonObject closure = new JsonObject();
|
||||
closure.addProperty(SavingsApiConstants.closedOnDateParamName, rdAccount.maturityDate().toString());
|
||||
closure.addProperty(SavingsApiConstants.dateFormatParamName, "yyyy-MM-dd");
|
||||
closure.addProperty(SavingsApiConstants.localeParamName, "en");
|
||||
closure.addProperty(DepositsApiConstants.onAccountClosureIdParamName, instruction.getValue());
|
||||
if (payoutSavingsId != null) {
|
||||
closure.addProperty(DepositsApiConstants.toSavingsAccountIdParamName, payoutSavingsId);
|
||||
}
|
||||
closure.addProperty(DepositsApiConstants.transferDescriptionParamName, "Apply maturity instructions");
|
||||
return JsonCommand.fromJsonElement(rdAccount.getId(), closure, new FromJsonHelper());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
|
||||
-7
@@ -127,7 +127,6 @@ public class DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
|
||||
private final BusinessEventNotifierService businessEventNotifierService;
|
||||
private final SavingsAccountAssembler savingsAccountAssembler;
|
||||
private final ClientNomineeWritePlatformService clientNomineeWritePlatformService;
|
||||
private final RecurringDepositProductRules recurringDepositProductRules;
|
||||
|
||||
/*
|
||||
* Guaranteed to throw an exception no matter what the data integrity issue is.
|
||||
@@ -303,12 +302,9 @@ public class DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
|
||||
frequency = frequency == -1 ? 1 : frequency;
|
||||
account.generateSchedule(frequencyType, frequency, calendar);
|
||||
final boolean isPreMatureClosure = false;
|
||||
// The simple/compounding flag must be on the account before its maturity is projected.
|
||||
this.recurringDepositProductRules.applyInterestCalculationType(account);
|
||||
account.updateMaturityDateAndAmount(mc, isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd,
|
||||
financialYearBeginningMonth);
|
||||
account.validateApplicableInterestRate();
|
||||
this.recurringDepositProductRules.validateOnSubmit(account, command);
|
||||
savingAccountRepository.save(account);
|
||||
|
||||
// Save nominees if provided in payload (same transaction)
|
||||
@@ -555,15 +551,12 @@ public class DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
|
||||
frequency = frequency == -1 ? 1 : frequency;
|
||||
account.generateSchedule(frequencyType, frequency, calendar);
|
||||
final boolean isPreMatureClosure = false;
|
||||
// An application is not yet a contract, so an edit (possibly to a different product) re-reads it.
|
||||
this.recurringDepositProductRules.applyInterestCalculationType(account);
|
||||
account.updateMaturityDateAndAmount(mc, isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd,
|
||||
financialYearBeginningMonth);
|
||||
account.validateApplicableInterestRate();
|
||||
this.savingAccountRepository.save(account);
|
||||
|
||||
}
|
||||
this.recurringDepositProductRules.validateOnModify(account, command);
|
||||
|
||||
// update calendar details
|
||||
if (!account.isCalendarInherited()) {
|
||||
|
||||
-8
@@ -499,10 +499,6 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead
|
||||
sqlBuilder.append("sp.balance_frequency as balanceFrequency, ");
|
||||
sqlBuilder.append("sp.account_closed_zero_balance_in_day_closing as accountClosedZeroBalanceInDayClosing, ");
|
||||
sqlBuilder.append("sp.balance_swap_option as balanceSwapOption, sp.msme as msme, ");
|
||||
sqlBuilder.append("sp.lien_percentage as lienPercentage, ");
|
||||
sqlBuilder.append("sp.missed_installment_grace_days as missedInstallmentGraceDays, ");
|
||||
sqlBuilder.append("sp.missed_installment_penalty_type as missedInstallmentPenaltyType, ");
|
||||
sqlBuilder.append("sp.missed_installment_penalty_amount as missedInstallmentPenaltyAmount, ");
|
||||
sqlBuilder.append("sp.auto_debit_facility as autoDebitFacility, sp.loan_facility as loanFacility, ");
|
||||
sqlBuilder.append("sp.premature_withdrawal_option as prematureWithdrawalOption, sp.tenure_flexibility as tenureFlexibility, ");
|
||||
sqlBuilder.append("sp.lien_marked as lienMarked, sp.maturity_instructions as maturityInstructions, ");
|
||||
@@ -627,10 +623,6 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead
|
||||
result.setPrematureWithdrawalOption(prematureWithdrawalOption);
|
||||
result.setTenureFlexibility(tenureFlexibility);
|
||||
result.setLienMarked(lienMarked);
|
||||
result.setLienPercentage(rs.getBigDecimal("lienPercentage"));
|
||||
result.setMissedInstallmentGraceDays(JdbcSupport.getInteger(rs, "missedInstallmentGraceDays"));
|
||||
result.setMissedInstallmentPenaltyType(JdbcSupport.getInteger(rs, "missedInstallmentPenaltyType"));
|
||||
result.setMissedInstallmentPenaltyAmount(rs.getBigDecimal("missedInstallmentPenaltyAmount"));
|
||||
result.setMaturityInstructions(maturityInstructions);
|
||||
result.setCreditMaturityProceeds(creditMaturityProceeds);
|
||||
result.setPenaltyGracePeriodMissedInstallments(penaltyGracePeriodMissedInstallments);
|
||||
|
||||
+27
-140
@@ -29,47 +29,19 @@ import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonQuery;
|
||||
import org.apache.fineract.infrastructure.core.domain.LocalDateInterval;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
|
||||
import org.apache.fineract.portfolio.savings.SavingsCompoundingInterestPeriodType;
|
||||
import org.apache.fineract.portfolio.savings.SavingsInterestCalculationDaysInYearType;
|
||||
import org.apache.fineract.portfolio.savings.SavingsInterestCalculationType;
|
||||
import org.apache.fineract.portfolio.savings.SavingsPostingInterestPeriodType;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsHelper;
|
||||
import org.apache.fineract.portfolio.savings.domain.interest.PostingPeriod;
|
||||
import org.apache.fineract.portfolio.savings.domain.interest.SavingsAccountTransactionDetailsForPostingPeriod;
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class RecurringDepositAccountInterestCalculationServiceImpl implements RecurringDepositAccountInterestCalculationService {
|
||||
|
||||
private static final String PRODUCT_ID_PARAM = "productId";
|
||||
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final SavingsHelper savingsHelper;
|
||||
private final ConfigurationDomainService configurationDomainService;
|
||||
private final RecurringDepositProductRules recurringDepositProductRules;
|
||||
|
||||
/** The interest settings of an RD product, as an account opened under it will be calculated with. */
|
||||
private record ProductInterestTerms(SavingsCompoundingInterestPeriodType compounding, SavingsPostingInterestPeriodType posting,
|
||||
SavingsInterestCalculationType calculationType, SavingsInterestCalculationDaysInYearType daysInYear, boolean simpleInterest,
|
||||
BigDecimal minBalanceForInterest, MonetaryCurrency currency) {}
|
||||
|
||||
@Override
|
||||
public HashMap<String, Object> calculateInterest(JsonQuery query) {
|
||||
@@ -81,120 +53,27 @@ public class RecurringDepositAccountInterestCalculationServiceImpl implements Re
|
||||
element);
|
||||
LocalDate submittedOnDate = this.fromApiJsonHelper.extractLocalDateNamed("submittedOnDate", element);
|
||||
if (submittedOnDate == null) {
|
||||
submittedOnDate = DateUtils.getBusinessLocalDate();
|
||||
submittedOnDate = LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
}
|
||||
final Long productId = this.fromApiJsonHelper.extractLongNamed(PRODUCT_ID_PARAM, element);
|
||||
final ProductInterestTerms terms = productId == null ? null : productInterestTerms(productId);
|
||||
|
||||
final HashMap<String, Object> result = new HashMap<>();
|
||||
BigDecimal maturityAmount = this.calculateInterestInternal(depositAmount, annualInterestRate, tenureInMonths,
|
||||
interestCompoundingPeriodInMonths, submittedOnDate);
|
||||
|
||||
BigDecimal totalPrincipal = depositAmount.multiply(BigDecimal.valueOf(tenureInMonths));
|
||||
BigDecimal maturityAmount;
|
||||
if (terms != null) {
|
||||
// The product's own settings, through the same engine an opened account is calculated with - so the figure
|
||||
// shown before opening is the maturity amount the account will carry.
|
||||
final List<Map<String, Object>> postings = new ArrayList<>();
|
||||
final BigDecimal totalInterest = projectInterest(terms, depositAmount, annualInterestRate, tenureInMonths.intValue(),
|
||||
submittedOnDate, postings);
|
||||
maturityAmount = totalPrincipal.add(totalInterest);
|
||||
result.put("interestCalculation", terms.simpleInterest() ? "Simple" : "Compounding");
|
||||
result.put("compoundingPeriod", terms.compounding().name());
|
||||
result.put("postingPeriod", terms.posting().name());
|
||||
result.put("postingSchedule", postings);
|
||||
result.put("daysInYear", terms.daysInYear().getValue());
|
||||
} else {
|
||||
maturityAmount = this.calculateInterestInternal(depositAmount, annualInterestRate, tenureInMonths,
|
||||
interestCompoundingPeriodInMonths, submittedOnDate);
|
||||
result.put("daysInYear", 365);
|
||||
}
|
||||
BigDecimal totalInterest = maturityAmount.subtract(totalPrincipal);
|
||||
|
||||
String warning = terms != null ? "Calculated with the product's interest settings, assuming every installment is paid on its due date"
|
||||
: "This is an approximate calculated amount - it may vary slightly when the account is created";
|
||||
String warning = "This is an approximate calculated amount - it may vary slightly when the account is created";
|
||||
|
||||
HashMap<String, Object> result = new HashMap<>();
|
||||
result.put("maturityAmount", maturityAmount);
|
||||
result.put("totalPrincipal", totalPrincipal);
|
||||
result.put("totalInterest", totalInterest);
|
||||
result.put("daysInYear", 365); // Standard for this product as per image
|
||||
result.put("warning", warning);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private ProductInterestTerms productInterestTerms(final Long productId) {
|
||||
try {
|
||||
return this.jdbcTemplate.queryForObject("""
|
||||
SELECT interest_compounding_period_enum, interest_posting_period_enum, interest_calculation_type_enum,
|
||||
interest_calculation_days_in_year_type_enum, interest_calculate_selector,
|
||||
min_balance_for_interest_calculation, currency_code, currency_digits, currency_multiplesof
|
||||
FROM m_savings_product WHERE id = ? AND deposit_type_enum = 300
|
||||
""", (rs, rowNum) -> {
|
||||
final Integer selector = (Integer) rs.getObject("interest_calculate_selector");
|
||||
final Integer multiplesOf = (Integer) rs.getObject("currency_multiplesof");
|
||||
return new ProductInterestTerms(SavingsCompoundingInterestPeriodType.fromInt(rs.getInt("interest_compounding_period_enum")),
|
||||
SavingsPostingInterestPeriodType.fromInt(rs.getInt("interest_posting_period_enum")),
|
||||
SavingsInterestCalculationType.fromInt(rs.getInt("interest_calculation_type_enum")),
|
||||
SavingsInterestCalculationDaysInYearType.fromInt(rs.getInt("interest_calculation_days_in_year_type_enum")),
|
||||
this.recurringDepositProductRules.isSimpleInterestOption(selector), rs.getBigDecimal("min_balance_for_interest_calculation"),
|
||||
new MonetaryCurrency(rs.getString("currency_code"), rs.getInt("currency_digits"), multiplesOf));
|
||||
}, productId);
|
||||
} catch (final EmptyResultDataAccessException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interest on a deposit of {@code installment} every month from {@code startDate} for {@code months} months, worked
|
||||
* out by the account engine itself (posting periods, compounding periods, daily balances, days in year, simple or
|
||||
* compounding) up to the day before maturity - the same calculation that gives an opened account its maturity
|
||||
* amount. Each posting period's credit is added to {@code postings}.
|
||||
*/
|
||||
private BigDecimal projectInterest(final ProductInterestTerms terms, final BigDecimal installment, final BigDecimal annualRate,
|
||||
final int months, final LocalDate startDate, final List<Map<String, Object>> postings) {
|
||||
final LocalDate termEnd = startDate.plusMonths(months).minusDays(1);
|
||||
final MonetaryCurrency currency = terms.currency();
|
||||
|
||||
final List<SavingsAccountTransactionDetailsForPostingPeriod> deposits = new ArrayList<>();
|
||||
for (int k = 0; k < months; k++) {
|
||||
final LocalDate dueDate = startDate.plusMonths(k);
|
||||
final LocalDate balanceEnd = k == months - 1 ? termEnd : startDate.plusMonths(k + 1L).minusDays(1);
|
||||
final BigDecimal runningBalance = installment.multiply(BigDecimal.valueOf(k + 1L));
|
||||
final int days = LocalDateInterval.create(dueDate, balanceEnd).daysInPeriodInclusiveOfEndDate();
|
||||
deposits.add(new SavingsAccountTransactionDetailsForPostingPeriod(null, dueDate, balanceEnd, runningBalance, installment, currency,
|
||||
days, true, false, false, false, false));
|
||||
}
|
||||
|
||||
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
|
||||
final boolean postingAtPeriodEnd = this.configurationDomainService.isSavingsInterestPostingAtCurrentPeriodEnd();
|
||||
final List<LocalDateInterval> intervals = this.savingsHelper.determineInterestPostingPeriods(startDate, termEnd, terms.posting(),
|
||||
financialYearBeginningMonth, new ArrayList<>());
|
||||
final BigDecimal rateAsFraction = annualRate.divide(BigDecimal.valueOf(100L), MathContext.DECIMAL64);
|
||||
final Money minBalance = Money.of(currency, terms.minBalanceForInterest() == null ? BigDecimal.ZERO : terms.minBalanceForInterest());
|
||||
|
||||
final List<PostingPeriod> periods = new ArrayList<>();
|
||||
Money periodStartingBalance = Money.zero(currency);
|
||||
for (final LocalDateInterval interval : intervals) {
|
||||
final PostingPeriod period = PostingPeriod.createFrom(interval, periodStartingBalance, deposits, currency, terms.compounding(),
|
||||
terms.calculationType(), rateAsFraction, terms.daysInYear().getValue(), termEnd, new ArrayList<>(), false, minBalance,
|
||||
postingAtPeriodEnd, false, financialYearBeginningMonth);
|
||||
periodStartingBalance = period.closingBalance();
|
||||
periods.add(period);
|
||||
}
|
||||
this.savingsHelper.calculateInterestForAllPostingPeriods(currency, periods, null, false, terms.simpleInterest());
|
||||
|
||||
BigDecimal total = BigDecimal.ZERO;
|
||||
for (final PostingPeriod period : periods) {
|
||||
final BigDecimal interest = period.getInterestEarned().getAmount();
|
||||
total = total.add(interest);
|
||||
final Map<String, Object> posting = new LinkedHashMap<>();
|
||||
// The last period is credited on the maturity date itself.
|
||||
final LocalDate periodEnd = period.getPeriodInterval().endDate();
|
||||
final LocalDate postedOn = periodEnd.isBefore(termEnd) ? periodEnd : termEnd.plusDays(1);
|
||||
posting.put("date", postedOn.toString());
|
||||
posting.put("interest", interest);
|
||||
postings.add(posting);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
public BigDecimal calculateInterestInternal(BigDecimal installmentAmount, BigDecimal annualInterestRate, Long tenureInMonths,
|
||||
Long interestCompoundingPeriodInMonths, LocalDate submittedOnDate) {
|
||||
MathContext mc = MoneyHelper.getMathContext();
|
||||
@@ -210,31 +89,39 @@ public class RecurringDepositAccountInterestCalculationServiceImpl implements Re
|
||||
for (int m = 1; m <= totalMonths; m++) {
|
||||
// Add monthly installment at the start of the month
|
||||
currentBalance = currentBalance.add(installmentAmount);
|
||||
|
||||
|
||||
// Get actual days in the current month
|
||||
int daysInMonth = date.lengthOfMonth();
|
||||
|
||||
|
||||
// Calculate interest for the month using daily compounding
|
||||
double multiplier = Math.pow(1.0 + dailyRate, daysInMonth);
|
||||
currentBalance = currentBalance.multiply(BigDecimal.valueOf(multiplier), mc);
|
||||
|
||||
|
||||
// Move to next month
|
||||
date = date.plusMonths(1);
|
||||
}
|
||||
maturityAmount = currentBalance;
|
||||
} else {
|
||||
// Periodic Compounding (Monthly, Quarterly, etc.)
|
||||
BigDecimal monthlyRate = annualInterestRate.divide(BigDecimal.valueOf(1200), mc);
|
||||
BigDecimal i = monthlyRate;
|
||||
int n = totalMonths;
|
||||
|
||||
// Standard RD formula for monthly compounding:
|
||||
// M = P * ((1+i)^n - 1) / i * (1+i)
|
||||
// For other periods, we can approximate or use a loop.
|
||||
// Using loop for consistency with various compounding periods.
|
||||
BigDecimal compoundingFrequencyMonths = BigDecimal.valueOf(interestCompoundingPeriodInMonths);
|
||||
BigDecimal ratePerCompounding = annualInterestRate.divide(BigDecimal.valueOf(100), mc).multiply(compoundingFrequencyMonths)
|
||||
.divide(BigDecimal.valueOf(12), mc);
|
||||
BigDecimal ratePerCompounding = annualInterestRate.divide(BigDecimal.valueOf(100), mc)
|
||||
.multiply(compoundingFrequencyMonths).divide(BigDecimal.valueOf(12), mc);
|
||||
|
||||
for (int m = 1; m <= totalMonths; m++) {
|
||||
BigDecimal monthsRemaining = BigDecimal.valueOf(totalMonths - m + 1);
|
||||
BigDecimal numberOfCompoundings = monthsRemaining.divide(compoundingFrequencyMonths, mc);
|
||||
|
||||
BigDecimal installmentMaturity = installmentAmount
|
||||
.multiply(BigDecimal.ONE.add(ratePerCompounding).pow(numberOfCompoundings.intValue(), mc), mc);
|
||||
|
||||
|
||||
BigDecimal installmentMaturity = installmentAmount.multiply(
|
||||
BigDecimal.ONE.add(ratePerCompounding).pow(numberOfCompoundings.intValue(), mc), mc);
|
||||
|
||||
// Add remaining partial compounding period interest (simple interest for the rest)
|
||||
BigDecimal remainingMonths = monthsRemaining.remainder(compoundingFrequencyMonths);
|
||||
if (remainingMonths.compareTo(BigDecimal.ZERO) > 0) {
|
||||
@@ -242,11 +129,11 @@ public class RecurringDepositAccountInterestCalculationServiceImpl implements Re
|
||||
.divide(BigDecimal.valueOf(1200), mc);
|
||||
installmentMaturity = installmentMaturity.add(partialInterest);
|
||||
}
|
||||
|
||||
|
||||
maturityAmount = maturityAmount.add(installmentMaturity);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return maturityAmount.setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
}
|
||||
|
||||
-157
@@ -1,157 +0,0 @@
|
||||
/**
|
||||
* 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.portfolio.savings.service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.portfolio.account.PortfolioAccountType;
|
||||
import org.apache.fineract.portfolio.account.data.AccountTransferDTO;
|
||||
import org.apache.fineract.portfolio.account.domain.AccountTransferType;
|
||||
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.TransactionDefinition;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
/**
|
||||
* RD Auto Debit: on each due date, collects the installment of every recurring deposit opened with Auto Debit from
|
||||
* the Savings account linked to it, as an ordinary account transfer (debit the savings account, credit the deposit),
|
||||
* so it is booked, journalled and shown on both statements exactly like a transfer the customer made themselves.
|
||||
*
|
||||
* <ul>
|
||||
* <li>Only installments due on or before the business date and not yet fully paid are collected - oldest first, one
|
||||
* transfer each, each in its own transaction.</li>
|
||||
* <li>An installment the savings account cannot cover is skipped and stays overdue; nothing is partly collected, and
|
||||
* the rest of that deposit's installments wait for the next run so they are never paid out of order. Every other
|
||||
* deposit is unaffected.</li>
|
||||
* <li>Retrying is the product's choice ("Allow Debit Txn after due date"): with it on, anything still unpaid is
|
||||
* collected on a later run; with it off, an installment is debited only on its due date and a missed one stays
|
||||
* overdue for the customer to pay (and for the missed-installment penalty, if the product has one).</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class RecurringDepositAutoDebitService {
|
||||
|
||||
/**
|
||||
* Unpaid installments, plus per deposit what has been due up to today and what has actually been paid in. The two
|
||||
* totals matter because a deposit opened without "adjust advance payments" never allocates money paid before a
|
||||
* due date to that installment - the schedule shows it unpaid though the customer already paid at the counter.
|
||||
*/
|
||||
private static final String DUE_INSTALLMENTS_SQL = """
|
||||
SELECT rd.id AS rd_id, rd.linked_account_id AS savings_id, s.duedate AS due_date,
|
||||
s.deposit_amount - COALESCE(s.deposit_amount_completed_derived, 0) AS outstanding,
|
||||
(SELECT COALESCE(SUM(s2.deposit_amount), 0) FROM m_mandatory_savings_schedule s2
|
||||
WHERE s2.savings_account_id = rd.id AND s2.duedate <= ?) AS total_due,
|
||||
(SELECT COALESCE(SUM(t.amount), 0) FROM m_savings_account_transaction t
|
||||
WHERE t.savings_account_id = rd.id AND t.transaction_type_enum = 1 AND t.is_reversed = false) AS total_paid
|
||||
FROM m_savings_account rd
|
||||
JOIN m_savings_product sp ON sp.id = rd.product_id
|
||||
JOIN m_mandatory_savings_schedule s ON s.savings_account_id = rd.id
|
||||
JOIN m_savings_account sav ON sav.id = rd.linked_account_id
|
||||
WHERE rd.deposit_type_enum = 300
|
||||
AND rd.status_enum = 300
|
||||
AND rd.auto_debit_facility = true
|
||||
AND UPPER(rd.linked_account_type) = 'SAVINGS'
|
||||
AND sav.deposit_type_enum = 100
|
||||
AND sav.status_enum = 300
|
||||
AND COALESCE(s.completed_derived, false) = false
|
||||
AND s.duedate <= ?
|
||||
AND (COALESCE(sp.allow_debit_txn_after_due_date, false) = true OR s.duedate = ?)
|
||||
AND s.deposit_amount > COALESCE(s.deposit_amount_completed_derived, 0)
|
||||
ORDER BY rd.id, s.duedate
|
||||
""";
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final AccountTransfersWritePlatformService accountTransfersWritePlatformService;
|
||||
private final TransactionTemplate perInstallmentTransaction;
|
||||
|
||||
public RecurringDepositAutoDebitService(final JdbcTemplate jdbcTemplate,
|
||||
final AccountTransfersWritePlatformService accountTransfersWritePlatformService,
|
||||
final PlatformTransactionManager transactionManager) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
this.accountTransfersWritePlatformService = accountTransfersWritePlatformService;
|
||||
this.perInstallmentTransaction = new TransactionTemplate(transactionManager);
|
||||
this.perInstallmentTransaction.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
|
||||
}
|
||||
|
||||
/** @return how many installments were collected */
|
||||
public int collectDueInstallments() {
|
||||
final LocalDate businessDate = DateUtils.getBusinessLocalDate();
|
||||
final List<DueInstallment> dueInstallments = this.jdbcTemplate.query(DUE_INSTALLMENTS_SQL,
|
||||
(rs, rowNum) -> new DueInstallment(rs.getLong("rd_id"), rs.getLong("savings_id"), rs.getObject("due_date", LocalDate.class),
|
||||
rs.getBigDecimal("outstanding"), rs.getBigDecimal("total_due").subtract(rs.getBigDecimal("total_paid"))),
|
||||
businessDate, businessDate, businessDate);
|
||||
|
||||
int collected = 0;
|
||||
Long blockedDeposit = null;
|
||||
Long currentDeposit = null;
|
||||
BigDecimal stillShort = BigDecimal.ZERO;
|
||||
for (final DueInstallment installment : dueInstallments) {
|
||||
if (installment.recurringDepositId().equals(blockedDeposit)) {
|
||||
continue;
|
||||
}
|
||||
if (!installment.recurringDepositId().equals(currentDeposit)) {
|
||||
currentDeposit = installment.recurringDepositId();
|
||||
// What the customer still owes up to today - zero when they already paid it at the counter.
|
||||
stillShort = installment.shortfall().max(BigDecimal.ZERO);
|
||||
}
|
||||
final BigDecimal amount = installment.outstanding().min(stillShort);
|
||||
if (amount.signum() <= 0) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
this.perInstallmentTransaction.executeWithoutResult(status -> collect(installment, amount, businessDate));
|
||||
stillShort = stillShort.subtract(amount);
|
||||
collected++;
|
||||
} catch (final RuntimeException e) {
|
||||
// Most often the linked savings account is short of funds. Leave this deposit's remaining
|
||||
// installments for the next run rather than paying a later one ahead of the one that failed.
|
||||
blockedDeposit = installment.recurringDepositId();
|
||||
log.warn("RD Auto Debit could not collect the installment due {} on recurring deposit {} from savings account {}: {}",
|
||||
installment.dueDate(), installment.recurringDepositId(), installment.savingsAccountId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
log.info("RD Auto Debit collected {} of {} due installment(s) on {}", collected, dueInstallments.size(), businessDate);
|
||||
return collected;
|
||||
}
|
||||
|
||||
private void collect(final DueInstallment installment, final BigDecimal amount, final LocalDate businessDate) {
|
||||
final SavingsAccount fromSavingsAccount = null;
|
||||
final SavingsAccount toSavingsAccount = null;
|
||||
final boolean isRegularTransaction = true;
|
||||
final boolean isExceptionForBalanceCheck = false;
|
||||
final AccountTransferDTO transfer = new AccountTransferDTO(businessDate, amount, PortfolioAccountType.SAVINGS,
|
||||
PortfolioAccountType.SAVINGS, installment.savingsAccountId(), installment.recurringDepositId(),
|
||||
"RD Auto Debit - installment due " + installment.dueDate(), null, null, null, null, null, null, null,
|
||||
AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, ExternalId.empty(), null, toSavingsAccount, fromSavingsAccount,
|
||||
isRegularTransaction, isExceptionForBalanceCheck);
|
||||
this.accountTransfersWritePlatformService.transferFunds(transfer);
|
||||
}
|
||||
|
||||
/** shortfall = everything due on the deposit up to today, less everything actually paid into it. */
|
||||
private record DueInstallment(Long recurringDepositId, Long savingsAccountId, LocalDate dueDate, BigDecimal outstanding,
|
||||
BigDecimal shortfall) {}
|
||||
}
|
||||
-176
@@ -1,176 +0,0 @@
|
||||
/**
|
||||
* 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.portfolio.savings.service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Lien on a recurring deposit pledged as security for a loan.
|
||||
*
|
||||
* <p>
|
||||
* The lien itself is the existing guarantor mechanism: adding the deposit as a guarantor account on a loan records a
|
||||
* funding line, and the guarantor module places the amount on hold when the loan is approved/disbursed, releases it as
|
||||
* the loan is repaid, and releases the rest when the loan closes. What this class adds is what that mechanism never
|
||||
* did for a recurring deposit:
|
||||
* <ul>
|
||||
* <li>only an RD whose product has <b>Lien Marked</b> on may be pledged, and never for more than the product's lien
|
||||
* percentage of its balance (the whole balance when none is set), less what it already secures;</li>
|
||||
* <li>a pledged RD cannot be closed - premature or on maturity - while any loan it secures is still pending or
|
||||
* running. Before this, the hold only surfaced as an "insufficient balance" error deep inside the closure, and a pledge
|
||||
* on a loan not yet approved (no hold placed yet) did not stop the deposit being paid out at all;</li>
|
||||
* <li>which loans hold how much, so the RD screens can show it.</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class RecurringDepositLienService {
|
||||
|
||||
/**
|
||||
* Pledges still in force: the funding line is active and the loan is submitted (100), approved (200) or active
|
||||
* (300). A loan that is closed, rejected or withdrawn no longer binds the deposit.
|
||||
*/
|
||||
private static final String ACTIVE_LIENS_SQL = """
|
||||
SELECT l.id AS loan_id, l.account_no AS loan_account_no, l.loan_status_id AS loan_status,
|
||||
COALESCE(gfd.amount_remaining_derived, gfd.amount) AS lien_amount
|
||||
FROM m_guarantor_funding_details gfd
|
||||
JOIN m_portfolio_account_associations paa ON paa.id = gfd.account_associations_id
|
||||
JOIN m_loan l ON l.id = paa.loan_account_id
|
||||
WHERE paa.linked_savings_account_id = ?
|
||||
AND gfd.status_enum = 100
|
||||
AND l.loan_status_id IN (100, 200, 300)
|
||||
AND COALESCE(gfd.amount_remaining_derived, gfd.amount) > 0
|
||||
ORDER BY l.id
|
||||
""";
|
||||
|
||||
private static final BigDecimal ONE_HUNDRED = BigDecimal.valueOf(100);
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
public record Lien(Long loanId, String loanAccountNo, Integer loanStatus, BigDecimal amount) {}
|
||||
|
||||
/**
|
||||
* The loan limit a deposit carries today: whether its product allows a lien at all, the percentage that applies
|
||||
* (the deposit's own, else its product's, else 100), and what that comes to on the current balance.
|
||||
*/
|
||||
public record LienLimit(boolean lienAllowed, BigDecimal accountPercentage, BigDecimal productPercentage,
|
||||
BigDecimal effectivePercentage, BigDecimal balance, BigDecimal maxLoanAmount) {}
|
||||
|
||||
private static final String LIEN_LIMIT_SQL = """
|
||||
SELECT sp.lien_marked AS lien_marked, sa.lien_percentage AS account_pct, sp.lien_percentage AS product_pct,
|
||||
COALESCE(sa.account_balance_derived, 0) AS balance
|
||||
FROM m_savings_account sa
|
||||
JOIN m_savings_product sp ON sp.id = sa.product_id
|
||||
WHERE sa.id = ?
|
||||
""";
|
||||
|
||||
public LienLimit lienLimit(final Long depositAccountId) {
|
||||
final List<LienLimit> rows = this.jdbcTemplate.query(LIEN_LIMIT_SQL, (rs, rowNum) -> {
|
||||
final boolean lienAllowed = rs.getBoolean("lien_marked");
|
||||
final BigDecimal accountPct = rs.getBigDecimal("account_pct");
|
||||
final BigDecimal productPct = rs.getBigDecimal("product_pct");
|
||||
final BigDecimal balance = rs.getBigDecimal("balance");
|
||||
final BigDecimal effective = accountPct != null ? accountPct : productPct != null ? productPct : ONE_HUNDRED;
|
||||
final BigDecimal maxLoan = lienAllowed ? balance.multiply(effective).divide(ONE_HUNDRED, 2, RoundingMode.DOWN)
|
||||
: BigDecimal.ZERO;
|
||||
return new LienLimit(lienAllowed, accountPct, productPct, effective, balance, maxLoan);
|
||||
}, depositAccountId);
|
||||
return rows.isEmpty() ? null : rows.get(0);
|
||||
}
|
||||
|
||||
public List<Lien> activeLiens(final Long depositAccountId) {
|
||||
return this.jdbcTemplate.query(ACTIVE_LIENS_SQL, (rs, rowNum) -> new Lien(rs.getLong("loan_id"), rs.getString("loan_account_no"),
|
||||
rs.getInt("loan_status"), rs.getBigDecimal("lien_amount")), depositAccountId);
|
||||
}
|
||||
|
||||
public BigDecimal totalLien(final Long depositAccountId) {
|
||||
return activeLiens(depositAccountId).stream().map(Lien::amount).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
}
|
||||
|
||||
/** Refuses to pay out a recurring deposit that still secures a loan. */
|
||||
public void assertNoActiveLien(final Long depositAccountId, final String action) {
|
||||
final List<Lien> liens = activeLiens(depositAccountId);
|
||||
if (liens.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
final BigDecimal total = liens.stream().map(Lien::amount).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
final String loans = liens.stream().map(lien -> lien.loanAccountNo() + " (" + money(lien.amount()) + ")")
|
||||
.collect(Collectors.joining(", "));
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.recurringdeposit.lien.marked",
|
||||
"This recurring deposit has a lien of " + money(total) + " for loan(s) " + loans + ". It cannot be " + action
|
||||
+ " until the loan is repaid or the guarantee is released.",
|
||||
depositAccountId, total);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rules for pledging an account as loan security. Only recurring deposits are checked here; other account types
|
||||
* keep the guarantor module's existing behaviour.
|
||||
*
|
||||
* @param excludingLoanId
|
||||
* the loan being edited, whose own earlier pledge must not count against the new amount
|
||||
*/
|
||||
public void validatePledge(final SavingsAccount account, final BigDecimal amount, final Long excludingLoanId) {
|
||||
if (account == null || !account.depositAccountType().isRecurringDeposit()) {
|
||||
return;
|
||||
}
|
||||
if (account.savingsProduct() == null || !account.savingsProduct().isLienMarked()) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.recurringdeposit.lien.not.allowed",
|
||||
"Recurring deposit " + account.getAccountNumber() + " cannot be pledged: its product does not have Lien Marked enabled.",
|
||||
account.getId());
|
||||
}
|
||||
if (!account.isActive()) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.recurringdeposit.lien.account.not.active",
|
||||
"Recurring deposit " + account.getAccountNumber() + " is not active, so it cannot be pledged.", account.getId());
|
||||
}
|
||||
final BigDecimal alreadyPledged = activeLiens(account.getId()).stream()
|
||||
.filter(lien -> excludingLoanId == null || !excludingLoanId.equals(lien.loanId())).map(Lien::amount)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
// The lien percentage caps the total lien at that share of the balance as it stands now, when the loan is
|
||||
// taken (90% of 1,00,000 = 90,000 across every loan the deposit secures). The deposit's own percentage, set
|
||||
// when it was opened, wins; else the product's; neither means the whole balance.
|
||||
final BigDecimal lienPercentage = account.getLienPercentage() != null ? account.getLienPercentage()
|
||||
: account.savingsProduct().getLienPercentage();
|
||||
final BigDecimal balance = account.getAccountBalance();
|
||||
final BigDecimal pledgeable = lienPercentage == null ? balance
|
||||
: balance.multiply(lienPercentage).divide(ONE_HUNDRED, 2, RoundingMode.DOWN);
|
||||
final BigDecimal available = pledgeable.subtract(alreadyPledged);
|
||||
if (amount != null && amount.compareTo(available) > 0) {
|
||||
final String limitText = lienPercentage == null ? "balance " + balance.toPlainString()
|
||||
: lienPercentage.stripTrailingZeros().toPlainString() + "% of balance " + balance.toPlainString() + " = "
|
||||
+ pledgeable.toPlainString();
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.recurringdeposit.lien.exceeds.limit",
|
||||
"Recurring deposit " + account.getAccountNumber() + " can secure at most " + available.max(BigDecimal.ZERO).toPlainString()
|
||||
+ " (" + limitText + ", less " + money(alreadyPledged) + " already pledged); "
|
||||
+ money(amount) + " was requested.",
|
||||
account.getId(), amount, available);
|
||||
}
|
||||
}
|
||||
|
||||
/** Rupees to the paisa for messages - "10000.00", not the stored "10000.000000". */
|
||||
private static String money(final BigDecimal amount) {
|
||||
return amount == null ? "0.00" : amount.setScale(2, RoundingMode.HALF_UP).toPlainString();
|
||||
}
|
||||
}
|
||||
-242
@@ -1,242 +0,0 @@
|
||||
/**
|
||||
* 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.portfolio.savings.service;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.portfolio.savings.DepositAccountType;
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.TransactionDefinition;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
/**
|
||||
* Missed-installment penalty for recurring deposits (product setting "Penalty / Grace Period for Missed
|
||||
* Installments").
|
||||
*
|
||||
* <p>
|
||||
* Every day, each installment still unpaid when its due date plus the product's grace days has passed is charged
|
||||
* the product's penalty once - a flat amount, or a percentage of the installment. The penalty is the system charge
|
||||
* "RD Missed Installment Penalty" added to the deposit and paid out of its balance, so it shows on the statement as
|
||||
* a charge and is booked to the product's Income From Penalties account like any other penalty.
|
||||
*
|
||||
* <ul>
|
||||
* <li>One penalty per installment, ever: m_rd_installment_penalty records which installments have been charged,
|
||||
* and an installment paid late afterwards is not charged again.</li>
|
||||
* <li>Paying an installment within the grace days costs nothing.</li>
|
||||
* <li>When the deposit does not yet hold enough to pay the penalty (the first installment missed, say), the charge
|
||||
* stays on the account as due and every later run tries again.</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class RecurringDepositMissedInstallmentPenaltyService {
|
||||
|
||||
public static final String PENALTY_CHARGE_NAME = "RD Missed Installment Penalty";
|
||||
public static final int PENALTY_TYPE_FLAT = 1;
|
||||
public static final int PENALTY_TYPE_PERCENT = 2;
|
||||
|
||||
private static final String DATE_FORMAT = "yyyy-MM-dd";
|
||||
private static final BigDecimal ONE_HUNDRED = BigDecimal.valueOf(100);
|
||||
|
||||
/** Unpaid installments of active RDs whose product has the penalty on and that have not been penalised yet. */
|
||||
private static final String CANDIDATES_SQL = """
|
||||
SELECT s.id AS installment_id, rd.id AS rd_id, s.duedate AS due_date, s.deposit_amount AS installment_amount,
|
||||
COALESCE(sp.missed_installment_grace_days, 0) AS grace_days,
|
||||
COALESCE(sp.missed_installment_penalty_type, 1) AS penalty_type,
|
||||
sp.missed_installment_penalty_amount AS penalty_amount,
|
||||
(SELECT COALESCE(SUM(s2.deposit_amount), 0) FROM m_mandatory_savings_schedule s2
|
||||
WHERE s2.savings_account_id = rd.id AND s2.duedate <= s.duedate) AS due_through_installment,
|
||||
(SELECT COALESCE(SUM(t.amount), 0) FROM m_savings_account_transaction t
|
||||
WHERE t.savings_account_id = rd.id AND t.transaction_type_enum = 1 AND t.is_reversed = false) AS total_paid
|
||||
FROM m_savings_account rd
|
||||
JOIN m_savings_product sp ON sp.id = rd.product_id
|
||||
JOIN m_mandatory_savings_schedule s ON s.savings_account_id = rd.id
|
||||
WHERE rd.deposit_type_enum = 300
|
||||
AND rd.status_enum = 300
|
||||
AND sp.penalty_grace_period_missed_installments = true
|
||||
AND COALESCE(sp.missed_installment_penalty_amount, 0) > 0
|
||||
AND COALESCE(s.completed_derived, false) = false
|
||||
AND s.deposit_amount > COALESCE(s.deposit_amount_completed_derived, 0)
|
||||
AND s.duedate < ?
|
||||
AND NOT EXISTS (SELECT 1 FROM m_rd_installment_penalty p WHERE p.installment_id = s.id)
|
||||
ORDER BY rd.id, s.duedate
|
||||
""";
|
||||
|
||||
/**
|
||||
* Penalty charges still unpaid on active recurring deposits. Found by the penalty charge definition rather than
|
||||
* through the tracking table, so nothing depends on the charge id being known at the moment it was added.
|
||||
*/
|
||||
private static final String UNPAID_SQL = """
|
||||
SELECT sac.savings_account_id AS rd_id, sac.id AS charge_id, sac.amount_outstanding_derived AS outstanding
|
||||
FROM m_savings_account_charge sac
|
||||
JOIN m_savings_account rd ON rd.id = sac.savings_account_id
|
||||
WHERE sac.charge_id = ?
|
||||
AND rd.deposit_type_enum = 300
|
||||
AND rd.status_enum = 300
|
||||
AND sac.is_active = true
|
||||
AND COALESCE(sac.is_paid_derived, false) = false
|
||||
AND COALESCE(sac.waived, false) = false
|
||||
AND sac.amount_outstanding_derived > 0
|
||||
ORDER BY sac.id
|
||||
""";
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final DepositAccountWritePlatformService depositAccountWritePlatformService;
|
||||
private final FromJsonHelper fromJsonHelper;
|
||||
private final TransactionTemplate ownTransaction;
|
||||
|
||||
public RecurringDepositMissedInstallmentPenaltyService(final JdbcTemplate jdbcTemplate,
|
||||
final DepositAccountWritePlatformService depositAccountWritePlatformService, final FromJsonHelper fromJsonHelper,
|
||||
final PlatformTransactionManager transactionManager) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
this.depositAccountWritePlatformService = depositAccountWritePlatformService;
|
||||
this.fromJsonHelper = fromJsonHelper;
|
||||
this.ownTransaction = new TransactionTemplate(transactionManager);
|
||||
this.ownTransaction.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
|
||||
}
|
||||
|
||||
/**
|
||||
* paidInFull: everything due up to and including this installment has actually been paid into the deposit - e.g.
|
||||
* in advance at the counter, which a deposit opened without "adjust advance payments" never allocates to the
|
||||
* installment itself. Such an installment only looks unpaid and must not be penalised.
|
||||
*/
|
||||
private record Candidate(Long installmentId, Long rdId, LocalDate dueDate, BigDecimal installmentAmount, int graceDays, int penaltyType,
|
||||
BigDecimal penaltyAmount, boolean paidInFull) {}
|
||||
|
||||
private record Unpaid(Long rdId, Long chargeId, BigDecimal outstanding) {}
|
||||
|
||||
/** @return how many installments were newly penalised */
|
||||
public int applyPenalties() {
|
||||
final LocalDate businessDate = DateUtils.getBusinessLocalDate();
|
||||
final Long penaltyChargeId = penaltyChargeId();
|
||||
if (penaltyChargeId == null) {
|
||||
log.warn("Charge '{}' not found - missed-installment penalties cannot be applied", PENALTY_CHARGE_NAME);
|
||||
return 0;
|
||||
}
|
||||
|
||||
final List<Candidate> candidates = this.jdbcTemplate.query(CANDIDATES_SQL,
|
||||
(rs, rowNum) -> new Candidate(rs.getLong("installment_id"), rs.getLong("rd_id"), rs.getObject("due_date", LocalDate.class),
|
||||
rs.getBigDecimal("installment_amount"), rs.getInt("grace_days"), rs.getInt("penalty_type"),
|
||||
rs.getBigDecimal("penalty_amount"),
|
||||
rs.getBigDecimal("total_paid").compareTo(rs.getBigDecimal("due_through_installment")) >= 0),
|
||||
businessDate);
|
||||
|
||||
int penalised = 0;
|
||||
for (final Candidate candidate : candidates) {
|
||||
// Grace: due on the 10th with 5 grace days is penalised from the 16th.
|
||||
if (!candidate.dueDate().plusDays(candidate.graceDays()).isBefore(businessDate) || candidate.paidInFull()) {
|
||||
continue;
|
||||
}
|
||||
final BigDecimal penalty = penaltyFor(candidate);
|
||||
if (penalty.signum() <= 0) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
this.ownTransaction.executeWithoutResult(status -> charge(candidate, penalty, penaltyChargeId, businessDate));
|
||||
penalised++;
|
||||
} catch (final RuntimeException e) {
|
||||
log.warn("Could not charge the missed-installment penalty on RD {} (installment due {}): {}", candidate.rdId(),
|
||||
candidate.dueDate(), e.getMessage());
|
||||
}
|
||||
}
|
||||
// Collect everything now due - tonight's penalties and any an earlier run could not cover.
|
||||
collectUnpaid(penaltyChargeId, businessDate);
|
||||
log.info("RD missed-installment penalty: {} installment(s) penalised on {}", penalised, businessDate);
|
||||
return penalised;
|
||||
}
|
||||
|
||||
/** Flat amount, or the percentage of this installment, rounded to the paisa. */
|
||||
static BigDecimal penaltyFor(final Candidate candidate) {
|
||||
if (candidate.penaltyType() == PENALTY_TYPE_PERCENT) {
|
||||
return candidate.installmentAmount().multiply(candidate.penaltyAmount()).divide(ONE_HUNDRED, 2, RoundingMode.HALF_UP);
|
||||
}
|
||||
return candidate.penaltyAmount().setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
/** Records the installment as penalised and adds the penalty charge to the deposit, as one unit. */
|
||||
private void charge(final Candidate candidate, final BigDecimal penalty, final Long penaltyChargeId, final LocalDate businessDate) {
|
||||
this.jdbcTemplate.update("""
|
||||
INSERT INTO m_rd_installment_penalty (installment_id, savings_account_id, penalty_amount, applied_on_date)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""", candidate.installmentId(), candidate.rdId(), penalty, businessDate);
|
||||
final JsonObject body = new JsonObject();
|
||||
body.addProperty("chargeId", penaltyChargeId);
|
||||
body.addProperty("amount", penalty);
|
||||
body.addProperty("dueDate", businessDate.toString());
|
||||
body.addProperty("dateFormat", DATE_FORMAT);
|
||||
body.addProperty("locale", "en");
|
||||
final CommandProcessingResult result = this.depositAccountWritePlatformService.addSavingsAccountCharge(command(body, candidate.rdId()),
|
||||
DepositAccountType.RECURRING_DEPOSIT);
|
||||
// The charge's id is only known here if the add happened to flush; keep the link when it is, for reference.
|
||||
if (result.getResourceId() != null) {
|
||||
this.jdbcTemplate.update("UPDATE m_rd_installment_penalty SET savings_account_charge_id = ? WHERE installment_id = ?",
|
||||
result.getResourceId(), candidate.installmentId());
|
||||
}
|
||||
}
|
||||
|
||||
private void collectUnpaid(final Long penaltyChargeId, final LocalDate businessDate) {
|
||||
final List<Unpaid> unpaid = this.jdbcTemplate.query(UNPAID_SQL,
|
||||
(rs, rowNum) -> new Unpaid(rs.getLong("rd_id"), rs.getLong("charge_id"), rs.getBigDecimal("outstanding")), penaltyChargeId);
|
||||
for (final Unpaid charge : unpaid) {
|
||||
tryPay(charge.rdId(), charge.chargeId(), charge.outstanding(), businessDate);
|
||||
}
|
||||
}
|
||||
|
||||
/** Pays the penalty from the deposit's balance; if it cannot be covered yet the charge simply stays due. */
|
||||
private void tryPay(final Long rdId, final Long chargeId, final BigDecimal amount, final LocalDate businessDate) {
|
||||
final JsonObject body = new JsonObject();
|
||||
body.addProperty("amount", amount);
|
||||
body.addProperty("dueDate", businessDate.toString());
|
||||
body.addProperty("dateFormat", DATE_FORMAT);
|
||||
body.addProperty("locale", "en");
|
||||
try {
|
||||
this.ownTransaction.executeWithoutResult(status -> this.depositAccountWritePlatformService.payCharge(rdId, chargeId,
|
||||
command(body, rdId), DepositAccountType.RECURRING_DEPOSIT));
|
||||
} catch (final RuntimeException e) {
|
||||
log.info("Missed-installment penalty {} on RD {} left due (not collected yet): {}", chargeId, rdId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private Long penaltyChargeId() {
|
||||
try {
|
||||
return this.jdbcTemplate.queryForObject(
|
||||
"SELECT id FROM m_charge WHERE name = ? AND is_deleted = false AND is_active = true ORDER BY id LIMIT 1", Long.class,
|
||||
PENALTY_CHARGE_NAME);
|
||||
} catch (final EmptyResultDataAccessException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** A command carrying the deposit's id the way the charge endpoints receive it from the API. */
|
||||
private JsonCommand command(final JsonObject body, final Long rdId) {
|
||||
return new JsonCommand(null, body.toString(), body, this.fromJsonHelper, null, rdId, null, null, null, null, rdId, null, null,
|
||||
null, null, null, null, null);
|
||||
}
|
||||
}
|
||||
-301
@@ -1,301 +0,0 @@
|
||||
/**
|
||||
* 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.portfolio.savings.service;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.codes.domain.CodeValueRepository;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||
import org.apache.fineract.portfolio.savings.domain.DepositProductAmountDetails;
|
||||
import org.apache.fineract.portfolio.savings.domain.RecurringDepositAccount;
|
||||
import org.apache.fineract.portfolio.savings.domain.RecurringDepositProduct;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsProduct;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* The recurring-deposit product settings that only mean something when an account is opened against them. Each one
|
||||
* used to be stored on the product and then ignored; they are enforced here, on the server, so a client that skips
|
||||
* the screen's own checks cannot open an account the product does not allow.
|
||||
*
|
||||
* <ul>
|
||||
* <li>Effective Date - the first day the product can be used. No account may be opened before it.</li>
|
||||
* <li>Effective To - the last day the product is on sale. No new account may be opened after it; accounts already
|
||||
* open run to maturity as normal.</li>
|
||||
* <li>End Date - the day the product is wound up. No account may be opened that would still be running after it, so
|
||||
* every account opened under the product has matured by then.</li>
|
||||
* <li>Deposit Amount Min / Max - bounds on the installment the customer commits to each period.</li>
|
||||
* <li>Nomination Facility - when on, at least one nominee is required; when off, nominees cannot be recorded.</li>
|
||||
* <li>Auto Debit Facility - only offered when the product allows it, and only with a linked Savings account to debit,
|
||||
* since that is where the installment is collected from.</li>
|
||||
* <li>Interest Calculation Type - copied onto the account as its simple/compounding flag (see
|
||||
* {@link #isSimpleInterestOption}).</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class RecurringDepositProductRules {
|
||||
|
||||
/** Label of the InterestCalculationTypeOption code value that switches a recurring deposit to simple interest. */
|
||||
public static final String SIMPLE_INTEREST_OPTION_LABEL = "Simple";
|
||||
|
||||
private static final String LINKED_ACCOUNT_TYPE_SAVINGS = "SAVINGS";
|
||||
|
||||
private static final BigDecimal ONE_HUNDRED = BigDecimal.valueOf(100);
|
||||
|
||||
private final CodeValueRepository codeValueRepository;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
/**
|
||||
* Fixes the account's simple/compounding flag from its product. Must run before the maturity amount is projected,
|
||||
* which is calculated with it.
|
||||
*/
|
||||
public void applyInterestCalculationType(final RecurringDepositAccount account) {
|
||||
account.setSimpleInterest(isSimpleInterestOption(account.savingsProduct().getInterestCalculateSelector()));
|
||||
}
|
||||
|
||||
/** Rules for a brand-new application. Runs after the maturity date is known, which End Date is checked against. */
|
||||
public void validateOnSubmit(final RecurringDepositAccount account, final JsonCommand command) {
|
||||
final SavingsProduct product = account.savingsProduct();
|
||||
applyProductPreClosurePenalty(account, product);
|
||||
applyLienPercentage(account, product, command, true);
|
||||
validateOpeningWindow(account, product);
|
||||
validateInstallmentAmount(account, product);
|
||||
validateNomination(product, command);
|
||||
validateAutoDebit(account, product);
|
||||
validateIntroducer(account, product);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rules for an application edited before approval. Nominees are only checked when the edit sends them, since an
|
||||
* edit that leaves them out keeps the ones already recorded.
|
||||
*/
|
||||
public void validateOnModify(final RecurringDepositAccount account, final JsonCommand command) {
|
||||
final SavingsProduct product = account.savingsProduct();
|
||||
applyProductPreClosurePenalty(account, product);
|
||||
applyLienPercentage(account, product, command, false);
|
||||
validateOpeningWindow(account, product);
|
||||
validateInstallmentAmount(account, product);
|
||||
if (hasNomineesProperty(command)) {
|
||||
validateNomination(product, command);
|
||||
}
|
||||
validateAutoDebit(account, product);
|
||||
validateIntroducer(account, product);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a product's Interest Calculation Type is "Simple". The option is a tenant-editable code value, so it is
|
||||
* matched by label rather than by a hard-coded id; anything else - no selection, "Compounding", an unknown id - is
|
||||
* compounding, which is how every recurring deposit was calculated before the option was honoured.
|
||||
*/
|
||||
public boolean isSimpleInterestOption(final Integer interestCalculateSelector) {
|
||||
if (interestCalculateSelector == null) {
|
||||
return false;
|
||||
}
|
||||
return this.codeValueRepository.findById(interestCalculateSelector.longValue())
|
||||
.map(codeValue -> SIMPLE_INTEREST_OPTION_LABEL.equalsIgnoreCase(StringUtils.trim(codeValue.getLabel()))).orElse(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* How much of this deposit may be given as loan. Only a Lien Marked product allows a lien at all; the account's
|
||||
* percentage may be lowered from the product's but never raised above it. When the application sends none, a new
|
||||
* account takes the product's percentage and an edited one keeps what it had.
|
||||
*/
|
||||
private void applyLienPercentage(final RecurringDepositAccount account, final SavingsProduct product, final JsonCommand command,
|
||||
final boolean isSubmit) {
|
||||
if (!product.isLienMarked()) {
|
||||
account.setLienPercentage(null);
|
||||
return;
|
||||
}
|
||||
final BigDecimal productLimit = product.getLienPercentage() == null ? ONE_HUNDRED : product.getLienPercentage();
|
||||
if (!command.parameterExists("lienPercentage") || command.bigDecimalValueOfParameterNamed("lienPercentage") == null) {
|
||||
if (isSubmit) {
|
||||
account.setLienPercentage(product.getLienPercentage());
|
||||
}
|
||||
return;
|
||||
}
|
||||
final BigDecimal requested = command.bigDecimalValueOfParameterNamed("lienPercentage");
|
||||
if (requested.signum() <= 0 || requested.compareTo(productLimit) > 0) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.recurringdeposit.lien.percentage.out.of.range",
|
||||
"Loan against this deposit must be more than 0% and at most " + productLimit.stripTrailingZeros().toPlainString()
|
||||
+ "% (the limit of product '" + product.getName() + "'); " + requested.stripTrailingZeros().toPlainString() + "% was entered.",
|
||||
requested, productLimit);
|
||||
}
|
||||
account.setLienPercentage(requested);
|
||||
}
|
||||
|
||||
private void validateOpeningWindow(final RecurringDepositAccount account, final SavingsProduct product) {
|
||||
final LocalDate openedOn = account.getSubmittedOnDate();
|
||||
if (openedOn == null) {
|
||||
return;
|
||||
}
|
||||
final LocalDate effectiveFrom = product.getEffectiveDate();
|
||||
if (effectiveFrom != null && openedOn.isBefore(effectiveFrom)) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.recurringdeposit.product.not.yet.effective",
|
||||
"Product '" + product.getName() + "' can only be used from " + effectiveFrom + "; this application is dated "
|
||||
+ openedOn + ".",
|
||||
effectiveFrom, openedOn);
|
||||
}
|
||||
final LocalDate effectiveTo = product.getEffectiveTo();
|
||||
if (effectiveTo != null && openedOn.isAfter(effectiveTo)) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.recurringdeposit.product.closed.for.new.accounts",
|
||||
"Product '" + product.getName() + "' stopped accepting new accounts after " + effectiveTo
|
||||
+ "; this application is dated " + openedOn + ".",
|
||||
effectiveTo, openedOn);
|
||||
}
|
||||
final LocalDate endDate = product.getEndDate();
|
||||
final LocalDate maturityDate = account.maturityDate();
|
||||
if (endDate != null && maturityDate != null && maturityDate.isAfter(endDate)) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.recurringdeposit.maturity.after.product.end.date",
|
||||
"This deposit would mature on " + maturityDate + ", after product '" + product.getName() + "' ends on " + endDate
|
||||
+ ". Choose a shorter tenure.",
|
||||
maturityDate, endDate);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* "Apply Penal Interest (less)" is the product's decision, not the account's: whatever the application sent, the
|
||||
* deposit takes the product's penalty - on or off, the same percentage and the same period (Whole Term / Till
|
||||
* Premature Withdrawal). A product without the penalty means no penal interest on premature closure.
|
||||
*/
|
||||
private void applyProductPreClosurePenalty(final RecurringDepositAccount account, final SavingsProduct product) {
|
||||
if (product instanceof RecurringDepositProduct rdProduct && rdProduct.depositProductTermAndPreClosure() != null) {
|
||||
account.applyProductPreClosurePenalty(rdProduct.depositProductTermAndPreClosure().depositPreClosureDetail());
|
||||
}
|
||||
}
|
||||
|
||||
/** A deposit whose tenure is extended must still mature on or before its product's End Date. */
|
||||
public void validateMaturityWithinProductEndDate(final RecurringDepositAccount account) {
|
||||
final SavingsProduct product = account.savingsProduct();
|
||||
final LocalDate endDate = product.getEndDate();
|
||||
final LocalDate maturityDate = account.maturityDate();
|
||||
if (endDate != null && maturityDate != null && maturityDate.isAfter(endDate)) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.recurringdeposit.maturity.after.product.end.date",
|
||||
"This deposit would mature on " + maturityDate + ", after product '" + product.getName() + "' ends on " + endDate
|
||||
+ ". Choose a shorter tenure.",
|
||||
maturityDate, endDate);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateInstallmentAmount(final RecurringDepositAccount account, final SavingsProduct product) {
|
||||
if (!(product instanceof RecurringDepositProduct rdProduct) || rdProduct.depositProductTermAndPreClosure() == null) {
|
||||
return;
|
||||
}
|
||||
final DepositProductAmountDetails limits = rdProduct.depositProductTermAndPreClosure().depositProductAmountDetails();
|
||||
final BigDecimal installment = account.getRecurringDetail() == null ? null
|
||||
: account.getRecurringDetail().mandatoryRecommendedDepositAmount();
|
||||
if (limits == null || installment == null) {
|
||||
return;
|
||||
}
|
||||
final BigDecimal min = limits.getMinDepositAmount();
|
||||
if (min != null && min.signum() > 0 && installment.compareTo(min) < 0) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.recurringdeposit.installment.below.product.minimum",
|
||||
"Installment amount " + installment.stripTrailingZeros().toPlainString() + " is below the product minimum of " + min.stripTrailingZeros().toPlainString()
|
||||
+ ".",
|
||||
installment, min);
|
||||
}
|
||||
final BigDecimal max = limits.getMaxDepositAmount();
|
||||
if (max != null && max.signum() > 0 && installment.compareTo(max) > 0) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.recurringdeposit.installment.above.product.maximum",
|
||||
"Installment amount " + installment.stripTrailingZeros().toPlainString() + " is above the product maximum of " + max.stripTrailingZeros().toPlainString()
|
||||
+ ".",
|
||||
installment, max);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateNomination(final SavingsProduct product, final JsonCommand command) {
|
||||
final boolean hasNominees = hasNominees(command);
|
||||
if (product.isNominationFacility() && !hasNominees) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.recurringdeposit.nominee.required",
|
||||
"Product '" + product.getName() + "' has Nomination Facility enabled - add at least one nominee.");
|
||||
}
|
||||
if (!product.isNominationFacility() && hasNominees) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.recurringdeposit.nomination.not.offered",
|
||||
"Product '" + product.getName() + "' does not offer a Nomination Facility, so nominees cannot be recorded.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* "Introducer Required" on the product: the deposit must name an introducer - an existing, active customer of the
|
||||
* bank, and not the depositor themselves.
|
||||
*/
|
||||
private void validateIntroducer(final RecurringDepositAccount account, final SavingsProduct product) {
|
||||
if (!product.isIntroducerRequired()) {
|
||||
return;
|
||||
}
|
||||
final Long introducerId = account.getIntroducerClientId();
|
||||
if (introducerId == null) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.recurringdeposit.introducer.required",
|
||||
"Product '" + product.getName() + "' requires an introducer - choose an existing customer as introducer.");
|
||||
}
|
||||
if (introducerId.equals(account.clientId())) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.recurringdeposit.introducer.self",
|
||||
"The depositor cannot be their own introducer.");
|
||||
}
|
||||
final Integer active = this.jdbcTemplate.queryForObject("SELECT COUNT(*) FROM m_client WHERE id = ? AND status_enum = 300",
|
||||
Integer.class, introducerId);
|
||||
if (active == null || active == 0) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.recurringdeposit.introducer.not.active",
|
||||
"The introducer must be an active customer of the bank.");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateAutoDebit(final RecurringDepositAccount account, final SavingsProduct product) {
|
||||
if (!Boolean.TRUE.equals(account.getAutoDebitFacility())) {
|
||||
return;
|
||||
}
|
||||
if (!product.isAutoDebitFacility()) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.recurringdeposit.auto.debit.not.offered",
|
||||
"Product '" + product.getName() + "' does not offer Auto Debit.");
|
||||
}
|
||||
final Long linkedAccountId = account.getLinkedAccountId();
|
||||
if (linkedAccountId == null || !LINKED_ACCOUNT_TYPE_SAVINGS.equalsIgnoreCase(account.getLinkedAccountType())) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.recurringdeposit.auto.debit.linked.savings.required",
|
||||
"Auto Debit collects each installment from the linked account, so link an active Savings account to use it.");
|
||||
}
|
||||
final Integer matches = this.jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(*) FROM m_savings_account WHERE id = ? AND client_id = ? AND deposit_type_enum = 100 AND status_enum = 300",
|
||||
Integer.class, linkedAccountId, account.clientId());
|
||||
if (matches == null || matches == 0) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.recurringdeposit.auto.debit.linked.savings.not.active",
|
||||
"Linked account " + linkedAccountId + " is not an active Savings account of this client, so Auto Debit cannot use it.",
|
||||
linkedAccountId);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hasNomineesProperty(final JsonCommand command) {
|
||||
final JsonObject root = command.parsedJson().getAsJsonObject();
|
||||
return root.has("nominees") || root.has("nominee");
|
||||
}
|
||||
|
||||
private static boolean hasNominees(final JsonCommand command) {
|
||||
final JsonObject root = command.parsedJson().getAsJsonObject();
|
||||
final JsonElement nominees = root.get("nominees");
|
||||
if (nominees != null && nominees.isJsonArray()) {
|
||||
return !nominees.getAsJsonArray().isEmpty();
|
||||
}
|
||||
final JsonElement nominee = root.get("nominee");
|
||||
return nominee != null && nominee.isJsonObject();
|
||||
}
|
||||
}
|
||||
+1
-3
@@ -374,10 +374,8 @@ public class SavingsAccountInterestPostingServiceImpl implements SavingsAccountI
|
||||
// this runs per account inside a batch job, so a null deposit type must not take the whole run down.
|
||||
final boolean interestTransferEnabled = savingsAccountData.isTransferInterestToLinkedAccount()
|
||||
&& DepositAccountType.FIXED_DEPOSIT.getValue().equals(savingsAccountData.getDepositTypeId());
|
||||
// A simple-interest recurring deposit must post here exactly what its own maturity projection promised, so the
|
||||
// flag is the same one SavingsAccount#calculateInterestUsing honours.
|
||||
this.savingsHelper.calculateInterestForAllPostingPeriods(monetaryCurrency, allPostingPeriods,
|
||||
getLockedInUntilLocalDate(savingsAccountData), interestTransferEnabled, savingsAccountData.isSimpleInterest());
|
||||
getLockedInUntilLocalDate(savingsAccountData), interestTransferEnabled);
|
||||
|
||||
savingsAccountData.getSummary().updateFromInterestPeriodSummaries(monetaryCurrency, allPostingPeriods);
|
||||
|
||||
|
||||
+3
-7
@@ -438,12 +438,10 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead
|
||||
LocalDate yesterday = DateUtils.getBusinessLocalDate().minusDays(1);
|
||||
// A product on the daily-interest timetable is paid by the pooled posting instead
|
||||
// (SavingsPooledInterestPostingService). Posting it here too would pay its members twice.
|
||||
// FD products are never on that timetable, so they still post here. A recurring deposit (deposit_type_enum
|
||||
// 300) is left out: its posting periods are credited by the nightly deposit maturity job instead
|
||||
// (RecurringDepositAccount#postPeriodicInterest), at the chart rate and on deposits alone. The filters sit inside the paging subquery so a page is never cut short by
|
||||
// accounts that are then dropped.
|
||||
// FD and RD products are never on that timetable, so they still post here. The filter sits inside the paging
|
||||
// subquery so a page is never cut short by accounts that are then dropped.
|
||||
String sql = "select " + this.savingAccountMapperForInterestPosting.schema()
|
||||
+ "join (select a.id from m_savings_account a where a.id > ? and a.status_enum = ? and a.deposit_type_enum <> 300 "
|
||||
+ "join (select a.id from m_savings_account a where a.id > ? and a.status_enum = ? "
|
||||
+ "and not exists (select 1 from m_savings_interest_posting_schedule s "
|
||||
+ "where s.savings_product_id = a.product_id and s.is_active = true) limit ?) b on b.id = sa.id ";
|
||||
if (backdatedTxnsAllowedTill) {
|
||||
@@ -472,7 +470,6 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead
|
||||
sqlBuilder.append("sa.id as id, sa.account_no as accountNo, sa.external_id as externalId, ");
|
||||
sqlBuilder.append("sa.deposit_type_enum as depositType, ");
|
||||
sqlBuilder.append("datp.transfer_interest_to_linked_account as transferInterestToLinkedAccount, ");
|
||||
sqlBuilder.append("sa.is_simple_interest as isSimpleInterest, ");
|
||||
sqlBuilder.append("c.id as clientId, c.office_id as clientOfficeId, ");
|
||||
sqlBuilder.append("g.id as groupId, g.office_id as groupOfficeId, ");
|
||||
sqlBuilder.append("sa.status_enum as statusEnum, ");
|
||||
@@ -842,7 +839,6 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead
|
||||
// Monthly-payout deposits must not compound: their interest leaves the account every
|
||||
// period, so each period earns on the principal alone.
|
||||
savingsAccountData.setTransferInterestToLinkedAccount(rs.getBoolean("transferInterestToLinkedAccount"));
|
||||
savingsAccountData.setSimpleInterest(rs.getBoolean("isSimpleInterest"));
|
||||
}
|
||||
|
||||
if (!transMap.containsValue(transactionId)) {
|
||||
|
||||
+4
-8
@@ -106,8 +106,6 @@ import org.apache.fineract.commands.domain.CommandSourceRepository;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityLimitRepository;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsProductRepository;
|
||||
import org.apache.fineract.portfolio.savings.service.FacilityChargeAutoApplyService;
|
||||
import org.apache.fineract.portfolio.savings.service.RecurringDepositLienService;
|
||||
import org.apache.fineract.portfolio.savings.service.RecurringDepositProductRules;
|
||||
import org.apache.fineract.portfolio.savings.service.DepositAccountInterestRateChartReadPlatformService;
|
||||
import org.apache.fineract.portfolio.savings.service.DepositAccountInterestRateChartReadPlatformServiceImpl;
|
||||
import org.apache.fineract.portfolio.savings.service.DepositAccountOnHoldTransactionReadPlatformService;
|
||||
@@ -299,8 +297,7 @@ public class SavingsConfiguration {
|
||||
ConfigurationDomainService configurationDomainService, HolidayRepositoryWrapper holidayRepository,
|
||||
WorkingDaysRepositoryWrapper workingDaysRepository,
|
||||
DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository,
|
||||
DenominationBreakdownWriter denominationBreakdownWriter, JdbcTemplate jdbcTemplate,
|
||||
RecurringDepositLienService recurringDepositLienService, RecurringDepositProductRules recurringDepositProductRules
|
||||
DenominationBreakdownWriter denominationBreakdownWriter, JdbcTemplate jdbcTemplate
|
||||
|
||||
) {
|
||||
return new DepositAccountWritePlatformServiceJpaRepositoryImpl(context, savingAccountRepositoryWrapper,
|
||||
@@ -310,7 +307,7 @@ public class SavingsConfiguration {
|
||||
chargeRepository, savingsAccountChargeRepository, accountAssociationsReadPlatformService,
|
||||
accountTransfersWritePlatformService, depositAccountReadPlatformService, calendarInstanceRepository,
|
||||
configurationDomainService, holidayRepository, workingDaysRepository, depositAccountOnHoldTransactionRepository,
|
||||
denominationBreakdownWriter, jdbcTemplate, recurringDepositLienService, recurringDepositProductRules);
|
||||
denominationBreakdownWriter, jdbcTemplate);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -326,14 +323,13 @@ public class SavingsConfiguration {
|
||||
FromJsonHelper fromJsonHelper, CalendarInstanceRepository calendarInstanceRepository,
|
||||
ConfigurationDomainService configurationDomainService, AccountNumberFormatRepositoryWrapper accountNumberFormatRepository,
|
||||
BusinessEventNotifierService businessEventNotifierService, SavingsAccountAssembler savingsAccountAssembler,
|
||||
ClientNomineeWritePlatformService clientNomineeWritePlatformService,
|
||||
RecurringDepositProductRules recurringDepositProductRules) {
|
||||
ClientNomineeWritePlatformService clientNomineeWritePlatformService) {
|
||||
return new DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl(context, savingAccountRepository,
|
||||
fixedDepositAccountRepository, recurringDepositAccountRepository, depositAccountAssembler, depositAccountDataValidator,
|
||||
accountNumberGenerator, clientRepository, groupRepository, savingsProductRepository, noteRepository, staffRepository,
|
||||
savingsAccountApplicationTransitionApiJsonValidator, savingsAccountChargeAssembler, accountAssociationsRepository,
|
||||
fromJsonHelper, calendarInstanceRepository, configurationDomainService, accountNumberFormatRepository,
|
||||
businessEventNotifierService, savingsAccountAssembler, clientNomineeWritePlatformService, recurringDepositProductRules);
|
||||
businessEventNotifierService, savingsAccountAssembler, clientNomineeWritePlatformService);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -167,9 +167,6 @@
|
||||
<include file="parts/0592_enable_teller_cashier_maker_checker.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0592_tds_declarations_and_challans.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0593_tds_interest_receipts_and_deductions.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0594_recurring_deposit_product_rules.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0595_recurring_deposit_lien_percentage.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0596_recurring_deposit_missed_installment_penalty.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0597_enable_recurring_deposit_product_maker_checker.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0598_recurring_deposit_extend_tenure_permission.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0593_investment_masters.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0594_investment_purchase.xml" relativeToChangelogFile="true"/>
|
||||
</databaseChangeLog>
|
||||
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
<?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">
|
||||
|
||||
<!--
|
||||
Treasury Investment masters, following the bank's existing CBS screens:
|
||||
- m_investment_party : Bank / Party Master (whom an investment is bought from / placed with)
|
||||
- m_investment_script : Script Master (what is bought - Govt Security, Bond, Mutual Fund, ...)
|
||||
Party and script codes are generated by the application (PTY-001, G00001). Both rows are
|
||||
tied to the investment (asset) GL they are booked to.
|
||||
-->
|
||||
|
||||
<changeSet author="cbs-anshul" id="0593-1-create-investment-party">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not><tableExists tableName="m_investment_party"/></not>
|
||||
</preConditions>
|
||||
<createTable tableName="m_investment_party">
|
||||
<column name="id" type="BIGINT" autoIncrement="true"><constraints nullable="false" primaryKey="true"/></column>
|
||||
<column name="party_code" type="VARCHAR(20)"><constraints nullable="false"/></column>
|
||||
<column name="bond_type" type="VARCHAR(20)"><constraints nullable="false"/></column>
|
||||
<column name="party_name" type="VARCHAR(150)"><constraints nullable="false"/></column>
|
||||
<column name="address1" type="VARCHAR(150)"/>
|
||||
<column name="address2" type="VARCHAR(150)"/>
|
||||
<column name="address3" type="VARCHAR(150)"/>
|
||||
<column name="city" type="VARCHAR(100)"/>
|
||||
<column name="pincode" type="VARCHAR(10)"/>
|
||||
<column name="phone" type="VARCHAR(20)"/>
|
||||
<column name="fax" type="VARCHAR(20)"/>
|
||||
<column name="bank_type" type="VARCHAR(60)"/>
|
||||
<column name="gl_account_id" type="BIGINT"><constraints nullable="false"/></column>
|
||||
<column name="rbi_code" type="VARCHAR(30)"/>
|
||||
<column name="exposure_limit" type="DECIMAL(19,6)"/>
|
||||
<column name="lei_no" type="VARCHAR(30)"/>
|
||||
<column name="cp_bank_name" type="VARCHAR(150)"/>
|
||||
<column name="cp_branch_name" type="VARCHAR(150)"/>
|
||||
<column name="cp_account_no" type="VARCHAR(40)"/>
|
||||
<column name="cp_ifsc" type="VARCHAR(11)"/>
|
||||
<column name="is_active" type="BOOLEAN" defaultValueBoolean="true"><constraints nullable="false"/></column>
|
||||
<column name="created_by" type="BIGINT"><constraints nullable="false"/></column>
|
||||
<column name="created_on_utc" type="DATETIME"><constraints nullable="false"/></column>
|
||||
<column name="last_modified_by" type="BIGINT"><constraints nullable="false"/></column>
|
||||
<column name="last_modified_on_utc" type="DATETIME"><constraints nullable="false"/></column>
|
||||
</createTable>
|
||||
<addUniqueConstraint tableName="m_investment_party" columnNames="party_code" constraintName="uq_investment_party_code"/>
|
||||
<addForeignKeyConstraint baseTableName="m_investment_party" baseColumnNames="gl_account_id" constraintName="fk_investment_party_gl_account"
|
||||
referencedTableName="acc_gl_account" referencedColumnNames="id"/>
|
||||
<createIndex tableName="m_investment_party" indexName="idx_investment_party_bond_type">
|
||||
<column name="bond_type"/>
|
||||
</createIndex>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="cbs-anshul" id="0593-2-create-investment-script">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not><tableExists tableName="m_investment_script"/></not>
|
||||
</preConditions>
|
||||
<createTable tableName="m_investment_script">
|
||||
<column name="id" type="BIGINT" autoIncrement="true"><constraints nullable="false" primaryKey="true"/></column>
|
||||
<column name="script_code" type="VARCHAR(20)"><constraints nullable="false"/></column>
|
||||
<column name="bond_type" type="VARCHAR(20)"><constraints nullable="false"/></column>
|
||||
<column name="slr_type" type="VARCHAR(20)"/>
|
||||
<column name="script_desc" type="VARCHAR(150)"><constraints nullable="false"/></column>
|
||||
<column name="gl_account_id" type="BIGINT"><constraints nullable="false"/></column>
|
||||
<column name="isin" type="VARCHAR(20)"/>
|
||||
<column name="face_value_per_unit" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
|
||||
<column name="int_rate" type="DECIMAL(9,4)"/>
|
||||
<column name="issue_date" type="DATE"/>
|
||||
<column name="maturity_date" type="DATE"/>
|
||||
<column name="company_name" type="VARCHAR(150)"/>
|
||||
<column name="address1" type="VARCHAR(150)"/>
|
||||
<column name="address2" type="VARCHAR(150)"/>
|
||||
<column name="address3" type="VARCHAR(150)"/>
|
||||
<column name="city" type="VARCHAR(100)"/>
|
||||
<column name="pincode" type="VARCHAR(10)"/>
|
||||
<column name="phone" type="VARCHAR(20)"/>
|
||||
<column name="fax" type="VARCHAR(20)"/>
|
||||
<column name="corp_code" type="VARCHAR(30)"/>
|
||||
<column name="is_active" type="BOOLEAN" defaultValueBoolean="true"><constraints nullable="false"/></column>
|
||||
<column name="created_by" type="BIGINT"><constraints nullable="false"/></column>
|
||||
<column name="created_on_utc" type="DATETIME"><constraints nullable="false"/></column>
|
||||
<column name="last_modified_by" type="BIGINT"><constraints nullable="false"/></column>
|
||||
<column name="last_modified_on_utc" type="DATETIME"><constraints nullable="false"/></column>
|
||||
</createTable>
|
||||
<addUniqueConstraint tableName="m_investment_script" columnNames="script_code" constraintName="uq_investment_script_code"/>
|
||||
<addForeignKeyConstraint baseTableName="m_investment_script" baseColumnNames="gl_account_id" constraintName="fk_investment_script_gl_account"
|
||||
referencedTableName="acc_gl_account" referencedColumnNames="id"/>
|
||||
<createIndex tableName="m_investment_script" indexName="idx_investment_script_bond_type">
|
||||
<column name="bond_type"/>
|
||||
</createIndex>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="cbs-anshul" id="0593-3-investment-master-permissions">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code IN ('READ_INVESTMENTPARTY', 'CREATE_INVESTMENTPARTY', 'UPDATE_INVESTMENTPARTY', 'DELETE_INVESTMENTPARTY', 'READ_INVESTMENTSCRIPT', 'CREATE_INVESTMENTSCRIPT', 'UPDATE_INVESTMENTSCRIPT', 'CREATE_INVESTMENTPARTY_CHECKER', 'UPDATE_INVESTMENTPARTY_CHECKER', 'DELETE_INVESTMENTPARTY_CHECKER', 'CREATE_INVESTMENTSCRIPT_CHECKER', 'UPDATE_INVESTMENTSCRIPT_CHECKER')</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="portfolio"/>
|
||||
<column name="code" value="READ_INVESTMENTPARTY"/>
|
||||
<column name="entity_name" value="INVESTMENTPARTY"/>
|
||||
<column name="action_name" value="READ"/>
|
||||
<column name="can_maker_checker" valueBoolean="false"/>
|
||||
</insert>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="portfolio"/>
|
||||
<column name="code" value="CREATE_INVESTMENTPARTY"/>
|
||||
<column name="entity_name" value="INVESTMENTPARTY"/>
|
||||
<column name="action_name" value="CREATE"/>
|
||||
<column name="can_maker_checker" valueBoolean="false"/>
|
||||
</insert>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="portfolio"/>
|
||||
<column name="code" value="UPDATE_INVESTMENTPARTY"/>
|
||||
<column name="entity_name" value="INVESTMENTPARTY"/>
|
||||
<column name="action_name" value="UPDATE"/>
|
||||
<column name="can_maker_checker" valueBoolean="false"/>
|
||||
</insert>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="portfolio"/>
|
||||
<column name="code" value="DELETE_INVESTMENTPARTY"/>
|
||||
<column name="entity_name" value="INVESTMENTPARTY"/>
|
||||
<column name="action_name" value="DELETE"/>
|
||||
<column name="can_maker_checker" valueBoolean="false"/>
|
||||
</insert>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="portfolio"/>
|
||||
<column name="code" value="READ_INVESTMENTSCRIPT"/>
|
||||
<column name="entity_name" value="INVESTMENTSCRIPT"/>
|
||||
<column name="action_name" value="READ"/>
|
||||
<column name="can_maker_checker" valueBoolean="false"/>
|
||||
</insert>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="portfolio"/>
|
||||
<column name="code" value="CREATE_INVESTMENTSCRIPT"/>
|
||||
<column name="entity_name" value="INVESTMENTSCRIPT"/>
|
||||
<column name="action_name" value="CREATE"/>
|
||||
<column name="can_maker_checker" valueBoolean="false"/>
|
||||
</insert>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="portfolio"/>
|
||||
<column name="code" value="UPDATE_INVESTMENTSCRIPT"/>
|
||||
<column name="entity_name" value="INVESTMENTSCRIPT"/>
|
||||
<column name="action_name" value="UPDATE"/>
|
||||
<column name="can_maker_checker" valueBoolean="false"/>
|
||||
</insert>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="portfolio"/>
|
||||
<column name="code" value="CREATE_INVESTMENTPARTY_CHECKER"/>
|
||||
<column name="entity_name" value="INVESTMENTPARTY"/>
|
||||
<column name="action_name" value="CREATE_CHECKER"/>
|
||||
<column name="can_maker_checker" valueBoolean="false"/>
|
||||
</insert>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="portfolio"/>
|
||||
<column name="code" value="UPDATE_INVESTMENTPARTY_CHECKER"/>
|
||||
<column name="entity_name" value="INVESTMENTPARTY"/>
|
||||
<column name="action_name" value="UPDATE_CHECKER"/>
|
||||
<column name="can_maker_checker" valueBoolean="false"/>
|
||||
</insert>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="portfolio"/>
|
||||
<column name="code" value="DELETE_INVESTMENTPARTY_CHECKER"/>
|
||||
<column name="entity_name" value="INVESTMENTPARTY"/>
|
||||
<column name="action_name" value="DELETE_CHECKER"/>
|
||||
<column name="can_maker_checker" valueBoolean="false"/>
|
||||
</insert>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="portfolio"/>
|
||||
<column name="code" value="CREATE_INVESTMENTSCRIPT_CHECKER"/>
|
||||
<column name="entity_name" value="INVESTMENTSCRIPT"/>
|
||||
<column name="action_name" value="CREATE_CHECKER"/>
|
||||
<column name="can_maker_checker" valueBoolean="false"/>
|
||||
</insert>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="portfolio"/>
|
||||
<column name="code" value="UPDATE_INVESTMENTSCRIPT_CHECKER"/>
|
||||
<column name="entity_name" value="INVESTMENTSCRIPT"/>
|
||||
<column name="action_name" value="UPDATE_CHECKER"/>
|
||||
<column name="can_maker_checker" valueBoolean="false"/>
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
<!-- Whoever already sees the Investment menu gets to read and maintain its masters. Maker-checker
|
||||
is left off (can_maker_checker=false); it can be switched on per action from
|
||||
Configure Maker Checker Tasks, and the _CHECKER rows above let a checker role approve. -->
|
||||
<changeSet author="cbs-anshul" id="0593-4-grant-investment-master-permissions">
|
||||
<sql>
|
||||
INSERT INTO m_role_permission (role_id, permission_id)
|
||||
SELECT rp.role_id, p.id
|
||||
FROM m_role_permission rp
|
||||
JOIN m_permission menu ON menu.id = rp.permission_id AND menu.code = 'MENU_INVESTMENT'
|
||||
CROSS JOIN m_permission p
|
||||
WHERE p.code IN ('READ_INVESTMENTPARTY', 'CREATE_INVESTMENTPARTY', 'UPDATE_INVESTMENTPARTY', 'DELETE_INVESTMENTPARTY', 'READ_INVESTMENTSCRIPT', 'CREATE_INVESTMENTSCRIPT', 'UPDATE_INVESTMENTSCRIPT')
|
||||
AND NOT EXISTS (SELECT 1 FROM m_role_permission x WHERE x.role_id = rp.role_id AND x.permission_id = p.id)
|
||||
</sql>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
<?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">
|
||||
|
||||
<!--
|
||||
Investment Purchase Entry: one row per deal, which is the holding that interest, sale and
|
||||
redemption later work against (balance_units tracks what is still held). Units, book value,
|
||||
accrued interest and settlement amount are computed by the application. The optional legacy
|
||||
deal fields (trade no, authorities, lien, delivery mode, ...) are kept in other_details as JSON.
|
||||
No GL entry is posted by this table yet.
|
||||
-->
|
||||
|
||||
<changeSet author="cbs-anshul" id="0594-1-create-investment-purchase">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not><tableExists tableName="m_investment_purchase"/></not>
|
||||
</preConditions>
|
||||
<createTable tableName="m_investment_purchase">
|
||||
<column name="id" type="BIGINT" autoIncrement="true"><constraints nullable="false" primaryKey="true"/></column>
|
||||
<column name="deal_no" type="VARCHAR(20)"><constraints nullable="false"/></column>
|
||||
<column name="cert_no" type="VARCHAR(20)"><constraints nullable="false"/></column>
|
||||
<column name="bond_type" type="VARCHAR(20)"><constraints nullable="false"/></column>
|
||||
<column name="script_id" type="BIGINT"><constraints nullable="false"/></column>
|
||||
<column name="party_id" type="BIGINT"><constraints nullable="false"/></column>
|
||||
<column name="deal_date" type="DATE"><constraints nullable="false"/></column>
|
||||
<column name="deal_time" type="VARCHAR(8)"/>
|
||||
<column name="settlement_type" type="VARCHAR(5)"><constraints nullable="false"/></column>
|
||||
<column name="settlement_date" type="DATE"><constraints nullable="false"/></column>
|
||||
<column name="face_value" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
|
||||
<column name="purchase_rate" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
|
||||
<column name="units" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
|
||||
<column name="balance_units" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
|
||||
<column name="brokerage" type="DECIMAL(19,6)"/>
|
||||
<column name="stamp_duty" type="DECIMAL(19,6)"/>
|
||||
<column name="book_value" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
|
||||
<column name="premium_discount" type="DECIMAL(19,6)"/>
|
||||
<column name="int_rate" type="DECIMAL(9,4)"/>
|
||||
<column name="issue_date" type="DATE"/>
|
||||
<column name="maturity_date" type="DATE"/>
|
||||
<column name="last_int_date" type="DATE"/>
|
||||
<column name="accrued_days" type="INT"/>
|
||||
<column name="accrued_interest" type="DECIMAL(19,6)"/>
|
||||
<column name="settlement_amount" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
|
||||
<column name="ytm" type="DECIMAL(9,4)"/>
|
||||
<column name="category" type="VARCHAR(40)"><constraints nullable="false"/></column>
|
||||
<column name="sub_type" type="VARCHAR(40)"/>
|
||||
<column name="investment_gl_account_id" type="BIGINT"><constraints nullable="false"/></column>
|
||||
<column name="cr_gl_account_id" type="BIGINT"><constraints nullable="false"/></column>
|
||||
<column name="other_details" type="VARCHAR(4000)"/>
|
||||
<column name="status" type="VARCHAR(20)"><constraints nullable="false"/></column>
|
||||
<column name="created_by" type="BIGINT"><constraints nullable="false"/></column>
|
||||
<column name="created_on_utc" type="DATETIME"><constraints nullable="false"/></column>
|
||||
<column name="last_modified_by" type="BIGINT"><constraints nullable="false"/></column>
|
||||
<column name="last_modified_on_utc" type="DATETIME"><constraints nullable="false"/></column>
|
||||
</createTable>
|
||||
<addUniqueConstraint tableName="m_investment_purchase" columnNames="deal_no" constraintName="uq_investment_purchase_deal_no"/>
|
||||
<addUniqueConstraint tableName="m_investment_purchase" columnNames="cert_no" constraintName="uq_investment_purchase_cert_no"/>
|
||||
<addForeignKeyConstraint baseTableName="m_investment_purchase" baseColumnNames="script_id" constraintName="fk_investment_purchase_script"
|
||||
referencedTableName="m_investment_script" referencedColumnNames="id"/>
|
||||
<addForeignKeyConstraint baseTableName="m_investment_purchase" baseColumnNames="party_id" constraintName="fk_investment_purchase_party"
|
||||
referencedTableName="m_investment_party" referencedColumnNames="id"/>
|
||||
<addForeignKeyConstraint baseTableName="m_investment_purchase" baseColumnNames="investment_gl_account_id" constraintName="fk_investment_purchase_inv_gl"
|
||||
referencedTableName="acc_gl_account" referencedColumnNames="id"/>
|
||||
<addForeignKeyConstraint baseTableName="m_investment_purchase" baseColumnNames="cr_gl_account_id" constraintName="fk_investment_purchase_cr_gl"
|
||||
referencedTableName="acc_gl_account" referencedColumnNames="id"/>
|
||||
<createIndex tableName="m_investment_purchase" indexName="idx_investment_purchase_script">
|
||||
<column name="script_id"/>
|
||||
</createIndex>
|
||||
<createIndex tableName="m_investment_purchase" indexName="idx_investment_purchase_bond_type_status">
|
||||
<column name="bond_type"/>
|
||||
<column name="status"/>
|
||||
</createIndex>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="cbs-anshul" id="0594-2-investment-purchase-permissions">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code IN ('READ_INVESTMENTPURCHASE', 'CREATE_INVESTMENTPURCHASE', 'DELETE_INVESTMENTPURCHASE', 'CREATE_INVESTMENTPURCHASE_CHECKER', 'DELETE_INVESTMENTPURCHASE_CHECKER')</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="portfolio"/>
|
||||
<column name="code" value="READ_INVESTMENTPURCHASE"/>
|
||||
<column name="entity_name" value="INVESTMENTPURCHASE"/>
|
||||
<column name="action_name" value="READ"/>
|
||||
<column name="can_maker_checker" valueBoolean="false"/>
|
||||
</insert>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="portfolio"/>
|
||||
<column name="code" value="CREATE_INVESTMENTPURCHASE"/>
|
||||
<column name="entity_name" value="INVESTMENTPURCHASE"/>
|
||||
<column name="action_name" value="CREATE"/>
|
||||
<column name="can_maker_checker" valueBoolean="false"/>
|
||||
</insert>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="portfolio"/>
|
||||
<column name="code" value="DELETE_INVESTMENTPURCHASE"/>
|
||||
<column name="entity_name" value="INVESTMENTPURCHASE"/>
|
||||
<column name="action_name" value="DELETE"/>
|
||||
<column name="can_maker_checker" valueBoolean="false"/>
|
||||
</insert>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="portfolio"/>
|
||||
<column name="code" value="CREATE_INVESTMENTPURCHASE_CHECKER"/>
|
||||
<column name="entity_name" value="INVESTMENTPURCHASE"/>
|
||||
<column name="action_name" value="CREATE_CHECKER"/>
|
||||
<column name="can_maker_checker" valueBoolean="false"/>
|
||||
</insert>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="portfolio"/>
|
||||
<column name="code" value="DELETE_INVESTMENTPURCHASE_CHECKER"/>
|
||||
<column name="entity_name" value="INVESTMENTPURCHASE"/>
|
||||
<column name="action_name" value="DELETE_CHECKER"/>
|
||||
<column name="can_maker_checker" valueBoolean="false"/>
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
<!-- Same roles as the masters: whoever sees the Investment menu. Maker-checker stays off until
|
||||
switched on from Configure Maker Checker Tasks. -->
|
||||
<changeSet author="cbs-anshul" id="0594-3-grant-investment-purchase-permissions">
|
||||
<sql>
|
||||
INSERT INTO m_role_permission (role_id, permission_id)
|
||||
SELECT rp.role_id, p.id
|
||||
FROM m_role_permission rp
|
||||
JOIN m_permission menu ON menu.id = rp.permission_id AND menu.code = 'MENU_INVESTMENT'
|
||||
CROSS JOIN m_permission p
|
||||
WHERE p.code IN ('READ_INVESTMENTPURCHASE', 'CREATE_INVESTMENTPURCHASE', 'DELETE_INVESTMENTPURCHASE')
|
||||
AND NOT EXISTS (SELECT 1 FROM m_role_permission x WHERE x.role_id = rp.role_id AND x.permission_id = p.id)
|
||||
</sql>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
-89
@@ -1,89 +0,0 @@
|
||||
<?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.9.xsd">
|
||||
|
||||
<!--
|
||||
Recurring deposit product settings that used to be stored and ignored.
|
||||
|
||||
1. Interest Calculation Type gains a "Simple" option. A recurring deposit opened under a product set to
|
||||
Simple never compounds its interest. The choice is copied onto the account when it opens
|
||||
(is_simple_interest) and kept for its life, like its chart and compounding period, so a later product
|
||||
edit cannot change what an open deposit was promised. Every existing account defaults to false -
|
||||
compounding - which is how all of them were calculated until now, so nothing already open moves.
|
||||
|
||||
2. RD Auto Debit job: collects each due installment of a deposit opened with Auto Debit from the
|
||||
Savings account linked to it.
|
||||
-->
|
||||
|
||||
<changeSet author="fineract" id="0594-1-savings-account-is-simple-interest">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<columnExists tableName="m_savings_account" columnName="is_simple_interest"/>
|
||||
</not>
|
||||
</preConditions>
|
||||
<addColumn tableName="m_savings_account">
|
||||
<column name="is_simple_interest" type="BOOLEAN" defaultValueBoolean="false">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="0594-2-interest-calculation-type-simple">
|
||||
<!-- Matched by label in code (RecurringDepositProductRules), so a tenant that already added its own
|
||||
"Simple" value keeps it and no duplicate is created. -->
|
||||
<sql>
|
||||
INSERT INTO m_code_value (code_id, code_value, code_description, order_position, code_score, is_active, is_mandatory)
|
||||
SELECT c.id, 'Simple', 'Simple', 3, NULL, true, false FROM m_code c
|
||||
WHERE c.code_name = 'InterestCalculationTypeOption'
|
||||
AND NOT EXISTS (SELECT 1 FROM m_code_value v WHERE v.code_id = c.id AND LOWER(v.code_value) = 'simple');
|
||||
</sql>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="0594-3-execute-rd-auto-debit-job">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM job WHERE name = 'Execute RD Auto Debit'</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="job">
|
||||
<column name="name" value="Execute RD Auto Debit"/>
|
||||
<column name="display_name" value="Execute RD Auto Debit"/>
|
||||
<!-- short_name is NOT NULL and capped at 8 characters; see 0145_job_short_name.xml. -->
|
||||
<column name="short_name" value="RDAUTODB"/>
|
||||
<!-- 00:30 daily: after the midnight business-date sync, so "due today" means the new day. Anything a
|
||||
run cannot collect stays overdue and is retried by the next run. -->
|
||||
<column name="cron_expression" value="0 30 0 1/1 * ? *"/>
|
||||
<column name="create_time" valueDate="${current_datetime}"/>
|
||||
<column name="task_priority" valueNumeric="5"/>
|
||||
<column name="group_name"/>
|
||||
<column name="previous_run_start_time"/>
|
||||
<column name="job_key" value="Execute RD Auto DebitJobDetail1 _ DEFAULT"/>
|
||||
<column name="initializing_errorlog"/>
|
||||
<column name="is_active" valueBoolean="true"/>
|
||||
<column name="currently_running" valueBoolean="false"/>
|
||||
<column name="updates_allowed" valueBoolean="true"/>
|
||||
<column name="scheduler_group" valueNumeric="1"/>
|
||||
<column name="is_misfired" valueBoolean="false"/>
|
||||
<column name="node_id" valueNumeric="1"/>
|
||||
<column name="is_mismatched_job" valueBoolean="true"/>
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
-67
@@ -1,67 +0,0 @@
|
||||
<?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.9.xsd">
|
||||
|
||||
<!--
|
||||
Lien Marked's loan limit: the most a deposit may secure, as a percentage of its balance (e.g. 90 = a loan of
|
||||
up to 90% of what is in the deposit). Null keeps today's behaviour - the whole unpledged balance.
|
||||
|
||||
Added to the product and to the pending-edit table, since a product edit waits there for approval and is
|
||||
replayed from it; a column only on the product would be dropped by every approved edit.
|
||||
-->
|
||||
|
||||
<changeSet author="fineract" id="0595-1-savings-product-lien-percentage">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<columnExists tableName="m_savings_product" columnName="lien_percentage"/>
|
||||
</not>
|
||||
</preConditions>
|
||||
<addColumn tableName="m_savings_product">
|
||||
<column name="lien_percentage" type="DECIMAL(5,2)"/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="0595-2-saving-product-temp-lien-percentage">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<tableExists tableName="m_saving_product_temp"/>
|
||||
<not>
|
||||
<columnExists tableName="m_saving_product_temp" columnName="lien_percentage"/>
|
||||
</not>
|
||||
</preConditions>
|
||||
<addColumn tableName="m_saving_product_temp">
|
||||
<column name="lien_percentage" type="DECIMAL(5,2)"/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
<!-- The deposit's own limit, chosen when it is opened (never above its product's). Null = product's. -->
|
||||
<changeSet author="fineract" id="0595-3-savings-account-lien-percentage">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<columnExists tableName="m_savings_account" columnName="lien_percentage"/>
|
||||
</not>
|
||||
</preConditions>
|
||||
<addColumn tableName="m_savings_account">
|
||||
<column name="lien_percentage" type="DECIMAL(5,2)"/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
-128
@@ -1,128 +0,0 @@
|
||||
<?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.9.xsd">
|
||||
|
||||
<!--
|
||||
Missed-installment penalty for recurring deposits ("Penalty / Grace Period for Missed Installments").
|
||||
|
||||
The product gains grace days, a penalty type (1 = flat amount, 2 = % of the installment) and the amount -
|
||||
on the product and on the pending-edit table, since a product edit waits there for approval and is replayed
|
||||
from it. A daily job charges each installment still unpaid after its grace days once, using the system
|
||||
charge seeded below, and records it in m_rd_installment_penalty so it is never charged twice.
|
||||
-->
|
||||
|
||||
<changeSet author="fineract" id="0596-1-savings-product-missed-installment-penalty">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<columnExists tableName="m_savings_product" columnName="missed_installment_penalty_amount"/>
|
||||
</not>
|
||||
</preConditions>
|
||||
<addColumn tableName="m_savings_product">
|
||||
<column name="missed_installment_grace_days" type="INT"/>
|
||||
<column name="missed_installment_penalty_type" type="INT"/>
|
||||
<column name="missed_installment_penalty_amount" type="DECIMAL(19,6)"/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="0596-2-saving-product-temp-missed-installment-penalty">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<tableExists tableName="m_saving_product_temp"/>
|
||||
<not>
|
||||
<columnExists tableName="m_saving_product_temp" columnName="missed_installment_penalty_amount"/>
|
||||
</not>
|
||||
</preConditions>
|
||||
<addColumn tableName="m_saving_product_temp">
|
||||
<column name="missed_installment_grace_days" type="INT"/>
|
||||
<column name="missed_installment_penalty_type" type="INT"/>
|
||||
<column name="missed_installment_penalty_amount" type="DECIMAL(19,6)"/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="0596-3-rd-installment-penalty-table">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<tableExists tableName="m_rd_installment_penalty"/>
|
||||
</not>
|
||||
</preConditions>
|
||||
<createTable tableName="m_rd_installment_penalty">
|
||||
<column name="id" type="BIGINT" autoIncrement="true">
|
||||
<constraints primaryKey="true" nullable="false"/>
|
||||
</column>
|
||||
<!-- One penalty per installment, ever. -->
|
||||
<column name="installment_id" type="BIGINT">
|
||||
<constraints nullable="false" unique="true" uniqueConstraintName="uq_rd_installment_penalty_installment"/>
|
||||
</column>
|
||||
<column name="savings_account_id" type="BIGINT">
|
||||
<constraints nullable="false" foreignKeyName="fk_rd_installment_penalty_account"
|
||||
referencedTableName="m_savings_account" referencedColumnNames="id"/>
|
||||
</column>
|
||||
<column name="savings_account_charge_id" type="BIGINT"/>
|
||||
<column name="penalty_amount" type="DECIMAL(19,6)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="applied_on_date" type="DATE">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="0596-4-rd-missed-installment-penalty-charge">
|
||||
<!-- Specified-due-date savings penalty in the organisation's currency; the amount is set per installment by
|
||||
the job, so the definition's own amount stays 0. Matched by name, so a tenant's own copy is reused. -->
|
||||
<sql>
|
||||
INSERT INTO m_charge (name, currency_code, charge_applies_to_enum, charge_time_enum, charge_calculation_enum,
|
||||
amount, is_penalty, is_active, is_deleted)
|
||||
SELECT 'RD Missed Installment Penalty', oc.code, 2, 2, 1, 0, true, true, false
|
||||
FROM (SELECT code FROM m_organisation_currency ORDER BY id LIMIT 1) oc
|
||||
WHERE NOT EXISTS (SELECT 1 FROM m_charge c WHERE c.name = 'RD Missed Installment Penalty' AND c.is_deleted = false);
|
||||
</sql>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="0596-5-apply-rd-missed-installment-penalty-job">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM job WHERE name = 'Apply RD Missed Installment Penalty'</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="job">
|
||||
<column name="name" value="Apply RD Missed Installment Penalty"/>
|
||||
<column name="display_name" value="Apply RD Missed Installment Penalty"/>
|
||||
<!-- short_name is NOT NULL and capped at 8 characters; see 0145_job_short_name.xml. -->
|
||||
<column name="short_name" value="RDPENLTY"/>
|
||||
<!-- 00:45 daily: after the business-date sync and after RD Auto Debit (00:30), so an installment the
|
||||
auto debit collects tonight is never penalised. -->
|
||||
<column name="cron_expression" value="0 45 0 1/1 * ? *"/>
|
||||
<column name="create_time" valueDate="${current_datetime}"/>
|
||||
<column name="task_priority" valueNumeric="5"/>
|
||||
<column name="group_name"/>
|
||||
<column name="previous_run_start_time"/>
|
||||
<column name="job_key" value="Apply RD Missed Installment PenaltyJobDetail1 _ DEFAULT"/>
|
||||
<column name="initializing_errorlog"/>
|
||||
<column name="is_active" valueBoolean="true"/>
|
||||
<column name="currently_running" valueBoolean="false"/>
|
||||
<column name="updates_allowed" valueBoolean="true"/>
|
||||
<column name="scheduler_group" valueNumeric="1"/>
|
||||
<column name="is_misfired" valueBoolean="false"/>
|
||||
<column name="node_id" valueNumeric="1"/>
|
||||
<column name="is_mismatched_job" valueBoolean="true"/>
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
-88
@@ -1,88 +0,0 @@
|
||||
<?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.9.xsd">
|
||||
|
||||
<!--
|
||||
Recurring deposit product creation goes to the checker like every other product. 0472 covered loan, savings,
|
||||
current and share products and 0474 fixed deposits; recurring deposits were left out, so a new RD product
|
||||
went live the moment it was saved.
|
||||
-->
|
||||
<changeSet id="0597_add_recurringdepositproduct_checker_permission_if_not_exists" author="fineract">
|
||||
<sql>
|
||||
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||
SELECT 'portfolio', 'CREATE_RECURRINGDEPOSITPRODUCT_CHECKER', 'RECURRINGDEPOSITPRODUCT', 'CREATE_CHECKER', FALSE
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM m_permission WHERE code = 'CREATE_RECURRINGDEPOSITPRODUCT_CHECKER'
|
||||
);
|
||||
</sql>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="0597_enable_recurring_deposit_product_creation_maker_checker" author="fineract">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="1">
|
||||
SELECT COUNT(*) FROM m_permission WHERE code = 'CREATE_RECURRINGDEPOSITPRODUCT'
|
||||
</sqlCheck>
|
||||
</preConditions>
|
||||
<sql>
|
||||
UPDATE m_permission
|
||||
SET can_maker_checker = TRUE
|
||||
WHERE code = 'CREATE_RECURRINGDEPOSITPRODUCT';
|
||||
</sql>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="0597_grant_recurring_deposit_product_creation_maker_checker_permissions_to_roles" author="fineract">
|
||||
<sql>
|
||||
INSERT INTO m_role_permission (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM m_role r
|
||||
CROSS JOIN m_permission p
|
||||
WHERE r.name = 'Super user'
|
||||
AND p.code IN ('CREATE_RECURRINGDEPOSITPRODUCT', 'CREATE_RECURRINGDEPOSITPRODUCT_CHECKER')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM m_role_permission rp
|
||||
WHERE rp.role_id = r.id AND rp.permission_id = p.id
|
||||
);
|
||||
INSERT INTO m_role_permission (role_id, permission_id)
|
||||
SELECT DISTINCT ur.role_id, p.id
|
||||
FROM m_appuser u
|
||||
JOIN m_appuser_role ur ON ur.appuser_id = u.id
|
||||
CROSS JOIN m_permission p
|
||||
WHERE u.username = 'bankadmin'
|
||||
AND p.code IN ('CREATE_RECURRINGDEPOSITPRODUCT', 'CREATE_RECURRINGDEPOSITPRODUCT_CHECKER')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM m_role_permission rp
|
||||
WHERE rp.role_id = ur.role_id AND rp.permission_id = p.id
|
||||
);
|
||||
INSERT INTO m_role_permission (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM m_role r
|
||||
CROSS JOIN m_permission p
|
||||
WHERE LOWER(r.name) = 'clerk'
|
||||
AND p.code IN ('CREATE_RECURRINGDEPOSITPRODUCT', 'CREATE_RECURRINGDEPOSITPRODUCT_CHECKER')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM m_role_permission rp
|
||||
WHERE rp.role_id = r.id AND rp.permission_id = p.id
|
||||
);
|
||||
</sql>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
<?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.9.xsd">
|
||||
|
||||
<!--
|
||||
Tenure Flexibility: extending a running recurring deposit's term. Goes to the checker like a premature closure,
|
||||
and is granted to whichever roles may prematurely close a recurring deposit.
|
||||
-->
|
||||
<changeSet id="0598_add_recurring_deposit_extend_tenure_permissions" author="fineract">
|
||||
<sql>
|
||||
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||
SELECT 'transaction_savings', 'EXTENDTENURE_RECURRINGDEPOSITACCOUNT', 'RECURRINGDEPOSITACCOUNT', 'EXTENDTENURE', TRUE
|
||||
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'EXTENDTENURE_RECURRINGDEPOSITACCOUNT');
|
||||
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||
SELECT 'transaction_savings', 'EXTENDTENURE_RECURRINGDEPOSITACCOUNT_CHECKER', 'RECURRINGDEPOSITACCOUNT', 'EXTENDTENURE_CHECKER', FALSE
|
||||
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'EXTENDTENURE_RECURRINGDEPOSITACCOUNT_CHECKER');
|
||||
</sql>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="0598_grant_recurring_deposit_extend_tenure_permissions" author="fineract">
|
||||
<sql>
|
||||
INSERT INTO m_role_permission (role_id, permission_id)
|
||||
SELECT DISTINCT rp.role_id, np.id
|
||||
FROM m_role_permission rp
|
||||
JOIN m_permission op ON op.id = rp.permission_id
|
||||
JOIN m_permission np ON np.code = REPLACE(op.code, 'PREMATURECLOSE_', 'EXTENDTENURE_')
|
||||
WHERE op.code IN ('PREMATURECLOSE_RECURRINGDEPOSITACCOUNT', 'PREMATURECLOSE_RECURRINGDEPOSITACCOUNT_CHECKER')
|
||||
AND NOT EXISTS (SELECT 1 FROM m_role_permission x WHERE x.role_id = rp.role_id AND x.permission_id = np.id);
|
||||
</sql>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
-27
@@ -511,33 +511,6 @@ public class CommandSourceServiceTest {
|
||||
Mockito.verify(commandSource, Mockito.never()).markAsChecked(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcessCommand_RecurringDepositProduct_SuperUser_EnforcesMakerChecker() {
|
||||
NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class);
|
||||
JsonCommand jsonCommand = Mockito.mock(JsonCommand.class);
|
||||
CommandSource commandSource = Mockito.mock(CommandSource.class);
|
||||
AppUser superUser = Mockito.mock(AppUser.class);
|
||||
|
||||
Mockito.when(commandSource.getPermissionCode()).thenReturn("CREATE_RECURRINGDEPOSITPRODUCT");
|
||||
Mockito.when(commandSource.getEntityName()).thenReturn("RECURRINGDEPOSITPRODUCT");
|
||||
Mockito.when(commandSource.getActionName()).thenReturn("CREATE");
|
||||
Mockito.when(commandSource.getId()).thenReturn(115L);
|
||||
Mockito.when(commandSource.getResourceId()).thenReturn(215L);
|
||||
Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("CREATE_RECURRINGDEPOSITPRODUCT")).thenReturn(true);
|
||||
Mockito.when(superUser.isCheckerSuperUser()).thenReturn(true);
|
||||
|
||||
org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder()
|
||||
.withCommandId(1L).build();
|
||||
Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult);
|
||||
|
||||
Assertions.assertThrows(org.apache.fineract.commands.exception.RollbackTransactionNotApprovedException.class, () -> {
|
||||
underTest.processCommand(handler, jsonCommand, commandSource, superUser, false);
|
||||
});
|
||||
|
||||
Mockito.verify(commandSource).markAsAwaitingApproval();
|
||||
Mockito.verify(commandSource, Mockito.never()).markAsChecked(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcessCommand_FixedDepositProduct_CheckerApproval_MarksAsChecked() {
|
||||
NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class);
|
||||
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* 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.portfolio.investment.service;
|
||||
|
||||
import static org.apache.fineract.portfolio.investment.service.InvestmentInterestCalculator.TYPE_COUPON;
|
||||
import static org.apache.fineract.portfolio.investment.service.InvestmentInterestCalculator.TYPE_PROVISION;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentInterestCalculator.ScheduleRow;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Figures from Jitendra Sir's worked example: 7.50% GOI 2034, face value 1 crore, coupons 10 Feb / 10 Aug, bought on
|
||||
* 08-10-2026.
|
||||
*/
|
||||
class InvestmentInterestCalculatorTest {
|
||||
|
||||
private static final BigDecimal ONE_CRORE = new BigDecimal("10000000");
|
||||
private static final BigDecimal RATE = new BigDecimal("7.50");
|
||||
private static final LocalDate MATURITY = LocalDate.of(2034, 8, 10);
|
||||
private static final LocalDate LAST_COUPON = LocalDate.of(2026, 8, 10);
|
||||
private static final LocalDate DEAL = LocalDate.of(2026, 10, 8);
|
||||
|
||||
@Test
|
||||
void days360CountsEveryMonthAsThirtyDays() {
|
||||
assertEquals(50, InvestmentInterestCalculator.days360(LAST_COUPON, LocalDate.of(2026, 9, 30)));
|
||||
assertEquals(58, InvestmentInterestCalculator.days360(LAST_COUPON, DEAL));
|
||||
assertEquals(90, InvestmentInterestCalculator.days360(LocalDate.of(2026, 6, 30), LocalDate.of(2026, 9, 30)));
|
||||
assertEquals(50, InvestmentInterestCalculator.days360(LocalDate.of(2027, 2, 10), LocalDate.of(2027, 3, 31)));
|
||||
assertEquals(180, InvestmentInterestCalculator.days360(LAST_COUPON, LocalDate.of(2027, 2, 10)));
|
||||
assertEquals(360, InvestmentInterestCalculator.days360(LAST_COUPON, LocalDate.of(2027, 8, 10)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void days360IsZeroWhenDatesAreMissingOrReversed() {
|
||||
assertEquals(0, InvestmentInterestCalculator.days360(null, DEAL));
|
||||
assertEquals(0, InvestmentInterestCalculator.days360(DEAL, null));
|
||||
assertEquals(0, InvestmentInterestCalculator.days360(DEAL, DEAL));
|
||||
assertEquals(0, InvestmentInterestCalculator.days360(DEAL, LAST_COUPON));
|
||||
}
|
||||
|
||||
@Test
|
||||
void interestForDaysRoundsToTheRupee() {
|
||||
assertEquals(new BigDecimal("120833"), InvestmentInterestCalculator.interestForDays(ONE_CRORE, RATE, 58));
|
||||
assertEquals(new BigDecimal("104167"), InvestmentInterestCalculator.interestForDays(ONE_CRORE, RATE, 50));
|
||||
assertEquals(new BigDecimal("375000"), InvestmentInterestCalculator.interestForDays(ONE_CRORE, RATE, 180));
|
||||
}
|
||||
|
||||
@Test
|
||||
void halfYearCouponIsFaceValueTimesRateOverTwo() {
|
||||
assertEquals(new BigDecimal("375000"), InvestmentInterestCalculator.halfYearCoupon(ONE_CRORE, RATE));
|
||||
assertEquals(new BigDecimal("362500"), InvestmentInterestCalculator.halfYearCoupon(ONE_CRORE, new BigDecimal("7.25")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void couponDatesRunSixMonthlyFromMaturity() {
|
||||
assertEquals(LAST_COUPON, InvestmentInterestCalculator.lastCouponOnOrBefore(MATURITY, DEAL));
|
||||
assertEquals(LAST_COUPON, InvestmentInterestCalculator.lastCouponOnOrBefore(MATURITY, LAST_COUPON));
|
||||
|
||||
final List<LocalDate> coupons = InvestmentInterestCalculator.couponDatesAfter(MATURITY, LAST_COUPON);
|
||||
assertEquals(16, coupons.size());
|
||||
assertEquals(LocalDate.of(2027, 2, 10), coupons.get(0));
|
||||
assertEquals(LocalDate.of(2027, 8, 10), coupons.get(1));
|
||||
assertEquals(MATURITY, coupons.get(15));
|
||||
}
|
||||
|
||||
@Test
|
||||
void monthEndMaturityKeepsMonthEndCoupons() {
|
||||
final LocalDate maturity = LocalDate.of(2030, 8, 31);
|
||||
final List<LocalDate> coupons = InvestmentInterestCalculator.couponDatesAfter(maturity, LocalDate.of(2029, 8, 31));
|
||||
assertEquals(List.of(LocalDate.of(2030, 2, 28), maturity), coupons);
|
||||
}
|
||||
|
||||
@Test
|
||||
void closingDatesAreMarchAndSeptemberEnds() {
|
||||
assertEquals(List.of(LocalDate.of(2027, 3, 31), LocalDate.of(2027, 9, 30)),
|
||||
InvestmentInterestCalculator.closingDates(DEAL, LocalDate.of(2028, 3, 30)));
|
||||
assertEquals(List.of(LocalDate.of(2026, 9, 30)),
|
||||
InvestmentInterestCalculator.closingDates(LocalDate.of(2026, 9, 30), LocalDate.of(2026, 9, 30)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void scheduleClearsPurchasedInterestAndProvisionsAtEachClosing() {
|
||||
final List<ScheduleRow> rows = InvestmentInterestCalculator.schedule(ONE_CRORE, RATE, MATURITY, LAST_COUPON, DEAL,
|
||||
new BigDecimal("120833"));
|
||||
|
||||
// 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());
|
||||
|
||||
// First coupon: 1,20,833 paid to the seller is recovered, only the rest is our income.
|
||||
assertRow(rows.get(0), TYPE_COUPON, LocalDate.of(2027, 2, 10), LocalDate.of(2026, 8, 11), 180, "375000", "120833", "254167");
|
||||
// 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");
|
||||
// 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(3), TYPE_PROVISION, LocalDate.of(2027, 9, 30), LocalDate.of(2027, 8, 11), 50, "104167", "0", "104167");
|
||||
|
||||
final ScheduleRow last = rows.get(rows.size() - 1);
|
||||
assertEquals(TYPE_COUPON, last.type());
|
||||
assertEquals(MATURITY, last.date());
|
||||
|
||||
// Over the life, income booked = all coupons − interest bought from the seller.
|
||||
final BigDecimal totalIncome = rows.stream().map(ScheduleRow::amount).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
assertEquals(new BigDecimal("375000").multiply(BigDecimal.valueOf(16)).subtract(new BigDecimal("120833")), totalIncome);
|
||||
}
|
||||
|
||||
@Test
|
||||
void closingOnACouponDateIsNotProvisionedTwice() {
|
||||
// Coupons on 31 Mar / 30 Sep: every closing is a coupon day, so there are no provision lines.
|
||||
final List<ScheduleRow> rows = InvestmentInterestCalculator.schedule(ONE_CRORE, RATE, LocalDate.of(2028, 3, 31),
|
||||
LocalDate.of(2026, 9, 30), DEAL, BigDecimal.ZERO);
|
||||
assertEquals(3, rows.size());
|
||||
assertTrue(rows.stream().allMatch(r -> TYPE_COUPON.equals(r.type())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void scheduleIsEmptyWithoutRateOrDates() {
|
||||
assertTrue(InvestmentInterestCalculator.schedule(ONE_CRORE, BigDecimal.ZERO, MATURITY, LAST_COUPON, DEAL, null).isEmpty());
|
||||
assertTrue(InvestmentInterestCalculator.schedule(ONE_CRORE, RATE, null, LAST_COUPON, DEAL, null).isEmpty());
|
||||
assertTrue(InvestmentInterestCalculator.schedule(ONE_CRORE, RATE, MATURITY, null, DEAL, null).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void dealNumberFollowsTheAprilToMarchFinancialYear() {
|
||||
assertEquals("202627", InvestmentPurchaseWritePlatformServiceImpl.dealNoPrefix(LocalDate.of(2026, 4, 1)));
|
||||
assertEquals("202627", InvestmentPurchaseWritePlatformServiceImpl.dealNoPrefix(DEAL));
|
||||
assertEquals("202627", InvestmentPurchaseWritePlatformServiceImpl.dealNoPrefix(LocalDate.of(2027, 3, 31)));
|
||||
assertEquals("202728", InvestmentPurchaseWritePlatformServiceImpl.dealNoPrefix(LocalDate.of(2027, 4, 1)));
|
||||
assertEquals("209900", InvestmentPurchaseWritePlatformServiceImpl.dealNoPrefix(LocalDate.of(2099, 12, 1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void approxYtmRisesWhenBoughtAtADiscount() {
|
||||
final LocalDate issue = LocalDate.of(2024, 8, 10);
|
||||
assertEquals(new BigDecimal("7.59"),
|
||||
InvestmentPurchaseWritePlatformServiceImpl.approxYtm(RATE, new BigDecimal("99.50"), issue, MATURITY));
|
||||
assertEquals(new BigDecimal("7.50"),
|
||||
InvestmentPurchaseWritePlatformServiceImpl.approxYtm(RATE, new BigDecimal("100"), issue, MATURITY));
|
||||
assertEquals(RATE, InvestmentPurchaseWritePlatformServiceImpl.approxYtm(RATE, new BigDecimal("99.50"), null, MATURITY));
|
||||
}
|
||||
|
||||
private static void assertRow(final ScheduleRow row, final String type, final LocalDate date, final LocalDate from, final int days,
|
||||
final String gross, final String alreadyBooked, final String amount) {
|
||||
assertEquals(type, row.type());
|
||||
assertEquals(date, row.date());
|
||||
assertEquals(from, row.fromDate());
|
||||
assertEquals(date, row.toDate());
|
||||
assertEquals(days, row.days());
|
||||
assertEquals(new BigDecimal(gross), row.grossInterest());
|
||||
assertEquals(new BigDecimal(alreadyBooked), row.alreadyBooked());
|
||||
assertEquals(new BigDecimal(amount), row.amount());
|
||||
}
|
||||
}
|
||||
+1
-5
@@ -233,11 +233,7 @@ public class InterestRateChart extends AbstractPersistableCustom<Long> {
|
||||
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
|
||||
.resource(INTERESTRATE_CHART_RESOURCE_NAME);
|
||||
|
||||
// A chart edited on its own (PUT /interestratecharts/{id}) has no sibling charts to check overlap against and
|
||||
// no product to read a currency from: new slabs take the currency of the slabs already on the chart.
|
||||
final String currencyCode = setOfChartSlabs().stream().map(slab -> slab.slabFields().getCurrencyCode())
|
||||
.filter(Objects::nonNull).findFirst().orElse(null);
|
||||
this.update(command, actualChanges, baseDataValidator, Set.of(), currencyCode);
|
||||
this.update(command, actualChanges, baseDataValidator, null, null);
|
||||
|
||||
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
||||
|
||||
|
||||
-4
@@ -66,10 +66,6 @@ public class InterestRateChartSlabFields {
|
||||
return this.amountRangeTo;
|
||||
}
|
||||
|
||||
public String getCurrencyCode() {
|
||||
return this.currencyCode;
|
||||
}
|
||||
|
||||
@Column(name = "amount_range_to", scale = 6, precision = 19)
|
||||
private BigDecimal amountRangeTo;
|
||||
|
||||
|
||||
-7
@@ -140,8 +140,6 @@ public final class RecurringDepositAccountData extends DepositAccountData {
|
||||
private Boolean prematureWithdrawalOption;
|
||||
private Boolean tenureFlexibility;
|
||||
private Boolean lienMarked;
|
||||
/** Template only: the product's Lien Marked loan limit (% of balance), the default and ceiling at opening. */
|
||||
private BigDecimal lienPercentage;
|
||||
private Boolean maturityInstructions;
|
||||
private Boolean creditMaturityProceeds;
|
||||
private Boolean penaltyGracePeriodMissedInstallments;
|
||||
@@ -175,10 +173,6 @@ public final class RecurringDepositAccountData extends DepositAccountData {
|
||||
|
||||
public void setJointHolders(java.util.List<java.util.Map<String, Object>> v) { this.jointHolders = v; }
|
||||
|
||||
public void setLienPercentage(final BigDecimal lienPercentage) {
|
||||
this.lienPercentage = lienPercentage;
|
||||
}
|
||||
|
||||
public void setCbsAccountFields(final String accountStatus, final String modeOfOperation,
|
||||
final Long introducerClientId, final String introducerName, final String introducerCifNumber,
|
||||
final String specialCategory, final String weakerSection,
|
||||
@@ -236,7 +230,6 @@ public final class RecurringDepositAccountData extends DepositAccountData {
|
||||
this.prematureWithdrawalOption = src.prematureWithdrawalOption;
|
||||
this.tenureFlexibility = src.tenureFlexibility;
|
||||
this.lienMarked = src.lienMarked;
|
||||
this.lienPercentage = src.lienPercentage;
|
||||
this.maturityInstructions = src.maturityInstructions;
|
||||
this.creditMaturityProceeds = src.creditMaturityProceeds;
|
||||
this.penaltyGracePeriodMissedInstallments = src.penaltyGracePeriodMissedInstallments;
|
||||
|
||||
+2
-22
@@ -90,10 +90,6 @@ public final class RecurringDepositProductData extends DepositProductData {
|
||||
private Boolean prematureWithdrawalOption;
|
||||
private Boolean tenureFlexibility;
|
||||
private Boolean lienMarked;
|
||||
private BigDecimal lienPercentage;
|
||||
private Integer missedInstallmentGraceDays;
|
||||
private Integer missedInstallmentPenaltyType;
|
||||
private BigDecimal missedInstallmentPenaltyAmount;
|
||||
private Boolean maturityInstructions;
|
||||
private Boolean creditMaturityProceeds;
|
||||
private Boolean penaltyGracePeriodMissedInstallments;
|
||||
@@ -137,10 +133,6 @@ public final class RecurringDepositProductData extends DepositProductData {
|
||||
public void setPrematureWithdrawalOption(Boolean v) { this.prematureWithdrawalOption = v; }
|
||||
public void setTenureFlexibility(Boolean v) { this.tenureFlexibility = v; }
|
||||
public void setLienMarked(Boolean v) { this.lienMarked = v; }
|
||||
public void setLienPercentage(BigDecimal v) { this.lienPercentage = v; }
|
||||
public void setMissedInstallmentGraceDays(Integer v) { this.missedInstallmentGraceDays = v; }
|
||||
public void setMissedInstallmentPenaltyType(Integer v) { this.missedInstallmentPenaltyType = v; }
|
||||
public void setMissedInstallmentPenaltyAmount(BigDecimal v) { this.missedInstallmentPenaltyAmount = v; }
|
||||
public void setMaturityInstructions(Boolean v) { this.maturityInstructions = v; }
|
||||
public void setCreditMaturityProceeds(Boolean v) { this.creditMaturityProceeds = v; }
|
||||
public void setPenaltyGracePeriodMissedInstallments(Boolean v) { this.penaltyGracePeriodMissedInstallments = v; }
|
||||
@@ -154,8 +146,6 @@ public final class RecurringDepositProductData extends DepositProductData {
|
||||
/** Copy all CBS custom fields from source into this instance. */
|
||||
public void copyCbsFieldsFrom(final RecurringDepositProductData src) {
|
||||
if (src == null) return;
|
||||
this.setOtherGlMappings(src.getOtherGlMappings());
|
||||
this.setOtherGlMappingsMap(src.getOtherGlMappingsMap());
|
||||
this.accountType = src.accountType;
|
||||
this.customerSegment = src.customerSegment;
|
||||
this.productCode = src.productCode;
|
||||
@@ -184,10 +174,6 @@ public final class RecurringDepositProductData extends DepositProductData {
|
||||
this.prematureWithdrawalOption = src.prematureWithdrawalOption;
|
||||
this.tenureFlexibility = src.tenureFlexibility;
|
||||
this.lienMarked = src.lienMarked;
|
||||
this.lienPercentage = src.lienPercentage;
|
||||
this.missedInstallmentGraceDays = src.missedInstallmentGraceDays;
|
||||
this.missedInstallmentPenaltyType = src.missedInstallmentPenaltyType;
|
||||
this.missedInstallmentPenaltyAmount = src.missedInstallmentPenaltyAmount;
|
||||
this.maturityInstructions = src.maturityInstructions;
|
||||
this.creditMaturityProceeds = src.creditMaturityProceeds;
|
||||
this.penaltyGracePeriodMissedInstallments = src.penaltyGracePeriodMissedInstallments;
|
||||
@@ -419,9 +405,8 @@ public final class RecurringDepositProductData extends DepositProductData {
|
||||
final Collection<EnumOptionData> periodFrequencyTypeOptions = null;
|
||||
final Collection<TaxGroupData> taxGroupOptions = null;
|
||||
|
||||
final RecurringDepositProductData result = new RecurringDepositProductData(depositProductData.id, depositProductData.name,
|
||||
depositProductData.shortName, depositProductData.description, depositProductData.currency,
|
||||
depositProductData.nominalAnnualInterestRate,
|
||||
return new RecurringDepositProductData(depositProductData.id, depositProductData.name, depositProductData.shortName,
|
||||
depositProductData.description, depositProductData.currency, depositProductData.nominalAnnualInterestRate,
|
||||
depositProductData.interestCompoundingPeriodType, depositProductData.interestPostingPeriodType,
|
||||
depositProductData.interestCalculationType, depositProductData.interestCalculationDaysInYearType,
|
||||
depositProductData.lockinPeriodFrequency, depositProductData.lockinPeriodFrequencyType,
|
||||
@@ -435,11 +420,6 @@ public final class RecurringDepositProductData extends DepositProductData {
|
||||
inMultiplesOfDepositTermType, isMandatoryDeposit, allowWithdrawal, adjustAdvanceTowardsFuturePayments,
|
||||
periodFrequencyTypeOptions, minDepositAmount, depositAmount, maxDepositAmount, depositProductData.withHoldTax,
|
||||
depositProductData.taxGroup, taxGroupOptions);
|
||||
// The product's named GL mappings (e.g. "Recurring Deposit Interest Paid"), as FixedDepositProductData carries them.
|
||||
// Without them the edit screen showed those rows blank and saving it dropped the mapping.
|
||||
result.setOtherGlMappings(depositProductData.getOtherGlMappings());
|
||||
result.setOtherGlMappingsMap(depositProductData.getOtherGlMappingsMap());
|
||||
return result;
|
||||
}
|
||||
|
||||
public static RecurringDepositProductData lookup(final Long id, final String name) {
|
||||
|
||||
-17
@@ -250,18 +250,6 @@ public class DepositAccountTermAndPreClosure extends AbstractPersistableCustom<L
|
||||
}
|
||||
}
|
||||
|
||||
/** The premature-closure penalty set to exactly the product's: applicable or not, the percentage and its period. */
|
||||
public void applyProductPreClosureDetail(final DepositPreClosureDetail productDetail) {
|
||||
if (productDetail == null) {
|
||||
return;
|
||||
}
|
||||
this.preClosureDetail = DepositPreClosureDetail.createFrom(productDetail.preClosurePenalApplicable(),
|
||||
productDetail.preClosurePenalApplicable() ? productDetail.preClosurePenalInterest() : null,
|
||||
productDetail.preClosurePenalApplicable() && productDetail.preClosurePenalInterestOnTypeId() != null
|
||||
? productDetail.preClosurePenalInterestOnType()
|
||||
: null);
|
||||
}
|
||||
|
||||
public DepositPreClosureDetail depositPreClosureDetail() {
|
||||
if (this.preClosureDetail == null) {
|
||||
this.preClosureDetail = new DepositPreClosureDetail();
|
||||
@@ -284,11 +272,6 @@ public class DepositAccountTermAndPreClosure extends AbstractPersistableCustom<L
|
||||
return this.depositPeriod;
|
||||
}
|
||||
|
||||
/** A running deposit's term lengthened (Tenure Flexibility); the period stays in its existing unit. */
|
||||
public void extendDepositPeriod(final Integer newDepositPeriod) {
|
||||
this.depositPeriod = newDepositPeriod;
|
||||
}
|
||||
|
||||
public Integer depositPeriodFrequency() {
|
||||
return this.depositPeriodFrequency;
|
||||
}
|
||||
|
||||
-13
@@ -393,19 +393,6 @@ public class DepositProductAssembler {
|
||||
recurringDepositProduct.setPrematureWithdrawalOption(command.booleanPrimitiveValueOfParameterNamed("prematureWithdrawalOption"));
|
||||
recurringDepositProduct.setTenureFlexibility(command.booleanPrimitiveValueOfParameterNamed("tenureFlexibility"));
|
||||
recurringDepositProduct.setLienMarked(command.booleanPrimitiveValueOfParameterNamed("lienMarked"));
|
||||
if (command.parameterExists("lienPercentage")) {
|
||||
recurringDepositProduct.setLienPercentage(command.bigDecimalValueOfParameterNamed("lienPercentage"));
|
||||
}
|
||||
if (command.parameterExists("missedInstallmentGraceDays")) {
|
||||
recurringDepositProduct.setMissedInstallmentGraceDays(command.integerValueOfParameterNamed("missedInstallmentGraceDays"));
|
||||
}
|
||||
if (command.parameterExists("missedInstallmentPenaltyType")) {
|
||||
recurringDepositProduct.setMissedInstallmentPenaltyType(command.integerValueOfParameterNamed("missedInstallmentPenaltyType"));
|
||||
}
|
||||
if (command.parameterExists("missedInstallmentPenaltyAmount")) {
|
||||
recurringDepositProduct
|
||||
.setMissedInstallmentPenaltyAmount(command.bigDecimalValueOfParameterNamed("missedInstallmentPenaltyAmount"));
|
||||
}
|
||||
recurringDepositProduct.setMaturityInstructions(command.booleanPrimitiveValueOfParameterNamed("maturityInstructions"));
|
||||
recurringDepositProduct.setCreditMaturityProceeds(command.booleanPrimitiveValueOfParameterNamed("creditMaturityProceeds"));
|
||||
recurringDepositProduct.setPenaltyGracePeriodMissedInstallments(
|
||||
|
||||
-5
@@ -70,11 +70,6 @@ public class DepositProductTermAndPreClosure extends AbstractPersistableCustom<L
|
||||
actualChanges.putAll(this.preClosureDetail.update(command, baseDataValidator));
|
||||
}
|
||||
|
||||
// A product created without any deposit term has no embedded term detail at all (every column null), so an
|
||||
// edit that sets one - e.g. a minimum term - would otherwise be dropped silently.
|
||||
if (this.depositTermDetail == null && (command.hasParameter("minDepositTerm") || command.hasParameter("maxDepositTerm"))) {
|
||||
this.depositTermDetail = DepositTermDetail.createFrom(null, null, null, null, null, null);
|
||||
}
|
||||
if (this.depositTermDetail != null) {
|
||||
actualChanges.putAll(this.depositTermDetail.update(command, baseDataValidator));
|
||||
}
|
||||
|
||||
-20
@@ -289,22 +289,6 @@ public final class ProductPendingEditFieldMapper {
|
||||
f.setLienMarked(command.booleanObjectValueOfParameterNamed("lienMarked"));
|
||||
present.add("lienMarked");
|
||||
}
|
||||
if (command.hasParameter("lienPercentage")) {
|
||||
f.setLienPercentage(command.bigDecimalValueOfParameterNamed("lienPercentage"));
|
||||
present.add("lienPercentage");
|
||||
}
|
||||
if (command.hasParameter("missedInstallmentGraceDays")) {
|
||||
f.setMissedInstallmentGraceDays(command.integerValueOfParameterNamed("missedInstallmentGraceDays"));
|
||||
present.add("missedInstallmentGraceDays");
|
||||
}
|
||||
if (command.hasParameter("missedInstallmentPenaltyType")) {
|
||||
f.setMissedInstallmentPenaltyType(command.integerValueOfParameterNamed("missedInstallmentPenaltyType"));
|
||||
present.add("missedInstallmentPenaltyType");
|
||||
}
|
||||
if (command.hasParameter("missedInstallmentPenaltyAmount")) {
|
||||
f.setMissedInstallmentPenaltyAmount(command.bigDecimalValueOfParameterNamed("missedInstallmentPenaltyAmount"));
|
||||
present.add("missedInstallmentPenaltyAmount");
|
||||
}
|
||||
if (command.hasParameter("maturityInstructions")) {
|
||||
f.setMaturityInstructions(command.booleanObjectValueOfParameterNamed("maturityInstructions"));
|
||||
present.add("maturityInstructions");
|
||||
@@ -410,10 +394,6 @@ public final class ProductPendingEditFieldMapper {
|
||||
putBoolean(root, submitted, "prematureWithdrawalOption", f.getPrematureWithdrawalOption());
|
||||
putBoolean(root, submitted, "tenureFlexibility", f.getTenureFlexibility());
|
||||
putBoolean(root, submitted, "lienMarked", f.getLienMarked());
|
||||
putNumber(root, submitted, "lienPercentage", f.getLienPercentage());
|
||||
putNumber(root, submitted, "missedInstallmentGraceDays", f.getMissedInstallmentGraceDays());
|
||||
putNumber(root, submitted, "missedInstallmentPenaltyType", f.getMissedInstallmentPenaltyType());
|
||||
putNumber(root, submitted, "missedInstallmentPenaltyAmount", f.getMissedInstallmentPenaltyAmount());
|
||||
putBoolean(root, submitted, "maturityInstructions", f.getMaturityInstructions());
|
||||
putBoolean(root, submitted, "creditMaturityProceeds", f.getCreditMaturityProceeds());
|
||||
putBoolean(root, submitted, "penaltyGracePeriodMissedInstallments", f.getPenaltyGracePeriodMissedInstallments());
|
||||
|
||||
+1
-34
@@ -1143,7 +1143,7 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
|
||||
}
|
||||
|
||||
this.savingsHelper.calculateInterestForAllPostingPeriods(this.currency, allPostingPeriods, getLockedInUntilDate(),
|
||||
isTransferInterestToOtherAccount(), isSimpleInterest());
|
||||
isTransferInterestToOtherAccount());
|
||||
|
||||
this.summary.updateFromInterestPeriodSummaries(this.currency, allPostingPeriods);
|
||||
}
|
||||
@@ -3876,23 +3876,6 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Whether earned interest must never compound. Only a recurring deposit opts in, from its product, when opened. */
|
||||
protected boolean isSimpleInterest() {
|
||||
return this.simpleInterest;
|
||||
}
|
||||
|
||||
public void setSimpleInterest(final boolean simpleInterest) {
|
||||
this.simpleInterest = simpleInterest;
|
||||
}
|
||||
|
||||
public BigDecimal getLienPercentage() {
|
||||
return this.lienPercentage;
|
||||
}
|
||||
|
||||
public void setLienPercentage(final BigDecimal lienPercentage) {
|
||||
this.lienPercentage = lienPercentage;
|
||||
}
|
||||
|
||||
public boolean accountSubmittedAndActivationOnSameDate() {
|
||||
return getActivationDate() != null && DateUtils.isEqual(getActivationDate(), getSubmittedOnDate());
|
||||
|
||||
@@ -4717,22 +4700,6 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
|
||||
@Column(name = "auto_debit_facility", nullable = false, columnDefinition = "boolean default false")
|
||||
private Boolean autoDebitFacility = false;
|
||||
|
||||
/**
|
||||
* Taken from the product's Interest Calculation Type when a recurring deposit is opened and kept for the account's
|
||||
* life, like its chart and compounding settings - a later product edit must not change what an open deposit was
|
||||
* promised.
|
||||
*/
|
||||
@Column(name = "is_simple_interest", nullable = false, columnDefinition = "boolean default false")
|
||||
private boolean simpleInterest = false;
|
||||
|
||||
/**
|
||||
* For a recurring deposit under a Lien Marked product: the most of it that may be given as loan, as a percentage
|
||||
* of its balance at the time it is pledged. Set when the deposit is opened (never above the product's percentage);
|
||||
* null falls back to the product's percentage.
|
||||
*/
|
||||
@Column(name = "lien_percentage", scale = 2, precision = 5)
|
||||
private BigDecimal lienPercentage;
|
||||
|
||||
@Column(name = "is_senior_citizen", nullable = false, columnDefinition = "boolean default false")
|
||||
private boolean isSeniorCitizen = false;
|
||||
|
||||
|
||||
-12
@@ -211,18 +211,6 @@ public class SavingsCoreEditFields {
|
||||
@Column(name = "lien_marked")
|
||||
private Boolean lienMarked;
|
||||
|
||||
@Column(name = "lien_percentage")
|
||||
private BigDecimal lienPercentage;
|
||||
|
||||
@Column(name = "missed_installment_grace_days")
|
||||
private Integer missedInstallmentGraceDays;
|
||||
|
||||
@Column(name = "missed_installment_penalty_type")
|
||||
private Integer missedInstallmentPenaltyType;
|
||||
|
||||
@Column(name = "missed_installment_penalty_amount")
|
||||
private BigDecimal missedInstallmentPenaltyAmount;
|
||||
|
||||
@Column(name = "maturity_instructions")
|
||||
private Boolean maturityInstructions;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user