Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8f8aa39b7 | ||
|
|
3a320b9c09 | ||
|
|
8ba0420398 | ||
|
|
6be32272c1 | ||
|
|
b47a34b5ef | ||
|
|
04d1eb20f0 |
-192
@@ -95,198 +95,6 @@ public class CommandWrapperBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder createInvestmentParty() {
|
||||
this.actionName = "CREATE";
|
||||
this.entityName = "INVESTMENTPARTY";
|
||||
this.entityId = null;
|
||||
this.href = "/investment/parties";
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder updateInvestmentParty(final Long partyId) {
|
||||
this.actionName = "UPDATE";
|
||||
this.entityName = "INVESTMENTPARTY";
|
||||
this.entityId = partyId;
|
||||
this.href = "/investment/parties/" + partyId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder deleteInvestmentParty(final Long partyId) {
|
||||
this.actionName = "DELETE";
|
||||
this.entityName = "INVESTMENTPARTY";
|
||||
this.entityId = partyId;
|
||||
this.href = "/investment/parties/" + partyId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder createInvestmentScript() {
|
||||
this.actionName = "CREATE";
|
||||
this.entityName = "INVESTMENTSCRIPT";
|
||||
this.entityId = null;
|
||||
this.href = "/investment/scripts";
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder updateInvestmentScript(final Long scriptId) {
|
||||
this.actionName = "UPDATE";
|
||||
this.entityName = "INVESTMENTSCRIPT";
|
||||
this.entityId = scriptId;
|
||||
this.href = "/investment/scripts/" + scriptId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder createInvestmentPurchase() {
|
||||
this.actionName = "CREATE";
|
||||
this.entityName = "INVESTMENTPURCHASE";
|
||||
this.entityId = null;
|
||||
this.href = "/investment/purchases";
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder deleteInvestmentPurchase(final Long purchaseId) {
|
||||
this.actionName = "DELETE";
|
||||
this.entityName = "INVESTMENTPURCHASE";
|
||||
this.entityId = purchaseId;
|
||||
this.href = "/investment/purchases/" + purchaseId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder 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() {
|
||||
this.actionName = "CREATE";
|
||||
this.entityName = "CHEQUESTOCKLOT";
|
||||
|
||||
+1
-1
@@ -92,7 +92,7 @@ public class TenantMenuAccessApiResource {
|
||||
"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_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}.
|
||||
|
||||
-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.portfolio.cashreport.service.VaultPositionService;
|
||||
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.SavingsInterestPostingReportService;
|
||||
import org.apache.fineract.portfolio.savings.fdinterest.FixedDepositInterestPostingOrchestrator;
|
||||
@@ -97,16 +95,8 @@ public class BranchDayRegisterApiResource {
|
||||
private final SavingsDailyInterestOrchestrator savingsDailyInterestOrchestrator;
|
||||
private final FixedDepositInterestPostingReportService fdInterestPostingReportService;
|
||||
private final FixedDepositInterestPostingOrchestrator fdInterestPostingOrchestrator;
|
||||
private final InvestmentPurchaseReadPlatformService investmentPurchaseReadPlatformService;
|
||||
private final InvestmentFdInterestService investmentFdInterestService;
|
||||
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 ──────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -270,13 +260,6 @@ public class BranchDayRegisterApiResource {
|
||||
final Map<String, Object> fdInterest = fdInterestPostingReportService.dayCloseInterestView(officeId, businessDate);
|
||||
result.put("fdInterestPending", fdInterest.get("pending"));
|
||||
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);
|
||||
}
|
||||
|
||||
+7
@@ -56,6 +56,7 @@ import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.client.domain.KycExpiryStage;
|
||||
import org.apache.fineract.portfolio.client.service.KycRestrictionGuard;
|
||||
import org.apache.fineract.portfolio.savings.service.FixedDepositFundingValidator;
|
||||
import org.apache.fineract.portfolio.voucher.service.VoucherLine;
|
||||
import org.apache.fineract.portfolio.voucher.service.VoucherService;
|
||||
import org.slf4j.Logger;
|
||||
@@ -98,6 +99,7 @@ public class CashierDepositRequestApiResource {
|
||||
private final PlatformSecurityContext context;
|
||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||
private final VoucherService voucherService;
|
||||
private final FixedDepositFundingValidator fixedDepositFundingValidator;
|
||||
private final Gson gson = new GsonBuilder().serializeNulls().create();
|
||||
|
||||
// ── POST /v1/cashier/deposit-requests ── Cashier submits new deposit request ──
|
||||
@@ -155,6 +157,11 @@ public class CashierDepositRequestApiResource {
|
||||
throw apiError(400, "Amount must be greater than zero");
|
||||
}
|
||||
kycRestrictionGuard.check(savingsAccountId, KycRestrictionGuard.Operation.CREDIT);
|
||||
// An FD is funded once, with exactly its principal. Told here at the counter rather than when the
|
||||
// manager approves; the FD deposit command checks the same again at posting.
|
||||
if (!isGlType && "FIXED_DEPOSIT".equals(resolveDepositAccountCategory(savingsAccountId, accountNo))) {
|
||||
fixedDepositFundingValidator.validateFirstFunding(savingsAccountId, amount);
|
||||
}
|
||||
validateDenominationTotal(denominationData, amount);
|
||||
validateDdLimit(ddFaceValue);
|
||||
|
||||
|
||||
+14
-1
@@ -20,6 +20,7 @@ import org.apache.fineract.portfolio.cashreport.service.CashierRoleHelper;
|
||||
import org.apache.fineract.portfolio.cashreport.service.VaultPositionService;
|
||||
import org.apache.fineract.portfolio.denominationmaster.service.CountedCashLedger;
|
||||
import org.apache.fineract.portfolio.denominationmaster.service.DenominationBreakdownWriter;
|
||||
import org.apache.fineract.portfolio.voucher.service.VoucherService;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
@@ -36,6 +37,7 @@ public class CashReportApiResource {
|
||||
private final DenominationBreakdownWriter denominationBreakdownWriter;
|
||||
private final CashierRoleHelper cashierRoleHelper;
|
||||
private final VaultPositionService vaultPositionService;
|
||||
private final VoucherService voucherService;
|
||||
private final Gson gson = new Gson();
|
||||
|
||||
/**
|
||||
@@ -329,7 +331,18 @@ public class CashReportApiResource {
|
||||
BigDecimal priorCreditTotal = BigDecimal.ZERO;
|
||||
BigDecimal priorDebitTotal = BigDecimal.ZERO;
|
||||
|
||||
// Where each voucher stands after a correction: a revoked voucher is listed, marked, but its money now sits on
|
||||
// the voucher that replaced it, so it must not be counted in the day's totals a second time.
|
||||
final Map<String, Map<String, Object>> marks = voucherService.voucherMarks(
|
||||
rows.stream().map(r -> (String) r.get("voucherNo")).toList());
|
||||
|
||||
for (final Map<String, Object> row : rows) {
|
||||
final Map<String, Object> mark = marks.get((String) row.get("voucherNo"));
|
||||
row.put("voucherStatus", mark == null ? "ACTIVE" : mark.get("voucherStatus"));
|
||||
row.put("revokedByVoucherNo", mark == null ? null : mark.get("revokedByVoucherNo"));
|
||||
row.put("replacesVoucherNo", mark == null ? null : mark.get("replacesVoucherNo"));
|
||||
row.put("correctionNote", mark == null ? null : mark.get("correctionNote"));
|
||||
final boolean revoked = "REVOKED".equals(row.get("voucherStatus"));
|
||||
if (sideFilter != null && !sideFilter.equals(row.get("side"))) {
|
||||
continue;
|
||||
}
|
||||
@@ -350,7 +363,7 @@ public class CashReportApiResource {
|
||||
// was already earned, and the day it belongs to has been counted and signed off. Adding
|
||||
// it to this day's totals would make them disagree with the ledger for either date.
|
||||
final boolean priorDay = "SYSTEM_PRIOR".equals(row.get("section"));
|
||||
if (!priorDay && ("APPROVED".equalsIgnoreCase(rowStatus) || "COMPLETED".equalsIgnoreCase(rowStatus)
|
||||
if (!priorDay && !revoked && ("APPROVED".equalsIgnoreCase(rowStatus) || "COMPLETED".equalsIgnoreCase(rowStatus)
|
||||
|| "WITHDRAWN".equalsIgnoreCase(rowStatus) || "CLOSED".equalsIgnoreCase(rowStatus))) {
|
||||
final BigDecimal amount = (BigDecimal) row.get("amount");
|
||||
if (amount != null) {
|
||||
|
||||
+186
-10
@@ -57,9 +57,23 @@ public class CashTransferApiResource {
|
||||
private final VaultPositionService vaultPositionService;
|
||||
private final Gson gson = new Gson();
|
||||
|
||||
/**
|
||||
* Cash no longer leaves the vault on one person's say-so: the allocation is raised as a request (see
|
||||
* {@link #submitRequest}) and only a different person's approval posts it. The posting itself is
|
||||
* {@link #postTransfer}.
|
||||
*/
|
||||
@POST
|
||||
public String create(String body) {
|
||||
this.context.authenticatedUser().validateHasPermissionTo("CREATE_CASHTRANSFER");
|
||||
throw new PlatformDataIntegrityException("error.msg.cashtransfer.request.required",
|
||||
"A cash transfer is raised as a request and posted when a manager approves it.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a transfer and, unless {@code dryRun}, posts it: the header and its denominations, then the GL entry.
|
||||
* A dry run stops after every check has passed, which is what a request is held to when it is raised and again
|
||||
* when it is approved, since the vault can change in between.
|
||||
*/
|
||||
private String postTransfer(final String body, final Long performedBy, final String performedByUsername, final boolean dryRun) {
|
||||
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
||||
|
||||
final LocalDate transferDate = json.has("transferDate") && !json.get("transferDate").isJsonNull()
|
||||
@@ -102,6 +116,9 @@ public class CashTransferApiResource {
|
||||
}
|
||||
|
||||
validateVaultHasSufficientCash(fromTellerId, total, denominations, transferDate);
|
||||
if (dryRun) {
|
||||
return "{}";
|
||||
}
|
||||
|
||||
CashTransfer header = new CashTransfer();
|
||||
header.setTransferDate(transferDate);
|
||||
@@ -112,15 +129,9 @@ public class CashTransferApiResource {
|
||||
header.setTransferAmount(total);
|
||||
header.setNote(note);
|
||||
header.setCreatedAt(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()));
|
||||
try {
|
||||
AppUser user = this.context.authenticatedUser();
|
||||
if (user != null) {
|
||||
header.setPerformedBy(user.getId());
|
||||
header.setPerformedByUsername(user.getUsername());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// resolving the current user must never block recording the transfer
|
||||
}
|
||||
// Recorded against the person who raised the request, not the one who approved it.
|
||||
header.setPerformedBy(performedBy);
|
||||
header.setPerformedByUsername(performedByUsername);
|
||||
|
||||
// Save header + details in one committed transaction, then GL dispatch separately.
|
||||
// This prevents EclipseLink from flushing pending detail rows inside Fineract's
|
||||
@@ -159,6 +170,171 @@ public class CashTransferApiResource {
|
||||
return gson.toJson(result);
|
||||
}
|
||||
|
||||
// ── Maker-checker: the allocation is a request until a different person approves it ────────────
|
||||
|
||||
/** The maker raises the allocation. Nothing moves yet: every check runs, and the request waits for a checker. */
|
||||
@POST
|
||||
@Path("requests")
|
||||
public String submitRequest(final String body) {
|
||||
final AppUser user = this.context.authenticatedUser();
|
||||
user.validateHasPermissionTo("CREATE_CASHTRANSFER");
|
||||
postTransfer(body, user.getId(), user.getUsername(), true);
|
||||
|
||||
// The same request, from the same person, is not raised twice while the first is still waiting: a second tap or
|
||||
// a second click would otherwise release the same cash twice once both were approved.
|
||||
final Integer identical = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(*) FROM m_cash_transfer_request WHERE status = ? AND maker_id = ? AND request_body = ?", Integer.class,
|
||||
REQUEST_PENDING, user.getId(), body);
|
||||
if (identical != null && identical > 0) {
|
||||
throw new PlatformDataIntegrityException("error.msg.cashtransfer.request.duplicate",
|
||||
"An identical cash transfer request from you is already waiting for approval.");
|
||||
}
|
||||
|
||||
final JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
||||
final JsonArray denominations = json.getAsJsonArray("denominations");
|
||||
final Long id = jdbcTemplate.queryForObject("""
|
||||
INSERT INTO m_cash_transfer_request
|
||||
(transfer_date, from_teller_id, to_cashier_id, from_party, to_party, note, total_amount, request_body,
|
||||
status, maker_id, maker_name, maker_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
RETURNING id
|
||||
""", Long.class, textOrNull(json, "transferDate") == null ? null : java.sql.Date.valueOf(textOrNull(json, "transferDate")),
|
||||
json.get("fromTellerId").getAsLong(), json.get("toCashierId").getAsLong(), textOrNull(json, "fromParty"),
|
||||
textOrNull(json, "toParty"), textOrNull(json, "note"), sumTotal(denominations), body, REQUEST_PENDING, user.getId(),
|
||||
user.getDisplayName(), java.sql.Timestamp.from(java.time.Instant.now()));
|
||||
final JsonObject result = new JsonObject();
|
||||
result.addProperty("resourceId", id);
|
||||
result.addProperty("status", REQUEST_PENDING);
|
||||
return gson.toJson(result);
|
||||
}
|
||||
|
||||
/** Requests by status (comma separated); {@code mine} narrows them to the ones the caller raised. */
|
||||
@GET
|
||||
@Path("requests")
|
||||
public String listRequests(@QueryParam("status") final String status,
|
||||
@QueryParam("mine") @jakarta.ws.rs.DefaultValue("false") final boolean mine) {
|
||||
final AppUser user = this.context.authenticatedUser();
|
||||
final StringBuilder sql = new StringBuilder("SELECT * FROM m_cash_transfer_request WHERE 1=1");
|
||||
final List<Object> params = new java.util.ArrayList<>();
|
||||
if (status != null && !status.isBlank()) {
|
||||
final String[] statuses = status.split(",");
|
||||
sql.append(" AND status IN (").append("?,".repeat(statuses.length), 0, statuses.length * 2 - 1).append(")");
|
||||
for (final String s : statuses) {
|
||||
params.add(s.trim().toUpperCase());
|
||||
}
|
||||
}
|
||||
if (mine) {
|
||||
sql.append(" AND maker_id = ?");
|
||||
params.add(user.getId());
|
||||
}
|
||||
sql.append(" ORDER BY id DESC LIMIT 200");
|
||||
final List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql.toString(), params.toArray());
|
||||
for (final Map<String, Object> row : rows) {
|
||||
// The notes asked for, so the approver sees what they are releasing without opening anything.
|
||||
final Object requestBody = row.remove("request_body");
|
||||
row.put("denominations", requestBody == null ? new JsonArray()
|
||||
: JsonParser.parseString(requestBody.toString()).getAsJsonObject().getAsJsonArray("denominations"));
|
||||
}
|
||||
return gson.toJson(rows);
|
||||
}
|
||||
|
||||
/** The checker releases the cash: re-checks the vault, then posts the transfer exactly as a direct one used to. */
|
||||
@POST
|
||||
@Path("requests/{id}/approve")
|
||||
public String approveRequest(@PathParam("id") final Long id) {
|
||||
final AppUser user = this.context.authenticatedUser();
|
||||
requireChecker(user);
|
||||
final Map<String, Object> r = loadRequest(id);
|
||||
requirePending(r, "approved");
|
||||
final Long makerId = r.get("maker_id") instanceof Number n ? n.longValue() : null;
|
||||
if (makerId != null && makerId.equals(user.getId())) {
|
||||
throw new PlatformDataIntegrityException("error.msg.cashtransfer.request.same.user",
|
||||
"Maker-checker: you cannot approve a cash transfer you raised yourself. Ask another manager.");
|
||||
}
|
||||
final String posted = postTransfer((String) r.get("request_body"), makerId, (String) r.get("maker_name"), false);
|
||||
final JsonObject postedJson = JsonParser.parseString(posted).getAsJsonObject();
|
||||
final Long transferId = postedJson.has("resourceId") ? postedJson.get("resourceId").getAsLong() : null;
|
||||
|
||||
final int updated = jdbcTemplate.update("""
|
||||
UPDATE m_cash_transfer_request SET status = ?, checker_id = ?, checker_name = ?, checker_at = ?, cash_transfer_id = ?
|
||||
WHERE id = ? AND status = ?
|
||||
""", REQUEST_APPROVED, user.getId(), user.getDisplayName(), java.sql.Timestamp.from(java.time.Instant.now()),
|
||||
transferId, id, REQUEST_PENDING);
|
||||
if (updated == 0) {
|
||||
throw new PlatformDataIntegrityException("error.msg.cashtransfer.request.actioned",
|
||||
"This request was already actioned by another manager.");
|
||||
}
|
||||
final JsonObject result = new JsonObject();
|
||||
result.addProperty("resourceId", id);
|
||||
result.addProperty("status", REQUEST_APPROVED);
|
||||
if (transferId != null) {
|
||||
result.addProperty("cashTransferId", transferId);
|
||||
}
|
||||
if (postedJson.has("glWarning")) {
|
||||
result.add("glWarning", postedJson.get("glWarning"));
|
||||
}
|
||||
return gson.toJson(result);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("requests/{id}/reject")
|
||||
public String rejectRequest(@PathParam("id") final Long id, final String body) {
|
||||
final AppUser user = this.context.authenticatedUser();
|
||||
requireChecker(user);
|
||||
final Map<String, Object> r = loadRequest(id);
|
||||
requirePending(r, "rejected");
|
||||
final JsonObject json = body == null || body.isBlank() ? new JsonObject() : JsonParser.parseString(body).getAsJsonObject();
|
||||
final String reason = textOrNull(json, "reason");
|
||||
if (reason == null || reason.isBlank()) {
|
||||
throw new PlatformDataIntegrityException("error.msg.cashtransfer.request.reason", "A reason is required to reject.");
|
||||
}
|
||||
jdbcTemplate.update("""
|
||||
UPDATE m_cash_transfer_request SET status = ?, checker_id = ?, checker_name = ?, checker_at = ?, rejection_reason = ?
|
||||
WHERE id = ? AND status = ?
|
||||
""", REQUEST_REJECTED, user.getId(), user.getDisplayName(), java.sql.Timestamp.from(java.time.Instant.now()),
|
||||
reason.trim(), id, REQUEST_PENDING);
|
||||
final JsonObject result = new JsonObject();
|
||||
result.addProperty("resourceId", id);
|
||||
result.addProperty("status", REQUEST_REJECTED);
|
||||
return gson.toJson(result);
|
||||
}
|
||||
|
||||
private static final String REQUEST_PENDING = "PENDING";
|
||||
private static final String REQUEST_APPROVED = "APPROVED";
|
||||
private static final String REQUEST_REJECTED = "REJECTED";
|
||||
|
||||
private Map<String, Object> loadRequest(final Long id) {
|
||||
final List<Map<String, Object>> rows = jdbcTemplate.queryForList("SELECT * FROM m_cash_transfer_request WHERE id = ?", id);
|
||||
if (rows.isEmpty()) {
|
||||
throw new PlatformDataIntegrityException("error.msg.cashtransfer.request.not.found", "Cash transfer request " + id + " not found.");
|
||||
}
|
||||
return rows.get(0);
|
||||
}
|
||||
|
||||
private void requirePending(final Map<String, Object> request, final String action) {
|
||||
if (!REQUEST_PENDING.equals(request.get("status"))) {
|
||||
throw new PlatformDataIntegrityException("error.msg.cashtransfer.request.status",
|
||||
"Only PENDING requests can be " + action + ". Current status: " + request.get("status"));
|
||||
}
|
||||
}
|
||||
|
||||
/** Checker authorization, the same rule the withdrawal and deposit request registers use. */
|
||||
private void requireChecker(final AppUser user) {
|
||||
if (user.hasAnyPermission("ALL_FUNCTIONS")) {
|
||||
return;
|
||||
}
|
||||
final boolean isChecker = user.getRoles().stream().map(role -> role.getName() == null ? "" : role.getName().toLowerCase().trim())
|
||||
.anyMatch(n -> n.contains("manager") || n.contains("head cashier"));
|
||||
if (!isChecker) {
|
||||
throw new PlatformDataIntegrityException("error.msg.cashtransfer.request.checker",
|
||||
"Only a Bank Manager (or Head Cashier) can approve or reject a cash transfer.");
|
||||
}
|
||||
}
|
||||
|
||||
private static String textOrNull(final JsonObject json, final String key) {
|
||||
return json.has(key) && !json.get(key).isJsonNull() ? json.get(key).getAsString() : null;
|
||||
}
|
||||
|
||||
@GET
|
||||
public String getAll(
|
||||
@QueryParam("offset") @jakarta.ws.rs.DefaultValue("0") int offset,
|
||||
|
||||
+283
@@ -0,0 +1,283 @@
|
||||
/**
|
||||
* 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.servlet.http.HttpServletRequest;
|
||||
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.Context;
|
||||
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.CicCatalog;
|
||||
import org.apache.fineract.portfolio.cic.service.CicConfigService;
|
||||
import org.apache.fineract.portfolio.cic.service.CicConsentService;
|
||||
import org.apache.fineract.portfolio.cic.service.CicEnquiryService;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* CIC / credit bureau enquiry: bureau whitelist and configuration, API mapping, borrower consent and enquiries.
|
||||
*
|
||||
* Permissions: READ_CICBUREAU / UPDATE_CICBUREAU (configuration and mapping), READ_CICENQUIRY / CREATE_CICENQUIRY,
|
||||
* READ_CICCONSENT / CREATE_CICCONSENT / REVOKE_CICCONSENT.
|
||||
*/
|
||||
@Path("/v1/cic")
|
||||
@Component
|
||||
@Tag(name = "CIC Credit Bureau", description = "Credit bureau (CIBIL, Experian, Equifax, CRIF High Mark) whitelist, API mapping, consent and enquiries.")
|
||||
@RequiredArgsConstructor
|
||||
public class CicApiResource {
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final CicConfigService configService;
|
||||
private final CicConsentService consentService;
|
||||
private final CicEnquiryService enquiryService;
|
||||
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("consentModes", CicCatalog.CONSENT_MODES);
|
||||
c.put("purposes", CicCatalog.PURPOSES);
|
||||
c.put("cbsFields", CicCatalog.CBS_FIELDS);
|
||||
c.put("resultFields", CicCatalog.RESULT_FIELDS);
|
||||
c.put("transforms", CicCatalog.TRANSFORMS);
|
||||
c.put("defaultConsentText", CicConsentService.DEFAULT_TEXT);
|
||||
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_CICENQUIRY", "CREATE_CICENQUIRY", "CREATE_CICCONSENT");
|
||||
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(enquiryService.preview(id, json(body)));
|
||||
}
|
||||
|
||||
// ── Client profile ────────────────────────────────────────────────────────
|
||||
|
||||
@GET
|
||||
@Path("clients/{clientId}/profile")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public String clientProfile(@PathParam("clientId") final Long clientId, @QueryParam("bureauId") final Long bureauId) {
|
||||
requireAny("CREATE_CICENQUIRY", "READ_CICENQUIRY", "CREATE_CICCONSENT");
|
||||
return gson.toJson(enquiryService.clientProfile(clientId, bureauId));
|
||||
}
|
||||
|
||||
// ── Consents ──────────────────────────────────────────────────────────────
|
||||
|
||||
@GET
|
||||
@Path("consents")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public String consents(@QueryParam("clientId") final Long clientId, @QueryParam("status") final String status,
|
||||
@QueryParam("search") final String search, @QueryParam("offset") @DefaultValue("0") final int offset,
|
||||
@QueryParam("limit") @DefaultValue("20") final int limit) {
|
||||
require("READ_CICCONSENT");
|
||||
return gson.toJson(consentService.list(clientId, status, search, Math.max(0, offset), clamp(limit)));
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("consents")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public String createConsent(final String body, @Context final HttpServletRequest request) {
|
||||
final AppUser user = require("CREATE_CICCONSENT");
|
||||
return gson.toJson(consentService.create(json(body), user, clientIp(request)));
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("consents/{id}/revoke")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public String revokeConsent(@PathParam("id") final Long id, final String body) {
|
||||
final AppUser user = require("REVOKE_CICCONSENT");
|
||||
final JsonObject in = json(body);
|
||||
return gson.toJson(consentService.revoke(id, in.has("reason") && !in.get("reason").isJsonNull() ? in.get("reason").getAsString() : null,
|
||||
user));
|
||||
}
|
||||
|
||||
// ── Enquiries ─────────────────────────────────────────────────────────────
|
||||
|
||||
@GET
|
||||
@Path("enquiries")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public String enquiries(@QueryParam("clientId") final Long clientId, @QueryParam("bureauId") final Long bureauId,
|
||||
@QueryParam("status") final String status, @QueryParam("fromDate") final String fromDate, @QueryParam("toDate") final String toDate,
|
||||
@QueryParam("search") final String search, @QueryParam("offset") @DefaultValue("0") final int offset,
|
||||
@QueryParam("limit") @DefaultValue("20") final int limit) {
|
||||
require("READ_CICENQUIRY");
|
||||
return gson.toJson(enquiryService.list(clientId, bureauId, status, fromDate, toDate, search, Math.max(0, offset), clamp(limit)));
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("enquiries/{id}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public String enquiry(@PathParam("id") final Long id) {
|
||||
require("READ_CICENQUIRY");
|
||||
return gson.toJson(enquiryService.get(id));
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("enquiries")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public String enquire(final String body) {
|
||||
final AppUser user = require("CREATE_CICENQUIRY");
|
||||
return gson.toJson(enquiryService.enquire(json(body), user));
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("dashboard")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public String dashboard() {
|
||||
require("READ_CICENQUIRY");
|
||||
return gson.toJson(enquiryService.dashboard());
|
||||
}
|
||||
|
||||
// ── 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 int clamp(final int limit) {
|
||||
return Math.max(1, Math.min(200, limit));
|
||||
}
|
||||
|
||||
private static String clientIp(final HttpServletRequest request) {
|
||||
if (request == null) {
|
||||
return null;
|
||||
}
|
||||
final String forwarded = request.getHeader("X-Forwarded-For");
|
||||
if (StringUtils.isNotBlank(forwarded)) {
|
||||
return forwarded.split(",")[0].trim();
|
||||
}
|
||||
return request.getRemoteAddr();
|
||||
}
|
||||
}
|
||||
+247
@@ -0,0 +1,247 @@
|
||||
/**
|
||||
* 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, and the transforms applied when a CBS value is written into a
|
||||
* bureau request or a bureau value is read back.
|
||||
*/
|
||||
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 List<String> ENDPOINT_TYPES = List.of("CONSUMER_ENQUIRY", "COMMERCIAL_ENQUIRY");
|
||||
public static final List<String> HTTP_METHODS = List.of("POST", "PUT", "GET");
|
||||
public static final List<String> CONSENT_MODES = List.of("SIGNED_FORM", "OTP", "DIGITAL_ACCEPTANCE");
|
||||
|
||||
/** Enquiry purposes with the code sent to the bureau (enquiry.purposeCode). */
|
||||
public static final Map<String, String> PURPOSES = new LinkedHashMap<>();
|
||||
static {
|
||||
PURPOSES.put("NEW_LOAN", "New loan application");
|
||||
PURPOSES.put("CREDIT_CARD", "Credit card / overdraft");
|
||||
PURPOSES.put("LOAN_RENEWAL", "Renewal / enhancement");
|
||||
PURPOSES.put("ACCOUNT_REVIEW", "Account review / monitoring");
|
||||
PURPOSES.put("GUARANTOR_CHECK", "Guarantor / co-applicant check");
|
||||
PURPOSES.put("COLLECTION", "Collection / recovery");
|
||||
PURPOSES.put("OTHER", "Other");
|
||||
}
|
||||
|
||||
/** CBS fields a REQUEST mapping can take its value from. */
|
||||
public static final Map<String, String> CBS_FIELDS = new LinkedHashMap<>();
|
||||
static {
|
||||
CBS_FIELDS.put("client.firstName", "Client first name");
|
||||
CBS_FIELDS.put("client.middleName", "Client middle name");
|
||||
CBS_FIELDS.put("client.lastName", "Client last name");
|
||||
CBS_FIELDS.put("client.fullName", "Client full / display name");
|
||||
CBS_FIELDS.put("client.dob", "Date of birth");
|
||||
CBS_FIELDS.put("client.gender", "Gender");
|
||||
CBS_FIELDS.put("client.pan", "PAN");
|
||||
CBS_FIELDS.put("client.aadhaarMasked", "Aadhaar (masked)");
|
||||
CBS_FIELDS.put("client.ckycNumber", "CKYC number");
|
||||
CBS_FIELDS.put("client.mobile", "Mobile number");
|
||||
CBS_FIELDS.put("client.email", "Email");
|
||||
CBS_FIELDS.put("client.addressLine1", "Address line 1");
|
||||
CBS_FIELDS.put("client.addressLine2", "Address line 2");
|
||||
CBS_FIELDS.put("client.city", "City / town");
|
||||
CBS_FIELDS.put("client.state", "State");
|
||||
CBS_FIELDS.put("client.pincode", "PIN code");
|
||||
CBS_FIELDS.put("client.cif", "CIF / client account number");
|
||||
CBS_FIELDS.put("client.id", "Client id");
|
||||
CBS_FIELDS.put("client.branch", "Branch name");
|
||||
CBS_FIELDS.put("enquiry.reference", "Enquiry reference number");
|
||||
CBS_FIELDS.put("enquiry.date", "Enquiry date (business date)");
|
||||
CBS_FIELDS.put("enquiry.amount", "Loan amount applied for");
|
||||
CBS_FIELDS.put("enquiry.purposeCode", "Enquiry purpose code");
|
||||
CBS_FIELDS.put("enquiry.purpose", "Enquiry purpose description");
|
||||
CBS_FIELDS.put("enquiry.applicationNo", "Loan application number");
|
||||
CBS_FIELDS.put("bureau.memberCode", "Bank's member code at the bureau");
|
||||
CBS_FIELDS.put("bureau.username", "Bureau user id");
|
||||
}
|
||||
|
||||
/** Normalised result fields a RESPONSE mapping can fill. */
|
||||
public static final Map<String, String> RESULT_FIELDS = new LinkedHashMap<>();
|
||||
static {
|
||||
RESULT_FIELDS.put("hit", "Record found (true/false)");
|
||||
RESULT_FIELDS.put("reportId", "Bureau report / control number");
|
||||
RESULT_FIELDS.put("score", "Credit score");
|
||||
RESULT_FIELDS.put("scoreVersion", "Score model / version");
|
||||
RESULT_FIELDS.put("totalAccounts", "Total accounts");
|
||||
RESULT_FIELDS.put("activeAccounts", "Active accounts");
|
||||
RESULT_FIELDS.put("overdueAccounts", "Overdue accounts");
|
||||
RESULT_FIELDS.put("writtenOffAccounts", "Written-off / settled accounts");
|
||||
RESULT_FIELDS.put("totalOutstanding", "Total outstanding");
|
||||
RESULT_FIELDS.put("totalOverdue", "Total overdue");
|
||||
RESULT_FIELDS.put("enquiriesLast6Months", "Enquiries in last 6 months");
|
||||
RESULT_FIELDS.put("maxDpd", "Highest days past due");
|
||||
RESULT_FIELDS.put("accounts", "Account list (array)");
|
||||
RESULT_FIELDS.put("enquiries", "Enquiry list (array)");
|
||||
RESULT_FIELDS.put("errorCode", "Bureau error code");
|
||||
RESULT_FIELDS.put("errorMessage", "Bureau error message");
|
||||
}
|
||||
|
||||
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_YYYY_MM_DD", "Date yyyy-MM-dd");
|
||||
TRANSFORMS.put("GENDER_MF", "Gender M / F / T");
|
||||
TRANSFORMS.put("GENDER_CODE", "Gender code 1=F 2=M 3=T");
|
||||
TRANSFORMS.put("STATE_CODE", "State to 2-digit code");
|
||||
TRANSFORMS.put("AMOUNT_INTEGER", "Amount, whole rupees");
|
||||
TRANSFORMS.put("NUMBER", "Number");
|
||||
TRANSFORMS.put("BOOLEAN", "true / false");
|
||||
}
|
||||
|
||||
/** Fields masked wherever a request is stored or shown. */
|
||||
public static final Set<String> SENSITIVE_FIELDS = Set.of("client.pan", "client.mobile", "client.email", "client.aadhaarMasked");
|
||||
|
||||
private static final Map<String, String> STATE_CODES = new LinkedHashMap<>();
|
||||
static {
|
||||
String[][] states = { { "JAMMU AND KASHMIR", "01" }, { "HIMACHAL PRADESH", "02" }, { "PUNJAB", "03" }, { "CHANDIGARH", "04" },
|
||||
{ "UTTARAKHAND", "05" }, { "UTTARANCHAL", "05" }, { "HARYANA", "06" }, { "DELHI", "07" }, { "RAJASTHAN", "08" },
|
||||
{ "UTTAR PRADESH", "09" }, { "BIHAR", "10" }, { "SIKKIM", "11" }, { "ARUNACHAL PRADESH", "12" }, { "NAGALAND", "13" },
|
||||
{ "MANIPUR", "14" }, { "MIZORAM", "15" }, { "TRIPURA", "16" }, { "MEGHALAYA", "17" }, { "ASSAM", "18" },
|
||||
{ "WEST BENGAL", "19" }, { "JHARKHAND", "20" }, { "ODISHA", "21" }, { "ORISSA", "21" }, { "CHHATTISGARH", "22" },
|
||||
{ "MADHYA PRADESH", "23" }, { "GUJARAT", "24" }, { "DAMAN AND DIU", "25" }, { "DADRA AND NAGAR HAVELI", "26" },
|
||||
{ "MAHARASHTRA", "27" }, { "KARNATAKA", "29" }, { "GOA", "30" }, { "LAKSHADWEEP", "31" }, { "KERALA", "32" },
|
||||
{ "TAMIL NADU", "33" }, { "PUDUCHERRY", "34" }, { "PONDICHERRY", "34" }, { "ANDAMAN AND NICOBAR ISLANDS", "35" },
|
||||
{ "TELANGANA", "36" }, { "ANDHRA PRADESH", "37" }, { "LADAKH", "38" } };
|
||||
for (final String[] s : states) {
|
||||
STATE_CODES.put(s[0], s[1]);
|
||||
}
|
||||
}
|
||||
|
||||
private static final DateTimeFormatter ISO = DateTimeFormatter.ISO_LOCAL_DATE;
|
||||
|
||||
/** Applies a REQUEST transform to a CBS value; null stays null. */
|
||||
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;
|
||||
}
|
||||
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 "DATE_DDMMYYYY":
|
||||
return date(s, "ddMMyyyy");
|
||||
case "DATE_DD_MM_YYYY":
|
||||
return date(s, "dd-MM-yyyy");
|
||||
case "DATE_YYYY_MM_DD":
|
||||
return date(s, "yyyy-MM-dd");
|
||||
case "GENDER_MF": {
|
||||
final String g = s.toUpperCase(Locale.ROOT);
|
||||
return new JsonPrimitive(g.startsWith("F") ? "F" : g.startsWith("M") ? "M" : "T");
|
||||
}
|
||||
case "GENDER_CODE": {
|
||||
final String g = s.toUpperCase(Locale.ROOT);
|
||||
return new JsonPrimitive(g.startsWith("F") ? "1" : g.startsWith("M") ? "2" : "3");
|
||||
}
|
||||
case "STATE_CODE": {
|
||||
final String key = s.toUpperCase(Locale.ROOT).replace("&", "AND").replaceAll("\\s+", " ").trim();
|
||||
final String code = STATE_CODES.get(key);
|
||||
return new JsonPrimitive(code != null ? code : s);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private static JsonElement date(final String iso, final String pattern) {
|
||||
try {
|
||||
return new JsonPrimitive(LocalDate.parse(iso.length() > 10 ? iso.substring(0, 10) : iso, ISO).format(DateTimeFormatter.ofPattern(pattern)));
|
||||
} catch (final RuntimeException e) {
|
||||
return new JsonPrimitive(iso);
|
||||
}
|
||||
}
|
||||
|
||||
/** Converts a value read from a bureau response for a RESPONSE mapping; 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 {
|
||||
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)
|
||||
|| "HIT".equalsIgnoreCase(s));
|
||||
case "UPPER":
|
||||
return new JsonPrimitive(s.toUpperCase(Locale.ROOT));
|
||||
case "LOWER":
|
||||
return new JsonPrimitive(s.toLowerCase(Locale.ROOT));
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
} catch (final RuntimeException e) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/** ABCDE1234F -> ABxxxx234F style masking for logs and previews. */
|
||||
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);
|
||||
}
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* 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.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* The client data a bureau enquiry is built from, as a flat map keyed by the CBS field names in
|
||||
* {@link CicCatalog#CBS_FIELDS} (client.pan, client.dob ...).
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CicClientProfileService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
public Map<String, Object> load(final Long clientId) {
|
||||
final List<Map<String, Object>> rows = jdbcTemplate.queryForList("""
|
||||
SELECT c.id, c.firstname, c.middlename, c.lastname, c.fullname, c.display_name, c.date_of_birth,
|
||||
g.code_value AS gender, c.mobile_no, c.email_address, c.account_no, c.status_enum, c.office_id,
|
||||
o.name AS office_name,
|
||||
(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_masked,
|
||||
(SELECT k.ckyc_number FROM m_client_kyc k WHERE k.client_id = c.id AND k.ckyc_number IS NOT NULL
|
||||
ORDER BY k.id DESC LIMIT 1) AS ckyc_number
|
||||
FROM m_client c
|
||||
LEFT JOIN m_code_value g ON g.id = c.gender_cv_id
|
||||
LEFT JOIN m_office o ON o.id = c.office_id
|
||||
WHERE c.id = ?
|
||||
""", clientId);
|
||||
if (rows.isEmpty()) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.cic.client.not.found", "Client " + clientId + " was not found.");
|
||||
}
|
||||
final Map<String, Object> c = rows.get(0);
|
||||
final Map<String, Object> ctx = new LinkedHashMap<>();
|
||||
ctx.put("client.id", c.get("id"));
|
||||
String first = str(c.get("firstname"));
|
||||
String last = str(c.get("lastname"));
|
||||
final String display = str(c.get("display_name"));
|
||||
if (first == null && last == null && display != null) {
|
||||
// An entity or a client captured with a single full name: split it the way bureaus expect.
|
||||
final String[] parts = display.trim().split("\\s+");
|
||||
first = parts[0];
|
||||
last = parts.length > 1 ? parts[parts.length - 1] : null;
|
||||
}
|
||||
ctx.put("client.firstName", first);
|
||||
ctx.put("client.middleName", str(c.get("middlename")));
|
||||
ctx.put("client.lastName", last);
|
||||
ctx.put("client.fullName", display != null ? display : str(c.get("fullname")));
|
||||
ctx.put("client.dob", c.get("date_of_birth") == null ? null : c.get("date_of_birth").toString());
|
||||
ctx.put("client.gender", str(c.get("gender")));
|
||||
ctx.put("client.pan", str(c.get("pan")));
|
||||
ctx.put("client.aadhaarMasked", str(c.get("aadhaar_masked")));
|
||||
ctx.put("client.ckycNumber", str(c.get("ckyc_number")));
|
||||
ctx.put("client.mobile", str(c.get("mobile_no")));
|
||||
ctx.put("client.email", str(c.get("email_address")));
|
||||
ctx.put("client.cif", str(c.get("account_no")));
|
||||
ctx.put("client.branch", str(c.get("office_name")));
|
||||
ctx.put("client.officeId", c.get("office_id"));
|
||||
ctx.put("client.status", c.get("status_enum"));
|
||||
|
||||
try {
|
||||
final List<Map<String, Object>> addresses = jdbcTemplate.queryForList("""
|
||||
SELECT a.address_line_1, a.address_line_2, a.street, a.town_village, a.city, a.postal_code,
|
||||
sp.code_value AS state_name
|
||||
FROM m_client_address ca
|
||||
JOIN m_address a ON a.id = ca.address_id
|
||||
LEFT JOIN m_code_value sp ON sp.id = a.state_province_id
|
||||
WHERE ca.client_id = ? AND (ca.is_active IS NULL OR ca.is_active = TRUE)
|
||||
ORDER BY ca.id
|
||||
LIMIT 1
|
||||
""", clientId);
|
||||
if (!addresses.isEmpty()) {
|
||||
final Map<String, Object> a = addresses.get(0);
|
||||
ctx.put("client.addressLine1", first(str(a.get("address_line_1")), str(a.get("street"))));
|
||||
ctx.put("client.addressLine2", str(a.get("address_line_2")));
|
||||
ctx.put("client.city", first(str(a.get("city")), str(a.get("town_village"))));
|
||||
ctx.put("client.state", str(a.get("state_name")));
|
||||
ctx.put("client.pincode", str(a.get("postal_code")));
|
||||
}
|
||||
} catch (final RuntimeException e) {
|
||||
// A tenant whose address tables differ simply sends no address; the enquiry still works on PAN + DOB.
|
||||
log.warn("CIC: could not read the address of client {}: {}", clientId, e.getMessage());
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
private static String str(final Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
final String s = value.toString().trim();
|
||||
return s.isEmpty() ? null : s;
|
||||
}
|
||||
|
||||
private static String first(final String a, final String b) {
|
||||
return a != null ? a : b;
|
||||
}
|
||||
}
|
||||
+454
@@ -0,0 +1,454 @@
|
||||
/**
|
||||
* 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.math.BigDecimal;
|
||||
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("reportValidityDays", row.get("report_validity_days"));
|
||||
v.put("enquiryCharge", row.get("enquiry_charge"));
|
||||
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("enquiriesLast30Days", jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(*) FROM m_cic_enquiry WHERE bureau_id = ? AND requested_on >= ?", Integer.class, id,
|
||||
Timestamp.from(Instant.now().minusSeconds(30L * 86400))));
|
||||
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 int validity = intOf(in, "reportValidityDays", ((Number) row.get("report_validity_days")).intValue());
|
||||
if (validity < 0 || validity > 365) {
|
||||
throw fail("error.msg.cic.validity.invalid", "Report reuse window must be between 0 and 365 days (0 = always pull fresh).");
|
||||
}
|
||||
final BigDecimal charge = in.has("enquiryCharge") && !in.get("enquiryCharge").isJsonNull()
|
||||
? in.get("enquiryCharge").getAsBigDecimal()
|
||||
: (BigDecimal) row.get("enquiry_charge");
|
||||
if (charge != null && charge.signum() < 0) {
|
||||
throw fail("error.msg.cic.charge.invalid", "Enquiry charge cannot be negative.");
|
||||
}
|
||||
|
||||
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 = ?,
|
||||
report_validity_days = ?, enquiry_charge = ?, 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, validity, charge,
|
||||
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 it would fail every enquiry 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 enquiries may run against 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; enquiries 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.");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
/**
|
||||
* 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.JsonObject;
|
||||
import java.sql.Date;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
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 borrower's consent to a credit bureau pull. No enquiry runs without an active, unexpired consent for the client
|
||||
* (for that bureau or for any bureau), and the consent used is recorded on the enquiry.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CicConsentService {
|
||||
|
||||
public static final String DEFAULT_TEXT = "I/We hereby authorise the Bank to obtain my/our credit information report from any Credit"
|
||||
+ " Information Company (CIBIL, Experian, Equifax, CRIF High Mark) for the purpose stated, to share my/our details with"
|
||||
+ " them for this purpose, and to use the report to assess my/our application. This consent is given voluntarily.";
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final CicClientProfileService clientProfileService;
|
||||
|
||||
@Transactional
|
||||
public Map<String, Object> create(final JsonObject in, final AppUser user, final String ipAddress) {
|
||||
final Long clientId = in.has("clientId") && !in.get("clientId").isJsonNull() ? in.get("clientId").getAsLong() : null;
|
||||
if (clientId == null) {
|
||||
throw CicConfigService.fail("error.msg.cic.consent.client.required", "Client is required.");
|
||||
}
|
||||
final Map<String, Object> client = clientProfileService.load(clientId);
|
||||
final Long bureauId = in.has("bureauId") && !in.get("bureauId").isJsonNull() ? in.get("bureauId").getAsLong() : null;
|
||||
if (bureauId != null && jdbcTemplate.queryForObject("SELECT COUNT(*) FROM m_cic_bureau WHERE id = ?", Integer.class, bureauId) == 0) {
|
||||
throw CicConfigService.fail("error.msg.cic.bureau.not.found", "Credit bureau " + bureauId + " was not found.");
|
||||
}
|
||||
final String purpose = StringUtils.defaultIfBlank(CicConfigService.text(in, "purpose", "NEW_LOAN"), "NEW_LOAN").toUpperCase();
|
||||
if (!CicCatalog.PURPOSES.containsKey(purpose)) {
|
||||
throw CicConfigService.fail("error.msg.cic.purpose.invalid", "Unknown purpose '" + purpose + "'.");
|
||||
}
|
||||
final String mode = StringUtils.defaultIfBlank(CicConfigService.text(in, "consentMode", "SIGNED_FORM"), "SIGNED_FORM").toUpperCase();
|
||||
if (!CicCatalog.CONSENT_MODES.contains(mode)) {
|
||||
throw CicConfigService.fail("error.msg.cic.consent.mode.invalid", "Consent mode must be one of " + CicCatalog.CONSENT_MODES + ".");
|
||||
}
|
||||
final String documentRef = StringUtils.trimToNull(CicConfigService.text(in, "documentRef", null));
|
||||
final String otpReference = StringUtils.trimToNull(CicConfigService.text(in, "otpReference", null));
|
||||
if ("SIGNED_FORM".equals(mode) && documentRef == null) {
|
||||
throw CicConfigService.fail("error.msg.cic.consent.document.required",
|
||||
"A signed consent form needs its document reference (form number or uploaded document name).");
|
||||
}
|
||||
if ("OTP".equals(mode) && otpReference == null) {
|
||||
throw CicConfigService.fail("error.msg.cic.consent.otp.required", "An OTP consent needs the OTP / transaction reference.");
|
||||
}
|
||||
final boolean declared = in.has("customerDeclarationAccepted") && in.get("customerDeclarationAccepted").getAsBoolean();
|
||||
if (!declared) {
|
||||
throw CicConfigService.fail("error.msg.cic.consent.declaration.required",
|
||||
"Confirm that the customer has read and accepted the consent declaration.");
|
||||
}
|
||||
final int validityDays = in.has("validityDays") && !in.get("validityDays").isJsonNull() ? in.get("validityDays").getAsInt() : 30;
|
||||
if (validityDays < 1 || validityDays > 365) {
|
||||
throw CicConfigService.fail("error.msg.cic.consent.validity.invalid", "Consent validity must be between 1 and 365 days.");
|
||||
}
|
||||
final LocalDate from = DateUtils.getBusinessLocalDate();
|
||||
final LocalDate until = from.plusDays(validityDays - 1L);
|
||||
final String ref = "CNS-" + from.format(DateTimeFormatter.BASIC_ISO_DATE) + "-"
|
||||
+ String.format("%06d", ThreadLocalRandom.current().nextInt(1_000_000));
|
||||
final String consentText = StringUtils.defaultIfBlank(CicConfigService.text(in, "consentText", null), DEFAULT_TEXT);
|
||||
|
||||
jdbcTemplate.update("""
|
||||
INSERT INTO m_cic_consent (consent_ref, client_id, client_name, application_no, bureau_id, purpose, consent_mode, consent_text,
|
||||
document_ref, otp_reference, valid_from, valid_until, status, captured_by, captured_by_name,
|
||||
captured_on, ip_address)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'ACTIVE', ?, ?, ?, ?)
|
||||
""", ref, clientId, client.get("client.fullName"), StringUtils.trimToNull(CicConfigService.text(in, "applicationNo", null)),
|
||||
bureauId, purpose, mode, consentText, documentRef, otpReference, Date.valueOf(from), Date.valueOf(until), user.getId(),
|
||||
user.getDisplayName(), Timestamp.from(Instant.now()), StringUtils.abbreviate(ipAddress, 64));
|
||||
final Long id = jdbcTemplate.queryForObject("SELECT id FROM m_cic_consent WHERE consent_ref = ?", Long.class, ref);
|
||||
return get(id);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Map<String, Object> revoke(final Long id, final String reason, final AppUser user) {
|
||||
final Map<String, Object> c = get(id);
|
||||
if (!"ACTIVE".equals(c.get("status"))) {
|
||||
throw CicConfigService.fail("error.msg.cic.consent.not.active", "Only an active consent can be revoked (this one is " + c.get("status") + ").");
|
||||
}
|
||||
if (StringUtils.isBlank(reason)) {
|
||||
throw CicConfigService.fail("error.msg.cic.consent.reason.required", "Give the reason for revoking the consent.");
|
||||
}
|
||||
jdbcTemplate.update("UPDATE m_cic_consent SET status = 'REVOKED', revoked_reason = ?, revoked_by = ?, revoked_on = ? WHERE id = ?",
|
||||
StringUtils.abbreviate(reason.trim(), 500), user.getId(), Timestamp.from(Instant.now()), id);
|
||||
return get(id);
|
||||
}
|
||||
|
||||
public Map<String, Object> get(final Long id) {
|
||||
final List<Map<String, Object>> rows = jdbcTemplate.queryForList(
|
||||
"SELECT c.*, b.code AS bureau_code FROM m_cic_consent c LEFT JOIN m_cic_bureau b ON b.id = c.bureau_id WHERE c.id = ?", id);
|
||||
if (rows.isEmpty()) {
|
||||
throw CicConfigService.fail("error.msg.cic.consent.not.found", "Consent " + id + " was not found.");
|
||||
}
|
||||
return view(rows.get(0));
|
||||
}
|
||||
|
||||
public Map<String, Object> list(final Long clientId, final String status, final String search, final int offset, final int limit) {
|
||||
final StringBuilder where = new StringBuilder(" WHERE 1 = 1");
|
||||
final List<Object> args = new ArrayList<>();
|
||||
if (clientId != null) {
|
||||
where.append(" AND c.client_id = ?");
|
||||
args.add(clientId);
|
||||
}
|
||||
final LocalDate today = DateUtils.getBusinessLocalDate();
|
||||
if (StringUtils.isNotBlank(status)) {
|
||||
switch (status.toUpperCase()) {
|
||||
case "ACTIVE" -> {
|
||||
where.append(" AND c.status = 'ACTIVE' AND c.valid_until >= ?");
|
||||
args.add(Date.valueOf(today));
|
||||
}
|
||||
case "EXPIRED" -> {
|
||||
where.append(" AND c.status = 'ACTIVE' AND c.valid_until < ?");
|
||||
args.add(Date.valueOf(today));
|
||||
}
|
||||
default -> {
|
||||
where.append(" AND c.status = ?");
|
||||
args.add(status.toUpperCase());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (StringUtils.isNotBlank(search)) {
|
||||
where.append(" AND (LOWER(c.client_name) LIKE ? OR LOWER(c.consent_ref) LIKE ? OR LOWER(c.application_no) LIKE ?)");
|
||||
final String like = "%" + search.trim().toLowerCase() + "%";
|
||||
args.add(like);
|
||||
args.add(like);
|
||||
args.add(like);
|
||||
}
|
||||
final Integer total = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM m_cic_consent c" + where, Integer.class, args.toArray());
|
||||
final List<Object> pageArgs = new ArrayList<>(args);
|
||||
pageArgs.add(limit);
|
||||
pageArgs.add(offset);
|
||||
final List<Map<String, Object>> items = new ArrayList<>();
|
||||
for (final Map<String, Object> row : jdbcTemplate.queryForList(
|
||||
"SELECT c.*, b.code AS bureau_code FROM m_cic_consent c LEFT JOIN m_cic_bureau b ON b.id = c.bureau_id" + where
|
||||
+ " ORDER BY c.id DESC LIMIT ? OFFSET ?",
|
||||
pageArgs.toArray())) {
|
||||
items.add(view(row));
|
||||
}
|
||||
final Map<String, Object> res = new LinkedHashMap<>();
|
||||
res.put("totalFilteredRecords", total);
|
||||
res.put("pageItems", items);
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* The consent an enquiry runs under: the one named, or the latest active, unexpired consent of this client that
|
||||
* covers this bureau (a consent with no bureau covers every bureau).
|
||||
*/
|
||||
public Map<String, Object> requireValid(final Long clientId, final Long bureauId, final Long consentId) {
|
||||
final LocalDate today = DateUtils.getBusinessLocalDate();
|
||||
final List<Map<String, Object>> rows;
|
||||
if (consentId != null) {
|
||||
rows = jdbcTemplate.queryForList("SELECT * FROM m_cic_consent WHERE id = ?", consentId);
|
||||
if (rows.isEmpty()) {
|
||||
throw CicConfigService.fail("error.msg.cic.consent.not.found", "Consent " + consentId + " was not found.");
|
||||
}
|
||||
final Map<String, Object> c = rows.get(0);
|
||||
if (((Number) c.get("client_id")).longValue() != clientId) {
|
||||
throw CicConfigService.fail("error.msg.cic.consent.other.client", "Consent " + c.get("consent_ref") + " belongs to another client.");
|
||||
}
|
||||
if (c.get("bureau_id") != null && ((Number) c.get("bureau_id")).longValue() != bureauId) {
|
||||
throw CicConfigService.fail("error.msg.cic.consent.other.bureau", "Consent " + c.get("consent_ref") + " was given for another bureau.");
|
||||
}
|
||||
if (!"ACTIVE".equals(c.get("status")) || ((Date) c.get("valid_until")).toLocalDate().isBefore(today)) {
|
||||
throw CicConfigService.fail("error.msg.cic.consent.not.valid", "Consent " + c.get("consent_ref") + " is revoked or expired.");
|
||||
}
|
||||
return c;
|
||||
}
|
||||
rows = jdbcTemplate.queryForList("""
|
||||
SELECT * FROM m_cic_consent
|
||||
WHERE client_id = ? AND status = 'ACTIVE' AND valid_until >= ? AND (bureau_id IS NULL OR bureau_id = ?)
|
||||
ORDER BY id DESC LIMIT 1
|
||||
""", clientId, Date.valueOf(today), bureauId);
|
||||
if (rows.isEmpty()) {
|
||||
throw CicConfigService.fail("error.msg.cic.consent.missing",
|
||||
"No valid credit bureau consent is on record for this customer. Capture the customer's consent first.");
|
||||
}
|
||||
return rows.get(0);
|
||||
}
|
||||
|
||||
private static Map<String, Object> view(final Map<String, Object> row) {
|
||||
final Map<String, Object> v = new LinkedHashMap<>();
|
||||
v.put("id", row.get("id"));
|
||||
v.put("consentRef", row.get("consent_ref"));
|
||||
v.put("clientId", row.get("client_id"));
|
||||
v.put("clientName", row.get("client_name"));
|
||||
v.put("applicationNo", row.get("application_no"));
|
||||
v.put("bureauId", row.get("bureau_id"));
|
||||
v.put("bureauCode", row.get("bureau_code") == null ? "ANY" : row.get("bureau_code"));
|
||||
v.put("purpose", row.get("purpose"));
|
||||
v.put("consentMode", row.get("consent_mode"));
|
||||
v.put("consentText", row.get("consent_text"));
|
||||
v.put("documentRef", row.get("document_ref"));
|
||||
v.put("otpReference", row.get("otp_reference"));
|
||||
v.put("validFrom", row.get("valid_from") == null ? null : row.get("valid_from").toString());
|
||||
v.put("validUntil", row.get("valid_until") == null ? null : row.get("valid_until").toString());
|
||||
String status = (String) row.get("status");
|
||||
if ("ACTIVE".equals(status) && row.get("valid_until") instanceof Date d && d.toLocalDate().isBefore(DateUtils.getBusinessLocalDate())) {
|
||||
status = "EXPIRED";
|
||||
}
|
||||
v.put("status", status);
|
||||
v.put("revokedReason", row.get("revoked_reason"));
|
||||
v.put("revokedOn", row.get("revoked_on") == null ? null : row.get("revoked_on").toString());
|
||||
v.put("capturedByName", row.get("captured_by_name"));
|
||||
v.put("capturedOn", row.get("captured_on") == null ? null : row.get("captured_on").toString());
|
||||
v.put("ipAddress", row.get("ip_address"));
|
||||
return v;
|
||||
}
|
||||
}
|
||||
+615
@@ -0,0 +1,615 @@
|
||||
/**
|
||||
* 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.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import java.math.BigDecimal;
|
||||
import java.sql.Date;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Runs credit bureau enquiries: builds the bureau request from the client's data through the bureau's REQUEST
|
||||
* mapping, enforces whitelist and consent, reuses a recent report when allowed, calls the bureau (or the simulator),
|
||||
* reads the result through the RESPONSE mapping, and logs every enquiry with its masked request and raw response.
|
||||
*
|
||||
* Deliberately not @Transactional: the enquiry row is written once, at the end, whatever the outcome, so a failed
|
||||
* call is on record as FAILED rather than rolled away.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CicEnquiryService {
|
||||
|
||||
private static final Pattern PLACEHOLDER = Pattern.compile("\\{\\{\\s*([A-Za-z0-9_.]+)\\s*}}");
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final CicConfigService configService;
|
||||
private final CicConsentService consentService;
|
||||
private final CicClientProfileService clientProfileService;
|
||||
private final CicHttpGateway gateway;
|
||||
private final Gson gson = new GsonBuilder().serializeNulls().create();
|
||||
|
||||
/** A bureau request as it would be sent. */
|
||||
public record BuiltRequest(String url, String method, String contentType, Map<String, String> headers, JsonObject body,
|
||||
JsonObject maskedBody, List<String> missingMandatory) {}
|
||||
|
||||
// ── Request building ──────────────────────────────────────────────────────
|
||||
|
||||
private Map<String, Object> context(final Map<String, Object> bureau, final Long clientId, final BigDecimal amount, final String purpose,
|
||||
final String applicationNo, final String reference) {
|
||||
final Map<String, Object> ctx = new LinkedHashMap<>(clientProfileService.load(clientId));
|
||||
ctx.put("enquiry.reference", reference);
|
||||
ctx.put("enquiry.date", DateUtils.getBusinessLocalDate().toString());
|
||||
ctx.put("enquiry.amount", amount);
|
||||
ctx.put("enquiry.purposeCode", purpose);
|
||||
ctx.put("enquiry.purpose", CicCatalog.PURPOSES.get(purpose));
|
||||
ctx.put("enquiry.applicationNo", applicationNo);
|
||||
ctx.put("bureau.memberCode", bureau.get("member_code"));
|
||||
ctx.put("bureau.username", bureau.get("username"));
|
||||
return ctx;
|
||||
}
|
||||
|
||||
public BuiltRequest build(final Map<String, Object> bureau, final Map<String, Object> endpoint, final Map<String, Object> ctx) {
|
||||
final Long endpointId = ((Number) endpoint.get("id")).longValue();
|
||||
final JsonElement base = JsonParser.parseString(StringUtils.defaultIfBlank((String) endpoint.get("base_body"), "{}"));
|
||||
final JsonObject body = base.isJsonObject() ? base.getAsJsonObject() : new JsonObject();
|
||||
final JsonObject masked = body.deepCopy();
|
||||
final List<String> missing = new ArrayList<>();
|
||||
for (final Map<String, Object> m : configService.mappingRows(endpointId, "REQUEST")) {
|
||||
final String source = (String) m.get("source_field");
|
||||
final String target = (String) m.get("target_field");
|
||||
Object raw = source == null ? null : ctx.get(source);
|
||||
if ((raw == null || raw.toString().isBlank()) && m.get("default_value") != null) {
|
||||
raw = m.get("default_value");
|
||||
}
|
||||
JsonElement value;
|
||||
try {
|
||||
value = CicCatalog.applyRequestTransform((String) m.get("transform"), raw);
|
||||
} catch (final RuntimeException e) {
|
||||
value = null;
|
||||
}
|
||||
if (value == null) {
|
||||
if (Boolean.TRUE.equals(m.get("is_mandatory"))) {
|
||||
missing.add(CicCatalog.CBS_FIELDS.getOrDefault(source, String.valueOf(source)) + " (" + target + ")");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
CicJsonPath.set(body, target, value);
|
||||
final boolean sensitive = source != null && CicCatalog.SENSITIVE_FIELDS.contains(source);
|
||||
CicJsonPath.set(masked, target, sensitive && value.isJsonPrimitive() ? new JsonPrimitive(CicCatalog.mask(value.getAsString())) : value);
|
||||
}
|
||||
final Map<String, String> headers = new LinkedHashMap<>();
|
||||
final JsonElement hj = JsonParser.parseString(StringUtils.defaultIfBlank((String) endpoint.get("headers_json"), "{}"));
|
||||
if (hj.isJsonObject()) {
|
||||
for (final Map.Entry<String, JsonElement> h : hj.getAsJsonObject().entrySet()) {
|
||||
headers.put(h.getKey(), resolvePlaceholders(h.getValue().getAsString(), ctx));
|
||||
}
|
||||
}
|
||||
final String url = CicHttpGateway.joinUrl((String) bureau.get("base_url"), (String) endpoint.get("path"));
|
||||
return new BuiltRequest(url, (String) endpoint.get("http_method"), (String) endpoint.get("content_type"), headers, body, masked,
|
||||
missing);
|
||||
}
|
||||
|
||||
private static String resolvePlaceholders(final String template, final Map<String, Object> ctx) {
|
||||
final Matcher m = PLACEHOLDER.matcher(template);
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
while (m.find()) {
|
||||
final Object v = ctx.get(m.group(1));
|
||||
m.appendReplacement(sb, Matcher.quoteReplacement(v == null ? "" : v.toString()));
|
||||
}
|
||||
m.appendTail(sb);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private Map<String, Object> activeEndpoint(final Map<String, Object> bureau, final String enquiryType) {
|
||||
final Map<String, Object> endpoint = configService.endpointRow(((Number) bureau.get("id")).longValue(), enquiryType);
|
||||
if (endpoint == null || !Boolean.TRUE.equals(endpoint.get("is_active"))) {
|
||||
throw CicConfigService.fail("error.msg.cic.endpoint.inactive",
|
||||
bureau.get("code") + " has no active " + enquiryType + " endpoint. Configure it under API Mapping.");
|
||||
}
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
/** Dry run: the request exactly as it would go to the bureau (sensitive values masked), with what is missing. */
|
||||
public Map<String, Object> preview(final Long bureauId, final JsonObject in) {
|
||||
final Map<String, Object> bureau = configService.bureauRow(bureauId);
|
||||
final Long clientId = longOf(in, "clientId");
|
||||
if (clientId == null) {
|
||||
throw CicConfigService.fail("error.msg.cic.client.required", "Choose a client for the preview.");
|
||||
}
|
||||
final String enquiryType = StringUtils.defaultIfBlank(CicConfigService.text(in, "enquiryType", "CONSUMER_ENQUIRY"), "CONSUMER_ENQUIRY")
|
||||
.toUpperCase();
|
||||
final String purpose = StringUtils.defaultIfBlank(CicConfigService.text(in, "purpose", "NEW_LOAN"), "NEW_LOAN").toUpperCase();
|
||||
final BigDecimal amount = in.has("loanAmount") && !in.get("loanAmount").isJsonNull() ? in.get("loanAmount").getAsBigDecimal()
|
||||
: new BigDecimal("100000");
|
||||
final Map<String, Object> endpoint = configService.endpointRow(bureauId, enquiryType);
|
||||
if (endpoint == null) {
|
||||
throw CicConfigService.fail("error.msg.cic.endpoint.missing", bureau.get("code") + " has no " + enquiryType + " endpoint yet.");
|
||||
}
|
||||
final Map<String, Object> ctx = context(bureau, clientId, amount, purpose, CicConfigService.text(in, "applicationNo", null),
|
||||
"PREVIEW-" + System.currentTimeMillis());
|
||||
final BuiltRequest req = build(bureau, endpoint, ctx);
|
||||
final Map<String, Object> res = new LinkedHashMap<>();
|
||||
res.put("environment", bureau.get("environment"));
|
||||
res.put("url", "SIMULATOR".equals(bureau.get("environment")) ? "(simulator - no network call)" : req.url());
|
||||
res.put("method", req.method());
|
||||
res.put("headers", req.headers());
|
||||
res.put("authentication", bureau.get("auth_type"));
|
||||
res.put("body", gson.toJson(req.maskedBody()));
|
||||
res.put("missingMandatory", req.missingMandatory());
|
||||
res.put("clientData", maskedContext(ctx));
|
||||
return res;
|
||||
}
|
||||
|
||||
private static Map<String, Object> maskedContext(final Map<String, Object> ctx) {
|
||||
final Map<String, Object> out = new LinkedHashMap<>();
|
||||
ctx.forEach((k, v) -> {
|
||||
if (k.startsWith("client.") && !"client.officeId".equals(k) && !"client.status".equals(k)) {
|
||||
out.put(k, v != null && CicCatalog.SENSITIVE_FIELDS.contains(k) ? CicCatalog.mask(v.toString()) : v);
|
||||
}
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
/** What the screens show about a client before an enquiry: the data that will go out, and the consent on file. */
|
||||
public Map<String, Object> clientProfile(final Long clientId, final Long bureauId) {
|
||||
final Map<String, Object> ctx = clientProfileService.load(clientId);
|
||||
final Map<String, Object> res = new LinkedHashMap<>();
|
||||
res.put("clientId", clientId);
|
||||
res.put("data", maskedContext(ctx));
|
||||
Map<String, Object> consent = null;
|
||||
if (bureauId != null) {
|
||||
try {
|
||||
consent = consentService.requireValid(clientId, bureauId, null);
|
||||
} catch (final RuntimeException e) {
|
||||
consent = null;
|
||||
}
|
||||
} else {
|
||||
final List<Map<String, Object>> rows = jdbcTemplate.queryForList("""
|
||||
SELECT * FROM m_cic_consent WHERE client_id = ? AND status = 'ACTIVE' AND valid_until >= ? ORDER BY id DESC LIMIT 1
|
||||
""", clientId, Date.valueOf(DateUtils.getBusinessLocalDate()));
|
||||
consent = rows.isEmpty() ? null : rows.get(0);
|
||||
}
|
||||
Map<String, Object> consentView = null;
|
||||
if (consent != null) {
|
||||
consentView = new LinkedHashMap<>();
|
||||
consentView.put("id", consent.get("id"));
|
||||
consentView.put("consentRef", consent.get("consent_ref"));
|
||||
consentView.put("validUntil", String.valueOf(consent.get("valid_until")));
|
||||
consentView.put("consentMode", consent.get("consent_mode"));
|
||||
consentView.put("bureauId", consent.get("bureau_id"));
|
||||
}
|
||||
res.put("validConsent", consentView);
|
||||
final List<Map<String, Object>> last = jdbcTemplate.queryForList("""
|
||||
SELECT e.id, e.enquiry_ref, e.bureau_code, e.status, e.score, e.requested_on FROM m_cic_enquiry e
|
||||
WHERE e.client_id = ? ORDER BY e.id DESC LIMIT 5
|
||||
""", clientId);
|
||||
final List<Map<String, Object>> recent = new ArrayList<>();
|
||||
for (final Map<String, Object> r : last) {
|
||||
final Map<String, Object> v = new LinkedHashMap<>();
|
||||
v.put("id", r.get("id"));
|
||||
v.put("enquiryRef", r.get("enquiry_ref"));
|
||||
v.put("bureauCode", r.get("bureau_code"));
|
||||
v.put("status", r.get("status"));
|
||||
v.put("score", r.get("score"));
|
||||
v.put("requestedOn", String.valueOf(r.get("requested_on")));
|
||||
recent.add(v);
|
||||
}
|
||||
res.put("recentEnquiries", recent);
|
||||
return res;
|
||||
}
|
||||
|
||||
// ── Enquiry ───────────────────────────────────────────────────────────────
|
||||
|
||||
public Map<String, Object> enquire(final JsonObject in, final AppUser user) {
|
||||
final Long clientId = longOf(in, "clientId");
|
||||
if (clientId == null) {
|
||||
throw CicConfigService.fail("error.msg.cic.client.required", "Choose the customer to enquire on.");
|
||||
}
|
||||
Long bureauId = longOf(in, "bureauId");
|
||||
if (bureauId == null) {
|
||||
final List<Long> def = jdbcTemplate.queryForList(
|
||||
"SELECT id FROM m_cic_bureau WHERE whitelisted = TRUE ORDER BY is_default DESC, id LIMIT 1", Long.class);
|
||||
if (def.isEmpty()) {
|
||||
throw CicConfigService.fail("error.msg.cic.none.whitelisted",
|
||||
"No credit bureau is whitelisted for this bank. Whitelist one under CIC Configuration.");
|
||||
}
|
||||
bureauId = def.get(0);
|
||||
}
|
||||
final Map<String, Object> bureau = configService.bureauRow(bureauId);
|
||||
if (!Boolean.TRUE.equals(bureau.get("whitelisted"))) {
|
||||
throw CicConfigService.fail("error.msg.cic.bureau.not.whitelisted",
|
||||
bureau.get("code") + " is not whitelisted for this bank. Only whitelisted bureaus can be enquired.");
|
||||
}
|
||||
configService.assertReadyForLive(bureau);
|
||||
|
||||
final String enquiryType = StringUtils.defaultIfBlank(CicConfigService.text(in, "enquiryType", "CONSUMER_ENQUIRY"), "CONSUMER_ENQUIRY")
|
||||
.toUpperCase();
|
||||
if (!CicCatalog.ENDPOINT_TYPES.contains(enquiryType)) {
|
||||
throw CicConfigService.fail("error.msg.cic.endpoint.type.invalid", "Enquiry type must be one of " + CicCatalog.ENDPOINT_TYPES + ".");
|
||||
}
|
||||
final String purpose = StringUtils.defaultIfBlank(CicConfigService.text(in, "purpose", "NEW_LOAN"), "NEW_LOAN").toUpperCase();
|
||||
if (!CicCatalog.PURPOSES.containsKey(purpose)) {
|
||||
throw CicConfigService.fail("error.msg.cic.purpose.invalid", "Unknown enquiry purpose '" + purpose + "'.");
|
||||
}
|
||||
final BigDecimal amount = in.has("loanAmount") && !in.get("loanAmount").isJsonNull() ? in.get("loanAmount").getAsBigDecimal() : null;
|
||||
if (amount == null || amount.signum() <= 0) {
|
||||
throw CicConfigService.fail("error.msg.cic.amount.required", "Enter the loan / credit amount the enquiry is for.");
|
||||
}
|
||||
final String applicationNo = StringUtils.trimToNull(CicConfigService.text(in, "applicationNo", null));
|
||||
final boolean forceFresh = in.has("forceFresh") && !in.get("forceFresh").isJsonNull() && in.get("forceFresh").getAsBoolean();
|
||||
|
||||
final Map<String, Object> consent = consentService.requireValid(clientId, bureauId, longOf(in, "consentId"));
|
||||
final Long consentId = ((Number) consent.get("id")).longValue();
|
||||
final Map<String, Object> endpoint = activeEndpoint(bureau, enquiryType);
|
||||
|
||||
final LocalDate today = DateUtils.getBusinessLocalDate();
|
||||
final String reference = "CIC" + today.format(DateTimeFormatter.ofPattern("yyMMdd"))
|
||||
+ String.format("%08d", ThreadLocalRandom.current().nextInt(100_000_000));
|
||||
final Map<String, Object> ctx = context(bureau, clientId, amount, purpose, applicationNo, reference);
|
||||
|
||||
final Row row = new Row();
|
||||
row.reference = reference;
|
||||
row.bureau = bureau;
|
||||
row.clientId = clientId;
|
||||
row.clientName = (String) ctx.get("client.fullName");
|
||||
row.officeId = ctx.get("client.officeId");
|
||||
row.applicationNo = applicationNo;
|
||||
row.consentId = consentId;
|
||||
row.enquiryType = enquiryType;
|
||||
row.purpose = purpose;
|
||||
row.amount = amount;
|
||||
row.user = user;
|
||||
|
||||
// Reuse a recent successful report of the same bureau instead of paying for, and leaving, another hard enquiry.
|
||||
final int validityDays = ((Number) bureau.get("report_validity_days")).intValue();
|
||||
if (!forceFresh && validityDays > 0) {
|
||||
final List<Map<String, Object>> recent = jdbcTemplate.queryForList("""
|
||||
SELECT * FROM m_cic_enquiry
|
||||
WHERE client_id = ? AND bureau_id = ? AND enquiry_type = ? AND status = 'SUCCESS' AND requested_on >= ?
|
||||
ORDER BY id DESC LIMIT 1
|
||||
""", clientId, bureauId, enquiryType, Timestamp.from(Instant.now().minusSeconds(validityDays * 86400L)));
|
||||
if (!recent.isEmpty()) {
|
||||
final Map<String, Object> src = recent.get(0);
|
||||
row.status = "REUSED";
|
||||
row.score = src.get("score") == null ? null : ((Number) src.get("score")).intValue();
|
||||
row.resultJson = (String) src.get("result_json");
|
||||
row.requestMasked = (String) src.get("request_masked");
|
||||
row.responseRaw = (String) src.get("response_raw");
|
||||
row.reusedFromId = ((Number) src.get("id")).longValue();
|
||||
row.charge = BigDecimal.ZERO;
|
||||
row.errorMessage = "Report " + src.get("enquiry_ref") + " of " + src.get("requested_on") + " reused (within " + validityDays
|
||||
+ " days). Pull fresh to override.";
|
||||
row.durationMs = 0;
|
||||
return insert(row);
|
||||
}
|
||||
}
|
||||
|
||||
final BuiltRequest req = build(bureau, endpoint, ctx);
|
||||
row.requestMasked = gson.toJson(Map.of("method", req.method(), "url", req.url(), "headers", req.headers(), "body", req.maskedBody()));
|
||||
row.charge = (BigDecimal) bureau.get("enquiry_charge");
|
||||
if (!req.missingMandatory().isEmpty()) {
|
||||
row.status = "FAILED";
|
||||
row.errorMessage = "Customer data missing for mandatory bureau fields: " + String.join(", ", req.missingMandatory());
|
||||
row.charge = BigDecimal.ZERO;
|
||||
row.durationMs = 0;
|
||||
return insert(row);
|
||||
}
|
||||
|
||||
// Call the bureau, or the simulator.
|
||||
final long started = System.currentTimeMillis();
|
||||
JsonElement response;
|
||||
if ("SIMULATOR".equals(bureau.get("environment"))) {
|
||||
response = CicSimulator.respond((String) bureau.get("code"), ctx, today, reference);
|
||||
row.httpStatus = 200;
|
||||
row.responseRaw = gson.toJson(response);
|
||||
} else {
|
||||
final CicHttpGateway.HttpResult r = gateway.send(bureau, req.method(), req.url(), req.headers(), req.contentType(),
|
||||
"GET".equalsIgnoreCase(req.method()) ? null : gson.toJson(req.body()));
|
||||
row.httpStatus = r.status();
|
||||
row.responseRaw = r.body();
|
||||
if (!r.ok()) {
|
||||
row.status = "FAILED";
|
||||
row.errorMessage = r.status() == 0 ? "Bureau not reachable: " + r.error()
|
||||
: "Bureau returned HTTP " + r.status() + ": " + StringUtils.abbreviate(StringUtils.defaultString(r.body()), 500);
|
||||
row.durationMs = (int) (System.currentTimeMillis() - started);
|
||||
return insert(row);
|
||||
}
|
||||
try {
|
||||
response = JsonParser.parseString(StringUtils.defaultIfBlank(r.body(), "{}"));
|
||||
} catch (final RuntimeException e) {
|
||||
row.status = "FAILED";
|
||||
row.errorMessage = "Bureau response is not valid JSON: " + e.getMessage();
|
||||
row.durationMs = (int) (System.currentTimeMillis() - started);
|
||||
return insert(row);
|
||||
}
|
||||
}
|
||||
row.durationMs = (int) (System.currentTimeMillis() - started);
|
||||
|
||||
final JsonObject result = parse(((Number) endpoint.get("id")).longValue(), response);
|
||||
row.resultJson = gson.toJson(result);
|
||||
row.score = result.has("score") && result.get("score").isJsonPrimitive() ? scoreOf(result.get("score")) : null;
|
||||
final boolean noHit = result.has("hit") && result.get("hit").isJsonPrimitive() && !truthy(result.get("hit"));
|
||||
if (result.has("errorCode") && !result.get("errorCode").isJsonNull() && StringUtils.isNotBlank(result.get("errorCode").getAsString())) {
|
||||
row.status = "FAILED";
|
||||
row.errorMessage = "Bureau error " + result.get("errorCode").getAsString()
|
||||
+ (result.has("errorMessage") && !result.get("errorMessage").isJsonNull() ? ": " + result.get("errorMessage").getAsString() : "");
|
||||
} else if (noHit) {
|
||||
row.status = "NO_HIT";
|
||||
row.errorMessage = "No record found at " + bureau.get("code") + " (new to credit).";
|
||||
} else {
|
||||
row.status = "SUCCESS";
|
||||
}
|
||||
return insert(row);
|
||||
}
|
||||
|
||||
private JsonObject parse(final Long endpointId, final JsonElement response) {
|
||||
final JsonObject result = new JsonObject();
|
||||
for (final Map<String, Object> m : configService.mappingRows(endpointId, "RESPONSE")) {
|
||||
JsonElement value = null;
|
||||
try {
|
||||
value = CicJsonPath.get(response, (String) m.get("source_field"));
|
||||
} catch (final RuntimeException e) {
|
||||
log.debug("CIC: response path {} unreadable: {}", m.get("source_field"), e.getMessage());
|
||||
}
|
||||
if (value == null && m.get("default_value") != null) {
|
||||
value = new JsonPrimitive((String) m.get("default_value"));
|
||||
}
|
||||
if (value != null) {
|
||||
result.add((String) m.get("target_field"), CicCatalog.applyResponseTransform((String) m.get("transform"), value));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Integer scoreOf(final JsonElement e) {
|
||||
try {
|
||||
final String digits = e.getAsString().replaceAll("[^0-9\\-]", "");
|
||||
return digits.isEmpty() ? null : Integer.valueOf(new BigDecimal(e.getAsString().replaceAll("[^0-9.\\-]", "")).intValue());
|
||||
} catch (final RuntimeException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean truthy(final JsonElement e) {
|
||||
final String s = e.getAsString();
|
||||
return "true".equalsIgnoreCase(s) || "1".equals(s) || "Y".equalsIgnoreCase(s) || "YES".equalsIgnoreCase(s) || "HIT".equalsIgnoreCase(s);
|
||||
}
|
||||
|
||||
/** Everything one enquiry row carries; written once, at the end. */
|
||||
private static final class Row {
|
||||
|
||||
private String reference;
|
||||
private Map<String, Object> bureau;
|
||||
private Long clientId;
|
||||
private String clientName;
|
||||
private Object officeId;
|
||||
private String applicationNo;
|
||||
private Long consentId;
|
||||
private String enquiryType;
|
||||
private String purpose;
|
||||
private BigDecimal amount;
|
||||
private String status;
|
||||
private Integer score;
|
||||
private Integer httpStatus;
|
||||
private String errorMessage;
|
||||
private String requestMasked;
|
||||
private String responseRaw;
|
||||
private String resultJson;
|
||||
private Long reusedFromId;
|
||||
private BigDecimal charge;
|
||||
private int durationMs;
|
||||
private AppUser user;
|
||||
}
|
||||
|
||||
private Map<String, Object> insert(final Row r) {
|
||||
jdbcTemplate.update("""
|
||||
INSERT INTO m_cic_enquiry (enquiry_ref, bureau_id, bureau_code, client_id, client_name, application_no, consent_id, enquiry_type,
|
||||
purpose, loan_amount, status, score, http_status, error_message, request_masked, response_raw,
|
||||
result_json, reused_from_id, charge_amount, environment, office_id, requested_by, requested_by_name,
|
||||
requested_on, duration_ms)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", r.reference, r.bureau.get("id"), r.bureau.get("code"), r.clientId, r.clientName, r.applicationNo, r.consentId,
|
||||
r.enquiryType, r.purpose, r.amount, r.status, r.score, r.httpStatus, StringUtils.abbreviate(r.errorMessage, 2000),
|
||||
r.requestMasked, r.responseRaw, r.resultJson, r.reusedFromId, r.charge, r.bureau.get("environment"),
|
||||
r.officeId instanceof Number n ? n.longValue() : null, r.user.getId(), r.user.getDisplayName(), Timestamp.from(Instant.now()),
|
||||
r.durationMs);
|
||||
final Long id = jdbcTemplate.queryForObject("SELECT id FROM m_cic_enquiry WHERE enquiry_ref = ?", Long.class, r.reference);
|
||||
log.info("CIC enquiry {} on client {} at {}: {} (score {})", r.reference, r.clientId, r.bureau.get("code"), r.status, r.score);
|
||||
return get(id);
|
||||
}
|
||||
|
||||
// ── Reads ────────────────────────────────────────────────────────────────
|
||||
|
||||
public Map<String, Object> get(final Long id) {
|
||||
final List<Map<String, Object>> rows = jdbcTemplate.queryForList("""
|
||||
SELECT e.*, c.consent_ref, b.name AS bureau_name, src.enquiry_ref AS reused_from_ref
|
||||
FROM m_cic_enquiry e
|
||||
LEFT JOIN m_cic_consent c ON c.id = e.consent_id
|
||||
LEFT JOIN m_cic_bureau b ON b.id = e.bureau_id
|
||||
LEFT JOIN m_cic_enquiry src ON src.id = e.reused_from_id
|
||||
WHERE e.id = ?
|
||||
""", id);
|
||||
if (rows.isEmpty()) {
|
||||
throw CicConfigService.fail("error.msg.cic.enquiry.not.found", "Enquiry " + id + " was not found.");
|
||||
}
|
||||
final Map<String, Object> v = summaryView(rows.get(0));
|
||||
final Map<String, Object> row = rows.get(0);
|
||||
v.put("consentRef", row.get("consent_ref"));
|
||||
v.put("bureauName", row.get("bureau_name"));
|
||||
v.put("reusedFromRef", row.get("reused_from_ref"));
|
||||
v.put("httpStatus", row.get("http_status"));
|
||||
v.put("requestMasked", row.get("request_masked"));
|
||||
v.put("responseRaw", row.get("response_raw"));
|
||||
v.put("result", row.get("result_json") == null ? null : JsonParser.parseString((String) row.get("result_json")));
|
||||
return v;
|
||||
}
|
||||
|
||||
private static Map<String, Object> summaryView(final Map<String, Object> row) {
|
||||
final Map<String, Object> v = new LinkedHashMap<>();
|
||||
v.put("id", row.get("id"));
|
||||
v.put("enquiryRef", row.get("enquiry_ref"));
|
||||
v.put("bureauId", row.get("bureau_id"));
|
||||
v.put("bureauCode", row.get("bureau_code"));
|
||||
v.put("clientId", row.get("client_id"));
|
||||
v.put("clientName", row.get("client_name"));
|
||||
v.put("applicationNo", row.get("application_no"));
|
||||
v.put("enquiryType", row.get("enquiry_type"));
|
||||
v.put("purpose", row.get("purpose"));
|
||||
v.put("loanAmount", row.get("loan_amount"));
|
||||
v.put("status", row.get("status"));
|
||||
v.put("score", row.get("score"));
|
||||
v.put("errorMessage", row.get("error_message"));
|
||||
v.put("chargeAmount", row.get("charge_amount"));
|
||||
v.put("environment", row.get("environment"));
|
||||
v.put("requestedByName", row.get("requested_by_name"));
|
||||
v.put("requestedOn", row.get("requested_on") == null ? null : row.get("requested_on").toString());
|
||||
v.put("durationMs", row.get("duration_ms"));
|
||||
return v;
|
||||
}
|
||||
|
||||
public Map<String, Object> list(final Long clientId, final Long bureauId, final String status, final String fromDate, final String toDate,
|
||||
final String search, final int offset, final int limit) {
|
||||
final StringBuilder where = new StringBuilder(" WHERE 1 = 1");
|
||||
final List<Object> args = new ArrayList<>();
|
||||
if (clientId != null) {
|
||||
where.append(" AND e.client_id = ?");
|
||||
args.add(clientId);
|
||||
}
|
||||
if (bureauId != null) {
|
||||
where.append(" AND e.bureau_id = ?");
|
||||
args.add(bureauId);
|
||||
}
|
||||
if (StringUtils.isNotBlank(status)) {
|
||||
where.append(" AND e.status = ?");
|
||||
args.add(status.trim().toUpperCase());
|
||||
}
|
||||
if (StringUtils.isNotBlank(fromDate)) {
|
||||
where.append(" AND e.requested_on >= ?");
|
||||
args.add(Timestamp.valueOf(LocalDate.parse(fromDate).atStartOfDay()));
|
||||
}
|
||||
if (StringUtils.isNotBlank(toDate)) {
|
||||
where.append(" AND e.requested_on < ?");
|
||||
args.add(Timestamp.valueOf(LocalDate.parse(toDate).plusDays(1).atStartOfDay()));
|
||||
}
|
||||
if (StringUtils.isNotBlank(search)) {
|
||||
where.append(" AND (LOWER(e.client_name) LIKE ? OR LOWER(e.enquiry_ref) LIKE ? OR LOWER(e.application_no) LIKE ?)");
|
||||
final String like = "%" + search.trim().toLowerCase() + "%";
|
||||
args.add(like);
|
||||
args.add(like);
|
||||
args.add(like);
|
||||
}
|
||||
final Integer total = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM m_cic_enquiry e" + where, Integer.class, args.toArray());
|
||||
final List<Object> pageArgs = new ArrayList<>(args);
|
||||
pageArgs.add(limit);
|
||||
pageArgs.add(offset);
|
||||
final List<Map<String, Object>> items = new ArrayList<>();
|
||||
for (final Map<String, Object> row : jdbcTemplate
|
||||
.queryForList("SELECT e.* FROM m_cic_enquiry e" + where + " ORDER BY e.id DESC LIMIT ? OFFSET ?", pageArgs.toArray())) {
|
||||
items.add(summaryView(row));
|
||||
}
|
||||
final Map<String, Object> res = new LinkedHashMap<>();
|
||||
res.put("totalFilteredRecords", total);
|
||||
res.put("pageItems", items);
|
||||
return res;
|
||||
}
|
||||
|
||||
public Map<String, Object> dashboard() {
|
||||
final Timestamp since30 = Timestamp.from(Instant.now().minusSeconds(30L * 86400));
|
||||
final Timestamp todayStart = Timestamp.valueOf(LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).atStartOfDay());
|
||||
final Map<String, Object> res = new LinkedHashMap<>();
|
||||
res.put("enquiriesToday", count("SELECT COUNT(*) FROM m_cic_enquiry WHERE requested_on >= ?", todayStart));
|
||||
res.put("enquiriesLast30Days", count("SELECT COUNT(*) FROM m_cic_enquiry WHERE requested_on >= ?", since30));
|
||||
final Map<String, Object> byStatus = new LinkedHashMap<>();
|
||||
for (final Map<String, Object> r : jdbcTemplate
|
||||
.queryForList("SELECT status, COUNT(*) AS n FROM m_cic_enquiry WHERE requested_on >= ? GROUP BY status", since30)) {
|
||||
byStatus.put((String) r.get("status"), r.get("n"));
|
||||
}
|
||||
res.put("byStatusLast30Days", byStatus);
|
||||
res.put("averageScoreLast30Days", jdbcTemplate.queryForObject(
|
||||
"SELECT AVG(score) FROM m_cic_enquiry WHERE status IN ('SUCCESS','REUSED') AND score IS NOT NULL AND requested_on >= ?",
|
||||
BigDecimal.class, since30));
|
||||
res.put("chargesLast30Days", jdbcTemplate.queryForObject(
|
||||
"SELECT COALESCE(SUM(charge_amount), 0) FROM m_cic_enquiry WHERE requested_on >= ?", BigDecimal.class, since30));
|
||||
final List<Map<String, Object>> byBureau = new ArrayList<>();
|
||||
for (final Map<String, Object> r : jdbcTemplate.queryForList("""
|
||||
SELECT b.id, b.code, b.name, b.whitelisted, b.environment, b.is_default,
|
||||
(SELECT COUNT(*) FROM m_cic_enquiry e WHERE e.bureau_id = b.id AND e.requested_on >= ?) AS enquiries,
|
||||
(SELECT COUNT(*) FROM m_cic_enquiry e WHERE e.bureau_id = b.id AND e.requested_on >= ? AND e.status = 'FAILED') AS failed
|
||||
FROM m_cic_bureau b ORDER BY b.is_default DESC, b.id
|
||||
""", since30, since30)) {
|
||||
final Map<String, Object> v = new LinkedHashMap<>();
|
||||
v.put("id", r.get("id"));
|
||||
v.put("code", r.get("code"));
|
||||
v.put("name", r.get("name"));
|
||||
v.put("whitelisted", r.get("whitelisted"));
|
||||
v.put("environment", r.get("environment"));
|
||||
v.put("isDefault", r.get("is_default"));
|
||||
v.put("enquiries", r.get("enquiries"));
|
||||
v.put("failed", r.get("failed"));
|
||||
byBureau.add(v);
|
||||
}
|
||||
res.put("bureaus", byBureau);
|
||||
res.put("activeConsents", count("SELECT COUNT(*) FROM m_cic_consent WHERE status = 'ACTIVE' AND valid_until >= ?",
|
||||
Date.valueOf(DateUtils.getBusinessLocalDate())));
|
||||
final List<Map<String, Object>> bands = new ArrayList<>();
|
||||
final int[][] ranges = { { 300, 549 }, { 550, 649 }, { 650, 699 }, { 700, 749 }, { 750, 900 } };
|
||||
final String[] labels = { "Poor (300-549)", "Fair (550-649)", "Good (650-699)", "Very good (700-749)", "Excellent (750+)" };
|
||||
for (int i = 0; i < ranges.length; i++) {
|
||||
final Map<String, Object> band = new LinkedHashMap<>();
|
||||
band.put("label", labels[i]);
|
||||
band.put("count", count("SELECT COUNT(*) FROM m_cic_enquiry WHERE status = 'SUCCESS' AND score BETWEEN ? AND ? AND requested_on >= ?",
|
||||
ranges[i][0], ranges[i][1], since30));
|
||||
bands.add(band);
|
||||
}
|
||||
res.put("scoreBandsLast30Days", bands);
|
||||
res.put("recent", list(null, null, null, null, null, null, 0, 10).get("pageItems"));
|
||||
return res;
|
||||
}
|
||||
|
||||
private Integer count(final String sql, final Object... args) {
|
||||
return jdbcTemplate.queryForObject(sql, Integer.class, args);
|
||||
}
|
||||
|
||||
private static Long longOf(final JsonObject in, final String key) {
|
||||
return in != null && in.has(key) && !in.get(key).isJsonNull() && !in.get(key).getAsString().isBlank() ? in.get(key).getAsLong() : null;
|
||||
}
|
||||
}
|
||||
+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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* 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.util.Map;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* Stand-in bureau for the SIMULATOR environment: answers in the shape of the seeded generic mapping, deterministically
|
||||
* per PAN (the same customer always gets the same report), so the whole enquiry flow can be exercised and trained on
|
||||
* before a bureau's real credentials are in place. Never used once the bureau is set to UAT or PRODUCTION.
|
||||
*/
|
||||
public final class CicSimulator {
|
||||
|
||||
private static final String[] LENDERS = { "STATE BANK OF INDIA", "HDFC BANK", "ICICI BANK", "AXIS BANK", "BAJAJ FINANCE",
|
||||
"PUNJAB NATIONAL BANK", "KOTAK MAHINDRA BANK", "MUTHOOT FINANCE" };
|
||||
private static final String[] TYPES = { "PERSONAL LOAN", "CREDIT CARD", "HOUSING LOAN", "AUTO LOAN", "GOLD LOAN", "CONSUMER LOAN",
|
||||
"KISAN CREDIT CARD", "TWO-WHEELER LOAN" };
|
||||
private static final String[] PURPOSES = { "PERSONAL LOAN", "CREDIT CARD", "AUTO LOAN", "BUSINESS LOAN", "GOLD LOAN" };
|
||||
|
||||
private CicSimulator() {}
|
||||
|
||||
public static JsonObject respond(final String bureauCode, final Map<String, Object> ctx, final LocalDate businessDate,
|
||||
final String reference) {
|
||||
final String key = String.valueOf(ctx.get("client.pan") != null ? ctx.get("client.pan") : ctx.get("client.id"));
|
||||
final long seed = Math.abs((long) (key + "|" + bureauCode).hashCode());
|
||||
final Random r = new Random(seed);
|
||||
|
||||
final JsonObject report = new JsonObject();
|
||||
report.addProperty("reportId", bureauCode + "-SIM-" + reference);
|
||||
report.addProperty("bureau", bureauCode);
|
||||
report.addProperty("generatedOn", businessDate.toString());
|
||||
|
||||
final JsonObject root = new JsonObject();
|
||||
root.add("report", report);
|
||||
|
||||
// Roughly one customer in fifteen is new to credit.
|
||||
if (seed % 15 == 0) {
|
||||
report.addProperty("hit", false);
|
||||
report.addProperty("message", "No record found for the given identifiers (new to credit)");
|
||||
return root;
|
||||
}
|
||||
report.addProperty("hit", true);
|
||||
|
||||
final int accountsCount = 1 + r.nextInt(6);
|
||||
final JsonArray accounts = new JsonArray();
|
||||
int active = 0;
|
||||
int overdueCount = 0;
|
||||
int writtenOff = 0;
|
||||
long outstanding = 0;
|
||||
long overdue = 0;
|
||||
int maxDpd = 0;
|
||||
final boolean stressed = seed % 7 == 0;
|
||||
for (int i = 0; i < accountsCount; i++) {
|
||||
final JsonObject a = new JsonObject();
|
||||
final long sanctioned = (5 + r.nextInt(95)) * 10_000L;
|
||||
final boolean closed = r.nextInt(4) == 0;
|
||||
final long balance = closed ? 0 : (long) (sanctioned * (0.1 + r.nextDouble() * 0.8));
|
||||
int dpd = 0;
|
||||
if (!closed && (stressed || r.nextInt(6) == 0)) {
|
||||
dpd = stressed ? 30 + r.nextInt(150) : r.nextInt(45);
|
||||
}
|
||||
final long due = dpd > 0 ? Math.max(1000, balance / 20) : 0;
|
||||
final boolean wo = stressed && dpd > 120 && r.nextBoolean();
|
||||
a.addProperty("memberName", LENDERS[r.nextInt(LENDERS.length)]);
|
||||
a.addProperty("accountType", TYPES[r.nextInt(TYPES.length)]);
|
||||
a.addProperty("ownership", r.nextInt(5) == 0 ? "JOINT" : "INDIVIDUAL");
|
||||
a.addProperty("openedOn", businessDate.minusMonths(6 + r.nextInt(84)).toString());
|
||||
a.addProperty("sanctionedAmount", sanctioned);
|
||||
a.addProperty("currentBalance", balance);
|
||||
a.addProperty("amountOverdue", due);
|
||||
a.addProperty("dpd", dpd);
|
||||
a.addProperty("status", wo ? "WRITTEN_OFF" : closed ? "CLOSED" : "ACTIVE");
|
||||
accounts.add(a);
|
||||
if (!closed) {
|
||||
active++;
|
||||
outstanding += balance;
|
||||
}
|
||||
if (due > 0) {
|
||||
overdueCount++;
|
||||
overdue += due;
|
||||
}
|
||||
if (wo) {
|
||||
writtenOff++;
|
||||
}
|
||||
maxDpd = Math.max(maxDpd, dpd);
|
||||
}
|
||||
|
||||
final int enquiriesCount = r.nextInt(stressed ? 9 : 5);
|
||||
final JsonArray enquiries = new JsonArray();
|
||||
for (int i = 0; i < enquiriesCount; i++) {
|
||||
final JsonObject e = new JsonObject();
|
||||
e.addProperty("memberName", LENDERS[r.nextInt(LENDERS.length)]);
|
||||
e.addProperty("enquiryDate", businessDate.minusDays(5 + r.nextInt(170)).toString());
|
||||
e.addProperty("purpose", PURPOSES[r.nextInt(PURPOSES.length)]);
|
||||
e.addProperty("amount", (1 + r.nextInt(50)) * 20_000L);
|
||||
enquiries.add(e);
|
||||
}
|
||||
|
||||
int score = 650 + r.nextInt(200);
|
||||
score -= Math.min(260, maxDpd * 2) + writtenOff * 60 + Math.max(0, enquiriesCount - 3) * 12;
|
||||
score = Math.max(300, Math.min(900, score));
|
||||
|
||||
final JsonObject scoreObj = new JsonObject();
|
||||
scoreObj.addProperty("value", score);
|
||||
scoreObj.addProperty("version", bureauCode + " SIMULATED v1");
|
||||
report.add("score", scoreObj);
|
||||
|
||||
final JsonObject summary = new JsonObject();
|
||||
summary.addProperty("totalAccounts", accountsCount);
|
||||
summary.addProperty("activeAccounts", active);
|
||||
summary.addProperty("overdueAccounts", overdueCount);
|
||||
summary.addProperty("writtenOffAccounts", writtenOff);
|
||||
summary.addProperty("totalOutstanding", outstanding);
|
||||
summary.addProperty("totalOverdue", overdue);
|
||||
summary.addProperty("enquiriesLast6Months", enquiriesCount);
|
||||
summary.addProperty("maxDpd", maxDpd);
|
||||
report.add("summary", summary);
|
||||
report.add("accounts", accounts);
|
||||
report.add("enquiries", enquiries);
|
||||
return root;
|
||||
}
|
||||
}
|
||||
+81
@@ -69,6 +69,8 @@ import org.apache.fineract.infrastructure.security.service.PlatformSecurityConte
|
||||
import org.apache.fineract.portfolio.PortfolioProductType;
|
||||
import org.apache.fineract.portfolio.voucher.service.VoucherLine;
|
||||
import org.apache.fineract.portfolio.client.service.KycRestrictionGuard;
|
||||
import org.apache.fineract.portfolio.savings.service.DepositAccountWritePlatformService;
|
||||
import org.apache.fineract.portfolio.savings.service.FixedDepositFundingValidator;
|
||||
import org.apache.fineract.portfolio.voucher.service.VoucherService;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
@@ -90,6 +92,8 @@ public class ClerkDisbursementRequestApiResource {
|
||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||
private final FineractRequestContextHolder fineractRequestContextHolder;
|
||||
private final VoucherService voucherService;
|
||||
private final FixedDepositFundingValidator fixedDepositFundingValidator;
|
||||
private final DepositAccountWritePlatformService depositAccountWritePlatformService;
|
||||
private final Gson gson = new GsonBuilder().serializeNulls().create();
|
||||
|
||||
// ── POST /v1/clerk/disbursement-requests ── Clerk submits new request ────────
|
||||
@@ -202,6 +206,9 @@ public class ClerkDisbursementRequestApiResource {
|
||||
}
|
||||
}
|
||||
|
||||
// Fixed deposit legs: credit-only, funded once with exactly the principal, from the customer's own account.
|
||||
validateFixedDepositLegs(creditEntries, requestType);
|
||||
|
||||
// Validate balance when both DR and CR entries present (collection mode)
|
||||
if (totalDr.compareTo(BigDecimal.ZERO) > 0 && totalCr.compareTo(BigDecimal.ZERO) > 0
|
||||
&& totalDr.subtract(totalCr).abs().compareTo(new BigDecimal("0.01")) > 0) {
|
||||
@@ -491,6 +498,10 @@ public class ClerkDisbursementRequestApiResource {
|
||||
rejectRecurringDepositLeg(entry, entryType);
|
||||
}
|
||||
}
|
||||
// Re-checked here, not only at submit: another request (or a counter deposit) may have funded the
|
||||
// same FD while this one waited, and a refusal now still leaves the request PENDING.
|
||||
final Set<Long> fundedFixedDeposits = validateFixedDepositLegs(creditEntries,
|
||||
record.get("request_type") != null ? (String) record.get("request_type") : "DISBURSEMENT");
|
||||
|
||||
// Atomic PENDING → PROCESSING lock: prevents two managers from concurrently
|
||||
// posting duplicate Fineract transactions for the same request.
|
||||
@@ -773,6 +784,12 @@ public class ClerkDisbursementRequestApiResource {
|
||||
}
|
||||
} // end else (disbursement/collection mode)
|
||||
|
||||
// A funded FD's maturity date and amount were projected off its principal with no money in it;
|
||||
// the direct credit above does not recompute them, so do it now that the money has landed.
|
||||
for (final Long fdId : fundedFixedDeposits) {
|
||||
depositAccountWritePlatformService.recalculateFixedDepositMaturity(fdId);
|
||||
}
|
||||
|
||||
} catch (Exception ex) {
|
||||
// Compensate: undo all posted Fineract transactions in reverse order (best-effort).
|
||||
// This restores account balances even when a later entry fails mid-processing.
|
||||
@@ -1223,6 +1240,70 @@ public class ClerkDisbursementRequestApiResource {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixed deposit legs of a clerk request, recognised by id (deposit_type_enum) rather than the entry's label.
|
||||
* <ul>
|
||||
* <li>An FD is never debited here: money leaves an FD only through its maturity or premature closure. The
|
||||
* direct book debit used by Bank Transfer and Collection would otherwise bypass the FD's own refusal.</li>
|
||||
* <li>An FD credit is its first and only funding, for exactly its principal, and an FD appears once.</li>
|
||||
* <li>The FDs funded by one request belong to one customer, and every Savings/Current account debited to fund
|
||||
* them belongs to that customer (or is held jointly) and can pay its amount. A GL source (Disbursement) is
|
||||
* the bank's own money and has no customer to compare.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @return the ids of the FDs this request funds, for the maturity recalculation after posting
|
||||
*/
|
||||
private Set<Long> validateFixedDepositLegs(final JsonArray entries, final String requestType) {
|
||||
final Set<Long> fdIds = new LinkedHashSet<>();
|
||||
Long firstFdId = null;
|
||||
Long fdClientId = null;
|
||||
for (final JsonElement el : entries) {
|
||||
final JsonObject entry = el.getAsJsonObject();
|
||||
final String type = entry.has("accountType") ? entry.get("accountType").getAsString() : "SAVINGS";
|
||||
if ("GL".equals(type) || "CURRENT".equals(type) || !entry.has("savingsAccountId")
|
||||
|| entry.get("savingsAccountId").isJsonNull()) {
|
||||
continue;
|
||||
}
|
||||
final Long accountId = entry.get("savingsAccountId").getAsLong();
|
||||
if (!fixedDepositFundingValidator.isFixedDeposit(accountId)) {
|
||||
continue;
|
||||
}
|
||||
final String acNo = entry.has("accountNo") ? entry.get("accountNo").getAsString() : String.valueOf(accountId);
|
||||
if ("Dr".equals(entry.has("dbCr") ? entry.get("dbCr").getAsString() : "Cr")) {
|
||||
throw apiError(422, "Fixed deposit " + acNo + " cannot be debited. Money leaves a fixed deposit only"
|
||||
+ " through its maturity or premature closure.");
|
||||
}
|
||||
if (!fdIds.add(accountId)) {
|
||||
throw apiError(422, "Fixed deposit " + acNo + " appears more than once. A fixed deposit is funded once,"
|
||||
+ " with its full principal in one entry.");
|
||||
}
|
||||
final BigDecimal amount = parseBigDecimal(entry.has("amount") ? entry.get("amount").getAsString() : null);
|
||||
final Long clientId = fixedDepositFundingValidator.validateFirstFunding(accountId, amount);
|
||||
if (firstFdId == null) {
|
||||
firstFdId = accountId;
|
||||
fdClientId = clientId;
|
||||
} else if (!Objects.equals(fdClientId, clientId)) {
|
||||
throw apiError(422, "One " + voucherTypeLabelFor(requestType) + " can fund fixed deposits of one customer"
|
||||
+ " only. Raise a separate request for each customer.");
|
||||
}
|
||||
}
|
||||
if (fdIds.isEmpty()) {
|
||||
return fdIds;
|
||||
}
|
||||
for (final JsonElement el : entries) {
|
||||
final JsonObject entry = el.getAsJsonObject();
|
||||
final String type = entry.has("accountType") ? entry.get("accountType").getAsString() : "SAVINGS";
|
||||
if (!"Dr".equals(entry.has("dbCr") ? entry.get("dbCr").getAsString() : "Cr") || "GL".equals(type)
|
||||
|| !entry.has("savingsAccountId") || entry.get("savingsAccountId").isJsonNull()) {
|
||||
continue;
|
||||
}
|
||||
final BigDecimal amount = parseBigDecimal(entry.has("amount") ? entry.get("amount").getAsString() : null);
|
||||
fixedDepositFundingValidator.validateFundingSource(firstFdId, fdClientId, entry.get("savingsAccountId").getAsLong(),
|
||||
"CURRENT".equals(type), amount);
|
||||
}
|
||||
return fdIds;
|
||||
}
|
||||
|
||||
/** Savings status_enum for an account still open for business (Fineract's SavingsAccountStatusType.ACTIVE). */
|
||||
private static final int SAVINGS_STATUS_ACTIVE = 300;
|
||||
|
||||
|
||||
+166
-16
@@ -48,6 +48,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity;
|
||||
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||
import org.apache.fineract.commands.service.CommandWrapperBuilder;
|
||||
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
|
||||
@@ -56,6 +57,7 @@ import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.client.domain.KycExpiryStage;
|
||||
import org.apache.fineract.portfolio.client.service.KycRestrictionGuard;
|
||||
import org.apache.fineract.portfolio.savings.service.FixedDepositFundingValidator;
|
||||
import org.apache.fineract.portfolio.voucher.service.VoucherLine;
|
||||
import org.apache.fineract.portfolio.voucher.service.VoucherService;
|
||||
import org.slf4j.Logger;
|
||||
@@ -84,6 +86,7 @@ public class ClerkWithdrawalRequestApiResource {
|
||||
private final PlatformSecurityContext context;
|
||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||
private final VoucherService voucherService;
|
||||
private final FixedDepositFundingValidator fixedDepositFundingValidator;
|
||||
private final Gson gson = new GsonBuilder().serializeNulls().create();
|
||||
|
||||
// ── POST /v1/clerk/withdrawal-requests ── Clerk submits new request ─────────
|
||||
@@ -110,13 +113,28 @@ public class ClerkWithdrawalRequestApiResource {
|
||||
final String tokNo = fromJsonHelper.extractStringNamed("tokNo", root);
|
||||
final String txnDate = fromJsonHelper.extractStringNamed("txnDate", root);
|
||||
final String denominationData = fromJsonHelper.extractStringNamed("denominationData", root);
|
||||
// A GL withdrawal slip (e.g. Sundry Creditors paid out in cash) carries the GL and the cash GL it is
|
||||
// paid from instead of a savings account; it is approved and disbursed exactly like any other slip.
|
||||
final boolean isGl = "GL".equalsIgnoreCase(accountType);
|
||||
final Long glAccountId = fromJsonHelper.extractLongNamed("glAccountId", root);
|
||||
final Long contraGlId = fromJsonHelper.extractLongNamed("contraGlId", root);
|
||||
final Long officeId = fromJsonHelper.extractLongNamed("officeId", root);
|
||||
final String currencyCode = fromJsonHelper.extractStringNamed("currencyCode", root);
|
||||
|
||||
if (accountNo == null || accountNo.isBlank()) {
|
||||
throw apiError(400, "Account number is required");
|
||||
}
|
||||
if (savingsAccountId == null) {
|
||||
if (isGl) {
|
||||
validateGlWithdrawalAccounts(glAccountId, contraGlId);
|
||||
} else if (savingsAccountId == null) {
|
||||
throw apiError(400, "Savings account id is required for a withdrawal request");
|
||||
}
|
||||
// Money leaves a fixed deposit only through its maturity or premature closure, never a withdrawal slip.
|
||||
// Checked by id: the slip's own label is not trusted, and a CURRENT id belongs to m_current_account.
|
||||
if (!isGl && !"CURRENT".equalsIgnoreCase(accountType) && fixedDepositFundingValidator.isFixedDeposit(savingsAccountId)) {
|
||||
throw apiError(422, "Account " + accountNo + " is a fixed deposit and cannot be withdrawn from."
|
||||
+ " Money leaves a fixed deposit only through its maturity or premature closure.");
|
||||
}
|
||||
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
throw apiError(400, "Amount must be greater than zero");
|
||||
}
|
||||
@@ -124,7 +142,9 @@ public class ClerkWithdrawalRequestApiResource {
|
||||
throw apiError(400, "Token number (TOK. NO.) is required");
|
||||
}
|
||||
validateTokenUniqueToday(tokNo, null);
|
||||
kycRestrictionGuard.check(savingsAccountId, KycRestrictionGuard.Operation.DEBIT);
|
||||
if (!isGl) {
|
||||
kycRestrictionGuard.check(savingsAccountId, KycRestrictionGuard.Operation.DEBIT);
|
||||
}
|
||||
|
||||
final String activityLog = buildActivityLog(null, "SUBMITTED", currentUser.getUsername(), null);
|
||||
|
||||
@@ -132,8 +152,9 @@ public class ClerkWithdrawalRequestApiResource {
|
||||
INSERT INTO m_clerk_withdrawal_request
|
||||
(account_no, account_type, savings_account_id, client_id, client_name, amount,
|
||||
payment_type_id, payment_type_name, description, cheque_no, bank_name, tok_no, txn_date,
|
||||
status, clerk_id, clerk_name, clerk_username, denomination_data, activity_log, submitted_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
status, clerk_id, clerk_name, clerk_username, denomination_data, activity_log, submitted_at,
|
||||
gl_account_id, contra_gl_id, office_id, currency_code)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
RETURNING id
|
||||
""",
|
||||
Long.class,
|
||||
@@ -142,7 +163,9 @@ public class ClerkWithdrawalRequestApiResource {
|
||||
txnDate != null ? Date.valueOf(txnDate) : null,
|
||||
"PENDING",
|
||||
currentUser.getId(), currentUser.getDisplayName(), currentUser.getUsername(),
|
||||
denominationData, activityLog, Timestamp.from(Instant.now())
|
||||
denominationData, activityLog, Timestamp.from(Instant.now()),
|
||||
isGl ? glAccountId : null, isGl ? contraGlId : null, isGl ? (officeId != null ? officeId : 1L) : null,
|
||||
isGl ? (currencyCode != null ? currencyCode : "INR") : null
|
||||
);
|
||||
|
||||
// Globally unique voucher number from the central m_voucher register
|
||||
@@ -198,7 +221,8 @@ public class ClerkWithdrawalRequestApiResource {
|
||||
+ " txn_date, status, clerk_id, clerk_name, clerk_username,"
|
||||
+ " manager_id, manager_name, manager_username, rejection_reason, txn_id,"
|
||||
+ " denomination_data, activity_log, submitted_at, actioned_at,"
|
||||
+ " cashier_id, disbursed_by, disbursed_at, voucher_no"
|
||||
+ " cashier_id, disbursed_by, disbursed_at, voucher_no,"
|
||||
+ " gl_account_id, contra_gl_id, office_id, currency_code"
|
||||
+ " FROM m_clerk_withdrawal_request " + where
|
||||
+ " ORDER BY submitted_at DESC LIMIT ? OFFSET ?",
|
||||
pageParams.toArray());
|
||||
@@ -252,7 +276,8 @@ public class ClerkWithdrawalRequestApiResource {
|
||||
payment_type_name, description, tok_no, txn_date, status,
|
||||
clerk_name, manager_name, manager_username, rejection_reason, txn_id,
|
||||
submitted_at, actioned_at,
|
||||
cashier_id, disbursed_by, disbursed_at, voucher_no
|
||||
cashier_id, disbursed_by, disbursed_at, voucher_no,
|
||||
gl_account_id, contra_gl_id, office_id, currency_code
|
||||
FROM m_clerk_withdrawal_request
|
||||
""" + where + " ORDER BY submitted_at DESC LIMIT ? OFFSET ?",
|
||||
pageParams.toArray()
|
||||
@@ -276,7 +301,8 @@ public class ClerkWithdrawalRequestApiResource {
|
||||
txn_date, status, clerk_id, clerk_name, clerk_username,
|
||||
manager_id, manager_name, manager_username, rejection_reason, txn_id,
|
||||
denomination_data, activity_log, submitted_at, actioned_at,
|
||||
cashier_id, disbursed_by, disbursed_at, voucher_no
|
||||
cashier_id, disbursed_by, disbursed_at, voucher_no,
|
||||
gl_account_id, contra_gl_id, office_id, currency_code
|
||||
FROM m_clerk_withdrawal_request WHERE id = ?
|
||||
""", id);
|
||||
if (rows.isEmpty()) {
|
||||
@@ -295,7 +321,8 @@ public class ClerkWithdrawalRequestApiResource {
|
||||
final List<Map<String, Object>> rows = jdbcTemplate.queryForList("""
|
||||
SELECT id, account_no, account_type, savings_account_id, client_id, client_name,
|
||||
amount, payment_type_id, payment_type_name, description, tok_no, status,
|
||||
clerk_id, clerk_name, manager_id, manager_name, submitted_at, actioned_at, voucher_no
|
||||
clerk_id, clerk_name, manager_id, manager_name, submitted_at, actioned_at, voucher_no,
|
||||
gl_account_id, contra_gl_id, office_id, currency_code
|
||||
FROM m_clerk_withdrawal_request
|
||||
WHERE tok_no = ? AND CAST(submitted_at AS DATE) = CURRENT_DATE
|
||||
ORDER BY submitted_at DESC LIMIT 1
|
||||
@@ -455,13 +482,16 @@ public class ClerkWithdrawalRequestApiResource {
|
||||
"Maker-checker: you cannot approve a withdrawal request you submitted yourself. Ask another manager to review it.");
|
||||
}
|
||||
|
||||
final boolean isGlRequest = "GL".equalsIgnoreCase((String) record.get("account_type"));
|
||||
final Long savingsAccountId = record.get("savings_account_id") != null
|
||||
? ((Number) record.get("savings_account_id")).longValue() : null;
|
||||
// Checked again on approval, not only on submission: a request can sit in the queue for days,
|
||||
// and the account can cross the dormancy threshold while it waits. The savings withdrawal
|
||||
// below refuses a dormant account anyway, but that failure would surface to the manager as a
|
||||
// rejected transaction after the approval was recorded; this says why, before anything moves.
|
||||
kycRestrictionGuard.check(savingsAccountId, KycRestrictionGuard.Operation.DEBIT);
|
||||
if (!isGlRequest) {
|
||||
kycRestrictionGuard.check(savingsAccountId, KycRestrictionGuard.Operation.DEBIT);
|
||||
}
|
||||
final BigDecimal amount = new BigDecimal(record.get("amount").toString());
|
||||
final Object ptId = record.get("payment_type_id");
|
||||
final Object txnDateObj = record.get("txn_date");
|
||||
@@ -469,7 +499,11 @@ public class ClerkWithdrawalRequestApiResource {
|
||||
final String description = (String) record.get("description");
|
||||
|
||||
String txnId = null;
|
||||
if (savingsAccountId != null) {
|
||||
if (isGlRequest) {
|
||||
// Dr the GL account, Cr the branch cash GL -- posted here, at approval, like the savings withdrawal.
|
||||
validateGlWithdrawalAccounts(toLong(record.get("gl_account_id")), toLong(record.get("contra_gl_id")));
|
||||
txnId = executeGlWithdrawalJournal(record, amount, ptId, txnDateStr, description);
|
||||
} else if (savingsAccountId != null) {
|
||||
final boolean isCurrentAccount = isCurrentAccountById(savingsAccountId, (String) record.get("account_no"));
|
||||
txnId = isCurrentAccount
|
||||
? executeCurrentAccountWithdrawal(savingsAccountId, amount, ptId, txnDateStr, description)
|
||||
@@ -477,14 +511,18 @@ public class ClerkWithdrawalRequestApiResource {
|
||||
}
|
||||
|
||||
// Link voucher to the created savings txn and stamp voucher_no on it
|
||||
if (txnId != null) {
|
||||
if (txnId != null && !isGlRequest) {
|
||||
voucherService.linkSavingsTransaction(
|
||||
VoucherService.REF_CLERK_WITHDRAWAL_REQUEST, id, Long.valueOf(txnId));
|
||||
}
|
||||
|
||||
// The sides of the slip. A withdrawal is money leaving the member, so their account is
|
||||
// debited and the till that pays it out is credited.
|
||||
addWithdrawalVoucherLines(record, savingsAccountId, amount, txnId);
|
||||
if (isGlRequest) {
|
||||
addGlWithdrawalVoucherLines(record, amount);
|
||||
} else {
|
||||
addWithdrawalVoucherLines(record, savingsAccountId, amount, txnId);
|
||||
}
|
||||
|
||||
final String updatedLog = buildActivityLog(
|
||||
(String) record.get("activity_log"), "APPROVED", currentUser.getUsername(), null
|
||||
@@ -689,8 +727,8 @@ public class ClerkWithdrawalRequestApiResource {
|
||||
if (denomMap == null || denomMap.size() == 0) return;
|
||||
|
||||
final Object txnIdObj = record.get("txn_id");
|
||||
final Long txnId = txnIdObj != null ? Long.valueOf(txnIdObj.toString()) : null;
|
||||
if (txnId == null) return; // no savings txn linked — cannot record denomination
|
||||
final Long txnId = txnIdObj != null ? parseTxnKey(txnIdObj.toString()) : null;
|
||||
if (txnId == null) return; // no txn linked — cannot record denomination
|
||||
|
||||
final Object saObj = record.get("savings_account_id");
|
||||
final Long savingsAccountId = saObj != null ? Long.valueOf(saObj.toString()) : null;
|
||||
@@ -793,6 +831,117 @@ public class ClerkWithdrawalRequestApiResource {
|
||||
return rows.isEmpty() ? null : rows.get(0);
|
||||
}
|
||||
|
||||
private static Long toLong(final Object value) {
|
||||
return value instanceof Number n ? n.longValue() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A savings transaction id is numeric; a GL journal entry's is a hex string. Both are reduced to one
|
||||
* Long the same way CashierGlDenominationApiResource does, so a GL withdrawal's notes group under
|
||||
* the id of its own journal entry in the cashier's position and closing report.
|
||||
*/
|
||||
private Long parseTxnKey(final String raw) {
|
||||
if (raw == null || raw.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Long.valueOf(raw);
|
||||
} catch (final NumberFormatException ignored) {
|
||||
try {
|
||||
final String hex = raw.length() > 15 ? raw.substring(raw.length() - 15) : raw;
|
||||
return Long.parseUnsignedLong(hex, 16);
|
||||
} catch (final NumberFormatException ignored2) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** A GL withdrawal pays cash out of a GL account: the account must not itself be a cash GL, and the
|
||||
* GL it is paid from must be one of the branch's mapped cash accounts. */
|
||||
private void validateGlWithdrawalAccounts(final Long glAccountId, final Long contraGlId) {
|
||||
if (glAccountId == null || contraGlId == null) {
|
||||
throw apiError(400, "glAccountId and contraGlId are required for a GL withdrawal request");
|
||||
}
|
||||
final List<Long> cashGlIds = jdbcTemplate.queryForList(
|
||||
"SELECT gl_account_id FROM acc_gl_financial_activity_account WHERE financial_activity_type IN (?, ?)",
|
||||
Long.class, FinancialActivity.CASH_AT_MAINVAULT.getValue(), FinancialActivity.CASH_AT_TELLER.getValue());
|
||||
if (!cashGlIds.contains(contraGlId)) {
|
||||
throw apiError(400, "A GL withdrawal must be paid from the branch cash vault GL.");
|
||||
}
|
||||
if (cashGlIds.contains(glAccountId)) {
|
||||
throw apiError(400, "The cash vault itself cannot be the GL account of a withdrawal slip.");
|
||||
}
|
||||
}
|
||||
|
||||
/** Dr the GL account, Cr the cash GL, through the journal-entry command (not the HTTP API, which is
|
||||
* switched off for manual postings). Returns the journal entry's transaction id. */
|
||||
private String executeGlWithdrawalJournal(final Map<?, ?> record, final BigDecimal amount,
|
||||
final Object paymentTypeId, final String txnDateStr, final String note) {
|
||||
final String today = LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||
final String date = txnDateStr != null && !txnDateStr.isBlank() ? formatForFineract(txnDateStr) : today;
|
||||
|
||||
final JsonObject debit = new JsonObject();
|
||||
debit.addProperty("glAccountId", toLong(record.get("gl_account_id")));
|
||||
debit.addProperty("amount", amount);
|
||||
final JsonObject credit = new JsonObject();
|
||||
credit.addProperty("glAccountId", toLong(record.get("contra_gl_id")));
|
||||
credit.addProperty("amount", amount);
|
||||
final JsonArray debits = new JsonArray();
|
||||
debits.add(debit);
|
||||
final JsonArray credits = new JsonArray();
|
||||
credits.add(credit);
|
||||
|
||||
final Object office = record.get("office_id");
|
||||
final Object currency = record.get("currency_code");
|
||||
final JsonObject payload = new JsonObject();
|
||||
payload.addProperty("officeId", office instanceof Number n ? n.longValue() : 1L);
|
||||
payload.addProperty("transactionDate", date);
|
||||
payload.addProperty("dateFormat", "dd MMMM yyyy");
|
||||
payload.addProperty("locale", "en");
|
||||
payload.addProperty("currencyCode", currency != null ? currency.toString() : "INR");
|
||||
if (note != null && !note.isBlank()) {
|
||||
payload.addProperty("comments", note);
|
||||
}
|
||||
if (paymentTypeId != null) {
|
||||
payload.addProperty("paymentTypeId", ((Number) paymentTypeId).longValue());
|
||||
}
|
||||
payload.add("credits", credits);
|
||||
payload.add("debits", debits);
|
||||
|
||||
final CommandWrapper commandRequest = new CommandWrapperBuilder().createJournalEntry().withJson(payload.toString()).build();
|
||||
// The request raised its own voucher at submission; the journal command must not raise a second.
|
||||
final CommandProcessingResult result = voucherService
|
||||
.withOwnVoucher(() -> commandsSourceWritePlatformService.logCommandSource(commandRequest));
|
||||
return result.getTransactionId();
|
||||
}
|
||||
|
||||
/** The two sides of an approved GL withdrawal on its voucher: the GL account is debited, the cash GL credited. */
|
||||
private void addGlWithdrawalVoucherLines(final Map<?, ?> record, final BigDecimal amount) {
|
||||
final String voucherNo = (String) record.get("voucher_no");
|
||||
if (voucherNo == null || voucherNo.isBlank()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final Long glId = toLong(record.get("gl_account_id"));
|
||||
final Long contraId = toLong(record.get("contra_gl_id"));
|
||||
final Map<String, Object> gl = glAccountForVoucher(glId);
|
||||
final Map<String, Object> contra = glAccountForVoucher(contraId);
|
||||
voucherService.addLines(voucherNo, List.of(
|
||||
VoucherLine.debitGl(glId, (String) gl.get("name"), (String) gl.get("gl_code"), amount),
|
||||
VoucherLine.creditGl(contraId, (String) contra.getOrDefault("name", "Cash in Hand"), (String) contra.get("gl_code"), amount)));
|
||||
} catch (final RuntimeException e) {
|
||||
LOG.error("Could not record voucher lines for GL withdrawal request voucher {}", voucherNo, e);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> glAccountForVoucher(final Long glAccountId) {
|
||||
if (glAccountId == null) {
|
||||
return Map.of();
|
||||
}
|
||||
return jdbcTemplate.queryForList("SELECT name, gl_code FROM acc_gl_account WHERE id = ?", glAccountId).stream().findFirst()
|
||||
.orElse(Map.of());
|
||||
}
|
||||
|
||||
private Map<?, ?> loadRecord(final Long id) {
|
||||
final List<Map<String, Object>> rows = jdbcTemplate.queryForList("""
|
||||
SELECT id, account_no, account_type, savings_account_id, client_id, client_name,
|
||||
@@ -800,7 +949,8 @@ public class ClerkWithdrawalRequestApiResource {
|
||||
txn_date, status, clerk_id, clerk_name, clerk_username,
|
||||
manager_id, manager_name, manager_username, rejection_reason, txn_id,
|
||||
denomination_data, activity_log, submitted_at, actioned_at,
|
||||
cashier_id, disbursed_by, disbursed_at, voucher_no
|
||||
cashier_id, disbursed_by, disbursed_at, voucher_no,
|
||||
gl_account_id, contra_gl_id, office_id, currency_code
|
||||
FROM m_clerk_withdrawal_request WHERE id = ?
|
||||
""", id);
|
||||
if (rows.isEmpty()) {
|
||||
|
||||
-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);
|
||||
}
|
||||
}
|
||||
-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 java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Constants for the Treasury Investment masters (Bank / Party Master and Script Master), modelled on the bank's existing
|
||||
* CBS Investment screens.
|
||||
*/
|
||||
public final class InvestmentApiConstants {
|
||||
|
||||
private InvestmentApiConstants() {}
|
||||
|
||||
public static final String ENTITY_PARTY = "INVESTMENTPARTY";
|
||||
public static final String ENTITY_SCRIPT = "INVESTMENTSCRIPT";
|
||||
public static final String RESOURCE_PARTY = "investmentparty";
|
||||
public static final String RESOURCE_SCRIPT = "investmentscript";
|
||||
|
||||
/** Instrument ("Bond Type") an investment party / script belongs to. */
|
||||
public static final Set<String> BOND_TYPES = Set.of("GOVT_SECURITY", "NON_SLR", "BOND", "MUTUAL_FUND", "FD", "SHARE");
|
||||
|
||||
/** Bond types that carry coupon terms (ISIN, interest rate, issue date are mandatory for these). */
|
||||
public static final Set<String> DEBT_BOND_TYPES = Set.of("GOVT_SECURITY", "NON_SLR", "BOND");
|
||||
|
||||
/** SLR classification on the Script Master ("GOI" = Central Government). */
|
||||
public static final Set<String> SLR_TYPES = Set.of("GOI", "STATE_GOVT", "TREASURY", "NON_SLR");
|
||||
|
||||
/** Script Code prefix per bond type, e.g. G00001 for the first Govt Security. */
|
||||
public static final Map<String, String> SCRIPT_CODE_PREFIX = Map.of("GOVT_SECURITY", "G", "NON_SLR", "N", "BOND", "B", "MUTUAL_FUND",
|
||||
"M", "FD", "F", "SHARE", "S");
|
||||
|
||||
public static final String PARTY_CODE_PREFIX = "PTY-";
|
||||
|
||||
// Common parameters
|
||||
public static final String BOND_TYPE = "bondType";
|
||||
public static final String GL_ACCOUNT_ID = "glAccountId";
|
||||
public static final String 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";
|
||||
|
||||
// Script parameters
|
||||
public static final String SLR_TYPE = "slrType";
|
||||
public static final String SCRIPT_DESC = "scriptDesc";
|
||||
public static final String ISIN = "isin";
|
||||
public static final String FACE_VALUE_PER_UNIT = "faceValuePerUnit";
|
||||
public static final String INT_RATE = "intRate";
|
||||
public static final String ISSUE_DATE = "issueDate";
|
||||
public static final String MATURITY_DATE = "maturityDate";
|
||||
public static final String COMPANY_NAME = "companyName";
|
||||
public static final String CORP_CODE = "corpCode";
|
||||
|
||||
public static final Set<String> PARTY_PARAMS = Set.of(BOND_TYPE, PARTY_NAME, ADDRESS1, ADDRESS2, ADDRESS3, CITY, PINCODE, PHONE, FAX,
|
||||
BANK_TYPE, GL_ACCOUNT_ID, INTEREST_GL_ACCOUNT_ID, RBI_CODE, EXPOSURE_LIMIT, LEI_NO, CP_BANK_NAME, CP_BRANCH_NAME, CP_ACCOUNT_NO, CP_IFSC, ACTIVE,
|
||||
LOCALE);
|
||||
|
||||
public static final Set<String> SCRIPT_PARAMS = Set.of(BOND_TYPE, SLR_TYPE, SCRIPT_DESC, GL_ACCOUNT_ID, ISIN, FACE_VALUE_PER_UNIT,
|
||||
INT_RATE, ISSUE_DATE, MATURITY_DATE, COMPANY_NAME, ADDRESS1, ADDRESS2, ADDRESS3, CITY, PINCODE, PHONE, FAX, CORP_CODE,
|
||||
ACTIVE, LOCALE, DATE_FORMAT);
|
||||
}
|
||||
-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);
|
||||
}
|
||||
}
|
||||
-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);
|
||||
}
|
||||
-186
@@ -1,186 +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.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) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(InvestmentFdApiConstants.ENTITY);
|
||||
return this.closureService.quote(fdId, type, date(closeDate), receivedAmount, tds, renewalAmount);
|
||||
}
|
||||
|
||||
@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) {
|
||||
return StringUtils.isBlank(value) ? null : LocalDate.parse(value.trim(), DATE);
|
||||
}
|
||||
}
|
||||
-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);
|
||||
}
|
||||
}
|
||||
-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);
|
||||
}
|
||||
}
|
||||
-74
@@ -1,74 +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";
|
||||
|
||||
/** 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);
|
||||
}
|
||||
-184
@@ -1,184 +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.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.investment.data.InvestmentInterestScheduleData;
|
||||
import org.apache.fineract.portfolio.investment.data.InvestmentPurchaseData;
|
||||
import org.apache.fineract.portfolio.investment.data.InvestmentSaleData;
|
||||
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;
|
||||
|
||||
@GET
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "List investment purchases (holdings)")
|
||||
public List<InvestmentPurchaseData> retrieveAll() {
|
||||
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
|
||||
return this.readPlatformService.retrieveAll();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{purchaseId}")
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Retrieve an investment purchase")
|
||||
public InvestmentPurchaseData retrieveOne(@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
|
||||
return this.readPlatformService.retrieveOne(purchaseId);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{purchaseId}/interest-schedule")
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Interest schedule of a holding", description = "Half-yearly coupons and 31 Mar / 30 Sep closing provisions "
|
||||
+ "from purchase to maturity. Calculated only — nothing is posted. asOnDate (dd-MM-yyyy) defaults to the business date.")
|
||||
public InvestmentInterestScheduleData retrieveInterestSchedule(
|
||||
@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId,
|
||||
@QueryParam("asOnDate") @Parameter(description = "dd-MM-yyyy") final String asOnDate) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
|
||||
final LocalDate asOn = StringUtils.isBlank(asOnDate) ? null
|
||||
: LocalDate.parse(asOnDate.trim(), DateTimeFormatter.ofPattern("dd-MM-yyyy"));
|
||||
return this.readPlatformService.retrieveInterestSchedule(purchaseId, asOn);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Save a purchase entry")
|
||||
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
final CommandWrapper request = new CommandWrapperBuilder().createInvestmentPurchase().withJson(apiRequestBodyAsJson).build();
|
||||
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
-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();
|
||||
}
|
||||
}
|
||||
-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;
|
||||
}
|
||||
}
|
||||
-103
@@ -1,103 +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;
|
||||
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()).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;
|
||||
}
|
||||
-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;
|
||||
}
|
||||
}
|
||||
-68
@@ -1,68 +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 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())
|
||||
.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);
|
||||
}
|
||||
}
|
||||
-72
@@ -1,72 +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 boolean active;
|
||||
|
||||
public static InvestmentScriptData from(final InvestmentScript s) {
|
||||
return InvestmentScriptData.builder().id(s.getId()).scriptCode(s.getScriptCode()).bondType(s.getBondType())
|
||||
.slrType(s.getSlrType()).scriptDesc(s.getScriptDesc()).glAccountId(s.getGlAccount().getId())
|
||||
.glCode(s.getGlAccount().getGlCode()).glName(s.getGlAccount().getName()).isin(s.getIsin())
|
||||
.faceValuePerUnit(s.getFaceValuePerUnit()).intRate(s.getIntRate()).issueDate(format(s.getIssueDate()))
|
||||
.maturityDate(format(s.getMaturityDate())).companyName(s.getCompanyName()).address1(s.getAddress1())
|
||||
.address2(s.getAddress2()).address3(s.getAddress3()).city(s.getCity()).pincode(s.getPincode()).phone(s.getPhone())
|
||||
.fax(s.getFax()).corpCode(s.getCorpCode()).active(s.isActive()).build();
|
||||
}
|
||||
|
||||
private static String format(final LocalDate date) {
|
||||
return date == null ? null : date.format(DATE_FORMAT);
|
||||
}
|
||||
}
|
||||
-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);
|
||||
}
|
||||
-110
@@ -1,110 +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;
|
||||
|
||||
@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 existsByScriptId(Long scriptId);
|
||||
}
|
||||
-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);
|
||||
}
|
||||
-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.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
|
||||
/**
|
||||
* Script Master — what is being bought (a Govt Security, bond, mutual fund scheme, share, …), following the bank's
|
||||
* existing CBS Script Master. A purchase picks these terms up instead of the dealer re-typing them.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "m_investment_script")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class InvestmentScript extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
|
||||
/** System-generated: bond-type letter + running number, e.g. G00001. */
|
||||
@Column(name = "script_code", nullable = false, length = 20, unique = true)
|
||||
private String scriptCode;
|
||||
|
||||
@Column(name = "bond_type", nullable = false, length = 20)
|
||||
private String bondType;
|
||||
|
||||
/** GOI / STATE_GOVT / TREASURY / NON_SLR. */
|
||||
@Column(name = "slr_type", length = 20)
|
||||
private String slrType;
|
||||
|
||||
@Column(name = "script_desc", nullable = false, length = 150)
|
||||
private String scriptDesc;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "gl_account_id", nullable = false)
|
||||
private GLAccount glAccount;
|
||||
|
||||
@Column(name = "isin", length = 20)
|
||||
private String isin;
|
||||
|
||||
@Column(name = "face_value_per_unit", scale = 6, precision = 19, nullable = false)
|
||||
private BigDecimal faceValuePerUnit;
|
||||
|
||||
@Column(name = "int_rate", scale = 4, precision = 9)
|
||||
private BigDecimal intRate;
|
||||
|
||||
@Column(name = "issue_date")
|
||||
private LocalDate issueDate;
|
||||
|
||||
@Column(name = "maturity_date")
|
||||
private LocalDate maturityDate;
|
||||
|
||||
@Column(name = "company_name", length = 150)
|
||||
private String companyName;
|
||||
|
||||
@Column(name = "address1", length = 150)
|
||||
private String address1;
|
||||
|
||||
@Column(name = "address2", length = 150)
|
||||
private String address2;
|
||||
|
||||
@Column(name = "address3", length = 150)
|
||||
private String address3;
|
||||
|
||||
@Column(name = "city", length = 100)
|
||||
private String city;
|
||||
|
||||
@Column(name = "pincode", length = 10)
|
||||
private String pincode;
|
||||
|
||||
@Column(name = "phone", length = 20)
|
||||
private String phone;
|
||||
|
||||
@Column(name = "fax", length = 20)
|
||||
private String fax;
|
||||
|
||||
@Column(name = "corp_code", length = 30)
|
||||
private String corpCode;
|
||||
|
||||
@Column(name = "is_active", nullable = false)
|
||||
private boolean active = true;
|
||||
}
|
||||
-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);
|
||||
}
|
||||
}
|
||||
-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 InvestmentScriptNotFoundException extends AbstractPlatformResourceNotFoundException {
|
||||
|
||||
public InvestmentScriptNotFoundException(final Long id) {
|
||||
super("error.msg.investmentscript.id.invalid", "Investment script with identifier " + id + " does not exist", id);
|
||||
}
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentAmortisationService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "INVESTMENTSALE", action = "AMORTISE")
|
||||
@RequiredArgsConstructor
|
||||
public class AmortiseInvestmentPremiumCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final InvestmentAmortisationService amortisationService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.amortisationService.amortise(command);
|
||||
}
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentDailyRateWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "INVESTMENTDAILYRATE", action = "CREATE")
|
||||
@RequiredArgsConstructor
|
||||
public class CreateInvestmentDailyRateCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final InvestmentDailyRateWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.createDailyRate(command);
|
||||
}
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentFdService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "INVESTMENTFD", action = "CREATE")
|
||||
@RequiredArgsConstructor
|
||||
public class CreateInvestmentFdCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final InvestmentFdService fdService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.fdService.create(command);
|
||||
}
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentMasterWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "INVESTMENTPARTY", action = "CREATE")
|
||||
@RequiredArgsConstructor
|
||||
public class CreateInvestmentPartyCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final InvestmentMasterWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.createParty(command);
|
||||
}
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentPurchaseWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "INVESTMENTPURCHASE", action = "CREATE")
|
||||
@RequiredArgsConstructor
|
||||
public class CreateInvestmentPurchaseCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final InvestmentPurchaseWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.createPurchase(command);
|
||||
}
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentSaleWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "INVESTMENTSALE", action = "CREATE")
|
||||
@RequiredArgsConstructor
|
||||
public class CreateInvestmentSaleCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final InvestmentSaleWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.createSale(command);
|
||||
}
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentMasterWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "INVESTMENTSCRIPT", action = "CREATE")
|
||||
@RequiredArgsConstructor
|
||||
public class CreateInvestmentScriptCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final InvestmentMasterWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.createScript(command);
|
||||
}
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentDailyRateWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "INVESTMENTDAILYRATE", action = "DELETE")
|
||||
@RequiredArgsConstructor
|
||||
public class DeleteInvestmentDailyRateCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final InvestmentDailyRateWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.deleteDailyRate(command.entityId());
|
||||
}
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentMasterWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "INVESTMENTPARTY", action = "DELETE")
|
||||
@RequiredArgsConstructor
|
||||
public class DeleteInvestmentPartyCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final InvestmentMasterWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.deleteParty(command.entityId());
|
||||
}
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentPurchaseWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "INVESTMENTPURCHASE", action = "DELETE")
|
||||
@RequiredArgsConstructor
|
||||
public class DeleteInvestmentPurchaseCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final InvestmentPurchaseWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.deletePurchase(command.entityId());
|
||||
}
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentSaleWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "INVESTMENTSALE", action = "DELETE")
|
||||
@RequiredArgsConstructor
|
||||
public class DeleteInvestmentSaleCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final InvestmentSaleWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.deleteSale(command.entityId());
|
||||
}
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentFdClosureService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "INVESTMENTFD", action = "MATURE")
|
||||
@RequiredArgsConstructor
|
||||
public class MatureInvestmentFdCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final InvestmentFdClosureService closureService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.closureService.mature(command.entityId(), command);
|
||||
}
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentFdInterestService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "INVESTMENTFD", action = "POSTINTEREST")
|
||||
@RequiredArgsConstructor
|
||||
public class PostInvestmentFdInterestCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final InvestmentFdInterestService interestService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.interestService.post(command.entityId(), command);
|
||||
}
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentInterestPostingWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "INVESTMENTINTEREST", action = "CREATE")
|
||||
@RequiredArgsConstructor
|
||||
public class PostInvestmentInterestCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final InvestmentInterestPostingWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.postInterest(command.entityId(), command);
|
||||
}
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentFdClosureService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "INVESTMENTFD", action = "PREMATURE")
|
||||
@RequiredArgsConstructor
|
||||
public class PrematureInvestmentFdCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final InvestmentFdClosureService closureService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.closureService.premature(command.entityId(), command);
|
||||
}
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentSaleWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "INVESTMENTSALE", action = "REDEEM")
|
||||
@RequiredArgsConstructor
|
||||
public class RedeemInvestmentPurchaseCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final InvestmentSaleWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.redeem(command.entityId(), command);
|
||||
}
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentFdClosureService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "INVESTMENTFD", action = "RENEW")
|
||||
@RequiredArgsConstructor
|
||||
public class RenewInvestmentFdCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final InvestmentFdClosureService closureService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.closureService.renew(command.entityId(), command);
|
||||
}
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentFdService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "INVESTMENTFD", action = "SETBASE")
|
||||
@RequiredArgsConstructor
|
||||
public class SetInvestmentDepositBaseCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final InvestmentFdService fdService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.fdService.setDepositBase(command);
|
||||
}
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentPurchaseWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "INVESTMENTPURCHASE", action = "SETTLE")
|
||||
@RequiredArgsConstructor
|
||||
public class SettleInvestmentPurchaseCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final InvestmentPurchaseWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.settlePurchase(command.entityId(), command);
|
||||
}
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentSaleWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "INVESTMENTSALE", action = "SETTLE")
|
||||
@RequiredArgsConstructor
|
||||
public class SettleInvestmentSaleCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final InvestmentSaleWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.settleSale(command.entityId(), command);
|
||||
}
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentSecurityTransferService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "INVESTMENTTRANSFER", action = "CREATE")
|
||||
@RequiredArgsConstructor
|
||||
public class TransferInvestmentSecurityCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final InvestmentSecurityTransferService transferService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.transferService.transfer(command.entityId(), command);
|
||||
}
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentMasterWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "INVESTMENTPARTY", action = "UPDATE")
|
||||
@RequiredArgsConstructor
|
||||
public class UpdateInvestmentPartyCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final InvestmentMasterWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.updateParty(command.entityId(), command);
|
||||
}
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentMasterWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "INVESTMENTSCRIPT", action = "UPDATE")
|
||||
@RequiredArgsConstructor
|
||||
public class UpdateInvestmentScriptCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final InvestmentMasterWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.updateScript(command.entityId(), command);
|
||||
}
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.investment.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentSaleWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@CommandType(entity = "INVESTMENTSALE", action = "WRITEOFF")
|
||||
@RequiredArgsConstructor
|
||||
public class WriteOffInvestmentPurchaseCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final InvestmentSaleWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.writeOff(command.entityId(), command);
|
||||
}
|
||||
}
|
||||
-103
@@ -1,103 +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.serialization;
|
||||
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentDailyRateApiConstants.CLOSE_RATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentDailyRateApiConstants.HIGH_RATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentDailyRateApiConstants.LOW_RATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentDailyRateApiConstants.OPEN_RATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentDailyRateApiConstants.PARAMS;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentDailyRateApiConstants.RATE_DATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentDailyRateApiConstants.RESOURCE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentDailyRateApiConstants.SCRIPT_ID;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import java.lang.reflect.Type;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/** Validates a Daily Script Master entry: close rate is mandatory and the day's rates must sit inside low to high. */
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public final class InvestmentDailyRateCommandFromApiJsonDeserializer {
|
||||
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
|
||||
public void validateForCreate(final String json) {
|
||||
if (StringUtils.isBlank(json)) {
|
||||
throw new InvalidJsonException();
|
||||
}
|
||||
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
||||
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, PARAMS);
|
||||
final JsonElement element = this.fromApiJsonHelper.parse(json);
|
||||
|
||||
final List<ApiParameterError> errors = new ArrayList<>();
|
||||
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(RESOURCE);
|
||||
|
||||
v.reset().parameter(SCRIPT_ID).value(this.fromApiJsonHelper.extractLongNamed(SCRIPT_ID, element)).notNull().longGreaterThanZero();
|
||||
v.reset().parameter(RATE_DATE).value(this.fromApiJsonHelper.extractLocalDateNamed(RATE_DATE, element)).notNull();
|
||||
final BigDecimal close = amount(CLOSE_RATE, element);
|
||||
final BigDecimal open = amount(OPEN_RATE, element);
|
||||
final BigDecimal high = amount(HIGH_RATE, element);
|
||||
final BigDecimal low = amount(LOW_RATE, element);
|
||||
v.reset().parameter(CLOSE_RATE).value(close).notNull().positiveAmount();
|
||||
v.reset().parameter(OPEN_RATE).value(open).ignoreIfNull().positiveAmount();
|
||||
v.reset().parameter(HIGH_RATE).value(high).ignoreIfNull().positiveAmount();
|
||||
v.reset().parameter(LOW_RATE).value(low).ignoreIfNull().positiveAmount();
|
||||
|
||||
if (high != null && low != null && high.compareTo(low) < 0) {
|
||||
v.reset().parameter(HIGH_RATE).failWithCodeAndMessage("less.than.low.rate", "High Rate cannot be below Low Rate");
|
||||
}
|
||||
outsideRange(v, CLOSE_RATE, "Close Rate", close, low, high);
|
||||
outsideRange(v, OPEN_RATE, "Open Rate", open, low, high);
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors);
|
||||
}
|
||||
}
|
||||
|
||||
private static void outsideRange(final DataValidatorBuilder v, final String param, final String label, final BigDecimal rate,
|
||||
final BigDecimal low, final BigDecimal high) {
|
||||
if (rate == null) {
|
||||
return;
|
||||
}
|
||||
if (low != null && rate.compareTo(low) < 0) {
|
||||
v.reset().parameter(param).failWithCodeAndMessage("below.low.rate", label + " cannot be below Low Rate");
|
||||
} else if (high != null && rate.compareTo(high) > 0) {
|
||||
v.reset().parameter(param).failWithCodeAndMessage("above.high.rate", label + " cannot be above High Rate");
|
||||
}
|
||||
}
|
||||
|
||||
private BigDecimal amount(final String param, final JsonElement element) {
|
||||
return this.fromApiJsonHelper.parameterExists(param, element)
|
||||
? this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(param, element)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
-73
@@ -1,73 +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.serialization;
|
||||
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentInterestApiConstants.BANK_GL_ACCOUNT_ID;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentInterestApiConstants.INT_DATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentInterestApiConstants.LINE_TYPE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentInterestApiConstants.PARAMS;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentInterestApiConstants.RESOURCE;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.portfolio.investment.service.InvestmentInterestCalculator;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/** Validates a request to post one interest schedule line. */
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public final class InvestmentInterestCommandFromApiJsonDeserializer {
|
||||
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
|
||||
public void validateForPost(final String json) {
|
||||
if (StringUtils.isBlank(json)) {
|
||||
throw new InvalidJsonException();
|
||||
}
|
||||
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
||||
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, PARAMS);
|
||||
final JsonElement element = this.fromApiJsonHelper.parse(json);
|
||||
|
||||
final List<ApiParameterError> errors = new ArrayList<>();
|
||||
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(RESOURCE);
|
||||
final String lineType = this.fromApiJsonHelper.extractStringNamed(LINE_TYPE, element);
|
||||
v.reset().parameter(LINE_TYPE).value(lineType).notBlank();
|
||||
if (StringUtils.isNotBlank(lineType)) {
|
||||
v.reset().parameter(LINE_TYPE).value(lineType).isOneOfTheseValues(InvestmentInterestCalculator.TYPE_COUPON,
|
||||
InvestmentInterestCalculator.TYPE_PROVISION);
|
||||
}
|
||||
v.reset().parameter(INT_DATE).value(this.fromApiJsonHelper.extractLocalDateNamed(INT_DATE, element)).notNull();
|
||||
v.reset().parameter(BANK_GL_ACCOUNT_ID).value(this.fromApiJsonHelper.extractLongNamed(BANK_GL_ACCOUNT_ID, element)).ignoreIfNull()
|
||||
.longGreaterThanZero();
|
||||
if (!errors.isEmpty()) {
|
||||
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
-264
@@ -1,264 +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.serialization;
|
||||
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ADDRESS1;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ADDRESS2;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ADDRESS3;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.BANK_TYPE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.BOND_TYPE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.BOND_TYPES;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CITY;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.COMPANY_NAME;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CORP_CODE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CP_ACCOUNT_NO;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CP_BANK_NAME;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CP_BRANCH_NAME;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CP_IFSC;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.DEBT_BOND_TYPES;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.EXPOSURE_LIMIT;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.FACE_VALUE_PER_UNIT;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.FAX;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.GL_ACCOUNT_ID;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.INTEREST_GL_ACCOUNT_ID;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.INT_RATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ISIN;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ISSUE_DATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.LEI_NO;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.MATURITY_DATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PARTY_NAME;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PARTY_PARAMS;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PHONE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PINCODE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.RBI_CODE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.RESOURCE_PARTY;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.RESOURCE_SCRIPT;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SCRIPT_DESC;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SCRIPT_PARAMS;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SLR_TYPE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SLR_TYPES;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import java.lang.reflect.Type;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/** Validates Bank / Party Master and Script Master requests before anything is written. */
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public final class InvestmentMasterCommandFromApiJsonDeserializer {
|
||||
|
||||
private static final String IFSC_PATTERN = "^[A-Z]{4}0[A-Z0-9]{6}$";
|
||||
private static final String PINCODE_PATTERN = "^[0-9]{6}$";
|
||||
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
|
||||
// -------------------- Bank / Party Master --------------------
|
||||
|
||||
public void validateForCreateParty(final String json) {
|
||||
validateParty(json, true);
|
||||
}
|
||||
|
||||
public void validateForUpdateParty(final String json) {
|
||||
validateParty(json, false);
|
||||
}
|
||||
|
||||
private void validateParty(final String json, final boolean create) {
|
||||
final JsonElement element = parse(json, PARTY_PARAMS);
|
||||
final List<ApiParameterError> errors = new ArrayList<>();
|
||||
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(RESOURCE_PARTY);
|
||||
|
||||
validateBondType(element, v, create);
|
||||
validateRequiredString(element, v, PARTY_NAME, 150, create);
|
||||
validateGlAccountId(element, v, create);
|
||||
if (this.fromApiJsonHelper.parameterExists(INTEREST_GL_ACCOUNT_ID, element)) {
|
||||
v.reset().parameter(INTEREST_GL_ACCOUNT_ID).value(this.fromApiJsonHelper.extractLongNamed(INTEREST_GL_ACCOUNT_ID, element))
|
||||
.ignoreIfNull().longGreaterThanZero();
|
||||
}
|
||||
validateAddress(element, v);
|
||||
validateOptionalString(element, v, BANK_TYPE, 60);
|
||||
validateOptionalString(element, v, RBI_CODE, 30);
|
||||
validateOptionalString(element, v, LEI_NO, 30);
|
||||
validateOptionalString(element, v, CP_BANK_NAME, 150);
|
||||
validateOptionalString(element, v, CP_BRANCH_NAME, 150);
|
||||
validateOptionalString(element, v, CP_ACCOUNT_NO, 40);
|
||||
|
||||
final String ifsc = this.fromApiJsonHelper.extractStringNamed(CP_IFSC, element);
|
||||
if (StringUtils.isNotBlank(ifsc)) {
|
||||
v.reset().parameter(CP_IFSC).value(ifsc.trim().toUpperCase()).matchesRegularExpression(IFSC_PATTERN,
|
||||
"IFSC must be 11 characters: 4 letters, 0, then 6 letters/digits");
|
||||
}
|
||||
|
||||
if (this.fromApiJsonHelper.parameterExists(EXPOSURE_LIMIT, element)) {
|
||||
final BigDecimal limit = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(EXPOSURE_LIMIT, element);
|
||||
v.reset().parameter(EXPOSURE_LIMIT).value(limit).ignoreIfNull().zeroOrPositiveAmount();
|
||||
}
|
||||
throwIfErrors(errors);
|
||||
}
|
||||
|
||||
// -------------------- Script Master --------------------
|
||||
|
||||
public void validateForCreateScript(final String json) {
|
||||
validateScript(json, true);
|
||||
}
|
||||
|
||||
public void validateForUpdateScript(final String json, final String existingBondType) {
|
||||
validateScript(json, false, existingBondType);
|
||||
}
|
||||
|
||||
private void validateScript(final String json, final boolean create) {
|
||||
validateScript(json, create, null);
|
||||
}
|
||||
|
||||
private void validateScript(final String json, final boolean create, final String existingBondType) {
|
||||
final JsonElement element = parse(json, SCRIPT_PARAMS);
|
||||
final List<ApiParameterError> errors = new ArrayList<>();
|
||||
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(RESOURCE_SCRIPT);
|
||||
|
||||
validateBondType(element, v, create);
|
||||
validateRequiredString(element, v, SCRIPT_DESC, 150, create);
|
||||
validateGlAccountId(element, v, create);
|
||||
|
||||
final String slrType = this.fromApiJsonHelper.extractStringNamed(SLR_TYPE, element);
|
||||
if (StringUtils.isNotBlank(slrType)) {
|
||||
v.reset().parameter(SLR_TYPE).value(slrType).isOneOfTheseValues(SLR_TYPES.toArray());
|
||||
}
|
||||
|
||||
final String bondType = create ? this.fromApiJsonHelper.extractStringNamed(BOND_TYPE, element) : existingBondType;
|
||||
final boolean debt = bondType != null && DEBT_BOND_TYPES.contains(bondType);
|
||||
|
||||
final String isin = this.fromApiJsonHelper.extractStringNamed(ISIN, element);
|
||||
if (create && debt) {
|
||||
v.reset().parameter(ISIN).value(isin).notBlank();
|
||||
}
|
||||
v.reset().parameter(ISIN).value(isin).ignoreIfNull().notExceedingLengthOf(20);
|
||||
|
||||
if (this.fromApiJsonHelper.parameterExists(INT_RATE, element) || (create && debt)) {
|
||||
final BigDecimal intRate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(INT_RATE, element);
|
||||
final DataValidatorBuilder rate = v.reset().parameter(INT_RATE).value(intRate);
|
||||
if (create && debt) {
|
||||
rate.notNull();
|
||||
}
|
||||
rate.ignoreIfNull().zeroOrPositiveAmount();
|
||||
}
|
||||
|
||||
if (this.fromApiJsonHelper.parameterExists(FACE_VALUE_PER_UNIT, element)) {
|
||||
final BigDecimal fv = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(FACE_VALUE_PER_UNIT, element);
|
||||
v.reset().parameter(FACE_VALUE_PER_UNIT).value(fv).ignoreIfNull().positiveAmount();
|
||||
}
|
||||
|
||||
LocalDate issueDate = null;
|
||||
if (this.fromApiJsonHelper.parameterExists(ISSUE_DATE, element) || (create && debt)) {
|
||||
issueDate = this.fromApiJsonHelper.extractLocalDateNamed(ISSUE_DATE, element);
|
||||
if (create && debt) {
|
||||
v.reset().parameter(ISSUE_DATE).value(issueDate).notNull();
|
||||
}
|
||||
}
|
||||
if (this.fromApiJsonHelper.parameterExists(MATURITY_DATE, element) || (create && debt)) {
|
||||
final LocalDate maturityDate = this.fromApiJsonHelper.extractLocalDateNamed(MATURITY_DATE, element);
|
||||
final DataValidatorBuilder maturity = v.reset().parameter(MATURITY_DATE).value(maturityDate);
|
||||
if (debt) {
|
||||
// Interest, sale, redemption and amortisation all run to the maturity date.
|
||||
maturity.notNull();
|
||||
}
|
||||
maturity.ignoreIfNull().validateDateAfter(issueDate);
|
||||
}
|
||||
|
||||
validateOptionalString(element, v, COMPANY_NAME, 150);
|
||||
validateOptionalString(element, v, CORP_CODE, 30);
|
||||
validateAddress(element, v);
|
||||
throwIfErrors(errors);
|
||||
}
|
||||
|
||||
// -------------------- shared --------------------
|
||||
|
||||
private void validateBondType(final JsonElement element, final DataValidatorBuilder v, final boolean required) {
|
||||
final String bondType = this.fromApiJsonHelper.extractStringNamed(BOND_TYPE, element);
|
||||
if (required) {
|
||||
v.reset().parameter(BOND_TYPE).value(bondType).notBlank();
|
||||
}
|
||||
if (StringUtils.isNotBlank(bondType)) {
|
||||
v.reset().parameter(BOND_TYPE).value(bondType).isOneOfTheseValues(BOND_TYPES.toArray());
|
||||
}
|
||||
}
|
||||
|
||||
private void validateGlAccountId(final JsonElement element, final DataValidatorBuilder v, final boolean required) {
|
||||
if (!required && !this.fromApiJsonHelper.parameterExists(GL_ACCOUNT_ID, element)) {
|
||||
return;
|
||||
}
|
||||
final Long glAccountId = this.fromApiJsonHelper.extractLongNamed(GL_ACCOUNT_ID, element);
|
||||
v.reset().parameter(GL_ACCOUNT_ID).value(glAccountId).notNull().longGreaterThanZero();
|
||||
}
|
||||
|
||||
private void validateRequiredString(final JsonElement element, final DataValidatorBuilder v, final String param, final int maxLength,
|
||||
final boolean required) {
|
||||
if (!required && !this.fromApiJsonHelper.parameterExists(param, element)) {
|
||||
return;
|
||||
}
|
||||
final String value = this.fromApiJsonHelper.extractStringNamed(param, element);
|
||||
v.reset().parameter(param).value(value).notBlank().notExceedingLengthOf(maxLength);
|
||||
}
|
||||
|
||||
private void validateOptionalString(final JsonElement element, final DataValidatorBuilder v, final String param,
|
||||
final int maxLength) {
|
||||
final String value = this.fromApiJsonHelper.extractStringNamed(param, element);
|
||||
v.reset().parameter(param).value(value).ignoreIfNull().notExceedingLengthOf(maxLength);
|
||||
}
|
||||
|
||||
private void validateAddress(final JsonElement element, final DataValidatorBuilder v) {
|
||||
validateOptionalString(element, v, ADDRESS1, 150);
|
||||
validateOptionalString(element, v, ADDRESS2, 150);
|
||||
validateOptionalString(element, v, ADDRESS3, 150);
|
||||
validateOptionalString(element, v, CITY, 100);
|
||||
validateOptionalString(element, v, PHONE, 20);
|
||||
validateOptionalString(element, v, FAX, 20);
|
||||
final String pincode = this.fromApiJsonHelper.extractStringNamed(PINCODE, element);
|
||||
if (StringUtils.isNotBlank(pincode)) {
|
||||
v.reset().parameter(PINCODE).value(pincode.trim()).matchesRegularExpression(PINCODE_PATTERN, "Pincode must be 6 digits");
|
||||
}
|
||||
}
|
||||
|
||||
private JsonElement parse(final String json, final Set<String> supportedParams) {
|
||||
if (StringUtils.isBlank(json)) {
|
||||
throw new InvalidJsonException();
|
||||
}
|
||||
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
||||
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, supportedParams);
|
||||
return this.fromApiJsonHelper.parse(json);
|
||||
}
|
||||
|
||||
private void throwIfErrors(final List<ApiParameterError> errors) {
|
||||
if (!errors.isEmpty()) {
|
||||
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
-142
@@ -1,142 +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.serialization;
|
||||
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.BOND_TYPE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.BROKERAGE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.CATEGORIES;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.CATEGORY;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.CONSIDER_AMT;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.CR_GL_ACCOUNT_ID;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.DEAL_DATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.DEAL_TIME;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.FACE_VALUE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.LAST_INT_DATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.NAV;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.OTHER_CHARGES;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.OTHER_DETAILS;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.PARAMS;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.PARTY_ID;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.PURCHASE_RATE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.RESOURCE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SCRIPT_ID;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SETTLEMENT_TYPE;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SETTLEMENT_TYPES;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.STAMP_DUTY;
|
||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SUB_TYPE;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import java.lang.reflect.Type;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.portfolio.investment.api.InvestmentApiConstants;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/** Validates a Purchase Entry before anything is computed or written. */
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public final class InvestmentPurchaseCommandFromApiJsonDeserializer {
|
||||
|
||||
private static final String TIME_PATTERN = "^([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$";
|
||||
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
|
||||
public void validateForCreate(final String json) {
|
||||
if (StringUtils.isBlank(json)) {
|
||||
throw new InvalidJsonException();
|
||||
}
|
||||
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
||||
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, PARAMS);
|
||||
final JsonElement element = this.fromApiJsonHelper.parse(json);
|
||||
|
||||
final List<ApiParameterError> errors = new ArrayList<>();
|
||||
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(RESOURCE);
|
||||
|
||||
final String bondType = this.fromApiJsonHelper.extractStringNamed(BOND_TYPE, element);
|
||||
v.reset().parameter(BOND_TYPE).value(bondType).notBlank();
|
||||
if (StringUtils.isNotBlank(bondType)) {
|
||||
v.reset().parameter(BOND_TYPE).value(bondType).isOneOfTheseValues(InvestmentApiConstants.BOND_TYPES.toArray());
|
||||
}
|
||||
v.reset().parameter(SCRIPT_ID).value(this.fromApiJsonHelper.extractLongNamed(SCRIPT_ID, element)).notNull().longGreaterThanZero();
|
||||
v.reset().parameter(PARTY_ID).value(this.fromApiJsonHelper.extractLongNamed(PARTY_ID, element)).notNull().longGreaterThanZero();
|
||||
v.reset().parameter(CR_GL_ACCOUNT_ID).value(this.fromApiJsonHelper.extractLongNamed(CR_GL_ACCOUNT_ID, element)).notNull()
|
||||
.longGreaterThanZero();
|
||||
v.reset().parameter(DEAL_DATE).value(this.fromApiJsonHelper.extractLocalDateNamed(DEAL_DATE, element)).notNull();
|
||||
|
||||
final String settlementType = this.fromApiJsonHelper.extractStringNamed(SETTLEMENT_TYPE, element);
|
||||
v.reset().parameter(SETTLEMENT_TYPE).value(settlementType).notBlank();
|
||||
if (StringUtils.isNotBlank(settlementType)) {
|
||||
v.reset().parameter(SETTLEMENT_TYPE).value(settlementType).isOneOfTheseValues(SETTLEMENT_TYPES.toArray());
|
||||
}
|
||||
|
||||
final String category = this.fromApiJsonHelper.extractStringNamed(CATEGORY, element);
|
||||
v.reset().parameter(CATEGORY).value(category).notBlank();
|
||||
if (StringUtils.isNotBlank(category)) {
|
||||
v.reset().parameter(CATEGORY).value(category).isOneOfTheseValues(CATEGORIES.toArray());
|
||||
}
|
||||
|
||||
if ("MUTUAL_FUND".equals(bondType)) {
|
||||
v.reset().parameter(CONSIDER_AMT).value(amount(CONSIDER_AMT, element)).notNull().positiveAmount();
|
||||
v.reset().parameter(NAV).value(amount(NAV, element)).notNull().positiveAmount();
|
||||
} else {
|
||||
v.reset().parameter(FACE_VALUE).value(amount(FACE_VALUE, element)).notNull().positiveAmount();
|
||||
v.reset().parameter(PURCHASE_RATE).value(amount(PURCHASE_RATE, element)).notNull().positiveAmount();
|
||||
v.reset().parameter(LAST_INT_DATE).value(this.fromApiJsonHelper.extractLocalDateNamed(LAST_INT_DATE, element)).notNull();
|
||||
}
|
||||
v.reset().parameter(BROKERAGE).value(amount(BROKERAGE, element)).ignoreIfNull().zeroOrPositiveAmount();
|
||||
v.reset().parameter(STAMP_DUTY).value(amount(STAMP_DUTY, element)).ignoreIfNull().zeroOrPositiveAmount();
|
||||
v.reset().parameter(OTHER_CHARGES).value(amount(OTHER_CHARGES, element)).ignoreIfNull().zeroOrPositiveAmount();
|
||||
|
||||
final String dealTime = this.fromApiJsonHelper.extractStringNamed(DEAL_TIME, element);
|
||||
if (StringUtils.isNotBlank(dealTime)) {
|
||||
v.reset().parameter(DEAL_TIME).value(dealTime).matchesRegularExpression(TIME_PATTERN, "Deal Time must be HH:mm:ss (24 hr)");
|
||||
}
|
||||
v.reset().parameter(SUB_TYPE).value(this.fromApiJsonHelper.extractStringNamed(SUB_TYPE, element)).ignoreIfNull()
|
||||
.notExceedingLengthOf(40);
|
||||
|
||||
if (this.fromApiJsonHelper.parameterExists(OTHER_DETAILS, element)) {
|
||||
final JsonElement other = element.getAsJsonObject().get(OTHER_DETAILS);
|
||||
if (!other.isJsonNull() && !other.isJsonObject()) {
|
||||
v.reset().parameter(OTHER_DETAILS).failWithCode("must.be.an.object");
|
||||
} else if (other.isJsonObject() && other.toString().length() > 4000) {
|
||||
v.reset().parameter(OTHER_DETAILS).failWithCode("too.long");
|
||||
}
|
||||
}
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors);
|
||||
}
|
||||
}
|
||||
|
||||
private BigDecimal amount(final String param, final JsonElement element) {
|
||||
return this.fromApiJsonHelper.parameterExists(param, element)
|
||||
? this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(param, element)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user