Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae69d126a9 | ||
|
|
e4a2c00d75 | ||
|
|
a8f8aa39b7 |
-192
@@ -95,198 +95,6 @@ public class CommandWrapperBuilder {
|
|||||||
return this;
|
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 settleInvestmentPurchase(final Long purchaseId) {
|
|
||||||
this.actionName = "SETTLE";
|
|
||||||
this.entityName = "INVESTMENTPURCHASE";
|
|
||||||
this.entityId = purchaseId;
|
|
||||||
this.href = "/investment/purchases/" + purchaseId + "/settle";
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder createInvestmentSale() {
|
|
||||||
this.actionName = "CREATE";
|
|
||||||
this.entityName = "INVESTMENTSALE";
|
|
||||||
this.entityId = null;
|
|
||||||
this.href = "/investment/sales";
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder settleInvestmentSale(final Long saleId) {
|
|
||||||
this.actionName = "SETTLE";
|
|
||||||
this.entityName = "INVESTMENTSALE";
|
|
||||||
this.entityId = saleId;
|
|
||||||
this.href = "/investment/sales/" + saleId + "/settle";
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder deleteInvestmentSale(final Long saleId) {
|
|
||||||
this.actionName = "DELETE";
|
|
||||||
this.entityName = "INVESTMENTSALE";
|
|
||||||
this.entityId = saleId;
|
|
||||||
this.href = "/investment/sales/" + saleId;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder createInvestmentFd() {
|
|
||||||
this.actionName = "CREATE";
|
|
||||||
this.entityName = "INVESTMENTFD";
|
|
||||||
this.entityId = null;
|
|
||||||
this.href = "/investment/fds";
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder matureInvestmentFd(final Long fdId) {
|
|
||||||
this.actionName = "MATURE";
|
|
||||||
this.entityName = "INVESTMENTFD";
|
|
||||||
this.entityId = fdId;
|
|
||||||
this.href = "/investment/fds/" + fdId + "/mature";
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder prematureInvestmentFd(final Long fdId) {
|
|
||||||
this.actionName = "PREMATURE";
|
|
||||||
this.entityName = "INVESTMENTFD";
|
|
||||||
this.entityId = fdId;
|
|
||||||
this.href = "/investment/fds/" + fdId + "/premature";
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder renewInvestmentFd(final Long fdId) {
|
|
||||||
this.actionName = "RENEW";
|
|
||||||
this.entityName = "INVESTMENTFD";
|
|
||||||
this.entityId = fdId;
|
|
||||||
this.href = "/investment/fds/" + fdId + "/renew";
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder postInvestmentFdInterest(final Long fdId) {
|
|
||||||
this.actionName = "POSTINTEREST";
|
|
||||||
this.entityName = "INVESTMENTFD";
|
|
||||||
this.entityId = fdId;
|
|
||||||
this.href = "/investment/fds/" + fdId + "/interest";
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder setInvestmentDepositBase() {
|
|
||||||
this.actionName = "SETBASE";
|
|
||||||
this.entityName = "INVESTMENTFD";
|
|
||||||
this.entityId = null;
|
|
||||||
this.href = "/investment/fds/deposit-base";
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder amortiseInvestmentPremium() {
|
|
||||||
this.actionName = "AMORTISE";
|
|
||||||
this.entityName = "INVESTMENTSALE";
|
|
||||||
this.entityId = null;
|
|
||||||
this.href = "/investment/amortisation";
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder writeOffInvestmentPurchase(final Long purchaseId) {
|
|
||||||
this.actionName = "WRITEOFF";
|
|
||||||
this.entityName = "INVESTMENTSALE";
|
|
||||||
this.entityId = purchaseId;
|
|
||||||
this.href = "/investment/purchases/" + purchaseId + "/write-off";
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder redeemInvestmentPurchase(final Long purchaseId) {
|
|
||||||
this.actionName = "REDEEM";
|
|
||||||
this.entityName = "INVESTMENTSALE";
|
|
||||||
this.entityId = purchaseId;
|
|
||||||
this.href = "/investment/purchases/" + purchaseId + "/redeem";
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder transferInvestmentSecurity(final Long purchaseId) {
|
|
||||||
this.actionName = "CREATE";
|
|
||||||
this.entityName = "INVESTMENTTRANSFER";
|
|
||||||
this.entityId = purchaseId;
|
|
||||||
this.href = "/investment/purchases/" + purchaseId + "/transfer";
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder postInvestmentInterest(final Long purchaseId) {
|
|
||||||
this.actionName = "CREATE";
|
|
||||||
this.entityName = "INVESTMENTINTEREST";
|
|
||||||
this.entityId = purchaseId;
|
|
||||||
this.href = "/investment/purchases/" + purchaseId + "/interest-postings";
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder createInvestmentDailyRate() {
|
|
||||||
this.actionName = "CREATE";
|
|
||||||
this.entityName = "INVESTMENTDAILYRATE";
|
|
||||||
this.entityId = null;
|
|
||||||
this.href = "/investment/daily-rates";
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder deleteInvestmentDailyRate(final Long rateId) {
|
|
||||||
this.actionName = "DELETE";
|
|
||||||
this.entityName = "INVESTMENTDAILYRATE";
|
|
||||||
this.entityId = rateId;
|
|
||||||
this.href = "/investment/daily-rates/" + rateId;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder createChequeStockLot() {
|
public CommandWrapperBuilder createChequeStockLot() {
|
||||||
this.actionName = "CREATE";
|
this.actionName = "CREATE";
|
||||||
this.entityName = "CHEQUESTOCKLOT";
|
this.entityName = "CHEQUESTOCKLOT";
|
||||||
|
|||||||
+5
-1
@@ -21,7 +21,11 @@ package org.apache.fineract.portfolio.aml.domain;
|
|||||||
/** Which transaction type(s) an {@link AmlTransactionRule} evaluates against. */
|
/** Which transaction type(s) an {@link AmlTransactionRule} evaluates against. */
|
||||||
public enum AmlAppliesTo {
|
public enum AmlAppliesTo {
|
||||||
|
|
||||||
SAVINGS, LOAN, TRANSFER, ALL;
|
/**
|
||||||
|
* CURRENT is a current / Cash Credit account. The engine is not yet fed current-account transactions, so a rule written
|
||||||
|
* for it is catalogued but does not fire until it is.
|
||||||
|
*/
|
||||||
|
SAVINGS, LOAN, TRANSFER, CURRENT, ALL;
|
||||||
|
|
||||||
public boolean matches(AmlTransactionType transactionType) {
|
public boolean matches(AmlTransactionType transactionType) {
|
||||||
return this == ALL || this.name().equals(transactionType.name());
|
return this == ALL || this.name().equals(transactionType.name());
|
||||||
|
|||||||
+9
@@ -152,6 +152,15 @@ public class AmlTransactionRule {
|
|||||||
return rule;
|
return rule;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Narrows a rule to one direction - CREDIT (money in) or DEBIT (money out), null for either - and, when asked, to cash
|
||||||
|
* only. The engine already honours both; a rule written without them applies to every credit and debit, cash or not.
|
||||||
|
*/
|
||||||
|
public void restrictTo(final String direction, final boolean cashOnly) {
|
||||||
|
this.direction = direction;
|
||||||
|
this.cashOnly = cashOnly;
|
||||||
|
}
|
||||||
|
|
||||||
public Long getId() {
|
public Long getId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|||||||
+35
@@ -27,6 +27,7 @@ import jakarta.persistence.JoinColumn;
|
|||||||
import jakarta.persistence.ManyToOne;
|
import jakarta.persistence.ManyToOne;
|
||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
|
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@@ -118,6 +119,16 @@ public class ClientRiskRule {
|
|||||||
@Column(name = "cc_relevant", nullable = false)
|
@Column(name = "cc_relevant", nullable = false)
|
||||||
private boolean ccRelevant;
|
private boolean ccRelevant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A rule authored from the Rules Master starts on trial: it is listed first and marked new until an officer marks it
|
||||||
|
* normal. Every rule that was already in the catalog is normal.
|
||||||
|
*/
|
||||||
|
@Column(name = "on_trial", nullable = false)
|
||||||
|
private boolean onTrial;
|
||||||
|
|
||||||
|
@Column(name = "trial_started_on")
|
||||||
|
private LocalDate trialStartedOn;
|
||||||
|
|
||||||
public static final String KIND_KYC = "KYC";
|
public static final String KIND_KYC = "KYC";
|
||||||
public static final String KIND_AML = "AML";
|
public static final String KIND_AML = "AML";
|
||||||
|
|
||||||
@@ -127,6 +138,30 @@ public class ClientRiskRule {
|
|||||||
return ccRelevant;
|
return ccRelevant;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isOnTrial() {
|
||||||
|
return onTrial;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDate getTrialStartedOn() {
|
||||||
|
return trialStartedOn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Puts a newly authored rule on trial from {@code startedOn}. */
|
||||||
|
public void startTrial(final LocalDate startedOn) {
|
||||||
|
this.onTrial = true;
|
||||||
|
this.trialStartedOn = startedOn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ends the trial: the rule is a normal part of the catalog. */
|
||||||
|
public void markNormal() {
|
||||||
|
this.onTrial = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Marks the scenario as one that watches Cash Credit accounts, so the Rules Master CC chip lists it. */
|
||||||
|
public void markCcRelevant(final boolean ccRelevant) {
|
||||||
|
this.ccRelevant = ccRelevant;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds a rule authored from the AML Rules Master screen. Occupation and occupation-category
|
* Builds a rule authored from the AML Rules Master screen. Occupation and occupation-category
|
||||||
* are attached separately by the caller, which owns the CodeValue lookup.
|
* are attached separately by the caller, which owns the CodeValue lookup.
|
||||||
|
|||||||
+4
@@ -42,4 +42,8 @@ public interface CurrentAccountReadPlatformService {
|
|||||||
|
|
||||||
List<Map<String, Object>> retrieveAll(Integer offset, Integer limit, String accountNo, String clientName,
|
List<Map<String, Object>> retrieveAll(Integer offset, Integer limit, String accountNo, String clientName,
|
||||||
String status, LocalDate fromDate, LocalDate toDate, LocalDate asOnDate);
|
String status, LocalDate fromDate, LocalDate toDate, LocalDate asOnDate);
|
||||||
|
|
||||||
|
/** As above; with {@code overdraftOnly}, only the Cash Credit accounts - those that allow an overdraft. */
|
||||||
|
List<Map<String, Object>> retrieveAll(Integer offset, Integer limit, String accountNo, String clientName,
|
||||||
|
String status, LocalDate fromDate, LocalDate toDate, LocalDate asOnDate, boolean overdraftOnly);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,8 +81,6 @@ dependencies {
|
|||||||
'org.apache.avro:avro',
|
'org.apache.avro:avro',
|
||||||
'jakarta.ws.rs:jakarta.ws.rs-api',
|
'jakarta.ws.rs:jakarta.ws.rs-api',
|
||||||
'org.glassfish.jersey.media:jersey-media-multipart',
|
'org.glassfish.jersey.media:jersey-media-multipart',
|
||||||
// Server-Sent Events (SseEventSink, Sse): the Approvals badge is pushed to the screen over one.
|
|
||||||
'org.glassfish.jersey.media:jersey-media-sse',
|
|
||||||
|
|
||||||
'org.springframework:spring-jms',
|
'org.springframework:spring-jms',
|
||||||
'org.springframework:spring-context-support',
|
'org.springframework:spring-context-support',
|
||||||
|
|||||||
-67
@@ -1570,76 +1570,9 @@ public class AccountingProcessorHelper {
|
|||||||
return mapping == null ? null : mapping.getGlAccount();
|
return mapping == null ? null : mapping.getGlAccount();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The clearing account for the savings-to-savings account transfer running on this thread, taken from a product's
|
|
||||||
* own Transfers In Suspense mapping (see {@link #useProductTransferSuspense}). Null outside such a transfer.
|
|
||||||
*/
|
|
||||||
private static final ThreadLocal<GLAccount> TRANSFER_CLEARING_ACCOUNT = new ThreadLocal<>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Makes both legs of a savings-to-savings account transfer (savings to RD by auto debit, savings to savings, a
|
|
||||||
* deposit paying out) clear through the Transfers In Suspense account set on the product's Accounting tab, rather
|
|
||||||
* than the organisation-wide Liability Transfer financial activity.
|
|
||||||
* <p>
|
|
||||||
* The receiving product's mapping is used first, then the paying product's: a savings product screen does not
|
|
||||||
* offer Transfers In Suspense, so for savings to RD it is the RD product's mapping. Both legs use the same account,
|
|
||||||
* so it nets to nil. Where neither product has one, nothing is set and the transfer clears through Liability
|
|
||||||
* Transfer as before.
|
|
||||||
* <p>
|
|
||||||
* Must be paired with {@link #clearProductTransferSuspense()} in a finally block.
|
|
||||||
*
|
|
||||||
* @return true when a product mapping was found and is now in use
|
|
||||||
*/
|
|
||||||
public boolean useProductTransferSuspense(final Long toSavingsProductId, final Long fromSavingsProductId) {
|
|
||||||
final int transfersSuspense = CashAccountsForSavings.TRANSFERS_SUSPENSE.getValue();
|
|
||||||
GLAccount account = optionalLinkedGLAccountForSavingsProduct(toSavingsProductId, transfersSuspense, null);
|
|
||||||
if (account == null) {
|
|
||||||
account = optionalLinkedGLAccountForSavingsProduct(fromSavingsProductId, transfersSuspense, null);
|
|
||||||
}
|
|
||||||
if (account == null) {
|
|
||||||
TRANSFER_CLEARING_ACCOUNT.remove();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
TRANSFER_CLEARING_ACCOUNT.set(account);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clearProductTransferSuspense() {
|
|
||||||
TRANSFER_CLEARING_ACCOUNT.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For undoing one leg of an account transfer: reverses through the clearing account that leg was actually posted
|
|
||||||
* to, whether a product's Transfers In Suspense or the Liability Transfer financial activity. A reversal works the
|
|
||||||
* accounts out again rather than copying the original rows, so without this a transfer posted to a product's
|
|
||||||
* suspense account would be reversed against Liability Transfer and leave both accounts open.
|
|
||||||
* <p>
|
|
||||||
* The clearing row is the credit of a withdrawal and the debit of a deposit (the largest, should a fee sit beside
|
|
||||||
* it). Nothing is set where the leg has no journal entries. Must be paired with
|
|
||||||
* {@link #clearProductTransferSuspense()} in a finally block.
|
|
||||||
*/
|
|
||||||
public void useClearingAccountPostedFor(final Long savingsTransactionId, final boolean isWithdrawal) {
|
|
||||||
TRANSFER_CLEARING_ACCOUNT.remove();
|
|
||||||
if (savingsTransactionId == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final List<Long> found = jdbcTemplate.queryForList("""
|
|
||||||
SELECT account_id FROM acc_gl_journal_entry
|
|
||||||
WHERE savings_transaction_id = ? AND type_enum = ? AND reversed = false
|
|
||||||
ORDER BY amount DESC, id
|
|
||||||
LIMIT 1
|
|
||||||
""", Long.class, savingsTransactionId, isWithdrawal ? JournalEntryType.CREDIT.getValue() : JournalEntryType.DEBIT.getValue());
|
|
||||||
if (!found.isEmpty()) {
|
|
||||||
glAccountRepository.findById(found.get(0)).ifPresent(TRANSFER_CLEARING_ACCOUNT::set);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private GLAccount getLinkedGLAccountForSavingsProduct(final Long savingsProductId, final int accountMappingTypeId,
|
private GLAccount getLinkedGLAccountForSavingsProduct(final Long savingsProductId, final int accountMappingTypeId,
|
||||||
final Long paymentTypeId) {
|
final Long paymentTypeId) {
|
||||||
GLAccount glAccount;
|
GLAccount glAccount;
|
||||||
if (accountMappingTypeId == FinancialActivity.LIABILITY_TRANSFER.getValue() && TRANSFER_CLEARING_ACCOUNT.get() != null) {
|
|
||||||
return TRANSFER_CLEARING_ACCOUNT.get();
|
|
||||||
}
|
|
||||||
if (isOrganizationAccount(accountMappingTypeId)) {
|
if (isOrganizationAccount(accountMappingTypeId)) {
|
||||||
FinancialActivityAccount financialActivityAccount = this.financialActivityAccountRepository
|
FinancialActivityAccount financialActivityAccount = this.financialActivityAccountRepository
|
||||||
.findByFinancialActivityTypeWithNotFoundDetection(accountMappingTypeId);
|
.findByFinancialActivityTypeWithNotFoundDetection(accountMappingTypeId);
|
||||||
|
|||||||
+1
-1
@@ -92,7 +92,7 @@ public class TenantMenuAccessApiResource {
|
|||||||
"MENU_CASHIER", "MENU_CLERK", "MENU_BRANCH_OPS", "MENU_CLEARING", "MENU_DEPOSITS", "MENU_ADVANCES",
|
"MENU_CASHIER", "MENU_CLERK", "MENU_BRANCH_OPS", "MENU_CLEARING", "MENU_DEPOSITS", "MENU_ADVANCES",
|
||||||
"MENU_LOAN_ORIGINATION", "MENU_SHARE", "MENU_NPA", "MENU_GENERAL_LEDGER", "MENU_SUB_LEDGER", "MENU_ACCOUNTING",
|
"MENU_LOAN_ORIGINATION", "MENU_SHARE", "MENU_NPA", "MENU_GENERAL_LEDGER", "MENU_SUB_LEDGER", "MENU_ACCOUNTING",
|
||||||
"MENU_CKYC_MGMT", "MENU_INSURANCE", "MENU_DIGITAL", "MENU_HRM", "MENU_ADMIN", "MENU_RBI", "MENU_INVESTMENT",
|
"MENU_CKYC_MGMT", "MENU_INSURANCE", "MENU_DIGITAL", "MENU_HRM", "MENU_ADMIN", "MENU_RBI", "MENU_INVESTMENT",
|
||||||
"MENU_REPORTS", "MENU_OTHERS", "MENU_CONFIG_WIZARD", "MENU_CHEQUEBOOK", "MENU_LETTER_MGMT");
|
"MENU_REPORTS", "MENU_OTHERS", "MENU_CONFIG_WIZARD", "MENU_CHEQUEBOOK", "MENU_LETTER_MGMT", "MENU_CREDIT_BUREAU");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This tenant's own menu-access list, for the tenant named in {@code Fineract-Platform-TenantId}.
|
* This tenant's own menu-access list, for the tenant named in {@code Fineract-Platform-TenantId}.
|
||||||
|
|||||||
+13
-43
@@ -144,22 +144,14 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
|||||||
|
|
||||||
final SavingsTransactionBooleanValues transactionBooleanValues = new SavingsTransactionBooleanValues(isAccountTransfer,
|
final SavingsTransactionBooleanValues transactionBooleanValues = new SavingsTransactionBooleanValues(isAccountTransfer,
|
||||||
isRegularTransaction, fromSavingsAccount.isWithdrawalFeeApplicableForTransfer(), isInterestTransfer, isWithdrawBalance);
|
isRegularTransaction, fromSavingsAccount.isWithdrawalFeeApplicableForTransfer(), isInterestTransfer, isWithdrawBalance);
|
||||||
|
final SavingsAccountTransaction withdrawal = this.savingsAccountDomainService.handleWithdrawal(fromSavingsAccount, fmt,
|
||||||
|
transactionDate, transactionAmount, paymentDetail, transactionBooleanValues, backdatedTxnsAllowedTill);
|
||||||
|
|
||||||
final Long toSavingsId = command.longValueOfParameterNamed(toAccountIdParamName);
|
final Long toSavingsId = command.longValueOfParameterNamed(toAccountIdParamName);
|
||||||
final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsId, backdatedTxnsAllowedTill);
|
final SavingsAccount toSavingsAccount = this.savingsAccountAssembler.assembleFrom(toSavingsId, backdatedTxnsAllowedTill);
|
||||||
|
|
||||||
// As in transferFunds: both legs clear through the product's Transfers In Suspense where one is mapped.
|
final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount, fmt, transactionDate,
|
||||||
final SavingsAccountTransaction withdrawal;
|
transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill);
|
||||||
final SavingsAccountTransaction deposit;
|
|
||||||
this.accountingProcessorHelper.useProductTransferSuspense(toSavingsAccount.productId(), fromSavingsAccount.productId());
|
|
||||||
try {
|
|
||||||
withdrawal = this.savingsAccountDomainService.handleWithdrawal(fromSavingsAccount, fmt, transactionDate, transactionAmount,
|
|
||||||
paymentDetail, transactionBooleanValues, backdatedTxnsAllowedTill);
|
|
||||||
deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount, fmt, transactionDate, transactionAmount,
|
|
||||||
paymentDetail, isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill);
|
|
||||||
} finally {
|
|
||||||
this.accountingProcessorHelper.clearProductTransferSuspense();
|
|
||||||
}
|
|
||||||
applyToRecurringDepositSchedule(toSavingsAccount, deposit);
|
applyToRecurringDepositSchedule(toSavingsAccount, deposit);
|
||||||
|
|
||||||
if (!fromSavingsAccount.getCurrency().getCode().equals(toSavingsAccount.getCurrency().getCode())) {
|
if (!fromSavingsAccount.getCurrency().getCode().equals(toSavingsAccount.getCurrency().getCode())) {
|
||||||
@@ -285,27 +277,14 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
|||||||
if (accountTransfer.getToLoanTransaction() != null) {
|
if (accountTransfer.getToLoanTransaction() != null) {
|
||||||
this.loanAccountDomainService.reverseTransfer(accountTransfer.getToLoanTransaction());
|
this.loanAccountDomainService.reverseTransfer(accountTransfer.getToLoanTransaction());
|
||||||
}
|
}
|
||||||
// Each leg is reversed through the clearing account it was posted to (a product's Transfers In Suspense or
|
|
||||||
// Liability Transfer), so the reversal clears that same account.
|
|
||||||
if (accountTransfer.getFromTransaction() != null) {
|
if (accountTransfer.getFromTransaction() != null) {
|
||||||
this.accountingProcessorHelper.useClearingAccountPostedFor(accountTransfer.getFromTransaction().getId(), true);
|
|
||||||
try {
|
|
||||||
this.savingsAccountWritePlatformService.undoTransaction(
|
this.savingsAccountWritePlatformService.undoTransaction(
|
||||||
accountTransfer.accountTransferDetails().fromSavingsAccount().getId(),
|
accountTransfer.accountTransferDetails().fromSavingsAccount().getId(), accountTransfer.getFromTransaction().getId(),
|
||||||
accountTransfer.getFromTransaction().getId(), true);
|
true);
|
||||||
} finally {
|
|
||||||
this.accountingProcessorHelper.clearProductTransferSuspense();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (accountTransfer.getToSavingsTransaction() != null) {
|
if (accountTransfer.getToSavingsTransaction() != null) {
|
||||||
this.accountingProcessorHelper.useClearingAccountPostedFor(accountTransfer.getToSavingsTransaction().getId(), false);
|
this.savingsAccountWritePlatformService.undoTransaction(accountTransfer.accountTransferDetails().toSavingsAccount().getId(),
|
||||||
try {
|
|
||||||
this.savingsAccountWritePlatformService.undoTransaction(
|
|
||||||
accountTransfer.accountTransferDetails().toSavingsAccount().getId(),
|
|
||||||
accountTransfer.getToSavingsTransaction().getId(), true);
|
accountTransfer.getToSavingsTransaction().getId(), true);
|
||||||
} finally {
|
|
||||||
this.accountingProcessorHelper.clearProductTransferSuspense();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
accountTransfer.reverse();
|
accountTransfer.reverse();
|
||||||
this.accountTransferRepository.save(accountTransfer);
|
this.accountTransferRepository.save(accountTransfer);
|
||||||
@@ -428,22 +407,13 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
|||||||
transactionDate = transactionDate.plusDays(1);
|
transactionDate = transactionDate.plusDays(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Both legs clear through the Transfers In Suspense account on the product's Accounting tab (the receiving
|
final SavingsAccountTransaction withdrawal = this.savingsAccountDomainService.handleWithdrawal(fromSavingsAccount,
|
||||||
// product's, else the paying one's) instead of the organisation-wide Liability Transfer mapping.
|
accountTransferDTO.getFmt(), transactionDate, accountTransferDTO.getTransactionAmount(),
|
||||||
final SavingsAccountTransaction withdrawal;
|
accountTransferDTO.getPaymentDetail(), transactionBooleanValues, backdatedTxnsAllowedTill);
|
||||||
final SavingsAccountTransaction deposit;
|
|
||||||
this.accountingProcessorHelper.useProductTransferSuspense(toSavingsAccount.productId(), fromSavingsAccount.productId());
|
|
||||||
try {
|
|
||||||
withdrawal = this.savingsAccountDomainService.handleWithdrawal(fromSavingsAccount, accountTransferDTO.getFmt(),
|
|
||||||
transactionDate, accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(),
|
|
||||||
transactionBooleanValues, backdatedTxnsAllowedTill);
|
|
||||||
|
|
||||||
deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount, accountTransferDTO.getFmt(), transactionDate,
|
final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount,
|
||||||
accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(), isAccountTransfer,
|
accountTransferDTO.getFmt(), transactionDate, accountTransferDTO.getTransactionAmount(),
|
||||||
isRegularTransaction, backdatedTxnsAllowedTill);
|
accountTransferDTO.getPaymentDetail(), isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill);
|
||||||
} finally {
|
|
||||||
this.accountingProcessorHelper.clearProductTransferSuspense();
|
|
||||||
}
|
|
||||||
applyToRecurringDepositSchedule(toSavingsAccount, deposit);
|
applyToRecurringDepositSchedule(toSavingsAccount, deposit);
|
||||||
|
|
||||||
accountTransferDetails = this.accountTransferAssembler.assembleSavingsToSavingsTransfer(accountTransferDTO, fromSavingsAccount,
|
accountTransferDetails = this.accountTransferAssembler.assembleSavingsToSavingsTransfer(accountTransferDTO, fromSavingsAccount,
|
||||||
|
|||||||
+882
-13
@@ -22,17 +22,24 @@ import com.google.gson.Gson;
|
|||||||
import com.google.gson.GsonBuilder;
|
import com.google.gson.GsonBuilder;
|
||||||
import jakarta.ws.rs.DefaultValue;
|
import jakarta.ws.rs.DefaultValue;
|
||||||
import jakarta.ws.rs.GET;
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.NotFoundException;
|
||||||
import jakarta.ws.rs.Path;
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.PathParam;
|
||||||
import jakarta.ws.rs.Produces;
|
import jakarta.ws.rs.Produces;
|
||||||
import jakarta.ws.rs.QueryParam;
|
import jakarta.ws.rs.QueryParam;
|
||||||
import jakarta.ws.rs.core.MediaType;
|
import jakarta.ws.rs.core.MediaType;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
|
import java.time.DayOfWeek;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.time.temporal.TemporalAdjusters;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.TreeMap;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
@@ -41,13 +48,19 @@ import org.springframework.stereotype.Component;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* How much came into, and went out of, each Cash Credit account over a window, set against what the customer declared
|
* How much came into, and went out of, each Cash Credit account over a window, set against what the customer declared
|
||||||
* and what the bank sanctioned.
|
* and what the bank sanctioned - and, for one account, where it came from and went, by what mode, what that adds up to,
|
||||||
|
* and which of the bank's CC rules it hits and which it does not.
|
||||||
*
|
*
|
||||||
* <p>
|
* <p>
|
||||||
* Read straight from the posted current-account transactions, so it works today without the AML engine being fed those
|
* Read straight from the posted current-account transactions, so it works today without the AML engine being fed those
|
||||||
* transactions. A Cash Credit account is a current account allowed to draw an overdraft; every active current account
|
* transactions. A Cash Credit account is a current account allowed to draw an overdraft; only those are listed. Accounts are
|
||||||
* is listed, with that flag, so a plain current account is not hidden from view. Accounts are limited to the caller's
|
* limited to the caller's branch hierarchy, and the busiest are listed first.
|
||||||
* branch hierarchy, and the busiest are listed first.
|
*
|
||||||
|
* <p>
|
||||||
|
* The rule checks apply each CC rule's own logic to the account's own transactions here, the way the engine would if it were
|
||||||
|
* fed them. A rule that turns on an officer's judgement, or that the system does not yet check, is shown as not checked
|
||||||
|
* rather than as passed. The observations and the fixed values behind the CC scenarios are indicative starting values, not
|
||||||
|
* findings; the bank's compliance team sets the real thresholds.
|
||||||
*/
|
*/
|
||||||
@Path("/v1/aml/cc-tracking")
|
@Path("/v1/aml/cc-tracking")
|
||||||
@Component
|
@Component
|
||||||
@@ -55,6 +68,30 @@ import org.springframework.stereotype.Component;
|
|||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
public class AmlCcTrackingApiResource {
|
public class AmlCcTrackingApiResource {
|
||||||
|
|
||||||
|
// ── Starting values for the observations and the CC scenarios (indicative; to be set by the bank) ─────────────────
|
||||||
|
/** Credits and debits this many times the declared turnover (or the sanctioned limit) are flagged. */
|
||||||
|
private static final BigDecimal TURNOVER_MULTIPLE = BigDecimal.valueOf(3);
|
||||||
|
/** A credit is "rapid in-out" when this share of it is paid out again within {@link #RAPID_DAYS} days. */
|
||||||
|
private static final BigDecimal RAPID_SHARE = new BigDecimal("0.80");
|
||||||
|
private static final int RAPID_DAYS = 3;
|
||||||
|
/** One party handling this share of all the money moved is a concentration. */
|
||||||
|
private static final BigDecimal CONCENTRATION_SHARE = new BigDecimal("0.50");
|
||||||
|
/** Cash making up this share of credits or debits is a high cash share. */
|
||||||
|
private static final BigDecimal CASH_SHARE = new BigDecimal("0.50");
|
||||||
|
/** Peak use of the sanctioned limit worth noting, and the days within which a return to credit counts as "cleared". */
|
||||||
|
private static final BigDecimal LIMIT_USE = new BigDecimal("0.80");
|
||||||
|
private static final int CLEARED_DAYS = 7;
|
||||||
|
/** A party both paying in and being paid, the smaller side at least this share of the larger. */
|
||||||
|
private static final BigDecimal TWO_WAY_SHARE = new BigDecimal("0.50");
|
||||||
|
/** Money with no counterparty recorded, as a share of everything moved. */
|
||||||
|
private static final BigDecimal UNRECORDED_SHARE = new BigDecimal("0.50");
|
||||||
|
/** The window's turnover this many times the account's own earlier average is a spike. */
|
||||||
|
private static final BigDecimal SPIKE_MULTIPLE = BigDecimal.valueOf(3);
|
||||||
|
|
||||||
|
private static final String HIT = "HIT";
|
||||||
|
private static final String NO_HIT = "NO_HIT";
|
||||||
|
private static final String NOT_CHECKED = "NOT_CHECKED";
|
||||||
|
|
||||||
/** Current-account transaction types 1 and 2 are a deposit and a withdrawal, as on a savings account. */
|
/** Current-account transaction types 1 and 2 are a deposit and a withdrawal, as on a savings account. */
|
||||||
private static final String SQL = """
|
private static final String SQL = """
|
||||||
SELECT ca.id AS account_id, ca.account_no, c.id AS client_id, c.display_name AS client_name,
|
SELECT ca.id AS account_id, ca.account_no, c.id AS client_id, c.display_name AS client_name,
|
||||||
@@ -75,16 +112,40 @@ public class AmlCcTrackingApiResource {
|
|||||||
AND t.transaction_type_enum IN (1, 2) AND t.transaction_date > ? AND t.transaction_date <= ?
|
AND t.transaction_type_enum IN (1, 2) AND t.transaction_date > ? AND t.transaction_date <= ?
|
||||||
LEFT JOIN m_payment_detail pd ON pd.id = t.payment_detail_id
|
LEFT JOIN m_payment_detail pd ON pd.id = t.payment_detail_id
|
||||||
LEFT JOIN m_payment_type pt ON pt.id = pd.payment_type_id
|
LEFT JOIN m_payment_type pt ON pt.id = pd.payment_type_id
|
||||||
WHERE ca.status_enum = 300
|
WHERE ca.status_enum = 300 AND ca.allow_overdraft = true
|
||||||
AND COALESCE(ca.office_id, c.office_id) IN (SELECT id FROM m_office WHERE hierarchy LIKE ?)
|
AND COALESCE(ca.office_id, c.office_id) IN (SELECT id FROM m_office WHERE hierarchy LIKE ?)
|
||||||
GROUP BY ca.id, c.id, o.id, k.id
|
GROUP BY ca.id, c.id, o.id, k.id
|
||||||
ORDER BY SUM(t.amount) DESC NULLS LAST, ca.account_no
|
ORDER BY SUM(t.amount) DESC NULLS LAST, ca.account_no
|
||||||
""";
|
""";
|
||||||
|
|
||||||
|
private static final String TRAIL_SELECT = """
|
||||||
|
SELECT t.id, t.current_account_id, t.transaction_date, t.transaction_type_enum, t.amount, t.ref_no, t.remarks,
|
||||||
|
t.running_balance_derived, pt.value AS payment_type, COALESCE(pt.is_cash_payment, false) AS is_cash,
|
||||||
|
pd.account_number AS cp_account, pd.bank_number AS cp_bank, pd.routing_code AS cp_ifsc, pd.check_number AS cheque_no
|
||||||
|
FROM m_current_account_transaction t
|
||||||
|
LEFT JOIN m_payment_detail pd ON pd.id = t.payment_detail_id
|
||||||
|
LEFT JOIN m_payment_type pt ON pt.id = pd.payment_type_id
|
||||||
|
WHERE t.is_reversed = false AND t.transaction_type_enum IN (1, 2)
|
||||||
|
AND t.transaction_date > ? AND t.transaction_date <= ?
|
||||||
|
""";
|
||||||
|
|
||||||
|
/** The CC rules: every AML scenario marked as watching a CC account, with the live-transaction rule behind it, if any. */
|
||||||
|
private static final String RULES_SQL = """
|
||||||
|
SELECT r.id, r.rule_name, r.risk_category, r.coverage_status, r.on_trial,
|
||||||
|
t.rule_type, t.threshold_amount, t.min_limit_amount, t.max_limit_amount, t.window_days, t.direction,
|
||||||
|
t.cash_only, t.min_transaction_count, t.subject_scope, t.is_active
|
||||||
|
FROM m_client_risk_rule r
|
||||||
|
LEFT JOIN m_aml_transaction_rule t ON t.source_client_risk_rule_id = r.id
|
||||||
|
WHERE r.cc_relevant = true AND r.rule_kind = 'AML'
|
||||||
|
ORDER BY r.on_trial DESC, r.id
|
||||||
|
""";
|
||||||
|
|
||||||
private final PlatformSecurityContext context;
|
private final PlatformSecurityContext context;
|
||||||
private final JdbcTemplate jdbcTemplate;
|
private final JdbcTemplate jdbcTemplate;
|
||||||
private final Gson gson = new GsonBuilder().serializeNulls().create();
|
private final Gson gson = new GsonBuilder().serializeNulls().create();
|
||||||
|
|
||||||
|
// ── The list ─────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
public String retrieve(@QueryParam("days") @DefaultValue("30") final int days) {
|
public String retrieve(@QueryParam("days") @DefaultValue("30") final int days) {
|
||||||
final String hierarchy = this.context.authenticatedUser().getOffice().getHierarchy();
|
final String hierarchy = this.context.authenticatedUser().getOffice().getHierarchy();
|
||||||
@@ -92,22 +153,28 @@ public class AmlCcTrackingApiResource {
|
|||||||
final LocalDate asOf = DateUtils.getBusinessLocalDate();
|
final LocalDate asOf = DateUtils.getBusinessLocalDate();
|
||||||
final LocalDate from = asOf.minusDays(window);
|
final LocalDate from = asOf.minusDays(window);
|
||||||
|
|
||||||
|
final List<Map<String, Object>> accountRows = this.jdbcTemplate.queryForList(SQL, java.sql.Date.valueOf(from),
|
||||||
|
java.sql.Date.valueOf(asOf), hierarchy + "%");
|
||||||
|
|
||||||
|
// The transactions and earlier history of all the accounts at once, to check each against the CC rules
|
||||||
|
final Map<Long, List<Txn>> txnsByAccount = loadTxns(from, asOf, null);
|
||||||
|
final Map<Long, BigDecimal> priorVolume = priorVolumes(from, window);
|
||||||
|
final List<CcRule> rules = loadRules();
|
||||||
|
|
||||||
final List<Map<String, Object>> rows = new ArrayList<>();
|
final List<Map<String, Object>> rows = new ArrayList<>();
|
||||||
int rank = 0;
|
int rank = 0;
|
||||||
for (final Map<String, Object> r : this.jdbcTemplate.queryForList(SQL, java.sql.Date.valueOf(from),
|
for (final Map<String, Object> r : accountRows) {
|
||||||
java.sql.Date.valueOf(asOf), hierarchy + "%")) {
|
final long accountId = ((Number) r.get("account_id")).longValue();
|
||||||
final BigDecimal credits = money(r.get("credits"));
|
final BigDecimal credits = money(r.get("credits"));
|
||||||
final BigDecimal debits = money(r.get("debits"));
|
final BigDecimal debits = money(r.get("debits"));
|
||||||
final BigDecimal limit = money(r.get("overdraft_limit"));
|
final BigDecimal limit = money(r.get("overdraft_limit"));
|
||||||
final BigDecimal balance = money(r.get("account_balance_derived"));
|
final BigDecimal balance = money(r.get("account_balance_derived"));
|
||||||
final BigDecimal declaredMonthly = r.get("declared_monthly_turnover") == null ? null : money(r.get("declared_monthly_turnover"));
|
final BigDecimal declaredMonthly = r.get("declared_monthly_turnover") == null ? null : money(r.get("declared_monthly_turnover"));
|
||||||
// What the declared monthly turnover comes to over this window, so a 7-day look is not judged against a month.
|
final BigDecimal declaredForWindow = scaledToWindow(declaredMonthly, window);
|
||||||
final BigDecimal declaredForWindow = declaredMonthly == null ? null
|
|
||||||
: declaredMonthly.multiply(BigDecimal.valueOf(window)).divide(BigDecimal.valueOf(30), 2, RoundingMode.HALF_UP);
|
|
||||||
|
|
||||||
final Map<String, Object> row = new LinkedHashMap<>();
|
final Map<String, Object> row = new LinkedHashMap<>();
|
||||||
row.put("rank", ++rank);
|
row.put("rank", ++rank);
|
||||||
row.put("accountId", r.get("account_id"));
|
row.put("accountId", accountId);
|
||||||
row.put("accountNo", r.get("account_no"));
|
row.put("accountNo", r.get("account_no"));
|
||||||
row.put("clientId", r.get("client_id"));
|
row.put("clientId", r.get("client_id"));
|
||||||
row.put("clientName", r.get("client_name"));
|
row.put("clientName", r.get("client_name"));
|
||||||
@@ -115,7 +182,6 @@ public class AmlCcTrackingApiResource {
|
|||||||
row.put("isCc", Boolean.TRUE.equals(r.get("allow_overdraft")));
|
row.put("isCc", Boolean.TRUE.equals(r.get("allow_overdraft")));
|
||||||
row.put("sanctionedLimit", limit);
|
row.put("sanctionedLimit", limit);
|
||||||
row.put("balance", balance);
|
row.put("balance", balance);
|
||||||
// How much of the sanctioned limit is drawn: the overdrawn part of the balance over the limit.
|
|
||||||
row.put("limitDrawnPercent", limit.signum() > 0 && balance.signum() < 0
|
row.put("limitDrawnPercent", limit.signum() > 0 && balance.signum() < 0
|
||||||
? balance.negate().multiply(BigDecimal.valueOf(100)).divide(limit, 1, RoundingMode.HALF_UP) : null);
|
? balance.negate().multiply(BigDecimal.valueOf(100)).divide(limit, 1, RoundingMode.HALF_UP) : null);
|
||||||
row.put("declaredMonthlyTurnover", declaredMonthly);
|
row.put("declaredMonthlyTurnover", declaredMonthly);
|
||||||
@@ -128,10 +194,28 @@ public class AmlCcTrackingApiResource {
|
|||||||
row.put("cashDebits", money(r.get("cash_debits")));
|
row.put("cashDebits", money(r.get("cash_debits")));
|
||||||
row.put("cashCreditPercent", credits.signum() > 0
|
row.put("cashCreditPercent", credits.signum() > 0
|
||||||
? money(r.get("cash_credits")).multiply(BigDecimal.valueOf(100)).divide(credits, 1, RoundingMode.HALF_UP) : null);
|
? money(r.get("cash_credits")).multiply(BigDecimal.valueOf(100)).divide(credits, 1, RoundingMode.HALF_UP) : null);
|
||||||
// Credits and debits as a multiple of what the customer said the business turns over in this window.
|
|
||||||
row.put("creditsVsDeclared", ratio(credits, declaredForWindow));
|
row.put("creditsVsDeclared", ratio(credits, declaredForWindow));
|
||||||
row.put("debitsVsDeclared", ratio(debits, declaredForWindow));
|
row.put("debitsVsDeclared", ratio(debits, declaredForWindow));
|
||||||
row.put("lastTxnDate", r.get("last_txn_date") == null ? null : r.get("last_txn_date").toString());
|
row.put("lastTxnDate", r.get("last_txn_date") == null ? null : r.get("last_txn_date").toString());
|
||||||
|
|
||||||
|
final List<Map<String, Object>> checks = checkRules(rules, txnsByAccount.getOrDefault(accountId, List.of()), limit,
|
||||||
|
declaredForWindow, priorVolume.get(accountId), window);
|
||||||
|
int hit = 0;
|
||||||
|
int noHit = 0;
|
||||||
|
int notChecked = 0;
|
||||||
|
for (final Map<String, Object> c : checks) {
|
||||||
|
final String status = (String) c.get("status");
|
||||||
|
if (HIT.equals(status)) {
|
||||||
|
hit++;
|
||||||
|
} else if (NO_HIT.equals(status)) {
|
||||||
|
noHit++;
|
||||||
|
} else {
|
||||||
|
notChecked++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
row.put("rulesHit", hit);
|
||||||
|
row.put("rulesNoHit", noHit);
|
||||||
|
row.put("rulesNotChecked", notChecked);
|
||||||
rows.add(row);
|
rows.add(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,6 +227,791 @@ public class AmlCcTrackingApiResource {
|
|||||||
return this.gson.toJson(out);
|
return this.gson.toJson(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── One account ──────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every credit and debit on one CC account in the window, with the mode and the party on the other side where the
|
||||||
|
* payment recorded them, totalled by mode and by party, an analysis of what it adds up to, and the CC rules checked against
|
||||||
|
* it. The account's own branch must be at or below the caller's.
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Path("{accountId}/trail")
|
||||||
|
public String trail(@PathParam("accountId") final Long accountId, @QueryParam("days") @DefaultValue("30") final int days) {
|
||||||
|
final String hierarchy = this.context.authenticatedUser().getOffice().getHierarchy();
|
||||||
|
final List<Map<String, Object>> account = this.jdbcTemplate.queryForList("""
|
||||||
|
SELECT ca.id, ca.account_no, c.display_name AS client_name, o.name AS branch_name, ca.overdraft_limit,
|
||||||
|
ca.account_balance_derived, ca.allow_overdraft,
|
||||||
|
COALESCE(NULLIF(c.monthly_turnover, 0), NULLIF(k.declared_annual_turnover, 0) / 12) AS declared_monthly_turnover
|
||||||
|
FROM m_current_account ca
|
||||||
|
LEFT JOIN m_client c ON c.id = ca.client_id
|
||||||
|
LEFT JOIN m_client_kyc k ON k.client_id = c.id
|
||||||
|
LEFT JOIN m_office o ON o.id = COALESCE(ca.office_id, c.office_id)
|
||||||
|
WHERE ca.id = ? AND COALESCE(ca.office_id, c.office_id) IN (SELECT id FROM m_office WHERE hierarchy LIKE ?)
|
||||||
|
""", accountId, hierarchy + "%");
|
||||||
|
if (account.isEmpty()) {
|
||||||
|
throw new NotFoundException("Account not found: " + accountId);
|
||||||
|
}
|
||||||
|
final int window = Math.min(Math.max(days, 1), 366);
|
||||||
|
final LocalDate asOf = DateUtils.getBusinessLocalDate();
|
||||||
|
final LocalDate from = asOf.minusDays(window);
|
||||||
|
|
||||||
|
final List<Txn> txns = loadTxns(from, asOf, accountId).getOrDefault(accountId, List.of());
|
||||||
|
|
||||||
|
final Map<String, Object> a = account.get(0);
|
||||||
|
final BigDecimal limit = money(a.get("overdraft_limit"));
|
||||||
|
final BigDecimal balance = money(a.get("account_balance_derived"));
|
||||||
|
final BigDecimal declaredMonthly = a.get("declared_monthly_turnover") == null ? null : money(a.get("declared_monthly_turnover"));
|
||||||
|
final BigDecimal declaredForWindow = scaledToWindow(declaredMonthly, window);
|
||||||
|
|
||||||
|
// Newest first for the list, oldest first for the analysis
|
||||||
|
final List<Map<String, Object>> txnRows = new ArrayList<>();
|
||||||
|
for (int i = txns.size() - 1; i >= 0; i--) {
|
||||||
|
final Txn t = txns.get(i);
|
||||||
|
final Map<String, Object> row = new LinkedHashMap<>();
|
||||||
|
row.put("id", t.id);
|
||||||
|
row.put("date", t.date.toString());
|
||||||
|
row.put("direction", t.credit ? "IN" : "OUT");
|
||||||
|
row.put("amount", t.amount);
|
||||||
|
row.put("mode", t.mode);
|
||||||
|
row.put("isCash", t.cash);
|
||||||
|
row.put("reference", t.reference);
|
||||||
|
row.put("chequeNo", t.chequeNo);
|
||||||
|
row.put("counterpartyAccount", t.partyAccount);
|
||||||
|
row.put("counterpartyBank", t.partyBank);
|
||||||
|
row.put("counterpartyIfsc", t.partyIfsc);
|
||||||
|
row.put("remarks", t.remarks);
|
||||||
|
row.put("balanceAfter", t.balanceAfter);
|
||||||
|
txnRows.add(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<String, Object> out = new LinkedHashMap<>();
|
||||||
|
out.put("accountNo", a.get("account_no"));
|
||||||
|
out.put("clientName", a.get("client_name"));
|
||||||
|
out.put("branchName", a.get("branch_name"));
|
||||||
|
out.put("isCc", Boolean.TRUE.equals(a.get("allow_overdraft")));
|
||||||
|
out.put("sanctionedLimit", limit);
|
||||||
|
out.put("balance", balance);
|
||||||
|
out.put("declaredMonthlyTurnover", declaredMonthly);
|
||||||
|
out.put("from", from.toString());
|
||||||
|
out.put("asOf", asOf.toString());
|
||||||
|
out.put("days", window);
|
||||||
|
out.put("transactions", txnRows);
|
||||||
|
out.putAll(analyse(txns, limit, declaredForWindow));
|
||||||
|
out.put("ruleChecks", checkRules(loadRules(), txns, limit, declaredForWindow, priorVolumes(from, window).get(accountId), window));
|
||||||
|
return this.gson.toJson(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Loading ──────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** The window's transactions, oldest first, by account - for one account, or for every current account. */
|
||||||
|
private Map<Long, List<Txn>> loadTxns(final LocalDate from, final LocalDate asOf, final Long accountId) {
|
||||||
|
final String sql = TRAIL_SELECT + (accountId == null ? "" : " AND t.current_account_id = ?")
|
||||||
|
+ " ORDER BY t.transaction_date ASC, t.id ASC";
|
||||||
|
final List<Map<String, Object>> rows = accountId == null
|
||||||
|
? this.jdbcTemplate.queryForList(sql, java.sql.Date.valueOf(from), java.sql.Date.valueOf(asOf))
|
||||||
|
: this.jdbcTemplate.queryForList(sql, java.sql.Date.valueOf(from), java.sql.Date.valueOf(asOf), accountId);
|
||||||
|
final Map<Long, List<Txn>> byAccount = new HashMap<>();
|
||||||
|
for (final Map<String, Object> r : rows) {
|
||||||
|
final Txn t = new Txn();
|
||||||
|
t.id = ((Number) r.get("id")).longValue();
|
||||||
|
t.date = ((java.sql.Date) r.get("transaction_date")).toLocalDate();
|
||||||
|
t.credit = ((Number) r.get("transaction_type_enum")).intValue() == 1;
|
||||||
|
t.amount = money(r.get("amount"));
|
||||||
|
t.reference = (String) r.get("ref_no");
|
||||||
|
t.remarks = (String) r.get("remarks");
|
||||||
|
t.balanceAfter = r.get("running_balance_derived") == null ? null : money(r.get("running_balance_derived"));
|
||||||
|
t.cash = Boolean.TRUE.equals(r.get("is_cash"));
|
||||||
|
t.chequeNo = (String) r.get("cheque_no");
|
||||||
|
t.partyAccount = (String) r.get("cp_account");
|
||||||
|
t.partyBank = (String) r.get("cp_bank");
|
||||||
|
t.partyIfsc = (String) r.get("cp_ifsc");
|
||||||
|
t.mode = modeOf(t.cash, (String) r.get("payment_type"), t.chequeNo, t.remarks);
|
||||||
|
t.party = partyOf(t);
|
||||||
|
byAccount.computeIfAbsent(((Number) r.get("current_account_id")).longValue(), k -> new ArrayList<>()).add(t);
|
||||||
|
}
|
||||||
|
return byAccount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The average turnover (credits plus debits) of each account over the three windows before this one, where there is any. */
|
||||||
|
private Map<Long, BigDecimal> priorVolumes(final LocalDate from, final int window) {
|
||||||
|
final Map<Long, BigDecimal> result = new HashMap<>();
|
||||||
|
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList("""
|
||||||
|
SELECT t.current_account_id, SUM(t.amount) AS total
|
||||||
|
FROM m_current_account_transaction t
|
||||||
|
WHERE t.is_reversed = false AND t.transaction_type_enum IN (1, 2)
|
||||||
|
AND t.transaction_date > ? AND t.transaction_date <= ?
|
||||||
|
GROUP BY t.current_account_id
|
||||||
|
""", java.sql.Date.valueOf(from.minusDays(3L * window)), java.sql.Date.valueOf(from));
|
||||||
|
for (final Map<String, Object> r : rows) {
|
||||||
|
result.put(((Number) r.get("current_account_id")).longValue(),
|
||||||
|
money(r.get("total")).divide(BigDecimal.valueOf(3), 2, RoundingMode.HALF_UP));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── The CC rules ─────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** One CC rule: the catalog entry and, where there is one, the live-transaction rule behind it. */
|
||||||
|
private static final class CcRule {
|
||||||
|
long id;
|
||||||
|
String name;
|
||||||
|
int riskCategory;
|
||||||
|
String coverage;
|
||||||
|
boolean onTrial;
|
||||||
|
String ruleType;
|
||||||
|
BigDecimal threshold;
|
||||||
|
BigDecimal min;
|
||||||
|
BigDecimal max;
|
||||||
|
Integer windowDays;
|
||||||
|
String direction;
|
||||||
|
boolean cashOnly;
|
||||||
|
Integer minCount;
|
||||||
|
String subjectScope;
|
||||||
|
boolean active;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<CcRule> loadRules() {
|
||||||
|
final List<CcRule> rules = new ArrayList<>();
|
||||||
|
for (final Map<String, Object> r : this.jdbcTemplate.queryForList(RULES_SQL)) {
|
||||||
|
final CcRule rule = new CcRule();
|
||||||
|
rule.id = ((Number) r.get("id")).longValue();
|
||||||
|
rule.name = (String) r.get("rule_name");
|
||||||
|
rule.riskCategory = r.get("risk_category") == null ? 0 : ((Number) r.get("risk_category")).intValue();
|
||||||
|
rule.coverage = (String) r.get("coverage_status");
|
||||||
|
rule.onTrial = Boolean.TRUE.equals(r.get("on_trial"));
|
||||||
|
rule.ruleType = (String) r.get("rule_type");
|
||||||
|
rule.threshold = r.get("threshold_amount") == null ? null : money(r.get("threshold_amount"));
|
||||||
|
rule.min = r.get("min_limit_amount") == null ? null : money(r.get("min_limit_amount"));
|
||||||
|
rule.max = r.get("max_limit_amount") == null ? null : money(r.get("max_limit_amount"));
|
||||||
|
rule.windowDays = r.get("window_days") == null ? null : ((Number) r.get("window_days")).intValue();
|
||||||
|
rule.direction = (String) r.get("direction");
|
||||||
|
rule.cashOnly = Boolean.TRUE.equals(r.get("cash_only"));
|
||||||
|
rule.minCount = r.get("min_transaction_count") == null ? null : ((Number) r.get("min_transaction_count")).intValue();
|
||||||
|
rule.subjectScope = (String) r.get("subject_scope");
|
||||||
|
rule.active = r.get("is_active") == null || Boolean.TRUE.equals(r.get("is_active"));
|
||||||
|
rules.add(rule);
|
||||||
|
}
|
||||||
|
return rules;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Each CC rule against one account's transactions. A rule with a live-transaction rule behind it is checked the way the
|
||||||
|
* engine would check it; a CC scenario is checked by its own test; a rule that turns on an officer's judgement, or that the
|
||||||
|
* system does not yet check, is NOT_CHECKED - never reported as passed.
|
||||||
|
*/
|
||||||
|
private List<Map<String, Object>> checkRules(final List<CcRule> rules, final List<Txn> txns, final BigDecimal limit,
|
||||||
|
final BigDecimal declaredForWindow, final BigDecimal priorAverageVolume, final int window) {
|
||||||
|
final Facts f = facts(txns, limit);
|
||||||
|
final List<Map<String, Object>> out = new ArrayList<>();
|
||||||
|
for (final CcRule rule : rules) {
|
||||||
|
final String[] result;
|
||||||
|
final String kind;
|
||||||
|
if (rule.name != null && rule.name.startsWith("CC - ")) {
|
||||||
|
kind = "CC scenario";
|
||||||
|
result = scenario(rule, f, txns, limit, declaredForWindow, priorAverageVolume);
|
||||||
|
} else if (rule.ruleType != null) {
|
||||||
|
kind = "Transaction rule";
|
||||||
|
result = transactionRule(rule, txns, window);
|
||||||
|
} else if ("MANUAL_REVIEW".equals(rule.coverage)) {
|
||||||
|
kind = "Manual review";
|
||||||
|
result = new String[] { NOT_CHECKED, "Turns on an officer's judgement, so the system does not check it." };
|
||||||
|
} else {
|
||||||
|
kind = "Planned";
|
||||||
|
result = new String[] { NOT_CHECKED, "Planned: the system does not check this yet." };
|
||||||
|
}
|
||||||
|
final Map<String, Object> m = new LinkedHashMap<>();
|
||||||
|
m.put("ruleId", rule.id);
|
||||||
|
m.put("name", rule.name);
|
||||||
|
m.put("kind", kind);
|
||||||
|
m.put("riskCategory", rule.riskCategory);
|
||||||
|
m.put("onTrial", rule.onTrial);
|
||||||
|
m.put("status", result[0]);
|
||||||
|
m.put("detail", result[1]);
|
||||||
|
out.add(m);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A live-transaction rule applied to this account's transactions, with the engine's own meaning of each rule type. */
|
||||||
|
private String[] transactionRule(final CcRule rule, final List<Txn> txns, final int tabWindow) {
|
||||||
|
if (!rule.active) {
|
||||||
|
return new String[] { NOT_CHECKED, "The rule is switched off." };
|
||||||
|
}
|
||||||
|
if (rule.subjectScope != null) {
|
||||||
|
return new String[] { NOT_CHECKED, "Applies to a kind of customer (" + rule.subjectScope + "), not to the transactions." };
|
||||||
|
}
|
||||||
|
if (rule.threshold == null) {
|
||||||
|
return new String[] { NOT_CHECKED, "The rule has no amount, so there is nothing to compare a transaction with." };
|
||||||
|
}
|
||||||
|
final List<Txn> pool = new ArrayList<>();
|
||||||
|
for (final Txn t : txns) {
|
||||||
|
if (rule.direction != null && "CREDIT".equalsIgnoreCase(rule.direction) != t.credit) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (rule.cashOnly && !t.cash) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
pool.add(t);
|
||||||
|
}
|
||||||
|
final String scope = (rule.direction == null ? "credit or debit" : rule.direction.toLowerCase()) + (rule.cashOnly ? " cash" : "")
|
||||||
|
+ " transaction";
|
||||||
|
|
||||||
|
if ("THRESHOLD".equals(rule.ruleType)) {
|
||||||
|
Txn largest = null;
|
||||||
|
int count = 0;
|
||||||
|
for (final Txn t : pool) {
|
||||||
|
if (t.amount.compareTo(rule.threshold) >= 0) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
if (largest == null || t.amount.compareTo(largest.amount) > 0) {
|
||||||
|
largest = t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (count > 0) {
|
||||||
|
return new String[] { HIT, count + " " + scope + "(s) of " + rupees(rule.threshold) + " or more; the largest is "
|
||||||
|
+ rupees(largest.amount) + " on " + largest.date + "." };
|
||||||
|
}
|
||||||
|
return new String[] { NO_HIT, largest == null ? "There is no " + scope + " in the window."
|
||||||
|
: "No " + scope + " reaches " + rupees(rule.threshold) + "; the largest is " + rupees(largest.amount) + "." };
|
||||||
|
}
|
||||||
|
|
||||||
|
// STRUCTURING and VELOCITY: enough transactions, each inside the rule's band, adding up to the amount, within its window
|
||||||
|
final int days = rule.windowDays != null && rule.windowDays > 0 ? rule.windowDays : tabWindow;
|
||||||
|
final List<Txn> inBand = new ArrayList<>();
|
||||||
|
for (final Txn t : pool) {
|
||||||
|
if ((rule.min == null || t.amount.compareTo(rule.min) >= 0) && (rule.max == null || t.amount.compareTo(rule.max) <= 0)) {
|
||||||
|
inBand.add(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final int needed = rule.minCount == null || rule.minCount <= 1 ? 1 : rule.minCount;
|
||||||
|
int bestCount = 0;
|
||||||
|
BigDecimal bestSum = BigDecimal.ZERO;
|
||||||
|
for (final Txn end : inBand) {
|
||||||
|
int count = 0;
|
||||||
|
BigDecimal sum = BigDecimal.ZERO;
|
||||||
|
for (final Txn t : inBand) {
|
||||||
|
if (!t.date.isAfter(end.date) && t.date.isAfter(end.date.minusDays(days))) {
|
||||||
|
count++;
|
||||||
|
sum = sum.add(t.amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (count >= needed && sum.compareTo(rule.threshold) >= 0) {
|
||||||
|
return new String[] { HIT, count + " " + scope + "(s) within " + days + " days add up to " + rupees(sum)
|
||||||
|
+ " (the rule fires at " + rupees(rule.threshold) + " from " + needed + " or more)." };
|
||||||
|
}
|
||||||
|
if (sum.compareTo(bestSum) > 0) {
|
||||||
|
bestSum = sum;
|
||||||
|
bestCount = count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new String[] { NO_HIT, inBand.isEmpty() ? "No " + scope + " inside the rule's band in the window."
|
||||||
|
: "At most " + bestCount + " " + scope + "(s) within " + days + " days, adding up to " + rupees(bestSum)
|
||||||
|
+ "; the rule needs " + needed + " or more adding up to " + rupees(rule.threshold) + "." };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The CC scenarios written for this tab, each by its own test. */
|
||||||
|
private String[] scenario(final CcRule rule, final Facts f, final List<Txn> txns, final BigDecimal limit,
|
||||||
|
final BigDecimal declaredForWindow, final BigDecimal priorAverageVolume) {
|
||||||
|
final String n = rule.name;
|
||||||
|
if (txns.isEmpty()) {
|
||||||
|
return new String[] { NO_HIT, "There is no activity on the account in the window." };
|
||||||
|
}
|
||||||
|
if (n.contains("credit turnover exceeds")) {
|
||||||
|
return turnover(f.in, "Credits (Cr)", declaredForWindow, limit);
|
||||||
|
}
|
||||||
|
if (n.contains("debit turnover exceeds")) {
|
||||||
|
return turnover(f.out, "Debits (Dr)", declaredForWindow, limit);
|
||||||
|
}
|
||||||
|
if (n.contains("substantially withdrawn within")) {
|
||||||
|
return f.rapidCount > 0
|
||||||
|
? new String[] { HIT, f.rapidCount + " credit(s) worth " + rupees(f.rapidValue) + " were mostly paid out within "
|
||||||
|
+ RAPID_DAYS + " days." }
|
||||||
|
: new String[] { NO_HIT, "No credit was mostly paid out again within " + RAPID_DAYS + " days." };
|
||||||
|
}
|
||||||
|
if (n.contains("Sudden spike")) {
|
||||||
|
if (priorAverageVolume == null || priorAverageVolume.signum() <= 0) {
|
||||||
|
return new String[] { NOT_CHECKED, "There is no earlier activity to compare this window with." };
|
||||||
|
}
|
||||||
|
final BigDecimal now = f.in.add(f.out);
|
||||||
|
final BigDecimal multiple = now.divide(priorAverageVolume, 2, RoundingMode.HALF_UP);
|
||||||
|
final String text = "This window moved " + rupees(now) + ", " + multiple + "x the account's own earlier average of "
|
||||||
|
+ rupees(priorAverageVolume) + ".";
|
||||||
|
return multiple.compareTo(SPIKE_MULTIPLE) >= 0 ? new String[] { HIT, text } : new String[] { NO_HIT, text };
|
||||||
|
}
|
||||||
|
if (n.contains("share of cash")) {
|
||||||
|
final BigDecimal inShare = f.in.signum() > 0 ? f.cashIn.divide(f.in, 4, RoundingMode.HALF_UP) : BigDecimal.ZERO;
|
||||||
|
final BigDecimal outShare = f.out.signum() > 0 ? f.cashOut.divide(f.out, 4, RoundingMode.HALF_UP) : BigDecimal.ZERO;
|
||||||
|
final String text = "Cash is " + percent(inShare) + " of credits and " + percent(outShare) + " of debits.";
|
||||||
|
return inShare.compareTo(CASH_SHARE) >= 0 || outShare.compareTo(CASH_SHARE) >= 0 ? new String[] { HIT, text }
|
||||||
|
: new String[] { NO_HIT, text };
|
||||||
|
}
|
||||||
|
if (n.contains("round tripping")) {
|
||||||
|
if (limit.signum() <= 0) {
|
||||||
|
return new String[] { NOT_CHECKED, "The account has no sanctioned limit." };
|
||||||
|
}
|
||||||
|
if (f.peakUse.compareTo(LIMIT_USE) >= 0 && f.clearedAfterPeak) {
|
||||||
|
return new String[] { HIT, "The limit was drawn to " + percent(f.peakUse) + " and the account was back in credit within "
|
||||||
|
+ CLEARED_DAYS + " days." };
|
||||||
|
}
|
||||||
|
return new String[] { NO_HIT, "The limit was drawn to at most " + percent(f.peakUse)
|
||||||
|
+ (f.peakUse.compareTo(LIMIT_USE) >= 0 ? ", but the account did not return to credit within " + CLEARED_DAYS + " days." : ".") };
|
||||||
|
}
|
||||||
|
if (n.contains("counterparty concentration")) {
|
||||||
|
if (f.top == null) {
|
||||||
|
return new String[] { NOT_CHECKED, "No transaction recorded the party on the other side." };
|
||||||
|
}
|
||||||
|
final String text = f.top.label + " handled " + percent(f.topShare) + " of the money moved.";
|
||||||
|
return f.topShare.compareTo(CONCENTRATION_SHARE) >= 0 && f.top.count >= 3 ? new String[] { HIT, text }
|
||||||
|
: new String[] { NO_HIT, text };
|
||||||
|
}
|
||||||
|
if (n.contains("Circular movement")) {
|
||||||
|
if (f.twoWay.isEmpty()) {
|
||||||
|
return new String[] { NO_HIT, "No party both paid in and was paid a similar amount." };
|
||||||
|
}
|
||||||
|
final Total first = f.twoWay.get(0);
|
||||||
|
return new String[] { HIT, first.label + " is on both sides: " + rupees(first.in) + " Cr and " + rupees(first.out) + " Dr." };
|
||||||
|
}
|
||||||
|
return new String[] { NOT_CHECKED, "Needs the customer's relationship data, which the system does not hold yet." };
|
||||||
|
}
|
||||||
|
|
||||||
|
private String[] turnover(final BigDecimal amount, final String label, final BigDecimal declaredForWindow, final BigDecimal limit) {
|
||||||
|
final BigDecimal vsDeclared = ratio(amount, declaredForWindow);
|
||||||
|
final BigDecimal vsLimit = ratio(amount, limit);
|
||||||
|
if (vsDeclared == null && vsLimit == null) {
|
||||||
|
return new String[] { NOT_CHECKED, "The customer has declared no turnover and the account has no sanctioned limit." };
|
||||||
|
}
|
||||||
|
final boolean hit = (vsDeclared != null && vsDeclared.compareTo(TURNOVER_MULTIPLE) >= 0)
|
||||||
|
|| (vsLimit != null && vsLimit.compareTo(TURNOVER_MULTIPLE) >= 0);
|
||||||
|
final String text = label + " of " + rupees(amount)
|
||||||
|
+ (vsDeclared != null ? " are " + vsDeclared + "x the declared turnover for the window" : "")
|
||||||
|
+ (vsLimit != null ? (vsDeclared != null ? " and " : " are ") + vsLimit + "x the sanctioned limit" : "") + ".";
|
||||||
|
return new String[] { hit ? HIT : NO_HIT, text };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Analysis ─────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** One posted credit or debit, as the analysis reads it. */
|
||||||
|
private static final class Txn {
|
||||||
|
long id;
|
||||||
|
LocalDate date;
|
||||||
|
boolean credit;
|
||||||
|
BigDecimal amount;
|
||||||
|
String reference;
|
||||||
|
String remarks;
|
||||||
|
BigDecimal balanceAfter;
|
||||||
|
boolean cash;
|
||||||
|
String chequeNo;
|
||||||
|
String partyAccount;
|
||||||
|
String partyBank;
|
||||||
|
String partyIfsc;
|
||||||
|
String mode;
|
||||||
|
String party;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Running in / out / count for one mode or party. */
|
||||||
|
private static final class Total {
|
||||||
|
String label;
|
||||||
|
String sub;
|
||||||
|
BigDecimal in = BigDecimal.ZERO;
|
||||||
|
BigDecimal out = BigDecimal.ZERO;
|
||||||
|
int count;
|
||||||
|
|
||||||
|
BigDecimal volume() {
|
||||||
|
return in.add(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean unrecorded() {
|
||||||
|
return sub == null && "Not recorded".equals(label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The figures the observations and the CC scenarios are both judged on, worked out once. */
|
||||||
|
private static final class Facts {
|
||||||
|
BigDecimal in = BigDecimal.ZERO;
|
||||||
|
BigDecimal out = BigDecimal.ZERO;
|
||||||
|
BigDecimal cashIn = BigDecimal.ZERO;
|
||||||
|
BigDecimal cashOut = BigDecimal.ZERO;
|
||||||
|
BigDecimal volume = BigDecimal.ZERO;
|
||||||
|
BigDecimal lowest;
|
||||||
|
BigDecimal peakUse = BigDecimal.ZERO;
|
||||||
|
boolean clearedAfterPeak;
|
||||||
|
int rapidCount;
|
||||||
|
BigDecimal rapidValue = BigDecimal.ZERO;
|
||||||
|
Map<String, Total> byParty = new LinkedHashMap<>();
|
||||||
|
Total top;
|
||||||
|
BigDecimal topShare = BigDecimal.ZERO;
|
||||||
|
List<Total> twoWay = new ArrayList<>();
|
||||||
|
BigDecimal unrecorded = BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Facts facts(final List<Txn> txns, final BigDecimal limit) {
|
||||||
|
final Facts f = new Facts();
|
||||||
|
Txn lowestTxn = null;
|
||||||
|
for (final Txn t : txns) {
|
||||||
|
if (t.credit) {
|
||||||
|
f.in = f.in.add(t.amount);
|
||||||
|
if (t.cash) {
|
||||||
|
f.cashIn = f.cashIn.add(t.amount);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
f.out = f.out.add(t.amount);
|
||||||
|
if (t.cash) {
|
||||||
|
f.cashOut = f.cashOut.add(t.amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (t.balanceAfter != null && (f.lowest == null || t.balanceAfter.compareTo(f.lowest) < 0)) {
|
||||||
|
f.lowest = t.balanceAfter;
|
||||||
|
lowestTxn = t;
|
||||||
|
}
|
||||||
|
add(f.byParty, t.party, t.party, t.partyBank, t);
|
||||||
|
}
|
||||||
|
f.volume = f.in.add(f.out);
|
||||||
|
|
||||||
|
// How hard the limit was used, and whether the account was back in credit soon after its worst point
|
||||||
|
if (limit.signum() > 0 && f.lowest != null && f.lowest.signum() < 0) {
|
||||||
|
f.peakUse = f.lowest.negate().divide(limit, 4, RoundingMode.HALF_UP);
|
||||||
|
for (final Txn t : txns) {
|
||||||
|
if (lowestTxn != null && t.id != lowestTxn.id && t.balanceAfter != null && t.balanceAfter.signum() >= 0
|
||||||
|
&& !t.date.isBefore(lowestTxn.date) && !t.date.isAfter(lowestTxn.date.plusDays(CLEARED_DAYS))) {
|
||||||
|
f.clearedAfterPeak = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Money in and straight out again. Each debit is set against one credit only - the earliest still unmatched that it
|
||||||
|
// falls within RAPID_DAYS of - so one payment out is not counted as the exit of several credits.
|
||||||
|
final List<Txn> credits = new ArrayList<>();
|
||||||
|
final Map<Long, BigDecimal> matched = new LinkedHashMap<>();
|
||||||
|
for (final Txn t : txns) {
|
||||||
|
if (t.credit) {
|
||||||
|
credits.add(t);
|
||||||
|
matched.put(t.id, BigDecimal.ZERO);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (final Txn d : txns) {
|
||||||
|
if (d.credit) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BigDecimal toPlace = d.amount;
|
||||||
|
for (final Txn c : credits) {
|
||||||
|
if (toPlace.signum() <= 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (d.date.isBefore(c.date) || d.date.isAfter(c.date.plusDays(RAPID_DAYS))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final BigDecimal room = c.amount.subtract(matched.get(c.id));
|
||||||
|
if (room.signum() <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final BigDecimal placed = room.min(toPlace);
|
||||||
|
matched.put(c.id, matched.get(c.id).add(placed));
|
||||||
|
toPlace = toPlace.subtract(placed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (final Txn c : credits) {
|
||||||
|
if (matched.get(c.id).compareTo(c.amount.multiply(RAPID_SHARE)) >= 0) {
|
||||||
|
f.rapidCount++;
|
||||||
|
f.rapidValue = f.rapidValue.add(c.amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The party handling the most, the parties on both sides, and what has no party recorded
|
||||||
|
for (final Total t : f.byParty.values()) {
|
||||||
|
if (t.unrecorded()) {
|
||||||
|
f.unrecorded = f.unrecorded.add(t.volume());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (f.top == null || t.volume().compareTo(f.top.volume()) > 0) {
|
||||||
|
f.top = t;
|
||||||
|
}
|
||||||
|
if (t.in.signum() > 0 && t.out.signum() > 0
|
||||||
|
&& t.in.min(t.out).divide(t.in.max(t.out), 4, RoundingMode.HALF_UP).compareTo(TWO_WAY_SHARE) >= 0) {
|
||||||
|
f.twoWay.add(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (f.top != null && f.volume.signum() > 0) {
|
||||||
|
f.topShare = f.top.volume().divide(f.volume, 4, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> analyse(final List<Txn> txns, final BigDecimal limit, final BigDecimal declaredForWindow) {
|
||||||
|
final Facts f = facts(txns, limit);
|
||||||
|
int inCount = 0;
|
||||||
|
int outCount = 0;
|
||||||
|
Txn largestIn = null;
|
||||||
|
Txn largestOut = null;
|
||||||
|
BigDecimal highest = null;
|
||||||
|
int belowZero = 0;
|
||||||
|
final Map<String, Total> byMode = new LinkedHashMap<>();
|
||||||
|
final TreeMap<LocalDate, Total> byWeek = new TreeMap<>();
|
||||||
|
final java.util.Set<LocalDate> days = new java.util.HashSet<>();
|
||||||
|
|
||||||
|
for (final Txn t : txns) {
|
||||||
|
days.add(t.date);
|
||||||
|
if (t.credit) {
|
||||||
|
inCount++;
|
||||||
|
if (largestIn == null || t.amount.compareTo(largestIn.amount) > 0) {
|
||||||
|
largestIn = t;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
outCount++;
|
||||||
|
if (largestOut == null || t.amount.compareTo(largestOut.amount) > 0) {
|
||||||
|
largestOut = t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (t.balanceAfter != null) {
|
||||||
|
highest = highest == null || t.balanceAfter.compareTo(highest) > 0 ? t.balanceAfter : highest;
|
||||||
|
if (t.balanceAfter.signum() < 0) {
|
||||||
|
belowZero++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
add(byMode, t.mode, t.mode, null, t);
|
||||||
|
final LocalDate weekStart = t.date.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));
|
||||||
|
add(byWeek, weekStart, weekStart.toString(), null, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
final Map<String, Object> summary = new LinkedHashMap<>();
|
||||||
|
summary.put("in", f.in);
|
||||||
|
summary.put("out", f.out);
|
||||||
|
summary.put("net", f.in.subtract(f.out));
|
||||||
|
summary.put("inCount", inCount);
|
||||||
|
summary.put("outCount", outCount);
|
||||||
|
summary.put("averageIn", inCount == 0 ? null : f.in.divide(BigDecimal.valueOf(inCount), 2, RoundingMode.HALF_UP));
|
||||||
|
summary.put("averageOut", outCount == 0 ? null : f.out.divide(BigDecimal.valueOf(outCount), 2, RoundingMode.HALF_UP));
|
||||||
|
summary.put("largestIn", largest(largestIn));
|
||||||
|
summary.put("largestOut", largest(largestOut));
|
||||||
|
summary.put("activeDays", days.size());
|
||||||
|
summary.put("lowestBalance", f.lowest);
|
||||||
|
summary.put("highestBalance", highest);
|
||||||
|
summary.put("timesBelowZero", belowZero);
|
||||||
|
summary.put("peakLimitUsedPercent", limit.signum() > 0 && f.lowest != null && f.lowest.signum() < 0
|
||||||
|
? f.lowest.negate().multiply(BigDecimal.valueOf(100)).divide(limit, 1, RoundingMode.HALF_UP) : null);
|
||||||
|
summary.put("cashIn", f.cashIn);
|
||||||
|
summary.put("cashOut", f.cashOut);
|
||||||
|
summary.put("creditsVsDeclared", ratio(f.in, declaredForWindow));
|
||||||
|
summary.put("debitsVsDeclared", ratio(f.out, declaredForWindow));
|
||||||
|
summary.put("declaredForWindow", declaredForWindow);
|
||||||
|
result.put("summary", summary);
|
||||||
|
|
||||||
|
result.put("byMode", totals(byMode, f.volume));
|
||||||
|
result.put("byCounterparty", totals(f.byParty, f.volume));
|
||||||
|
result.put("timeline", totals(byWeek, f.volume));
|
||||||
|
result.put("observations", observations(txns, f, limit, declaredForWindow));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <K> void add(final Map<K, Total> map, final K key, final String label, final String sub, final Txn t) {
|
||||||
|
final Total total = map.computeIfAbsent(key, k -> {
|
||||||
|
final Total fresh = new Total();
|
||||||
|
fresh.label = label;
|
||||||
|
fresh.sub = sub;
|
||||||
|
return fresh;
|
||||||
|
});
|
||||||
|
if (t.credit) {
|
||||||
|
total.in = total.in.add(t.amount);
|
||||||
|
} else {
|
||||||
|
total.out = total.out.add(t.amount);
|
||||||
|
}
|
||||||
|
total.count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <K> List<Map<String, Object>> totals(final Map<K, Total> map, final BigDecimal volume) {
|
||||||
|
final List<Total> list = new ArrayList<>(map.values());
|
||||||
|
// Modes and parties: biggest first. The timeline map is already in date order and keeps it.
|
||||||
|
if (!(map instanceof TreeMap)) {
|
||||||
|
list.sort(Comparator.comparing(Total::volume).reversed());
|
||||||
|
}
|
||||||
|
final List<Map<String, Object>> rows = new ArrayList<>();
|
||||||
|
for (final Total t : list) {
|
||||||
|
final Map<String, Object> row = new LinkedHashMap<>();
|
||||||
|
row.put("label", t.label);
|
||||||
|
row.put("sub", t.sub);
|
||||||
|
row.put("in", t.in);
|
||||||
|
row.put("out", t.out);
|
||||||
|
row.put("count", t.count);
|
||||||
|
row.put("sharePercent", volume.signum() > 0
|
||||||
|
? t.volume().multiply(BigDecimal.valueOf(100)).divide(volume, 1, RoundingMode.HALF_UP) : null);
|
||||||
|
rows.add(row);
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, Object> largest(final Txn t) {
|
||||||
|
if (t == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final Map<String, Object> m = new LinkedHashMap<>();
|
||||||
|
m.put("amount", t.amount);
|
||||||
|
m.put("date", t.date.toString());
|
||||||
|
m.put("party", t.party);
|
||||||
|
m.put("mode", t.mode);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** What stands out, with the figures behind it. Starting values only - see the class note. */
|
||||||
|
private List<Map<String, Object>> observations(final List<Txn> txns, final Facts f, final BigDecimal limit,
|
||||||
|
final BigDecimal declaredForWindow) {
|
||||||
|
final List<Map<String, Object>> list = new ArrayList<>();
|
||||||
|
|
||||||
|
if (txns.isEmpty()) {
|
||||||
|
list.add(observation("INFO", "No activity", "There are no credits (Cr) or debits (Dr) on this account in the window."));
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (declaredForWindow == null) {
|
||||||
|
list.add(observation("INFO", "Declared turnover is missing",
|
||||||
|
"The customer has not declared a turnover, so the activity cannot be compared with the business profile."));
|
||||||
|
} else {
|
||||||
|
final BigDecimal inMultiple = ratio(f.in, declaredForWindow);
|
||||||
|
final BigDecimal outMultiple = ratio(f.out, declaredForWindow);
|
||||||
|
if (inMultiple != null && inMultiple.compareTo(TURNOVER_MULTIPLE) >= 0) {
|
||||||
|
list.add(observation("HIGH", "Credits far above the declared turnover", "Credits (Cr) of " + rupees(f.in) + " are "
|
||||||
|
+ inMultiple + "x the turnover declared for this window (" + rupees(declaredForWindow) + ")."));
|
||||||
|
}
|
||||||
|
if (outMultiple != null && outMultiple.compareTo(TURNOVER_MULTIPLE) >= 0) {
|
||||||
|
list.add(observation("HIGH", "Debits far above the declared turnover", "Debits (Dr) of " + rupees(f.out) + " are "
|
||||||
|
+ outMultiple + "x the turnover declared for this window (" + rupees(declaredForWindow) + ")."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (f.rapidCount > 0) {
|
||||||
|
list.add(observation("MEDIUM", "Money paid out soon after it came in", f.rapidCount + " credit(s) (Cr) worth "
|
||||||
|
+ rupees(f.rapidValue) + " were mostly (" + RAPID_SHARE.multiply(BigDecimal.valueOf(100)).intValue()
|
||||||
|
+ "% or more) paid out again within " + RAPID_DAYS + " days."));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (f.top != null && f.topShare.compareTo(CONCENTRATION_SHARE) >= 0) {
|
||||||
|
list.add(observation("MEDIUM", "One party handles most of the money", f.top.label
|
||||||
|
+ (f.top.sub == null ? "" : " (" + f.top.sub + ")") + " accounts for " + percent(f.topShare) + " of everything moved ("
|
||||||
|
+ rupees(f.top.in) + " Cr, " + rupees(f.top.out) + " Dr)."));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final Total t : f.twoWay) {
|
||||||
|
list.add(observation("HIGH", "Same party on both sides", t.label + " is on both sides: credits (Cr) of " + rupees(t.in)
|
||||||
|
+ " came from it and debits (Dr) of " + rupees(t.out) + " went to it."));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (f.in.signum() > 0 && f.cashIn.divide(f.in, 4, RoundingMode.HALF_UP).compareTo(CASH_SHARE) >= 0) {
|
||||||
|
list.add(observation("MEDIUM", "High share of cash in credits", rupees(f.cashIn) + " of " + rupees(f.in)
|
||||||
|
+ " credited came in as cash."));
|
||||||
|
}
|
||||||
|
if (f.out.signum() > 0 && f.cashOut.divide(f.out, 4, RoundingMode.HALF_UP).compareTo(CASH_SHARE) >= 0) {
|
||||||
|
list.add(observation("MEDIUM", "High share of cash in debits", rupees(f.cashOut) + " of " + rupees(f.out)
|
||||||
|
+ " debited went out as cash."));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (limit.signum() > 0 && f.peakUse.compareTo(LIMIT_USE) >= 0) {
|
||||||
|
list.add(observation("MEDIUM", "Sanctioned limit nearly used", "The account went down to " + rupees(f.lowest.negate())
|
||||||
|
+ " Dr, which is " + percent(f.peakUse) + " of the limit."));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (f.volume.signum() > 0 && f.unrecorded.divide(f.volume, 4, RoundingMode.HALF_UP).compareTo(UNRECORDED_SHARE) >= 0) {
|
||||||
|
list.add(observation("INFO", "Other side not recorded", rupees(f.unrecorded) + " of the " + rupees(f.volume)
|
||||||
|
+ " moved has no counterparty recorded, so where it came from or went cannot be told."));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (list.isEmpty()) {
|
||||||
|
list.add(observation("OK", "Nothing stands out", "None of the starting checks was triggered for this window."));
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Why each kind of observation is worth a look, shown under it so an officer does not have to guess. */
|
||||||
|
private static final Map<String, String> WHY = Map.of(
|
||||||
|
"Declared turnover is missing",
|
||||||
|
"Without a declared turnover the bank cannot tell whether this activity is normal for the customer's business.",
|
||||||
|
"Credits far above the declared turnover",
|
||||||
|
"Money received well beyond what the business says it turns over can mean the account is used for something other than "
|
||||||
|
+ "the declared business.",
|
||||||
|
"Debits far above the declared turnover",
|
||||||
|
"Money paid out well beyond the declared turnover can mean funds are being moved on rather than spent in the business.",
|
||||||
|
"Money paid out soon after it came in",
|
||||||
|
"Funds that come in and go straight out within days can mean the account is only a pass-through.",
|
||||||
|
"One party handles most of the money",
|
||||||
|
"Depending on one party concentrates risk and can mean the account mainly serves that party.",
|
||||||
|
"Same party on both sides",
|
||||||
|
"Money going to a party and coming back from it can be a round trip with no real business behind it.",
|
||||||
|
"High share of cash in credits",
|
||||||
|
"Cash is harder to trace to its source than a bank transfer, so a high share of it deserves a look.",
|
||||||
|
"High share of cash in debits",
|
||||||
|
"Large cash withdrawals leave no trail of where the money went.",
|
||||||
|
"Sanctioned limit nearly used",
|
||||||
|
"An account drawn close to its sanctioned limit has little room left and may be under strain or being used up fast.",
|
||||||
|
"Other side not recorded",
|
||||||
|
"Without the party on the other side the bank cannot tell where the money came from or went to.");
|
||||||
|
|
||||||
|
private static Map<String, Object> observation(final String level, final String title, final String detail) {
|
||||||
|
final Map<String, Object> m = new LinkedHashMap<>();
|
||||||
|
m.put("level", level);
|
||||||
|
m.put("title", title);
|
||||||
|
m.put("detail", detail);
|
||||||
|
m.put("why", WHY.get(title));
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String rupees(final BigDecimal value) {
|
||||||
|
return "₹" + String.format("%,.2f", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String percent(final BigDecimal fraction) {
|
||||||
|
return fraction.multiply(BigDecimal.valueOf(100)).setScale(1, RoundingMode.HALF_UP) + "%";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Mode and party ───────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How a payment moved. The bank's payment types are only Cash and Transfer, so a cheque, a demand draft, NEFT, RTGS, IMPS,
|
||||||
|
* UPI or an internal transfer is told from the cheque number the payment recorded and from the words in its payment type
|
||||||
|
* and remarks. Where nothing says, it is the payment type itself.
|
||||||
|
*/
|
||||||
|
private static String modeOf(final boolean cash, final String paymentType, final String chequeNo, final String remarks) {
|
||||||
|
if (cash) {
|
||||||
|
return "Cash";
|
||||||
|
}
|
||||||
|
final String text = ((paymentType == null ? "" : paymentType) + " " + (remarks == null ? "" : remarks)).toUpperCase();
|
||||||
|
if (hasWord(text, "RTGS")) {
|
||||||
|
return "RTGS";
|
||||||
|
}
|
||||||
|
if (hasWord(text, "NEFT")) {
|
||||||
|
return "NEFT";
|
||||||
|
}
|
||||||
|
if (hasWord(text, "IMPS")) {
|
||||||
|
return "IMPS";
|
||||||
|
}
|
||||||
|
if (hasWord(text, "UPI")) {
|
||||||
|
return "UPI";
|
||||||
|
}
|
||||||
|
if (hasWord(text, "DEMAND DRAFT") || hasWord(text, "DD")) {
|
||||||
|
return "Demand Draft";
|
||||||
|
}
|
||||||
|
if (chequeNo != null && !chequeNo.isBlank() || hasWord(text, "CHEQUE") || hasWord(text, "CHECK")) {
|
||||||
|
return "Cheque";
|
||||||
|
}
|
||||||
|
if (hasWord(text, "INTERNAL TRANSFER")) {
|
||||||
|
return "Internal transfer";
|
||||||
|
}
|
||||||
|
return paymentType == null || paymentType.isBlank() ? "Not recorded" : paymentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether {@code word} stands alone in {@code text}, so "UPI" is not found inside some other word. */
|
||||||
|
private static boolean hasWord(final String text, final String word) {
|
||||||
|
return java.util.regex.Pattern.compile("(^|[^A-Z0-9])" + java.util.regex.Pattern.quote(word) + "([^A-Z0-9]|$)").matcher(text)
|
||||||
|
.find();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The party on the other side, named by the account the payment recorded; "Not recorded" when it recorded none. */
|
||||||
|
private static String partyOf(final Txn t) {
|
||||||
|
return t.partyAccount == null || t.partyAccount.isBlank() ? "Not recorded" : t.partyAccount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BigDecimal scaledToWindow(final BigDecimal declaredMonthly, final int window) {
|
||||||
|
return declaredMonthly == null ? null
|
||||||
|
: declaredMonthly.multiply(BigDecimal.valueOf(window)).divide(BigDecimal.valueOf(30), 2, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
|
||||||
private static BigDecimal money(final Object value) {
|
private static BigDecimal money(final Object value) {
|
||||||
return value == null ? BigDecimal.ZERO : new BigDecimal(value.toString());
|
return value == null ? BigDecimal.ZERO : new BigDecimal(value.toString());
|
||||||
}
|
}
|
||||||
|
|||||||
-67
@@ -1,67 +0,0 @@
|
|||||||
package org.apache.fineract.portfolio.approvals.api;
|
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import jakarta.ws.rs.DefaultValue;
|
|
||||||
import jakarta.ws.rs.GET;
|
|
||||||
import jakarta.ws.rs.Path;
|
|
||||||
import jakarta.ws.rs.Produces;
|
|
||||||
import jakarta.ws.rs.QueryParam;
|
|
||||||
import jakarta.ws.rs.core.CacheControl;
|
|
||||||
import jakarta.ws.rs.core.Context;
|
|
||||||
import jakarta.ws.rs.core.MediaType;
|
|
||||||
import jakarta.ws.rs.core.Response;
|
|
||||||
import jakarta.ws.rs.sse.Sse;
|
|
||||||
import jakarta.ws.rs.sse.SseEventSink;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
|
||||||
import org.apache.fineract.portfolio.approvals.service.ApprovalCountsReadPlatformService;
|
|
||||||
import org.apache.fineract.portfolio.approvals.service.ApprovalCountsStreamService;
|
|
||||||
import org.apache.fineract.useradministration.domain.AppUser;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* How many requests wait on every Approval Center tab, for the badge on each tab and the Total on the Approvals menu.
|
|
||||||
* Numbers only: opening a tab still loads that tab's own list. The counting is in
|
|
||||||
* {@link ApprovalCountsReadPlatformService}; the live stream is in {@link ApprovalCountsStreamService}.
|
|
||||||
*/
|
|
||||||
@Path("/v1/approvals/counts")
|
|
||||||
@Component
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
public class ApprovalCountsApiResource {
|
|
||||||
|
|
||||||
private final PlatformSecurityContext context;
|
|
||||||
private final ApprovalCountsReadPlatformService readService;
|
|
||||||
private final ApprovalCountsStreamService streamService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The counts as they are now, answered at once. {@code fresh=true} skips the few seconds the last answer is held
|
|
||||||
* for. Screens that can stream use {@link #stream} instead; this is for a one-off read and as a fallback.
|
|
||||||
*/
|
|
||||||
@GET
|
|
||||||
public Response counts(@QueryParam("fresh") @DefaultValue("false") final boolean fresh) {
|
|
||||||
final AppUser user = this.context.authenticatedUser();
|
|
||||||
final CacheControl noStore = new CacheControl();
|
|
||||||
noStore.setNoStore(true);
|
|
||||||
return Response.ok(this.readService.retrieveHeld(user, fresh).toJson()).cacheControl(noStore).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Server-Sent Events: the connection stays open and a {@code counts} event is written the moment the numbers change,
|
|
||||||
* so a screen neither polls nor reloads. The first event carries the numbers as they stand.
|
|
||||||
*/
|
|
||||||
@GET
|
|
||||||
@Path("stream")
|
|
||||||
@Produces(MediaType.SERVER_SENT_EVENTS)
|
|
||||||
public void stream(@Context final SseEventSink sink, @Context final Sse sse, @Context final HttpServletResponse response) {
|
|
||||||
// Ask reverse proxies not to hold the events back.
|
|
||||||
response.setHeader("X-Accel-Buffering", "no");
|
|
||||||
response.setHeader("Cache-Control", "no-cache, no-store");
|
|
||||||
try {
|
|
||||||
this.streamService.open(this.readService.planFor(this.context.authenticatedUser()), sink, sse);
|
|
||||||
} catch (final RuntimeException e) {
|
|
||||||
sink.close();
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-14
@@ -1,14 +0,0 @@
|
|||||||
package org.apache.fineract.portfolio.approvals.data;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* One Approval Center tab and the question that counts what waits on it.
|
|
||||||
*
|
|
||||||
* @param key
|
|
||||||
* the tab's route segment, which is also the key its badge reads on the screen
|
|
||||||
* @param permission
|
|
||||||
* the permission the tab is shown under (READ_* is also met by ALL_FUNCTIONS_READ); null means every caller sees
|
|
||||||
* the tab
|
|
||||||
* @param sql
|
|
||||||
* a scalar sub-select answering "how many", written with the named parameters of {@link ApprovalCountsPlan}
|
|
||||||
*/
|
|
||||||
public record ApprovalCounter(String key, String permission, String sql) {}
|
|
||||||
-27
@@ -1,27 +0,0 @@
|
|||||||
package org.apache.fineract.portfolio.approvals.data;
|
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* What is waiting for approval: the total, the number for each tab that has anything, and a version that is equal
|
|
||||||
* exactly when the numbers are, which is how a change is told from a repeat.
|
|
||||||
*/
|
|
||||||
public record ApprovalCountsData(long total, Map<String, Long> counts, String version) {
|
|
||||||
|
|
||||||
private static final Gson GSON = new Gson();
|
|
||||||
|
|
||||||
public static ApprovalCountsData of(final Map<String, Long> counts) {
|
|
||||||
final long total = counts.values().stream().mapToLong(Long::longValue).sum();
|
|
||||||
return new ApprovalCountsData(total, counts, Integer.toHexString(counts.hashCode()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public String toJson() {
|
|
||||||
final Map<String, Object> json = new LinkedHashMap<>();
|
|
||||||
json.put("total", this.total);
|
|
||||||
json.put("counts", this.counts);
|
|
||||||
json.put("version", this.version);
|
|
||||||
return GSON.toJson(json);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-22
@@ -1,22 +0,0 @@
|
|||||||
package org.apache.fineract.portfolio.approvals.data;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
|
|
||||||
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* What one caller is counted for: the tabs they may see and the values (office scope, user, checker rights) those
|
|
||||||
* tabs' queries are asked with. It is worked out on the request thread, because it reads the user's roles, and can then
|
|
||||||
* be used from any thread.
|
|
||||||
*
|
|
||||||
* @param tenant
|
|
||||||
* the tenant the counts are read from
|
|
||||||
* @param visible
|
|
||||||
* the tabs this caller may see, in the order the Approval Center shows them
|
|
||||||
* @param params
|
|
||||||
* the named parameters the tabs' queries use
|
|
||||||
* @param group
|
|
||||||
* equal for callers who are counted identically, so they can share one query
|
|
||||||
*/
|
|
||||||
public record ApprovalCountsPlan(FineractPlatformTenant tenant, List<ApprovalCounter> visible, MapSqlParameterSource params,
|
|
||||||
String group) {}
|
|
||||||
-122
@@ -1,122 +0,0 @@
|
|||||||
package org.apache.fineract.portfolio.approvals.service;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import org.apache.fineract.portfolio.approvals.data.ApprovalCounter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Every Approval Center tab that carries a badge, and the question that counts what waits on it. Each question is the
|
|
||||||
* one the tab asks of its own API (same table, same status, same office scope, same entries left out). To badge a new
|
|
||||||
* tab, add one line here and one badge on the screen.
|
|
||||||
*
|
|
||||||
* The queries use these named parameters, supplied per caller: {@code :h} (office hierarchy pattern), {@code :ho} (head
|
|
||||||
* office), {@code :uid} (user) and {@code :superChecker} (may check every maker-checker command).
|
|
||||||
*/
|
|
||||||
public final class ApprovalCounterRegistry {
|
|
||||||
|
|
||||||
private ApprovalCounterRegistry() {}
|
|
||||||
|
|
||||||
/** Entities whose maker-checker requests are shown under Product Approval, not in the Checker Inbox. */
|
|
||||||
private static final String PRODUCT_ENTITIES = "'LOANPRODUCT','SAVINGSPRODUCT','CURRENTPRODUCT','SHAREPRODUCT',"
|
|
||||||
+ "'FIXEDDEPOSITPRODUCT','RECURRINGDEPOSITPRODUCT','CHARGE'";
|
|
||||||
|
|
||||||
/** Product edits that are staged in m_saving_product_temp, so Product Approval already lists them from there. */
|
|
||||||
private static final String STAGED_PRODUCT_ENTITIES = "'SAVINGSPRODUCT','CURRENTPRODUCT','SHAREPRODUCT',"
|
|
||||||
+ "'FIXEDDEPOSITPRODUCT','RECURRINGDEPOSITPRODUCT'";
|
|
||||||
|
|
||||||
/** Accounts whose UPDATE is staged for Pending Account Edits, so the Checker Inbox leaves it out. */
|
|
||||||
private static final String STAGED_ACCOUNT_ENTITIES = "'SAVINGSACCOUNT','CURRENTACCOUNT'";
|
|
||||||
|
|
||||||
private static final String DDS_ENTITIES = "'PARTY','AGENT','PARTYTRANSACTION','DDSACCOUNTTYPE','COMMISSIONSLAB',"
|
|
||||||
+ "'DDSBATCH','DDSCOLLECTIONENTRY'";
|
|
||||||
|
|
||||||
/** Maker-checker commands awaiting a checker, limited to the offices and checker rights the caller has. */
|
|
||||||
private static final String MC_PENDING = "FROM m_portfolio_command_source aud WHERE aud.status = 2 "
|
|
||||||
+ "AND (:ho OR EXISTS (SELECT 1 FROM m_office o2 WHERE o2.id = aud.office_id AND o2.hierarchy LIKE :h)) "
|
|
||||||
+ "AND (:superChecker OR EXISTS (SELECT 1 FROM m_permission p JOIN m_role_permission rp ON rp.permission_id = p.id "
|
|
||||||
+ "JOIN m_appuser_role ur ON ur.role_id = rp.role_id AND ur.appuser_id = :uid "
|
|
||||||
+ "WHERE p.code LIKE '%\\_CHECKER' AND REPLACE(p.action_name, '_CHECKER', '') = aud.action_name "
|
|
||||||
+ "AND p.entity_name = aud.entity_name)) ";
|
|
||||||
|
|
||||||
/** Where the person who raised a loan or account sits, for the office scope the tab applies. */
|
|
||||||
private static final String CLIENT_OFFICE = "EXISTS (SELECT 1 FROM m_office o WHERE o.id = COALESCE(c.office_id, g.office_id) "
|
|
||||||
+ "AND o.hierarchy LIKE :h)";
|
|
||||||
|
|
||||||
/** The one tab whose list is open to managers and head cashiers only. */
|
|
||||||
public static final String DISBURSEMENT_REQUESTS = "disbursement-requests";
|
|
||||||
|
|
||||||
/** The tabs, in the order the Approval Center shows them. A null permission means every caller sees the tab. */
|
|
||||||
public static final List<ApprovalCounter> ALL = List.of(
|
|
||||||
// ── Fineract's own maker-checker and pending lists ────────────────────────────
|
|
||||||
new ApprovalCounter("checker-inbox", "ALL_FUNCTIONS_READ",
|
|
||||||
"SELECT count(*) " + MC_PENDING + "AND upper(aud.entity_name) NOT IN (" + PRODUCT_ENTITIES + ") "
|
|
||||||
+ "AND NOT (upper(aud.entity_name) IN (" + STAGED_ACCOUNT_ENTITIES + ") AND upper(aud.action_name) = 'UPDATE')"),
|
|
||||||
new ApprovalCounter("client-approval", "READ_CLIENT",
|
|
||||||
"SELECT count(*) FROM m_client c WHERE c.status_enum = 100 "
|
|
||||||
+ "AND EXISTS (SELECT 1 FROM m_office o WHERE o.id = c.office_id AND o.hierarchy LIKE :h)"),
|
|
||||||
new ApprovalCounter("loan-approval", "READ_LOAN",
|
|
||||||
"SELECT count(*) FROM m_loan l LEFT JOIN m_client c ON c.id = l.client_id LEFT JOIN m_group g ON g.id = l.group_id "
|
|
||||||
+ "WHERE l.loan_status_id = 100 AND " + CLIENT_OFFICE),
|
|
||||||
new ApprovalCounter("loan-disbursal", "READ_LOAN",
|
|
||||||
"SELECT count(*) FROM m_loan l LEFT JOIN m_client c ON c.id = l.client_id LEFT JOIN m_group g ON g.id = l.group_id "
|
|
||||||
+ "WHERE l.loan_status_id = 200 AND " + CLIENT_OFFICE),
|
|
||||||
new ApprovalCounter("reschedule-loan", "RESCHEDULE_LOAN",
|
|
||||||
"SELECT count(*) FROM m_loan_reschedule_request WHERE status_enum = 100"),
|
|
||||||
new ApprovalCounter("pending-account", "ALL_FUNCTIONS_READ",
|
|
||||||
"SELECT (SELECT count(*) FROM m_savings_account s LEFT JOIN m_client c ON c.id = s.client_id "
|
|
||||||
+ "LEFT JOIN m_group g ON g.id = s.group_id WHERE s.status_enum = 100 "
|
|
||||||
+ "AND COALESCE(s.terminate_true, false) = false AND " + CLIENT_OFFICE
|
|
||||||
+ ") + (SELECT count(*) FROM m_current_account ca WHERE ca.status_enum = 100 "
|
|
||||||
+ "AND COALESCE(ca.terminate_true, false) = false "
|
|
||||||
+ "AND EXISTS (SELECT 1 FROM m_office o WHERE o.id = ca.office_id AND o.hierarchy LIKE :h))"),
|
|
||||||
new ApprovalCounter("pending-share-accounts", "ALL_FUNCTIONS_READ",
|
|
||||||
"SELECT count(*) FROM m_share_account WHERE status_enum = 100"),
|
|
||||||
new ApprovalCounter("product-approval", "ALL_FUNCTIONS_READ",
|
|
||||||
"SELECT (SELECT count(*) FROM m_saving_product_temp WHERE status = 'PENDING' "
|
|
||||||
+ "AND product_type IN ('SAVINGS','CURRENT','SHARE','FD','RD')) + (SELECT count(*) " + MC_PENDING
|
|
||||||
+ "AND upper(aud.entity_name) IN (" + PRODUCT_ENTITIES + ") "
|
|
||||||
+ "AND NOT (upper(aud.entity_name) IN (" + STAGED_PRODUCT_ENTITIES + ") AND upper(aud.action_name) = 'UPDATE'))"),
|
|
||||||
new ApprovalCounter("pending-savings-account-edits", "ALL_FUNCTIONS_READ",
|
|
||||||
"SELECT (SELECT count(*) FROM m_savings_account_pending_edit WHERE status = 'PENDING') "
|
|
||||||
+ "+ (SELECT count(*) FROM m_current_account_pending_edit WHERE status = 'PENDING') "
|
|
||||||
+ "+ (SELECT count(*) FROM m_share_account_pending_edit WHERE status = 'PENDING')"),
|
|
||||||
new ApprovalCounter("pending-client-edits", "ALL_FUNCTIONS_READ",
|
|
||||||
"SELECT count(*) FROM m_client_pending_edit pe JOIN m_client c ON c.id = pe.client_id WHERE pe.status = 'PENDING'"),
|
|
||||||
new ApprovalCounter("pending-user-edits", "ALL_FUNCTIONS_READ",
|
|
||||||
"SELECT count(*) FROM m_user_pending_request WHERE status = 'PENDING'"),
|
|
||||||
// ── Requests raised by clerks, cashiers and managers ──────────────────────────
|
|
||||||
new ApprovalCounter("withdrawal-requests", "ALL_FUNCTIONS_READ",
|
|
||||||
"SELECT count(*) FROM m_clerk_withdrawal_request WHERE status = 'PENDING'"),
|
|
||||||
new ApprovalCounter("cash-transfers", "ALL_FUNCTIONS_READ",
|
|
||||||
"SELECT count(*) FROM m_cash_transfer_request WHERE status = 'PENDING'"),
|
|
||||||
new ApprovalCounter("voucher-corrections", "ALL_FUNCTIONS_READ",
|
|
||||||
"SELECT count(*) FROM m_voucher_correction WHERE status IN ('UNPASS_PENDING','CORRECTION_PENDING')"),
|
|
||||||
new ApprovalCounter("share-withdrawal-requests", "ALL_FUNCTIONS_READ",
|
|
||||||
"SELECT count(*) FROM m_clerk_share_withdrawal_request WHERE status = 'PENDING'"),
|
|
||||||
new ApprovalCounter("dividend-requests", "ALL_FUNCTIONS_READ",
|
|
||||||
"SELECT count(*) FROM m_share_product_dividend_pay_out p JOIN m_share_product sp ON sp.id = p.product_id "
|
|
||||||
+ "WHERE p.status = 100"),
|
|
||||||
new ApprovalCounter("share-addition-requests", "ALL_FUNCTIONS_READ",
|
|
||||||
"SELECT count(*) FROM m_share_account_transactions t JOIN m_share_account sa ON sa.id = t.account_id "
|
|
||||||
+ "JOIN m_client c ON c.id = sa.client_id JOIN m_share_product sp ON sp.id = sa.product_id "
|
|
||||||
+ "WHERE t.is_active = true AND t.status_enum = 100 AND t.type_enum = 500"),
|
|
||||||
new ApprovalCounter("deposit-requests", "ALL_FUNCTIONS_READ",
|
|
||||||
"SELECT count(*) FROM m_cashier_deposit_request WHERE status = 'PENDING'"),
|
|
||||||
new ApprovalCounter(DISBURSEMENT_REQUESTS, "ALL_FUNCTIONS_READ",
|
|
||||||
"SELECT count(*) FROM m_clerk_disbursement_request WHERE status = 'PENDING'"),
|
|
||||||
new ApprovalCounter("dds-approvals", null,
|
|
||||||
"SELECT count(*) FROM m_portfolio_command_source c JOIN m_permission p "
|
|
||||||
+ "ON p.code = c.action_name || '_' || c.entity_name AND p.can_maker_checker = true "
|
|
||||||
+ "WHERE c.status = 2 AND c.entity_name IN (" + DDS_ENTITIES + ")"),
|
|
||||||
new ApprovalCounter("chequebook-requests", "READ_CHEQUEBOOKPENDINGREQUEST",
|
|
||||||
"SELECT count(*) FROM m_chequebook_pending_request WHERE status = 'PENDING'"),
|
|
||||||
new ApprovalCounter("closure-requests", "ALL_FUNCTIONS_READ",
|
|
||||||
"SELECT count(*) FROM m_clerk_closure_request WHERE status = 'PENDING'"),
|
|
||||||
new ApprovalCounter("dd-issue-approvals", null,
|
|
||||||
"SELECT count(*) FROM m_sponsor_dd_issue_request WHERE status = 'PENDING'"),
|
|
||||||
new ApprovalCounter("dd-reprint-approvals", null,
|
|
||||||
"SELECT count(*) FROM m_sponsor_dd_reprint_request WHERE status = 'PENDING'"),
|
|
||||||
new ApprovalCounter("dd-revoke-approvals", null,
|
|
||||||
"SELECT count(*) FROM m_sponsor_dd_revoke_request WHERE status = 'PENDING'"),
|
|
||||||
new ApprovalCounter("risk-change-approvals", null,
|
|
||||||
"SELECT count(*) FROM m_client_risk_change_request WHERE status = 'PENDING'"));
|
|
||||||
}
|
|
||||||
-24
@@ -1,24 +0,0 @@
|
|||||||
package org.apache.fineract.portfolio.approvals.service;
|
|
||||||
|
|
||||||
import org.apache.fineract.portfolio.approvals.data.ApprovalCountsData;
|
|
||||||
import org.apache.fineract.portfolio.approvals.data.ApprovalCountsPlan;
|
|
||||||
import org.apache.fineract.useradministration.domain.AppUser;
|
|
||||||
|
|
||||||
/** Reads how many requests wait on each Approval Center tab, in one statement. */
|
|
||||||
public interface ApprovalCountsReadPlatformService {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Works out what the caller is counted for. Call it on the request thread: it reads the user's roles and the
|
|
||||||
* tenant of the current request.
|
|
||||||
*/
|
|
||||||
ApprovalCountsPlan planFor(AppUser user);
|
|
||||||
|
|
||||||
/** Counts now, for the tabs in the plan. The plan's tenant must be the current tenant of the calling thread. */
|
|
||||||
ApprovalCountsData retrieve(ApprovalCountsPlan plan);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Counts for the caller, from the answer held for a few seconds when there is one, so many screens asking together
|
|
||||||
* cost the database one query. {@code fresh} skips the held answer.
|
|
||||||
*/
|
|
||||||
ApprovalCountsData retrieveHeld(AppUser user, boolean fresh);
|
|
||||||
}
|
|
||||||
-114
@@ -1,114 +0,0 @@
|
|||||||
package org.apache.fineract.portfolio.approvals.service;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
|
|
||||||
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
|
||||||
import org.apache.fineract.portfolio.approvals.data.ApprovalCounter;
|
|
||||||
import org.apache.fineract.portfolio.approvals.data.ApprovalCountsData;
|
|
||||||
import org.apache.fineract.portfolio.approvals.data.ApprovalCountsPlan;
|
|
||||||
import org.apache.fineract.useradministration.domain.AppUser;
|
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
|
||||||
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
|
|
||||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* All the visible tabs' questions go to the database as one statement (one scalar sub-select per tab), so the cost is
|
|
||||||
* one connection and one plan instead of a call per tab. A tab the caller may not see is simply not asked.
|
|
||||||
*/
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class ApprovalCountsReadPlatformServiceImpl implements ApprovalCountsReadPlatformService {
|
|
||||||
|
|
||||||
/** Screens asking together read one answer instead of each running the statement. */
|
|
||||||
private static final long HOLD_MILLIS = 3_000L;
|
|
||||||
|
|
||||||
private record Held(long at, ApprovalCountsData data) {}
|
|
||||||
|
|
||||||
private final JdbcTemplate jdbcTemplate;
|
|
||||||
private final Map<String, Held> held = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ApprovalCountsPlan planFor(final AppUser user) {
|
|
||||||
final List<ApprovalCounter> visible = new ArrayList<>();
|
|
||||||
for (final ApprovalCounter counter : ApprovalCounterRegistry.ALL) {
|
|
||||||
if (isVisible(user, counter)) {
|
|
||||||
visible.add(counter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
final String hierarchy = user.getOffice().getHierarchy();
|
|
||||||
final boolean superChecker = !user.hasNotPermissionForAnyOf("ALL_FUNCTIONS", "CHECKER_SUPER_USER");
|
|
||||||
final MapSqlParameterSource params = new MapSqlParameterSource().addValue("h", hierarchy + "%")
|
|
||||||
.addValue("ho", ".".equals(hierarchy)).addValue("uid", user.getId()).addValue("superChecker", superChecker);
|
|
||||||
final FineractPlatformTenant tenant = ThreadLocalContextUtil.getTenant();
|
|
||||||
// Callers counted the same way share one query: only a limited checker's numbers depend on who they are.
|
|
||||||
final String group = tenant.getTenantIdentifier() + '|' + hierarchy + '|' + superChecker + '|'
|
|
||||||
+ (superChecker ? "" : user.getId()) + '|' + visible.stream().map(ApprovalCounter::key).collect(Collectors.joining(","));
|
|
||||||
return new ApprovalCountsPlan(tenant, visible, params, group);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ApprovalCountsData retrieve(final ApprovalCountsPlan plan) {
|
|
||||||
final Map<String, Long> counts = new LinkedHashMap<>();
|
|
||||||
final List<ApprovalCounter> visible = plan.visible();
|
|
||||||
if (!visible.isEmpty()) {
|
|
||||||
final StringBuilder sql = new StringBuilder("SELECT ");
|
|
||||||
for (int i = 0; i < visible.size(); i++) {
|
|
||||||
sql.append(i == 0 ? "" : ", ").append('(').append(visible.get(i).sql()).append(") AS c").append(i);
|
|
||||||
}
|
|
||||||
final Map<String, Object> row = new NamedParameterJdbcTemplate(this.jdbcTemplate).queryForMap(sql.toString(),
|
|
||||||
plan.params());
|
|
||||||
for (int i = 0; i < visible.size(); i++) {
|
|
||||||
final long n = row.get("c" + i) instanceof Number number ? number.longValue() : 0L;
|
|
||||||
if (n > 0) {
|
|
||||||
counts.put(visible.get(i).key(), n);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ApprovalCountsData.of(counts);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ApprovalCountsData retrieveHeld(final AppUser user, final boolean fresh) {
|
|
||||||
final ApprovalCountsPlan plan = planFor(user);
|
|
||||||
final String holdKey = plan.tenant().getTenantIdentifier() + ':' + user.getId();
|
|
||||||
final long now = System.currentTimeMillis();
|
|
||||||
final Held recent = this.held.get(holdKey);
|
|
||||||
if (!fresh && recent != null && now - recent.at() < HOLD_MILLIS) {
|
|
||||||
return recent.data();
|
|
||||||
}
|
|
||||||
final ApprovalCountsData data = retrieve(plan);
|
|
||||||
this.held.put(holdKey, new Held(now, data));
|
|
||||||
if (this.held.size() > 500) {
|
|
||||||
this.held.entrySet().removeIf(e -> now - e.getValue().at() >= HOLD_MILLIS);
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The tab's own visibility rule: its permission (READ_* is also met by ALL_FUNCTIONS_READ) and, for one tab, its role. */
|
|
||||||
private boolean isVisible(final AppUser user, final ApprovalCounter counter) {
|
|
||||||
if (counter.permission() != null) {
|
|
||||||
final boolean allowed = counter.permission().startsWith("READ_")
|
|
||||||
? user.hasAnyPermission(counter.permission(), "ALL_FUNCTIONS_READ")
|
|
||||||
: user.hasAnyPermission(counter.permission());
|
|
||||||
if (!allowed) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return !ApprovalCounterRegistry.DISBURSEMENT_REQUESTS.equals(counter.key()) || isChecker(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isChecker(final AppUser user) {
|
|
||||||
if (user.hasAnyPermission("ALL_FUNCTIONS")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return user.getRoles().stream().map(r -> r.getName() == null ? "" : r.getName().toLowerCase().trim())
|
|
||||||
.anyMatch(n -> n.contains("manager") || n.contains("head cashier"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-145
@@ -1,145 +0,0 @@
|
|||||||
package org.apache.fineract.portfolio.approvals.service;
|
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
|
||||||
import jakarta.annotation.PreDestroy;
|
|
||||||
import jakarta.ws.rs.sse.OutboundSseEvent;
|
|
||||||
import jakarta.ws.rs.sse.Sse;
|
|
||||||
import jakarta.ws.rs.sse.SseEventSink;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
|
||||||
import org.apache.fineract.portfolio.approvals.data.ApprovalCountsData;
|
|
||||||
import org.apache.fineract.portfolio.approvals.data.ApprovalCountsPlan;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Keeps the Approvals badge live. A screen listens on a Server-Sent Events stream and is written to the moment its
|
|
||||||
* numbers change, so it neither polls nor reloads.
|
|
||||||
*
|
|
||||||
* One shared check, every {@value #CHECK_MILLIS} ms, serves every open screen, and screens counted the same way share
|
|
||||||
* its query. A comment line every {@value #PING_MILLIS} ms keeps proxies from closing an idle connection and tells the
|
|
||||||
* server when a screen has gone. A stream ends after {@value #LIFETIME_MILLIS} ms so the screen reconnects with a clean
|
|
||||||
* connection, which bounds what a browser accumulates on one.
|
|
||||||
*/
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class ApprovalCountsStreamService {
|
|
||||||
|
|
||||||
/** How often the open screens are looked at: the longest a change waits before it is pushed. */
|
|
||||||
private static final long CHECK_MILLIS = 1_000L;
|
|
||||||
|
|
||||||
private static final long PING_MILLIS = 15_000L;
|
|
||||||
|
|
||||||
private static final long LIFETIME_MILLIS = 300_000L;
|
|
||||||
|
|
||||||
/** A screen listening, and the version of the numbers it was last sent. */
|
|
||||||
private static final class Listener {
|
|
||||||
|
|
||||||
private final ApprovalCountsPlan plan;
|
|
||||||
private final SseEventSink sink;
|
|
||||||
private final Sse sse;
|
|
||||||
private final long expiresAt;
|
|
||||||
private volatile String version;
|
|
||||||
private volatile long lastSent;
|
|
||||||
|
|
||||||
private Listener(final ApprovalCountsPlan plan, final SseEventSink sink, final Sse sse, final long expiresAt) {
|
|
||||||
this.plan = plan;
|
|
||||||
this.sink = sink;
|
|
||||||
this.sse = sse;
|
|
||||||
this.expiresAt = expiresAt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final ApprovalCountsReadPlatformService readService;
|
|
||||||
private final Set<Listener> listeners = ConcurrentHashMap.newKeySet();
|
|
||||||
private final ScheduledExecutorService ticker = Executors.newSingleThreadScheduledExecutor(runnable -> {
|
|
||||||
final Thread thread = new Thread(runnable, "approval-counts-stream");
|
|
||||||
thread.setDaemon(true);
|
|
||||||
return thread;
|
|
||||||
});
|
|
||||||
|
|
||||||
@PostConstruct
|
|
||||||
void startTicker() {
|
|
||||||
this.ticker.scheduleWithFixedDelay(this::tick, CHECK_MILLIS, CHECK_MILLIS, TimeUnit.MILLISECONDS);
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreDestroy
|
|
||||||
void stopTicker() {
|
|
||||||
this.ticker.shutdownNow();
|
|
||||||
this.listeners.forEach(this::close);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Sends the numbers as they stand, then keeps the screen informed. Call it on the request thread. */
|
|
||||||
public void open(final ApprovalCountsPlan plan, final SseEventSink sink, final Sse sse) {
|
|
||||||
final Listener listener = new Listener(plan, sink, sse, System.currentTimeMillis() + LIFETIME_MILLIS);
|
|
||||||
try {
|
|
||||||
publish(listener, this.readService.retrieve(plan));
|
|
||||||
} catch (final RuntimeException e) {
|
|
||||||
sink.close();
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
this.listeners.add(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Looks at the numbers for every open screen and writes to the ones whose numbers moved. */
|
|
||||||
private void tick() {
|
|
||||||
if (this.listeners.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final long now = System.currentTimeMillis();
|
|
||||||
final Map<String, ApprovalCountsData> computed = new HashMap<>();
|
|
||||||
for (final Listener listener : this.listeners) {
|
|
||||||
try {
|
|
||||||
if (listener.sink.isClosed() || now >= listener.expiresAt) {
|
|
||||||
close(listener);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
ApprovalCountsData data = computed.get(listener.plan.group());
|
|
||||||
if (data == null) {
|
|
||||||
ThreadLocalContextUtil.setTenant(listener.plan.tenant());
|
|
||||||
data = this.readService.retrieve(listener.plan);
|
|
||||||
computed.put(listener.plan.group(), data);
|
|
||||||
}
|
|
||||||
if (!data.version().equals(listener.version)) {
|
|
||||||
publish(listener, data);
|
|
||||||
} else if (now - listener.lastSent >= PING_MILLIS) {
|
|
||||||
listener.lastSent = now;
|
|
||||||
send(listener, listener.sse.newEventBuilder().comment("ping").build());
|
|
||||||
}
|
|
||||||
} catch (final RuntimeException e) {
|
|
||||||
close(listener);
|
|
||||||
} finally {
|
|
||||||
ThreadLocalContextUtil.clearTenant();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void publish(final Listener listener, final ApprovalCountsData data) {
|
|
||||||
listener.version = data.version();
|
|
||||||
listener.lastSent = System.currentTimeMillis();
|
|
||||||
send(listener, listener.sse.newEvent("counts", data.toJson()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void send(final Listener listener, final OutboundSseEvent event) {
|
|
||||||
listener.sink.send(event).whenComplete((sent, failure) -> {
|
|
||||||
if (failure != null) {
|
|
||||||
close(listener);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void close(final Listener listener) {
|
|
||||||
this.listeners.remove(listener);
|
|
||||||
try {
|
|
||||||
listener.sink.close();
|
|
||||||
} catch (final RuntimeException ignored) {
|
|
||||||
// The screen is already gone.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-17
@@ -46,8 +46,6 @@ import org.apache.fineract.infrastructure.core.service.DateUtils;
|
|||||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
import org.apache.fineract.portfolio.cashreport.service.VaultPositionService;
|
import org.apache.fineract.portfolio.cashreport.service.VaultPositionService;
|
||||||
import org.apache.fineract.portfolio.denominationmaster.service.CountedCashLedger;
|
import org.apache.fineract.portfolio.denominationmaster.service.CountedCashLedger;
|
||||||
import org.apache.fineract.portfolio.investment.service.InvestmentFdInterestService;
|
|
||||||
import org.apache.fineract.portfolio.investment.service.InvestmentPurchaseReadPlatformService;
|
|
||||||
import org.apache.fineract.portfolio.savings.dailyinterest.SavingsDailyInterestOrchestrator;
|
import org.apache.fineract.portfolio.savings.dailyinterest.SavingsDailyInterestOrchestrator;
|
||||||
import org.apache.fineract.portfolio.savings.dailyinterest.SavingsInterestPostingReportService;
|
import org.apache.fineract.portfolio.savings.dailyinterest.SavingsInterestPostingReportService;
|
||||||
import org.apache.fineract.portfolio.savings.fdinterest.FixedDepositInterestPostingOrchestrator;
|
import org.apache.fineract.portfolio.savings.fdinterest.FixedDepositInterestPostingOrchestrator;
|
||||||
@@ -97,16 +95,8 @@ public class BranchDayRegisterApiResource {
|
|||||||
private final SavingsDailyInterestOrchestrator savingsDailyInterestOrchestrator;
|
private final SavingsDailyInterestOrchestrator savingsDailyInterestOrchestrator;
|
||||||
private final FixedDepositInterestPostingReportService fdInterestPostingReportService;
|
private final FixedDepositInterestPostingReportService fdInterestPostingReportService;
|
||||||
private final FixedDepositInterestPostingOrchestrator fdInterestPostingOrchestrator;
|
private final FixedDepositInterestPostingOrchestrator fdInterestPostingOrchestrator;
|
||||||
private final InvestmentPurchaseReadPlatformService investmentPurchaseReadPlatformService;
|
|
||||||
private final InvestmentFdInterestService investmentFdInterestService;
|
|
||||||
private final Gson gson = new Gson();
|
private final Gson gson = new Gson();
|
||||||
|
|
||||||
private boolean isHeadOffice(final Long officeId) {
|
|
||||||
final Integer topLevel = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM m_office WHERE id = ? AND parent_id IS NULL",
|
|
||||||
Integer.class, officeId);
|
|
||||||
return topLevel != null && topLevel > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── GET /v1/branchday ── the register itself ──────────────────────────────
|
// ── GET /v1/branchday ── the register itself ──────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -270,13 +260,6 @@ public class BranchDayRegisterApiResource {
|
|||||||
final Map<String, Object> fdInterest = fdInterestPostingReportService.dayCloseInterestView(officeId, businessDate);
|
final Map<String, Object> fdInterest = fdInterestPostingReportService.dayCloseInterestView(officeId, businessDate);
|
||||||
result.put("fdInterestPending", fdInterest.get("pending"));
|
result.put("fdInterestPending", fdInterest.get("pending"));
|
||||||
result.put("fdInterestDue", fdInterest.get("due"));
|
result.put("fdInterestDue", fdInterest.get("due"));
|
||||||
// Investment interest vouchers (coupon / provision) whose date has come but which are not posted yet.
|
|
||||||
// A reminder only: it never stops the close (Jitendra Sir). Investments are booked at Head Office, so
|
|
||||||
// only the Head Office close shows it.
|
|
||||||
result.put("investmentInterestPending",
|
|
||||||
isHeadOffice(officeId) ? investmentPurchaseReadPlatformService.retrieveDueInterest(0) : List.of());
|
|
||||||
// The same for FDs placed with other banks: interest vouchers and maturities due. A reminder only.
|
|
||||||
result.put("investmentFdPending", isHeadOffice(officeId) ? investmentFdInterestService.due(0) : List.of());
|
|
||||||
|
|
||||||
return gson.toJson(result);
|
return gson.toJson(result);
|
||||||
}
|
}
|
||||||
|
|||||||
+319
@@ -0,0 +1,319 @@
|
|||||||
|
/**
|
||||||
|
* 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.cic.api;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.ws.rs.Consumes;
|
||||||
|
import jakarta.ws.rs.DefaultValue;
|
||||||
|
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.QueryParam;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.fineract.infrastructure.security.exception.NoAuthorizationException;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.cic.service.CicAlertService;
|
||||||
|
import org.apache.fineract.portfolio.cic.service.CicCatalog;
|
||||||
|
import org.apache.fineract.portfolio.cic.service.CicConfigService;
|
||||||
|
import org.apache.fineract.useradministration.domain.AppUser;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CIC / credit bureau: bureau whitelist and configuration, API mapping, and the enquiry alerts the CICs send when one of
|
||||||
|
* the bank's customers is enquired by another lender.
|
||||||
|
*
|
||||||
|
* Permissions: READ_CICBUREAU / UPDATE_CICBUREAU (configuration and mapping), READ_CICALERT (dashboard and register),
|
||||||
|
* UPDATE_CICALERT (work an alert), FETCH_CICALERT (fetch now / import a file).
|
||||||
|
*/
|
||||||
|
@Path("/v1/cic")
|
||||||
|
@Component
|
||||||
|
@Tag(name = "CIC Credit Bureau", description = "Credit bureau (CIBIL, Experian, Equifax, CRIF High Mark) whitelist, API mapping and enquiry alerts.")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CicApiResource {
|
||||||
|
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final CicConfigService configService;
|
||||||
|
private final CicAlertService alertService;
|
||||||
|
private final Gson gson = new GsonBuilder().serializeNulls().create();
|
||||||
|
|
||||||
|
// ── Catalog ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("catalog")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public String catalog() {
|
||||||
|
context.authenticatedUser();
|
||||||
|
final Map<String, Object> c = new LinkedHashMap<>();
|
||||||
|
c.put("environments", CicCatalog.ENVIRONMENTS);
|
||||||
|
c.put("authTypes", CicCatalog.AUTH_TYPES);
|
||||||
|
c.put("endpointTypes", CicCatalog.ENDPOINT_TYPES);
|
||||||
|
c.put("httpMethods", CicCatalog.HTTP_METHODS);
|
||||||
|
c.put("cbsFields", CicCatalog.CBS_FIELDS);
|
||||||
|
c.put("resultFields", CicCatalog.RESULT_FIELDS);
|
||||||
|
c.put("rootFields", CicCatalog.ROOT_FIELDS);
|
||||||
|
c.put("transforms", CicCatalog.TRANSFORMS);
|
||||||
|
c.put("alertStatuses", CicCatalog.ALERT_STATUSES);
|
||||||
|
c.put("matchTypes", CicCatalog.MATCH_TYPES);
|
||||||
|
return gson.toJson(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Bureaus ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("bureaus")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public String bureaus(@QueryParam("whitelistedOnly") @DefaultValue("false") final boolean whitelistedOnly) {
|
||||||
|
requireAny("READ_CICBUREAU", "READ_CICALERT");
|
||||||
|
return gson.toJson(configService.listBureaus(whitelistedOnly));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("bureaus/{id}")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public String bureau(@PathParam("id") final Long id) {
|
||||||
|
require("READ_CICBUREAU");
|
||||||
|
return gson.toJson(configService.getBureau(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PUT
|
||||||
|
@Path("bureaus/{id}")
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public String updateBureau(@PathParam("id") final Long id, final String body) {
|
||||||
|
final AppUser user = require("UPDATE_CICBUREAU");
|
||||||
|
return gson.toJson(configService.updateBureau(id, json(body), user));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PUT
|
||||||
|
@Path("bureaus/{id}/whitelist")
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public String whitelist(@PathParam("id") final Long id, final String body) {
|
||||||
|
final AppUser user = require("UPDATE_CICBUREAU");
|
||||||
|
final JsonObject in = json(body);
|
||||||
|
final boolean whitelisted = in.has("whitelisted") && !in.get("whitelisted").isJsonNull() && in.get("whitelisted").getAsBoolean();
|
||||||
|
return gson.toJson(configService.setWhitelisted(id, whitelisted, user));
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("bureaus/{id}/test-connection")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public String testConnection(@PathParam("id") final Long id) {
|
||||||
|
require("UPDATE_CICBUREAU");
|
||||||
|
return gson.toJson(configService.testConnection(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── API mapping ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("bureaus/{id}/endpoints/{type}")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public String endpoint(@PathParam("id") final Long id, @PathParam("type") final String type) {
|
||||||
|
require("READ_CICBUREAU");
|
||||||
|
return gson.toJson(configService.getEndpoint(id, type));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PUT
|
||||||
|
@Path("bureaus/{id}/endpoints/{type}")
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public String saveEndpoint(@PathParam("id") final Long id, @PathParam("type") final String type, final String body) {
|
||||||
|
final AppUser user = require("UPDATE_CICBUREAU");
|
||||||
|
return gson.toJson(configService.saveEndpoint(id, type, json(body), user));
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("bureaus/{id}/preview")
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public String preview(@PathParam("id") final Long id, final String body) {
|
||||||
|
require("READ_CICBUREAU");
|
||||||
|
return gson.toJson(alertService.preview(id, json(body)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("bureaus/{id}/parse-test")
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public String parseTest(@PathParam("id") final Long id, final String body) {
|
||||||
|
require("READ_CICBUREAU");
|
||||||
|
return gson.toJson(alertService.parseTest(id, json(body)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Fetch / import ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("bureaus/{id}/fetch")
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public String fetch(@PathParam("id") final Long id, final String body) {
|
||||||
|
final AppUser user = require("FETCH_CICALERT");
|
||||||
|
final JsonObject in = json(body);
|
||||||
|
return gson.toJson(alertService.fetch(id, text(in, "fromDate"), text(in, "toDate"), "MANUAL", user));
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("bureaus/{id}/import")
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public String importFile(@PathParam("id") final Long id, final String body) {
|
||||||
|
final AppUser user = require("FETCH_CICALERT");
|
||||||
|
return gson.toJson(alertService.importFile(id, json(body), user));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("fetch-runs")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public String fetchRuns(@QueryParam("bureauId") final Long bureauId, @QueryParam("offset") @DefaultValue("0") final int offset,
|
||||||
|
@QueryParam("limit") @DefaultValue("20") final int limit) {
|
||||||
|
requireAny("READ_CICALERT", "READ_CICBUREAU");
|
||||||
|
return gson.toJson(alertService.fetchRuns(bureauId, Math.max(0, offset), clamp(limit)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("fetch-runs/{id}")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public String fetchRun(@PathParam("id") final Long id) {
|
||||||
|
requireAny("READ_CICALERT", "READ_CICBUREAU");
|
||||||
|
return gson.toJson(alertService.fetchRun(id, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Alerts ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("dashboard")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public String dashboard(@QueryParam("days") @DefaultValue("30") final int days) {
|
||||||
|
require("READ_CICALERT");
|
||||||
|
return gson.toJson(alertService.dashboard(Math.max(1, Math.min(365, days))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("alerts")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public String alerts(@QueryParam("status") final String status, @QueryParam("match") final String match,
|
||||||
|
@QueryParam("clientId") final String clientId, @QueryParam("bureauId") final String bureauId,
|
||||||
|
@QueryParam("officeId") final String officeId, @QueryParam("loanType") final String loanType,
|
||||||
|
@QueryParam("institutionType") final String institutionType, @QueryParam("fromDate") final String fromDate,
|
||||||
|
@QueryParam("toDate") final String toDate, @QueryParam("minAmount") final String minAmount,
|
||||||
|
@QueryParam("search") final String search, @QueryParam("offset") @DefaultValue("0") final int offset,
|
||||||
|
@QueryParam("limit") @DefaultValue("20") final int limit) {
|
||||||
|
require("READ_CICALERT");
|
||||||
|
final JsonObject f = new JsonObject();
|
||||||
|
f.addProperty("status", status);
|
||||||
|
f.addProperty("match", match);
|
||||||
|
f.addProperty("clientId", clientId);
|
||||||
|
f.addProperty("bureauId", bureauId);
|
||||||
|
f.addProperty("officeId", officeId);
|
||||||
|
f.addProperty("loanType", loanType);
|
||||||
|
f.addProperty("institutionType", institutionType);
|
||||||
|
f.addProperty("fromDate", fromDate);
|
||||||
|
f.addProperty("toDate", toDate);
|
||||||
|
f.addProperty("minAmount", minAmount);
|
||||||
|
f.addProperty("search", search);
|
||||||
|
return gson.toJson(alertService.list(f, Math.max(0, offset), Math.max(1, Math.min(5000, limit))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("alerts/options")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public String alertOptions() {
|
||||||
|
require("READ_CICALERT");
|
||||||
|
return gson.toJson(alertService.filterOptions());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("alerts/{id}")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public String alert(@PathParam("id") final Long id) {
|
||||||
|
require("READ_CICALERT");
|
||||||
|
return gson.toJson(alertService.get(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PUT
|
||||||
|
@Path("alerts/{id}/status")
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public String updateStatus(@PathParam("id") final Long id, final String body) {
|
||||||
|
final AppUser user = require("UPDATE_CICALERT");
|
||||||
|
return gson.toJson(alertService.updateStatus(id, json(body), user));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PUT
|
||||||
|
@Path("alerts/{id}/client")
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public String linkClient(@PathParam("id") final Long id, final String body) {
|
||||||
|
final AppUser user = require("UPDATE_CICALERT");
|
||||||
|
return gson.toJson(alertService.linkClient(id, json(body), user));
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("alerts/mark-seen")
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public String markSeen(final String body) {
|
||||||
|
final AppUser user = require("UPDATE_CICALERT");
|
||||||
|
return gson.toJson(alertService.markSeen(json(body), user));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── helpers ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private AppUser require(final String permission) {
|
||||||
|
final AppUser user = context.authenticatedUser();
|
||||||
|
user.validateHasPermissionTo(permission);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AppUser requireAny(final String... permissions) {
|
||||||
|
final AppUser user = context.authenticatedUser();
|
||||||
|
if (user.hasAnyPermission("ALL_FUNCTIONS") || user.hasAnyPermission(permissions)) {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
throw new NoAuthorizationException("User has no authority to " + String.join(" / ", permissions));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonObject json(final String body) {
|
||||||
|
if (StringUtils.isBlank(body)) {
|
||||||
|
return new JsonObject();
|
||||||
|
}
|
||||||
|
final JsonElement e = JsonParser.parseString(body);
|
||||||
|
return e.isJsonObject() ? e.getAsJsonObject() : new JsonObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String text(final JsonObject in, final String key) {
|
||||||
|
return in.has(key) && !in.get(key).isJsonNull() ? in.get(key).getAsString() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int clamp(final int limit) {
|
||||||
|
return Math.max(1, Math.min(200, limit));
|
||||||
|
}
|
||||||
|
}
|
||||||
+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.cic.service;
|
||||||
|
|
||||||
|
import java.sql.Timestamp;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.fineract.infrastructure.businessdate.service.BusinessDateReadPlatformService;
|
||||||
|
import org.apache.fineract.infrastructure.core.config.FineractProperties;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.ActionContext;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.tenant.TenantDetailsService;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches enquiry alerts automatically: every few minutes, for every tenant, each whitelisted bureau with automatic
|
||||||
|
* fetch on is fetched once its interval has passed since the last fetch. Runs only on the batch-manager node.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CicAlertScheduler {
|
||||||
|
|
||||||
|
private final FineractProperties fineractProperties;
|
||||||
|
private final TenantDetailsService tenantDetailsService;
|
||||||
|
private final BusinessDateReadPlatformService businessDateReadPlatformService;
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
private final CicAlertService alertService;
|
||||||
|
|
||||||
|
@Scheduled(fixedDelayString = "${cbs.cic.alert-scheduler-delay-ms:300000}", initialDelayString = "${cbs.cic.alert-scheduler-initial-delay-ms:180000}")
|
||||||
|
public void fetchDueAlerts() {
|
||||||
|
if (!fineractProperties.getMode().isBatchManagerEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (final FineractPlatformTenant tenant : tenantDetailsService.findAllTenants()) {
|
||||||
|
try {
|
||||||
|
ThreadLocalContextUtil.setTenant(tenant);
|
||||||
|
ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
|
||||||
|
ThreadLocalContextUtil.setBusinessDates(businessDateReadPlatformService.getBusinessDates());
|
||||||
|
final List<Map<String, Object>> due = jdbcTemplate.queryForList("""
|
||||||
|
SELECT id, code, alert_fetch_interval_min, last_fetch_on FROM m_cic_bureau
|
||||||
|
WHERE whitelisted = TRUE AND alert_auto_fetch = TRUE
|
||||||
|
""");
|
||||||
|
final long now = Instant.now().toEpochMilli();
|
||||||
|
for (final Map<String, Object> b : due) {
|
||||||
|
final long intervalMs = ((Number) b.get("alert_fetch_interval_min")).longValue() * 60_000L;
|
||||||
|
final Object last = b.get("last_fetch_on");
|
||||||
|
if (last instanceof Timestamp ts && now - ts.getTime() < intervalMs) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
alertService.fetch(((Number) b.get("id")).longValue(), null, null, "SCHEDULED", null);
|
||||||
|
} catch (final RuntimeException e) {
|
||||||
|
// A bureau that is not ready (no URL, no credential ...) is reported on the run / bureau, not retried here.
|
||||||
|
log.warn("CIC scheduled alert fetch of {} for tenant {} failed: {}", b.get("code"), tenant.getTenantIdentifier(),
|
||||||
|
e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (final RuntimeException e) {
|
||||||
|
// A tenant whose database does not have the CIC tables yet.
|
||||||
|
log.debug("CIC alert scheduler skipped tenant {}: {}", tenant.getTenantIdentifier(), e.getMessage());
|
||||||
|
} finally {
|
||||||
|
ThreadLocalContextUtil.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1041
File diff suppressed because it is too large
Load Diff
+215
@@ -0,0 +1,215 @@
|
|||||||
|
/**
|
||||||
|
* 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.cic.service;
|
||||||
|
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonPrimitive;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Everything the API Mapping screen can choose from for the ENQUIRY_ALERT call, and the transforms applied when a
|
||||||
|
* value is written into the bureau request or read back from the bureau response.
|
||||||
|
*/
|
||||||
|
public final class CicCatalog {
|
||||||
|
|
||||||
|
private CicCatalog() {}
|
||||||
|
|
||||||
|
public static final List<String> ENVIRONMENTS = List.of("SIMULATOR", "UAT", "PRODUCTION");
|
||||||
|
public static final List<String> AUTH_TYPES = List.of("NONE", "BASIC", "API_KEY", "BEARER_TOKEN", "OAUTH2_CLIENT_CREDENTIALS");
|
||||||
|
public static final String ALERT_ENDPOINT = "ENQUIRY_ALERT";
|
||||||
|
public static final List<String> ENDPOINT_TYPES = List.of(ALERT_ENDPOINT);
|
||||||
|
public static final List<String> HTTP_METHODS = List.of("POST", "PUT", "GET");
|
||||||
|
public static final List<String> ALERT_STATUSES = List.of("NEW", "SEEN", "ACTIONED", "DISMISSED");
|
||||||
|
public static final List<String> MATCH_TYPES = List.of("PAN", "AADHAAR", "CUSTOMER_REF", "MANUAL", "NONE");
|
||||||
|
|
||||||
|
/** Values a REQUEST mapping can take: the fetch window and the bank's identity at the bureau. */
|
||||||
|
public static final Map<String, String> CBS_FIELDS = new LinkedHashMap<>();
|
||||||
|
static {
|
||||||
|
CBS_FIELDS.put("fetch.reference", "Fetch reference number");
|
||||||
|
CBS_FIELDS.put("fetch.fromDate", "Alerts from date");
|
||||||
|
CBS_FIELDS.put("fetch.toDate", "Alerts to date");
|
||||||
|
CBS_FIELDS.put("fetch.date", "Request date (business date)");
|
||||||
|
CBS_FIELDS.put("bureau.memberCode", "Bank's member code at the bureau");
|
||||||
|
CBS_FIELDS.put("bureau.username", "Bureau user id");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result fields a RESPONSE mapping can fill. errorCode / errorMessage / alerts are read from the root of the
|
||||||
|
* response; "alerts" points at the list, and every other field is read from each item of that list.
|
||||||
|
*/
|
||||||
|
public static final Map<String, String> RESULT_FIELDS = new LinkedHashMap<>();
|
||||||
|
static {
|
||||||
|
RESULT_FIELDS.put("alerts", "List of alerts (array) - root");
|
||||||
|
RESULT_FIELDS.put("errorCode", "Bureau error code - root");
|
||||||
|
RESULT_FIELDS.put("errorMessage", "Bureau error message - root");
|
||||||
|
RESULT_FIELDS.put("alertRef", "Alert / enquiry id at the bureau");
|
||||||
|
RESULT_FIELDS.put("pan", "Customer PAN");
|
||||||
|
RESULT_FIELDS.put("aadhaar", "Customer Aadhaar (masked or last 4)");
|
||||||
|
RESULT_FIELDS.put("customerName", "Customer name");
|
||||||
|
RESULT_FIELDS.put("dateOfBirth", "Customer date of birth");
|
||||||
|
RESULT_FIELDS.put("customerRef", "Our customer reference (CIF / account no) reported to the bureau");
|
||||||
|
RESULT_FIELDS.put("institutionCode", "Enquiring lender's member code");
|
||||||
|
RESULT_FIELDS.put("institutionName", "Enquiring lender (other bank / NBFC)");
|
||||||
|
RESULT_FIELDS.put("institutionType", "Lender type (Bank / NBFC / MFI ...)");
|
||||||
|
RESULT_FIELDS.put("enquiryDate", "Enquiry date");
|
||||||
|
RESULT_FIELDS.put("loanType", "Loan type applied for");
|
||||||
|
RESULT_FIELDS.put("amount", "Loan amount applied for");
|
||||||
|
RESULT_FIELDS.put("purpose", "Enquiry purpose");
|
||||||
|
RESULT_FIELDS.put("location", "Branch / city of the enquiry");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Result fields read from the root of the response, not from each alert. */
|
||||||
|
public static final Set<String> ROOT_FIELDS = Set.of("alerts", "errorCode", "errorMessage");
|
||||||
|
|
||||||
|
public static final Map<String, String> TRANSFORMS = new LinkedHashMap<>();
|
||||||
|
static {
|
||||||
|
TRANSFORMS.put("NONE", "As is");
|
||||||
|
TRANSFORMS.put("UPPER", "UPPER CASE");
|
||||||
|
TRANSFORMS.put("LOWER", "lower case");
|
||||||
|
TRANSFORMS.put("TRIM", "Trim spaces");
|
||||||
|
TRANSFORMS.put("DIGITS_ONLY", "Digits only");
|
||||||
|
TRANSFORMS.put("DATE_DDMMYYYY", "Date ddMMyyyy");
|
||||||
|
TRANSFORMS.put("DATE_DD_MM_YYYY", "Date dd-MM-yyyy");
|
||||||
|
TRANSFORMS.put("DATE_DD_SLASH_MM_YYYY", "Date dd/MM/yyyy");
|
||||||
|
TRANSFORMS.put("DATE_YYYY_MM_DD", "Date yyyy-MM-dd");
|
||||||
|
TRANSFORMS.put("AMOUNT_INTEGER", "Amount, whole rupees");
|
||||||
|
TRANSFORMS.put("NUMBER", "Number");
|
||||||
|
TRANSFORMS.put("BOOLEAN", "true / false");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final Map<String, String> DATE_PATTERNS = Map.of("DATE_DDMMYYYY", "ddMMyyyy", "DATE_DD_MM_YYYY", "dd-MM-yyyy",
|
||||||
|
"DATE_DD_SLASH_MM_YYYY", "dd/MM/yyyy", "DATE_YYYY_MM_DD", "yyyy-MM-dd");
|
||||||
|
|
||||||
|
private static final DateTimeFormatter ISO = DateTimeFormatter.ISO_LOCAL_DATE;
|
||||||
|
|
||||||
|
/** Applies a REQUEST transform to a CBS value; null stays null. Dates come in as ISO yyyy-MM-dd. */
|
||||||
|
public static JsonElement applyRequestTransform(final String transform, final Object raw) {
|
||||||
|
if (raw == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final String t = transform == null || transform.isBlank() ? "NONE" : transform;
|
||||||
|
final String s = String.valueOf(raw).trim();
|
||||||
|
if (s.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (DATE_PATTERNS.containsKey(t)) {
|
||||||
|
try {
|
||||||
|
return new JsonPrimitive(LocalDate.parse(s.length() > 10 ? s.substring(0, 10) : s, ISO)
|
||||||
|
.format(DateTimeFormatter.ofPattern(DATE_PATTERNS.get(t))));
|
||||||
|
} catch (final RuntimeException e) {
|
||||||
|
return new JsonPrimitive(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch (t) {
|
||||||
|
case "UPPER":
|
||||||
|
return new JsonPrimitive(s.toUpperCase(Locale.ROOT));
|
||||||
|
case "LOWER":
|
||||||
|
return new JsonPrimitive(s.toLowerCase(Locale.ROOT));
|
||||||
|
case "TRIM":
|
||||||
|
return new JsonPrimitive(s.replaceAll("\\s+", " "));
|
||||||
|
case "DIGITS_ONLY": {
|
||||||
|
final String digits = s.replaceAll("\\D", "");
|
||||||
|
return digits.isEmpty() ? null : new JsonPrimitive(digits);
|
||||||
|
}
|
||||||
|
case "AMOUNT_INTEGER":
|
||||||
|
return new JsonPrimitive(new BigDecimal(s).setScale(0, RoundingMode.HALF_UP).toBigInteger());
|
||||||
|
case "NUMBER":
|
||||||
|
return new JsonPrimitive(new BigDecimal(s).stripTrailingZeros());
|
||||||
|
case "BOOLEAN":
|
||||||
|
return new JsonPrimitive(Boolean.parseBoolean(s) || "1".equals(s) || "Y".equalsIgnoreCase(s));
|
||||||
|
default:
|
||||||
|
return new JsonPrimitive(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a value read from a bureau response. A DATE_* transform names the format the bureau sends, and the
|
||||||
|
* value comes out as ISO yyyy-MM-dd. Arrays and objects pass through.
|
||||||
|
*/
|
||||||
|
public static JsonElement applyResponseTransform(final String transform, final JsonElement value) {
|
||||||
|
if (value == null || value.isJsonNull() || !value.isJsonPrimitive()) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
final String t = transform == null || transform.isBlank() ? "NONE" : transform;
|
||||||
|
final String s = value.getAsString().trim();
|
||||||
|
try {
|
||||||
|
if (DATE_PATTERNS.containsKey(t)) {
|
||||||
|
return new JsonPrimitive(parseDate(s, DATE_PATTERNS.get(t)).toString());
|
||||||
|
}
|
||||||
|
switch (t) {
|
||||||
|
case "NUMBER":
|
||||||
|
case "AMOUNT_INTEGER": {
|
||||||
|
final String numeric = s.replaceAll("[^0-9.\\-]", "");
|
||||||
|
return numeric.isEmpty() ? null : new JsonPrimitive(new BigDecimal(numeric));
|
||||||
|
}
|
||||||
|
case "BOOLEAN":
|
||||||
|
return new JsonPrimitive("true".equalsIgnoreCase(s) || "1".equals(s) || "Y".equalsIgnoreCase(s) || "YES".equalsIgnoreCase(s));
|
||||||
|
case "UPPER":
|
||||||
|
return new JsonPrimitive(s.toUpperCase(Locale.ROOT));
|
||||||
|
case "LOWER":
|
||||||
|
return new JsonPrimitive(s.toLowerCase(Locale.ROOT));
|
||||||
|
case "TRIM":
|
||||||
|
return new JsonPrimitive(s.replaceAll("\\s+", " "));
|
||||||
|
case "DIGITS_ONLY":
|
||||||
|
return new JsonPrimitive(s.replaceAll("\\D", ""));
|
||||||
|
default:
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
} catch (final RuntimeException e) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parses a bureau date in the given pattern, falling back to ISO. */
|
||||||
|
public static LocalDate parseDate(final String s, final String pattern) {
|
||||||
|
final String v = s.trim();
|
||||||
|
try {
|
||||||
|
return LocalDate.parse(v, DateTimeFormatter.ofPattern(pattern));
|
||||||
|
} catch (final RuntimeException e) {
|
||||||
|
return LocalDate.parse(v.length() > 10 ? v.substring(0, 10) : v, ISO);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ABCDE1234F -> AB****234F style masking for screens and logs. */
|
||||||
|
public static String mask(final String value) {
|
||||||
|
if (value == null || value.length() <= 4) {
|
||||||
|
return value == null ? null : "****";
|
||||||
|
}
|
||||||
|
final int keepStart = Math.min(2, value.length() / 4);
|
||||||
|
final int keepEnd = Math.min(4, value.length() / 3);
|
||||||
|
return value.substring(0, keepStart) + "*".repeat(value.length() - keepStart - keepEnd) + value.substring(value.length() - keepEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Aadhaar shown as XXXX-XXXX-1234 whatever form the bureau sends it in. */
|
||||||
|
public static String maskAadhaar(final String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final String digits = value.replaceAll("\\D", "");
|
||||||
|
return digits.length() < 4 ? null : "XXXX-XXXX-" + digits.substring(digits.length() - 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
+463
@@ -0,0 +1,463 @@
|
|||||||
|
/**
|
||||||
|
* 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.cic.service;
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.sql.Timestamp;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||||
|
import org.apache.fineract.useradministration.domain.AppUser;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The bank's bureau master (whitelist, environment, connection, encrypted credentials) and each bureau's API mapping:
|
||||||
|
* endpoint, headers, base body and the REQUEST / RESPONSE field mappings.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CicConfigService {
|
||||||
|
|
||||||
|
private static final Pattern RESULT_KEY = Pattern.compile("^[A-Za-z][A-Za-z0-9_]{0,99}$");
|
||||||
|
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
private final CicHttpGateway gateway;
|
||||||
|
|
||||||
|
// ── Bureaus ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Full row including the encrypted secret - internal use only, never returned by the API. */
|
||||||
|
public Map<String, Object> bureauRow(final Long id) {
|
||||||
|
final List<Map<String, Object>> rows = jdbcTemplate.queryForList("SELECT * FROM m_cic_bureau WHERE id = ?", id);
|
||||||
|
if (rows.isEmpty()) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.cic.bureau.not.found", "Credit bureau " + id + " was not found.");
|
||||||
|
}
|
||||||
|
return rows.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Map<String, Object>> listBureaus(final boolean whitelistedOnly) {
|
||||||
|
final List<Map<String, Object>> out = new ArrayList<>();
|
||||||
|
for (final Map<String, Object> row : jdbcTemplate.queryForList(
|
||||||
|
"SELECT * FROM m_cic_bureau" + (whitelistedOnly ? " WHERE whitelisted = TRUE" : "") + " ORDER BY is_default DESC, id")) {
|
||||||
|
out.add(publicView(row));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getBureau(final Long id) {
|
||||||
|
return publicView(bureauRow(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A bureau row as the screens see it: never the secret itself, only whether one is set. */
|
||||||
|
private Map<String, Object> publicView(final Map<String, Object> row) {
|
||||||
|
final Map<String, Object> v = new LinkedHashMap<>();
|
||||||
|
final Long id = ((Number) row.get("id")).longValue();
|
||||||
|
v.put("id", id);
|
||||||
|
v.put("code", row.get("code"));
|
||||||
|
v.put("name", row.get("name"));
|
||||||
|
v.put("description", row.get("description"));
|
||||||
|
v.put("whitelisted", row.get("whitelisted"));
|
||||||
|
v.put("isDefault", row.get("is_default"));
|
||||||
|
v.put("environment", row.get("environment"));
|
||||||
|
v.put("baseUrl", row.get("base_url"));
|
||||||
|
v.put("memberCode", row.get("member_code"));
|
||||||
|
v.put("authType", row.get("auth_type"));
|
||||||
|
v.put("username", row.get("username"));
|
||||||
|
v.put("apiKeyHeader", row.get("api_key_header"));
|
||||||
|
v.put("hasSecret", row.get("secret_enc") != null && !row.get("secret_enc").toString().isBlank());
|
||||||
|
v.put("tokenUrl", row.get("token_url"));
|
||||||
|
v.put("clientId", row.get("client_id"));
|
||||||
|
v.put("timeoutMs", row.get("timeout_ms"));
|
||||||
|
v.put("alertAutoFetch", row.get("alert_auto_fetch"));
|
||||||
|
v.put("alertFetchIntervalMin", row.get("alert_fetch_interval_min"));
|
||||||
|
v.put("alertLookbackDays", row.get("alert_lookback_days"));
|
||||||
|
v.put("lastFetchOn", row.get("last_fetch_on") == null ? null : row.get("last_fetch_on").toString());
|
||||||
|
v.put("lastFetchStatus", row.get("last_fetch_status"));
|
||||||
|
v.put("lastFetchMessage", row.get("last_fetch_message"));
|
||||||
|
v.put("lastTestStatus", row.get("last_test_status"));
|
||||||
|
v.put("lastTestMessage", row.get("last_test_message"));
|
||||||
|
v.put("lastTestedOn", row.get("last_tested_on") == null ? null : row.get("last_tested_on").toString());
|
||||||
|
v.put("updatedOn", row.get("updated_on") == null ? null : row.get("updated_on").toString());
|
||||||
|
v.put("activeEndpoints", jdbcTemplate.queryForObject(
|
||||||
|
"SELECT COUNT(*) FROM m_cic_bureau_endpoint WHERE bureau_id = ? AND is_active = TRUE", Integer.class, id));
|
||||||
|
v.put("alertsLast30Days", jdbcTemplate.queryForObject("SELECT COUNT(*) FROM m_cic_alert WHERE bureau_id = ? AND received_on >= ?",
|
||||||
|
Integer.class, id, Timestamp.from(Instant.now().minusSeconds(30L * 86400))));
|
||||||
|
v.put("newAlerts", jdbcTemplate.queryForObject("SELECT COUNT(*) FROM m_cic_alert WHERE bureau_id = ? AND status = 'NEW'",
|
||||||
|
Integer.class, id));
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Map<String, Object> updateBureau(final Long id, final JsonObject in, final AppUser user) {
|
||||||
|
final Map<String, Object> row = bureauRow(id);
|
||||||
|
final String environment = upper(text(in, "environment", (String) row.get("environment")));
|
||||||
|
if (!CicCatalog.ENVIRONMENTS.contains(environment)) {
|
||||||
|
throw fail("error.msg.cic.environment.invalid", "Environment must be one of " + CicCatalog.ENVIRONMENTS + ".");
|
||||||
|
}
|
||||||
|
final String authType = upper(text(in, "authType", (String) row.get("auth_type")));
|
||||||
|
if (!CicCatalog.AUTH_TYPES.contains(authType)) {
|
||||||
|
throw fail("error.msg.cic.auth.invalid", "Authentication must be one of " + CicCatalog.AUTH_TYPES + ".");
|
||||||
|
}
|
||||||
|
final String baseUrl = trimToNull(text(in, "baseUrl", (String) row.get("base_url")));
|
||||||
|
if (baseUrl != null && !StringUtils.startsWithAny(baseUrl, "https://", "http://")) {
|
||||||
|
throw fail("error.msg.cic.url.invalid", "Base URL must start with https:// (or http:// for a test server).");
|
||||||
|
}
|
||||||
|
if ("PRODUCTION".equals(environment) && baseUrl != null && !baseUrl.startsWith("https://")) {
|
||||||
|
throw fail("error.msg.cic.url.https", "A PRODUCTION bureau must be reached over https://.");
|
||||||
|
}
|
||||||
|
final String tokenUrl = trimToNull(text(in, "tokenUrl", (String) row.get("token_url")));
|
||||||
|
final int timeoutMs = intOf(in, "timeoutMs", ((Number) row.get("timeout_ms")).intValue());
|
||||||
|
if (timeoutMs < 1000 || timeoutMs > 300000) {
|
||||||
|
throw fail("error.msg.cic.timeout.invalid", "Timeout must be between 1,000 and 300,000 milliseconds.");
|
||||||
|
}
|
||||||
|
final boolean autoFetch = in.has("alertAutoFetch") && !in.get("alertAutoFetch").isJsonNull() ? in.get("alertAutoFetch").getAsBoolean()
|
||||||
|
: Boolean.TRUE.equals(row.get("alert_auto_fetch"));
|
||||||
|
final int interval = intOf(in, "alertFetchIntervalMin", ((Number) row.get("alert_fetch_interval_min")).intValue());
|
||||||
|
if (interval < 15 || interval > 1440) {
|
||||||
|
throw fail("error.msg.cic.interval.invalid", "Automatic fetch interval must be between 15 minutes and 24 hours (1440).");
|
||||||
|
}
|
||||||
|
final int lookback = intOf(in, "alertLookbackDays", ((Number) row.get("alert_lookback_days")).intValue());
|
||||||
|
if (lookback < 1 || lookback > 90) {
|
||||||
|
throw fail("error.msg.cic.lookback.invalid", "Look-back window must be between 1 and 90 days.");
|
||||||
|
}
|
||||||
|
|
||||||
|
String secretEnc = (String) row.get("secret_enc");
|
||||||
|
if (in.has("clearSecret") && in.get("clearSecret").getAsBoolean()) {
|
||||||
|
secretEnc = null;
|
||||||
|
} else if (in.has("secret") && !in.get("secret").isJsonNull() && !in.get("secret").getAsString().isBlank()) {
|
||||||
|
secretEnc = gateway.encrypt(in.get("secret").getAsString());
|
||||||
|
}
|
||||||
|
|
||||||
|
final boolean makeDefault = in.has("isDefault") && !in.get("isDefault").isJsonNull() && in.get("isDefault").getAsBoolean();
|
||||||
|
if (makeDefault) {
|
||||||
|
jdbcTemplate.update("UPDATE m_cic_bureau SET is_default = FALSE WHERE id <> ?", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
jdbcTemplate.update("""
|
||||||
|
UPDATE m_cic_bureau SET name = ?, description = ?, environment = ?, base_url = ?, member_code = ?, auth_type = ?,
|
||||||
|
username = ?, api_key_header = ?, secret_enc = ?, token_url = ?, client_id = ?, timeout_ms = ?,
|
||||||
|
alert_auto_fetch = ?, alert_fetch_interval_min = ?, alert_lookback_days = ?, is_default = ?, updated_by = ?,
|
||||||
|
updated_on = ?
|
||||||
|
WHERE id = ?
|
||||||
|
""", StringUtils.defaultIfBlank(text(in, "name", (String) row.get("name")), (String) row.get("name")),
|
||||||
|
trimToNull(text(in, "description", (String) row.get("description"))), environment, baseUrl,
|
||||||
|
trimToNull(text(in, "memberCode", (String) row.get("member_code"))), authType,
|
||||||
|
trimToNull(text(in, "username", (String) row.get("username"))),
|
||||||
|
StringUtils.defaultIfBlank(text(in, "apiKeyHeader", (String) row.get("api_key_header")), "X-API-KEY"), secretEnc, tokenUrl,
|
||||||
|
trimToNull(text(in, "clientId", (String) row.get("client_id"))), timeoutMs, autoFetch, interval, lookback,
|
||||||
|
makeDefault || Boolean.TRUE.equals(row.get("is_default")), user.getId(), Timestamp.from(Instant.now()), id);
|
||||||
|
gateway.evictToken(id);
|
||||||
|
|
||||||
|
// A whitelisted bureau must stay usable after the edit; otherwise every alert fetch would fail silently.
|
||||||
|
if (Boolean.TRUE.equals(row.get("whitelisted"))) {
|
||||||
|
assertReadyForLive(bureauRow(id));
|
||||||
|
}
|
||||||
|
return getBureau(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Map<String, Object> setWhitelisted(final Long id, final boolean whitelisted, final AppUser user) {
|
||||||
|
final Map<String, Object> row = bureauRow(id);
|
||||||
|
if (whitelisted) {
|
||||||
|
assertReadyForLive(row);
|
||||||
|
}
|
||||||
|
jdbcTemplate.update("UPDATE m_cic_bureau SET whitelisted = ?, updated_by = ?, updated_on = ? WHERE id = ?", whitelisted,
|
||||||
|
user.getId(), Timestamp.from(Instant.now()), id);
|
||||||
|
return getBureau(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** What a bureau needs before alerts may be fetched from it. */
|
||||||
|
public void assertReadyForLive(final Map<String, Object> row) {
|
||||||
|
final String code = (String) row.get("code");
|
||||||
|
final Integer endpoints = jdbcTemplate.queryForObject(
|
||||||
|
"SELECT COUNT(*) FROM m_cic_bureau_endpoint WHERE bureau_id = ? AND is_active = TRUE", Integer.class, row.get("id"));
|
||||||
|
if (endpoints == null || endpoints == 0) {
|
||||||
|
throw fail("error.msg.cic.no.endpoint", code + " has no active API endpoint. Configure it under API Mapping first.");
|
||||||
|
}
|
||||||
|
if ("SIMULATOR".equals(row.get("environment"))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank((String) row.get("base_url"))) {
|
||||||
|
throw fail("error.msg.cic.no.url", code + " has no base URL for " + row.get("environment") + ".");
|
||||||
|
}
|
||||||
|
final String auth = (String) row.get("auth_type");
|
||||||
|
final boolean hasSecret = row.get("secret_enc") != null && !row.get("secret_enc").toString().isBlank();
|
||||||
|
if (!"NONE".equals(auth) && !hasSecret) {
|
||||||
|
throw fail("error.msg.cic.no.secret", code + " uses " + auth + " but no password / key / secret is stored.");
|
||||||
|
}
|
||||||
|
if ("BASIC".equals(auth) && StringUtils.isBlank((String) row.get("username"))) {
|
||||||
|
throw fail("error.msg.cic.no.username", code + " uses BASIC authentication but has no username.");
|
||||||
|
}
|
||||||
|
if ("OAUTH2_CLIENT_CREDENTIALS".equals(auth)
|
||||||
|
&& (StringUtils.isBlank((String) row.get("token_url")) || StringUtils.isBlank((String) row.get("client_id")))) {
|
||||||
|
throw fail("error.msg.cic.no.oauth", code + " uses OAuth2 but the token URL or client id is missing.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reachability and credentials check; the result is kept on the bureau for the configuration screen. */
|
||||||
|
public Map<String, Object> testConnection(final Long id) {
|
||||||
|
final Map<String, Object> row = bureauRow(id);
|
||||||
|
String status;
|
||||||
|
String message;
|
||||||
|
if ("SIMULATOR".equals(row.get("environment"))) {
|
||||||
|
status = "OK";
|
||||||
|
message = "Simulator mode - no network call is made; alerts are answered by the built-in simulator.";
|
||||||
|
} else if (StringUtils.isBlank((String) row.get("base_url"))) {
|
||||||
|
status = "FAILED";
|
||||||
|
message = "No base URL configured.";
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
if ("OAUTH2_CLIENT_CREDENTIALS".equals(row.get("auth_type"))) {
|
||||||
|
gateway.evictToken(id);
|
||||||
|
gateway.oauthToken(row);
|
||||||
|
}
|
||||||
|
final CicHttpGateway.HttpResult r = gateway.send(row, "GET", (String) row.get("base_url"), Map.of(), null, null);
|
||||||
|
if (r.status() == 0) {
|
||||||
|
status = "FAILED";
|
||||||
|
message = "Not reachable: " + r.error();
|
||||||
|
} else if (r.status() == 401 || r.status() == 403) {
|
||||||
|
status = "FAILED";
|
||||||
|
message = "Reachable, but the bureau refused the credentials (HTTP " + r.status() + ").";
|
||||||
|
} else {
|
||||||
|
status = "OK";
|
||||||
|
message = "Reachable (HTTP " + r.status() + " in " + r.durationMs() + " ms)"
|
||||||
|
+ ("OAUTH2_CLIENT_CREDENTIALS".equals(row.get("auth_type")) ? ", OAuth2 token obtained." : ".");
|
||||||
|
}
|
||||||
|
} catch (final IOException | RuntimeException e) {
|
||||||
|
status = "FAILED";
|
||||||
|
message = e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jdbcTemplate.update("UPDATE m_cic_bureau SET last_test_status = ?, last_test_message = ?, last_tested_on = ? WHERE id = ?", status,
|
||||||
|
StringUtils.abbreviate(message, 1000), Timestamp.from(Instant.now()), id);
|
||||||
|
final Map<String, Object> res = new LinkedHashMap<>();
|
||||||
|
res.put("status", status);
|
||||||
|
res.put("message", message);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Endpoints and mappings ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public Map<String, Object> endpointRow(final Long bureauId, final String type) {
|
||||||
|
final List<Map<String, Object>> rows = jdbcTemplate.queryForList(
|
||||||
|
"SELECT * FROM m_cic_bureau_endpoint WHERE bureau_id = ? AND endpoint_type = ?", bureauId, type);
|
||||||
|
return rows.isEmpty() ? null : rows.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Map<String, Object>> mappingRows(final Long endpointId, final String direction) {
|
||||||
|
return jdbcTemplate.queryForList(
|
||||||
|
"SELECT * FROM m_cic_field_mapping WHERE endpoint_id = ? AND direction = ? ORDER BY sort_order, id", endpointId, direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getEndpoint(final Long bureauId, final String type) {
|
||||||
|
bureauRow(bureauId);
|
||||||
|
final String t = upper(type);
|
||||||
|
if (!CicCatalog.ENDPOINT_TYPES.contains(t)) {
|
||||||
|
throw fail("error.msg.cic.endpoint.type.invalid", "Endpoint type must be one of " + CicCatalog.ENDPOINT_TYPES + ".");
|
||||||
|
}
|
||||||
|
final Map<String, Object> e = endpointRow(bureauId, t);
|
||||||
|
final Map<String, Object> v = new LinkedHashMap<>();
|
||||||
|
v.put("bureauId", bureauId);
|
||||||
|
v.put("endpointType", t);
|
||||||
|
v.put("exists", e != null);
|
||||||
|
v.put("id", e == null ? null : e.get("id"));
|
||||||
|
v.put("httpMethod", e == null ? "POST" : e.get("http_method"));
|
||||||
|
v.put("path", e == null ? null : e.get("path"));
|
||||||
|
v.put("contentType", e == null ? "application/json" : e.get("content_type"));
|
||||||
|
v.put("headersJson", e == null ? "{}" : e.get("headers_json"));
|
||||||
|
v.put("baseBody", e == null ? "{}" : e.get("base_body"));
|
||||||
|
v.put("isActive", e == null ? Boolean.FALSE : e.get("is_active"));
|
||||||
|
v.put("requestMappings", e == null ? List.of() : mappingView(mappingRows(((Number) e.get("id")).longValue(), "REQUEST")));
|
||||||
|
v.put("responseMappings", e == null ? List.of() : mappingView(mappingRows(((Number) e.get("id")).longValue(), "RESPONSE")));
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Map<String, Object>> mappingView(final List<Map<String, Object>> rows) {
|
||||||
|
final List<Map<String, Object>> out = new ArrayList<>();
|
||||||
|
for (final Map<String, Object> m : rows) {
|
||||||
|
final Map<String, Object> v = new LinkedHashMap<>();
|
||||||
|
v.put("id", m.get("id"));
|
||||||
|
v.put("sourceField", m.get("source_field"));
|
||||||
|
v.put("targetField", m.get("target_field"));
|
||||||
|
v.put("transform", m.get("transform"));
|
||||||
|
v.put("defaultValue", m.get("default_value"));
|
||||||
|
v.put("mandatory", m.get("is_mandatory"));
|
||||||
|
v.put("sortOrder", m.get("sort_order"));
|
||||||
|
out.add(v);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Saves an endpoint and replaces its whole mapping in one transaction, after validating every row. */
|
||||||
|
@Transactional
|
||||||
|
public Map<String, Object> saveEndpoint(final Long bureauId, final String type, final JsonObject in, final AppUser user) {
|
||||||
|
final Map<String, Object> bureau = bureauRow(bureauId);
|
||||||
|
final String t = upper(type);
|
||||||
|
if (!CicCatalog.ENDPOINT_TYPES.contains(t)) {
|
||||||
|
throw fail("error.msg.cic.endpoint.type.invalid", "Endpoint type must be one of " + CicCatalog.ENDPOINT_TYPES + ".");
|
||||||
|
}
|
||||||
|
final String method = upper(StringUtils.defaultIfBlank(text(in, "httpMethod", "POST"), "POST"));
|
||||||
|
if (!CicCatalog.HTTP_METHODS.contains(method)) {
|
||||||
|
throw fail("error.msg.cic.method.invalid", "HTTP method must be one of " + CicCatalog.HTTP_METHODS + ".");
|
||||||
|
}
|
||||||
|
final String headersJson = StringUtils.defaultIfBlank(text(in, "headersJson", "{}"), "{}");
|
||||||
|
final String baseBody = StringUtils.defaultIfBlank(text(in, "baseBody", "{}"), "{}");
|
||||||
|
requireJsonObject(headersJson, "Headers");
|
||||||
|
requireJsonObject(baseBody, "Base request body");
|
||||||
|
for (final Map.Entry<String, JsonElement> h : JsonParser.parseString(headersJson).getAsJsonObject().entrySet()) {
|
||||||
|
if (!h.getValue().isJsonPrimitive()) {
|
||||||
|
throw fail("error.msg.cic.header.invalid", "Header '" + h.getKey() + "' must be a plain text value.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final boolean active = !in.has("isActive") || in.get("isActive").isJsonNull() || in.get("isActive").getAsBoolean();
|
||||||
|
|
||||||
|
final List<Object[]> rows = new ArrayList<>();
|
||||||
|
validateMappings(in.has("requestMappings") ? in.getAsJsonArray("requestMappings") : new JsonArray(), "REQUEST", rows);
|
||||||
|
validateMappings(in.has("responseMappings") ? in.getAsJsonArray("responseMappings") : new JsonArray(), "RESPONSE", rows);
|
||||||
|
if (active && rows.stream().noneMatch(r -> "REQUEST".equals(r[0]))) {
|
||||||
|
throw fail("error.msg.cic.no.request.mapping", "An active endpoint needs at least one REQUEST mapping.");
|
||||||
|
}
|
||||||
|
if (active && rows.stream().noneMatch(r -> "RESPONSE".equals(r[0]) && "alerts".equals(r[2]))) {
|
||||||
|
throw fail("error.msg.cic.no.alert.list",
|
||||||
|
"Map the response path of the alert list to the result field 'alerts' (e.g. data.alerts), or the alerts cannot be read.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> existing = endpointRow(bureauId, t);
|
||||||
|
final Timestamp now = Timestamp.from(Instant.now());
|
||||||
|
if (existing == null) {
|
||||||
|
jdbcTemplate.update("""
|
||||||
|
INSERT INTO m_cic_bureau_endpoint (bureau_id, endpoint_type, http_method, path, content_type, headers_json, base_body,
|
||||||
|
is_active, updated_by, updated_on)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""", bureauId, t, method, trimToNull(text(in, "path", null)),
|
||||||
|
StringUtils.defaultIfBlank(text(in, "contentType", "application/json"), "application/json"), headersJson, baseBody, active,
|
||||||
|
user.getId(), now);
|
||||||
|
existing = endpointRow(bureauId, t);
|
||||||
|
} else {
|
||||||
|
jdbcTemplate.update("""
|
||||||
|
UPDATE m_cic_bureau_endpoint SET http_method = ?, path = ?, content_type = ?, headers_json = ?, base_body = ?, is_active = ?,
|
||||||
|
updated_by = ?, updated_on = ?
|
||||||
|
WHERE id = ?
|
||||||
|
""", method, trimToNull(text(in, "path", null)),
|
||||||
|
StringUtils.defaultIfBlank(text(in, "contentType", "application/json"), "application/json"), headersJson, baseBody, active,
|
||||||
|
user.getId(), now, existing.get("id"));
|
||||||
|
}
|
||||||
|
final Long endpointId = ((Number) existing.get("id")).longValue();
|
||||||
|
jdbcTemplate.update("DELETE FROM m_cic_field_mapping WHERE endpoint_id = ?", endpointId);
|
||||||
|
for (final Object[] r : rows) {
|
||||||
|
jdbcTemplate.update("""
|
||||||
|
INSERT INTO m_cic_field_mapping (endpoint_id, direction, source_field, target_field, transform, default_value, is_mandatory,
|
||||||
|
sort_order)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""", endpointId, r[0], r[1], r[2], r[3], r[4], r[5], r[6]);
|
||||||
|
}
|
||||||
|
// Switching off the last active endpoint of a whitelisted bureau would leave it whitelisted but unusable.
|
||||||
|
if (Boolean.TRUE.equals(bureau.get("whitelisted"))) {
|
||||||
|
assertReadyForLive(bureauRow(bureauId));
|
||||||
|
}
|
||||||
|
return getEndpoint(bureauId, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateMappings(final JsonArray array, final String direction, final List<Object[]> out) {
|
||||||
|
int order = 0;
|
||||||
|
for (final JsonElement el : array) {
|
||||||
|
final JsonObject m = el.getAsJsonObject();
|
||||||
|
order += 10;
|
||||||
|
final String source = trimToNull(text(m, "sourceField", null));
|
||||||
|
final String target = trimToNull(text(m, "targetField", null));
|
||||||
|
final String transform = StringUtils.defaultIfBlank(upper(text(m, "transform", "NONE")), "NONE");
|
||||||
|
final String defaultValue = trimToNull(text(m, "defaultValue", null));
|
||||||
|
final boolean mandatory = m.has("mandatory") && !m.get("mandatory").isJsonNull() && m.get("mandatory").getAsBoolean();
|
||||||
|
final int sortOrder = m.has("sortOrder") && !m.get("sortOrder").isJsonNull() ? m.get("sortOrder").getAsInt() : order;
|
||||||
|
final String label = direction + " row " + (order / 10);
|
||||||
|
if (!CicCatalog.TRANSFORMS.containsKey(transform)) {
|
||||||
|
throw fail("error.msg.cic.transform.invalid", label + ": unknown transform '" + transform + "'.");
|
||||||
|
}
|
||||||
|
if (target == null) {
|
||||||
|
throw fail("error.msg.cic.mapping.target.required", label + ": the target field is required.");
|
||||||
|
}
|
||||||
|
if ("REQUEST".equals(direction)) {
|
||||||
|
if (source == null && defaultValue == null) {
|
||||||
|
throw fail("error.msg.cic.mapping.source.required", label + " (" + target + "): choose a CBS field or give a fixed value.");
|
||||||
|
}
|
||||||
|
if (source != null && !CicCatalog.CBS_FIELDS.containsKey(source)) {
|
||||||
|
throw fail("error.msg.cic.mapping.source.invalid", label + ": unknown CBS field '" + source + "'.");
|
||||||
|
}
|
||||||
|
if (!CicJsonPath.isValid(target)) {
|
||||||
|
throw fail("error.msg.cic.mapping.path.invalid", label + ": '" + target + "' is not a valid JSON path (e.g. consumer.ids[0].number).");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (source == null || !CicJsonPath.isValid(source)) {
|
||||||
|
throw fail("error.msg.cic.mapping.path.invalid",
|
||||||
|
label + ": the bureau response path is missing or invalid (e.g. report.score.value).");
|
||||||
|
}
|
||||||
|
if (!RESULT_KEY.matcher(target).matches()) {
|
||||||
|
throw fail("error.msg.cic.mapping.result.invalid", label + ": result field '" + target + "' must be a simple name.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.add(new Object[] { direction, source, target, transform, defaultValue, mandatory, sortOrder });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── helpers ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static void requireJsonObject(final String text, final String label) {
|
||||||
|
try {
|
||||||
|
if (!JsonParser.parseString(text).isJsonObject()) {
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
|
} catch (final RuntimeException e) {
|
||||||
|
throw fail("error.msg.cic.json.invalid", label + " must be a valid JSON object.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static String text(final JsonObject in, final String key, final String fallback) {
|
||||||
|
if (in == null || !in.has(key)) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
return in.get(key).isJsonNull() ? null : in.get(key).getAsString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int intOf(final JsonObject in, final String key, final int fallback) {
|
||||||
|
return in.has(key) && !in.get(key).isJsonNull() ? in.get(key).getAsInt() : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String upper(final String s) {
|
||||||
|
return s == null ? null : s.trim().toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String trimToNull(final String s) {
|
||||||
|
return StringUtils.trimToNull(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
static GeneralPlatformDomainRuleException fail(final String code, final String message) {
|
||||||
|
return new GeneralPlatformDomainRuleException(code, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
+194
@@ -0,0 +1,194 @@
|
|||||||
|
/**
|
||||||
|
* 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.cic.service;
|
||||||
|
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import okhttp3.FormBody;
|
||||||
|
import okhttp3.MediaType;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.RequestBody;
|
||||||
|
import okhttp3.Response;
|
||||||
|
import okhttp3.ResponseBody;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.database.DatabasePasswordEncryptor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Outbound HTTP to a credit bureau: URL joining, authentication (Basic, API key, bearer, OAuth2 client credentials
|
||||||
|
* with an in-memory token cache), per-bureau timeouts. The bureau's secret is stored encrypted and is only decrypted
|
||||||
|
* here, for the call itself.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CicHttpGateway {
|
||||||
|
|
||||||
|
private static final int MAX_BODY_CHARS = 2_000_000;
|
||||||
|
|
||||||
|
private final DatabasePasswordEncryptor encryptor;
|
||||||
|
private final Map<Long, CachedToken> tokens = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private record CachedToken(String token, Instant expiresAt) {}
|
||||||
|
|
||||||
|
/** What came back from the bureau. A transport failure is a status of 0 with the error in {@code body}. */
|
||||||
|
public record HttpResult(int status, String body, long durationMs, String error) {
|
||||||
|
|
||||||
|
public boolean ok() {
|
||||||
|
return status >= 200 && status < 300;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String encrypt(final String plain) {
|
||||||
|
return plain == null ? null : encryptor.encrypt(plain);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String secretOf(final Map<String, Object> bureau) {
|
||||||
|
final Object enc = bureau.get("secret_enc");
|
||||||
|
if (enc == null || enc.toString().isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return encryptor.decrypt(enc.toString());
|
||||||
|
} catch (final RuntimeException e) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.cic.secret.unreadable",
|
||||||
|
"The stored credential of " + bureau.get("code") + " cannot be decrypted (was the master password changed?)."
|
||||||
|
+ " Re-enter it on the bureau configuration.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String joinUrl(final String baseUrl, final String path) {
|
||||||
|
if (StringUtils.startsWithAny(path, "http://", "https://")) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
final String base = StringUtils.defaultString(baseUrl).replaceAll("/+$", "");
|
||||||
|
if (StringUtils.isBlank(path)) {
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
return base + (path.startsWith("/") ? path : "/" + path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private OkHttpClient client(final Map<String, Object> bureau) {
|
||||||
|
final int timeout = bureau.get("timeout_ms") instanceof Number n && n.intValue() > 0 ? n.intValue() : 30000;
|
||||||
|
return new OkHttpClient.Builder().connectTimeout(timeout, TimeUnit.MILLISECONDS).readTimeout(timeout, TimeUnit.MILLISECONDS)
|
||||||
|
.writeTimeout(timeout, TimeUnit.MILLISECONDS).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sends one request to the bureau with its configured authentication. */
|
||||||
|
public HttpResult send(final Map<String, Object> bureau, final String method, final String url, final Map<String, String> headers,
|
||||||
|
final String contentType, final String body) {
|
||||||
|
final long started = System.currentTimeMillis();
|
||||||
|
try {
|
||||||
|
final Request.Builder rb = new Request.Builder().url(url);
|
||||||
|
headers.forEach((k, v) -> {
|
||||||
|
if (StringUtils.isNotBlank(k) && v != null) {
|
||||||
|
rb.header(k, v);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
applyAuth(bureau, rb);
|
||||||
|
final String m = StringUtils.defaultIfBlank(method, "POST").toUpperCase();
|
||||||
|
if ("GET".equals(m)) {
|
||||||
|
rb.get();
|
||||||
|
} else {
|
||||||
|
final MediaType mediaType = MediaType.parse(StringUtils.defaultIfBlank(contentType, "application/json"));
|
||||||
|
rb.method(m, RequestBody.create(StringUtils.defaultString(body), mediaType));
|
||||||
|
}
|
||||||
|
try (Response response = client(bureau).newCall(rb.build()).execute()) {
|
||||||
|
return new HttpResult(response.code(), read(response.body()), System.currentTimeMillis() - started, null);
|
||||||
|
}
|
||||||
|
} catch (final IOException | IllegalArgumentException e) {
|
||||||
|
log.warn("CIC {}: call to {} failed: {}", bureau.get("code"), url, e.getMessage());
|
||||||
|
return new HttpResult(0, null, System.currentTimeMillis() - started, e.getClass().getSimpleName() + ": " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String read(final ResponseBody body) throws IOException {
|
||||||
|
if (body == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final String text = new String(body.bytes(), StandardCharsets.UTF_8);
|
||||||
|
return text.length() > MAX_BODY_CHARS ? text.substring(0, MAX_BODY_CHARS) : text;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyAuth(final Map<String, Object> bureau, final Request.Builder rb) throws IOException {
|
||||||
|
final String auth = StringUtils.defaultIfBlank((String) bureau.get("auth_type"), "NONE");
|
||||||
|
final String username = (String) bureau.get("username");
|
||||||
|
switch (auth) {
|
||||||
|
case "BASIC" -> {
|
||||||
|
final String raw = StringUtils.defaultString(username) + ":" + StringUtils.defaultString(secretOf(bureau));
|
||||||
|
rb.header("Authorization", "Basic " + Base64.getEncoder().encodeToString(raw.getBytes(StandardCharsets.UTF_8)));
|
||||||
|
}
|
||||||
|
case "API_KEY" -> rb.header(StringUtils.defaultIfBlank((String) bureau.get("api_key_header"), "X-API-KEY"),
|
||||||
|
StringUtils.defaultString(secretOf(bureau)));
|
||||||
|
case "BEARER_TOKEN" -> rb.header("Authorization", "Bearer " + StringUtils.defaultString(secretOf(bureau)));
|
||||||
|
case "OAUTH2_CLIENT_CREDENTIALS" -> rb.header("Authorization", "Bearer " + oauthToken(bureau));
|
||||||
|
default -> {
|
||||||
|
// NONE: the bureau authenticates by IP whitelisting / mutual TLS at the network layer.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** OAuth2 client-credentials token, cached until a minute before it expires. */
|
||||||
|
public String oauthToken(final Map<String, Object> bureau) throws IOException {
|
||||||
|
final Long id = ((Number) bureau.get("id")).longValue();
|
||||||
|
final CachedToken cached = tokens.get(id);
|
||||||
|
if (cached != null && Instant.now().isBefore(cached.expiresAt())) {
|
||||||
|
return cached.token();
|
||||||
|
}
|
||||||
|
final String tokenUrl = (String) bureau.get("token_url");
|
||||||
|
if (StringUtils.isBlank(tokenUrl)) {
|
||||||
|
throw new IOException("OAuth2 token URL is not configured");
|
||||||
|
}
|
||||||
|
final FormBody form = new FormBody.Builder().add("grant_type", "client_credentials")
|
||||||
|
.add("client_id", StringUtils.defaultString((String) bureau.get("client_id")))
|
||||||
|
.add("client_secret", StringUtils.defaultString(secretOf(bureau))).build();
|
||||||
|
try (Response response = client(bureau).newCall(new Request.Builder().url(tokenUrl).post(form).build()).execute()) {
|
||||||
|
final String text = read(response.body());
|
||||||
|
if (!response.isSuccessful()) {
|
||||||
|
throw new IOException("Token endpoint returned HTTP " + response.code() + ": " + StringUtils.abbreviate(text, 300));
|
||||||
|
}
|
||||||
|
final JsonElement json = JsonParser.parseString(StringUtils.defaultIfBlank(text, "{}"));
|
||||||
|
final JsonObject obj = json.isJsonObject() ? json.getAsJsonObject() : new JsonObject();
|
||||||
|
if (!obj.has("access_token")) {
|
||||||
|
throw new IOException("Token endpoint response has no access_token");
|
||||||
|
}
|
||||||
|
final long expiresIn = obj.has("expires_in") ? obj.get("expires_in").getAsLong() : 300L;
|
||||||
|
final String token = obj.get("access_token").getAsString();
|
||||||
|
tokens.put(id, new CachedToken(token, Instant.now().plusSeconds(Math.max(30, expiresIn - 60))));
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Forget a cached token (after the bureau's credentials change). */
|
||||||
|
public void evictToken(final Long bureauId) {
|
||||||
|
tokens.remove(bureauId);
|
||||||
|
}
|
||||||
|
}
|
||||||
+133
@@ -0,0 +1,133 @@
|
|||||||
|
/**
|
||||||
|
* 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.cic.service;
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonNull;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimal dotted JSON path used by the bureau API mapping: {@code consumer.name.firstName},
|
||||||
|
* {@code consumer.ids[0].number}, {@code report.score.value}. Setting creates the missing objects and arrays on the
|
||||||
|
* way; reading returns null for anything absent.
|
||||||
|
*/
|
||||||
|
public final class CicJsonPath {
|
||||||
|
|
||||||
|
private static final Pattern SEGMENT = Pattern.compile("^([^\\[\\]]*)((?:\\[\\d+\\])*)$");
|
||||||
|
private static final Pattern INDEX = Pattern.compile("\\[(\\d+)\\]");
|
||||||
|
|
||||||
|
private CicJsonPath() {}
|
||||||
|
|
||||||
|
/** One step of a path: an object key, optionally followed by array indexes. */
|
||||||
|
private record Step(String key, List<Integer> indexes) {}
|
||||||
|
|
||||||
|
private static List<Step> parse(final String path) {
|
||||||
|
if (path == null || path.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("Empty JSON path");
|
||||||
|
}
|
||||||
|
final List<Step> steps = new ArrayList<>();
|
||||||
|
for (final String raw : path.trim().split("\\.")) {
|
||||||
|
final Matcher m = SEGMENT.matcher(raw);
|
||||||
|
if (!m.matches()) {
|
||||||
|
throw new IllegalArgumentException("Invalid JSON path segment '" + raw + "' in '" + path + "'");
|
||||||
|
}
|
||||||
|
final List<Integer> indexes = new ArrayList<>();
|
||||||
|
final Matcher im = INDEX.matcher(m.group(2));
|
||||||
|
while (im.find()) {
|
||||||
|
indexes.add(Integer.parseInt(im.group(1)));
|
||||||
|
}
|
||||||
|
steps.add(new Step(m.group(1), indexes));
|
||||||
|
}
|
||||||
|
return steps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether the path is syntactically valid. */
|
||||||
|
public static boolean isValid(final String path) {
|
||||||
|
try {
|
||||||
|
parse(path);
|
||||||
|
return true;
|
||||||
|
} catch (final IllegalArgumentException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JsonElement get(final JsonElement root, final String path) {
|
||||||
|
JsonElement current = root;
|
||||||
|
for (final Step step : parse(path)) {
|
||||||
|
if (!step.key().isEmpty()) {
|
||||||
|
if (current == null || !current.isJsonObject()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
current = current.getAsJsonObject().get(step.key());
|
||||||
|
}
|
||||||
|
for (final Integer index : step.indexes()) {
|
||||||
|
if (current == null || !current.isJsonArray() || current.getAsJsonArray().size() <= index) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
current = current.getAsJsonArray().get(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return current == null || current.isJsonNull() ? null : current;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void set(final JsonObject root, final String path, final JsonElement value) {
|
||||||
|
final List<Step> steps = parse(path);
|
||||||
|
JsonElement current = root;
|
||||||
|
for (int s = 0; s < steps.size(); s++) {
|
||||||
|
final Step step = steps.get(s);
|
||||||
|
final boolean lastStep = s == steps.size() - 1;
|
||||||
|
if (!step.key().isEmpty()) {
|
||||||
|
final JsonObject obj = current.getAsJsonObject();
|
||||||
|
if (lastStep && step.indexes().isEmpty()) {
|
||||||
|
obj.add(step.key(), value == null ? JsonNull.INSTANCE : value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JsonElement next = obj.get(step.key());
|
||||||
|
if (next == null || next.isJsonNull()) {
|
||||||
|
next = step.indexes().isEmpty() ? new JsonObject() : new JsonArray();
|
||||||
|
obj.add(step.key(), next);
|
||||||
|
}
|
||||||
|
current = next;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < step.indexes().size(); i++) {
|
||||||
|
final int index = step.indexes().get(i);
|
||||||
|
final boolean lastIndex = lastStep && i == step.indexes().size() - 1;
|
||||||
|
final JsonArray array = current.getAsJsonArray();
|
||||||
|
while (array.size() <= index) {
|
||||||
|
array.add(JsonNull.INSTANCE);
|
||||||
|
}
|
||||||
|
if (lastIndex) {
|
||||||
|
array.set(index, value == null ? JsonNull.INSTANCE : value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JsonElement next = array.get(index);
|
||||||
|
if (next == null || next.isJsonNull()) {
|
||||||
|
next = i < step.indexes().size() - 1 ? new JsonArray() : new JsonObject();
|
||||||
|
array.set(index, next);
|
||||||
|
}
|
||||||
|
current = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+153
@@ -0,0 +1,153 @@
|
|||||||
|
/**
|
||||||
|
* 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.cic.service;
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Random;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stand-in CIC for the SIMULATOR environment. Plays the bureau's enquiry-alert service: for every day of the asked
|
||||||
|
* window it reports a few of this bank's own customers as enquired by other lenders, in the shape of the seeded generic
|
||||||
|
* mapping. Deterministic per bureau and day, so fetching the same window again yields the same alerts (and they are
|
||||||
|
* de-duplicated). Now and then it also sends an alert for an unknown customer and one raised by this bank itself, so
|
||||||
|
* the unmatched and own-bank paths can be seen too. Never used once the bureau is set to UAT or PRODUCTION.
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CicSimulator {
|
||||||
|
|
||||||
|
private static final String[][] LENDERS = { { "HDFC BANK LTD", "BANK" }, { "STATE BANK OF INDIA", "BANK" }, { "ICICI BANK LTD", "BANK" },
|
||||||
|
{ "BAJAJ FINANCE LTD", "NBFC" }, { "MUTHOOT FINANCE LTD", "NBFC" }, { "UJJIVAN SMALL FINANCE BANK", "SMALL FINANCE BANK" },
|
||||||
|
{ "BANDHAN BANK LTD", "BANK" }, { "SATIN CREDITCARE NETWORK", "MFI" }, { "MADHYA PRADESH GRAMIN BANK", "REGIONAL RURAL BANK" },
|
||||||
|
{ "SARASWAT CO-OPERATIVE BANK", "CO-OPERATIVE BANK" }, { "TATA CAPITAL LTD", "NBFC" }, { "PUNJAB NATIONAL BANK", "BANK" } };
|
||||||
|
private static final Object[][] LOAN_TYPES = { { "PERSONAL LOAN", 50_000, 1_000_000 }, { "HOUSING LOAN", 800_000, 6_000_000 },
|
||||||
|
{ "AUTO LOAN", 300_000, 1_500_000 }, { "TWO-WHEELER LOAN", 50_000, 200_000 }, { "GOLD LOAN", 20_000, 500_000 },
|
||||||
|
{ "BUSINESS LOAN", 200_000, 5_000_000 }, { "CREDIT CARD", 25_000, 300_000 }, { "KISAN CREDIT CARD", 50_000, 300_000 },
|
||||||
|
{ "EDUCATION LOAN", 200_000, 2_000_000 }, { "CONSUMER DURABLE LOAN", 10_000, 150_000 },
|
||||||
|
{ "LOAN AGAINST PROPERTY", 500_000, 5_000_000 }, { "MICROFINANCE LOAN", 20_000, 100_000 } };
|
||||||
|
private static final String[] CITIES = { "BHOPAL", "INDORE", "VIDISHA", "SAGAR", "JABALPUR", "GWALIOR", "BASODA", "RAISEN" };
|
||||||
|
private static final DateTimeFormatter DDMMYYYY = DateTimeFormatter.ofPattern("ddMMyyyy");
|
||||||
|
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
|
||||||
|
public JsonObject respond(final Map<String, Object> bureau, final LocalDate from, final LocalDate to, final String reference) {
|
||||||
|
final String code = String.valueOf(bureau.get("code"));
|
||||||
|
final String ownMemberCode = bureau.get("member_code") == null ? "OWNBANK" : bureau.get("member_code").toString();
|
||||||
|
final List<Map<String, Object>> clients = jdbcTemplate.queryForList("""
|
||||||
|
SELECT c.id, c.display_name, c.account_no, c.date_of_birth,
|
||||||
|
(SELECT k.pan_number FROM m_client_kyc k WHERE k.client_id = c.id AND k.pan_number IS NOT NULL
|
||||||
|
ORDER BY k.id DESC LIMIT 1) AS pan,
|
||||||
|
(SELECT k.aadhaar_number_masked FROM m_client_kyc k WHERE k.client_id = c.id
|
||||||
|
AND k.aadhaar_number_masked IS NOT NULL ORDER BY k.id DESC LIMIT 1) AS aadhaar
|
||||||
|
FROM m_client c
|
||||||
|
WHERE c.status_enum = 300
|
||||||
|
ORDER BY c.id
|
||||||
|
LIMIT 300
|
||||||
|
""");
|
||||||
|
|
||||||
|
final JsonArray alerts = new JsonArray();
|
||||||
|
final long days = Math.min(90, ChronoUnit.DAYS.between(from, to));
|
||||||
|
for (int d = 0; d <= days; d++) {
|
||||||
|
final LocalDate day = from.plusDays(d);
|
||||||
|
final Random r = new Random(Math.abs((long) (code + "|" + day).hashCode()));
|
||||||
|
final int count = clients.isEmpty() ? 0 : r.nextInt(4);
|
||||||
|
int seq = 0;
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
alerts.add(alert(code, day, ++seq, clients.get(r.nextInt(clients.size())), r, null));
|
||||||
|
}
|
||||||
|
if (r.nextInt(8) == 0) {
|
||||||
|
// A consumer the bureau has on file for this bank but whom the bank no longer has (or never had) under that id.
|
||||||
|
alerts.add(alert(code, day, ++seq, Map.of("display_name", "UNKNOWN CUSTOMER", "pan", "ZZZPZ" + (1000 + r.nextInt(8999)) + "Z"),
|
||||||
|
r, null));
|
||||||
|
}
|
||||||
|
if (!clients.isEmpty() && r.nextInt(10) == 0) {
|
||||||
|
// An enquiry this bank made itself - the bureau reports it, the CBS must skip it.
|
||||||
|
alerts.add(alert(code, day, ++seq, clients.get(r.nextInt(clients.size())), r, ownMemberCode));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final JsonObject header = new JsonObject();
|
||||||
|
header.addProperty("status", "SUCCESS");
|
||||||
|
header.addProperty("referenceNumber", reference);
|
||||||
|
header.addProperty("bureau", code);
|
||||||
|
header.addProperty("fromDate", from.format(DDMMYYYY));
|
||||||
|
header.addProperty("toDate", to.format(DDMMYYYY));
|
||||||
|
final JsonObject data = new JsonObject();
|
||||||
|
data.addProperty("totalAlerts", alerts.size());
|
||||||
|
data.add("alerts", alerts);
|
||||||
|
final JsonObject root = new JsonObject();
|
||||||
|
root.add("responseHeader", header);
|
||||||
|
root.add("data", data);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonObject alert(final String code, final LocalDate day, final int seq, final Map<String, Object> client, final Random r,
|
||||||
|
final String forcedMemberCode) {
|
||||||
|
final JsonObject consumer = new JsonObject();
|
||||||
|
consumer.addProperty("name", String.valueOf(client.get("display_name")).toUpperCase());
|
||||||
|
final Object pan = client.get("pan");
|
||||||
|
final Object aadhaar = client.get("aadhaar");
|
||||||
|
if (pan != null && !pan.toString().isBlank()) {
|
||||||
|
consumer.addProperty("pan", pan.toString().trim().toUpperCase());
|
||||||
|
} else if (aadhaar != null && !aadhaar.toString().isBlank()) {
|
||||||
|
consumer.addProperty("aadhaar", "XXXXXXXX" + lastFour(aadhaar.toString()));
|
||||||
|
} else if (client.get("account_no") != null) {
|
||||||
|
consumer.addProperty("memberReference", client.get("account_no").toString());
|
||||||
|
}
|
||||||
|
if (client.get("date_of_birth") != null) {
|
||||||
|
consumer.addProperty("dateOfBirth", LocalDate.parse(client.get("date_of_birth").toString().substring(0, 10)).format(DDMMYYYY));
|
||||||
|
}
|
||||||
|
|
||||||
|
final int lenderIdx = r.nextInt(LENDERS.length);
|
||||||
|
final Object[] type = LOAN_TYPES[r.nextInt(LOAN_TYPES.length)];
|
||||||
|
final int min = (Integer) type[1];
|
||||||
|
final int max = (Integer) type[2];
|
||||||
|
final long amount = Math.round((min + r.nextDouble() * (max - min)) / 5000.0) * 5000L;
|
||||||
|
|
||||||
|
final JsonObject enquiry = new JsonObject();
|
||||||
|
enquiry.addProperty("memberCode", forcedMemberCode != null ? forcedMemberCode : "SIM" + String.format("%04d", 101 + lenderIdx));
|
||||||
|
enquiry.addProperty("memberName", forcedMemberCode != null ? "THIS BANK" : LENDERS[lenderIdx][0]);
|
||||||
|
enquiry.addProperty("memberType", forcedMemberCode != null ? "BANK" : LENDERS[lenderIdx][1]);
|
||||||
|
enquiry.addProperty("date", day.format(DDMMYYYY));
|
||||||
|
enquiry.addProperty("productType", (String) type[0]);
|
||||||
|
enquiry.addProperty("amount", amount);
|
||||||
|
enquiry.addProperty("purpose", r.nextInt(5) == 0 ? "LOAN RENEWAL / ENHANCEMENT" : "NEW LOAN APPLICATION");
|
||||||
|
enquiry.addProperty("location", CITIES[r.nextInt(CITIES.length)]);
|
||||||
|
|
||||||
|
final JsonObject a = new JsonObject();
|
||||||
|
a.addProperty("alertId", code + "-" + day.format(DDMMYYYY) + "-" + seq);
|
||||||
|
a.add("consumer", consumer);
|
||||||
|
a.add("enquiry", enquiry);
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String lastFour(final String value) {
|
||||||
|
final String digits = value.replaceAll("\\D", "");
|
||||||
|
return digits.length() >= 4 ? digits.substring(digits.length() - 4) : "0000";
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-235
@@ -83,13 +83,12 @@ public class KycContactAuditApiResource {
|
|||||||
@Operation(summary = "Notices per day", description = "Newest day first: customers reached, notices sent and their outcomes.")
|
@Operation(summary = "Notices per day", description = "Newest day first: customers reached, notices sent and their outcomes.")
|
||||||
public String days(@QueryParam("from") final String from, @QueryParam("to") final String to,
|
public String days(@QueryParam("from") final String from, @QueryParam("to") final String to,
|
||||||
@QueryParam("channel") final String channel, @QueryParam("status") final String status,
|
@QueryParam("channel") final String channel, @QueryParam("status") final String status,
|
||||||
@QueryParam("search") final String search, @QueryParam("real") final Boolean real) {
|
@QueryParam("search") final String search) {
|
||||||
this.context.authenticatedUser().validateHasReadPermission(ClientApiConstants.CLIENT_RESOURCE_NAME);
|
this.context.authenticatedUser().validateHasReadPermission(ClientApiConstants.CLIENT_RESOURCE_NAME);
|
||||||
|
|
||||||
final List<Object> args = new ArrayList<>();
|
final List<Object> args = new ArrayList<>();
|
||||||
final String where = filters(from, to, channel, status, search, real, args);
|
final String where = filters(from, to, channel, status, search, args);
|
||||||
final String sql = "SELECT CAST(l.contacted_at AS DATE) AS d, COUNT(*) AS notices, COUNT(DISTINCT l.client_id) AS customers, "
|
final String sql = "SELECT CAST(l.contacted_at AS DATE) AS d, COUNT(*) AS notices, COUNT(DISTINCT l.client_id) AS customers, "
|
||||||
+ "COUNT(DISTINCT CASE WHEN l.delivery_status = 'SENT' THEN l.client_id END) AS notified, "
|
|
||||||
+ "SUM(CASE WHEN l.delivery_status = 'SENT' THEN 1 ELSE 0 END) AS delivered, "
|
+ "SUM(CASE WHEN l.delivery_status = 'SENT' THEN 1 ELSE 0 END) AS delivered, "
|
||||||
+ "SUM(CASE WHEN l.delivery_status = 'FAILED' THEN 1 ELSE 0 END) AS failed, "
|
+ "SUM(CASE WHEN l.delivery_status = 'FAILED' THEN 1 ELSE 0 END) AS failed, "
|
||||||
+ "SUM(CASE WHEN l.delivery_status = 'SKIPPED_NO_DESTINATION' THEN 1 ELSE 0 END) AS no_contact, "
|
+ "SUM(CASE WHEN l.delivery_status = 'SKIPPED_NO_DESTINATION' THEN 1 ELSE 0 END) AS no_contact, "
|
||||||
@@ -103,8 +102,6 @@ public class KycContactAuditApiResource {
|
|||||||
item.put("date", String.valueOf(r.get("d")));
|
item.put("date", String.valueOf(r.get("d")));
|
||||||
item.put("notices", number(r.get("notices")));
|
item.put("notices", number(r.get("notices")));
|
||||||
item.put("customers", number(r.get("customers")));
|
item.put("customers", number(r.get("customers")));
|
||||||
// Customers a notice actually reached that day, as against customers it was tried for.
|
|
||||||
item.put("notifiedCustomers", number(r.get("notified")));
|
|
||||||
item.put("delivered", number(r.get("delivered")));
|
item.put("delivered", number(r.get("delivered")));
|
||||||
item.put("failed", number(r.get("failed")));
|
item.put("failed", number(r.get("failed")));
|
||||||
item.put("noContact", number(r.get("no_contact")));
|
item.put("noContact", number(r.get("no_contact")));
|
||||||
@@ -121,12 +118,12 @@ public class KycContactAuditApiResource {
|
|||||||
@Operation(summary = "Retrieve customer notices on record", description = "Newest first, a page at a time. Returns total and rows.")
|
@Operation(summary = "Retrieve customer notices on record", description = "Newest first, a page at a time. Returns total and rows.")
|
||||||
public String retrieve(@QueryParam("from") final String from, @QueryParam("to") final String to,
|
public String retrieve(@QueryParam("from") final String from, @QueryParam("to") final String to,
|
||||||
@QueryParam("channel") final String channel, @QueryParam("status") final String status,
|
@QueryParam("channel") final String channel, @QueryParam("status") final String status,
|
||||||
@QueryParam("search") final String search, @QueryParam("real") final Boolean real,
|
@QueryParam("search") final String search, @QueryParam("offset") final Integer offset,
|
||||||
@QueryParam("offset") final Integer offset, @QueryParam("limit") final Integer limit) {
|
@QueryParam("limit") final Integer limit) {
|
||||||
this.context.authenticatedUser().validateHasReadPermission(ClientApiConstants.CLIENT_RESOURCE_NAME);
|
this.context.authenticatedUser().validateHasReadPermission(ClientApiConstants.CLIENT_RESOURCE_NAME);
|
||||||
|
|
||||||
final List<Object> args = new ArrayList<>();
|
final List<Object> args = new ArrayList<>();
|
||||||
final String where = filters(from, to, channel, status, search, real, args);
|
final String where = filters(from, to, channel, status, search, args);
|
||||||
|
|
||||||
final Long total = this.jdbcTemplate.queryForObject("SELECT COUNT(*)" + FROM_CLAUSE + where, Long.class, args.toArray());
|
final Long total = this.jdbcTemplate.queryForObject("SELECT COUNT(*)" + FROM_CLAUSE + where, Long.class, args.toArray());
|
||||||
|
|
||||||
@@ -163,193 +160,10 @@ public class KycContactAuditApiResource {
|
|||||||
return this.gson.toJson(out);
|
return this.gson.toJson(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* One line per customer: how many notices, how they fared, and the latest. Newest activity first, a page at a time.
|
|
||||||
* Answers "who was contacted, and what happened" without paging through every day.
|
|
||||||
*/
|
|
||||||
@GET
|
|
||||||
@Path("customers")
|
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
|
||||||
@Operation(summary = "Notices per customer", description = "Same filters as the day view. Returns total and rows.")
|
|
||||||
public String customers(@QueryParam("from") final String from, @QueryParam("to") final String to,
|
|
||||||
@QueryParam("channel") final String channel, @QueryParam("status") final String status,
|
|
||||||
@QueryParam("search") final String search, @QueryParam("real") final Boolean real,
|
|
||||||
@QueryParam("offset") final Integer offset, @QueryParam("limit") final Integer limit) {
|
|
||||||
this.context.authenticatedUser().validateHasReadPermission(ClientApiConstants.CLIENT_RESOURCE_NAME);
|
|
||||||
|
|
||||||
final List<Object> args = new ArrayList<>();
|
|
||||||
final String where = filters(from, to, channel, status, search, real, args);
|
|
||||||
|
|
||||||
final Long total = this.jdbcTemplate.queryForObject("SELECT COUNT(DISTINCT l.client_id)" + FROM_CLAUSE + where, Long.class,
|
|
||||||
args.toArray());
|
|
||||||
|
|
||||||
final int rows = Math.min(Math.max(limit == null ? 50 : limit, 1), MAX_PAGE);
|
|
||||||
final int skip = Math.max(offset == null ? 0 : offset, 0);
|
|
||||||
final String sql = "SELECT l.client_id, c.display_name, c.account_no, COUNT(*) AS notices, "
|
|
||||||
+ "SUM(CASE WHEN l.delivery_status = 'SENT' THEN 1 ELSE 0 END) AS delivered, "
|
|
||||||
+ "SUM(CASE WHEN l.delivery_status = 'FAILED' THEN 1 ELSE 0 END) AS failed, "
|
|
||||||
+ "SUM(CASE WHEN l.delivery_status IN ('SKIPPED_NO_DESTINATION', 'PROVIDER_NOT_CONFIGURED') THEN 1 ELSE 0 END) AS not_sent, "
|
|
||||||
+ "SUM(CASE WHEN l.delivery_status = 'NOT_APPLICABLE' THEN 1 ELSE 0 END) AS recorded, "
|
|
||||||
+ "MAX(l.contacted_at) AS last_at" + FROM_CLAUSE + where
|
|
||||||
+ " GROUP BY l.client_id, c.display_name, c.account_no ORDER BY last_at DESC, l.client_id DESC LIMIT " + rows + " OFFSET "
|
|
||||||
+ skip;
|
|
||||||
|
|
||||||
final List<Map<String, Object>> list = new ArrayList<>();
|
|
||||||
final List<Long> pageIds = new ArrayList<>();
|
|
||||||
for (final Map<String, Object> r : this.jdbcTemplate.queryForList(sql, args.toArray())) {
|
|
||||||
final Map<String, Object> item = new LinkedHashMap<>();
|
|
||||||
item.put("clientId", r.get("client_id"));
|
|
||||||
item.put("clientName", r.get("display_name"));
|
|
||||||
item.put("accountNo", r.get("account_no"));
|
|
||||||
item.put("notices", number(r.get("notices")));
|
|
||||||
item.put("delivered", number(r.get("delivered")));
|
|
||||||
item.put("failed", number(r.get("failed")));
|
|
||||||
item.put("notSent", number(r.get("not_sent")));
|
|
||||||
item.put("recorded", number(r.get("recorded")));
|
|
||||||
final Object at = r.get("last_at");
|
|
||||||
item.put("lastAt", at == null ? null : String.valueOf(at).replace(' ', 'T'));
|
|
||||||
pageIds.add(((Number) r.get("client_id")).longValue());
|
|
||||||
list.add(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The latest notice's channel and outcome, for the customers on this page only.
|
|
||||||
if (!pageIds.isEmpty()) {
|
|
||||||
final String marks = String.join(",", pageIds.stream().map(x -> "?").toList());
|
|
||||||
final Map<Long, Map<String, Object>> byId = new LinkedHashMap<>();
|
|
||||||
list.forEach((m) -> byId.put(((Number) m.get("clientId")).longValue(), m));
|
|
||||||
final List<Object> latestArgs = new ArrayList<>(pageIds);
|
|
||||||
for (final Map<String, Object> r : this.jdbcTemplate.queryForList(
|
|
||||||
"SELECT client_id, channel, delivery_status FROM m_client_kyc_contact_log WHERE client_id IN (" + marks
|
|
||||||
+ ") ORDER BY contacted_at DESC, id DESC",
|
|
||||||
latestArgs.toArray())) {
|
|
||||||
final Map<String, Object> item = byId.get(((Number) r.get("client_id")).longValue());
|
|
||||||
if (item != null && !item.containsKey("lastChannel")) {
|
|
||||||
item.put("lastChannel", r.get("channel"));
|
|
||||||
item.put("lastStatus", r.get("delivery_status"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final Map<String, Object> out = new LinkedHashMap<>();
|
|
||||||
out.put("total", total == null ? 0 : total);
|
|
||||||
out.put("rows", list);
|
|
||||||
return this.gson.toJson(out);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* What the listed customers have been sent so far: the latest notice and which channels have been done.
|
|
||||||
* One round trip for a whole screen of customers, so a list can show "last notice" without a call per row.
|
|
||||||
*/
|
|
||||||
@GET
|
|
||||||
@Path("clients")
|
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
|
||||||
@Operation(summary = "Notice summary for some customers", description = "ids is a comma-separated list of client ids (up to 500).")
|
|
||||||
public String clients(@QueryParam("ids") final String ids) {
|
|
||||||
this.context.authenticatedUser().validateHasReadPermission(ClientApiConstants.CLIENT_RESOURCE_NAME);
|
|
||||||
|
|
||||||
final List<Long> clientIds = new ArrayList<>();
|
|
||||||
if (StringUtils.isNotBlank(ids)) {
|
|
||||||
for (final String part : ids.split(",")) {
|
|
||||||
if (StringUtils.isNumeric(part.trim()) && clientIds.size() < MAX_PAGE) {
|
|
||||||
clientIds.add(Long.valueOf(part.trim()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (clientIds.isEmpty()) {
|
|
||||||
return this.gson.toJson(new ArrayList<>());
|
|
||||||
}
|
|
||||||
|
|
||||||
final String marks = String.join(",", clientIds.stream().map(x -> "?").toList());
|
|
||||||
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(
|
|
||||||
"SELECT client_id, channel, delivery_status, contacted_at, message_body FROM m_client_kyc_contact_log WHERE client_id IN (" + marks
|
|
||||||
+ ") ORDER BY contacted_at DESC, id DESC",
|
|
||||||
clientIds.toArray());
|
|
||||||
|
|
||||||
final Map<Long, Map<String, Object>> byClient = new LinkedHashMap<>();
|
|
||||||
// Every customer asked for comes back, with or without notices, so the screen knows who has no mobile or email.
|
|
||||||
for (final Map<String, Object> c : this.jdbcTemplate.queryForList(
|
|
||||||
"SELECT c.id, c.mobile_no, c.email_address, "
|
|
||||||
+ "COALESCE((SELECT SUM(s.account_balance_derived) FROM m_savings_account s WHERE s.client_id = c.id AND s.status_enum = 300), 0) "
|
|
||||||
+ "+ COALESCE((SELECT SUM(a.account_balance_derived) FROM m_current_account a WHERE a.client_id = c.id AND a.status_enum = 300), 0) AS balance "
|
|
||||||
+ "FROM m_client c WHERE c.id IN (" + marks + ")", clientIds.toArray())) {
|
|
||||||
final Long id = ((Number) c.get("id")).longValue();
|
|
||||||
final Map<String, Object> m = new LinkedHashMap<>();
|
|
||||||
m.put("clientId", id);
|
|
||||||
m.put("count", 0);
|
|
||||||
m.put("lastAt", null);
|
|
||||||
m.put("lastChannel", null);
|
|
||||||
m.put("lastStatus", null);
|
|
||||||
m.put("doneChannels", new ArrayList<String>());
|
|
||||||
m.put("doneByStage", new LinkedHashMap<String, List<String>>());
|
|
||||||
m.put("noticesByStage", new LinkedHashMap<String, Integer>());
|
|
||||||
// What the customer holds in open savings and current accounts.
|
|
||||||
m.put("balance", c.get("balance"));
|
|
||||||
m.put("hasMobile", StringUtils.isNotBlank((String) c.get("mobile_no")));
|
|
||||||
m.put("hasEmail", StringUtils.isNotBlank((String) c.get("email_address")));
|
|
||||||
// Masked, never the full value: the screen only needs to show that something is on file.
|
|
||||||
m.put("mobileMasked", maskMobile((String) c.get("mobile_no")));
|
|
||||||
m.put("emailMasked", maskEmail((String) c.get("email_address")));
|
|
||||||
byClient.put(id, m);
|
|
||||||
}
|
|
||||||
for (final Map<String, Object> r : rows) {
|
|
||||||
final Long clientId = ((Number) r.get("client_id")).longValue();
|
|
||||||
final Map<String, Object> item = byClient.computeIfAbsent(clientId, k -> {
|
|
||||||
final Map<String, Object> m = new LinkedHashMap<>();
|
|
||||||
m.put("clientId", k);
|
|
||||||
m.put("count", 0);
|
|
||||||
m.put("lastAt", null);
|
|
||||||
m.put("lastChannel", null);
|
|
||||||
m.put("lastStatus", null);
|
|
||||||
m.put("doneChannels", new ArrayList<String>());
|
|
||||||
m.put("doneByStage", new LinkedHashMap<String, List<String>>());
|
|
||||||
m.put("noticesByStage", new LinkedHashMap<String, Integer>());
|
|
||||||
return m;
|
|
||||||
});
|
|
||||||
if (item.get("lastAt") == null) {
|
|
||||||
// Rows come newest first, so the first one seen is the latest notice.
|
|
||||||
final Object at = r.get("contacted_at");
|
|
||||||
item.put("lastAt", at == null ? null : String.valueOf(at).replace(' ', 'T'));
|
|
||||||
item.put("lastChannel", r.get("channel"));
|
|
||||||
item.put("lastStatus", r.get("delivery_status"));
|
|
||||||
}
|
|
||||||
item.put("count", ((Integer) item.get("count")) + 1);
|
|
||||||
// A channel counts as done once it reached the customer, or was recorded (a call or a visit).
|
|
||||||
final String status = String.valueOf(r.get("delivery_status"));
|
|
||||||
final String channel = String.valueOf(r.get("channel"));
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
final List<String> done = (List<String>) item.get("doneChannels");
|
|
||||||
if (("SENT".equals(status) || "NOT_APPLICABLE".equals(status)) && !done.contains(channel)) {
|
|
||||||
done.add(channel);
|
|
||||||
}
|
|
||||||
// The same, by the stage the notice was written for, so a notice sent at an earlier stage is not taken for this one.
|
|
||||||
// A call, a visit or any other record that is not a stage notice is filed under ANY: it counts at whatever stage the customer is at.
|
|
||||||
final String found = stageOfNotice((String) r.get("message_body"));
|
|
||||||
final String stage = found == null ? "ANY" : found;
|
|
||||||
if ("SENT".equals(status) || "NOT_APPLICABLE".equals(status)) {
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
final Map<String, List<String>> byStage = (Map<String, List<String>>) item.get("doneByStage");
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
final Map<String, Integer> counts = (Map<String, Integer>) item.get("noticesByStage");
|
|
||||||
counts.merge(stage, 1, Integer::sum);
|
|
||||||
final List<String> inStage = byStage.computeIfAbsent(stage, k -> new ArrayList<>());
|
|
||||||
if (!inStage.contains(channel)) {
|
|
||||||
inStage.add(channel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this.gson.toJson(new ArrayList<>(byClient.values()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The WHERE tail shared by both views; fills {@code args} in step with its placeholders. */
|
/** The WHERE tail shared by both views; fills {@code args} in step with its placeholders. */
|
||||||
private String filters(final String from, final String to, final String channel, final String status, final String search,
|
private String filters(final String from, final String to, final String channel, final String status, final String search,
|
||||||
final Boolean real, final List<Object> args) {
|
final List<Object> args) {
|
||||||
final StringBuilder where = new StringBuilder();
|
final StringBuilder where = new StringBuilder();
|
||||||
if (Boolean.TRUE.equals(real)) {
|
|
||||||
// An attempt that went nowhere (no number or address on file, or no gateway set up) is not a notice a customer received.
|
|
||||||
where.append(" AND l.delivery_status IN ('SENT', 'FAILED', 'NOT_APPLICABLE')");
|
|
||||||
}
|
|
||||||
if (StringUtils.isNotBlank(from)) {
|
if (StringUtils.isNotBlank(from)) {
|
||||||
where.append(" AND l.contacted_at >= ?");
|
where.append(" AND l.contacted_at >= ?");
|
||||||
args.add(Timestamp.valueOf(parse(from, "from").atStartOfDay()));
|
args.add(Timestamp.valueOf(parse(from, "from").atStartOfDay()));
|
||||||
@@ -376,49 +190,6 @@ public class KycContactAuditApiResource {
|
|||||||
return where.toString();
|
return where.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The stage a notice was written for, read from its wording (the composer words every stage differently). Null for a call,
|
|
||||||
* a visit or any message that is not one of the stage notices.
|
|
||||||
*/
|
|
||||||
private static String stageOfNotice(final String body) {
|
|
||||||
if (StringUtils.isBlank(body)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (body.startsWith("Your KYC expires on")) {
|
|
||||||
return "EXPIRING";
|
|
||||||
}
|
|
||||||
if (body.startsWith("Your KYC expired on")) {
|
|
||||||
if (body.contains("account is now inoperative")) {
|
|
||||||
return "INOPERATIVE";
|
|
||||||
}
|
|
||||||
return body.contains("debits on your account are now stopped") ? "PARTIAL_FREEZE" : "GRACE";
|
|
||||||
}
|
|
||||||
if (body.startsWith("Your account ")) {
|
|
||||||
if (body.contains("has been closed")) {
|
|
||||||
return "ESCHEAT";
|
|
||||||
}
|
|
||||||
return body.contains("is dormant") ? "DORMANT" : body.contains("is inactive") ? "INACTIVE" : null;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String maskMobile(final String mobile) {
|
|
||||||
if (StringUtils.isBlank(mobile)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final String digits = mobile.trim();
|
|
||||||
return digits.length() <= 4 ? digits : "XXXXXX" + digits.substring(digits.length() - 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String maskEmail(final String email) {
|
|
||||||
if (StringUtils.isBlank(email) || !email.contains("@")) {
|
|
||||||
return StringUtils.isBlank(email) ? null : email;
|
|
||||||
}
|
|
||||||
final String trimmed = email.trim();
|
|
||||||
final int at = trimmed.indexOf('@');
|
|
||||||
return trimmed.substring(0, Math.min(1, at)) + "***" + trimmed.substring(at);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static long number(final Object value) {
|
private static long number(final Object value) {
|
||||||
return value == null ? 0 : ((Number) value).longValue();
|
return value == null ? 0 : ((Number) value).longValue();
|
||||||
}
|
}
|
||||||
|
|||||||
-1
@@ -108,7 +108,6 @@ public class KycNoticeQueueApiResource {
|
|||||||
final JsonObject out = new JsonObject();
|
final JsonObject out = new JsonObject();
|
||||||
out.addProperty("queued", result.queued());
|
out.addProperty("queued", result.queued());
|
||||||
out.addProperty("skipped", result.skipped());
|
out.addProperty("skipped", result.skipped());
|
||||||
out.addProperty("unreachable", result.unreachable());
|
|
||||||
return out.toString();
|
return out.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
@@ -24,6 +24,7 @@ import jakarta.ws.rs.Consumes;
|
|||||||
import jakarta.ws.rs.GET;
|
import jakarta.ws.rs.GET;
|
||||||
import jakarta.ws.rs.POST;
|
import jakarta.ws.rs.POST;
|
||||||
import jakarta.ws.rs.Path;
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.PathParam;
|
||||||
import jakarta.ws.rs.Produces;
|
import jakarta.ws.rs.Produces;
|
||||||
import jakarta.ws.rs.core.MediaType;
|
import jakarta.ws.rs.core.MediaType;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -69,4 +70,14 @@ public class KycRiskRulesApiResource {
|
|||||||
final KycRiskRuleData rule = this.writePlatformService.createRule(request);
|
final KycRiskRuleData rule = this.writePlatformService.createRule(request);
|
||||||
return this.toApiJsonSerializer.serialize(rule);
|
return this.toApiJsonSerializer.serialize(rule);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("{ruleId}/mark-normal")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Mark a rule as normal", description = "Ends the trial of a newly added rule. It stays in the catalog as a normal rule.")
|
||||||
|
public String markNormal(@PathParam("ruleId") final Long ruleId) {
|
||||||
|
this.context.authenticatedUser().validateHasUpdatePermission(ClientApiConstants.CLIENT_RESOURCE_NAME);
|
||||||
|
return this.toApiJsonSerializer.serialize(this.writePlatformService.markNormal(ruleId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-1
@@ -66,6 +66,10 @@ public final class KycRiskRuleData implements Serializable {
|
|||||||
private final BigDecimal txnMaxLimitAmount;
|
private final BigDecimal txnMaxLimitAmount;
|
||||||
/** True when the scenario applies to Cash Credit (overdraft-bearing current) accounts. */
|
/** True when the scenario applies to Cash Credit (overdraft-bearing current) accounts. */
|
||||||
private final boolean ccRelevant;
|
private final boolean ccRelevant;
|
||||||
|
/** True while a newly authored rule is on trial; it is listed first until an officer marks it normal. */
|
||||||
|
private final boolean onTrial;
|
||||||
|
/** When the trial began (ISO date), or null for a rule that is not on trial. */
|
||||||
|
private final String trialStartedOn;
|
||||||
|
|
||||||
public KycRiskRuleData(final Long id, final String ruleName, final String ruleKind, final Integer legalFormId,
|
public KycRiskRuleData(final Long id, final String ruleName, final String ruleKind, final Integer legalFormId,
|
||||||
final Long occupationCategoryCvId, final String occupationCategoryName,
|
final Long occupationCategoryCvId, final String occupationCategoryName,
|
||||||
@@ -78,7 +82,8 @@ public final class KycRiskRuleData implements Serializable {
|
|||||||
final String willfulDefaulter, final String ecgcSalFlag, final String dormantFlag,
|
final String willfulDefaulter, final String ecgcSalFlag, final String dormantFlag,
|
||||||
final BigDecimal txnThresholdAmount, final String txnRuleType, final Integer txnWindowDays,
|
final BigDecimal txnThresholdAmount, final String txnRuleType, final Integer txnWindowDays,
|
||||||
final BigDecimal txnThresholdAmountMediumRisk, final BigDecimal txnThresholdAmountHighRisk,
|
final BigDecimal txnThresholdAmountMediumRisk, final BigDecimal txnThresholdAmountHighRisk,
|
||||||
final BigDecimal txnMinLimitAmount, final BigDecimal txnMaxLimitAmount, final boolean ccRelevant) {
|
final BigDecimal txnMinLimitAmount, final BigDecimal txnMaxLimitAmount, final boolean ccRelevant,
|
||||||
|
final boolean onTrial, final String trialStartedOn) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.ruleName = ruleName;
|
this.ruleName = ruleName;
|
||||||
this.ruleKind = ruleKind;
|
this.ruleKind = ruleKind;
|
||||||
@@ -109,6 +114,8 @@ public final class KycRiskRuleData implements Serializable {
|
|||||||
this.txnMinLimitAmount = txnMinLimitAmount;
|
this.txnMinLimitAmount = txnMinLimitAmount;
|
||||||
this.txnMaxLimitAmount = txnMaxLimitAmount;
|
this.txnMaxLimitAmount = txnMaxLimitAmount;
|
||||||
this.ccRelevant = ccRelevant;
|
this.ccRelevant = ccRelevant;
|
||||||
|
this.onTrial = onTrial;
|
||||||
|
this.trialStartedOn = trialStartedOn;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Long getId() {
|
public Long getId() {
|
||||||
@@ -230,4 +237,12 @@ public final class KycRiskRuleData implements Serializable {
|
|||||||
public boolean isCcRelevant() {
|
public boolean isCcRelevant() {
|
||||||
return ccRelevant;
|
return ccRelevant;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isOnTrial() {
|
||||||
|
return onTrial;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTrialStartedOn() {
|
||||||
|
return trialStartedOn;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-2
@@ -104,15 +104,26 @@ public class KycRiskRuleRequest {
|
|||||||
@Schema(description = "THRESHOLD | STRUCTURING | VELOCITY. Defaults to THRESHOLD.", example = "THRESHOLD")
|
@Schema(description = "THRESHOLD | STRUCTURING | VELOCITY. Defaults to THRESHOLD.", example = "THRESHOLD")
|
||||||
private String txnRuleType;
|
private String txnRuleType;
|
||||||
|
|
||||||
@Schema(description = "SAVINGS | LOAN | TRANSFER | ALL. Defaults to ALL.", example = "ALL")
|
@Schema(description = "SAVINGS | LOAN | TRANSFER | CURRENT | ALL. Defaults to ALL. CURRENT is a current / Cash Credit account.", example = "ALL")
|
||||||
private String txnAppliesTo;
|
private String txnAppliesTo;
|
||||||
|
|
||||||
|
@Schema(description = "CREDIT (money in) or DEBIT (money out). Omit for either direction.", example = "CREDIT")
|
||||||
|
private String txnDirection;
|
||||||
|
|
||||||
|
@Schema(description = "True when the rule is about cash only. Defaults to false.")
|
||||||
|
private Boolean txnCashOnly;
|
||||||
|
|
||||||
|
@Schema(description = "True when the scenario watches Cash Credit accounts, so the Rules Master CC chip lists it.")
|
||||||
|
private Boolean ccRelevant;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True when the officer has described a live-transaction rule rather than a pure KYC profile
|
* True when the officer has described a live-transaction rule rather than a pure KYC profile
|
||||||
* rule, and a companion {@code m_aml_transaction_rule} row is therefore warranted.
|
* rule, and a companion {@code m_aml_transaction_rule} row is therefore warranted.
|
||||||
*/
|
*/
|
||||||
public boolean hasTransactionRule() {
|
public boolean hasTransactionRule() {
|
||||||
return this.txnMinLimitAmount != null || this.txnMaxLimitAmount != null || this.txnThresholdAmount != null
|
return this.txnMinLimitAmount != null || this.txnMaxLimitAmount != null || this.txnThresholdAmount != null
|
||||||
|| (this.txnWindowDays != null && this.txnWindowDays > 0);
|
|| (this.txnWindowDays != null && this.txnWindowDays > 0)
|
||||||
|
|| (this.txnDirection != null && !this.txnDirection.isBlank()) || Boolean.TRUE.equals(this.txnCashOnly)
|
||||||
|
|| (this.txnAppliesTo != null && !this.txnAppliesTo.isBlank() && !"ALL".equalsIgnoreCase(this.txnAppliesTo.trim()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-16
@@ -122,36 +122,28 @@ public class KycNoticeComposer {
|
|||||||
return "Your account is inoperative";
|
return "Your account is inoperative";
|
||||||
}
|
}
|
||||||
|
|
||||||
/** What the customer is told, by the stage they are at: where they stand and what happens next, and in how many days. */
|
/** What the customer is told, by the stage they are at: where they stand and what happens next. */
|
||||||
public static String kycMessage(final LocalDate expiry, final long offset) {
|
public static String kycMessage(final LocalDate expiry, final long offset) {
|
||||||
final String expired = expiry.format(DATE);
|
final String expired = expiry.format(DATE);
|
||||||
final String freeze = expiry.plusDays(DEBIT_FREEZE_DAYS).format(DATE);
|
final String freeze = expiry.plusDays(DEBIT_FREEZE_DAYS).format(DATE);
|
||||||
final String inoperative = expiry.plusDays(INOPERATIVE_DAYS).format(DATE);
|
final String inoperative = expiry.plusDays(INOPERATIVE_DAYS).format(DATE);
|
||||||
final String freezeIn = days(DEBIT_FREEZE_DAYS - offset);
|
|
||||||
final String inoperativeIn = days(INOPERATIVE_DAYS - offset);
|
|
||||||
if (offset < 0) {
|
if (offset < 0) {
|
||||||
return "Your KYC expires on " + expired + " (in " + days(-offset) + "). Please visit your branch with valid ID and address"
|
return "Your KYC expires on " + expired + " (in " + (-offset) + " days). Please visit your branch with valid ID and address"
|
||||||
+ " proof to renew it. If it is not renewed, debits on your account will stop on " + freeze + " (in " + freezeIn
|
+ " proof to renew it. If it is not renewed, debits on your account will stop on " + freeze
|
||||||
+ ") and the account will become inoperative on " + inoperative + " (in " + inoperativeIn + ").";
|
+ " and the account will become inoperative on " + inoperative + ".";
|
||||||
}
|
}
|
||||||
if (offset < DEBIT_FREEZE_DAYS) {
|
if (offset < DEBIT_FREEZE_DAYS) {
|
||||||
return "Your KYC expired on " + expired + ". Please renew it at your branch now. If it is not renewed, debits on your"
|
return "Your KYC expired on " + expired + ". Please renew it at your branch now. If it is not renewed, debits on your"
|
||||||
+ " account will stop on " + freeze + " (in " + freezeIn + ") and the account will become inoperative on "
|
+ " account will stop on " + freeze + " and the account will become inoperative on " + inoperative + ".";
|
||||||
+ inoperative + " (in " + inoperativeIn + ").";
|
|
||||||
}
|
}
|
||||||
if (offset < INOPERATIVE_DAYS) {
|
if (offset < INOPERATIVE_DAYS) {
|
||||||
return "Your KYC expired on " + expired + " and debits on your account are now stopped. Please renew your KYC at your"
|
return "Your KYC expired on " + expired + " and debits on your account are now stopped. Please renew your KYC at your"
|
||||||
+ " branch; if it is not renewed, the account will become inoperative on " + inoperative + " (in " + inoperativeIn
|
+ " branch; if it is not renewed, the account will become inoperative on " + inoperative + ".";
|
||||||
+ ").";
|
|
||||||
}
|
}
|
||||||
return "Your KYC expired on " + expired + " and your account is now inoperative. Please visit your branch with valid"
|
return "Your KYC expired on " + expired + " and your account is now inoperative. Please visit your branch with valid"
|
||||||
+ " documents to renew your KYC and have the account restored.";
|
+ " documents to renew your KYC and have the account restored.";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String days(final long n) {
|
|
||||||
return n == 1 ? "1 day" : n + " days";
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Inactive / dormant / escheated accounts ────────────────────────────────
|
// ── Inactive / dormant / escheated accounts ────────────────────────────────
|
||||||
|
|
||||||
public static String dormantSubject(final DormantAccountData account) {
|
public static String dormantSubject(final DormantAccountData account) {
|
||||||
@@ -174,7 +166,7 @@ public class KycNoticeComposer {
|
|||||||
if (sub != null && sub == SUB_STATUS_DORMANT) {
|
if (sub != null && sub == SUB_STATUS_DORMANT) {
|
||||||
final String escheat = account.getDaysToEscheat() != null && account.getDaysToEscheat() > 0
|
final String escheat = account.getDaysToEscheat() != null && account.getDaysToEscheat() > 0
|
||||||
? " If it stays dormant, the balance will be transferred to RBI's Depositor Education and Awareness Fund on "
|
? " If it stays dormant, the balance will be transferred to RBI's Depositor Education and Awareness Fund on "
|
||||||
+ today.plusDays(account.getDaysToEscheat()).format(DATE) + " (in " + days(account.getDaysToEscheat()) + ")."
|
+ today.plusDays(account.getDaysToEscheat()).format(DATE) + "."
|
||||||
: "";
|
: "";
|
||||||
return "Your account " + account.getAccountNo() + " is dormant: there has been no transaction by you since " + since
|
return "Your account " + account.getAccountNo() + " is dormant: there has been no transaction by you since " + since
|
||||||
+ ", so debits are stopped. To reactivate it, please visit your branch with valid KYC documents to verify your"
|
+ ", so debits are stopped. To reactivate it, please visit your branch with valid KYC documents to verify your"
|
||||||
@@ -182,7 +174,7 @@ public class KycNoticeComposer {
|
|||||||
}
|
}
|
||||||
final String dormant = account.getDaysToDormancy() != null && account.getDaysToDormancy() > 0
|
final String dormant = account.getDaysToDormancy() != null && account.getDaysToDormancy() > 0
|
||||||
? " If there is still no transaction, it will become dormant on " + today.plusDays(account.getDaysToDormancy()).format(DATE)
|
? " If there is still no transaction, it will become dormant on " + today.plusDays(account.getDaysToDormancy()).format(DATE)
|
||||||
+ " (in " + days(account.getDaysToDormancy()) + ") and debits will be stopped."
|
+ " and debits will be stopped."
|
||||||
: "";
|
: "";
|
||||||
return "Your account " + account.getAccountNo() + " is inactive: there has been no transaction by you since " + since
|
return "Your account " + account.getAccountNo() + " is inactive: there has been no transaction by you since " + since
|
||||||
+ ". Please make a transaction or visit your branch." + dormant;
|
+ ". Please make a transaction or visit your branch." + dormant;
|
||||||
|
|||||||
+2
-5
@@ -36,11 +36,8 @@ public interface KycNoticeQueueService {
|
|||||||
record QueueItem(Long clientId, String noticeType, String channel, String accountNo, String stageLabel) {
|
record QueueItem(Long clientId, String noticeType, String channel, String accountNo, String stageLabel) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** How many were queued, and how many were left out because the same notice was already waiting. */
|
||||||
* How many were queued, how many were left out because the same notice was already waiting, and how many because the
|
record EnqueueResult(int queued, int skipped) {
|
||||||
* customer has no mobile number or email address to send it to.
|
|
||||||
*/
|
|
||||||
record EnqueueResult(int queued, int skipped, int unreachable) {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+1
-12
@@ -78,7 +78,6 @@ public class KycNoticeQueueServiceImpl implements KycNoticeQueueService {
|
|||||||
|
|
||||||
int queued = 0;
|
int queued = 0;
|
||||||
int skipped = 0;
|
int skipped = 0;
|
||||||
int unreachable = 0;
|
|
||||||
for (final QueueItem item : items) {
|
for (final QueueItem item : items) {
|
||||||
final KycContactChannel channel = item.channel() == null ? null : KycContactChannel.fromString(item.channel());
|
final KycContactChannel channel = item.channel() == null ? null : KycContactChannel.fromString(item.channel());
|
||||||
if (item.clientId() == null || channel == null || !channel.isPlatformSent()) {
|
if (item.clientId() == null || channel == null || !channel.isPlatformSent()) {
|
||||||
@@ -88,16 +87,6 @@ public class KycNoticeQueueServiceImpl implements KycNoticeQueueService {
|
|||||||
if (!KycNoticeComposer.TYPE_KYC.equals(type) && !KycNoticeComposer.TYPE_DORMANCY.equals(type)) {
|
if (!KycNoticeComposer.TYPE_KYC.equals(type) && !KycNoticeComposer.TYPE_DORMANCY.equals(type)) {
|
||||||
throw validationError("noticeType", "Notice type must be KYC or DORMANCY.");
|
throw validationError("noticeType", "Notice type must be KYC or DORMANCY.");
|
||||||
}
|
}
|
||||||
// A customer with no mobile number (or no email, for Email) cannot be sent this. Queuing it would only write a
|
|
||||||
// "not sent" record every time, so it is left out and counted — those customers are found and reached another way.
|
|
||||||
final Map<String, Object> contact = this.jdbcTemplate.queryForList("SELECT mobile_no, email_address FROM m_client WHERE id = ?",
|
|
||||||
item.clientId()).stream().findFirst().orElse(null);
|
|
||||||
final String destination = contact == null ? null
|
|
||||||
: (String) contact.get(channel == KycContactChannel.EMAIL ? "email_address" : "mobile_no");
|
|
||||||
if (StringUtils.isBlank(destination)) {
|
|
||||||
unreachable++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// The same notice already waiting is not queued twice; a double click must not message a customer twice.
|
// The same notice already waiting is not queued twice; a double click must not message a customer twice.
|
||||||
final Integer waiting = this.jdbcTemplate.queryForObject("SELECT COUNT(*) FROM m_kyc_notice_queue WHERE client_id = ? "
|
final Integer waiting = this.jdbcTemplate.queryForObject("SELECT COUNT(*) FROM m_kyc_notice_queue WHERE client_id = ? "
|
||||||
+ "AND channel = ? AND notice_type = ? AND status IN ('" + PENDING + "', '" + SENDING + "')", Integer.class,
|
+ "AND channel = ? AND notice_type = ? AND status IN ('" + PENDING + "', '" + SENDING + "')", Integer.class,
|
||||||
@@ -113,7 +102,7 @@ public class KycNoticeQueueServiceImpl implements KycNoticeQueueService {
|
|||||||
Timestamp.valueOf(when), Timestamp.valueOf(now));
|
Timestamp.valueOf(when), Timestamp.valueOf(now));
|
||||||
queued++;
|
queued++;
|
||||||
}
|
}
|
||||||
return new EnqueueResult(queued, skipped, unreachable);
|
return new EnqueueResult(queued, skipped);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+2
-14
@@ -57,15 +57,7 @@ import org.springframework.stereotype.Service;
|
|||||||
public class KycReminderServiceImpl implements KycReminderService {
|
public class KycReminderServiceImpl implements KycReminderService {
|
||||||
|
|
||||||
/** Days from expiry (negative = before) at which a customer's KYC stage changes. */
|
/** Days from expiry (negative = before) at which a customer's KYC stage changes. */
|
||||||
/**
|
private static final int[] KYC_MILESTONES = { -30, -15, -7, 0, 80, 90, 170, 180 };
|
||||||
* Days from expiry (negative = before) at which a KYC reminder goes out: 30, 15, 7 and 1 day before it expires, the
|
|
||||||
* day it expires, then 7 and 1 day before debits stop (day 90) and before the account goes inoperative (day 180), and
|
|
||||||
* on each of those two days themselves.
|
|
||||||
*/
|
|
||||||
private static final int[] KYC_MILESTONES = { -30, -15, -7, -1, 0, 83, 89, 90, 173, 179, 180 };
|
|
||||||
|
|
||||||
/** An account is warned 7 days and 1 day before it moves to its next stage. */
|
|
||||||
private static final long WARN_DAYS = 7;
|
|
||||||
|
|
||||||
private static final int SUB_STATUS_INACTIVE = 100;
|
private static final int SUB_STATUS_INACTIVE = 100;
|
||||||
private static final int SUB_STATUS_DORMANT = 200;
|
private static final int SUB_STATUS_DORMANT = 200;
|
||||||
@@ -145,11 +137,7 @@ public class KycReminderServiceImpl implements KycReminderService {
|
|||||||
int attempted = 0;
|
int attempted = 0;
|
||||||
for (final DormantAccountData account : worst.values()) {
|
for (final DormantAccountData account : worst.values()) {
|
||||||
try {
|
try {
|
||||||
// The step is "E" (the account has just entered this stage), "D7" (a week before the next) or "D1" (the day
|
final String marker = "[AUTO DORMANCY S" + account.getSubStatus() + " " + account.getLastCustomerTransactionDate() + "]";
|
||||||
// before). The latest one reached is what the customer is told, once.
|
|
||||||
final Long remaining = account.getSubStatus() == SUB_STATUS_DORMANT ? account.getDaysToEscheat() : account.getDaysToDormancy();
|
|
||||||
final String step = remaining != null && remaining <= 1 ? "D1" : remaining != null && remaining <= WARN_DAYS ? "D7" : "E";
|
|
||||||
final String marker = "[AUTO DORMANCY S" + account.getSubStatus() + " " + account.getLastCustomerTransactionDate() + " " + step + "]";
|
|
||||||
final String subject = KycNoticeComposer.dormantSubject(account);
|
final String subject = KycNoticeComposer.dormantSubject(account);
|
||||||
attempted += sendBoth(account.getClientId(), marker, subject, KycNoticeComposer.dormantMessage(account, today));
|
attempted += sendBoth(account.getClientId(), marker, subject, KycNoticeComposer.dormantMessage(account, today));
|
||||||
} catch (final RuntimeException e) {
|
} catch (final RuntimeException e) {
|
||||||
|
|||||||
+3
-1
@@ -93,7 +93,9 @@ public class KycRiskRulesReadPlatformServiceImpl implements KycRiskRulesReadPlat
|
|||||||
txnRule != null ? txnRule.getThresholdAmountHighRisk() : null,
|
txnRule != null ? txnRule.getThresholdAmountHighRisk() : null,
|
||||||
txnRule != null ? txnRule.getMinLimitAmount() : null,
|
txnRule != null ? txnRule.getMinLimitAmount() : null,
|
||||||
txnRule != null ? txnRule.getMaxLimitAmount() : null,
|
txnRule != null ? txnRule.getMaxLimitAmount() : null,
|
||||||
rule.isCcRelevant()
|
rule.isCcRelevant(),
|
||||||
|
rule.isOnTrial(),
|
||||||
|
rule.getTrialStartedOn() == null ? null : rule.getTrialStartedOn().toString()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -31,4 +31,7 @@ public interface KycRiskRulesWritePlatformService {
|
|||||||
* @return the persisted rule exactly as the catalog GET would return it
|
* @return the persisted rule exactly as the catalog GET would return it
|
||||||
*/
|
*/
|
||||||
KycRiskRuleData createRule(KycRiskRuleRequest request);
|
KycRiskRuleData createRule(KycRiskRuleRequest request);
|
||||||
|
|
||||||
|
/** Ends a rule's trial: it stays in the catalog as a normal rule. Returns the rule as the catalog GET would. */
|
||||||
|
KycRiskRuleData markNormal(Long ruleId);
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-2
@@ -28,6 +28,7 @@ import org.apache.fineract.infrastructure.codes.domain.CodeValueRepositoryWrappe
|
|||||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||||
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
import org.apache.fineract.portfolio.aml.domain.AmlAppliesTo;
|
import org.apache.fineract.portfolio.aml.domain.AmlAppliesTo;
|
||||||
import org.apache.fineract.portfolio.aml.domain.AmlRuleType;
|
import org.apache.fineract.portfolio.aml.domain.AmlRuleType;
|
||||||
import org.apache.fineract.portfolio.aml.domain.AmlTransactionRule;
|
import org.apache.fineract.portfolio.aml.domain.AmlTransactionRule;
|
||||||
@@ -81,6 +82,13 @@ public class KycRiskRulesWritePlatformServiceImpl implements KycRiskRulesWritePl
|
|||||||
rule.setOccupation(occupation);
|
rule.setOccupation(occupation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A rule written for a Cash Credit account, or marked so, is listed under the CC chip.
|
||||||
|
rule.markCcRelevant(Boolean.TRUE.equals(request.getCcRelevant())
|
||||||
|
|| "CURRENT".equalsIgnoreCase(StringUtils.defaultString(request.getTxnAppliesTo()).trim()));
|
||||||
|
|
||||||
|
// A rule authored here starts on trial, from the business date, until an officer marks it normal.
|
||||||
|
rule.startTrial(DateUtils.getBusinessLocalDate());
|
||||||
|
|
||||||
final ClientRiskRule saved = this.repository.saveAndFlush(rule);
|
final ClientRiskRule saved = this.repository.saveAndFlush(rule);
|
||||||
|
|
||||||
if (request.hasTransactionRule()) {
|
if (request.hasTransactionRule()) {
|
||||||
@@ -138,8 +146,21 @@ public class KycRiskRulesWritePlatformServiceImpl implements KycRiskRulesWritePl
|
|||||||
final AmlAppliesTo appliesTo = StringUtils.isBlank(request.getTxnAppliesTo()) ? AmlAppliesTo.ALL
|
final AmlAppliesTo appliesTo = StringUtils.isBlank(request.getTxnAppliesTo()) ? AmlAppliesTo.ALL
|
||||||
: AmlAppliesTo.valueOf(request.getTxnAppliesTo().trim().toUpperCase());
|
: AmlAppliesTo.valueOf(request.getTxnAppliesTo().trim().toUpperCase());
|
||||||
|
|
||||||
return AmlTransactionRule.createForRiskRule(saved.getId(), saved.getReason(), appliesTo, ruleType,
|
final AmlTransactionRule transactionRule = AmlTransactionRule.createForRiskRule(saved.getId(), saved.getReason(), appliesTo,
|
||||||
request.getTxnWindowDays(), threshold, request.getTxnMinLimitAmount(), request.getTxnMaxLimitAmount());
|
ruleType, request.getTxnWindowDays(), threshold, request.getTxnMinLimitAmount(), request.getTxnMaxLimitAmount());
|
||||||
|
transactionRule.restrictTo(StringUtils.isBlank(request.getTxnDirection()) ? null : request.getTxnDirection().trim().toUpperCase(),
|
||||||
|
Boolean.TRUE.equals(request.getTxnCashOnly()));
|
||||||
|
return transactionRule;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public KycRiskRuleData markNormal(final Long ruleId) {
|
||||||
|
final ClientRiskRule rule = this.repository.findById(ruleId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Client risk rule " + ruleId + " not found"));
|
||||||
|
rule.markNormal();
|
||||||
|
this.repository.saveAndFlush(rule);
|
||||||
|
return this.readPlatformService.retrieveRule(ruleId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Integer nextOrderPosition() {
|
private Integer nextOrderPosition() {
|
||||||
@@ -182,6 +203,10 @@ public class KycRiskRulesWritePlatformServiceImpl implements KycRiskRulesWritePl
|
|||||||
|
|
||||||
validateEnum(validator, "txnRuleType", request.getTxnRuleType(), AmlRuleType.class);
|
validateEnum(validator, "txnRuleType", request.getTxnRuleType(), AmlRuleType.class);
|
||||||
validateEnum(validator, "txnAppliesTo", request.getTxnAppliesTo(), AmlAppliesTo.class);
|
validateEnum(validator, "txnAppliesTo", request.getTxnAppliesTo(), AmlAppliesTo.class);
|
||||||
|
if (StringUtils.isNotBlank(request.getTxnDirection()) && !"CREDIT".equalsIgnoreCase(request.getTxnDirection().trim())
|
||||||
|
&& !"DEBIT".equalsIgnoreCase(request.getTxnDirection().trim())) {
|
||||||
|
validator.reset().parameter("txnDirection").value(request.getTxnDirection()).failWithCode("must.be.credit.or.debit");
|
||||||
|
}
|
||||||
|
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
throw new PlatformApiDataValidationException(errors);
|
throw new PlatformApiDataValidationException(errors);
|
||||||
|
|||||||
-83
@@ -1,83 +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.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.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.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.InvestmentAmortisationData;
|
|
||||||
import org.apache.fineract.portfolio.investment.data.InvestmentAmortisationDueData;
|
|
||||||
import org.apache.fineract.portfolio.investment.service.InvestmentAmortisationService;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
@Path("/v1/investment/amortisation")
|
|
||||||
@Component
|
|
||||||
@Tag(name = "Investment Premium Amortisation", description = "Premium of HTM holdings amortised straight-line to maturity every 31 March.")
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class InvestmentAmortisationApiResource {
|
|
||||||
|
|
||||||
private final PlatformSecurityContext context;
|
|
||||||
private final InvestmentAmortisationService amortisationService;
|
|
||||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Path("{purchaseId}")
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "Amortisation schedule of a holding (06 Amortize Entry)")
|
|
||||||
public InvestmentAmortisationData schedule(@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId) {
|
|
||||||
this.context.authenticatedUser().validateHasReadPermission(InvestmentSaleApiConstants.ENTITY);
|
|
||||||
return this.amortisationService.schedule(purchaseId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Path("due")
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "Holdings due for amortisation on a 31 March (07 Amortize Yearly Process)", description = "fyEnd dd-MM-yyyy")
|
|
||||||
public List<InvestmentAmortisationDueData> due(@QueryParam("fyEnd") @Parameter(description = "dd-MM-yyyy") final String fyEnd) {
|
|
||||||
this.context.authenticatedUser().validateHasReadPermission(InvestmentSaleApiConstants.ENTITY);
|
|
||||||
return this.amortisationService.due(LocalDate.parse(fyEnd.trim(), DateTimeFormatter.ofPattern("dd-MM-yyyy")));
|
|
||||||
}
|
|
||||||
|
|
||||||
@POST
|
|
||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "Post amortisation for a 31 March", description = "With purchaseId: that holding (Amortize Entry); without: "
|
|
||||||
+ "every holding due (Amortize Yearly Process). One JV per holding: Dr 500604 / Cr premium GL.")
|
|
||||||
public CommandProcessingResult amortise(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
|
||||||
final CommandWrapper request = new CommandWrapperBuilder().amortiseInvestmentPremium().withJson(apiRequestBodyAsJson).build();
|
|
||||||
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-122
@@ -1,122 +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.investment.api;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
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");
|
|
||||||
|
|
||||||
/** Bond types that are non-SLR investments, counted against the RBI limit of 10% of deposits. */
|
|
||||||
public static final Set<String> NON_SLR_BOND_TYPES = Set.of("NON_SLR", "BOND", "MUTUAL_FUND");
|
|
||||||
|
|
||||||
/** Bond types that must carry a credit rating: corporate bonds, CPs and CDs. */
|
|
||||||
public static final Set<String> RATED_BOND_TYPES = Set.of("NON_SLR", "BOND");
|
|
||||||
|
|
||||||
/** Credit ratings on the Script Master: long-term (bonds) and short-term (CP / CD) scales, best first. */
|
|
||||||
public static final List<String> RATINGS = List.of("AAA", "AA+", "AA", "AA-", "A+", "A", "A-", "BBB+", "BBB", "BBB-", "BB+", "BB",
|
|
||||||
"BB-", "B+", "B", "B-", "C", "D", "A1+", "A1", "A2+", "A2", "A3+", "A3", "A4+", "A4", "UNRATED");
|
|
||||||
|
|
||||||
/** RBI: corporate bonds rated "A" or higher, CPs and CDs rated "A2" or higher. */
|
|
||||||
public static final Set<String> ELIGIBLE_RATINGS = Set.of("AAA", "AA+", "AA", "AA-", "A+", "A", "A-", "A1+", "A1", "A2+", "A2");
|
|
||||||
|
|
||||||
/** Credit rating agencies registered with SEBI. */
|
|
||||||
public static final List<String> RATING_AGENCIES = List.of("CRISIL", "ICRA", "CARE", "INDIA_RATINGS", "ACUITE", "BRICKWORK",
|
|
||||||
"INFOMERICS");
|
|
||||||
|
|
||||||
/** RBI: only debt and money-market mutual funds; their sub types start with this. */
|
|
||||||
public static final String DEBT_MF_SUB_TYPE_PREFIX = "DEBT";
|
|
||||||
|
|
||||||
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 INTEREST_GL_ACCOUNT_ID = "interestGlAccountId";
|
|
||||||
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";
|
|
||||||
public static final String TDS_APPLICABLE = "tdsApplicable";
|
|
||||||
public static final String TDS_RATE = "tdsRate";
|
|
||||||
|
|
||||||
// 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 String RATING = "rating";
|
|
||||||
public static final String RATING_AGENCY = "ratingAgency";
|
|
||||||
public static final String LISTED = "listed";
|
|
||||||
|
|
||||||
public static final Set<String> PARTY_PARAMS = Set.of(BOND_TYPE, PARTY_NAME, ADDRESS1, ADDRESS2, ADDRESS3, CITY, PINCODE, PHONE, FAX,
|
|
||||||
BANK_TYPE, GL_ACCOUNT_ID, INTEREST_GL_ACCOUNT_ID, RBI_CODE, EXPOSURE_LIMIT, LEI_NO, CP_BANK_NAME, CP_BRANCH_NAME, CP_ACCOUNT_NO, CP_IFSC, ACTIVE,
|
|
||||||
TDS_APPLICABLE, TDS_RATE, LOCALE);
|
|
||||||
|
|
||||||
public static final Set<String> SCRIPT_PARAMS = Set.of(BOND_TYPE, SLR_TYPE, SCRIPT_DESC, GL_ACCOUNT_ID, ISIN, FACE_VALUE_PER_UNIT,
|
|
||||||
INT_RATE, ISSUE_DATE, MATURITY_DATE, COMPANY_NAME, ADDRESS1, ADDRESS2, ADDRESS3, CITY, PINCODE, PHONE, FAX, CORP_CODE,
|
|
||||||
RATING, RATING_AGENCY, LISTED, ACTIVE, LOCALE, DATE_FORMAT);
|
|
||||||
}
|
|
||||||
-66
@@ -1,66 +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.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.GET;
|
|
||||||
import jakarta.ws.rs.Path;
|
|
||||||
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.time.format.DateTimeParseException;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
|
||||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
|
||||||
import org.apache.fineract.portfolio.investment.data.InvestmentBucketReportData;
|
|
||||||
import org.apache.fineract.portfolio.investment.service.InvestmentBucketReportService;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
@Path("/v1/investment/bucket-report")
|
|
||||||
@Component
|
|
||||||
@Tag(name = "Investment Bucket Wise", description = "Investments held on a date, split by the time left to maturity.")
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class InvestmentBucketReportApiResource {
|
|
||||||
|
|
||||||
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
|
||||||
|
|
||||||
private final PlatformSecurityContext context;
|
|
||||||
private final InvestmentBucketReportService reportService;
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "Bucket Wise Investment", description = "asOnDate dd-MM-yyyy; default today")
|
|
||||||
public InvestmentBucketReportData report(@QueryParam("asOnDate") @Parameter(description = "dd-MM-yyyy") final String asOnDate) {
|
|
||||||
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
|
|
||||||
if (asOnDate == null || asOnDate.isBlank()) {
|
|
||||||
return this.reportService.report(DateUtils.getBusinessLocalDate());
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return this.reportService.report(LocalDate.parse(asOnDate.trim(), DATE));
|
|
||||||
} catch (final DateTimeParseException e) {
|
|
||||||
throw new GeneralPlatformDomainRuleException("error.msg.investment.report.date.invalid", "Enter the date as DD-MM-YYYY",
|
|
||||||
asOnDate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-41
@@ -1,41 +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.investment.api;
|
|
||||||
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/** Constants for the Daily Script Master (market rate of a script for a day). */
|
|
||||||
public final class InvestmentDailyRateApiConstants {
|
|
||||||
|
|
||||||
private InvestmentDailyRateApiConstants() {}
|
|
||||||
|
|
||||||
public static final String ENTITY = "INVESTMENTDAILYRATE";
|
|
||||||
public static final String RESOURCE = "investmentdailyrate";
|
|
||||||
|
|
||||||
public static final String SCRIPT_ID = "scriptId";
|
|
||||||
public static final String RATE_DATE = "rateDate";
|
|
||||||
public static final String OPEN_RATE = "openRate";
|
|
||||||
public static final String HIGH_RATE = "highRate";
|
|
||||||
public static final String LOW_RATE = "lowRate";
|
|
||||||
public static final String CLOSE_RATE = "closeRate";
|
|
||||||
public static final String LOCALE = "locale";
|
|
||||||
public static final String DATE_FORMAT = "dateFormat";
|
|
||||||
|
|
||||||
public static final Set<String> PARAMS = Set.of(SCRIPT_ID, RATE_DATE, OPEN_RATE, HIGH_RATE, LOW_RATE, CLOSE_RATE, LOCALE, DATE_FORMAT);
|
|
||||||
}
|
|
||||||
-96
@@ -1,96 +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.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.core.service.DateUtils;
|
|
||||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
|
||||||
import org.apache.fineract.portfolio.investment.data.InvestmentDailyRateData;
|
|
||||||
import org.apache.fineract.portfolio.investment.service.InvestmentDailyRateReadPlatformService;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
@Path("/v1/investment/daily-rates")
|
|
||||||
@Component
|
|
||||||
@Tag(name = "Investment Daily Script Master", description = "Market rate of each script for a day; Purchase Entry takes its rate from here.")
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class InvestmentDailyRateApiResource {
|
|
||||||
|
|
||||||
private final PlatformSecurityContext context;
|
|
||||||
private final InvestmentDailyRateReadPlatformService readPlatformService;
|
|
||||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "List daily script rates, newest first")
|
|
||||||
public List<InvestmentDailyRateData> retrieveAll(@QueryParam("scriptId") @Parameter(description = "scriptId") final Long scriptId) {
|
|
||||||
this.context.authenticatedUser().validateHasReadPermission(InvestmentDailyRateApiConstants.ENTITY);
|
|
||||||
return this.readPlatformService.retrieveAll(scriptId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Path("latest")
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "Newest rate of a script on or before a date", description = "onDate (dd-MM-yyyy) defaults to the business "
|
|
||||||
+ "date. Returns no content when no rate has been entered for the script.")
|
|
||||||
public InvestmentDailyRateData retrieveLatest(@QueryParam("scriptId") @Parameter(description = "scriptId") final Long scriptId,
|
|
||||||
@QueryParam("onDate") @Parameter(description = "dd-MM-yyyy") final String onDate) {
|
|
||||||
this.context.authenticatedUser().validateHasReadPermission(InvestmentDailyRateApiConstants.ENTITY);
|
|
||||||
final LocalDate on = StringUtils.isBlank(onDate) ? DateUtils.getBusinessLocalDate()
|
|
||||||
: LocalDate.parse(onDate.trim(), DateTimeFormatter.ofPattern("dd-MM-yyyy"));
|
|
||||||
return this.readPlatformService.retrieveLatest(scriptId, on);
|
|
||||||
}
|
|
||||||
|
|
||||||
@POST
|
|
||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "Save a daily script rate")
|
|
||||||
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
|
||||||
final CommandWrapper request = new CommandWrapperBuilder().createInvestmentDailyRate().withJson(apiRequestBodyAsJson).build();
|
|
||||||
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
@DELETE
|
|
||||||
@Path("{rateId}")
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "Drop a daily script rate")
|
|
||||||
public CommandProcessingResult delete(@PathParam("rateId") @Parameter(description = "rateId") final Long rateId) {
|
|
||||||
final CommandWrapper request = new CommandWrapperBuilder().deleteInvestmentDailyRate(rateId).build();
|
|
||||||
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-78
@@ -1,78 +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.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.GET;
|
|
||||||
import jakarta.ws.rs.Path;
|
|
||||||
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.time.format.DateTimeParseException;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
|
||||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
|
||||||
import org.apache.fineract.portfolio.investment.data.InvestmentDiaryData;
|
|
||||||
import org.apache.fineract.portfolio.investment.service.InvestmentDiaryService;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
@Path("/v1/investment/diary")
|
|
||||||
@Component
|
|
||||||
@Tag(name = "Investment Due Date Diary", description = "Coupons, provisions, amortisation, FD payouts and maturities due in a period.")
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class InvestmentDiaryApiResource {
|
|
||||||
|
|
||||||
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
|
||||||
/** Default window when no dates are given: today and the next 30 days. */
|
|
||||||
private static final int DEFAULT_DAYS = 30;
|
|
||||||
|
|
||||||
private final PlatformSecurityContext context;
|
|
||||||
private final InvestmentDiaryService diaryService;
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "Due Date Diary for all investments", description = "fromDate / toDate dd-MM-yyyy; default today to 30 days ahead")
|
|
||||||
public InvestmentDiaryData diary(@QueryParam("fromDate") @Parameter(description = "dd-MM-yyyy") final String fromDate,
|
|
||||||
@QueryParam("toDate") @Parameter(description = "dd-MM-yyyy") final String toDate) {
|
|
||||||
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
|
|
||||||
final LocalDate today = DateUtils.getBusinessLocalDate();
|
|
||||||
final LocalDate from = date(fromDate, today);
|
|
||||||
final LocalDate to = date(toDate, from.plusDays(DEFAULT_DAYS));
|
|
||||||
if (to.isBefore(from)) {
|
|
||||||
throw new GeneralPlatformDomainRuleException("error.msg.investment.diary.dates", "The To date cannot be before the From date");
|
|
||||||
}
|
|
||||||
return this.diaryService.diary(from, to);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static LocalDate date(final String value, final LocalDate fallback) {
|
|
||||||
if (value == null || value.isBlank()) {
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return LocalDate.parse(value.trim(), DATE);
|
|
||||||
} catch (final DateTimeParseException e) {
|
|
||||||
throw new GeneralPlatformDomainRuleException("error.msg.investment.diary.date.invalid", "Enter the date as DD-MM-YYYY", value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-119
@@ -1,119 +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.investment.api;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/** Fixed deposits placed with other banks. */
|
|
||||||
public final class InvestmentFdApiConstants {
|
|
||||||
|
|
||||||
private InvestmentFdApiConstants() {}
|
|
||||||
|
|
||||||
public static final String ENTITY = "INVESTMENTFD";
|
|
||||||
public static final String BOND_TYPE_FD = "FD";
|
|
||||||
|
|
||||||
public static final String STATUS_ACTIVE = "ACTIVE";
|
|
||||||
public static final String STATUS_MATURED = "MATURED";
|
|
||||||
public static final String STATUS_PREMATURE = "PREMATURE";
|
|
||||||
public static final String STATUS_RENEWED = "RENEWED";
|
|
||||||
|
|
||||||
/** When the interest is paid: all at maturity (the usual case), or every half year / year. */
|
|
||||||
public static final String PAYOUT_AT_MATURITY = "MATURITY";
|
|
||||||
public static final String PAYOUT_HALF_YEARLY = "HALF_YEARLY";
|
|
||||||
public static final String PAYOUT_YEARLY = "YEARLY";
|
|
||||||
public static final Set<String> PAYOUTS = Set.of(PAYOUT_AT_MATURITY, PAYOUT_HALF_YEARLY, PAYOUT_YEARLY);
|
|
||||||
|
|
||||||
/** How the maturity amount is worked out, to compare with the bank's figure (Jitendra Sir). */
|
|
||||||
public static final String CALC_SIMPLE = "SIMPLE";
|
|
||||||
public static final String CALC_MONTHLY = "MONTHLY";
|
|
||||||
public static final String CALC_QUARTERLY = "QUARTERLY";
|
|
||||||
public static final String CALC_HALF_YEARLY = "HALF_YEARLY";
|
|
||||||
public static final String CALC_YEARLY = "YEARLY";
|
|
||||||
public static final Set<String> CALC_TYPES = Set.of(CALC_SIMPLE, CALC_MONTHLY, CALC_QUARTERLY, CALC_HALF_YEARLY, CALC_YEARLY);
|
|
||||||
|
|
||||||
/** RBI: deposits with other banks within 20% of total deposits as on the previous 31 March, and within 5% with one bank. */
|
|
||||||
public static final BigDecimal GROSS_LIMIT_PERCENT = BigDecimal.valueOf(20);
|
|
||||||
public static final BigDecimal BANK_LIMIT_PERCENT = BigDecimal.valueOf(5);
|
|
||||||
/** Current accounts with banks also count towards the 20% inter-bank exposure. */
|
|
||||||
public static final String BANK_BALANCE_HEADER_GL_CODE = "100200";
|
|
||||||
/** Deposits of the bank, for the suggested total-deposits figure: everything under the deposit header, plus RD. */
|
|
||||||
public static final String DEPOSIT_HEADER_GL_CODE = "200100";
|
|
||||||
public static final String RD_DEPOSIT_GL_CODE = "200201";
|
|
||||||
|
|
||||||
public static final String PARTY_ID = "partyId";
|
|
||||||
public static final String CERT_NO = "certNo";
|
|
||||||
public static final String CERT_DATE = "certDate";
|
|
||||||
public static final String DEAL_DATE = "dealDate";
|
|
||||||
public static final String AMOUNT = "amount";
|
|
||||||
public static final String INT_RATE = "intRate";
|
|
||||||
public static final String MATURITY_DATE = "maturityDate";
|
|
||||||
public static final String PAYOUT = "payout";
|
|
||||||
public static final String CALC_TYPE = "calcType";
|
|
||||||
public static final String BANK_GL_ACCOUNT_ID = "bankGlAccountId";
|
|
||||||
public static final String REMARKS = "remarks";
|
|
||||||
public static final String AS_ON_DATE = "asOnDate";
|
|
||||||
public static final String TOTAL_DEPOSITS = "totalDeposits";
|
|
||||||
public static final String LOCALE = "locale";
|
|
||||||
public static final String DATE_FORMAT = "dateFormat";
|
|
||||||
|
|
||||||
public static final String CLOSURE_MATURITY = "MATURITY";
|
|
||||||
public static final String CLOSURE_PREMATURE = "PREMATURE";
|
|
||||||
public static final String CLOSURE_RENEWAL = "RENEWAL";
|
|
||||||
public static final String LINE_PROVISION = "PROVISION";
|
|
||||||
public static final String LINE_PAYOUT = "PAYOUT";
|
|
||||||
public static final String INTEREST_RECEIVABLE_GL_CODE = "100603";
|
|
||||||
/** Tax the other bank deducts from the interest, recoverable from the tax department. */
|
|
||||||
public static final String TDS_RECEIVABLE_GL_CODE = "100602";
|
|
||||||
|
|
||||||
public static final String CLOSE_DATE = "closeDate";
|
|
||||||
public static final String RECEIVED_AMOUNT = "receivedAmount";
|
|
||||||
public static final String TDS = "tds";
|
|
||||||
public static final String PENAL_AMOUNT = "penalAmount";
|
|
||||||
public static final String PRINCIPAL_BANK_GL_ACCOUNT_ID = "principalBankGlAccountId";
|
|
||||||
public static final String INTEREST_BANK_GL_ACCOUNT_ID = "interestBankGlAccountId";
|
|
||||||
public static final String NEW_CERT_NO = "newCertNo";
|
|
||||||
public static final String NEW_CERT_DATE = "newCertDate";
|
|
||||||
public static final String NEW_INT_RATE = "newIntRate";
|
|
||||||
public static final String NEW_MATURITY_DATE = "newMaturityDate";
|
|
||||||
public static final String NEW_PAYOUT = "newPayout";
|
|
||||||
public static final String NEW_CALC_TYPE = "newCalcType";
|
|
||||||
public static final String MATURITY_AMOUNT = "maturityAmount";
|
|
||||||
public static final String RENEWAL_AMOUNT = "renewalAmount";
|
|
||||||
public static final String DIFF_BANK_GL_ACCOUNT_ID = "diffBankGlAccountId";
|
|
||||||
|
|
||||||
public static final String LINE_MATURITY = "MATURITY";
|
|
||||||
public static final String LINE_TYPE = "lineType";
|
|
||||||
public static final String INT_DATE = "intDate";
|
|
||||||
/** Reminders show lines due within this many days (as for Govt securities). */
|
|
||||||
public static final int DUE_WITHIN_DAYS = 2;
|
|
||||||
public static final Set<String> INTEREST_PARAMS = Set.of(LINE_TYPE, INT_DATE, RECEIVED_AMOUNT, TDS, BANK_GL_ACCOUNT_ID, LOCALE,
|
|
||||||
DATE_FORMAT);
|
|
||||||
|
|
||||||
public static final Set<String> MATURE_PARAMS = Set.of(RECEIVED_AMOUNT, TDS, PRINCIPAL_BANK_GL_ACCOUNT_ID, INTEREST_BANK_GL_ACCOUNT_ID,
|
|
||||||
REMARKS, LOCALE, DATE_FORMAT);
|
|
||||||
public static final Set<String> PREMATURE_PARAMS = Set.of(CLOSE_DATE, RECEIVED_AMOUNT, TDS, PENAL_AMOUNT, PRINCIPAL_BANK_GL_ACCOUNT_ID,
|
|
||||||
INTEREST_BANK_GL_ACCOUNT_ID, REMARKS, LOCALE, DATE_FORMAT);
|
|
||||||
public static final Set<String> RENEW_PARAMS = Set.of(NEW_CERT_NO, NEW_CERT_DATE, NEW_INT_RATE, NEW_MATURITY_DATE, NEW_PAYOUT,
|
|
||||||
NEW_CALC_TYPE, MATURITY_AMOUNT, RENEWAL_AMOUNT, TDS, DIFF_BANK_GL_ACCOUNT_ID, REMARKS, LOCALE, DATE_FORMAT);
|
|
||||||
|
|
||||||
public static final Set<String> FD_PARAMS = Set.of(PARTY_ID, CERT_NO, CERT_DATE, DEAL_DATE, AMOUNT, INT_RATE, MATURITY_DATE, PAYOUT,
|
|
||||||
CALC_TYPE, BANK_GL_ACCOUNT_ID, REMARKS, LOCALE, DATE_FORMAT);
|
|
||||||
public static final Set<String> BASE_PARAMS = Set.of(AS_ON_DATE, TOTAL_DEPOSITS, REMARKS, LOCALE, DATE_FORMAT);
|
|
||||||
}
|
|
||||||
-199
@@ -1,199 +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.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.Path;
|
|
||||||
import jakarta.ws.rs.PathParam;
|
|
||||||
import jakarta.ws.rs.Produces;
|
|
||||||
import jakarta.ws.rs.QueryParam;
|
|
||||||
import jakarta.ws.rs.core.MediaType;
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
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.core.exception.GeneralPlatformDomainRuleException;
|
|
||||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
|
||||||
import org.apache.fineract.portfolio.investment.data.InvestmentFdClosureData;
|
|
||||||
import org.apache.fineract.portfolio.investment.data.InvestmentFdData;
|
|
||||||
import org.apache.fineract.portfolio.investment.data.InvestmentFdDueData;
|
|
||||||
import org.apache.fineract.portfolio.investment.data.InvestmentFdScheduleData;
|
|
||||||
import org.apache.fineract.portfolio.investment.service.InvestmentFdClosureService;
|
|
||||||
import org.apache.fineract.portfolio.investment.service.InvestmentFdInterestService;
|
|
||||||
import org.apache.fineract.portfolio.investment.service.InvestmentFdService;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
@Path("/v1/investment/fds")
|
|
||||||
@Component
|
|
||||||
@Tag(name = "Investment FD", description = "Fixed deposits the bank places with other banks.")
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class InvestmentFdApiResource {
|
|
||||||
|
|
||||||
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
|
||||||
|
|
||||||
private final PlatformSecurityContext context;
|
|
||||||
private final InvestmentFdService fdService;
|
|
||||||
private final InvestmentFdClosureService closureService;
|
|
||||||
private final InvestmentFdInterestService interestService;
|
|
||||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "All FDs placed with other banks")
|
|
||||||
public List<InvestmentFdData> retrieveAll() {
|
|
||||||
this.context.authenticatedUser().validateHasReadPermission(InvestmentFdApiConstants.ENTITY);
|
|
||||||
return this.fdService.retrieveAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Path("quote")
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "Preview an FD: interest, maturity amount and the RBI limits", description = "Dates dd-MM-yyyy")
|
|
||||||
public InvestmentFdData quote(@QueryParam("partyId") final Long partyId, @QueryParam("amount") final BigDecimal amount,
|
|
||||||
@QueryParam("intRate") final BigDecimal intRate, @QueryParam("dealDate") final String dealDate,
|
|
||||||
@QueryParam("maturityDate") final String maturityDate, @QueryParam("payout") final String payout,
|
|
||||||
@QueryParam("calcType") final String calcType) {
|
|
||||||
this.context.authenticatedUser().validateHasReadPermission(InvestmentFdApiConstants.ENTITY);
|
|
||||||
return this.fdService.quote(partyId, amount, intRate, date(dealDate), date(maturityDate), payout, calcType);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Path("due")
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "FD interest vouchers and maturities due", description = "Due now or within withinDays (default 2)")
|
|
||||||
public List<InvestmentFdDueData> due(@QueryParam("withinDays") final Integer withinDays) {
|
|
||||||
this.context.authenticatedUser().validateHasReadPermission(InvestmentFdApiConstants.ENTITY);
|
|
||||||
return this.interestService.due(withinDays == null ? InvestmentFdApiConstants.DUE_WITHIN_DAYS : withinDays);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Path("{fdId}/interest-schedule")
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "Interest lines of an FD", description = "31 Mar / 30 Sep provisions, payouts and maturity")
|
|
||||||
public InvestmentFdScheduleData schedule(@PathParam("fdId") final Long fdId) {
|
|
||||||
this.context.authenticatedUser().validateHasReadPermission(InvestmentFdApiConstants.ENTITY);
|
|
||||||
return this.interestService.schedule(fdId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@POST
|
|
||||||
@Path("{fdId}/interest")
|
|
||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "Post the next interest line of an FD", description = "lineType PROVISION | PAYOUT, intDate; payout: receivedAmount, tds")
|
|
||||||
public CommandProcessingResult postInterest(@PathParam("fdId") final Long fdId, @Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
|
||||||
return this.commandsSourceWritePlatformService
|
|
||||||
.logCommandSource(new CommandWrapperBuilder().postInvestmentFdInterest(fdId).withJson(apiRequestBodyAsJson).build());
|
|
||||||
}
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Path("{fdId}")
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "One FD")
|
|
||||||
public InvestmentFdData retrieveOne(@PathParam("fdId") @Parameter(description = "fdId") final Long fdId) {
|
|
||||||
this.context.authenticatedUser().validateHasReadPermission(InvestmentFdApiConstants.ENTITY);
|
|
||||||
return this.fdService.retrieveOne(fdId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@POST
|
|
||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "Place an FD", description = "Dr the bank's FD GL / Cr our bank GL, dated the deal date; refused beyond the RBI limits.")
|
|
||||||
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
|
||||||
final CommandWrapper request = new CommandWrapperBuilder().createInvestmentFd().withJson(apiRequestBodyAsJson).build();
|
|
||||||
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Path("{fdId}/closure-quote")
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "Preview a maturity, premature closure or renewal", description = "type MATURITY | PREMATURE | RENEWAL")
|
|
||||||
public InvestmentFdClosureData closureQuote(@PathParam("fdId") final Long fdId, @QueryParam("type") final String type,
|
|
||||||
@QueryParam("closeDate") final String closeDate, @QueryParam("receivedAmount") final BigDecimal receivedAmount,
|
|
||||||
@QueryParam("tds") final BigDecimal tds, @QueryParam("renewalAmount") final BigDecimal renewalAmount,
|
|
||||||
@QueryParam("principalBankGlAccountId") final Long principalBankGlAccountId,
|
|
||||||
@QueryParam("interestBankGlAccountId") final Long interestBankGlAccountId,
|
|
||||||
@QueryParam("diffBankGlAccountId") final Long diffBankGlAccountId) {
|
|
||||||
this.context.authenticatedUser().validateHasReadPermission(InvestmentFdApiConstants.ENTITY);
|
|
||||||
return this.closureService.quote(fdId, type, date(closeDate), receivedAmount, tds, renewalAmount, principalBankGlAccountId,
|
|
||||||
interestBankGlAccountId, diffBankGlAccountId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@POST
|
|
||||||
@Path("{fdId}/mature")
|
|
||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "Close an FD at maturity", description = "Dated the maturity date; amount as credited by the bank, optional TDS.")
|
|
||||||
public CommandProcessingResult mature(@PathParam("fdId") final Long fdId, @Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
|
||||||
return this.commandsSourceWritePlatformService
|
|
||||||
.logCommandSource(new CommandWrapperBuilder().matureInvestmentFd(fdId).withJson(apiRequestBodyAsJson).build());
|
|
||||||
}
|
|
||||||
|
|
||||||
@POST
|
|
||||||
@Path("{fdId}/premature")
|
|
||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "Close an FD before maturity", description = "Not calculated: the amount received (and any penal interest) is entered.")
|
|
||||||
public CommandProcessingResult premature(@PathParam("fdId") final Long fdId, @Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
|
||||||
return this.commandsSourceWritePlatformService
|
|
||||||
.logCommandSource(new CommandWrapperBuilder().prematureInvestmentFd(fdId).withJson(apiRequestBodyAsJson).build());
|
|
||||||
}
|
|
||||||
|
|
||||||
@POST
|
|
||||||
@Path("{fdId}/renew")
|
|
||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "Renew an FD at maturity", description = "New certificate, rate and maturity; money added or taken out goes through a bank GL.")
|
|
||||||
public CommandProcessingResult renew(@PathParam("fdId") final Long fdId, @Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
|
||||||
return this.commandsSourceWritePlatformService
|
|
||||||
.logCommandSource(new CommandWrapperBuilder().renewInvestmentFd(fdId).withJson(apiRequestBodyAsJson).build());
|
|
||||||
}
|
|
||||||
|
|
||||||
@POST
|
|
||||||
@Path("deposit-base")
|
|
||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "Record total deposits as on a 31 March", description = "The base of the RBI limits for the following year.")
|
|
||||||
public CommandProcessingResult setDepositBase(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
|
||||||
final CommandWrapper request = new CommandWrapperBuilder().setInvestmentDepositBase().withJson(apiRequestBodyAsJson).build();
|
|
||||||
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static LocalDate date(final String value) {
|
|
||||||
if (StringUtils.isBlank(value)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return LocalDate.parse(value.trim(), DATE);
|
|
||||||
} catch (final java.time.format.DateTimeParseException e) {
|
|
||||||
// A half-typed date is a bad request, not a server error.
|
|
||||||
throw new GeneralPlatformDomainRuleException("error.msg.investmentfd.date.invalid", "Enter the date as DD-MM-YYYY", value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-51
@@ -1,51 +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.investment.api;
|
|
||||||
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/** Constants for posting an interest schedule line (coupon or closing provision) of a holding to GL. */
|
|
||||||
public final class InvestmentInterestApiConstants {
|
|
||||||
|
|
||||||
private InvestmentInterestApiConstants() {}
|
|
||||||
|
|
||||||
public static final String ENTITY = "INVESTMENTINTEREST";
|
|
||||||
public static final String RESOURCE = "investmentinterest";
|
|
||||||
|
|
||||||
public static final String LINE_TYPE = "lineType";
|
|
||||||
public static final String INT_DATE = "intDate";
|
|
||||||
/** Bank GL the coupon is received into; defaults to the purchase's paying bank. */
|
|
||||||
public static final String BANK_GL_ACCOUNT_ID = "bankGlAccountId";
|
|
||||||
public static final String LOCALE = "locale";
|
|
||||||
public static final String DATE_FORMAT = "dateFormat";
|
|
||||||
|
|
||||||
public static final Set<String> PARAMS = Set.of(LINE_TYPE, INT_DATE, BANK_GL_ACCOUNT_ID, LOCALE, DATE_FORMAT);
|
|
||||||
|
|
||||||
/** A line shows as a task this many days before its date (Jitendra Sir). */
|
|
||||||
public static final int DUE_WITHIN_DAYS = 2;
|
|
||||||
|
|
||||||
public static final String INTEREST_RECEIVABLE_GL_CODE = "100603";
|
|
||||||
public static final String INTEREST_INCOME_GOVT_SECURITIES_GL_CODE = "400401";
|
|
||||||
public static final String INTEREST_INCOME_BONDS_GL_CODE = "400402";
|
|
||||||
|
|
||||||
/** Income GL for a holding's interest: SLR govt securities, or bonds / debentures for the rest. */
|
|
||||||
public static String interestIncomeGlCode(final String bondType) {
|
|
||||||
return "GOVT_SECURITY".equals(bondType) ? INTEREST_INCOME_GOVT_SECURITIES_GL_CODE : INTEREST_INCOME_BONDS_GL_CODE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-65
@@ -1,65 +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.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.GET;
|
|
||||||
import jakarta.ws.rs.Path;
|
|
||||||
import jakarta.ws.rs.Produces;
|
|
||||||
import jakarta.ws.rs.QueryParam;
|
|
||||||
import jakarta.ws.rs.core.MediaType;
|
|
||||||
import java.util.List;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
|
||||||
import org.apache.fineract.portfolio.investment.data.InvestmentInterestDueData;
|
|
||||||
import org.apache.fineract.portfolio.investment.data.InvestmentInterestPostingData;
|
|
||||||
import org.apache.fineract.portfolio.investment.service.InvestmentPurchaseReadPlatformService;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
@Path("/v1/investment/interest-postings")
|
|
||||||
@Component
|
|
||||||
@Tag(name = "Investment Interest Posting", description = "Interest lines of holdings that are due to be posted with a voucher.")
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class InvestmentInterestApiResource {
|
|
||||||
|
|
||||||
private final PlatformSecurityContext context;
|
|
||||||
private final InvestmentPurchaseReadPlatformService readPlatformService;
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "Posted interest lines", description = "Coupons and closing provisions already posted to GL with their "
|
|
||||||
+ "voucher, newest first.")
|
|
||||||
public List<InvestmentInterestPostingData> retrieveAll() {
|
|
||||||
this.context.authenticatedUser().validateHasReadPermission(InvestmentInterestApiConstants.ENTITY);
|
|
||||||
return this.readPlatformService.retrieveInterestPostings();
|
|
||||||
}
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Path("due")
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "Interest lines due", description = "For every active holding, its next unposted interest line when that "
|
|
||||||
+ "line's date is past or within withinDays days (default 2).")
|
|
||||||
public List<InvestmentInterestDueData> due(@QueryParam("withinDays") @Parameter(description = "withinDays") final Integer withinDays) {
|
|
||||||
this.context.authenticatedUser().validateHasReadPermission(InvestmentInterestApiConstants.ENTITY);
|
|
||||||
return this.readPlatformService
|
|
||||||
.retrieveDueInterest(withinDays == null ? InvestmentInterestApiConstants.DUE_WITHIN_DAYS : withinDays);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-79
@@ -1,79 +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.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.GET;
|
|
||||||
import jakarta.ws.rs.Path;
|
|
||||||
import jakarta.ws.rs.Produces;
|
|
||||||
import jakarta.ws.rs.QueryParam;
|
|
||||||
import jakarta.ws.rs.core.MediaType;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.Month;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.time.format.DateTimeParseException;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
|
||||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
|
||||||
import org.apache.fineract.portfolio.investment.data.InvestmentInterestReportData;
|
|
||||||
import org.apache.fineract.portfolio.investment.service.InvestmentInterestReportService;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
@Path("/v1/investment/interest-report")
|
|
||||||
@Component
|
|
||||||
@Tag(name = "Investment Interest Received & Receivable", description = "Interest received in a period and receivable at its end.")
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class InvestmentInterestReportApiResource {
|
|
||||||
|
|
||||||
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
|
||||||
|
|
||||||
private final PlatformSecurityContext context;
|
|
||||||
private final InvestmentInterestReportService reportService;
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "Total Interest Received & Receivable", description = "fromDate / toDate dd-MM-yyyy; default 1 April of the "
|
|
||||||
+ "current financial year to today")
|
|
||||||
public InvestmentInterestReportData report(@QueryParam("fromDate") @Parameter(description = "dd-MM-yyyy") final String fromDate,
|
|
||||||
@QueryParam("toDate") @Parameter(description = "dd-MM-yyyy") final String toDate) {
|
|
||||||
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
|
|
||||||
final LocalDate today = DateUtils.getBusinessLocalDate();
|
|
||||||
final LocalDate fyStart = LocalDate.of(today.getMonthValue() >= 4 ? today.getYear() : today.getYear() - 1, Month.APRIL, 1);
|
|
||||||
final LocalDate from = date(fromDate, fyStart);
|
|
||||||
final LocalDate to = date(toDate, today);
|
|
||||||
if (to.isBefore(from)) {
|
|
||||||
throw new GeneralPlatformDomainRuleException("error.msg.investment.report.dates", "The To date cannot be before the From date");
|
|
||||||
}
|
|
||||||
return this.reportService.report(from, to);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static LocalDate date(final String value, final LocalDate fallback) {
|
|
||||||
if (value == null || value.isBlank()) {
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return LocalDate.parse(value.trim(), DATE);
|
|
||||||
} catch (final DateTimeParseException e) {
|
|
||||||
throw new GeneralPlatformDomainRuleException("error.msg.investment.report.date.invalid", "Enter the date as DD-MM-YYYY", value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-99
@@ -1,99 +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.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-76
@@ -1,76 +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.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)");
|
|
||||||
|
|
||||||
/** Saved deal; nothing posted to GL until it is settled. */
|
|
||||||
public static final String STATUS_PENDING_SETTLEMENT = "PENDING_SETTLEMENT";
|
|
||||||
/** Settled: posted to GL on the settlement date, now a holding. */
|
|
||||||
public static final String STATUS_ACTIVE = "ACTIVE";
|
|
||||||
/** Every unit sold before maturity. */
|
|
||||||
public static final String STATUS_SOLD = "SOLD";
|
|
||||||
/** Repaid at maturity. */
|
|
||||||
public static final String STATUS_REDEEMED = "REDEEMED";
|
|
||||||
/** Dropped before settlement; kept so its deal and certificate numbers are never given out again. */
|
|
||||||
public static final String STATUS_DROPPED = "DROPPED";
|
|
||||||
|
|
||||||
/** GLs the settlement entry debits besides the script's investment GL. */
|
|
||||||
public static final String BROKEN_PERIOD_INTEREST_GL_CODE = "500606";
|
|
||||||
public static final String STAMP_DUTY_GL_CODE = "500601";
|
|
||||||
public static final String BROKERAGE_GL_CODE = "500602";
|
|
||||||
public static final String OTHER_CHARGES_GL_CODE = "500603";
|
|
||||||
|
|
||||||
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 OTHER_CHARGES = "otherCharges";
|
|
||||||
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, OTHER_CHARGES, CATEGORY, LAST_INT_DATE, CR_GL_ACCOUNT_ID, SUB_TYPE,
|
|
||||||
OTHER_DETAILS, LOCALE, DATE_FORMAT);
|
|
||||||
}
|
|
||||||
-203
@@ -1,203 +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.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.math.BigDecimal;
|
|
||||||
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.core.service.DateUtils;
|
|
||||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
|
||||||
import org.apache.fineract.portfolio.investment.data.InvestmentInterestScheduleData;
|
|
||||||
import org.apache.fineract.portfolio.investment.data.InvestmentNonSlrLimitData;
|
|
||||||
import org.apache.fineract.portfolio.investment.data.InvestmentPurchaseData;
|
|
||||||
import org.apache.fineract.portfolio.investment.data.InvestmentSaleData;
|
|
||||||
import org.apache.fineract.portfolio.investment.service.InvestmentNonSlrService;
|
|
||||||
import org.apache.fineract.portfolio.investment.service.InvestmentPurchaseReadPlatformService;
|
|
||||||
import org.apache.fineract.portfolio.investment.service.InvestmentSaleReadPlatformService;
|
|
||||||
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 InvestmentSaleReadPlatformService saleReadPlatformService;
|
|
||||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
|
||||||
private final InvestmentNonSlrService nonSlrService;
|
|
||||||
|
|
||||||
@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("non-slr-limit")
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "RBI non-SLR limits for a purchase", description = "Where a non-SLR purchase of amount (book value) in scriptId "
|
|
||||||
+ "on dealDate (dd-MM-yyyy, default today) stands against the 10% limits; any of them may be left out.")
|
|
||||||
public InvestmentNonSlrLimitData nonSlrLimit(@QueryParam("scriptId") @Parameter(description = "scriptId") final Long scriptId,
|
|
||||||
@QueryParam("dealDate") @Parameter(description = "dd-MM-yyyy") final String dealDate,
|
|
||||||
@QueryParam("amount") @Parameter(description = "amount") final BigDecimal amount) {
|
|
||||||
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
|
|
||||||
final LocalDate date = StringUtils.isBlank(dealDate) ? DateUtils.getBusinessLocalDate()
|
|
||||||
: LocalDate.parse(dealDate.trim(), DateTimeFormatter.ofPattern("dd-MM-yyyy"));
|
|
||||||
return this.nonSlrService.limits(date, scriptId, amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
@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);
|
|
||||||
}
|
|
||||||
|
|
||||||
@POST
|
|
||||||
@Path("{purchaseId}/settle")
|
|
||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "Settle a purchase", description = "On or after the settlement date: posts the purchase to GL dated the "
|
|
||||||
+ "settlement date and makes the deal an active holding.")
|
|
||||||
public CommandProcessingResult settle(@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId,
|
|
||||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
|
||||||
final CommandWrapper request = new CommandWrapperBuilder().settleInvestmentPurchase(purchaseId)
|
|
||||||
.withJson(StringUtils.isBlank(apiRequestBodyAsJson) ? "{}" : apiRequestBodyAsJson).build();
|
|
||||||
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
@POST
|
|
||||||
@Path("{purchaseId}/interest-postings")
|
|
||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "Post an interest line", description = "Posts the next interest schedule line (coupon or closing provision) "
|
|
||||||
+ "of a holding to GL with a JV voucher, on or after its date and in date order.")
|
|
||||||
public CommandProcessingResult postInterest(@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId,
|
|
||||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
|
||||||
final CommandWrapper request = new CommandWrapperBuilder().postInvestmentInterest(purchaseId).withJson(apiRequestBodyAsJson)
|
|
||||||
.build();
|
|
||||||
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Path("{purchaseId}/redemption-quote")
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "What the redemption at maturity will post", description = "Face value back against the book value still "
|
|
||||||
+ "held; refused before maturity or while a sale is pending.")
|
|
||||||
public InvestmentSaleData redemptionQuote(@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId) {
|
|
||||||
this.context.authenticatedUser().validateHasReadPermission(InvestmentSaleApiConstants.ENTITY);
|
|
||||||
return this.saleReadPlatformService.quoteRedemption(purchaseId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@POST
|
|
||||||
@Path("{purchaseId}/redeem")
|
|
||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "Redeem at maturity", description = "On or after maturity, once every coupon is posted: posts the face value "
|
|
||||||
+ "received, dated the maturity date, and closes the holding.")
|
|
||||||
public CommandProcessingResult redeem(@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId,
|
|
||||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
|
||||||
final CommandWrapper request = new CommandWrapperBuilder().redeemInvestmentPurchase(purchaseId)
|
|
||||||
.withJson(StringUtils.isBlank(apiRequestBodyAsJson) ? "{}" : apiRequestBodyAsJson).build();
|
|
||||||
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
@POST
|
|
||||||
@Path("{purchaseId}/write-off")
|
|
||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "Write off part of the book value", description = "Dr Loss on Investments / Cr Investment for the amount "
|
|
||||||
+ "given; the units stay held at the reduced book value.")
|
|
||||||
public CommandProcessingResult writeOff(@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId,
|
|
||||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
|
||||||
final CommandWrapper request = new CommandWrapperBuilder().writeOffInvestmentPurchase(purchaseId).withJson(apiRequestBodyAsJson)
|
|
||||||
.build();
|
|
||||||
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
@POST
|
|
||||||
@Path("{purchaseId}/transfer")
|
|
||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "GOI Category Transfer", description = "Moves the holding to another security (scriptId) of the same coupon "
|
|
||||||
+ "rate, maturity and unit value; the book value moves between GLs only when the two scripts use different GLs.")
|
|
||||||
public CommandProcessingResult transfer(@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId,
|
|
||||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
|
||||||
final CommandWrapper request = new CommandWrapperBuilder().transferInvestmentSecurity(purchaseId).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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-70
@@ -1,70 +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.investment.api;
|
|
||||||
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/** Sale (before maturity) and redemption (at maturity) of investment holdings. */
|
|
||||||
public final class InvestmentSaleApiConstants {
|
|
||||||
|
|
||||||
private InvestmentSaleApiConstants() {}
|
|
||||||
|
|
||||||
public static final String ENTITY = "INVESTMENTSALE";
|
|
||||||
public static final String RESOURCE = "investmentsale";
|
|
||||||
|
|
||||||
public static final String TYPE_SELL = "SELL";
|
|
||||||
public static final String TYPE_REDEEM = "REDEEM";
|
|
||||||
/** Part of the book value written off as a loss; the units stay held. */
|
|
||||||
public static final String TYPE_WRITEOFF = "WRITEOFF";
|
|
||||||
/** A year-end amortisation of premium (31 March); recorded like a sale so the book value comes down. */
|
|
||||||
public static final String TYPE_AMORTISE = "AMORTISE";
|
|
||||||
|
|
||||||
public static final String STATUS_PENDING_SETTLEMENT = "PENDING_SETTLEMENT";
|
|
||||||
public static final String STATUS_SETTLED = "SETTLED";
|
|
||||||
|
|
||||||
/** Profit on sale / redemption (income) and loss on sale / redemption (expense). */
|
|
||||||
public static final String PROFIT_ON_SALE_GL_CODE = "400405";
|
|
||||||
public static final String LOSS_ON_SALE_GL_CODE = "500605";
|
|
||||||
/** Premium still on the books at maturity is written off to the amortisation expense. */
|
|
||||||
public static final String PREMIUM_AMORTISATION_GL_CODE = "500604";
|
|
||||||
/** Premium paid over face value, held here until amortised (Jitendra Sir). */
|
|
||||||
public static final String PREMIUM_ON_INVESTMENTS_GL_CODE = "100707";
|
|
||||||
|
|
||||||
public static final String PURCHASE_ID = "purchaseId";
|
|
||||||
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 UNITS = "units";
|
|
||||||
public static final String SALE_RATE = "saleRate";
|
|
||||||
public static final String BROKERAGE = "brokerage";
|
|
||||||
public static final String BANK_GL_ACCOUNT_ID = "bankGlAccountId";
|
|
||||||
public static final String SANCTION_AUTHORITY = "sanctionAuthority";
|
|
||||||
public static final String REMARKS = "remarks";
|
|
||||||
public static final String AMOUNT = "amount";
|
|
||||||
public static final String FY_END = "fyEnd";
|
|
||||||
public static final String LOCALE = "locale";
|
|
||||||
public static final String DATE_FORMAT = "dateFormat";
|
|
||||||
|
|
||||||
public static final Set<String> SELL_PARAMS = Set.of(PURCHASE_ID, PARTY_ID, DEAL_DATE, DEAL_TIME, SETTLEMENT_TYPE, UNITS, SALE_RATE,
|
|
||||||
BROKERAGE, BANK_GL_ACCOUNT_ID, SANCTION_AUTHORITY, REMARKS, LOCALE, DATE_FORMAT);
|
|
||||||
public static final Set<String> REDEEM_PARAMS = Set.of(BANK_GL_ACCOUNT_ID, REMARKS, LOCALE, DATE_FORMAT);
|
|
||||||
public static final Set<String> WRITEOFF_PARAMS = Set.of(AMOUNT, REMARKS, LOCALE, DATE_FORMAT);
|
|
||||||
public static final Set<String> AMORTISE_PARAMS = Set.of(PURCHASE_ID, FY_END, LOCALE, DATE_FORMAT);
|
|
||||||
}
|
|
||||||
-116
@@ -1,116 +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.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.math.BigDecimal;
|
|
||||||
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.InvestmentSaleData;
|
|
||||||
import org.apache.fineract.portfolio.investment.service.InvestmentSaleReadPlatformService;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
@Path("/v1/investment/sales")
|
|
||||||
@Component
|
|
||||||
@Tag(name = "Investment Sale", description = "Sale of holdings before maturity (all or part of the units), settled on the settlement date.")
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class InvestmentSaleApiResource {
|
|
||||||
|
|
||||||
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
|
||||||
|
|
||||||
private final PlatformSecurityContext context;
|
|
||||||
private final InvestmentSaleReadPlatformService readPlatformService;
|
|
||||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "List sales and redemptions, newest first")
|
|
||||||
public List<InvestmentSaleData> retrieveAll() {
|
|
||||||
this.context.authenticatedUser().validateHasReadPermission(InvestmentSaleApiConstants.ENTITY);
|
|
||||||
return this.readPlatformService.retrieveAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Path("quote")
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "Work out a sale before saving it", description = "Price, interest from the buyer, cost, profit or loss "
|
|
||||||
+ "and the net amount for the units, rate and deal date given (dd-MM-yyyy).")
|
|
||||||
public InvestmentSaleData quote(@QueryParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId,
|
|
||||||
@QueryParam("dealDate") @Parameter(description = "dd-MM-yyyy") final String dealDate,
|
|
||||||
@QueryParam("settlementType") @Parameter(description = "T+0 / T+1 / T+2") final String settlementType,
|
|
||||||
@QueryParam("units") @Parameter(description = "units") final BigDecimal units,
|
|
||||||
@QueryParam("saleRate") @Parameter(description = "saleRate") final BigDecimal saleRate,
|
|
||||||
@QueryParam("brokerage") @Parameter(description = "brokerage") final BigDecimal brokerage,
|
|
||||||
@QueryParam("bankGlAccountId") @Parameter(description = "bankGlAccountId") final Long bankGlAccountId) {
|
|
||||||
this.context.authenticatedUser().validateHasReadPermission(InvestmentSaleApiConstants.ENTITY);
|
|
||||||
final LocalDate deal = LocalDate.parse(dealDate.trim(), DATE);
|
|
||||||
return this.readPlatformService.quoteSale(purchaseId, deal, StringUtils.defaultIfBlank(settlementType, "T+0"), units, saleRate,
|
|
||||||
brokerage, bankGlAccountId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@POST
|
|
||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "Save a sale", description = "Saved pending; nothing reaches the GL until it is settled.")
|
|
||||||
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
|
||||||
final CommandWrapper request = new CommandWrapperBuilder().createInvestmentSale().withJson(apiRequestBodyAsJson).build();
|
|
||||||
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
@POST
|
|
||||||
@Path("{saleId}/settle")
|
|
||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "Settle a sale", description = "On or after the settlement date: posts the sale to GL dated the settlement "
|
|
||||||
+ "date and reduces the units held.")
|
|
||||||
public CommandProcessingResult settle(@PathParam("saleId") @Parameter(description = "saleId") final Long saleId,
|
|
||||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
|
||||||
final CommandWrapper request = new CommandWrapperBuilder().settleInvestmentSale(saleId)
|
|
||||||
.withJson(StringUtils.isBlank(apiRequestBodyAsJson) ? "{}" : apiRequestBodyAsJson).build();
|
|
||||||
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
@DELETE
|
|
||||||
@Path("{saleId}")
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "Drop a pending sale")
|
|
||||||
public CommandProcessingResult delete(@PathParam("saleId") @Parameter(description = "saleId") final Long saleId) {
|
|
||||||
final CommandWrapper request = new CommandWrapperBuilder().deleteInvestmentSale(saleId).build();
|
|
||||||
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-89
@@ -1,89 +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.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-50
@@ -1,50 +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.investment.api;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
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.util.List;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
|
||||||
import org.apache.fineract.portfolio.investment.data.InvestmentSecurityTransferData;
|
|
||||||
import org.apache.fineract.portfolio.investment.service.InvestmentSecurityTransferService;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
@Path("/v1/investment/transfers")
|
|
||||||
@Component
|
|
||||||
@Tag(name = "Investment Security Transfer", description = "GOI Category Transfer: a holding moved to another security of the same terms.")
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class InvestmentTransferApiResource {
|
|
||||||
|
|
||||||
private final PlatformSecurityContext context;
|
|
||||||
private final InvestmentSecurityTransferService transferService;
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "List security transfers, newest first")
|
|
||||||
public List<InvestmentSecurityTransferData> retrieveAll() {
|
|
||||||
this.context.authenticatedUser().validateHasReadPermission("INVESTMENTTRANSFER");
|
|
||||||
return this.transferService.retrieveAll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-64
@@ -1,64 +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.investment.data;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.List;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
/** Premium amortisation schedule of an HTM holding: one line per 31 March to maturity, then the rest at maturity. */
|
|
||||||
@Getter
|
|
||||||
@Builder
|
|
||||||
public class InvestmentAmortisationData {
|
|
||||||
|
|
||||||
private final Long purchaseId;
|
|
||||||
private final String dealNo;
|
|
||||||
private final String certNo;
|
|
||||||
private final String scriptCode;
|
|
||||||
private final String scriptDesc;
|
|
||||||
private final String category;
|
|
||||||
private final String settlementDate;
|
|
||||||
private final String maturityDate;
|
|
||||||
private final BigDecimal faceValue;
|
|
||||||
private final BigDecimal premium;
|
|
||||||
private final BigDecimal unamortisedPremium;
|
|
||||||
private final BigDecimal bookValue;
|
|
||||||
private final String premiumGlCode;
|
|
||||||
private final List<Row> rows;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Builder
|
|
||||||
public static class Row {
|
|
||||||
|
|
||||||
/** YEAR (31 March) or MATURITY (the remainder, written off on redemption). */
|
|
||||||
private final String kind;
|
|
||||||
private final String fyEnd;
|
|
||||||
private final String fromDate;
|
|
||||||
private final String toDate;
|
|
||||||
private final long days;
|
|
||||||
private final BigDecimal amount;
|
|
||||||
private final BigDecimal premiumAfter;
|
|
||||||
private final boolean posted;
|
|
||||||
private final String voucherNo;
|
|
||||||
private final String glTransactionId;
|
|
||||||
/** Next line to post and its 31 March has come. */
|
|
||||||
private final boolean postable;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-40
@@ -1,40 +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.investment.data;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
/** A holding's premium amortisation due for a 31 March (Amortize Yearly Process). */
|
|
||||||
@Getter
|
|
||||||
@Builder
|
|
||||||
public class InvestmentAmortisationDueData {
|
|
||||||
|
|
||||||
private final Long purchaseId;
|
|
||||||
private final String dealNo;
|
|
||||||
private final String certNo;
|
|
||||||
private final String scriptCode;
|
|
||||||
private final String scriptDesc;
|
|
||||||
private final String fyEnd;
|
|
||||||
private final String fromDate;
|
|
||||||
private final long days;
|
|
||||||
private final BigDecimal unamortisedPremium;
|
|
||||||
private final BigDecimal amount;
|
|
||||||
}
|
|
||||||
-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.investment.data;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.List;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bucket Wise Investment: Govt Securities, bonds and FDs held on a date, split by the time left to maturity (up to 1
|
|
||||||
* year, over 1 to 3, over 3 to 5, over 5 years).
|
|
||||||
*/
|
|
||||||
@Getter
|
|
||||||
@Builder
|
|
||||||
public class InvestmentBucketReportData {
|
|
||||||
|
|
||||||
private final String asOnDate;
|
|
||||||
private final List<Bucket> buckets;
|
|
||||||
private final Bucket total;
|
|
||||||
private final List<Row> rows;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Builder
|
|
||||||
public static class Bucket {
|
|
||||||
|
|
||||||
private final String code;
|
|
||||||
private final String label;
|
|
||||||
private final int govtCount;
|
|
||||||
private final BigDecimal govtFaceValue;
|
|
||||||
private final BigDecimal govtBookValue;
|
|
||||||
private final int bondCount;
|
|
||||||
private final BigDecimal bondFaceValue;
|
|
||||||
private final BigDecimal bondBookValue;
|
|
||||||
private final int fdCount;
|
|
||||||
private final BigDecimal fdPrincipal;
|
|
||||||
/** Book value of securities and bonds plus FD principal. */
|
|
||||||
private final BigDecimal totalAmount;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Builder
|
|
||||||
public static class Row {
|
|
||||||
|
|
||||||
/** Code of the bucket the holding falls in. */
|
|
||||||
private final String bucket;
|
|
||||||
/** GOVT_SECURITY, BOND (Non-SLR and other bonds) or FD. */
|
|
||||||
private final String instrument;
|
|
||||||
private final Long refId;
|
|
||||||
private final String dealNo;
|
|
||||||
/** Script, or bank and certificate of an FD. */
|
|
||||||
private final String particulars;
|
|
||||||
/** HTM / AFS / HFT of a security; blank for an FD. */
|
|
||||||
private final String category;
|
|
||||||
private final BigDecimal rate;
|
|
||||||
private final String maturityDate;
|
|
||||||
/** Days from the as-on date to maturity; negative when maturity has passed but it is not yet redeemed or closed. */
|
|
||||||
private final long daysToMaturity;
|
|
||||||
/** Face value held, or the FD principal. */
|
|
||||||
private final BigDecimal faceValue;
|
|
||||||
/** Cost less what sales, amortisation and write-offs took off, or the FD principal. */
|
|
||||||
private final BigDecimal bookValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-50
@@ -1,50 +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.investment.data;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentDailyRate;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Builder
|
|
||||||
public class InvestmentDailyRateData {
|
|
||||||
|
|
||||||
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
|
||||||
|
|
||||||
private final Long id;
|
|
||||||
private final Long scriptId;
|
|
||||||
private final String scriptCode;
|
|
||||||
private final String scriptDesc;
|
|
||||||
private final String bondType;
|
|
||||||
private final String rateDate;
|
|
||||||
private final BigDecimal openRate;
|
|
||||||
private final BigDecimal highRate;
|
|
||||||
private final BigDecimal lowRate;
|
|
||||||
private final BigDecimal closeRate;
|
|
||||||
|
|
||||||
public static InvestmentDailyRateData from(final InvestmentDailyRate r) {
|
|
||||||
return InvestmentDailyRateData.builder().id(r.getId()).scriptId(r.getScript().getId()).scriptCode(r.getScript().getScriptCode())
|
|
||||||
.scriptDesc(r.getScript().getScriptDesc()).bondType(r.getScript().getBondType())
|
|
||||||
.rateDate(r.getRateDate().format(DATE_FORMAT)).openRate(r.getOpenRate()).highRate(r.getHighRate()).lowRate(r.getLowRate())
|
|
||||||
.closeRate(r.getCloseRate()).build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-61
@@ -1,61 +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.investment.data;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.List;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Due Date Diary: every dated event of the active investments in a period — Govt Security / bond coupons and 31 Mar /
|
|
||||||
* 30 Sep provisions, maturities, premium amortisation, and FD provisions, payouts and maturities.
|
|
||||||
*/
|
|
||||||
@Getter
|
|
||||||
@Builder
|
|
||||||
public class InvestmentDiaryData {
|
|
||||||
|
|
||||||
private final String fromDate;
|
|
||||||
private final String toDate;
|
|
||||||
private final String asOnDate;
|
|
||||||
private final int pendingCount;
|
|
||||||
private final int overdueCount;
|
|
||||||
/** Money expected in the period from coupons, FD payouts and maturities not yet received. */
|
|
||||||
private final BigDecimal expectedInflow;
|
|
||||||
private final List<Row> rows;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Builder
|
|
||||||
public static class Row {
|
|
||||||
|
|
||||||
private final String date;
|
|
||||||
/** COUPON, PROVISION, MATURITY, AMORTISATION (Govt Security / bond); FD_PROVISION, FD_PAYOUT, FD_MATURITY. */
|
|
||||||
private final String kind;
|
|
||||||
/** SECURITY or FD — which screen the entry belongs to. */
|
|
||||||
private final String instrument;
|
|
||||||
private final Long refId;
|
|
||||||
private final String dealNo;
|
|
||||||
/** Script, or bank and certificate of an FD. */
|
|
||||||
private final String particulars;
|
|
||||||
private final BigDecimal amount;
|
|
||||||
/** POSTED, OVERDUE, DUE (today), UPCOMING. */
|
|
||||||
private final String status;
|
|
||||||
private final String voucherNo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-65
@@ -1,65 +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.investment.data;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.List;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
/** What closing an FD (maturity, premature or renewal) works out to, and the voucher it posts. */
|
|
||||||
@Getter
|
|
||||||
@Builder
|
|
||||||
public class InvestmentFdClosureData {
|
|
||||||
|
|
||||||
private final Long fdId;
|
|
||||||
private final String dealNo;
|
|
||||||
private final String type;
|
|
||||||
private final String closeDate;
|
|
||||||
private final BigDecimal principal;
|
|
||||||
/** Interest the system works out for the period, to compare with the bank's figure. */
|
|
||||||
private final BigDecimal expectedInterest;
|
|
||||||
/** Interest already booked by provisions and not yet received. */
|
|
||||||
private final BigDecimal provisioned;
|
|
||||||
/** Money credited to our bank by the other bank (net of TDS). */
|
|
||||||
private final BigDecimal receivedAmount;
|
|
||||||
private final BigDecimal tds;
|
|
||||||
private final BigDecimal penalAmount;
|
|
||||||
/** Gross interest as per the bank: received + TDS − principal. */
|
|
||||||
private final BigDecimal interest;
|
|
||||||
/** Interest income of this entry: interest less what was provisioned (negative = reversed from income). */
|
|
||||||
private final BigDecimal income;
|
|
||||||
/** Bank's interest less the system's. */
|
|
||||||
private final BigDecimal difference;
|
|
||||||
private final BigDecimal renewalAmount;
|
|
||||||
/** Renewal: money added (positive) or taken out (negative) against the maturity amount. */
|
|
||||||
private final BigDecimal topUp;
|
|
||||||
private final List<Line> lines;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@AllArgsConstructor
|
|
||||||
public static class Line {
|
|
||||||
|
|
||||||
private final String side;
|
|
||||||
private final String glCode;
|
|
||||||
private final String glName;
|
|
||||||
private final BigDecimal amount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-107
@@ -1,107 +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.investment.data;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.time.temporal.ChronoUnit;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentFd;
|
|
||||||
|
|
||||||
/** An FD placed with another bank; also the shape of a purchase preview (with the limit check). */
|
|
||||||
@Getter
|
|
||||||
@Builder
|
|
||||||
public class InvestmentFdData {
|
|
||||||
|
|
||||||
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
|
||||||
|
|
||||||
private final Long id;
|
|
||||||
private final String dealNo;
|
|
||||||
private final Long partyId;
|
|
||||||
private final String partyCode;
|
|
||||||
private final String partyName;
|
|
||||||
/** TDS rate the bank deducts on interest; null when it does not deduct TDS. */
|
|
||||||
private final BigDecimal tdsRate;
|
|
||||||
private final String certNo;
|
|
||||||
private final String certDate;
|
|
||||||
private final String dealDate;
|
|
||||||
private final BigDecimal principal;
|
|
||||||
private final BigDecimal intRate;
|
|
||||||
private final String maturityDate;
|
|
||||||
private final long days;
|
|
||||||
private final String payout;
|
|
||||||
private final String calcType;
|
|
||||||
private final BigDecimal expectedInterest;
|
|
||||||
private final BigDecimal expectedMaturityAmount;
|
|
||||||
/** Interest paid each half year / year for a payout FD. */
|
|
||||||
private final BigDecimal periodInterest;
|
|
||||||
private final String fdGlCode;
|
|
||||||
private final String fdGlName;
|
|
||||||
private final String interestGlCode;
|
|
||||||
private final String interestGlName;
|
|
||||||
private final Long bankGlAccountId;
|
|
||||||
private final String bankGlCode;
|
|
||||||
private final String bankGlName;
|
|
||||||
private final String status;
|
|
||||||
private final String remarks;
|
|
||||||
private final String glTransactionId;
|
|
||||||
private final String voucherNo;
|
|
||||||
private final String renewedFromDealNo;
|
|
||||||
private final String closedOnDate;
|
|
||||||
private final String closureType;
|
|
||||||
private final BigDecimal closureAmount;
|
|
||||||
private final BigDecimal closureInterest;
|
|
||||||
private final BigDecimal closureTds;
|
|
||||||
private final BigDecimal closurePenal;
|
|
||||||
private final String closureVoucherNo;
|
|
||||||
private final String closureGlTransactionId;
|
|
||||||
private final InvestmentFdLimitData limit;
|
|
||||||
|
|
||||||
public static InvestmentFdData from(final InvestmentFd f, final BigDecimal periodInterest) {
|
|
||||||
return builder().id(f.getId()).dealNo(f.getDealNo()).partyId(f.getParty().getId()).partyCode(f.getParty().getPartyCode())
|
|
||||||
.partyName(f.getParty().getPartyName())
|
|
||||||
.tdsRate(f.getParty().isTdsApplicable() ? f.getParty().getTdsRate() : null).certNo(f.getCertNo())
|
|
||||||
.certDate(format(f.getCertDate()))
|
|
||||||
.dealDate(format(f.getDealDate())).principal(f.getPrincipal()).intRate(f.getIntRate())
|
|
||||||
.maturityDate(format(f.getMaturityDate())).days(ChronoUnit.DAYS.between(f.getDealDate(), f.getMaturityDate()))
|
|
||||||
.payout(f.getPayout()).calcType(f.getCalcType()).expectedInterest(f.getExpectedInterest())
|
|
||||||
.expectedMaturityAmount(f.getExpectedMaturityAmount()).periodInterest(periodInterest).fdGlCode(code(f.getFdGlAccount()))
|
|
||||||
.fdGlName(name(f.getFdGlAccount())).interestGlCode(code(f.getInterestGlAccount())).interestGlName(name(f.getInterestGlAccount()))
|
|
||||||
.bankGlAccountId(f.getBankGlAccount().getId()).bankGlCode(code(f.getBankGlAccount())).bankGlName(name(f.getBankGlAccount()))
|
|
||||||
.status(f.getStatus()).remarks(f.getRemarks()).glTransactionId(f.getGlTransactionId()).voucherNo(f.getVoucherNo())
|
|
||||||
.renewedFromDealNo(f.getRenewedFrom() == null ? null : f.getRenewedFrom().getDealNo()).closedOnDate(format(f.getClosedOnDate()))
|
|
||||||
.closureType(f.getClosureType()).closureAmount(f.getClosureAmount()).closureInterest(f.getClosureInterest())
|
|
||||||
.closureTds(f.getClosureTds()).closurePenal(f.getClosurePenal()).closureVoucherNo(f.getClosureVoucherNo()).closureGlTransactionId(f.getClosureGlTransactionId()).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String code(final GLAccount gl) {
|
|
||||||
return gl == null ? null : gl.getGlCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String name(final GLAccount gl) {
|
|
||||||
return gl == null ? null : gl.getName();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String format(final LocalDate date) {
|
|
||||||
return date == null ? null : date.format(DATE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-40
@@ -1,40 +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.investment.data;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
/** The next interest voucher (or the maturity) of an FD that is due, or due within the reminder window. */
|
|
||||||
@Getter
|
|
||||||
@Builder
|
|
||||||
public class InvestmentFdDueData {
|
|
||||||
|
|
||||||
private final Long fdId;
|
|
||||||
private final String dealNo;
|
|
||||||
private final String partyName;
|
|
||||||
private final String certNo;
|
|
||||||
/** PROVISION, PAYOUT or MATURITY. */
|
|
||||||
private final String lineType;
|
|
||||||
private final String intDate;
|
|
||||||
private final BigDecimal amount;
|
|
||||||
private final boolean postable;
|
|
||||||
private final boolean overdue;
|
|
||||||
}
|
|
||||||
-46
@@ -1,46 +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.investment.data;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
/** Where a new FD stands against the RBI limits on deposits with other banks. */
|
|
||||||
@Getter
|
|
||||||
@Builder
|
|
||||||
public class InvestmentFdLimitData {
|
|
||||||
|
|
||||||
/** The 31 March the limits are based on. */
|
|
||||||
private final String asOnDate;
|
|
||||||
/** Total deposits as on that date, as entered; empty until someone enters it. */
|
|
||||||
private final BigDecimal totalDeposits;
|
|
||||||
/** The same figure worked out from the deposit GLs, as a suggestion. */
|
|
||||||
private final BigDecimal glTotalDeposits;
|
|
||||||
private final BigDecimal grossLimit;
|
|
||||||
private final BigDecimal fdOutstanding;
|
|
||||||
private final BigDecimal bankBalances;
|
|
||||||
private final BigDecimal grossAfter;
|
|
||||||
private final BigDecimal bankLimit;
|
|
||||||
private final String partyName;
|
|
||||||
private final BigDecimal bankAfter;
|
|
||||||
private final BigDecimal newAmount;
|
|
||||||
private final boolean withinLimits;
|
|
||||||
private final String message;
|
|
||||||
}
|
|
||||||
-58
@@ -1,58 +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.investment.data;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.List;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
/** Interest lines of an FD: 31 Mar / 30 Sep provisions, payouts of a half-yearly / yearly FD, and maturity. */
|
|
||||||
@Getter
|
|
||||||
@Builder
|
|
||||||
public class InvestmentFdScheduleData {
|
|
||||||
|
|
||||||
private final InvestmentFdData fd;
|
|
||||||
/** Provisioned interest not yet received (interest receivable of this FD). */
|
|
||||||
private final BigDecimal provisioned;
|
|
||||||
private final List<Row> rows;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Builder
|
|
||||||
public static class Row {
|
|
||||||
|
|
||||||
/** PROVISION, PAYOUT or MATURITY (shown for information; closed on the Maturity screen). */
|
|
||||||
private final String lineType;
|
|
||||||
private final String intDate;
|
|
||||||
private final String fromDate;
|
|
||||||
private final String toDate;
|
|
||||||
private final long days;
|
|
||||||
private final BigDecimal grossInterest;
|
|
||||||
/** Payout: the part already provisioned, cleared from interest receivable. */
|
|
||||||
private final BigDecimal alreadyBooked;
|
|
||||||
private final BigDecimal tds;
|
|
||||||
/** Provision: interest booked. Payout: interest income of the payout (gross less already booked). */
|
|
||||||
private final BigDecimal amount;
|
|
||||||
private final boolean posted;
|
|
||||||
private final String voucherNo;
|
|
||||||
private final String glTransactionId;
|
|
||||||
/** The next line to post, and its date has come. */
|
|
||||||
private final boolean postable;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-43
@@ -1,43 +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.investment.data;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
/** The next unposted interest line of a holding that is due, or due within the reminder window — a task to post. */
|
|
||||||
@Getter
|
|
||||||
@Builder
|
|
||||||
public class InvestmentInterestDueData {
|
|
||||||
|
|
||||||
private final Long purchaseId;
|
|
||||||
private final String dealNo;
|
|
||||||
private final String certNo;
|
|
||||||
private final String scriptCode;
|
|
||||||
private final String scriptDesc;
|
|
||||||
private final String lineType;
|
|
||||||
private final String intDate;
|
|
||||||
private final BigDecimal grossInterest;
|
|
||||||
private final BigDecimal amount;
|
|
||||||
/** The date has come: it can be posted now. */
|
|
||||||
private final boolean postable;
|
|
||||||
/** The date has passed without posting. */
|
|
||||||
private final boolean overdue;
|
|
||||||
}
|
|
||||||
-51
@@ -1,51 +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.investment.data;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
/** An interest line (coupon or closing provision) already posted to GL with its voucher — for the transaction log. */
|
|
||||||
@Getter
|
|
||||||
@Builder
|
|
||||||
public class InvestmentInterestPostingData {
|
|
||||||
|
|
||||||
private final Long id;
|
|
||||||
private final Long purchaseId;
|
|
||||||
private final String dealNo;
|
|
||||||
private final String certNo;
|
|
||||||
private final String bondType;
|
|
||||||
private final String scriptDesc;
|
|
||||||
private final String lineType;
|
|
||||||
private final String intDate;
|
|
||||||
private final String fromDate;
|
|
||||||
private final String toDate;
|
|
||||||
private final BigDecimal grossInterest;
|
|
||||||
private final BigDecimal alreadyBooked;
|
|
||||||
private final BigDecimal amount;
|
|
||||||
/** Debit side: the bank for a coupon, interest receivable for a provision. */
|
|
||||||
private final String drGlCode;
|
|
||||||
private final String drGlName;
|
|
||||||
private final String incomeGlCode;
|
|
||||||
private final String incomeGlName;
|
|
||||||
private final String glTransactionId;
|
|
||||||
private final String voucherNo;
|
|
||||||
private final String postedOnDate;
|
|
||||||
}
|
|
||||||
-64
@@ -1,64 +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.investment.data;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.List;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Total Interest Received & Receivable: per Govt Security / bond deal and FD, the interest received in a period and
|
|
||||||
* the interest earned up to its last day that has not come in yet.
|
|
||||||
*/
|
|
||||||
@Getter
|
|
||||||
@Builder
|
|
||||||
public class InvestmentInterestReportData {
|
|
||||||
|
|
||||||
private final String fromDate;
|
|
||||||
private final String toDate;
|
|
||||||
private final BigDecimal totalReceived;
|
|
||||||
private final BigDecimal totalReceivable;
|
|
||||||
private final BigDecimal totalProvisioned;
|
|
||||||
private final BigDecimal total;
|
|
||||||
private final List<Row> rows;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Builder
|
|
||||||
public static class Row {
|
|
||||||
|
|
||||||
/** SECURITY or FD. */
|
|
||||||
private final String instrument;
|
|
||||||
private final Long refId;
|
|
||||||
private final String dealNo;
|
|
||||||
/** Script, or bank and certificate of an FD. */
|
|
||||||
private final String particulars;
|
|
||||||
private final String status;
|
|
||||||
/** Face value held, or the FD principal. */
|
|
||||||
private final BigDecimal principal;
|
|
||||||
private final BigDecimal rate;
|
|
||||||
/** Interest that came in during the period (coupons, FD payouts, interest paid at closure), gross of TDS. */
|
|
||||||
private final BigDecimal received;
|
|
||||||
/** Interest earned up to the To date and not yet received. */
|
|
||||||
private final BigDecimal receivable;
|
|
||||||
/** Part of the receivable already booked by 31 Mar / 30 Sep provisions (Interest Receivable 100603). */
|
|
||||||
private final BigDecimal provisioned;
|
|
||||||
private final BigDecimal total;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-91
@@ -1,91 +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.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;
|
|
||||||
/**
|
|
||||||
* Broken-period interest paid to the seller at purchase — booked as an expense (GL 500606), not in this schedule.
|
|
||||||
*/
|
|
||||||
private final BigDecimal purchasedAccruedInterest;
|
|
||||||
private final BigDecimal couponPerHalfYear;
|
|
||||||
private final String asOnDate;
|
|
||||||
private final String interestGlCode;
|
|
||||||
private final String interestGlName;
|
|
||||||
/** Bank the coupon is received into by default: the purchase's paying bank. */
|
|
||||||
private final Long bankGlAccountId;
|
|
||||||
private final String bankGlCode;
|
|
||||||
private final String bankGlName;
|
|
||||||
private final String status;
|
|
||||||
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;
|
|
||||||
/** Posted to GL: its JV voucher and GL transaction. */
|
|
||||||
private final boolean posted;
|
|
||||||
private final String voucherNo;
|
|
||||||
private final String glTransactionId;
|
|
||||||
private final String postedOnDate;
|
|
||||||
/** The next line to post and its date has come. */
|
|
||||||
private final boolean postable;
|
|
||||||
/** Unposted and its date is within the reminder window (or past). */
|
|
||||||
private final boolean dueSoon;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-50
@@ -1,50 +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.investment.data;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
/** Where a new non-SLR purchase stands against the RBI limits on non-SLR and unlisted non-SLR investments. */
|
|
||||||
@Getter
|
|
||||||
@Builder
|
|
||||||
public class InvestmentNonSlrLimitData {
|
|
||||||
|
|
||||||
/** The 31 March the limits are based on. */
|
|
||||||
private final String asOnDate;
|
|
||||||
/** Total deposits as on that date, as entered for the FD limits; empty until someone enters it. */
|
|
||||||
private final BigDecimal totalDeposits;
|
|
||||||
/** 10% of total deposits. */
|
|
||||||
private final BigDecimal nonSlrLimit;
|
|
||||||
/** Book value of non-SLR investments held or pending settlement, before this deal. */
|
|
||||||
private final BigDecimal nonSlrOutstanding;
|
|
||||||
private final BigDecimal nonSlrAfter;
|
|
||||||
/** Book value of non-SLR investments held on that 31 March. */
|
|
||||||
private final BigDecimal nonSlrBase;
|
|
||||||
/** 10% of the non-SLR investments held on that 31 March. */
|
|
||||||
private final BigDecimal unlistedLimit;
|
|
||||||
private final BigDecimal unlistedOutstanding;
|
|
||||||
private final BigDecimal unlistedAfter;
|
|
||||||
private final BigDecimal newAmount;
|
|
||||||
/** Whether this deal's security is listed or proposed to be listed; unlisted ones also count against the unlisted limit. */
|
|
||||||
private final boolean listed;
|
|
||||||
private final boolean withinLimits;
|
|
||||||
private final String message;
|
|
||||||
}
|
|
||||||
-71
@@ -1,71 +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.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 Long interestGlAccountId;
|
|
||||||
private final String interestGlCode;
|
|
||||||
private final String interestGlName;
|
|
||||||
private final boolean tdsApplicable;
|
|
||||||
private final BigDecimal tdsRate;
|
|
||||||
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())
|
|
||||||
.interestGlAccountId(p.getInterestGlAccount() == null ? null : p.getInterestGlAccount().getId())
|
|
||||||
.interestGlCode(p.getInterestGlAccount() == null ? null : p.getInterestGlAccount().getGlCode())
|
|
||||||
.interestGlName(p.getInterestGlAccount() == null ? null : p.getInterestGlAccount().getName())
|
|
||||||
.tdsApplicable(p.isTdsApplicable()).tdsRate(p.getTdsRate()).rbiCode(p.getRbiCode()).limit(p.getExposureLimit())
|
|
||||||
.leiNo(p.getLeiNo()).cpBankName(p.getCpBankName())
|
|
||||||
.cpBranchName(p.getCpBranchName()).cpAccountNo(p.getCpAccountNo()).cpIfsc(p.getCpIfsc()).active(p.isActive()).build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-100
@@ -1,100 +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.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 otherCharges;
|
|
||||||
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;
|
|
||||||
private final String glTransactionId;
|
|
||||||
private final String settledOnDate;
|
|
||||||
|
|
||||||
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()).otherCharges(p.getOtherCharges()).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()).glTransactionId(p.getGlTransactionId())
|
|
||||||
.settledOnDate(format(p.getSettledOnDate())).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String format(final LocalDate date) {
|
|
||||||
return date == null ? null : date.format(DATE_FORMAT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-102
@@ -1,102 +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.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.InvestmentSale;
|
|
||||||
|
|
||||||
/** A sale or redemption of a holding — also the shape of a quote before it is saved. */
|
|
||||||
@Getter
|
|
||||||
@Builder
|
|
||||||
public class InvestmentSaleData {
|
|
||||||
|
|
||||||
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
|
||||||
|
|
||||||
private final Long id;
|
|
||||||
private final Long purchaseId;
|
|
||||||
private final String dealNo;
|
|
||||||
private final String certNo;
|
|
||||||
private final String bondType;
|
|
||||||
private final String scriptCode;
|
|
||||||
private final String scriptDesc;
|
|
||||||
private final String saleType;
|
|
||||||
private final Long partyId;
|
|
||||||
private final String partyName;
|
|
||||||
private final String dealDate;
|
|
||||||
private final String dealTime;
|
|
||||||
private final String settlementType;
|
|
||||||
private final String settlementDate;
|
|
||||||
private final BigDecimal units;
|
|
||||||
private final BigDecimal faceValue;
|
|
||||||
private final BigDecimal saleRate;
|
|
||||||
private final BigDecimal consideration;
|
|
||||||
private final Integer accruedDays;
|
|
||||||
private final BigDecimal accruedInterest;
|
|
||||||
private final BigDecimal receivableCleared;
|
|
||||||
/** Interest from the buyer not yet booked: accrued interest less the receivable cleared. */
|
|
||||||
private final BigDecimal interestIncome;
|
|
||||||
private final BigDecimal brokerage;
|
|
||||||
private final BigDecimal bookValueSold;
|
|
||||||
private final BigDecimal premiumCleared;
|
|
||||||
private final BigDecimal profitLoss;
|
|
||||||
private final BigDecimal netAmount;
|
|
||||||
private final Long bankGlAccountId;
|
|
||||||
private final String bankGlCode;
|
|
||||||
private final String bankGlName;
|
|
||||||
private final String investmentGlCode;
|
|
||||||
/** GL the premium share is credited to: 100707, or the investment GL for deals settled before it was used. */
|
|
||||||
private final String premiumGlCode;
|
|
||||||
private final String sanctionAuthority;
|
|
||||||
private final String remarks;
|
|
||||||
private final String status;
|
|
||||||
private final String glTransactionId;
|
|
||||||
private final String voucherNo;
|
|
||||||
private final String settledOnDate;
|
|
||||||
|
|
||||||
public static InvestmentSaleData from(final InvestmentSale s) {
|
|
||||||
return builder().id(s.getId()).purchaseId(s.getPurchase().getId()).dealNo(s.getPurchase().getDealNo())
|
|
||||||
.certNo(s.getPurchase().getCertNo()).bondType(s.getPurchase().getBondType())
|
|
||||||
.scriptCode(s.getPurchase().getScript().getScriptCode()).scriptDesc(s.getPurchase().getScript().getScriptDesc())
|
|
||||||
.saleType(s.getSaleType()).partyId(s.getParty() == null ? null : s.getParty().getId())
|
|
||||||
.partyName(s.getParty() == null ? null : s.getParty().getPartyName()).dealDate(format(s.getDealDate()))
|
|
||||||
.dealTime(s.getDealTime()).settlementType(s.getSettlementType()).settlementDate(format(s.getSettlementDate()))
|
|
||||||
.units(s.getUnits()).faceValue(s.getFaceValue()).saleRate(s.getSaleRate()).consideration(s.getConsideration())
|
|
||||||
.accruedDays(s.getAccruedDays()).accruedInterest(s.getAccruedInterest()).receivableCleared(s.getReceivableCleared())
|
|
||||||
.interestIncome(zero(s.getAccruedInterest()).subtract(zero(s.getReceivableCleared()))).brokerage(s.getBrokerage())
|
|
||||||
.bookValueSold(s.getBookValueSold()).premiumCleared(s.getPremiumCleared()).profitLoss(s.getProfitLoss()).netAmount(s.getNetAmount())
|
|
||||||
.bankGlAccountId(s.getBankGlAccount().getId()).bankGlCode(s.getBankGlAccount().getGlCode())
|
|
||||||
.bankGlName(s.getBankGlAccount().getName()).investmentGlCode(s.getPurchase().getInvestmentGlAccount().getGlCode())
|
|
||||||
.premiumGlCode(s.getPurchase().getPremiumGlAccount() != null ? s.getPurchase().getPremiumGlAccount().getGlCode()
|
|
||||||
: s.getPurchase().getInvestmentGlAccount().getGlCode())
|
|
||||||
.sanctionAuthority(s.getSanctionAuthority()).remarks(s.getRemarks()).status(s.getStatus())
|
|
||||||
.glTransactionId(s.getGlTransactionId()).voucherNo(s.getVoucherNo()).settledOnDate(format(s.getSettledOnDate())).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static BigDecimal zero(final BigDecimal v) {
|
|
||||||
return v == null ? BigDecimal.ZERO : v;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String format(final LocalDate date) {
|
|
||||||
return date == null ? null : date.format(DATE_FORMAT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-76
@@ -1,76 +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.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 String rating;
|
|
||||||
private final String ratingAgency;
|
|
||||||
private final boolean listed;
|
|
||||||
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()).rating(s.getRating()).ratingAgency(s.getRatingAgency()).listed(s.isListed())
|
|
||||||
.active(s.isActive()).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String format(final LocalDate date) {
|
|
||||||
return date == null ? null : date.format(DATE_FORMAT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-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.investment.data;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentSecurityTransfer;
|
|
||||||
|
|
||||||
/** A GOI Category Transfer of a holding, for the transaction log. */
|
|
||||||
@Getter
|
|
||||||
@Builder
|
|
||||||
public class InvestmentSecurityTransferData {
|
|
||||||
|
|
||||||
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
|
||||||
|
|
||||||
private final Long id;
|
|
||||||
private final Long purchaseId;
|
|
||||||
private final String dealNo;
|
|
||||||
private final String certNo;
|
|
||||||
private final String bondType;
|
|
||||||
private final String fromScriptCode;
|
|
||||||
private final String fromScriptDesc;
|
|
||||||
private final String toScriptCode;
|
|
||||||
private final String toScriptDesc;
|
|
||||||
private final String fromGlCode;
|
|
||||||
private final String toGlCode;
|
|
||||||
private final String toGlName;
|
|
||||||
private final String transferDate;
|
|
||||||
private final BigDecimal units;
|
|
||||||
private final BigDecimal bookValue;
|
|
||||||
private final String remarks;
|
|
||||||
private final String glTransactionId;
|
|
||||||
private final String voucherNo;
|
|
||||||
|
|
||||||
public static InvestmentSecurityTransferData from(final InvestmentSecurityTransfer t) {
|
|
||||||
return builder().id(t.getId()).purchaseId(t.getPurchase().getId()).dealNo(t.getPurchase().getDealNo())
|
|
||||||
.certNo(t.getPurchase().getCertNo()).bondType(t.getPurchase().getBondType()).fromScriptCode(t.getFromScript().getScriptCode())
|
|
||||||
.fromScriptDesc(t.getFromScript().getScriptDesc()).toScriptCode(t.getToScript().getScriptCode())
|
|
||||||
.toScriptDesc(t.getToScript().getScriptDesc()).fromGlCode(t.getFromGlAccount().getGlCode())
|
|
||||||
.toGlCode(t.getToGlAccount().getGlCode()).toGlName(t.getToGlAccount().getName())
|
|
||||||
.transferDate(t.getTransferDate().format(DATE_FORMAT)).units(t.getUnits()).bookValue(t.getBookValue())
|
|
||||||
.remarks(t.getRemarks()).glTransactionId(t.getGlTransactionId()).voucherNo(t.getVoucherNo()).build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-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.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.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Daily Script Master: the market rate of a script for one day, as the bank's existing CBS keeps it. Purchase Entry
|
|
||||||
* takes its rate from here (Jitendra Sir). Close rate is mandatory; open, high and low are optional.
|
|
||||||
*/
|
|
||||||
@Entity
|
|
||||||
@Table(name = "m_investment_daily_rate")
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor
|
|
||||||
public class InvestmentDailyRate extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|
||||||
|
|
||||||
@ManyToOne
|
|
||||||
@JoinColumn(name = "script_id", nullable = false)
|
|
||||||
private InvestmentScript script;
|
|
||||||
|
|
||||||
@Column(name = "rate_date", nullable = false)
|
|
||||||
private LocalDate rateDate;
|
|
||||||
|
|
||||||
@Column(name = "open_rate", scale = 6, precision = 19)
|
|
||||||
private BigDecimal openRate;
|
|
||||||
|
|
||||||
@Column(name = "high_rate", scale = 6, precision = 19)
|
|
||||||
private BigDecimal highRate;
|
|
||||||
|
|
||||||
@Column(name = "low_rate", scale = 6, precision = 19)
|
|
||||||
private BigDecimal lowRate;
|
|
||||||
|
|
||||||
@Column(name = "close_rate", scale = 6, precision = 19, nullable = false)
|
|
||||||
private BigDecimal closeRate;
|
|
||||||
}
|
|
||||||
-34
@@ -1,34 +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.investment.domain;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
|
|
||||||
public interface InvestmentDailyRateRepository extends JpaRepository<InvestmentDailyRate, Long> {
|
|
||||||
|
|
||||||
boolean existsByScriptIdAndRateDate(Long scriptId, LocalDate rateDate);
|
|
||||||
|
|
||||||
/** The newest rate of a script on or before a date. */
|
|
||||||
Optional<InvestmentDailyRate> findFirstByScriptIdAndRateDateLessThanEqualOrderByRateDateDesc(Long scriptId, LocalDate onDate);
|
|
||||||
|
|
||||||
List<InvestmentDailyRate> findByScriptIdOrderByRateDateDesc(Long scriptId);
|
|
||||||
}
|
|
||||||
-47
@@ -1,47 +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.investment.domain;
|
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
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.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
|
||||||
|
|
||||||
/** The bank's total deposits as on a 31 March: the base for the RBI limits on deposits placed with other banks. */
|
|
||||||
@Entity
|
|
||||||
@Table(name = "m_investment_deposit_base")
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor
|
|
||||||
public class InvestmentDepositBase extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|
||||||
|
|
||||||
@Column(name = "as_on_date", nullable = false, unique = true)
|
|
||||||
private LocalDate asOnDate;
|
|
||||||
|
|
||||||
@Column(name = "total_deposits", scale = 6, precision = 19, nullable = false)
|
|
||||||
private BigDecimal totalDeposits;
|
|
||||||
|
|
||||||
@Column(name = "remarks", length = 500)
|
|
||||||
private String remarks;
|
|
||||||
}
|
|
||||||
-28
@@ -1,28 +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.investment.domain;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.Optional;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
|
|
||||||
public interface InvestmentDepositBaseRepository extends JpaRepository<InvestmentDepositBase, Long> {
|
|
||||||
|
|
||||||
Optional<InvestmentDepositBase> findByAsOnDate(LocalDate asOnDate);
|
|
||||||
}
|
|
||||||
-135
@@ -1,135 +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.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;
|
|
||||||
|
|
||||||
/** A fixed deposit the bank has placed with another bank, from placement to maturity, premature closure or renewal. */
|
|
||||||
@Entity
|
|
||||||
@Table(name = "m_investment_fd")
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor
|
|
||||||
public class InvestmentFd extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|
||||||
|
|
||||||
@Column(name = "deal_no", nullable = false, length = 20, unique = true)
|
|
||||||
private String dealNo;
|
|
||||||
|
|
||||||
@ManyToOne
|
|
||||||
@JoinColumn(name = "party_id", nullable = false)
|
|
||||||
private InvestmentParty party;
|
|
||||||
|
|
||||||
/** The bank's FD GL (from the party), debited when the money is placed. */
|
|
||||||
@ManyToOne
|
|
||||||
@JoinColumn(name = "fd_gl_account_id", nullable = false)
|
|
||||||
private GLAccount fdGlAccount;
|
|
||||||
|
|
||||||
@ManyToOne
|
|
||||||
@JoinColumn(name = "interest_gl_account_id", nullable = false)
|
|
||||||
private GLAccount interestGlAccount;
|
|
||||||
|
|
||||||
/** Certificate (receipt) number issued by the other bank. */
|
|
||||||
@Column(name = "cert_no", nullable = false, length = 50)
|
|
||||||
private String certNo;
|
|
||||||
|
|
||||||
@Column(name = "cert_date")
|
|
||||||
private LocalDate certDate;
|
|
||||||
|
|
||||||
@Column(name = "deal_date", nullable = false)
|
|
||||||
private LocalDate dealDate;
|
|
||||||
|
|
||||||
@Column(name = "principal", scale = 6, precision = 19, nullable = false)
|
|
||||||
private BigDecimal principal;
|
|
||||||
|
|
||||||
@Column(name = "int_rate", scale = 6, precision = 19, nullable = false)
|
|
||||||
private BigDecimal intRate;
|
|
||||||
|
|
||||||
@Column(name = "maturity_date", nullable = false)
|
|
||||||
private LocalDate maturityDate;
|
|
||||||
|
|
||||||
@Column(name = "payout", nullable = false, length = 20)
|
|
||||||
private String payout;
|
|
||||||
|
|
||||||
@Column(name = "calc_type", nullable = false, length = 20)
|
|
||||||
private String calcType;
|
|
||||||
|
|
||||||
@Column(name = "expected_interest", scale = 6, precision = 19)
|
|
||||||
private BigDecimal expectedInterest;
|
|
||||||
|
|
||||||
@Column(name = "expected_maturity_amount", scale = 6, precision = 19)
|
|
||||||
private BigDecimal expectedMaturityAmount;
|
|
||||||
|
|
||||||
/** Our bank account the money was paid from. */
|
|
||||||
@ManyToOne
|
|
||||||
@JoinColumn(name = "bank_gl_account_id", nullable = false)
|
|
||||||
private GLAccount bankGlAccount;
|
|
||||||
|
|
||||||
/** The FD this one renews, if any. */
|
|
||||||
@ManyToOne
|
|
||||||
@JoinColumn(name = "renewed_from_id")
|
|
||||||
private InvestmentFd renewedFrom;
|
|
||||||
|
|
||||||
@Column(name = "status", nullable = false, length = 20)
|
|
||||||
private String status;
|
|
||||||
|
|
||||||
@Column(name = "remarks", length = 500)
|
|
||||||
private String remarks;
|
|
||||||
|
|
||||||
@Column(name = "gl_transaction_id", length = 50)
|
|
||||||
private String glTransactionId;
|
|
||||||
|
|
||||||
@Column(name = "voucher_no", length = 30)
|
|
||||||
private String voucherNo;
|
|
||||||
|
|
||||||
@Column(name = "closed_on_date")
|
|
||||||
private LocalDate closedOnDate;
|
|
||||||
|
|
||||||
/** MATURITY, PREMATURE or RENEWAL. */
|
|
||||||
@Column(name = "closure_type", length = 20)
|
|
||||||
private String closureType;
|
|
||||||
|
|
||||||
@Column(name = "closure_amount", scale = 6, precision = 19)
|
|
||||||
private BigDecimal closureAmount;
|
|
||||||
|
|
||||||
@Column(name = "closure_interest", scale = 6, precision = 19)
|
|
||||||
private BigDecimal closureInterest;
|
|
||||||
|
|
||||||
@Column(name = "closure_tds", scale = 6, precision = 19)
|
|
||||||
private BigDecimal closureTds;
|
|
||||||
|
|
||||||
@Column(name = "closure_penal", scale = 6, precision = 19)
|
|
||||||
private BigDecimal closurePenal;
|
|
||||||
|
|
||||||
@Column(name = "closure_gl_transaction_id", length = 50)
|
|
||||||
private String closureGlTransactionId;
|
|
||||||
|
|
||||||
@Column(name = "closure_voucher_no", length = 30)
|
|
||||||
private String closureVoucherNo;
|
|
||||||
}
|
|
||||||
-87
@@ -1,87 +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.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;
|
|
||||||
|
|
||||||
/** A posted interest line of an FD: a 31 Mar / 30 Sep provision, or a half-yearly / yearly payout. */
|
|
||||||
@Entity
|
|
||||||
@Table(name = "m_investment_fd_interest")
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor
|
|
||||||
public class InvestmentFdInterest extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|
||||||
|
|
||||||
@ManyToOne
|
|
||||||
@JoinColumn(name = "fd_id", nullable = false)
|
|
||||||
private InvestmentFd fd;
|
|
||||||
|
|
||||||
/** PROVISION or PAYOUT. */
|
|
||||||
@Column(name = "line_type", nullable = false, length = 20)
|
|
||||||
private String lineType;
|
|
||||||
|
|
||||||
@Column(name = "int_date", nullable = false)
|
|
||||||
private LocalDate intDate;
|
|
||||||
|
|
||||||
@Column(name = "from_date")
|
|
||||||
private LocalDate fromDate;
|
|
||||||
|
|
||||||
@Column(name = "to_date")
|
|
||||||
private LocalDate toDate;
|
|
||||||
|
|
||||||
@Column(name = "days")
|
|
||||||
private Integer days;
|
|
||||||
|
|
||||||
@Column(name = "gross_interest", scale = 6, precision = 19)
|
|
||||||
private BigDecimal grossInterest;
|
|
||||||
|
|
||||||
/** For a payout: the part already provisioned, cleared from interest receivable. */
|
|
||||||
@Column(name = "already_booked", scale = 6, precision = 19)
|
|
||||||
private BigDecimal alreadyBooked;
|
|
||||||
|
|
||||||
@Column(name = "tds", scale = 6, precision = 19)
|
|
||||||
private BigDecimal tds;
|
|
||||||
|
|
||||||
@Column(name = "amount", scale = 6, precision = 19, nullable = false)
|
|
||||||
private BigDecimal amount;
|
|
||||||
|
|
||||||
@ManyToOne
|
|
||||||
@JoinColumn(name = "bank_gl_account_id")
|
|
||||||
private GLAccount bankGlAccount;
|
|
||||||
|
|
||||||
@Column(name = "gl_transaction_id", length = 50)
|
|
||||||
private String glTransactionId;
|
|
||||||
|
|
||||||
@Column(name = "voucher_no", length = 30)
|
|
||||||
private String voucherNo;
|
|
||||||
|
|
||||||
@Column(name = "posted_on_date")
|
|
||||||
private LocalDate postedOnDate;
|
|
||||||
}
|
|
||||||
-27
@@ -1,27 +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.investment.domain;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
|
|
||||||
public interface InvestmentFdInterestRepository extends JpaRepository<InvestmentFdInterest, Long> {
|
|
||||||
|
|
||||||
List<InvestmentFdInterest> findByFdIdOrderByIntDateAscIdAsc(Long fdId);
|
|
||||||
}
|
|
||||||
-38
@@ -1,38 +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.investment.domain;
|
|
||||||
|
|
||||||
import jakarta.persistence.LockModeType;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
import org.springframework.data.jpa.repository.Lock;
|
|
||||||
import org.springframework.data.jpa.repository.Query;
|
|
||||||
import org.springframework.data.repository.query.Param;
|
|
||||||
|
|
||||||
public interface InvestmentFdRepository extends JpaRepository<InvestmentFd, Long> {
|
|
||||||
|
|
||||||
@Lock(value = LockModeType.PESSIMISTIC_WRITE)
|
|
||||||
@Query("select f from InvestmentFd f where f.id = :fdId")
|
|
||||||
Optional<InvestmentFd> findByIdWithLock(@Param("fdId") Long fdId);
|
|
||||||
|
|
||||||
List<InvestmentFd> findByStatusOrderByIdAsc(String status);
|
|
||||||
|
|
||||||
boolean existsByPartyIdAndCertNoIgnoreCase(Long partyId, String certNo);
|
|
||||||
}
|
|
||||||
-84
@@ -1,84 +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.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;
|
|
||||||
|
|
||||||
/** One interest schedule line of a holding (coupon received or closing provision) that has been posted to GL. */
|
|
||||||
@Entity
|
|
||||||
@Table(name = "m_investment_interest_posting")
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor
|
|
||||||
public class InvestmentInterestPosting extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|
||||||
|
|
||||||
@ManyToOne
|
|
||||||
@JoinColumn(name = "purchase_id", nullable = false)
|
|
||||||
private InvestmentPurchase purchase;
|
|
||||||
|
|
||||||
/** COUPON or PROVISION. */
|
|
||||||
@Column(name = "line_type", nullable = false, length = 20)
|
|
||||||
private String lineType;
|
|
||||||
|
|
||||||
@Column(name = "int_date", nullable = false)
|
|
||||||
private LocalDate intDate;
|
|
||||||
|
|
||||||
@Column(name = "from_date")
|
|
||||||
private LocalDate fromDate;
|
|
||||||
|
|
||||||
@Column(name = "to_date")
|
|
||||||
private LocalDate toDate;
|
|
||||||
|
|
||||||
@Column(name = "days")
|
|
||||||
private Integer days;
|
|
||||||
|
|
||||||
@Column(name = "gross_interest", scale = 6, precision = 19)
|
|
||||||
private BigDecimal grossInterest;
|
|
||||||
|
|
||||||
@Column(name = "already_booked", scale = 6, precision = 19)
|
|
||||||
private BigDecimal alreadyBooked;
|
|
||||||
|
|
||||||
@Column(name = "amount", scale = 6, precision = 19, nullable = false)
|
|
||||||
private BigDecimal amount;
|
|
||||||
|
|
||||||
/** Bank the coupon was received into (coupon lines only). */
|
|
||||||
@ManyToOne
|
|
||||||
@JoinColumn(name = "bank_gl_account_id")
|
|
||||||
private GLAccount bankGlAccount;
|
|
||||||
|
|
||||||
@Column(name = "gl_transaction_id", length = 50)
|
|
||||||
private String glTransactionId;
|
|
||||||
|
|
||||||
@Column(name = "voucher_no", length = 30)
|
|
||||||
private String voucherNo;
|
|
||||||
|
|
||||||
@Column(name = "posted_on_date")
|
|
||||||
private LocalDate postedOnDate;
|
|
||||||
}
|
|
||||||
-27
@@ -1,27 +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.investment.domain;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
|
|
||||||
public interface InvestmentInterestPostingRepository extends JpaRepository<InvestmentInterestPosting, Long> {
|
|
||||||
|
|
||||||
List<InvestmentInterestPosting> findByPurchaseIdOrderByIntDateAsc(Long purchaseId);
|
|
||||||
}
|
|
||||||
-118
@@ -1,118 +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.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;
|
|
||||||
|
|
||||||
/** Interest income GL of the FDs placed with this bank (Bond Type FD only). */
|
|
||||||
@ManyToOne
|
|
||||||
@JoinColumn(name = "interest_gl_account_id")
|
|
||||||
private GLAccount interestGlAccount;
|
|
||||||
|
|
||||||
/** Whether this bank deducts TDS from the interest on FDs placed with it (some banks do, some do not). */
|
|
||||||
@Column(name = "tds_applicable", nullable = false)
|
|
||||||
private boolean tdsApplicable;
|
|
||||||
|
|
||||||
/** TDS rate in per cent, set only when the bank deducts TDS. */
|
|
||||||
@Column(name = "tds_rate", scale = 4, precision = 9)
|
|
||||||
private BigDecimal tdsRate;
|
|
||||||
|
|
||||||
@Column(name = "rbi_code", length = 30)
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
-28
@@ -1,28 +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.investment.domain;
|
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
|
|
||||||
public interface InvestmentPartyRepository extends JpaRepository<InvestmentParty, Long> {
|
|
||||||
|
|
||||||
/** Locks the bank so two FDs with it are checked against the 5% limit one after the other. */
|
|
||||||
@org.springframework.data.jpa.repository.Lock(value = jakarta.persistence.LockModeType.PESSIMISTIC_WRITE)
|
|
||||||
@org.springframework.data.jpa.repository.Query("select p from InvestmentParty p where p.id = :partyId")
|
|
||||||
java.util.Optional<InvestmentParty> findByIdWithLock(@org.springframework.data.repository.query.Param("partyId") Long partyId);}
|
|
||||||
-163
@@ -1,163 +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.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 = "other_charges", scale = 6, precision = 19)
|
|
||||||
private BigDecimal otherCharges;
|
|
||||||
|
|
||||||
@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;
|
|
||||||
|
|
||||||
/** GL holding the premium paid over face value (100707) until it is amortised; empty when it sits in the investment GL. */
|
|
||||||
@ManyToOne
|
|
||||||
@JoinColumn(name = "premium_gl_account_id")
|
|
||||||
private GLAccount premiumGlAccount;
|
|
||||||
|
|
||||||
@Column(name = "other_details", length = 4000)
|
|
||||||
private String otherDetails;
|
|
||||||
|
|
||||||
@Column(name = "status", nullable = false, length = 20)
|
|
||||||
private String status;
|
|
||||||
|
|
||||||
/** Journal entry transaction posted when the deal was settled. */
|
|
||||||
@Column(name = "gl_transaction_id", length = 50)
|
|
||||||
private String glTransactionId;
|
|
||||||
|
|
||||||
@Column(name = "settled_on_date")
|
|
||||||
private LocalDate settledOnDate;
|
|
||||||
}
|
|
||||||
-36
@@ -1,36 +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.investment.domain;
|
|
||||||
|
|
||||||
import jakarta.persistence.LockModeType;
|
|
||||||
import java.util.Optional;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
import org.springframework.data.jpa.repository.Lock;
|
|
||||||
import org.springframework.data.jpa.repository.Query;
|
|
||||||
import org.springframework.data.repository.query.Param;
|
|
||||||
|
|
||||||
public interface InvestmentPurchaseRepository extends JpaRepository<InvestmentPurchase, Long> {
|
|
||||||
|
|
||||||
/** Locks the deal row so two postings on one deal (double click, two users) run one after the other. */
|
|
||||||
@Lock(value = LockModeType.PESSIMISTIC_WRITE)
|
|
||||||
@Query("select p from InvestmentPurchase p where p.id = :purchaseId")
|
|
||||||
Optional<InvestmentPurchase> findByIdWithLock(@Param("purchaseId") Long purchaseId);
|
|
||||||
|
|
||||||
boolean existsByScriptIdAndStatusNot(Long scriptId, String status);
|
|
||||||
}
|
|
||||||
-132
@@ -1,132 +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.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;
|
|
||||||
|
|
||||||
/** A sale of some or all units of a holding before maturity (SELL), or its repayment at maturity (REDEEM). */
|
|
||||||
@Entity
|
|
||||||
@Table(name = "m_investment_sale")
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor
|
|
||||||
public class InvestmentSale extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|
||||||
|
|
||||||
@ManyToOne
|
|
||||||
@JoinColumn(name = "purchase_id", nullable = false)
|
|
||||||
private InvestmentPurchase purchase;
|
|
||||||
|
|
||||||
/** SELL, REDEEM, WRITEOFF or AMORTISE. */
|
|
||||||
@Column(name = "sale_type", nullable = false, length = 10)
|
|
||||||
private String saleType;
|
|
||||||
|
|
||||||
/** Buyer of a sale; empty for a redemption. */
|
|
||||||
@ManyToOne
|
|
||||||
@JoinColumn(name = "party_id")
|
|
||||||
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", length = 5)
|
|
||||||
private String settlementType;
|
|
||||||
|
|
||||||
@Column(name = "settlement_date", nullable = false)
|
|
||||||
private LocalDate settlementDate;
|
|
||||||
|
|
||||||
@Column(name = "units", scale = 6, precision = 19, nullable = false)
|
|
||||||
private BigDecimal units;
|
|
||||||
|
|
||||||
@Column(name = "face_value", scale = 6, precision = 19, nullable = false)
|
|
||||||
private BigDecimal faceValue;
|
|
||||||
|
|
||||||
/** Price per ₹100 of face value; 100 for a redemption. */
|
|
||||||
@Column(name = "sale_rate", scale = 6, precision = 19)
|
|
||||||
private BigDecimal saleRate;
|
|
||||||
|
|
||||||
/** Price received for the units, without interest. */
|
|
||||||
@Column(name = "consideration", scale = 6, precision = 19, nullable = false)
|
|
||||||
private BigDecimal consideration;
|
|
||||||
|
|
||||||
@Column(name = "accrued_days")
|
|
||||||
private Integer accruedDays;
|
|
||||||
|
|
||||||
/** Interest the buyer pays us for the days since the last coupon. */
|
|
||||||
@Column(name = "accrued_interest", scale = 6, precision = 19)
|
|
||||||
private BigDecimal accruedInterest;
|
|
||||||
|
|
||||||
/** Part of that interest already booked by a closing provision, cleared from interest receivable. */
|
|
||||||
@Column(name = "receivable_cleared", scale = 6, precision = 19)
|
|
||||||
private BigDecimal receivableCleared;
|
|
||||||
|
|
||||||
@Column(name = "brokerage", scale = 6, precision = 19)
|
|
||||||
private BigDecimal brokerage;
|
|
||||||
|
|
||||||
/** Cost of the units sold, taken off the investment GL. */
|
|
||||||
@Column(name = "book_value_sold", scale = 6, precision = 19, nullable = false)
|
|
||||||
private BigDecimal bookValueSold;
|
|
||||||
|
|
||||||
/** Part of the book value taken off that was unamortised premium (credited to the premium GL). */
|
|
||||||
@Column(name = "premium_cleared", scale = 6, precision = 19)
|
|
||||||
private BigDecimal premiumCleared;
|
|
||||||
|
|
||||||
/** Consideration less cost: positive is profit, negative is loss. */
|
|
||||||
@Column(name = "profit_loss", scale = 6, precision = 19, nullable = false)
|
|
||||||
private BigDecimal profitLoss;
|
|
||||||
|
|
||||||
/** Money into the bank: consideration plus interest, less brokerage. */
|
|
||||||
@Column(name = "net_amount", scale = 6, precision = 19, nullable = false)
|
|
||||||
private BigDecimal netAmount;
|
|
||||||
|
|
||||||
@ManyToOne
|
|
||||||
@JoinColumn(name = "bank_gl_account_id", nullable = false)
|
|
||||||
private GLAccount bankGlAccount;
|
|
||||||
|
|
||||||
@Column(name = "sanction_authority", length = 100)
|
|
||||||
private String sanctionAuthority;
|
|
||||||
|
|
||||||
@Column(name = "remarks", length = 500)
|
|
||||||
private String remarks;
|
|
||||||
|
|
||||||
@Column(name = "status", nullable = false, length = 20)
|
|
||||||
private String status;
|
|
||||||
|
|
||||||
@Column(name = "gl_transaction_id", length = 50)
|
|
||||||
private String glTransactionId;
|
|
||||||
|
|
||||||
@Column(name = "voucher_no", length = 30)
|
|
||||||
private String voucherNo;
|
|
||||||
|
|
||||||
@Column(name = "settled_on_date")
|
|
||||||
private LocalDate settledOnDate;
|
|
||||||
}
|
|
||||||
-40
@@ -1,40 +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.investment.domain;
|
|
||||||
|
|
||||||
import jakarta.persistence.LockModeType;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
import org.springframework.data.jpa.repository.Lock;
|
|
||||||
import org.springframework.data.jpa.repository.Query;
|
|
||||||
import org.springframework.data.repository.query.Param;
|
|
||||||
|
|
||||||
public interface InvestmentSaleRepository extends JpaRepository<InvestmentSale, Long> {
|
|
||||||
|
|
||||||
List<InvestmentSale> findByPurchaseIdOrderByIdAsc(Long purchaseId);
|
|
||||||
|
|
||||||
@Lock(value = LockModeType.PESSIMISTIC_WRITE)
|
|
||||||
@Query("select s from InvestmentSale s where s.id = :saleId")
|
|
||||||
Optional<InvestmentSale> findByIdWithLock(@Param("saleId") Long saleId);
|
|
||||||
|
|
||||||
/** The deal of a sale, without loading either, so the deal can be locked first. */
|
|
||||||
@Query("select s.purchase.id from InvestmentSale s where s.id = :saleId")
|
|
||||||
Optional<Long> findPurchaseIdBySaleId(@Param("saleId") Long saleId);
|
|
||||||
}
|
|
||||||
-118
@@ -1,118 +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.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;
|
|
||||||
|
|
||||||
/** Credit rating, long-term (AAA … D) for bonds or short-term (A1+ … A4) for CPs / CDs; UNRATED when none. */
|
|
||||||
@Column(name = "rating", length = 10)
|
|
||||||
private String rating;
|
|
||||||
|
|
||||||
@Column(name = "rating_agency", length = 30)
|
|
||||||
private String ratingAgency;
|
|
||||||
|
|
||||||
/** Listed on an exchange, or proposed to be listed (RBI counts those as listed when bought). */
|
|
||||||
@Column(name = "is_listed", nullable = false)
|
|
||||||
private boolean listed = true;
|
|
||||||
|
|
||||||
@Column(name = "is_active", nullable = false)
|
|
||||||
private boolean active = true;
|
|
||||||
}
|
|
||||||
-27
@@ -1,27 +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.investment.domain;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
|
|
||||||
public interface InvestmentScriptRepository extends JpaRepository<InvestmentScript, Long> {
|
|
||||||
|
|
||||||
List<InvestmentScript> findByIsinIgnoreCase(String isin);
|
|
||||||
}
|
|
||||||
-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.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;
|
|
||||||
|
|
||||||
/** A holding moved from one government security script to another of the same terms (GOI Category Transfer). */
|
|
||||||
@Entity
|
|
||||||
@Table(name = "m_investment_security_transfer")
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor
|
|
||||||
public class InvestmentSecurityTransfer extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|
||||||
|
|
||||||
@ManyToOne
|
|
||||||
@JoinColumn(name = "purchase_id", nullable = false)
|
|
||||||
private InvestmentPurchase purchase;
|
|
||||||
|
|
||||||
@ManyToOne
|
|
||||||
@JoinColumn(name = "from_script_id", nullable = false)
|
|
||||||
private InvestmentScript fromScript;
|
|
||||||
|
|
||||||
@ManyToOne
|
|
||||||
@JoinColumn(name = "to_script_id", nullable = false)
|
|
||||||
private InvestmentScript toScript;
|
|
||||||
|
|
||||||
@ManyToOne
|
|
||||||
@JoinColumn(name = "from_gl_account_id", nullable = false)
|
|
||||||
private GLAccount fromGlAccount;
|
|
||||||
|
|
||||||
@ManyToOne
|
|
||||||
@JoinColumn(name = "to_gl_account_id", nullable = false)
|
|
||||||
private GLAccount toGlAccount;
|
|
||||||
|
|
||||||
@Column(name = "transfer_date", nullable = false)
|
|
||||||
private LocalDate transferDate;
|
|
||||||
|
|
||||||
@Column(name = "units", scale = 6, precision = 19, nullable = false)
|
|
||||||
private BigDecimal units;
|
|
||||||
|
|
||||||
/** Book value carried over; moved between GLs only when the two scripts map to different GLs. */
|
|
||||||
@Column(name = "book_value", scale = 6, precision = 19, nullable = false)
|
|
||||||
private BigDecimal bookValue;
|
|
||||||
|
|
||||||
@Column(name = "remarks", length = 500)
|
|
||||||
private String remarks;
|
|
||||||
|
|
||||||
@Column(name = "gl_transaction_id", length = 50)
|
|
||||||
private String glTransactionId;
|
|
||||||
|
|
||||||
@Column(name = "voucher_no", length = 30)
|
|
||||||
private String voucherNo;
|
|
||||||
}
|
|
||||||
-23
@@ -1,23 +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.investment.domain;
|
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
|
|
||||||
public interface InvestmentSecurityTransferRepository extends JpaRepository<InvestmentSecurityTransfer, Long> {}
|
|
||||||
-28
@@ -1,28 +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.investment.exception;
|
|
||||||
|
|
||||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
|
|
||||||
|
|
||||||
public class InvestmentDailyRateNotFoundException extends AbstractPlatformResourceNotFoundException {
|
|
||||||
|
|
||||||
public InvestmentDailyRateNotFoundException(final Long id) {
|
|
||||||
super("error.msg.investmentdailyrate.id.invalid", "Daily script rate with identifier " + id + " does not exist", id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-28
@@ -1,28 +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.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
@@ -1,28 +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.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user