Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33e7aef618 | ||
|
|
d5f188258f | ||
|
|
ff772d03b1 | ||
|
|
8db4fc7fee | ||
|
|
9bd1e5306f | ||
|
|
140c76dac9 | ||
|
|
13ad567a8b | ||
|
|
c3bb771d8a | ||
|
|
52ca2bfc03 | ||
|
|
3ef5a684ae | ||
|
|
9b1e381e07 | ||
|
|
4522f1305d | ||
|
|
ac563badaa | ||
|
|
861e2e196e | ||
|
|
8ad90f0b00 | ||
|
|
5606b88fbc | ||
|
|
4bb71382a1 | ||
|
|
859cabd07d | ||
|
|
6380e0d7d2 | ||
|
|
4ad508ee96 | ||
|
|
6eb562548c | ||
|
|
41aeb2b89a | ||
|
|
2569e0f836 | ||
|
|
81b5850b62 |
+15
-1
@@ -169,12 +169,26 @@ public class CommandSourceService {
|
|||||||
|| "RECURRINGDEPOSITPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
|| "RECURRINGDEPOSITPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|| "USER".equalsIgnoreCase(commandSource.getEntityName())
|
|| "USER".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|| "ROLE".equalsIgnoreCase(commandSource.getEntityName())
|
|| "ROLE".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|| isDdsEntity(commandSource.getEntityName())) {
|
|| isDdsEntity(commandSource.getEntityName())
|
||||||
|
|| isInvestmentEntity(commandSource.getEntityName())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Investment module's maker-checker entities. Deals, settlements and their vouchers are entered by one person and
|
||||||
|
* approved by another, so a super user's investment work goes to a checker as well. Unlike DDS, a super user may still
|
||||||
|
* check an entry they made themselves. Daily rates are not here: they are entered without approval.
|
||||||
|
*/
|
||||||
|
public static boolean isInvestmentEntity(final String entityName) {
|
||||||
|
return entityName != null && switch (entityName.toUpperCase()) {
|
||||||
|
case "INVESTMENTPARTY", "INVESTMENTSCRIPT", "INVESTMENTPURCHASE", "INVESTMENTSALE", "INVESTMENTFD", "INVESTMENTTRANSFER",
|
||||||
|
"INVESTMENTINTEREST", "INVESTMENTVALUATION" -> true;
|
||||||
|
default -> false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The DDS module's maker-checker entities. A super user's DDS work goes to a checker like anyone
|
* The DDS module's maker-checker entities. A super user's DDS work goes to a checker like anyone
|
||||||
* else's: DDS moves customers' money, and every such move is to be approved by a second person.
|
* else's: DDS moves customers' money, and every such move is to be approved by a second person.
|
||||||
|
|||||||
+216
@@ -95,6 +95,222 @@ public class CommandWrapperBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder createInvestmentParty() {
|
||||||
|
this.actionName = "CREATE";
|
||||||
|
this.entityName = "INVESTMENTPARTY";
|
||||||
|
this.entityId = null;
|
||||||
|
this.href = "/investment/parties";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder updateInvestmentParty(final Long partyId) {
|
||||||
|
this.actionName = "UPDATE";
|
||||||
|
this.entityName = "INVESTMENTPARTY";
|
||||||
|
this.entityId = partyId;
|
||||||
|
this.href = "/investment/parties/" + partyId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder deleteInvestmentParty(final Long partyId) {
|
||||||
|
this.actionName = "DELETE";
|
||||||
|
this.entityName = "INVESTMENTPARTY";
|
||||||
|
this.entityId = partyId;
|
||||||
|
this.href = "/investment/parties/" + partyId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder createInvestmentScript() {
|
||||||
|
this.actionName = "CREATE";
|
||||||
|
this.entityName = "INVESTMENTSCRIPT";
|
||||||
|
this.entityId = null;
|
||||||
|
this.href = "/investment/scripts";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder updateInvestmentScript(final Long scriptId) {
|
||||||
|
this.actionName = "UPDATE";
|
||||||
|
this.entityName = "INVESTMENTSCRIPT";
|
||||||
|
this.entityId = scriptId;
|
||||||
|
this.href = "/investment/scripts/" + scriptId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder createInvestmentPurchase() {
|
||||||
|
this.actionName = "CREATE";
|
||||||
|
this.entityName = "INVESTMENTPURCHASE";
|
||||||
|
this.entityId = null;
|
||||||
|
this.href = "/investment/purchases";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder deleteInvestmentPurchase(final Long purchaseId) {
|
||||||
|
this.actionName = "DELETE";
|
||||||
|
this.entityName = "INVESTMENTPURCHASE";
|
||||||
|
this.entityId = purchaseId;
|
||||||
|
this.href = "/investment/purchases/" + purchaseId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder settleInvestmentPurchase(final Long purchaseId) {
|
||||||
|
this.actionName = "SETTLE";
|
||||||
|
this.entityName = "INVESTMENTPURCHASE";
|
||||||
|
this.entityId = purchaseId;
|
||||||
|
this.href = "/investment/purchases/" + purchaseId + "/settle";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder createInvestmentSale() {
|
||||||
|
this.actionName = "CREATE";
|
||||||
|
this.entityName = "INVESTMENTSALE";
|
||||||
|
this.entityId = null;
|
||||||
|
this.href = "/investment/sales";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder settleInvestmentSale(final Long saleId) {
|
||||||
|
this.actionName = "SETTLE";
|
||||||
|
this.entityName = "INVESTMENTSALE";
|
||||||
|
this.entityId = saleId;
|
||||||
|
this.href = "/investment/sales/" + saleId + "/settle";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder deleteInvestmentSale(final Long saleId) {
|
||||||
|
this.actionName = "DELETE";
|
||||||
|
this.entityName = "INVESTMENTSALE";
|
||||||
|
this.entityId = saleId;
|
||||||
|
this.href = "/investment/sales/" + saleId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder createInvestmentFd() {
|
||||||
|
this.actionName = "CREATE";
|
||||||
|
this.entityName = "INVESTMENTFD";
|
||||||
|
this.entityId = null;
|
||||||
|
this.href = "/investment/fds";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder matureInvestmentFd(final Long fdId) {
|
||||||
|
this.actionName = "MATURE";
|
||||||
|
this.entityName = "INVESTMENTFD";
|
||||||
|
this.entityId = fdId;
|
||||||
|
this.href = "/investment/fds/" + fdId + "/mature";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder prematureInvestmentFd(final Long fdId) {
|
||||||
|
this.actionName = "PREMATURE";
|
||||||
|
this.entityName = "INVESTMENTFD";
|
||||||
|
this.entityId = fdId;
|
||||||
|
this.href = "/investment/fds/" + fdId + "/premature";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder renewInvestmentFd(final Long fdId) {
|
||||||
|
this.actionName = "RENEW";
|
||||||
|
this.entityName = "INVESTMENTFD";
|
||||||
|
this.entityId = fdId;
|
||||||
|
this.href = "/investment/fds/" + fdId + "/renew";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder postInvestmentFdInterest(final Long fdId) {
|
||||||
|
this.actionName = "POSTINTEREST";
|
||||||
|
this.entityName = "INVESTMENTFD";
|
||||||
|
this.entityId = fdId;
|
||||||
|
this.href = "/investment/fds/" + fdId + "/interest";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder setInvestmentDepositBase() {
|
||||||
|
this.actionName = "SETBASE";
|
||||||
|
this.entityName = "INVESTMENTFD";
|
||||||
|
this.entityId = null;
|
||||||
|
this.href = "/investment/fds/deposit-base";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder amortiseInvestmentPremium() {
|
||||||
|
this.actionName = "AMORTISE";
|
||||||
|
this.entityName = "INVESTMENTSALE";
|
||||||
|
this.entityId = null;
|
||||||
|
this.href = "/investment/amortisation";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder writeOffInvestmentPurchase(final Long purchaseId) {
|
||||||
|
this.actionName = "WRITEOFF";
|
||||||
|
this.entityName = "INVESTMENTSALE";
|
||||||
|
this.entityId = purchaseId;
|
||||||
|
this.href = "/investment/purchases/" + purchaseId + "/write-off";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder redeemInvestmentPurchase(final Long purchaseId) {
|
||||||
|
this.actionName = "REDEEM";
|
||||||
|
this.entityName = "INVESTMENTSALE";
|
||||||
|
this.entityId = purchaseId;
|
||||||
|
this.href = "/investment/purchases/" + purchaseId + "/redeem";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder switchInvestmentFund(final Long purchaseId) {
|
||||||
|
this.actionName = "SWITCH";
|
||||||
|
this.entityName = "INVESTMENTSALE";
|
||||||
|
this.entityId = purchaseId;
|
||||||
|
this.href = "/investment/purchases/" + purchaseId + "/switch";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder dividendInvestmentPurchase(final Long purchaseId) {
|
||||||
|
this.actionName = "DIVIDEND";
|
||||||
|
this.entityName = "INVESTMENTPURCHASE";
|
||||||
|
this.entityId = purchaseId;
|
||||||
|
this.href = "/investment/purchases/" + purchaseId + "/dividends";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder createInvestmentValuation() {
|
||||||
|
this.actionName = "CREATE";
|
||||||
|
this.entityName = "INVESTMENTVALUATION";
|
||||||
|
this.entityId = null;
|
||||||
|
this.href = "/investment/valuations";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder transferInvestmentSecurity(final Long purchaseId) {
|
||||||
|
this.actionName = "CREATE";
|
||||||
|
this.entityName = "INVESTMENTTRANSFER";
|
||||||
|
this.entityId = purchaseId;
|
||||||
|
this.href = "/investment/purchases/" + purchaseId + "/transfer";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder postInvestmentInterest(final Long purchaseId) {
|
||||||
|
this.actionName = "CREATE";
|
||||||
|
this.entityName = "INVESTMENTINTEREST";
|
||||||
|
this.entityId = purchaseId;
|
||||||
|
this.href = "/investment/purchases/" + purchaseId + "/interest-postings";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder createInvestmentDailyRate() {
|
||||||
|
this.actionName = "CREATE";
|
||||||
|
this.entityName = "INVESTMENTDAILYRATE";
|
||||||
|
this.entityId = null;
|
||||||
|
this.href = "/investment/daily-rates";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder deleteInvestmentDailyRate(final Long rateId) {
|
||||||
|
this.actionName = "DELETE";
|
||||||
|
this.entityName = "INVESTMENTDAILYRATE";
|
||||||
|
this.entityId = rateId;
|
||||||
|
this.href = "/investment/daily-rates/" + rateId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public CommandWrapperBuilder createChequeStockLot() {
|
public CommandWrapperBuilder createChequeStockLot() {
|
||||||
this.actionName = "CREATE";
|
this.actionName = "CREATE";
|
||||||
this.entityName = "CHEQUESTOCKLOT";
|
this.entityName = "CHEQUESTOCKLOT";
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import jakarta.persistence.OneToMany;
|
|||||||
import jakarta.persistence.OrderBy;
|
import jakarta.persistence.OrderBy;
|
||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
import jakarta.persistence.UniqueConstraint;
|
import jakarta.persistence.UniqueConstraint;
|
||||||
import jakarta.persistence.Version;
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -62,15 +61,6 @@ import org.apache.fineract.portfolio.client.domain.Client;
|
|||||||
@Table(name = "m_dds_agent", uniqueConstraints = { @UniqueConstraint(columnNames = { "agent_code" }, name = "agent_code_UNIQUE") })
|
@Table(name = "m_dds_agent", uniqueConstraints = { @UniqueConstraint(columnNames = { "agent_code" }, name = "agent_code_UNIQUE") })
|
||||||
public class Agent extends AbstractAuditableCustom {
|
public class Agent extends AbstractAuditableCustom {
|
||||||
|
|
||||||
/**
|
|
||||||
* Optimistic lock, as a savings account has: two saves of the same row from different requests -
|
|
||||||
* two collections at once, or a collection during a posting run - cannot both win; the second is
|
|
||||||
* refused and retried instead of quietly overwriting the first one's balance.
|
|
||||||
*/
|
|
||||||
@Version
|
|
||||||
@Column(name = "version", nullable = false)
|
|
||||||
private Long version;
|
|
||||||
|
|
||||||
@ManyToOne
|
@ManyToOne
|
||||||
@JoinColumn(name = "client_id", nullable = false)
|
@JoinColumn(name = "client_id", nullable = false)
|
||||||
private Client client;
|
private Client client;
|
||||||
|
|||||||
-10
@@ -69,16 +69,6 @@ public class CommissionSlab extends AbstractAuditableCustom {
|
|||||||
* Corrects this slab, reporting only what actually changed so the audit trail records the
|
* Corrects this slab, reporting only what actually changed so the audit trail records the
|
||||||
* correction rather than a wholesale rewrite.
|
* correction rather than a wholesale rewrite.
|
||||||
*/
|
*/
|
||||||
/** Moves where this slab starts - used when the slab before it is corrected, so the two still meet. */
|
|
||||||
public void startAt(final BigDecimal from) {
|
|
||||||
this.slabFrom = from;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Moves where this slab ends - used when the slab after it is corrected, so the two still meet. */
|
|
||||||
public void endAt(final BigDecimal to) {
|
|
||||||
this.slabTo = to;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<String, Object> update(final JsonCommand command) {
|
public Map<String, Object> update(final JsonCommand command) {
|
||||||
final Map<String, Object> actualChanges = new LinkedHashMap<>(4);
|
final Map<String, Object> actualChanges = new LinkedHashMap<>(4);
|
||||||
|
|
||||||
|
|||||||
+1
-10
@@ -41,6 +41,7 @@ public final class DdsAccountTypeData implements Serializable {
|
|||||||
private final String accountTypeDescription;
|
private final String accountTypeDescription;
|
||||||
private final Boolean autoInterestPost;
|
private final Boolean autoInterestPost;
|
||||||
private final String autoInterestPostWhen;
|
private final String autoInterestPostWhen;
|
||||||
|
private final Integer autoInterestPostDay;
|
||||||
/** Whether an account on this scheme closes itself on its maturity date. */
|
/** Whether an account on this scheme closes itself on its maturity date. */
|
||||||
private final Boolean maturityAutoClose;
|
private final Boolean maturityAutoClose;
|
||||||
/**
|
/**
|
||||||
@@ -55,16 +56,6 @@ public final class DdsAccountTypeData implements Serializable {
|
|||||||
/** The scheme's annual interest rate, which an account opened on it takes. */
|
/** The scheme's annual interest rate, which an account opened on it takes. */
|
||||||
private final BigDecimal interestRate;
|
private final BigDecimal interestRate;
|
||||||
private final Boolean active;
|
private final Boolean active;
|
||||||
/** The product's own ledger accounts (see DdsAccountType), each with its code and name to show it by. */
|
|
||||||
private final Long depositLiabilityGlId;
|
|
||||||
private final String depositLiabilityGlCode;
|
|
||||||
private final String depositLiabilityGlName;
|
|
||||||
private final Long interestExpenseGlId;
|
|
||||||
private final String interestExpenseGlCode;
|
|
||||||
private final String interestExpenseGlName;
|
|
||||||
private final Long commissionExpenseGlId;
|
|
||||||
private final String commissionExpenseGlCode;
|
|
||||||
private final String commissionExpenseGlName;
|
|
||||||
private final LocalDateTime createdDate;
|
private final LocalDateTime createdDate;
|
||||||
private final LocalDateTime lastModifiedDate;
|
private final LocalDateTime lastModifiedDate;
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-38
@@ -60,6 +60,10 @@ public class DdsAccountType extends AbstractAuditableCustom {
|
|||||||
@Column(name = "auto_interest_post_when", length = 20)
|
@Column(name = "auto_interest_post_when", length = 20)
|
||||||
private String autoInterestPostWhen;
|
private String autoInterestPostWhen;
|
||||||
|
|
||||||
|
/** Day of the month interest posts on. Only meaningful when {@code autoInterestPost} is set. */
|
||||||
|
@Column(name = "auto_interest_post_day")
|
||||||
|
private Integer autoInterestPostDay;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether an account on this scheme closes itself the day it matures, instead of waiting for a
|
* Whether an account on this scheme closes itself the day it matures, instead of waiting for a
|
||||||
* clerk to pay it out by hand.
|
* clerk to pay it out by hand.
|
||||||
@@ -115,24 +119,6 @@ public class DdsAccountType extends AbstractAuditableCustom {
|
|||||||
@Column(name = "is_active", nullable = false)
|
@Column(name = "is_active", nullable = false)
|
||||||
private boolean active;
|
private boolean active;
|
||||||
|
|
||||||
/**
|
|
||||||
* The ledger accounts this product posts through, chosen on its Accounting tab the way a savings
|
|
||||||
* product chooses its own. DDS posts through these alone - there is no bank-wide account to fall
|
|
||||||
* back on - so a product missing one stops its postings with a message naming what to set.
|
|
||||||
*
|
|
||||||
* What the bank owes this product's customers - DDS Customer Deposits, or the product's own.
|
|
||||||
*/
|
|
||||||
@Column(name = "deposit_liability_gl_id")
|
|
||||||
private Long depositLiabilityGlId;
|
|
||||||
|
|
||||||
/** What interest paid to this product's customers costs the bank. */
|
|
||||||
@Column(name = "interest_expense_gl_id")
|
|
||||||
private Long interestExpenseGlId;
|
|
||||||
|
|
||||||
/** What the agents' commission on collecting for this product costs the bank. */
|
|
||||||
@Column(name = "commission_expense_gl_id")
|
|
||||||
private Long commissionExpenseGlId;
|
|
||||||
|
|
||||||
protected DdsAccountType() {
|
protected DdsAccountType() {
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
@@ -144,14 +130,14 @@ public class DdsAccountType extends AbstractAuditableCustom {
|
|||||||
this.autoInterestPost = command.booleanPrimitiveValueOfParameterNamed("autoInterestPost");
|
this.autoInterestPost = command.booleanPrimitiveValueOfParameterNamed("autoInterestPost");
|
||||||
this.autoInterestPostWhen = StringUtils
|
this.autoInterestPostWhen = StringUtils
|
||||||
.defaultIfEmpty(command.stringValueOfParameterNamedAllowingNull("autoInterestPostWhen"), null);
|
.defaultIfEmpty(command.stringValueOfParameterNamedAllowingNull("autoInterestPostWhen"), null);
|
||||||
|
this.autoInterestPostDay = this.autoInterestPost
|
||||||
|
? command.integerValueOfParameterNamedDefaultToNullIfZero("autoInterestPostDay")
|
||||||
|
: null;
|
||||||
this.maturityAutoClose = command.booleanPrimitiveValueOfParameterNamed("maturityAutoClose");
|
this.maturityAutoClose = command.booleanPrimitiveValueOfParameterNamed("maturityAutoClose");
|
||||||
this.prematureLockinMonths = valueOr(command, "prematureLockinMonths", DEFAULT_LOCKIN_MONTHS);
|
this.prematureLockinMonths = valueOr(command, "prematureLockinMonths", DEFAULT_LOCKIN_MONTHS);
|
||||||
this.prematureInterestReduction = valueOr(command, "prematureInterestReduction", DEFAULT_INTEREST_REDUCTION);
|
this.prematureInterestReduction = valueOr(command, "prematureInterestReduction", DEFAULT_INTEREST_REDUCTION);
|
||||||
this.prematureAgentCommissionPenalty = valueOr(command, "prematureAgentCommissionPenalty", DEFAULT_AGENT_PENALTY);
|
this.prematureAgentCommissionPenalty = valueOr(command, "prematureAgentCommissionPenalty", DEFAULT_AGENT_PENALTY);
|
||||||
this.interestRate = command.bigDecimalValueOfParameterNamed("interestRate");
|
this.interestRate = command.bigDecimalValueOfParameterNamed("interestRate");
|
||||||
this.depositLiabilityGlId = command.longValueOfParameterNamed("depositLiabilityGlId");
|
|
||||||
this.interestExpenseGlId = command.longValueOfParameterNamed("interestExpenseGlId");
|
|
||||||
this.commissionExpenseGlId = command.longValueOfParameterNamed("commissionExpenseGlId");
|
|
||||||
// A newly created account type is available for use straight away.
|
// A newly created account type is available for use straight away.
|
||||||
this.active = !command.parameterExists("active") || command.booleanPrimitiveValueOfParameterNamed("active");
|
this.active = !command.parameterExists("active") || command.booleanPrimitiveValueOfParameterNamed("active");
|
||||||
}
|
}
|
||||||
@@ -226,12 +212,23 @@ public class DdsAccountType extends AbstractAuditableCustom {
|
|||||||
this.autoInterestPostWhen = StringUtils.defaultIfEmpty(newValue, null);
|
this.autoInterestPostWhen = StringUtils.defaultIfEmpty(newValue, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (command.isChangeInIntegerParameterNamed("autoInterestPostDay", this.autoInterestPostDay)) {
|
||||||
|
final Integer newValue = command.integerValueOfParameterNamedDefaultToNullIfZero("autoInterestPostDay");
|
||||||
|
actualChanges.put("autoInterestPostDay", newValue);
|
||||||
|
this.autoInterestPostDay = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
if (command.isChangeInBooleanParameterNamed("maturityAutoClose", this.maturityAutoClose)) {
|
if (command.isChangeInBooleanParameterNamed("maturityAutoClose", this.maturityAutoClose)) {
|
||||||
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("maturityAutoClose");
|
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("maturityAutoClose");
|
||||||
actualChanges.put("maturityAutoClose", newValue);
|
actualChanges.put("maturityAutoClose", newValue);
|
||||||
this.maturityAutoClose = newValue;
|
this.maturityAutoClose = newValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A posting day only means anything while auto-posting is on.
|
||||||
|
if (!this.autoInterestPost) {
|
||||||
|
this.autoInterestPostDay = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (command.isChangeInIntegerParameterNamed("prematureLockinMonths", this.prematureLockinMonths)) {
|
if (command.isChangeInIntegerParameterNamed("prematureLockinMonths", this.prematureLockinMonths)) {
|
||||||
final Integer newValue = command.integerValueOfParameterNamedDefaultToNullIfZero("prematureLockinMonths");
|
final Integer newValue = command.integerValueOfParameterNamedDefaultToNullIfZero("prematureLockinMonths");
|
||||||
actualChanges.put("prematureLockinMonths", newValue);
|
actualChanges.put("prematureLockinMonths", newValue);
|
||||||
@@ -259,23 +256,6 @@ public class DdsAccountType extends AbstractAuditableCustom {
|
|||||||
this.interestRate = newValue;
|
this.interestRate = newValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The product's ledger accounts. All three are required; the deserializer refuses a null.
|
|
||||||
if (command.parameterExists("depositLiabilityGlId")
|
|
||||||
&& command.isChangeInLongParameterNamed("depositLiabilityGlId", this.depositLiabilityGlId)) {
|
|
||||||
this.depositLiabilityGlId = command.longValueOfParameterNamed("depositLiabilityGlId");
|
|
||||||
actualChanges.put("depositLiabilityGlId", this.depositLiabilityGlId);
|
|
||||||
}
|
|
||||||
if (command.parameterExists("interestExpenseGlId")
|
|
||||||
&& command.isChangeInLongParameterNamed("interestExpenseGlId", this.interestExpenseGlId)) {
|
|
||||||
this.interestExpenseGlId = command.longValueOfParameterNamed("interestExpenseGlId");
|
|
||||||
actualChanges.put("interestExpenseGlId", this.interestExpenseGlId);
|
|
||||||
}
|
|
||||||
if (command.parameterExists("commissionExpenseGlId")
|
|
||||||
&& command.isChangeInLongParameterNamed("commissionExpenseGlId", this.commissionExpenseGlId)) {
|
|
||||||
this.commissionExpenseGlId = command.longValueOfParameterNamed("commissionExpenseGlId");
|
|
||||||
actualChanges.put("commissionExpenseGlId", this.commissionExpenseGlId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command.isChangeInBooleanParameterNamed("active", this.active)) {
|
if (command.isChangeInBooleanParameterNamed("active", this.active)) {
|
||||||
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("active");
|
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed("active");
|
||||||
actualChanges.put("active", newValue);
|
actualChanges.put("active", newValue);
|
||||||
|
|||||||
-23
@@ -25,7 +25,6 @@ import jakarta.persistence.FetchType;
|
|||||||
import jakarta.persistence.OneToMany;
|
import jakarta.persistence.OneToMany;
|
||||||
import jakarta.persistence.OrderBy;
|
import jakarta.persistence.OrderBy;
|
||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
import jakarta.persistence.Version;
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -46,15 +45,6 @@ import org.apache.fineract.infrastructure.core.domain.AbstractAuditableCustom;
|
|||||||
@Table(name = "m_dds_batch_run")
|
@Table(name = "m_dds_batch_run")
|
||||||
public class BatchRun extends AbstractAuditableCustom {
|
public class BatchRun extends AbstractAuditableCustom {
|
||||||
|
|
||||||
/**
|
|
||||||
* Optimistic lock, as a savings account has: two saves of the same row from different requests -
|
|
||||||
* two collections at once, or a collection during a posting run - cannot both win; the second is
|
|
||||||
* refused and retried instead of quietly overwriting the first one's balance.
|
|
||||||
*/
|
|
||||||
@Version
|
|
||||||
@Column(name = "version", nullable = false)
|
|
||||||
private Long version;
|
|
||||||
|
|
||||||
@Column(name = "job_type", length = 40, nullable = false)
|
@Column(name = "job_type", length = 40, nullable = false)
|
||||||
private String jobType;
|
private String jobType;
|
||||||
|
|
||||||
@@ -97,14 +87,6 @@ public class BatchRun extends AbstractAuditableCustom {
|
|||||||
@Column(name = "rows_posted", nullable = false)
|
@Column(name = "rows_posted", nullable = false)
|
||||||
private Integer rowsPosted = 0;
|
private Integer rowsPosted = 0;
|
||||||
|
|
||||||
/**
|
|
||||||
* What the run actually moved, beside totalAmount - what it was generated to move. They differ when
|
|
||||||
* rows were passed over at posting (the money had already been paid by another run), and the run
|
|
||||||
* says so rather than reading as though the whole total went through.
|
|
||||||
*/
|
|
||||||
@Column(name = "posted_amount", scale = 6, precision = 19)
|
|
||||||
private BigDecimal postedAmount;
|
|
||||||
|
|
||||||
/** Why a run stopped, when it did. A background run has nowhere else to say so. */
|
/** Why a run stopped, when it did. A background run has nowhere else to say so. */
|
||||||
@Column(name = "failure_reason", length = 500)
|
@Column(name = "failure_reason", length = 500)
|
||||||
private String failureReason;
|
private String failureReason;
|
||||||
@@ -173,11 +155,6 @@ public class BatchRun extends AbstractAuditableCustom {
|
|||||||
this.rowsPosted = posted;
|
this.rowsPosted = posted;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void recordPosted(final int posted, final BigDecimal amount) {
|
|
||||||
this.rowsPosted = posted;
|
|
||||||
this.postedAmount = amount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void markPosted(final LocalDate postedOnDate) {
|
public void markPosted(final LocalDate postedOnDate) {
|
||||||
this.status = STATUS_POSTED;
|
this.status = STATUS_POSTED;
|
||||||
this.postedOnDate = postedOnDate;
|
this.postedOnDate = postedOnDate;
|
||||||
|
|||||||
-10
@@ -23,7 +23,6 @@ import jakarta.persistence.Entity;
|
|||||||
import jakarta.persistence.JoinColumn;
|
import jakarta.persistence.JoinColumn;
|
||||||
import jakarta.persistence.ManyToOne;
|
import jakarta.persistence.ManyToOne;
|
||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
import jakarta.persistence.Version;
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@@ -50,15 +49,6 @@ import org.apache.fineract.portfolio.dds.party.domain.Party;
|
|||||||
@Table(name = "m_dds_collection_entry")
|
@Table(name = "m_dds_collection_entry")
|
||||||
public class CollectionEntry extends AbstractAuditableCustom {
|
public class CollectionEntry extends AbstractAuditableCustom {
|
||||||
|
|
||||||
/**
|
|
||||||
* Optimistic lock, as a savings account has: two saves of the same row from different requests -
|
|
||||||
* two collections at once, or a collection during a posting run - cannot both win; the second is
|
|
||||||
* refused and retried instead of quietly overwriting the first one's balance.
|
|
||||||
*/
|
|
||||||
@Version
|
|
||||||
@Column(name = "version", nullable = false)
|
|
||||||
private Long version;
|
|
||||||
|
|
||||||
/** Waiting for a clerk to count the cash behind it. */
|
/** Waiting for a clerk to count the cash behind it. */
|
||||||
public static final String PENDING = "PENDING";
|
public static final String PENDING = "PENDING";
|
||||||
|
|
||||||
|
|||||||
-6
@@ -178,11 +178,5 @@ public final class PartyData implements Serializable {
|
|||||||
private final String nomineeSplitMode;
|
private final String nomineeSplitMode;
|
||||||
private final String status;
|
private final String status;
|
||||||
private final LocalDate closeDate;
|
private final LocalDate closeDate;
|
||||||
/** The product by id, the account currency, and who opened (made operative) and closed it - as a savings account shows. */
|
|
||||||
private final Long accountTypeId;
|
|
||||||
private final String currencyCode;
|
|
||||||
private final LocalDate activatedOnDate;
|
|
||||||
private final String activatedByUsername;
|
|
||||||
private final String closedByUsername;
|
|
||||||
private final Collection<PartyNomineeData> nominees;
|
private final Collection<PartyNomineeData> nominees;
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-54
@@ -28,7 +28,6 @@ import jakarta.persistence.OneToMany;
|
|||||||
import jakarta.persistence.OrderBy;
|
import jakarta.persistence.OrderBy;
|
||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
import jakarta.persistence.UniqueConstraint;
|
import jakarta.persistence.UniqueConstraint;
|
||||||
import jakarta.persistence.Version;
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.MathContext;
|
import java.math.MathContext;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
@@ -61,15 +60,6 @@ import org.apache.fineract.portfolio.client.domain.Client;
|
|||||||
@Table(name = "m_dds_party", uniqueConstraints = { @UniqueConstraint(columnNames = { "party_code" }, name = "party_code_UNIQUE") })
|
@Table(name = "m_dds_party", uniqueConstraints = { @UniqueConstraint(columnNames = { "party_code" }, name = "party_code_UNIQUE") })
|
||||||
public class Party extends AbstractAuditableCustom {
|
public class Party extends AbstractAuditableCustom {
|
||||||
|
|
||||||
/**
|
|
||||||
* Optimistic lock, as a savings account has: two saves of the same row from different requests -
|
|
||||||
* two collections at once, or a collection during a posting run - cannot both win; the second is
|
|
||||||
* refused and retried instead of quietly overwriting the first one's balance.
|
|
||||||
*/
|
|
||||||
@Version
|
|
||||||
@Column(name = "version", nullable = false)
|
|
||||||
private Long version;
|
|
||||||
|
|
||||||
/** DDS accounts carry a flat interest rate unless the officer explicitly overrides it at creation. */
|
/** DDS accounts carry a flat interest rate unless the officer explicitly overrides it at creation. */
|
||||||
private static final BigDecimal DEFAULT_INTEREST_RATE = new BigDecimal("2");
|
private static final BigDecimal DEFAULT_INTEREST_RATE = new BigDecimal("2");
|
||||||
|
|
||||||
@@ -314,29 +304,6 @@ public class Party extends AbstractAuditableCustom {
|
|||||||
@Column(name = "close_date")
|
@Column(name = "close_date")
|
||||||
private LocalDate closeDate;
|
private LocalDate closeDate;
|
||||||
|
|
||||||
/**
|
|
||||||
* The DDS product the account is on, by id - subType carries its code for the screens, but a code
|
|
||||||
* can be renamed and an id cannot, so this is what ties the account to its product's terms and
|
|
||||||
* ledger accounts (as m_savings_account.product_id does).
|
|
||||||
*/
|
|
||||||
@Column(name = "account_type_id")
|
|
||||||
private Long accountTypeId;
|
|
||||||
|
|
||||||
/** The account's currency, as a savings account records its own - set from the bank's currency on opening. */
|
|
||||||
@Column(name = "currency_code", length = 3)
|
|
||||||
private String currencyCode;
|
|
||||||
|
|
||||||
/** When the account became operative, and who made it so - the checker, where the opening was approved. */
|
|
||||||
@Column(name = "activatedon_date")
|
|
||||||
private LocalDate activatedOnDate;
|
|
||||||
|
|
||||||
@Column(name = "activatedon_userid")
|
|
||||||
private Long activatedByUserId;
|
|
||||||
|
|
||||||
/** Who closed the account, beside closeDate - when it was closed. */
|
|
||||||
@Column(name = "closedon_userid")
|
|
||||||
private Long closedByUserId;
|
|
||||||
|
|
||||||
@OneToMany(mappedBy = "party", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
|
@OneToMany(mappedBy = "party", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
|
||||||
@OrderBy("id")
|
@OrderBy("id")
|
||||||
private List<PartyNominee> nominees = new ArrayList<>();
|
private List<PartyNominee> nominees = new ArrayList<>();
|
||||||
@@ -525,13 +492,10 @@ public class Party extends AbstractAuditableCustom {
|
|||||||
this.savingsAccountId = savingsAccountId;
|
this.savingsAccountId = savingsAccountId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Numbers the account from its id, the same way 0514 numbered the accounts already open. */
|
||||||
* Numbers the account from the DDS account serial (m_dds_account_number_serial), not from its id: an
|
public void assignAccountNo() {
|
||||||
* id is spent by every save that rolls back, and the bank's account numbers must run without gaps.
|
if (this.accountNo == null && getId() != null) {
|
||||||
*/
|
this.accountNo = "DDS" + String.format("%09d", getId());
|
||||||
public void assignAccountNo(final long serial) {
|
|
||||||
if (this.accountNo == null) {
|
|
||||||
this.accountNo = "DDS" + String.format("%09d", serial);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -575,22 +539,9 @@ public class Party extends AbstractAuditableCustom {
|
|||||||
* is settled to zero (a full Withdraw Deposite/payout) before it is closed, not closed with money
|
* is settled to zero (a full Withdraw Deposite/payout) before it is closed, not closed with money
|
||||||
* still owed to it.
|
* still owed to it.
|
||||||
*/
|
*/
|
||||||
public void close(final LocalDate closeDate, final Long closedBy) {
|
public void close(final LocalDate closeDate) {
|
||||||
this.status = STATUS_CLOSED;
|
this.status = STATUS_CLOSED;
|
||||||
this.closeDate = closeDate;
|
this.closeDate = closeDate;
|
||||||
this.closedByUserId = closedBy;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Ties the account to its product by id; called whenever subType is set or changed. */
|
|
||||||
public void linkProduct(final Long productId) {
|
|
||||||
this.accountTypeId = productId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Stamps the opening: the account's currency, and the day and user that made it operative. */
|
|
||||||
public void recordActivation(final String currency, final LocalDate on, final Long by) {
|
|
||||||
this.currencyCode = currency;
|
|
||||||
this.activatedOnDate = on;
|
|
||||||
this.activatedByUserId = by;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
-12
@@ -22,7 +22,6 @@ import java.io.Serial;
|
|||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
||||||
@@ -52,15 +51,4 @@ public final class PartyTransactionData implements Serializable {
|
|||||||
private final BigDecimal balanceAfterTransaction;
|
private final BigDecimal balanceAfterTransaction;
|
||||||
/** The DDS voucher this line moved under, e.g. DDS/29092026/0001; empty on lines from before vouchers. */
|
/** The DDS voucher this line moved under, e.g. DDS/29092026/0001; empty on lines from before vouchers. */
|
||||||
private final String voucherNo;
|
private final String voucherNo;
|
||||||
/** The branch the line is booked at, the business day it was recorded, and the journal it was booked under. */
|
|
||||||
private final Long officeId;
|
|
||||||
private final String officeName;
|
|
||||||
private final LocalDate submittedOnDate;
|
|
||||||
private final String journalTransactionId;
|
|
||||||
/** The line's own unique reference (m_dds_party_transaction.ref_no). */
|
|
||||||
private final String refNo;
|
|
||||||
/** Who keyed the line and when, as a savings transaction shows it. */
|
|
||||||
private final String createdByUsername;
|
|
||||||
private final LocalDateTime createdDate;
|
|
||||||
private final String agentName;
|
|
||||||
}
|
}
|
||||||
|
|||||||
-58
@@ -23,14 +23,11 @@ import jakarta.persistence.Entity;
|
|||||||
import jakarta.persistence.JoinColumn;
|
import jakarta.persistence.JoinColumn;
|
||||||
import jakarta.persistence.ManyToOne;
|
import jakarta.persistence.ManyToOne;
|
||||||
import jakarta.persistence.PostPersist;
|
import jakarta.persistence.PostPersist;
|
||||||
import jakarta.persistence.PrePersist;
|
|
||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.UUID;
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableCustom;
|
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableCustom;
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
|
||||||
import org.apache.fineract.organisation.agent.domain.Agent;
|
import org.apache.fineract.organisation.agent.domain.Agent;
|
||||||
import org.apache.fineract.portfolio.dds.party.domain.Party;
|
import org.apache.fineract.portfolio.dds.party.domain.Party;
|
||||||
import org.apache.fineract.portfolio.dds.voucher.DdsVoucherScope;
|
import org.apache.fineract.portfolio.dds.voucher.DdsVoucherScope;
|
||||||
@@ -90,36 +87,6 @@ public class PartyTransaction extends AbstractAuditableCustom {
|
|||||||
@Column(name = "balance_after_transaction", scale = 6, precision = 19, nullable = false)
|
@Column(name = "balance_after_transaction", scale = 6, precision = 19, nullable = false)
|
||||||
private BigDecimal balanceAfterTransaction;
|
private BigDecimal balanceAfterTransaction;
|
||||||
|
|
||||||
/** The branch the line is booked at - the party's, or its agent's for a line with no party - as savings rows carry. */
|
|
||||||
@Column(name = "office_id")
|
|
||||||
private Long officeId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The business day the line was recorded, beside transactionDate - the day the money moved. They
|
|
||||||
* differ when a collection is applied from a cashier's receipt days after it was collected.
|
|
||||||
*/
|
|
||||||
@Column(name = "submitted_on_date")
|
|
||||||
private LocalDate submittedOnDate;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The acc_gl_journal_entry.transaction_id the line was booked under, so a passbook line and its
|
|
||||||
* journal can be found from each other. Null for a line that moves nothing on the books (a transfer
|
|
||||||
* between two customers owed on the same account).
|
|
||||||
*/
|
|
||||||
@Column(name = "journal_transaction_id", length = 100)
|
|
||||||
private String journalTransactionId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The line's own reference, unique across DDS, as a savings transaction carries ref_no: what a
|
|
||||||
* statement, a complaint or a reconciliation quotes to name exactly one line.
|
|
||||||
*/
|
|
||||||
@Column(name = "ref_no", length = 40, nullable = false, unique = true)
|
|
||||||
private String refNo;
|
|
||||||
|
|
||||||
/** The voucher the line was printed on - stamped by DdsVoucherService once the operation's voucher is raised. */
|
|
||||||
@Column(name = "voucher_no", length = 30, insertable = false, updatable = false)
|
|
||||||
private String voucherNo;
|
|
||||||
|
|
||||||
protected PartyTransaction() {
|
protected PartyTransaction() {
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
@@ -141,31 +108,6 @@ public class PartyTransaction extends AbstractAuditableCustom {
|
|||||||
this.tokenNo = tokenNo;
|
this.tokenNo = tokenNo;
|
||||||
this.description = description;
|
this.description = description;
|
||||||
this.balanceAfterTransaction = balanceAfterTransaction;
|
this.balanceAfterTransaction = balanceAfterTransaction;
|
||||||
if (party != null && party.getOffice() != null) {
|
|
||||||
this.officeId = party.getOffice().getId();
|
|
||||||
} else if (agent != null && agent.getOffice() != null) {
|
|
||||||
this.officeId = agent.getOffice().getId();
|
|
||||||
} else if (agent != null && agent.getClient() != null && agent.getClient().getOffice() != null) {
|
|
||||||
// An agent saved without a branch is still a client of one.
|
|
||||||
this.officeId = agent.getClient().getOffice().getId();
|
|
||||||
}
|
|
||||||
this.submittedOnDate = DateUtils.getBusinessLocalDate();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Names the journal this line was booked under, where the caller knows it better than the operation's last one. */
|
|
||||||
public PartyTransaction linkJournal(final String transactionId) {
|
|
||||||
this.journalTransactionId = transactionId;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PrePersist
|
|
||||||
private void linkToJournal() {
|
|
||||||
if (this.refNo == null) {
|
|
||||||
this.refNo = "DDST" + UUID.randomUUID().toString().replace("-", "").substring(0, 20).toUpperCase();
|
|
||||||
}
|
|
||||||
if (this.journalTransactionId == null) {
|
|
||||||
this.journalTransactionId = DdsVoucherScope.lastLedgerTransaction();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
-20
@@ -48,8 +48,6 @@ public final class DdsVoucherScope {
|
|||||||
private final Set<String> ledgerTransactionIds = new LinkedHashSet<>();
|
private final Set<String> ledgerTransactionIds = new LinkedHashSet<>();
|
||||||
private final List<Long> partyTransactionIds = new ArrayList<>();
|
private final List<Long> partyTransactionIds = new ArrayList<>();
|
||||||
private final List<Line> extraLines = new ArrayList<>();
|
private final List<Line> extraLines = new ArrayList<>();
|
||||||
/** The journal most recently written in this operation - what the next passbook line saved belongs to. */
|
|
||||||
private String lastLedgerTransactionId;
|
|
||||||
|
|
||||||
private DdsVoucherScope() {}
|
private DdsVoucherScope() {}
|
||||||
|
|
||||||
@@ -74,24 +72,6 @@ public final class DdsVoucherScope {
|
|||||||
final DdsVoucherScope scope = CURRENT.get();
|
final DdsVoucherScope scope = CURRENT.get();
|
||||||
if (scope != null && transactionId != null) {
|
if (scope != null && transactionId != null) {
|
||||||
scope.ledgerTransactionIds.add(transactionId);
|
scope.ledgerTransactionIds.add(transactionId);
|
||||||
scope.lastLedgerTransactionId = transactionId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The journal the passbook line being saved now belongs to: every DDS movement writes its journal
|
|
||||||
* first and its passbook line straight after, so it is the last one written in this operation.
|
|
||||||
*/
|
|
||||||
public static String lastLedgerTransaction() {
|
|
||||||
final DdsVoucherScope scope = CURRENT.get();
|
|
||||||
return scope == null ? null : scope.lastLedgerTransactionId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** For a movement that writes no journal, so its passbook line does not claim the previous one's. */
|
|
||||||
public static void forgetLastLedgerTransaction() {
|
|
||||||
final DdsVoucherScope scope = CURRENT.get();
|
|
||||||
if (scope != null) {
|
|
||||||
scope.lastLedgerTransactionId = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-9
@@ -303,13 +303,9 @@ public class AgentWritePlatformServiceJpaRepositoryImpl implements AgentWritePla
|
|||||||
+ "join m_savings_product p on p.id = a.product_id "
|
+ "join m_savings_product p on p.id = a.product_id "
|
||||||
+ "where a.account_no = ? and a.status_enum = 300";
|
+ "where a.account_no = ? and a.status_enum = 300";
|
||||||
|
|
||||||
/**
|
/** The product DDS opens customers' scheme accounts under - see 0492. */
|
||||||
* Whether a savings account is a DDS customer's scheme account - one a DDS party is linked to.
|
private static final String DDS_DEPOSIT_PRODUCT_SQL = "select value from c_configuration "
|
||||||
* Read off the parties themselves rather than a configured product id: what makes an account a
|
+ "where name = 'dds-deposit-product' and enabled = true";
|
||||||
* scheme account is that a scheme runs on it, whatever product it was opened under.
|
|
||||||
*/
|
|
||||||
private static final String DDS_SCHEME_ACCOUNT_SQL = "select count(*) from m_dds_party p "
|
|
||||||
+ "join m_savings_account a on a.id = p.savings_account_id where a.account_no = ?";
|
|
||||||
|
|
||||||
private static final int SAVINGS_DEPOSIT_TYPE = 100;
|
private static final int SAVINGS_DEPOSIT_TYPE = 100;
|
||||||
|
|
||||||
@@ -357,8 +353,9 @@ public class AgentWritePlatformServiceJpaRepositoryImpl implements AgentWritePla
|
|||||||
// A DDS scheme account is a savings account too, so the check above lets it through - but it
|
// A DDS scheme account is a savings account too, so the check above lets it through - but it
|
||||||
// belongs to a customer's scheme, and commission paid into it would be counted as that
|
// belongs to a customer's scheme, and commission paid into it would be counted as that
|
||||||
// customer's deposits by every DDS interest run from then on.
|
// customer's deposits by every DDS interest run from then on.
|
||||||
final Integer schemes = this.jdbcTemplate.queryForObject(DDS_SCHEME_ACCOUNT_SQL, Integer.class, accountNo); // NOSONAR
|
final Long productId = ((Number) account.get("product_id")).longValue();
|
||||||
if (schemes != null && schemes > 0) {
|
final List<Long> ddsProduct = this.jdbcTemplate.queryForList(DDS_DEPOSIT_PRODUCT_SQL, Long.class); // NOSONAR
|
||||||
|
if (!ddsProduct.isEmpty() && productId.equals(ddsProduct.get(0))) {
|
||||||
throw new AgentSavingsAccountNotOperativeException(accountNo, "DDS scheme");
|
throw new AgentSavingsAccountNotOperativeException(accountNo, "DDS scheme");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-4
@@ -44,8 +44,6 @@ public final class CommissionSlabCommandFromApiJsonDeserializer {
|
|||||||
// A List, not a Set: isOneOfTheseStringValues only lowercases both sides in its List overload.
|
// A List, not a Set: isOneOfTheseStringValues only lowercases both sides in its List overload.
|
||||||
private static final List<String> SUPPORTED_SLAB_TYPES = List.of("ACCOUNT", "COMMISSION");
|
private static final List<String> SUPPORTED_SLAB_TYPES = List.of("ACCOUNT", "COMMISSION");
|
||||||
|
|
||||||
private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);
|
|
||||||
|
|
||||||
private static final Set<String> SUPPORTED_PARAMETERS = new HashSet<>(Arrays.asList("slabs", "dateFormat", "locale"));
|
private static final Set<String> SUPPORTED_PARAMETERS = new HashSet<>(Arrays.asList("slabs", "dateFormat", "locale"));
|
||||||
|
|
||||||
private static final Set<String> SUPPORTED_UPDATE_PARAMETERS = new HashSet<>(
|
private static final Set<String> SUPPORTED_UPDATE_PARAMETERS = new HashSet<>(
|
||||||
@@ -95,7 +93,7 @@ public final class CommissionSlabCommandFromApiJsonDeserializer {
|
|||||||
v.reset().parameterAtIndexArray("slabTo", i).value(extractBigDecimal(slabJson, "slabTo")).notNull()
|
v.reset().parameterAtIndexArray("slabTo", i).value(extractBigDecimal(slabJson, "slabTo")).notNull()
|
||||||
.zeroOrPositiveAmount();
|
.zeroOrPositiveAmount();
|
||||||
v.reset().parameterAtIndexArray("commissionPercent", i).value(extractBigDecimal(slabJson, "commissionPercent"))
|
v.reset().parameterAtIndexArray("commissionPercent", i).value(extractBigDecimal(slabJson, "commissionPercent"))
|
||||||
.notNull().zeroOrPositiveAmount().notGreaterThanMax(HUNDRED);
|
.notNull().zeroOrPositiveAmount();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +134,7 @@ public final class CommissionSlabCommandFromApiJsonDeserializer {
|
|||||||
if (this.fromApiJsonHelper.parameterExists("commissionPercent", element)) {
|
if (this.fromApiJsonHelper.parameterExists("commissionPercent", element)) {
|
||||||
v.reset().parameter("commissionPercent")
|
v.reset().parameter("commissionPercent")
|
||||||
.value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("commissionPercent", element)).notNull()
|
.value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("commissionPercent", element)).notNull()
|
||||||
.zeroOrPositiveAmount().notGreaterThanMax(HUNDRED);
|
.zeroOrPositiveAmount();
|
||||||
}
|
}
|
||||||
|
|
||||||
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
||||||
|
|||||||
-103
@@ -21,21 +21,16 @@ package org.apache.fineract.organisation.commissionslab.service;
|
|||||||
import com.google.gson.JsonArray;
|
import com.google.gson.JsonArray;
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
import jakarta.persistence.PersistenceException;
|
import jakarta.persistence.PersistenceException;
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.math.RoundingMode;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
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.CommandProcessingResult;
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||||
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
|
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
|
||||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
|
||||||
import org.apache.fineract.organisation.commissionslab.domain.CommissionSlab;
|
import org.apache.fineract.organisation.commissionslab.domain.CommissionSlab;
|
||||||
import org.apache.fineract.organisation.commissionslab.domain.CommissionSlabRepository;
|
import org.apache.fineract.organisation.commissionslab.domain.CommissionSlabRepository;
|
||||||
import org.apache.fineract.organisation.commissionslab.exception.CommissionSlabNotFoundException;
|
import org.apache.fineract.organisation.commissionslab.exception.CommissionSlabNotFoundException;
|
||||||
@@ -73,14 +68,6 @@ public class CommissionSlabWritePlatformServiceJpaRepositoryImpl implements Comm
|
|||||||
slabs.add(slab);
|
slabs.add(slab);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The chart each type will have once these are added - what is stored plus this request -
|
|
||||||
// must still read as one unbroken scale from 0.
|
|
||||||
for (final String type : slabs.stream().map(CommissionSlab::getSlabType).distinct().toList()) {
|
|
||||||
final List<CommissionSlab> chart = new ArrayList<>(this.commissionSlabRepository.findByType(type));
|
|
||||||
chart.addAll(slabs.stream().filter(s -> s.getSlabType().equals(type)).toList());
|
|
||||||
rejectBrokenChart(type, chart);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.commissionSlabRepository.saveAllAndFlush(slabs);
|
this.commissionSlabRepository.saveAllAndFlush(slabs);
|
||||||
|
|
||||||
return new CommandProcessingResultBuilder() //
|
return new CommandProcessingResultBuilder() //
|
||||||
@@ -106,9 +93,6 @@ public class CommissionSlabWritePlatformServiceJpaRepositoryImpl implements Comm
|
|||||||
|
|
||||||
final CommissionSlab slab = this.commissionSlabRepository.findById(slabId)
|
final CommissionSlab slab = this.commissionSlabRepository.findById(slabId)
|
||||||
.orElseThrow(() -> new CommissionSlabNotFoundException(slabId));
|
.orElseThrow(() -> new CommissionSlabNotFoundException(slabId));
|
||||||
final String oldType = slab.getSlabType();
|
|
||||||
final BigDecimal oldFrom = slab.getSlabFrom();
|
|
||||||
final BigDecimal oldTo = slab.getSlabTo();
|
|
||||||
final Map<String, Object> changes = slab.update(command);
|
final Map<String, Object> changes = slab.update(command);
|
||||||
|
|
||||||
// Re-checked after the change, against every other slab of its type. A correction can just as
|
// Re-checked after the change, against every other slab of its type. A correction can just as
|
||||||
@@ -117,38 +101,8 @@ public class CommissionSlabWritePlatformServiceJpaRepositoryImpl implements Comm
|
|||||||
if (!changes.isEmpty()) {
|
if (!changes.isEmpty()) {
|
||||||
final List<CommissionSlab> others = this.commissionSlabRepository.findByType(slab.getSlabType()).stream()
|
final List<CommissionSlab> others = this.commissionSlabRepository.findByType(slab.getSlabType()).stream()
|
||||||
.filter(other -> !other.getId().equals(slab.getId())).toList();
|
.filter(other -> !other.getId().equals(slab.getId())).toList();
|
||||||
|
|
||||||
// A boundary moved within the same chart takes its neighbour with it, so the two slabs still
|
|
||||||
// meet a paisa apart and the chart stays unbroken: 10,000 -> 12,000 on this slab's To moves
|
|
||||||
// the next slab's From from 10,000.01 to 12,000.01.
|
|
||||||
final List<CommissionSlab> moved = new ArrayList<>();
|
|
||||||
if (oldType.equals(slab.getSlabType())) {
|
|
||||||
for (final CommissionSlab other : others) {
|
|
||||||
if (slab.getSlabTo().compareTo(oldTo) != 0 && other.getSlabFrom().compareTo(oldTo.add(PAISA)) == 0) {
|
|
||||||
other.startAt(slab.getSlabTo().add(PAISA));
|
|
||||||
moved.add(other);
|
|
||||||
}
|
|
||||||
if (slab.getSlabFrom().compareTo(oldFrom) != 0 && other.getSlabTo().compareTo(oldFrom.subtract(PAISA)) == 0) {
|
|
||||||
other.endAt(slab.getSlabFrom().subtract(PAISA));
|
|
||||||
moved.add(other);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rejectIfOverlapping(slab, others);
|
rejectIfOverlapping(slab, others);
|
||||||
final List<CommissionSlab> chart = new ArrayList<>(others);
|
|
||||||
chart.add(slab);
|
|
||||||
rejectBrokenChart(slab.getSlabType(), chart);
|
|
||||||
if (!oldType.equals(slab.getSlabType())) {
|
|
||||||
// Moved to the other type: the chart it left must still be whole too.
|
|
||||||
rejectBrokenChart(oldType, this.commissionSlabRepository.findByType(oldType).stream()
|
|
||||||
.filter(other -> !other.getId().equals(slab.getId())).toList());
|
|
||||||
}
|
|
||||||
this.commissionSlabRepository.saveAll(moved);
|
|
||||||
this.commissionSlabRepository.saveAndFlush(slab);
|
this.commissionSlabRepository.saveAndFlush(slab);
|
||||||
if (!moved.isEmpty()) {
|
|
||||||
changes.put("neighboursAdjusted", moved.size());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(slabId).with(changes).build();
|
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(slabId).with(changes).build();
|
||||||
@@ -162,63 +116,6 @@ public class CommissionSlabWritePlatformServiceJpaRepositoryImpl implements Comm
|
|||||||
* explain it. Ranges are inclusive at both ends, so touching endpoints count as an overlap: a
|
* explain it. Ranges are inclusive at both ends, so touching endpoints count as an overlap: a
|
||||||
* slab ending at 50000 and the next starting at 50000 both match exactly 50000.
|
* slab ending at 50000 and the next starting at 50000 both match exactly 50000.
|
||||||
*/
|
*/
|
||||||
/** Collections are rupees and paise, so consecutive slabs meet one paisa apart. */
|
|
||||||
private static final BigDecimal PAISA = new BigDecimal("0.01");
|
|
||||||
private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A chart the commission run can always read: every slab From <= To, a percentage of 0 to 100, money
|
|
||||||
* to the paisa, starting at 0, and each slab beginning exactly a paisa after the one before ends.
|
|
||||||
*
|
|
||||||
* The run matches from <= collection <= to and stops with "no commission slab" for an agent whose
|
|
||||||
* collection no slab covers - so a gap, or a chart starting above 0, is not a gentler setting, it is
|
|
||||||
* a run that fails. Refused here, when the chart is saved, rather than discovered on payday.
|
|
||||||
*/
|
|
||||||
private void rejectBrokenChart(final String type, final List<CommissionSlab> chart) {
|
|
||||||
final List<CommissionSlab> sorted = chart.stream().sorted(Comparator.comparing(CommissionSlab::getSlabFrom)).toList();
|
|
||||||
final List<ApiParameterError> errors = new ArrayList<>();
|
|
||||||
for (int i = 0; i < sorted.size(); i++) {
|
|
||||||
final CommissionSlab slab = sorted.get(i);
|
|
||||||
final String range = plain(slab.getSlabFrom()) + " - " + plain(slab.getSlabTo());
|
|
||||||
if (slab.getSlabFrom().compareTo(slab.getSlabTo()) > 0) {
|
|
||||||
errors.add(chartError("from.after.to", type + " slab " + range + ": From is more than To.", "slabFrom"));
|
|
||||||
}
|
|
||||||
if (slab.getSlabFrom().stripTrailingZeros().scale() > 2 || slab.getSlabTo().stripTrailingZeros().scale() > 2) {
|
|
||||||
errors.add(chartError("paise", type + " slab " + range + ": amounts can have at most two decimals (paise).", "slabFrom"));
|
|
||||||
}
|
|
||||||
if (slab.getCommissionPercent().signum() < 0 || slab.getCommissionPercent().compareTo(HUNDRED) > 0) {
|
|
||||||
errors.add(chartError("percent", type + " slab " + range + ": the percentage must be from 0 to 100.",
|
|
||||||
"commissionPercent"));
|
|
||||||
}
|
|
||||||
if (i == 0) {
|
|
||||||
if (slab.getSlabFrom().signum() != 0) {
|
|
||||||
errors.add(chartError("not.from.zero", type + " slabs must start at 0 - nothing covers 0 to "
|
|
||||||
+ plain(slab.getSlabFrom().subtract(PAISA)) + ".", "slabFrom"));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
final BigDecimal expected = sorted.get(i - 1).getSlabTo().add(PAISA);
|
|
||||||
if (slab.getSlabFrom().compareTo(expected) > 0) {
|
|
||||||
errors.add(chartError("gap", type + " slabs leave a gap: nothing covers " + plain(expected) + " to "
|
|
||||||
+ plain(slab.getSlabFrom().subtract(PAISA)) + ". Start this slab at " + plain(expected) + ".",
|
|
||||||
"slabFrom"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!errors.isEmpty()) {
|
|
||||||
throw new PlatformApiDataValidationException("error.msg.commissionslab.chart.broken",
|
|
||||||
"The " + type + " commission chart would not cover every collection: " + errors.get(0).getDefaultUserMessage(),
|
|
||||||
errors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ApiParameterError chartError(final String code, final String message, final String parameter) {
|
|
||||||
return ApiParameterError.parameterError("error.msg.commissionslab." + code, message, parameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String plain(final BigDecimal value) {
|
|
||||||
return value.setScale(2, RoundingMode.HALF_UP).toPlainString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void rejectIfOverlapping(final CommissionSlab candidate, final List<CommissionSlab> existing) {
|
private void rejectIfOverlapping(final CommissionSlab candidate, final List<CommissionSlab> existing) {
|
||||||
for (final CommissionSlab other : existing) {
|
for (final CommissionSlab other : existing) {
|
||||||
if (!other.getSlabType().equals(candidate.getSlabType())) {
|
if (!other.getSlabType().equals(candidate.getSlabType())) {
|
||||||
|
|||||||
+6
-37
@@ -29,32 +29,6 @@ public final class ApprovalCounterRegistry {
|
|||||||
private static final String DDS_ENTITIES = "'PARTY','AGENT','PARTYTRANSACTION','DDSACCOUNTTYPE','COMMISSIONSLAB',"
|
private static final String DDS_ENTITIES = "'PARTY','AGENT','PARTYTRANSACTION','DDSACCOUNTTYPE','COMMISSIONSLAB',"
|
||||||
+ "'DDSBATCH','DDSCOLLECTIONENTRY'";
|
+ "'DDSBATCH','DDSCOLLECTIONENTRY'";
|
||||||
|
|
||||||
/** DDS commands held for a checker, as the DDS approval queue reads them. Aliased {@code c}. */
|
|
||||||
private static final String DDS_PENDING = "FROM m_portfolio_command_source c JOIN m_permission p "
|
|
||||||
+ "ON p.code = c.action_name || '_' || c.entity_name AND p.can_maker_checker = true WHERE c.status = 2 ";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DDS product and DDS account requests, listed under Product Approval and Pending Account with every
|
|
||||||
* other product and account rather than under DDS Approvals or the Checker Inbox. Written for any
|
|
||||||
* alias, since the Checker Inbox counts with {@code aud} and the DDS counts with {@code c}.
|
|
||||||
*/
|
|
||||||
private static String ddsProductRequest(final String alias) {
|
|
||||||
return "(" + alias + ".entity_name = 'DDSACCOUNTTYPE' AND " + alias + ".action_name IN ('CREATE','UPDATE'))";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String ddsAccountRequest(final String alias) {
|
|
||||||
return "(" + alias + ".entity_name = 'PARTY' AND " + alias + ".action_name IN ('CREATE','UPDATE'))";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A DDS agent request of any kind - opening, editing or closing one. An agent has no voucher and no
|
|
||||||
* amount, so these wait under their own Agent Approval tab rather than among the money entries of
|
|
||||||
* DDS Approvals or the Checker Inbox.
|
|
||||||
*/
|
|
||||||
private static String ddsAgentRequest(final String alias) {
|
|
||||||
return "(" + alias + ".entity_name = 'AGENT')";
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Maker-checker commands awaiting a checker, limited to the offices and checker rights the caller has. */
|
/** Maker-checker commands awaiting a checker, limited to the offices and checker rights the caller has. */
|
||||||
private static final String MC_PENDING = "FROM m_portfolio_command_source aud WHERE aud.status = 2 "
|
private static final String MC_PENDING = "FROM m_portfolio_command_source aud WHERE aud.status = 2 "
|
||||||
+ "AND (:ho OR EXISTS (SELECT 1 FROM m_office o2 WHERE o2.id = aud.office_id AND o2.hierarchy LIKE :h)) "
|
+ "AND (:ho OR EXISTS (SELECT 1 FROM m_office o2 WHERE o2.id = aud.office_id AND o2.hierarchy LIKE :h)) "
|
||||||
@@ -75,9 +49,7 @@ public final class ApprovalCounterRegistry {
|
|||||||
// ── Fineract's own maker-checker and pending lists ────────────────────────────
|
// ── Fineract's own maker-checker and pending lists ────────────────────────────
|
||||||
new ApprovalCounter("checker-inbox", "ALL_FUNCTIONS_READ",
|
new ApprovalCounter("checker-inbox", "ALL_FUNCTIONS_READ",
|
||||||
"SELECT count(*) " + MC_PENDING + "AND upper(aud.entity_name) NOT IN (" + PRODUCT_ENTITIES + ") "
|
"SELECT count(*) " + MC_PENDING + "AND upper(aud.entity_name) NOT IN (" + PRODUCT_ENTITIES + ") "
|
||||||
+ "AND NOT (upper(aud.entity_name) IN (" + STAGED_ACCOUNT_ENTITIES + ") AND upper(aud.action_name) = 'UPDATE') "
|
+ "AND NOT (upper(aud.entity_name) IN (" + STAGED_ACCOUNT_ENTITIES + ") AND upper(aud.action_name) = 'UPDATE')"),
|
||||||
+ "AND NOT " + ddsProductRequest("aud") + " AND NOT " + ddsAccountRequest("aud")
|
|
||||||
+ " AND NOT " + ddsAgentRequest("aud")),
|
|
||||||
new ApprovalCounter("client-approval", "READ_CLIENT",
|
new ApprovalCounter("client-approval", "READ_CLIENT",
|
||||||
"SELECT count(*) FROM m_client c WHERE c.status_enum = 100 "
|
"SELECT count(*) FROM m_client c WHERE c.status_enum = 100 "
|
||||||
+ "AND EXISTS (SELECT 1 FROM m_office o WHERE o.id = c.office_id AND o.hierarchy LIKE :h)"),
|
+ "AND EXISTS (SELECT 1 FROM m_office o WHERE o.id = c.office_id AND o.hierarchy LIKE :h)"),
|
||||||
@@ -95,16 +67,14 @@ public final class ApprovalCounterRegistry {
|
|||||||
+ "AND COALESCE(s.terminate_true, false) = false AND " + CLIENT_OFFICE
|
+ "AND COALESCE(s.terminate_true, false) = false AND " + CLIENT_OFFICE
|
||||||
+ ") + (SELECT count(*) FROM m_current_account ca WHERE ca.status_enum = 100 "
|
+ ") + (SELECT count(*) FROM m_current_account ca WHERE ca.status_enum = 100 "
|
||||||
+ "AND COALESCE(ca.terminate_true, false) = false "
|
+ "AND COALESCE(ca.terminate_true, false) = false "
|
||||||
+ "AND EXISTS (SELECT 1 FROM m_office o WHERE o.id = ca.office_id AND o.hierarchy LIKE :h)) "
|
+ "AND EXISTS (SELECT 1 FROM m_office o WHERE o.id = ca.office_id AND o.hierarchy LIKE :h))"),
|
||||||
+ "+ (SELECT count(*) " + DDS_PENDING + "AND " + ddsAccountRequest("c") + ")"),
|
|
||||||
new ApprovalCounter("pending-share-accounts", "ALL_FUNCTIONS_READ",
|
new ApprovalCounter("pending-share-accounts", "ALL_FUNCTIONS_READ",
|
||||||
"SELECT count(*) FROM m_share_account WHERE status_enum = 100"),
|
"SELECT count(*) FROM m_share_account WHERE status_enum = 100"),
|
||||||
new ApprovalCounter("product-approval", "ALL_FUNCTIONS_READ",
|
new ApprovalCounter("product-approval", "ALL_FUNCTIONS_READ",
|
||||||
"SELECT (SELECT count(*) FROM m_saving_product_temp WHERE status = 'PENDING' "
|
"SELECT (SELECT count(*) FROM m_saving_product_temp WHERE status = 'PENDING' "
|
||||||
+ "AND product_type IN ('SAVINGS','CURRENT','SHARE','FD','RD')) + (SELECT count(*) " + MC_PENDING
|
+ "AND product_type IN ('SAVINGS','CURRENT','SHARE','FD','RD')) + (SELECT count(*) " + MC_PENDING
|
||||||
+ "AND upper(aud.entity_name) IN (" + PRODUCT_ENTITIES + ") "
|
+ "AND upper(aud.entity_name) IN (" + PRODUCT_ENTITIES + ") "
|
||||||
+ "AND NOT (upper(aud.entity_name) IN (" + STAGED_PRODUCT_ENTITIES + ") AND upper(aud.action_name) = 'UPDATE')) "
|
+ "AND NOT (upper(aud.entity_name) IN (" + STAGED_PRODUCT_ENTITIES + ") AND upper(aud.action_name) = 'UPDATE'))"),
|
||||||
+ "+ (SELECT count(*) " + DDS_PENDING + "AND " + ddsProductRequest("c") + ")"),
|
|
||||||
new ApprovalCounter("pending-savings-account-edits", "ALL_FUNCTIONS_READ",
|
new ApprovalCounter("pending-savings-account-edits", "ALL_FUNCTIONS_READ",
|
||||||
"SELECT (SELECT count(*) FROM m_savings_account_pending_edit WHERE status = 'PENDING') "
|
"SELECT (SELECT count(*) FROM m_savings_account_pending_edit WHERE status = 'PENDING') "
|
||||||
+ "+ (SELECT count(*) FROM m_current_account_pending_edit WHERE status = 'PENDING') "
|
+ "+ (SELECT count(*) FROM m_current_account_pending_edit WHERE status = 'PENDING') "
|
||||||
@@ -134,10 +104,9 @@ public final class ApprovalCounterRegistry {
|
|||||||
new ApprovalCounter(DISBURSEMENT_REQUESTS, "ALL_FUNCTIONS_READ",
|
new ApprovalCounter(DISBURSEMENT_REQUESTS, "ALL_FUNCTIONS_READ",
|
||||||
"SELECT count(*) FROM m_clerk_disbursement_request WHERE status = 'PENDING'"),
|
"SELECT count(*) FROM m_clerk_disbursement_request WHERE status = 'PENDING'"),
|
||||||
new ApprovalCounter("dds-approvals", null,
|
new ApprovalCounter("dds-approvals", null,
|
||||||
"SELECT count(*) " + DDS_PENDING + "AND c.entity_name IN (" + DDS_ENTITIES + ") "
|
"SELECT count(*) FROM m_portfolio_command_source c JOIN m_permission p "
|
||||||
+ "AND NOT " + ddsProductRequest("c") + " AND NOT " + ddsAccountRequest("c")
|
+ "ON p.code = c.action_name || '_' || c.entity_name AND p.can_maker_checker = true "
|
||||||
+ " AND NOT " + ddsAgentRequest("c")),
|
+ "WHERE c.status = 2 AND c.entity_name IN (" + DDS_ENTITIES + ")"),
|
||||||
new ApprovalCounter("agent-approvals", null, "SELECT count(*) " + DDS_PENDING + "AND " + ddsAgentRequest("c")),
|
|
||||||
new ApprovalCounter("chequebook-requests", "READ_CHEQUEBOOKPENDINGREQUEST",
|
new ApprovalCounter("chequebook-requests", "READ_CHEQUEBOOKPENDINGREQUEST",
|
||||||
"SELECT count(*) FROM m_chequebook_pending_request WHERE status = 'PENDING'"),
|
"SELECT count(*) FROM m_chequebook_pending_request WHERE status = 'PENDING'"),
|
||||||
new ApprovalCounter("closure-requests", "ALL_FUNCTIONS_READ",
|
new ApprovalCounter("closure-requests", "ALL_FUNCTIONS_READ",
|
||||||
|
|||||||
+17
@@ -46,6 +46,8 @@ import org.apache.fineract.infrastructure.core.service.DateUtils;
|
|||||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
import org.apache.fineract.portfolio.cashreport.service.VaultPositionService;
|
import org.apache.fineract.portfolio.cashreport.service.VaultPositionService;
|
||||||
import org.apache.fineract.portfolio.denominationmaster.service.CountedCashLedger;
|
import org.apache.fineract.portfolio.denominationmaster.service.CountedCashLedger;
|
||||||
|
import org.apache.fineract.portfolio.investment.service.InvestmentFdInterestService;
|
||||||
|
import org.apache.fineract.portfolio.investment.service.InvestmentPurchaseReadPlatformService;
|
||||||
import org.apache.fineract.portfolio.savings.dailyinterest.SavingsDailyInterestOrchestrator;
|
import org.apache.fineract.portfolio.savings.dailyinterest.SavingsDailyInterestOrchestrator;
|
||||||
import org.apache.fineract.portfolio.savings.dailyinterest.SavingsInterestPostingReportService;
|
import org.apache.fineract.portfolio.savings.dailyinterest.SavingsInterestPostingReportService;
|
||||||
import org.apache.fineract.portfolio.savings.fdinterest.FixedDepositInterestPostingOrchestrator;
|
import org.apache.fineract.portfolio.savings.fdinterest.FixedDepositInterestPostingOrchestrator;
|
||||||
@@ -95,8 +97,16 @@ public class BranchDayRegisterApiResource {
|
|||||||
private final SavingsDailyInterestOrchestrator savingsDailyInterestOrchestrator;
|
private final SavingsDailyInterestOrchestrator savingsDailyInterestOrchestrator;
|
||||||
private final FixedDepositInterestPostingReportService fdInterestPostingReportService;
|
private final FixedDepositInterestPostingReportService fdInterestPostingReportService;
|
||||||
private final FixedDepositInterestPostingOrchestrator fdInterestPostingOrchestrator;
|
private final FixedDepositInterestPostingOrchestrator fdInterestPostingOrchestrator;
|
||||||
|
private final InvestmentPurchaseReadPlatformService investmentPurchaseReadPlatformService;
|
||||||
|
private final InvestmentFdInterestService investmentFdInterestService;
|
||||||
private final Gson gson = new Gson();
|
private final Gson gson = new Gson();
|
||||||
|
|
||||||
|
private boolean isHeadOffice(final Long officeId) {
|
||||||
|
final Integer topLevel = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM m_office WHERE id = ? AND parent_id IS NULL",
|
||||||
|
Integer.class, officeId);
|
||||||
|
return topLevel != null && topLevel > 0;
|
||||||
|
}
|
||||||
|
|
||||||
// ── GET /v1/branchday ── the register itself ──────────────────────────────
|
// ── GET /v1/branchday ── the register itself ──────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -260,6 +270,13 @@ public class BranchDayRegisterApiResource {
|
|||||||
final Map<String, Object> fdInterest = fdInterestPostingReportService.dayCloseInterestView(officeId, businessDate);
|
final Map<String, Object> fdInterest = fdInterestPostingReportService.dayCloseInterestView(officeId, businessDate);
|
||||||
result.put("fdInterestPending", fdInterest.get("pending"));
|
result.put("fdInterestPending", fdInterest.get("pending"));
|
||||||
result.put("fdInterestDue", fdInterest.get("due"));
|
result.put("fdInterestDue", fdInterest.get("due"));
|
||||||
|
// Investment interest vouchers (coupon / provision) whose date has come but which are not posted yet.
|
||||||
|
// A reminder only: it never stops the close (Jitendra Sir). Investments are booked at Head Office, so
|
||||||
|
// only the Head Office close shows it.
|
||||||
|
result.put("investmentInterestPending",
|
||||||
|
isHeadOffice(officeId) ? investmentPurchaseReadPlatformService.retrieveDueInterest(0) : List.of());
|
||||||
|
// The same for FDs placed with other banks: interest vouchers and maturities due. A reminder only.
|
||||||
|
result.put("investmentFdPending", isHeadOffice(officeId) ? investmentFdInterestService.due(0) : List.of());
|
||||||
|
|
||||||
return gson.toJson(result);
|
return gson.toJson(result);
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-5
@@ -53,16 +53,14 @@ public class CustomerSearchApiResource {
|
|||||||
@GET
|
@GET
|
||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
@Operation(summary = "Search for a customer", description = "AND-combines whichever of clientId/name/mobileNo/accountNumber/aadhaarNumber/panNumber/address/accountTypes are supplied - at least one is required. accountTypes is a comma-separated list (SAVINGS,CURRENT,FIXED_DEPOSIT,RECURRING_DEPOSIT).")
|
@Operation(summary = "Search for a customer", description = "AND-combines whichever of name/accountNumber/aadhaarNumber/panNumber/address/accountTypes are supplied - at least one is required. accountTypes is a comma-separated list (SAVINGS,CURRENT,FIXED_DEPOSIT,RECURRING_DEPOSIT).")
|
||||||
public Collection<CustomerSearchResultData> search(@QueryParam("clientId") final String clientId,
|
public Collection<CustomerSearchResultData> search(@QueryParam("name") final String name,
|
||||||
@QueryParam("name") final String name, @QueryParam("mobileNo") final String mobileNo,
|
|
||||||
@QueryParam("accountNumber") final String accountNumber, @QueryParam("aadhaarNumber") final String aadhaarNumber,
|
@QueryParam("accountNumber") final String accountNumber, @QueryParam("aadhaarNumber") final String aadhaarNumber,
|
||||||
@QueryParam("panNumber") final String panNumber, @QueryParam("address") final String address,
|
@QueryParam("panNumber") final String panNumber, @QueryParam("address") final String address,
|
||||||
@QueryParam("accountTypes") final String accountTypes) {
|
@QueryParam("accountTypes") final String accountTypes) {
|
||||||
final List<String> accountTypeList = accountTypes == null || accountTypes.isBlank() ? List.of()
|
final List<String> accountTypeList = accountTypes == null || accountTypes.isBlank() ? List.of()
|
||||||
: Arrays.asList(accountTypes.split(","));
|
: Arrays.asList(accountTypes.split(","));
|
||||||
return safely(() -> this.readPlatformService.search(clientId, name, mobileNo, accountNumber, aadhaarNumber, panNumber, address,
|
return safely(() -> this.readPlatformService.search(name, accountNumber, aadhaarNumber, panNumber, address, accountTypeList));
|
||||||
accountTypeList));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Surfaces IllegalArgumentException as a clean 400 instead of an opaque 500. */
|
/** Surfaces IllegalArgumentException as a clean 400 instead of an opaque 500. */
|
||||||
|
|||||||
+3
-4
@@ -27,13 +27,12 @@ public interface CustomerSearchReadPlatformService {
|
|||||||
/**
|
/**
|
||||||
* Multi-field AND search - only non-blank parameters are applied. At least one must be
|
* Multi-field AND search - only non-blank parameters are applied. At least one must be
|
||||||
* supplied. name/address are partial, case-insensitive matches; accountNumber/aadhaarNumber/
|
* supplied. name/address are partial, case-insensitive matches; accountNumber/aadhaarNumber/
|
||||||
* panNumber are exact matches (they're precise identifiers, not free text). clientId is an exact match
|
* panNumber are exact matches (they're precise identifiers, not free text). accountTypes, if
|
||||||
* on the client's id and mobileNo a partial match on their mobile number. accountTypes, if
|
|
||||||
* non-empty, restricts results to those account types only (SAVINGS/CURRENT/FIXED_DEPOSIT/
|
* non-empty, restricts results to those account types only (SAVINGS/CURRENT/FIXED_DEPOSIT/
|
||||||
* RECURRING_DEPOSIT) - a client with no account, or only accounts of other types, is excluded
|
* RECURRING_DEPOSIT) - a client with no account, or only accounts of other types, is excluded
|
||||||
* entirely when this filter is active (unlike the unfiltered case, where a client with no
|
* entirely when this filter is active (unlike the unfiltered case, where a client with no
|
||||||
* account at all still appears once for informational purposes).
|
* account at all still appears once for informational purposes).
|
||||||
*/
|
*/
|
||||||
Collection<CustomerSearchResultData> search(String clientId, String name, String mobileNo, String accountNumber, String aadhaarNumber,
|
Collection<CustomerSearchResultData> search(String name, String accountNumber, String aadhaarNumber, String panNumber,
|
||||||
String panNumber, String address, List<String> accountTypes);
|
String address, List<String> accountTypes);
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-28
@@ -69,11 +69,8 @@ public class CustomerSearchReadPlatformServiceImpl implements CustomerSearchRead
|
|||||||
""";
|
""";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<CustomerSearchResultData> search(final String clientId, final String name, final String mobileNo,
|
public Collection<CustomerSearchResultData> search(final String name, final String accountNumber, final String aadhaarNumber,
|
||||||
final String accountNumber, final String aadhaarNumber, final String panNumber, final String address,
|
final String panNumber, final String address, final List<String> accountTypes) {
|
||||||
final List<String> accountTypes) {
|
|
||||||
final boolean hasClientId = clientId != null && !clientId.isBlank();
|
|
||||||
final boolean hasMobile = mobileNo != null && !mobileNo.isBlank();
|
|
||||||
final boolean hasName = name != null && !name.isBlank();
|
final boolean hasName = name != null && !name.isBlank();
|
||||||
final boolean hasAccountNumber = accountNumber != null && !accountNumber.isBlank();
|
final boolean hasAccountNumber = accountNumber != null && !accountNumber.isBlank();
|
||||||
final boolean hasAadhaar = aadhaarNumber != null && !aadhaarNumber.isBlank();
|
final boolean hasAadhaar = aadhaarNumber != null && !aadhaarNumber.isBlank();
|
||||||
@@ -82,12 +79,9 @@ public class CustomerSearchReadPlatformServiceImpl implements CustomerSearchRead
|
|||||||
final List<String> effectiveAccountTypes = accountTypes == null ? List.of()
|
final List<String> effectiveAccountTypes = accountTypes == null ? List.of()
|
||||||
: accountTypes.stream().filter(t -> t != null && !t.isBlank()).map(String::trim).map(String::toUpperCase).toList();
|
: accountTypes.stream().filter(t -> t != null && !t.isBlank()).map(String::trim).map(String::toUpperCase).toList();
|
||||||
|
|
||||||
if (!hasClientId && !hasMobile && !hasName && !hasAccountNumber && !hasAadhaar && !hasPan && !hasAddress
|
if (!hasName && !hasAccountNumber && !hasAadhaar && !hasPan && !hasAddress && effectiveAccountTypes.isEmpty()) {
|
||||||
&& effectiveAccountTypes.isEmpty()) {
|
throw new IllegalArgumentException("Enter at least one search field - name, account number, Aadhaar, PAN, or address.");
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Enter at least one search field - client ID, name, mobile number, account number, Aadhaar, PAN, or address.");
|
|
||||||
}
|
}
|
||||||
final Long clientIdValue = hasClientId ? parseClientId(clientId) : null;
|
|
||||||
for (final String type : effectiveAccountTypes) {
|
for (final String type : effectiveAccountTypes) {
|
||||||
if (!VALID_ACCOUNT_TYPES.contains(type)) {
|
if (!VALID_ACCOUNT_TYPES.contains(type)) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
@@ -98,19 +92,10 @@ public class CustomerSearchReadPlatformServiceImpl implements CustomerSearchRead
|
|||||||
final StringBuilder sql = new StringBuilder(BASE_SELECT).append(" WHERE 1=1 ");
|
final StringBuilder sql = new StringBuilder(BASE_SELECT).append(" WHERE 1=1 ");
|
||||||
final List<Object> params = new ArrayList<>();
|
final List<Object> params = new ArrayList<>();
|
||||||
|
|
||||||
if (hasClientId) {
|
|
||||||
sql.append(" AND c.id = ? ");
|
|
||||||
params.add(clientIdValue);
|
|
||||||
}
|
|
||||||
if (hasName) {
|
if (hasName) {
|
||||||
sql.append(" AND LOWER(c.display_name) LIKE LOWER(?) ");
|
sql.append(" AND LOWER(c.display_name) LIKE LOWER(?) ");
|
||||||
params.add("%" + name.trim() + "%");
|
params.add("%" + name.trim() + "%");
|
||||||
}
|
}
|
||||||
if (hasMobile) {
|
|
||||||
// Partial, so the last digits of a number find it as well as the whole of it.
|
|
||||||
sql.append(" AND c.mobile_no LIKE ? ");
|
|
||||||
params.add("%" + mobileNo.trim() + "%");
|
|
||||||
}
|
|
||||||
if (hasAccountNumber) {
|
if (hasAccountNumber) {
|
||||||
sql.append(" AND acct.account_no = ? ");
|
sql.append(" AND acct.account_no = ? ");
|
||||||
params.add(accountNumber.trim());
|
params.add(accountNumber.trim());
|
||||||
@@ -146,15 +131,6 @@ public class CustomerSearchReadPlatformServiceImpl implements CustomerSearchRead
|
|||||||
return this.jdbcTemplate.query(sql.toString(), new CustomerSearchResultMapper(), params.toArray());
|
return this.jdbcTemplate.query(sql.toString(), new CustomerSearchResultMapper(), params.toArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A client id is a whole number; anything else is the caller's mistake to be told about, not a server error. */
|
|
||||||
private static Long parseClientId(final String clientId) {
|
|
||||||
try {
|
|
||||||
return Long.valueOf(clientId.trim());
|
|
||||||
} catch (final NumberFormatException e) {
|
|
||||||
throw new IllegalArgumentException("Client ID must be a number.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class CustomerSearchResultMapper implements RowMapper<CustomerSearchResultData> {
|
private static final class CustomerSearchResultMapper implements RowMapper<CustomerSearchResultData> {
|
||||||
@Override
|
@Override
|
||||||
public CustomerSearchResultData mapRow(final ResultSet rs, final int rowNum) throws SQLException {
|
public CustomerSearchResultData mapRow(final ResultSet rs, final int rowNum) throws SQLException {
|
||||||
|
|||||||
+148
-258
@@ -19,13 +19,9 @@
|
|||||||
package org.apache.fineract.portfolio.dds.accounting;
|
package org.apache.fineract.portfolio.dds.accounting;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
|
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
|
||||||
@@ -87,16 +83,35 @@ import org.springframework.stereotype.Component;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class DdsLedger {
|
public class DdsLedger {
|
||||||
|
|
||||||
|
/** Cash an agent has collected but not yet handed to a branch. Set by 0493. */
|
||||||
|
public static final String AGENT_CASH = "dds-agent-cash-account";
|
||||||
|
|
||||||
|
/** What an agent's commission costs the bank. Set by 0491. */
|
||||||
|
public static final String COMMISSION_EXPENSE = "dds-agent-commission-expense-account";
|
||||||
|
|
||||||
|
/** The savings product DDS opens customer accounts under. Set by 0492. */
|
||||||
|
public static final String DEPOSIT_PRODUCT = "dds-deposit-product";
|
||||||
|
|
||||||
|
/** What DDS customers are owed on accounts that are DDS's own rather than savings accounts. Set by 0516. */
|
||||||
|
public static final String DEPOSIT_LIABILITY = "dds-deposit-liability-account";
|
||||||
|
|
||||||
|
/** What interest on DDS's own accounts costs the bank. Set by 0518. */
|
||||||
|
public static final String INTEREST_EXPENSE = "dds-interest-expense-account";
|
||||||
|
|
||||||
private static final String ORGANISATION_CURRENCY_SQL = "select code from m_organisation_currency order by id limit 1";
|
private static final String ORGANISATION_CURRENCY_SQL = "select code from m_organisation_currency order by id limit 1";
|
||||||
|
|
||||||
|
private static final String PRODUCT_CURRENCY_SQL = "select currency_code from m_savings_product where id = ?";
|
||||||
|
|
||||||
|
private static final String CONFIGURED_ACCOUNT_SQL = "select value from c_configuration where name = ? and enabled = true";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The accounts money can be parked in for a customer with no account here. Every bank names its
|
* The accounts money can be parked in for a customer with no account here. Every bank names its
|
||||||
* Sundry account differently, so none is assumed: every open posting liability account is offered
|
* Sundry account differently, so none is assumed: every open posting liability account is offered
|
||||||
* and the clerk searches it by code or name and picks their bank's own. Liability, because the bank
|
* and the clerk searches it by code or name and picks their bank's own. Liability, because the bank
|
||||||
* now owes the money - an income or expense account would book it as the bank's own. Posting,
|
* now owes the money - an income or expense account would book it as the bank's own. Posting,
|
||||||
* because a header cannot take an entry. And never an account whose balance is meant to equal the
|
* because a header cannot take an entry. And never an account whose balance is meant to equal the
|
||||||
* sum of customer or agent accounts - a DDS product's customer deposits account, a savings or
|
* sum of customer or agent accounts - DDS Customer Deposits, a savings or deposit product's control
|
||||||
* deposit product's control account, an agent's own cash account - since parked money in any of
|
* account, a current-account control, an agent's own cash account - since parked money in any of
|
||||||
* those would leave it no longer adding up.
|
* those would leave it no longer adding up.
|
||||||
*/
|
*/
|
||||||
private static final String SUNDRY_ACCOUNTS_SQL = "select g.id as \"id\", g.gl_code as \"glCode\", g.name as \"name\" "
|
private static final String SUNDRY_ACCOUNTS_SQL = "select g.id as \"id\", g.gl_code as \"glCode\", g.name as \"name\" "
|
||||||
@@ -105,7 +120,8 @@ public class DdsLedger {
|
|||||||
+ "and g.id not in (select m.gl_account_id from acc_product_mapping m where m.product_type = 2 "
|
+ "and g.id not in (select m.gl_account_id from acc_product_mapping m where m.product_type = 2 "
|
||||||
+ "and m.financial_account_type = 2 and m.gl_account_id is not null) "
|
+ "and m.financial_account_type = 2 and m.gl_account_id is not null) "
|
||||||
+ "and g.id not in (select a.gl_account_id from m_dds_agent a where a.gl_account_id is not null) "
|
+ "and g.id not in (select a.gl_account_id from m_dds_agent a where a.gl_account_id is not null) "
|
||||||
+ "and g.id not in (select t.deposit_liability_gl_id from m_dds_account_type t where t.deposit_liability_gl_id is not null) "
|
+ "and g.id not in (select c.value from c_configuration c where c.name in ('dds-deposit-liability-account', "
|
||||||
|
+ "'dds-current-account-control-account', 'dds-agent-cash-account') and c.value is not null) "
|
||||||
+ "order by g.gl_code";
|
+ "order by g.gl_code";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -148,7 +164,7 @@ public class DdsLedger {
|
|||||||
* Deposits, so a transfer between them writes no leg, yet the slip still has to name both.
|
* Deposits, so a transfer between them writes no leg, yet the slip still has to name both.
|
||||||
*/
|
*/
|
||||||
public void voucherSideOnDeposits(final String entryType, final Party party, final BigDecimal amount, final String description) {
|
public void voucherSideOnDeposits(final String entryType, final Party party, final BigDecimal amount, final String description) {
|
||||||
DdsVoucherScope.addLine(new DdsVoucherScope.Line(entryType, depositLiabilityOf(party).getId(),
|
DdsVoucherScope.addLine(new DdsVoucherScope.Line(entryType, configured(DEPOSIT_LIABILITY).getId(),
|
||||||
party.getPartyCode() + " · " + party.getPartyName(), party.getAccountNo(), amount, description));
|
party.getPartyCode() + " · " + party.getPartyName(), party.getAccountNo(), amount, description));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,6 +183,17 @@ public class DdsLedger {
|
|||||||
return deposit;
|
return deposit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves cash from an agent's hands into a branch. No customer account is involved - the customers
|
||||||
|
* were credited when their money was collected; this is the bank catching up with the cash.
|
||||||
|
*/
|
||||||
|
public void settle(final Office office, final LocalDate date, final BigDecimal amount, final String description,
|
||||||
|
final GLAccount agentCashAccount) {
|
||||||
|
final Long productId = depositProductId();
|
||||||
|
postPair(office, currencyOf(productId), date, amount, productAccount(productId, FUND_SOURCE), agentCashOr(agentCashAccount),
|
||||||
|
description, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hands the customers of one application their money from the cashier's DDS receipts, in one entry.
|
* Hands the customers of one application their money from the cashier's DDS receipts, in one entry.
|
||||||
*
|
*
|
||||||
@@ -180,13 +207,11 @@ public class DdsLedger {
|
|||||||
* DR the account the receipts credited the agent's account clears
|
* DR the account the receipts credited the agent's account clears
|
||||||
* CR DDS Customer Deposits what the bank now owes the customers
|
* CR DDS Customer Deposits what the bank now owes the customers
|
||||||
* </pre>
|
* </pre>
|
||||||
*
|
|
||||||
* One pair per customer-deposits account rather than one in all: each DDS product names its own
|
|
||||||
* (see depositLiabilityOf), so customers on two products are owed on two accounts.
|
|
||||||
*/
|
*/
|
||||||
public void applyAgentReceipts(final Office office, final LocalDate date, final BigDecimal amount, final GLAccount receiptAccount,
|
public void applyAgentReceipts(final Office office, final LocalDate date, final BigDecimal amount, final GLAccount receiptAccount,
|
||||||
final GLAccount depositAccount, final String transactionId, final String description) {
|
final String transactionId, final String description) {
|
||||||
writePair(office, organisationCurrency(), transactionId, date, amount, receiptAccount, depositAccount, description, null, null);
|
writePair(office, organisationCurrency(), transactionId, date, amount, receiptAccount, configured(DEPOSIT_LIABILITY), description,
|
||||||
|
null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -217,7 +242,7 @@ public class DdsLedger {
|
|||||||
collect(party.getSavingsAccountId(), date, amount, description, agentCashAccount);
|
collect(party.getSavingsAccountId(), date, amount, description, agentCashAccount);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ddsPair(party, date, amount, agentCashOr(agentCashAccount), depositLiabilityOf(party), description);
|
ddsPair(party, date, amount, agentCashOr(agentCashAccount), configured(DEPOSIT_LIABILITY), description);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Interest onto the party's account: Dr interest expense, Cr what the customer is owed. */
|
/** Interest onto the party's account: Dr interest expense, Cr what the customer is owed. */
|
||||||
@@ -226,7 +251,7 @@ public class DdsLedger {
|
|||||||
postInterest(party.getSavingsAccountId(), date, amount, description);
|
postInterest(party.getSavingsAccountId(), date, amount, description);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ddsPair(party, date, amount, interestExpenseOf(party), depositLiabilityOf(party), description);
|
ddsPair(party, date, amount, configured(INTEREST_EXPENSE), configured(DEPOSIT_LIABILITY), description);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Interest taken back on an early closure - the exact opposite of {@link #postInterest(Party, LocalDate, BigDecimal, String)}. */
|
/** Interest taken back on an early closure - the exact opposite of {@link #postInterest(Party, LocalDate, BigDecimal, String)}. */
|
||||||
@@ -235,7 +260,7 @@ public class DdsLedger {
|
|||||||
reverseInterest(party.getSavingsAccountId(), date, amount, description);
|
reverseInterest(party.getSavingsAccountId(), date, amount, description);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ddsPair(party, date, amount, depositLiabilityOf(party), interestExpenseOf(party), description);
|
ddsPair(party, date, amount, configured(DEPOSIT_LIABILITY), configured(INTEREST_EXPENSE), description);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -250,7 +275,7 @@ public class DdsLedger {
|
|||||||
}
|
}
|
||||||
final SavingsAccount to = load(toSavingsAccountId);
|
final SavingsAccount to = load(toSavingsAccountId);
|
||||||
final SavingsAccountTransaction deposit = depositWithoutAccounting(to, date, amount);
|
final SavingsAccountTransaction deposit = depositWithoutAccounting(to, date, amount);
|
||||||
postPair(to, deposit, date, amount, depositLiabilityOf(party), controlAccountOf(to), description);
|
postPair(to, deposit, date, amount, configured(DEPOSIT_LIABILITY), controlAccountOf(to), description);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -270,23 +295,24 @@ public class DdsLedger {
|
|||||||
final String description) {
|
final String description) {
|
||||||
final GLAccount owed;
|
final GLAccount owed;
|
||||||
final Office office;
|
final Office office;
|
||||||
Long withdrawalId = null;
|
|
||||||
Long savingsAccountId = null;
|
|
||||||
if (party.getSavingsAccountId() != null) {
|
if (party.getSavingsAccountId() != null) {
|
||||||
final SavingsAccount account = load(party.getSavingsAccountId());
|
final SavingsAccount account = load(party.getSavingsAccountId());
|
||||||
withdrawalId = withdraw(account, date, amount).getId();
|
final boolean wasBlocked = unlockDebitsIfBlocked(account);
|
||||||
savingsAccountId = account.getId();
|
final SavingsTransactionBooleanValues flags = new SavingsTransactionBooleanValues(false, true, false, false, false, true);
|
||||||
|
this.savingsAccountDomainService.handleWithdrawal(account, DateUtils.DEFAULT_DATE_FORMATTER, date, amount, null, flags,
|
||||||
|
this.savingsAccountAssembler.getPivotConfigStatus());
|
||||||
|
relockDebits(account, wasBlocked);
|
||||||
owed = controlAccountOf(account);
|
owed = controlAccountOf(account);
|
||||||
office = account.office();
|
office = account.office();
|
||||||
} else {
|
} else {
|
||||||
owed = depositLiabilityOf(party);
|
owed = configured(DEPOSIT_LIABILITY);
|
||||||
office = officeOf(party);
|
office = party.getOffice() != null ? party.getOffice() : party.getAgent().getOffice();
|
||||||
}
|
}
|
||||||
|
|
||||||
creditCurrentAccount(currentAccountId, date, amount, description);
|
creditCurrentAccount(currentAccountId, date, amount, description);
|
||||||
final String transactionId = withdrawalId != null ? "S" + withdrawalId : partyLegId(party);
|
final String transactionId = "DDS-" + party.getPartyCode() + "-" + UUID.randomUUID().toString().substring(0, 8);
|
||||||
writePair(office, organisationCurrency(), transactionId, date, amount, owed, currentAccountControl(currentAccountId), description,
|
writePair(office, organisationCurrency(), transactionId, date, amount, owed, currentAccountControl(currentAccountId), description,
|
||||||
withdrawalId, savingsAccountId);
|
null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String CURRENT_ACCOUNT_SQL = "select status_enum, account_balance_derived, office_id from m_current_account where id = ?";
|
private static final String CURRENT_ACCOUNT_SQL = "select status_enum, account_balance_derived, office_id from m_current_account where id = ?";
|
||||||
@@ -303,6 +329,9 @@ public class DdsLedger {
|
|||||||
+ " join acc_product_mapping apm on apm.product_id = ca.product_id "
|
+ " join acc_product_mapping apm on apm.product_id = ca.product_id "
|
||||||
+ " where ca.id = ? and apm.product_type = 2 and apm.financial_account_type = 2 and apm.payment_type is null";
|
+ " where ca.id = ? and apm.product_type = 2 and apm.financial_account_type = 2 and apm.payment_type is null";
|
||||||
|
|
||||||
|
private static final String CURRENT_CONTROL_OVERRIDE_SQL = "select value from c_configuration "
|
||||||
|
+ " where name = 'dds-current-account-control-account' and enabled = true";
|
||||||
|
|
||||||
/** A deposit row and the balances on the current account, refused unless it is active. */
|
/** A deposit row and the balances on the current account, refused unless it is active. */
|
||||||
private void creditCurrentAccount(final Long currentAccountId, final LocalDate date, final BigDecimal amount, final String description) {
|
private void creditCurrentAccount(final Long currentAccountId, final LocalDate date, final BigDecimal amount, final String description) {
|
||||||
final List<java.util.Map<String, Object>> rows = this.jdbcTemplate.queryForList(CURRENT_ACCOUNT_SQL, currentAccountId); // NOSONAR
|
final List<java.util.Map<String, Object>> rows = this.jdbcTemplate.queryForList(CURRENT_ACCOUNT_SQL, currentAccountId); // NOSONAR
|
||||||
@@ -319,16 +348,23 @@ public class DdsLedger {
|
|||||||
this.jdbcTemplate.update(CURRENT_BALANCE_SQL, amount, amount, currentAccountId); // NOSONAR
|
this.jdbcTemplate.update(CURRENT_BALANCE_SQL, amount, amount, currentAccountId); // NOSONAR
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The current account's control account, from the current product's own accounting. */
|
/**
|
||||||
|
* The current account's control account: its product's mapping when that is the only one, and the
|
||||||
|
* setting when the product shares an id - and so its mapping rows - with a savings product.
|
||||||
|
*/
|
||||||
private GLAccount currentAccountControl(final Long currentAccountId) {
|
private GLAccount currentAccountControl(final Long currentAccountId) {
|
||||||
final List<Long> ids = this.jdbcTemplate.queryForList(CURRENT_CONTROL_SQL, Long.class, currentAccountId); // NOSONAR
|
final List<Long> ids = this.jdbcTemplate.queryForList(CURRENT_CONTROL_SQL, Long.class, currentAccountId); // NOSONAR
|
||||||
if (ids.size() == 1) {
|
if (ids.size() == 1) {
|
||||||
return this.glAccountRepository.findById(ids.get(0))
|
return this.glAccountRepository.findById(ids.get(0))
|
||||||
.orElseThrow(() -> new DdsLedgerNotConfiguredException("control account for current account " + currentAccountId));
|
.orElseThrow(() -> new DdsLedgerNotConfiguredException("control account for current account " + currentAccountId));
|
||||||
}
|
}
|
||||||
|
final List<Long> override = this.jdbcTemplate.queryForList(CURRENT_CONTROL_OVERRIDE_SQL, Long.class); // NOSONAR
|
||||||
|
if (!override.isEmpty() && override.get(0) != null) {
|
||||||
|
return this.glAccountRepository.findById(override.get(0))
|
||||||
|
.orElseThrow(() -> new DdsLedgerNotConfiguredException("dds-current-account-control-account"));
|
||||||
|
}
|
||||||
throw new DdsLedgerNotConfiguredException(ids.isEmpty() ? "control account for current account " + currentAccountId
|
throw new DdsLedgerNotConfiguredException(ids.isEmpty() ? "control account for current account " + currentAccountId
|
||||||
: "a single control account for current account " + currentAccountId
|
: "dds-current-account-control-account (the current product's GL mapping is shared with a savings product of the same id)");
|
||||||
+ " (its current product's GL mapping is shared with a savings product of the same id)");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The party's balance parked in the Sundry account chosen for it - see sundryAccount. */
|
/** The party's balance parked in the Sundry account chosen for it - see sundryAccount. */
|
||||||
@@ -338,7 +374,7 @@ public class DdsLedger {
|
|||||||
payOutToPayable(party.getSavingsAccountId(), date, amount, description, parkIn);
|
payOutToPayable(party.getSavingsAccountId(), date, amount, description, parkIn);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ddsPair(party, date, amount, depositLiabilityOf(party), parkIn, description);
|
ddsPair(party, date, amount, configured(DEPOSIT_LIABILITY), parkIn, description);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The accounts a customer's balance may be parked in, by GL code. None is chosen for the clerk. */
|
/** The accounts a customer's balance may be parked in, by GL code. None is chosen for the clerk. */
|
||||||
@@ -381,191 +417,44 @@ public class DdsLedger {
|
|||||||
transfer(from.getSavingsAccountId(), to.getSavingsAccountId(), date, amount, description);
|
transfer(from.getSavingsAccountId(), to.getSavingsAccountId(), date, amount, description);
|
||||||
} else if (fromSavings) {
|
} else if (fromSavings) {
|
||||||
final SavingsAccount account = load(from.getSavingsAccountId());
|
final SavingsAccount account = load(from.getSavingsAccountId());
|
||||||
final SavingsAccountTransaction withdrawal = withdraw(account, date, amount);
|
final boolean wasBlocked = unlockDebitsIfBlocked(account);
|
||||||
|
final SavingsTransactionBooleanValues flags = new SavingsTransactionBooleanValues(false, true, false, false, false, true);
|
||||||
|
this.savingsAccountDomainService.handleWithdrawal(account, DateUtils.DEFAULT_DATE_FORMATTER, date, amount, null, flags,
|
||||||
|
this.savingsAccountAssembler.getPivotConfigStatus());
|
||||||
|
relockDebits(account, wasBlocked);
|
||||||
postPair(account.office(), account.getCurrency().getCode(), date, amount, controlAccountOf(account),
|
postPair(account.office(), account.getCurrency().getCode(), date, amount, controlAccountOf(account),
|
||||||
depositLiabilityOf(from), description, withdrawal.getId(), account.getId());
|
configured(DEPOSIT_LIABILITY), description, null, account.getId());
|
||||||
} else if (toSavings) {
|
} else if (toSavings) {
|
||||||
final SavingsAccount account = load(to.getSavingsAccountId());
|
final SavingsAccount account = load(to.getSavingsAccountId());
|
||||||
final SavingsAccountTransaction deposit = depositWithoutAccounting(account, date, amount);
|
final SavingsAccountTransaction deposit = depositWithoutAccounting(account, date, amount);
|
||||||
postPair(account, deposit, date, amount, depositLiabilityOf(to), controlAccountOf(account), description);
|
postPair(account, deposit, date, amount, configured(DEPOSIT_LIABILITY), controlAccountOf(account), description);
|
||||||
} else {
|
} else {
|
||||||
final GLAccount fromDeposits = depositLiabilityOf(from);
|
// Both on DDS Customer Deposits: nothing to book, but the slip still names who paid whom.
|
||||||
final GLAccount toDeposits = depositLiabilityOf(to);
|
voucherSideOnDeposits("DR", from, amount, description);
|
||||||
if (fromDeposits.getId().equals(toDeposits.getId())) {
|
voucherSideOnDeposits("CR", to, amount, description);
|
||||||
// Both owed on the same account: nothing to book, but the slip still names who paid whom -
|
|
||||||
// and the two passbook lines claim no journal, since none was written for them.
|
|
||||||
DdsVoucherScope.forgetLastLedgerTransaction();
|
|
||||||
voucherSideOnDeposits("DR", from, amount, description);
|
|
||||||
voucherSideOnDeposits("CR", to, amount, description);
|
|
||||||
} else {
|
|
||||||
// Two products with customer-deposit accounts of their own: the debt moves from one to the other.
|
|
||||||
ddsPair(from, date, amount, fromDeposits, toDeposits, description);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** One pair for a party with no savings account: at the party's own branch, in the bank's currency. */
|
/** One pair for a party with no savings account: at the party's own branch, in the bank's currency. */
|
||||||
private void ddsPair(final Party party, final LocalDate date, final BigDecimal amount, final GLAccount debit, final GLAccount credit,
|
private void ddsPair(final Party party, final LocalDate date, final BigDecimal amount, final GLAccount debit, final GLAccount credit,
|
||||||
final String description) {
|
final String description) {
|
||||||
writePair(officeOf(party), organisationCurrency(), partyLegId(party), date, amount, debit, credit, description, null, null);
|
final Office office = party.getOffice() != null ? party.getOffice() : party.getAgent().getOffice();
|
||||||
|
final String transactionId = "DDS-" + party.getPartyCode() + "-" + UUID.randomUUID().toString().substring(0, 8);
|
||||||
|
writePair(office, organisationCurrency(), transactionId, date, amount, debit, credit, description, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The account playing the agent's part on an entry - the agent's own. An agent cannot be saved
|
* Which account plays the agent's part on an entry: the agent's own, or the scheme's shared one
|
||||||
* without one, so a missing one here is an agent set up before that was required.
|
* when they have none.
|
||||||
|
*
|
||||||
|
* Only the account differs. Both sides of the entry, their direction and the amount are the same
|
||||||
|
* either way - an agent with their own account is not accounted for differently, only separately.
|
||||||
|
* That is what makes the shared account a safe fallback rather than a second behaviour: an agent
|
||||||
|
* set up before this existed posts exactly as they always did, and a branch can move agents onto
|
||||||
|
* their own accounts one at a time.
|
||||||
*/
|
*/
|
||||||
private GLAccount agentCashOr(final GLAccount agentCashAccount) {
|
private GLAccount agentCashOr(final GLAccount agentCashAccount) {
|
||||||
if (agentCashAccount == null) {
|
return agentCashAccount != null ? agentCashAccount : configured(AGENT_CASH);
|
||||||
throw new DdsLedgerNotConfiguredException("the agent's GL account (Organization > Agent Master)");
|
|
||||||
}
|
|
||||||
return agentCashAccount;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------
|
|
||||||
// A party's product's own ledger accounts.
|
|
||||||
//
|
|
||||||
// Each DDS product names the accounts it posts through on its Accounting tab (0620), as a savings
|
|
||||||
// product does, and those are the only accounts DDS posts through - there is no bank-wide account
|
|
||||||
// behind them. A product that has not named one stops the posting with a message saying which
|
|
||||||
// account to set on which product, rather than the money landing somewhere nobody chose.
|
|
||||||
// ---------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/** Columns of m_dds_account_type a product's account may be read from - never anything a caller typed. */
|
|
||||||
private static final Set<String> PRODUCT_GL_COLUMNS = Set.of("deposit_liability_gl_id", "interest_expense_gl_id",
|
|
||||||
"commission_expense_gl_id");
|
|
||||||
|
|
||||||
/** What the bank owes this party - their product's customer-deposits account. */
|
|
||||||
public GLAccount depositLiabilityOf(final Party party) {
|
|
||||||
return productAccount(party, "deposit_liability_gl_id", "DDS Customer Deposits");
|
|
||||||
}
|
|
||||||
|
|
||||||
/** What this party's interest costs the bank - their product's interest-expense account. */
|
|
||||||
public GLAccount interestExpenseOf(final Party party) {
|
|
||||||
return productAccount(party, "interest_expense_gl_id", "Interest Expense");
|
|
||||||
}
|
|
||||||
|
|
||||||
private GLAccount productAccount(final Party party, final String column, final String label) {
|
|
||||||
if (!PRODUCT_GL_COLUMNS.contains(column)) {
|
|
||||||
throw new IllegalArgumentException(column);
|
|
||||||
}
|
|
||||||
final String productCode = party == null || party.getSubType() == null ? null : party.getSubType().trim().toUpperCase();
|
|
||||||
if (productCode == null) {
|
|
||||||
throw new DdsLedgerNotConfiguredException("a DDS product for party " + (party == null ? "?" : party.getPartyCode()));
|
|
||||||
}
|
|
||||||
// By the id the account is tied to; by its code only for an account saved before ids were kept.
|
|
||||||
final List<Long> ids = party.getAccountTypeId() != null
|
|
||||||
? this.jdbcTemplate.queryForList("select " + column + " from m_dds_account_type where id = ?", Long.class, // NOSONAR
|
|
||||||
party.getAccountTypeId())
|
|
||||||
: this.jdbcTemplate.queryForList("select " + column + " from m_dds_account_type where account_type_code = ?", // NOSONAR
|
|
||||||
Long.class, productCode);
|
|
||||||
if (ids.isEmpty()) {
|
|
||||||
throw new DdsLedgerNotConfiguredException("DDS product " + productCode + " (party " + party.getPartyCode() + ")");
|
|
||||||
}
|
|
||||||
return accountNamed(ids.get(0), productCode, label);
|
|
||||||
}
|
|
||||||
|
|
||||||
private GLAccount accountNamed(final Long glAccountId, final String productCode, final String label) {
|
|
||||||
final Optional<GLAccount> account = glAccountId == null ? Optional.empty() : this.glAccountRepository.findById(glAccountId);
|
|
||||||
return account.orElseThrow(() -> new DdsLedgerNotConfiguredException(
|
|
||||||
"the " + label + " account on DDS product " + productCode + " (Products > DDS Products > Edit > Accounting)"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------
|
|
||||||
// An agent's commission, by product.
|
|
||||||
//
|
|
||||||
// Commission is paid to an agent in one sum for a round that may collect for several products, and
|
|
||||||
// each product books its share on its own commission account. The share is each product's part of
|
|
||||||
// the collections the commission was earned on: those a commission run has counted and no payment
|
|
||||||
// has yet covered. Paying marks them, so the next payment is shared out on the next collections.
|
|
||||||
// ---------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
private static final String COMMISSION_BASIS_SQL = "select upper(trim(p.sub_type)) as \"product\", t.commission_expense_gl_id as \"glId\", "
|
|
||||||
+ "sum(pt.amount) as \"volume\" from m_dds_party_transaction pt join m_dds_party p on p.id = pt.party_id "
|
|
||||||
+ "left join m_dds_account_type t on t.account_type_code = upper(trim(p.sub_type)) "
|
|
||||||
+ "where pt.agent_id = ? and pt.transaction_type = 'PARTY_TRAN' and pt.direction = 'CREDIT' "
|
|
||||||
+ "and pt.commission_batch_run_id is not null and pt.commission_batch_run_id < ? %s group by upper(trim(p.sub_type)), t.commission_expense_gl_id "
|
|
||||||
+ "order by upper(trim(p.sub_type))";
|
|
||||||
|
|
||||||
private static final String UNPAID_COMMISSION_FILTER = "and pt.commission_paid_batch_run_id is null";
|
|
||||||
|
|
||||||
private static final String MARK_COMMISSION_PAID_SQL = "update m_dds_party_transaction set commission_paid_batch_run_id = ? "
|
|
||||||
+ "where agent_id = ? and transaction_type = 'PARTY_TRAN' and direction = 'CREDIT' and commission_batch_run_id is not null "
|
|
||||||
+ "and commission_batch_run_id < ? and commission_paid_batch_run_id is null";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An agent's commission split across the commission accounts of the products they collected for,
|
|
||||||
* in proportion to what they collected for each. The last share takes the rounding, so the shares
|
|
||||||
* always add up to the amount paid.
|
|
||||||
*
|
|
||||||
* Only collections counted by commission runs before the paying run are its basis: a run counted
|
|
||||||
* after this payment was generated earned commission this payment does not include. Collections
|
|
||||||
* counted before payments were tracked by run have no payment marked against them; if nothing
|
|
||||||
* unpaid is left, the agent's counted collections as a whole set the proportions instead.
|
|
||||||
*/
|
|
||||||
public Map<GLAccount, BigDecimal> commissionSplit(final Long agentId, final BigDecimal amount, final Long payingRunId) {
|
|
||||||
List<Map<String, Object>> basis = this.jdbcTemplate.queryForList(String.format(COMMISSION_BASIS_SQL, UNPAID_COMMISSION_FILTER), // NOSONAR
|
|
||||||
agentId, payingRunId);
|
|
||||||
if (basis.isEmpty()) {
|
|
||||||
basis = this.jdbcTemplate.queryForList(String.format(COMMISSION_BASIS_SQL, ""), agentId, payingRunId); // NOSONAR
|
|
||||||
}
|
|
||||||
return shareOut(basis, amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The collections a commission run would count if it were posted now: not counted by any run yet, up to a date. */
|
|
||||||
private static final String COMMISSION_PENDING_SQL = "select upper(trim(p.sub_type)) as \"product\", t.commission_expense_gl_id as \"glId\", "
|
|
||||||
+ "sum(pt.amount) as \"volume\" from m_dds_party_transaction pt join m_dds_party p on p.id = pt.party_id "
|
|
||||||
+ "left join m_dds_account_type t on t.account_type_code = upper(trim(p.sub_type)) "
|
|
||||||
+ "where pt.agent_id = ? and pt.transaction_type = 'PARTY_TRAN' and pt.direction = 'CREDIT' "
|
|
||||||
+ "and pt.commission_batch_run_id is null and pt.transaction_date <= ? group by upper(trim(p.sub_type)), "
|
|
||||||
+ "t.commission_expense_gl_id order by upper(trim(p.sub_type))";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Where an agent's commission will be booked once it is posted - for a calculation run, which books
|
|
||||||
* nothing itself but whose approval should say which commission accounts the Posting run will debit.
|
|
||||||
* The split is over the collections the calculation counts (none counted yet, up to its end date).
|
|
||||||
*/
|
|
||||||
public Map<GLAccount, BigDecimal> projectedCommissionSplit(final Long agentId, final BigDecimal amount, final LocalDate through) {
|
|
||||||
return shareOut(this.jdbcTemplate.queryForList(COMMISSION_PENDING_SQL, agentId, java.sql.Date.valueOf(through)), amount); // NOSONAR
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Shares an amount over the commission accounts of the products collected for, by volume; the last takes the rounding. */
|
|
||||||
private Map<GLAccount, BigDecimal> shareOut(final List<Map<String, Object>> basis, final BigDecimal amount) {
|
|
||||||
if (basis.isEmpty()) {
|
|
||||||
throw new DdsLedgerNotConfiguredException("a collection by this agent to tell which DDS product's Agent Commission Expense "
|
|
||||||
+ "account the commission belongs to");
|
|
||||||
}
|
|
||||||
|
|
||||||
final Map<Long, GLAccount> accounts = new LinkedHashMap<>();
|
|
||||||
final Map<Long, BigDecimal> volumes = new LinkedHashMap<>();
|
|
||||||
BigDecimal total = BigDecimal.ZERO;
|
|
||||||
for (final Map<String, Object> row : basis) {
|
|
||||||
final String product = (String) row.get("product");
|
|
||||||
final Long glId = row.get("glId") == null ? null : ((Number) row.get("glId")).longValue();
|
|
||||||
final GLAccount account = accountNamed(glId, product, "Agent Commission Expense");
|
|
||||||
final BigDecimal volume = (BigDecimal) row.get("volume");
|
|
||||||
accounts.putIfAbsent(account.getId(), account);
|
|
||||||
volumes.merge(account.getId(), volume, BigDecimal::add);
|
|
||||||
total = total.add(volume);
|
|
||||||
}
|
|
||||||
|
|
||||||
final Map<GLAccount, BigDecimal> shares = new LinkedHashMap<>();
|
|
||||||
BigDecimal allotted = BigDecimal.ZERO;
|
|
||||||
final List<Long> ids = List.copyOf(volumes.keySet());
|
|
||||||
for (int i = 0; i < ids.size(); i++) {
|
|
||||||
final Long id = ids.get(i);
|
|
||||||
final BigDecimal share = i == ids.size() - 1 || total.signum() == 0 ? amount.subtract(allotted)
|
|
||||||
: amount.multiply(volumes.get(id)).divide(total, 2, RoundingMode.HALF_UP);
|
|
||||||
allotted = allotted.add(share);
|
|
||||||
if (share.signum() != 0) {
|
|
||||||
shares.put(accounts.get(id), share);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return shares;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Marks the collections a commission payment was shared out on, so the next payment starts after them. */
|
|
||||||
public void markCommissionPaid(final Long agentId, final Long batchRunId) {
|
|
||||||
this.jdbcTemplate.update(MARK_COMMISSION_PAID_SQL, batchRunId, agentId, batchRunId); // NOSONAR
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -601,9 +490,15 @@ public class DdsLedger {
|
|||||||
public void reverseInterest(final Long savingsAccountId, final LocalDate date, final BigDecimal amount, final String description) {
|
public void reverseInterest(final Long savingsAccountId, final LocalDate date, final BigDecimal amount, final String description) {
|
||||||
final SavingsAccount account = load(savingsAccountId);
|
final SavingsAccount account = load(savingsAccountId);
|
||||||
|
|
||||||
final SavingsAccountTransaction withdrawal = withdraw(account, date, amount);
|
// DDS's own movement, so the debit block comes off for it and goes straight back on.
|
||||||
|
final boolean wasBlocked = unlockDebitsIfBlocked(account);
|
||||||
|
final SavingsTransactionBooleanValues flags = new SavingsTransactionBooleanValues(false, true, false, false, false, true);
|
||||||
|
this.savingsAccountDomainService.handleWithdrawal(account, DateUtils.DEFAULT_DATE_FORMATTER, date, amount, null, flags,
|
||||||
|
this.savingsAccountAssembler.getPivotConfigStatus());
|
||||||
|
relockDebits(account, wasBlocked);
|
||||||
|
|
||||||
postPair(account.office(), account.getCurrency().getCode(), date, amount, controlAccountOf(account),
|
postPair(account.office(), account.getCurrency().getCode(), date, amount, controlAccountOf(account),
|
||||||
productAccount(account.productId(), INTEREST_ON_SAVINGS), description, withdrawal.getId(), account.getId());
|
productAccount(account.productId(), INTEREST_ON_SAVINGS), description, null, account.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -620,15 +515,15 @@ public class DdsLedger {
|
|||||||
|
|
||||||
// The scheme account is blocked for debit so nobody can take the money out at a counter. This
|
// The scheme account is blocked for debit so nobody can take the money out at a counter. This
|
||||||
// movement is DDS's own, so the block comes off for it and goes straight back on.
|
// movement is DDS's own, so the block comes off for it and goes straight back on.
|
||||||
final SavingsAccountTransaction withdrawal = withdraw(from, date, amount);
|
final boolean wasBlocked = unlockDebitsIfBlocked(from);
|
||||||
|
final SavingsTransactionBooleanValues withdrawalFlags = new SavingsTransactionBooleanValues(false, true, false, false, false, true);
|
||||||
|
this.savingsAccountDomainService.handleWithdrawal(from, DateUtils.DEFAULT_DATE_FORMATTER, date, amount, null, withdrawalFlags,
|
||||||
|
this.savingsAccountAssembler.getPivotConfigStatus());
|
||||||
|
relockDebits(from, wasBlocked);
|
||||||
|
|
||||||
final SavingsAccountTransaction deposit = depositWithoutAccounting(to, date, amount);
|
final SavingsAccountTransaction deposit = depositWithoutAccounting(to, date, amount);
|
||||||
|
|
||||||
// One leg on each account, each carrying its own account transaction, so both sides of the
|
postPair(to, deposit, date, amount, controlAccountOf(from), controlAccountOf(to), description);
|
||||||
// movement can be traced from the journal - not only the deposit.
|
|
||||||
writeLeg(from.office(), from.getCurrency().getCode(), "S" + withdrawal.getId(), date, amount, controlAccountOf(from),
|
|
||||||
JournalEntryType.DEBIT, description, withdrawal.getId(), from.getId());
|
|
||||||
writeLeg(from.office(), from.getCurrency().getCode(), "S" + withdrawal.getId(), date, amount, controlAccountOf(to),
|
|
||||||
JournalEntryType.CREDIT, description, deposit.getId(), to.getId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -661,9 +556,15 @@ public class DdsLedger {
|
|||||||
final GLAccount parkIn) {
|
final GLAccount parkIn) {
|
||||||
final SavingsAccount account = load(savingsAccountId);
|
final SavingsAccount account = load(savingsAccountId);
|
||||||
|
|
||||||
final SavingsAccountTransaction withdrawal = withdraw(account, date, amount);
|
// DDS's own movement, so the debit block comes off for it and goes straight back on.
|
||||||
postPair(account.office(), account.getCurrency().getCode(), date, amount, controlAccountOf(account), parkIn, description,
|
final boolean wasBlocked = unlockDebitsIfBlocked(account);
|
||||||
withdrawal.getId(), account.getId());
|
final SavingsTransactionBooleanValues flags = new SavingsTransactionBooleanValues(false, true, false, false, false, true);
|
||||||
|
this.savingsAccountDomainService.handleWithdrawal(account, DateUtils.DEFAULT_DATE_FORMATTER, date, amount, null, flags,
|
||||||
|
this.savingsAccountAssembler.getPivotConfigStatus());
|
||||||
|
relockDebits(account, wasBlocked);
|
||||||
|
|
||||||
|
postPair(account.office(), account.getCurrency().getCode(), date, amount, controlAccountOf(account), parkIn,
|
||||||
|
description, null, account.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -691,18 +592,12 @@ public class DdsLedger {
|
|||||||
return deposit;
|
return deposit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Credits an agent's own savings account with commission the bank owes them. */
|
||||||
* Credits an agent's own savings account with commission the bank owes them - one deposit to the
|
|
||||||
* agent, and one entry per product on that product's commission account (see commissionSplit).
|
|
||||||
*/
|
|
||||||
public SavingsAccountTransaction payCommission(final Long savingsAccountId, final LocalDate date, final BigDecimal amount,
|
public SavingsAccountTransaction payCommission(final Long savingsAccountId, final LocalDate date, final BigDecimal amount,
|
||||||
final String description, final Map<GLAccount, BigDecimal> shares) {
|
final String description) {
|
||||||
final SavingsAccount account = load(savingsAccountId);
|
final SavingsAccount account = load(savingsAccountId);
|
||||||
final SavingsAccountTransaction deposit = depositWithoutAccounting(account, date, amount);
|
final SavingsAccountTransaction deposit = depositWithoutAccounting(account, date, amount);
|
||||||
final GLAccount control = controlAccountOf(account);
|
postPair(account, deposit, date, amount, configured(COMMISSION_EXPENSE), controlAccountOf(account), description);
|
||||||
for (final Map.Entry<GLAccount, BigDecimal> share : shares.entrySet()) {
|
|
||||||
postPair(account, deposit, date, share.getValue(), share.getKey(), control, description);
|
|
||||||
}
|
|
||||||
return deposit;
|
return deposit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -772,60 +667,47 @@ public class DdsLedger {
|
|||||||
private void postPair(final Office office, final String currencyCode, final LocalDate date, final BigDecimal amount,
|
private void postPair(final Office office, final String currencyCode, final LocalDate date, final BigDecimal amount,
|
||||||
final GLAccount debit, final GLAccount credit, final String description, final Long savingsTransactionId,
|
final GLAccount debit, final GLAccount credit, final String description, final Long savingsTransactionId,
|
||||||
final Long savingsAccountId) {
|
final Long savingsAccountId) {
|
||||||
final String transactionId = savingsTransactionId == null ? "DDS-" + UUID.randomUUID() : "S" + savingsTransactionId;
|
final String transactionId = savingsTransactionId == null ? "DDS" + date + amount : "S" + savingsTransactionId;
|
||||||
writePair(office, currencyCode, transactionId, date, amount, debit, credit, description, savingsTransactionId, savingsAccountId);
|
writePair(office, currencyCode, transactionId, date, amount, debit, credit, description, savingsTransactionId, savingsAccountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void writePair(final Office office, final String currencyCode, final String transactionId, final LocalDate date,
|
private void writePair(final Office office, final String currencyCode, final String transactionId, final LocalDate date,
|
||||||
final BigDecimal amount, final GLAccount debit, final GLAccount credit, final String description,
|
final BigDecimal amount, final GLAccount debit, final GLAccount credit, final String description,
|
||||||
final Long savingsTransactionId, final Long savingsAccountId) {
|
final Long savingsTransactionId, final Long savingsAccountId) {
|
||||||
writeLeg(office, currencyCode, transactionId, date, amount, debit, JournalEntryType.DEBIT, description, savingsTransactionId,
|
// Noted for the voucher of the operation this leg belongs to - see DdsVoucherService.
|
||||||
savingsAccountId);
|
|
||||||
writeLeg(office, currencyCode, transactionId, date, amount, credit, JournalEntryType.CREDIT, description, savingsTransactionId,
|
|
||||||
savingsAccountId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeLeg(final Office office, final String currencyCode, final String transactionId, final LocalDate date,
|
|
||||||
final BigDecimal amount, final GLAccount account, final JournalEntryType type, final String description,
|
|
||||||
final Long savingsTransactionId, final Long savingsAccountId) {
|
|
||||||
// Noted for the voucher of the operation this leg belongs to - see DdsVoucherService - and as the
|
|
||||||
// journal the next passbook line of the operation is linked to (m_dds_party_transaction.journal_transaction_id).
|
|
||||||
DdsVoucherScope.recordLedgerTransaction(transactionId);
|
DdsVoucherScope.recordLedgerTransaction(transactionId);
|
||||||
this.journalEntryRepository.saveAndFlush(JournalEntry.createNew(office, null, account, currencyCode, transactionId, false, date, type,
|
for (final JournalEntryType type : List.of(JournalEntryType.DEBIT, JournalEntryType.CREDIT)) {
|
||||||
amount, description, PortfolioProductType.SAVING.getValue(), savingsAccountId, null, null, savingsTransactionId, null,
|
this.journalEntryRepository.saveAndFlush(JournalEntry.createNew(office, null,
|
||||||
null));
|
JournalEntryType.DEBIT.equals(type) ? debit : credit, currencyCode, transactionId, false, date, type, amount,
|
||||||
|
description, PortfolioProductType.SAVING.getValue(), savingsAccountId, null, null, savingsTransactionId, null, null));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** A ledger account named by a setting, so a bank can point it at one of their own. */
|
||||||
* A journal id for a leg on DDS's own books - the party's code to find it by and a full UUID to keep it
|
private GLAccount configured(final String settingName) {
|
||||||
* unique: DDS-PT0003-1b4e... Never built from the date and amount, which two parties can share.
|
final List<Long> ids = this.jdbcTemplate.queryForList(CONFIGURED_ACCOUNT_SQL, Long.class, settingName); // NOSONAR
|
||||||
*/
|
if (ids.isEmpty() || ids.get(0) == null) {
|
||||||
private static String partyLegId(final Party party) {
|
throw new DdsLedgerNotConfiguredException(settingName);
|
||||||
return "DDS-" + party.getPartyCode() + "-" + UUID.randomUUID().toString().replace("-", "").substring(0, 16);
|
}
|
||||||
}
|
return this.glAccountRepository.findById(ids.get(0)).orElseThrow(() -> new DdsLedgerNotConfiguredException(settingName));
|
||||||
|
|
||||||
/** The branch a party's legs are booked at: the party's own, or its agent's for a party opened without one. */
|
|
||||||
public Office officeOf(final Party party) {
|
|
||||||
return party.getOffice() != null ? party.getOffice() : party.getAgent().getOffice();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes money off a scheme account that DDS keeps blocked for debits, as DDS's own movement - the block
|
|
||||||
* comes off for it and goes straight back on - and returns the withdrawal so its journal can name it.
|
|
||||||
*/
|
|
||||||
private SavingsAccountTransaction withdraw(final SavingsAccount account, final LocalDate date, final BigDecimal amount) {
|
|
||||||
final boolean wasBlocked = unlockDebitsIfBlocked(account);
|
|
||||||
final SavingsTransactionBooleanValues flags = new SavingsTransactionBooleanValues(false, true, false, false, false, true);
|
|
||||||
final SavingsAccountTransaction withdrawal = this.savingsAccountDomainService.handleWithdrawal(account,
|
|
||||||
DateUtils.DEFAULT_DATE_FORMATTER, date, amount, null, flags, this.savingsAccountAssembler.getPivotConfigStatus());
|
|
||||||
relockDebits(account, wasBlocked);
|
|
||||||
return withdrawal;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private GLAccount controlAccountOf(final SavingsAccount account) {
|
private GLAccount controlAccountOf(final SavingsAccount account) {
|
||||||
return productAccount(account.productId(), CONTROL_ACCOUNT);
|
return productAccount(account.productId(), CONTROL_ACCOUNT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The savings product DDS opens customer accounts under. A settlement touches no single customer,
|
||||||
|
* so its ledger accounts and currency come from the product rather than from an account.
|
||||||
|
*/
|
||||||
|
private Long depositProductId() {
|
||||||
|
final List<Long> ids = this.jdbcTemplate.queryForList(CONFIGURED_ACCOUNT_SQL, Long.class, DEPOSIT_PRODUCT); // NOSONAR
|
||||||
|
if (ids.isEmpty() || ids.get(0) == null) {
|
||||||
|
throw new DdsLedgerNotConfiguredException(DEPOSIT_PRODUCT);
|
||||||
|
}
|
||||||
|
return ids.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
/** The bank's own currency, for an entry that belongs to no savings product. */
|
/** The bank's own currency, for an entry that belongs to no savings product. */
|
||||||
private String organisationCurrency() {
|
private String organisationCurrency() {
|
||||||
final List<String> codes = this.jdbcTemplate.queryForList(ORGANISATION_CURRENCY_SQL, String.class); // NOSONAR
|
final List<String> codes = this.jdbcTemplate.queryForList(ORGANISATION_CURRENCY_SQL, String.class); // NOSONAR
|
||||||
@@ -835,6 +717,14 @@ public class DdsLedger {
|
|||||||
return codes.get(0);
|
return codes.get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Read off the product rather than assumed, so a bank on another currency is not silently wrong. */
|
||||||
|
private String currencyOf(final Long productId) {
|
||||||
|
final List<String> codes = this.jdbcTemplate.queryForList(PRODUCT_CURRENCY_SQL, String.class, productId); // NOSONAR
|
||||||
|
if (codes.isEmpty()) {
|
||||||
|
throw new DdsLedgerNotConfiguredException("currency for savings product " + productId);
|
||||||
|
}
|
||||||
|
return codes.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
/** An account off the savings product's own mapping, so this stays right per product. */
|
/** An account off the savings product's own mapping, so this stays right per product. */
|
||||||
private GLAccount productAccount(final Long productId, final int financialAccountType) {
|
private GLAccount productAccount(final Long productId, final int financialAccountType) {
|
||||||
|
|||||||
+7
-24
@@ -22,7 +22,6 @@ import java.math.BigDecimal;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -336,9 +335,6 @@ public class DdsVoucherService {
|
|||||||
LAST_ISSUED.set(voucherNo);
|
LAST_ISSUED.set(voucherNo);
|
||||||
|
|
||||||
stamp("update m_dds_party_transaction set voucher_no = ? where id in (%s)", voucherNo, scope.getPartyTransactionIds());
|
stamp("update m_dds_party_transaction set voucher_no = ? where id in (%s)", voucherNo, scope.getPartyTransactionIds());
|
||||||
// The journal legs carry the voucher too, in ref_num, so the slip, the passbook lines and the
|
|
||||||
// journal can each be found from the others - as a savings voucher's legs can.
|
|
||||||
stampLegs(voucherNo, scope.getLedgerTransactionIds());
|
|
||||||
stamp("update m_savings_account_transaction set voucher_no = ? where voucher_no is null and id in (%s)", voucherNo,
|
stamp("update m_savings_account_transaction set voucher_no = ? where voucher_no is null and id in (%s)", voucherNo,
|
||||||
savingsTransactions);
|
savingsTransactions);
|
||||||
}
|
}
|
||||||
@@ -360,7 +356,7 @@ public class DdsVoucherService {
|
|||||||
return new ArrayList<>();
|
return new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
final Set<Long> depositGlIds = depositLiabilityGlIds();
|
final Long depositGlId = depositLiabilityGlId();
|
||||||
final Map<String, Map<String, Object>> parties = new LinkedHashMap<>();
|
final Map<String, Map<String, Object>> parties = new LinkedHashMap<>();
|
||||||
for (final Map<String, Object> leg : this.jdbcTemplate.queryForList(String.format(LEGS_SQL, placeholders(transactionIds)), // NOSONAR
|
for (final Map<String, Object> leg : this.jdbcTemplate.queryForList(String.format(LEGS_SQL, placeholders(transactionIds)), // NOSONAR
|
||||||
transactionIds.toArray())) {
|
transactionIds.toArray())) {
|
||||||
@@ -387,7 +383,7 @@ public class DdsVoucherService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final Matcher partyLeg = PARTY_LEG.matcher(String.valueOf(leg.get("transactionId")));
|
final Matcher partyLeg = PARTY_LEG.matcher(String.valueOf(leg.get("transactionId")));
|
||||||
if (partyLeg.matches() && depositGlIds.contains(glId)) {
|
if (partyLeg.matches() && glId.equals(depositGlId)) {
|
||||||
final String partyCode = partyLeg.group(1);
|
final String partyCode = partyLeg.group(1);
|
||||||
final Map<String, Object> party = parties.computeIfAbsent(partyCode,
|
final Map<String, Object> party = parties.computeIfAbsent(partyCode,
|
||||||
code -> this.jdbcTemplate.queryForList(PARTY_SQL, code).stream().findFirst().orElse(Map.of())); // NOSONAR
|
code -> this.jdbcTemplate.queryForList(PARTY_SQL, code).stream().findFirst().orElse(Map.of())); // NOSONAR
|
||||||
@@ -421,24 +417,11 @@ public class DdsVoucherService {
|
|||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** The GL that carries what DDS customers are owed - a leg on it is the customer's own. */
|
||||||
* The GLs that carry what DDS customers are owed - a leg on one is the customer's own. Each DDS
|
private Long depositLiabilityGlId() {
|
||||||
* product names its own on its Accounting tab (0620).
|
final List<Long> ids = this.jdbcTemplate.queryForList( // NOSONAR
|
||||||
*/
|
"select value from c_configuration where name = 'dds-deposit-liability-account' and enabled = true", Long.class);
|
||||||
private Set<Long> depositLiabilityGlIds() {
|
return ids.isEmpty() ? null : ids.get(0);
|
||||||
return new HashSet<>(this.jdbcTemplate.queryForList( // NOSONAR
|
|
||||||
"select deposit_liability_gl_id from m_dds_account_type where deposit_liability_gl_id is not null", Long.class));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void stampLegs(final String voucherNo, final Collection<String> transactionIds) {
|
|
||||||
if (transactionIds.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final List<Object> args = new ArrayList<>();
|
|
||||||
args.add(voucherNo);
|
|
||||||
args.addAll(transactionIds);
|
|
||||||
this.jdbcTemplate.update("update acc_gl_journal_entry set ref_num = ? where ref_num is null and transaction_id in (" // NOSONAR
|
|
||||||
+ placeholders(transactionIds) + ")", args.toArray());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void stamp(final String sql, final String voucherNo, final Collection<Long> ids) {
|
private void stamp(final String sql, final String voucherNo, final Collection<Long> ids) {
|
||||||
|
|||||||
+6
-2
@@ -27,7 +27,11 @@ public interface DdsAccountTypeRepository extends JpaRepository<DdsAccountType,
|
|||||||
|
|
||||||
Optional<DdsAccountType> findByAccountTypeCode(String accountTypeCode);
|
Optional<DdsAccountType> findByAccountTypeCode(String accountTypeCode);
|
||||||
|
|
||||||
/** Schemes that post interest on their own, at the end of each posting period. */
|
/**
|
||||||
@Query("select t from DdsAccountType t where t.active = true and t.autoInterestPost = true order by t.accountTypeCode")
|
* Schemes that post interest on their own. A posting day is required to act on one, so a scheme
|
||||||
|
* left without a day is not picked up - it would otherwise mean "post monthly, some day".
|
||||||
|
*/
|
||||||
|
@Query("select t from DdsAccountType t where t.active = true and t.autoInterestPost = true "
|
||||||
|
+ "and t.autoInterestPostDay is not null order by t.accountTypeCode")
|
||||||
List<DdsAccountType> findDueForAutoInterestPost();
|
List<DdsAccountType> findDueForAutoInterestPost();
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-22
@@ -39,15 +39,9 @@ import org.springframework.stereotype.Component;
|
|||||||
@Component
|
@Component
|
||||||
public final class DdsAccountTypeCommandFromApiJsonDeserializer {
|
public final class DdsAccountTypeCommandFromApiJsonDeserializer {
|
||||||
|
|
||||||
/*
|
|
||||||
* "autoInterestPostDay" is still accepted, and ignored, so a change held for approval before the
|
|
||||||
* posting day was retired - or an older screen still sending it - is not refused. Interest now
|
|
||||||
* posts at the end of each period, so there is no day to set.
|
|
||||||
*/
|
|
||||||
private static final Set<String> SUPPORTED_PARAMETERS = new HashSet<>(Arrays.asList("accountTypeGroup", "accountTypeCode",
|
private static final Set<String> SUPPORTED_PARAMETERS = new HashSet<>(Arrays.asList("accountTypeGroup", "accountTypeCode",
|
||||||
"accountTypeDescription", "autoInterestPost", "autoInterestPostWhen", "autoInterestPostDay", "maturityAutoClose", "active",
|
"accountTypeDescription", "autoInterestPost", "autoInterestPostWhen", "autoInterestPostDay", "maturityAutoClose", "active",
|
||||||
"prematureLockinMonths", "prematureInterestReduction", "prematureAgentCommissionPenalty", "interestRate",
|
"prematureLockinMonths", "prematureInterestReduction", "prematureAgentCommissionPenalty", "interestRate",
|
||||||
"depositLiabilityGlId", "interestExpenseGlId", "commissionExpenseGlId",
|
|
||||||
"dateFormat", "locale"));
|
"dateFormat", "locale"));
|
||||||
|
|
||||||
/** A rate, not an amount - nothing here may exceed the whole of what it is taken from. */
|
/** A rate, not an amount - nothing here may exceed the whole of what it is taken from. */
|
||||||
@@ -72,14 +66,6 @@ public final class DdsAccountTypeCommandFromApiJsonDeserializer {
|
|||||||
v.reset().parameter("accountTypeDescription")
|
v.reset().parameter("accountTypeDescription")
|
||||||
.value(this.fromApiJsonHelper.extractStringNamed("accountTypeDescription", element)).notBlank()
|
.value(this.fromApiJsonHelper.extractStringNamed("accountTypeDescription", element)).notBlank()
|
||||||
.notExceedingLengthOf(200);
|
.notExceedingLengthOf(200);
|
||||||
// Mandatory, as a savings product's control and interest accounts are: every posting for the
|
|
||||||
// product's customers and agents goes through them, with nothing bank-wide behind them.
|
|
||||||
v.reset().parameter("depositLiabilityGlId").value(this.fromApiJsonHelper.extractLongNamed("depositLiabilityGlId", element))
|
|
||||||
.notNull().integerGreaterThanZero();
|
|
||||||
v.reset().parameter("interestExpenseGlId").value(this.fromApiJsonHelper.extractLongNamed("interestExpenseGlId", element))
|
|
||||||
.notNull().integerGreaterThanZero();
|
|
||||||
v.reset().parameter("commissionExpenseGlId").value(this.fromApiJsonHelper.extractLongNamed("commissionExpenseGlId", element))
|
|
||||||
.notNull().integerGreaterThanZero();
|
|
||||||
// Mandatory: every account opened on the scheme takes this rate.
|
// Mandatory: every account opened on the scheme takes this rate.
|
||||||
v.reset().parameter("interestRate").value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("interestRate", element))
|
v.reset().parameter("interestRate").value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("interestRate", element))
|
||||||
.notNull().zeroOrPositiveAmount().notGreaterThanMax(HUNDRED);
|
.notNull().zeroOrPositiveAmount().notGreaterThanMax(HUNDRED);
|
||||||
@@ -113,14 +99,6 @@ public final class DdsAccountTypeCommandFromApiJsonDeserializer {
|
|||||||
.notNull().zeroOrPositiveAmount().notGreaterThanMax(HUNDRED);
|
.notNull().zeroOrPositiveAmount().notGreaterThanMax(HUNDRED);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Once named, a product's customer and interest accounts can be changed but not cleared.
|
|
||||||
for (final String required : List.of("depositLiabilityGlId", "interestExpenseGlId", "commissionExpenseGlId")) {
|
|
||||||
if (this.fromApiJsonHelper.parameterExists(required, element)) {
|
|
||||||
v.reset().parameter(required).value(this.fromApiJsonHelper.extractLongNamed(required, element)).notNull()
|
|
||||||
.integerGreaterThanZero();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
validateOptionalFields(element, v);
|
validateOptionalFields(element, v);
|
||||||
|
|
||||||
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
||||||
@@ -143,6 +121,11 @@ public final class DdsAccountTypeCommandFromApiJsonDeserializer {
|
|||||||
.value(this.fromApiJsonHelper.extractStringNamed("autoInterestPostWhen", element)).ignoreIfNull()
|
.value(this.fromApiJsonHelper.extractStringNamed("autoInterestPostWhen", element)).ignoreIfNull()
|
||||||
.notExceedingLengthOf(20);
|
.notExceedingLengthOf(20);
|
||||||
}
|
}
|
||||||
|
if (this.fromApiJsonHelper.parameterExists("autoInterestPostDay", element)) {
|
||||||
|
v.reset().parameter("autoInterestPostDay")
|
||||||
|
.value(this.fromApiJsonHelper.extractIntegerSansLocaleNamed("autoInterestPostDay", element)).ignoreIfNull()
|
||||||
|
.inMinMaxRange(1, 31);
|
||||||
|
}
|
||||||
|
|
||||||
// Premature closure terms. Zero is allowed throughout - a scheme may close early from day
|
// Premature closure terms. Zero is allowed throughout - a scheme may close early from day
|
||||||
// one, or charge the customer or the agent nothing - so these are floored at zero, not at one.
|
// one, or charge the customer or the agent nothing - so these are floored at zero, not at one.
|
||||||
|
|||||||
+4
-21
@@ -43,24 +43,15 @@ public class DdsAccountTypeReadPlatformServiceImpl implements DdsAccountTypeRead
|
|||||||
public String schema() {
|
public String schema() {
|
||||||
return " t.id as id, t.account_type_group as accountTypeGroup, t.account_type_code as accountTypeCode, "
|
return " t.id as id, t.account_type_group as accountTypeGroup, t.account_type_code as accountTypeCode, "
|
||||||
+ " t.account_type_description as accountTypeDescription, t.auto_interest_post as autoInterestPost, "
|
+ " t.account_type_description as accountTypeDescription, t.auto_interest_post as autoInterestPost, "
|
||||||
+ " t.auto_interest_post_when as autoInterestPostWhen, "
|
+ " t.auto_interest_post_when as autoInterestPostWhen, t.auto_interest_post_day as autoInterestPostDay, "
|
||||||
+ " t.maturity_auto_close as maturityAutoClose, "
|
+ " t.maturity_auto_close as maturityAutoClose, "
|
||||||
+ " t.premature_lockin_months as prematureLockinMonths, "
|
+ " t.premature_lockin_months as prematureLockinMonths, "
|
||||||
+ " t.premature_interest_reduction as prematureInterestReduction, "
|
+ " t.premature_interest_reduction as prematureInterestReduction, "
|
||||||
+ " t.premature_agent_commission_penalty as prematureAgentCommissionPenalty, "
|
+ " t.premature_agent_commission_penalty as prematureAgentCommissionPenalty, "
|
||||||
+ " t.interest_rate as interestRate, "
|
+ " t.interest_rate as interestRate, "
|
||||||
+ " t.is_active as active, "
|
+ " t.is_active as active, "
|
||||||
+ " t.created_date as createdDate, t.lastmodified_date as lastModifiedDate, "
|
+ " t.created_date as createdDate, t.lastmodified_date as lastModifiedDate "
|
||||||
+ " t.deposit_liability_gl_id as depositLiabilityGlId, dl.gl_code as depositLiabilityGlCode, "
|
+ " from m_dds_account_type t";
|
||||||
+ " dl.name as depositLiabilityGlName, "
|
|
||||||
+ " t.interest_expense_gl_id as interestExpenseGlId, ie.gl_code as interestExpenseGlCode, "
|
|
||||||
+ " ie.name as interestExpenseGlName, "
|
|
||||||
+ " t.commission_expense_gl_id as commissionExpenseGlId, ce.gl_code as commissionExpenseGlCode, "
|
|
||||||
+ " ce.name as commissionExpenseGlName "
|
|
||||||
+ " from m_dds_account_type t "
|
|
||||||
+ " left join acc_gl_account dl on dl.id = t.deposit_liability_gl_id "
|
|
||||||
+ " left join acc_gl_account ie on ie.id = t.interest_expense_gl_id "
|
|
||||||
+ " left join acc_gl_account ce on ce.id = t.commission_expense_gl_id";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -72,21 +63,13 @@ public class DdsAccountTypeReadPlatformServiceImpl implements DdsAccountTypeRead
|
|||||||
.accountTypeDescription(rs.getString("accountTypeDescription")) //
|
.accountTypeDescription(rs.getString("accountTypeDescription")) //
|
||||||
.autoInterestPost(rs.getBoolean("autoInterestPost")) //
|
.autoInterestPost(rs.getBoolean("autoInterestPost")) //
|
||||||
.autoInterestPostWhen(rs.getString("autoInterestPostWhen")) //
|
.autoInterestPostWhen(rs.getString("autoInterestPostWhen")) //
|
||||||
|
.autoInterestPostDay(JdbcSupport.getInteger(rs, "autoInterestPostDay")) //
|
||||||
.maturityAutoClose(rs.getBoolean("maturityAutoClose")) //
|
.maturityAutoClose(rs.getBoolean("maturityAutoClose")) //
|
||||||
.prematureLockinMonths(JdbcSupport.getInteger(rs, "prematureLockinMonths")) //
|
.prematureLockinMonths(JdbcSupport.getInteger(rs, "prematureLockinMonths")) //
|
||||||
.prematureInterestReduction(rs.getBigDecimal("prematureInterestReduction")) //
|
.prematureInterestReduction(rs.getBigDecimal("prematureInterestReduction")) //
|
||||||
.prematureAgentCommissionPenalty(rs.getBigDecimal("prematureAgentCommissionPenalty")) //
|
.prematureAgentCommissionPenalty(rs.getBigDecimal("prematureAgentCommissionPenalty")) //
|
||||||
.interestRate(rs.getBigDecimal("interestRate")) //
|
.interestRate(rs.getBigDecimal("interestRate")) //
|
||||||
.active(rs.getBoolean("active")) //
|
.active(rs.getBoolean("active")) //
|
||||||
.depositLiabilityGlId(JdbcSupport.getLong(rs, "depositLiabilityGlId")) //
|
|
||||||
.depositLiabilityGlCode(rs.getString("depositLiabilityGlCode")) //
|
|
||||||
.depositLiabilityGlName(rs.getString("depositLiabilityGlName")) //
|
|
||||||
.interestExpenseGlId(JdbcSupport.getLong(rs, "interestExpenseGlId")) //
|
|
||||||
.interestExpenseGlCode(rs.getString("interestExpenseGlCode")) //
|
|
||||||
.interestExpenseGlName(rs.getString("interestExpenseGlName")) //
|
|
||||||
.commissionExpenseGlId(JdbcSupport.getLong(rs, "commissionExpenseGlId")) //
|
|
||||||
.commissionExpenseGlCode(rs.getString("commissionExpenseGlCode")) //
|
|
||||||
.commissionExpenseGlName(rs.getString("commissionExpenseGlName")) //
|
|
||||||
.createdDate(localDateTime(rs, "createdDate")) //
|
.createdDate(localDateTime(rs, "createdDate")) //
|
||||||
.lastModifiedDate(localDateTime(rs, "lastModifiedDate")) //
|
.lastModifiedDate(localDateTime(rs, "lastModifiedDate")) //
|
||||||
.build();
|
.build();
|
||||||
|
|||||||
-63
@@ -19,7 +19,6 @@
|
|||||||
package org.apache.fineract.portfolio.dds.accounttype.service;
|
package org.apache.fineract.portfolio.dds.accounttype.service;
|
||||||
|
|
||||||
import jakarta.persistence.PersistenceException;
|
import jakarta.persistence.PersistenceException;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -36,10 +35,7 @@ import org.apache.fineract.portfolio.dds.accounttype.exception.DdsAccountTypeNot
|
|||||||
import org.apache.fineract.portfolio.dds.accounttype.serialization.DdsAccountTypeCommandFromApiJsonDeserializer;
|
import org.apache.fineract.portfolio.dds.accounttype.serialization.DdsAccountTypeCommandFromApiJsonDeserializer;
|
||||||
import org.apache.fineract.portfolio.dds.party.domain.Party;
|
import org.apache.fineract.portfolio.dds.party.domain.Party;
|
||||||
import org.apache.fineract.portfolio.dds.party.domain.PartyRepository;
|
import org.apache.fineract.portfolio.dds.party.domain.PartyRepository;
|
||||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
|
||||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
|
||||||
import org.springframework.dao.DataIntegrityViolationException;
|
import org.springframework.dao.DataIntegrityViolationException;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
|
||||||
import org.springframework.orm.jpa.JpaSystemException;
|
import org.springframework.orm.jpa.JpaSystemException;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -50,15 +46,6 @@ public class DdsAccountTypeWritePlatformServiceJpaRepositoryImpl implements DdsA
|
|||||||
private final DdsAccountTypeCommandFromApiJsonDeserializer fromApiJsonDeserializer;
|
private final DdsAccountTypeCommandFromApiJsonDeserializer fromApiJsonDeserializer;
|
||||||
private final DdsAccountTypeRepository ddsAccountTypeRepository;
|
private final DdsAccountTypeRepository ddsAccountTypeRepository;
|
||||||
private final PartyRepository partyRepository;
|
private final PartyRepository partyRepository;
|
||||||
private final JdbcTemplate jdbcTemplate;
|
|
||||||
|
|
||||||
private static final String GL_SQL = "select classification_enum, account_usage, disabled, gl_code, name from acc_gl_account where id = ?";
|
|
||||||
|
|
||||||
/** acc_gl_account.classification_enum: 2 liability, 5 expense. */
|
|
||||||
private static final int LIABILITY = 2;
|
|
||||||
private static final int EXPENSE = 5;
|
|
||||||
/** acc_gl_account.account_usage 1 - a detail account an entry can be posted to, not a header. */
|
|
||||||
private static final int DETAIL = 1;
|
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
@@ -66,7 +53,6 @@ public class DdsAccountTypeWritePlatformServiceJpaRepositoryImpl implements DdsA
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
this.fromApiJsonDeserializer.validateForCreate(command.json());
|
this.fromApiJsonDeserializer.validateForCreate(command.json());
|
||||||
validateLedgerAccounts(command);
|
|
||||||
|
|
||||||
final DdsAccountType accountType = DdsAccountType.fromJson(command);
|
final DdsAccountType accountType = DdsAccountType.fromJson(command);
|
||||||
this.ddsAccountTypeRepository.saveAndFlush(accountType);
|
this.ddsAccountTypeRepository.saveAndFlush(accountType);
|
||||||
@@ -91,7 +77,6 @@ public class DdsAccountTypeWritePlatformServiceJpaRepositoryImpl implements DdsA
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
this.fromApiJsonDeserializer.validateForUpdate(command.json());
|
this.fromApiJsonDeserializer.validateForUpdate(command.json());
|
||||||
validateLedgerAccounts(command);
|
|
||||||
|
|
||||||
final DdsAccountType accountType = this.ddsAccountTypeRepository.findById(accountTypeId)
|
final DdsAccountType accountType = this.ddsAccountTypeRepository.findById(accountTypeId)
|
||||||
.orElseThrow(() -> new DdsAccountTypeNotFoundException(accountTypeId));
|
.orElseThrow(() -> new DdsAccountTypeNotFoundException(accountTypeId));
|
||||||
@@ -115,13 +100,6 @@ public class DdsAccountTypeWritePlatformServiceJpaRepositoryImpl implements DdsA
|
|||||||
changesOnly.put("accountsRepriced", openAccounts.size());
|
changesOnly.put("accountsRepriced", openAccounts.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
// A renamed product: the accounts on it - tied to it by id - show the new code.
|
|
||||||
if (changesOnly.containsKey("accountTypeCode")) {
|
|
||||||
final int moved = this.jdbcTemplate.update("update m_dds_party set sub_type = ? where account_type_id = ?", // NOSONAR
|
|
||||||
accountType.getAccountTypeCode(), accountTypeId);
|
|
||||||
changesOnly.put("accountsRenamed", moved);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new CommandProcessingResultBuilder() //
|
return new CommandProcessingResultBuilder() //
|
||||||
.withCommandId(command.commandId()) //
|
.withCommandId(command.commandId()) //
|
||||||
.withEntityId(accountTypeId) //
|
.withEntityId(accountTypeId) //
|
||||||
@@ -137,47 +115,6 @@ public class DdsAccountTypeWritePlatformServiceJpaRepositoryImpl implements DdsA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Each ledger account a product names must be one an entry can land on, and of the kind its job
|
|
||||||
* needs - the same rule the savings product's Accounting tab applies by offering only those. The
|
|
||||||
* customers' money is owed, so a liability; their interest and the agents' commission are costs, so
|
|
||||||
* expenses.
|
|
||||||
*/
|
|
||||||
private void validateLedgerAccounts(final JsonCommand command) {
|
|
||||||
final List<ApiParameterError> errors = new ArrayList<>();
|
|
||||||
checkLedgerAccount(command, "depositLiabilityGlId", "Customer Deposits", List.of(LIABILITY), errors);
|
|
||||||
checkLedgerAccount(command, "interestExpenseGlId", "Interest Expense", List.of(EXPENSE), errors);
|
|
||||||
checkLedgerAccount(command, "commissionExpenseGlId", "Agent Commission Expense", List.of(EXPENSE), errors);
|
|
||||||
if (!errors.isEmpty()) {
|
|
||||||
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkLedgerAccount(final JsonCommand command, final String parameter, final String label, final List<Integer> kinds,
|
|
||||||
final List<ApiParameterError> errors) {
|
|
||||||
final Long glId = command.parameterExists(parameter) ? command.longValueOfParameterNamed(parameter) : null;
|
|
||||||
if (glId == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(GL_SQL, glId); // NOSONAR
|
|
||||||
final String code = "error.msg.dds.account.type." + parameter + ".invalid";
|
|
||||||
if (rows.isEmpty()) {
|
|
||||||
errors.add(ApiParameterError.parameterError(code, label + ": GL account " + glId + " does not exist.", parameter));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final Map<String, Object> gl = rows.get(0);
|
|
||||||
final String name = gl.get("name") + " (" + gl.get("gl_code") + ")";
|
|
||||||
if (Boolean.TRUE.equals(gl.get("disabled"))) {
|
|
||||||
errors.add(ApiParameterError.parameterError(code, label + ": " + name + " is disabled.", parameter));
|
|
||||||
} else if (((Number) gl.get("account_usage")).intValue() != DETAIL) {
|
|
||||||
errors.add(ApiParameterError.parameterError(code, label + ": " + name + " is a header account and cannot take entries.",
|
|
||||||
parameter));
|
|
||||||
} else if (!kinds.contains(((Number) gl.get("classification_enum")).intValue())) {
|
|
||||||
errors.add(ApiParameterError.parameterError(code, label + ": " + name + " is not "
|
|
||||||
+ (kinds.get(0) == LIABILITY ? "a liability" : "an expense") + " account.", parameter));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleDataIntegrityIssues(final Throwable realCause, final Exception dve) {
|
private void handleDataIntegrityIssues(final Throwable realCause, final Exception dve) {
|
||||||
if (realCause.getMessage() != null && realCause.getMessage().contains("account_type_code")) {
|
if (realCause.getMessage() != null && realCause.getMessage().contains("account_type_code")) {
|
||||||
throw new PlatformDataIntegrityException("error.msg.dds.account.type.duplicate.code",
|
throw new PlatformDataIntegrityException("error.msg.dds.account.type.duplicate.code",
|
||||||
|
|||||||
+2
-3
@@ -43,8 +43,7 @@ public class DdsAccountTypeConfiguration {
|
|||||||
@ConditionalOnMissingBean(DdsAccountTypeWritePlatformService.class)
|
@ConditionalOnMissingBean(DdsAccountTypeWritePlatformService.class)
|
||||||
public DdsAccountTypeWritePlatformService ddsAccountTypeWritePlatformService(
|
public DdsAccountTypeWritePlatformService ddsAccountTypeWritePlatformService(
|
||||||
DdsAccountTypeCommandFromApiJsonDeserializer fromApiJsonDeserializer, DdsAccountTypeRepository ddsAccountTypeRepository,
|
DdsAccountTypeCommandFromApiJsonDeserializer fromApiJsonDeserializer, DdsAccountTypeRepository ddsAccountTypeRepository,
|
||||||
PartyRepository partyRepository, JdbcTemplate jdbcTemplate) {
|
PartyRepository partyRepository) {
|
||||||
return new DdsAccountTypeWritePlatformServiceJpaRepositoryImpl(fromApiJsonDeserializer, ddsAccountTypeRepository, partyRepository,
|
return new DdsAccountTypeWritePlatformServiceJpaRepositoryImpl(fromApiJsonDeserializer, ddsAccountTypeRepository, partyRepository);
|
||||||
jdbcTemplate);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-509
@@ -26,12 +26,9 @@ import java.util.ArrayList;
|
|||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.fineract.commands.exception.CommandNotAwaitingApprovalException;
|
import org.apache.fineract.commands.exception.CommandNotAwaitingApprovalException;
|
||||||
import org.apache.fineract.commands.exception.CommandNotFoundException;
|
import org.apache.fineract.commands.exception.CommandNotFoundException;
|
||||||
import org.apache.fineract.portfolio.dds.accounting.DdsLedger;
|
|
||||||
import org.apache.fineract.portfolio.dds.accounting.exception.DdsLedgerNotConfiguredException;
|
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@@ -66,7 +63,6 @@ public class DdsApprovalReadService {
|
|||||||
private static final String REF_KEY_PREFIX = "DDS_COMMAND:";
|
private static final String REF_KEY_PREFIX = "DDS_COMMAND:";
|
||||||
|
|
||||||
private final JdbcTemplate jdbcTemplate;
|
private final JdbcTemplate jdbcTemplate;
|
||||||
private final DdsLedger ddsLedger;
|
|
||||||
|
|
||||||
public List<Map<String, Object>> list(final String status, final String fromDate, final String toDate) {
|
public List<Map<String, Object>> list(final String status, final String fromDate, final String toDate) {
|
||||||
final StringBuilder sql = new StringBuilder(ENTRIES_SQL).append(" and c.status = ? ");
|
final StringBuilder sql = new StringBuilder(ENTRIES_SQL).append(" and c.status = ? ");
|
||||||
@@ -99,423 +95,9 @@ public class DdsApprovalReadService {
|
|||||||
final Map<String, Object> row = describe(found.get(0));
|
final Map<String, Object> row = describe(found.get(0));
|
||||||
row.put("description", text(jsonOf(found.get(0).get("json")), "withdrawDescription"));
|
row.put("description", text(jsonOf(found.get(0).get("json")), "withdrawDescription"));
|
||||||
row.put("lines", linesFor(row, found.get(0)));
|
row.put("lines", linesFor(row, found.get(0)));
|
||||||
row.put("fields", fieldsFor(row, found.get(0)));
|
|
||||||
if (isAgentRequest(row)) {
|
|
||||||
row.put("guarantors", guarantorsOf(jsonOf(found.get(0).get("json"))));
|
|
||||||
}
|
|
||||||
if (isPartyRequest(row)) {
|
|
||||||
final JsonObject partyJson = jsonOf(found.get(0).get("json"));
|
|
||||||
row.put("nominees", nomineesOf(partyJson));
|
|
||||||
row.put("nomineeSplit", text(partyJson, "nomineeSplitMode"));
|
|
||||||
}
|
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A DDS account (party) being opened or changed - the entries whose review shows the account and its nominees. */
|
|
||||||
private static boolean isPartyRequest(final Map<String, Object> row) {
|
|
||||||
return "PARTY".equals(row.get("entity")) && ("CREATE".equals(row.get("action")) || "UPDATE".equals(row.get("action")));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** What a DDS account request carries, in the order the Party Master Entry screen asks for it. */
|
|
||||||
private record PartyField(String parameter, String label, String group, String kind) {}
|
|
||||||
|
|
||||||
private static final String PARTY_GROUP_CUSTOMER = "Customer & Scheme";
|
|
||||||
private static final String PARTY_GROUP_PERSONAL = "Personal & Contact";
|
|
||||||
private static final String PARTY_GROUP_MINOR = "Minor / Guardian";
|
|
||||||
private static final String PARTY_GROUP_TERMS = "Scheme Terms";
|
|
||||||
private static final String PARTY_GROUP_STATUS = "Balances & Status";
|
|
||||||
private static final String PARTY_GROUP_PAYOUT = "Maturity Payout";
|
|
||||||
|
|
||||||
private static final List<PartyField> PARTY_FIELDS = List.of( //
|
|
||||||
new PartyField("clientId", "Client", PARTY_GROUP_CUSTOMER, "client"), //
|
|
||||||
new PartyField("subType", "Scheme", PARTY_GROUP_CUSTOMER, "scheme"), //
|
|
||||||
new PartyField("agentId", "Agent", PARTY_GROUP_CUSTOMER, "agent"), //
|
|
||||||
new PartyField("officeId", "Branch", PARTY_GROUP_CUSTOMER, "office"), //
|
|
||||||
new PartyField("nominal", "Nominal", PARTY_GROUP_CUSTOMER, "yesno"), //
|
|
||||||
new PartyField("partyName", "Party Name", PARTY_GROUP_PERSONAL, "text"), //
|
|
||||||
new PartyField("partyName1", "Party Name 1", PARTY_GROUP_PERSONAL, "text"), //
|
|
||||||
new PartyField("address1", "Address 1", PARTY_GROUP_PERSONAL, "text"), //
|
|
||||||
new PartyField("address2", "Address 2", PARTY_GROUP_PERSONAL, "text"), //
|
|
||||||
new PartyField("address3", "Address 3", PARTY_GROUP_PERSONAL, "text"), //
|
|
||||||
new PartyField("city", "City", PARTY_GROUP_PERSONAL, "text"), //
|
|
||||||
new PartyField("pincode", "Pincode", PARTY_GROUP_PERSONAL, "text"), //
|
|
||||||
new PartyField("gender", "Gender", PARTY_GROUP_PERSONAL, "text"), //
|
|
||||||
new PartyField("telephone", "Telephone", PARTY_GROUP_PERSONAL, "text"), //
|
|
||||||
new PartyField("mobileNoSms", "Mobile No (SMS)", PARTY_GROUP_PERSONAL, "text"), //
|
|
||||||
new PartyField("aadharNo", "Aadhar No", PARTY_GROUP_PERSONAL, "text"), //
|
|
||||||
new PartyField("panCardNo", "PAN Card No", PARTY_GROUP_PERSONAL, "text"), //
|
|
||||||
new PartyField("ckycId", "CKYC ID", PARTY_GROUP_PERSONAL, "text"), //
|
|
||||||
new PartyField("operation", "Operation", PARTY_GROUP_PERSONAL, "text"), //
|
|
||||||
new PartyField("birthDate", "Birth Date", PARTY_GROUP_PERSONAL, "text"), //
|
|
||||||
new PartyField("occupation", "Occupation", PARTY_GROUP_PERSONAL, "text"), //
|
|
||||||
new PartyField("risk", "Risk", PARTY_GROUP_PERSONAL, "text"), //
|
|
||||||
new PartyField("adultMinor", "Adult / Minor", PARTY_GROUP_MINOR, "text"), //
|
|
||||||
new PartyField("guardianClientId", "Guardian", PARTY_GROUP_MINOR, "client"), //
|
|
||||||
new PartyField("relation", "Relation", PARTY_GROUP_MINOR, "text"), //
|
|
||||||
new PartyField("minorDate", "Comes of Age On", PARTY_GROUP_MINOR, "text"), //
|
|
||||||
new PartyField("minAccountOpt", "Minor Account Option", PARTY_GROUP_MINOR, "text"), //
|
|
||||||
new PartyField("openingDate", "Opening Date", PARTY_GROUP_TERMS, "text"), //
|
|
||||||
new PartyField("dailyDepositAmount", "Daily Deposit", PARTY_GROUP_TERMS, "money"), //
|
|
||||||
new PartyField("days", "Days", PARTY_GROUP_TERMS, "text"), //
|
|
||||||
new PartyField("month", "Months", PARTY_GROUP_TERMS, "text"), //
|
|
||||||
new PartyField("depositAmount", "Deposit Amount", PARTY_GROUP_TERMS, "money"), //
|
|
||||||
new PartyField("maturityDate", "Maturity Date", PARTY_GROUP_TERMS, "text"), //
|
|
||||||
new PartyField("ledgerNo", "Ledger No", PARTY_GROUP_TERMS, "text"), //
|
|
||||||
new PartyField("reopenDate", "Reopen Date", PARTY_GROUP_TERMS, "text"), //
|
|
||||||
new PartyField("freezeCode", "Freeze Code", PARTY_GROUP_STATUS, "text"), //
|
|
||||||
new PartyField("lien", "Lien", PARTY_GROUP_STATUS, "yesno"), //
|
|
||||||
new PartyField("lienReason", "Lien Reason", PARTY_GROUP_STATUS, "text"), //
|
|
||||||
new PartyField("lienReasonOther", "Lien Reason (Other)", PARTY_GROUP_STATUS, "text"), //
|
|
||||||
new PartyField("isSmsActive", "SMS Active", PARTY_GROUP_STATUS, "yesno"), //
|
|
||||||
new PartyField("isEmailActive", "Email Active", PARTY_GROUP_STATUS, "yesno"), //
|
|
||||||
new PartyField("od1Flag", "OD 1", PARTY_GROUP_STATUS, "yesno"), //
|
|
||||||
new PartyField("od2Flag", "OD 2", PARTY_GROUP_STATUS, "yesno"), //
|
|
||||||
new PartyField("payDate", "Pay Date", PARTY_GROUP_STATUS, "text"), //
|
|
||||||
new PartyField("interestDate", "Interest Date", PARTY_GROUP_STATUS, "text"), //
|
|
||||||
new PartyField("spDescription", "Description", PARTY_GROUP_STATUS, "text"), //
|
|
||||||
new PartyField("advCode", "Adv Code", PARTY_GROUP_PAYOUT, "text"), //
|
|
||||||
new PartyField("creditToBranch", "Credit To Branch", PARTY_GROUP_PAYOUT, "text"), //
|
|
||||||
new PartyField("creditToAccount", "Credit To A/C", PARTY_GROUP_PAYOUT, "text"), //
|
|
||||||
new PartyField("creditToAccountType", "Credit To A/C Type", PARTY_GROUP_PAYOUT, "text"));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A DDS account request laid out for the checker: the client, scheme, agent and branch by name
|
|
||||||
* rather than by number, in the sections the screen has. Only what the request carries is shown, so
|
|
||||||
* an edit lists just the fields it changes, and a field left empty is left out.
|
|
||||||
*/
|
|
||||||
private List<Map<String, Object>> partyFieldsFor(final JsonObject json) {
|
|
||||||
final List<Map<String, Object>> fields = new ArrayList<>();
|
|
||||||
for (final PartyField field : PARTY_FIELDS) {
|
|
||||||
if (!json.has(field.parameter()) || json.get(field.parameter()).isJsonNull()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final String raw = json.get(field.parameter()).getAsString();
|
|
||||||
if (raw.isBlank()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final String value = switch (field.kind()) {
|
|
||||||
case "client" -> clientLabel(raw);
|
|
||||||
case "scheme" -> nameOr(first("select account_type_code || ' · ' || account_type_description from m_dds_account_type "
|
|
||||||
+ "where account_type_code = ?", raw), raw);
|
|
||||||
case "agent" -> nameOr(first("select agent_code || ' · ' || coalesce(agent_name, '') from m_dds_agent where id = ?",
|
|
||||||
parseLong(raw)), "Agent #" + raw);
|
|
||||||
case "office" -> nameOr(first("select name from m_office where id = ?", parseLong(raw)), "Branch #" + raw);
|
|
||||||
case "yesno" -> Boolean.parseBoolean(raw.trim()) ? "Yes" : "No";
|
|
||||||
case "money" -> "₹" + plain(raw);
|
|
||||||
default -> raw.trim();
|
|
||||||
};
|
|
||||||
final Map<String, Object> out = new LinkedHashMap<>();
|
|
||||||
out.put("key", field.parameter());
|
|
||||||
out.put("label", field.label());
|
|
||||||
out.put("group", field.group());
|
|
||||||
out.put("format", "yesno".equals(field.kind()) ? "yesno" : "text");
|
|
||||||
out.put("value", value);
|
|
||||||
fields.add(out);
|
|
||||||
}
|
|
||||||
return fields;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The nominees a DDS account request names, with what each is to receive. */
|
|
||||||
private List<Map<String, Object>> nomineesOf(final JsonObject json) {
|
|
||||||
final List<Map<String, Object>> nominees = new ArrayList<>();
|
|
||||||
if (!json.has("nominees") || !json.get("nominees").isJsonArray()) {
|
|
||||||
return nominees;
|
|
||||||
}
|
|
||||||
for (final JsonElement element : json.getAsJsonArray("nominees")) {
|
|
||||||
if (!element.isJsonObject()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final JsonObject n = element.getAsJsonObject();
|
|
||||||
final Map<String, Object> out = new LinkedHashMap<>();
|
|
||||||
out.put("name", text(n, "name"));
|
|
||||||
out.put("relation", text(n, "relation"));
|
|
||||||
out.put("gender", text(n, "gender"));
|
|
||||||
out.put("mobileNo", text(n, "mobileNo"));
|
|
||||||
out.put("birthDate", text(n, "birthDate"));
|
|
||||||
out.put("nominationDate", text(n, "nominationDate"));
|
|
||||||
out.put("guardianName", text(n, "guardianName"));
|
|
||||||
out.put("share", text(n, "sharePercent") != null ? plain(text(n, "sharePercent")) + "%"
|
|
||||||
: text(n, "sequenceNo") != null ? "Order " + text(n, "sequenceNo") : null);
|
|
||||||
nominees.add(out);
|
|
||||||
}
|
|
||||||
return nominees;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** An agent being opened or changed - the entries whose review shows the agent and their guarantors. */
|
|
||||||
private static boolean isAgentRequest(final Map<String, Object> row) {
|
|
||||||
return "AGENT".equals(row.get("entity")) && ("CREATE".equals(row.get("action")) || "UPDATE".equals(row.get("action")));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** What an agent request carries, in the order the Agent Master Entry screen asks for it. */
|
|
||||||
private record AgentField(String parameter, String label, String group, String kind) {}
|
|
||||||
|
|
||||||
private static final String AGENT_GROUP = "Agent";
|
|
||||||
private static final String AGENT_ADDRESS_GROUP = "Address & Contact";
|
|
||||||
private static final String AGENT_ACCOUNTS_GROUP = "Branch & Accounts";
|
|
||||||
private static final String AGENT_BALANCES_GROUP = "Balances";
|
|
||||||
|
|
||||||
private static final List<AgentField> AGENT_FIELDS = List.of( //
|
|
||||||
new AgentField("clientId", "Client", AGENT_GROUP, "client"), //
|
|
||||||
new AgentField("agentNameId", "Agent Name", AGENT_GROUP, "text"), //
|
|
||||||
new AgentField("agentBirthDate", "Birth Date", AGENT_GROUP, "text"), //
|
|
||||||
new AgentField("agentOpeningDate", "Opening Date", AGENT_GROUP, "text"), //
|
|
||||||
new AgentField("agentAddress1", "Address (Line 1)", AGENT_ADDRESS_GROUP, "text"), //
|
|
||||||
new AgentField("agentAddress2", "Address (Line 2)", AGENT_ADDRESS_GROUP, "text"), //
|
|
||||||
new AgentField("agentCity", "City", AGENT_ADDRESS_GROUP, "text"), //
|
|
||||||
new AgentField("agentPinCode", "PinCode", AGENT_ADDRESS_GROUP, "text"), //
|
|
||||||
new AgentField("agentTelephone", "Telephone", AGENT_ADDRESS_GROUP, "text"), //
|
|
||||||
new AgentField("officeId", "Home Branch", AGENT_ACCOUNTS_GROUP, "office"), //
|
|
||||||
new AgentField("glAccountId", "GL Account", AGENT_ACCOUNTS_GROUP, "gl"), //
|
|
||||||
new AgentField("agentSavingAccountType", "Saving A/C Type", AGENT_ACCOUNTS_GROUP, "text"), //
|
|
||||||
new AgentField("agentSavingAccountNo", "Saving A/C No", AGENT_ACCOUNTS_GROUP, "text"), //
|
|
||||||
new AgentField("agentOpeningBalance", "Opening Balance", AGENT_BALANCES_GROUP, "money"));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An agent request laid out for the checker: the client, the branch and the ledger account by name
|
|
||||||
* rather than by number, in the sections the screen has. Only what the request carries is shown, so
|
|
||||||
* an edit lists just the fields it changes.
|
|
||||||
*/
|
|
||||||
private List<Map<String, Object>> agentFieldsFor(final JsonObject json) {
|
|
||||||
final List<Map<String, Object>> fields = new ArrayList<>();
|
|
||||||
for (final AgentField field : AGENT_FIELDS) {
|
|
||||||
if (!json.has(field.parameter()) || json.get(field.parameter()).isJsonNull()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final String raw = json.get(field.parameter()).getAsString();
|
|
||||||
// A new agent starts with nothing, so a zero opening balance is not something to approve and
|
|
||||||
// is left out. One that is not zero - an edit that sets it - is shown.
|
|
||||||
if ("money".equals(field.kind()) && isZero(raw)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final String value = switch (field.kind()) {
|
|
||||||
case "client" -> clientLabel(raw);
|
|
||||||
case "office" -> nameOr(first("select name from m_office where id = ?", parseLong(raw)), "Branch #" + raw);
|
|
||||||
case "gl" -> nameOr(first("select gl_code || ' · ' || name from acc_gl_account where id = ?", parseLong(raw)),
|
|
||||||
"Account #" + raw);
|
|
||||||
case "money" -> "₹" + plain(raw);
|
|
||||||
default -> raw.trim();
|
|
||||||
};
|
|
||||||
final Map<String, Object> out = new LinkedHashMap<>();
|
|
||||||
out.put("key", field.parameter());
|
|
||||||
out.put("label", field.label());
|
|
||||||
out.put("group", field.group());
|
|
||||||
out.put("format", "text");
|
|
||||||
out.put("value", value);
|
|
||||||
fields.add(out);
|
|
||||||
}
|
|
||||||
return fields;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The guarantors an agent request names, each with the client they are by number and name. */
|
|
||||||
private List<Map<String, Object>> guarantorsOf(final JsonObject json) {
|
|
||||||
final List<Map<String, Object>> guarantors = new ArrayList<>();
|
|
||||||
if (!json.has("guarantors") || !json.get("guarantors").isJsonArray()) {
|
|
||||||
return guarantors;
|
|
||||||
}
|
|
||||||
for (final JsonElement element : json.getAsJsonArray("guarantors")) {
|
|
||||||
if (!element.isJsonObject()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final JsonObject g = element.getAsJsonObject();
|
|
||||||
final Map<String, Object> out = new LinkedHashMap<>();
|
|
||||||
out.put("client", text(g, "clientId") == null ? null : clientLabel(text(g, "clientId")));
|
|
||||||
out.put("name", text(g, "guarantorName"));
|
|
||||||
out.put("telephone", text(g, "telephone"));
|
|
||||||
out.put("address", joined(", ", text(g, "address1"), text(g, "address2"), text(g, "city"), text(g, "pinCode")));
|
|
||||||
guarantors.add(out);
|
|
||||||
}
|
|
||||||
return guarantors;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An agent's code, name, client, branch, savings account and guarantor count. An agent already on
|
|
||||||
* the books is read as they stand; one still being opened does not exist yet, so is read from the
|
|
||||||
* request and has no code until approval gives it one.
|
|
||||||
*/
|
|
||||||
private Map<String, Object> agentColumns(final Long agentId, final JsonObject json) {
|
|
||||||
final Map<String, Object> columns = new LinkedHashMap<>();
|
|
||||||
if (agentId != null) {
|
|
||||||
columns.putAll(this.jdbcTemplate.queryForList( // NOSONAR
|
|
||||||
"select a.agent_code as \"agentCode\", a.agent_name as \"agentName\", c.display_name as \"clientName\", "
|
|
||||||
+ "o.name as \"officeName\", a.agent_saving_account_no as \"savingAccountNo\", "
|
|
||||||
+ "(select count(*) from m_dds_agent_guarantor g where g.agent_id = a.id) as \"guarantorCount\" "
|
|
||||||
+ "from m_dds_agent a left join m_client c on c.id = a.client_id left join m_office o on o.id = a.office_id "
|
|
||||||
+ "where a.id = ?",
|
|
||||||
agentId).stream().findFirst().orElse(Map.of()));
|
|
||||||
return columns;
|
|
||||||
}
|
|
||||||
final String clientId = text(json, "clientId");
|
|
||||||
columns.put("agentCode", null);
|
|
||||||
columns.put("agentName", text(json, "agentNameId"));
|
|
||||||
columns.put("clientName", clientId == null ? null : first("select display_name from m_client where id = ?", parseLong(clientId)));
|
|
||||||
final String officeId = text(json, "officeId");
|
|
||||||
columns.put("officeName", officeId == null ? null : first("select name from m_office where id = ?", parseLong(officeId)));
|
|
||||||
columns.put("savingAccountNo", text(json, "agentSavingAccountNo"));
|
|
||||||
columns.put("guarantorCount", json.has("guarantors") && json.get("guarantors").isJsonArray() ? json.getAsJsonArray("guarantors").size() : 0);
|
|
||||||
return columns;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String clientLabel(final String clientId) {
|
|
||||||
final String name = first("select display_name from m_client where id = ?", parseLong(clientId));
|
|
||||||
return name == null ? "Client #" + clientId : clientId + " · " + name;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The name an agent request gives, or the client it is for when none was typed. */
|
|
||||||
private String agentNameOf(final JsonObject json) {
|
|
||||||
final String name = text(json, "agentNameId");
|
|
||||||
if (name != null && !name.isBlank()) {
|
|
||||||
return name.trim();
|
|
||||||
}
|
|
||||||
final String clientId = text(json, "clientId");
|
|
||||||
return clientId == null ? "" : clientLabel(clientId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isZero(final String amount) {
|
|
||||||
try {
|
|
||||||
return new BigDecimal(amount.trim()).signum() == 0;
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String nameOr(final String found, final String fallback) {
|
|
||||||
return found == null ? fallback : found;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String joined(final String separator, final String... parts) {
|
|
||||||
final List<String> present = new ArrayList<>();
|
|
||||||
for (final String part : parts) {
|
|
||||||
if (part != null && !part.isBlank()) {
|
|
||||||
present.add(part.trim());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return String.join(separator, present);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A number an agent request carried as text, or -1 - which no row has - when it was not one. */
|
|
||||||
private static Long parseLong(final String value) {
|
|
||||||
try {
|
|
||||||
return Long.valueOf(value.trim());
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
return -1L;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A DDS product's terms, in the order the Account Type screen asks for them: the column it is
|
|
||||||
* stored in, the parameter the request carries it as, its label, how it reads, and the section of
|
|
||||||
* the review it is shown in.
|
|
||||||
*/
|
|
||||||
private record ProductField(String column, String parameter, String label, String format, String group) {}
|
|
||||||
|
|
||||||
private static final String GROUP_PRODUCT = "Product";
|
|
||||||
private static final String GROUP_INTEREST = "Interest";
|
|
||||||
private static final String GROUP_CLOSURE = "Maturity & Premature Closure";
|
|
||||||
private static final String GROUP_ACCOUNTING = "Accounting";
|
|
||||||
|
|
||||||
private static final List<ProductField> PRODUCT_FIELDS = List.of( //
|
|
||||||
new ProductField("account_type_group", "accountTypeGroup", "Account Type Group", "text", GROUP_PRODUCT), //
|
|
||||||
new ProductField("account_type_code", "accountTypeCode", "Account Type Code", "text", GROUP_PRODUCT), //
|
|
||||||
new ProductField("account_type_description", "accountTypeDescription", "Description", "text", GROUP_PRODUCT), //
|
|
||||||
new ProductField("is_active", "active", "Active", "yesno", GROUP_PRODUCT), //
|
|
||||||
new ProductField("interest_rate", "interestRate", "Interest Rate", "percent", GROUP_INTEREST), //
|
|
||||||
new ProductField("auto_interest_post", "autoInterestPost", "Auto Interest Post", "yesno", GROUP_INTEREST), //
|
|
||||||
new ProductField("auto_interest_post_when", "autoInterestPostWhen", "Auto Interest Post When", "text", GROUP_INTEREST), //
|
|
||||||
new ProductField("maturity_auto_close", "maturityAutoClose", "Maturity Auto Close", "yesno", GROUP_CLOSURE), //
|
|
||||||
new ProductField("premature_lockin_months", "prematureLockinMonths", "Premature Lock-in", "months", GROUP_CLOSURE), //
|
|
||||||
new ProductField("premature_interest_reduction", "prematureInterestReduction", "Premature Interest Reduction", "percent",
|
|
||||||
GROUP_CLOSURE), //
|
|
||||||
new ProductField("premature_agent_commission_penalty", "prematureAgentCommissionPenalty", "Agent Commission Penalty",
|
|
||||||
"percent", GROUP_CLOSURE), //
|
|
||||||
new ProductField("deposit_liability_gl_id", "depositLiabilityGlId", "DDS Customer Deposits (Liability)", "gl",
|
|
||||||
GROUP_ACCOUNTING), //
|
|
||||||
new ProductField("interest_expense_gl_id", "interestExpenseGlId", "Interest Expense", "gl", GROUP_ACCOUNTING), //
|
|
||||||
new ProductField("commission_expense_gl_id", "commissionExpenseGlId", "Agent Commission Expense", "gl", GROUP_ACCOUNTING));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* What an entry that moves no money actually asks for, field by field, so the checker can read the
|
|
||||||
* terms they are approving rather than only a code.
|
|
||||||
*
|
|
||||||
* An edit still waiting is shown against the product as it stands, with each changed field marked,
|
|
||||||
* because what matters there is what will change. Once actioned, the product has already moved on,
|
|
||||||
* so only the values the request carried are shown.
|
|
||||||
*/
|
|
||||||
private List<Map<String, Object>> fieldsFor(final Map<String, Object> row, final Map<String, Object> entry) {
|
|
||||||
if (isAgentRequest(row)) {
|
|
||||||
return agentFieldsFor(jsonOf(entry.get("json")));
|
|
||||||
}
|
|
||||||
if (isPartyRequest(row)) {
|
|
||||||
return partyFieldsFor(jsonOf(entry.get("json")));
|
|
||||||
}
|
|
||||||
if (!"DDSACCOUNTTYPE".equals(row.get("entity"))) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
final JsonObject json = jsonOf(entry.get("json"));
|
|
||||||
final Long resourceId = (Long) row.get("resourceId");
|
|
||||||
final boolean compare = "UPDATE".equals(row.get("action")) && "PENDING".equals(row.get("status")) && resourceId != null;
|
|
||||||
final Map<String, Object> current = compare
|
|
||||||
? this.jdbcTemplate.queryForList("select * from m_dds_account_type where id = ?", resourceId).stream().findFirst() // NOSONAR
|
|
||||||
.orElse(Map.of())
|
|
||||||
: Map.of();
|
|
||||||
|
|
||||||
final List<Map<String, Object>> fields = new ArrayList<>();
|
|
||||||
for (final ProductField field : PRODUCT_FIELDS) {
|
|
||||||
final boolean requested = json.has(field.parameter()) && !json.get(field.parameter()).isJsonNull();
|
|
||||||
final String proposed = requested ? display(json.get(field.parameter()).getAsString(), field.format()) : null;
|
|
||||||
final Map<String, Object> out = new LinkedHashMap<>();
|
|
||||||
out.put("key", field.parameter());
|
|
||||||
out.put("label", field.label());
|
|
||||||
out.put("group", field.group());
|
|
||||||
out.put("format", field.format());
|
|
||||||
if (compare) {
|
|
||||||
final Object stored = current.get(field.column());
|
|
||||||
final String was = stored == null ? null : display(String.valueOf(stored), field.format());
|
|
||||||
final String now = requested ? proposed : was;
|
|
||||||
out.put("current", was);
|
|
||||||
out.put("value", now);
|
|
||||||
out.put("changed", requested && !Objects.equals(was, now));
|
|
||||||
} else if (requested) {
|
|
||||||
out.put("value", proposed);
|
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
fields.add(out);
|
|
||||||
}
|
|
||||||
return fields;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** As format, and a ledger account id read as the account itself: "DDS CUSTOMER DEPOSITS (200601)". */
|
|
||||||
private String display(final String value, final String format) {
|
|
||||||
if (!"gl".equals(format) || value == null || value.isBlank()) {
|
|
||||||
return format(value, format);
|
|
||||||
}
|
|
||||||
final Map<String, Object> gl = this.jdbcTemplate.queryForList("select gl_code, name from acc_gl_account where id = ?", // NOSONAR
|
|
||||||
parseLong(value)).stream().findFirst().orElse(Map.of());
|
|
||||||
return gl.isEmpty() ? "GL " + value.trim() + " (not found)" : gl.get("name") + " (" + gl.get("gl_code") + ")";
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A stored or requested value as the screen shows it: Yes/No, 2%, 12 months. */
|
|
||||||
private static String format(final String value, final String format) {
|
|
||||||
if (value == null || value.isBlank()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return switch (format) {
|
|
||||||
case "yesno" -> Boolean.parseBoolean(value.trim()) ? "Yes" : "No";
|
|
||||||
case "percent" -> plain(value) + "%";
|
|
||||||
case "months" -> plain(value) + " month" + ("1".equals(plain(value)) ? "" : "s");
|
|
||||||
default -> value.trim();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 2.000000 reads as 2, and 1.50 as 1.5, so a stored rate and a typed one compare equal. */
|
|
||||||
private static String plain(final String value) {
|
|
||||||
try {
|
|
||||||
return new BigDecimal(value.trim()).stripTrailingZeros().toPlainString();
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
return value.trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The debit and credit sides of the entry, as its voucher shows them. Once the money has moved they
|
* The debit and credit sides of the entry, as its voucher shows them. Once the money has moved they
|
||||||
* are the voucher's own lines; while it waits they are what approving it will move, worked out from
|
* are the voucher's own lines; while it waits they are what approving it will move, worked out from
|
||||||
@@ -578,49 +160,13 @@ public class DdsApprovalReadService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "DDSBATCH:POST" -> {
|
case "DDSBATCH:POST" -> {
|
||||||
// The expense side, on the accounts of the DDS products concerned - interest on each
|
final String expense = first("select c.name from m_dds_batch_run r join c_configuration c on c.name = "
|
||||||
// customer's product's interest account, commission shared over the commission accounts
|
+ "case when r.job_type like 'COMMISSION%' then 'dds-agent-commission-expense-account' "
|
||||||
// of the products each agent collected for, as the posting itself will book it.
|
+ "else 'dds-interest-expense-account' end where r.id = ?", resourceId);
|
||||||
final String jobType = first("select job_type from m_dds_batch_run where id = ?", resourceId);
|
final Map<String, Object> gl = expense == null ? Map.of()
|
||||||
// A calculation books nothing itself; its lines are what the matching Posting run will
|
: this.jdbcTemplate.queryForList("select g.gl_code as \"code\", g.name from c_configuration c " // NOSONAR
|
||||||
// book, shown so the checker sees which accounts and ledger accounts the figures belong to.
|
+ "join acc_gl_account g on g.id = c.value where c.name = ?", expense).stream().findFirst().orElse(Map.of());
|
||||||
final boolean projected = jobType != null && jobType.endsWith("_CALCULATION");
|
lines.add(line("DR", (String) gl.get("code"), (String) gl.get("name"), "GL", amount));
|
||||||
if (jobType != null && jobType.startsWith("COMMISSION")) {
|
|
||||||
final Map<String, BigDecimal> byAccount = new LinkedHashMap<>();
|
|
||||||
final Map<String, String> names = new LinkedHashMap<>();
|
|
||||||
for (final Map<String, Object> runRow : this.jdbcTemplate.queryForList( // NOSONAR
|
|
||||||
"select agent_id, amount, to_date from m_dds_batch_run_row where batch_run_id = ? and amount > 0 order by id",
|
|
||||||
resourceId)) {
|
|
||||||
try {
|
|
||||||
final long agentOfRow = ((Number) runRow.get("agent_id")).longValue();
|
|
||||||
final BigDecimal amountOfRow = (BigDecimal) runRow.get("amount");
|
|
||||||
(projected && runRow.get("to_date") != null
|
|
||||||
? this.ddsLedger.projectedCommissionSplit(agentOfRow, amountOfRow,
|
|
||||||
((java.sql.Date) runRow.get("to_date")).toLocalDate())
|
|
||||||
: this.ddsLedger.commissionSplit(agentOfRow, amountOfRow, resourceId))
|
|
||||||
.forEach((account, share) -> {
|
|
||||||
byAccount.merge(account.getGlCode(), share, BigDecimal::add);
|
|
||||||
names.put(account.getGlCode(), account.getName());
|
|
||||||
});
|
|
||||||
} catch (final DdsLedgerNotConfiguredException e) {
|
|
||||||
byAccount.merge("", (BigDecimal) runRow.get("amount"), BigDecimal::add);
|
|
||||||
names.put("", "Agent Commission Expense not set on the DDS product");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
byAccount.forEach((code, share) -> lines.add(line("DR", code.isEmpty() ? null : code, names.get(code), "GL", share)));
|
|
||||||
} else {
|
|
||||||
for (final Map<String, Object> gl : this.jdbcTemplate.queryForList( // NOSONAR
|
|
||||||
"select g.gl_code as \"code\", coalesce(g.name, 'Interest Expense not set on DDS product ' || "
|
|
||||||
+ "upper(trim(p.sub_type))) as \"name\", sum(r.amount) as \"amount\" from m_dds_batch_run_row r "
|
|
||||||
+ "join m_dds_party p on p.id = r.party_id "
|
|
||||||
+ "left join m_dds_account_type t on t.account_type_code = upper(trim(p.sub_type)) "
|
|
||||||
+ "left join acc_gl_account g on g.id = t.interest_expense_gl_id "
|
|
||||||
+ "where r.batch_run_id = ? and r.amount > 0 group by g.gl_code, g.name, upper(trim(p.sub_type)) "
|
|
||||||
+ "order by g.gl_code",
|
|
||||||
resourceId)) {
|
|
||||||
lines.add(line("DR", (String) gl.get("code"), (String) gl.get("name"), "GL", (BigDecimal) gl.get("amount")));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (final Map<String, Object> credited : this.jdbcTemplate.queryForList( // NOSONAR
|
for (final Map<String, Object> credited : this.jdbcTemplate.queryForList( // NOSONAR
|
||||||
"select coalesce(p.account_no, a.agent_saving_account_no) as \"accountNo\", "
|
"select coalesce(p.account_no, a.agent_saving_account_no) as \"accountNo\", "
|
||||||
+ "coalesce(p.party_code || ' · ' || p.party_name, a.agent_code || ' · ' || a.agent_name) as \"accountName\", "
|
+ "coalesce(p.party_code || ' · ' || p.party_name, a.agent_code || ' · ' || a.agent_name) as \"accountName\", "
|
||||||
@@ -631,10 +177,6 @@ public class DdsApprovalReadService {
|
|||||||
lines.add(line("CR", (String) credited.get("accountNo"), (String) credited.get("accountName"),
|
lines.add(line("CR", (String) credited.get("accountNo"), (String) credited.get("accountName"),
|
||||||
(String) credited.get("type"), (BigDecimal) credited.get("amount")));
|
(String) credited.get("type"), (BigDecimal) credited.get("amount")));
|
||||||
}
|
}
|
||||||
if (projected) {
|
|
||||||
// Marked, so the review says these entries are made by the Posting run, not on approval.
|
|
||||||
lines.forEach(each -> each.put("projected", true));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
default -> {
|
default -> {
|
||||||
// A change to a party, agent or scheme moves no money and has no sides.
|
// A change to a party, agent or scheme moves no money and has no sides.
|
||||||
@@ -772,15 +314,11 @@ public class DdsApprovalReadService {
|
|||||||
case "CLOSE" -> "Close Agent";
|
case "CLOSE" -> "Close Agent";
|
||||||
default -> "Edit Agent";
|
default -> "Edit Agent";
|
||||||
};
|
};
|
||||||
details = resourceId != null ? agent(resourceId) : agentNameOf(json);
|
details = resourceId != null ? agent(resourceId) : String.valueOf(text(json, "agentName"));
|
||||||
}
|
}
|
||||||
case "DDSACCOUNTTYPE:CREATE", "DDSACCOUNTTYPE:UPDATE" -> {
|
case "DDSACCOUNTTYPE:CREATE", "DDSACCOUNTTYPE:UPDATE" -> {
|
||||||
type = "CREATE".equals(action) ? "New DDS Product" : "Edit DDS Product";
|
type = "CREATE".equals(action) ? "New DDS Product" : "Edit DDS Product";
|
||||||
details = text(json, "accountTypeCode") != null ? text(json, "accountTypeCode") : scheme(resourceId);
|
details = text(json, "accountTypeCode") != null ? text(json, "accountTypeCode") : scheme(resourceId);
|
||||||
final String description = text(json, "accountTypeDescription");
|
|
||||||
if (description != null && !description.isBlank()) {
|
|
||||||
details = details == null || details.isBlank() ? description : details + " · " + description;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case "COMMISSIONSLAB:CREATE", "COMMISSIONSLAB:UPDATE" -> {
|
case "COMMISSIONSLAB:CREATE", "COMMISSIONSLAB:UPDATE" -> {
|
||||||
type = "CREATE".equals(action) ? "New Commission Slab" : "Edit Commission Slab";
|
type = "CREATE".equals(action) ? "New Commission Slab" : "Edit Commission Slab";
|
||||||
@@ -804,14 +342,6 @@ public class DdsApprovalReadService {
|
|||||||
row.put("type", type);
|
row.put("type", type);
|
||||||
row.put("details", details);
|
row.put("details", details);
|
||||||
row.put("amount", amount);
|
row.put("amount", amount);
|
||||||
if ("PARTY".equals(entity) && ("CREATE".equals(action) || "UPDATE".equals(action))) {
|
|
||||||
// The account in the columns Approvals -> Pending Account lists every other account by.
|
|
||||||
row.putAll(accountColumns(resourceId, json));
|
|
||||||
}
|
|
||||||
if ("AGENT".equals(entity)) {
|
|
||||||
// The agent in the columns Approvals -> Agent Approval lists them by.
|
|
||||||
row.putAll(agentColumns(resourceId, json));
|
|
||||||
}
|
|
||||||
final Long commandId = ((Number) entry.get("id")).longValue();
|
final Long commandId = ((Number) entry.get("id")).longValue();
|
||||||
final String voucherNo = switch (status) {
|
final String voucherNo = switch (status) {
|
||||||
case STATUS_PROCESSED -> voucherFor(commandId, entity, action, resourceId, json, (String) entry.get("madeOn"));
|
case STATUS_PROCESSED -> voucherFor(commandId, entity, action, resourceId, json, (String) entry.get("madeOn"));
|
||||||
@@ -898,37 +428,6 @@ public class DdsApprovalReadService {
|
|||||||
return found == null ? "Agent #" + agentId : found.trim();
|
return found == null ? "Agent #" + agentId : found.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A DDS account's number, holder, scheme, branch and agent. An edit reads the account as it
|
|
||||||
* stands; a new account does not exist until approved, so it is read from the request, and has
|
|
||||||
* no number yet.
|
|
||||||
*/
|
|
||||||
private Map<String, Object> accountColumns(final Long partyId, final JsonObject json) {
|
|
||||||
final Map<String, Object> columns = new LinkedHashMap<>();
|
|
||||||
if (partyId != null) {
|
|
||||||
final Map<String, Object> stored = this.jdbcTemplate.queryForList( // NOSONAR
|
|
||||||
"select p.account_no as \"accountNo\", p.party_name as \"clientName\", p.sub_type as \"subType\", "
|
|
||||||
+ "o.name as \"officeName\", trim(a.agent_code || ' ' || coalesce(a.agent_name, '')) as \"agentName\" "
|
|
||||||
+ "from m_dds_party p left join m_office o on o.id = p.office_id "
|
|
||||||
+ "left join m_dds_agent a on a.id = p.agent_id where p.id = ?",
|
|
||||||
partyId).stream().findFirst().orElse(Map.of());
|
|
||||||
columns.putAll(stored);
|
|
||||||
} else {
|
|
||||||
columns.put("accountNo", null);
|
|
||||||
columns.put("clientName", text(json, "partyName"));
|
|
||||||
columns.put("subType", text(json, "subType"));
|
|
||||||
final Long officeId = longOf(json, "officeId");
|
|
||||||
columns.put("officeName", officeId == null ? null : first("select name from m_office where id = ?", officeId));
|
|
||||||
final Long agentId = longOf(json, "agentId");
|
|
||||||
columns.put("agentName", agentId == null ? null : agent(agentId));
|
|
||||||
}
|
|
||||||
final String subType = (String) columns.remove("subType");
|
|
||||||
final String description = subType == null ? null
|
|
||||||
: first("select account_type_description from m_dds_account_type where account_type_code = ?", subType);
|
|
||||||
columns.put("productName", description == null ? subType : subType + " · " + description);
|
|
||||||
return columns;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String scheme(final Long accountTypeId) {
|
private String scheme(final Long accountTypeId) {
|
||||||
return accountTypeId == null ? "" : first("select account_type_code from m_dds_account_type where id = ?", accountTypeId);
|
return accountTypeId == null ? "" : first("select account_type_code from m_dds_account_type where id = ?", accountTypeId);
|
||||||
}
|
}
|
||||||
|
|||||||
-17
@@ -22,7 +22,6 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.ws.rs.Consumes;
|
import jakarta.ws.rs.Consumes;
|
||||||
import jakarta.ws.rs.DELETE;
|
|
||||||
import jakarta.ws.rs.GET;
|
import jakarta.ws.rs.GET;
|
||||||
import jakarta.ws.rs.POST;
|
import jakarta.ws.rs.POST;
|
||||||
import jakarta.ws.rs.Path;
|
import jakarta.ws.rs.Path;
|
||||||
@@ -39,8 +38,6 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
|||||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
import org.apache.fineract.portfolio.dds.batch.data.BatchRunData;
|
import org.apache.fineract.portfolio.dds.batch.data.BatchRunData;
|
||||||
import org.apache.fineract.portfolio.dds.batch.service.BatchRunReadPlatformService;
|
import org.apache.fineract.portfolio.dds.batch.service.BatchRunReadPlatformService;
|
||||||
import org.apache.fineract.portfolio.dds.batch.service.BatchRunWritePlatformService;
|
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Path("/v1/dds/batchruns")
|
@Path("/v1/dds/batchruns")
|
||||||
@@ -55,20 +52,6 @@ public class BatchRunsApiResource {
|
|||||||
private final BatchRunReadPlatformService readPlatformService;
|
private final BatchRunReadPlatformService readPlatformService;
|
||||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||||
private final DdsVoucherService ddsVoucherService;
|
private final DdsVoucherService ddsVoucherService;
|
||||||
private final BatchRunWritePlatformService writePlatformService;
|
|
||||||
|
|
||||||
@DELETE
|
|
||||||
@Path("{batchRunId}")
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "Discard a previewed DDS Batch Run", description = "Deletes a run that was generated but never posted - "
|
|
||||||
+ "the preview the screen's Discard button throws away. Refused for a run that is posted, posting, failed, or whose "
|
|
||||||
+ "posting is waiting for approval.")
|
|
||||||
public CommandProcessingResult discard(@PathParam("batchRunId") @Parameter(description = "batchRunId") final Long batchRunId) {
|
|
||||||
// Whoever may generate a preview may throw one away; it moved nothing.
|
|
||||||
this.context.authenticatedUser().validateHasPermissionTo("GENERATE_" + RESOURCE_NAME_FOR_PERMISSIONS);
|
|
||||||
this.writePlatformService.discardBatchRun(batchRunId);
|
|
||||||
return new CommandProcessingResultBuilder().withEntityId(batchRunId).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
|||||||
-6
@@ -30,12 +30,6 @@ public interface BatchRunWritePlatformService {
|
|||||||
|
|
||||||
CommandProcessingResult postBatchRun(Long batchRunId, JsonCommand command);
|
CommandProcessingResult postBatchRun(Long batchRunId, JsonCommand command);
|
||||||
|
|
||||||
/**
|
|
||||||
* Throws away a run that was only previewed - generated and never posted, nor waiting for a
|
|
||||||
* checker to approve its posting. Nothing was applied by generating it, so nothing is undone.
|
|
||||||
*/
|
|
||||||
void discardBatchRun(Long batchRunId);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Posts interest for every scheme whose Account Type says to do it automatically and whose posting
|
* Posts interest for every scheme whose Account Type says to do it automatically and whose posting
|
||||||
* day falls on the given date. Driven by the Post DDS Interest scheduled job.
|
* day falls on the given date. Driven by the Post DDS Interest scheduled job.
|
||||||
|
|||||||
+34
-100
@@ -29,8 +29,6 @@ import java.util.Locale;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
|
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
||||||
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
||||||
@@ -85,7 +83,6 @@ import org.springframework.transaction.support.TransactionSynchronizationManager
|
|||||||
* re-running it for the same period finds no days left; a posting takes its amount off the parked
|
* re-running it for the same period finds no days left; a posting takes its amount off the parked
|
||||||
* figure, so re-posting finds nothing left to move.
|
* figure, so re-posting finds nothing left to move.
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class BatchRunWritePlatformServiceJpaRepositoryImpl implements BatchRunWritePlatformService {
|
public class BatchRunWritePlatformServiceJpaRepositoryImpl implements BatchRunWritePlatformService {
|
||||||
|
|
||||||
@@ -144,13 +141,9 @@ public class BatchRunWritePlatformServiceJpaRepositoryImpl implements BatchRunWr
|
|||||||
private static final String COMMISSION_ACCOUNT_SQL = "select a.id, a.status_enum, a.product_id, p.deposit_type_enum "
|
private static final String COMMISSION_ACCOUNT_SQL = "select a.id, a.status_enum, a.product_id, p.deposit_type_enum "
|
||||||
+ "from m_savings_account a join m_savings_product p on p.id = a.product_id where a.account_no = ?";
|
+ "from m_savings_account a join m_savings_product p on p.id = a.product_id where a.account_no = ?";
|
||||||
|
|
||||||
/**
|
/** The product DDS opens customers' scheme accounts under - see 0492. */
|
||||||
* Whether a savings account is a DDS customer's scheme account - one a DDS party is linked to.
|
private static final String DDS_DEPOSIT_PRODUCT_SQL = "select value from c_configuration "
|
||||||
* Read off the parties themselves rather than a configured product id: what makes an account a
|
+ "where name = 'dds-deposit-product' and enabled = true";
|
||||||
* scheme account is that a scheme runs on it, whatever product it was opened under.
|
|
||||||
*/
|
|
||||||
private static final String DDS_SCHEME_ACCOUNT_SQL = "select count(*) from m_dds_party p "
|
|
||||||
+ "join m_savings_account a on a.id = p.savings_account_id where a.account_no = ?";
|
|
||||||
|
|
||||||
private static final int SAVINGS_DEPOSIT_TYPE = 100;
|
private static final int SAVINGS_DEPOSIT_TYPE = 100;
|
||||||
private static final int FIXED_DEPOSIT_TYPE = 200;
|
private static final int FIXED_DEPOSIT_TYPE = 200;
|
||||||
@@ -196,9 +189,6 @@ public class BatchRunWritePlatformServiceJpaRepositoryImpl implements BatchRunWr
|
|||||||
default -> throw new IllegalStateException("Unsupported DDS batch job type: " + jobType);
|
default -> throw new IllegalStateException("Unsupported DDS batch job type: " + jobType);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A new preview of the same job replaces the one before it: generating twice used to leave two
|
|
||||||
// identical "not posted" runs on the list, and nobody could tell which one to post.
|
|
||||||
discardSupersededPreviews(jobType, subType);
|
|
||||||
this.batchRunRepository.saveAndFlush(batchRun);
|
this.batchRunRepository.saveAndFlush(batchRun);
|
||||||
|
|
||||||
return new CommandProcessingResultBuilder() //
|
return new CommandProcessingResultBuilder() //
|
||||||
@@ -207,35 +197,6 @@ public class BatchRunWritePlatformServiceJpaRepositoryImpl implements BatchRunWr
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A run that is only a preview: calculated, never posted, not posting or failed, and with no
|
|
||||||
* posting of it waiting for a checker (a held posting names the run, and approving it later must
|
|
||||||
* find it). Only such a run is ever deleted - generating applies nothing, so nothing is lost.
|
|
||||||
*/
|
|
||||||
private static final String PREVIEW_ONLY = "r.status = 'CALCULATED' and not exists (select 1 from m_portfolio_command_source c "
|
|
||||||
+ "where c.entity_name = 'DDSBATCH' and c.resource_id = r.id and c.status = 2)";
|
|
||||||
|
|
||||||
private void discardSupersededPreviews(final String jobType, final String subType) {
|
|
||||||
final String sameJob = "r.job_type = ? and coalesce(r.sub_type, '') = coalesce(?, '') and " + PREVIEW_ONLY;
|
|
||||||
this.jdbcTemplate.update("delete from m_dds_batch_run_row where batch_run_id in (select r.id from m_dds_batch_run r where " // NOSONAR
|
|
||||||
+ sameJob + ")", jobType, subType);
|
|
||||||
this.jdbcTemplate.update("delete from m_dds_batch_run r where " + sameJob, jobType, subType); // NOSONAR
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
@Override
|
|
||||||
public void discardBatchRun(final Long batchRunId) {
|
|
||||||
final List<Long> found = this.jdbcTemplate.queryForList("select r.id from m_dds_batch_run r where r.id = ? and " + PREVIEW_ONLY, // NOSONAR
|
|
||||||
Long.class, batchRunId);
|
|
||||||
if (found.isEmpty()) {
|
|
||||||
throw new PlatformDataIntegrityException("error.msg.dds.batch.run.not.discardable",
|
|
||||||
"Batch run #" + batchRunId + " cannot be discarded: only a run that was generated and not posted (or sent for"
|
|
||||||
+ " posting approval) can be.");
|
|
||||||
}
|
|
||||||
this.jdbcTemplate.update("delete from m_dds_batch_run_row where batch_run_id = ?", batchRunId); // NOSONAR
|
|
||||||
this.jdbcTemplate.update("delete from m_dds_batch_run where id = ?", batchRunId); // NOSONAR
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the "Branch" checklist selection - null/empty means no filter ("All"). Matched against
|
* Parses the "Branch" checklist selection - null/empty means no filter ("All"). Matched against
|
||||||
* Agent.homeBranch by name, since that field is plain text (not a real Office FK) mirroring the
|
* Agent.homeBranch by name, since that field is plain text (not a real Office FK) mirroring the
|
||||||
@@ -635,47 +596,26 @@ public class BatchRunWritePlatformServiceJpaRepositoryImpl implements BatchRunWr
|
|||||||
final String jobType = batchRun.getJobType();
|
final String jobType = batchRun.getJobType();
|
||||||
final LocalDate jobDate = batchRun.getJobDate();
|
final LocalDate jobDate = batchRun.getJobDate();
|
||||||
|
|
||||||
// Only rows that actually moved money are counted, and their sum kept beside the run's total: a
|
|
||||||
// posting row is passed over when another run has already paid what it lists, and the run must
|
|
||||||
// not read as though it paid it again.
|
|
||||||
int done = 0;
|
int done = 0;
|
||||||
BigDecimal moved = BigDecimal.ZERO;
|
|
||||||
for (final BatchRunRow row : batchRun.getRows()) {
|
for (final BatchRunRow row : batchRun.getRows()) {
|
||||||
final boolean applied = switch (jobType) {
|
switch (jobType) {
|
||||||
case INTEREST_CALCULATION -> {
|
case INTEREST_CALCULATION -> accrue(row, jobDate, false);
|
||||||
accrue(row, jobDate, false);
|
case OD_INTEREST_CALCULATION -> accrue(row, jobDate, true);
|
||||||
yield true;
|
|
||||||
}
|
|
||||||
case OD_INTEREST_CALCULATION -> {
|
|
||||||
accrue(row, jobDate, true);
|
|
||||||
yield true;
|
|
||||||
}
|
|
||||||
case INTEREST_POSTING -> postInterest(row, batchRun, false);
|
case INTEREST_POSTING -> postInterest(row, batchRun, false);
|
||||||
case OD_INTEREST_POSTING -> postInterest(row, batchRun, true);
|
case OD_INTEREST_POSTING -> postInterest(row, batchRun, true);
|
||||||
case COMMISSION_CALCULATION -> {
|
case COMMISSION_CALCULATION -> accrueCommission(row, batchRun);
|
||||||
accrueCommission(row, batchRun);
|
|
||||||
yield true;
|
|
||||||
}
|
|
||||||
case COMMISSION_POSTING -> payCommission(row, batchRun);
|
case COMMISSION_POSTING -> payCommission(row, batchRun);
|
||||||
case MATURITY_AUTO_CLOSE -> {
|
case MATURITY_AUTO_CLOSE -> closeAtMaturity(row);
|
||||||
closeAtMaturity(row);
|
|
||||||
yield true;
|
|
||||||
}
|
|
||||||
default -> throw new IllegalStateException("Unsupported DDS batch job type: " + jobType);
|
default -> throw new IllegalStateException("Unsupported DDS batch job type: " + jobType);
|
||||||
};
|
|
||||||
if (applied) {
|
|
||||||
done++;
|
|
||||||
moved = moved.add(row.getAmount() == null ? BigDecimal.ZERO : row.getAmount());
|
|
||||||
} else {
|
|
||||||
log.warn("DDS batch run #{}: row {} passed over - what it lists was already paid", batchRun.getId(), row.getId());
|
|
||||||
}
|
}
|
||||||
|
done++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only at the end. A crediting run is one transaction from start to finish, so a count written
|
// Only at the end. A crediting run is one transaction from start to finish, so a count written
|
||||||
// part-way through would be invisible outside it until the run commits - and writing it from a
|
// part-way through would be invisible outside it until the run commits - and writing it from a
|
||||||
// transaction of its own would have two of them contending for the same row. The status says
|
// transaction of its own would have two of them contending for the same row. The status says
|
||||||
// POSTING while it works, which is what a clerk actually needs to know.
|
// POSTING while it works, which is what a clerk actually needs to know.
|
||||||
batchRun.recordPosted(done, moved);
|
batchRun.recordProgress(done);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -722,10 +662,10 @@ public class BatchRunWritePlatformServiceJpaRepositoryImpl implements BatchRunWr
|
|||||||
* same accrued interest would each list it in full, and paying both would credit interest that
|
* same accrued interest would each list it in full, and paying both would credit interest that
|
||||||
* was never earned.
|
* was never earned.
|
||||||
*/
|
*/
|
||||||
private boolean postInterest(final BatchRunRow row, final BatchRun batchRun, final boolean odInterest) {
|
private void postInterest(final BatchRunRow row, final BatchRun batchRun, final boolean odInterest) {
|
||||||
final Party party = partyOf(row.getPartyId());
|
final Party party = partyOf(row.getPartyId());
|
||||||
if (party.accruedInterest(odInterest).compareTo(row.getAmount()) < 0) {
|
if (party.accruedInterest(odInterest).compareTo(row.getAmount()) < 0) {
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final BigDecimal newBalance = party.postInterest(row.getAmount(), odInterest);
|
final BigDecimal newBalance = party.postInterest(row.getAmount(), odInterest);
|
||||||
@@ -738,7 +678,6 @@ public class BatchRunWritePlatformServiceJpaRepositoryImpl implements BatchRunWr
|
|||||||
final String transactionType = odInterest ? "OD_INTEREST_POST" : "INTEREST_POST";
|
final String transactionType = odInterest ? "OD_INTEREST_POST" : "INTEREST_POST";
|
||||||
this.partyTransactionRepository.save(new PartyTransaction(party, party.getAgent(), batchRun.getJobDate(), transactionType,
|
this.partyTransactionRepository.save(new PartyTransaction(party, party.getAgent(), batchRun.getJobDate(), transactionType,
|
||||||
DIRECTION_CREDIT, null, null, null, row.getAmount(), null, null, "DDS batch run #" + batchRun.getId(), newBalance));
|
DIRECTION_CREDIT, null, null, null, row.getAmount(), null, null, "DDS batch run #" + batchRun.getId(), newBalance));
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -777,12 +716,12 @@ public class BatchRunWritePlatformServiceJpaRepositoryImpl implements BatchRunWr
|
|||||||
* customers and not yet handed to a branch, which commission has nothing to do with. The entry is
|
* customers and not yet handed to a branch, which commission has nothing to do with. The entry is
|
||||||
* recorded against the balance as it stands, so the ledger reads consistently.
|
* recorded against the balance as it stands, so the ledger reads consistently.
|
||||||
*/
|
*/
|
||||||
private boolean payCommission(final BatchRunRow row, final BatchRun batchRun) {
|
private void payCommission(final BatchRunRow row, final BatchRun batchRun) {
|
||||||
final Agent agent = agentOf(row.getAgentId());
|
final Agent agent = agentOf(row.getAgentId());
|
||||||
// Only what is actually owed can be paid: two posting runs generated over the same owed
|
// Only what is actually owed can be paid: two posting runs generated over the same owed
|
||||||
// figure would each list it in full, and paying both would pay commission twice.
|
// figure would each list it in full, and paying both would pay commission twice.
|
||||||
if (agent.getCommission().compareTo(row.getAmount()) < 0) {
|
if (agent.getCommission().compareTo(row.getAmount()) < 0) {
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
agent.payOutCommission(row.getAmount());
|
agent.payOutCommission(row.getAmount());
|
||||||
|
|
||||||
@@ -791,7 +730,6 @@ public class BatchRunWritePlatformServiceJpaRepositoryImpl implements BatchRunWr
|
|||||||
this.partyTransactionRepository.save(new PartyTransaction(null, agent, batchRun.getJobDate(), "COMMISSION_POST", DIRECTION_CREDIT,
|
this.partyTransactionRepository.save(new PartyTransaction(null, agent, batchRun.getJobDate(), "COMMISSION_POST", DIRECTION_CREDIT,
|
||||||
null, null, agent.getAgentSavingAccountNo(), row.getAmount(), null, null, "DDS batch run #" + batchRun.getId(),
|
null, null, agent.getAgentSavingAccountNo(), row.getAmount(), null, null, "DDS batch run #" + batchRun.getId(),
|
||||||
agent.getAgentCurrentBalance()));
|
agent.getAgentCurrentBalance()));
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -835,8 +773,8 @@ public class BatchRunWritePlatformServiceJpaRepositoryImpl implements BatchRunWr
|
|||||||
+ " Set an ordinary savings account for this agent");
|
+ " Set an ordinary savings account for this agent");
|
||||||
}
|
}
|
||||||
|
|
||||||
final Integer schemes = this.jdbcTemplate.queryForObject(DDS_SCHEME_ACCOUNT_SQL, Integer.class, accountNo); // NOSONAR
|
final List<Long> ddsProduct = this.jdbcTemplate.queryForList(DDS_DEPOSIT_PRODUCT_SQL, Long.class); // NOSONAR
|
||||||
if (schemes != null && schemes > 0) {
|
if (!ddsProduct.isEmpty() && productId.equals(ddsProduct.get(0))) {
|
||||||
throw new AgentCommissionAccountUnusableException(agent.getAgentCode(), "account " + accountNo
|
throw new AgentCommissionAccountUnusableException(agent.getAgentCode(), "account " + accountNo
|
||||||
+ " is a DDS scheme account belonging to a customer, so commission paid into it would be counted as their"
|
+ " is a DDS scheme account belonging to a customer, so commission paid into it would be counted as their"
|
||||||
+ " deposits. Set an ordinary savings account for this agent");
|
+ " deposits. Set an ordinary savings account for this agent");
|
||||||
@@ -849,12 +787,8 @@ public class BatchRunWritePlatformServiceJpaRepositoryImpl implements BatchRunWr
|
|||||||
"account " + accountNo + " is " + statusName(status) + " rather than active, so it cannot take a deposit yet");
|
"account " + accountNo + " is " + statusName(status) + " rather than active, so it cannot take a deposit yet");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Each product the agent collected for books its share on its own commission account; the
|
|
||||||
// collections the share was worked out on are then marked as paid for.
|
|
||||||
final Map<GLAccount, BigDecimal> shares = this.ddsLedger.commissionSplit(agent.getId(), amount, batchRun.getId());
|
|
||||||
this.ddsLedger.payCommission(((Number) account.get("id")).longValue(), batchRun.getJobDate(), amount,
|
this.ddsLedger.payCommission(((Number) account.get("id")).longValue(), batchRun.getJobDate(), amount,
|
||||||
"DDS commission, batch run #" + batchRun.getId(), shares);
|
"DDS commission, batch run #" + batchRun.getId());
|
||||||
this.ddsLedger.markCommissionPaid(agent.getId(), batchRun.getId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fineract's savings statuses, named the way the screens name them. */
|
/** Fineract's savings statuses, named the way the screens name them. */
|
||||||
@@ -876,36 +810,28 @@ public class BatchRunWritePlatformServiceJpaRepositoryImpl implements BatchRunWr
|
|||||||
// ---------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Posts interest for every scheme whose posting period ended yesterday.
|
* Posts interest for every scheme that is due today.
|
||||||
*
|
*
|
||||||
* A scheme that posts automatically has decided it needs no review, so this does both halves in
|
* A scheme that posts automatically has decided it needs no review, so this does both halves in
|
||||||
* one go: it calculates the interest since the last run and then posts it into the customers'
|
* one go: it calculates the interest since the last run and then posts it into the customers'
|
||||||
* balances. Both halves go through the same code the screen's Post button uses, and both leave a
|
* balances. Both halves go through the same code the screen's Post button uses, and both leave a
|
||||||
* run behind, so an automatic posting is as visible and as auditable as a manual one.
|
* run behind, so an automatic posting is as visible and as auditable as a manual one.
|
||||||
*
|
*
|
||||||
* Interest posts at the end of the period - the last day of the month, quarter, half-year or
|
* Runs are scoped to one scheme at a time, because the posting day belongs to the scheme.
|
||||||
* year - as it does on savings and fixed deposits, so there is no posting day to configure and
|
|
||||||
* no month whose length can trip one up. The job runs just after midnight, so it looks back at
|
|
||||||
* the day that has just closed: posting on the 1st through the last day of the previous month
|
|
||||||
* takes in everything collected on that last day, which a run at 00:20 on the day itself would
|
|
||||||
* not yet have seen.
|
|
||||||
*
|
|
||||||
* Runs are scoped to one scheme at a time, because the posting frequency belongs to the scheme.
|
|
||||||
*/
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
public int postDueAutoInterest(final LocalDate businessDate) {
|
public int postDueAutoInterest(final LocalDate businessDate) {
|
||||||
final LocalDate periodEnd = businessDate.minusDays(1);
|
|
||||||
int posted = 0;
|
int posted = 0;
|
||||||
|
|
||||||
for (final DdsAccountType accountType : this.ddsAccountTypeRepository.findDueForAutoInterestPost()) {
|
for (final DdsAccountType accountType : this.ddsAccountTypeRepository.findDueForAutoInterestPost()) {
|
||||||
if (!isPeriodEnd(accountType, periodEnd)) {
|
if (!isDueOn(accountType, businessDate)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
final String subType = accountType.getAccountTypeCode();
|
final String subType = accountType.getAccountTypeCode();
|
||||||
calculateAndPost(INTEREST_CALCULATION, periodEnd, subType);
|
calculateAndPost(INTEREST_CALCULATION, businessDate, subType);
|
||||||
calculateAndPost(INTEREST_POSTING, periodEnd, subType);
|
calculateAndPost(INTEREST_POSTING, businessDate, subType);
|
||||||
posted++;
|
posted++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -913,11 +839,19 @@ public class BatchRunWritePlatformServiceJpaRepositoryImpl implements BatchRunWr
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the given date closes one of this scheme's posting periods: the last day of a month
|
* Whether this scheme's posting day falls on the given date.
|
||||||
* the scheme's frequency posts in. February's end is the 28th or 29th, whichever the year has.
|
*
|
||||||
|
* A day beyond the length of the month means the end of that month: a scheme set to 31 posts on
|
||||||
|
* 31 January, 28 February and 30 April. Without that, February would simply be skipped and the
|
||||||
|
* account would lose a month's interest every year.
|
||||||
*/
|
*/
|
||||||
private boolean isPeriodEnd(final DdsAccountType accountType, final LocalDate date) {
|
private boolean isDueOn(final DdsAccountType accountType, final LocalDate date) {
|
||||||
return monthIsDue(accountType.getAutoInterestPostWhen(), date) && date.getDayOfMonth() == date.lengthOfMonth();
|
if (!monthIsDue(accountType.getAutoInterestPostWhen(), date)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final int configuredDay = accountType.getAutoInterestPostDay();
|
||||||
|
final int effectiveDay = Math.min(configuredDay, date.lengthOfMonth());
|
||||||
|
return date.getDayOfMonth() == effectiveDay;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
-11
@@ -115,7 +115,6 @@ public final class CollectionEntryCommandFromApiJsonDeserializer {
|
|||||||
.inMinMaxRange(1, DdsSchedule.CATCH_UP_DAYS);
|
.inMinMaxRange(1, DdsSchedule.CATCH_UP_DAYS);
|
||||||
} else {
|
} else {
|
||||||
v.reset().parameter(ENTRIES).parameterAtIndexArray("amount", i).value(decimalOf(row)).notNull().positiveAmount();
|
v.reset().parameter(ENTRIES).parameterAtIndexArray("amount", i).value(decimalOf(row)).notNull().positiveAmount();
|
||||||
toThePaisa(v, decimalOf(row));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final String mode = stringOf(row, "transactionMode");
|
final String mode = stringOf(row, "transactionMode");
|
||||||
@@ -202,14 +201,4 @@ public final class CollectionEntryCommandFromApiJsonDeserializer {
|
|||||||
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors);
|
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* Money is kept to the paisa, as a savings account keeps it: 100.555 is refused rather than stored
|
|
||||||
* on the passbook and the journal as typed.
|
|
||||||
*/
|
|
||||||
private static void toThePaisa(final DataValidatorBuilder v, final BigDecimal amount) {
|
|
||||||
if (amount != null && amount.stripTrailingZeros().scale() > 2) {
|
|
||||||
v.failWithCodeAndMessage("more.than.two.decimals", "An amount can have at most two decimal places (paise).", amount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-33
@@ -49,7 +49,6 @@ import org.apache.fineract.infrastructure.security.service.PlatformSecurityConte
|
|||||||
import org.apache.fineract.organisation.agent.domain.Agent;
|
import org.apache.fineract.organisation.agent.domain.Agent;
|
||||||
import org.apache.fineract.organisation.agent.domain.AgentRepository;
|
import org.apache.fineract.organisation.agent.domain.AgentRepository;
|
||||||
import org.apache.fineract.organisation.agent.exception.AgentNotFoundException;
|
import org.apache.fineract.organisation.agent.exception.AgentNotFoundException;
|
||||||
import org.apache.fineract.organisation.office.domain.Office;
|
|
||||||
import org.apache.fineract.portfolio.dds.collection.domain.CollectionEntry;
|
import org.apache.fineract.portfolio.dds.collection.domain.CollectionEntry;
|
||||||
import org.apache.fineract.portfolio.dds.collection.domain.CollectionEntryRepository;
|
import org.apache.fineract.portfolio.dds.collection.domain.CollectionEntryRepository;
|
||||||
import org.apache.fineract.portfolio.dds.collection.exception.CollectionEntryNotFoundException;
|
import org.apache.fineract.portfolio.dds.collection.exception.CollectionEntryNotFoundException;
|
||||||
@@ -337,54 +336,32 @@ public class CollectionEntryWritePlatformServiceJpaRepositoryImpl implements Col
|
|||||||
}
|
}
|
||||||
final String description = "DDS collection by " + agent.getAgentCode() + ", voucher " + String.join(", ", voucherNos);
|
final String description = "DDS collection by " + agent.getAgentCode() + ", voucher " + String.join(", ", voucherNos);
|
||||||
|
|
||||||
// What the customers on DDS's own books are now owed, by the customer-deposits account of their
|
BigDecimal pooled = BigDecimal.ZERO;
|
||||||
// product and the branch they bank at - two products owe on two accounts, and a branch's DDS
|
|
||||||
// Customer Deposits must add up to its own customers' balances.
|
|
||||||
final String journalId = "DDSR-A" + applicationId;
|
|
||||||
final Map<String, BigDecimal> pooled = new LinkedHashMap<>();
|
|
||||||
final Map<String, GLAccount> depositAccounts = new LinkedHashMap<>();
|
|
||||||
final Map<String, Office> offices = new LinkedHashMap<>();
|
|
||||||
for (final CollectionEntry entry : pending) {
|
for (final CollectionEntry entry : pending) {
|
||||||
final Party party = collectableParty(entry.getParty().getPartyCode(), agent);
|
final Party party = collectableParty(entry.getParty().getPartyCode(), agent);
|
||||||
|
|
||||||
final boolean pooledEntry = party.getSavingsAccountId() == null;
|
if (party.getSavingsAccountId() != null) {
|
||||||
if (!pooledEntry) {
|
|
||||||
this.ddsLedger.applyAgentReceiptToSavings(party, settledOn, entry.getAmount(), receiptAccount, description);
|
this.ddsLedger.applyAgentReceiptToSavings(party, settledOn, entry.getAmount(), receiptAccount, description);
|
||||||
} else {
|
} else {
|
||||||
final GLAccount deposits = this.ddsLedger.depositLiabilityOf(party);
|
pooled = pooled.add(entry.getAmount());
|
||||||
final Office office = this.ddsLedger.officeOf(party);
|
|
||||||
final String key = office.getId() + ":" + deposits.getId();
|
|
||||||
depositAccounts.putIfAbsent(key, deposits);
|
|
||||||
offices.putIfAbsent(key, office);
|
|
||||||
pooled.merge(key, entry.getAmount(), BigDecimal::add);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The customer's own statement records the day the money was collected, as it always has.
|
// The customer's own statement records the day the money was collected, as it always has.
|
||||||
// The agent's cash in hand is not raised: this cash was counted in at the counter, not
|
// The agent's cash in hand is not raised: this cash was counted in at the counter, not
|
||||||
// carried.
|
// carried.
|
||||||
final BigDecimal balanceAfter = party.credit(entry.getAmount());
|
final BigDecimal balanceAfter = party.credit(entry.getAmount());
|
||||||
final PartyTransaction line = new PartyTransaction(party, agent, entry.getEntryDate(), "PARTY_TRAN", "CREDIT",
|
final PartyTransaction transaction = this.partyTransactionRepository.saveAndFlush(new PartyTransaction(party, agent,
|
||||||
entry.getTransactionMode(), "GL", receiptAccount.getGlCode(), entry.getAmount(), entry.getChequeNo(),
|
entry.getEntryDate(), "PARTY_TRAN", "CREDIT", entry.getTransactionMode(), "GL", receiptAccount.getGlCode(),
|
||||||
entry.getTokenNo(), description, balanceAfter);
|
entry.getAmount(), entry.getChequeNo(), entry.getTokenNo(), description, balanceAfter));
|
||||||
// A pooled line's journal is written after the loop, so it is named here rather than taken
|
|
||||||
// from whatever the operation wrote last.
|
|
||||||
final PartyTransaction transaction = this.partyTransactionRepository
|
|
||||||
.saveAndFlush(pooledEntry ? line.linkJournal(journalId) : line);
|
|
||||||
|
|
||||||
entry.markSettledByReceipt(settledOn, transaction.getId(), applicationId);
|
entry.markSettledByReceipt(settledOn, transaction.getId(), applicationId);
|
||||||
this.collectionEntryRepository.save(entry);
|
this.collectionEntryRepository.save(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Everyone on DDS's own accounts, in one pair per branch and customer-deposits account, all under one journal id.
|
// Everyone on DDS's own accounts, in one pair.
|
||||||
boolean journalled = false;
|
final String journalId = "DDSR-A" + applicationId;
|
||||||
for (final Map.Entry<String, BigDecimal> owed : pooled.entrySet()) {
|
if (pooled.signum() > 0) {
|
||||||
if (owed.getValue().signum() > 0) {
|
this.ddsLedger.applyAgentReceipts(agent.getOffice(), settledOn, pooled, receiptAccount, journalId, description);
|
||||||
this.ddsLedger.applyAgentReceipts(offices.get(owed.getKey()), settledOn, owed.getValue(), receiptAccount,
|
|
||||||
depositAccounts.get(owed.getKey()), journalId, description);
|
|
||||||
journalled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (journalled) {
|
|
||||||
this.jdbcTemplate.update(SET_JOURNAL_SQL, journalId, applicationId); // NOSONAR
|
this.jdbcTemplate.update(SET_JOURNAL_SQL, journalId, applicationId); // NOSONAR
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
-17
@@ -110,23 +110,6 @@ public class PartiesApiResource {
|
|||||||
return readPlatformService.retrieveCreditableAccounts(clientId);
|
return readPlatformService.retrieveCreditableAccounts(clientId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
|
||||||
@Path("nextcode")
|
|
||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
|
||||||
@Operation(summary = "What the Party Master Entry screen shows before saving",
|
|
||||||
description = "partyCode: the code the next DDS party will be given - worked out the way saving does it (PT and one "
|
|
||||||
+ "more than the highest number on the books), but only an estimate: another party saved first, or one "
|
|
||||||
+ "waiting for approval, can take it. lastAccountNo: the number of the DDS account opened most recently, "
|
|
||||||
+ "null when there is none. Both are for display; nothing is saved from them.")
|
|
||||||
public Map<String, Object> retrieveNextPartyCode() {
|
|
||||||
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
|
||||||
final Map<String, Object> result = new java.util.LinkedHashMap<>();
|
|
||||||
result.put("partyCode", readPlatformService.nextPartyCode());
|
|
||||||
result.put("lastAccountNo", readPlatformService.lastAccountNo());
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("sundryaccounts")
|
@Path("sundryaccounts")
|
||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
|||||||
-13
@@ -35,19 +35,6 @@ public interface PartyReadPlatformService {
|
|||||||
*/
|
*/
|
||||||
Collection<Map<String, Object>> retrieveCreditableAccounts(Long clientId);
|
Collection<Map<String, Object>> retrieveCreditableAccounts(Long clientId);
|
||||||
|
|
||||||
/**
|
|
||||||
* The code the next party saved will be given, for a screen to show before it is saved. It is worked
|
|
||||||
* out the way saving does it - "PT" and one more than the highest number on the books - but only an
|
|
||||||
* estimate: another party saved first, or one waiting for approval, can take it.
|
|
||||||
*/
|
|
||||||
String nextPartyCode();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The account number of the DDS account opened most recently, or null when none has been - what the
|
|
||||||
* Party Master Entry screen shows as Last A/c. Read only: nothing is saved from it.
|
|
||||||
*/
|
|
||||||
String lastAccountNo();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How a closed account was closed - early or at maturity, by whom, what the balance came to and
|
* How a closed account was closed - early or at maturity, by whom, what the balance came to and
|
||||||
* where it went - gathered from its transactions, the ledger and the command that closed it.
|
* where it went - gathered from its transactions, the ledger and the command that closed it.
|
||||||
|
|||||||
+3
-29
@@ -104,10 +104,7 @@ public class PartyReadPlatformServiceImpl implements PartyReadPlatformService {
|
|||||||
+ " p.freeze_code as freezeCode, p.adv_code as advCode, p.credit_to_branch as creditToBranch, "
|
+ " p.freeze_code as freezeCode, p.adv_code as advCode, p.credit_to_branch as creditToBranch, "
|
||||||
+ " p.credit_to_account_type as creditToAccountType, p.credit_to_account as creditToAccount, "
|
+ " p.credit_to_account_type as creditToAccountType, p.credit_to_account as creditToAccount, "
|
||||||
+ " p.nominee_applicable as nomineeApplicable, p.nominee_split_mode as nomineeSplitMode, "
|
+ " p.nominee_applicable as nomineeApplicable, p.nominee_split_mode as nomineeSplitMode, "
|
||||||
+ " p.status as status, p.close_date as closeDate, p.account_type_id as accountTypeId, "
|
+ " p.status as status, p.close_date as closeDate "
|
||||||
+ " p.currency_code as currencyCode, p.activatedon_date as activatedOnDate, "
|
|
||||||
+ " (select u.username from m_appuser u where u.id = p.activatedon_userid) as activatedByUsername, "
|
|
||||||
+ " (select u.username from m_appuser u where u.id = p.closedon_userid) as closedByUsername "
|
|
||||||
// Left join: sub_type is a plain code column rather than a foreign key, so a party
|
// Left join: sub_type is a plain code column rather than a foreign key, so a party
|
||||||
// whose account type was renamed away or predates the master still lists, just
|
// whose account type was renamed away or predates the master still lists, just
|
||||||
// without a description.
|
// without a description.
|
||||||
@@ -217,11 +214,6 @@ public class PartyReadPlatformServiceImpl implements PartyReadPlatformService {
|
|||||||
.nomineeSplitMode(rs.getString("nomineeSplitMode")) //
|
.nomineeSplitMode(rs.getString("nomineeSplitMode")) //
|
||||||
.status(rs.getString("status")) //
|
.status(rs.getString("status")) //
|
||||||
.closeDate(JdbcSupport.getLocalDate(rs, "closeDate")) //
|
.closeDate(JdbcSupport.getLocalDate(rs, "closeDate")) //
|
||||||
.accountTypeId(JdbcSupport.getLong(rs, "accountTypeId")) //
|
|
||||||
.currencyCode(rs.getString("currencyCode")) //
|
|
||||||
.activatedOnDate(JdbcSupport.getLocalDate(rs, "activatedOnDate")) //
|
|
||||||
.activatedByUsername(rs.getString("activatedByUsername")) //
|
|
||||||
.closedByUsername(rs.getString("closedByUsername")) //
|
|
||||||
.nominees(new ArrayList<>()) //
|
.nominees(new ArrayList<>()) //
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
@@ -334,32 +326,14 @@ public class PartyReadPlatformServiceImpl implements PartyReadPlatformService {
|
|||||||
+ "p.name as \"productName\", a.account_balance_derived as balance, 'SAVINGS' as \"accountKind\" from m_savings_account a "
|
+ "p.name as \"productName\", a.account_balance_derived as balance, 'SAVINGS' as \"accountKind\" from m_savings_account a "
|
||||||
+ "join m_savings_product p on p.id = a.product_id "
|
+ "join m_savings_product p on p.id = a.product_id "
|
||||||
+ "where a.client_id = ? and a.status_enum = 300 and p.deposit_type_enum = 100 "
|
+ "where a.client_id = ? and a.status_enum = 300 and p.deposit_type_enum = 100 "
|
||||||
+ "and not exists (select 1 from m_dds_party dp where dp.savings_account_id = a.id) "
|
+ "and a.product_id is distinct from (select cast(value as bigint) from c_configuration "
|
||||||
|
+ "where name = 'dds-deposit-product' and enabled = true) "
|
||||||
+ "union all "
|
+ "union all "
|
||||||
+ "select c.id as id, c.account_no as \"accountNo\", cp.name as \"productName\", c.account_balance_derived as balance, "
|
+ "select c.id as id, c.account_no as \"accountNo\", cp.name as \"productName\", c.account_balance_derived as balance, "
|
||||||
+ "'CURRENT' as \"accountKind\" from m_current_account c join m_current_product cp on cp.id = c.product_id "
|
+ "'CURRENT' as \"accountKind\" from m_current_account c join m_current_product cp on cp.id = c.product_id "
|
||||||
+ "where c.client_id = ? and c.status_enum = 300"
|
+ "where c.client_id = ? and c.status_enum = 300"
|
||||||
+ ") accounts order by \"accountNo\"";
|
+ ") accounts order by \"accountNo\"";
|
||||||
|
|
||||||
@Override
|
|
||||||
public String nextPartyCode() {
|
|
||||||
// The same figure saving a party works from - the highest number among every code on the books,
|
|
||||||
// closed parties included - so what a screen shows is what saving would give it right now.
|
|
||||||
final Long highest = this.jdbcTemplate.queryForObject( // NOSONAR
|
|
||||||
"select coalesce(max(cast(substring(party_code, 3) as bigint)), 0) from m_dds_party where party_code like 'PT%'",
|
|
||||||
Long.class);
|
|
||||||
return String.format("PT%04d", (highest == null ? 0L : highest) + 1L);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String lastAccountNo() {
|
|
||||||
// The highest number on the DDS account serial - the account opened last. Closed accounts count:
|
|
||||||
// this says what was opened, not what is still open.
|
|
||||||
final List<String> found = this.jdbcTemplate.queryForList( // NOSONAR
|
|
||||||
"select account_no from m_dds_party where account_no is not null order by account_no desc limit 1", String.class);
|
|
||||||
return found.isEmpty() ? null : found.get(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<Map<String, Object>> retrieveCreditableAccounts(final Long clientId) {
|
public Collection<Map<String, Object>> retrieveCreditableAccounts(final Long clientId) {
|
||||||
return this.jdbcTemplate.queryForList(CREDITABLE_ACCOUNTS_SQL, clientId, clientId); // NOSONAR
|
return this.jdbcTemplate.queryForList(CREDITABLE_ACCOUNTS_SQL, clientId, clientId); // NOSONAR
|
||||||
|
|||||||
+34
-114
@@ -29,7 +29,6 @@ import java.time.LocalDate;
|
|||||||
import java.time.Period;
|
import java.time.Period;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -80,10 +79,10 @@ import org.apache.fineract.portfolio.dds.party.exception.PartySubTypeNotActiveEx
|
|||||||
import org.apache.fineract.portfolio.dds.party.serialization.PartyCommandFromApiJsonDeserializer;
|
import org.apache.fineract.portfolio.dds.party.serialization.PartyCommandFromApiJsonDeserializer;
|
||||||
import org.apache.fineract.portfolio.dds.transaction.domain.PartyTransaction;
|
import org.apache.fineract.portfolio.dds.transaction.domain.PartyTransaction;
|
||||||
import org.apache.fineract.portfolio.dds.transaction.domain.PartyTransactionRepository;
|
import org.apache.fineract.portfolio.dds.transaction.domain.PartyTransactionRepository;
|
||||||
|
import org.apache.fineract.portfolio.savings.data.SavingsAccountDataDTO;
|
||||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
|
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
|
||||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepository;
|
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepository;
|
||||||
import org.apache.fineract.portfolio.savings.service.SavingsApplicationProcessWritePlatformService;
|
import org.apache.fineract.portfolio.savings.service.SavingsApplicationProcessWritePlatformService;
|
||||||
import org.apache.fineract.useradministration.domain.AppUser;
|
|
||||||
import org.springframework.dao.DataIntegrityViolationException;
|
import org.springframework.dao.DataIntegrityViolationException;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.orm.jpa.JpaSystemException;
|
import org.springframework.orm.jpa.JpaSystemException;
|
||||||
@@ -135,20 +134,15 @@ public class PartyWritePlatformServiceJpaRepositoryImpl implements PartyWritePla
|
|||||||
final String partyCode = generateNextPartyCode();
|
final String partyCode = generateNextPartyCode();
|
||||||
final Party party = Party.fromJson(client, agent, officeNamed(command.longValueOfParameterNamed(OFFICE_ID)), partyCode,
|
final Party party = Party.fromJson(client, agent, officeNamed(command.longValueOfParameterNamed(OFFICE_ID)), partyCode,
|
||||||
command);
|
command);
|
||||||
final DdsAccountType scheme = schemeOf(party);
|
party.takeSchemeRate(schemeOf(party).getInterestRate());
|
||||||
party.takeSchemeRate(scheme.getInterestRate());
|
|
||||||
party.linkProduct(scheme.getId());
|
|
||||||
// Operative from the day it is opened - by whoever executes this, the checker where the
|
|
||||||
// opening went through maker-checker - in the bank's currency.
|
|
||||||
party.recordActivation(organisationCurrency(), DateUtils.getBusinessLocalDate(), currentUserId());
|
|
||||||
|
|
||||||
linkSavingsAccountIfProvided(party, clientId, command);
|
linkSavingsAccountIfProvided(party, clientId, command);
|
||||||
addNomineesFromCommand(party, command);
|
addNomineesFromCommand(party, command);
|
||||||
|
|
||||||
// The party is the DDS account: nothing is opened in the savings tables for it. Its number
|
// The party is the DDS account: nothing is opened in the savings tables for it. Its
|
||||||
// is the next on the DDS account serial, taken in this transaction - so an opening that
|
// number comes from its id, so it is given once the first save has made one.
|
||||||
// fails or is rolled back hands its number back, and the numbers run without gaps.
|
this.partyRepository.saveAndFlush(party);
|
||||||
party.assignAccountNo(nextAccountSerial());
|
party.assignAccountNo();
|
||||||
this.partyRepository.saveAndFlush(party);
|
this.partyRepository.saveAndFlush(party);
|
||||||
|
|
||||||
return new CommandProcessingResultBuilder() //
|
return new CommandProcessingResultBuilder() //
|
||||||
@@ -173,10 +167,6 @@ public class PartyWritePlatformServiceJpaRepositoryImpl implements PartyWritePla
|
|||||||
this.fromApiJsonDeserializer.validateForUpdate(command.json());
|
this.fromApiJsonDeserializer.validateForUpdate(command.json());
|
||||||
|
|
||||||
final Party partyForUpdate = this.partyRepository.findById(partyId).orElseThrow(() -> new PartyNotFoundException(partyId));
|
final Party partyForUpdate = this.partyRepository.findById(partyId).orElseThrow(() -> new PartyNotFoundException(partyId));
|
||||||
refuseEditOfClosedAccount(partyForUpdate);
|
|
||||||
// Asked before the update changes anything: is the branch this request names a different one?
|
|
||||||
final boolean officeMoves = command.parameterExists(OFFICE_ID) && (partyForUpdate.getOffice() == null
|
|
||||||
|| !partyForUpdate.getOffice().getId().equals(command.longValueOfParameterNamed(OFFICE_ID)));
|
|
||||||
|
|
||||||
// Checked against whoever owns the party once this update lands, not who owns it now -
|
// Checked against whoever owns the party once this update lands, not who owns it now -
|
||||||
// moving the party to another customer and naming the old one's account in the same save
|
// moving the party to another customer and naming the old one's account in the same save
|
||||||
@@ -186,16 +176,13 @@ public class PartyWritePlatformServiceJpaRepositoryImpl implements PartyWritePla
|
|||||||
validateCreditToAccount(owningClientId, command);
|
validateCreditToAccount(owningClientId, command);
|
||||||
|
|
||||||
final Map<String, Object> changesOnly = partyForUpdate.update(command);
|
final Map<String, Object> changesOnly = partyForUpdate.update(command);
|
||||||
refuseChangeOfLockedTerms(partyForUpdate, changesOnly, officeMoves);
|
|
||||||
|
|
||||||
// Only when it actually changed: a party already sitting on a since-retired account type
|
// Only when it actually changed: a party already sitting on a since-retired account type
|
||||||
// must stay editable for everything else.
|
// must stay editable for everything else.
|
||||||
if (changesOnly.containsKey("subType")) {
|
if (changesOnly.containsKey("subType")) {
|
||||||
validateSubType((String) changesOnly.get("subType"));
|
validateSubType((String) changesOnly.get("subType"));
|
||||||
// Moved to another scheme: it takes that scheme's rate, and is tied to it by id.
|
// Moved to another scheme: it takes that scheme's rate.
|
||||||
final DdsAccountType newScheme = schemeOf(partyForUpdate);
|
partyForUpdate.takeSchemeRate(schemeOf(partyForUpdate).getInterestRate());
|
||||||
partyForUpdate.takeSchemeRate(newScheme.getInterestRate());
|
|
||||||
partyForUpdate.linkProduct(newScheme.getId());
|
|
||||||
changesOnly.put("interestRate", partyForUpdate.getInterestRate());
|
changesOnly.put("interestRate", partyForUpdate.getInterestRate());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,7 +245,7 @@ public class PartyWritePlatformServiceJpaRepositoryImpl implements PartyWritePla
|
|||||||
}
|
}
|
||||||
|
|
||||||
final LocalDate closeDate = command.localDateValueOfParameterNamed("closeDate");
|
final LocalDate closeDate = command.localDateValueOfParameterNamed("closeDate");
|
||||||
party.close(closeDate, currentUserId());
|
party.close(closeDate);
|
||||||
this.partyRepository.saveAndFlush(party);
|
this.partyRepository.saveAndFlush(party);
|
||||||
|
|
||||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(partyId).build();
|
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(partyId).build();
|
||||||
@@ -360,7 +347,7 @@ public class PartyWritePlatformServiceJpaRepositoryImpl implements PartyWritePla
|
|||||||
// open only leaves a second step to forget - an account that reads as active in every report and
|
// open only leaves a second step to forget - an account that reads as active in every report and
|
||||||
// that an agent could still collect on. The same as Maturity Auto Close does, on the day the
|
// that an agent could still collect on. The same as Maturity Auto Close does, on the day the
|
||||||
// money moved.
|
// money moved.
|
||||||
party.close(DateUtils.getBusinessLocalDate(), currentUserId());
|
party.close(DateUtils.getBusinessLocalDate());
|
||||||
this.partyRepository.saveAndFlush(party);
|
this.partyRepository.saveAndFlush(party);
|
||||||
|
|
||||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(partyId).build();
|
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(partyId).build();
|
||||||
@@ -458,7 +445,7 @@ public class PartyWritePlatformServiceJpaRepositoryImpl implements PartyWritePla
|
|||||||
// The balance is nil now, which is the one thing closing insists on. Closed on the business
|
// The balance is nil now, which is the one thing closing insists on. Closed on the business
|
||||||
// date rather than the maturity date: this is the day the money actually moved, and a close
|
// date rather than the maturity date: this is the day the money actually moved, and a close
|
||||||
// date earlier than its own payout would read as though the account had been shut first.
|
// date earlier than its own payout would read as though the account had been shut first.
|
||||||
party.close(DateUtils.getBusinessLocalDate(), currentUserId());
|
party.close(DateUtils.getBusinessLocalDate());
|
||||||
this.partyRepository.saveAndFlush(party);
|
this.partyRepository.saveAndFlush(party);
|
||||||
|
|
||||||
return new CommandProcessingResultBuilder().withEntityId(partyId).build();
|
return new CommandProcessingResultBuilder().withEntityId(partyId).build();
|
||||||
@@ -691,8 +678,9 @@ public class PartyWritePlatformServiceJpaRepositoryImpl implements PartyWritePla
|
|||||||
// Sending one maturity into another moves money between two collection accounts instead of
|
// Sending one maturity into another moves money between two collection accounts instead of
|
||||||
// handing it back, and if that scheme has already closed the money is stranded in an account
|
// handing it back, and if that scheme has already closed the money is stranded in an account
|
||||||
// nobody can operate. The same refusal an agent's commission account gets.
|
// nobody can operate. The same refusal an agent's commission account gets.
|
||||||
final Integer schemes = this.jdbcTemplate.queryForObject(DDS_SCHEME_ACCOUNT_SQL, Integer.class, accountNo); // NOSONAR
|
final Long productId = ((Number) account.get("product_id")).longValue();
|
||||||
if (schemes != null && schemes > 0) {
|
final List<Long> ddsProduct = this.jdbcTemplate.queryForList(DEPOSIT_PRODUCT_SQL, Long.class); // NOSONAR
|
||||||
|
if (!ddsProduct.isEmpty() && productId.equals(ddsProduct.get(0))) {
|
||||||
throw new PartyCreditAccountUnusableException(accountNo, "it is a DDS scheme account, not an account to be paid into");
|
throw new PartyCreditAccountUnusableException(accountNo, "it is a DDS scheme account, not an account to be paid into");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -717,13 +705,9 @@ public class PartyWritePlatformServiceJpaRepositoryImpl implements PartyWritePla
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Set by 0492. Names the savings product a DDS customer's real account is opened under. */
|
||||||
* Whether a savings account is a DDS customer's scheme account - one a DDS party is linked to.
|
private static final String DEPOSIT_PRODUCT_SQL = "select value from c_configuration "
|
||||||
* Read off the parties themselves rather than a configured product id: what makes an account a
|
+ "where name = 'dds-deposit-product' and enabled = true";
|
||||||
* scheme account is that a scheme runs on it, whatever product it was opened under.
|
|
||||||
*/
|
|
||||||
private static final String DDS_SCHEME_ACCOUNT_SQL = "select count(*) from m_dds_party p "
|
|
||||||
+ "join m_savings_account a on a.id = p.savings_account_id where a.account_no = ?";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens the customer's real deposit account, unless one was named and linked above.
|
* Opens the customer's real deposit account, unless one was named and linked above.
|
||||||
@@ -740,10 +724,22 @@ public class PartyWritePlatformServiceJpaRepositoryImpl implements PartyWritePla
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// DDS keeps its customers' accounts itself (0514), with each DDS product's own ledger accounts
|
final List<Long> productIds = this.jdbcTemplate.queryForList(DEPOSIT_PRODUCT_SQL, Long.class); // NOSONAR
|
||||||
// behind them, and no savings product is named for DDS any more - so there is no savings
|
if (productIds.isEmpty() || productIds.get(0) == null) {
|
||||||
// account to open. A party's balance is already on the books through its product.
|
throw new DdsDepositProductNotConfiguredException();
|
||||||
throw new DdsDepositProductNotConfiguredException();
|
}
|
||||||
|
|
||||||
|
final SavingsAccountDataDTO accountDetails = new SavingsAccountDataDTO(client, null, productIds.get(0), party.getOpeningDate(),
|
||||||
|
this.context.authenticatedUser(), DateUtils.DEFAULT_DATE_FORMATTER);
|
||||||
|
final CommandProcessingResult result = this.savingsApplicationProcessWritePlatformService.createActiveApplication(accountDetails);
|
||||||
|
final Long savingsAccountId = result.getSavingsId() != null ? result.getSavingsId() : result.getResourceId();
|
||||||
|
party.linkSavingsAccount(savingsAccountId);
|
||||||
|
|
||||||
|
// Withdrawals are blocked from the moment the account exists. A scheme account is a savings
|
||||||
|
// account like any other, and left open a customer could empty a twelve-month scheme at the
|
||||||
|
// counter the next day - which is the one thing daily deposit saving exists to prevent.
|
||||||
|
// Deposits are untouched; only the way out is closed, and DDS lifts it for its own payouts.
|
||||||
|
this.ddsLedger.lockDebits(savingsAccountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -754,68 +750,6 @@ public class PartyWritePlatformServiceJpaRepositoryImpl implements PartyWritePla
|
|||||||
* counted at another. Refuses an id that names no branch rather than leaving the account
|
* counted at another. Refuses an id that names no branch rather than leaving the account
|
||||||
* unplaced.
|
* unplaced.
|
||||||
*/
|
*/
|
||||||
/**
|
|
||||||
* Who is doing this, for the account's own record of it. A background run has no logged-in user;
|
|
||||||
* it is recorded as the system administrator, as the audit columns record it (AuditorAwareImpl).
|
|
||||||
*/
|
|
||||||
private Long currentUserId() {
|
|
||||||
final AppUser user = this.context.getAuthenticatedUserIfPresent();
|
|
||||||
return user != null ? user.getId() : SYSTEM_ADMIN_USER_ID;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final Long SYSTEM_ADMIN_USER_ID = 1L;
|
|
||||||
|
|
||||||
/** The bank's currency - a DDS account is opened in it. */
|
|
||||||
private String organisationCurrency() {
|
|
||||||
final List<String> codes = this.jdbcTemplate.queryForList("select code from m_organisation_currency order by id limit 1", // NOSONAR
|
|
||||||
String.class);
|
|
||||||
return codes.isEmpty() ? null : codes.get(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* What an active DDS account's scheme is built on, and so cannot change while it runs - as a savings
|
|
||||||
* account's product, currency and opening cannot: the customer and product, the branch whose books
|
|
||||||
* carry it, the opening and its balance, and the deposit terms (daily amount, deposit, days, months)
|
|
||||||
* that its maturity amount and interest are worked out from, with the dates and amounts that follow
|
|
||||||
* from them. Changing one mid-scheme would leave the balance, the interest already posted and the
|
|
||||||
* maturity figure answering to terms the account no longer has. The collecting agent is locked too:
|
|
||||||
* the agent's pending collections, commission and cash in hand are counted against the account.
|
|
||||||
* Contact details, nominees, lien and freeze, SMS/e-mail and where the maturity is paid stay editable.
|
|
||||||
*/
|
|
||||||
private static final List<String> LOCKED_WHEN_ACTIVE = List.of("clientId", "subType", "agentId", "openingDate", "openingBalance",
|
|
||||||
"dailyDepositAmount", "depositAmount", "days", "month", "interestDate", "payDate", "paymentAmount", "reopenDate",
|
|
||||||
"lastAccount");
|
|
||||||
|
|
||||||
private void refuseEditOfClosedAccount(final Party party) {
|
|
||||||
if (party.isClosed()) {
|
|
||||||
final String message = "DDS account " + party.getAccountNo() + " is closed and cannot be edited.";
|
|
||||||
throw new PlatformApiDataValidationException("error.msg.dds.party.closed.not.editable", message,
|
|
||||||
List.of(ApiParameterError.parameterError("error.msg.dds.party.closed.not.editable", message, "status")));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void refuseChangeOfLockedTerms(final Party party, final Map<String, Object> changes, final boolean officeMoves) {
|
|
||||||
if (!Party.STATUS_ACTIVE.equals(party.getStatus())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final List<ApiParameterError> errors = new ArrayList<>();
|
|
||||||
for (final String field : LOCKED_WHEN_ACTIVE) {
|
|
||||||
if (changes.containsKey(field)) {
|
|
||||||
errors.add(ApiParameterError.parameterError("error.msg.dds.party.term.locked." + field,
|
|
||||||
"'" + field + "' cannot be changed on an active DDS account.", field));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (officeMoves) {
|
|
||||||
errors.add(ApiParameterError.parameterError("error.msg.dds.party.term.locked." + OFFICE_ID,
|
|
||||||
"The branch cannot be changed on an active DDS account.", OFFICE_ID));
|
|
||||||
}
|
|
||||||
if (!errors.isEmpty()) {
|
|
||||||
// Thrown inside the update's transaction, so nothing the request changed is kept.
|
|
||||||
throw new PlatformApiDataValidationException("error.msg.dds.party.terms.locked",
|
|
||||||
"These cannot be changed on an active DDS account: " + errors.size() + " field(s).", errors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Office officeNamed(final Long officeId) {
|
private Office officeNamed(final Long officeId) {
|
||||||
return this.officeRepositoryWrapper.findOneWithNotFoundDetection(officeId);
|
return this.officeRepositoryWrapper.findOneWithNotFoundDetection(officeId);
|
||||||
}
|
}
|
||||||
@@ -950,7 +884,7 @@ public class PartyWritePlatformServiceJpaRepositoryImpl implements PartyWritePla
|
|||||||
|
|
||||||
payOutRemainder(party, command, closeDate, lockinOverrideReason);
|
payOutRemainder(party, command, closeDate, lockinOverrideReason);
|
||||||
|
|
||||||
party.close(closeDate, currentUserId());
|
party.close(closeDate);
|
||||||
this.partyRepository.saveAndFlush(party);
|
this.partyRepository.saveAndFlush(party);
|
||||||
|
|
||||||
final Map<String, Object> changes = new HashMap<>();
|
final Map<String, Object> changes = new HashMap<>();
|
||||||
@@ -1136,20 +1070,6 @@ public class PartyWritePlatformServiceJpaRepositoryImpl implements PartyWritePla
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The next DDS account number. The update locks the serial's one row until this transaction ends,
|
|
||||||
* so two openings at once queue on it rather than taking the same number, and a rollback undoes the
|
|
||||||
* increment - unlike a sequence, which never gives a number back.
|
|
||||||
*/
|
|
||||||
private long nextAccountSerial() {
|
|
||||||
final Long next = this.jdbcTemplate.queryForObject( // NOSONAR
|
|
||||||
"update m_dds_account_number_serial set last_value = last_value + 1 where id = 1 returning last_value", Long.class);
|
|
||||||
if (next == null) {
|
|
||||||
throw new IllegalStateException("m_dds_account_number_serial has no row - migration 0624 has not run");
|
|
||||||
}
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String generateNextPartyCode() {
|
private String generateNextPartyCode() {
|
||||||
final Long maxSuffix = this.partyRepository.retrieveMaxPartyCodeSuffix();
|
final Long maxSuffix = this.partyRepository.retrieveMaxPartyCodeSuffix();
|
||||||
final long nextSuffix = (maxSuffix == null ? 0L : maxSuffix) + 1L;
|
final long nextSuffix = (maxSuffix == null ? 0L : maxSuffix) + 1L;
|
||||||
|
|||||||
-14
@@ -22,7 +22,6 @@ import com.google.gson.JsonElement;
|
|||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
import com.google.gson.reflect.TypeToken;
|
import com.google.gson.reflect.TypeToken;
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
@@ -162,7 +161,6 @@ public final class PartyTransactionCommandFromApiJsonDeserializer {
|
|||||||
} else {
|
} else {
|
||||||
v.reset().parameter(COLLECTIONS).parameterAtIndexArray("amount", i).value(extractBigDecimal(row, "amount"))
|
v.reset().parameter(COLLECTIONS).parameterAtIndexArray("amount", i).value(extractBigDecimal(row, "amount"))
|
||||||
.notNull().positiveAmount();
|
.notNull().positiveAmount();
|
||||||
toThePaisa(v, extractBigDecimal(row, "amount"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,11 +171,9 @@ public final class PartyTransactionCommandFromApiJsonDeserializer {
|
|||||||
.notBlank();
|
.notBlank();
|
||||||
v.reset().parameter("withdrawAmount").value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("withdrawAmount", element))
|
v.reset().parameter("withdrawAmount").value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("withdrawAmount", element))
|
||||||
.notNull().positiveAmount();
|
.notNull().positiveAmount();
|
||||||
toThePaisa(v, this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("withdrawAmount", element));
|
|
||||||
} else if ("AGENT_TRAN".equals(transactionType)) {
|
} else if ("AGENT_TRAN".equals(transactionType)) {
|
||||||
v.reset().parameter("amount").value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("amount", element)).notNull()
|
v.reset().parameter("amount").value(this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("amount", element)).notNull()
|
||||||
.positiveAmount();
|
.positiveAmount();
|
||||||
toThePaisa(v, this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("amount", element));
|
|
||||||
|
|
||||||
// An agent handing its round's takings to the branch is a counter transaction like any
|
// An agent handing its round's takings to the branch is a counter transaction like any
|
||||||
// other, so it says how the money arrived on the same terms a collection does.
|
// other, so it says how the money arrived on the same terms a collection does.
|
||||||
@@ -211,14 +207,4 @@ public final class PartyTransactionCommandFromApiJsonDeserializer {
|
|||||||
dataValidationErrors);
|
dataValidationErrors);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* Money is kept to the paisa, as a savings account keeps it: 100.555 is refused rather than stored
|
|
||||||
* on the passbook and the journal as typed.
|
|
||||||
*/
|
|
||||||
private static void toThePaisa(final DataValidatorBuilder v, final BigDecimal amount) {
|
|
||||||
if (amount != null && amount.stripTrailingZeros().scale() > 2) {
|
|
||||||
v.failWithCodeAndMessage("more.than.two.decimals", "An amount can have at most two decimal places (paise).", amount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-14
@@ -45,13 +45,9 @@ public class PartyTransactionReadPlatformServiceImpl implements PartyTransaction
|
|||||||
+ " t.agent_id as agentId, a.agent_code as agentCode, t.transaction_date as transactionDate, "
|
+ " t.agent_id as agentId, a.agent_code as agentCode, t.transaction_date as transactionDate, "
|
||||||
+ " t.transaction_type as transactionType, t.direction as direction, t.transaction_mode as transactionMode, "
|
+ " t.transaction_type as transactionType, t.direction as direction, t.transaction_mode as transactionMode, "
|
||||||
+ " t.account_type as accountType, t.account_no as accountNo, t.amount as amount, t.cheque_no as chequeNo, "
|
+ " t.account_type as accountType, t.account_no as accountNo, t.amount as amount, t.cheque_no as chequeNo, "
|
||||||
+ " t.token_no as tokenNo, t.description as description, t.balance_after_transaction as balanceAfterTransaction, t.voucher_no as voucherNo, "
|
+ " t.token_no as tokenNo, t.description as description, t.balance_after_transaction as balanceAfterTransaction, t.voucher_no as voucherNo "
|
||||||
+ " t.office_id as officeId, o.name as officeName, t.submitted_on_date as submittedOnDate, "
|
|
||||||
+ " t.journal_transaction_id as journalTransactionId, t.ref_no as refNo, "
|
|
||||||
+ " u.username as createdByUsername, t.created_date as createdDate, a.agent_name as agentName "
|
|
||||||
+ " from m_dds_party_transaction t left join m_dds_party p on p.id = t.party_id "
|
+ " from m_dds_party_transaction t left join m_dds_party p on p.id = t.party_id "
|
||||||
+ " join m_dds_agent a on a.id = t.agent_id left join m_office o on o.id = t.office_id "
|
+ " join m_dds_agent a on a.id = t.agent_id";
|
||||||
+ " left join m_appuser u on u.id = t.createdby_id";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -75,14 +71,6 @@ public class PartyTransactionReadPlatformServiceImpl implements PartyTransaction
|
|||||||
.description(rs.getString("description")) //
|
.description(rs.getString("description")) //
|
||||||
.balanceAfterTransaction(rs.getBigDecimal("balanceAfterTransaction")) //
|
.balanceAfterTransaction(rs.getBigDecimal("balanceAfterTransaction")) //
|
||||||
.voucherNo(rs.getString("voucherNo")) //
|
.voucherNo(rs.getString("voucherNo")) //
|
||||||
.officeId(JdbcSupport.getLong(rs, "officeId")) //
|
|
||||||
.officeName(rs.getString("officeName")) //
|
|
||||||
.submittedOnDate(JdbcSupport.getLocalDate(rs, "submittedOnDate")) //
|
|
||||||
.journalTransactionId(rs.getString("journalTransactionId")) //
|
|
||||||
.refNo(rs.getString("refNo")) //
|
|
||||||
.createdByUsername(rs.getString("createdByUsername")) //
|
|
||||||
.createdDate(rs.getTimestamp("createdDate") == null ? null : rs.getTimestamp("createdDate").toLocalDateTime()) //
|
|
||||||
.agentName(rs.getString("agentName")) //
|
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-6
@@ -53,6 +53,7 @@ import org.apache.fineract.portfolio.dds.party.exception.PartyNotFoundException;
|
|||||||
import org.apache.fineract.portfolio.dds.transaction.domain.DdsTransactionMode;
|
import org.apache.fineract.portfolio.dds.transaction.domain.DdsTransactionMode;
|
||||||
import org.apache.fineract.portfolio.dds.transaction.domain.PartyTransaction;
|
import org.apache.fineract.portfolio.dds.transaction.domain.PartyTransaction;
|
||||||
import org.apache.fineract.portfolio.dds.transaction.domain.PartyTransactionRepository;
|
import org.apache.fineract.portfolio.dds.transaction.domain.PartyTransactionRepository;
|
||||||
|
import org.apache.fineract.portfolio.dds.transaction.exception.InsufficientAgentBalanceException;
|
||||||
import org.apache.fineract.portfolio.dds.transaction.exception.PartyAdvanceAmountMismatchException;
|
import org.apache.fineract.portfolio.dds.transaction.exception.PartyAdvanceAmountMismatchException;
|
||||||
import org.apache.fineract.portfolio.dds.transaction.exception.PartyArrearsDaysExceededException;
|
import org.apache.fineract.portfolio.dds.transaction.exception.PartyArrearsDaysExceededException;
|
||||||
import org.apache.fineract.portfolio.dds.transaction.exception.PartyHasNoDailyAmountException;
|
import org.apache.fineract.portfolio.dds.transaction.exception.PartyHasNoDailyAmountException;
|
||||||
@@ -275,12 +276,28 @@ public class PartyTransactionWritePlatformServiceJpaRepositoryImpl implements Pa
|
|||||||
// receipt tagged DDS into the agent's own GL account - and applied to the customers from Agent
|
// receipt tagged DDS into the agent's own GL account - and applied to the customers from Agent
|
||||||
// Tran > Apply DDS Receipt. Taking the same cash in here as well would be a second way in for
|
// Tran > Apply DDS Receipt. Taking the same cash in here as well would be a second way in for
|
||||||
// one bag of notes, so this way is closed, and says where the right one is.
|
// one bag of notes, so this way is closed, and says where the right one is.
|
||||||
throw new PlatformDataIntegrityException("error.msg.dds.agent.tran.moved.to.cashier",
|
if (agent != null) {
|
||||||
"An agent's cash is no longer taken in here. The cashier receipts it at Cashier > Transaction Entry "
|
throw new PlatformDataIntegrityException("error.msg.dds.agent.tran.moved.to.cashier",
|
||||||
+ "(Receipt, Cash, GL, Entry Type DDS, A/C No " + (agent != null && agent.getGlAccount() != null
|
"An agent's cash is no longer taken in here. The cashier receipts it at Cashier > Transaction Entry "
|
||||||
? agent.getGlAccount().getGlCode()
|
+ "(Receipt, Cash, GL, Entry Type DDS, A/C No " + (agent.getGlAccount() != null ? agent.getGlAccount().getGlCode()
|
||||||
: "the agent's GL account") + "), and it is applied from Daily Transaction Detail > Agent Tran > "
|
: "the agent's GL account") + "), and it is applied from Daily Transaction Detail > Agent Tran > "
|
||||||
+ "Apply DDS Receipt.");
|
+ "Apply DDS Receipt.");
|
||||||
|
}
|
||||||
|
final BigDecimal amount = command.bigDecimalValueOfParameterNamed("amount");
|
||||||
|
if (agent.getAgentCurrentBalance().compareTo(amount) < 0) {
|
||||||
|
throw new InsufficientAgentBalanceException(agent.getAgentCode());
|
||||||
|
}
|
||||||
|
final BigDecimal newBalance = agent.settle(amount);
|
||||||
|
|
||||||
|
// The cash reaches the branch. No customer is involved - they were credited when their money
|
||||||
|
// was collected; this is the bank catching up with where the notes actually are.
|
||||||
|
this.ddsLedger.settle(agent.getOffice(), transactionDate, amount, "DDS settlement by " + agent.getAgentCode(), agent.getGlAccount());
|
||||||
|
|
||||||
|
return new PartyTransaction(null, agent, transactionDate, "AGENT_TRAN", "DEBIT",
|
||||||
|
DdsTransactionMode.canonicalOrOriginal(command.stringValueOfParameterNamedAllowingNull("transactionMode")),
|
||||||
|
command.stringValueOfParameterNamedAllowingNull("accountType"),
|
||||||
|
command.stringValueOfParameterNamedAllowingNull("accountNo"), amount, null, null,
|
||||||
|
command.stringValueOfParameterNamedAllowingNull("description"), newBalance);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<PartyTransaction> postWithdrawDeposite(final Agent agent, final LocalDate transactionDate,
|
private List<PartyTransaction> postWithdrawDeposite(final Agent agent, final LocalDate transactionDate,
|
||||||
|
|||||||
+81
@@ -0,0 +1,81 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, 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.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(InvestmentApiDates.parse(fyEnd, null, "financial year end"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+151
@@ -0,0 +1,151 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.api;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constants for the Treasury Investment masters (Bank / Party Master and Script Master), modelled on the bank's existing
|
||||||
|
* CBS Investment screens.
|
||||||
|
*/
|
||||||
|
public final class InvestmentApiConstants {
|
||||||
|
|
||||||
|
private InvestmentApiConstants() {}
|
||||||
|
|
||||||
|
public static final String ENTITY_PARTY = "INVESTMENTPARTY";
|
||||||
|
public static final String ENTITY_SCRIPT = "INVESTMENTSCRIPT";
|
||||||
|
public static final String RESOURCE_PARTY = "investmentparty";
|
||||||
|
public static final String RESOURCE_SCRIPT = "investmentscript";
|
||||||
|
|
||||||
|
/** Instrument ("Bond Type") an investment party / script belongs to. */
|
||||||
|
public static final Set<String> BOND_TYPES = Set.of("GOVT_SECURITY", "NON_SLR", "BOND", "MUTUAL_FUND", "FD", "SHARE");
|
||||||
|
|
||||||
|
/** Bond types that carry coupon terms (ISIN, interest rate, issue date are mandatory for these). */
|
||||||
|
public static final Set<String> DEBT_BOND_TYPES = Set.of("GOVT_SECURITY", "NON_SLR", "BOND");
|
||||||
|
|
||||||
|
/** SLR classification on the Script Master ("GOI" = Central Government). */
|
||||||
|
public static final Set<String> SLR_TYPES = Set.of("GOI", "STATE_GOVT", "TREASURY", "NON_SLR");
|
||||||
|
|
||||||
|
/** Script Code prefix per bond type, e.g. G00001 for the first Govt Security. */
|
||||||
|
public static final Map<String, String> SCRIPT_CODE_PREFIX = Map.of("GOVT_SECURITY", "G", "NON_SLR", "N", "BOND", "B", "MUTUAL_FUND",
|
||||||
|
"M", "FD", "F", "SHARE", "S");
|
||||||
|
|
||||||
|
/** Bond types that are non-SLR investments, counted against the RBI limit of 10% of deposits. */
|
||||||
|
public static final Set<String> NON_SLR_BOND_TYPES = Set.of("NON_SLR", "BOND", "MUTUAL_FUND");
|
||||||
|
|
||||||
|
/** Bond types that must carry a credit rating: corporate bonds, CPs and CDs. */
|
||||||
|
public static final Set<String> RATED_BOND_TYPES = Set.of("NON_SLR", "BOND");
|
||||||
|
|
||||||
|
/** Credit ratings on the Script Master: long-term (bonds) and short-term (CP / CD) scales, best first. */
|
||||||
|
public static final List<String> RATINGS = List.of("AAA", "AA+", "AA", "AA-", "A+", "A", "A-", "BBB+", "BBB", "BBB-", "BB+", "BB",
|
||||||
|
"BB-", "B+", "B", "B-", "C", "D", "A1+", "A1", "A2+", "A2", "A3+", "A3", "A4+", "A4", "UNRATED");
|
||||||
|
|
||||||
|
/** RBI: corporate bonds rated "A" or higher, CPs and CDs rated "A2" or higher. */
|
||||||
|
public static final Set<String> ELIGIBLE_RATINGS = Set.of("AAA", "AA+", "AA", "AA-", "A+", "A", "A-", "A1+", "A1", "A2+", "A2");
|
||||||
|
|
||||||
|
/** Short-term scale, for CPs and CDs (issued for up to one year); bonds carry the long-term scale. */
|
||||||
|
public static final Set<String> SHORT_TERM_RATINGS = Set.of("A1+", "A1", "A2+", "A2", "A3+", "A3", "A4+", "A4");
|
||||||
|
|
||||||
|
/** Credit rating agencies registered with SEBI. */
|
||||||
|
public static final List<String> RATING_AGENCIES = List.of("CRISIL", "ICRA", "CARE", "INDIA_RATINGS", "ACUITE", "BRICKWORK",
|
||||||
|
"INFOMERICS");
|
||||||
|
|
||||||
|
/** RBI: only debt and money-market mutual funds; their sub types start with this. */
|
||||||
|
public static final String DEBT_MF_SUB_TYPE_PREFIX = "DEBT";
|
||||||
|
|
||||||
|
/** Share investments: only these institutions (no shares of companies outside the co-operative sector). */
|
||||||
|
public static final String SHARE = "SHARE";
|
||||||
|
public static final String INSTITUTION_STATE_COOP_BANK = "STATE_COOP_BANK";
|
||||||
|
public static final String INSTITUTION_DCCB = "DCCB";
|
||||||
|
public static final String INSTITUTION_OTHER_COOP_SOCIETY = "OTHER_COOP_SOCIETY";
|
||||||
|
/** Membership shares of NPCI, CCIL, SWIFT, the Umbrella Organisation of UCBs and IDPIC. */
|
||||||
|
public static final String INSTITUTION_MEMBERSHIP_MIC_UO = "MEMBERSHIP_MIC_UO";
|
||||||
|
public static final Set<String> INSTITUTION_TYPES = Set.of(INSTITUTION_STATE_COOP_BANK, INSTITUTION_DCCB, INSTITUTION_OTHER_COOP_SOCIETY,
|
||||||
|
INSTITUTION_MEMBERSHIP_MIC_UO);
|
||||||
|
/** Co-operative institutions: their shares are valued on the dividend / balance sheet rules. */
|
||||||
|
public static final Set<String> COOP_INSTITUTION_TYPES = Set.of(INSTITUTION_STATE_COOP_BANK, INSTITUTION_DCCB,
|
||||||
|
INSTITUTION_OTHER_COOP_SOCIETY);
|
||||||
|
|
||||||
|
public static final String PARTY_CODE_PREFIX = "PTY-";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deal slip: how the deal was confirmed to the counterparty (entered with the deal), and the counterparty's
|
||||||
|
* confirmation the back office records when it settles the deal.
|
||||||
|
*/
|
||||||
|
public static final Set<String> CONFIRMATION_MODES = Set.of("EMAIL", "NDS_OM", "FAX", "LETTER", "PHONE");
|
||||||
|
public static final String CONFIRMATION_MODE = "confirmationMode";
|
||||||
|
public static final String CONFIRMATION_RECEIVED_ON = "confirmationReceivedOn";
|
||||||
|
public static final String CONFIRMATION_REF = "confirmationRef";
|
||||||
|
|
||||||
|
// Common parameters
|
||||||
|
public static final String BOND_TYPE = "bondType";
|
||||||
|
public static final String GL_ACCOUNT_ID = "glAccountId";
|
||||||
|
public static final String INTEREST_GL_ACCOUNT_ID = "interestGlAccountId";
|
||||||
|
public static final String ADDRESS1 = "address1";
|
||||||
|
public static final String ADDRESS2 = "address2";
|
||||||
|
public static final String ADDRESS3 = "address3";
|
||||||
|
public static final String CITY = "city";
|
||||||
|
public static final String PINCODE = "pincode";
|
||||||
|
public static final String PHONE = "phone";
|
||||||
|
public static final String FAX = "fax";
|
||||||
|
public static final String ACTIVE = "active";
|
||||||
|
public static final String LOCALE = "locale";
|
||||||
|
public static final String DATE_FORMAT = "dateFormat";
|
||||||
|
|
||||||
|
// Party parameters
|
||||||
|
public static final String PARTY_NAME = "partyName";
|
||||||
|
public static final String BANK_TYPE = "bankType";
|
||||||
|
public static final String RBI_CODE = "rbiCode";
|
||||||
|
public static final String EXPOSURE_LIMIT = "limit";
|
||||||
|
public static final String LEI_NO = "leiNo";
|
||||||
|
public static final String CP_BANK_NAME = "cpBankName";
|
||||||
|
public static final String CP_BRANCH_NAME = "cpBranchName";
|
||||||
|
public static final String CP_ACCOUNT_NO = "cpAccountNo";
|
||||||
|
public static final String CP_IFSC = "cpIfsc";
|
||||||
|
public static final String TDS_APPLICABLE = "tdsApplicable";
|
||||||
|
public static final String TDS_RATE = "tdsRate";
|
||||||
|
public static final String INSTITUTION_TYPE = "institutionType";
|
||||||
|
public static final String SUBSCRIBED_CAPITAL = "subscribedCapital";
|
||||||
|
public static final String LAST_BALANCE_SHEET_DATE = "lastBalanceSheetDate";
|
||||||
|
public static final String IN_LIQUIDATION = "inLiquidation";
|
||||||
|
|
||||||
|
// Script parameters
|
||||||
|
public static final String SLR_TYPE = "slrType";
|
||||||
|
public static final String SCRIPT_DESC = "scriptDesc";
|
||||||
|
public static final String ISIN = "isin";
|
||||||
|
public static final String FACE_VALUE_PER_UNIT = "faceValuePerUnit";
|
||||||
|
public static final String INT_RATE = "intRate";
|
||||||
|
public static final String ISSUE_DATE = "issueDate";
|
||||||
|
public static final String MATURITY_DATE = "maturityDate";
|
||||||
|
public static final String COMPANY_NAME = "companyName";
|
||||||
|
public static final String CORP_CODE = "corpCode";
|
||||||
|
public static final String RATING = "rating";
|
||||||
|
public static final String RATING_AGENCY = "ratingAgency";
|
||||||
|
public static final String LISTED = "listed";
|
||||||
|
|
||||||
|
public static final Set<String> PARTY_PARAMS = Set.of(BOND_TYPE, PARTY_NAME, ADDRESS1, ADDRESS2, ADDRESS3, CITY, PINCODE, PHONE, FAX,
|
||||||
|
BANK_TYPE, GL_ACCOUNT_ID, INTEREST_GL_ACCOUNT_ID, RBI_CODE, EXPOSURE_LIMIT, LEI_NO, CP_BANK_NAME, CP_BRANCH_NAME, CP_ACCOUNT_NO, CP_IFSC, ACTIVE,
|
||||||
|
TDS_APPLICABLE, TDS_RATE, INSTITUTION_TYPE, SUBSCRIBED_CAPITAL, LAST_BALANCE_SHEET_DATE, IN_LIQUIDATION, LOCALE, DATE_FORMAT);
|
||||||
|
|
||||||
|
public static final Set<String> SCRIPT_PARAMS = Set.of(BOND_TYPE, SLR_TYPE, SCRIPT_DESC, GL_ACCOUNT_ID, ISIN, FACE_VALUE_PER_UNIT,
|
||||||
|
INT_RATE, ISSUE_DATE, MATURITY_DATE, COMPANY_NAME, ADDRESS1, ADDRESS2, ADDRESS3, CITY, PINCODE, PHONE, FAX, CORP_CODE,
|
||||||
|
RATING, RATING_AGENCY, LISTED, ACTIVE, LOCALE, DATE_FORMAT);
|
||||||
|
}
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, 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.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.format.DateTimeParseException;
|
||||||
|
import java.time.format.ResolverStyle;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||||
|
|
||||||
|
/** Query-parameter dates of the Investment APIs: DD-MM-YYYY, strictly (31-02 is refused, not moved to month end). */
|
||||||
|
public final class InvestmentApiDates {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-uuuu").withResolverStyle(ResolverStyle.STRICT);
|
||||||
|
|
||||||
|
private InvestmentApiDates() {}
|
||||||
|
|
||||||
|
/** The date in {@code value}, or {@code fallback} when it is blank; a malformed date is a clear 403, not a 500. */
|
||||||
|
public static LocalDate parse(final String value, final LocalDate fallback, final String name) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
if (fallback == null) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.investment.date.required", "Enter the " + name + " as DD-MM-YYYY",
|
||||||
|
name);
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return LocalDate.parse(value.trim(), DATE);
|
||||||
|
} catch (final DateTimeParseException e) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.investment.date.invalid", "Enter the " + name + " as DD-MM-YYYY",
|
||||||
|
value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+72
@@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.api;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.QueryParam;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.format.DateTimeParseException;
|
||||||
|
import java.time.format.ResolverStyle;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.investment.data.InvestmentBucketReportData;
|
||||||
|
import org.apache.fineract.portfolio.investment.service.InvestmentBucketReportService;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Path("/v1/investment/bucket-report")
|
||||||
|
@Component
|
||||||
|
@Tag(name = "Investment Bucket Wise", description = "Investments held on a date, split by the time left to maturity.")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class InvestmentBucketReportApiResource {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-uuuu").withResolverStyle(ResolverStyle.STRICT);
|
||||||
|
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final InvestmentBucketReportService reportService;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Bucket Wise Investment", description = "asOnDate dd-MM-yyyy; default today")
|
||||||
|
public InvestmentBucketReportData report(@QueryParam("asOnDate") @Parameter(description = "dd-MM-yyyy") final String asOnDate) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
|
||||||
|
if (asOnDate == null || asOnDate.isBlank()) {
|
||||||
|
return this.reportService.report(DateUtils.getBusinessLocalDate());
|
||||||
|
}
|
||||||
|
final LocalDate asOn;
|
||||||
|
try {
|
||||||
|
asOn = LocalDate.parse(asOnDate.trim(), DATE);
|
||||||
|
} catch (final DateTimeParseException e) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.investment.report.date.invalid", "Enter the date as DD-MM-YYYY",
|
||||||
|
asOnDate);
|
||||||
|
}
|
||||||
|
if (asOn.isAfter(DateUtils.getBusinessLocalDate())) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.investment.report.future", "The as-on date cannot be after today");
|
||||||
|
}
|
||||||
|
return this.reportService.report(asOn);
|
||||||
|
}
|
||||||
|
}
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, 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);
|
||||||
|
}
|
||||||
+93
@@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, 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.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.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 = InvestmentApiDates.parse(onDate, DateUtils.getBusinessLocalDate(), "date");
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+72
@@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.api;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.QueryParam;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.investment.data.InvestmentDealSlipData;
|
||||||
|
import org.apache.fineract.portfolio.investment.service.InvestmentDealSlipService;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Path("/v1/investment/deal-slips")
|
||||||
|
@Component
|
||||||
|
@Tag(name = "Investment Deal Slips", description = "Serially numbered deal slips of every purchase, sale, refund, redemption and "
|
||||||
|
+ "switch, cancelled ones included, with the confirmation and who entered, approved and settled each deal.")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class InvestmentDealSlipApiResource {
|
||||||
|
|
||||||
|
/** Default window when no dates are given: the last 30 days. */
|
||||||
|
private static final int DEFAULT_DAYS = 30;
|
||||||
|
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final InvestmentDealSlipService dealSlipService;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Deal slip register", description = "Deals dated fromDate to toDate (dd-MM-yyyy; default the last 30 days) "
|
||||||
|
+ "in slip number order; or the one slip numbered slipNo (a purchase deal number or a sale number).")
|
||||||
|
public List<InvestmentDealSlipData> retrieve(@QueryParam("fromDate") @Parameter(description = "dd-MM-yyyy") final String fromDate,
|
||||||
|
@QueryParam("toDate") @Parameter(description = "dd-MM-yyyy") final String toDate,
|
||||||
|
@QueryParam("slipNo") @Parameter(description = "202627000001 or 202627S00001") final String slipNo) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
|
||||||
|
if (StringUtils.isNotBlank(slipNo)) {
|
||||||
|
return this.dealSlipService.slip(slipNo);
|
||||||
|
}
|
||||||
|
final LocalDate today = DateUtils.getBusinessLocalDate();
|
||||||
|
final LocalDate to = InvestmentApiDates.parse(toDate, today, "To date");
|
||||||
|
final LocalDate from = InvestmentApiDates.parse(fromDate, to.minusDays(DEFAULT_DAYS), "From date");
|
||||||
|
if (to.isBefore(from)) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.investment.dealslip.dates", "The To date cannot be before the From date");
|
||||||
|
}
|
||||||
|
return this.dealSlipService.register(from, to);
|
||||||
|
}
|
||||||
|
}
|
||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.api;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.QueryParam;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.format.DateTimeParseException;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.investment.data.InvestmentDiaryData;
|
||||||
|
import org.apache.fineract.portfolio.investment.service.InvestmentDiaryService;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Path("/v1/investment/diary")
|
||||||
|
@Component
|
||||||
|
@Tag(name = "Investment Due Date Diary", description = "Coupons, provisions, amortisation, FD payouts and maturities due in a period.")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class InvestmentDiaryApiResource {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
||||||
|
/** Default window when no dates are given: today and the next 30 days. */
|
||||||
|
private static final int DEFAULT_DAYS = 30;
|
||||||
|
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final InvestmentDiaryService diaryService;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Due Date Diary for all investments", description = "fromDate / toDate dd-MM-yyyy; default today to 30 days ahead")
|
||||||
|
public InvestmentDiaryData diary(@QueryParam("fromDate") @Parameter(description = "dd-MM-yyyy") final String fromDate,
|
||||||
|
@QueryParam("toDate") @Parameter(description = "dd-MM-yyyy") final String toDate) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
|
||||||
|
final LocalDate today = DateUtils.getBusinessLocalDate();
|
||||||
|
final LocalDate from = date(fromDate, today);
|
||||||
|
final LocalDate to = date(toDate, from.plusDays(DEFAULT_DAYS));
|
||||||
|
if (to.isBefore(from)) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.investment.diary.dates", "The To date cannot be before the From date");
|
||||||
|
}
|
||||||
|
return this.diaryService.diary(from, to);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LocalDate date(final String value, final LocalDate fallback) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return LocalDate.parse(value.trim(), DATE);
|
||||||
|
} catch (final DateTimeParseException e) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.investment.diary.date.invalid", "Enter the date as DD-MM-YYYY", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+120
@@ -0,0 +1,120 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.api;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/** Fixed deposits placed with other banks. */
|
||||||
|
public final class InvestmentFdApiConstants {
|
||||||
|
|
||||||
|
private InvestmentFdApiConstants() {}
|
||||||
|
|
||||||
|
public static final String ENTITY = "INVESTMENTFD";
|
||||||
|
public static final String BOND_TYPE_FD = "FD";
|
||||||
|
|
||||||
|
public static final String STATUS_ACTIVE = "ACTIVE";
|
||||||
|
public static final String STATUS_MATURED = "MATURED";
|
||||||
|
public static final String STATUS_PREMATURE = "PREMATURE";
|
||||||
|
public static final String STATUS_RENEWED = "RENEWED";
|
||||||
|
|
||||||
|
/** When the interest is paid: all at maturity (the usual case), or every half year / year. */
|
||||||
|
public static final String PAYOUT_AT_MATURITY = "MATURITY";
|
||||||
|
public static final String PAYOUT_HALF_YEARLY = "HALF_YEARLY";
|
||||||
|
public static final String PAYOUT_YEARLY = "YEARLY";
|
||||||
|
public static final Set<String> PAYOUTS = Set.of(PAYOUT_AT_MATURITY, PAYOUT_HALF_YEARLY, PAYOUT_YEARLY);
|
||||||
|
|
||||||
|
/** How the maturity amount is worked out, to compare with the bank's figure (Jitendra Sir). */
|
||||||
|
public static final String CALC_SIMPLE = "SIMPLE";
|
||||||
|
public static final String CALC_MONTHLY = "MONTHLY";
|
||||||
|
public static final String CALC_QUARTERLY = "QUARTERLY";
|
||||||
|
public static final String CALC_HALF_YEARLY = "HALF_YEARLY";
|
||||||
|
public static final String CALC_YEARLY = "YEARLY";
|
||||||
|
public static final Set<String> CALC_TYPES = Set.of(CALC_SIMPLE, CALC_MONTHLY, CALC_QUARTERLY, CALC_HALF_YEARLY, CALC_YEARLY);
|
||||||
|
|
||||||
|
/** RBI: deposits with other banks within 20% of total deposits as on the previous 31 March, and within 5% with one bank. */
|
||||||
|
public static final BigDecimal GROSS_LIMIT_PERCENT = BigDecimal.valueOf(20);
|
||||||
|
public static final BigDecimal BANK_LIMIT_PERCENT = BigDecimal.valueOf(5);
|
||||||
|
/** Current accounts with banks also count towards the 20% inter-bank exposure. */
|
||||||
|
public static final String BANK_BALANCE_HEADER_GL_CODE = "100200";
|
||||||
|
/** Deposits of the bank, for the suggested total-deposits figure: everything under the deposit header, plus RD. */
|
||||||
|
public static final String DEPOSIT_HEADER_GL_CODE = "200100";
|
||||||
|
public static final String RD_DEPOSIT_GL_CODE = "200201";
|
||||||
|
|
||||||
|
public static final String PARTY_ID = "partyId";
|
||||||
|
public static final String CERT_NO = "certNo";
|
||||||
|
public static final String CERT_DATE = "certDate";
|
||||||
|
public static final String DEAL_DATE = "dealDate";
|
||||||
|
public static final String AMOUNT = "amount";
|
||||||
|
public static final String INT_RATE = "intRate";
|
||||||
|
public static final String MATURITY_DATE = "maturityDate";
|
||||||
|
public static final String PAYOUT = "payout";
|
||||||
|
public static final String CALC_TYPE = "calcType";
|
||||||
|
public static final String BANK_GL_ACCOUNT_ID = "bankGlAccountId";
|
||||||
|
public static final String REMARKS = "remarks";
|
||||||
|
public static final String AS_ON_DATE = "asOnDate";
|
||||||
|
public static final String TOTAL_DEPOSITS = "totalDeposits";
|
||||||
|
public static final String OWNED_FUNDS = "ownedFunds";
|
||||||
|
public static final String LOCALE = "locale";
|
||||||
|
public static final String DATE_FORMAT = "dateFormat";
|
||||||
|
|
||||||
|
public static final String CLOSURE_MATURITY = "MATURITY";
|
||||||
|
public static final String CLOSURE_PREMATURE = "PREMATURE";
|
||||||
|
public static final String CLOSURE_RENEWAL = "RENEWAL";
|
||||||
|
public static final String LINE_PROVISION = "PROVISION";
|
||||||
|
public static final String LINE_PAYOUT = "PAYOUT";
|
||||||
|
public static final String INTEREST_RECEIVABLE_GL_CODE = "100603";
|
||||||
|
/** Tax the other bank deducts from the interest, recoverable from the tax department. */
|
||||||
|
public static final String TDS_RECEIVABLE_GL_CODE = "100602";
|
||||||
|
|
||||||
|
public static final String CLOSE_DATE = "closeDate";
|
||||||
|
public static final String RECEIVED_AMOUNT = "receivedAmount";
|
||||||
|
public static final String TDS = "tds";
|
||||||
|
public static final String PENAL_AMOUNT = "penalAmount";
|
||||||
|
public static final String PRINCIPAL_BANK_GL_ACCOUNT_ID = "principalBankGlAccountId";
|
||||||
|
public static final String INTEREST_BANK_GL_ACCOUNT_ID = "interestBankGlAccountId";
|
||||||
|
public static final String NEW_CERT_NO = "newCertNo";
|
||||||
|
public static final String NEW_CERT_DATE = "newCertDate";
|
||||||
|
public static final String NEW_INT_RATE = "newIntRate";
|
||||||
|
public static final String NEW_MATURITY_DATE = "newMaturityDate";
|
||||||
|
public static final String NEW_PAYOUT = "newPayout";
|
||||||
|
public static final String NEW_CALC_TYPE = "newCalcType";
|
||||||
|
public static final String MATURITY_AMOUNT = "maturityAmount";
|
||||||
|
public static final String RENEWAL_AMOUNT = "renewalAmount";
|
||||||
|
public static final String DIFF_BANK_GL_ACCOUNT_ID = "diffBankGlAccountId";
|
||||||
|
|
||||||
|
public static final String LINE_MATURITY = "MATURITY";
|
||||||
|
public static final String LINE_TYPE = "lineType";
|
||||||
|
public static final String INT_DATE = "intDate";
|
||||||
|
/** Reminders show lines due within this many days (as for Govt securities). */
|
||||||
|
public static final int DUE_WITHIN_DAYS = 2;
|
||||||
|
public static final Set<String> INTEREST_PARAMS = Set.of(LINE_TYPE, INT_DATE, RECEIVED_AMOUNT, TDS, BANK_GL_ACCOUNT_ID, LOCALE,
|
||||||
|
DATE_FORMAT);
|
||||||
|
|
||||||
|
public static final Set<String> MATURE_PARAMS = Set.of(RECEIVED_AMOUNT, TDS, PRINCIPAL_BANK_GL_ACCOUNT_ID, INTEREST_BANK_GL_ACCOUNT_ID,
|
||||||
|
REMARKS, LOCALE, DATE_FORMAT);
|
||||||
|
public static final Set<String> PREMATURE_PARAMS = Set.of(CLOSE_DATE, RECEIVED_AMOUNT, TDS, PENAL_AMOUNT, PRINCIPAL_BANK_GL_ACCOUNT_ID,
|
||||||
|
INTEREST_BANK_GL_ACCOUNT_ID, REMARKS, LOCALE, DATE_FORMAT);
|
||||||
|
public static final Set<String> RENEW_PARAMS = Set.of(NEW_CERT_NO, NEW_CERT_DATE, NEW_INT_RATE, NEW_MATURITY_DATE, NEW_PAYOUT,
|
||||||
|
NEW_CALC_TYPE, MATURITY_AMOUNT, RENEWAL_AMOUNT, TDS, DIFF_BANK_GL_ACCOUNT_ID, REMARKS, LOCALE, DATE_FORMAT);
|
||||||
|
|
||||||
|
public static final Set<String> FD_PARAMS = Set.of(PARTY_ID, CERT_NO, CERT_DATE, DEAL_DATE, AMOUNT, INT_RATE, MATURITY_DATE, PAYOUT,
|
||||||
|
CALC_TYPE, BANK_GL_ACCOUNT_ID, REMARKS, LOCALE, DATE_FORMAT);
|
||||||
|
public static final Set<String> BASE_PARAMS = Set.of(AS_ON_DATE, TOTAL_DEPOSITS, OWNED_FUNDS, REMARKS, LOCALE, DATE_FORMAT);
|
||||||
|
}
|
||||||
+199
@@ -0,0 +1,199 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.api;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.ws.rs.Consumes;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.POST;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.PathParam;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.QueryParam;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||||
|
import org.apache.fineract.commands.service.CommandWrapperBuilder;
|
||||||
|
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.investment.data.InvestmentFdClosureData;
|
||||||
|
import org.apache.fineract.portfolio.investment.data.InvestmentFdData;
|
||||||
|
import org.apache.fineract.portfolio.investment.data.InvestmentFdDueData;
|
||||||
|
import org.apache.fineract.portfolio.investment.data.InvestmentFdScheduleData;
|
||||||
|
import org.apache.fineract.portfolio.investment.service.InvestmentFdClosureService;
|
||||||
|
import org.apache.fineract.portfolio.investment.service.InvestmentFdInterestService;
|
||||||
|
import org.apache.fineract.portfolio.investment.service.InvestmentFdService;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Path("/v1/investment/fds")
|
||||||
|
@Component
|
||||||
|
@Tag(name = "Investment FD", description = "Fixed deposits the bank places with other banks.")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class InvestmentFdApiResource {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
||||||
|
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final InvestmentFdService fdService;
|
||||||
|
private final InvestmentFdClosureService closureService;
|
||||||
|
private final InvestmentFdInterestService interestService;
|
||||||
|
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "All FDs placed with other banks")
|
||||||
|
public List<InvestmentFdData> retrieveAll() {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(InvestmentFdApiConstants.ENTITY);
|
||||||
|
return this.fdService.retrieveAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("quote")
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Preview an FD: interest, maturity amount and the RBI limits", description = "Dates dd-MM-yyyy")
|
||||||
|
public InvestmentFdData quote(@QueryParam("partyId") final Long partyId, @QueryParam("amount") final BigDecimal amount,
|
||||||
|
@QueryParam("intRate") final BigDecimal intRate, @QueryParam("dealDate") final String dealDate,
|
||||||
|
@QueryParam("maturityDate") final String maturityDate, @QueryParam("payout") final String payout,
|
||||||
|
@QueryParam("calcType") final String calcType) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(InvestmentFdApiConstants.ENTITY);
|
||||||
|
return this.fdService.quote(partyId, amount, intRate, date(dealDate), date(maturityDate), payout, calcType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("due")
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "FD interest vouchers and maturities due", description = "Due now or within withinDays (default 2)")
|
||||||
|
public List<InvestmentFdDueData> due(@QueryParam("withinDays") final Integer withinDays) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(InvestmentFdApiConstants.ENTITY);
|
||||||
|
return this.interestService.due(withinDays == null ? InvestmentFdApiConstants.DUE_WITHIN_DAYS : withinDays);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("{fdId}/interest-schedule")
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Interest lines of an FD", description = "31 Mar / 30 Sep provisions, payouts and maturity")
|
||||||
|
public InvestmentFdScheduleData schedule(@PathParam("fdId") final Long fdId) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(InvestmentFdApiConstants.ENTITY);
|
||||||
|
return this.interestService.schedule(fdId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("{fdId}/interest")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Post the next interest line of an FD", description = "lineType PROVISION | PAYOUT, intDate; payout: receivedAmount, tds")
|
||||||
|
public CommandProcessingResult postInterest(@PathParam("fdId") final Long fdId, @Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
return this.commandsSourceWritePlatformService
|
||||||
|
.logCommandSource(new CommandWrapperBuilder().postInvestmentFdInterest(fdId).withJson(apiRequestBodyAsJson).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("{fdId}")
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "One FD")
|
||||||
|
public InvestmentFdData retrieveOne(@PathParam("fdId") @Parameter(description = "fdId") final Long fdId) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(InvestmentFdApiConstants.ENTITY);
|
||||||
|
return this.fdService.retrieveOne(fdId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Place an FD", description = "Dr the bank's FD GL / Cr our bank GL, dated the deal date; refused beyond the RBI limits.")
|
||||||
|
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
final CommandWrapper request = new CommandWrapperBuilder().createInvestmentFd().withJson(apiRequestBodyAsJson).build();
|
||||||
|
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("{fdId}/closure-quote")
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Preview a maturity, premature closure or renewal", description = "type MATURITY | PREMATURE | RENEWAL")
|
||||||
|
public InvestmentFdClosureData closureQuote(@PathParam("fdId") final Long fdId, @QueryParam("type") final String type,
|
||||||
|
@QueryParam("closeDate") final String closeDate, @QueryParam("receivedAmount") final BigDecimal receivedAmount,
|
||||||
|
@QueryParam("tds") final BigDecimal tds, @QueryParam("renewalAmount") final BigDecimal renewalAmount,
|
||||||
|
@QueryParam("principalBankGlAccountId") final Long principalBankGlAccountId,
|
||||||
|
@QueryParam("interestBankGlAccountId") final Long interestBankGlAccountId,
|
||||||
|
@QueryParam("diffBankGlAccountId") final Long diffBankGlAccountId) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(InvestmentFdApiConstants.ENTITY);
|
||||||
|
return this.closureService.quote(fdId, type, date(closeDate), receivedAmount, tds, renewalAmount, principalBankGlAccountId,
|
||||||
|
interestBankGlAccountId, diffBankGlAccountId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("{fdId}/mature")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Close an FD at maturity", description = "Dated the maturity date; amount as credited by the bank, optional TDS.")
|
||||||
|
public CommandProcessingResult mature(@PathParam("fdId") final Long fdId, @Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
return this.commandsSourceWritePlatformService
|
||||||
|
.logCommandSource(new CommandWrapperBuilder().matureInvestmentFd(fdId).withJson(apiRequestBodyAsJson).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("{fdId}/premature")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Close an FD before maturity", description = "Not calculated: the amount received (and any penal interest) is entered.")
|
||||||
|
public CommandProcessingResult premature(@PathParam("fdId") final Long fdId, @Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
return this.commandsSourceWritePlatformService
|
||||||
|
.logCommandSource(new CommandWrapperBuilder().prematureInvestmentFd(fdId).withJson(apiRequestBodyAsJson).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("{fdId}/renew")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Renew an FD at maturity", description = "New certificate, rate and maturity; money added or taken out goes through a bank GL.")
|
||||||
|
public CommandProcessingResult renew(@PathParam("fdId") final Long fdId, @Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
return this.commandsSourceWritePlatformService
|
||||||
|
.logCommandSource(new CommandWrapperBuilder().renewInvestmentFd(fdId).withJson(apiRequestBodyAsJson).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("deposit-base")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Record total deposits as on a 31 March", description = "The base of the RBI limits for the following year.")
|
||||||
|
public CommandProcessingResult setDepositBase(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
final CommandWrapper request = new CommandWrapperBuilder().setInvestmentDepositBase().withJson(apiRequestBodyAsJson).build();
|
||||||
|
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LocalDate date(final String value) {
|
||||||
|
if (StringUtils.isBlank(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return LocalDate.parse(value.trim(), DATE);
|
||||||
|
} catch (final java.time.format.DateTimeParseException e) {
|
||||||
|
// A half-typed date is a bad request, not a server error.
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.investmentfd.date.invalid", "Enter the date as DD-MM-YYYY", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.api;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/** Constants for posting an interest schedule line (coupon or closing provision) of a holding to GL. */
|
||||||
|
public final class InvestmentInterestApiConstants {
|
||||||
|
|
||||||
|
private InvestmentInterestApiConstants() {}
|
||||||
|
|
||||||
|
public static final String ENTITY = "INVESTMENTINTEREST";
|
||||||
|
public static final String RESOURCE = "investmentinterest";
|
||||||
|
|
||||||
|
public static final String LINE_TYPE = "lineType";
|
||||||
|
public static final String INT_DATE = "intDate";
|
||||||
|
/** Bank GL the coupon is received into; defaults to the purchase's paying bank. */
|
||||||
|
public static final String BANK_GL_ACCOUNT_ID = "bankGlAccountId";
|
||||||
|
public static final String LOCALE = "locale";
|
||||||
|
public static final String DATE_FORMAT = "dateFormat";
|
||||||
|
|
||||||
|
public static final Set<String> PARAMS = Set.of(LINE_TYPE, INT_DATE, BANK_GL_ACCOUNT_ID, LOCALE, DATE_FORMAT);
|
||||||
|
|
||||||
|
/** A line shows as a task this many days before its date (Jitendra Sir). */
|
||||||
|
public static final int DUE_WITHIN_DAYS = 2;
|
||||||
|
|
||||||
|
public static final String INTEREST_RECEIVABLE_GL_CODE = "100603";
|
||||||
|
public static final String INTEREST_INCOME_GOVT_SECURITIES_GL_CODE = "400401";
|
||||||
|
public static final String INTEREST_INCOME_BONDS_GL_CODE = "400402";
|
||||||
|
|
||||||
|
/** Income GL for a holding's interest: SLR govt securities, or bonds / debentures for the rest. */
|
||||||
|
public static String interestIncomeGlCode(final String bondType) {
|
||||||
|
return "GOVT_SECURITY".equals(bondType) ? INTEREST_INCOME_GOVT_SECURITIES_GL_CODE : INTEREST_INCOME_BONDS_GL_CODE;
|
||||||
|
}
|
||||||
|
}
|
||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+84
@@ -0,0 +1,84 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.api;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.QueryParam;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.Month;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.format.DateTimeParseException;
|
||||||
|
import java.time.format.ResolverStyle;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.investment.data.InvestmentInterestReportData;
|
||||||
|
import org.apache.fineract.portfolio.investment.service.InvestmentInterestReportService;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Path("/v1/investment/interest-report")
|
||||||
|
@Component
|
||||||
|
@Tag(name = "Investment Interest Received & Receivable", description = "Interest received in a period and receivable at its end.")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class InvestmentInterestReportApiResource {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-uuuu").withResolverStyle(ResolverStyle.STRICT);
|
||||||
|
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final InvestmentInterestReportService reportService;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Total Interest Received & Receivable", description = "fromDate / toDate dd-MM-yyyy; default 1 April of the "
|
||||||
|
+ "current financial year to today")
|
||||||
|
public InvestmentInterestReportData report(@QueryParam("fromDate") @Parameter(description = "dd-MM-yyyy") final String fromDate,
|
||||||
|
@QueryParam("toDate") @Parameter(description = "dd-MM-yyyy") final String toDate) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
|
||||||
|
final LocalDate today = DateUtils.getBusinessLocalDate();
|
||||||
|
final LocalDate to = date(toDate, today);
|
||||||
|
if (to.isAfter(today)) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.investment.report.future", "The To date cannot be after today");
|
||||||
|
}
|
||||||
|
// Without a From date the report runs from 1 April of the To date's financial year.
|
||||||
|
final LocalDate fyStart = LocalDate.of(to.getMonthValue() >= 4 ? to.getYear() : to.getYear() - 1, Month.APRIL, 1);
|
||||||
|
final LocalDate from = date(fromDate, fyStart);
|
||||||
|
if (to.isBefore(from)) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.investment.report.dates", "The To date cannot be before the From date");
|
||||||
|
}
|
||||||
|
return this.reportService.report(from, to);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LocalDate date(final String value, final LocalDate fallback) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return LocalDate.parse(value.trim(), DATE);
|
||||||
|
} catch (final DateTimeParseException e) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.investment.report.date.invalid", "Enter the date as DD-MM-YYYY", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+99
@@ -0,0 +1,99 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.api;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.ws.rs.Consumes;
|
||||||
|
import jakarta.ws.rs.DELETE;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.POST;
|
||||||
|
import jakarta.ws.rs.PUT;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.PathParam;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||||
|
import org.apache.fineract.commands.service.CommandWrapperBuilder;
|
||||||
|
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.investment.data.InvestmentPartyData;
|
||||||
|
import org.apache.fineract.portfolio.investment.service.InvestmentMasterReadPlatformService;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Path("/v1/investment/parties")
|
||||||
|
@Component
|
||||||
|
@Tag(name = "Investment Bank / Party Master", description = "Counterparties an investment is bought from or placed with.")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class InvestmentPartyApiResource {
|
||||||
|
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final InvestmentMasterReadPlatformService readPlatformService;
|
||||||
|
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "List investment parties")
|
||||||
|
public List<InvestmentPartyData> retrieveAll() {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(InvestmentApiConstants.ENTITY_PARTY);
|
||||||
|
return this.readPlatformService.retrieveAllParties();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("{partyId}")
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Retrieve an investment party")
|
||||||
|
public InvestmentPartyData retrieveOne(@PathParam("partyId") @Parameter(description = "partyId") final Long partyId) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(InvestmentApiConstants.ENTITY_PARTY);
|
||||||
|
return this.readPlatformService.retrieveParty(partyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Create an investment party")
|
||||||
|
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
final CommandWrapper request = new CommandWrapperBuilder().createInvestmentParty().withJson(apiRequestBodyAsJson).build();
|
||||||
|
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PUT
|
||||||
|
@Path("{partyId}")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Update an investment party")
|
||||||
|
public CommandProcessingResult update(@PathParam("partyId") @Parameter(description = "partyId") final Long partyId,
|
||||||
|
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
final CommandWrapper request = new CommandWrapperBuilder().updateInvestmentParty(partyId).withJson(apiRequestBodyAsJson).build();
|
||||||
|
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DELETE
|
||||||
|
@Path("{partyId}")
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Drop an investment party")
|
||||||
|
public CommandProcessingResult delete(@PathParam("partyId") @Parameter(description = "partyId") final Long partyId) {
|
||||||
|
final CommandWrapper request = new CommandWrapperBuilder().deleteInvestmentParty(partyId).build();
|
||||||
|
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
+87
@@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.api;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/** Constants for the Investment Purchase Entry (a deal that creates a holding). */
|
||||||
|
public final class InvestmentPurchaseApiConstants {
|
||||||
|
|
||||||
|
private InvestmentPurchaseApiConstants() {}
|
||||||
|
|
||||||
|
public static final String ENTITY = "INVESTMENTPURCHASE";
|
||||||
|
public static final String RESOURCE = "investmentpurchase";
|
||||||
|
|
||||||
|
public static final Set<String> SETTLEMENT_TYPES = Set.of("T+0", "T+1", "T+2");
|
||||||
|
public static final Set<String> CATEGORIES = Set.of("HTM (Held To Maturity)", "AFS (Available For Sale)", "HFT (Held For Trading)");
|
||||||
|
|
||||||
|
/** Saved deal; nothing posted to GL until it is settled. */
|
||||||
|
public static final String STATUS_PENDING_SETTLEMENT = "PENDING_SETTLEMENT";
|
||||||
|
/** Settled: posted to GL on the settlement date, now a holding. */
|
||||||
|
public static final String STATUS_ACTIVE = "ACTIVE";
|
||||||
|
/** Every unit sold before maturity. */
|
||||||
|
public static final String STATUS_SOLD = "SOLD";
|
||||||
|
/** Repaid at maturity. */
|
||||||
|
public static final String STATUS_REDEEMED = "REDEEMED";
|
||||||
|
/** Dropped before settlement; kept so its deal and certificate numbers are never given out again. */
|
||||||
|
public static final String STATUS_DROPPED = "DROPPED";
|
||||||
|
|
||||||
|
/** GLs the settlement entry debits besides the script's investment GL. */
|
||||||
|
public static final String BROKEN_PERIOD_INTEREST_GL_CODE = "500606";
|
||||||
|
public static final String STAMP_DUTY_GL_CODE = "500601";
|
||||||
|
public static final String BROKERAGE_GL_CODE = "500602";
|
||||||
|
public static final String OTHER_CHARGES_GL_CODE = "500603";
|
||||||
|
|
||||||
|
public static final String BOND_TYPE = "bondType";
|
||||||
|
public static final String SCRIPT_ID = "scriptId";
|
||||||
|
public static final String PARTY_ID = "partyId";
|
||||||
|
public static final String DEAL_DATE = "dealDate";
|
||||||
|
public static final String DEAL_TIME = "dealTime";
|
||||||
|
public static final String SETTLEMENT_TYPE = "settlementType";
|
||||||
|
public static final String FACE_VALUE = "faceValue";
|
||||||
|
public static final String PURCHASE_RATE = "purchaseRate";
|
||||||
|
public static final String CONSIDER_AMT = "considerAmt";
|
||||||
|
public static final String NAV = "nav";
|
||||||
|
public static final String STAMP_DUTY = "stampDuty";
|
||||||
|
public static final String BROKERAGE = "brokerage";
|
||||||
|
public static final String OTHER_CHARGES = "otherCharges";
|
||||||
|
public static final String CATEGORY = "category";
|
||||||
|
public static final String LAST_INT_DATE = "lastIntDate";
|
||||||
|
public static final String CR_GL_ACCOUNT_ID = "crGlAccountId";
|
||||||
|
public static final String SUB_TYPE = "subType";
|
||||||
|
/** Optional legacy fields (trade no, authorities, lien, delivery mode, …) kept as a JSON object. */
|
||||||
|
public static final String OTHER_DETAILS = "otherDetails";
|
||||||
|
public static final String LOCALE = "locale";
|
||||||
|
public static final String DATE_FORMAT = "dateFormat";
|
||||||
|
|
||||||
|
public static final Set<String> PARAMS = Set.of(BOND_TYPE, SCRIPT_ID, PARTY_ID, DEAL_DATE, DEAL_TIME, SETTLEMENT_TYPE, FACE_VALUE,
|
||||||
|
PURCHASE_RATE, CONSIDER_AMT, NAV, STAMP_DUTY, BROKERAGE, OTHER_CHARGES, CATEGORY, LAST_INT_DATE, CR_GL_ACCOUNT_ID, SUB_TYPE,
|
||||||
|
OTHER_DETAILS, InvestmentApiConstants.CONFIRMATION_MODE, LOCALE, DATE_FORMAT);
|
||||||
|
/** Settle may carry the counterparty's confirmation of the deal. */
|
||||||
|
public static final Set<String> SETTLE_PARAMS = Set.of(InvestmentApiConstants.CONFIRMATION_RECEIVED_ON,
|
||||||
|
InvestmentApiConstants.CONFIRMATION_REF, LOCALE, DATE_FORMAT);
|
||||||
|
|
||||||
|
/** Dividend received on a share holding: Dr Bank / Cr 400404 Dividend on Investments, dated the day it is received. */
|
||||||
|
public static final String DIVIDEND_GL_CODE = "400404";
|
||||||
|
public static final String DIVIDEND_DATE = "dividendDate";
|
||||||
|
public static final String AMOUNT = "amount";
|
||||||
|
public static final String BANK_GL_ACCOUNT_ID = "bankGlAccountId";
|
||||||
|
public static final String REMARKS = "remarks";
|
||||||
|
public static final Set<String> DIVIDEND_PARAMS = Set.of(DIVIDEND_DATE, AMOUNT, BANK_GL_ACCOUNT_ID, REMARKS, LOCALE, DATE_FORMAT);
|
||||||
|
}
|
||||||
+252
@@ -0,0 +1,252 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, 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.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.InvestmentDividendData;
|
||||||
|
import org.apache.fineract.portfolio.investment.data.InvestmentInterestScheduleData;
|
||||||
|
import org.apache.fineract.portfolio.investment.data.InvestmentNonSlrLimitData;
|
||||||
|
import org.apache.fineract.portfolio.investment.data.InvestmentPurchaseData;
|
||||||
|
import org.apache.fineract.portfolio.investment.data.InvestmentSaleData;
|
||||||
|
import org.apache.fineract.portfolio.investment.data.InvestmentShareLimitData;
|
||||||
|
import org.apache.fineract.portfolio.investment.service.InvestmentNonSlrService;
|
||||||
|
import org.apache.fineract.portfolio.investment.service.InvestmentPurchaseReadPlatformService;
|
||||||
|
import org.apache.fineract.portfolio.investment.service.InvestmentSaleReadPlatformService;
|
||||||
|
import org.apache.fineract.portfolio.investment.service.InvestmentShareService;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Path("/v1/investment/purchases")
|
||||||
|
@Component
|
||||||
|
@Tag(name = "Investment Purchase Entry", description = "Investment deals (Govt Security, Bond, Mutual Fund, …) — each becomes a holding.")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class InvestmentPurchaseApiResource {
|
||||||
|
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final InvestmentPurchaseReadPlatformService readPlatformService;
|
||||||
|
private final InvestmentSaleReadPlatformService saleReadPlatformService;
|
||||||
|
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||||
|
private final InvestmentNonSlrService nonSlrService;
|
||||||
|
private final InvestmentShareService shareService;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "List investment purchases (holdings)")
|
||||||
|
public List<InvestmentPurchaseData> retrieveAll() {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
|
||||||
|
return this.readPlatformService.retrieveAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("non-slr-limit")
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "RBI non-SLR limits for a purchase", description = "Where a non-SLR purchase of amount (book value) in scriptId "
|
||||||
|
+ "on dealDate (dd-MM-yyyy, default today) stands against the 10% limits; any of them may be left out.")
|
||||||
|
public InvestmentNonSlrLimitData nonSlrLimit(@QueryParam("scriptId") @Parameter(description = "scriptId") final Long scriptId,
|
||||||
|
@QueryParam("dealDate") @Parameter(description = "dd-MM-yyyy") final String dealDate,
|
||||||
|
@QueryParam("amount") @Parameter(description = "amount") final BigDecimal amount) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
|
||||||
|
final LocalDate date = InvestmentApiDates.parse(dealDate, DateUtils.getBusinessLocalDate(), "deal date");
|
||||||
|
return this.nonSlrService.limits(date, scriptId, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("share-limit")
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "RBI limits for a share purchase", description = "Where a purchase of amount (book value) of shares of partyId "
|
||||||
|
+ "on dealDate (dd-MM-yyyy, default today) stands against the 2% of owned funds and 5% of the society's capital limits; "
|
||||||
|
+ "membership shares are exempt.")
|
||||||
|
public InvestmentShareLimitData shareLimit(@QueryParam("partyId") @Parameter(description = "partyId") final Long partyId,
|
||||||
|
@QueryParam("dealDate") @Parameter(description = "dd-MM-yyyy") final String dealDate,
|
||||||
|
@QueryParam("amount") @Parameter(description = "amount") final BigDecimal amount) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
|
||||||
|
final LocalDate date = InvestmentApiDates.parse(dealDate, DateUtils.getBusinessLocalDate(), "deal date");
|
||||||
|
return this.shareService.limits(date, partyId, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("dividends")
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Dividends received on share holdings", description = "Newest first, with their vouchers.")
|
||||||
|
public List<InvestmentDividendData> retrieveDividends() {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(InvestmentPurchaseApiConstants.ENTITY);
|
||||||
|
return this.shareService.retrieveDividends();
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 : InvestmentApiDates.parse(asOnDate, null, "as-on date");
|
||||||
|
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}/dividends")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Record a dividend received on a share holding", description = "dividendDate, amount, bankGlAccountId, "
|
||||||
|
+ "remarks: Dr the bank GL / Cr 400404 Dividend on Investments, dated the day it was received.")
|
||||||
|
public CommandProcessingResult postDividend(@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId,
|
||||||
|
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
final CommandWrapper request = new CommandWrapperBuilder().dividendInvestmentPurchase(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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("{purchaseId}/switch")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Mutual fund switch", description = "switchDate, units, fromNav, toScriptId, toNav, subType, remarks: the units "
|
||||||
|
+ "leave this fund at fromNav and a new deal opens in the other scheme the same day; no bank leg.")
|
||||||
|
public CommandProcessingResult switchFund(@PathParam("purchaseId") @Parameter(description = "purchaseId") final Long purchaseId,
|
||||||
|
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
final CommandWrapper request = new CommandWrapperBuilder().switchInvestmentFund(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+89
@@ -0,0 +1,89 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.api;
|
||||||
|
|
||||||
|
import 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";
|
||||||
|
/** Units of a mutual fund switched at NAV into another scheme the same day; no money comes to the bank. */
|
||||||
|
public static final String TYPE_SWITCH = "SWITCH";
|
||||||
|
|
||||||
|
public static final String STATUS_PENDING_SETTLEMENT = "PENDING_SETTLEMENT";
|
||||||
|
public static final String STATUS_SETTLED = "SETTLED";
|
||||||
|
/** Dropped before settlement; kept so its sale number stays accounted for in the deal slip register. */
|
||||||
|
public static final String STATUS_DROPPED = "DROPPED";
|
||||||
|
|
||||||
|
/** Sales that are deals with a deal slip of their own (a sale number); write-offs and amortisation are not. */
|
||||||
|
public static final Set<String> NUMBERED_TYPES = Set.of(TYPE_SELL, TYPE_REDEEM, TYPE_SWITCH);
|
||||||
|
|
||||||
|
/** 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 BROKER_CODE = "brokerCode";
|
||||||
|
public static final String AMOUNT = "amount";
|
||||||
|
public static final String FY_END = "fyEnd";
|
||||||
|
public static final String SWITCH_DATE = "switchDate";
|
||||||
|
public static final String FROM_NAV = "fromNav";
|
||||||
|
public static final String TO_SCRIPT_ID = "toScriptId";
|
||||||
|
public static final String TO_NAV = "toNav";
|
||||||
|
public static final String SUB_TYPE = "subType";
|
||||||
|
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, BROKER_CODE, InvestmentApiConstants.CONFIRMATION_MODE, LOCALE,
|
||||||
|
DATE_FORMAT);
|
||||||
|
/** Settle may carry the counterparty's confirmation of the deal. */
|
||||||
|
public static final Set<String> SETTLE_PARAMS = Set.of(InvestmentApiConstants.CONFIRMATION_RECEIVED_ON,
|
||||||
|
InvestmentApiConstants.CONFIRMATION_REF, 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);
|
||||||
|
public static final Set<String> SWITCH_PARAMS = Set.of(SWITCH_DATE, UNITS, FROM_NAV, TO_SCRIPT_ID, TO_NAV, SUB_TYPE, REMARKS, LOCALE,
|
||||||
|
DATE_FORMAT);
|
||||||
|
}
|
||||||
+113
@@ -0,0 +1,113 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, 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.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 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 = InvestmentApiDates.parse(dealDate, null, "deal date");
|
||||||
|
return this.readPlatformService.quoteSale(purchaseId, deal, StringUtils.defaultIfBlank(settlementType, "T+0"), units, saleRate,
|
||||||
|
brokerage, bankGlAccountId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Save a sale", description = "Saved pending; nothing reaches the GL until it is settled.")
|
||||||
|
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
final CommandWrapper request = new CommandWrapperBuilder().createInvestmentSale().withJson(apiRequestBodyAsJson).build();
|
||||||
|
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("{saleId}/settle")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Settle a sale", description = "On or after the settlement date: posts the sale to GL dated the settlement "
|
||||||
|
+ "date and reduces the units held.")
|
||||||
|
public CommandProcessingResult settle(@PathParam("saleId") @Parameter(description = "saleId") final Long saleId,
|
||||||
|
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
final CommandWrapper request = new CommandWrapperBuilder().settleInvestmentSale(saleId)
|
||||||
|
.withJson(StringUtils.isBlank(apiRequestBodyAsJson) ? "{}" : apiRequestBodyAsJson).build();
|
||||||
|
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DELETE
|
||||||
|
@Path("{saleId}")
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Drop a pending sale")
|
||||||
|
public CommandProcessingResult delete(@PathParam("saleId") @Parameter(description = "saleId") final Long saleId) {
|
||||||
|
final CommandWrapper request = new CommandWrapperBuilder().deleteInvestmentSale(saleId).build();
|
||||||
|
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
+89
@@ -0,0 +1,89 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.api;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.ws.rs.Consumes;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.POST;
|
||||||
|
import jakarta.ws.rs.PUT;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.PathParam;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||||
|
import org.apache.fineract.commands.service.CommandWrapperBuilder;
|
||||||
|
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.investment.data.InvestmentScriptData;
|
||||||
|
import org.apache.fineract.portfolio.investment.service.InvestmentMasterReadPlatformService;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Path("/v1/investment/scripts")
|
||||||
|
@Component
|
||||||
|
@Tag(name = "Investment Script Master", description = "Securities / instruments the bank invests in (Govt Security, Bond, Mutual Fund, …).")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class InvestmentScriptApiResource {
|
||||||
|
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final InvestmentMasterReadPlatformService readPlatformService;
|
||||||
|
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "List investment scripts")
|
||||||
|
public List<InvestmentScriptData> retrieveAll() {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(InvestmentApiConstants.ENTITY_SCRIPT);
|
||||||
|
return this.readPlatformService.retrieveAllScripts();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("{scriptId}")
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Retrieve an investment script")
|
||||||
|
public InvestmentScriptData retrieveOne(@PathParam("scriptId") @Parameter(description = "scriptId") final Long scriptId) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(InvestmentApiConstants.ENTITY_SCRIPT);
|
||||||
|
return this.readPlatformService.retrieveScript(scriptId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Create an investment script")
|
||||||
|
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
final CommandWrapper request = new CommandWrapperBuilder().createInvestmentScript().withJson(apiRequestBodyAsJson).build();
|
||||||
|
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PUT
|
||||||
|
@Path("{scriptId}")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Update an investment script")
|
||||||
|
public CommandProcessingResult update(@PathParam("scriptId") @Parameter(description = "scriptId") final Long scriptId,
|
||||||
|
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
final CommandWrapper request = new CommandWrapperBuilder().updateInvestmentScript(scriptId).withJson(apiRequestBodyAsJson).build();
|
||||||
|
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, 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;
|
||||||
|
|
||||||
|
/** Market valuation (MTM) of AFS and HFT holdings, with net depreciation held in the Investment Depreciation Reserve. */
|
||||||
|
public final class InvestmentValuationApiConstants {
|
||||||
|
|
||||||
|
private InvestmentValuationApiConstants() {}
|
||||||
|
|
||||||
|
public static final String ENTITY = "INVESTMENTVALUATION";
|
||||||
|
public static final String RESOURCE = "investmentvaluation";
|
||||||
|
|
||||||
|
/** Categories that are marked to market: HFT monthly, AFS quarterly (or more often). HTM is carried at cost. */
|
||||||
|
public static final Set<String> CATEGORIES = Set.of("AFS", "HFT");
|
||||||
|
|
||||||
|
/** Classes within which gains and losses are netted; one class never offsets another. */
|
||||||
|
public static final String CLASS_GOVT = "GOVT_SECURITY";
|
||||||
|
public static final String CLASS_BOND = "CORPORATE_BOND";
|
||||||
|
public static final String CLASS_FUND = "MUTUAL_FUND";
|
||||||
|
/** Shares are valued by rule, not at a market rate: face value, nil, or ₹1 per institution (see the valuation service). */
|
||||||
|
public static final String CLASS_SHARE = "SHARE";
|
||||||
|
|
||||||
|
/** Depreciation charged to P&L (Provisions & Contingencies) and the reserve it is held in. */
|
||||||
|
public static final String DEPRECIATION_PROVISION_GL_CODE = "500504";
|
||||||
|
public static final String DEPRECIATION_RESERVE_GL_CODE = "200205";
|
||||||
|
|
||||||
|
public static final String VALUATION_DATE = "valuationDate";
|
||||||
|
public static final String CATEGORY = "category";
|
||||||
|
public static final String LOCALE = "locale";
|
||||||
|
public static final String DATE_FORMAT = "dateFormat";
|
||||||
|
|
||||||
|
public static final Set<String> RUN_PARAMS = Set.of(VALUATION_DATE, CATEGORY, LOCALE, DATE_FORMAT);
|
||||||
|
}
|
||||||
+92
@@ -0,0 +1,92 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, 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.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.core.service.DateUtils;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.investment.data.InvestmentValuationData;
|
||||||
|
import org.apache.fineract.portfolio.investment.service.InvestmentValuationService;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Path("/v1/investment/valuations")
|
||||||
|
@Component
|
||||||
|
@Tag(name = "Investment Valuation", description = "Market valuation (MTM) of AFS / HFT holdings; net depreciation held in the IDR.")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class InvestmentValuationApiResource {
|
||||||
|
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final InvestmentValuationService valuationService;
|
||||||
|
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Valuation runs, newest first")
|
||||||
|
public List<InvestmentValuationData> retrieveAll() {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(InvestmentValuationApiConstants.ENTITY);
|
||||||
|
return this.valuationService.retrieveAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("preview")
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "What a valuation would find and post, without saving", description = "valuationDate dd-MM-yyyy (default "
|
||||||
|
+ "today), category AFS or HFT")
|
||||||
|
public InvestmentValuationData preview(@QueryParam("valuationDate") @Parameter(description = "dd-MM-yyyy") final String valuationDate,
|
||||||
|
@QueryParam("category") @Parameter(description = "AFS or HFT") final String category) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(InvestmentValuationApiConstants.ENTITY);
|
||||||
|
return this.valuationService.preview(InvestmentApiDates.parse(valuationDate, DateUtils.getBusinessLocalDate(), "valuation date"),
|
||||||
|
category);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("{valuationId}")
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "One valuation run with its security-wise lines")
|
||||||
|
public InvestmentValuationData retrieveOne(@PathParam("valuationId") @Parameter(description = "valuationId") final Long valuationId) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(InvestmentValuationApiConstants.ENTITY);
|
||||||
|
return this.valuationService.retrieveOne(valuationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Run and post a valuation", description = "valuationDate, category (AFS / HFT). Posts the change in IDR: "
|
||||||
|
+ "Dr 500504 / Cr 200205 when it grows, the reverse when it falls.")
|
||||||
|
public CommandProcessingResult run(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
final CommandWrapper request = new CommandWrapperBuilder().createInvestmentValuation().withJson(apiRequestBodyAsJson).build();
|
||||||
|
return this.commandsSourceWritePlatformService.logCommandSource(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/** Premium amortisation schedule of an HTM holding: one line per 31 March to maturity, then the rest at maturity. */
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class InvestmentAmortisationData {
|
||||||
|
|
||||||
|
private final Long purchaseId;
|
||||||
|
private final String dealNo;
|
||||||
|
private final String certNo;
|
||||||
|
private final String scriptCode;
|
||||||
|
private final String scriptDesc;
|
||||||
|
private final String category;
|
||||||
|
private final String settlementDate;
|
||||||
|
private final String maturityDate;
|
||||||
|
private final BigDecimal faceValue;
|
||||||
|
private final BigDecimal premium;
|
||||||
|
private final BigDecimal unamortisedPremium;
|
||||||
|
private final BigDecimal bookValue;
|
||||||
|
private final String premiumGlCode;
|
||||||
|
private final List<Row> rows;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public static class Row {
|
||||||
|
|
||||||
|
/** YEAR (31 March) or MATURITY (the remainder, written off on redemption). */
|
||||||
|
private final String kind;
|
||||||
|
private final String fyEnd;
|
||||||
|
private final String fromDate;
|
||||||
|
private final String toDate;
|
||||||
|
private final long days;
|
||||||
|
private final BigDecimal amount;
|
||||||
|
private final BigDecimal premiumAfter;
|
||||||
|
private final boolean posted;
|
||||||
|
private final String voucherNo;
|
||||||
|
private final String glTransactionId;
|
||||||
|
/** Next line to post and its 31 March has come. */
|
||||||
|
private final boolean postable;
|
||||||
|
}
|
||||||
|
}
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/** A holding's premium amortisation due for a 31 March (Amortize Yearly Process). */
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class InvestmentAmortisationDueData {
|
||||||
|
|
||||||
|
private final Long purchaseId;
|
||||||
|
private final String dealNo;
|
||||||
|
private final String certNo;
|
||||||
|
private final String scriptCode;
|
||||||
|
private final String scriptDesc;
|
||||||
|
private final String fyEnd;
|
||||||
|
private final String fromDate;
|
||||||
|
private final long days;
|
||||||
|
private final BigDecimal unamortisedPremium;
|
||||||
|
private final BigDecimal amount;
|
||||||
|
}
|
||||||
+80
@@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bucket Wise Investment: Govt Securities, bonds and FDs held on a date, split by the time left to maturity (up to 1
|
||||||
|
* year, over 1 to 3, over 3 to 5, over 5 years).
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class InvestmentBucketReportData {
|
||||||
|
|
||||||
|
private final String asOnDate;
|
||||||
|
private final List<Bucket> buckets;
|
||||||
|
private final Bucket total;
|
||||||
|
private final List<Row> rows;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public static class Bucket {
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
private final String label;
|
||||||
|
private final int govtCount;
|
||||||
|
private final BigDecimal govtFaceValue;
|
||||||
|
private final BigDecimal govtBookValue;
|
||||||
|
private final int bondCount;
|
||||||
|
private final BigDecimal bondFaceValue;
|
||||||
|
private final BigDecimal bondBookValue;
|
||||||
|
private final int fdCount;
|
||||||
|
private final BigDecimal fdPrincipal;
|
||||||
|
/** Book value of securities and bonds plus FD principal. */
|
||||||
|
private final BigDecimal totalAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public static class Row {
|
||||||
|
|
||||||
|
/** Code of the bucket the holding falls in. */
|
||||||
|
private final String bucket;
|
||||||
|
/** GOVT_SECURITY, BOND (Non-SLR and other bonds) or FD. */
|
||||||
|
private final String instrument;
|
||||||
|
private final Long refId;
|
||||||
|
private final String dealNo;
|
||||||
|
/** Script, or bank and certificate of an FD. */
|
||||||
|
private final String particulars;
|
||||||
|
/** HTM / AFS / HFT of a security; blank for an FD. */
|
||||||
|
private final String category;
|
||||||
|
private final BigDecimal rate;
|
||||||
|
private final String maturityDate;
|
||||||
|
/** Days from the as-on date to maturity; negative when maturity has passed but it is not yet redeemed or closed. */
|
||||||
|
private final long daysToMaturity;
|
||||||
|
/** Face value held, or the FD principal. */
|
||||||
|
private final BigDecimal faceValue;
|
||||||
|
/** Cost less what sales, amortisation and write-offs took off, or the FD principal. */
|
||||||
|
private final BigDecimal bookValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+94
@@ -0,0 +1,94 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One deal slip: a purchase (BUY) or a sale, refund, redemption or switch, with everything the slip must show — the
|
||||||
|
* counterparty, the security, amount, price, deal date and time, settlement date, how the deal was confirmed, direct or
|
||||||
|
* through a broker (and the brokerage), and who entered, approved, settled or cancelled it. A dropped deal stays on the
|
||||||
|
* register as CANCELLED so its number is accounted for.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class InvestmentDealSlipData {
|
||||||
|
|
||||||
|
/** Purchase deal number (202627000001) or sale number (202627S00001). */
|
||||||
|
private final String slipNo;
|
||||||
|
/** BUY, SELL, REFUND (shares), REDEEM or SWITCH. */
|
||||||
|
private final String side;
|
||||||
|
/** PENDING_SETTLEMENT, SETTLED or CANCELLED. */
|
||||||
|
private final String status;
|
||||||
|
/** Status of the purchase / sale record itself (ACTIVE, SOLD, DROPPED, …). */
|
||||||
|
private final String recordStatus;
|
||||||
|
private final Long purchaseId;
|
||||||
|
private final Long saleId;
|
||||||
|
/** Deal number of the holding (for a sale, the purchase it sells from). */
|
||||||
|
private final String dealNo;
|
||||||
|
private final String certNo;
|
||||||
|
private final String dealDate;
|
||||||
|
private final String dealTime;
|
||||||
|
private final String settlementType;
|
||||||
|
private final String settlementDate;
|
||||||
|
private final String partyCode;
|
||||||
|
private final String counterparty;
|
||||||
|
private final String bondType;
|
||||||
|
private final String scriptCode;
|
||||||
|
private final String scriptDesc;
|
||||||
|
private final String isin;
|
||||||
|
private final String category;
|
||||||
|
private final BigDecimal units;
|
||||||
|
private final BigDecimal faceValue;
|
||||||
|
/** Price per ₹100 of face value, NAV per unit or price per share. */
|
||||||
|
private final BigDecimal price;
|
||||||
|
/** Price of the units without interest or charges. */
|
||||||
|
private final BigDecimal consideration;
|
||||||
|
private final BigDecimal accruedInterest;
|
||||||
|
private final BigDecimal brokerage;
|
||||||
|
/** Stamp duty and other charges of a purchase. */
|
||||||
|
private final BigDecimal otherCharges;
|
||||||
|
/** Money paid out (purchase) or received (sale). */
|
||||||
|
private final BigDecimal netAmount;
|
||||||
|
private final String bankGlCode;
|
||||||
|
private final String bankGlName;
|
||||||
|
/** DIRECT or BROKER. */
|
||||||
|
private final String dealMode;
|
||||||
|
private final String brokerCode;
|
||||||
|
private final String tradeNo;
|
||||||
|
private final String orderNo;
|
||||||
|
private final String confirmationMode;
|
||||||
|
private final String confirmationReceivedOn;
|
||||||
|
private final String confirmationRef;
|
||||||
|
private final String enteredBy;
|
||||||
|
private final String enteredOn;
|
||||||
|
private final String approvedBy;
|
||||||
|
private final String approvedOn;
|
||||||
|
private final String settledBy;
|
||||||
|
private final String settledOn;
|
||||||
|
private final String settleApprovedBy;
|
||||||
|
private final String settleApprovedOn;
|
||||||
|
private final String cancelledBy;
|
||||||
|
private final String cancelledOn;
|
||||||
|
private final String voucherNo;
|
||||||
|
private final String glTransactionId;
|
||||||
|
private final String remarks;
|
||||||
|
}
|
||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Due Date Diary: every dated event of the active investments in a period — Govt Security / bond coupons and 31 Mar /
|
||||||
|
* 30 Sep provisions, maturities, premium amortisation, and FD provisions, payouts and maturities.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class InvestmentDiaryData {
|
||||||
|
|
||||||
|
private final String fromDate;
|
||||||
|
private final String toDate;
|
||||||
|
private final String asOnDate;
|
||||||
|
private final int pendingCount;
|
||||||
|
private final int overdueCount;
|
||||||
|
/** Money expected in the period from coupons, FD payouts and maturities not yet received. */
|
||||||
|
private final BigDecimal expectedInflow;
|
||||||
|
private final List<Row> rows;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public static class Row {
|
||||||
|
|
||||||
|
private final String date;
|
||||||
|
/** COUPON, PROVISION, MATURITY, AMORTISATION (Govt Security / bond); FD_PROVISION, FD_PAYOUT, FD_MATURITY. */
|
||||||
|
private final String kind;
|
||||||
|
/** SECURITY or FD — which screen the entry belongs to. */
|
||||||
|
private final String instrument;
|
||||||
|
private final Long refId;
|
||||||
|
private final String dealNo;
|
||||||
|
/** Script, or bank and certificate of an FD. */
|
||||||
|
private final String particulars;
|
||||||
|
private final BigDecimal amount;
|
||||||
|
/** POSTED, OVERDUE, DUE (today), UPCOMING. */
|
||||||
|
private final String status;
|
||||||
|
private final String voucherNo;
|
||||||
|
}
|
||||||
|
}
|
||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, 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.InvestmentDividend;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentPurchase;
|
||||||
|
|
||||||
|
/** A dividend received on a share holding, with its voucher. */
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class InvestmentDividendData {
|
||||||
|
|
||||||
|
private final Long id;
|
||||||
|
private final Long purchaseId;
|
||||||
|
private final String dealNo;
|
||||||
|
private final String certNo;
|
||||||
|
private final String scriptCode;
|
||||||
|
private final String scriptDesc;
|
||||||
|
private final Long partyId;
|
||||||
|
private final String partyName;
|
||||||
|
private final String dividendDate;
|
||||||
|
private final BigDecimal amount;
|
||||||
|
private final String bankGlCode;
|
||||||
|
private final String bankGlName;
|
||||||
|
private final String remarks;
|
||||||
|
private final String glTransactionId;
|
||||||
|
private final String voucherNo;
|
||||||
|
|
||||||
|
public static InvestmentDividendData from(final InvestmentDividend d) {
|
||||||
|
final InvestmentPurchase p = d.getPurchase();
|
||||||
|
return InvestmentDividendData.builder().id(d.getId()).purchaseId(p.getId()).dealNo(p.getDealNo()).certNo(p.getCertNo())
|
||||||
|
.scriptCode(p.getScript().getScriptCode()).scriptDesc(p.getScript().getScriptDesc()).partyId(p.getParty().getId())
|
||||||
|
.partyName(p.getParty().getPartyName()).dividendDate(d.getDividendDate().format(DateTimeFormatter.ofPattern("dd-MM-yyyy")))
|
||||||
|
.amount(d.getAmount()).bankGlCode(d.getBankGlAccount().getGlCode()).bankGlName(d.getBankGlAccount().getName())
|
||||||
|
.remarks(d.getRemarks()).glTransactionId(d.getGlTransactionId()).voucherNo(d.getVoucherNo()).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/** What closing an FD (maturity, premature or renewal) works out to, and the voucher it posts. */
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class InvestmentFdClosureData {
|
||||||
|
|
||||||
|
private final Long fdId;
|
||||||
|
private final String dealNo;
|
||||||
|
private final String type;
|
||||||
|
private final String closeDate;
|
||||||
|
private final BigDecimal principal;
|
||||||
|
/** Interest the system works out for the period, to compare with the bank's figure. */
|
||||||
|
private final BigDecimal expectedInterest;
|
||||||
|
/** Interest already booked by provisions and not yet received. */
|
||||||
|
private final BigDecimal provisioned;
|
||||||
|
/** Money credited to our bank by the other bank (net of TDS). */
|
||||||
|
private final BigDecimal receivedAmount;
|
||||||
|
private final BigDecimal tds;
|
||||||
|
private final BigDecimal penalAmount;
|
||||||
|
/** Gross interest as per the bank: received + TDS − principal. */
|
||||||
|
private final BigDecimal interest;
|
||||||
|
/** Interest income of this entry: interest less what was provisioned (negative = reversed from income). */
|
||||||
|
private final BigDecimal income;
|
||||||
|
/** Bank's interest less the system's. */
|
||||||
|
private final BigDecimal difference;
|
||||||
|
private final BigDecimal renewalAmount;
|
||||||
|
/** Renewal: money added (positive) or taken out (negative) against the maturity amount. */
|
||||||
|
private final BigDecimal topUp;
|
||||||
|
private final List<Line> lines;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class Line {
|
||||||
|
|
||||||
|
private final String side;
|
||||||
|
private final String glCode;
|
||||||
|
private final String glName;
|
||||||
|
private final BigDecimal amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
+107
@@ -0,0 +1,107 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentFd;
|
||||||
|
|
||||||
|
/** An FD placed with another bank; also the shape of a purchase preview (with the limit check). */
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class InvestmentFdData {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
||||||
|
|
||||||
|
private final Long id;
|
||||||
|
private final String dealNo;
|
||||||
|
private final Long partyId;
|
||||||
|
private final String partyCode;
|
||||||
|
private final String partyName;
|
||||||
|
/** TDS rate the bank deducts on interest; null when it does not deduct TDS. */
|
||||||
|
private final BigDecimal tdsRate;
|
||||||
|
private final String certNo;
|
||||||
|
private final String certDate;
|
||||||
|
private final String dealDate;
|
||||||
|
private final BigDecimal principal;
|
||||||
|
private final BigDecimal intRate;
|
||||||
|
private final String maturityDate;
|
||||||
|
private final long days;
|
||||||
|
private final String payout;
|
||||||
|
private final String calcType;
|
||||||
|
private final BigDecimal expectedInterest;
|
||||||
|
private final BigDecimal expectedMaturityAmount;
|
||||||
|
/** Interest paid each half year / year for a payout FD. */
|
||||||
|
private final BigDecimal periodInterest;
|
||||||
|
private final String fdGlCode;
|
||||||
|
private final String fdGlName;
|
||||||
|
private final String interestGlCode;
|
||||||
|
private final String interestGlName;
|
||||||
|
private final Long bankGlAccountId;
|
||||||
|
private final String bankGlCode;
|
||||||
|
private final String bankGlName;
|
||||||
|
private final String status;
|
||||||
|
private final String remarks;
|
||||||
|
private final String glTransactionId;
|
||||||
|
private final String voucherNo;
|
||||||
|
private final String renewedFromDealNo;
|
||||||
|
private final String closedOnDate;
|
||||||
|
private final String closureType;
|
||||||
|
private final BigDecimal closureAmount;
|
||||||
|
private final BigDecimal closureInterest;
|
||||||
|
private final BigDecimal closureTds;
|
||||||
|
private final BigDecimal closurePenal;
|
||||||
|
private final String closureVoucherNo;
|
||||||
|
private final String closureGlTransactionId;
|
||||||
|
private final InvestmentFdLimitData limit;
|
||||||
|
|
||||||
|
public static InvestmentFdData from(final InvestmentFd f, final BigDecimal periodInterest) {
|
||||||
|
return builder().id(f.getId()).dealNo(f.getDealNo()).partyId(f.getParty().getId()).partyCode(f.getParty().getPartyCode())
|
||||||
|
.partyName(f.getParty().getPartyName())
|
||||||
|
.tdsRate(f.getParty().isTdsApplicable() ? f.getParty().getTdsRate() : null).certNo(f.getCertNo())
|
||||||
|
.certDate(format(f.getCertDate()))
|
||||||
|
.dealDate(format(f.getDealDate())).principal(f.getPrincipal()).intRate(f.getIntRate())
|
||||||
|
.maturityDate(format(f.getMaturityDate())).days(ChronoUnit.DAYS.between(f.getDealDate(), f.getMaturityDate()))
|
||||||
|
.payout(f.getPayout()).calcType(f.getCalcType()).expectedInterest(f.getExpectedInterest())
|
||||||
|
.expectedMaturityAmount(f.getExpectedMaturityAmount()).periodInterest(periodInterest).fdGlCode(code(f.getFdGlAccount()))
|
||||||
|
.fdGlName(name(f.getFdGlAccount())).interestGlCode(code(f.getInterestGlAccount())).interestGlName(name(f.getInterestGlAccount()))
|
||||||
|
.bankGlAccountId(f.getBankGlAccount().getId()).bankGlCode(code(f.getBankGlAccount())).bankGlName(name(f.getBankGlAccount()))
|
||||||
|
.status(f.getStatus()).remarks(f.getRemarks()).glTransactionId(f.getGlTransactionId()).voucherNo(f.getVoucherNo())
|
||||||
|
.renewedFromDealNo(f.getRenewedFrom() == null ? null : f.getRenewedFrom().getDealNo()).closedOnDate(format(f.getClosedOnDate()))
|
||||||
|
.closureType(f.getClosureType()).closureAmount(f.getClosureAmount()).closureInterest(f.getClosureInterest())
|
||||||
|
.closureTds(f.getClosureTds()).closurePenal(f.getClosurePenal()).closureVoucherNo(f.getClosureVoucherNo()).closureGlTransactionId(f.getClosureGlTransactionId()).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String code(final GLAccount gl) {
|
||||||
|
return gl == null ? null : gl.getGlCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String name(final GLAccount gl) {
|
||||||
|
return gl == null ? null : gl.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String format(final LocalDate date) {
|
||||||
|
return date == null ? null : date.format(DATE);
|
||||||
|
}
|
||||||
|
}
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/** The next interest voucher (or the maturity) of an FD that is due, or due within the reminder window. */
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class InvestmentFdDueData {
|
||||||
|
|
||||||
|
private final Long fdId;
|
||||||
|
private final String dealNo;
|
||||||
|
private final String partyName;
|
||||||
|
private final String certNo;
|
||||||
|
/** PROVISION, PAYOUT or MATURITY. */
|
||||||
|
private final String lineType;
|
||||||
|
private final String intDate;
|
||||||
|
/** What the bank credits (net of TDS); a provision line: what it books. */
|
||||||
|
private final BigDecimal amount;
|
||||||
|
private final BigDecimal grossInterest;
|
||||||
|
private final BigDecimal tds;
|
||||||
|
private final boolean postable;
|
||||||
|
private final boolean overdue;
|
||||||
|
}
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/** Where a new FD stands against the RBI limits on deposits with other banks. */
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class InvestmentFdLimitData {
|
||||||
|
|
||||||
|
/** The 31 March the limits are based on. */
|
||||||
|
private final String asOnDate;
|
||||||
|
/** Total deposits as on that date, as entered; empty until someone enters it. */
|
||||||
|
private final BigDecimal totalDeposits;
|
||||||
|
/** Owned funds (paid-up share capital + reserves) as on that date, the base of the 2% limit on co-operative shares. */
|
||||||
|
private final BigDecimal ownedFunds;
|
||||||
|
/** The same figure worked out from the deposit GLs, as a suggestion. */
|
||||||
|
private final BigDecimal glTotalDeposits;
|
||||||
|
private final BigDecimal grossLimit;
|
||||||
|
private final BigDecimal fdOutstanding;
|
||||||
|
private final BigDecimal bankBalances;
|
||||||
|
private final BigDecimal grossAfter;
|
||||||
|
private final BigDecimal bankLimit;
|
||||||
|
private final String partyName;
|
||||||
|
private final BigDecimal bankAfter;
|
||||||
|
private final BigDecimal newAmount;
|
||||||
|
private final boolean withinLimits;
|
||||||
|
private final String message;
|
||||||
|
}
|
||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/** Interest lines of an FD: 31 Mar / 30 Sep provisions, payouts of a half-yearly / yearly FD, and maturity. */
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class InvestmentFdScheduleData {
|
||||||
|
|
||||||
|
private final InvestmentFdData fd;
|
||||||
|
/** Provisioned interest not yet received (interest receivable of this FD). */
|
||||||
|
private final BigDecimal provisioned;
|
||||||
|
private final List<Row> rows;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public static class Row {
|
||||||
|
|
||||||
|
/** PROVISION, PAYOUT or MATURITY (shown for information; closed on the Maturity screen). */
|
||||||
|
private final String lineType;
|
||||||
|
private final String intDate;
|
||||||
|
private final String fromDate;
|
||||||
|
private final String toDate;
|
||||||
|
private final long days;
|
||||||
|
private final BigDecimal grossInterest;
|
||||||
|
/** Payout: the part already provisioned, cleared from interest receivable. */
|
||||||
|
private final BigDecimal alreadyBooked;
|
||||||
|
private final BigDecimal tds;
|
||||||
|
/** Provision: interest booked. Payout: interest income of the payout (gross less already booked). */
|
||||||
|
private final BigDecimal amount;
|
||||||
|
private final boolean posted;
|
||||||
|
private final String voucherNo;
|
||||||
|
private final String glTransactionId;
|
||||||
|
/** The next line to post, and its date has come. */
|
||||||
|
private final boolean postable;
|
||||||
|
}
|
||||||
|
}
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/** The next unposted interest line of a holding that is due, or due within the reminder window — a task to post. */
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class InvestmentInterestDueData {
|
||||||
|
|
||||||
|
private final Long purchaseId;
|
||||||
|
private final String dealNo;
|
||||||
|
private final String certNo;
|
||||||
|
private final String scriptCode;
|
||||||
|
private final String scriptDesc;
|
||||||
|
private final String lineType;
|
||||||
|
private final String intDate;
|
||||||
|
private final BigDecimal grossInterest;
|
||||||
|
private final BigDecimal amount;
|
||||||
|
/** The date has come: it can be posted now. */
|
||||||
|
private final boolean postable;
|
||||||
|
/** The date has passed without posting. */
|
||||||
|
private final boolean overdue;
|
||||||
|
}
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/** An interest line (coupon or closing provision) already posted to GL with its voucher — for the transaction log. */
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class InvestmentInterestPostingData {
|
||||||
|
|
||||||
|
private final Long id;
|
||||||
|
private final Long purchaseId;
|
||||||
|
private final String dealNo;
|
||||||
|
private final String certNo;
|
||||||
|
private final String bondType;
|
||||||
|
private final String scriptDesc;
|
||||||
|
private final String lineType;
|
||||||
|
private final String intDate;
|
||||||
|
private final String fromDate;
|
||||||
|
private final String toDate;
|
||||||
|
private final BigDecimal grossInterest;
|
||||||
|
private final BigDecimal alreadyBooked;
|
||||||
|
private final BigDecimal amount;
|
||||||
|
/** Debit side: the bank for a coupon, interest receivable for a provision. */
|
||||||
|
private final String drGlCode;
|
||||||
|
private final String drGlName;
|
||||||
|
private final String incomeGlCode;
|
||||||
|
private final String incomeGlName;
|
||||||
|
private final String glTransactionId;
|
||||||
|
private final String voucherNo;
|
||||||
|
private final String postedOnDate;
|
||||||
|
}
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Total Interest Received & Receivable: per Govt Security / bond deal and FD, the interest received in a period and
|
||||||
|
* the interest earned up to its last day that has not come in yet.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class InvestmentInterestReportData {
|
||||||
|
|
||||||
|
private final String fromDate;
|
||||||
|
private final String toDate;
|
||||||
|
private final BigDecimal totalReceived;
|
||||||
|
private final BigDecimal totalReceivable;
|
||||||
|
private final BigDecimal totalProvisioned;
|
||||||
|
private final BigDecimal total;
|
||||||
|
private final List<Row> rows;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public static class Row {
|
||||||
|
|
||||||
|
/** SECURITY or FD. */
|
||||||
|
private final String instrument;
|
||||||
|
private final Long refId;
|
||||||
|
private final String dealNo;
|
||||||
|
/** Script, or bank and certificate of an FD. */
|
||||||
|
private final String particulars;
|
||||||
|
private final String status;
|
||||||
|
/** Face value of the deal as bought, or the FD principal. */
|
||||||
|
private final BigDecimal principal;
|
||||||
|
private final BigDecimal rate;
|
||||||
|
/** Interest that came in during the period (coupons, FD payouts, interest paid at closure), gross of TDS. */
|
||||||
|
private final BigDecimal received;
|
||||||
|
/** Interest earned up to the To date and not yet received. */
|
||||||
|
private final BigDecimal receivable;
|
||||||
|
/** Part of the receivable already booked by 31 Mar / 30 Sep provisions (Interest Receivable 100603). */
|
||||||
|
private final BigDecimal provisioned;
|
||||||
|
private final BigDecimal total;
|
||||||
|
}
|
||||||
|
}
|
||||||
+91
@@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interest schedule of one holding: every half-yearly coupon and every 31 March / 30 September closing provision from
|
||||||
|
* purchase to maturity. Calculated, not yet posted.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class InvestmentInterestScheduleData {
|
||||||
|
|
||||||
|
private final Long purchaseId;
|
||||||
|
private final String dealNo;
|
||||||
|
private final String certNo;
|
||||||
|
private final String scriptCode;
|
||||||
|
private final String scriptDesc;
|
||||||
|
private final BigDecimal faceValue;
|
||||||
|
private final BigDecimal unitValue;
|
||||||
|
private final BigDecimal balanceUnits;
|
||||||
|
private final BigDecimal purchaseRate;
|
||||||
|
private final BigDecimal bookValue;
|
||||||
|
private final BigDecimal intRate;
|
||||||
|
private final String dealDate;
|
||||||
|
private final String lastIntDate;
|
||||||
|
private final String maturityDate;
|
||||||
|
/**
|
||||||
|
* Broken-period interest paid to the seller at purchase — booked as an expense (GL 500606), not in this schedule.
|
||||||
|
*/
|
||||||
|
private final BigDecimal purchasedAccruedInterest;
|
||||||
|
private final BigDecimal couponPerHalfYear;
|
||||||
|
private final String asOnDate;
|
||||||
|
private final String interestGlCode;
|
||||||
|
private final String interestGlName;
|
||||||
|
/** Bank the coupon is received into by default: the purchase's paying bank. */
|
||||||
|
private final Long bankGlAccountId;
|
||||||
|
private final String bankGlCode;
|
||||||
|
private final String bankGlName;
|
||||||
|
private final String status;
|
||||||
|
private final List<Row> rows;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public static class Row {
|
||||||
|
|
||||||
|
/** COUPON (cash received) or PROVISION (closing accrual). */
|
||||||
|
private final String type;
|
||||||
|
private final String intDate;
|
||||||
|
private final String fromDate;
|
||||||
|
private final String toDate;
|
||||||
|
private final int days;
|
||||||
|
/** Coupon received, or interest earned since the last coupon up to the closing. */
|
||||||
|
private final BigDecimal grossInterest;
|
||||||
|
/** Interest Receivable balance before this line (purchased accrued interest / earlier provision). */
|
||||||
|
private final BigDecimal alreadyBooked;
|
||||||
|
/** Provision to book now, or the part of the coupon that is new income. */
|
||||||
|
private final BigDecimal amount;
|
||||||
|
/** True when the date is on or before the as-on date. */
|
||||||
|
private final boolean due;
|
||||||
|
/** Posted to GL: its JV voucher and GL transaction. */
|
||||||
|
private final boolean posted;
|
||||||
|
private final String voucherNo;
|
||||||
|
private final String glTransactionId;
|
||||||
|
private final String postedOnDate;
|
||||||
|
/** The next line to post and its date has come. */
|
||||||
|
private final boolean postable;
|
||||||
|
/** Unposted and its date is within the reminder window (or past). */
|
||||||
|
private final boolean dueSoon;
|
||||||
|
}
|
||||||
|
}
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/** Where a new non-SLR purchase stands against the RBI limits on non-SLR and unlisted non-SLR investments. */
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class InvestmentNonSlrLimitData {
|
||||||
|
|
||||||
|
/** The 31 March the limits are based on. */
|
||||||
|
private final String asOnDate;
|
||||||
|
/** Total deposits as on that date, as entered for the FD limits; empty until someone enters it. */
|
||||||
|
private final BigDecimal totalDeposits;
|
||||||
|
/** 10% of total deposits. */
|
||||||
|
private final BigDecimal nonSlrLimit;
|
||||||
|
/** Book value of non-SLR investments held or pending settlement, before this deal. */
|
||||||
|
private final BigDecimal nonSlrOutstanding;
|
||||||
|
private final BigDecimal nonSlrAfter;
|
||||||
|
/** Book value of non-SLR investments held on that 31 March. */
|
||||||
|
private final BigDecimal nonSlrBase;
|
||||||
|
/** 10% of the non-SLR investments held on that 31 March. */
|
||||||
|
private final BigDecimal unlistedLimit;
|
||||||
|
private final BigDecimal unlistedOutstanding;
|
||||||
|
private final BigDecimal unlistedAfter;
|
||||||
|
private final BigDecimal newAmount;
|
||||||
|
/** Whether this deal's security is listed or proposed to be listed; unlisted ones also count against the unlisted limit. */
|
||||||
|
private final boolean listed;
|
||||||
|
private final boolean withinLimits;
|
||||||
|
private final String message;
|
||||||
|
}
|
||||||
+80
@@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, 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.InvestmentParty;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class InvestmentPartyData {
|
||||||
|
|
||||||
|
private final Long id;
|
||||||
|
private final String partyCode;
|
||||||
|
private final String bondType;
|
||||||
|
private final String partyName;
|
||||||
|
private final String address1;
|
||||||
|
private final String address2;
|
||||||
|
private final String address3;
|
||||||
|
private final String city;
|
||||||
|
private final String pincode;
|
||||||
|
private final String phone;
|
||||||
|
private final String fax;
|
||||||
|
private final String bankType;
|
||||||
|
private final Long glAccountId;
|
||||||
|
private final String glCode;
|
||||||
|
private final String glName;
|
||||||
|
private final Long interestGlAccountId;
|
||||||
|
private final String interestGlCode;
|
||||||
|
private final String interestGlName;
|
||||||
|
private final boolean tdsApplicable;
|
||||||
|
private final BigDecimal tdsRate;
|
||||||
|
private final String rbiCode;
|
||||||
|
private final BigDecimal limit;
|
||||||
|
private final String leiNo;
|
||||||
|
private final String cpBankName;
|
||||||
|
private final String cpBranchName;
|
||||||
|
private final String cpAccountNo;
|
||||||
|
private final String cpIfsc;
|
||||||
|
private final String institutionType;
|
||||||
|
private final BigDecimal subscribedCapital;
|
||||||
|
private final String lastBalanceSheetDate;
|
||||||
|
private final boolean inLiquidation;
|
||||||
|
private final boolean active;
|
||||||
|
|
||||||
|
public static InvestmentPartyData from(final InvestmentParty p) {
|
||||||
|
return InvestmentPartyData.builder().id(p.getId()).partyCode(p.getPartyCode()).bondType(p.getBondType())
|
||||||
|
.partyName(p.getPartyName()).address1(p.getAddress1()).address2(p.getAddress2()).address3(p.getAddress3())
|
||||||
|
.city(p.getCity()).pincode(p.getPincode()).phone(p.getPhone()).fax(p.getFax()).bankType(p.getBankType())
|
||||||
|
.glAccountId(p.getGlAccount().getId()).glCode(p.getGlAccount().getGlCode()).glName(p.getGlAccount().getName())
|
||||||
|
.interestGlAccountId(p.getInterestGlAccount() == null ? null : p.getInterestGlAccount().getId())
|
||||||
|
.interestGlCode(p.getInterestGlAccount() == null ? null : p.getInterestGlAccount().getGlCode())
|
||||||
|
.interestGlName(p.getInterestGlAccount() == null ? null : p.getInterestGlAccount().getName())
|
||||||
|
.tdsApplicable(p.isTdsApplicable()).tdsRate(p.getTdsRate()).rbiCode(p.getRbiCode()).limit(p.getExposureLimit())
|
||||||
|
.leiNo(p.getLeiNo()).cpBankName(p.getCpBankName())
|
||||||
|
.cpBranchName(p.getCpBranchName()).cpAccountNo(p.getCpAccountNo()).cpIfsc(p.getCpIfsc())
|
||||||
|
.institutionType(p.getInstitutionType()).subscribedCapital(p.getSubscribedCapital())
|
||||||
|
.lastBalanceSheetDate(p.getLastBalanceSheetDate() == null ? null
|
||||||
|
: p.getLastBalanceSheetDate().format(DateTimeFormatter.ofPattern("dd-MM-yyyy")))
|
||||||
|
.inLiquidation(p.isInLiquidation()).active(p.isActive()).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
+105
@@ -0,0 +1,105 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.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 confirmationMode;
|
||||||
|
private final String confirmationReceivedOn;
|
||||||
|
private final String confirmationRef;
|
||||||
|
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()).confirmationMode(p.getConfirmationMode())
|
||||||
|
.confirmationReceivedOn(format(p.getConfirmationReceivedOn())).confirmationRef(p.getConfirmationRef())
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+110
@@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, 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;
|
||||||
|
/** Deal slip number of the sale itself, e.g. 202627S00001 (empty for a write-off or amortisation). */
|
||||||
|
private final String saleNo;
|
||||||
|
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 brokerCode;
|
||||||
|
private final String confirmationMode;
|
||||||
|
private final String confirmationReceivedOn;
|
||||||
|
private final String confirmationRef;
|
||||||
|
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()).saleNo(s.getSaleNo())
|
||||||
|
.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()).brokerCode(s.getBrokerCode())
|
||||||
|
.confirmationMode(s.getConfirmationMode()).confirmationReceivedOn(format(s.getConfirmationReceivedOn()))
|
||||||
|
.confirmationRef(s.getConfirmationRef()).status(s.getStatus())
|
||||||
|
.glTransactionId(s.getGlTransactionId()).voucherNo(s.getVoucherNo()).settledOnDate(format(s.getSettledOnDate())).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BigDecimal zero(final BigDecimal v) {
|
||||||
|
return v == null ? BigDecimal.ZERO : v;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String format(final LocalDate date) {
|
||||||
|
return date == null ? null : date.format(DATE_FORMAT);
|
||||||
|
}
|
||||||
|
}
|
||||||
+76
@@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.apache.fineract.portfolio.investment.domain.InvestmentScript;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class InvestmentScriptData {
|
||||||
|
|
||||||
|
/** Dates go out as DD-MM-YYYY, the format the Investment screens use throughout. */
|
||||||
|
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy");
|
||||||
|
|
||||||
|
private final Long id;
|
||||||
|
private final String scriptCode;
|
||||||
|
private final String bondType;
|
||||||
|
private final String slrType;
|
||||||
|
private final String scriptDesc;
|
||||||
|
private final Long glAccountId;
|
||||||
|
private final String glCode;
|
||||||
|
private final String glName;
|
||||||
|
private final String isin;
|
||||||
|
private final BigDecimal faceValuePerUnit;
|
||||||
|
private final BigDecimal intRate;
|
||||||
|
private final String issueDate;
|
||||||
|
private final String maturityDate;
|
||||||
|
private final String companyName;
|
||||||
|
private final String address1;
|
||||||
|
private final String address2;
|
||||||
|
private final String address3;
|
||||||
|
private final String city;
|
||||||
|
private final String pincode;
|
||||||
|
private final String phone;
|
||||||
|
private final String fax;
|
||||||
|
private final String corpCode;
|
||||||
|
private final String rating;
|
||||||
|
private final String ratingAgency;
|
||||||
|
private final boolean listed;
|
||||||
|
private final boolean active;
|
||||||
|
|
||||||
|
public static InvestmentScriptData from(final InvestmentScript s) {
|
||||||
|
return InvestmentScriptData.builder().id(s.getId()).scriptCode(s.getScriptCode()).bondType(s.getBondType())
|
||||||
|
.slrType(s.getSlrType()).scriptDesc(s.getScriptDesc()).glAccountId(s.getGlAccount().getId())
|
||||||
|
.glCode(s.getGlAccount().getGlCode()).glName(s.getGlAccount().getName()).isin(s.getIsin())
|
||||||
|
.faceValuePerUnit(s.getFaceValuePerUnit()).intRate(s.getIntRate()).issueDate(format(s.getIssueDate()))
|
||||||
|
.maturityDate(format(s.getMaturityDate())).companyName(s.getCompanyName()).address1(s.getAddress1())
|
||||||
|
.address2(s.getAddress2()).address3(s.getAddress3()).city(s.getCity()).pincode(s.getPincode()).phone(s.getPhone())
|
||||||
|
.fax(s.getFax()).corpCode(s.getCorpCode()).rating(s.getRating()).ratingAgency(s.getRatingAgency()).listed(s.isListed())
|
||||||
|
.active(s.isActive()).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String format(final LocalDate date) {
|
||||||
|
return date == null ? null : date.format(DATE_FORMAT);
|
||||||
|
}
|
||||||
|
}
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Where a share purchase stands against the RBI limits: shares of co-operative societies within 2% of owned funds, and
|
||||||
|
* within 5% of one society's subscribed capital. Membership shares (DCCB, State Co-op Bank, NPCI / CCIL / SWIFT, Umbrella
|
||||||
|
* Organisation, IDPIC) may exceed them.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class InvestmentShareLimitData {
|
||||||
|
|
||||||
|
/** The 31 March the owned funds are taken from. */
|
||||||
|
private final String asOnDate;
|
||||||
|
private final String institutionType;
|
||||||
|
/** True for membership shares, which the limits do not bind. */
|
||||||
|
private final boolean exempt;
|
||||||
|
private final BigDecimal ownedFunds;
|
||||||
|
private final BigDecimal coopLimit;
|
||||||
|
private final BigDecimal coopOutstanding;
|
||||||
|
private final BigDecimal coopAfter;
|
||||||
|
private final String partyName;
|
||||||
|
private final BigDecimal subscribedCapital;
|
||||||
|
private final BigDecimal institutionLimit;
|
||||||
|
private final BigDecimal institutionOutstanding;
|
||||||
|
private final BigDecimal institutionAfter;
|
||||||
|
private final BigDecimal newAmount;
|
||||||
|
private final boolean withinLimits;
|
||||||
|
private final String message;
|
||||||
|
}
|
||||||
+88
@@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A market valuation (MTM) of AFS or HFT holdings: a preview before posting (no id) or a posted run. Each security is
|
||||||
|
* valued at the Daily Script Master rate; the IDR needed per class is its net loss, a net gain is ignored.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class InvestmentValuationData {
|
||||||
|
|
||||||
|
private final Long id;
|
||||||
|
private final String valuationDate;
|
||||||
|
private final String category;
|
||||||
|
private final BigDecimal requiredTotal;
|
||||||
|
/** IDR held from the category's previous run. */
|
||||||
|
private final BigDecimal previousTotal;
|
||||||
|
/** Posted (or to be posted): more provision when positive, a write-back when negative. */
|
||||||
|
private final BigDecimal delta;
|
||||||
|
private final String provisionGlCode;
|
||||||
|
private final String reserveGlCode;
|
||||||
|
private final String glTransactionId;
|
||||||
|
private final String voucherNo;
|
||||||
|
/** Date of the category's last run, if any; a new run must be dated after it. */
|
||||||
|
private final String lastRunDate;
|
||||||
|
/** Scripts held with no rate on or before the valuation date: the run is refused until they are entered. */
|
||||||
|
private final List<String> missingRates;
|
||||||
|
/** A preview can be posted: no rate missing, dated after the last run, and something to value or to write back. */
|
||||||
|
private final boolean postable;
|
||||||
|
private final List<ClassTotal> classes;
|
||||||
|
private final List<Line> lines;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public static class ClassTotal {
|
||||||
|
|
||||||
|
/** GOVT_SECURITY, CORPORATE_BOND or MUTUAL_FUND. */
|
||||||
|
private final String assetClass;
|
||||||
|
private final BigDecimal bookValue;
|
||||||
|
private final BigDecimal marketValue;
|
||||||
|
private final BigDecimal netDifference;
|
||||||
|
private final BigDecimal required;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public static class Line {
|
||||||
|
|
||||||
|
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 assetClass;
|
||||||
|
private final BigDecimal units;
|
||||||
|
private final BigDecimal bookValue;
|
||||||
|
private final BigDecimal rate;
|
||||||
|
private final String rateDate;
|
||||||
|
private final BigDecimal marketValue;
|
||||||
|
private final BigDecimal difference;
|
||||||
|
/** How a share was valued; empty for a holding valued at its market rate. */
|
||||||
|
private final String valuationBasis;
|
||||||
|
private final boolean npi;
|
||||||
|
}
|
||||||
|
}
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Daily Script Master: the market rate of a script for one day, as the bank's existing CBS keeps it. Purchase Entry
|
||||||
|
* takes its rate from here (Jitendra Sir). Close rate is mandatory; open, high and low are optional.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_investment_daily_rate")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class InvestmentDailyRate extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "script_id", nullable = false)
|
||||||
|
private InvestmentScript script;
|
||||||
|
|
||||||
|
@Column(name = "rate_date", nullable = false)
|
||||||
|
private LocalDate rateDate;
|
||||||
|
|
||||||
|
@Column(name = "open_rate", scale = 6, precision = 19)
|
||||||
|
private BigDecimal openRate;
|
||||||
|
|
||||||
|
@Column(name = "high_rate", scale = 6, precision = 19)
|
||||||
|
private BigDecimal highRate;
|
||||||
|
|
||||||
|
@Column(name = "low_rate", scale = 6, precision = 19)
|
||||||
|
private BigDecimal lowRate;
|
||||||
|
|
||||||
|
@Column(name = "close_rate", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal closeRate;
|
||||||
|
}
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.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);
|
||||||
|
}
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||||
|
|
||||||
|
/** The bank's total deposits as on a 31 March: the base for the RBI limits on deposits placed with other banks. */
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_investment_deposit_base")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class InvestmentDepositBase extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||||
|
|
||||||
|
@Column(name = "as_on_date", nullable = false, unique = true)
|
||||||
|
private LocalDate asOnDate;
|
||||||
|
|
||||||
|
@Column(name = "total_deposits", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal totalDeposits;
|
||||||
|
|
||||||
|
/** Owned funds (paid-up share capital + reserves) on the same date: shares of co-operative societies are limited to 2% of it. */
|
||||||
|
@Column(name = "owned_funds", scale = 6, precision = 19)
|
||||||
|
private BigDecimal ownedFunds;
|
||||||
|
|
||||||
|
@Column(name = "remarks", length = 500)
|
||||||
|
private String remarks;
|
||||||
|
}
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.domain;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface InvestmentDepositBaseRepository extends JpaRepository<InvestmentDepositBase, Long> {
|
||||||
|
|
||||||
|
Optional<InvestmentDepositBase> findByAsOnDate(LocalDate asOnDate);
|
||||||
|
}
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, 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 dividend received on a share holding: booked as income when it is received (Dr Bank / Cr Dividend on Investments). */
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_investment_dividend")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class InvestmentDividend extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "purchase_id", nullable = false)
|
||||||
|
private InvestmentPurchase purchase;
|
||||||
|
|
||||||
|
@Column(name = "dividend_date", nullable = false)
|
||||||
|
private LocalDate dividendDate;
|
||||||
|
|
||||||
|
@Column(name = "amount", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal amount;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "bank_gl_account_id", nullable = false)
|
||||||
|
private GLAccount bankGlAccount;
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, 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 org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface InvestmentDividendRepository extends JpaRepository<InvestmentDividend, Long> {
|
||||||
|
|
||||||
|
List<InvestmentDividend> findAllByOrderByDividendDateDescIdDesc();
|
||||||
|
|
||||||
|
/** Whether the institution has paid us any dividend on or before a date (on any of our holdings of its shares). */
|
||||||
|
boolean existsByPurchasePartyIdAndDividendDateLessThanEqual(Long partyId, LocalDate date);
|
||||||
|
}
|
||||||
+135
@@ -0,0 +1,135 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||||
|
|
||||||
|
/** A fixed deposit the bank has placed with another bank, from placement to maturity, premature closure or renewal. */
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_investment_fd")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class InvestmentFd extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||||
|
|
||||||
|
@Column(name = "deal_no", nullable = false, length = 20, unique = true)
|
||||||
|
private String dealNo;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "party_id", nullable = false)
|
||||||
|
private InvestmentParty party;
|
||||||
|
|
||||||
|
/** The bank's FD GL (from the party), debited when the money is placed. */
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "fd_gl_account_id", nullable = false)
|
||||||
|
private GLAccount fdGlAccount;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "interest_gl_account_id", nullable = false)
|
||||||
|
private GLAccount interestGlAccount;
|
||||||
|
|
||||||
|
/** Certificate (receipt) number issued by the other bank. */
|
||||||
|
@Column(name = "cert_no", nullable = false, length = 50)
|
||||||
|
private String certNo;
|
||||||
|
|
||||||
|
@Column(name = "cert_date")
|
||||||
|
private LocalDate certDate;
|
||||||
|
|
||||||
|
@Column(name = "deal_date", nullable = false)
|
||||||
|
private LocalDate dealDate;
|
||||||
|
|
||||||
|
@Column(name = "principal", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal principal;
|
||||||
|
|
||||||
|
@Column(name = "int_rate", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal intRate;
|
||||||
|
|
||||||
|
@Column(name = "maturity_date", nullable = false)
|
||||||
|
private LocalDate maturityDate;
|
||||||
|
|
||||||
|
@Column(name = "payout", nullable = false, length = 20)
|
||||||
|
private String payout;
|
||||||
|
|
||||||
|
@Column(name = "calc_type", nullable = false, length = 20)
|
||||||
|
private String calcType;
|
||||||
|
|
||||||
|
@Column(name = "expected_interest", scale = 6, precision = 19)
|
||||||
|
private BigDecimal expectedInterest;
|
||||||
|
|
||||||
|
@Column(name = "expected_maturity_amount", scale = 6, precision = 19)
|
||||||
|
private BigDecimal expectedMaturityAmount;
|
||||||
|
|
||||||
|
/** Our bank account the money was paid from. */
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "bank_gl_account_id", nullable = false)
|
||||||
|
private GLAccount bankGlAccount;
|
||||||
|
|
||||||
|
/** The FD this one renews, if any. */
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "renewed_from_id")
|
||||||
|
private InvestmentFd renewedFrom;
|
||||||
|
|
||||||
|
@Column(name = "status", nullable = false, length = 20)
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
@Column(name = "remarks", length = 500)
|
||||||
|
private String remarks;
|
||||||
|
|
||||||
|
@Column(name = "gl_transaction_id", length = 50)
|
||||||
|
private String glTransactionId;
|
||||||
|
|
||||||
|
@Column(name = "voucher_no", length = 30)
|
||||||
|
private String voucherNo;
|
||||||
|
|
||||||
|
@Column(name = "closed_on_date")
|
||||||
|
private LocalDate closedOnDate;
|
||||||
|
|
||||||
|
/** MATURITY, PREMATURE or RENEWAL. */
|
||||||
|
@Column(name = "closure_type", length = 20)
|
||||||
|
private String closureType;
|
||||||
|
|
||||||
|
@Column(name = "closure_amount", scale = 6, precision = 19)
|
||||||
|
private BigDecimal closureAmount;
|
||||||
|
|
||||||
|
@Column(name = "closure_interest", scale = 6, precision = 19)
|
||||||
|
private BigDecimal closureInterest;
|
||||||
|
|
||||||
|
@Column(name = "closure_tds", scale = 6, precision = 19)
|
||||||
|
private BigDecimal closureTds;
|
||||||
|
|
||||||
|
@Column(name = "closure_penal", scale = 6, precision = 19)
|
||||||
|
private BigDecimal closurePenal;
|
||||||
|
|
||||||
|
@Column(name = "closure_gl_transaction_id", length = 50)
|
||||||
|
private String closureGlTransactionId;
|
||||||
|
|
||||||
|
@Column(name = "closure_voucher_no", length = 30)
|
||||||
|
private String closureVoucherNo;
|
||||||
|
}
|
||||||
+87
@@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||||
|
|
||||||
|
/** A posted interest line of an FD: a 31 Mar / 30 Sep provision, or a half-yearly / yearly payout. */
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_investment_fd_interest")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class InvestmentFdInterest extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "fd_id", nullable = false)
|
||||||
|
private InvestmentFd fd;
|
||||||
|
|
||||||
|
/** PROVISION or PAYOUT. */
|
||||||
|
@Column(name = "line_type", nullable = false, length = 20)
|
||||||
|
private String lineType;
|
||||||
|
|
||||||
|
@Column(name = "int_date", nullable = false)
|
||||||
|
private LocalDate intDate;
|
||||||
|
|
||||||
|
@Column(name = "from_date")
|
||||||
|
private LocalDate fromDate;
|
||||||
|
|
||||||
|
@Column(name = "to_date")
|
||||||
|
private LocalDate toDate;
|
||||||
|
|
||||||
|
@Column(name = "days")
|
||||||
|
private Integer days;
|
||||||
|
|
||||||
|
@Column(name = "gross_interest", scale = 6, precision = 19)
|
||||||
|
private BigDecimal grossInterest;
|
||||||
|
|
||||||
|
/** For a payout: the part already provisioned, cleared from interest receivable. */
|
||||||
|
@Column(name = "already_booked", scale = 6, precision = 19)
|
||||||
|
private BigDecimal alreadyBooked;
|
||||||
|
|
||||||
|
@Column(name = "tds", scale = 6, precision = 19)
|
||||||
|
private BigDecimal tds;
|
||||||
|
|
||||||
|
@Column(name = "amount", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal amount;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "bank_gl_account_id")
|
||||||
|
private GLAccount bankGlAccount;
|
||||||
|
|
||||||
|
@Column(name = "gl_transaction_id", length = 50)
|
||||||
|
private String glTransactionId;
|
||||||
|
|
||||||
|
@Column(name = "voucher_no", length = 30)
|
||||||
|
private String voucherNo;
|
||||||
|
|
||||||
|
@Column(name = "posted_on_date")
|
||||||
|
private LocalDate postedOnDate;
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.domain;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface InvestmentFdInterestRepository extends JpaRepository<InvestmentFdInterest, Long> {
|
||||||
|
|
||||||
|
List<InvestmentFdInterest> findByFdIdOrderByIntDateAscIdAsc(Long fdId);
|
||||||
|
}
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.LockModeType;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Lock;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
|
||||||
|
public interface InvestmentFdRepository extends JpaRepository<InvestmentFd, Long> {
|
||||||
|
|
||||||
|
@Lock(value = LockModeType.PESSIMISTIC_WRITE)
|
||||||
|
@Query("select f from InvestmentFd f where f.id = :fdId")
|
||||||
|
Optional<InvestmentFd> findByIdWithLock(@Param("fdId") Long fdId);
|
||||||
|
|
||||||
|
List<InvestmentFd> findByStatusOrderByIdAsc(String status);
|
||||||
|
|
||||||
|
boolean existsByPartyIdAndCertNoIgnoreCase(Long partyId, String certNo);
|
||||||
|
}
|
||||||
+84
@@ -0,0 +1,84 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.investment.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||||
|
|
||||||
|
/** One interest schedule line of a holding (coupon received or closing provision) that has been posted to GL. */
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_investment_interest_posting")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class InvestmentInterestPosting extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "purchase_id", nullable = false)
|
||||||
|
private InvestmentPurchase purchase;
|
||||||
|
|
||||||
|
/** COUPON or PROVISION. */
|
||||||
|
@Column(name = "line_type", nullable = false, length = 20)
|
||||||
|
private String lineType;
|
||||||
|
|
||||||
|
@Column(name = "int_date", nullable = false)
|
||||||
|
private LocalDate intDate;
|
||||||
|
|
||||||
|
@Column(name = "from_date")
|
||||||
|
private LocalDate fromDate;
|
||||||
|
|
||||||
|
@Column(name = "to_date")
|
||||||
|
private LocalDate toDate;
|
||||||
|
|
||||||
|
@Column(name = "days")
|
||||||
|
private Integer days;
|
||||||
|
|
||||||
|
@Column(name = "gross_interest", scale = 6, precision = 19)
|
||||||
|
private BigDecimal grossInterest;
|
||||||
|
|
||||||
|
@Column(name = "already_booked", scale = 6, precision = 19)
|
||||||
|
private BigDecimal alreadyBooked;
|
||||||
|
|
||||||
|
@Column(name = "amount", scale = 6, precision = 19, nullable = false)
|
||||||
|
private BigDecimal amount;
|
||||||
|
|
||||||
|
/** Bank the coupon was received into (coupon lines only). */
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "bank_gl_account_id")
|
||||||
|
private GLAccount bankGlAccount;
|
||||||
|
|
||||||
|
@Column(name = "gl_transaction_id", length = 50)
|
||||||
|
private String glTransactionId;
|
||||||
|
|
||||||
|
@Column(name = "voucher_no", length = 30)
|
||||||
|
private String voucherNo;
|
||||||
|
|
||||||
|
@Column(name = "posted_on_date")
|
||||||
|
private LocalDate postedOnDate;
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, 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);
|
||||||
|
}
|
||||||
+135
@@ -0,0 +1,135 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bank / Party Master — the counterparty an investment is bought from or placed with (bank, RBI, issuer, AMC, broker),
|
||||||
|
* following the bank's existing CBS Bank / Party Master.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_investment_party")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class InvestmentParty extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||||
|
|
||||||
|
@Column(name = "party_code", nullable = false, length = 20, unique = true)
|
||||||
|
private String partyCode;
|
||||||
|
|
||||||
|
@Column(name = "bond_type", nullable = false, length = 20)
|
||||||
|
private String bondType;
|
||||||
|
|
||||||
|
@Column(name = "party_name", nullable = false, length = 150)
|
||||||
|
private String partyName;
|
||||||
|
|
||||||
|
@Column(name = "address1", length = 150)
|
||||||
|
private String address1;
|
||||||
|
|
||||||
|
@Column(name = "address2", length = 150)
|
||||||
|
private String address2;
|
||||||
|
|
||||||
|
@Column(name = "address3", length = 150)
|
||||||
|
private String address3;
|
||||||
|
|
||||||
|
@Column(name = "city", length = 100)
|
||||||
|
private String city;
|
||||||
|
|
||||||
|
@Column(name = "pincode", length = 10)
|
||||||
|
private String pincode;
|
||||||
|
|
||||||
|
@Column(name = "phone", length = 20)
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
@Column(name = "fax", length = 20)
|
||||||
|
private String fax;
|
||||||
|
|
||||||
|
@Column(name = "bank_type", length = 60)
|
||||||
|
private String bankType;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "gl_account_id", nullable = false)
|
||||||
|
private GLAccount glAccount;
|
||||||
|
|
||||||
|
/** Interest income GL of the FDs placed with this bank (Bond Type FD only). */
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "interest_gl_account_id")
|
||||||
|
private GLAccount interestGlAccount;
|
||||||
|
|
||||||
|
/** Whether this bank deducts TDS from the interest on FDs placed with it (some banks do, some do not). */
|
||||||
|
@Column(name = "tds_applicable", nullable = false)
|
||||||
|
private boolean tdsApplicable;
|
||||||
|
|
||||||
|
/** TDS rate in per cent, set only when the bank deducts TDS. */
|
||||||
|
@Column(name = "tds_rate", scale = 4, precision = 9)
|
||||||
|
private BigDecimal tdsRate;
|
||||||
|
|
||||||
|
@Column(name = "rbi_code", length = 30)
|
||||||
|
private String rbiCode;
|
||||||
|
|
||||||
|
@Column(name = "exposure_limit", scale = 6, precision = 19)
|
||||||
|
private BigDecimal exposureLimit;
|
||||||
|
|
||||||
|
@Column(name = "lei_no", length = 30)
|
||||||
|
private String leiNo;
|
||||||
|
|
||||||
|
@Column(name = "cp_bank_name", length = 150)
|
||||||
|
private String cpBankName;
|
||||||
|
|
||||||
|
@Column(name = "cp_branch_name", length = 150)
|
||||||
|
private String cpBranchName;
|
||||||
|
|
||||||
|
@Column(name = "cp_account_no", length = 40)
|
||||||
|
private String cpAccountNo;
|
||||||
|
|
||||||
|
@Column(name = "cp_ifsc", length = 11)
|
||||||
|
private String cpIfsc;
|
||||||
|
|
||||||
|
/** Share institutions only: DCCB, State Co-op Bank, other co-operative society, or MIC / Umbrella Organisation. */
|
||||||
|
@Column(name = "institution_type", length = 30)
|
||||||
|
private String institutionType;
|
||||||
|
|
||||||
|
/** The institution's subscribed share capital: our holding in a co-operative society is limited to 5% of it. */
|
||||||
|
@Column(name = "subscribed_capital", scale = 6, precision = 19)
|
||||||
|
private BigDecimal subscribedCapital;
|
||||||
|
|
||||||
|
/** Date of the institution's latest balance sheet we have; one older than 18 months values its shares at ₹1. */
|
||||||
|
@Column(name = "last_balance_sheet_date")
|
||||||
|
private LocalDate lastBalanceSheetDate;
|
||||||
|
|
||||||
|
/** An institution in liquidation: its shares are fully provided for. */
|
||||||
|
@Column(name = "is_in_liquidation", nullable = false)
|
||||||
|
private boolean inLiquidation;
|
||||||
|
|
||||||
|
/** An inactive party is kept for history but not offered for new deals. */
|
||||||
|
@Column(name = "is_active", nullable = false)
|
||||||
|
private boolean active = true;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user