Compare commits

..
Author SHA1 Message Date
niteshandClaude Sonnet 5.5 6be32272c1 feat(cashier): maker-checker on cash transfer
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Waiting to run
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Waiting to run
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Waiting to run
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Waiting to run
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Waiting to run
Fineract Documentation build / build (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Waiting to run
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Waiting to run
Fineract Liquibase Only mode - PostgreSQL / test (push) Waiting to run
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Waiting to run
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Waiting to run
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Waiting to run
Cash no longer leaves the vault on one person's say-so. The allocation to a cashier is raised as a request and
posted only when a different manager or head cashier approves it.

- POST /cashtransfer/requests raises the request after every vault check (total and note by note); the same
  checks run again at approval, since the vault can change in between.
- Approve posts the transfer exactly as the direct one did and records it against the maker; reject needs a
  reason; the maker cannot approve their own request.
- An identical request from the same maker is refused while the first is still pending.
- The direct POST /cashtransfer is closed so the approval cannot be bypassed.
- Migration 0603 adds m_cash_transfer_request; m_cash_transfer and every position query are unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 12:27:22 +05:30
niteshandClaude Sonnet 5.5 b47a34b5ef feat(vouchers): voucher correction for cash deposits posted to the wrong account
A cash deposit posted to the wrong savings account is corrected without ever undoing it behind anyone's back:
the voucher is unpassed (admin or clerk), a manager approves, the cashier enters the correct account, and a
manager approves again. Only the final approval moves money: the deposit is reversed on the wrong account, the
same cash is deposited into the right one under a new voucher, and the cashier's denominations follow it.

- VoucherCorrectionApiResource: eligible list, unpass, approve/reject unpass, account lookup, correction,
  approve/reject correction, close request, voided transactions.
- The wrong account's balance is checked when the cashier enters the correction and again at approval.
- A cashier whose customer has already used the money closes the request and deposits separately.
- Roles are enforced on the server: unpass for admin/clerk, correction and close for a cashier, approvals for
  a manager or head cashier other than the maker.
- Migration 0601 adds m_voucher_correction; 0602 drops the status columns an earlier draft put on m_voucher,
  since the correction status is read from m_voucher_correction alone.
- Voucher registers (register, My Vouchers, day book, voucher detail) report each voucher's correction status
  and leave a revoked voucher out of their totals, so its cash is not counted twice.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 12:27:21 +05:30
niteshandClaude Sonnet 5.5 04d1eb20f0 feat(clerk-withdrawal): GL withdrawal slips through request, approval and cashier payment
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
A GL withdrawal slip (e.g. Sundry Creditors paid out in cash) now follows the same
maker-checker path as a savings withdrawal:
- the request carries gl_account_id, contra_gl_id, office_id and currency_code
  (new nullable columns, changeset 0600) instead of a savings account;
- at submit it is validated: the paying GL must be a mapped cash vault GL and the
  slip's own GL must not be a cash GL;
- at manager approval it posts Dr the GL / Cr the cash vault GL as a journal entry;
- the cashier pays it against its token, and the denomination and voucher lines are
  recorded under the journal entry's id. KYC debit checks are skipped for GL requests.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 13:51:01 +05:30
108 changed files with 1242 additions and 8438 deletions
@@ -95,150 +95,6 @@ public class CommandWrapperBuilder {
return this;
}
public CommandWrapperBuilder createInvestmentParty() {
this.actionName = "CREATE";
this.entityName = "INVESTMENTPARTY";
this.entityId = null;
this.href = "/investment/parties";
return this;
}
public CommandWrapperBuilder updateInvestmentParty(final Long partyId) {
this.actionName = "UPDATE";
this.entityName = "INVESTMENTPARTY";
this.entityId = partyId;
this.href = "/investment/parties/" + partyId;
return this;
}
public CommandWrapperBuilder deleteInvestmentParty(final Long partyId) {
this.actionName = "DELETE";
this.entityName = "INVESTMENTPARTY";
this.entityId = partyId;
this.href = "/investment/parties/" + partyId;
return this;
}
public CommandWrapperBuilder createInvestmentScript() {
this.actionName = "CREATE";
this.entityName = "INVESTMENTSCRIPT";
this.entityId = null;
this.href = "/investment/scripts";
return this;
}
public CommandWrapperBuilder updateInvestmentScript(final Long scriptId) {
this.actionName = "UPDATE";
this.entityName = "INVESTMENTSCRIPT";
this.entityId = scriptId;
this.href = "/investment/scripts/" + scriptId;
return this;
}
public CommandWrapperBuilder createInvestmentPurchase() {
this.actionName = "CREATE";
this.entityName = "INVESTMENTPURCHASE";
this.entityId = null;
this.href = "/investment/purchases";
return this;
}
public CommandWrapperBuilder deleteInvestmentPurchase(final Long purchaseId) {
this.actionName = "DELETE";
this.entityName = "INVESTMENTPURCHASE";
this.entityId = purchaseId;
this.href = "/investment/purchases/" + purchaseId;
return this;
}
public CommandWrapperBuilder settleInvestmentPurchase(final Long purchaseId) {
this.actionName = "SETTLE";
this.entityName = "INVESTMENTPURCHASE";
this.entityId = purchaseId;
this.href = "/investment/purchases/" + purchaseId + "/settle";
return this;
}
public CommandWrapperBuilder createInvestmentSale() {
this.actionName = "CREATE";
this.entityName = "INVESTMENTSALE";
this.entityId = null;
this.href = "/investment/sales";
return this;
}
public CommandWrapperBuilder settleInvestmentSale(final Long saleId) {
this.actionName = "SETTLE";
this.entityName = "INVESTMENTSALE";
this.entityId = saleId;
this.href = "/investment/sales/" + saleId + "/settle";
return this;
}
public CommandWrapperBuilder deleteInvestmentSale(final Long saleId) {
this.actionName = "DELETE";
this.entityName = "INVESTMENTSALE";
this.entityId = saleId;
this.href = "/investment/sales/" + saleId;
return this;
}
public CommandWrapperBuilder amortiseInvestmentPremium() {
this.actionName = "AMORTISE";
this.entityName = "INVESTMENTSALE";
this.entityId = null;
this.href = "/investment/amortisation";
return this;
}
public CommandWrapperBuilder writeOffInvestmentPurchase(final Long purchaseId) {
this.actionName = "WRITEOFF";
this.entityName = "INVESTMENTSALE";
this.entityId = purchaseId;
this.href = "/investment/purchases/" + purchaseId + "/write-off";
return this;
}
public CommandWrapperBuilder redeemInvestmentPurchase(final Long purchaseId) {
this.actionName = "REDEEM";
this.entityName = "INVESTMENTSALE";
this.entityId = purchaseId;
this.href = "/investment/purchases/" + purchaseId + "/redeem";
return this;
}
public CommandWrapperBuilder transferInvestmentSecurity(final Long purchaseId) {
this.actionName = "CREATE";
this.entityName = "INVESTMENTTRANSFER";
this.entityId = purchaseId;
this.href = "/investment/purchases/" + purchaseId + "/transfer";
return this;
}
public CommandWrapperBuilder postInvestmentInterest(final Long purchaseId) {
this.actionName = "CREATE";
this.entityName = "INVESTMENTINTEREST";
this.entityId = purchaseId;
this.href = "/investment/purchases/" + purchaseId + "/interest-postings";
return this;
}
public CommandWrapperBuilder createInvestmentDailyRate() {
this.actionName = "CREATE";
this.entityName = "INVESTMENTDAILYRATE";
this.entityId = null;
this.href = "/investment/daily-rates";
return this;
}
public CommandWrapperBuilder deleteInvestmentDailyRate(final Long rateId) {
this.actionName = "DELETE";
this.entityName = "INVESTMENTDAILYRATE";
this.entityId = rateId;
this.href = "/investment/daily-rates/" + rateId;
return this;
}
public CommandWrapperBuilder createChequeStockLot() {
this.actionName = "CREATE";
this.entityName = "CHEQUESTOCKLOT";
@@ -46,7 +46,6 @@ import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.cashreport.service.VaultPositionService;
import org.apache.fineract.portfolio.denominationmaster.service.CountedCashLedger;
import org.apache.fineract.portfolio.investment.service.InvestmentPurchaseReadPlatformService;
import org.apache.fineract.portfolio.savings.dailyinterest.SavingsDailyInterestOrchestrator;
import org.apache.fineract.portfolio.savings.dailyinterest.SavingsInterestPostingReportService;
import org.apache.fineract.portfolio.savings.fdinterest.FixedDepositInterestPostingOrchestrator;
@@ -96,15 +95,8 @@ public class BranchDayRegisterApiResource {
private final SavingsDailyInterestOrchestrator savingsDailyInterestOrchestrator;
private final FixedDepositInterestPostingReportService fdInterestPostingReportService;
private final FixedDepositInterestPostingOrchestrator fdInterestPostingOrchestrator;
private final InvestmentPurchaseReadPlatformService investmentPurchaseReadPlatformService;
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 ──────────────────────────────
/**
@@ -268,11 +260,6 @@ public class BranchDayRegisterApiResource {
final Map<String, Object> fdInterest = fdInterestPostingReportService.dayCloseInterestView(officeId, businessDate);
result.put("fdInterestPending", fdInterest.get("pending"));
result.put("fdInterestDue", fdInterest.get("due"));
// Investment interest vouchers (coupon / provision) whose date has come but which are not posted yet.
// A reminder only: it never stops the close (Jitendra Sir). Investments are booked at Head Office, so
// only the Head Office close shows it.
result.put("investmentInterestPending",
isHeadOffice(officeId) ? investmentPurchaseReadPlatformService.retrieveDueInterest(0) : List.of());
return gson.toJson(result);
}
@@ -20,6 +20,7 @@ import org.apache.fineract.portfolio.cashreport.service.CashierRoleHelper;
import org.apache.fineract.portfolio.cashreport.service.VaultPositionService;
import org.apache.fineract.portfolio.denominationmaster.service.CountedCashLedger;
import org.apache.fineract.portfolio.denominationmaster.service.DenominationBreakdownWriter;
import org.apache.fineract.portfolio.voucher.service.VoucherService;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@@ -36,6 +37,7 @@ public class CashReportApiResource {
private final DenominationBreakdownWriter denominationBreakdownWriter;
private final CashierRoleHelper cashierRoleHelper;
private final VaultPositionService vaultPositionService;
private final VoucherService voucherService;
private final Gson gson = new Gson();
/**
@@ -329,7 +331,18 @@ public class CashReportApiResource {
BigDecimal priorCreditTotal = BigDecimal.ZERO;
BigDecimal priorDebitTotal = BigDecimal.ZERO;
// Where each voucher stands after a correction: a revoked voucher is listed, marked, but its money now sits on
// the voucher that replaced it, so it must not be counted in the day's totals a second time.
final Map<String, Map<String, Object>> marks = voucherService.voucherMarks(
rows.stream().map(r -> (String) r.get("voucherNo")).toList());
for (final Map<String, Object> row : rows) {
final Map<String, Object> mark = marks.get((String) row.get("voucherNo"));
row.put("voucherStatus", mark == null ? "ACTIVE" : mark.get("voucherStatus"));
row.put("revokedByVoucherNo", mark == null ? null : mark.get("revokedByVoucherNo"));
row.put("replacesVoucherNo", mark == null ? null : mark.get("replacesVoucherNo"));
row.put("correctionNote", mark == null ? null : mark.get("correctionNote"));
final boolean revoked = "REVOKED".equals(row.get("voucherStatus"));
if (sideFilter != null && !sideFilter.equals(row.get("side"))) {
continue;
}
@@ -350,7 +363,7 @@ public class CashReportApiResource {
// was already earned, and the day it belongs to has been counted and signed off. Adding
// it to this day's totals would make them disagree with the ledger for either date.
final boolean priorDay = "SYSTEM_PRIOR".equals(row.get("section"));
if (!priorDay && ("APPROVED".equalsIgnoreCase(rowStatus) || "COMPLETED".equalsIgnoreCase(rowStatus)
if (!priorDay && !revoked && ("APPROVED".equalsIgnoreCase(rowStatus) || "COMPLETED".equalsIgnoreCase(rowStatus)
|| "WITHDRAWN".equalsIgnoreCase(rowStatus) || "CLOSED".equalsIgnoreCase(rowStatus))) {
final BigDecimal amount = (BigDecimal) row.get("amount");
if (amount != null) {
@@ -57,9 +57,23 @@ public class CashTransferApiResource {
private final VaultPositionService vaultPositionService;
private final Gson gson = new Gson();
/**
* Cash no longer leaves the vault on one person's say-so: the allocation is raised as a request (see
* {@link #submitRequest}) and only a different person's approval posts it. The posting itself is
* {@link #postTransfer}.
*/
@POST
public String create(String body) {
this.context.authenticatedUser().validateHasPermissionTo("CREATE_CASHTRANSFER");
throw new PlatformDataIntegrityException("error.msg.cashtransfer.request.required",
"A cash transfer is raised as a request and posted when a manager approves it.");
}
/**
* Validates a transfer and, unless {@code dryRun}, posts it: the header and its denominations, then the GL entry.
* A dry run stops after every check has passed, which is what a request is held to when it is raised and again
* when it is approved, since the vault can change in between.
*/
private String postTransfer(final String body, final Long performedBy, final String performedByUsername, final boolean dryRun) {
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
final LocalDate transferDate = json.has("transferDate") && !json.get("transferDate").isJsonNull()
@@ -102,6 +116,9 @@ public class CashTransferApiResource {
}
validateVaultHasSufficientCash(fromTellerId, total, denominations, transferDate);
if (dryRun) {
return "{}";
}
CashTransfer header = new CashTransfer();
header.setTransferDate(transferDate);
@@ -112,15 +129,9 @@ public class CashTransferApiResource {
header.setTransferAmount(total);
header.setNote(note);
header.setCreatedAt(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant()));
try {
AppUser user = this.context.authenticatedUser();
if (user != null) {
header.setPerformedBy(user.getId());
header.setPerformedByUsername(user.getUsername());
}
} catch (Exception e) {
// resolving the current user must never block recording the transfer
}
// Recorded against the person who raised the request, not the one who approved it.
header.setPerformedBy(performedBy);
header.setPerformedByUsername(performedByUsername);
// Save header + details in one committed transaction, then GL dispatch separately.
// This prevents EclipseLink from flushing pending detail rows inside Fineract's
@@ -159,6 +170,171 @@ public class CashTransferApiResource {
return gson.toJson(result);
}
// ── Maker-checker: the allocation is a request until a different person approves it ────────────
/** The maker raises the allocation. Nothing moves yet: every check runs, and the request waits for a checker. */
@POST
@Path("requests")
public String submitRequest(final String body) {
final AppUser user = this.context.authenticatedUser();
user.validateHasPermissionTo("CREATE_CASHTRANSFER");
postTransfer(body, user.getId(), user.getUsername(), true);
// The same request, from the same person, is not raised twice while the first is still waiting: a second tap or
// a second click would otherwise release the same cash twice once both were approved.
final Integer identical = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM m_cash_transfer_request WHERE status = ? AND maker_id = ? AND request_body = ?", Integer.class,
REQUEST_PENDING, user.getId(), body);
if (identical != null && identical > 0) {
throw new PlatformDataIntegrityException("error.msg.cashtransfer.request.duplicate",
"An identical cash transfer request from you is already waiting for approval.");
}
final JsonObject json = JsonParser.parseString(body).getAsJsonObject();
final JsonArray denominations = json.getAsJsonArray("denominations");
final Long id = jdbcTemplate.queryForObject("""
INSERT INTO m_cash_transfer_request
(transfer_date, from_teller_id, to_cashier_id, from_party, to_party, note, total_amount, request_body,
status, maker_id, maker_name, maker_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
RETURNING id
""", Long.class, textOrNull(json, "transferDate") == null ? null : java.sql.Date.valueOf(textOrNull(json, "transferDate")),
json.get("fromTellerId").getAsLong(), json.get("toCashierId").getAsLong(), textOrNull(json, "fromParty"),
textOrNull(json, "toParty"), textOrNull(json, "note"), sumTotal(denominations), body, REQUEST_PENDING, user.getId(),
user.getDisplayName(), java.sql.Timestamp.from(java.time.Instant.now()));
final JsonObject result = new JsonObject();
result.addProperty("resourceId", id);
result.addProperty("status", REQUEST_PENDING);
return gson.toJson(result);
}
/** Requests by status (comma separated); {@code mine} narrows them to the ones the caller raised. */
@GET
@Path("requests")
public String listRequests(@QueryParam("status") final String status,
@QueryParam("mine") @jakarta.ws.rs.DefaultValue("false") final boolean mine) {
final AppUser user = this.context.authenticatedUser();
final StringBuilder sql = new StringBuilder("SELECT * FROM m_cash_transfer_request WHERE 1=1");
final List<Object> params = new java.util.ArrayList<>();
if (status != null && !status.isBlank()) {
final String[] statuses = status.split(",");
sql.append(" AND status IN (").append("?,".repeat(statuses.length), 0, statuses.length * 2 - 1).append(")");
for (final String s : statuses) {
params.add(s.trim().toUpperCase());
}
}
if (mine) {
sql.append(" AND maker_id = ?");
params.add(user.getId());
}
sql.append(" ORDER BY id DESC LIMIT 200");
final List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql.toString(), params.toArray());
for (final Map<String, Object> row : rows) {
// The notes asked for, so the approver sees what they are releasing without opening anything.
final Object requestBody = row.remove("request_body");
row.put("denominations", requestBody == null ? new JsonArray()
: JsonParser.parseString(requestBody.toString()).getAsJsonObject().getAsJsonArray("denominations"));
}
return gson.toJson(rows);
}
/** The checker releases the cash: re-checks the vault, then posts the transfer exactly as a direct one used to. */
@POST
@Path("requests/{id}/approve")
public String approveRequest(@PathParam("id") final Long id) {
final AppUser user = this.context.authenticatedUser();
requireChecker(user);
final Map<String, Object> r = loadRequest(id);
requirePending(r, "approved");
final Long makerId = r.get("maker_id") instanceof Number n ? n.longValue() : null;
if (makerId != null && makerId.equals(user.getId())) {
throw new PlatformDataIntegrityException("error.msg.cashtransfer.request.same.user",
"Maker-checker: you cannot approve a cash transfer you raised yourself. Ask another manager.");
}
final String posted = postTransfer((String) r.get("request_body"), makerId, (String) r.get("maker_name"), false);
final JsonObject postedJson = JsonParser.parseString(posted).getAsJsonObject();
final Long transferId = postedJson.has("resourceId") ? postedJson.get("resourceId").getAsLong() : null;
final int updated = jdbcTemplate.update("""
UPDATE m_cash_transfer_request SET status = ?, checker_id = ?, checker_name = ?, checker_at = ?, cash_transfer_id = ?
WHERE id = ? AND status = ?
""", REQUEST_APPROVED, user.getId(), user.getDisplayName(), java.sql.Timestamp.from(java.time.Instant.now()),
transferId, id, REQUEST_PENDING);
if (updated == 0) {
throw new PlatformDataIntegrityException("error.msg.cashtransfer.request.actioned",
"This request was already actioned by another manager.");
}
final JsonObject result = new JsonObject();
result.addProperty("resourceId", id);
result.addProperty("status", REQUEST_APPROVED);
if (transferId != null) {
result.addProperty("cashTransferId", transferId);
}
if (postedJson.has("glWarning")) {
result.add("glWarning", postedJson.get("glWarning"));
}
return gson.toJson(result);
}
@POST
@Path("requests/{id}/reject")
public String rejectRequest(@PathParam("id") final Long id, final String body) {
final AppUser user = this.context.authenticatedUser();
requireChecker(user);
final Map<String, Object> r = loadRequest(id);
requirePending(r, "rejected");
final JsonObject json = body == null || body.isBlank() ? new JsonObject() : JsonParser.parseString(body).getAsJsonObject();
final String reason = textOrNull(json, "reason");
if (reason == null || reason.isBlank()) {
throw new PlatformDataIntegrityException("error.msg.cashtransfer.request.reason", "A reason is required to reject.");
}
jdbcTemplate.update("""
UPDATE m_cash_transfer_request SET status = ?, checker_id = ?, checker_name = ?, checker_at = ?, rejection_reason = ?
WHERE id = ? AND status = ?
""", REQUEST_REJECTED, user.getId(), user.getDisplayName(), java.sql.Timestamp.from(java.time.Instant.now()),
reason.trim(), id, REQUEST_PENDING);
final JsonObject result = new JsonObject();
result.addProperty("resourceId", id);
result.addProperty("status", REQUEST_REJECTED);
return gson.toJson(result);
}
private static final String REQUEST_PENDING = "PENDING";
private static final String REQUEST_APPROVED = "APPROVED";
private static final String REQUEST_REJECTED = "REJECTED";
private Map<String, Object> loadRequest(final Long id) {
final List<Map<String, Object>> rows = jdbcTemplate.queryForList("SELECT * FROM m_cash_transfer_request WHERE id = ?", id);
if (rows.isEmpty()) {
throw new PlatformDataIntegrityException("error.msg.cashtransfer.request.not.found", "Cash transfer request " + id + " not found.");
}
return rows.get(0);
}
private void requirePending(final Map<String, Object> request, final String action) {
if (!REQUEST_PENDING.equals(request.get("status"))) {
throw new PlatformDataIntegrityException("error.msg.cashtransfer.request.status",
"Only PENDING requests can be " + action + ". Current status: " + request.get("status"));
}
}
/** Checker authorization, the same rule the withdrawal and deposit request registers use. */
private void requireChecker(final AppUser user) {
if (user.hasAnyPermission("ALL_FUNCTIONS")) {
return;
}
final boolean isChecker = user.getRoles().stream().map(role -> role.getName() == null ? "" : role.getName().toLowerCase().trim())
.anyMatch(n -> n.contains("manager") || n.contains("head cashier"));
if (!isChecker) {
throw new PlatformDataIntegrityException("error.msg.cashtransfer.request.checker",
"Only a Bank Manager (or Head Cashier) can approve or reject a cash transfer.");
}
}
private static String textOrNull(final JsonObject json, final String key) {
return json.has(key) && !json.get(key).isJsonNull() ? json.get(key).getAsString() : null;
}
@GET
public String getAll(
@QueryParam("offset") @jakarta.ws.rs.DefaultValue("0") int offset,
@@ -48,6 +48,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
@@ -110,11 +111,20 @@ public class ClerkWithdrawalRequestApiResource {
final String tokNo = fromJsonHelper.extractStringNamed("tokNo", root);
final String txnDate = fromJsonHelper.extractStringNamed("txnDate", root);
final String denominationData = fromJsonHelper.extractStringNamed("denominationData", root);
// A GL withdrawal slip (e.g. Sundry Creditors paid out in cash) carries the GL and the cash GL it is
// paid from instead of a savings account; it is approved and disbursed exactly like any other slip.
final boolean isGl = "GL".equalsIgnoreCase(accountType);
final Long glAccountId = fromJsonHelper.extractLongNamed("glAccountId", root);
final Long contraGlId = fromJsonHelper.extractLongNamed("contraGlId", root);
final Long officeId = fromJsonHelper.extractLongNamed("officeId", root);
final String currencyCode = fromJsonHelper.extractStringNamed("currencyCode", root);
if (accountNo == null || accountNo.isBlank()) {
throw apiError(400, "Account number is required");
}
if (savingsAccountId == null) {
if (isGl) {
validateGlWithdrawalAccounts(glAccountId, contraGlId);
} else if (savingsAccountId == null) {
throw apiError(400, "Savings account id is required for a withdrawal request");
}
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
@@ -124,7 +134,9 @@ public class ClerkWithdrawalRequestApiResource {
throw apiError(400, "Token number (TOK. NO.) is required");
}
validateTokenUniqueToday(tokNo, null);
kycRestrictionGuard.check(savingsAccountId, KycRestrictionGuard.Operation.DEBIT);
if (!isGl) {
kycRestrictionGuard.check(savingsAccountId, KycRestrictionGuard.Operation.DEBIT);
}
final String activityLog = buildActivityLog(null, "SUBMITTED", currentUser.getUsername(), null);
@@ -132,8 +144,9 @@ public class ClerkWithdrawalRequestApiResource {
INSERT INTO m_clerk_withdrawal_request
(account_no, account_type, savings_account_id, client_id, client_name, amount,
payment_type_id, payment_type_name, description, cheque_no, bank_name, tok_no, txn_date,
status, clerk_id, clerk_name, clerk_username, denomination_data, activity_log, submitted_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
status, clerk_id, clerk_name, clerk_username, denomination_data, activity_log, submitted_at,
gl_account_id, contra_gl_id, office_id, currency_code)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
RETURNING id
""",
Long.class,
@@ -142,7 +155,9 @@ public class ClerkWithdrawalRequestApiResource {
txnDate != null ? Date.valueOf(txnDate) : null,
"PENDING",
currentUser.getId(), currentUser.getDisplayName(), currentUser.getUsername(),
denominationData, activityLog, Timestamp.from(Instant.now())
denominationData, activityLog, Timestamp.from(Instant.now()),
isGl ? glAccountId : null, isGl ? contraGlId : null, isGl ? (officeId != null ? officeId : 1L) : null,
isGl ? (currencyCode != null ? currencyCode : "INR") : null
);
// Globally unique voucher number from the central m_voucher register
@@ -198,7 +213,8 @@ public class ClerkWithdrawalRequestApiResource {
+ " txn_date, status, clerk_id, clerk_name, clerk_username,"
+ " manager_id, manager_name, manager_username, rejection_reason, txn_id,"
+ " denomination_data, activity_log, submitted_at, actioned_at,"
+ " cashier_id, disbursed_by, disbursed_at, voucher_no"
+ " cashier_id, disbursed_by, disbursed_at, voucher_no,"
+ " gl_account_id, contra_gl_id, office_id, currency_code"
+ " FROM m_clerk_withdrawal_request " + where
+ " ORDER BY submitted_at DESC LIMIT ? OFFSET ?",
pageParams.toArray());
@@ -252,7 +268,8 @@ public class ClerkWithdrawalRequestApiResource {
payment_type_name, description, tok_no, txn_date, status,
clerk_name, manager_name, manager_username, rejection_reason, txn_id,
submitted_at, actioned_at,
cashier_id, disbursed_by, disbursed_at, voucher_no
cashier_id, disbursed_by, disbursed_at, voucher_no,
gl_account_id, contra_gl_id, office_id, currency_code
FROM m_clerk_withdrawal_request
""" + where + " ORDER BY submitted_at DESC LIMIT ? OFFSET ?",
pageParams.toArray()
@@ -276,7 +293,8 @@ public class ClerkWithdrawalRequestApiResource {
txn_date, status, clerk_id, clerk_name, clerk_username,
manager_id, manager_name, manager_username, rejection_reason, txn_id,
denomination_data, activity_log, submitted_at, actioned_at,
cashier_id, disbursed_by, disbursed_at, voucher_no
cashier_id, disbursed_by, disbursed_at, voucher_no,
gl_account_id, contra_gl_id, office_id, currency_code
FROM m_clerk_withdrawal_request WHERE id = ?
""", id);
if (rows.isEmpty()) {
@@ -295,7 +313,8 @@ public class ClerkWithdrawalRequestApiResource {
final List<Map<String, Object>> rows = jdbcTemplate.queryForList("""
SELECT id, account_no, account_type, savings_account_id, client_id, client_name,
amount, payment_type_id, payment_type_name, description, tok_no, status,
clerk_id, clerk_name, manager_id, manager_name, submitted_at, actioned_at, voucher_no
clerk_id, clerk_name, manager_id, manager_name, submitted_at, actioned_at, voucher_no,
gl_account_id, contra_gl_id, office_id, currency_code
FROM m_clerk_withdrawal_request
WHERE tok_no = ? AND CAST(submitted_at AS DATE) = CURRENT_DATE
ORDER BY submitted_at DESC LIMIT 1
@@ -455,13 +474,16 @@ public class ClerkWithdrawalRequestApiResource {
"Maker-checker: you cannot approve a withdrawal request you submitted yourself. Ask another manager to review it.");
}
final boolean isGlRequest = "GL".equalsIgnoreCase((String) record.get("account_type"));
final Long savingsAccountId = record.get("savings_account_id") != null
? ((Number) record.get("savings_account_id")).longValue() : null;
// Checked again on approval, not only on submission: a request can sit in the queue for days,
// and the account can cross the dormancy threshold while it waits. The savings withdrawal
// below refuses a dormant account anyway, but that failure would surface to the manager as a
// rejected transaction after the approval was recorded; this says why, before anything moves.
kycRestrictionGuard.check(savingsAccountId, KycRestrictionGuard.Operation.DEBIT);
if (!isGlRequest) {
kycRestrictionGuard.check(savingsAccountId, KycRestrictionGuard.Operation.DEBIT);
}
final BigDecimal amount = new BigDecimal(record.get("amount").toString());
final Object ptId = record.get("payment_type_id");
final Object txnDateObj = record.get("txn_date");
@@ -469,7 +491,11 @@ public class ClerkWithdrawalRequestApiResource {
final String description = (String) record.get("description");
String txnId = null;
if (savingsAccountId != null) {
if (isGlRequest) {
// Dr the GL account, Cr the branch cash GL -- posted here, at approval, like the savings withdrawal.
validateGlWithdrawalAccounts(toLong(record.get("gl_account_id")), toLong(record.get("contra_gl_id")));
txnId = executeGlWithdrawalJournal(record, amount, ptId, txnDateStr, description);
} else if (savingsAccountId != null) {
final boolean isCurrentAccount = isCurrentAccountById(savingsAccountId, (String) record.get("account_no"));
txnId = isCurrentAccount
? executeCurrentAccountWithdrawal(savingsAccountId, amount, ptId, txnDateStr, description)
@@ -477,14 +503,18 @@ public class ClerkWithdrawalRequestApiResource {
}
// Link voucher to the created savings txn and stamp voucher_no on it
if (txnId != null) {
if (txnId != null && !isGlRequest) {
voucherService.linkSavingsTransaction(
VoucherService.REF_CLERK_WITHDRAWAL_REQUEST, id, Long.valueOf(txnId));
}
// The sides of the slip. A withdrawal is money leaving the member, so their account is
// debited and the till that pays it out is credited.
addWithdrawalVoucherLines(record, savingsAccountId, amount, txnId);
if (isGlRequest) {
addGlWithdrawalVoucherLines(record, amount);
} else {
addWithdrawalVoucherLines(record, savingsAccountId, amount, txnId);
}
final String updatedLog = buildActivityLog(
(String) record.get("activity_log"), "APPROVED", currentUser.getUsername(), null
@@ -689,8 +719,8 @@ public class ClerkWithdrawalRequestApiResource {
if (denomMap == null || denomMap.size() == 0) return;
final Object txnIdObj = record.get("txn_id");
final Long txnId = txnIdObj != null ? Long.valueOf(txnIdObj.toString()) : null;
if (txnId == null) return; // no savings txn linked — cannot record denomination
final Long txnId = txnIdObj != null ? parseTxnKey(txnIdObj.toString()) : null;
if (txnId == null) return; // no txn linked — cannot record denomination
final Object saObj = record.get("savings_account_id");
final Long savingsAccountId = saObj != null ? Long.valueOf(saObj.toString()) : null;
@@ -793,6 +823,117 @@ public class ClerkWithdrawalRequestApiResource {
return rows.isEmpty() ? null : rows.get(0);
}
private static Long toLong(final Object value) {
return value instanceof Number n ? n.longValue() : null;
}
/**
* A savings transaction id is numeric; a GL journal entry's is a hex string. Both are reduced to one
* Long the same way CashierGlDenominationApiResource does, so a GL withdrawal's notes group under
* the id of its own journal entry in the cashier's position and closing report.
*/
private Long parseTxnKey(final String raw) {
if (raw == null || raw.isBlank()) {
return null;
}
try {
return Long.valueOf(raw);
} catch (final NumberFormatException ignored) {
try {
final String hex = raw.length() > 15 ? raw.substring(raw.length() - 15) : raw;
return Long.parseUnsignedLong(hex, 16);
} catch (final NumberFormatException ignored2) {
return null;
}
}
}
/** A GL withdrawal pays cash out of a GL account: the account must not itself be a cash GL, and the
* GL it is paid from must be one of the branch's mapped cash accounts. */
private void validateGlWithdrawalAccounts(final Long glAccountId, final Long contraGlId) {
if (glAccountId == null || contraGlId == null) {
throw apiError(400, "glAccountId and contraGlId are required for a GL withdrawal request");
}
final List<Long> cashGlIds = jdbcTemplate.queryForList(
"SELECT gl_account_id FROM acc_gl_financial_activity_account WHERE financial_activity_type IN (?, ?)",
Long.class, FinancialActivity.CASH_AT_MAINVAULT.getValue(), FinancialActivity.CASH_AT_TELLER.getValue());
if (!cashGlIds.contains(contraGlId)) {
throw apiError(400, "A GL withdrawal must be paid from the branch cash vault GL.");
}
if (cashGlIds.contains(glAccountId)) {
throw apiError(400, "The cash vault itself cannot be the GL account of a withdrawal slip.");
}
}
/** Dr the GL account, Cr the cash GL, through the journal-entry command (not the HTTP API, which is
* switched off for manual postings). Returns the journal entry's transaction id. */
private String executeGlWithdrawalJournal(final Map<?, ?> record, final BigDecimal amount,
final Object paymentTypeId, final String txnDateStr, final String note) {
final String today = LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
final String date = txnDateStr != null && !txnDateStr.isBlank() ? formatForFineract(txnDateStr) : today;
final JsonObject debit = new JsonObject();
debit.addProperty("glAccountId", toLong(record.get("gl_account_id")));
debit.addProperty("amount", amount);
final JsonObject credit = new JsonObject();
credit.addProperty("glAccountId", toLong(record.get("contra_gl_id")));
credit.addProperty("amount", amount);
final JsonArray debits = new JsonArray();
debits.add(debit);
final JsonArray credits = new JsonArray();
credits.add(credit);
final Object office = record.get("office_id");
final Object currency = record.get("currency_code");
final JsonObject payload = new JsonObject();
payload.addProperty("officeId", office instanceof Number n ? n.longValue() : 1L);
payload.addProperty("transactionDate", date);
payload.addProperty("dateFormat", "dd MMMM yyyy");
payload.addProperty("locale", "en");
payload.addProperty("currencyCode", currency != null ? currency.toString() : "INR");
if (note != null && !note.isBlank()) {
payload.addProperty("comments", note);
}
if (paymentTypeId != null) {
payload.addProperty("paymentTypeId", ((Number) paymentTypeId).longValue());
}
payload.add("credits", credits);
payload.add("debits", debits);
final CommandWrapper commandRequest = new CommandWrapperBuilder().createJournalEntry().withJson(payload.toString()).build();
// The request raised its own voucher at submission; the journal command must not raise a second.
final CommandProcessingResult result = voucherService
.withOwnVoucher(() -> commandsSourceWritePlatformService.logCommandSource(commandRequest));
return result.getTransactionId();
}
/** The two sides of an approved GL withdrawal on its voucher: the GL account is debited, the cash GL credited. */
private void addGlWithdrawalVoucherLines(final Map<?, ?> record, final BigDecimal amount) {
final String voucherNo = (String) record.get("voucher_no");
if (voucherNo == null || voucherNo.isBlank()) {
return;
}
try {
final Long glId = toLong(record.get("gl_account_id"));
final Long contraId = toLong(record.get("contra_gl_id"));
final Map<String, Object> gl = glAccountForVoucher(glId);
final Map<String, Object> contra = glAccountForVoucher(contraId);
voucherService.addLines(voucherNo, List.of(
VoucherLine.debitGl(glId, (String) gl.get("name"), (String) gl.get("gl_code"), amount),
VoucherLine.creditGl(contraId, (String) contra.getOrDefault("name", "Cash in Hand"), (String) contra.get("gl_code"), amount)));
} catch (final RuntimeException e) {
LOG.error("Could not record voucher lines for GL withdrawal request voucher {}", voucherNo, e);
}
}
private Map<String, Object> glAccountForVoucher(final Long glAccountId) {
if (glAccountId == null) {
return Map.of();
}
return jdbcTemplate.queryForList("SELECT name, gl_code FROM acc_gl_account WHERE id = ?", glAccountId).stream().findFirst()
.orElse(Map.of());
}
private Map<?, ?> loadRecord(final Long id) {
final List<Map<String, Object>> rows = jdbcTemplate.queryForList("""
SELECT id, account_no, account_type, savings_account_id, client_id, client_name,
@@ -800,7 +941,8 @@ public class ClerkWithdrawalRequestApiResource {
txn_date, status, clerk_id, clerk_name, clerk_username,
manager_id, manager_name, manager_username, rejection_reason, txn_id,
denomination_data, activity_log, submitted_at, actioned_at,
cashier_id, disbursed_by, disbursed_at, voucher_no
cashier_id, disbursed_by, disbursed_at, voucher_no,
gl_account_id, contra_gl_id, office_id, currency_code
FROM m_clerk_withdrawal_request WHERE id = ?
""", id);
if (rows.isEmpty()) {
@@ -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);
}
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
@@ -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);
}
}
@@ -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);
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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();
}
}
@@ -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;
}
}
@@ -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;
}
@@ -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();
}
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
}
@@ -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();
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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();
}
}
@@ -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;
}
@@ -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);
}
@@ -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;
}
@@ -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);
}
@@ -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;
}
@@ -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> {}
@@ -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;
}
@@ -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 InvestmentPurchaseRepository extends JpaRepository<InvestmentPurchase, Long> {}
@@ -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;
}
@@ -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 InvestmentSaleRepository extends JpaRepository<InvestmentSale, Long> {
List<InvestmentSale> findByPurchaseIdOrderByIdAsc(Long purchaseId);
}
@@ -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;
}
@@ -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);
}
@@ -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;
}
@@ -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> {}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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());
}
}
@@ -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());
}
}
@@ -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());
}
}
@@ -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());
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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);
}
}
}
@@ -1,254 +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)) {
final LocalDate maturityDate = this.fromApiJsonHelper.extractLocalDateNamed(MATURITY_DATE, element);
v.reset().parameter(MATURITY_DATE).value(maturityDate).ignoreIfNull().validateDateAfter(issueDate);
}
validateOptionalString(element, v, COMPANY_NAME, 150);
validateOptionalString(element, v, CORP_CODE, 30);
validateAddress(element, v);
throwIfErrors(errors);
}
// -------------------- shared --------------------
private void validateBondType(final JsonElement element, final DataValidatorBuilder v, final boolean required) {
final String bondType = this.fromApiJsonHelper.extractStringNamed(BOND_TYPE, element);
if (required) {
v.reset().parameter(BOND_TYPE).value(bondType).notBlank();
}
if (StringUtils.isNotBlank(bondType)) {
v.reset().parameter(BOND_TYPE).value(bondType).isOneOfTheseValues(BOND_TYPES.toArray());
}
}
private void validateGlAccountId(final JsonElement element, final DataValidatorBuilder v, final boolean required) {
if (!required && !this.fromApiJsonHelper.parameterExists(GL_ACCOUNT_ID, element)) {
return;
}
final Long glAccountId = this.fromApiJsonHelper.extractLongNamed(GL_ACCOUNT_ID, element);
v.reset().parameter(GL_ACCOUNT_ID).value(glAccountId).notNull().longGreaterThanZero();
}
private void validateRequiredString(final JsonElement element, final DataValidatorBuilder v, final String param, final int maxLength,
final boolean required) {
if (!required && !this.fromApiJsonHelper.parameterExists(param, element)) {
return;
}
final String value = this.fromApiJsonHelper.extractStringNamed(param, element);
v.reset().parameter(param).value(value).notBlank().notExceedingLengthOf(maxLength);
}
private void validateOptionalString(final JsonElement element, final DataValidatorBuilder v, final String param,
final int maxLength) {
final String value = this.fromApiJsonHelper.extractStringNamed(param, element);
v.reset().parameter(param).value(value).ignoreIfNull().notExceedingLengthOf(maxLength);
}
private void validateAddress(final JsonElement element, final DataValidatorBuilder v) {
validateOptionalString(element, v, ADDRESS1, 150);
validateOptionalString(element, v, ADDRESS2, 150);
validateOptionalString(element, v, ADDRESS3, 150);
validateOptionalString(element, v, CITY, 100);
validateOptionalString(element, v, PHONE, 20);
validateOptionalString(element, v, FAX, 20);
final String pincode = this.fromApiJsonHelper.extractStringNamed(PINCODE, element);
if (StringUtils.isNotBlank(pincode)) {
v.reset().parameter(PINCODE).value(pincode.trim()).matchesRegularExpression(PINCODE_PATTERN, "Pincode must be 6 digits");
}
}
private JsonElement parse(final String json, final Set<String> supportedParams) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, supportedParams);
return this.fromApiJsonHelper.parse(json);
}
private void throwIfErrors(final List<ApiParameterError> errors) {
if (!errors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors);
}
}
}
@@ -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;
}
}
@@ -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);
}
}
}
@@ -1,292 +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 = purchase(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 p : this.purchaseRepository.findAll(Sort.by(Sort.Direction.ASC, "id"))) {
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);
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);
final CommandProcessingResult posted = 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.getTransactionId());
final Object voucherNo = 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)
&& 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 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);
}
}
}
@@ -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);
}
}
@@ -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);
}
@@ -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();
}
}
@@ -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;
}
}
@@ -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);
}
@@ -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.findById(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);
}
}
@@ -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.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) {
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);
}
}
@@ -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);
}
@@ -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));
}
}
@@ -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);
}
@@ -1,314 +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.function.Consumer;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
import org.apache.fineract.accounting.glaccount.domain.GLAccountRepositoryWrapper;
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.portfolio.investment.domain.InvestmentParty;
import org.apache.fineract.portfolio.investment.domain.InvestmentPartyRepository;
import org.apache.fineract.portfolio.investment.domain.InvestmentScript;
import org.apache.fineract.portfolio.investment.domain.InvestmentScriptRepository;
import org.apache.fineract.portfolio.investment.exception.InvestmentPartyNotFoundException;
import org.apache.fineract.portfolio.investment.exception.InvestmentScriptNotFoundException;
import org.apache.fineract.portfolio.investment.serialization.InvestmentMasterCommandFromApiJsonDeserializer;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class InvestmentMasterWritePlatformServiceImpl implements InvestmentMasterWritePlatformService {
private final InvestmentMasterCommandFromApiJsonDeserializer validator;
private final InvestmentPartyRepository partyRepository;
private final InvestmentScriptRepository scriptRepository;
private final GLAccountRepositoryWrapper glAccountRepository;
private final JdbcTemplate jdbcTemplate;
// -------------------- Bank / Party Master --------------------
@Override
@Transactional
public CommandProcessingResult createParty(final JsonCommand command) {
this.validator.validateForCreateParty(command.json());
final InvestmentParty party = new InvestmentParty();
party.setPartyCode(nextPartyCode());
applyParty(party, command);
if (command.parameterExists(ACTIVE)) {
party.setActive(command.booleanPrimitiveValueOfParameterNamed(ACTIVE));
}
final InvestmentParty saved = saveParty(party);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(saved.getId())
.withResourceIdAsString(saved.getPartyCode()).build();
}
@Override
@Transactional
public CommandProcessingResult updateParty(final Long partyId, final JsonCommand command) {
this.validator.validateForUpdateParty(command.json());
final InvestmentParty party = this.partyRepository.findById(partyId).orElseThrow(() -> new InvestmentPartyNotFoundException(partyId));
applyParty(party, command);
if (command.parameterExists(ACTIVE)) {
party.setActive(command.booleanPrimitiveValueOfParameterNamed(ACTIVE));
}
saveParty(party);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(partyId)
.withResourceIdAsString(party.getPartyCode()).build();
}
@Override
@Transactional
public CommandProcessingResult deleteParty(final Long partyId) {
final InvestmentParty party = this.partyRepository.findById(partyId).orElseThrow(() -> new InvestmentPartyNotFoundException(partyId));
this.partyRepository.delete(party);
this.partyRepository.flush();
return new CommandProcessingResultBuilder().withEntityId(partyId).withResourceIdAsString(party.getPartyCode()).build();
}
/** Copies every party field present in the request onto the entity (absent fields are left unchanged). */
private void applyParty(final InvestmentParty p, final JsonCommand command) {
setIfPresent(command, BOND_TYPE, p::setBondType);
setIfPresent(command, PARTY_NAME, p::setPartyName);
setIfPresent(command, ADDRESS1, p::setAddress1);
setIfPresent(command, ADDRESS2, p::setAddress2);
setIfPresent(command, ADDRESS3, p::setAddress3);
setIfPresent(command, CITY, p::setCity);
setIfPresent(command, PINCODE, p::setPincode);
setIfPresent(command, PHONE, p::setPhone);
setIfPresent(command, FAX, p::setFax);
setIfPresent(command, BANK_TYPE, p::setBankType);
setIfPresent(command, RBI_CODE, p::setRbiCode);
setIfPresent(command, LEI_NO, p::setLeiNo);
setIfPresent(command, CP_BANK_NAME, p::setCpBankName);
setIfPresent(command, CP_BRANCH_NAME, p::setCpBranchName);
setIfPresent(command, CP_ACCOUNT_NO, p::setCpAccountNo);
if (command.parameterExists(CP_IFSC)) {
final String ifsc = command.stringValueOfParameterNamedAllowingNull(CP_IFSC);
p.setCpIfsc(StringUtils.isBlank(ifsc) ? null : ifsc.trim().toUpperCase());
}
if (command.parameterExists(EXPOSURE_LIMIT)) {
p.setExposureLimit(command.bigDecimalValueOfParameterNamed(EXPOSURE_LIMIT));
}
if (command.parameterExists(GL_ACCOUNT_ID)) {
p.setGlAccount(postableAssetGl(command.longValueOfParameterNamed(GL_ACCOUNT_ID)));
}
}
private InvestmentParty saveParty(final InvestmentParty party) {
try {
return this.partyRepository.saveAndFlush(party);
} catch (final DataIntegrityViolationException e) {
throw new PlatformDataIntegrityException("error.msg.investmentparty.duplicate", "A party with this code already exists",
"partyCode", party.getPartyCode(), e);
}
}
/** Next code in the PTY-001, PTY-002 … series. */
private String nextPartyCode() {
final Integer max = this.jdbcTemplate.queryForObject(
"SELECT MAX(CAST(SUBSTRING(party_code FROM 5) AS INTEGER)) FROM m_investment_party WHERE party_code ~ '^PTY-[0-9]+$'",
Integer.class);
return PARTY_CODE_PREFIX + String.format("%03d", (max == null ? 0 : max) + 1);
}
// -------------------- Script Master --------------------
@Override
@Transactional
public CommandProcessingResult createScript(final JsonCommand command) {
this.validator.validateForCreateScript(command.json());
final InvestmentScript script = new InvestmentScript();
script.setBondType(command.stringValueOfParameterNamed(BOND_TYPE));
script.setScriptCode(nextScriptCode(script.getBondType()));
script.setFaceValuePerUnit(defaultFaceValue(script.getBondType()));
applyScript(script, command);
if (command.parameterExists(ACTIVE)) {
script.setActive(command.booleanPrimitiveValueOfParameterNamed(ACTIVE));
}
final InvestmentScript saved = saveScript(script);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(saved.getId())
.withResourceIdAsString(saved.getScriptCode()).build();
}
@Override
@Transactional
public CommandProcessingResult updateScript(final Long scriptId, final JsonCommand command) {
final InvestmentScript script = this.scriptRepository.findById(scriptId)
.orElseThrow(() -> new InvestmentScriptNotFoundException(scriptId));
this.validator.validateForUpdateScript(command.json(), script.getBondType());
if (command.parameterExists(BOND_TYPE) && !script.getBondType().equals(command.stringValueOfParameterNamed(BOND_TYPE))) {
// The script code carries the bond-type letter (G00001), so the type cannot change once created.
throw new GeneralPlatformDomainRuleException("error.msg.investmentscript.bondtype.immutable",
"Bond Type of an existing script cannot be changed", script.getScriptCode());
}
applyScript(script, command);
if (command.parameterExists(ACTIVE)) {
script.setActive(command.booleanPrimitiveValueOfParameterNamed(ACTIVE));
}
saveScript(script);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(scriptId)
.withResourceIdAsString(script.getScriptCode()).build();
}
private void applyScript(final InvestmentScript s, final JsonCommand command) {
setIfPresent(command, SLR_TYPE, s::setSlrType);
setIfPresent(command, SCRIPT_DESC, s::setScriptDesc);
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());
}
}
}
@@ -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);
}
}
@@ -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);
}
@@ -1,325 +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 (!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.findById(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.findById(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);
}
}
@@ -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);
}
}
@@ -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));
}
}
@@ -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);
}
@@ -1,282 +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) {
final InvestmentSale pending = sale(saleId);
if (!STATUS_PENDING_SETTLEMENT.equals(pending.getStatus())) {
throw InvestmentSaleQuoter.rule("settle.not.pending", "This sale is not pending settlement");
}
final InvestmentPurchase p = pending.getPurchase();
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);
final BigDecimal book = this.quoter.remainingBook(p);
if (!STATUS_ACTIVE.equals(p.getStatus())) {
throw InvestmentSaleQuoter.rule("not.active", "Deal " + p.getDealNo() + " is not an active (settled) holding");
}
if (amount.compareTo(book) >= 0) {
throw InvestmentSaleQuoter.rule("writeoff.exceeds", "The write-off must be less than the book value of "
+ book.stripTrailingZeros().toPlainString() + "; 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.findById(purchaseId).orElseThrow(() -> new InvestmentPurchaseNotFoundException(purchaseId));
}
private InvestmentSale sale(final Long saleId) {
return this.saleRepository.findById(saleId)
.orElseThrow(() -> InvestmentSaleQuoter.rule("sale.not.found", "Investment sale " + saleId + " does not exist"));
}
}
@@ -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.findById(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);
}
}
@@ -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());
}
}
@@ -628,9 +628,15 @@ public class VoucherVerificationApiResource {
BigDecimal total = BigDecimal.ZERO;
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) {
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);
}
@@ -638,6 +644,7 @@ public class VoucherVerificationApiResource {
m.put("voucherNo", r.get("voucher_no"));
m.put("voucherType", r.get("voucher_type"));
m.put("amount", amount);
putVoucherMark(m, mark);
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.
// 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 verifiedCount = 0;
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) {
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
? "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)) {
receivedTotal = receivedTotal.add(amount);
} else if ("DR".equals(side)) {
@@ -897,6 +909,7 @@ public class VoucherVerificationApiResource {
m.put("raisedBy", r.get("created_by_username"));
m.put("branchName", r.get("branch_name"));
m.put("verificationStatus", r.get("verification_status"));
putVoucherMark(m, mark);
m.put("verifiedByName", r.get("verified_by_name"));
m.put("verifiedAt", r.get("verified_at") == null ? null : r.get("verified_at").toString());
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
* 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
@Path("detail")
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("createdAt", h.get("created_at") == null ? null : h.get("created_at").toString());
out.put("verificationStatus", h.get("verification_status"));
putVoucherMark(out, voucherService.voucherMarks(List.of(voucherNo)).get(voucherNo));
out.put("lines", lines);
out.put("totalDebit", totalDebit);
out.put("totalCredit", totalCredit);
@@ -595,6 +595,66 @@ public class VoucherService {
""", 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,
* 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/0593_fd_interest_posting_approval.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/0594_investment_purchase.xml" relativeToChangelogFile="true"/>
<include file="parts/0595_investment_purchase_other_charges.xml" relativeToChangelogFile="true"/>
<include file="parts/0596_investment_daily_rate.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"/>
<include file="parts/0600_clerk_withdrawal_request_gl.xml" relativeToChangelogFile="true"/>
<include file="parts/0601_voucher_correction.xml" relativeToChangelogFile="true"/>
<include file="parts/0602_voucher_drop_status_columns.xml" relativeToChangelogFile="true"/>
<include file="parts/0603_cash_transfer_request.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -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>
@@ -1,149 +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">
<!--
Investment Purchase Entry: one row per deal, which is the holding that interest, sale and
redemption later work against (balance_units tracks what is still held). Units, book value,
accrued interest and settlement amount are computed by the application. The optional legacy
deal fields (trade no, authorities, lien, delivery mode, ...) are kept in other_details as JSON.
No GL entry is posted by this table yet.
-->
<changeSet author="cbs-anshul" id="0594-1-create-investment-purchase">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_investment_purchase"/></not>
</preConditions>
<createTable tableName="m_investment_purchase">
<column name="id" type="BIGINT" autoIncrement="true"><constraints nullable="false" primaryKey="true"/></column>
<column name="deal_no" type="VARCHAR(20)"><constraints nullable="false"/></column>
<column name="cert_no" type="VARCHAR(20)"><constraints nullable="false"/></column>
<column name="bond_type" type="VARCHAR(20)"><constraints nullable="false"/></column>
<column name="script_id" type="BIGINT"><constraints nullable="false"/></column>
<column name="party_id" type="BIGINT"><constraints nullable="false"/></column>
<column name="deal_date" type="DATE"><constraints nullable="false"/></column>
<column name="deal_time" type="VARCHAR(8)"/>
<column name="settlement_type" type="VARCHAR(5)"><constraints nullable="false"/></column>
<column name="settlement_date" type="DATE"><constraints nullable="false"/></column>
<column name="face_value" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
<column name="purchase_rate" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
<column name="units" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
<column name="balance_units" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
<column name="brokerage" type="DECIMAL(19,6)"/>
<column name="stamp_duty" type="DECIMAL(19,6)"/>
<column name="book_value" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
<column name="premium_discount" type="DECIMAL(19,6)"/>
<column name="int_rate" type="DECIMAL(9,4)"/>
<column name="issue_date" type="DATE"/>
<column name="maturity_date" type="DATE"/>
<column name="last_int_date" type="DATE"/>
<column name="accrued_days" type="INT"/>
<column name="accrued_interest" type="DECIMAL(19,6)"/>
<column name="settlement_amount" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
<column name="ytm" type="DECIMAL(9,4)"/>
<column name="category" type="VARCHAR(40)"><constraints nullable="false"/></column>
<column name="sub_type" type="VARCHAR(40)"/>
<column name="investment_gl_account_id" type="BIGINT"><constraints nullable="false"/></column>
<column name="cr_gl_account_id" type="BIGINT"><constraints nullable="false"/></column>
<column name="other_details" type="VARCHAR(4000)"/>
<column name="status" type="VARCHAR(20)"><constraints nullable="false"/></column>
<column name="created_by" type="BIGINT"><constraints nullable="false"/></column>
<column name="created_on_utc" type="DATETIME"><constraints nullable="false"/></column>
<column name="last_modified_by" type="BIGINT"><constraints nullable="false"/></column>
<column name="last_modified_on_utc" type="DATETIME"><constraints nullable="false"/></column>
</createTable>
<addUniqueConstraint tableName="m_investment_purchase" columnNames="deal_no" constraintName="uq_investment_purchase_deal_no"/>
<addUniqueConstraint tableName="m_investment_purchase" columnNames="cert_no" constraintName="uq_investment_purchase_cert_no"/>
<addForeignKeyConstraint baseTableName="m_investment_purchase" baseColumnNames="script_id" constraintName="fk_investment_purchase_script"
referencedTableName="m_investment_script" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="m_investment_purchase" baseColumnNames="party_id" constraintName="fk_investment_purchase_party"
referencedTableName="m_investment_party" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="m_investment_purchase" baseColumnNames="investment_gl_account_id" constraintName="fk_investment_purchase_inv_gl"
referencedTableName="acc_gl_account" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="m_investment_purchase" baseColumnNames="cr_gl_account_id" constraintName="fk_investment_purchase_cr_gl"
referencedTableName="acc_gl_account" referencedColumnNames="id"/>
<createIndex tableName="m_investment_purchase" indexName="idx_investment_purchase_script">
<column name="script_id"/>
</createIndex>
<createIndex tableName="m_investment_purchase" indexName="idx_investment_purchase_bond_type_status">
<column name="bond_type"/>
<column name="status"/>
</createIndex>
</changeSet>
<changeSet author="cbs-anshul" id="0594-2-investment-purchase-permissions">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code IN ('READ_INVESTMENTPURCHASE', 'CREATE_INVESTMENTPURCHASE', 'DELETE_INVESTMENTPURCHASE', 'CREATE_INVESTMENTPURCHASE_CHECKER', 'DELETE_INVESTMENTPURCHASE_CHECKER')</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="READ_INVESTMENTPURCHASE"/>
<column name="entity_name" value="INVESTMENTPURCHASE"/>
<column name="action_name" value="READ"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="CREATE_INVESTMENTPURCHASE"/>
<column name="entity_name" value="INVESTMENTPURCHASE"/>
<column name="action_name" value="CREATE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="DELETE_INVESTMENTPURCHASE"/>
<column name="entity_name" value="INVESTMENTPURCHASE"/>
<column name="action_name" value="DELETE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="CREATE_INVESTMENTPURCHASE_CHECKER"/>
<column name="entity_name" value="INVESTMENTPURCHASE"/>
<column name="action_name" value="CREATE_CHECKER"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="DELETE_INVESTMENTPURCHASE_CHECKER"/>
<column name="entity_name" value="INVESTMENTPURCHASE"/>
<column name="action_name" value="DELETE_CHECKER"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
<!-- Same roles as the masters: whoever sees the Investment menu. Maker-checker stays off until
switched on from Configure Maker Checker Tasks. -->
<changeSet author="cbs-anshul" id="0594-3-grant-investment-purchase-permissions">
<sql>
INSERT INTO m_role_permission (role_id, permission_id)
SELECT rp.role_id, p.id
FROM m_role_permission rp
JOIN m_permission menu ON menu.id = rp.permission_id AND menu.code = 'MENU_INVESTMENT'
CROSS JOIN m_permission p
WHERE p.code IN ('READ_INVESTMENTPURCHASE', 'CREATE_INVESTMENTPURCHASE', 'DELETE_INVESTMENTPURCHASE')
AND NOT EXISTS (SELECT 1 FROM m_role_permission x WHERE x.role_id = rp.role_id AND x.permission_id = p.id)
</sql>
</changeSet>
</databaseChangeLog>
@@ -1,40 +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">
<!--
Other charges on an investment deal (legal, SGL, CCIL, ...), kept next to brokerage and stamp
duty. All three are expenses of the deal (Jitendra Sir): they are paid with the settlement but
are not part of the book value of the investment.
-->
<changeSet author="cbs-anshul" id="0595-1-add-investment-purchase-other-charges">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_investment_purchase" columnName="other_charges"/></not>
</preConditions>
<addColumn tableName="m_investment_purchase">
<column name="other_charges" type="DECIMAL(19,6)"/>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -1,108 +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">
<!--
Daily Script Master: the market rate of each script for a day (open / high / low / close),
entered by treasury from FBIL / CCIL. Purchase Entry takes its rate from here instead of the
dealer typing it (Jitendra Sir). One row per script per day.
-->
<changeSet author="cbs-anshul" id="0596-1-create-investment-daily-rate">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_investment_daily_rate"/></not>
</preConditions>
<createTable tableName="m_investment_daily_rate">
<column name="id" type="BIGINT" autoIncrement="true"><constraints nullable="false" primaryKey="true"/></column>
<column name="script_id" type="BIGINT"><constraints nullable="false"/></column>
<column name="rate_date" type="DATE"><constraints nullable="false"/></column>
<column name="open_rate" type="DECIMAL(19,6)"/>
<column name="high_rate" type="DECIMAL(19,6)"/>
<column name="low_rate" type="DECIMAL(19,6)"/>
<column name="close_rate" type="DECIMAL(19,6)"><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_daily_rate" columnNames="script_id, rate_date"
constraintName="uq_investment_daily_rate_script_date"/>
<addForeignKeyConstraint baseTableName="m_investment_daily_rate" baseColumnNames="script_id" constraintName="fk_investment_daily_rate_script"
referencedTableName="m_investment_script" referencedColumnNames="id"/>
</changeSet>
<changeSet author="cbs-anshul" id="0596-2-investment-daily-rate-permissions">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code IN ('READ_INVESTMENTDAILYRATE', 'CREATE_INVESTMENTDAILYRATE', 'DELETE_INVESTMENTDAILYRATE', 'CREATE_INVESTMENTDAILYRATE_CHECKER', 'DELETE_INVESTMENTDAILYRATE_CHECKER')</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="READ_INVESTMENTDAILYRATE"/>
<column name="entity_name" value="INVESTMENTDAILYRATE"/>
<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_INVESTMENTDAILYRATE"/>
<column name="entity_name" value="INVESTMENTDAILYRATE"/>
<column name="action_name" value="CREATE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="DELETE_INVESTMENTDAILYRATE"/>
<column name="entity_name" value="INVESTMENTDAILYRATE"/>
<column name="action_name" value="DELETE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="CREATE_INVESTMENTDAILYRATE_CHECKER"/>
<column name="entity_name" value="INVESTMENTDAILYRATE"/>
<column name="action_name" value="CREATE_CHECKER"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="DELETE_INVESTMENTDAILYRATE_CHECKER"/>
<column name="entity_name" value="INVESTMENTDAILYRATE"/>
<column name="action_name" value="DELETE_CHECKER"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
<!-- Same roles as the other investment screens: whoever sees the Investment menu. -->
<changeSet author="cbs-anshul" id="0596-3-grant-investment-daily-rate-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_INVESTMENTDAILYRATE', 'CREATE_INVESTMENTDAILYRATE', 'DELETE_INVESTMENTDAILYRATE')
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>
@@ -1,74 +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">
<!--
Purchase settlement: a deal is saved as PENDING_SETTLEMENT and posted to GL only when it is settled on or
after its settlement date (settlement-date accounting). The journal entry transaction id and the settlement
day are kept on the deal. Settlement goes through maker-checker so the dealer cannot post their own deal.
-->
<changeSet author="cbs-anshul" id="0597-1-add-investment-purchase-settlement-columns">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_investment_purchase" columnName="gl_transaction_id"/></not>
</preConditions>
<addColumn tableName="m_investment_purchase">
<column name="gl_transaction_id" type="VARCHAR(50)"/>
<column name="settled_on_date" type="DATE"/>
</addColumn>
</changeSet>
<changeSet author="cbs-anshul" id="0597-2-investment-purchase-settle-permissions">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code IN ('SETTLE_INVESTMENTPURCHASE', 'SETTLE_INVESTMENTPURCHASE_CHECKER')</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="SETTLE_INVESTMENTPURCHASE"/>
<column name="entity_name" value="INVESTMENTPURCHASE"/>
<column name="action_name" value="SETTLE"/>
<column name="can_maker_checker" valueBoolean="true"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="SETTLE_INVESTMENTPURCHASE_CHECKER"/>
<column name="entity_name" value="INVESTMENTPURCHASE"/>
<column name="action_name" value="SETTLE_CHECKER"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
<!-- Same roles as the other investment screens; any other user holding the checker permission approves. -->
<changeSet author="cbs-anshul" id="0597-3-grant-investment-purchase-settle-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 ('SETTLE_INVESTMENTPURCHASE', 'SETTLE_INVESTMENTPURCHASE_CHECKER')
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>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements.
-->
<!--
A GL withdrawal slip (e.g. Sundry Creditors paid out in cash) goes through the same request, manager approval and
cashier disbursement as a savings withdrawal, so the request carries the GL account and the cash GL it is paid
from. All four columns are nullable: savings, current and deposit requests do not use them.
-->
<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.1.xsd">
<changeSet id="0600_clerk_withdrawal_request_add_gl_columns" author="cbsnexora">
<preConditions onFail="MARK_RAN">
<tableExists tableName="m_clerk_withdrawal_request"/>
<not><columnExists tableName="m_clerk_withdrawal_request" columnName="gl_account_id"/></not>
</preConditions>
<addColumn tableName="m_clerk_withdrawal_request">
<column name="gl_account_id" type="BIGINT"/>
<column name="contra_gl_id" type="BIGINT"/>
<column name="office_id" type="BIGINT"/>
<column name="currency_code" type="VARCHAR(10)"/>
</addColumn>
</changeSet>
</databaseChangeLog>

Some files were not shown because too many files have changed in this diff Show More