Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a320b9c09 | ||
|
|
8ba0420398 | ||
|
|
6be32272c1 | ||
|
|
b47a34b5ef | ||
|
|
04d1eb20f0 |
-144
@@ -95,150 +95,6 @@ public class CommandWrapperBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CommandWrapperBuilder createInvestmentParty() {
|
|
||||||
this.actionName = "CREATE";
|
|
||||||
this.entityName = "INVESTMENTPARTY";
|
|
||||||
this.entityId = null;
|
|
||||||
this.href = "/investment/parties";
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder updateInvestmentParty(final Long partyId) {
|
|
||||||
this.actionName = "UPDATE";
|
|
||||||
this.entityName = "INVESTMENTPARTY";
|
|
||||||
this.entityId = partyId;
|
|
||||||
this.href = "/investment/parties/" + partyId;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder deleteInvestmentParty(final Long partyId) {
|
|
||||||
this.actionName = "DELETE";
|
|
||||||
this.entityName = "INVESTMENTPARTY";
|
|
||||||
this.entityId = partyId;
|
|
||||||
this.href = "/investment/parties/" + partyId;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder createInvestmentScript() {
|
|
||||||
this.actionName = "CREATE";
|
|
||||||
this.entityName = "INVESTMENTSCRIPT";
|
|
||||||
this.entityId = null;
|
|
||||||
this.href = "/investment/scripts";
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder updateInvestmentScript(final Long scriptId) {
|
|
||||||
this.actionName = "UPDATE";
|
|
||||||
this.entityName = "INVESTMENTSCRIPT";
|
|
||||||
this.entityId = scriptId;
|
|
||||||
this.href = "/investment/scripts/" + scriptId;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder createInvestmentPurchase() {
|
|
||||||
this.actionName = "CREATE";
|
|
||||||
this.entityName = "INVESTMENTPURCHASE";
|
|
||||||
this.entityId = null;
|
|
||||||
this.href = "/investment/purchases";
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder deleteInvestmentPurchase(final Long purchaseId) {
|
|
||||||
this.actionName = "DELETE";
|
|
||||||
this.entityName = "INVESTMENTPURCHASE";
|
|
||||||
this.entityId = purchaseId;
|
|
||||||
this.href = "/investment/purchases/" + purchaseId;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder settleInvestmentPurchase(final Long purchaseId) {
|
|
||||||
this.actionName = "SETTLE";
|
|
||||||
this.entityName = "INVESTMENTPURCHASE";
|
|
||||||
this.entityId = purchaseId;
|
|
||||||
this.href = "/investment/purchases/" + purchaseId + "/settle";
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder createInvestmentSale() {
|
|
||||||
this.actionName = "CREATE";
|
|
||||||
this.entityName = "INVESTMENTSALE";
|
|
||||||
this.entityId = null;
|
|
||||||
this.href = "/investment/sales";
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder settleInvestmentSale(final Long saleId) {
|
|
||||||
this.actionName = "SETTLE";
|
|
||||||
this.entityName = "INVESTMENTSALE";
|
|
||||||
this.entityId = saleId;
|
|
||||||
this.href = "/investment/sales/" + saleId + "/settle";
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder deleteInvestmentSale(final Long saleId) {
|
|
||||||
this.actionName = "DELETE";
|
|
||||||
this.entityName = "INVESTMENTSALE";
|
|
||||||
this.entityId = saleId;
|
|
||||||
this.href = "/investment/sales/" + saleId;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder amortiseInvestmentPremium() {
|
|
||||||
this.actionName = "AMORTISE";
|
|
||||||
this.entityName = "INVESTMENTSALE";
|
|
||||||
this.entityId = null;
|
|
||||||
this.href = "/investment/amortisation";
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder writeOffInvestmentPurchase(final Long purchaseId) {
|
|
||||||
this.actionName = "WRITEOFF";
|
|
||||||
this.entityName = "INVESTMENTSALE";
|
|
||||||
this.entityId = purchaseId;
|
|
||||||
this.href = "/investment/purchases/" + purchaseId + "/write-off";
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder redeemInvestmentPurchase(final Long purchaseId) {
|
|
||||||
this.actionName = "REDEEM";
|
|
||||||
this.entityName = "INVESTMENTSALE";
|
|
||||||
this.entityId = purchaseId;
|
|
||||||
this.href = "/investment/purchases/" + purchaseId + "/redeem";
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder transferInvestmentSecurity(final Long purchaseId) {
|
|
||||||
this.actionName = "CREATE";
|
|
||||||
this.entityName = "INVESTMENTTRANSFER";
|
|
||||||
this.entityId = purchaseId;
|
|
||||||
this.href = "/investment/purchases/" + purchaseId + "/transfer";
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder postInvestmentInterest(final Long purchaseId) {
|
|
||||||
this.actionName = "CREATE";
|
|
||||||
this.entityName = "INVESTMENTINTEREST";
|
|
||||||
this.entityId = purchaseId;
|
|
||||||
this.href = "/investment/purchases/" + purchaseId + "/interest-postings";
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder createInvestmentDailyRate() {
|
|
||||||
this.actionName = "CREATE";
|
|
||||||
this.entityName = "INVESTMENTDAILYRATE";
|
|
||||||
this.entityId = null;
|
|
||||||
this.href = "/investment/daily-rates";
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder deleteInvestmentDailyRate(final Long rateId) {
|
|
||||||
this.actionName = "DELETE";
|
|
||||||
this.entityName = "INVESTMENTDAILYRATE";
|
|
||||||
this.entityId = rateId;
|
|
||||||
this.href = "/investment/daily-rates/" + rateId;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandWrapperBuilder createChequeStockLot() {
|
public CommandWrapperBuilder createChequeStockLot() {
|
||||||
this.actionName = "CREATE";
|
this.actionName = "CREATE";
|
||||||
this.entityName = "CHEQUESTOCKLOT";
|
this.entityName = "CHEQUESTOCKLOT";
|
||||||
|
|||||||
-13
@@ -46,7 +46,6 @@ import org.apache.fineract.infrastructure.core.service.DateUtils;
|
|||||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
import org.apache.fineract.portfolio.cashreport.service.VaultPositionService;
|
import org.apache.fineract.portfolio.cashreport.service.VaultPositionService;
|
||||||
import org.apache.fineract.portfolio.denominationmaster.service.CountedCashLedger;
|
import org.apache.fineract.portfolio.denominationmaster.service.CountedCashLedger;
|
||||||
import org.apache.fineract.portfolio.investment.service.InvestmentPurchaseReadPlatformService;
|
|
||||||
import org.apache.fineract.portfolio.savings.dailyinterest.SavingsDailyInterestOrchestrator;
|
import org.apache.fineract.portfolio.savings.dailyinterest.SavingsDailyInterestOrchestrator;
|
||||||
import org.apache.fineract.portfolio.savings.dailyinterest.SavingsInterestPostingReportService;
|
import org.apache.fineract.portfolio.savings.dailyinterest.SavingsInterestPostingReportService;
|
||||||
import org.apache.fineract.portfolio.savings.fdinterest.FixedDepositInterestPostingOrchestrator;
|
import org.apache.fineract.portfolio.savings.fdinterest.FixedDepositInterestPostingOrchestrator;
|
||||||
@@ -96,15 +95,8 @@ public class BranchDayRegisterApiResource {
|
|||||||
private final SavingsDailyInterestOrchestrator savingsDailyInterestOrchestrator;
|
private final SavingsDailyInterestOrchestrator savingsDailyInterestOrchestrator;
|
||||||
private final FixedDepositInterestPostingReportService fdInterestPostingReportService;
|
private final FixedDepositInterestPostingReportService fdInterestPostingReportService;
|
||||||
private final FixedDepositInterestPostingOrchestrator fdInterestPostingOrchestrator;
|
private final FixedDepositInterestPostingOrchestrator fdInterestPostingOrchestrator;
|
||||||
private final InvestmentPurchaseReadPlatformService investmentPurchaseReadPlatformService;
|
|
||||||
private final Gson gson = new Gson();
|
private final Gson gson = new Gson();
|
||||||
|
|
||||||
private boolean isHeadOffice(final Long officeId) {
|
|
||||||
final Integer topLevel = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM m_office WHERE id = ? AND parent_id IS NULL",
|
|
||||||
Integer.class, officeId);
|
|
||||||
return topLevel != null && topLevel > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── GET /v1/branchday ── the register itself ──────────────────────────────
|
// ── GET /v1/branchday ── the register itself ──────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -268,11 +260,6 @@ public class BranchDayRegisterApiResource {
|
|||||||
final Map<String, Object> fdInterest = fdInterestPostingReportService.dayCloseInterestView(officeId, businessDate);
|
final Map<String, Object> fdInterest = fdInterestPostingReportService.dayCloseInterestView(officeId, businessDate);
|
||||||
result.put("fdInterestPending", fdInterest.get("pending"));
|
result.put("fdInterestPending", fdInterest.get("pending"));
|
||||||
result.put("fdInterestDue", fdInterest.get("due"));
|
result.put("fdInterestDue", fdInterest.get("due"));
|
||||||
// Investment interest vouchers (coupon / provision) whose date has come but which are not posted yet.
|
|
||||||
// A reminder only: it never stops the close (Jitendra Sir). Investments are booked at Head Office, so
|
|
||||||
// only the Head Office close shows it.
|
|
||||||
result.put("investmentInterestPending",
|
|
||||||
isHeadOffice(officeId) ? investmentPurchaseReadPlatformService.retrieveDueInterest(0) : List.of());
|
|
||||||
|
|
||||||
return gson.toJson(result);
|
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.infrastructure.security.service.PlatformSecurityContext;
|
||||||
import org.apache.fineract.portfolio.client.domain.KycExpiryStage;
|
import org.apache.fineract.portfolio.client.domain.KycExpiryStage;
|
||||||
import org.apache.fineract.portfolio.client.service.KycRestrictionGuard;
|
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.VoucherLine;
|
||||||
import org.apache.fineract.portfolio.voucher.service.VoucherService;
|
import org.apache.fineract.portfolio.voucher.service.VoucherService;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -98,6 +99,7 @@ public class CashierDepositRequestApiResource {
|
|||||||
private final PlatformSecurityContext context;
|
private final PlatformSecurityContext context;
|
||||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||||
private final VoucherService voucherService;
|
private final VoucherService voucherService;
|
||||||
|
private final FixedDepositFundingValidator fixedDepositFundingValidator;
|
||||||
private final Gson gson = new GsonBuilder().serializeNulls().create();
|
private final Gson gson = new GsonBuilder().serializeNulls().create();
|
||||||
|
|
||||||
// ── POST /v1/cashier/deposit-requests ── Cashier submits new deposit request ──
|
// ── 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");
|
throw apiError(400, "Amount must be greater than zero");
|
||||||
}
|
}
|
||||||
kycRestrictionGuard.check(savingsAccountId, KycRestrictionGuard.Operation.CREDIT);
|
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);
|
validateDenominationTotal(denominationData, amount);
|
||||||
validateDdLimit(ddFaceValue);
|
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.cashreport.service.VaultPositionService;
|
||||||
import org.apache.fineract.portfolio.denominationmaster.service.CountedCashLedger;
|
import org.apache.fineract.portfolio.denominationmaster.service.CountedCashLedger;
|
||||||
import org.apache.fineract.portfolio.denominationmaster.service.DenominationBreakdownWriter;
|
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.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
@@ -36,6 +37,7 @@ public class CashReportApiResource {
|
|||||||
private final DenominationBreakdownWriter denominationBreakdownWriter;
|
private final DenominationBreakdownWriter denominationBreakdownWriter;
|
||||||
private final CashierRoleHelper cashierRoleHelper;
|
private final CashierRoleHelper cashierRoleHelper;
|
||||||
private final VaultPositionService vaultPositionService;
|
private final VaultPositionService vaultPositionService;
|
||||||
|
private final VoucherService voucherService;
|
||||||
private final Gson gson = new Gson();
|
private final Gson gson = new Gson();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -329,7 +331,18 @@ public class CashReportApiResource {
|
|||||||
BigDecimal priorCreditTotal = BigDecimal.ZERO;
|
BigDecimal priorCreditTotal = BigDecimal.ZERO;
|
||||||
BigDecimal priorDebitTotal = 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) {
|
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"))) {
|
if (sideFilter != null && !sideFilter.equals(row.get("side"))) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -350,7 +363,7 @@ public class CashReportApiResource {
|
|||||||
// was already earned, and the day it belongs to has been counted and signed off. Adding
|
// 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.
|
// 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"));
|
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))) {
|
|| "WITHDRAWN".equalsIgnoreCase(rowStatus) || "CLOSED".equalsIgnoreCase(rowStatus))) {
|
||||||
final BigDecimal amount = (BigDecimal) row.get("amount");
|
final BigDecimal amount = (BigDecimal) row.get("amount");
|
||||||
if (amount != null) {
|
if (amount != null) {
|
||||||
|
|||||||
+186
-10
@@ -57,9 +57,23 @@ public class CashTransferApiResource {
|
|||||||
private final VaultPositionService vaultPositionService;
|
private final VaultPositionService vaultPositionService;
|
||||||
private final Gson gson = new Gson();
|
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
|
@POST
|
||||||
public String create(String body) {
|
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();
|
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
||||||
|
|
||||||
final LocalDate transferDate = json.has("transferDate") && !json.get("transferDate").isJsonNull()
|
final LocalDate transferDate = json.has("transferDate") && !json.get("transferDate").isJsonNull()
|
||||||
@@ -102,6 +116,9 @@ public class CashTransferApiResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
validateVaultHasSufficientCash(fromTellerId, total, denominations, transferDate);
|
validateVaultHasSufficientCash(fromTellerId, total, denominations, transferDate);
|
||||||
|
if (dryRun) {
|
||||||
|
return "{}";
|
||||||
|
}
|
||||||
|
|
||||||
CashTransfer header = new CashTransfer();
|
CashTransfer header = new CashTransfer();
|
||||||
header.setTransferDate(transferDate);
|
header.setTransferDate(transferDate);
|
||||||
@@ -112,15 +129,9 @@ public class CashTransferApiResource {
|
|||||||
header.setTransferAmount(total);
|
header.setTransferAmount(total);
|
||||||
header.setNote(note);
|
header.setNote(note);
|
||||||
header.setCreatedAt(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()));
|
header.setCreatedAt(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()));
|
||||||
try {
|
// Recorded against the person who raised the request, not the one who approved it.
|
||||||
AppUser user = this.context.authenticatedUser();
|
header.setPerformedBy(performedBy);
|
||||||
if (user != null) {
|
header.setPerformedByUsername(performedByUsername);
|
||||||
header.setPerformedBy(user.getId());
|
|
||||||
header.setPerformedByUsername(user.getUsername());
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
// resolving the current user must never block recording the transfer
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save header + details in one committed transaction, then GL dispatch separately.
|
// Save header + details in one committed transaction, then GL dispatch separately.
|
||||||
// This prevents EclipseLink from flushing pending detail rows inside Fineract's
|
// This prevents EclipseLink from flushing pending detail rows inside Fineract's
|
||||||
@@ -159,6 +170,171 @@ public class CashTransferApiResource {
|
|||||||
return gson.toJson(result);
|
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
|
@GET
|
||||||
public String getAll(
|
public String getAll(
|
||||||
@QueryParam("offset") @jakarta.ws.rs.DefaultValue("0") int offset,
|
@QueryParam("offset") @jakarta.ws.rs.DefaultValue("0") int offset,
|
||||||
|
|||||||
+81
@@ -69,6 +69,8 @@ import org.apache.fineract.infrastructure.security.service.PlatformSecurityConte
|
|||||||
import org.apache.fineract.portfolio.PortfolioProductType;
|
import org.apache.fineract.portfolio.PortfolioProductType;
|
||||||
import org.apache.fineract.portfolio.voucher.service.VoucherLine;
|
import org.apache.fineract.portfolio.voucher.service.VoucherLine;
|
||||||
import org.apache.fineract.portfolio.client.service.KycRestrictionGuard;
|
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.portfolio.voucher.service.VoucherService;
|
||||||
import org.apache.fineract.useradministration.domain.AppUser;
|
import org.apache.fineract.useradministration.domain.AppUser;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
@@ -90,6 +92,8 @@ public class ClerkDisbursementRequestApiResource {
|
|||||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||||
private final FineractRequestContextHolder fineractRequestContextHolder;
|
private final FineractRequestContextHolder fineractRequestContextHolder;
|
||||||
private final VoucherService voucherService;
|
private final VoucherService voucherService;
|
||||||
|
private final FixedDepositFundingValidator fixedDepositFundingValidator;
|
||||||
|
private final DepositAccountWritePlatformService depositAccountWritePlatformService;
|
||||||
private final Gson gson = new GsonBuilder().serializeNulls().create();
|
private final Gson gson = new GsonBuilder().serializeNulls().create();
|
||||||
|
|
||||||
// ── POST /v1/clerk/disbursement-requests ── Clerk submits new request ────────
|
// ── 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)
|
// Validate balance when both DR and CR entries present (collection mode)
|
||||||
if (totalDr.compareTo(BigDecimal.ZERO) > 0 && totalCr.compareTo(BigDecimal.ZERO) > 0
|
if (totalDr.compareTo(BigDecimal.ZERO) > 0 && totalCr.compareTo(BigDecimal.ZERO) > 0
|
||||||
&& totalDr.subtract(totalCr).abs().compareTo(new BigDecimal("0.01")) > 0) {
|
&& totalDr.subtract(totalCr).abs().compareTo(new BigDecimal("0.01")) > 0) {
|
||||||
@@ -491,6 +498,10 @@ public class ClerkDisbursementRequestApiResource {
|
|||||||
rejectRecurringDepositLeg(entry, entryType);
|
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
|
// Atomic PENDING → PROCESSING lock: prevents two managers from concurrently
|
||||||
// posting duplicate Fineract transactions for the same request.
|
// posting duplicate Fineract transactions for the same request.
|
||||||
@@ -773,6 +784,12 @@ public class ClerkDisbursementRequestApiResource {
|
|||||||
}
|
}
|
||||||
} // end else (disbursement/collection mode)
|
} // 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) {
|
} catch (Exception ex) {
|
||||||
// Compensate: undo all posted Fineract transactions in reverse order (best-effort).
|
// Compensate: undo all posted Fineract transactions in reverse order (best-effort).
|
||||||
// This restores account balances even when a later entry fails mid-processing.
|
// 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). */
|
/** Savings status_enum for an account still open for business (Fineract's SavingsAccountStatusType.ACTIVE). */
|
||||||
private static final int SAVINGS_STATUS_ACTIVE = 300;
|
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.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity;
|
||||||
import org.apache.fineract.commands.domain.CommandWrapper;
|
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||||
import org.apache.fineract.commands.service.CommandWrapperBuilder;
|
import org.apache.fineract.commands.service.CommandWrapperBuilder;
|
||||||
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
|
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
|
||||||
@@ -56,6 +57,7 @@ import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
|||||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
import org.apache.fineract.portfolio.client.domain.KycExpiryStage;
|
import org.apache.fineract.portfolio.client.domain.KycExpiryStage;
|
||||||
import org.apache.fineract.portfolio.client.service.KycRestrictionGuard;
|
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.VoucherLine;
|
||||||
import org.apache.fineract.portfolio.voucher.service.VoucherService;
|
import org.apache.fineract.portfolio.voucher.service.VoucherService;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -84,6 +86,7 @@ public class ClerkWithdrawalRequestApiResource {
|
|||||||
private final PlatformSecurityContext context;
|
private final PlatformSecurityContext context;
|
||||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||||
private final VoucherService voucherService;
|
private final VoucherService voucherService;
|
||||||
|
private final FixedDepositFundingValidator fixedDepositFundingValidator;
|
||||||
private final Gson gson = new GsonBuilder().serializeNulls().create();
|
private final Gson gson = new GsonBuilder().serializeNulls().create();
|
||||||
|
|
||||||
// ── POST /v1/clerk/withdrawal-requests ── Clerk submits new request ─────────
|
// ── 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 tokNo = fromJsonHelper.extractStringNamed("tokNo", root);
|
||||||
final String txnDate = fromJsonHelper.extractStringNamed("txnDate", root);
|
final String txnDate = fromJsonHelper.extractStringNamed("txnDate", root);
|
||||||
final String denominationData = fromJsonHelper.extractStringNamed("denominationData", 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()) {
|
if (accountNo == null || accountNo.isBlank()) {
|
||||||
throw apiError(400, "Account number is required");
|
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");
|
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) {
|
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
throw apiError(400, "Amount must be greater than zero");
|
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");
|
throw apiError(400, "Token number (TOK. NO.) is required");
|
||||||
}
|
}
|
||||||
validateTokenUniqueToday(tokNo, null);
|
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);
|
final String activityLog = buildActivityLog(null, "SUBMITTED", currentUser.getUsername(), null);
|
||||||
|
|
||||||
@@ -132,8 +152,9 @@ public class ClerkWithdrawalRequestApiResource {
|
|||||||
INSERT INTO m_clerk_withdrawal_request
|
INSERT INTO m_clerk_withdrawal_request
|
||||||
(account_no, account_type, savings_account_id, client_id, client_name, amount,
|
(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,
|
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)
|
status, clerk_id, clerk_name, clerk_username, denomination_data, activity_log, submitted_at,
|
||||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
gl_account_id, contra_gl_id, office_id, currency_code)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
""",
|
""",
|
||||||
Long.class,
|
Long.class,
|
||||||
@@ -142,7 +163,9 @@ public class ClerkWithdrawalRequestApiResource {
|
|||||||
txnDate != null ? Date.valueOf(txnDate) : null,
|
txnDate != null ? Date.valueOf(txnDate) : null,
|
||||||
"PENDING",
|
"PENDING",
|
||||||
currentUser.getId(), currentUser.getDisplayName(), currentUser.getUsername(),
|
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
|
// 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,"
|
+ " txn_date, status, clerk_id, clerk_name, clerk_username,"
|
||||||
+ " manager_id, manager_name, manager_username, rejection_reason, txn_id,"
|
+ " manager_id, manager_name, manager_username, rejection_reason, txn_id,"
|
||||||
+ " denomination_data, activity_log, submitted_at, actioned_at,"
|
+ " 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
|
+ " FROM m_clerk_withdrawal_request " + where
|
||||||
+ " ORDER BY submitted_at DESC LIMIT ? OFFSET ?",
|
+ " ORDER BY submitted_at DESC LIMIT ? OFFSET ?",
|
||||||
pageParams.toArray());
|
pageParams.toArray());
|
||||||
@@ -252,7 +276,8 @@ public class ClerkWithdrawalRequestApiResource {
|
|||||||
payment_type_name, description, tok_no, txn_date, status,
|
payment_type_name, description, tok_no, txn_date, status,
|
||||||
clerk_name, manager_name, manager_username, rejection_reason, txn_id,
|
clerk_name, manager_name, manager_username, rejection_reason, txn_id,
|
||||||
submitted_at, actioned_at,
|
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
|
FROM m_clerk_withdrawal_request
|
||||||
""" + where + " ORDER BY submitted_at DESC LIMIT ? OFFSET ?",
|
""" + where + " ORDER BY submitted_at DESC LIMIT ? OFFSET ?",
|
||||||
pageParams.toArray()
|
pageParams.toArray()
|
||||||
@@ -276,7 +301,8 @@ public class ClerkWithdrawalRequestApiResource {
|
|||||||
txn_date, status, clerk_id, clerk_name, clerk_username,
|
txn_date, status, clerk_id, clerk_name, clerk_username,
|
||||||
manager_id, manager_name, manager_username, rejection_reason, txn_id,
|
manager_id, manager_name, manager_username, rejection_reason, txn_id,
|
||||||
denomination_data, activity_log, submitted_at, actioned_at,
|
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 = ?
|
FROM m_clerk_withdrawal_request WHERE id = ?
|
||||||
""", id);
|
""", id);
|
||||||
if (rows.isEmpty()) {
|
if (rows.isEmpty()) {
|
||||||
@@ -295,7 +321,8 @@ public class ClerkWithdrawalRequestApiResource {
|
|||||||
final List<Map<String, Object>> rows = jdbcTemplate.queryForList("""
|
final List<Map<String, Object>> rows = jdbcTemplate.queryForList("""
|
||||||
SELECT id, account_no, account_type, savings_account_id, client_id, client_name,
|
SELECT id, account_no, account_type, savings_account_id, client_id, client_name,
|
||||||
amount, payment_type_id, payment_type_name, description, tok_no, status,
|
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
|
FROM m_clerk_withdrawal_request
|
||||||
WHERE tok_no = ? AND CAST(submitted_at AS DATE) = CURRENT_DATE
|
WHERE tok_no = ? AND CAST(submitted_at AS DATE) = CURRENT_DATE
|
||||||
ORDER BY submitted_at DESC LIMIT 1
|
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.");
|
"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
|
final Long savingsAccountId = record.get("savings_account_id") != null
|
||||||
? ((Number) record.get("savings_account_id")).longValue() : 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,
|
// 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
|
// 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
|
// 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.
|
// 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 BigDecimal amount = new BigDecimal(record.get("amount").toString());
|
||||||
final Object ptId = record.get("payment_type_id");
|
final Object ptId = record.get("payment_type_id");
|
||||||
final Object txnDateObj = record.get("txn_date");
|
final Object txnDateObj = record.get("txn_date");
|
||||||
@@ -469,7 +499,11 @@ public class ClerkWithdrawalRequestApiResource {
|
|||||||
final String description = (String) record.get("description");
|
final String description = (String) record.get("description");
|
||||||
|
|
||||||
String txnId = null;
|
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"));
|
final boolean isCurrentAccount = isCurrentAccountById(savingsAccountId, (String) record.get("account_no"));
|
||||||
txnId = isCurrentAccount
|
txnId = isCurrentAccount
|
||||||
? executeCurrentAccountWithdrawal(savingsAccountId, amount, ptId, txnDateStr, description)
|
? 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
|
// Link voucher to the created savings txn and stamp voucher_no on it
|
||||||
if (txnId != null) {
|
if (txnId != null && !isGlRequest) {
|
||||||
voucherService.linkSavingsTransaction(
|
voucherService.linkSavingsTransaction(
|
||||||
VoucherService.REF_CLERK_WITHDRAWAL_REQUEST, id, Long.valueOf(txnId));
|
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
|
// 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.
|
// 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(
|
final String updatedLog = buildActivityLog(
|
||||||
(String) record.get("activity_log"), "APPROVED", currentUser.getUsername(), null
|
(String) record.get("activity_log"), "APPROVED", currentUser.getUsername(), null
|
||||||
@@ -689,8 +727,8 @@ public class ClerkWithdrawalRequestApiResource {
|
|||||||
if (denomMap == null || denomMap.size() == 0) return;
|
if (denomMap == null || denomMap.size() == 0) return;
|
||||||
|
|
||||||
final Object txnIdObj = record.get("txn_id");
|
final Object txnIdObj = record.get("txn_id");
|
||||||
final Long txnId = txnIdObj != null ? Long.valueOf(txnIdObj.toString()) : null;
|
final Long txnId = txnIdObj != null ? parseTxnKey(txnIdObj.toString()) : null;
|
||||||
if (txnId == null) return; // no savings txn linked — cannot record denomination
|
if (txnId == null) return; // no txn linked — cannot record denomination
|
||||||
|
|
||||||
final Object saObj = record.get("savings_account_id");
|
final Object saObj = record.get("savings_account_id");
|
||||||
final Long savingsAccountId = saObj != null ? Long.valueOf(saObj.toString()) : null;
|
final Long savingsAccountId = saObj != null ? Long.valueOf(saObj.toString()) : null;
|
||||||
@@ -793,6 +831,117 @@ public class ClerkWithdrawalRequestApiResource {
|
|||||||
return rows.isEmpty() ? null : rows.get(0);
|
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) {
|
private Map<?, ?> loadRecord(final Long id) {
|
||||||
final List<Map<String, Object>> rows = jdbcTemplate.queryForList("""
|
final List<Map<String, Object>> rows = jdbcTemplate.queryForList("""
|
||||||
SELECT id, account_no, account_type, savings_account_id, client_id, client_name,
|
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,
|
txn_date, status, clerk_id, clerk_name, clerk_username,
|
||||||
manager_id, manager_name, manager_username, rejection_reason, txn_id,
|
manager_id, manager_name, manager_username, rejection_reason, txn_id,
|
||||||
denomination_data, activity_log, submitted_at, actioned_at,
|
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 = ?
|
FROM m_clerk_withdrawal_request WHERE id = ?
|
||||||
""", id);
|
""", id);
|
||||||
if (rows.isEmpty()) {
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-95
@@ -1,95 +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 ADDRESS1 = "address1";
|
|
||||||
public static final String ADDRESS2 = "address2";
|
|
||||||
public static final String ADDRESS3 = "address3";
|
|
||||||
public static final String CITY = "city";
|
|
||||||
public static final String PINCODE = "pincode";
|
|
||||||
public static final String PHONE = "phone";
|
|
||||||
public static final String FAX = "fax";
|
|
||||||
public static final String ACTIVE = "active";
|
|
||||||
public static final String LOCALE = "locale";
|
|
||||||
public static final String DATE_FORMAT = "dateFormat";
|
|
||||||
|
|
||||||
// Party parameters
|
|
||||||
public static final String PARTY_NAME = "partyName";
|
|
||||||
public static final String BANK_TYPE = "bankType";
|
|
||||||
public static final String RBI_CODE = "rbiCode";
|
|
||||||
public static final String EXPOSURE_LIMIT = "limit";
|
|
||||||
public static final String LEI_NO = "leiNo";
|
|
||||||
public static final String CP_BANK_NAME = "cpBankName";
|
|
||||||
public static final String CP_BRANCH_NAME = "cpBranchName";
|
|
||||||
public static final String CP_ACCOUNT_NO = "cpAccountNo";
|
|
||||||
public static final String CP_IFSC = "cpIfsc";
|
|
||||||
|
|
||||||
// Script parameters
|
|
||||||
public static final String SLR_TYPE = "slrType";
|
|
||||||
public static final String SCRIPT_DESC = "scriptDesc";
|
|
||||||
public static final String ISIN = "isin";
|
|
||||||
public static final String FACE_VALUE_PER_UNIT = "faceValuePerUnit";
|
|
||||||
public static final String INT_RATE = "intRate";
|
|
||||||
public static final String ISSUE_DATE = "issueDate";
|
|
||||||
public static final String MATURITY_DATE = "maturityDate";
|
|
||||||
public static final String COMPANY_NAME = "companyName";
|
|
||||||
public static final String CORP_CODE = "corpCode";
|
|
||||||
|
|
||||||
public static final Set<String> PARTY_PARAMS = Set.of(BOND_TYPE, PARTY_NAME, ADDRESS1, ADDRESS2, ADDRESS3, CITY, PINCODE, PHONE, FAX,
|
|
||||||
BANK_TYPE, GL_ACCOUNT_ID, RBI_CODE, EXPOSURE_LIMIT, LEI_NO, CP_BANK_NAME, CP_BRANCH_NAME, CP_ACCOUNT_NO, CP_IFSC, ACTIVE,
|
|
||||||
LOCALE);
|
|
||||||
|
|
||||||
public static final Set<String> SCRIPT_PARAMS = Set.of(BOND_TYPE, SLR_TYPE, SCRIPT_DESC, GL_ACCOUNT_ID, ISIN, FACE_VALUE_PER_UNIT,
|
|
||||||
INT_RATE, ISSUE_DATE, MATURITY_DATE, COMPANY_NAME, ADDRESS1, ADDRESS2, ADDRESS3, CITY, PINCODE, PHONE, FAX, CORP_CODE,
|
|
||||||
ACTIVE, LOCALE, DATE_FORMAT);
|
|
||||||
}
|
|
||||||
-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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-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 lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentParty;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Builder
|
|
||||||
public class InvestmentPartyData {
|
|
||||||
|
|
||||||
private final Long id;
|
|
||||||
private final String partyCode;
|
|
||||||
private final String bondType;
|
|
||||||
private final String partyName;
|
|
||||||
private final String address1;
|
|
||||||
private final String address2;
|
|
||||||
private final String address3;
|
|
||||||
private final String city;
|
|
||||||
private final String pincode;
|
|
||||||
private final String phone;
|
|
||||||
private final String fax;
|
|
||||||
private final String bankType;
|
|
||||||
private final Long glAccountId;
|
|
||||||
private final String glCode;
|
|
||||||
private final String glName;
|
|
||||||
private final String rbiCode;
|
|
||||||
private final BigDecimal limit;
|
|
||||||
private final String leiNo;
|
|
||||||
private final String cpBankName;
|
|
||||||
private final String cpBranchName;
|
|
||||||
private final String cpAccountNo;
|
|
||||||
private final String cpIfsc;
|
|
||||||
private final boolean active;
|
|
||||||
|
|
||||||
public static InvestmentPartyData from(final InvestmentParty p) {
|
|
||||||
return InvestmentPartyData.builder().id(p.getId()).partyCode(p.getPartyCode()).bondType(p.getBondType())
|
|
||||||
.partyName(p.getPartyName()).address1(p.getAddress1()).address2(p.getAddress2()).address3(p.getAddress3())
|
|
||||||
.city(p.getCity()).pincode(p.getPincode()).phone(p.getPhone()).fax(p.getFax()).bankType(p.getBankType())
|
|
||||||
.glAccountId(p.getGlAccount().getId()).glCode(p.getGlAccount().getGlCode()).glName(p.getGlAccount().getName())
|
|
||||||
.rbiCode(p.getRbiCode()).limit(p.getExposureLimit()).leiNo(p.getLeiNo()).cpBankName(p.getCpBankName())
|
|
||||||
.cpBranchName(p.getCpBranchName()).cpAccountNo(p.getCpAccountNo()).cpIfsc(p.getCpIfsc()).active(p.isActive()).build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-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);
|
|
||||||
}
|
|
||||||
-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);
|
|
||||||
}
|
|
||||||
-105
@@ -1,105 +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;
|
|
||||||
|
|
||||||
@Column(name = "rbi_code", length = 30)
|
|
||||||
private String rbiCode;
|
|
||||||
|
|
||||||
@Column(name = "exposure_limit", scale = 6, precision = 19)
|
|
||||||
private BigDecimal exposureLimit;
|
|
||||||
|
|
||||||
@Column(name = "lei_no", length = 30)
|
|
||||||
private String leiNo;
|
|
||||||
|
|
||||||
@Column(name = "cp_bank_name", length = 150)
|
|
||||||
private String cpBankName;
|
|
||||||
|
|
||||||
@Column(name = "cp_branch_name", length = 150)
|
|
||||||
private String cpBranchName;
|
|
||||||
|
|
||||||
@Column(name = "cp_account_no", length = 40)
|
|
||||||
private String cpAccountNo;
|
|
||||||
|
|
||||||
@Column(name = "cp_ifsc", length = 11)
|
|
||||||
private String cpIfsc;
|
|
||||||
|
|
||||||
/** An inactive party is kept for history but not offered for new deals. */
|
|
||||||
@Column(name = "is_active", nullable = false)
|
|
||||||
private boolean active = true;
|
|
||||||
}
|
|
||||||
-23
@@ -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 InvestmentPartyRepository extends JpaRepository<InvestmentParty, Long> {}
|
|
||||||
-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.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.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.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.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-259
@@ -1,259 +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.INT_RATE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ISIN;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ISSUE_DATE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.LEI_NO;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.MATURITY_DATE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PARTY_NAME;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PARTY_PARAMS;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PHONE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PINCODE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.RBI_CODE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.RESOURCE_PARTY;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.RESOURCE_SCRIPT;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SCRIPT_DESC;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SCRIPT_PARAMS;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SLR_TYPE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SLR_TYPES;
|
|
||||||
|
|
||||||
import com.google.gson.JsonElement;
|
|
||||||
import com.google.gson.reflect.TypeToken;
|
|
||||||
import java.lang.reflect.Type;
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
|
||||||
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
|
||||||
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
|
|
||||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
|
||||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
/** Validates Bank / Party Master and Script Master requests before anything is written. */
|
|
||||||
@Component
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public final class InvestmentMasterCommandFromApiJsonDeserializer {
|
|
||||||
|
|
||||||
private static final String IFSC_PATTERN = "^[A-Z]{4}0[A-Z0-9]{6}$";
|
|
||||||
private static final String PINCODE_PATTERN = "^[0-9]{6}$";
|
|
||||||
|
|
||||||
private final FromJsonHelper fromApiJsonHelper;
|
|
||||||
|
|
||||||
// -------------------- Bank / Party Master --------------------
|
|
||||||
|
|
||||||
public void validateForCreateParty(final String json) {
|
|
||||||
validateParty(json, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void validateForUpdateParty(final String json) {
|
|
||||||
validateParty(json, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void validateParty(final String json, final boolean create) {
|
|
||||||
final JsonElement element = parse(json, PARTY_PARAMS);
|
|
||||||
final List<ApiParameterError> errors = new ArrayList<>();
|
|
||||||
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(RESOURCE_PARTY);
|
|
||||||
|
|
||||||
validateBondType(element, v, create);
|
|
||||||
validateRequiredString(element, v, PARTY_NAME, 150, create);
|
|
||||||
validateGlAccountId(element, v, create);
|
|
||||||
validateAddress(element, v);
|
|
||||||
validateOptionalString(element, v, BANK_TYPE, 60);
|
|
||||||
validateOptionalString(element, v, RBI_CODE, 30);
|
|
||||||
validateOptionalString(element, v, LEI_NO, 30);
|
|
||||||
validateOptionalString(element, v, CP_BANK_NAME, 150);
|
|
||||||
validateOptionalString(element, v, CP_BRANCH_NAME, 150);
|
|
||||||
validateOptionalString(element, v, CP_ACCOUNT_NO, 40);
|
|
||||||
|
|
||||||
final String ifsc = this.fromApiJsonHelper.extractStringNamed(CP_IFSC, element);
|
|
||||||
if (StringUtils.isNotBlank(ifsc)) {
|
|
||||||
v.reset().parameter(CP_IFSC).value(ifsc.trim().toUpperCase()).matchesRegularExpression(IFSC_PATTERN,
|
|
||||||
"IFSC must be 11 characters: 4 letters, 0, then 6 letters/digits");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.fromApiJsonHelper.parameterExists(EXPOSURE_LIMIT, element)) {
|
|
||||||
final BigDecimal limit = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(EXPOSURE_LIMIT, element);
|
|
||||||
v.reset().parameter(EXPOSURE_LIMIT).value(limit).ignoreIfNull().zeroOrPositiveAmount();
|
|
||||||
}
|
|
||||||
throwIfErrors(errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------- Script Master --------------------
|
|
||||||
|
|
||||||
public void validateForCreateScript(final String json) {
|
|
||||||
validateScript(json, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void validateForUpdateScript(final String json, final String existingBondType) {
|
|
||||||
validateScript(json, false, existingBondType);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void validateScript(final String json, final boolean create) {
|
|
||||||
validateScript(json, create, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void validateScript(final String json, final boolean create, final String existingBondType) {
|
|
||||||
final JsonElement element = parse(json, SCRIPT_PARAMS);
|
|
||||||
final List<ApiParameterError> errors = new ArrayList<>();
|
|
||||||
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(RESOURCE_SCRIPT);
|
|
||||||
|
|
||||||
validateBondType(element, v, create);
|
|
||||||
validateRequiredString(element, v, SCRIPT_DESC, 150, create);
|
|
||||||
validateGlAccountId(element, v, create);
|
|
||||||
|
|
||||||
final String slrType = this.fromApiJsonHelper.extractStringNamed(SLR_TYPE, element);
|
|
||||||
if (StringUtils.isNotBlank(slrType)) {
|
|
||||||
v.reset().parameter(SLR_TYPE).value(slrType).isOneOfTheseValues(SLR_TYPES.toArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
final String bondType = create ? this.fromApiJsonHelper.extractStringNamed(BOND_TYPE, element) : existingBondType;
|
|
||||||
final boolean debt = bondType != null && DEBT_BOND_TYPES.contains(bondType);
|
|
||||||
|
|
||||||
final String isin = this.fromApiJsonHelper.extractStringNamed(ISIN, element);
|
|
||||||
if (create && debt) {
|
|
||||||
v.reset().parameter(ISIN).value(isin).notBlank();
|
|
||||||
}
|
|
||||||
v.reset().parameter(ISIN).value(isin).ignoreIfNull().notExceedingLengthOf(20);
|
|
||||||
|
|
||||||
if (this.fromApiJsonHelper.parameterExists(INT_RATE, element) || (create && debt)) {
|
|
||||||
final BigDecimal intRate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(INT_RATE, element);
|
|
||||||
final DataValidatorBuilder rate = v.reset().parameter(INT_RATE).value(intRate);
|
|
||||||
if (create && debt) {
|
|
||||||
rate.notNull();
|
|
||||||
}
|
|
||||||
rate.ignoreIfNull().zeroOrPositiveAmount();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.fromApiJsonHelper.parameterExists(FACE_VALUE_PER_UNIT, element)) {
|
|
||||||
final BigDecimal fv = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(FACE_VALUE_PER_UNIT, element);
|
|
||||||
v.reset().parameter(FACE_VALUE_PER_UNIT).value(fv).ignoreIfNull().positiveAmount();
|
|
||||||
}
|
|
||||||
|
|
||||||
LocalDate issueDate = null;
|
|
||||||
if (this.fromApiJsonHelper.parameterExists(ISSUE_DATE, element) || (create && debt)) {
|
|
||||||
issueDate = this.fromApiJsonHelper.extractLocalDateNamed(ISSUE_DATE, element);
|
|
||||||
if (create && debt) {
|
|
||||||
v.reset().parameter(ISSUE_DATE).value(issueDate).notNull();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.fromApiJsonHelper.parameterExists(MATURITY_DATE, element) || (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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-126
@@ -1,126 +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.InvestmentSaleApiConstants.AMOUNT;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.BANK_GL_ACCOUNT_ID;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.BROKERAGE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.DEAL_DATE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.PARTY_ID;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.PURCHASE_ID;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.REDEEM_PARAMS;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.REMARKS;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.RESOURCE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.SALE_RATE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.SANCTION_AUTHORITY;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.SELL_PARAMS;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.SETTLEMENT_TYPE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.UNITS;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.WRITEOFF_PARAMS;
|
|
||||||
|
|
||||||
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 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.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
/** Validates a sale entry and a redemption request. */
|
|
||||||
@Component
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public final class InvestmentSaleCommandFromApiJsonDeserializer {
|
|
||||||
|
|
||||||
private final FromJsonHelper fromApiJsonHelper;
|
|
||||||
|
|
||||||
public void validateForSell(final String json) {
|
|
||||||
final JsonElement element = parse(json, SELL_PARAMS);
|
|
||||||
final List<ApiParameterError> errors = new ArrayList<>();
|
|
||||||
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(RESOURCE);
|
|
||||||
v.reset().parameter(PURCHASE_ID).value(this.fromApiJsonHelper.extractLongNamed(PURCHASE_ID, element)).notNull()
|
|
||||||
.longGreaterThanZero();
|
|
||||||
v.reset().parameter(PARTY_ID).value(this.fromApiJsonHelper.extractLongNamed(PARTY_ID, element)).notNull().longGreaterThanZero();
|
|
||||||
v.reset().parameter(BANK_GL_ACCOUNT_ID).value(this.fromApiJsonHelper.extractLongNamed(BANK_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(InvestmentPurchaseApiConstants.SETTLEMENT_TYPES.toArray());
|
|
||||||
}
|
|
||||||
v.reset().parameter(UNITS).value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(UNITS, element)).notNull()
|
|
||||||
.positiveAmount();
|
|
||||||
v.reset().parameter(SALE_RATE).value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(SALE_RATE, element)).notNull()
|
|
||||||
.positiveAmount();
|
|
||||||
v.reset().parameter(BROKERAGE).value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(BROKERAGE, element)).ignoreIfNull()
|
|
||||||
.zeroOrPositiveAmount();
|
|
||||||
v.reset().parameter(SANCTION_AUTHORITY).value(this.fromApiJsonHelper.extractStringNamed(SANCTION_AUTHORITY, element))
|
|
||||||
.ignoreIfNull().notExceedingLengthOf(100);
|
|
||||||
v.reset().parameter(REMARKS).value(this.fromApiJsonHelper.extractStringNamed(REMARKS, element)).ignoreIfNull()
|
|
||||||
.notExceedingLengthOf(500);
|
|
||||||
throwIfAny(errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void validateForRedeem(final String json) {
|
|
||||||
final JsonElement element = parse(json, REDEEM_PARAMS);
|
|
||||||
final List<ApiParameterError> errors = new ArrayList<>();
|
|
||||||
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(RESOURCE);
|
|
||||||
v.reset().parameter(BANK_GL_ACCOUNT_ID).value(this.fromApiJsonHelper.extractLongNamed(BANK_GL_ACCOUNT_ID, element)).ignoreIfNull()
|
|
||||||
.longGreaterThanZero();
|
|
||||||
v.reset().parameter(REMARKS).value(this.fromApiJsonHelper.extractStringNamed(REMARKS, element)).ignoreIfNull()
|
|
||||||
.notExceedingLengthOf(500);
|
|
||||||
throwIfAny(errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void validateForWriteOff(final String json) {
|
|
||||||
final JsonElement element = parse(json, WRITEOFF_PARAMS);
|
|
||||||
final List<ApiParameterError> errors = new ArrayList<>();
|
|
||||||
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(RESOURCE);
|
|
||||||
v.reset().parameter(AMOUNT).value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(AMOUNT, element)).notNull()
|
|
||||||
.positiveAmount();
|
|
||||||
v.reset().parameter(REMARKS).value(this.fromApiJsonHelper.extractStringNamed(REMARKS, element)).notBlank()
|
|
||||||
.notExceedingLengthOf(500);
|
|
||||||
throwIfAny(errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
private JsonElement parse(final String json, final Set<String> params) {
|
|
||||||
if (StringUtils.isBlank(json)) {
|
|
||||||
throw new InvalidJsonException();
|
|
||||||
}
|
|
||||||
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
|
||||||
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, params);
|
|
||||||
return this.fromApiJsonHelper.parse(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void throwIfAny(final List<ApiParameterError> errors) {
|
|
||||||
if (!errors.isEmpty()) {
|
|
||||||
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-304
@@ -1,304 +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.service;
|
|
||||||
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.STATUS_ACTIVE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.AMORTISE_PARAMS;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.FY_END;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.PREMIUM_AMORTISATION_GL_CODE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.PURCHASE_ID;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.STATUS_SETTLED;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.TYPE_AMORTISE;
|
|
||||||
|
|
||||||
import com.google.gson.JsonElement;
|
|
||||||
import com.google.gson.reflect.TypeToken;
|
|
||||||
import java.lang.reflect.Type;
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.math.RoundingMode;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.Month;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.time.temporal.ChronoUnit;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
|
||||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
|
||||||
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.infrastructure.core.service.DateUtils;
|
|
||||||
import org.apache.fineract.portfolio.investment.data.InvestmentAmortisationData;
|
|
||||||
import org.apache.fineract.portfolio.investment.data.InvestmentAmortisationDueData;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchase;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchaseRepository;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentSale;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentSaleRepository;
|
|
||||||
import org.apache.fineract.portfolio.investment.exception.InvestmentPurchaseNotFoundException;
|
|
||||||
import org.springframework.data.domain.Sort;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Premium amortisation of HTM holdings: the premium paid over face value is written off straight-line, day by day, over
|
|
||||||
* the period remaining to maturity (RBI; method and 31 March timing confirmed with Jitendra Sir). Each 31 March posts the
|
|
||||||
* share for the days since the last amortisation (or settlement): unamortised premium x days to 31 March / days to
|
|
||||||
* maturity — so a part sale, which takes its share of premium with it, is followed automatically. The remainder after
|
|
||||||
* the last 31 March is written off with the redemption. Entry: Dr 500604 Amortisation of Premium / Cr Premium GL.
|
|
||||||
*/
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class InvestmentAmortisationService {
|
|
||||||
|
|
||||||
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
|
||||||
private static final String HTM_PREFIX = "HTM";
|
|
||||||
|
|
||||||
private final FromJsonHelper fromApiJsonHelper;
|
|
||||||
private final InvestmentPurchaseRepository purchaseRepository;
|
|
||||||
private final InvestmentSaleRepository saleRepository;
|
|
||||||
private final InvestmentSaleQuoter quoter;
|
|
||||||
private final InvestmentJournalPoster journalPoster;
|
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
public InvestmentAmortisationData schedule(final Long purchaseId) {
|
|
||||||
final InvestmentPurchase p = purchase(purchaseId);
|
|
||||||
final List<InvestmentSale> posted = amortisations(p);
|
|
||||||
final LocalDate today = DateUtils.getBusinessLocalDate();
|
|
||||||
final List<InvestmentAmortisationData.Row> rows = new ArrayList<>();
|
|
||||||
BigDecimal premium = this.quoter.unamortisedPremium(p);
|
|
||||||
LocalDate start = p.getSettlementDate();
|
|
||||||
boolean nextFound = false;
|
|
||||||
for (final LocalDate fyEnd : yearEnds(p)) {
|
|
||||||
final Optional<InvestmentSale> done = posted.stream().filter(x -> x.getDealDate().equals(fyEnd)).findFirst();
|
|
||||||
final long days = ChronoUnit.DAYS.between(start, fyEnd);
|
|
||||||
if (done.isPresent()) {
|
|
||||||
rows.add(row("YEAR", fyEnd, start, days, done.get().getBookValueSold(), null, true, done.get(), false));
|
|
||||||
} else {
|
|
||||||
final BigDecimal amount = share(premium, start, fyEnd, p.getMaturityDate());
|
|
||||||
premium = premium.subtract(amount);
|
|
||||||
final boolean postable = STATUS_ACTIVE.equals(p.getStatus()) && !nextFound && !fyEnd.isAfter(today);
|
|
||||||
nextFound = true;
|
|
||||||
rows.add(row("YEAR", fyEnd, start, days, amount, premium, false, null, postable));
|
|
||||||
}
|
|
||||||
start = fyEnd;
|
|
||||||
}
|
|
||||||
if (premium.signum() > 0 && STATUS_ACTIVE.equals(p.getStatus())) {
|
|
||||||
rows.add(row("MATURITY", p.getMaturityDate(), start, ChronoUnit.DAYS.between(start, p.getMaturityDate()), premium,
|
|
||||||
BigDecimal.ZERO, false, null, false));
|
|
||||||
}
|
|
||||||
return InvestmentAmortisationData.builder().purchaseId(p.getId()).dealNo(p.getDealNo()).certNo(p.getCertNo())
|
|
||||||
.scriptCode(p.getScript().getScriptCode()).scriptDesc(p.getScript().getScriptDesc()).category(p.getCategory())
|
|
||||||
.settlementDate(p.getSettlementDate().format(DATE)).maturityDate(p.getMaturityDate().format(DATE))
|
|
||||||
.faceValue(p.getBalanceUnits().multiply(p.getScript().getFaceValuePerUnit()))
|
|
||||||
.premium(p.getPremiumDiscount() == null ? BigDecimal.ZERO : p.getPremiumDiscount().max(BigDecimal.ZERO))
|
|
||||||
.unamortisedPremium(this.quoter.unamortisedPremium(p)).bookValue(this.quoter.remainingBook(p))
|
|
||||||
.premiumGlCode(p.getPremiumGlAccount() != null ? p.getPremiumGlAccount().getGlCode() : p.getInvestmentGlAccount().getGlCode())
|
|
||||||
.rows(rows).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** HTM holdings with premium whose next amortisation falls on {@code fyEnd} (Amortize Yearly Process list). */
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
public List<InvestmentAmortisationDueData> due(final LocalDate fyEnd) {
|
|
||||||
final List<InvestmentAmortisationDueData> due = new ArrayList<>();
|
|
||||||
for (final InvestmentPurchase p : this.purchaseRepository.findAll(Sort.by(Sort.Direction.ASC, "id"))) {
|
|
||||||
if (eligible(p) && fyEnd.equals(nextYearEnd(p))) {
|
|
||||||
final LocalDate start = periodStart(p);
|
|
||||||
final BigDecimal premium = this.quoter.unamortisedPremium(p);
|
|
||||||
due.add(InvestmentAmortisationDueData.builder().purchaseId(p.getId()).dealNo(p.getDealNo()).certNo(p.getCertNo())
|
|
||||||
.scriptCode(p.getScript().getScriptCode()).scriptDesc(p.getScript().getScriptDesc()).fyEnd(fyEnd.format(DATE))
|
|
||||||
.fromDate(start.format(DATE)).days(ChronoUnit.DAYS.between(start, fyEnd)).unamortisedPremium(premium)
|
|
||||||
.amount(share(premium, start, fyEnd, p.getMaturityDate())).build());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return due;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Posts the amortisation for {@code fyEnd} (a 31 March on or before today): for one holding when purchaseId is
|
|
||||||
* given (Amortize Entry), else for every holding due that year (Amortize Yearly Process). One JV per holding.
|
|
||||||
*/
|
|
||||||
@Transactional
|
|
||||||
public CommandProcessingResult amortise(final JsonCommand command) {
|
|
||||||
validate(command.json());
|
|
||||||
final LocalDate fyEnd = command.localDateValueOfParameterNamed(FY_END);
|
|
||||||
if (fyEnd.getMonth() != Month.MARCH || fyEnd.getDayOfMonth() != 31) {
|
|
||||||
throw InvestmentSaleQuoter.rule("amortise.not.yearend", "Amortisation is done as on 31 March");
|
|
||||||
}
|
|
||||||
if (fyEnd.isAfter(DateUtils.getBusinessLocalDate())) {
|
|
||||||
throw InvestmentSaleQuoter.rule("amortise.future", "Amortisation for " + fyEnd.format(DATE) + " can be done on or after that date");
|
|
||||||
}
|
|
||||||
final Long purchaseId = command.longValueOfParameterNamed(PURCHASE_ID);
|
|
||||||
final List<InvestmentPurchase> targets = new ArrayList<>();
|
|
||||||
if (purchaseId != null) {
|
|
||||||
final InvestmentPurchase p = lockedPurchase(purchaseId);
|
|
||||||
if (!eligible(p)) {
|
|
||||||
throw InvestmentSaleQuoter.rule("amortise.not.eligible",
|
|
||||||
"Deal " + p.getDealNo() + " has no premium left to amortise (only active HTM holdings bought above face value)");
|
|
||||||
}
|
|
||||||
final LocalDate next = nextYearEnd(p);
|
|
||||||
if (!fyEnd.equals(next)) {
|
|
||||||
throw InvestmentSaleQuoter.rule("amortise.out.of.order", next == null ? "Deal " + p.getDealNo() + " has no 31 March to amortise"
|
|
||||||
: "Amortise deal " + p.getDealNo() + " year by year: the next is " + next.format(DATE));
|
|
||||||
}
|
|
||||||
targets.add(p);
|
|
||||||
} else {
|
|
||||||
for (final InvestmentPurchase found : this.purchaseRepository.findAll(Sort.by(Sort.Direction.ASC, "id"))) {
|
|
||||||
if (eligible(found) && fyEnd.equals(nextYearEnd(found))) {
|
|
||||||
// Lock, then check again: another run may have posted this year meanwhile.
|
|
||||||
final InvestmentPurchase p = lockedPurchase(found.getId());
|
|
||||||
if (eligible(p) && fyEnd.equals(nextYearEnd(p))) {
|
|
||||||
targets.add(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (targets.isEmpty()) {
|
|
||||||
throw InvestmentSaleQuoter.rule("amortise.nothing.due", "No holding has premium amortisation due for " + fyEnd.format(DATE));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
final List<String> vouchers = new ArrayList<>();
|
|
||||||
Long lastId = null;
|
|
||||||
for (final InvestmentPurchase p : targets) {
|
|
||||||
final InvestmentSale s = post(p, fyEnd);
|
|
||||||
if (s.getVoucherNo() != null) {
|
|
||||||
vouchers.add(s.getVoucherNo());
|
|
||||||
}
|
|
||||||
lastId = s.getId();
|
|
||||||
}
|
|
||||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(lastId)
|
|
||||||
.withResourceIdAsString(String.join(", ", vouchers)).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private InvestmentSale post(final InvestmentPurchase p, final LocalDate fyEnd) {
|
|
||||||
final LocalDate start = periodStart(p);
|
|
||||||
final BigDecimal amount = share(this.quoter.unamortisedPremium(p), start, fyEnd, p.getMaturityDate());
|
|
||||||
final String fy = (fyEnd.getYear() - 1) + "-" + String.format("%02d", fyEnd.getYear() % 100);
|
|
||||||
// A share that rounds to nothing is still recorded, so the year is done and the next one can follow; it needs no JV.
|
|
||||||
final CommandProcessingResult posted = amount.signum() == 0 ? null
|
|
||||||
: this.journalPoster.post(fyEnd, p.getDealNo(),
|
|
||||||
"Premium amortisation FY " + fy + ": deal " + p.getDealNo() + ", " + p.getScript().getScriptCode() + " "
|
|
||||||
+ p.getScript().getScriptDesc() + " (" + ChronoUnit.DAYS.between(start, fyEnd) + " days)",
|
|
||||||
new InvestmentJournalPoster.Lines().debit(this.journalPoster.glId(PREMIUM_AMORTISATION_GL_CODE), amount)
|
|
||||||
.credit(InvestmentSaleQuoter.premiumGlId(p), amount));
|
|
||||||
final InvestmentSale s = new InvestmentSale();
|
|
||||||
s.setPurchase(p);
|
|
||||||
s.setSaleType(TYPE_AMORTISE);
|
|
||||||
s.setDealDate(fyEnd);
|
|
||||||
s.setSettlementDate(fyEnd);
|
|
||||||
s.setUnits(BigDecimal.ZERO);
|
|
||||||
s.setFaceValue(BigDecimal.ZERO);
|
|
||||||
s.setConsideration(BigDecimal.ZERO);
|
|
||||||
s.setAccruedDays((int) ChronoUnit.DAYS.between(start, fyEnd));
|
|
||||||
s.setAccruedInterest(BigDecimal.ZERO);
|
|
||||||
s.setReceivableCleared(BigDecimal.ZERO);
|
|
||||||
s.setBrokerage(BigDecimal.ZERO);
|
|
||||||
s.setBookValueSold(amount);
|
|
||||||
s.setPremiumCleared(amount);
|
|
||||||
s.setProfitLoss(amount.negate());
|
|
||||||
s.setNetAmount(BigDecimal.ZERO);
|
|
||||||
s.setBankGlAccount(p.getCrGlAccount());
|
|
||||||
s.setRemarks("Premium amortisation FY " + fy);
|
|
||||||
s.setStatus(STATUS_SETTLED);
|
|
||||||
s.setGlTransactionId(posted == null ? null : posted.getTransactionId());
|
|
||||||
final Object voucherNo = posted == null || posted.getChanges() == null ? null : posted.getChanges().get("voucherNo");
|
|
||||||
s.setVoucherNo(voucherNo == null ? null : voucherNo.toString());
|
|
||||||
s.setSettledOnDate(DateUtils.getBusinessLocalDate());
|
|
||||||
return this.saleRepository.saveAndFlush(s);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Straight-line by days: the share of the unamortised premium for start..yearEnd out of start..maturity. */
|
|
||||||
static BigDecimal share(final BigDecimal premium, final LocalDate start, final LocalDate yearEnd, final LocalDate maturity) {
|
|
||||||
final long total = ChronoUnit.DAYS.between(start, maturity);
|
|
||||||
if (premium.signum() <= 0 || total <= 0) {
|
|
||||||
return BigDecimal.ZERO;
|
|
||||||
}
|
|
||||||
return premium.multiply(BigDecimal.valueOf(ChronoUnit.DAYS.between(start, yearEnd))).divide(BigDecimal.valueOf(total), 2,
|
|
||||||
RoundingMode.HALF_UP);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean eligible(final InvestmentPurchase p) {
|
|
||||||
return STATUS_ACTIVE.equals(p.getStatus()) && p.getCategory() != null && p.getCategory().startsWith(HTM_PREFIX)
|
|
||||||
&& p.getMaturityDate() != null && this.quoter.unamortisedPremium(p).signum() > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The first 31 March after the last amortisation (or the settlement) that is still before maturity. */
|
|
||||||
private LocalDate nextYearEnd(final InvestmentPurchase p) {
|
|
||||||
final LocalDate start = periodStart(p);
|
|
||||||
return yearEnds(p).stream().filter(d -> d.isAfter(start)).findFirst().orElse(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private LocalDate periodStart(final InvestmentPurchase p) {
|
|
||||||
return amortisations(p).stream().map(InvestmentSale::getDealDate).max(LocalDate::compareTo).orElse(p.getSettlementDate());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 31 March dates after the settlement and before maturity. */
|
|
||||||
private static List<LocalDate> yearEnds(final InvestmentPurchase p) {
|
|
||||||
final List<LocalDate> ends = new ArrayList<>();
|
|
||||||
for (int y = p.getSettlementDate().getYear(); y <= p.getMaturityDate().getYear(); y++) {
|
|
||||||
final LocalDate d = LocalDate.of(y, Month.MARCH, 31);
|
|
||||||
if (d.isAfter(p.getSettlementDate()) && d.isBefore(p.getMaturityDate())) {
|
|
||||||
ends.add(d);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ends;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<InvestmentSale> amortisations(final InvestmentPurchase p) {
|
|
||||||
return this.saleRepository.findByPurchaseIdOrderByIdAsc(p.getId()).stream()
|
|
||||||
.filter(s -> TYPE_AMORTISE.equals(s.getSaleType()) && STATUS_SETTLED.equals(s.getStatus())).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static InvestmentAmortisationData.Row row(final String kind, final LocalDate end, final LocalDate start, final long days,
|
|
||||||
final BigDecimal amount, final BigDecimal after, final boolean posted, final InvestmentSale done, final boolean postable) {
|
|
||||||
return InvestmentAmortisationData.Row.builder().kind(kind).fyEnd(end.format(DATE)).fromDate(start.format(DATE))
|
|
||||||
.toDate(end.format(DATE)).days(days).amount(amount).premiumAfter(after).posted(posted)
|
|
||||||
.voucherNo(done == null ? null : done.getVoucherNo()).glTransactionId(done == null ? null : done.getGlTransactionId())
|
|
||||||
.postable(postable).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private InvestmentPurchase lockedPurchase(final Long purchaseId) {
|
|
||||||
return this.purchaseRepository.findByIdWithLock(purchaseId).orElseThrow(() -> new InvestmentPurchaseNotFoundException(purchaseId));
|
|
||||||
}
|
|
||||||
|
|
||||||
private InvestmentPurchase purchase(final Long purchaseId) {
|
|
||||||
return this.purchaseRepository.findById(purchaseId).orElseThrow(() -> new InvestmentPurchaseNotFoundException(purchaseId));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void validate(final String json) {
|
|
||||||
if (StringUtils.isBlank(json)) {
|
|
||||||
throw new InvalidJsonException();
|
|
||||||
}
|
|
||||||
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
|
||||||
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, AMORTISE_PARAMS);
|
|
||||||
final JsonElement element = this.fromApiJsonHelper.parse(json);
|
|
||||||
final List<ApiParameterError> errors = new ArrayList<>();
|
|
||||||
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource("investmentsale");
|
|
||||||
v.reset().parameter(FY_END).value(this.fromApiJsonHelper.extractLocalDateNamed(FY_END, element)).notNull();
|
|
||||||
v.reset().parameter(PURCHASE_ID).value(this.fromApiJsonHelper.extractLongNamed(PURCHASE_ID, element)).ignoreIfNull()
|
|
||||||
.longGreaterThanZero();
|
|
||||||
if (!errors.isEmpty()) {
|
|
||||||
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-56
@@ -1,56 +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.service;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.List;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.apache.fineract.portfolio.investment.data.InvestmentDailyRateData;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentDailyRateRepository;
|
|
||||||
import org.springframework.data.domain.PageRequest;
|
|
||||||
import org.springframework.data.domain.Sort;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
public class InvestmentDailyRateReadPlatformService {
|
|
||||||
|
|
||||||
/** The register shows the latest rates first; older ones are reached by picking a script. */
|
|
||||||
private static final int MAX_ROWS = 500;
|
|
||||||
|
|
||||||
private final InvestmentDailyRateRepository dailyRateRepository;
|
|
||||||
|
|
||||||
public List<InvestmentDailyRateData> retrieveAll(final Long scriptId) {
|
|
||||||
if (scriptId != null) {
|
|
||||||
return this.dailyRateRepository.findByScriptIdOrderByRateDateDesc(scriptId).stream().map(InvestmentDailyRateData::from)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
return this.dailyRateRepository
|
|
||||||
.findAll(PageRequest.of(0, MAX_ROWS, Sort.by(Sort.Direction.DESC, "rateDate").and(Sort.by(Sort.Direction.DESC, "id"))))
|
|
||||||
.stream().map(InvestmentDailyRateData::from).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The newest rate of a script on or before {@code onDate}; null when none has been entered. */
|
|
||||||
public InvestmentDailyRateData retrieveLatest(final Long scriptId, final LocalDate onDate) {
|
|
||||||
return this.dailyRateRepository.findFirstByScriptIdAndRateDateLessThanEqualOrderByRateDateDesc(scriptId, onDate)
|
|
||||||
.map(InvestmentDailyRateData::from).orElse(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-29
@@ -1,29 +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.service;
|
|
||||||
|
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
|
||||||
|
|
||||||
public interface InvestmentDailyRateWritePlatformService {
|
|
||||||
|
|
||||||
CommandProcessingResult createDailyRate(JsonCommand command);
|
|
||||||
|
|
||||||
CommandProcessingResult deleteDailyRate(Long rateId);
|
|
||||||
}
|
|
||||||
-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.service;
|
|
||||||
|
|
||||||
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.RATE_DATE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentDailyRateApiConstants.SCRIPT_ID;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
|
||||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentDailyRate;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentDailyRateRepository;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentScript;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentScriptRepository;
|
|
||||||
import org.apache.fineract.portfolio.investment.exception.InvestmentDailyRateNotFoundException;
|
|
||||||
import org.apache.fineract.portfolio.investment.exception.InvestmentScriptNotFoundException;
|
|
||||||
import org.apache.fineract.portfolio.investment.serialization.InvestmentDailyRateCommandFromApiJsonDeserializer;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
/** Saves and drops Daily Script Master rates. A wrong rate is corrected by dropping it and entering it again. */
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class InvestmentDailyRateWritePlatformServiceImpl implements InvestmentDailyRateWritePlatformService {
|
|
||||||
|
|
||||||
private final InvestmentDailyRateCommandFromApiJsonDeserializer validator;
|
|
||||||
private final InvestmentDailyRateRepository dailyRateRepository;
|
|
||||||
private final InvestmentScriptRepository scriptRepository;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public CommandProcessingResult createDailyRate(final JsonCommand command) {
|
|
||||||
this.validator.validateForCreate(command.json());
|
|
||||||
final Long scriptId = command.longValueOfParameterNamed(SCRIPT_ID);
|
|
||||||
final InvestmentScript script = this.scriptRepository.findById(scriptId)
|
|
||||||
.orElseThrow(() -> new InvestmentScriptNotFoundException(scriptId));
|
|
||||||
final LocalDate rateDate = command.localDateValueOfParameterNamed(RATE_DATE);
|
|
||||||
if (this.dailyRateRepository.existsByScriptIdAndRateDate(scriptId, rateDate)) {
|
|
||||||
throw new GeneralPlatformDomainRuleException("error.msg.investmentdailyrate.duplicate",
|
|
||||||
"A rate for " + script.getScriptCode() + " on this date already exists. Drop it first to correct it.");
|
|
||||||
}
|
|
||||||
|
|
||||||
final InvestmentDailyRate rate = new InvestmentDailyRate();
|
|
||||||
rate.setScript(script);
|
|
||||||
rate.setRateDate(rateDate);
|
|
||||||
rate.setOpenRate(command.bigDecimalValueOfParameterNamed(OPEN_RATE));
|
|
||||||
rate.setHighRate(command.bigDecimalValueOfParameterNamed(HIGH_RATE));
|
|
||||||
rate.setLowRate(command.bigDecimalValueOfParameterNamed(LOW_RATE));
|
|
||||||
rate.setCloseRate(command.bigDecimalValueOfParameterNamed(CLOSE_RATE));
|
|
||||||
final InvestmentDailyRate saved = this.dailyRateRepository.saveAndFlush(rate);
|
|
||||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(saved.getId())
|
|
||||||
.withResourceIdAsString(script.getScriptCode()).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public CommandProcessingResult deleteDailyRate(final Long rateId) {
|
|
||||||
final InvestmentDailyRate rate = this.dailyRateRepository.findById(rateId)
|
|
||||||
.orElseThrow(() -> new InvestmentDailyRateNotFoundException(rateId));
|
|
||||||
this.dailyRateRepository.delete(rate);
|
|
||||||
this.dailyRateRepository.flush();
|
|
||||||
return new CommandProcessingResultBuilder().withEntityId(rateId).build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-169
@@ -1,169 +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.service;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.math.RoundingMode;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.Month;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.TreeSet;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interest arithmetic for half-yearly coupon securities, as the bank's existing CBS does it (Jitendra Sir):
|
|
||||||
* <ul>
|
|
||||||
* <li>days are counted 30/360 — every month is 30 days, a half-year is 180, a year 360;</li>
|
|
||||||
* <li>the coupon is fixed: face value × rate ÷ 2, paid on the maturity day/month and six months either side;</li>
|
|
||||||
* <li>at each closing (31 March, 30 September) the interest earned since the last coupon is booked as a provision (Dr
|
|
||||||
* Interest Receivable / Cr Interest Income), even though the cash arrives at the next coupon;</li>
|
|
||||||
* <li>when the coupon arrives, the provision already booked is cleared and only the rest is new income.</li>
|
|
||||||
* </ul>
|
|
||||||
* Broken-period interest paid to the seller at purchase is an expense of its own (RBI UCB Investment Directions, para
|
|
||||||
* 152; GL 500606), so it is not carried here: the first coupon is income in full.
|
|
||||||
*/
|
|
||||||
public final class InvestmentInterestCalculator {
|
|
||||||
|
|
||||||
private static final BigDecimal THIRTY_SIX_THOUSAND = BigDecimal.valueOf(36000);
|
|
||||||
private static final BigDecimal TWO_HUNDRED = BigDecimal.valueOf(200);
|
|
||||||
|
|
||||||
public static final String TYPE_PROVISION = "PROVISION";
|
|
||||||
public static final String TYPE_COUPON = "COUPON";
|
|
||||||
|
|
||||||
private InvestmentInterestCalculator() {}
|
|
||||||
|
|
||||||
/** One line of the interest schedule. */
|
|
||||||
public record ScheduleRow(String type, LocalDate date, LocalDate fromDate, LocalDate toDate, int days, BigDecimal grossInterest,
|
|
||||||
BigDecimal alreadyBooked, BigDecimal amount) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Days between two dates on a 30/360 basis. Counting runs from the day after {@code from} up to and including
|
|
||||||
* {@code to}: 10-08 → 30-09 is 50 days, 10-08 → 08-10 is 58 days, 30-06 → 30-09 is 90 days.
|
|
||||||
*/
|
|
||||||
public static int days360(final LocalDate from, final LocalDate to) {
|
|
||||||
if (from == null || to == null || !to.isAfter(from)) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
final int d1 = Math.min(from.getDayOfMonth(), 30);
|
|
||||||
final int d2 = Math.min(to.getDayOfMonth(), 30);
|
|
||||||
return (to.getYear() - from.getYear()) * 360 + (to.getMonthValue() - from.getMonthValue()) * 30 + (d2 - d1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Interest on {@code faceValue} at {@code ratePercent} p.a. for {@code days} (30/360), rounded to the rupee. */
|
|
||||||
public static BigDecimal interestForDays(final BigDecimal faceValue, final BigDecimal ratePercent, final int days) {
|
|
||||||
return faceValue.multiply(ratePercent).multiply(BigDecimal.valueOf(days)).divide(THIRTY_SIX_THOUSAND, 0, RoundingMode.HALF_UP);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The fixed half-yearly coupon: face value × rate ÷ 2. */
|
|
||||||
public static BigDecimal halfYearCoupon(final BigDecimal faceValue, final BigDecimal ratePercent) {
|
|
||||||
return faceValue.multiply(ratePercent).divide(TWO_HUNDRED, 0, RoundingMode.HALF_UP);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** k-th coupon date counting back from maturity — measured from maturity each time so month-end days survive. */
|
|
||||||
private static LocalDate couponBack(final LocalDate maturity, final int k) {
|
|
||||||
return maturity.minusMonths(6L * k);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The most recent coupon date on or before {@code on}. */
|
|
||||||
public static LocalDate lastCouponOnOrBefore(final LocalDate maturity, final LocalDate on) {
|
|
||||||
int k = 0;
|
|
||||||
LocalDate d = maturity;
|
|
||||||
while (d.isAfter(on)) {
|
|
||||||
k++;
|
|
||||||
d = couponBack(maturity, k);
|
|
||||||
}
|
|
||||||
return d;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Coupon dates strictly after {@code from}, up to and including maturity, in date order. */
|
|
||||||
public static List<LocalDate> couponDatesAfter(final LocalDate maturity, final LocalDate from) {
|
|
||||||
final List<LocalDate> dates = new ArrayList<>();
|
|
||||||
int k = 0;
|
|
||||||
LocalDate d = maturity;
|
|
||||||
while (d.isAfter(from)) {
|
|
||||||
dates.add(0, d);
|
|
||||||
k++;
|
|
||||||
d = couponBack(maturity, k);
|
|
||||||
}
|
|
||||||
return dates;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Closing dates (31 March, 30 September) from {@code from} up to {@code to}, inclusive. */
|
|
||||||
public static List<LocalDate> closingDates(final LocalDate from, final LocalDate to) {
|
|
||||||
final List<LocalDate> dates = new ArrayList<>();
|
|
||||||
for (int year = from.getYear(); year <= to.getYear(); year++) {
|
|
||||||
for (final LocalDate closing : List.of(LocalDate.of(year, Month.MARCH, 31), LocalDate.of(year, Month.SEPTEMBER, 30))) {
|
|
||||||
if (!closing.isBefore(from) && !closing.isAfter(to)) {
|
|
||||||
dates.add(closing);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dates;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The full interest schedule of a holding, from purchase to maturity.
|
|
||||||
*
|
|
||||||
* @param faceValue
|
|
||||||
* face value currently held
|
|
||||||
* @param ratePercent
|
|
||||||
* coupon rate % p.a.
|
|
||||||
* @param maturity
|
|
||||||
* maturity date (coupons fall on its day/month and six months either side)
|
|
||||||
* @param lastIntDate
|
|
||||||
* coupon date immediately before the purchase
|
|
||||||
* @param dealDate
|
|
||||||
* date the holding became ours (the purchase settlement date) — closings before it belong to the seller
|
|
||||||
*/
|
|
||||||
public static List<ScheduleRow> schedule(final BigDecimal faceValue, final BigDecimal ratePercent, final LocalDate maturity,
|
|
||||||
final LocalDate lastIntDate, final LocalDate dealDate) {
|
|
||||||
final List<ScheduleRow> rows = new ArrayList<>();
|
|
||||||
if (faceValue == null || ratePercent == null || ratePercent.signum() <= 0 || maturity == null || lastIntDate == null
|
|
||||||
|| dealDate == null || !maturity.isAfter(lastIntDate)) {
|
|
||||||
return rows;
|
|
||||||
}
|
|
||||||
final List<LocalDate> coupons = couponDatesAfter(maturity, lastIntDate);
|
|
||||||
final TreeSet<LocalDate> events = new TreeSet<>(coupons);
|
|
||||||
for (final LocalDate closing : closingDates(dealDate, maturity)) {
|
|
||||||
if (closing.isAfter(lastIntDate) && !events.contains(closing)) {
|
|
||||||
events.add(closing);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final BigDecimal coupon = halfYearCoupon(faceValue, ratePercent);
|
|
||||||
BigDecimal receivable = BigDecimal.ZERO;
|
|
||||||
LocalDate previousCoupon = lastIntDate;
|
|
||||||
for (final LocalDate date : events) {
|
|
||||||
final int days = days360(previousCoupon, date);
|
|
||||||
if (coupons.contains(date)) {
|
|
||||||
rows.add(new ScheduleRow(TYPE_COUPON, date, previousCoupon.plusDays(1), date, days, coupon, receivable,
|
|
||||||
coupon.subtract(receivable)));
|
|
||||||
receivable = BigDecimal.ZERO;
|
|
||||||
previousCoupon = date;
|
|
||||||
} else {
|
|
||||||
final BigDecimal earned = interestForDays(faceValue, ratePercent, days);
|
|
||||||
rows.add(new ScheduleRow(TYPE_PROVISION, date, previousCoupon.plusDays(1), date, days, earned, receivable,
|
|
||||||
earned.subtract(receivable)));
|
|
||||||
receivable = earned;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return rows;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-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.service;
|
|
||||||
|
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
|
||||||
|
|
||||||
public interface InvestmentInterestPostingWritePlatformService {
|
|
||||||
|
|
||||||
CommandProcessingResult postInterest(Long purchaseId, JsonCommand command);
|
|
||||||
}
|
|
||||||
-145
@@ -1,145 +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.service;
|
|
||||||
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentInterestApiConstants.BANK_GL_ACCOUNT_ID;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentInterestApiConstants.INTEREST_RECEIVABLE_GL_CODE;
|
|
||||||
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.InvestmentPurchaseApiConstants.STATUS_ACTIVE;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.util.List;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
|
|
||||||
import org.apache.fineract.accounting.glaccount.domain.GLAccountRepositoryWrapper;
|
|
||||||
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
|
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
|
||||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
|
||||||
import org.apache.fineract.portfolio.investment.api.InvestmentInterestApiConstants;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentInterestPosting;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentInterestPostingRepository;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchase;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchaseRepository;
|
|
||||||
import org.apache.fineract.portfolio.investment.exception.InvestmentPurchaseNotFoundException;
|
|
||||||
import org.apache.fineract.portfolio.investment.serialization.InvestmentInterestCommandFromApiJsonDeserializer;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Posts one interest schedule line of a holding to GL with its JV voucher, on or after its date and in date order:
|
|
||||||
* <ul>
|
|
||||||
* <li>provision (31 March / 30 September): Dr Interest Receivable / Cr Interest Income;</li>
|
|
||||||
* <li>coupon: Dr Bank (full coupon) / Cr Interest Receivable (what the provision booked) / Cr Interest Income (the
|
|
||||||
* rest).</li>
|
|
||||||
* </ul>
|
|
||||||
* The amounts come from the schedule, never from the request, so the voucher always matches what the screen shows.
|
|
||||||
*/
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class InvestmentInterestPostingWritePlatformServiceImpl implements InvestmentInterestPostingWritePlatformService {
|
|
||||||
|
|
||||||
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
|
||||||
|
|
||||||
private final InvestmentInterestCommandFromApiJsonDeserializer validator;
|
|
||||||
private final InvestmentPurchaseRepository purchaseRepository;
|
|
||||||
private final InvestmentInterestPostingRepository postingRepository;
|
|
||||||
private final InvestmentPurchaseReadPlatformService readPlatformService;
|
|
||||||
private final InvestmentJournalPoster journalPoster;
|
|
||||||
private final GLAccountRepositoryWrapper glAccountRepository;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public CommandProcessingResult postInterest(final Long purchaseId, final JsonCommand command) {
|
|
||||||
this.validator.validateForPost(command.json());
|
|
||||||
final InvestmentPurchase p = this.purchaseRepository.findByIdWithLock(purchaseId)
|
|
||||||
.orElseThrow(() -> new InvestmentPurchaseNotFoundException(purchaseId));
|
|
||||||
if (!STATUS_ACTIVE.equals(p.getStatus())) {
|
|
||||||
throw rule("not.active", "Deal " + p.getDealNo() + " is not an active (settled) holding");
|
|
||||||
}
|
|
||||||
final String lineType = command.stringValueOfParameterNamed(LINE_TYPE);
|
|
||||||
final LocalDate intDate = command.localDateValueOfParameterNamed(INT_DATE);
|
|
||||||
if (intDate.isAfter(DateUtils.getBusinessLocalDate())) {
|
|
||||||
throw rule("before.date", "This interest line can be posted on or after " + intDate.format(DATE));
|
|
||||||
}
|
|
||||||
|
|
||||||
final List<InvestmentInterestPosting> posted = this.postingRepository.findByPurchaseIdOrderByIntDateAsc(purchaseId);
|
|
||||||
final InvestmentInterestCalculator.ScheduleRow next = this.readPlatformService.scheduleRows(p).stream()
|
|
||||||
.filter(r -> posted.stream().noneMatch(x -> x.getLineType().equals(r.type()) && x.getIntDate().equals(r.date())))
|
|
||||||
.findFirst().orElseThrow(() -> rule("all.posted", "All interest lines of deal " + p.getDealNo() + " are already posted"));
|
|
||||||
if (!next.type().equals(lineType) || !next.date().equals(intDate)) {
|
|
||||||
throw rule("out.of.order", "Post the interest lines in date order: next for deal " + p.getDealNo() + " is the "
|
|
||||||
+ next.type().toLowerCase() + " of " + next.date().format(DATE));
|
|
||||||
}
|
|
||||||
|
|
||||||
final Long receivableGl = this.journalPoster.glId(INTEREST_RECEIVABLE_GL_CODE);
|
|
||||||
final Long incomeGl = this.journalPoster.glId(InvestmentInterestApiConstants.interestIncomeGlCode(p.getBondType()));
|
|
||||||
final InvestmentJournalPoster.Lines lines = new InvestmentJournalPoster.Lines();
|
|
||||||
GLAccount bankGl = null;
|
|
||||||
if (InvestmentInterestCalculator.TYPE_COUPON.equals(lineType)) {
|
|
||||||
bankGl = command.parameterExists(BANK_GL_ACCOUNT_ID) && command.longValueOfParameterNamed(BANK_GL_ACCOUNT_ID) != null
|
|
||||||
? postableAssetGl(command.longValueOfParameterNamed(BANK_GL_ACCOUNT_ID))
|
|
||||||
: p.getCrGlAccount();
|
|
||||||
lines.debit(bankGl.getId(), next.grossInterest()).credit(receivableGl, next.alreadyBooked()).credit(incomeGl, next.amount());
|
|
||||||
} else {
|
|
||||||
lines.debit(receivableGl, next.amount()).credit(incomeGl, next.amount());
|
|
||||||
}
|
|
||||||
final String what = InvestmentInterestCalculator.TYPE_COUPON.equals(lineType) ? "Coupon received" : "Interest provision";
|
|
||||||
final CommandProcessingResult result = this.journalPoster.post(intDate, p.getDealNo(), what + " " + intDate.format(DATE) + ": deal "
|
|
||||||
+ p.getDealNo() + ", " + p.getScript().getScriptCode() + " " + p.getScript().getScriptDesc(), lines);
|
|
||||||
|
|
||||||
final InvestmentInterestPosting posting = new InvestmentInterestPosting();
|
|
||||||
posting.setPurchase(p);
|
|
||||||
posting.setLineType(lineType);
|
|
||||||
posting.setIntDate(intDate);
|
|
||||||
posting.setFromDate(next.fromDate());
|
|
||||||
posting.setToDate(next.toDate());
|
|
||||||
posting.setDays(next.days());
|
|
||||||
posting.setGrossInterest(next.grossInterest());
|
|
||||||
posting.setAlreadyBooked(next.alreadyBooked());
|
|
||||||
posting.setAmount(next.amount());
|
|
||||||
posting.setBankGlAccount(bankGl);
|
|
||||||
posting.setGlTransactionId(result.getTransactionId());
|
|
||||||
final Object voucherNo = result.getChanges() == null ? null : result.getChanges().get("voucherNo");
|
|
||||||
posting.setVoucherNo(voucherNo == null ? null : voucherNo.toString());
|
|
||||||
posting.setPostedOnDate(DateUtils.getBusinessLocalDate());
|
|
||||||
final InvestmentInterestPosting saved = this.postingRepository.saveAndFlush(posting);
|
|
||||||
|
|
||||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(saved.getId())
|
|
||||||
.withSubEntityId(purchaseId).withResourceIdAsString(posting.getVoucherNo()).withTransactionId(result.getTransactionId())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A bank the coupon is received into must be an enabled ASSET detail GL. */
|
|
||||||
private GLAccount postableAssetGl(final Long glAccountId) {
|
|
||||||
final GLAccount gl = this.glAccountRepository.findOneWithNotFoundDetection(glAccountId);
|
|
||||||
if (gl.isHeaderAccount() || gl.isDisabled() || !GLAccountType.ASSET.getValue().equals(gl.getType())) {
|
|
||||||
throw rule("bank.glaccount.invalid", "Bank GL " + gl.getGlCode() + " must be an enabled Asset detail account");
|
|
||||||
}
|
|
||||||
return gl;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static GeneralPlatformDomainRuleException rule(final String code, final String message) {
|
|
||||||
return new GeneralPlatformDomainRuleException("error.msg.investmentinterest." + code, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-112
@@ -1,112 +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.service;
|
|
||||||
|
|
||||||
import com.google.gson.JsonArray;
|
|
||||||
import com.google.gson.JsonObject;
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.util.Locale;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.apache.fineract.accounting.glaccount.domain.GLAccountRepositoryWrapper;
|
|
||||||
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
|
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
|
||||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Posts investment entries to GL through the journal entry command, like any manual entry: it checks the GLs and
|
|
||||||
* accounting closures, joins the caller's transaction (so under maker-checker nothing persists until approval) and
|
|
||||||
* raises the JV voucher. Investments are a Head Office function, so every entry is booked there, in INR.
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class InvestmentJournalPoster {
|
|
||||||
|
|
||||||
private static final String DATE_FORMAT = "dd MMMM yyyy";
|
|
||||||
|
|
||||||
private final JournalEntryWritePlatformService journalEntryWritePlatformService;
|
|
||||||
private final FromJsonHelper fromApiJsonHelper;
|
|
||||||
private final GLAccountRepositoryWrapper glAccountRepository;
|
|
||||||
private final JdbcTemplate jdbcTemplate;
|
|
||||||
|
|
||||||
/** Debit and credit lines of one entry; zero or missing amounts are left out. */
|
|
||||||
public static final class Lines {
|
|
||||||
|
|
||||||
private final JsonArray debits = new JsonArray();
|
|
||||||
private final JsonArray credits = new JsonArray();
|
|
||||||
|
|
||||||
public Lines debit(final Long glAccountId, final BigDecimal amount) {
|
|
||||||
add(this.debits, glAccountId, amount);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Lines credit(final Long glAccountId, final BigDecimal amount) {
|
|
||||||
add(this.credits, glAccountId, amount);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void add(final JsonArray lines, final Long glAccountId, final BigDecimal amount) {
|
|
||||||
if (amount != null && amount.signum() < 0) {
|
|
||||||
// A negative line means the deal's figures are inconsistent; dropping it would post an unbalanced entry.
|
|
||||||
throw new org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException(
|
|
||||||
"error.msg.investment.journal.negative.line", "A journal line of " + amount.toPlainString()
|
|
||||||
+ " came out negative, so the entry was not posted; check the deal's book value",
|
|
||||||
glAccountId);
|
|
||||||
}
|
|
||||||
if (amount != null && amount.signum() > 0) {
|
|
||||||
final JsonObject line = new JsonObject();
|
|
||||||
line.addProperty("glAccountId", glAccountId);
|
|
||||||
line.addProperty("amount", amount);
|
|
||||||
lines.add(line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long glId(final String glCode) {
|
|
||||||
return this.glAccountRepository.findOneByGlCodeWithNotFoundDetection(glCode).getId();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Posts the entry dated {@code date}; the result carries the GL transaction id and, in its changes, the voucherNo.
|
|
||||||
*/
|
|
||||||
public CommandProcessingResult post(final LocalDate date, final String referenceNumber, final String comments, final Lines lines) {
|
|
||||||
final JsonObject payload = new JsonObject();
|
|
||||||
payload.addProperty("officeId", headOfficeId());
|
|
||||||
payload.addProperty("transactionDate", date.format(DateTimeFormatter.ofPattern(DATE_FORMAT, Locale.ENGLISH)));
|
|
||||||
payload.addProperty("dateFormat", DATE_FORMAT);
|
|
||||||
payload.addProperty("locale", "en");
|
|
||||||
payload.addProperty("currencyCode", "INR");
|
|
||||||
payload.addProperty("referenceNumber", referenceNumber);
|
|
||||||
payload.addProperty("comments", comments);
|
|
||||||
payload.add("debits", lines.debits);
|
|
||||||
payload.add("credits", lines.credits);
|
|
||||||
final String json = payload.toString();
|
|
||||||
final JsonCommand journalEntryCommand = JsonCommand.from(json, this.fromApiJsonHelper.parse(json), this.fromApiJsonHelper, null,
|
|
||||||
null, null, null, null, null, null, null, null, null, null, null, null, null);
|
|
||||||
return this.journalEntryWritePlatformService.createJournalEntry(journalEntryCommand);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Long headOfficeId() {
|
|
||||||
return this.jdbcTemplate.queryForObject("SELECT id FROM m_office WHERE parent_id IS NULL ORDER BY id LIMIT 1", Long.class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-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.service;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import org.apache.fineract.portfolio.investment.data.InvestmentPartyData;
|
|
||||||
import org.apache.fineract.portfolio.investment.data.InvestmentScriptData;
|
|
||||||
|
|
||||||
public interface InvestmentMasterReadPlatformService {
|
|
||||||
|
|
||||||
List<InvestmentPartyData> retrieveAllParties();
|
|
||||||
|
|
||||||
InvestmentPartyData retrieveParty(Long partyId);
|
|
||||||
|
|
||||||
List<InvestmentScriptData> retrieveAllScripts();
|
|
||||||
|
|
||||||
InvestmentScriptData retrieveScript(Long scriptId);
|
|
||||||
}
|
|
||||||
-65
@@ -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.service;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.apache.fineract.portfolio.investment.data.InvestmentPartyData;
|
|
||||||
import org.apache.fineract.portfolio.investment.data.InvestmentScriptData;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPartyRepository;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentScriptRepository;
|
|
||||||
import org.apache.fineract.portfolio.investment.exception.InvestmentPartyNotFoundException;
|
|
||||||
import org.apache.fineract.portfolio.investment.exception.InvestmentScriptNotFoundException;
|
|
||||||
import org.springframework.data.domain.Sort;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
/** Masters are small (tens of rows), so they are listed whole, newest first, the way the screens show them. */
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
public class InvestmentMasterReadPlatformServiceImpl implements InvestmentMasterReadPlatformService {
|
|
||||||
|
|
||||||
private static final Sort NEWEST_FIRST = Sort.by(Sort.Direction.DESC, "id");
|
|
||||||
|
|
||||||
private final InvestmentPartyRepository partyRepository;
|
|
||||||
private final InvestmentScriptRepository scriptRepository;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<InvestmentPartyData> retrieveAllParties() {
|
|
||||||
return this.partyRepository.findAll(NEWEST_FIRST).stream().map(InvestmentPartyData::from).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InvestmentPartyData retrieveParty(final Long partyId) {
|
|
||||||
return this.partyRepository.findById(partyId).map(InvestmentPartyData::from)
|
|
||||||
.orElseThrow(() -> new InvestmentPartyNotFoundException(partyId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<InvestmentScriptData> retrieveAllScripts() {
|
|
||||||
return this.scriptRepository.findAll(NEWEST_FIRST).stream().map(InvestmentScriptData::from).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InvestmentScriptData retrieveScript(final Long scriptId) {
|
|
||||||
return this.scriptRepository.findById(scriptId).map(InvestmentScriptData::from)
|
|
||||||
.orElseThrow(() -> new InvestmentScriptNotFoundException(scriptId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-35
@@ -1,35 +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.service;
|
|
||||||
|
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
|
||||||
|
|
||||||
public interface InvestmentMasterWritePlatformService {
|
|
||||||
|
|
||||||
CommandProcessingResult createParty(JsonCommand command);
|
|
||||||
|
|
||||||
CommandProcessingResult updateParty(Long partyId, JsonCommand command);
|
|
||||||
|
|
||||||
CommandProcessingResult deleteParty(Long partyId);
|
|
||||||
|
|
||||||
CommandProcessingResult createScript(JsonCommand command);
|
|
||||||
|
|
||||||
CommandProcessingResult updateScript(Long scriptId, JsonCommand command);
|
|
||||||
}
|
|
||||||
-347
@@ -1,347 +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.service;
|
|
||||||
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ACTIVE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ADDRESS1;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ADDRESS2;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ADDRESS3;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.BANK_TYPE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.BOND_TYPE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CITY;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.COMPANY_NAME;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CORP_CODE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CP_ACCOUNT_NO;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CP_BANK_NAME;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CP_BRANCH_NAME;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.CP_IFSC;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.EXPOSURE_LIMIT;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.FACE_VALUE_PER_UNIT;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.FAX;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.GL_ACCOUNT_ID;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.INT_RATE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ISIN;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.ISSUE_DATE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.LEI_NO;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.MATURITY_DATE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PARTY_CODE_PREFIX;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PARTY_NAME;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PHONE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.PINCODE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.RBI_CODE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SCRIPT_CODE_PREFIX;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SCRIPT_DESC;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.SLR_TYPE;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
|
|
||||||
import org.apache.fineract.accounting.glaccount.domain.GLAccountRepositoryWrapper;
|
|
||||||
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
|
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
|
||||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
|
||||||
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentParty;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPartyRepository;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchaseRepository;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentScript;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentScriptRepository;
|
|
||||||
import org.apache.fineract.portfolio.investment.exception.InvestmentPartyNotFoundException;
|
|
||||||
import org.apache.fineract.portfolio.investment.exception.InvestmentScriptNotFoundException;
|
|
||||||
import org.apache.fineract.portfolio.investment.serialization.InvestmentMasterCommandFromApiJsonDeserializer;
|
|
||||||
import org.springframework.dao.DataIntegrityViolationException;
|
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class InvestmentMasterWritePlatformServiceImpl implements InvestmentMasterWritePlatformService {
|
|
||||||
|
|
||||||
private final InvestmentMasterCommandFromApiJsonDeserializer validator;
|
|
||||||
private final InvestmentPartyRepository partyRepository;
|
|
||||||
private final InvestmentScriptRepository scriptRepository;
|
|
||||||
private final InvestmentPurchaseRepository purchaseRepository;
|
|
||||||
private final GLAccountRepositoryWrapper glAccountRepository;
|
|
||||||
private final JdbcTemplate jdbcTemplate;
|
|
||||||
|
|
||||||
// -------------------- Bank / Party Master --------------------
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public CommandProcessingResult createParty(final JsonCommand command) {
|
|
||||||
this.validator.validateForCreateParty(command.json());
|
|
||||||
final InvestmentParty party = new InvestmentParty();
|
|
||||||
party.setPartyCode(nextPartyCode());
|
|
||||||
applyParty(party, command);
|
|
||||||
if (command.parameterExists(ACTIVE)) {
|
|
||||||
party.setActive(command.booleanPrimitiveValueOfParameterNamed(ACTIVE));
|
|
||||||
}
|
|
||||||
final InvestmentParty saved = saveParty(party);
|
|
||||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(saved.getId())
|
|
||||||
.withResourceIdAsString(saved.getPartyCode()).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public CommandProcessingResult updateParty(final Long partyId, final JsonCommand command) {
|
|
||||||
this.validator.validateForUpdateParty(command.json());
|
|
||||||
final InvestmentParty party = this.partyRepository.findById(partyId).orElseThrow(() -> new InvestmentPartyNotFoundException(partyId));
|
|
||||||
applyParty(party, command);
|
|
||||||
if (command.parameterExists(ACTIVE)) {
|
|
||||||
party.setActive(command.booleanPrimitiveValueOfParameterNamed(ACTIVE));
|
|
||||||
}
|
|
||||||
saveParty(party);
|
|
||||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(partyId)
|
|
||||||
.withResourceIdAsString(party.getPartyCode()).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public CommandProcessingResult deleteParty(final Long partyId) {
|
|
||||||
final InvestmentParty party = this.partyRepository.findById(partyId).orElseThrow(() -> new InvestmentPartyNotFoundException(partyId));
|
|
||||||
this.partyRepository.delete(party);
|
|
||||||
this.partyRepository.flush();
|
|
||||||
return new CommandProcessingResultBuilder().withEntityId(partyId).withResourceIdAsString(party.getPartyCode()).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Copies every party field present in the request onto the entity (absent fields are left unchanged). */
|
|
||||||
private void applyParty(final InvestmentParty p, final JsonCommand command) {
|
|
||||||
setIfPresent(command, BOND_TYPE, p::setBondType);
|
|
||||||
setIfPresent(command, PARTY_NAME, p::setPartyName);
|
|
||||||
setIfPresent(command, ADDRESS1, p::setAddress1);
|
|
||||||
setIfPresent(command, ADDRESS2, p::setAddress2);
|
|
||||||
setIfPresent(command, ADDRESS3, p::setAddress3);
|
|
||||||
setIfPresent(command, CITY, p::setCity);
|
|
||||||
setIfPresent(command, PINCODE, p::setPincode);
|
|
||||||
setIfPresent(command, PHONE, p::setPhone);
|
|
||||||
setIfPresent(command, FAX, p::setFax);
|
|
||||||
setIfPresent(command, BANK_TYPE, p::setBankType);
|
|
||||||
setIfPresent(command, RBI_CODE, p::setRbiCode);
|
|
||||||
setIfPresent(command, LEI_NO, p::setLeiNo);
|
|
||||||
setIfPresent(command, CP_BANK_NAME, p::setCpBankName);
|
|
||||||
setIfPresent(command, CP_BRANCH_NAME, p::setCpBranchName);
|
|
||||||
setIfPresent(command, CP_ACCOUNT_NO, p::setCpAccountNo);
|
|
||||||
if (command.parameterExists(CP_IFSC)) {
|
|
||||||
final String ifsc = command.stringValueOfParameterNamedAllowingNull(CP_IFSC);
|
|
||||||
p.setCpIfsc(StringUtils.isBlank(ifsc) ? null : ifsc.trim().toUpperCase());
|
|
||||||
}
|
|
||||||
if (command.parameterExists(EXPOSURE_LIMIT)) {
|
|
||||||
p.setExposureLimit(command.bigDecimalValueOfParameterNamed(EXPOSURE_LIMIT));
|
|
||||||
}
|
|
||||||
if (command.parameterExists(GL_ACCOUNT_ID)) {
|
|
||||||
p.setGlAccount(postableAssetGl(command.longValueOfParameterNamed(GL_ACCOUNT_ID)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private InvestmentParty saveParty(final InvestmentParty party) {
|
|
||||||
try {
|
|
||||||
return this.partyRepository.saveAndFlush(party);
|
|
||||||
} catch (final DataIntegrityViolationException e) {
|
|
||||||
throw new PlatformDataIntegrityException("error.msg.investmentparty.duplicate", "A party with this code already exists",
|
|
||||||
"partyCode", party.getPartyCode(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Next code in the PTY-001, PTY-002 … series. */
|
|
||||||
private String nextPartyCode() {
|
|
||||||
final Integer max = this.jdbcTemplate.queryForObject(
|
|
||||||
"SELECT MAX(CAST(SUBSTRING(party_code FROM 5) AS INTEGER)) FROM m_investment_party WHERE party_code ~ '^PTY-[0-9]+$'",
|
|
||||||
Integer.class);
|
|
||||||
return PARTY_CODE_PREFIX + String.format("%03d", (max == null ? 0 : max) + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------- Script Master --------------------
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public CommandProcessingResult createScript(final JsonCommand command) {
|
|
||||||
this.validator.validateForCreateScript(command.json());
|
|
||||||
final InvestmentScript script = new InvestmentScript();
|
|
||||||
script.setBondType(command.stringValueOfParameterNamed(BOND_TYPE));
|
|
||||||
script.setScriptCode(nextScriptCode(script.getBondType()));
|
|
||||||
script.setFaceValuePerUnit(defaultFaceValue(script.getBondType()));
|
|
||||||
applyScript(script, command);
|
|
||||||
if (command.parameterExists(ACTIVE)) {
|
|
||||||
script.setActive(command.booleanPrimitiveValueOfParameterNamed(ACTIVE));
|
|
||||||
}
|
|
||||||
final InvestmentScript saved = saveScript(script);
|
|
||||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(saved.getId())
|
|
||||||
.withResourceIdAsString(saved.getScriptCode()).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public CommandProcessingResult updateScript(final Long scriptId, final JsonCommand command) {
|
|
||||||
final InvestmentScript script = this.scriptRepository.findById(scriptId)
|
|
||||||
.orElseThrow(() -> new InvestmentScriptNotFoundException(scriptId));
|
|
||||||
this.validator.validateForUpdateScript(command.json(), script.getBondType());
|
|
||||||
if (command.parameterExists(BOND_TYPE) && !script.getBondType().equals(command.stringValueOfParameterNamed(BOND_TYPE))) {
|
|
||||||
// The script code carries the bond-type letter (G00001), so the type cannot change once created.
|
|
||||||
throw new GeneralPlatformDomainRuleException("error.msg.investmentscript.bondtype.immutable",
|
|
||||||
"Bond Type of an existing script cannot be changed", script.getScriptCode());
|
|
||||||
}
|
|
||||||
requireTermsUnchangedOnceUsed(script, command);
|
|
||||||
applyScript(script, command);
|
|
||||||
if (command.parameterExists(ACTIVE)) {
|
|
||||||
script.setActive(command.booleanPrimitiveValueOfParameterNamed(ACTIVE));
|
|
||||||
}
|
|
||||||
saveScript(script);
|
|
||||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(scriptId)
|
|
||||||
.withResourceIdAsString(script.getScriptCode()).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unit value, coupon rate and maturity drive the interest, sale, redemption and amortisation of every deal on the
|
|
||||||
* script, so they are fixed once a deal uses it; a different security is a new script (or a GOI category transfer).
|
|
||||||
*/
|
|
||||||
private void requireTermsUnchangedOnceUsed(final InvestmentScript s, final JsonCommand command) {
|
|
||||||
final List<String> changed = new ArrayList<>();
|
|
||||||
if (command.parameterExists(FACE_VALUE_PER_UNIT) && command.bigDecimalValueOfParameterNamed(FACE_VALUE_PER_UNIT) != null
|
|
||||||
&& !sameAmount(command.bigDecimalValueOfParameterNamed(FACE_VALUE_PER_UNIT), s.getFaceValuePerUnit())) {
|
|
||||||
changed.add("face value per unit");
|
|
||||||
}
|
|
||||||
if (command.parameterExists(INT_RATE) && !sameAmount(command.bigDecimalValueOfParameterNamed(INT_RATE), s.getIntRate())) {
|
|
||||||
changed.add("interest rate");
|
|
||||||
}
|
|
||||||
if (command.parameterExists(MATURITY_DATE)
|
|
||||||
&& !Objects.equals(command.localDateValueOfParameterNamed(MATURITY_DATE), s.getMaturityDate())) {
|
|
||||||
changed.add("maturity date");
|
|
||||||
}
|
|
||||||
if (!changed.isEmpty() && this.purchaseRepository.existsByScriptId(s.getId())) {
|
|
||||||
throw new GeneralPlatformDomainRuleException("error.msg.investmentscript.terms.in.use", "Script " + s.getScriptCode()
|
|
||||||
+ " is already used by deals, so its " + String.join(", ", changed) + " cannot be changed", s.getScriptCode());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean sameAmount(final BigDecimal a, final BigDecimal b) {
|
|
||||||
return a == null ? b == null : b != null && a.compareTo(b) == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void applyScript(final InvestmentScript s, final JsonCommand command) {
|
|
||||||
setIfPresent(command, SLR_TYPE, s::setSlrType);
|
|
||||||
setIfPresent(command, SCRIPT_DESC, s::setScriptDesc);
|
|
||||||
if (command.parameterExists(ISIN)) {
|
|
||||||
requireUniqueIsin(s, command.stringValueOfParameterNamed(ISIN));
|
|
||||||
}
|
|
||||||
setIfPresent(command, ISIN, s::setIsin);
|
|
||||||
setIfPresent(command, COMPANY_NAME, s::setCompanyName);
|
|
||||||
setIfPresent(command, ADDRESS1, s::setAddress1);
|
|
||||||
setIfPresent(command, ADDRESS2, s::setAddress2);
|
|
||||||
setIfPresent(command, ADDRESS3, s::setAddress3);
|
|
||||||
setIfPresent(command, CITY, s::setCity);
|
|
||||||
setIfPresent(command, PINCODE, s::setPincode);
|
|
||||||
setIfPresent(command, PHONE, s::setPhone);
|
|
||||||
setIfPresent(command, FAX, s::setFax);
|
|
||||||
setIfPresent(command, CORP_CODE, s::setCorpCode);
|
|
||||||
if (command.parameterExists(FACE_VALUE_PER_UNIT) && command.bigDecimalValueOfParameterNamed(FACE_VALUE_PER_UNIT) != null) {
|
|
||||||
s.setFaceValuePerUnit(command.bigDecimalValueOfParameterNamed(FACE_VALUE_PER_UNIT));
|
|
||||||
}
|
|
||||||
if (command.parameterExists(INT_RATE)) {
|
|
||||||
s.setIntRate(command.bigDecimalValueOfParameterNamed(INT_RATE));
|
|
||||||
}
|
|
||||||
if (command.parameterExists(ISSUE_DATE)) {
|
|
||||||
s.setIssueDate(command.localDateValueOfParameterNamed(ISSUE_DATE));
|
|
||||||
}
|
|
||||||
if (command.parameterExists(MATURITY_DATE)) {
|
|
||||||
s.setMaturityDate(command.localDateValueOfParameterNamed(MATURITY_DATE));
|
|
||||||
}
|
|
||||||
if (s.getIssueDate() != null && s.getMaturityDate() != null && s.getMaturityDate().isBefore(s.getIssueDate())) {
|
|
||||||
throw new GeneralPlatformDomainRuleException("error.msg.investmentscript.maturity.before.issue",
|
|
||||||
"Maturity date cannot be before the issue date");
|
|
||||||
}
|
|
||||||
if (command.parameterExists(GL_ACCOUNT_ID)) {
|
|
||||||
s.setGlAccount(postableAssetGl(command.longValueOfParameterNamed(GL_ACCOUNT_ID)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** An ISIN identifies one security, so two scripts cannot carry it; checked when it is entered or changed. */
|
|
||||||
private void requireUniqueIsin(final InvestmentScript s, final String isin) {
|
|
||||||
if (StringUtils.isBlank(isin) || isin.trim().equalsIgnoreCase(StringUtils.trimToEmpty(s.getIsin()))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.scriptRepository.findByIsinIgnoreCase(isin.trim()).stream().filter(other -> !other.getId().equals(s.getId())).findFirst()
|
|
||||||
.ifPresent(other -> {
|
|
||||||
throw new GeneralPlatformDomainRuleException("error.msg.investmentscript.isin.duplicate",
|
|
||||||
"ISIN " + isin.trim() + " is already used by script " + other.getScriptCode() + " " + other.getScriptDesc(),
|
|
||||||
isin.trim());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private InvestmentScript saveScript(final InvestmentScript script) {
|
|
||||||
try {
|
|
||||||
return this.scriptRepository.saveAndFlush(script);
|
|
||||||
} catch (final DataIntegrityViolationException e) {
|
|
||||||
throw new PlatformDataIntegrityException("error.msg.investmentscript.duplicate", "A script with this code already exists",
|
|
||||||
"scriptCode", script.getScriptCode(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Next code for the bond type — its letter plus a five-digit running number, e.g. G00001. */
|
|
||||||
private String nextScriptCode(final String bondType) {
|
|
||||||
final String prefix = SCRIPT_CODE_PREFIX.get(bondType);
|
|
||||||
final Integer max = this.jdbcTemplate.queryForObject(
|
|
||||||
"SELECT MAX(CAST(SUBSTRING(script_code FROM 2) AS INTEGER)) FROM m_investment_script WHERE script_code ~ ?",
|
|
||||||
Integer.class, "^" + prefix + "[0-9]{5}$");
|
|
||||||
return prefix + String.format("%05d", (max == null ? 0 : max) + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Face value of one unit: ₹100 for securities and bonds, as the existing CBS defaults it. */
|
|
||||||
private BigDecimal defaultFaceValue(final String bondType) {
|
|
||||||
return switch (bondType) {
|
|
||||||
case "MUTUAL_FUND" -> BigDecimal.valueOf(1000);
|
|
||||||
case "SHARE", "FD" -> BigDecimal.ONE;
|
|
||||||
default -> BigDecimal.valueOf(100);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------- shared --------------------
|
|
||||||
|
|
||||||
/** An investment is an asset, so the mapped GL must be an enabled ASSET detail account. */
|
|
||||||
private GLAccount postableAssetGl(final Long glAccountId) {
|
|
||||||
final GLAccount gl = this.glAccountRepository.findOneWithNotFoundDetection(glAccountId);
|
|
||||||
if (gl.isHeaderAccount()) {
|
|
||||||
throw new GeneralPlatformDomainRuleException("error.msg.investment.glaccount.header",
|
|
||||||
"GL " + gl.getGlCode() + " is a header account; choose a detail account", glAccountId);
|
|
||||||
}
|
|
||||||
if (gl.isDisabled()) {
|
|
||||||
throw new GeneralPlatformDomainRuleException("error.msg.investment.glaccount.disabled", "GL " + gl.getGlCode() + " is disabled",
|
|
||||||
glAccountId);
|
|
||||||
}
|
|
||||||
if (!GLAccountType.ASSET.getValue().equals(gl.getType())) {
|
|
||||||
throw new GeneralPlatformDomainRuleException("error.msg.investment.glaccount.not.asset",
|
|
||||||
"GL " + gl.getGlCode() + " is not an Asset account", glAccountId);
|
|
||||||
}
|
|
||||||
return gl;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void setIfPresent(final JsonCommand command, final String param, final Consumer<String> setter) {
|
|
||||||
if (command.parameterExists(param)) {
|
|
||||||
final String value = command.stringValueOfParameterNamedAllowingNull(param);
|
|
||||||
setter.accept(StringUtils.isBlank(value) ? null : value.trim());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-179
@@ -1,179 +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.service;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
|
|
||||||
import org.apache.fineract.accounting.glaccount.domain.GLAccountRepositoryWrapper;
|
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
|
||||||
import org.apache.fineract.portfolio.investment.api.InvestmentInterestApiConstants;
|
|
||||||
import org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants;
|
|
||||||
import org.apache.fineract.portfolio.investment.data.InvestmentInterestDueData;
|
|
||||||
import org.apache.fineract.portfolio.investment.data.InvestmentInterestPostingData;
|
|
||||||
import org.apache.fineract.portfolio.investment.data.InvestmentInterestScheduleData;
|
|
||||||
import org.apache.fineract.portfolio.investment.data.InvestmentPurchaseData;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentInterestPosting;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentInterestPostingRepository;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchase;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchaseRepository;
|
|
||||||
import org.apache.fineract.portfolio.investment.exception.InvestmentPurchaseNotFoundException;
|
|
||||||
import org.springframework.data.domain.Sort;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
public class InvestmentPurchaseReadPlatformService {
|
|
||||||
|
|
||||||
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
|
||||||
|
|
||||||
private final InvestmentPurchaseRepository purchaseRepository;
|
|
||||||
private final InvestmentInterestPostingRepository postingRepository;
|
|
||||||
private final GLAccountRepositoryWrapper glAccountRepository;
|
|
||||||
|
|
||||||
/** All deals, newest first — the Holdings register. */
|
|
||||||
public List<InvestmentPurchaseData> retrieveAll() {
|
|
||||||
return this.purchaseRepository.findAll(Sort.by(Sort.Direction.DESC, "id")).stream().map(InvestmentPurchaseData::from).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public InvestmentPurchaseData retrieveOne(final Long purchaseId) {
|
|
||||||
return InvestmentPurchaseData.from(find(purchaseId));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Coupon and closing-provision lines of a holding from purchase to maturity, worked out on the face value still
|
|
||||||
* held, with what has been posted. Lines on or before {@code asOnDate} (default: business date) are marked due; the
|
|
||||||
* next unposted line is postable once its date has come, and shows as due soon from {@code DUE_WITHIN_DAYS} before.
|
|
||||||
*/
|
|
||||||
public InvestmentInterestScheduleData retrieveInterestSchedule(final Long purchaseId, final LocalDate asOnDate) {
|
|
||||||
final InvestmentPurchase p = find(purchaseId);
|
|
||||||
final LocalDate today = DateUtils.getBusinessLocalDate();
|
|
||||||
final LocalDate asOn = asOnDate == null ? today : asOnDate;
|
|
||||||
final BigDecimal faceHeld = faceHeld(p);
|
|
||||||
final BigDecimal rate = p.getIntRate() == null ? BigDecimal.ZERO : p.getIntRate();
|
|
||||||
final List<InvestmentInterestPosting> posted = this.postingRepository.findByPurchaseIdOrderByIntDateAsc(p.getId());
|
|
||||||
final boolean active = InvestmentPurchaseApiConstants.STATUS_ACTIVE.equals(p.getStatus());
|
|
||||||
final LocalDate reminderUpTo = today.plusDays(InvestmentInterestApiConstants.DUE_WITHIN_DAYS);
|
|
||||||
|
|
||||||
final List<InvestmentInterestScheduleData.Row> rows = new ArrayList<>();
|
|
||||||
boolean nextFound = false;
|
|
||||||
for (final InvestmentInterestCalculator.ScheduleRow r : scheduleRows(p)) {
|
|
||||||
final Optional<InvestmentInterestPosting> done = postingFor(posted, r);
|
|
||||||
final boolean isNext = active && done.isEmpty() && !nextFound;
|
|
||||||
if (isNext) {
|
|
||||||
nextFound = true;
|
|
||||||
}
|
|
||||||
rows.add(InvestmentInterestScheduleData.Row.builder().type(r.type()).intDate(format(r.date())).fromDate(format(r.fromDate()))
|
|
||||||
.toDate(format(r.toDate())).days(r.days())
|
|
||||||
// A posted line shows what was posted, even after a part sale has changed the face value held.
|
|
||||||
.grossInterest(done.map(InvestmentInterestPosting::getGrossInterest).orElse(r.grossInterest()))
|
|
||||||
.alreadyBooked(done.map(InvestmentInterestPosting::getAlreadyBooked).orElse(r.alreadyBooked()))
|
|
||||||
.amount(done.map(InvestmentInterestPosting::getAmount).orElse(r.amount())).due(!r.date().isAfter(asOn))
|
|
||||||
.posted(done.isPresent())
|
|
||||||
.voucherNo(done.map(InvestmentInterestPosting::getVoucherNo).orElse(null))
|
|
||||||
.glTransactionId(done.map(InvestmentInterestPosting::getGlTransactionId).orElse(null))
|
|
||||||
.postedOnDate(done.map(x -> format(x.getPostedOnDate())).orElse(null)).postable(isNext && !r.date().isAfter(today))
|
|
||||||
.dueSoon(active && done.isEmpty() && !r.date().isAfter(reminderUpTo)).build());
|
|
||||||
}
|
|
||||||
|
|
||||||
final GLAccount incomeGl = this.glAccountRepository
|
|
||||||
.findOneByGlCodeWithNotFoundDetection(InvestmentInterestApiConstants.interestIncomeGlCode(p.getBondType()));
|
|
||||||
return InvestmentInterestScheduleData.builder().purchaseId(p.getId()).dealNo(p.getDealNo()).certNo(p.getCertNo())
|
|
||||||
.scriptCode(p.getScript().getScriptCode()).scriptDesc(p.getScript().getScriptDesc()).faceValue(faceHeld)
|
|
||||||
.unitValue(p.getScript().getFaceValuePerUnit()).balanceUnits(p.getBalanceUnits()).purchaseRate(p.getPurchaseRate())
|
|
||||||
.bookValue(p.getBookValue()).intRate(p.getIntRate()).dealDate(format(p.getDealDate()))
|
|
||||||
.lastIntDate(format(p.getLastIntDate())).maturityDate(format(p.getMaturityDate()))
|
|
||||||
.purchasedAccruedInterest(p.getAccruedInterest())
|
|
||||||
.couponPerHalfYear(rate.signum() > 0 ? InvestmentInterestCalculator.halfYearCoupon(faceHeld, rate) : BigDecimal.ZERO)
|
|
||||||
.asOnDate(format(asOn)).interestGlCode(incomeGl.getGlCode()).interestGlName(incomeGl.getName())
|
|
||||||
.bankGlAccountId(p.getCrGlAccount().getId()).bankGlCode(p.getCrGlAccount().getGlCode())
|
|
||||||
.bankGlName(p.getCrGlAccount().getName()).status(p.getStatus()).rows(rows).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The holding's interest schedule (from its settlement date to maturity) on the face value still held. */
|
|
||||||
public List<InvestmentInterestCalculator.ScheduleRow> scheduleRows(final InvestmentPurchase p) {
|
|
||||||
final BigDecimal rate = p.getIntRate() == null ? BigDecimal.ZERO : p.getIntRate();
|
|
||||||
return InvestmentInterestCalculator.schedule(faceHeld(p), rate, p.getMaturityDate(), p.getLastIntDate(), p.getSettlementDate());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tasks: for every active holding, its next unposted interest line when that line's date is past or within
|
|
||||||
* {@code withinDays} days — what has to be posted with a voucher now or shortly.
|
|
||||||
*/
|
|
||||||
public List<InvestmentInterestDueData> retrieveDueInterest(final int withinDays) {
|
|
||||||
final LocalDate today = DateUtils.getBusinessLocalDate();
|
|
||||||
final LocalDate upTo = today.plusDays(Math.max(withinDays, 0));
|
|
||||||
final List<InvestmentInterestDueData> due = new ArrayList<>();
|
|
||||||
for (final InvestmentPurchase p : this.purchaseRepository.findAll(Sort.by(Sort.Direction.ASC, "id"))) {
|
|
||||||
if (!InvestmentPurchaseApiConstants.STATUS_ACTIVE.equals(p.getStatus())) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final List<InvestmentInterestPosting> posted = this.postingRepository.findByPurchaseIdOrderByIntDateAsc(p.getId());
|
|
||||||
scheduleRows(p).stream().filter(r -> postingFor(posted, r).isEmpty()).findFirst().filter(r -> !r.date().isAfter(upTo))
|
|
||||||
.ifPresent(r -> due.add(InvestmentInterestDueData.builder().purchaseId(p.getId()).dealNo(p.getDealNo())
|
|
||||||
.certNo(p.getCertNo()).scriptCode(p.getScript().getScriptCode()).scriptDesc(p.getScript().getScriptDesc())
|
|
||||||
.lineType(r.type()).intDate(format(r.date())).grossInterest(r.grossInterest()).amount(r.amount())
|
|
||||||
.postable(!r.date().isAfter(today)).overdue(r.date().isBefore(today)).build()));
|
|
||||||
}
|
|
||||||
return due;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Interest lines already posted to GL, newest first — they appear in the transaction log beside the purchases. */
|
|
||||||
public List<InvestmentInterestPostingData> retrieveInterestPostings() {
|
|
||||||
final GLAccount receivableGl = this.glAccountRepository
|
|
||||||
.findOneByGlCodeWithNotFoundDetection(InvestmentInterestApiConstants.INTEREST_RECEIVABLE_GL_CODE);
|
|
||||||
return this.postingRepository.findAll(Sort.by(Sort.Direction.DESC, "intDate", "id")).stream().map(x -> {
|
|
||||||
final InvestmentPurchase p = x.getPurchase();
|
|
||||||
final GLAccount incomeGl = this.glAccountRepository
|
|
||||||
.findOneByGlCodeWithNotFoundDetection(InvestmentInterestApiConstants.interestIncomeGlCode(p.getBondType()));
|
|
||||||
final GLAccount drGl = x.getBankGlAccount() != null ? x.getBankGlAccount() : receivableGl;
|
|
||||||
return InvestmentInterestPostingData.builder().id(x.getId()).purchaseId(p.getId()).dealNo(p.getDealNo()).certNo(p.getCertNo())
|
|
||||||
.bondType(p.getBondType()).scriptDesc(p.getScript().getScriptDesc()).lineType(x.getLineType())
|
|
||||||
.intDate(format(x.getIntDate())).fromDate(format(x.getFromDate())).toDate(format(x.getToDate()))
|
|
||||||
.grossInterest(x.getGrossInterest()).alreadyBooked(x.getAlreadyBooked()).amount(x.getAmount())
|
|
||||||
.drGlCode(drGl.getGlCode()).drGlName(drGl.getName()).incomeGlCode(incomeGl.getGlCode())
|
|
||||||
.incomeGlName(incomeGl.getName()).glTransactionId(x.getGlTransactionId()).voucherNo(x.getVoucherNo())
|
|
||||||
.postedOnDate(format(x.getPostedOnDate())).build();
|
|
||||||
}).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Optional<InvestmentInterestPosting> postingFor(final List<InvestmentInterestPosting> posted,
|
|
||||||
final InvestmentInterestCalculator.ScheduleRow r) {
|
|
||||||
return posted.stream().filter(x -> x.getLineType().equals(r.type()) && x.getIntDate().equals(r.date())).findFirst();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static BigDecimal faceHeld(final InvestmentPurchase p) {
|
|
||||||
return p.getBalanceUnits().multiply(p.getScript().getFaceValuePerUnit());
|
|
||||||
}
|
|
||||||
|
|
||||||
private InvestmentPurchase find(final Long purchaseId) {
|
|
||||||
return this.purchaseRepository.findById(purchaseId).orElseThrow(() -> new InvestmentPurchaseNotFoundException(purchaseId));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String format(final LocalDate date) {
|
|
||||||
return date == null ? null : date.format(DATE_FORMAT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-31
@@ -1,31 +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.service;
|
|
||||||
|
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
|
||||||
|
|
||||||
public interface InvestmentPurchaseWritePlatformService {
|
|
||||||
|
|
||||||
CommandProcessingResult createPurchase(JsonCommand command);
|
|
||||||
|
|
||||||
CommandProcessingResult deletePurchase(Long purchaseId);
|
|
||||||
|
|
||||||
CommandProcessingResult settlePurchase(Long purchaseId, JsonCommand command);
|
|
||||||
}
|
|
||||||
-330
@@ -1,330 +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.service;
|
|
||||||
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.BOND_TYPE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.BROKEN_PERIOD_INTEREST_GL_CODE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.BROKERAGE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.BROKERAGE_GL_CODE;
|
|
||||||
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_CHARGES_GL_CODE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.OTHER_DETAILS;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.PARTY_ID;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.PURCHASE_RATE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SCRIPT_ID;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SETTLEMENT_TYPE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.STAMP_DUTY;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.STAMP_DUTY_GL_CODE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.STATUS_ACTIVE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.STATUS_PENDING_SETTLEMENT;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.SUB_TYPE;
|
|
||||||
|
|
||||||
import com.google.gson.JsonElement;
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.math.RoundingMode;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.time.temporal.ChronoUnit;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
|
|
||||||
import org.apache.fineract.accounting.glaccount.domain.GLAccountRepositoryWrapper;
|
|
||||||
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
|
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
|
||||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentParty;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPartyRepository;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchase;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchaseRepository;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentScript;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentScriptRepository;
|
|
||||||
import org.apache.fineract.portfolio.investment.exception.InvestmentPartyNotFoundException;
|
|
||||||
import org.apache.fineract.portfolio.investment.exception.InvestmentPurchaseNotFoundException;
|
|
||||||
import org.apache.fineract.portfolio.investment.exception.InvestmentScriptNotFoundException;
|
|
||||||
import org.apache.fineract.portfolio.investment.serialization.InvestmentPurchaseCommandFromApiJsonDeserializer;
|
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves a Purchase Entry as a holding. Every derived figure is computed here from the script master and the deal
|
|
||||||
* inputs, so the stored deal does not depend on what the screen calculated. No GL entry is posted yet.
|
|
||||||
*/
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class InvestmentPurchaseWritePlatformServiceImpl implements InvestmentPurchaseWritePlatformService {
|
|
||||||
|
|
||||||
private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);
|
|
||||||
|
|
||||||
private final InvestmentPurchaseCommandFromApiJsonDeserializer validator;
|
|
||||||
private final InvestmentPurchaseRepository purchaseRepository;
|
|
||||||
private final InvestmentScriptRepository scriptRepository;
|
|
||||||
private final InvestmentPartyRepository partyRepository;
|
|
||||||
private final GLAccountRepositoryWrapper glAccountRepository;
|
|
||||||
private final JdbcTemplate jdbcTemplate;
|
|
||||||
private final InvestmentJournalPoster journalPoster;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public CommandProcessingResult createPurchase(final JsonCommand command) {
|
|
||||||
this.validator.validateForCreate(command.json());
|
|
||||||
|
|
||||||
final String bondType = command.stringValueOfParameterNamed(BOND_TYPE);
|
|
||||||
final Long scriptId = command.longValueOfParameterNamed(SCRIPT_ID);
|
|
||||||
final InvestmentScript script = this.scriptRepository.findById(scriptId)
|
|
||||||
.orElseThrow(() -> new InvestmentScriptNotFoundException(scriptId));
|
|
||||||
final Long partyId = command.longValueOfParameterNamed(PARTY_ID);
|
|
||||||
final InvestmentParty party = this.partyRepository.findById(partyId)
|
|
||||||
.orElseThrow(() -> new InvestmentPartyNotFoundException(partyId));
|
|
||||||
if (!script.isActive()) {
|
|
||||||
throw rule("script.inactive", "Script " + script.getScriptCode() + " is inactive");
|
|
||||||
}
|
|
||||||
if (!script.getBondType().equals(bondType)) {
|
|
||||||
throw rule("script.bondtype.mismatch",
|
|
||||||
"Script " + script.getScriptCode() + " is a " + script.getBondType() + ", not " + bondType);
|
|
||||||
}
|
|
||||||
if (org.apache.fineract.portfolio.investment.api.InvestmentApiConstants.DEBT_BOND_TYPES.contains(bondType)
|
|
||||||
&& script.getMaturityDate() == null) {
|
|
||||||
throw rule("script.no.maturity",
|
|
||||||
"Script " + script.getScriptCode() + " has no maturity date; enter it in Script Master before buying");
|
|
||||||
}
|
|
||||||
if (!party.isActive()) {
|
|
||||||
throw rule("party.inactive", "Party " + party.getPartyCode() + " is inactive");
|
|
||||||
}
|
|
||||||
|
|
||||||
final LocalDate dealDate = command.localDateValueOfParameterNamed(DEAL_DATE);
|
|
||||||
if (script.getMaturityDate() != null && dealDate.isAfter(script.getMaturityDate())) {
|
|
||||||
throw rule("deal.after.maturity", "Deal date is after the security's maturity date " + script.getMaturityDate());
|
|
||||||
}
|
|
||||||
final String settlementType = command.stringValueOfParameterNamed(SETTLEMENT_TYPE);
|
|
||||||
|
|
||||||
final InvestmentPurchase p = new InvestmentPurchase();
|
|
||||||
p.setDealNo(nextDealNo(dealDate));
|
|
||||||
p.setCertNo(nextCertNo());
|
|
||||||
p.setBondType(bondType);
|
|
||||||
p.setScript(script);
|
|
||||||
p.setParty(party);
|
|
||||||
p.setDealDate(dealDate);
|
|
||||||
p.setDealTime(StringUtils.trimToNull(command.stringValueOfParameterNamedAllowingNull(DEAL_TIME)));
|
|
||||||
p.setSettlementType(settlementType);
|
|
||||||
p.setSettlementDate(dealDate.plusDays(Long.parseLong(settlementType.substring(2))));
|
|
||||||
p.setCategory(command.stringValueOfParameterNamed(CATEGORY));
|
|
||||||
p.setSubType(StringUtils.trimToNull(command.stringValueOfParameterNamedAllowingNull(SUB_TYPE)));
|
|
||||||
p.setIntRate(script.getIntRate());
|
|
||||||
p.setIssueDate(script.getIssueDate());
|
|
||||||
p.setMaturityDate(script.getMaturityDate());
|
|
||||||
p.setInvestmentGlAccount(script.getGlAccount());
|
|
||||||
p.setCrGlAccount(postableAssetGl(command.longValueOfParameterNamed(CR_GL_ACCOUNT_ID)));
|
|
||||||
p.setOtherDetails(otherDetails(command));
|
|
||||||
p.setStatus(STATUS_PENDING_SETTLEMENT);
|
|
||||||
|
|
||||||
// Brokerage, stamp duty and other charges are expenses of the deal (Jitendra Sir): paid with the
|
|
||||||
// settlement, but kept out of the book value so the charges stay visible on their own GLs.
|
|
||||||
p.setBrokerage(zeroIfNull(command.bigDecimalValueOfParameterNamed(BROKERAGE)));
|
|
||||||
p.setStampDuty(zeroIfNull(command.bigDecimalValueOfParameterNamed(STAMP_DUTY)));
|
|
||||||
p.setOtherCharges(zeroIfNull(command.bigDecimalValueOfParameterNamed(OTHER_CHARGES)));
|
|
||||||
if ("MUTUAL_FUND".equals(bondType)) {
|
|
||||||
computeMutualFund(p, command);
|
|
||||||
} else {
|
|
||||||
computeSecurity(p, command, script);
|
|
||||||
}
|
|
||||||
p.setSettlementAmount(p.getBookValue().add(p.getAccruedInterest()).add(totalCharges(p)));
|
|
||||||
|
|
||||||
final InvestmentPurchase saved = this.purchaseRepository.saveAndFlush(p);
|
|
||||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(saved.getId())
|
|
||||||
.withResourceIdAsString(saved.getDealNo()).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public CommandProcessingResult deletePurchase(final Long purchaseId) {
|
|
||||||
final InvestmentPurchase p = this.purchaseRepository.findByIdWithLock(purchaseId)
|
|
||||||
.orElseThrow(() -> new InvestmentPurchaseNotFoundException(purchaseId));
|
|
||||||
if (!STATUS_PENDING_SETTLEMENT.equals(p.getStatus())) {
|
|
||||||
throw rule("purchase.settled", "Deal " + p.getDealNo() + " is already settled and posted to GL, so it cannot be dropped");
|
|
||||||
}
|
|
||||||
if (p.getBalanceUnits().compareTo(p.getUnits()) != 0) {
|
|
||||||
throw rule("purchase.partly.sold", "Deal " + p.getDealNo() + " has been partly sold and cannot be dropped");
|
|
||||||
}
|
|
||||||
this.purchaseRepository.delete(p);
|
|
||||||
this.purchaseRepository.flush();
|
|
||||||
return new CommandProcessingResultBuilder().withEntityId(purchaseId).withResourceIdAsString(p.getDealNo()).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Settles a deal: on or after its settlement date the money has gone and the security has come in, so the purchase
|
|
||||||
* is posted to GL dated the settlement date (settlement-date accounting) and the deal becomes a holding. Posted
|
|
||||||
* through the journal entry command like any manual entry, inside this transaction, so under maker-checker nothing
|
|
||||||
* reaches the GL until the settlement is approved.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public CommandProcessingResult settlePurchase(final Long purchaseId, final JsonCommand command) {
|
|
||||||
final InvestmentPurchase p = this.purchaseRepository.findByIdWithLock(purchaseId)
|
|
||||||
.orElseThrow(() -> new InvestmentPurchaseNotFoundException(purchaseId));
|
|
||||||
if (!STATUS_PENDING_SETTLEMENT.equals(p.getStatus())) {
|
|
||||||
throw rule("settle.not.pending", "Deal " + p.getDealNo() + " is not pending settlement");
|
|
||||||
}
|
|
||||||
final LocalDate today = DateUtils.getBusinessLocalDate();
|
|
||||||
if (p.getSettlementDate().isAfter(today)) {
|
|
||||||
throw rule("settle.before.date", "Deal " + p.getDealNo() + " can be settled on or after its settlement date "
|
|
||||||
+ p.getSettlementDate().format(DateTimeFormatter.ofPattern("dd-MM-yyyy")));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Premium over face value goes to its own GL (100707) and is amortised to maturity; the investment GL takes the rest.
|
|
||||||
final BigDecimal premium = p.getPremiumDiscount() != null && p.getPremiumDiscount().signum() > 0 ? p.getPremiumDiscount()
|
|
||||||
: BigDecimal.ZERO;
|
|
||||||
if (premium.signum() > 0) {
|
|
||||||
p.setPremiumGlAccount(this.glAccountRepository.findOneByGlCodeWithNotFoundDetection(
|
|
||||||
org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.PREMIUM_ON_INVESTMENTS_GL_CODE));
|
|
||||||
}
|
|
||||||
final InvestmentJournalPoster.Lines lines = new InvestmentJournalPoster.Lines()
|
|
||||||
.debit(p.getInvestmentGlAccount().getId(), p.getBookValue().subtract(premium))
|
|
||||||
.debit(premium.signum() > 0 ? p.getPremiumGlAccount().getId() : null, premium)
|
|
||||||
.debit(this.journalPoster.glId(BROKEN_PERIOD_INTEREST_GL_CODE), p.getAccruedInterest())
|
|
||||||
.debit(this.journalPoster.glId(STAMP_DUTY_GL_CODE), p.getStampDuty())
|
|
||||||
.debit(this.journalPoster.glId(BROKERAGE_GL_CODE), p.getBrokerage())
|
|
||||||
.debit(this.journalPoster.glId(OTHER_CHARGES_GL_CODE), p.getOtherCharges())
|
|
||||||
.credit(p.getCrGlAccount().getId(), p.getSettlementAmount());
|
|
||||||
final CommandProcessingResult posted = this.journalPoster.post(p.getSettlementDate(), p.getDealNo(),
|
|
||||||
"Investment purchase settled: deal " + p.getDealNo() + ", " + p.getScript().getScriptCode() + " "
|
|
||||||
+ p.getScript().getScriptDesc() + ", " + p.getParty().getPartyName(),
|
|
||||||
lines);
|
|
||||||
|
|
||||||
p.setGlTransactionId(posted.getTransactionId());
|
|
||||||
p.setSettledOnDate(today);
|
|
||||||
p.setStatus(STATUS_ACTIVE);
|
|
||||||
this.purchaseRepository.saveAndFlush(p);
|
|
||||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(purchaseId)
|
|
||||||
.withResourceIdAsString(p.getDealNo()).withTransactionId(posted.getTransactionId()).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Govt Security / Bond: face value at a price per ₹100, plus interest owed to the seller since the last coupon. */
|
|
||||||
private void computeSecurity(final InvestmentPurchase p, final JsonCommand command, final InvestmentScript script) {
|
|
||||||
final BigDecimal faceValue = command.bigDecimalValueOfParameterNamed(FACE_VALUE);
|
|
||||||
final BigDecimal rate = command.bigDecimalValueOfParameterNamed(PURCHASE_RATE);
|
|
||||||
final LocalDate lastIntDate = command.localDateValueOfParameterNamed(LAST_INT_DATE);
|
|
||||||
if (lastIntDate.isAfter(p.getDealDate())) {
|
|
||||||
throw rule("lastint.after.deal", "Last interest date cannot be after the deal date");
|
|
||||||
}
|
|
||||||
final BigDecimal units = faceValue.divide(script.getFaceValuePerUnit(), 6, RoundingMode.HALF_UP);
|
|
||||||
final BigDecimal cost = faceValue.multiply(rate).divide(HUNDRED, 2, RoundingMode.HALF_UP);
|
|
||||||
// Broken-period interest runs to the settlement date, when the bond leaves the seller (RBI 83, Jitendra Sir).
|
|
||||||
final int days = InvestmentInterestCalculator.days360(lastIntDate, p.getSettlementDate());
|
|
||||||
final BigDecimal intRate = zeroIfNull(script.getIntRate());
|
|
||||||
final BigDecimal accrued = InvestmentInterestCalculator.interestForDays(faceValue, intRate, days);
|
|
||||||
|
|
||||||
p.setFaceValue(faceValue);
|
|
||||||
p.setPurchaseRate(rate);
|
|
||||||
p.setUnits(units);
|
|
||||||
p.setBalanceUnits(units);
|
|
||||||
p.setBookValue(cost);
|
|
||||||
p.setPremiumDiscount(faceValue.multiply(rate.subtract(HUNDRED)).divide(HUNDRED, 2, RoundingMode.HALF_UP));
|
|
||||||
p.setLastIntDate(lastIntDate);
|
|
||||||
p.setAccruedDays(days);
|
|
||||||
p.setAccruedInterest(accrued);
|
|
||||||
p.setYtm(approxYtm(intRate, rate, script.getIssueDate(), script.getMaturityDate()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Mutual fund: the amount invested buys units at the NAV. */
|
|
||||||
private void computeMutualFund(final InvestmentPurchase p, final JsonCommand command) {
|
|
||||||
final BigDecimal amount = command.bigDecimalValueOfParameterNamed(CONSIDER_AMT);
|
|
||||||
final BigDecimal nav = command.bigDecimalValueOfParameterNamed(NAV);
|
|
||||||
final BigDecimal units = amount.divide(nav, 3, RoundingMode.HALF_UP);
|
|
||||||
|
|
||||||
p.setFaceValue(amount);
|
|
||||||
p.setPurchaseRate(nav);
|
|
||||||
p.setUnits(units);
|
|
||||||
p.setBalanceUnits(units);
|
|
||||||
p.setBookValue(amount);
|
|
||||||
p.setAccruedInterest(BigDecimal.ZERO);
|
|
||||||
p.setAccruedDays(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static BigDecimal totalCharges(final InvestmentPurchase p) {
|
|
||||||
return p.getBrokerage().add(p.getStampDuty()).add(p.getOtherCharges());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Approximate yield: coupon adjusted for the discount/premium spread over the security's life, over the price. */
|
|
||||||
static BigDecimal approxYtm(final BigDecimal coupon, final BigDecimal price, final LocalDate issue, final LocalDate maturity) {
|
|
||||||
if (coupon.signum() == 0 || issue == null || maturity == null || !maturity.isAfter(issue)) {
|
|
||||||
return coupon;
|
|
||||||
}
|
|
||||||
final double years = ChronoUnit.DAYS.between(issue, maturity) / 365.25;
|
|
||||||
final double ytm = (coupon.doubleValue() + (100 - price.doubleValue()) / years) / price.doubleValue() * 100;
|
|
||||||
return BigDecimal.valueOf(ytm).setScale(2, RoundingMode.HALF_UP);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Financial-year deal number: FY start year + FY end (2 digits) + 6-digit running number, e.g. 202627000001. */
|
|
||||||
private String nextDealNo(final LocalDate dealDate) {
|
|
||||||
final String prefix = dealNoPrefix(dealDate);
|
|
||||||
final Integer max = this.jdbcTemplate.queryForObject(
|
|
||||||
"SELECT MAX(CAST(SUBSTRING(deal_no FROM 7) AS INTEGER)) FROM m_investment_purchase WHERE deal_no LIKE ?", Integer.class,
|
|
||||||
prefix + "%");
|
|
||||||
return prefix + String.format("%06d", (max == null ? 0 : max) + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Financial year (April–March) of the deal date: 2026-04-01 … 2027-03-31 → "202627". */
|
|
||||||
static String dealNoPrefix(final LocalDate dealDate) {
|
|
||||||
final int fyStart = dealDate.getMonthValue() >= 4 ? dealDate.getYear() : dealDate.getYear() - 1;
|
|
||||||
return fyStart + String.format("%02d", (fyStart + 1) % 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String nextCertNo() {
|
|
||||||
final Integer max = this.jdbcTemplate.queryForObject(
|
|
||||||
"SELECT MAX(CAST(cert_no AS INTEGER)) FROM m_investment_purchase WHERE cert_no ~ '^[0-9]+$'", Integer.class);
|
|
||||||
return String.valueOf((max == null ? 0 : max) + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String otherDetails(final JsonCommand command) {
|
|
||||||
if (!command.parameterExists(OTHER_DETAILS)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final JsonElement other = command.parsedJson().getAsJsonObject().get(OTHER_DETAILS);
|
|
||||||
return other == null || other.isJsonNull() ? null : other.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The paying bank account must be an enabled ASSET detail GL. */
|
|
||||||
private GLAccount postableAssetGl(final Long glAccountId) {
|
|
||||||
final GLAccount gl = this.glAccountRepository.findOneWithNotFoundDetection(glAccountId);
|
|
||||||
if (gl.isHeaderAccount() || gl.isDisabled() || !GLAccountType.ASSET.getValue().equals(gl.getType())) {
|
|
||||||
throw rule("cr.glaccount.invalid", "CR. To A/c " + gl.getGlCode() + " must be an enabled Asset detail account");
|
|
||||||
}
|
|
||||||
return gl;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static BigDecimal zeroIfNull(final BigDecimal value) {
|
|
||||||
return value == null ? BigDecimal.ZERO : value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static GeneralPlatformDomainRuleException rule(final String code, final String message) {
|
|
||||||
return new GeneralPlatformDomainRuleException("error.msg.investmentpurchase." + code, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-294
@@ -1,294 +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.service;
|
|
||||||
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.STATUS_ACTIVE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.STATUS_PENDING_SETTLEMENT;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.STATUS_SETTLED;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.TYPE_REDEEM;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.TYPE_SELL;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.math.RoundingMode;
|
|
||||||
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.accounting.glaccount.domain.GLAccount;
|
|
||||||
import org.apache.fineract.accounting.glaccount.domain.GLAccountRepositoryWrapper;
|
|
||||||
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
|
|
||||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
|
||||||
import org.apache.fineract.portfolio.investment.api.InvestmentApiConstants;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentInterestPosting;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentInterestPostingRepository;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentParty;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchase;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentSale;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentSaleRepository;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Works out a sale or a redemption of a holding — the same figures for the screen's preview, for saving and for
|
|
||||||
* posting, so the voucher always matches what was shown.
|
|
||||||
* <ul>
|
|
||||||
* <li>Sale: price = face value sold × rate / 100; cost = the units' share of the book value; profit or loss = price −
|
|
||||||
* cost. The voucher carries the principal only (Jitendra Sir): the interest since the last coupon that the buyer pays is
|
|
||||||
* worked out as a hint — the part a closing provision already booked as receivable and the rest as income — for a manual
|
|
||||||
* entry. No short sale: only units still held and not in another pending sale.</li>
|
|
||||||
* <li>Redemption: the face value comes back at maturity against the book value still held; a discount left is profit,
|
|
||||||
* a premium left is written off.</li>
|
|
||||||
* </ul>
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class InvestmentSaleQuoter {
|
|
||||||
|
|
||||||
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
|
||||||
private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);
|
|
||||||
private static final String HTM_PREFIX = "HTM";
|
|
||||||
|
|
||||||
private final InvestmentSaleRepository saleRepository;
|
|
||||||
private final InvestmentInterestPostingRepository postingRepository;
|
|
||||||
private final InvestmentPurchaseReadPlatformService purchaseReadPlatformService;
|
|
||||||
private final GLAccountRepositoryWrapper glAccountRepository;
|
|
||||||
|
|
||||||
/** Inputs of a sale entry. */
|
|
||||||
public record SellRequest(InvestmentParty party, LocalDate dealDate, String dealTime, String settlementType, BigDecimal units,
|
|
||||||
BigDecimal saleRate, BigDecimal brokerage, Long bankGlAccountId, String sanctionAuthority, String remarks) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A pending sale with its figures worked out; {@code ignoreSaleId} leaves the sale being settled out of the units
|
|
||||||
* check. A {@code preview} skips the checks on what the user has not typed yet (buyer, HTM sanction).
|
|
||||||
*/
|
|
||||||
public InvestmentSale quoteSale(final InvestmentPurchase p, final SellRequest r, final Long ignoreSaleId, final boolean preview) {
|
|
||||||
requireActiveDebtHolding(p);
|
|
||||||
if (r.party() != null && !r.party().isActive()) {
|
|
||||||
throw rule("party.inactive", "Party " + r.party().getPartyCode() + " is inactive");
|
|
||||||
}
|
|
||||||
if (r.dealDate().isBefore(p.getSettlementDate())) {
|
|
||||||
throw rule("deal.before.purchase", "A sale cannot be dated before the purchase settled on " + p.getSettlementDate().format(DATE));
|
|
||||||
}
|
|
||||||
if (r.dealDate().isAfter(DateUtils.getBusinessLocalDate())) {
|
|
||||||
throw rule("deal.future", "The sale deal date cannot be after today");
|
|
||||||
}
|
|
||||||
final LocalDate settlementDate = r.dealDate().plusDays(Long.parseLong(r.settlementType().substring(2)));
|
|
||||||
if (!settlementDate.isBefore(p.getMaturityDate())) {
|
|
||||||
throw rule("sell.at.maturity", "The security matures on " + p.getMaturityDate().format(DATE)
|
|
||||||
+ "; on or after maturity it is redeemed, not sold");
|
|
||||||
}
|
|
||||||
if (!preview && p.getCategory() != null && p.getCategory().startsWith(HTM_PREFIX)
|
|
||||||
&& (StringUtils.isBlank(r.sanctionAuthority()) || StringUtils.isBlank(r.remarks()))) {
|
|
||||||
throw rule("htm.sale.sanction", "An HTM security may be sold only with the Board's permission: enter the sanction "
|
|
||||||
+ "authority and the reason for the sale in Remarks");
|
|
||||||
}
|
|
||||||
final BigDecimal available = availableUnits(p, ignoreSaleId);
|
|
||||||
if (r.units().compareTo(available) > 0) {
|
|
||||||
throw rule("units.exceed", "Only " + available.stripTrailingZeros().toPlainString() + " units of deal " + p.getDealNo()
|
|
||||||
+ " are available to sell");
|
|
||||||
}
|
|
||||||
|
|
||||||
final BigDecimal unitValue = p.getScript().getFaceValuePerUnit();
|
|
||||||
final BigDecimal face = r.units().multiply(unitValue).setScale(2, RoundingMode.HALF_UP);
|
|
||||||
final BigDecimal consideration = face.multiply(r.saleRate()).divide(HUNDRED, 2, RoundingMode.HALF_UP);
|
|
||||||
final LocalDate lastCoupon = lastCoupon(p, settlementDate);
|
|
||||||
final int days = InvestmentInterestCalculator.days360(lastCoupon, settlementDate);
|
|
||||||
final BigDecimal rate = p.getIntRate() == null ? BigDecimal.ZERO : p.getIntRate();
|
|
||||||
final BigDecimal accrued = InvestmentInterestCalculator.interestForDays(face, rate, days);
|
|
||||||
final BigDecimal receivable = receivableShare(p, lastCoupon, settlementDate, r.units()).min(accrued);
|
|
||||||
final boolean allRemaining = r.units().compareTo(p.getBalanceUnits()) == 0;
|
|
||||||
// Cost is the units' share of the book value as it stands now (after earlier sales and write-offs).
|
|
||||||
final BigDecimal book = remainingBook(p);
|
|
||||||
final BigDecimal bookSold = allRemaining ? book : book.multiply(r.units()).divide(p.getBalanceUnits(), 2, RoundingMode.HALF_UP);
|
|
||||||
final BigDecimal brokerage = r.brokerage() == null ? BigDecimal.ZERO : r.brokerage();
|
|
||||||
|
|
||||||
final InvestmentSale s = new InvestmentSale();
|
|
||||||
s.setPurchase(p);
|
|
||||||
s.setSaleType(TYPE_SELL);
|
|
||||||
s.setParty(r.party());
|
|
||||||
s.setDealDate(r.dealDate());
|
|
||||||
s.setDealTime(r.dealTime());
|
|
||||||
s.setSettlementType(r.settlementType());
|
|
||||||
s.setSettlementDate(settlementDate);
|
|
||||||
s.setUnits(r.units());
|
|
||||||
s.setFaceValue(face);
|
|
||||||
s.setSaleRate(r.saleRate());
|
|
||||||
s.setConsideration(consideration);
|
|
||||||
s.setAccruedDays(days);
|
|
||||||
s.setAccruedInterest(accrued);
|
|
||||||
s.setReceivableCleared(receivable);
|
|
||||||
s.setBrokerage(brokerage);
|
|
||||||
s.setBookValueSold(bookSold);
|
|
||||||
final BigDecimal premium = unamortisedPremium(p);
|
|
||||||
s.setPremiumCleared(allRemaining ? premium : premium.multiply(r.units()).divide(p.getBalanceUnits(), 2, RoundingMode.HALF_UP));
|
|
||||||
s.setProfitLoss(consideration.subtract(bookSold));
|
|
||||||
// Jitendra Sir: the sale voucher carries the principal only; the interest is entered by hand (hint above).
|
|
||||||
s.setNetAmount(consideration.subtract(brokerage));
|
|
||||||
s.setBankGlAccount(r.bankGlAccountId() == null ? p.getCrGlAccount() : postableAssetGl(r.bankGlAccountId()));
|
|
||||||
s.setSanctionAuthority(StringUtils.trimToNull(r.sanctionAuthority()));
|
|
||||||
s.setRemarks(StringUtils.trimToNull(r.remarks()));
|
|
||||||
s.setStatus(STATUS_PENDING_SETTLEMENT);
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Repayment at maturity of all units still held, dated the maturity date. */
|
|
||||||
public InvestmentSale quoteRedemption(final InvestmentPurchase p, final Long bankGlAccountId, final String remarks) {
|
|
||||||
requireActiveDebtHolding(p);
|
|
||||||
if (p.getMaturityDate().isAfter(DateUtils.getBusinessLocalDate())) {
|
|
||||||
throw rule("redeem.before.maturity", "Deal " + p.getDealNo() + " matures on " + p.getMaturityDate().format(DATE)
|
|
||||||
+ "; it can be redeemed on or after that date");
|
|
||||||
}
|
|
||||||
if (availableUnits(p, null).compareTo(p.getBalanceUnits()) != 0) {
|
|
||||||
throw rule("redeem.pending.sale", "Deal " + p.getDealNo() + " has a pending sale; settle or drop it before redemption");
|
|
||||||
}
|
|
||||||
final BigDecimal face = p.getBalanceUnits().multiply(p.getScript().getFaceValuePerUnit()).setScale(2, RoundingMode.HALF_UP);
|
|
||||||
final BigDecimal book = remainingBook(p);
|
|
||||||
|
|
||||||
final InvestmentSale s = new InvestmentSale();
|
|
||||||
s.setPurchase(p);
|
|
||||||
s.setSaleType(TYPE_REDEEM);
|
|
||||||
s.setDealDate(p.getMaturityDate());
|
|
||||||
s.setSettlementDate(p.getMaturityDate());
|
|
||||||
s.setUnits(p.getBalanceUnits());
|
|
||||||
s.setFaceValue(face);
|
|
||||||
s.setSaleRate(HUNDRED);
|
|
||||||
s.setConsideration(face);
|
|
||||||
s.setAccruedDays(0);
|
|
||||||
s.setAccruedInterest(BigDecimal.ZERO);
|
|
||||||
s.setReceivableCleared(BigDecimal.ZERO);
|
|
||||||
s.setBrokerage(BigDecimal.ZERO);
|
|
||||||
s.setBookValueSold(book);
|
|
||||||
s.setPremiumCleared(unamortisedPremium(p));
|
|
||||||
s.setProfitLoss(face.subtract(book));
|
|
||||||
s.setNetAmount(face);
|
|
||||||
s.setBankGlAccount(bankGlAccountId == null ? p.getCrGlAccount() : postableAssetGl(bankGlAccountId));
|
|
||||||
s.setRemarks(StringUtils.trimToNull(remarks));
|
|
||||||
s.setStatus(STATUS_SETTLED);
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Every interest line up to {@code upTo} must be posted first, so the receivable cleared on a sale and the last
|
|
||||||
* coupon on a redemption are already in the books.
|
|
||||||
*/
|
|
||||||
public void requireInterestPostedUpTo(final InvestmentPurchase p, final LocalDate upTo) {
|
|
||||||
final List<InvestmentInterestPosting> posted = this.postingRepository.findByPurchaseIdOrderByIntDateAsc(p.getId());
|
|
||||||
this.purchaseReadPlatformService.scheduleRows(p).stream().filter(r -> !r.date().isAfter(upTo))
|
|
||||||
.filter(r -> posted.stream().noneMatch(x -> x.getLineType().equals(r.type()) && x.getIntDate().equals(r.date())))
|
|
||||||
.findFirst().ifPresent(r -> {
|
|
||||||
throw rule("interest.pending", "Post the " + r.type().toLowerCase() + " of " + r.date().format(DATE) + " for deal "
|
|
||||||
+ p.getDealNo() + " first (Interest Entry)");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Units still held less those in other pending sales. */
|
|
||||||
public BigDecimal availableUnits(final InvestmentPurchase p, final Long ignoreSaleId) {
|
|
||||||
BigDecimal available = p.getBalanceUnits();
|
|
||||||
for (final InvestmentSale s : this.saleRepository.findByPurchaseIdOrderByIdAsc(p.getId())) {
|
|
||||||
if (STATUS_PENDING_SETTLEMENT.equals(s.getStatus()) && !s.getId().equals(ignoreSaleId)) {
|
|
||||||
available = available.subtract(s.getUnits());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return available;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Premium paid over face value not yet taken off by amortisation, sales or the redemption. */
|
|
||||||
public BigDecimal unamortisedPremium(final InvestmentPurchase p) {
|
|
||||||
if (p.getPremiumDiscount() == null || p.getPremiumDiscount().signum() <= 0) {
|
|
||||||
return BigDecimal.ZERO;
|
|
||||||
}
|
|
||||||
BigDecimal premium = p.getPremiumDiscount();
|
|
||||||
for (final InvestmentSale s : this.saleRepository.findByPurchaseIdOrderByIdAsc(p.getId())) {
|
|
||||||
if (STATUS_SETTLED.equals(s.getStatus()) && s.getPremiumCleared() != null) {
|
|
||||||
premium = premium.subtract(s.getPremiumCleared());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return premium.max(BigDecimal.ZERO);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** GL the unamortised premium sits in: 100707 for deals settled since it was introduced, else the investment GL. */
|
|
||||||
public static Long premiumGlId(final InvestmentPurchase p) {
|
|
||||||
return p.getPremiumGlAccount() != null ? p.getPremiumGlAccount().getId() : p.getInvestmentGlAccount().getId();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Book value not yet taken off by settled sales. */
|
|
||||||
public BigDecimal remainingBook(final InvestmentPurchase p) {
|
|
||||||
BigDecimal book = p.getBookValue();
|
|
||||||
for (final InvestmentSale s : this.saleRepository.findByPurchaseIdOrderByIdAsc(p.getId())) {
|
|
||||||
if (STATUS_SETTLED.equals(s.getStatus())) {
|
|
||||||
book = book.subtract(s.getBookValueSold());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return book;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The coupon date the buyer's interest runs from: the last coupon on or before settlement, never before our purchase. */
|
|
||||||
private static LocalDate lastCoupon(final InvestmentPurchase p, final LocalDate settlementDate) {
|
|
||||||
final LocalDate coupon = InvestmentInterestCalculator.lastCouponOnOrBefore(p.getMaturityDate(), settlementDate);
|
|
||||||
return p.getLastIntDate() != null && p.getLastIntDate().isAfter(coupon) ? p.getLastIntDate() : coupon;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Share of the interest receivable still on the books for this coupon period (provisions posted after the last
|
|
||||||
* coupon, less what earlier sales cleared) that belongs to the units sold.
|
|
||||||
*/
|
|
||||||
private BigDecimal receivableShare(final InvestmentPurchase p, final LocalDate lastCoupon, final LocalDate settlementDate,
|
|
||||||
final BigDecimal units) {
|
|
||||||
BigDecimal outstanding = BigDecimal.ZERO;
|
|
||||||
for (final InvestmentInterestPosting x : this.postingRepository.findByPurchaseIdOrderByIntDateAsc(p.getId())) {
|
|
||||||
if (InvestmentInterestCalculator.TYPE_PROVISION.equals(x.getLineType()) && x.getIntDate().isAfter(lastCoupon)
|
|
||||||
&& !x.getIntDate().isAfter(settlementDate)) {
|
|
||||||
outstanding = outstanding.add(x.getAmount());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (final InvestmentSale s : this.saleRepository.findByPurchaseIdOrderByIdAsc(p.getId())) {
|
|
||||||
if (STATUS_SETTLED.equals(s.getStatus()) && s.getSettlementDate().isAfter(lastCoupon) && s.getReceivableCleared() != null) {
|
|
||||||
outstanding = outstanding.subtract(s.getReceivableCleared());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (outstanding.signum() <= 0) {
|
|
||||||
return BigDecimal.ZERO;
|
|
||||||
}
|
|
||||||
return outstanding.multiply(units).divide(p.getBalanceUnits(), 2, RoundingMode.HALF_UP);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void requireActiveDebtHolding(final InvestmentPurchase p) {
|
|
||||||
if (!STATUS_ACTIVE.equals(p.getStatus())) {
|
|
||||||
throw rule("not.active", "Deal " + p.getDealNo() + " is not an active (settled) holding");
|
|
||||||
}
|
|
||||||
if (!InvestmentApiConstants.DEBT_BOND_TYPES.contains(p.getBondType())) {
|
|
||||||
throw rule("type.unsupported", "Sale and redemption are available for Govt Security, Non-SLR and Bond holdings");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The bank the money comes into must be an enabled ASSET detail GL. */
|
|
||||||
private GLAccount postableAssetGl(final Long glAccountId) {
|
|
||||||
final GLAccount gl = this.glAccountRepository.findOneWithNotFoundDetection(glAccountId);
|
|
||||||
if (gl.isHeaderAccount() || gl.isDisabled() || !GLAccountType.ASSET.getValue().equals(gl.getType())) {
|
|
||||||
throw rule("bank.glaccount.invalid", "Bank GL " + gl.getGlCode() + " must be an enabled Asset detail account");
|
|
||||||
}
|
|
||||||
return gl;
|
|
||||||
}
|
|
||||||
|
|
||||||
static GeneralPlatformDomainRuleException rule(final String code, final String message) {
|
|
||||||
return new GeneralPlatformDomainRuleException("error.msg.investmentsale." + code, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-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.service;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.List;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.apache.fineract.portfolio.investment.data.InvestmentSaleData;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchase;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchaseRepository;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentSaleRepository;
|
|
||||||
import org.apache.fineract.portfolio.investment.exception.InvestmentPurchaseNotFoundException;
|
|
||||||
import org.springframework.data.domain.Sort;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
public class InvestmentSaleReadPlatformService {
|
|
||||||
|
|
||||||
private final InvestmentSaleRepository saleRepository;
|
|
||||||
private final InvestmentPurchaseRepository purchaseRepository;
|
|
||||||
private final InvestmentSaleQuoter quoter;
|
|
||||||
|
|
||||||
/** Sales and redemptions, newest first — pending ones included. */
|
|
||||||
public List<InvestmentSaleData> retrieveAll() {
|
|
||||||
return this.saleRepository.findAll(Sort.by(Sort.Direction.DESC, "id")).stream().map(InvestmentSaleData::from).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** What a sale would come to, before it is saved. */
|
|
||||||
public InvestmentSaleData quoteSale(final Long purchaseId, final LocalDate dealDate, final String settlementType, final BigDecimal units,
|
|
||||||
final BigDecimal saleRate, final BigDecimal brokerage, final Long bankGlAccountId) {
|
|
||||||
final InvestmentPurchase p = purchase(purchaseId);
|
|
||||||
return InvestmentSaleData.from(this.quoter.quoteSale(p, new InvestmentSaleQuoter.SellRequest(null, dealDate, null, settlementType,
|
|
||||||
units, saleRate, brokerage, bankGlAccountId, null, null), null, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** What the redemption at maturity will post, and the units still available to sell. */
|
|
||||||
public InvestmentSaleData quoteRedemption(final Long purchaseId) {
|
|
||||||
return InvestmentSaleData.from(this.quoter.quoteRedemption(purchase(purchaseId), null, null));
|
|
||||||
}
|
|
||||||
|
|
||||||
public BigDecimal availableUnits(final Long purchaseId) {
|
|
||||||
return this.quoter.availableUnits(purchase(purchaseId), null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private InvestmentPurchase purchase(final Long purchaseId) {
|
|
||||||
return this.purchaseRepository.findById(purchaseId).orElseThrow(() -> new InvestmentPurchaseNotFoundException(purchaseId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-35
@@ -1,35 +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.service;
|
|
||||||
|
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
|
||||||
|
|
||||||
public interface InvestmentSaleWritePlatformService {
|
|
||||||
|
|
||||||
CommandProcessingResult createSale(JsonCommand command);
|
|
||||||
|
|
||||||
CommandProcessingResult settleSale(Long saleId, JsonCommand command);
|
|
||||||
|
|
||||||
CommandProcessingResult deleteSale(Long saleId);
|
|
||||||
|
|
||||||
CommandProcessingResult redeem(Long purchaseId, JsonCommand command);
|
|
||||||
|
|
||||||
CommandProcessingResult writeOff(Long purchaseId, JsonCommand command);
|
|
||||||
}
|
|
||||||
-288
@@ -1,288 +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.service;
|
|
||||||
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.STATUS_ACTIVE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.STATUS_REDEEMED;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.STATUS_SOLD;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.AMOUNT;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.BANK_GL_ACCOUNT_ID;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.BROKERAGE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.DEAL_DATE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.DEAL_TIME;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.LOSS_ON_SALE_GL_CODE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.PARTY_ID;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.PREMIUM_AMORTISATION_GL_CODE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.PROFIT_ON_SALE_GL_CODE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.PURCHASE_ID;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.REMARKS;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.SALE_RATE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.SANCTION_AUTHORITY;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.SETTLEMENT_TYPE;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.STATUS_PENDING_SETTLEMENT;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.STATUS_SETTLED;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.TYPE_WRITEOFF;
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentSaleApiConstants.UNITS;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.math.RoundingMode;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentParty;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPartyRepository;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchase;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchaseRepository;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentSale;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentSaleRepository;
|
|
||||||
import org.apache.fineract.portfolio.investment.exception.InvestmentPartyNotFoundException;
|
|
||||||
import org.apache.fineract.portfolio.investment.exception.InvestmentPurchaseNotFoundException;
|
|
||||||
import org.apache.fineract.portfolio.investment.serialization.InvestmentSaleCommandFromApiJsonDeserializer;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sale and redemption of holdings.
|
|
||||||
* <ul>
|
|
||||||
* <li>A sale is saved pending; on or after its settlement date Settle works the figures out again on the books as they
|
|
||||||
* stand and posts, dated the settlement date: Dr Bank (price + interest − brokerage), Dr Brokerage / Cr Investment
|
|
||||||
* (cost), Cr Interest Receivable, Cr Interest Income, Cr Profit or Dr Loss.</li>
|
|
||||||
* <li>A redemption posts at once, dated the maturity date: Dr Bank (face value) / Cr Investment (book value), with a
|
|
||||||
* discount left going to profit and a premium left written off to the amortisation expense.</li>
|
|
||||||
* </ul>
|
|
||||||
*/
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class InvestmentSaleWritePlatformServiceImpl implements InvestmentSaleWritePlatformService {
|
|
||||||
|
|
||||||
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
|
||||||
private static final String BROKERAGE_GL_CODE = "500602";
|
|
||||||
|
|
||||||
private final InvestmentSaleCommandFromApiJsonDeserializer validator;
|
|
||||||
private final InvestmentSaleQuoter quoter;
|
|
||||||
private final InvestmentSaleRepository saleRepository;
|
|
||||||
private final InvestmentPurchaseRepository purchaseRepository;
|
|
||||||
private final InvestmentPartyRepository partyRepository;
|
|
||||||
private final InvestmentJournalPoster journalPoster;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public CommandProcessingResult createSale(final JsonCommand command) {
|
|
||||||
this.validator.validateForSell(command.json());
|
|
||||||
final InvestmentPurchase p = purchase(command.longValueOfParameterNamed(PURCHASE_ID));
|
|
||||||
final InvestmentSale sale = this.saleRepository.saveAndFlush(this.quoter.quoteSale(p, sellRequest(command), null, false));
|
|
||||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(sale.getId())
|
|
||||||
.withSubEntityId(p.getId()).withResourceIdAsString(p.getDealNo()).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public CommandProcessingResult settleSale(final Long saleId, final JsonCommand command) {
|
|
||||||
// Lock the deal before the sale, as every other posting on the deal does, so two settlements cannot interleave.
|
|
||||||
final InvestmentPurchase p = purchase(this.saleRepository.findPurchaseIdBySaleId(saleId)
|
|
||||||
.orElseThrow(() -> InvestmentSaleQuoter.rule("sale.not.found", "Investment sale " + saleId + " does not exist")));
|
|
||||||
final InvestmentSale pending = sale(saleId);
|
|
||||||
if (!STATUS_PENDING_SETTLEMENT.equals(pending.getStatus())) {
|
|
||||||
throw InvestmentSaleQuoter.rule("settle.not.pending", "This sale is not pending settlement");
|
|
||||||
}
|
|
||||||
if (pending.getSettlementDate().isAfter(DateUtils.getBusinessLocalDate())) {
|
|
||||||
throw InvestmentSaleQuoter.rule("settle.before.date",
|
|
||||||
"The sale can be settled on or after its settlement date " + pending.getSettlementDate().format(DATE));
|
|
||||||
}
|
|
||||||
this.quoter.requireInterestPostedUpTo(p, pending.getSettlementDate());
|
|
||||||
|
|
||||||
// The books may have moved since the sale was saved (a provision posted, another sale settled): work it out again.
|
|
||||||
final InvestmentSale s = this.quoter.quoteSale(p, new InvestmentSaleQuoter.SellRequest(pending.getParty(), pending.getDealDate(),
|
|
||||||
pending.getDealTime(), pending.getSettlementType(), pending.getUnits(), pending.getSaleRate(), pending.getBrokerage(),
|
|
||||||
pending.getBankGlAccount().getId(), pending.getSanctionAuthority(), pending.getRemarks()), saleId, false);
|
|
||||||
// Principal only; the interest from the buyer is a manual entry (shown as a hint on the screen).
|
|
||||||
final InvestmentJournalPoster.Lines lines = new InvestmentJournalPoster.Lines().debit(s.getBankGlAccount().getId(), s.getNetAmount())
|
|
||||||
.debit(this.journalPoster.glId(BROKERAGE_GL_CODE), s.getBrokerage())
|
|
||||||
.credit(p.getInvestmentGlAccount().getId(), s.getBookValueSold().subtract(s.getPremiumCleared()))
|
|
||||||
.credit(InvestmentSaleQuoter.premiumGlId(p), s.getPremiumCleared());
|
|
||||||
profitOrLoss(lines, s.getProfitLoss(), LOSS_ON_SALE_GL_CODE);
|
|
||||||
final CommandProcessingResult posted = this.journalPoster.post(s.getSettlementDate(), p.getDealNo(),
|
|
||||||
"Investment sale settled: deal " + p.getDealNo() + ", " + s.getUnits().stripTrailingZeros().toPlainString() + " units of "
|
|
||||||
+ p.getScript().getScriptCode() + " " + p.getScript().getScriptDesc() + " to " + s.getParty().getPartyName(),
|
|
||||||
lines);
|
|
||||||
|
|
||||||
copyFigures(s, pending);
|
|
||||||
pending.setStatus(STATUS_SETTLED);
|
|
||||||
pending.setGlTransactionId(posted.getTransactionId());
|
|
||||||
pending.setVoucherNo(voucherNo(posted));
|
|
||||||
pending.setSettledOnDate(DateUtils.getBusinessLocalDate());
|
|
||||||
this.saleRepository.saveAndFlush(pending);
|
|
||||||
|
|
||||||
p.setBalanceUnits(p.getBalanceUnits().subtract(pending.getUnits()));
|
|
||||||
if (p.getBalanceUnits().signum() == 0) {
|
|
||||||
p.setStatus(STATUS_SOLD);
|
|
||||||
}
|
|
||||||
this.purchaseRepository.saveAndFlush(p);
|
|
||||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(saleId).withSubEntityId(p.getId())
|
|
||||||
.withResourceIdAsString(pending.getVoucherNo()).withTransactionId(posted.getTransactionId()).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public CommandProcessingResult deleteSale(final Long saleId) {
|
|
||||||
final InvestmentSale s = sale(saleId);
|
|
||||||
if (!STATUS_PENDING_SETTLEMENT.equals(s.getStatus())) {
|
|
||||||
throw InvestmentSaleQuoter.rule("sale.settled", "This sale is already settled and posted to GL, so it cannot be dropped");
|
|
||||||
}
|
|
||||||
final Long purchaseId = s.getPurchase().getId();
|
|
||||||
this.saleRepository.delete(s);
|
|
||||||
this.saleRepository.flush();
|
|
||||||
return new CommandProcessingResultBuilder().withEntityId(saleId).withSubEntityId(purchaseId).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public CommandProcessingResult redeem(final Long purchaseId, final JsonCommand command) {
|
|
||||||
this.validator.validateForRedeem(command.json());
|
|
||||||
final InvestmentPurchase p = purchase(purchaseId);
|
|
||||||
final Long bankGlId = command.parameterExists(BANK_GL_ACCOUNT_ID) ? command.longValueOfParameterNamed(BANK_GL_ACCOUNT_ID) : null;
|
|
||||||
final InvestmentSale s = this.quoter.quoteRedemption(p, bankGlId, command.stringValueOfParameterNamed(REMARKS));
|
|
||||||
this.quoter.requireInterestPostedUpTo(p, p.getMaturityDate());
|
|
||||||
|
|
||||||
final InvestmentJournalPoster.Lines lines = new InvestmentJournalPoster.Lines().debit(s.getBankGlAccount().getId(), s.getNetAmount())
|
|
||||||
.credit(p.getInvestmentGlAccount().getId(), s.getBookValueSold().subtract(s.getPremiumCleared()))
|
|
||||||
.credit(InvestmentSaleQuoter.premiumGlId(p), s.getPremiumCleared());
|
|
||||||
profitOrLoss(lines, s.getProfitLoss(), PREMIUM_AMORTISATION_GL_CODE);
|
|
||||||
final CommandProcessingResult posted = this.journalPoster.post(s.getSettlementDate(), p.getDealNo(),
|
|
||||||
"Investment redeemed at maturity: deal " + p.getDealNo() + ", " + p.getScript().getScriptCode() + " "
|
|
||||||
+ p.getScript().getScriptDesc(),
|
|
||||||
lines);
|
|
||||||
|
|
||||||
s.setGlTransactionId(posted.getTransactionId());
|
|
||||||
s.setVoucherNo(voucherNo(posted));
|
|
||||||
s.setSettledOnDate(DateUtils.getBusinessLocalDate());
|
|
||||||
final InvestmentSale saved = this.saleRepository.saveAndFlush(s);
|
|
||||||
p.setBalanceUnits(BigDecimal.ZERO);
|
|
||||||
p.setStatus(STATUS_REDEEMED);
|
|
||||||
this.purchaseRepository.saveAndFlush(p);
|
|
||||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(saved.getId())
|
|
||||||
.withSubEntityId(purchaseId).withResourceIdAsString(saved.getVoucherNo()).withTransactionId(posted.getTransactionId())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes part of the book value off as a loss (Jitendra Sir: a security bought for 1 lakh now worth 95,000 is
|
|
||||||
* shown at 95,000): Dr Loss on Investments / Cr Investment, dated today. The units stay held; later sales and the
|
|
||||||
* redemption work from the reduced book value.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public CommandProcessingResult writeOff(final Long purchaseId, final JsonCommand command) {
|
|
||||||
this.validator.validateForWriteOff(command.json());
|
|
||||||
final InvestmentPurchase p = purchase(purchaseId);
|
|
||||||
final BigDecimal amount = command.bigDecimalValueOfParameterNamed(AMOUNT).setScale(2, RoundingMode.HALF_UP);
|
|
||||||
if (!STATUS_ACTIVE.equals(p.getStatus())) {
|
|
||||||
throw InvestmentSaleQuoter.rule("not.active", "Deal " + p.getDealNo() + " is not an active (settled) holding");
|
|
||||||
}
|
|
||||||
// The loss comes off the investment GL; premium still in its own GL (100707) is not there to write off, and taking
|
|
||||||
// more would leave the investment GL short when the deal is later sold or redeemed.
|
|
||||||
final BigDecimal limit = p.getPremiumGlAccount() != null ? this.quoter.remainingBook(p).subtract(this.quoter.unamortisedPremium(p))
|
|
||||||
: this.quoter.remainingBook(p);
|
|
||||||
if (amount.compareTo(limit) >= 0) {
|
|
||||||
throw InvestmentSaleQuoter.rule("writeoff.exceeds", "The write-off must be less than "
|
|
||||||
+ limit.stripTrailingZeros().toPlainString() + " (book value less premium still to be amortised); "
|
|
||||||
+ "sell or redeem the security to close it");
|
|
||||||
}
|
|
||||||
final LocalDate today = DateUtils.getBusinessLocalDate();
|
|
||||||
final CommandProcessingResult posted = this.journalPoster.post(today, p.getDealNo(),
|
|
||||||
"Investment written off: deal " + p.getDealNo() + ", " + p.getScript().getScriptCode() + " " + p.getScript().getScriptDesc(),
|
|
||||||
new InvestmentJournalPoster.Lines().debit(this.journalPoster.glId(LOSS_ON_SALE_GL_CODE), amount)
|
|
||||||
.credit(p.getInvestmentGlAccount().getId(), amount));
|
|
||||||
|
|
||||||
final InvestmentSale s = new InvestmentSale();
|
|
||||||
s.setPurchase(p);
|
|
||||||
s.setSaleType(TYPE_WRITEOFF);
|
|
||||||
s.setDealDate(today);
|
|
||||||
s.setSettlementDate(today);
|
|
||||||
s.setUnits(BigDecimal.ZERO);
|
|
||||||
s.setFaceValue(BigDecimal.ZERO);
|
|
||||||
s.setConsideration(BigDecimal.ZERO);
|
|
||||||
s.setAccruedDays(0);
|
|
||||||
s.setAccruedInterest(BigDecimal.ZERO);
|
|
||||||
s.setReceivableCleared(BigDecimal.ZERO);
|
|
||||||
s.setBrokerage(BigDecimal.ZERO);
|
|
||||||
s.setBookValueSold(amount);
|
|
||||||
s.setPremiumCleared(BigDecimal.ZERO);
|
|
||||||
s.setProfitLoss(amount.negate());
|
|
||||||
s.setNetAmount(BigDecimal.ZERO);
|
|
||||||
s.setBankGlAccount(p.getCrGlAccount());
|
|
||||||
s.setRemarks(command.stringValueOfParameterNamed(REMARKS).trim());
|
|
||||||
s.setStatus(STATUS_SETTLED);
|
|
||||||
s.setGlTransactionId(posted.getTransactionId());
|
|
||||||
s.setVoucherNo(voucherNo(posted));
|
|
||||||
s.setSettledOnDate(today);
|
|
||||||
final InvestmentSale saved = this.saleRepository.saveAndFlush(s);
|
|
||||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(saved.getId())
|
|
||||||
.withSubEntityId(purchaseId).withResourceIdAsString(saved.getVoucherNo()).withTransactionId(posted.getTransactionId())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Profit is credited to income; a shortfall is debited to {@code lossGlCode}. */
|
|
||||||
private void profitOrLoss(final InvestmentJournalPoster.Lines lines, final BigDecimal profitLoss, final String lossGlCode) {
|
|
||||||
if (profitLoss.signum() > 0) {
|
|
||||||
lines.credit(this.journalPoster.glId(PROFIT_ON_SALE_GL_CODE), profitLoss);
|
|
||||||
} else if (profitLoss.signum() < 0) {
|
|
||||||
lines.debit(this.journalPoster.glId(lossGlCode), profitLoss.negate());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private InvestmentSaleQuoter.SellRequest sellRequest(final JsonCommand command) {
|
|
||||||
final Long partyId = command.longValueOfParameterNamed(PARTY_ID);
|
|
||||||
final InvestmentParty party = this.partyRepository.findById(partyId).orElseThrow(() -> new InvestmentPartyNotFoundException(partyId));
|
|
||||||
final LocalDate dealDate = command.localDateValueOfParameterNamed(DEAL_DATE);
|
|
||||||
return new InvestmentSaleQuoter.SellRequest(party, dealDate, command.stringValueOfParameterNamed(DEAL_TIME),
|
|
||||||
command.stringValueOfParameterNamed(SETTLEMENT_TYPE), command.bigDecimalValueOfParameterNamed(UNITS),
|
|
||||||
command.bigDecimalValueOfParameterNamed(SALE_RATE), command.bigDecimalValueOfParameterNamed(BROKERAGE),
|
|
||||||
command.longValueOfParameterNamed(BANK_GL_ACCOUNT_ID), command.stringValueOfParameterNamed(SANCTION_AUTHORITY),
|
|
||||||
command.stringValueOfParameterNamed(REMARKS));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void copyFigures(final InvestmentSale from, final InvestmentSale to) {
|
|
||||||
to.setAccruedDays(from.getAccruedDays());
|
|
||||||
to.setAccruedInterest(from.getAccruedInterest());
|
|
||||||
to.setReceivableCleared(from.getReceivableCleared());
|
|
||||||
to.setBookValueSold(from.getBookValueSold());
|
|
||||||
to.setPremiumCleared(from.getPremiumCleared());
|
|
||||||
to.setProfitLoss(from.getProfitLoss());
|
|
||||||
to.setNetAmount(from.getNetAmount());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String voucherNo(final CommandProcessingResult posted) {
|
|
||||||
final Object voucherNo = posted.getChanges() == null ? null : posted.getChanges().get("voucherNo");
|
|
||||||
return voucherNo == null ? null : voucherNo.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private InvestmentPurchase purchase(final Long purchaseId) {
|
|
||||||
return this.purchaseRepository.findByIdWithLock(purchaseId).orElseThrow(() -> new InvestmentPurchaseNotFoundException(purchaseId));
|
|
||||||
}
|
|
||||||
|
|
||||||
private InvestmentSale sale(final Long saleId) {
|
|
||||||
return this.saleRepository.findByIdWithLock(saleId)
|
|
||||||
.orElseThrow(() -> InvestmentSaleQuoter.rule("sale.not.found", "Investment sale " + saleId + " does not exist"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-171
@@ -1,171 +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.service;
|
|
||||||
|
|
||||||
import static org.apache.fineract.portfolio.investment.api.InvestmentPurchaseApiConstants.STATUS_ACTIVE;
|
|
||||||
|
|
||||||
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.api.JsonCommand;
|
|
||||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
|
||||||
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
|
||||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
|
||||||
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.infrastructure.core.service.DateUtils;
|
|
||||||
import org.apache.fineract.portfolio.investment.api.InvestmentApiConstants;
|
|
||||||
import org.apache.fineract.portfolio.investment.data.InvestmentSecurityTransferData;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchase;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchaseRepository;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentScript;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentScriptRepository;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentSecurityTransfer;
|
|
||||||
import org.apache.fineract.portfolio.investment.domain.InvestmentSecurityTransferRepository;
|
|
||||||
import org.apache.fineract.portfolio.investment.exception.InvestmentPurchaseNotFoundException;
|
|
||||||
import org.apache.fineract.portfolio.investment.exception.InvestmentScriptNotFoundException;
|
|
||||||
import org.springframework.data.domain.Sort;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GOI Category Transfer as Jitendra Sir describes it: a holding is replaced by another government security of the same
|
|
||||||
* terms (same coupon rate, maturity and unit value, e.g. one state loan for another). Units, book value and the interest
|
|
||||||
* schedule carry over unchanged; when the new script maps to a different investment GL the book value is moved across,
|
|
||||||
* Dr new GL / Cr old GL, dated the transfer date.
|
|
||||||
*/
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class InvestmentSecurityTransferService {
|
|
||||||
|
|
||||||
public static final String SCRIPT_ID = "scriptId";
|
|
||||||
public static final String REMARKS = "remarks";
|
|
||||||
private static final Set<String> PARAMS = Set.of(SCRIPT_ID, REMARKS, "locale", "dateFormat");
|
|
||||||
|
|
||||||
private final FromJsonHelper fromApiJsonHelper;
|
|
||||||
private final InvestmentPurchaseRepository purchaseRepository;
|
|
||||||
private final InvestmentScriptRepository scriptRepository;
|
|
||||||
private final InvestmentSecurityTransferRepository transferRepository;
|
|
||||||
private final InvestmentSaleQuoter quoter;
|
|
||||||
private final InvestmentJournalPoster journalPoster;
|
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
public List<InvestmentSecurityTransferData> retrieveAll() {
|
|
||||||
return this.transferRepository.findAll(Sort.by(Sort.Direction.DESC, "id")).stream().map(InvestmentSecurityTransferData::from)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public CommandProcessingResult transfer(final Long purchaseId, final JsonCommand command) {
|
|
||||||
validate(command.json());
|
|
||||||
final InvestmentPurchase p = this.purchaseRepository.findByIdWithLock(purchaseId)
|
|
||||||
.orElseThrow(() -> new InvestmentPurchaseNotFoundException(purchaseId));
|
|
||||||
final Long scriptId = command.longValueOfParameterNamed(SCRIPT_ID);
|
|
||||||
final InvestmentScript to = this.scriptRepository.findById(scriptId).orElseThrow(() -> new InvestmentScriptNotFoundException(scriptId));
|
|
||||||
final InvestmentScript from = p.getScript();
|
|
||||||
if (!STATUS_ACTIVE.equals(p.getStatus()) || !InvestmentApiConstants.DEBT_BOND_TYPES.contains(p.getBondType())) {
|
|
||||||
throw rule("not.active", "Deal " + p.getDealNo() + " is not an active Govt Security / Bond holding");
|
|
||||||
}
|
|
||||||
if (this.quoter.availableUnits(p, null).compareTo(p.getBalanceUnits()) != 0) {
|
|
||||||
throw rule("pending.sale", "Deal " + p.getDealNo() + " has a pending sale; settle or drop it first");
|
|
||||||
}
|
|
||||||
if (to.getId().equals(from.getId())) {
|
|
||||||
throw rule("same.script", "Choose a different security to transfer to");
|
|
||||||
}
|
|
||||||
if (!to.isActive()) {
|
|
||||||
throw rule("script.inactive", "Script " + to.getScriptCode() + " is inactive");
|
|
||||||
}
|
|
||||||
if (!to.getBondType().equals(p.getBondType()) || !sameTerms(from, to)) {
|
|
||||||
throw rule("terms.differ", "Script " + to.getScriptCode() + " must have the same bond type, coupon rate, maturity date and "
|
|
||||||
+ "unit value as " + from.getScriptCode() + "; only the security changes, everything else stays the same");
|
|
||||||
}
|
|
||||||
|
|
||||||
final LocalDate today = DateUtils.getBusinessLocalDate();
|
|
||||||
final BigDecimal book = this.quoter.remainingBook(p);
|
|
||||||
final InvestmentSecurityTransfer t = new InvestmentSecurityTransfer();
|
|
||||||
t.setPurchase(p);
|
|
||||||
t.setFromScript(from);
|
|
||||||
t.setToScript(to);
|
|
||||||
t.setFromGlAccount(p.getInvestmentGlAccount());
|
|
||||||
t.setToGlAccount(to.getGlAccount());
|
|
||||||
t.setTransferDate(today);
|
|
||||||
t.setUnits(p.getBalanceUnits());
|
|
||||||
t.setBookValue(book);
|
|
||||||
t.setRemarks(StringUtils.trimToNull(command.stringValueOfParameterNamed(REMARKS)));
|
|
||||||
String transactionId = null;
|
|
||||||
if (!to.getGlAccount().getId().equals(p.getInvestmentGlAccount().getId())) {
|
|
||||||
// Unamortised premium kept in its own GL (100707) stays there; only the investment GL's part moves.
|
|
||||||
final BigDecimal moved = p.getPremiumGlAccount() != null ? book.subtract(this.quoter.unamortisedPremium(p)) : book;
|
|
||||||
final CommandProcessingResult posted = this.journalPoster.post(today, p.getDealNo(),
|
|
||||||
"GOI category transfer: deal " + p.getDealNo() + " from " + from.getScriptCode() + " " + from.getScriptDesc() + " to "
|
|
||||||
+ to.getScriptCode() + " " + to.getScriptDesc(),
|
|
||||||
new InvestmentJournalPoster.Lines().debit(to.getGlAccount().getId(), moved).credit(p.getInvestmentGlAccount().getId(),
|
|
||||||
moved));
|
|
||||||
transactionId = posted.getTransactionId();
|
|
||||||
final Object voucherNo = posted.getChanges() == null ? null : posted.getChanges().get("voucherNo");
|
|
||||||
t.setGlTransactionId(transactionId);
|
|
||||||
t.setVoucherNo(voucherNo == null ? null : voucherNo.toString());
|
|
||||||
}
|
|
||||||
final InvestmentSecurityTransfer saved = this.transferRepository.saveAndFlush(t);
|
|
||||||
p.setScript(to);
|
|
||||||
p.setInvestmentGlAccount(to.getGlAccount());
|
|
||||||
this.purchaseRepository.saveAndFlush(p);
|
|
||||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(saved.getId()).withSubEntityId(purchaseId)
|
|
||||||
.withResourceIdAsString(saved.getVoucherNo() == null ? p.getDealNo() : saved.getVoucherNo()).withTransactionId(transactionId)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean sameTerms(final InvestmentScript a, final InvestmentScript b) {
|
|
||||||
return a.getIntRate() != null && b.getIntRate() != null && a.getIntRate().compareTo(b.getIntRate()) == 0
|
|
||||||
&& a.getMaturityDate() != null && a.getMaturityDate().equals(b.getMaturityDate())
|
|
||||||
&& a.getFaceValuePerUnit().compareTo(b.getFaceValuePerUnit()) == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void validate(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("investmenttransfer");
|
|
||||||
v.reset().parameter(SCRIPT_ID).value(this.fromApiJsonHelper.extractLongNamed(SCRIPT_ID, element)).notNull().longGreaterThanZero();
|
|
||||||
v.reset().parameter(REMARKS).value(this.fromApiJsonHelper.extractStringNamed(REMARKS, element)).ignoreIfNull()
|
|
||||||
.notExceedingLengthOf(500);
|
|
||||||
if (!errors.isEmpty()) {
|
|
||||||
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static GeneralPlatformDomainRuleException rule(final String code, final String message) {
|
|
||||||
return new GeneralPlatformDomainRuleException("error.msg.investmenttransfer." + code, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+17
@@ -392,6 +392,9 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
|
|||||||
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
|
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
|
||||||
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
|
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
|
||||||
|
|
||||||
|
// An FD is funded once, with exactly its principal (see FixedDepositFundingValidator).
|
||||||
|
new FixedDepositFundingValidator(this.jdbcTemplate).validateFirstFunding(savingsId, transactionAmount);
|
||||||
|
|
||||||
final Map<String, Object> changes = new LinkedHashMap<>();
|
final Map<String, Object> changes = new LinkedHashMap<>();
|
||||||
final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes);
|
final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes);
|
||||||
this.denominationBreakdownWriter.validatePreviousDayClosed();
|
this.denominationBreakdownWriter.validatePreviousDayClosed();
|
||||||
@@ -422,6 +425,20 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public void recalculateFixedDepositMaturity(final Long savingsId) {
|
||||||
|
final FixedDepositAccount account = (FixedDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId,
|
||||||
|
DepositAccountType.FIXED_DEPOSIT);
|
||||||
|
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
|
||||||
|
.isSavingsInterestPostingAtCurrentPeriodEnd();
|
||||||
|
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
|
||||||
|
final boolean isPreMatureClosure = false;
|
||||||
|
account.updateMaturityDateAndAmount(MathContext.DECIMAL64, isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd,
|
||||||
|
financialYearBeginningMonth);
|
||||||
|
this.savingAccountRepositoryWrapper.saveAndFlush(account);
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
public CommandProcessingResult updateDepositAmountForRDAccount(Long savingsId, JsonCommand command) {
|
public CommandProcessingResult updateDepositAmountForRDAccount(Long savingsId, JsonCommand command) {
|
||||||
|
|||||||
+4
-21
@@ -48,7 +48,6 @@ import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
|||||||
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
|
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
|
||||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||||
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
|
||||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||||
import org.apache.fineract.infrastructure.event.business.domain.deposit.FixedDepositAccountCreateBusinessEvent;
|
import org.apache.fineract.infrastructure.event.business.domain.deposit.FixedDepositAccountCreateBusinessEvent;
|
||||||
import org.apache.fineract.infrastructure.event.business.domain.deposit.RecurringDepositAccountCreateBusinessEvent;
|
import org.apache.fineract.infrastructure.event.business.domain.deposit.RecurringDepositAccountCreateBusinessEvent;
|
||||||
@@ -184,16 +183,8 @@ public class DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
|
|||||||
final FixedDepositAccount account = (FixedDepositAccount) this.depositAccountAssembler.assembleFrom(command, submittedBy,
|
final FixedDepositAccount account = (FixedDepositAccount) this.depositAccountAssembler.assembleFrom(command, submittedBy,
|
||||||
DepositAccountType.FIXED_DEPOSIT);
|
DepositAccountType.FIXED_DEPOSIT);
|
||||||
|
|
||||||
if (account.getClient() != null && account.productId() != null) {
|
// A client may hold any number of Fixed Deposits on the same product, even while an earlier one is
|
||||||
long existingCount = this.savingAccountRepository.countActiveAccountsByClientAndProduct(
|
// still active, so there is deliberately no one-active-account-per-product check here.
|
||||||
account.getClient().getId(), account.productId());
|
|
||||||
if (existingCount > 0) {
|
|
||||||
throw new PlatformDataIntegrityException(
|
|
||||||
"error.msg.savings.account.duplicate.product.for.client",
|
|
||||||
"Client already has an active Fixed Deposit account with the same product.",
|
|
||||||
"productId", account.productId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final MathContext mc = MathContext.DECIMAL64;
|
final MathContext mc = MathContext.DECIMAL64;
|
||||||
final boolean isPreMatureClosure = false;
|
final boolean isPreMatureClosure = false;
|
||||||
@@ -269,16 +260,8 @@ public class DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
|
|||||||
final RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler.assembleFrom(command,
|
final RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler.assembleFrom(command,
|
||||||
submittedBy, DepositAccountType.RECURRING_DEPOSIT);
|
submittedBy, DepositAccountType.RECURRING_DEPOSIT);
|
||||||
|
|
||||||
if (account.getClient() != null && account.productId() != null) {
|
// A client may hold any number of Recurring Deposits on the same product, even while an earlier one
|
||||||
long existingCount = this.savingAccountRepository.countActiveAccountsByClientAndProduct(
|
// is still active, so there is deliberately no one-active-account-per-product check here.
|
||||||
account.getClient().getId(), account.productId());
|
|
||||||
if (existingCount > 0) {
|
|
||||||
throw new PlatformDataIntegrityException(
|
|
||||||
"error.msg.savings.account.duplicate.product.for.client",
|
|
||||||
"Client already has an active Recurring Deposit account with the same product.",
|
|
||||||
"productId", account.productId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.savingsAccountAssembler.applyCbsFields(command, command.parsedJson(), account);
|
this.savingsAccountAssembler.applyCbsFields(command, command.parsedJson(), account);
|
||||||
|
|
||||||
|
|||||||
+238
@@ -0,0 +1,238 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.savings.service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The rules a fixed deposit is funded under, shared by every path money can reach an FD by: the FD deposit
|
||||||
|
* command itself (cash at the counter, approved cashier deposit requests) and the clerk's Bank Transfer,
|
||||||
|
* Fund Transfer and Disbursement requests, which credit the account directly.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* An FD opens with no money in it and is funded exactly once, with exactly its principal. A second credit
|
||||||
|
* would silently change the principal the maturity was agreed on; anything more goes into a new FD (several
|
||||||
|
* FDs on the same product are allowed). Money never leaves an FD through these paths either - only through
|
||||||
|
* its maturity or premature closure.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* When the money comes from the customer's own Savings or Current account, that account must be able to pay
|
||||||
|
* it: active, not debit-frozen, out of any lock-in, belonging to the FD's client (or held jointly with them),
|
||||||
|
* and holding enough available balance once holds, liens, clearing holds and its minimum balance are set
|
||||||
|
* aside. The KYC debit restriction on that account is checked by the callers through KycRestrictionGuard.
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class FixedDepositFundingValidator {
|
||||||
|
|
||||||
|
/** m_savings_account.deposit_type_enum (Fineract DepositAccountType). */
|
||||||
|
public static final int DEPOSIT_TYPE_SAVINGS = 100;
|
||||||
|
public static final int DEPOSIT_TYPE_FIXED = 200;
|
||||||
|
|
||||||
|
private static final int STATUS_ACTIVE = 300;
|
||||||
|
private static final int TXN_TYPE_DEPOSIT = 1;
|
||||||
|
/** SavingsAccountSubStatusEnum.BLOCK (no debits or credits) and BLOCK_DEBIT. */
|
||||||
|
private static final int SUB_STATUS_BLOCK = 400;
|
||||||
|
private static final int SUB_STATUS_BLOCK_DEBIT = 600;
|
||||||
|
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
|
||||||
|
/** Whether this m_savings_account id is a fixed deposit (a CURRENT account id must not be passed here). */
|
||||||
|
public boolean isFixedDeposit(final Long savingsAccountId) {
|
||||||
|
if (savingsAccountId == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final Integer count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM m_savings_account WHERE id = ? AND deposit_type_enum = ?",
|
||||||
|
Integer.class, savingsAccountId, DEPOSIT_TYPE_FIXED);
|
||||||
|
return count != null && count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The FD's first and only funding: the account must be an active, not yet matured fixed deposit with no
|
||||||
|
* money in it, and the amount must be exactly its principal.
|
||||||
|
*
|
||||||
|
* @return the FD's client id, for the source-account checks
|
||||||
|
*/
|
||||||
|
public Long validateFirstFunding(final Long fdAccountId, final BigDecimal amount) {
|
||||||
|
final List<Map<String, Object>> rows = jdbcTemplate.queryForList("""
|
||||||
|
SELECT s.account_no, s.status_enum, s.deposit_type_enum, s.client_id, s.account_balance_derived,
|
||||||
|
t.deposit_amount, t.maturity_date
|
||||||
|
FROM m_savings_account s
|
||||||
|
LEFT JOIN m_deposit_account_term_and_preclosure t ON t.savings_account_id = s.id
|
||||||
|
WHERE s.id = ?
|
||||||
|
""", fdAccountId);
|
||||||
|
if (rows.isEmpty()) {
|
||||||
|
throw fail("error.msg.fd.funding.account.not.found", "Fixed deposit account " + fdAccountId + " was not found.");
|
||||||
|
}
|
||||||
|
final Map<String, Object> fd = rows.get(0);
|
||||||
|
final String accountNo = (String) fd.get("account_no");
|
||||||
|
|
||||||
|
if (intOf(fd.get("deposit_type_enum")) != DEPOSIT_TYPE_FIXED) {
|
||||||
|
throw fail("error.msg.fd.funding.not.a.fixed.deposit", "Account " + accountNo + " is not a fixed deposit.");
|
||||||
|
}
|
||||||
|
if (intOf(fd.get("status_enum")) != STATUS_ACTIVE) {
|
||||||
|
throw fail("error.msg.fd.funding.not.active",
|
||||||
|
"Fixed deposit " + accountNo + " is not active. Only an active fixed deposit can be funded.");
|
||||||
|
}
|
||||||
|
final LocalDate businessDate = DateUtils.getBusinessLocalDate();
|
||||||
|
final LocalDate maturityDate = fd.get("maturity_date") instanceof java.sql.Date d ? d.toLocalDate() : null;
|
||||||
|
if (maturityDate != null && !businessDate.isBefore(maturityDate)) {
|
||||||
|
throw fail("error.msg.fd.funding.matured",
|
||||||
|
"Fixed deposit " + accountNo + " has reached its maturity date (" + maturityDate + ") and can no longer be funded.");
|
||||||
|
}
|
||||||
|
|
||||||
|
final Integer deposits = jdbcTemplate.queryForObject(
|
||||||
|
"SELECT COUNT(*) FROM m_savings_account_transaction WHERE savings_account_id = ? AND transaction_type_enum = ?"
|
||||||
|
+ " AND is_reversed = false",
|
||||||
|
Integer.class, fdAccountId, TXN_TYPE_DEPOSIT);
|
||||||
|
if ((deposits != null && deposits > 0) || amountOf(fd.get("account_balance_derived")).signum() > 0) {
|
||||||
|
throw fail("error.msg.fd.funding.already.funded", "Fixed deposit " + accountNo
|
||||||
|
+ " is already funded. A fixed deposit is funded only once - open a new fixed deposit for more money.");
|
||||||
|
}
|
||||||
|
|
||||||
|
final BigDecimal principal = fd.get("deposit_amount") == null ? null : amountOf(fd.get("deposit_amount"));
|
||||||
|
if (principal == null || principal.signum() <= 0) {
|
||||||
|
throw fail("error.msg.fd.funding.principal.missing", "Fixed deposit " + accountNo + " has no deposit amount (principal) set.");
|
||||||
|
}
|
||||||
|
if (amount == null || amount.compareTo(principal) != 0) {
|
||||||
|
throw fail("error.msg.fd.funding.amount.mismatch", "Amount " + (amount == null ? "0" : amount.toPlainString())
|
||||||
|
+ " for fixed deposit " + accountNo + " must be exactly its principal " + principal.toPlainString() + " - not less, not more.");
|
||||||
|
}
|
||||||
|
return fd.get("client_id") == null ? null : ((Number) fd.get("client_id")).longValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Savings or Current account an FD is being funded from.
|
||||||
|
*
|
||||||
|
* @param fdAccountId
|
||||||
|
* the FD being funded
|
||||||
|
* @param fdClientId
|
||||||
|
* its client, from {@link #validateFirstFunding}
|
||||||
|
* @param sourceAccountId
|
||||||
|
* m_savings_account id, or m_current_account id when {@code sourceIsCurrent}
|
||||||
|
*/
|
||||||
|
public void validateFundingSource(final Long fdAccountId, final Long fdClientId, final Long sourceAccountId,
|
||||||
|
final boolean sourceIsCurrent, final BigDecimal amount) {
|
||||||
|
final List<Map<String, Object>> rows = sourceIsCurrent ? jdbcTemplate.queryForList("""
|
||||||
|
SELECT account_no, status_enum, sub_status_enum, client_id, account_balance_derived, on_hold_funds_derived,
|
||||||
|
total_savings_amount_on_hold, min_required_balance, enforce_min_required_balance,
|
||||||
|
allow_overdraft, overdraft_limit, lockedin_until_date_derived
|
||||||
|
FROM m_current_account WHERE id = ?
|
||||||
|
""", sourceAccountId) : jdbcTemplate.queryForList("""
|
||||||
|
SELECT account_no, status_enum, sub_status_enum, client_id, deposit_type_enum, account_balance_derived,
|
||||||
|
on_hold_funds_derived, total_savings_amount_on_hold, total_savings_amount_on_hold_clearing,
|
||||||
|
min_required_balance, enforce_min_required_balance, lockedin_until_date_derived
|
||||||
|
FROM m_savings_account WHERE id = ?
|
||||||
|
""", sourceAccountId);
|
||||||
|
if (rows.isEmpty()) {
|
||||||
|
throw fail("error.msg.fd.funding.source.not.found", "Source account " + sourceAccountId + " was not found.");
|
||||||
|
}
|
||||||
|
final Map<String, Object> src = rows.get(0);
|
||||||
|
final String accountNo = (String) src.get("account_no");
|
||||||
|
|
||||||
|
if (!sourceIsCurrent && intOf(src.get("deposit_type_enum")) != DEPOSIT_TYPE_SAVINGS) {
|
||||||
|
throw fail("error.msg.fd.funding.source.type",
|
||||||
|
"Account " + accountNo + " cannot fund a fixed deposit - only a Savings or Current account can.");
|
||||||
|
}
|
||||||
|
if (intOf(src.get("status_enum")) != STATUS_ACTIVE) {
|
||||||
|
throw fail("error.msg.fd.funding.source.not.active", "Source account " + accountNo + " is not active.");
|
||||||
|
}
|
||||||
|
final int subStatus = intOf(src.get("sub_status_enum"));
|
||||||
|
if (subStatus == SUB_STATUS_BLOCK || subStatus == SUB_STATUS_BLOCK_DEBIT) {
|
||||||
|
throw fail("error.msg.fd.funding.source.debit.frozen", "Source account " + accountNo + " is blocked for debits (debit freeze).");
|
||||||
|
}
|
||||||
|
final LocalDate lockedUntil = src.get("lockedin_until_date_derived") instanceof java.sql.Date d ? d.toLocalDate() : null;
|
||||||
|
if (lockedUntil != null && DateUtils.getBusinessLocalDate().isBefore(lockedUntil)) {
|
||||||
|
throw fail("error.msg.fd.funding.source.locked.in", "Source account " + accountNo + " is locked-in until " + lockedUntil + ".");
|
||||||
|
}
|
||||||
|
|
||||||
|
final Long sourceClientId = src.get("client_id") == null ? null : ((Number) src.get("client_id")).longValue();
|
||||||
|
if (!sameClientOrJointHolder(fdAccountId, fdClientId, sourceAccountId, sourceIsCurrent, sourceClientId)) {
|
||||||
|
throw fail("error.msg.fd.funding.source.other.client", "Source account " + accountNo
|
||||||
|
+ " does not belong to the fixed deposit's customer. Fund an FD only from the customer's own account"
|
||||||
|
+ " or one they hold jointly.");
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal available = amountOf(src.get("account_balance_derived")).subtract(amountOf(src.get("on_hold_funds_derived")))
|
||||||
|
.subtract(amountOf(src.get("total_savings_amount_on_hold")));
|
||||||
|
if (!sourceIsCurrent) {
|
||||||
|
available = available.subtract(amountOf(src.get("total_savings_amount_on_hold_clearing")));
|
||||||
|
}
|
||||||
|
if (Boolean.TRUE.equals(src.get("enforce_min_required_balance"))) {
|
||||||
|
available = available.subtract(amountOf(src.get("min_required_balance")));
|
||||||
|
}
|
||||||
|
if (sourceIsCurrent && Boolean.TRUE.equals(src.get("allow_overdraft"))) {
|
||||||
|
available = available.add(amountOf(src.get("overdraft_limit")));
|
||||||
|
}
|
||||||
|
if (amount == null || available.compareTo(amount) < 0) {
|
||||||
|
throw fail("error.msg.fd.funding.source.insufficient", "Source account " + accountNo + " has an available balance of "
|
||||||
|
+ available.max(BigDecimal.ZERO).toPlainString() + " (after holds, lien and minimum balance), less than "
|
||||||
|
+ (amount == null ? "0" : amount.toPlainString()) + ".");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean sameClientOrJointHolder(final Long fdAccountId, final Long fdClientId, final Long sourceAccountId,
|
||||||
|
final boolean sourceIsCurrent, final Long sourceClientId) {
|
||||||
|
if (fdClientId == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (Objects.equals(fdClientId, sourceClientId)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// The FD's customer is a joint holder on the source account ...
|
||||||
|
final Integer onSource = sourceIsCurrent
|
||||||
|
? jdbcTemplate.queryForObject("SELECT COUNT(*) FROM m_current_account_joint_holder WHERE current_account_id = ? AND client_id = ?",
|
||||||
|
Integer.class, sourceAccountId, fdClientId)
|
||||||
|
: jdbcTemplate.queryForObject(
|
||||||
|
"SELECT COUNT(*) FROM m_savings_account_joint_holders WHERE savings_account_id = ? AND client_id = ?", Integer.class,
|
||||||
|
sourceAccountId, fdClientId);
|
||||||
|
if (onSource != null && onSource > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// ... or the source account's customer is a joint holder on the FD.
|
||||||
|
if (sourceClientId == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final Integer onFd = jdbcTemplate.queryForObject(
|
||||||
|
"SELECT COUNT(*) FROM m_savings_account_joint_holders WHERE savings_account_id = ? AND client_id = ?", Integer.class,
|
||||||
|
fdAccountId, sourceClientId);
|
||||||
|
return onFd != null && onFd > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int intOf(final Object value) {
|
||||||
|
return value instanceof Number n ? n.intValue() : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BigDecimal amountOf(final Object value) {
|
||||||
|
return value instanceof BigDecimal b ? b : value instanceof Number n ? new BigDecimal(n.toString()) : BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GeneralPlatformDomainRuleException fail(final String code, final String message) {
|
||||||
|
return new GeneralPlatformDomainRuleException(code, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
+623
@@ -0,0 +1,623 @@
|
|||||||
|
/**
|
||||||
|
* 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.voucher.api;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.ws.rs.Consumes;
|
||||||
|
import jakarta.ws.rs.DefaultValue;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.NotFoundException;
|
||||||
|
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.WebApplicationException;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import jakarta.ws.rs.core.Response;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.sql.Timestamp;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
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.core.serialization.FromJsonHelper;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.cashreport.service.CashierRoleHelper;
|
||||||
|
import org.apache.fineract.portfolio.client.service.KycRestrictionGuard;
|
||||||
|
import org.apache.fineract.portfolio.voucher.service.VoucherLine;
|
||||||
|
import org.apache.fineract.portfolio.voucher.service.VoucherService;
|
||||||
|
import org.apache.fineract.useradministration.domain.AppUser;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Corrects a cash deposit that went to the wrong savings account, without ever "undoing" it behind the
|
||||||
|
* cashier's back.
|
||||||
|
*
|
||||||
|
* <ol>
|
||||||
|
* <li>The cashier picks the day's voucher and asks to <b>unpass</b> it (UNPASS_PENDING). Nothing moves.</li>
|
||||||
|
* <li>A manager approves the unpass (UNPASSED). Still nothing moves on any account: the voucher is simply
|
||||||
|
* waiting for its correction entry.</li>
|
||||||
|
* <li>The cashier opens the unpassed voucher, whose details are fixed, and changes only the account number
|
||||||
|
* (CORRECTION_PENDING).</li>
|
||||||
|
* <li>A manager approves the correction (COMPLETED). Only now do accounts change: the original deposit is
|
||||||
|
* reversed on the wrong account and the same cash is deposited into the right one, under a new voucher; the
|
||||||
|
* original voucher is revoked and the cashier's denominations follow the deposit.</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* The balance of the wrong account is checked twice, when the cashier enters the correction and again when it is
|
||||||
|
* approved, because the customer may have drawn the money in between.
|
||||||
|
*/
|
||||||
|
@Path("/v1/vouchers/corrections")
|
||||||
|
@Component
|
||||||
|
@Tag(name = "Voucher Corrections", description = "Unpass a wrongly-posted cash deposit voucher and re-post it to the right account.")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class VoucherCorrectionApiResource {
|
||||||
|
|
||||||
|
public static final String REF_VOUCHER_CORRECTION = "VOUCHER_CORRECTION";
|
||||||
|
|
||||||
|
private static final String UNPASS_PENDING = "UNPASS_PENDING";
|
||||||
|
private static final String UNPASSED = "UNPASSED";
|
||||||
|
private static final String UNPASS_REJECTED = "UNPASS_REJECTED";
|
||||||
|
private static final String CORRECTION_PENDING = "CORRECTION_PENDING";
|
||||||
|
private static final String COMPLETED = "COMPLETED";
|
||||||
|
private static final String CLOSED = "CLOSED";
|
||||||
|
private static final int SAVINGS_STATUS_ACTIVE = 300;
|
||||||
|
private static final int DEPOSIT_TYPE_SAVINGS = 100;
|
||||||
|
|
||||||
|
private static final DateTimeFormatter VOUCHER_DAY = DateTimeFormatter.ofPattern("ddMMyyyy");
|
||||||
|
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final FromJsonHelper fromJsonHelper;
|
||||||
|
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||||
|
private final VoucherService voucherService;
|
||||||
|
private final KycRestrictionGuard kycRestrictionGuard;
|
||||||
|
private final CashierRoleHelper cashierRoleHelper;
|
||||||
|
private final Gson gson = new GsonBuilder().serializeNulls().create();
|
||||||
|
|
||||||
|
// ── Today's vouchers the cashier may unpass ──────────────────────────────────
|
||||||
|
|
||||||
|
/** Today's cash deposit vouchers on a savings account that have not been reversed, revoked or taken up already. */
|
||||||
|
@GET
|
||||||
|
@Path("eligible")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public String eligible(@QueryParam("search") final String search) {
|
||||||
|
requireUnpasser(this.context.authenticatedUser());
|
||||||
|
final String day = DateUtils.getBusinessLocalDate().format(VOUCHER_DAY);
|
||||||
|
final List<Object> params = new ArrayList<>();
|
||||||
|
params.add("%/" + day + "/%");
|
||||||
|
final StringBuilder sql = new StringBuilder(ELIGIBLE_SQL);
|
||||||
|
if (search != null && !search.isBlank()) {
|
||||||
|
sql.append(" AND (v.voucher_no ILIKE ? OR sa.account_no ILIKE ? OR c.display_name ILIKE ?)");
|
||||||
|
final String like = "%" + search.trim() + "%";
|
||||||
|
params.add(like);
|
||||||
|
params.add(like);
|
||||||
|
params.add(like);
|
||||||
|
}
|
||||||
|
sql.append(" ORDER BY v.id DESC");
|
||||||
|
return gson.toJson(jdbcTemplate.queryForList(sql.toString(), params.toArray()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final String ELIGIBLE_SQL = """
|
||||||
|
SELECT v.voucher_no AS "voucherNo", v.amount, v.created_at AS "createdAt",
|
||||||
|
v.created_by_username AS "cashier", v.savings_txn_id AS "txnId",
|
||||||
|
t.savings_account_id AS "savingsAccountId", sa.account_no AS "accountNo",
|
||||||
|
c.display_name AS "clientName", pt.value AS "paymentType"
|
||||||
|
FROM m_voucher v
|
||||||
|
JOIN m_savings_account_transaction t ON t.id = v.savings_txn_id
|
||||||
|
JOIN m_savings_account sa ON sa.id = t.savings_account_id
|
||||||
|
LEFT JOIN m_client c ON c.id = sa.client_id
|
||||||
|
JOIN m_payment_detail pd ON pd.id = t.payment_detail_id
|
||||||
|
JOIN m_payment_type pt ON pt.id = pd.payment_type_id
|
||||||
|
WHERE v.voucher_type = 'DEPOSIT'
|
||||||
|
AND COALESCE(v.reference_type, '') <> 'VOUCHER_CORRECTION'
|
||||||
|
AND v.voucher_no LIKE ?
|
||||||
|
AND t.is_reversed = false
|
||||||
|
AND t.transaction_type_enum = 1
|
||||||
|
AND pt.is_cash_payment = true
|
||||||
|
AND sa.deposit_type_enum = 100
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM m_voucher_correction vc
|
||||||
|
WHERE vc.original_voucher_no = v.voucher_no
|
||||||
|
AND vc.status NOT IN ('UNPASS_REJECTED'))
|
||||||
|
""";
|
||||||
|
|
||||||
|
// ── Step 1: cashier unpasses a voucher ───────────────────────────────────────
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("unpass")
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Transactional
|
||||||
|
public String unpass(final String body) {
|
||||||
|
final AppUser user = this.context.authenticatedUser();
|
||||||
|
requireUnpasser(user);
|
||||||
|
final JsonElement root = fromJsonHelper.parse(body);
|
||||||
|
final String voucherNo = fromJsonHelper.extractStringNamed("voucherNo", root);
|
||||||
|
final String reason = fromJsonHelper.extractStringNamed("reason", root);
|
||||||
|
if (voucherNo == null || voucherNo.isBlank()) {
|
||||||
|
throw apiError(400, "Voucher number is required");
|
||||||
|
}
|
||||||
|
if (reason == null || reason.isBlank()) {
|
||||||
|
throw apiError(400, "Reason is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
final String day = DateUtils.getBusinessLocalDate().format(VOUCHER_DAY);
|
||||||
|
final List<Map<String, Object>> rows = jdbcTemplate.queryForList(ELIGIBLE_SQL + " AND v.voucher_no = ?", "%/" + day + "/%",
|
||||||
|
voucherNo.trim());
|
||||||
|
if (rows.isEmpty()) {
|
||||||
|
throw apiError(409, "Voucher " + voucherNo
|
||||||
|
+ " cannot be unpassed: only today's cash deposit vouchers on a savings account that are still active qualify.");
|
||||||
|
}
|
||||||
|
final Map<String, Object> v = rows.get(0);
|
||||||
|
|
||||||
|
final Long id = jdbcTemplate.queryForObject("""
|
||||||
|
INSERT INTO m_voucher_correction
|
||||||
|
(original_voucher_no, original_txn_id, wrong_savings_account_id, wrong_account_no, wrong_client_name,
|
||||||
|
amount, payment_type_id, txn_date, status, reason, unpass_by_id, unpass_by_name, unpass_at, activity_log)
|
||||||
|
VALUES (?,?,?,?,?,?,(SELECT pd.payment_type_id FROM m_savings_account_transaction t
|
||||||
|
JOIN m_payment_detail pd ON pd.id = t.payment_detail_id WHERE t.id = ?),
|
||||||
|
(SELECT transaction_date FROM m_savings_account_transaction WHERE id = ?),
|
||||||
|
?,?,?,?,?,?)
|
||||||
|
RETURNING id
|
||||||
|
""", Long.class, voucherNo.trim(), v.get("txnId"), v.get("savingsAccountId"), v.get("accountNo"), v.get("clientName"),
|
||||||
|
v.get("amount"), v.get("txnId"), v.get("txnId"), UNPASS_PENDING, reason.trim(), user.getId(), user.getDisplayName(),
|
||||||
|
Timestamp.from(Instant.now()), log(null, "UNPASS REQUESTED", user, reason.trim()));
|
||||||
|
return gson.toJson(Map.of("resourceId", id, "status", UNPASS_PENDING));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Lists ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Corrections by status. {@code status} may be a comma-separated list. The approval page asks for
|
||||||
|
* UNPASS_PENDING and CORRECTION_PENDING, the cashier's page for UNPASSED.
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public String list(@QueryParam("status") final String status, @QueryParam("mine") @DefaultValue("false") final boolean mine) {
|
||||||
|
final AppUser user = this.context.authenticatedUser();
|
||||||
|
final StringBuilder sql = new StringBuilder("SELECT * FROM m_voucher_correction WHERE 1=1");
|
||||||
|
final List<Object> params = new 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 (unpass_by_id = ? OR correction_by_id = ?)");
|
||||||
|
params.add(user.getId());
|
||||||
|
params.add(user.getId());
|
||||||
|
}
|
||||||
|
sql.append(" ORDER BY id DESC LIMIT 200");
|
||||||
|
return gson.toJson(jdbcTemplate.queryForList(sql.toString(), params.toArray()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("{id}")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public String get(@PathParam("id") final Long id) {
|
||||||
|
this.context.authenticatedUser();
|
||||||
|
return gson.toJson(load(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Step 2: manager approves / rejects the unpass ────────────────────────────
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("{id}/unpass/approve")
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Transactional
|
||||||
|
public String approveUnpass(@PathParam("id") final Long id) {
|
||||||
|
final AppUser user = this.context.authenticatedUser();
|
||||||
|
validateCheckerRole(user);
|
||||||
|
final Map<String, Object> r = load(id);
|
||||||
|
requireStatus(r, UNPASS_PENDING, "approved");
|
||||||
|
requireNotMaker(user, r.get("unpass_by_id"), "unpass request");
|
||||||
|
final int updated = jdbcTemplate.update("""
|
||||||
|
UPDATE m_voucher_correction SET status = ?, unpass_approved_by_id = ?, unpass_approved_by_name = ?,
|
||||||
|
unpass_approved_at = ?, activity_log = ?
|
||||||
|
WHERE id = ? AND status = ?
|
||||||
|
""", UNPASSED, user.getId(), user.getDisplayName(), Timestamp.from(Instant.now()),
|
||||||
|
log((String) r.get("activity_log"), "UNPASS APPROVED", user, null), id, UNPASS_PENDING);
|
||||||
|
if (updated == 0) {
|
||||||
|
throw apiError(409, "This request was already actioned by another manager.");
|
||||||
|
}
|
||||||
|
// Deliberately no account movement here: the deposit stays exactly where it is until the correction is approved.
|
||||||
|
return gson.toJson(Map.of("resourceId", id, "status", UNPASSED));
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("{id}/unpass/reject")
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Transactional
|
||||||
|
public String rejectUnpass(@PathParam("id") final Long id, final String body) {
|
||||||
|
final AppUser user = this.context.authenticatedUser();
|
||||||
|
validateCheckerRole(user);
|
||||||
|
final Map<String, Object> r = load(id);
|
||||||
|
requireStatus(r, UNPASS_PENDING, "rejected");
|
||||||
|
final String reason = requireReason(body);
|
||||||
|
jdbcTemplate.update("UPDATE m_voucher_correction SET status = ?, rejection_reason = ?, activity_log = ? WHERE id = ?", UNPASS_REJECTED,
|
||||||
|
reason, log((String) r.get("activity_log"), "UNPASS REJECTED", user, reason), id);
|
||||||
|
return gson.toJson(Map.of("resourceId", id, "status", UNPASS_REJECTED));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Step 3: cashier enters the correct account ───────────────────────────────
|
||||||
|
|
||||||
|
/** Resolves the account number the cashier typed: name, balance and whether it can receive the deposit. */
|
||||||
|
@GET
|
||||||
|
@Path("account-lookup")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public String accountLookup(@QueryParam("accountNo") final String accountNo) {
|
||||||
|
this.context.authenticatedUser();
|
||||||
|
final Map<String, Object> account = findSavingsAccount(accountNo);
|
||||||
|
if (account == null) {
|
||||||
|
throw apiError(404, "No savings account found for '" + accountNo + "'.");
|
||||||
|
}
|
||||||
|
return gson.toJson(account);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("{id}/correction")
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Transactional
|
||||||
|
public String submitCorrection(@PathParam("id") final Long id, final String body) {
|
||||||
|
final AppUser user = this.context.authenticatedUser();
|
||||||
|
// The corrected entry is the cashier's own: a deposit is taken at the counter, so only a cashier can re-enter it.
|
||||||
|
if (cashierRoleHelper.getCurrentCashierId() == null) {
|
||||||
|
throw apiError(403, "Only a cashier can enter the correction for a voucher. Your login is not linked to a cashier.");
|
||||||
|
}
|
||||||
|
final Map<String, Object> r = load(id);
|
||||||
|
requireStatus(r, UNPASSED, "corrected");
|
||||||
|
final String accountNo = fromJsonHelper.extractStringNamed("correctAccountNo", fromJsonHelper.parse(body));
|
||||||
|
final Map<String, Object> correct = validateCorrectAccount(r, accountNo);
|
||||||
|
checkWrongAccountBalance(r);
|
||||||
|
|
||||||
|
jdbcTemplate.update("""
|
||||||
|
UPDATE m_voucher_correction SET status = ?, correct_savings_account_id = ?, correct_account_no = ?,
|
||||||
|
correct_client_id = ?, correct_client_name = ?, correction_by_id = ?, correction_by_name = ?,
|
||||||
|
correction_at = ?, rejection_reason = NULL, activity_log = ?
|
||||||
|
WHERE id = ? AND status = ?
|
||||||
|
""", CORRECTION_PENDING, correct.get("id"), correct.get("accountNo"), correct.get("clientId"), correct.get("clientName"),
|
||||||
|
user.getId(), user.getDisplayName(), Timestamp.from(Instant.now()),
|
||||||
|
log((String) r.get("activity_log"), "CORRECTION SUBMITTED", user, "to " + correct.get("accountNo")), id, UNPASSED);
|
||||||
|
return gson.toJson(Map.of("resourceId", id, "status", CORRECTION_PENDING));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Step 4: manager approves / rejects the correction ────────────────────────
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("{id}/correction/approve")
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Transactional
|
||||||
|
public String approveCorrection(@PathParam("id") final Long id) {
|
||||||
|
final AppUser user = this.context.authenticatedUser();
|
||||||
|
validateCheckerRole(user);
|
||||||
|
final Map<String, Object> r = load(id);
|
||||||
|
requireStatus(r, CORRECTION_PENDING, "approved");
|
||||||
|
requireNotMaker(user, r.get("correction_by_id"), "correction");
|
||||||
|
|
||||||
|
// Both sides are checked again: a lot can happen between the cashier saving and the manager approving.
|
||||||
|
validateCorrectAccount(r, (String) r.get("correct_account_no"));
|
||||||
|
checkWrongAccountBalance(r);
|
||||||
|
|
||||||
|
final Long wrongId = toLong(r.get("wrong_savings_account_id"));
|
||||||
|
final Long correctId = toLong(r.get("correct_savings_account_id"));
|
||||||
|
final Long origTxnId = toLong(r.get("original_txn_id"));
|
||||||
|
final BigDecimal amount = new BigDecimal(r.get("amount").toString());
|
||||||
|
final String origVoucher = (String) r.get("original_voucher_no");
|
||||||
|
|
||||||
|
final Integer stillLive = jdbcTemplate.queryForObject(
|
||||||
|
"SELECT COUNT(*) FROM m_savings_account_transaction WHERE id = ? AND savings_account_id = ? AND is_reversed = false",
|
||||||
|
Integer.class, origTxnId, wrongId);
|
||||||
|
if (stillLive == null || stillLive == 0) {
|
||||||
|
throw apiError(409, "The original deposit has already been reversed; this voucher can no longer be corrected.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Take the deposit off the wrong account, 2. put the same cash into the right one.
|
||||||
|
final JsonObject undoPayload = new JsonObject();
|
||||||
|
voucherService.withOwnVoucher(() -> commandsSourceWritePlatformService.logCommandSource(
|
||||||
|
new CommandWrapperBuilder().undoSavingsAccountTransaction(wrongId, origTxnId).withJson(undoPayload.toString()).build()));
|
||||||
|
|
||||||
|
final String newTxnId = depositInto(correctId, amount, r);
|
||||||
|
final Long newTxnKey = Long.valueOf(newTxnId);
|
||||||
|
|
||||||
|
// 3. A new voucher for the corrected entry, in the name of the cashier who made it, with the same till side.
|
||||||
|
final String newVoucher = voucherService.createVoucher(VoucherService.TYPE_DEPOSIT, REF_VOUCHER_CORRECTION, id, amount, user);
|
||||||
|
jdbcTemplate.update("UPDATE m_voucher SET created_by = ?, created_by_username = (SELECT username FROM m_appuser WHERE id = ?) WHERE voucher_no = ?",
|
||||||
|
r.get("correction_by_id"), r.get("correction_by_id"), newVoucher);
|
||||||
|
voucherService.linkSavingsTransaction(REF_VOUCHER_CORRECTION, id, newTxnKey);
|
||||||
|
addNewVoucherLines(newVoucher, origVoucher, correctId, (String) r.get("correct_client_name"), (String) r.get("correct_account_no"),
|
||||||
|
amount);
|
||||||
|
|
||||||
|
// 4. The original voucher is not touched. That it is revoked, and by which voucher, is recorded on this
|
||||||
|
// correction (original_voucher_no / new_voucher_no) and read from there by the registers.
|
||||||
|
|
||||||
|
// 5. The notes the cashier took in follow the deposit to its new transaction and account.
|
||||||
|
jdbcTemplate.update("""
|
||||||
|
UPDATE m_savings_txn_denomination SET txn_id = ?, savings_account_id = ?, account_no = ?, client_id = ?
|
||||||
|
WHERE txn_id = ? AND savings_account_id = ?
|
||||||
|
""", newTxnKey, correctId, r.get("correct_account_no"), r.get("correct_client_id"), origTxnId, wrongId);
|
||||||
|
|
||||||
|
jdbcTemplate.update("""
|
||||||
|
UPDATE m_voucher_correction SET status = ?, correction_approved_by_id = ?, correction_approved_by_name = ?,
|
||||||
|
correction_approved_at = ?, new_voucher_no = ?, new_txn_id = ?, activity_log = ?
|
||||||
|
WHERE id = ? AND status = ?
|
||||||
|
""", COMPLETED, user.getId(), user.getDisplayName(), Timestamp.from(Instant.now()), newVoucher, newTxnKey,
|
||||||
|
log((String) r.get("activity_log"), "CORRECTION APPROVED", user, "new voucher " + newVoucher), id, CORRECTION_PENDING);
|
||||||
|
|
||||||
|
return gson.toJson(Map.of("resourceId", id, "status", COMPLETED, "newVoucherNo", newVoucher));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A rejected correction goes back to the cashier as UNPASSED, to enter the account again. */
|
||||||
|
@POST
|
||||||
|
@Path("{id}/correction/reject")
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Transactional
|
||||||
|
public String rejectCorrection(@PathParam("id") final Long id, final String body) {
|
||||||
|
final AppUser user = this.context.authenticatedUser();
|
||||||
|
validateCheckerRole(user);
|
||||||
|
final Map<String, Object> r = load(id);
|
||||||
|
requireStatus(r, CORRECTION_PENDING, "rejected");
|
||||||
|
final String reason = requireReason(body);
|
||||||
|
jdbcTemplate.update("""
|
||||||
|
UPDATE m_voucher_correction SET status = ?, rejection_reason = ?, correct_savings_account_id = NULL,
|
||||||
|
correct_account_no = NULL, correct_client_id = NULL, correct_client_name = NULL, activity_log = ?
|
||||||
|
WHERE id = ?
|
||||||
|
""", UNPASSED, reason, log((String) r.get("activity_log"), "CORRECTION REJECTED", user, reason), id);
|
||||||
|
return gson.toJson(Map.of("resourceId", id, "status", UNPASSED));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The cashier gives up the correction. Used when the wrong account no longer holds the money (the customer has
|
||||||
|
* already drawn it), so it cannot be taken back: the cashier then puts the amount into the right account from
|
||||||
|
* their own pocket as a fresh deposit, and this request is closed. No account moves here, and the original voucher
|
||||||
|
* stays as it was.
|
||||||
|
*/
|
||||||
|
@POST
|
||||||
|
@Path("{id}/close")
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Transactional
|
||||||
|
public String close(@PathParam("id") final Long id, final String body) {
|
||||||
|
final AppUser user = this.context.authenticatedUser();
|
||||||
|
if (cashierRoleHelper.getCurrentCashierId() == null) {
|
||||||
|
throw apiError(403, "Only a cashier can close a voucher correction.");
|
||||||
|
}
|
||||||
|
final Map<String, Object> r = load(id);
|
||||||
|
requireStatus(r, UNPASSED, "closed");
|
||||||
|
final String reason = requireReason(body);
|
||||||
|
final int updated = jdbcTemplate.update(
|
||||||
|
"UPDATE m_voucher_correction SET status = ?, rejection_reason = ?, activity_log = ? WHERE id = ? AND status = ?", CLOSED,
|
||||||
|
reason, log((String) r.get("activity_log"), "CLOSED BY CASHIER", user, reason), id, UNPASSED);
|
||||||
|
if (updated == 0) {
|
||||||
|
throw apiError(409, "This request was already actioned.");
|
||||||
|
}
|
||||||
|
return gson.toJson(Map.of("resourceId", id, "status", CLOSED));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── For the passbook and transaction list of the wrong account ───────────────
|
||||||
|
|
||||||
|
/** Transactions of one account that were removed by a completed correction, so its screens can leave them out. */
|
||||||
|
@GET
|
||||||
|
@Path("voided-transactions")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public String voidedTransactions(@QueryParam("savingsAccountId") final Long savingsAccountId) {
|
||||||
|
this.context.authenticatedUser();
|
||||||
|
final JsonArray ids = new JsonArray();
|
||||||
|
jdbcTemplate.queryForList(
|
||||||
|
"SELECT original_txn_id FROM m_voucher_correction WHERE status = ? AND wrong_savings_account_id = ?", Long.class, COMPLETED,
|
||||||
|
savingsAccountId).forEach(ids::add);
|
||||||
|
return gson.toJson(ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Unpassing a voucher is for the back office: an administrator or a clerk. The cashier does not unpass, only re-enters. */
|
||||||
|
private void requireUnpasser(final AppUser user) {
|
||||||
|
final boolean admin = user.hasAnyPermission("ALL_FUNCTIONS");
|
||||||
|
final boolean clerk = user.getRoles().stream().anyMatch(role -> role.getName() != null && "clerk".equals(role.getName().toLowerCase().trim()));
|
||||||
|
if (!admin && !clerk) {
|
||||||
|
throw apiError(403, "Only an administrator or a clerk can unpass a voucher.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> validateCorrectAccount(final Map<String, Object> r, final String accountNo) {
|
||||||
|
if (accountNo == null || accountNo.isBlank()) {
|
||||||
|
throw apiError(400, "The correct account number is required");
|
||||||
|
}
|
||||||
|
final Map<String, Object> correct = findSavingsAccount(accountNo);
|
||||||
|
if (correct == null) {
|
||||||
|
throw apiError(404, "No savings account found for '" + accountNo + "'.");
|
||||||
|
}
|
||||||
|
if (toLong(correct.get("id")).equals(toLong(r.get("wrong_savings_account_id")))) {
|
||||||
|
throw apiError(400, "The correct account is the same account the deposit was posted to.");
|
||||||
|
}
|
||||||
|
if (!Boolean.TRUE.equals(correct.get("canReceive"))) {
|
||||||
|
throw apiError(409, "Account " + correct.get("accountNo") + " is not active, so it cannot receive this deposit.");
|
||||||
|
}
|
||||||
|
final Object wrongCurrency = jdbcTemplate.queryForList("SELECT currency_code FROM m_savings_account WHERE id = ?", String.class,
|
||||||
|
r.get("wrong_savings_account_id")).stream().findFirst().orElse(null);
|
||||||
|
if (wrongCurrency != null && !wrongCurrency.equals(correct.get("currencyCode"))) {
|
||||||
|
throw apiError(409, "The correct account is in a different currency.");
|
||||||
|
}
|
||||||
|
kycRestrictionGuard.check(toLong(correct.get("id")), KycRestrictionGuard.Operation.CREDIT);
|
||||||
|
return correct;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The wrong account must still hold at least this deposit, or taking it back would overdraw it. */
|
||||||
|
private void checkWrongAccountBalance(final Map<String, Object> r) {
|
||||||
|
final BigDecimal balance = jdbcTemplate.queryForObject("SELECT account_balance_derived FROM m_savings_account WHERE id = ?",
|
||||||
|
BigDecimal.class, r.get("wrong_savings_account_id"));
|
||||||
|
final BigDecimal amount = new BigDecimal(r.get("amount").toString());
|
||||||
|
if (balance == null || balance.compareTo(amount) < 0) {
|
||||||
|
throw apiError(409, "Account " + r.get("wrong_account_no") + " no longer holds ₹" + amount + " (balance ₹"
|
||||||
|
+ (balance == null ? "0" : balance) + "), so the deposit cannot be taken back. The customer may have used the money.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> findSavingsAccount(final String accountNo) {
|
||||||
|
if (accountNo == null || accountNo.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final List<Map<String, Object>> rows = jdbcTemplate.queryForList("""
|
||||||
|
SELECT sa.id, sa.account_no AS "accountNo", sa.client_id AS "clientId", c.display_name AS "clientName",
|
||||||
|
sa.account_balance_derived AS "balance", sa.currency_code AS "currencyCode",
|
||||||
|
(sa.status_enum = ? AND sa.deposit_type_enum = ?) AS "canReceive"
|
||||||
|
FROM m_savings_account sa LEFT JOIN m_client c ON c.id = sa.client_id
|
||||||
|
WHERE sa.account_no = ?
|
||||||
|
""", SAVINGS_STATUS_ACTIVE, DEPOSIT_TYPE_SAVINGS, accountNo.trim());
|
||||||
|
return rows.isEmpty() ? null : rows.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String depositInto(final Long savingsAccountId, final BigDecimal amount, final Map<String, Object> r) {
|
||||||
|
final Object txnDate = r.get("txn_date");
|
||||||
|
final String date = txnDate == null ? DateUtils.getBusinessLocalDate().format(DateTimeFormatter.ofPattern("dd MMMM yyyy"))
|
||||||
|
: LocalDate.parse(txnDate.toString().substring(0, 10)).format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||||
|
final JsonObject payload = new JsonObject();
|
||||||
|
payload.addProperty("locale", "en");
|
||||||
|
payload.addProperty("dateFormat", "dd MMMM yyyy");
|
||||||
|
payload.addProperty("transactionDate", date);
|
||||||
|
payload.addProperty("transactionAmount", amount);
|
||||||
|
if (r.get("payment_type_id") != null) {
|
||||||
|
payload.addProperty("paymentTypeId", ((Number) r.get("payment_type_id")).longValue());
|
||||||
|
}
|
||||||
|
payload.addProperty("note", "Correction of voucher " + r.get("original_voucher_no"));
|
||||||
|
final CommandWrapper command = new CommandWrapperBuilder().savingsAccountDeposit(savingsAccountId).withJson(payload.toString())
|
||||||
|
.build();
|
||||||
|
final CommandProcessingResult result = voucherService.withOwnVoucher(() -> commandsSourceWritePlatformService.logCommandSource(command));
|
||||||
|
if (result.getResourceId() == null) {
|
||||||
|
throw apiError(500, "The corrected deposit could not be posted.");
|
||||||
|
}
|
||||||
|
return result.getResourceId().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The new voucher keeps the original's till side and names the right account on the credit side. */
|
||||||
|
private void addNewVoucherLines(final String newVoucher, final String origVoucher, final Long correctId, final String clientName,
|
||||||
|
final String accountNo, final BigDecimal amount) {
|
||||||
|
try {
|
||||||
|
final List<VoucherLine> lines = new ArrayList<>();
|
||||||
|
for (final Map<String, Object> l : jdbcTemplate.queryForList(
|
||||||
|
"SELECT gl_account_id, party_name, account_no FROM m_voucher_line WHERE voucher_no = ? AND entry_type = 'DR' ORDER BY line_no",
|
||||||
|
origVoucher)) {
|
||||||
|
lines.add(VoucherLine.debitGl(toLong(l.get("gl_account_id")), (String) l.get("party_name"), (String) l.get("account_no"),
|
||||||
|
amount));
|
||||||
|
}
|
||||||
|
if (lines.isEmpty()) {
|
||||||
|
lines.add(VoucherLine.debitGl(null, "Cash in Hand", null, amount));
|
||||||
|
}
|
||||||
|
lines.add(VoucherLine.creditSavings(correctId, clientName, accountNo, amount));
|
||||||
|
voucherService.addLines(newVoucher, lines);
|
||||||
|
} catch (final RuntimeException e) {
|
||||||
|
// The deposit is already posted; a voucher without its sides is better than undoing a correct posting.
|
||||||
|
org.slf4j.LoggerFactory.getLogger(VoucherCorrectionApiResource.class)
|
||||||
|
.error("Could not record the sides of correction voucher {}", newVoucher, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> load(final Long id) {
|
||||||
|
final List<Map<String, Object>> rows = jdbcTemplate.queryForList("SELECT * FROM m_voucher_correction WHERE id = ?", id);
|
||||||
|
if (rows.isEmpty()) {
|
||||||
|
throw new NotFoundException("Voucher correction not found: " + id);
|
||||||
|
}
|
||||||
|
return rows.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requireStatus(final Map<String, Object> r, final String expected, final String action) {
|
||||||
|
if (!expected.equals(r.get("status"))) {
|
||||||
|
throw apiError(409, "Only " + expected + " requests can be " + action + ". Current status: " + r.get("status"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requireNotMaker(final AppUser user, final Object makerId, final String what) {
|
||||||
|
if (makerId != null && toLong(makerId).equals(user.getId())) {
|
||||||
|
throw apiError(403, "Maker-checker: you cannot approve a " + what + " you raised yourself. Ask another manager.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String requireReason(final String body) {
|
||||||
|
final String reason = fromJsonHelper.extractStringNamed("reason", fromJsonHelper.parse(body != null && !body.isBlank() ? body : "{}"));
|
||||||
|
if (reason == null || reason.isBlank()) {
|
||||||
|
throw apiError(400, "Reason is required");
|
||||||
|
}
|
||||||
|
return reason.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Checker authorization, the same rule the withdrawal and deposit request registers use. */
|
||||||
|
private void validateCheckerRole(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 apiError(403, "Only a Bank Manager (or Head Cashier) can approve or reject voucher corrections.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Long toLong(final Object value) {
|
||||||
|
return value instanceof Number n ? n.longValue() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String log(final String existing, final String action, final AppUser user, final String note) {
|
||||||
|
final JsonArray entries = existing == null || existing.isBlank() ? new JsonArray() : gson.fromJson(existing, JsonArray.class);
|
||||||
|
final JsonObject e = new JsonObject();
|
||||||
|
e.addProperty("action", action);
|
||||||
|
e.addProperty("by", user.getUsername());
|
||||||
|
e.addProperty("at", Instant.now().toString());
|
||||||
|
if (note != null) {
|
||||||
|
e.addProperty("note", note);
|
||||||
|
}
|
||||||
|
entries.add(e);
|
||||||
|
return gson.toJson(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
private WebApplicationException apiError(final int httpStatus, final String message) {
|
||||||
|
final JsonObject body = new JsonObject();
|
||||||
|
body.addProperty("message", message);
|
||||||
|
body.addProperty("defaultUserMessage", message);
|
||||||
|
body.addProperty("httpStatus", httpStatus);
|
||||||
|
final JsonArray errors = new JsonArray();
|
||||||
|
final JsonObject err = new JsonObject();
|
||||||
|
err.addProperty("defaultUserMessage", message);
|
||||||
|
errors.add(err);
|
||||||
|
body.add("errors", errors);
|
||||||
|
return new WebApplicationException(Response.status(httpStatus).entity(gson.toJson(body)).type(MediaType.APPLICATION_JSON).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
+24
-2
@@ -628,9 +628,15 @@ public class VoucherVerificationApiResource {
|
|||||||
|
|
||||||
BigDecimal total = BigDecimal.ZERO;
|
BigDecimal total = BigDecimal.ZERO;
|
||||||
final List<Map<String, Object>> out = new java.util.ArrayList<>();
|
final List<Map<String, Object>> out = new java.util.ArrayList<>();
|
||||||
|
final Map<String, Map<String, Object>> marks = voucherService
|
||||||
|
.voucherMarks(rows.stream().map(x -> (String) x.get("voucher_no")).toList());
|
||||||
for (final Map<String, Object> r : rows) {
|
for (final Map<String, Object> r : rows) {
|
||||||
final BigDecimal amount = (BigDecimal) r.get("amount");
|
final BigDecimal amount = (BigDecimal) r.get("amount");
|
||||||
if (amount != null) {
|
final Map<String, Object> mark = marks.get((String) r.get("voucher_no"));
|
||||||
|
final boolean revoked = mark != null && "REVOKED".equals(mark.get("voucherStatus"));
|
||||||
|
// A revoked voucher stays on the register, marked, but its money is on the voucher that replaced it, so it
|
||||||
|
// is left out of the total rather than counted twice.
|
||||||
|
if (amount != null && !revoked) {
|
||||||
total = total.add(amount);
|
total = total.add(amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -638,6 +644,7 @@ public class VoucherVerificationApiResource {
|
|||||||
m.put("voucherNo", r.get("voucher_no"));
|
m.put("voucherNo", r.get("voucher_no"));
|
||||||
m.put("voucherType", r.get("voucher_type"));
|
m.put("voucherType", r.get("voucher_type"));
|
||||||
m.put("amount", amount);
|
m.put("amount", amount);
|
||||||
|
putVoucherMark(m, mark);
|
||||||
m.put("createdAt", r.get("created_at") == null ? null : r.get("created_at").toString());
|
m.put("createdAt", r.get("created_at") == null ? null : r.get("created_at").toString());
|
||||||
// The day the posting belongs to, and whether that differs from the day it was written.
|
// The day the posting belongs to, and whether that differs from the day it was written.
|
||||||
// A nightly run that finishes after midnight writes tomorrow's voucher for yesterday's
|
// A nightly run that finishes after midnight writes tomorrow's voucher for yesterday's
|
||||||
@@ -847,6 +854,8 @@ public class VoucherVerificationApiResource {
|
|||||||
int raisedCount = 0;
|
int raisedCount = 0;
|
||||||
int verifiedCount = 0;
|
int verifiedCount = 0;
|
||||||
final List<Map<String, Object>> out = new java.util.ArrayList<>();
|
final List<Map<String, Object>> out = new java.util.ArrayList<>();
|
||||||
|
final Map<String, Map<String, Object>> myWorkMarks = voucherService
|
||||||
|
.voucherMarks(rows.stream().map(x -> (String) x.get("voucher_no")).toList());
|
||||||
|
|
||||||
for (final Map<String, Object> r : rows) {
|
for (final Map<String, Object> r : rows) {
|
||||||
final BigDecimal amount = (BigDecimal) r.get("amount");
|
final BigDecimal amount = (BigDecimal) r.get("amount");
|
||||||
@@ -869,7 +878,10 @@ public class VoucherVerificationApiResource {
|
|||||||
final String side = ("CR".equals(rawSide) || "DR".equals(rawSide)) && lineCount > 0 && !tillTouched
|
final String side = ("CR".equals(rawSide) || "DR".equals(rawSide)) && lineCount > 0 && !tillTouched
|
||||||
? "JV" : rawSide;
|
? "JV" : rawSide;
|
||||||
|
|
||||||
if (amount != null) {
|
final Map<String, Object> mark = myWorkMarks.get(voucherNo);
|
||||||
|
final boolean revoked = mark != null && "REVOKED".equals(mark.get("voucherStatus"));
|
||||||
|
// A revoked voucher is shown, marked, but its money is on the voucher that replaced it, so it is not counted.
|
||||||
|
if (amount != null && !revoked) {
|
||||||
if ("CR".equals(side)) {
|
if ("CR".equals(side)) {
|
||||||
receivedTotal = receivedTotal.add(amount);
|
receivedTotal = receivedTotal.add(amount);
|
||||||
} else if ("DR".equals(side)) {
|
} else if ("DR".equals(side)) {
|
||||||
@@ -897,6 +909,7 @@ public class VoucherVerificationApiResource {
|
|||||||
m.put("raisedBy", r.get("created_by_username"));
|
m.put("raisedBy", r.get("created_by_username"));
|
||||||
m.put("branchName", r.get("branch_name"));
|
m.put("branchName", r.get("branch_name"));
|
||||||
m.put("verificationStatus", r.get("verification_status"));
|
m.put("verificationStatus", r.get("verification_status"));
|
||||||
|
putVoucherMark(m, mark);
|
||||||
m.put("verifiedByName", r.get("verified_by_name"));
|
m.put("verifiedByName", r.get("verified_by_name"));
|
||||||
m.put("verifiedAt", r.get("verified_at") == null ? null : r.get("verified_at").toString());
|
m.put("verifiedAt", r.get("verified_at") == null ? null : r.get("verified_at").toString());
|
||||||
m.put("debitParty", r.get("debit_party"));
|
m.put("debitParty", r.get("debit_party"));
|
||||||
@@ -968,6 +981,14 @@ public class VoucherVerificationApiResource {
|
|||||||
* returns an empty list rather than an error: the voucher is real, its posting simply was not
|
* returns an empty list rather than an error: the voucher is real, its posting simply was not
|
||||||
* captured at the time, and reporting it as missing would be wrong.
|
* captured at the time, and reporting it as missing would be wrong.
|
||||||
*/
|
*/
|
||||||
|
/** Adds where the voucher stands (active, revoked, under correction, corrected) to a register row. */
|
||||||
|
private void putVoucherMark(final Map<String, Object> row, final Map<String, Object> mark) {
|
||||||
|
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"));
|
||||||
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("detail")
|
@Path("detail")
|
||||||
public String detail(@QueryParam("voucherNo") final String voucherNo) {
|
public String detail(@QueryParam("voucherNo") final String voucherNo) {
|
||||||
@@ -1032,6 +1053,7 @@ public class VoucherVerificationApiResource {
|
|||||||
out.put("createdBy", h.get("created_by_username"));
|
out.put("createdBy", h.get("created_by_username"));
|
||||||
out.put("createdAt", h.get("created_at") == null ? null : h.get("created_at").toString());
|
out.put("createdAt", h.get("created_at") == null ? null : h.get("created_at").toString());
|
||||||
out.put("verificationStatus", h.get("verification_status"));
|
out.put("verificationStatus", h.get("verification_status"));
|
||||||
|
putVoucherMark(out, voucherService.voucherMarks(List.of(voucherNo)).get(voucherNo));
|
||||||
out.put("lines", lines);
|
out.put("lines", lines);
|
||||||
out.put("totalDebit", totalDebit);
|
out.put("totalDebit", totalDebit);
|
||||||
out.put("totalCredit", totalCredit);
|
out.put("totalCredit", totalCredit);
|
||||||
|
|||||||
+60
@@ -595,6 +595,66 @@ public class VoucherService {
|
|||||||
""", voucherNo);
|
""", voucherNo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Where each of these vouchers stands, for the registers that list vouchers.
|
||||||
|
*
|
||||||
|
* A voucher is ACTIVE until a correction touches it. REVOKED means its deposit was moved to another account and a
|
||||||
|
* new voucher replaced it (revokedByVoucherNo). A voucher with a correction under way reads UNPASS_PENDING,
|
||||||
|
* UNPASSED or CORRECTION_PENDING. The replacement itself reads CORRECTED, with the voucher it replaced in
|
||||||
|
* replacesVoucherNo. A register adds these to its rows and leaves a REVOKED voucher out of its totals, since its
|
||||||
|
* money now sits on the replacement.
|
||||||
|
*/
|
||||||
|
public Map<String, Map<String, Object>> voucherMarks(final java.util.Collection<String> voucherNos) {
|
||||||
|
final Map<String, Map<String, Object>> marks = new java.util.HashMap<>();
|
||||||
|
final List<String> nos = voucherNos == null ? List.of()
|
||||||
|
: voucherNos.stream().filter(n -> n != null && !n.isBlank()).distinct().toList();
|
||||||
|
if (nos.isEmpty()) {
|
||||||
|
return marks;
|
||||||
|
}
|
||||||
|
final String in = String.join(",", java.util.Collections.nCopies(nos.size(), "?"));
|
||||||
|
// Read from the correction register alone: the voucher itself carries no status, so nothing about a correction
|
||||||
|
// is ever written onto m_voucher and the voucher rows stay exactly as they were raised.
|
||||||
|
final List<Map<String, Object>> rows = jdbcTemplate.queryForList(
|
||||||
|
"SELECT v.voucher_no,"
|
||||||
|
+ " (SELECT vc.new_voucher_no FROM m_voucher_correction vc"
|
||||||
|
+ " WHERE vc.original_voucher_no = v.voucher_no AND vc.status = 'COMPLETED'"
|
||||||
|
+ " ORDER BY vc.id DESC LIMIT 1) AS revoked_by_voucher_no,"
|
||||||
|
+ " (SELECT vc.original_voucher_no FROM m_voucher_correction vc"
|
||||||
|
+ " WHERE vc.new_voucher_no = v.voucher_no AND vc.status = 'COMPLETED'"
|
||||||
|
+ " ORDER BY vc.id DESC LIMIT 1) AS replaces_voucher_no,"
|
||||||
|
+ " (SELECT vc.status FROM m_voucher_correction vc"
|
||||||
|
+ " WHERE vc.original_voucher_no = v.voucher_no"
|
||||||
|
+ " AND vc.status IN ('UNPASS_PENDING', 'UNPASSED', 'CORRECTION_PENDING', 'CLOSED')"
|
||||||
|
+ " ORDER BY vc.id DESC LIMIT 1) AS correction_status,"
|
||||||
|
// The reason behind it: why a closed request was closed, otherwise why the voucher was unpassed.
|
||||||
|
+ " (SELECT CASE WHEN vc.status = 'CLOSED' THEN vc.rejection_reason ELSE vc.reason END"
|
||||||
|
+ " FROM m_voucher_correction vc"
|
||||||
|
+ " WHERE vc.original_voucher_no = v.voucher_no"
|
||||||
|
+ " AND vc.status IN ('UNPASS_PENDING', 'UNPASSED', 'CORRECTION_PENDING', 'CLOSED', 'COMPLETED')"
|
||||||
|
+ " ORDER BY vc.id DESC LIMIT 1) AS correction_note"
|
||||||
|
+ " FROM m_voucher v WHERE v.voucher_no IN (" + in + ")",
|
||||||
|
nos.toArray());
|
||||||
|
for (final Map<String, Object> r : rows) {
|
||||||
|
final String status;
|
||||||
|
if (r.get("revoked_by_voucher_no") != null) {
|
||||||
|
status = "REVOKED";
|
||||||
|
} else if (r.get("correction_status") != null) {
|
||||||
|
status = (String) r.get("correction_status");
|
||||||
|
} else if (r.get("replaces_voucher_no") != null) {
|
||||||
|
status = "CORRECTED";
|
||||||
|
} else {
|
||||||
|
status = "ACTIVE";
|
||||||
|
}
|
||||||
|
final Map<String, Object> m = new java.util.LinkedHashMap<>();
|
||||||
|
m.put("voucherStatus", status);
|
||||||
|
m.put("revokedByVoucherNo", r.get("revoked_by_voucher_no"));
|
||||||
|
m.put("replacesVoucherNo", r.get("replaces_voucher_no"));
|
||||||
|
m.put("correctionNote", r.get("correction_note"));
|
||||||
|
marks.put((String) r.get("voucher_no"), m);
|
||||||
|
}
|
||||||
|
return marks;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Looks up the voucher for the given request and returns its voucher_no,
|
* Looks up the voucher for the given request and returns its voucher_no,
|
||||||
* or null if none exists (e.g. records created before vouchers existed).
|
* or null if none exists (e.g. records created before vouchers existed).
|
||||||
|
|||||||
@@ -174,13 +174,8 @@
|
|||||||
<include file="parts/0598_recurring_deposit_extend_tenure_permission.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0598_recurring_deposit_extend_tenure_permission.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0593_fd_interest_posting_approval.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0593_fd_interest_posting_approval.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0599_cash_exchange_customer_pan.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0599_cash_exchange_customer_pan.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0593_investment_masters.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0600_clerk_withdrawal_request_gl.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0594_investment_purchase.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0601_voucher_correction.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0595_investment_purchase_other_charges.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0602_voucher_drop_status_columns.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0596_investment_daily_rate.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0603_cash_transfer_request.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0597_investment_purchase_settlement.xml" relativeToChangelogFile="true"/>
|
|
||||||
<include file="parts/0600_investment_interest_posting.xml" relativeToChangelogFile="true"/>
|
|
||||||
<include file="parts/0601_investment_sale_redemption.xml" relativeToChangelogFile="true"/>
|
|
||||||
<include file="parts/0602_investment_security_transfer.xml" relativeToChangelogFile="true"/>
|
|
||||||
<include file="parts/0603_investment_premium_amortisation.xml" relativeToChangelogFile="true"/>
|
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|||||||
-216
@@ -1,216 +0,0 @@
|
|||||||
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!--
|
|
||||||
|
|
||||||
Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
or more contributor license agreements. See the NOTICE file
|
|
||||||
distributed with this work for additional information
|
|
||||||
regarding copyright ownership. The ASF licenses this file
|
|
||||||
to you under the Apache License, Version 2.0 (the
|
|
||||||
"License"); you may not use this file except in compliance
|
|
||||||
with the License. You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing,
|
|
||||||
software distributed under the License is distributed on an
|
|
||||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
KIND, either express or implied. See the License for the
|
|
||||||
specific language governing permissions and limitations
|
|
||||||
under the License.
|
|
||||||
|
|
||||||
-->
|
|
||||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.9.xsd">
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Treasury Investment masters, following the bank's existing CBS screens:
|
|
||||||
- m_investment_party : Bank / Party Master (whom an investment is bought from / placed with)
|
|
||||||
- m_investment_script : Script Master (what is bought - Govt Security, Bond, Mutual Fund, ...)
|
|
||||||
Party and script codes are generated by the application (PTY-001, G00001). Both rows are
|
|
||||||
tied to the investment (asset) GL they are booked to.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<changeSet author="cbs-anshul" id="0593-1-create-investment-party">
|
|
||||||
<preConditions onFail="MARK_RAN">
|
|
||||||
<not><tableExists tableName="m_investment_party"/></not>
|
|
||||||
</preConditions>
|
|
||||||
<createTable tableName="m_investment_party">
|
|
||||||
<column name="id" type="BIGINT" autoIncrement="true"><constraints nullable="false" primaryKey="true"/></column>
|
|
||||||
<column name="party_code" type="VARCHAR(20)"><constraints nullable="false"/></column>
|
|
||||||
<column name="bond_type" type="VARCHAR(20)"><constraints nullable="false"/></column>
|
|
||||||
<column name="party_name" type="VARCHAR(150)"><constraints nullable="false"/></column>
|
|
||||||
<column name="address1" type="VARCHAR(150)"/>
|
|
||||||
<column name="address2" type="VARCHAR(150)"/>
|
|
||||||
<column name="address3" type="VARCHAR(150)"/>
|
|
||||||
<column name="city" type="VARCHAR(100)"/>
|
|
||||||
<column name="pincode" type="VARCHAR(10)"/>
|
|
||||||
<column name="phone" type="VARCHAR(20)"/>
|
|
||||||
<column name="fax" type="VARCHAR(20)"/>
|
|
||||||
<column name="bank_type" type="VARCHAR(60)"/>
|
|
||||||
<column name="gl_account_id" type="BIGINT"><constraints nullable="false"/></column>
|
|
||||||
<column name="rbi_code" type="VARCHAR(30)"/>
|
|
||||||
<column name="exposure_limit" type="DECIMAL(19,6)"/>
|
|
||||||
<column name="lei_no" type="VARCHAR(30)"/>
|
|
||||||
<column name="cp_bank_name" type="VARCHAR(150)"/>
|
|
||||||
<column name="cp_branch_name" type="VARCHAR(150)"/>
|
|
||||||
<column name="cp_account_no" type="VARCHAR(40)"/>
|
|
||||||
<column name="cp_ifsc" type="VARCHAR(11)"/>
|
|
||||||
<column name="is_active" type="BOOLEAN" defaultValueBoolean="true"><constraints nullable="false"/></column>
|
|
||||||
<column name="created_by" type="BIGINT"><constraints nullable="false"/></column>
|
|
||||||
<column name="created_on_utc" type="DATETIME"><constraints nullable="false"/></column>
|
|
||||||
<column name="last_modified_by" type="BIGINT"><constraints nullable="false"/></column>
|
|
||||||
<column name="last_modified_on_utc" type="DATETIME"><constraints nullable="false"/></column>
|
|
||||||
</createTable>
|
|
||||||
<addUniqueConstraint tableName="m_investment_party" columnNames="party_code" constraintName="uq_investment_party_code"/>
|
|
||||||
<addForeignKeyConstraint baseTableName="m_investment_party" baseColumnNames="gl_account_id" constraintName="fk_investment_party_gl_account"
|
|
||||||
referencedTableName="acc_gl_account" referencedColumnNames="id"/>
|
|
||||||
<createIndex tableName="m_investment_party" indexName="idx_investment_party_bond_type">
|
|
||||||
<column name="bond_type"/>
|
|
||||||
</createIndex>
|
|
||||||
</changeSet>
|
|
||||||
|
|
||||||
<changeSet author="cbs-anshul" id="0593-2-create-investment-script">
|
|
||||||
<preConditions onFail="MARK_RAN">
|
|
||||||
<not><tableExists tableName="m_investment_script"/></not>
|
|
||||||
</preConditions>
|
|
||||||
<createTable tableName="m_investment_script">
|
|
||||||
<column name="id" type="BIGINT" autoIncrement="true"><constraints nullable="false" primaryKey="true"/></column>
|
|
||||||
<column name="script_code" type="VARCHAR(20)"><constraints nullable="false"/></column>
|
|
||||||
<column name="bond_type" type="VARCHAR(20)"><constraints nullable="false"/></column>
|
|
||||||
<column name="slr_type" type="VARCHAR(20)"/>
|
|
||||||
<column name="script_desc" type="VARCHAR(150)"><constraints nullable="false"/></column>
|
|
||||||
<column name="gl_account_id" type="BIGINT"><constraints nullable="false"/></column>
|
|
||||||
<column name="isin" type="VARCHAR(20)"/>
|
|
||||||
<column name="face_value_per_unit" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
|
|
||||||
<column name="int_rate" type="DECIMAL(9,4)"/>
|
|
||||||
<column name="issue_date" type="DATE"/>
|
|
||||||
<column name="maturity_date" type="DATE"/>
|
|
||||||
<column name="company_name" type="VARCHAR(150)"/>
|
|
||||||
<column name="address1" type="VARCHAR(150)"/>
|
|
||||||
<column name="address2" type="VARCHAR(150)"/>
|
|
||||||
<column name="address3" type="VARCHAR(150)"/>
|
|
||||||
<column name="city" type="VARCHAR(100)"/>
|
|
||||||
<column name="pincode" type="VARCHAR(10)"/>
|
|
||||||
<column name="phone" type="VARCHAR(20)"/>
|
|
||||||
<column name="fax" type="VARCHAR(20)"/>
|
|
||||||
<column name="corp_code" type="VARCHAR(30)"/>
|
|
||||||
<column name="is_active" type="BOOLEAN" defaultValueBoolean="true"><constraints nullable="false"/></column>
|
|
||||||
<column name="created_by" type="BIGINT"><constraints nullable="false"/></column>
|
|
||||||
<column name="created_on_utc" type="DATETIME"><constraints nullable="false"/></column>
|
|
||||||
<column name="last_modified_by" type="BIGINT"><constraints nullable="false"/></column>
|
|
||||||
<column name="last_modified_on_utc" type="DATETIME"><constraints nullable="false"/></column>
|
|
||||||
</createTable>
|
|
||||||
<addUniqueConstraint tableName="m_investment_script" columnNames="script_code" constraintName="uq_investment_script_code"/>
|
|
||||||
<addForeignKeyConstraint baseTableName="m_investment_script" baseColumnNames="gl_account_id" constraintName="fk_investment_script_gl_account"
|
|
||||||
referencedTableName="acc_gl_account" referencedColumnNames="id"/>
|
|
||||||
<createIndex tableName="m_investment_script" indexName="idx_investment_script_bond_type">
|
|
||||||
<column name="bond_type"/>
|
|
||||||
</createIndex>
|
|
||||||
</changeSet>
|
|
||||||
|
|
||||||
<changeSet author="cbs-anshul" id="0593-3-investment-master-permissions">
|
|
||||||
<preConditions onFail="MARK_RAN">
|
|
||||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code IN ('READ_INVESTMENTPARTY', 'CREATE_INVESTMENTPARTY', 'UPDATE_INVESTMENTPARTY', 'DELETE_INVESTMENTPARTY', 'READ_INVESTMENTSCRIPT', 'CREATE_INVESTMENTSCRIPT', 'UPDATE_INVESTMENTSCRIPT', 'CREATE_INVESTMENTPARTY_CHECKER', 'UPDATE_INVESTMENTPARTY_CHECKER', 'DELETE_INVESTMENTPARTY_CHECKER', 'CREATE_INVESTMENTSCRIPT_CHECKER', 'UPDATE_INVESTMENTSCRIPT_CHECKER')</sqlCheck>
|
|
||||||
</preConditions>
|
|
||||||
<insert tableName="m_permission">
|
|
||||||
<column name="grouping" value="portfolio"/>
|
|
||||||
<column name="code" value="READ_INVESTMENTPARTY"/>
|
|
||||||
<column name="entity_name" value="INVESTMENTPARTY"/>
|
|
||||||
<column name="action_name" value="READ"/>
|
|
||||||
<column name="can_maker_checker" valueBoolean="false"/>
|
|
||||||
</insert>
|
|
||||||
<insert tableName="m_permission">
|
|
||||||
<column name="grouping" value="portfolio"/>
|
|
||||||
<column name="code" value="CREATE_INVESTMENTPARTY"/>
|
|
||||||
<column name="entity_name" value="INVESTMENTPARTY"/>
|
|
||||||
<column name="action_name" value="CREATE"/>
|
|
||||||
<column name="can_maker_checker" valueBoolean="false"/>
|
|
||||||
</insert>
|
|
||||||
<insert tableName="m_permission">
|
|
||||||
<column name="grouping" value="portfolio"/>
|
|
||||||
<column name="code" value="UPDATE_INVESTMENTPARTY"/>
|
|
||||||
<column name="entity_name" value="INVESTMENTPARTY"/>
|
|
||||||
<column name="action_name" value="UPDATE"/>
|
|
||||||
<column name="can_maker_checker" valueBoolean="false"/>
|
|
||||||
</insert>
|
|
||||||
<insert tableName="m_permission">
|
|
||||||
<column name="grouping" value="portfolio"/>
|
|
||||||
<column name="code" value="DELETE_INVESTMENTPARTY"/>
|
|
||||||
<column name="entity_name" value="INVESTMENTPARTY"/>
|
|
||||||
<column name="action_name" value="DELETE"/>
|
|
||||||
<column name="can_maker_checker" valueBoolean="false"/>
|
|
||||||
</insert>
|
|
||||||
<insert tableName="m_permission">
|
|
||||||
<column name="grouping" value="portfolio"/>
|
|
||||||
<column name="code" value="READ_INVESTMENTSCRIPT"/>
|
|
||||||
<column name="entity_name" value="INVESTMENTSCRIPT"/>
|
|
||||||
<column name="action_name" value="READ"/>
|
|
||||||
<column name="can_maker_checker" valueBoolean="false"/>
|
|
||||||
</insert>
|
|
||||||
<insert tableName="m_permission">
|
|
||||||
<column name="grouping" value="portfolio"/>
|
|
||||||
<column name="code" value="CREATE_INVESTMENTSCRIPT"/>
|
|
||||||
<column name="entity_name" value="INVESTMENTSCRIPT"/>
|
|
||||||
<column name="action_name" value="CREATE"/>
|
|
||||||
<column name="can_maker_checker" valueBoolean="false"/>
|
|
||||||
</insert>
|
|
||||||
<insert tableName="m_permission">
|
|
||||||
<column name="grouping" value="portfolio"/>
|
|
||||||
<column name="code" value="UPDATE_INVESTMENTSCRIPT"/>
|
|
||||||
<column name="entity_name" value="INVESTMENTSCRIPT"/>
|
|
||||||
<column name="action_name" value="UPDATE"/>
|
|
||||||
<column name="can_maker_checker" valueBoolean="false"/>
|
|
||||||
</insert>
|
|
||||||
<insert tableName="m_permission">
|
|
||||||
<column name="grouping" value="portfolio"/>
|
|
||||||
<column name="code" value="CREATE_INVESTMENTPARTY_CHECKER"/>
|
|
||||||
<column name="entity_name" value="INVESTMENTPARTY"/>
|
|
||||||
<column name="action_name" value="CREATE_CHECKER"/>
|
|
||||||
<column name="can_maker_checker" valueBoolean="false"/>
|
|
||||||
</insert>
|
|
||||||
<insert tableName="m_permission">
|
|
||||||
<column name="grouping" value="portfolio"/>
|
|
||||||
<column name="code" value="UPDATE_INVESTMENTPARTY_CHECKER"/>
|
|
||||||
<column name="entity_name" value="INVESTMENTPARTY"/>
|
|
||||||
<column name="action_name" value="UPDATE_CHECKER"/>
|
|
||||||
<column name="can_maker_checker" valueBoolean="false"/>
|
|
||||||
</insert>
|
|
||||||
<insert tableName="m_permission">
|
|
||||||
<column name="grouping" value="portfolio"/>
|
|
||||||
<column name="code" value="DELETE_INVESTMENTPARTY_CHECKER"/>
|
|
||||||
<column name="entity_name" value="INVESTMENTPARTY"/>
|
|
||||||
<column name="action_name" value="DELETE_CHECKER"/>
|
|
||||||
<column name="can_maker_checker" valueBoolean="false"/>
|
|
||||||
</insert>
|
|
||||||
<insert tableName="m_permission">
|
|
||||||
<column name="grouping" value="portfolio"/>
|
|
||||||
<column name="code" value="CREATE_INVESTMENTSCRIPT_CHECKER"/>
|
|
||||||
<column name="entity_name" value="INVESTMENTSCRIPT"/>
|
|
||||||
<column name="action_name" value="CREATE_CHECKER"/>
|
|
||||||
<column name="can_maker_checker" valueBoolean="false"/>
|
|
||||||
</insert>
|
|
||||||
<insert tableName="m_permission">
|
|
||||||
<column name="grouping" value="portfolio"/>
|
|
||||||
<column name="code" value="UPDATE_INVESTMENTSCRIPT_CHECKER"/>
|
|
||||||
<column name="entity_name" value="INVESTMENTSCRIPT"/>
|
|
||||||
<column name="action_name" value="UPDATE_CHECKER"/>
|
|
||||||
<column name="can_maker_checker" valueBoolean="false"/>
|
|
||||||
</insert>
|
|
||||||
</changeSet>
|
|
||||||
|
|
||||||
<!-- Whoever already sees the Investment menu gets to read and maintain its masters. Maker-checker
|
|
||||||
is left off (can_maker_checker=false); it can be switched on per action from
|
|
||||||
Configure Maker Checker Tasks, and the _CHECKER rows above let a checker role approve. -->
|
|
||||||
<changeSet author="cbs-anshul" id="0593-4-grant-investment-master-permissions">
|
|
||||||
<sql>
|
|
||||||
INSERT INTO m_role_permission (role_id, permission_id)
|
|
||||||
SELECT rp.role_id, p.id
|
|
||||||
FROM m_role_permission rp
|
|
||||||
JOIN m_permission menu ON menu.id = rp.permission_id AND menu.code = 'MENU_INVESTMENT'
|
|
||||||
CROSS JOIN m_permission p
|
|
||||||
WHERE p.code IN ('READ_INVESTMENTPARTY', 'CREATE_INVESTMENTPARTY', 'UPDATE_INVESTMENTPARTY', 'DELETE_INVESTMENTPARTY', 'READ_INVESTMENTSCRIPT', 'CREATE_INVESTMENTSCRIPT', 'UPDATE_INVESTMENTSCRIPT')
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM m_role_permission x WHERE x.role_id = rp.role_id AND x.permission_id = p.id)
|
|
||||||
</sql>
|
|
||||||
</changeSet>
|
|
||||||
</databaseChangeLog>
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user