feat(dds): DDS product accounting tab; DDS posts only through product GLs
- DDS product names its own Customer Deposits, Interest Expense and Agent Commission Expense GLs (validated by kind), shown on the product view and in the approval review with old -> new on edits - DdsLedger posts every DDS movement through the party's product GLs; commission is shared over the products an agent collected for - dds-* settings removed from Global Configurations (0620); scheme accounts recognised by the DDS party linked to them, not a product id - 0621 drops the agent cash, matured payable and current control GL columns, which nothing posts to - Approval preview of interest/commission runs shows the product GLs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+10
@@ -55,6 +55,16 @@ 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;
|
||||||
}
|
}
|
||||||
|
|||||||
+38
@@ -115,6 +115,24 @@ 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() {
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
@@ -131,6 +149,9 @@ public class DdsAccountType extends AbstractAuditableCustom {
|
|||||||
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");
|
||||||
}
|
}
|
||||||
@@ -238,6 +259,23 @@ 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);
|
||||||
|
|||||||
+9
-6
@@ -303,9 +303,13 @@ 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. */
|
/**
|
||||||
private static final String DDS_DEPOSIT_PRODUCT_SQL = "select value from c_configuration "
|
* Whether a savings account is a DDS customer's scheme account - one a DDS party is linked to.
|
||||||
+ "where name = 'dds-deposit-product' and enabled = true";
|
* Read off the parties themselves rather than a configured product id: what makes an account a
|
||||||
|
* 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;
|
||||||
|
|
||||||
@@ -353,9 +357,8 @@ 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 Long productId = ((Number) account.get("product_id")).longValue();
|
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 AgentSavingsAccountNotOperativeException(accountNo, "DDS scheme");
|
throw new AgentSavingsAccountNotOperativeException(accountNo, "DDS scheme");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+168
-103
@@ -19,9 +19,13 @@
|
|||||||
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;
|
||||||
@@ -83,35 +87,16 @@ 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 - DDS Customer Deposits, a savings or deposit product's control
|
* sum of customer or agent accounts - a DDS product's customer deposits account, a savings or
|
||||||
* account, a current-account control, an agent's own cash account - since parked money in any of
|
* deposit product's control account, 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\" "
|
||||||
@@ -120,8 +105,7 @@ 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 c.value from c_configuration c where c.name in ('dds-deposit-liability-account', "
|
+ "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) "
|
||||||
+ "'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";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -164,7 +148,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, configured(DEPOSIT_LIABILITY).getId(),
|
DdsVoucherScope.addLine(new DdsVoucherScope.Line(entryType, depositLiabilityOf(party).getId(),
|
||||||
party.getPartyCode() + " · " + party.getPartyName(), party.getAccountNo(), amount, description));
|
party.getPartyCode() + " · " + party.getPartyName(), party.getAccountNo(), amount, description));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,17 +167,6 @@ 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.
|
||||||
*
|
*
|
||||||
@@ -207,11 +180,13 @@ 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 String transactionId, final String description) {
|
final GLAccount depositAccount, final String transactionId, final String description) {
|
||||||
writePair(office, organisationCurrency(), transactionId, date, amount, receiptAccount, configured(DEPOSIT_LIABILITY), description,
|
writePair(office, organisationCurrency(), transactionId, date, amount, receiptAccount, depositAccount, description, null, null);
|
||||||
null, null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -242,7 +217,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), configured(DEPOSIT_LIABILITY), description);
|
ddsPair(party, date, amount, agentCashOr(agentCashAccount), depositLiabilityOf(party), 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. */
|
||||||
@@ -251,7 +226,7 @@ public class DdsLedger {
|
|||||||
postInterest(party.getSavingsAccountId(), date, amount, description);
|
postInterest(party.getSavingsAccountId(), date, amount, description);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ddsPair(party, date, amount, configured(INTEREST_EXPENSE), configured(DEPOSIT_LIABILITY), description);
|
ddsPair(party, date, amount, interestExpenseOf(party), depositLiabilityOf(party), 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)}. */
|
||||||
@@ -260,7 +235,7 @@ public class DdsLedger {
|
|||||||
reverseInterest(party.getSavingsAccountId(), date, amount, description);
|
reverseInterest(party.getSavingsAccountId(), date, amount, description);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ddsPair(party, date, amount, configured(DEPOSIT_LIABILITY), configured(INTEREST_EXPENSE), description);
|
ddsPair(party, date, amount, depositLiabilityOf(party), interestExpenseOf(party), description);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -275,7 +250,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, configured(DEPOSIT_LIABILITY), controlAccountOf(to), description);
|
postPair(to, deposit, date, amount, depositLiabilityOf(party), controlAccountOf(to), description);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -305,7 +280,7 @@ public class DdsLedger {
|
|||||||
owed = controlAccountOf(account);
|
owed = controlAccountOf(account);
|
||||||
office = account.office();
|
office = account.office();
|
||||||
} else {
|
} else {
|
||||||
owed = configured(DEPOSIT_LIABILITY);
|
owed = depositLiabilityOf(party);
|
||||||
office = party.getOffice() != null ? party.getOffice() : party.getAgent().getOffice();
|
office = party.getOffice() != null ? party.getOffice() : party.getAgent().getOffice();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,9 +304,6 @@ 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
|
||||||
@@ -348,23 +320,16 @@ 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
|
||||||
: "dds-current-account-control-account (the current product's GL mapping is shared with a savings product of the same id)");
|
: "a single control account for current account " + currentAccountId
|
||||||
|
+ " (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. */
|
||||||
@@ -374,7 +339,7 @@ public class DdsLedger {
|
|||||||
payOutToPayable(party.getSavingsAccountId(), date, amount, description, parkIn);
|
payOutToPayable(party.getSavingsAccountId(), date, amount, description, parkIn);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ddsPair(party, date, amount, configured(DEPOSIT_LIABILITY), parkIn, description);
|
ddsPair(party, date, amount, depositLiabilityOf(party), 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. */
|
||||||
@@ -423,15 +388,22 @@ public class DdsLedger {
|
|||||||
this.savingsAccountAssembler.getPivotConfigStatus());
|
this.savingsAccountAssembler.getPivotConfigStatus());
|
||||||
relockDebits(account, wasBlocked);
|
relockDebits(account, wasBlocked);
|
||||||
postPair(account.office(), account.getCurrency().getCode(), date, amount, controlAccountOf(account),
|
postPair(account.office(), account.getCurrency().getCode(), date, amount, controlAccountOf(account),
|
||||||
configured(DEPOSIT_LIABILITY), description, null, account.getId());
|
depositLiabilityOf(from), 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, configured(DEPOSIT_LIABILITY), controlAccountOf(account), description);
|
postPair(account, deposit, date, amount, depositLiabilityOf(to), controlAccountOf(account), description);
|
||||||
} else {
|
} else {
|
||||||
// Both on DDS Customer Deposits: nothing to book, but the slip still names who paid whom.
|
final GLAccount fromDeposits = depositLiabilityOf(from);
|
||||||
voucherSideOnDeposits("DR", from, amount, description);
|
final GLAccount toDeposits = depositLiabilityOf(to);
|
||||||
voucherSideOnDeposits("CR", to, amount, description);
|
if (fromDeposits.getId().equals(toDeposits.getId())) {
|
||||||
|
// Both owed on the same account: nothing to book, but the slip still names who paid whom.
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,17 +416,133 @@ public class DdsLedger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Which account plays the agent's part on an entry: the agent's own, or the scheme's shared one
|
* The account playing the agent's part on an entry - the agent's own. An agent cannot be saved
|
||||||
* when they have none.
|
* without one, so a missing one here is an agent set up before that was required.
|
||||||
*
|
|
||||||
* 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) {
|
||||||
return agentCashAccount != null ? agentCashAccount : configured(AGENT_CASH);
|
if (agentCashAccount == null) {
|
||||||
|
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()));
|
||||||
|
}
|
||||||
|
final List<Long> ids = this.jdbcTemplate.queryForList("select " + column + " from m_dds_account_type " // NOSONAR
|
||||||
|
+ "where account_type_code = ?", 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 %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_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.
|
||||||
|
*
|
||||||
|
* 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) {
|
||||||
|
List<Map<String, Object>> basis = this.jdbcTemplate.queryForList(String.format(COMMISSION_BASIS_SQL, UNPAID_COMMISSION_FILTER), // NOSONAR
|
||||||
|
agentId);
|
||||||
|
if (basis.isEmpty()) {
|
||||||
|
basis = this.jdbcTemplate.queryForList(String.format(COMMISSION_BASIS_SQL, ""), agentId); // NOSONAR
|
||||||
|
}
|
||||||
|
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_EVEN);
|
||||||
|
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); // NOSONAR
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -592,12 +680,18 @@ 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 String description, final Map<GLAccount, BigDecimal> shares) {
|
||||||
final SavingsAccount account = load(savingsAccountId);
|
final SavingsAccount account = load(savingsAccountId);
|
||||||
final SavingsAccountTransaction deposit = depositWithoutAccounting(account, date, amount);
|
final SavingsAccountTransaction deposit = depositWithoutAccounting(account, date, amount);
|
||||||
postPair(account, deposit, date, amount, configured(COMMISSION_EXPENSE), controlAccountOf(account), description);
|
final GLAccount control = controlAccountOf(account);
|
||||||
|
for (final Map.Entry<GLAccount, BigDecimal> share : shares.entrySet()) {
|
||||||
|
postPair(account, deposit, date, share.getValue(), share.getKey(), control, description);
|
||||||
|
}
|
||||||
return deposit;
|
return deposit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -683,31 +777,10 @@ public class DdsLedger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A ledger account named by a setting, so a bank can point it at one of their own. */
|
|
||||||
private GLAccount configured(final String settingName) {
|
|
||||||
final List<Long> ids = this.jdbcTemplate.queryForList(CONFIGURED_ACCOUNT_SQL, Long.class, settingName); // NOSONAR
|
|
||||||
if (ids.isEmpty() || ids.get(0) == null) {
|
|
||||||
throw new DdsLedgerNotConfiguredException(settingName);
|
|
||||||
}
|
|
||||||
return this.glAccountRepository.findById(ids.get(0)).orElseThrow(() -> new DdsLedgerNotConfiguredException(settingName));
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
@@ -717,14 +790,6 @@ 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) {
|
||||||
|
|||||||
+10
-7
@@ -22,6 +22,7 @@ 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;
|
||||||
@@ -356,7 +357,7 @@ public class DdsVoucherService {
|
|||||||
return new ArrayList<>();
|
return new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
final Long depositGlId = depositLiabilityGlId();
|
final Set<Long> depositGlIds = depositLiabilityGlIds();
|
||||||
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())) {
|
||||||
@@ -383,7 +384,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() && glId.equals(depositGlId)) {
|
if (partyLeg.matches() && depositGlIds.contains(glId)) {
|
||||||
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
|
||||||
@@ -417,11 +418,13 @@ 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. */
|
/**
|
||||||
private Long depositLiabilityGlId() {
|
* The GLs that carry what DDS customers are owed - a leg on one is the customer's own. Each DDS
|
||||||
final List<Long> ids = this.jdbcTemplate.queryForList( // NOSONAR
|
* product names its own on its Accounting tab (0620).
|
||||||
"select value from c_configuration where name = 'dds-deposit-liability-account' and enabled = true", Long.class);
|
*/
|
||||||
return ids.isEmpty() ? null : ids.get(0);
|
private Set<Long> depositLiabilityGlIds() {
|
||||||
|
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 stamp(final String sql, final String voucherNo, final Collection<Long> ids) {
|
private void stamp(final String sql, final String voucherNo, final Collection<Long> ids) {
|
||||||
|
|||||||
+17
@@ -47,6 +47,7 @@ public final class DdsAccountTypeCommandFromApiJsonDeserializer {
|
|||||||
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. */
|
||||||
@@ -71,6 +72,14 @@ 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);
|
||||||
@@ -104,6 +113,14 @@ 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);
|
||||||
|
|||||||
+20
-2
@@ -50,8 +50,17 @@ public class DdsAccountTypeReadPlatformServiceImpl implements DdsAccountTypeRead
|
|||||||
+ " 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, "
|
||||||
+ " from m_dds_account_type t";
|
+ " t.deposit_liability_gl_id as depositLiabilityGlId, dl.gl_code as depositLiabilityGlCode, "
|
||||||
|
+ " 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
|
||||||
@@ -69,6 +78,15 @@ public class DdsAccountTypeReadPlatformServiceImpl implements DdsAccountTypeRead
|
|||||||
.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();
|
||||||
|
|||||||
+56
@@ -19,6 +19,7 @@
|
|||||||
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;
|
||||||
@@ -35,7 +36,10 @@ 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;
|
||||||
|
|
||||||
@@ -46,6 +50,15 @@ 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
|
||||||
@@ -53,6 +66,7 @@ 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);
|
||||||
@@ -77,6 +91,7 @@ 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,6 +130,47 @@ 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",
|
||||||
|
|||||||
+3
-2
@@ -43,7 +43,8 @@ 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) {
|
PartyRepository partyRepository, JdbcTemplate jdbcTemplate) {
|
||||||
return new DdsAccountTypeWritePlatformServiceJpaRepositoryImpl(fromApiJsonDeserializer, ddsAccountTypeRepository, partyRepository);
|
return new DdsAccountTypeWritePlatformServiceJpaRepositoryImpl(fromApiJsonDeserializer, ddsAccountTypeRepository, partyRepository,
|
||||||
|
jdbcTemplate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+55
-10
@@ -30,6 +30,8 @@ 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;
|
||||||
|
|
||||||
@@ -64,6 +66,7 @@ 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 = ? ");
|
||||||
@@ -408,6 +411,7 @@ public class DdsApprovalReadService {
|
|||||||
private static final String GROUP_PRODUCT = "Product";
|
private static final String GROUP_PRODUCT = "Product";
|
||||||
private static final String GROUP_INTEREST = "Interest";
|
private static final String GROUP_INTEREST = "Interest";
|
||||||
private static final String GROUP_CLOSURE = "Maturity & Premature Closure";
|
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( //
|
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_group", "accountTypeGroup", "Account Type Group", "text", GROUP_PRODUCT), //
|
||||||
@@ -422,7 +426,11 @@ public class DdsApprovalReadService {
|
|||||||
new ProductField("premature_interest_reduction", "prematureInterestReduction", "Premature Interest Reduction", "percent",
|
new ProductField("premature_interest_reduction", "prematureInterestReduction", "Premature Interest Reduction", "percent",
|
||||||
GROUP_CLOSURE), //
|
GROUP_CLOSURE), //
|
||||||
new ProductField("premature_agent_commission_penalty", "prematureAgentCommissionPenalty", "Agent Commission Penalty",
|
new ProductField("premature_agent_commission_penalty", "prematureAgentCommissionPenalty", "Agent Commission Penalty",
|
||||||
"percent", GROUP_CLOSURE));
|
"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
|
* What an entry that moves no money actually asks for, field by field, so the checker can read the
|
||||||
@@ -453,7 +461,7 @@ public class DdsApprovalReadService {
|
|||||||
final List<Map<String, Object>> fields = new ArrayList<>();
|
final List<Map<String, Object>> fields = new ArrayList<>();
|
||||||
for (final ProductField field : PRODUCT_FIELDS) {
|
for (final ProductField field : PRODUCT_FIELDS) {
|
||||||
final boolean requested = json.has(field.parameter()) && !json.get(field.parameter()).isJsonNull();
|
final boolean requested = json.has(field.parameter()) && !json.get(field.parameter()).isJsonNull();
|
||||||
final String proposed = requested ? format(json.get(field.parameter()).getAsString(), field.format()) : null;
|
final String proposed = requested ? display(json.get(field.parameter()).getAsString(), field.format()) : null;
|
||||||
final Map<String, Object> out = new LinkedHashMap<>();
|
final Map<String, Object> out = new LinkedHashMap<>();
|
||||||
out.put("key", field.parameter());
|
out.put("key", field.parameter());
|
||||||
out.put("label", field.label());
|
out.put("label", field.label());
|
||||||
@@ -461,7 +469,7 @@ public class DdsApprovalReadService {
|
|||||||
out.put("format", field.format());
|
out.put("format", field.format());
|
||||||
if (compare) {
|
if (compare) {
|
||||||
final Object stored = current.get(field.column());
|
final Object stored = current.get(field.column());
|
||||||
final String was = stored == null ? null : format(String.valueOf(stored), field.format());
|
final String was = stored == null ? null : display(String.valueOf(stored), field.format());
|
||||||
final String now = requested ? proposed : was;
|
final String now = requested ? proposed : was;
|
||||||
out.put("current", was);
|
out.put("current", was);
|
||||||
out.put("value", now);
|
out.put("value", now);
|
||||||
@@ -476,6 +484,16 @@ public class DdsApprovalReadService {
|
|||||||
return fields;
|
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. */
|
/** 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) {
|
private static String format(final String value, final String format) {
|
||||||
if (value == null || value.isBlank()) {
|
if (value == null || value.isBlank()) {
|
||||||
@@ -560,13 +578,40 @@ public class DdsApprovalReadService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "DDSBATCH:POST" -> {
|
case "DDSBATCH:POST" -> {
|
||||||
final String expense = first("select c.name from m_dds_batch_run r join c_configuration c on c.name = "
|
// The expense side, on the accounts of the DDS products concerned - interest on each
|
||||||
+ "case when r.job_type like 'COMMISSION%' then 'dds-agent-commission-expense-account' "
|
// customer's product's interest account, commission shared over the commission accounts
|
||||||
+ "else 'dds-interest-expense-account' end where r.id = ?", resourceId);
|
// of the products each agent collected for, as the posting itself will book it.
|
||||||
final Map<String, Object> gl = expense == null ? Map.of()
|
final String jobType = first("select job_type from m_dds_batch_run where id = ?", resourceId);
|
||||||
: this.jdbcTemplate.queryForList("select g.gl_code as \"code\", g.name from c_configuration c " // NOSONAR
|
if (jobType != null && jobType.startsWith("COMMISSION")) {
|
||||||
+ "join acc_gl_account g on g.id = c.value where c.name = ?", expense).stream().findFirst().orElse(Map.of());
|
final Map<String, BigDecimal> byAccount = new LinkedHashMap<>();
|
||||||
lines.add(line("DR", (String) gl.get("code"), (String) gl.get("name"), "GL", amount));
|
final Map<String, String> names = new LinkedHashMap<>();
|
||||||
|
for (final Map<String, Object> runRow : this.jdbcTemplate.queryForList( // NOSONAR
|
||||||
|
"select agent_id, amount from m_dds_batch_run_row where batch_run_id = ? and amount > 0 order by id", resourceId)) {
|
||||||
|
try {
|
||||||
|
this.ddsLedger.commissionSplit(((Number) runRow.get("agent_id")).longValue(), (BigDecimal) runRow.get("amount"))
|
||||||
|
.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\", "
|
||||||
|
|||||||
+15
-6
@@ -29,6 +29,7 @@ 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 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;
|
||||||
@@ -141,9 +142,13 @@ 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. */
|
/**
|
||||||
private static final String DDS_DEPOSIT_PRODUCT_SQL = "select value from c_configuration "
|
* Whether a savings account is a DDS customer's scheme account - one a DDS party is linked to.
|
||||||
+ "where name = 'dds-deposit-product' and enabled = true";
|
* Read off the parties themselves rather than a configured product id: what makes an account a
|
||||||
|
* 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;
|
||||||
@@ -773,8 +778,8 @@ public class BatchRunWritePlatformServiceJpaRepositoryImpl implements BatchRunWr
|
|||||||
+ " Set an ordinary savings account for this agent");
|
+ " Set an ordinary savings account for this agent");
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<Long> ddsProduct = this.jdbcTemplate.queryForList(DDS_DEPOSIT_PRODUCT_SQL, Long.class); // NOSONAR
|
final Integer schemes = this.jdbcTemplate.queryForObject(DDS_SCHEME_ACCOUNT_SQL, Integer.class, accountNo); // NOSONAR
|
||||||
if (!ddsProduct.isEmpty() && productId.equals(ddsProduct.get(0))) {
|
if (schemes != null && schemes > 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");
|
||||||
@@ -787,8 +792,12 @@ 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);
|
||||||
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());
|
"DDS commission, batch run #" + batchRun.getId(), shares);
|
||||||
|
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. */
|
||||||
|
|||||||
+17
-5
@@ -336,14 +336,19 @@ 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);
|
||||||
|
|
||||||
BigDecimal pooled = BigDecimal.ZERO;
|
// What the customers on DDS's own books are now owed, by the customer-deposits account of
|
||||||
|
// their product - two products may owe on two accounts.
|
||||||
|
final Map<Long, BigDecimal> pooled = new LinkedHashMap<>();
|
||||||
|
final Map<Long, GLAccount> depositAccounts = 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);
|
||||||
|
|
||||||
if (party.getSavingsAccountId() != null) {
|
if (party.getSavingsAccountId() != null) {
|
||||||
this.ddsLedger.applyAgentReceiptToSavings(party, settledOn, entry.getAmount(), receiptAccount, description);
|
this.ddsLedger.applyAgentReceiptToSavings(party, settledOn, entry.getAmount(), receiptAccount, description);
|
||||||
} else {
|
} else {
|
||||||
pooled = pooled.add(entry.getAmount());
|
final GLAccount deposits = this.ddsLedger.depositLiabilityOf(party);
|
||||||
|
depositAccounts.putIfAbsent(deposits.getId(), deposits);
|
||||||
|
pooled.merge(deposits.getId(), 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.
|
||||||
@@ -358,10 +363,17 @@ public class CollectionEntryWritePlatformServiceJpaRepositoryImpl implements Col
|
|||||||
this.collectionEntryRepository.save(entry);
|
this.collectionEntryRepository.save(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Everyone on DDS's own accounts, in one pair.
|
// Everyone on DDS's own accounts, in one pair per customer-deposits account, all under one journal id.
|
||||||
final String journalId = "DDSR-A" + applicationId;
|
final String journalId = "DDSR-A" + applicationId;
|
||||||
if (pooled.signum() > 0) {
|
boolean journalled = false;
|
||||||
this.ddsLedger.applyAgentReceipts(agent.getOffice(), settledOn, pooled, receiptAccount, journalId, description);
|
for (final Map.Entry<Long, BigDecimal> owed : pooled.entrySet()) {
|
||||||
|
if (owed.getValue().signum() > 0) {
|
||||||
|
this.ddsLedger.applyAgentReceipts(agent.getOffice(), 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-2
@@ -326,8 +326,7 @@ 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 a.product_id is distinct from (select cast(value as bigint) from c_configuration "
|
+ "and not exists (select 1 from m_dds_party dp where dp.savings_account_id = a.id) "
|
||||||
+ "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 "
|
||||||
|
|||||||
+13
-23
@@ -79,7 +79,6 @@ 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;
|
||||||
@@ -678,9 +677,8 @@ 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 Long productId = ((Number) account.get("product_id")).longValue();
|
final Integer schemes = this.jdbcTemplate.queryForObject(DDS_SCHEME_ACCOUNT_SQL, Integer.class, accountNo); // NOSONAR
|
||||||
final List<Long> ddsProduct = this.jdbcTemplate.queryForList(DEPOSIT_PRODUCT_SQL, Long.class); // NOSONAR
|
if (schemes != null && schemes > 0) {
|
||||||
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");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -705,9 +703,13 @@ public class PartyWritePlatformServiceJpaRepositoryImpl implements PartyWritePla
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Set by 0492. Names the savings product a DDS customer's real account is opened under. */
|
/**
|
||||||
private static final String DEPOSIT_PRODUCT_SQL = "select value from c_configuration "
|
* Whether a savings account is a DDS customer's scheme account - one a DDS party is linked to.
|
||||||
+ "where name = 'dds-deposit-product' and enabled = true";
|
* Read off the parties themselves rather than a configured product id: what makes an account a
|
||||||
|
* 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.
|
||||||
@@ -724,22 +726,10 @@ public class PartyWritePlatformServiceJpaRepositoryImpl implements PartyWritePla
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<Long> productIds = this.jdbcTemplate.queryForList(DEPOSIT_PRODUCT_SQL, Long.class); // NOSONAR
|
// DDS keeps its customers' accounts itself (0514), with each DDS product's own ledger accounts
|
||||||
if (productIds.isEmpty() || productIds.get(0) == null) {
|
// behind them, and no savings product is named for DDS any more - so there is no savings
|
||||||
throw new DdsDepositProductNotConfiguredException();
|
// account to open. A party's balance is already on the books through its product.
|
||||||
}
|
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+6
-23
@@ -53,7 +53,6 @@ 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;
|
||||||
@@ -276,28 +275,12 @@ 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.
|
||||||
if (agent != null) {
|
throw new PlatformDataIntegrityException("error.msg.dds.agent.tran.moved.to.cashier",
|
||||||
throw new PlatformDataIntegrityException("error.msg.dds.agent.tran.moved.to.cashier",
|
"An agent's cash is no longer taken in here. The cashier receipts it at Cashier > Transaction Entry "
|
||||||
"An agent's cash is no longer taken in here. The cashier receipts it at Cashier > Transaction Entry "
|
+ "(Receipt, Cash, GL, Entry Type DDS, A/C No " + (agent != null && agent.getGlAccount() != null
|
||||||
+ "(Receipt, Cash, GL, Entry Type DDS, A/C No " + (agent.getGlAccount() != null ? agent.getGlAccount().getGlCode()
|
? 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,
|
||||||
|
|||||||
@@ -191,4 +191,6 @@
|
|||||||
<include file="parts/0608_apply_rd_base_chart_to_rd_products.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0608_apply_rd_base_chart_to_rd_products.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0618_drop_dds_account_type_post_day.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0618_drop_dds_account_type_post_day.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0619_dds_gl_six_digit_codes_and_agent_gl.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0619_dds_gl_six_digit_codes_and_agent_gl.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0620_dds_product_gl_mapping.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0621_dds_product_drop_unused_gl.xml" relativeToChangelogFile="true"/>
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|||||||
+135
@@ -0,0 +1,135 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
or more contributor license agreements. See the NOTICE file
|
||||||
|
distributed with this work for additional information
|
||||||
|
regarding copyright ownership. The ASF licenses this file
|
||||||
|
to you under the Apache License, Version 2.0 (the
|
||||||
|
"License"); you may not use this file except in compliance
|
||||||
|
with the License. You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing,
|
||||||
|
software distributed under the License is distributed on an
|
||||||
|
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
KIND, either express or implied. See the License for the
|
||||||
|
specific language governing permissions and limitations
|
||||||
|
under the License.
|
||||||
|
-->
|
||||||
|
<databaseChangeLog
|
||||||
|
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||||
|
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Each DDS product names its own ledger accounts on its Accounting tab, the way a savings product
|
||||||
|
does, and DDS posts through those alone - the bank-wide dds-* account settings stop being read.
|
||||||
|
|
||||||
|
Six accounts are the product's: what the bank owes the product's customers (liability), what
|
||||||
|
their interest costs it (expense), what the agents' commission on collecting for it costs it
|
||||||
|
(expense), where a collection is held when the collecting agent has no ledger account of their
|
||||||
|
own (asset or liability), where a matured balance is parked when no account is named to pay it
|
||||||
|
into (liability), and the current-account control a payout into a current account is credited
|
||||||
|
to when the current product's own mapping is ambiguous (liability).
|
||||||
|
|
||||||
|
Existing products are filled from the settings they were posting through until now, so the first
|
||||||
|
posting after this lands exactly where the last one before it did.
|
||||||
|
-->
|
||||||
|
<changeSet id="0620_dds_product_gl_columns" author="CBS">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not>
|
||||||
|
<columnExists tableName="m_dds_account_type" columnName="deposit_liability_gl_id"/>
|
||||||
|
</not>
|
||||||
|
</preConditions>
|
||||||
|
<addColumn tableName="m_dds_account_type">
|
||||||
|
<column name="deposit_liability_gl_id" type="BIGINT"/>
|
||||||
|
<column name="interest_expense_gl_id" type="BIGINT"/>
|
||||||
|
<column name="commission_expense_gl_id" type="BIGINT"/>
|
||||||
|
<column name="agent_cash_gl_id" type="BIGINT"/>
|
||||||
|
<column name="matured_payable_gl_id" type="BIGINT"/>
|
||||||
|
<column name="current_control_gl_id" type="BIGINT"/>
|
||||||
|
</addColumn>
|
||||||
|
<addForeignKeyConstraint baseTableName="m_dds_account_type" baseColumnNames="deposit_liability_gl_id"
|
||||||
|
constraintName="fk_dds_account_type_deposit_gl" referencedTableName="acc_gl_account"
|
||||||
|
referencedColumnNames="id"/>
|
||||||
|
<addForeignKeyConstraint baseTableName="m_dds_account_type" baseColumnNames="interest_expense_gl_id"
|
||||||
|
constraintName="fk_dds_account_type_interest_gl" referencedTableName="acc_gl_account"
|
||||||
|
referencedColumnNames="id"/>
|
||||||
|
<addForeignKeyConstraint baseTableName="m_dds_account_type" baseColumnNames="commission_expense_gl_id"
|
||||||
|
constraintName="fk_dds_account_type_commission_gl" referencedTableName="acc_gl_account"
|
||||||
|
referencedColumnNames="id"/>
|
||||||
|
<addForeignKeyConstraint baseTableName="m_dds_account_type" baseColumnNames="agent_cash_gl_id"
|
||||||
|
constraintName="fk_dds_account_type_agent_cash_gl" referencedTableName="acc_gl_account"
|
||||||
|
referencedColumnNames="id"/>
|
||||||
|
<addForeignKeyConstraint baseTableName="m_dds_account_type" baseColumnNames="matured_payable_gl_id"
|
||||||
|
constraintName="fk_dds_account_type_matured_payable_gl" referencedTableName="acc_gl_account"
|
||||||
|
referencedColumnNames="id"/>
|
||||||
|
<addForeignKeyConstraint baseTableName="m_dds_account_type" baseColumnNames="current_control_gl_id"
|
||||||
|
constraintName="fk_dds_account_type_current_control_gl" referencedTableName="acc_gl_account"
|
||||||
|
referencedColumnNames="id"/>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="0620_dds_product_gl_backfill" author="CBS">
|
||||||
|
<sql>
|
||||||
|
update m_dds_account_type
|
||||||
|
set deposit_liability_gl_id = (select g.id from c_configuration c join acc_gl_account g on g.id = c.value
|
||||||
|
where c.name = 'dds-deposit-liability-account')
|
||||||
|
where deposit_liability_gl_id is null;
|
||||||
|
update m_dds_account_type
|
||||||
|
set interest_expense_gl_id = (select g.id from c_configuration c join acc_gl_account g on g.id = c.value
|
||||||
|
where c.name = 'dds-interest-expense-account')
|
||||||
|
where interest_expense_gl_id is null;
|
||||||
|
update m_dds_account_type
|
||||||
|
set commission_expense_gl_id = (select g.id from c_configuration c join acc_gl_account g on g.id = c.value
|
||||||
|
where c.name = 'dds-agent-commission-expense-account')
|
||||||
|
where commission_expense_gl_id is null;
|
||||||
|
update m_dds_account_type
|
||||||
|
set agent_cash_gl_id = (select g.id from c_configuration c join acc_gl_account g on g.id = c.value
|
||||||
|
where c.name = 'dds-agent-cash-account')
|
||||||
|
where agent_cash_gl_id is null;
|
||||||
|
update m_dds_account_type
|
||||||
|
set matured_payable_gl_id = (select g.id from c_configuration c join acc_gl_account g on g.id = c.value
|
||||||
|
where c.name = 'dds-matured-payable-account')
|
||||||
|
where matured_payable_gl_id is null;
|
||||||
|
update m_dds_account_type
|
||||||
|
set current_control_gl_id = (select g.id from c_configuration c join acc_gl_account g on g.id = c.value
|
||||||
|
where c.name = 'dds-current-account-control-account')
|
||||||
|
where current_control_gl_id is null;
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Commission is paid to an agent in one sum for collections that may span several products, and
|
||||||
|
each product now books its share on its own commission account. The share is worked out from the
|
||||||
|
collections the paid commission was earned on - those a commission run has counted and no payment
|
||||||
|
has yet covered - so a collection is marked with the run that paid for it.
|
||||||
|
-->
|
||||||
|
<changeSet id="0620_dds_party_transaction_commission_paid_run" author="CBS">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not>
|
||||||
|
<columnExists tableName="m_dds_party_transaction" columnName="commission_paid_batch_run_id"/>
|
||||||
|
</not>
|
||||||
|
</preConditions>
|
||||||
|
<addColumn tableName="m_dds_party_transaction">
|
||||||
|
<column name="commission_paid_batch_run_id" type="BIGINT"/>
|
||||||
|
</addColumn>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
The DDS settings on Global Configurations, removed. Every ledger account DDS posts through is now
|
||||||
|
named on the DDS product itself, and copied there above before these go. dds-deposit-product named
|
||||||
|
the savings product DDS customers' accounts were opened under before DDS kept its own (0514);
|
||||||
|
nothing opens one now, and a scheme account is recognised by the DDS party linked to it.
|
||||||
|
-->
|
||||||
|
<changeSet id="0620_dds_remove_configuration_settings" author="CBS">
|
||||||
|
<sql>
|
||||||
|
delete from c_configuration
|
||||||
|
where name in ('dds-deposit-liability-account', 'dds-interest-expense-account', 'dds-agent-commission-expense-account',
|
||||||
|
'dds-agent-cash-account', 'dds-matured-payable-account', 'dds-current-account-control-account',
|
||||||
|
'dds-deposit-product');
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
or more contributor license agreements. See the NOTICE file
|
||||||
|
distributed with this work for additional information
|
||||||
|
regarding copyright ownership. The ASF licenses this file
|
||||||
|
to you under the Apache License, Version 2.0 (the
|
||||||
|
"License"); you may not use this file except in compliance
|
||||||
|
with the License. You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing,
|
||||||
|
software distributed under the License is distributed on an
|
||||||
|
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
KIND, either express or implied. See the License for the
|
||||||
|
specific language governing permissions and limitations
|
||||||
|
under the License.
|
||||||
|
-->
|
||||||
|
<databaseChangeLog
|
||||||
|
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||||
|
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Three of the accounts 0620 put on the DDS product are never posted to, so they come off it:
|
||||||
|
|
||||||
|
agent_cash_gl_id - an agent cannot be saved without a ledger account of their own, so a
|
||||||
|
collection always lands on the agent's account and never on this one.
|
||||||
|
matured_payable_gl_id - the payout and premature-closure screens both require the clerk to pick
|
||||||
|
the Sundry account a balance is parked in, so there is never a default to use.
|
||||||
|
current_control_gl_id - a payout into a current account is credited to that current product's
|
||||||
|
own control account, from the current product's own accounting.
|
||||||
|
-->
|
||||||
|
<changeSet id="0621_dds_product_drop_agent_cash_gl" author="CBS">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<columnExists tableName="m_dds_account_type" columnName="agent_cash_gl_id"/>
|
||||||
|
</preConditions>
|
||||||
|
<dropForeignKeyConstraint baseTableName="m_dds_account_type" constraintName="fk_dds_account_type_agent_cash_gl"/>
|
||||||
|
<dropColumn tableName="m_dds_account_type" columnName="agent_cash_gl_id"/>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="0621_dds_product_drop_matured_payable_gl" author="CBS">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<columnExists tableName="m_dds_account_type" columnName="matured_payable_gl_id"/>
|
||||||
|
</preConditions>
|
||||||
|
<dropForeignKeyConstraint baseTableName="m_dds_account_type" constraintName="fk_dds_account_type_matured_payable_gl"/>
|
||||||
|
<dropColumn tableName="m_dds_account_type" columnName="matured_payable_gl_id"/>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="0621_dds_product_drop_current_control_gl" author="CBS">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<columnExists tableName="m_dds_account_type" columnName="current_control_gl_id"/>
|
||||||
|
</preConditions>
|
||||||
|
<dropForeignKeyConstraint baseTableName="m_dds_account_type" constraintName="fk_dds_account_type_current_control_gl"/>
|
||||||
|
<dropColumn tableName="m_dds_account_type" columnName="current_control_gl_id"/>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
Reference in New Issue
Block a user