fix(shares,savings): remove product-config voucher auto-resolve, fix charge/currency edit bugs, gate withdrawal on settlement
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
- ShareAdvanceCollectionApiResource: drop the hardcoded/auto-resolved Share Suspense GL lookup for by-voucher search; GL account and voucher are now both required, manually-selected inputs. apply() now accepts a separately-verified entry-fee receipt (chargeCollectionId) so a fee collected into a different GL than the share value is never swept into the Sundry leg or double-posted to income. - ClerkShareWithdrawalRequestApiResource: hard-block approval when collection_applied is false, so shares nobody has actually paid for can't be redeemed off an Active-but-unsettled account. - ProductPendingEditFieldMapper / ShareProductDataSerializer: fix maker-checker edits on share products silently dropping charges (chargesSelected vs charges field-name mismatch) and NPEing on approval when the replay JSON carries no currencyCode. - SavingsAccount: initialize the clearing-hold field to zero so new savings submissions don't violate the NOT NULL constraint on total_savings_amount_on_hold_clearing. - CashReportApiResource: fix voucher number resolution for GL-type ledger entries. - application.properties: revert local tenant DB password default. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+13
@@ -903,10 +903,23 @@ public class CashReportApiResource {
|
|||||||
// the real, formatted voucher number (e.g. "CR/001/2908/0001") stamped on the deposit/
|
// the real, formatted voucher number (e.g. "CR/001/2908/0001") stamped on the deposit/
|
||||||
// withdrawal request at approval time -- std.txn_id on its own is only the internal savings
|
// withdrawal request at approval time -- std.txn_id on its own is only the internal savings
|
||||||
// transaction id (e.g. "4"), never what was actually printed on the voucher.
|
// transaction id (e.g. "4"), never what was actually printed on the voucher.
|
||||||
|
//
|
||||||
|
// A GL-type deposit's own txn_id is a hex string (e.g. "a29ff54dc88f", from a journal-entry
|
||||||
|
// reference) rather than a plain decimal savings-transaction id, but std.txn_id is always
|
||||||
|
// stored as the DECIMAL parse of whatever id it was given -- so for a GL row it holds the
|
||||||
|
// hex string's numeric value (178807899015311), not the hex string itself, and a plain
|
||||||
|
// string-equality join against cdr.txn_id can never match it. The second half of each OR
|
||||||
|
// below re-derives that same decimal value from the hex string to match it -- guarded to
|
||||||
|
// only run on strings that are actually hex (so an ordinary decimal txn_id, which never
|
||||||
|
// contains a-f, is never sent through the hex cast at all).
|
||||||
final String baseGroupSql = "FROM m_savings_txn_denomination std "
|
final String baseGroupSql = "FROM m_savings_txn_denomination std "
|
||||||
+ "LEFT JOIN m_client mc ON mc.id = std.client_id "
|
+ "LEFT JOIN m_client mc ON mc.id = std.client_id "
|
||||||
+ "LEFT JOIN m_cashier_deposit_request cdr ON cdr.txn_id = CAST(std.txn_id AS VARCHAR) "
|
+ "LEFT JOIN m_cashier_deposit_request cdr ON cdr.txn_id = CAST(std.txn_id AS VARCHAR) "
|
||||||
|
+ " OR (cdr.txn_id ~ '^[0-9a-fA-F]{1,16}$' AND cdr.txn_id ~ '[a-fA-F]' "
|
||||||
|
+ " AND ('x' || lpad(cdr.txn_id, 16, '0'))::bit(64)::bigint = std.txn_id) "
|
||||||
+ "LEFT JOIN m_clerk_withdrawal_request cwr ON cwr.txn_id = CAST(std.txn_id AS VARCHAR) "
|
+ "LEFT JOIN m_clerk_withdrawal_request cwr ON cwr.txn_id = CAST(std.txn_id AS VARCHAR) "
|
||||||
|
+ " OR (cwr.txn_id ~ '^[0-9a-fA-F]{1,16}$' AND cwr.txn_id ~ '[a-fA-F]' "
|
||||||
|
+ " AND ('x' || lpad(cwr.txn_id, 16, '0'))::bit(64)::bigint = std.txn_id) "
|
||||||
+ "WHERE std.txn_date = CAST(? AS DATE) "
|
+ "WHERE std.txn_date = CAST(? AS DATE) "
|
||||||
+ officerFilter
|
+ officerFilter
|
||||||
+ " GROUP BY std.txn_id, std.account_no, mc.display_name, std.cif_no ";
|
+ " GROUP BY std.txn_id, std.account_no, mc.display_name, std.cif_no ";
|
||||||
|
|||||||
+164
-35
@@ -104,8 +104,6 @@ public class ShareAdvanceCollectionApiResource {
|
|||||||
// ── m_share_account.status_enum ───────────────────────────────────────
|
// ── m_share_account.status_enum ───────────────────────────────────────
|
||||||
private static final int STATUS_ACTIVE = 300;
|
private static final int STATUS_ACTIVE = 300;
|
||||||
|
|
||||||
// ── GL account the cashier deposits share money into ──────────────────
|
|
||||||
private static final String SHARE_SUNDRY_ACCOUNT_NO = "SHASUN001";
|
|
||||||
|
|
||||||
private final PlatformSecurityContext context;
|
private final PlatformSecurityContext context;
|
||||||
private final JdbcTemplate jdbcTemplate;
|
private final JdbcTemplate jdbcTemplate;
|
||||||
@@ -213,17 +211,18 @@ public class ShareAdvanceCollectionApiResource {
|
|||||||
@GET
|
@GET
|
||||||
@Path("/by-voucher")
|
@Path("/by-voucher")
|
||||||
public Response findByVoucherParam(@QueryParam("voucherNo") final String voucherNo,
|
public Response findByVoucherParam(@QueryParam("voucherNo") final String voucherNo,
|
||||||
@QueryParam("clientId") final Long clientIdHint) {
|
@QueryParam("clientId") final Long clientIdHint, @QueryParam("glAccountNo") final String glAccountNoOverride) {
|
||||||
if (voucherNo == null || voucherNo.isBlank()) {
|
if (voucherNo == null || voucherNo.isBlank()) {
|
||||||
throw new PlatformDataIntegrityException("error.msg.shareadvance.voucher.required",
|
throw new PlatformDataIntegrityException("error.msg.shareadvance.voucher.required",
|
||||||
"A voucher or receipt number is required.");
|
"A voucher or receipt number is required.");
|
||||||
}
|
}
|
||||||
return findByVoucher(voucherNo, clientIdHint);
|
return findByVoucher(voucherNo, clientIdHint, glAccountNoOverride);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/by-voucher/{voucherNo}")
|
@Path("/by-voucher/{voucherNo}")
|
||||||
public Response findByVoucher(@PathParam("voucherNo") final String voucherNo, @QueryParam("clientId") final Long clientIdHint) {
|
public Response findByVoucher(@PathParam("voucherNo") final String voucherNo, @QueryParam("clientId") final Long clientIdHint,
|
||||||
|
@QueryParam("glAccountNo") final String glAccountNoOverride) {
|
||||||
this.context.authenticatedUser();
|
this.context.authenticatedUser();
|
||||||
|
|
||||||
// Matched on voucher_no or receipt_no so a pre-voucher receipt number still resolves.
|
// Matched on voucher_no or receipt_no so a pre-voucher receipt number still resolves.
|
||||||
@@ -234,15 +233,19 @@ public class ShareAdvanceCollectionApiResource {
|
|||||||
+ " WHERE c.voucher_no = ? OR c.receipt_no = ? ORDER BY c.id DESC", voucherNo, voucherNo);
|
+ " WHERE c.voucher_no = ? OR c.receipt_no = ? ORDER BY c.id DESC", voucherNo, voucherNo);
|
||||||
|
|
||||||
if (rows.isEmpty()) {
|
if (rows.isEmpty()) {
|
||||||
// Cash is normally taken through the cashier's Transaction Entry deposit into the Share
|
// Cash is normally taken through the cashier's Transaction Entry deposit into the GL
|
||||||
// Sundry GL, which lives in m_cashier_deposit_request. Adopt that approved deposit as a
|
// account the counter deposited it to, which lives in m_cashier_deposit_request. Adopt
|
||||||
// collection so the member's voucher resolves here.
|
// that approved deposit as a collection so the member's voucher resolves here. Which GL
|
||||||
final Map<String, Object> adopted = adoptApprovedDeposit(voucherNo, clientIdHint);
|
// account to look in is never guessed from product configuration — the counter selects it
|
||||||
|
// by hand every time, since a product mapping can silently go missing or point at the
|
||||||
|
// wrong account and a plain voucher lookup should never depend on that being right.
|
||||||
|
final Map<String, Object> adopted = adoptApprovedDeposit(voucherNo, clientIdHint, glAccountNoOverride);
|
||||||
if (adopted != null) {
|
if (adopted != null) {
|
||||||
return Response.ok(gson.toJson(adopted)).build();
|
return Response.ok(gson.toJson(adopted)).build();
|
||||||
}
|
}
|
||||||
throw new PlatformDataIntegrityException("error.msg.shareadvance.voucher.notfound", "No share collection found for voucher "
|
throw new PlatformDataIntegrityException("error.msg.shareadvance.voucher.notfound", "No share collection found for voucher "
|
||||||
+ voucherNo + ". Check the number, or confirm the deposit has been approved by a manager.");
|
+ voucherNo + " in GL account " + glAccountNoOverride + ". Check the voucher number, the GL account, and confirm the "
|
||||||
|
+ "deposit has been approved by a manager.");
|
||||||
}
|
}
|
||||||
|
|
||||||
final Map<String, Object> row = rows.get(0);
|
final Map<String, Object> row = rows.get(0);
|
||||||
@@ -253,6 +256,55 @@ public class ShareAdvanceCollectionApiResource {
|
|||||||
return Response.ok(gson.toJson(row)).build();
|
return Response.ok(gson.toJson(row)).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
// GET — GL accounts selectable for a by-voucher lookup
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
/**
|
||||||
|
* Every active, postable GL account, for the GL account picker on the Apply Collection screen.
|
||||||
|
*
|
||||||
|
* Deliberately not filtered to whatever a share product's own Share Suspense mapping happens to
|
||||||
|
* point at today — that mapping is exactly what this lookup no longer trusts, since it can go
|
||||||
|
* missing or wrong without anyone noticing until a voucher search fails at the counter. The person
|
||||||
|
* applying a receipt picks the account the deposit actually went to, every time.
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Path("/glaccounts")
|
||||||
|
public Response glAccounts() {
|
||||||
|
this.context.authenticatedUser();
|
||||||
|
final List<Map<String, Object>> rows = jdbcTemplate.queryForList(
|
||||||
|
"SELECT id, gl_code, name FROM acc_gl_account WHERE disabled = false AND account_usage = 1 ORDER BY gl_code");
|
||||||
|
final Map<String, Object> out = new LinkedHashMap<>();
|
||||||
|
out.put("glAccounts", rows);
|
||||||
|
return Response.ok(gson.toJson(out)).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
// GET — What a share account needs before a collection can be applied
|
||||||
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
|
/**
|
||||||
|
* Share value plus any outstanding entry fee for one share account -- shown up front on the Apply
|
||||||
|
* Collection screen so the person at the counter knows the full amount to collect (and that it may
|
||||||
|
* come in as more than one receipt, e.g. one for the share value and a separate one for the fee)
|
||||||
|
* before a search ever comes up short, rather than learning it only from an error after the fact.
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Path("/requirement")
|
||||||
|
public Response requirement(@QueryParam("shareAccountId") final Long shareAccountId) {
|
||||||
|
this.context.authenticatedUser();
|
||||||
|
if (shareAccountId == null) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.shareadvance.accountid.required", "shareAccountId is required.");
|
||||||
|
}
|
||||||
|
final Map<String, Object> acct = loadAccount(shareAccountId);
|
||||||
|
final BigDecimal shareAmount = shareValueOf(shareAccountId);
|
||||||
|
final BigDecimal chargeAmount = outstandingChargeOf(shareAccountId);
|
||||||
|
final Map<String, Object> out = new LinkedHashMap<>();
|
||||||
|
out.put("accountNo", acct.get("account_no"));
|
||||||
|
out.put("shareAmount", shareAmount);
|
||||||
|
out.put("chargeAmount", chargeAmount);
|
||||||
|
out.put("required", shareAmount.add(chargeAmount));
|
||||||
|
return Response.ok(gson.toJson(out)).build();
|
||||||
|
}
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
// POST — Apply a receipt to an activated share account
|
// POST — Apply a receipt to an activated share account
|
||||||
// ══════════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
@@ -273,7 +325,8 @@ public class ShareAdvanceCollectionApiResource {
|
|||||||
// A member who pays in instalments has more than one receipt against the same application:
|
// A member who pays in instalments has more than one receipt against the same application:
|
||||||
// ₹4,500 today, the ₹500 balance tomorrow. Neither settles the account on its own, so they
|
// ₹4,500 today, the ₹500 balance tomorrow. Neither settles the account on its own, so they
|
||||||
// are applied together as one settlement. The path receipt leads; any others come in the
|
// are applied together as one settlement. The path receipt leads; any others come in the
|
||||||
// body.
|
// body. These are the SHARE-side receipts only -- money that actually sat in Sundry and
|
||||||
|
// genuinely needs to move out of it into Share Capital.
|
||||||
final List<Long> collectionIds = new java.util.ArrayList<>();
|
final List<Long> collectionIds = new java.util.ArrayList<>();
|
||||||
collectionIds.add(collectionId);
|
collectionIds.add(collectionId);
|
||||||
for (final Long additionalId : getLongList(json, "additionalCollectionIds")) {
|
for (final Long additionalId : getLongList(json, "additionalCollectionIds")) {
|
||||||
@@ -282,6 +335,14 @@ public class ShareAdvanceCollectionApiResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A separately-verified entry-fee receipt, when the member's fee was collected as its own
|
||||||
|
// receipt into whatever GL account the cashier actually deposited it to -- NOT into Sundry.
|
||||||
|
// This id proves the fee was paid; it never contributes to the Sundry leg below, and no GL
|
||||||
|
// entry moves its money, because it was never sitting in Sundry to begin with. Omit it (leave
|
||||||
|
// null) when the fee was paid as part of the same Sundry receipt(s) as the share value --
|
||||||
|
// that legacy, single-receipt case still needs the fee amount swept out of Sundry below.
|
||||||
|
final Long chargeCollectionId = getLong(json, "chargeCollectionId");
|
||||||
|
|
||||||
final List<Map<String, Object>> collections = new java.util.ArrayList<>();
|
final List<Map<String, Object>> collections = new java.util.ArrayList<>();
|
||||||
for (final Long id : collectionIds) {
|
for (final Long id : collectionIds) {
|
||||||
final Map<String, Object> row = loadCollection(id);
|
final Map<String, Object> row = loadCollection(id);
|
||||||
@@ -292,6 +353,15 @@ public class ShareAdvanceCollectionApiResource {
|
|||||||
collections.add(row);
|
collections.add(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, Object> chargeColl = null;
|
||||||
|
if (chargeCollectionId != null) {
|
||||||
|
chargeColl = loadCollection(chargeCollectionId);
|
||||||
|
if (!"UNAPPLIED".equals(chargeColl.get("status"))) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.shareadvance.already.applied",
|
||||||
|
"Entry fee receipt " + chargeColl.get("receipt_no") + " is already " + chargeColl.get("status") + ".");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final Map<String, Object> coll = collections.get(0);
|
final Map<String, Object> coll = collections.get(0);
|
||||||
|
|
||||||
final Map<String, Object> acct = loadAccount(shareAccountId);
|
final Map<String, Object> acct = loadAccount(shareAccountId);
|
||||||
@@ -306,6 +376,10 @@ public class ShareAdvanceCollectionApiResource {
|
|||||||
+ " belongs to a different member than share account " + acct.get("account_no") + ".");
|
+ " belongs to a different member than share account " + acct.get("account_no") + ".");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (chargeColl != null && !toLong(chargeColl.get("client_id")).equals(acctClientId)) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.shareadvance.client.mismatch", "Entry fee receipt "
|
||||||
|
+ chargeColl.get("receipt_no") + " belongs to a different member than share account " + acct.get("account_no") + ".");
|
||||||
|
}
|
||||||
|
|
||||||
final int statusEnum = toInt(acct.get("status_enum"));
|
final int statusEnum = toInt(acct.get("status_enum"));
|
||||||
if (statusEnum != STATUS_ACTIVE) {
|
if (statusEnum != STATUS_ACTIVE) {
|
||||||
@@ -320,7 +394,12 @@ public class ShareAdvanceCollectionApiResource {
|
|||||||
// Share value the account was opened for, and any entry fee attached to it.
|
// Share value the account was opened for, and any entry fee attached to it.
|
||||||
final BigDecimal shareAmount = shareValueOf(shareAccountId);
|
final BigDecimal shareAmount = shareValueOf(shareAccountId);
|
||||||
final BigDecimal chargeAmount = outstandingChargeOf(shareAccountId);
|
final BigDecimal chargeAmount = outstandingChargeOf(shareAccountId);
|
||||||
final BigDecimal required = shareAmount.add(chargeAmount);
|
|
||||||
|
// When the fee is being verified as its own receipt, the Sundry-side receipts only need to
|
||||||
|
// cover the share value -- the fee is proved paid separately below, and its money is never
|
||||||
|
// expected to be part of what came out of Sundry. Without a separate fee receipt, the fee is
|
||||||
|
// assumed bundled into the same Sundry receipt(s) as the share value, exactly as before.
|
||||||
|
final BigDecimal requiredForShareReceipts = chargeColl != null ? shareAmount : shareAmount.add(chargeAmount);
|
||||||
|
|
||||||
BigDecimal receiptAmount = BigDecimal.ZERO;
|
BigDecimal receiptAmount = BigDecimal.ZERO;
|
||||||
final StringBuilder receiptNos = new StringBuilder();
|
final StringBuilder receiptNos = new StringBuilder();
|
||||||
@@ -335,23 +414,41 @@ public class ShareAdvanceCollectionApiResource {
|
|||||||
// Short is blocked: share capital cannot be part-issued — the member either bought those
|
// Short is blocked: share capital cannot be part-issued — the member either bought those
|
||||||
// shares or did not — so settling for less would leave the account short by an amount owed
|
// shares or did not — so settling for less would leave the account short by an amount owed
|
||||||
// by nobody. The clerk collects the balance on another receipt and applies both together.
|
// by nobody. The clerk collects the balance on another receipt and applies both together.
|
||||||
if (receiptAmount.compareTo(required) < 0) {
|
if (receiptAmount.compareTo(requiredForShareReceipts) < 0) {
|
||||||
final BigDecimal shortfall = required.subtract(receiptAmount);
|
final BigDecimal shortfall = requiredForShareReceipts.subtract(receiptAmount);
|
||||||
throw new PlatformDataIntegrityException("error.msg.shareadvance.amount.short",
|
throw new PlatformDataIntegrityException("error.msg.shareadvance.amount.short",
|
||||||
"Receipt " + receiptNos + " totals " + receiptAmount + " but share account " + acct.get("account_no") + " requires "
|
"Receipt " + receiptNos + " totals " + receiptAmount + " but share account " + acct.get("account_no") + " requires "
|
||||||
+ required + " (shares " + shareAmount + " + charges " + chargeAmount + "). Collect the remaining " + shortfall
|
+ requiredForShareReceipts + ". Collect the remaining " + shortfall
|
||||||
+ " from the member, then apply both receipts together.");
|
+ " from the member, then apply both receipts together.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The fee receipt, when given, must cover the fee on its own -- checked separately from the
|
||||||
|
// share-side shortfall above, since the two are no longer one combined pot.
|
||||||
|
if (chargeColl != null && chargeAmount.compareTo(BigDecimal.ZERO) > 0) {
|
||||||
|
final BigDecimal chargeReceiptAmount = (BigDecimal) chargeColl.get("amount");
|
||||||
|
if (chargeReceiptAmount.compareTo(chargeAmount) < 0) {
|
||||||
|
final BigDecimal shortfall = chargeAmount.subtract(chargeReceiptAmount);
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.shareadvance.charge.amount.short",
|
||||||
|
"Entry fee receipt " + chargeColl.get("receipt_no") + " totals " + chargeReceiptAmount + " but share account "
|
||||||
|
+ acct.get("account_no") + " requires " + chargeAmount + " for the entry fee. Collect the remaining "
|
||||||
|
+ shortfall + " from the member.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Over-payment is allowed through rather than turning a member away over a rounded-up note.
|
// Over-payment is allowed through rather than turning a member away over a rounded-up note.
|
||||||
// The difference is credited to their linked savings account below.
|
// The difference is credited to their linked savings account below. Only the share-side
|
||||||
final BigDecimal excessAmount = receiptAmount.subtract(required);
|
// excess is handled here -- an over-payment on a separately-verified fee receipt would need
|
||||||
|
// its own GL leg to fund the savings credit, which this flow does not post (that receipt's
|
||||||
|
// money is left exactly where the cashier already put it), so it is left uncollected rather
|
||||||
|
// than silently unbalancing the ledger.
|
||||||
|
final BigDecimal excessAmount = receiptAmount.subtract(requiredForShareReceipts);
|
||||||
final Long savingsAccountId = toLong(acct.get("savings_account_id"));
|
final Long savingsAccountId = toLong(acct.get("savings_account_id"));
|
||||||
if (excessAmount.compareTo(BigDecimal.ZERO) > 0 && savingsAccountId == null) {
|
if (excessAmount.compareTo(BigDecimal.ZERO) > 0 && savingsAccountId == null) {
|
||||||
throw new PlatformDataIntegrityException("error.msg.shareadvance.excess.no.savings",
|
throw new PlatformDataIntegrityException("error.msg.shareadvance.excess.no.savings",
|
||||||
"Receipt " + receiptNos + " totals " + receiptAmount + " but share account " + acct.get("account_no")
|
"Receipt " + receiptNos + " totals " + receiptAmount + " but share account " + acct.get("account_no")
|
||||||
+ " requires only " + required + ". The extra " + excessAmount + " must be credited to the member's savings "
|
+ " requires only " + requiredForShareReceipts + ". The extra " + excessAmount
|
||||||
+ "account, but none is linked to this share account. Link one, or collect the exact amount.");
|
+ " must be credited to the member's savings account, but none is linked to this share account. Link one, or "
|
||||||
|
+ "collect the exact amount.");
|
||||||
}
|
}
|
||||||
|
|
||||||
final Long officeId = toLong(acct.get("office_id"));
|
final Long officeId = toLong(acct.get("office_id"));
|
||||||
@@ -367,8 +464,9 @@ public class ShareAdvanceCollectionApiResource {
|
|||||||
final String txnId = "SHAPP-" + System.nanoTime();
|
final String txnId = "SHAPP-" + System.nanoTime();
|
||||||
final String accountNo = (String) acct.get("account_no");
|
final String accountNo = (String) acct.get("account_no");
|
||||||
|
|
||||||
// Dr Sundry for everything collected, including any over-payment — the liability those
|
// Dr Sundry for the share-side receipts, including any over-payment on them — the liability
|
||||||
// receipts created is cleared in full, in one leg, so nothing is left behind in suspense.
|
// those receipts created is cleared in full. A verified fee receipt is deliberately excluded:
|
||||||
|
// it was never in Sundry, so nothing here should debit Sundry for it.
|
||||||
insertEntry(officeId, glSuspense, DEBIT, receiptAmount, txnId, entryDate, shareAccountId, userId, now, currencyCode,
|
insertEntry(officeId, glSuspense, DEBIT, receiptAmount, txnId, entryDate, shareAccountId, userId, now, currencyCode,
|
||||||
"Share application settled from Sundry | Receipt " + receiptNos + " | " + accountNo);
|
"Share application settled from Sundry | Receipt " + receiptNos + " | " + accountNo);
|
||||||
|
|
||||||
@@ -376,14 +474,19 @@ public class ShareAdvanceCollectionApiResource {
|
|||||||
insertEntry(officeId, glEquity, CREDIT, shareAmount, txnId, entryDate, shareAccountId, userId, now, currencyCode,
|
insertEntry(officeId, glEquity, CREDIT, shareAmount, txnId, entryDate, shareAccountId, userId, now, currencyCode,
|
||||||
"Share capital issued for " + accountNo);
|
"Share capital issued for " + accountNo);
|
||||||
|
|
||||||
// Cr Income from Fees for the entry fee, when one was charged.
|
// Cr Income from Fees for the entry fee, but only when it was NOT verified as its own receipt
|
||||||
|
// above -- that money already sits in whichever GL the cashier deposited it to (often the fee
|
||||||
|
// income account itself), so moving it again here would double it. When it was bundled into
|
||||||
|
// the same Sundry receipt(s) as the share value (chargeColl == null), it genuinely still needs
|
||||||
|
// to move, exactly as before.
|
||||||
if (chargeAmount.compareTo(BigDecimal.ZERO) > 0) {
|
if (chargeAmount.compareTo(BigDecimal.ZERO) > 0) {
|
||||||
// Resolved from the share product's "Income from Fees" mapping, which points at the
|
if (chargeColl == null) {
|
||||||
// Share Entry Fee Income GL — changing that mapping redirects the fee without a code change.
|
// Resolved from the share product's "Income from Fees" mapping, which points at the
|
||||||
final Long glIncome = requireGl(productId, FA_INCOME_FROM_FEES, "Share Entry Fee Income");
|
// Share Entry Fee Income GL — changing that mapping redirects the fee without a code change.
|
||||||
insertEntry(officeId, glIncome, CREDIT, chargeAmount, txnId, entryDate, shareAccountId, userId, now, currencyCode,
|
final Long glIncome = requireGl(productId, FA_INCOME_FROM_FEES, "Share Entry Fee Income");
|
||||||
"Share entry fee for " + accountNo);
|
insertEntry(officeId, glIncome, CREDIT, chargeAmount, txnId, entryDate, shareAccountId, userId, now, currencyCode,
|
||||||
|
"Share entry fee for " + accountNo);
|
||||||
|
}
|
||||||
jdbcTemplate.update("UPDATE m_share_account_charge SET is_paid_derived = true, amount_paid_derived = amount, "
|
jdbcTemplate.update("UPDATE m_share_account_charge SET is_paid_derived = true, amount_paid_derived = amount, "
|
||||||
+ " amount_outstanding_derived = 0 WHERE account_id = ? AND is_active = true", shareAccountId);
|
+ " amount_outstanding_derived = 0 WHERE account_id = ? AND is_active = true", shareAccountId);
|
||||||
}
|
}
|
||||||
@@ -399,23 +502,39 @@ public class ShareAdvanceCollectionApiResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Stamp the originating cashier deposits so the same vouchers cannot be applied twice.
|
// Stamp the originating cashier deposits so the same vouchers cannot be applied twice.
|
||||||
|
// Matched on voucher_no alone -- voucher numbers are unique across the whole cashier deposit
|
||||||
|
// table regardless of which GL account they were posted to, so no GL account lookup is needed
|
||||||
|
// here either. Covers the fee receipt's own deposit too, when one was verified.
|
||||||
for (final Map<String, Object> row : collections) {
|
for (final Map<String, Object> row : collections) {
|
||||||
if (row.get("voucher_no") != null) {
|
if (row.get("voucher_no") != null) {
|
||||||
jdbcTemplate.update(
|
jdbcTemplate.update(
|
||||||
"UPDATE m_cashier_deposit_request SET share_account_id = ?, share_applied_on = ?, "
|
"UPDATE m_cashier_deposit_request SET share_account_id = ?, share_applied_on = ?, "
|
||||||
+ " share_applied_txn_id = ? WHERE voucher_no = ? AND account_no = ? AND share_account_id IS NULL",
|
+ " share_applied_txn_id = ? WHERE voucher_no = ? AND share_account_id IS NULL",
|
||||||
shareAccountId, java.sql.Date.valueOf(entryDate), txnId, row.get("voucher_no"), SHARE_SUNDRY_ACCOUNT_NO);
|
shareAccountId, java.sql.Date.valueOf(entryDate), txnId, row.get("voucher_no"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (chargeColl != null && chargeColl.get("voucher_no") != null) {
|
||||||
|
jdbcTemplate.update(
|
||||||
|
"UPDATE m_cashier_deposit_request SET share_account_id = ?, share_applied_on = ?, "
|
||||||
|
+ " share_applied_txn_id = ? WHERE voucher_no = ? AND share_account_id IS NULL",
|
||||||
|
shareAccountId, java.sql.Date.valueOf(entryDate), txnId, chargeColl.get("voucher_no"));
|
||||||
|
}
|
||||||
|
|
||||||
// Every receipt in this settlement is marked applied, not just the one in the path — they
|
// Every receipt in this settlement is marked applied, not just the one in the path — they
|
||||||
// were all consumed by it, and an unmarked one would still show as money awaiting use.
|
// were all consumed by it, and an unmarked one would still show as money awaiting use. The fee
|
||||||
|
// receipt is marked applied too, even though it posted no GL entry of its own.
|
||||||
for (final Long id : collectionIds) {
|
for (final Long id : collectionIds) {
|
||||||
jdbcTemplate.update(
|
jdbcTemplate.update(
|
||||||
"UPDATE m_share_advance_collection SET status = 'APPLIED', applied_share_account_id = ?, "
|
"UPDATE m_share_advance_collection SET status = 'APPLIED', applied_share_account_id = ?, "
|
||||||
+ " applied_on_date = ?, applied_gl_transaction_id = ? WHERE id = ?",
|
+ " applied_on_date = ?, applied_gl_transaction_id = ? WHERE id = ?",
|
||||||
shareAccountId, java.sql.Date.valueOf(entryDate), txnId, id);
|
shareAccountId, java.sql.Date.valueOf(entryDate), txnId, id);
|
||||||
}
|
}
|
||||||
|
if (chargeCollectionId != null) {
|
||||||
|
jdbcTemplate.update(
|
||||||
|
"UPDATE m_share_advance_collection SET status = 'APPLIED', applied_share_account_id = ?, "
|
||||||
|
+ " applied_on_date = ?, applied_gl_transaction_id = ? WHERE id = ?",
|
||||||
|
shareAccountId, java.sql.Date.valueOf(entryDate), txnId, chargeCollectionId);
|
||||||
|
}
|
||||||
|
|
||||||
// Reveals the purchase transaction in the passbook and transactions views.
|
// Reveals the purchase transaction in the passbook and transactions views.
|
||||||
jdbcTemplate.update("UPDATE m_share_account SET collection_applied = true WHERE id = ?", shareAccountId);
|
jdbcTemplate.update("UPDATE m_share_account SET collection_applied = true WHERE id = ?", shareAccountId);
|
||||||
@@ -428,7 +547,8 @@ public class ShareAdvanceCollectionApiResource {
|
|||||||
out.put("accountNo", accountNo);
|
out.put("accountNo", accountNo);
|
||||||
out.put("shareAmount", shareAmount);
|
out.put("shareAmount", shareAmount);
|
||||||
out.put("chargeAmount", chargeAmount);
|
out.put("chargeAmount", chargeAmount);
|
||||||
out.put("totalApplied", required);
|
out.put("chargeReceiptVerifiedSeparately", chargeColl != null);
|
||||||
|
out.put("totalApplied", requiredForShareReceipts);
|
||||||
out.put("totalCollected", receiptAmount);
|
out.put("totalCollected", receiptAmount);
|
||||||
out.put("glTransactionId", txnId);
|
out.put("glTransactionId", txnId);
|
||||||
out.put("status", "APPLIED");
|
out.put("status", "APPLIED");
|
||||||
@@ -458,16 +578,25 @@ public class ShareAdvanceCollectionApiResource {
|
|||||||
* clerk is settling ({@code clientIdHint}). The amount still has to match the account exactly at apply time, which
|
* clerk is settling ({@code clientIdHint}). The amount still has to match the account exactly at apply time, which
|
||||||
* is what keeps the money tied to the right member.
|
* is what keeps the money tied to the right member.
|
||||||
*
|
*
|
||||||
* @return the newly created collection row, or null when no approved Sundry deposit matches.
|
* @param glAccountNoOverride
|
||||||
|
* required -- the GL account number the deposit was posted to, selected by the counter
|
||||||
|
* rather than resolved from any product's accounting configuration, so a broken or
|
||||||
|
* missing product mapping can never make a genuine voucher unreachable.
|
||||||
|
* @return the newly created collection row, or null when no approved deposit matches.
|
||||||
*/
|
*/
|
||||||
private Map<String, Object> adoptApprovedDeposit(final String voucherNo, final Long clientIdHint) {
|
private Map<String, Object> adoptApprovedDeposit(final String voucherNo, final Long clientIdHint, final String glAccountNoOverride) {
|
||||||
|
if (glAccountNoOverride == null || glAccountNoOverride.isBlank()) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.shareadvance.gl.account.required",
|
||||||
|
"Select the GL account this voucher was deposited to, then search again.");
|
||||||
|
}
|
||||||
|
final String shareSundryAccountNo = glAccountNoOverride.trim();
|
||||||
final List<Map<String, Object>> deposits = jdbcTemplate
|
final List<Map<String, Object>> deposits = jdbcTemplate
|
||||||
.queryForList(
|
.queryForList(
|
||||||
"SELECT d.id, d.voucher_no, d.client_id, d.amount, d.status, d.txn_date, d.txn_id,"
|
"SELECT d.id, d.voucher_no, d.client_id, d.amount, d.status, d.txn_date, d.txn_id,"
|
||||||
+ " d.office_id, d.currency_code, d.cashier_id, d.cashier_name, d.share_account_id"
|
+ " d.office_id, d.currency_code, d.cashier_id, d.cashier_name, d.share_account_id"
|
||||||
+ " FROM m_cashier_deposit_request d"
|
+ " FROM m_cashier_deposit_request d"
|
||||||
+ " WHERE d.voucher_no = ? AND d.account_no = ? AND d.status = 'APPROVED'" + " ORDER BY d.id DESC",
|
+ " WHERE d.voucher_no = ? AND d.account_no = ? AND d.status = 'APPROVED'" + " ORDER BY d.id DESC",
|
||||||
voucherNo, SHARE_SUNDRY_ACCOUNT_NO);
|
voucherNo, shareSundryAccountNo);
|
||||||
|
|
||||||
if (deposits.isEmpty()) {
|
if (deposits.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
+23
-1
@@ -324,7 +324,7 @@ public class ClerkShareWithdrawalRequestApiResource {
|
|||||||
final Long requestedShares = ((Number) record.get("requested_shares")).longValue();
|
final Long requestedShares = ((Number) record.get("requested_shares")).longValue();
|
||||||
|
|
||||||
final Map<String, Object> shareAccountRow = jdbcTemplate.queryForList("""
|
final Map<String, Object> shareAccountRow = jdbcTemplate.queryForList("""
|
||||||
SELECT total_approved_shares AS totalApprovedShares, status_enum AS statusEnum,
|
SELECT total_approved_shares AS totalApprovedShares, status_enum AS statusEnum, collection_applied AS collectionApplied,
|
||||||
savings_account_id AS savingsAccountId, client_id AS clientId, linked_loan_id AS linkedLoanId
|
savings_account_id AS savingsAccountId, client_id AS clientId, linked_loan_id AS linkedLoanId
|
||||||
FROM m_share_account WHERE id = ?
|
FROM m_share_account WHERE id = ?
|
||||||
""", shareAccountId).stream().findFirst().orElse(null);
|
""", shareAccountId).stream().findFirst().orElse(null);
|
||||||
@@ -340,6 +340,14 @@ public class ClerkShareWithdrawalRequestApiResource {
|
|||||||
: -1;
|
: -1;
|
||||||
checks.add(checkResult("Share account is Active", shareAccountStatus == 300, shareStatusLabel(shareAccountStatus)));
|
checks.add(checkResult("Share account is Active", shareAccountStatus == 300, shareStatusLabel(shareAccountStatus)));
|
||||||
|
|
||||||
|
// Active only means approved and activated -- not paid for. Real share capital is only posted
|
||||||
|
// once a Share Advance Collection is applied (see ShareAdvanceCollectionApiResource), and
|
||||||
|
// approve() below hard-blocks on this same flag, so a request failing this check can never
|
||||||
|
// actually be approved regardless of what a checker decides here.
|
||||||
|
final boolean collectionApplied = shareAccountRow != null && Boolean.TRUE.equals(shareAccountRow.get("collectionApplied"));
|
||||||
|
checks.add(checkResult("Shares have been paid for (Share Advance Collection applied)", collectionApplied,
|
||||||
|
collectionApplied ? "Settled" : "Not settled — no collection has been applied to this share account yet"));
|
||||||
|
|
||||||
final Long savingsAccountId = shareAccountRow != null && shareAccountRow.get("savingsAccountId") != null
|
final Long savingsAccountId = shareAccountRow != null && shareAccountRow.get("savingsAccountId") != null
|
||||||
? ((Number) shareAccountRow.get("savingsAccountId")).longValue()
|
? ((Number) shareAccountRow.get("savingsAccountId")).longValue()
|
||||||
: null;
|
: null;
|
||||||
@@ -589,6 +597,20 @@ public class ClerkShareWithdrawalRequestApiResource {
|
|||||||
final Object meetingDateObj = record.get("meeting_date");
|
final Object meetingDateObj = record.get("meeting_date");
|
||||||
final String resolutionNumber = (String) record.get("resolution_number");
|
final String resolutionNumber = (String) record.get("resolution_number");
|
||||||
|
|
||||||
|
// Account creation, approval and activation post no share-capital GL of their own -- a share
|
||||||
|
// account can be Active with "approved" shares that nobody has actually paid for yet, and only
|
||||||
|
// gets a real Dr Share Sundry / Cr Share Capital entry once a collection is applied (see
|
||||||
|
// ShareAdvanceCollectionApiResource). Redeeming before that would reverse capital that was
|
||||||
|
// never issued, so this is a hard block here, not just an advisory line in validate()'s
|
||||||
|
// checklist -- a checker approving off that checklist alone has no other backstop.
|
||||||
|
final Boolean collectionApplied = jdbcTemplate
|
||||||
|
.queryForList("SELECT collection_applied AS collectionApplied FROM m_share_account WHERE id = ?", shareAccountId).stream()
|
||||||
|
.findFirst().map(row -> (Boolean) row.get("collectionApplied")).orElse(null);
|
||||||
|
if (!Boolean.TRUE.equals(collectionApplied)) {
|
||||||
|
throw apiError(409, "Share account " + shareAccountId + " has not been settled yet -- no Share Advance Collection has been "
|
||||||
|
+ "applied to it, so these shares have not actually been paid for and cannot be redeemed.");
|
||||||
|
}
|
||||||
|
|
||||||
final JsonObject payload = new JsonObject();
|
final JsonObject payload = new JsonObject();
|
||||||
payload.addProperty("locale", "en");
|
payload.addProperty("locale", "en");
|
||||||
payload.addProperty("dateFormat", "dd MMMM yyyy");
|
payload.addProperty("dateFormat", "dd MMMM yyyy");
|
||||||
|
|||||||
+8
-1
@@ -395,7 +395,14 @@ public class ShareProductDataSerializer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.fromApiJsonHelper.parameterExists(ShareProductApiConstants.charges_paramname, element)) {
|
if (this.fromApiJsonHelper.parameterExists(ShareProductApiConstants.charges_paramname, element)) {
|
||||||
final String currencyCode = this.fromApiJsonHelper.extractStringNamed(ShareProductApiConstants.currency_paramname, element);
|
// Falls back to the product's own current currency when the update payload doesn't carry
|
||||||
|
// one -- a maker-checker edit that only touched charges replays a JSON containing just the
|
||||||
|
// submitted fields, with no currencyCode of its own, so requiring it here would make every
|
||||||
|
// charges-only edit crash rather than validate against the currency the product already has.
|
||||||
|
String currencyCode = this.fromApiJsonHelper.extractStringNamed(ShareProductApiConstants.currency_paramname, element);
|
||||||
|
if (currencyCode == null && product.getCurrency() != null) {
|
||||||
|
currencyCode = product.getCurrency().getCode();
|
||||||
|
}
|
||||||
Set<Charge> charges = assembleListOfProductCharges(element, currencyCode);
|
Set<Charge> charges = assembleListOfProductCharges(element, currencyCode);
|
||||||
if (product.setCharges(charges)) {
|
if (product.setCharges(charges)) {
|
||||||
actualChanges.put(ShareProductApiConstants.charges_paramname, charges);
|
actualChanges.put(ShareProductApiConstants.charges_paramname, charges);
|
||||||
|
|||||||
+3
-3
@@ -865,7 +865,7 @@ public class ShareProductWritePlatformServiceJpaRepositoryImpl implements ShareP
|
|||||||
this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, fieldName));
|
this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, fieldName));
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<Long> chargeIds = ProductPendingEditFieldMapper.extractChargeIds(command);
|
final List<Long> chargeIds = ProductPendingEditFieldMapper.extractChargeIds(command, ShareProductApiConstants.charges_paramname);
|
||||||
if (chargeIds != null) {
|
if (chargeIds != null) {
|
||||||
this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, "charges"));
|
this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, "charges"));
|
||||||
for (final Long chargeId : chargeIds) {
|
for (final Long chargeId : chargeIds) {
|
||||||
@@ -907,7 +907,7 @@ public class ShareProductWritePlatformServiceJpaRepositoryImpl implements ShareP
|
|||||||
}
|
}
|
||||||
if (submittedFields.contains("charges")) {
|
if (submittedFields.contains("charges")) {
|
||||||
ProductPendingEditFieldMapper.writeChargesToJson(root,
|
ProductPendingEditFieldMapper.writeChargesToJson(root,
|
||||||
this.pendingEditChargeRepository.findByPendingEditId(pendingEdit.getId()));
|
this.pendingEditChargeRepository.findByPendingEditId(pendingEdit.getId()), ShareProductApiConstants.charges_paramname);
|
||||||
}
|
}
|
||||||
if (submittedFields.contains("glArrayMappings")) {
|
if (submittedFields.contains("glArrayMappings")) {
|
||||||
ProductPendingEditFieldMapper.writeGlArrayMappingsToJson(root,
|
ProductPendingEditFieldMapper.writeGlArrayMappingsToJson(root,
|
||||||
@@ -944,7 +944,7 @@ public class ShareProductWritePlatformServiceJpaRepositoryImpl implements ShareP
|
|||||||
}
|
}
|
||||||
if (submittedFields.contains("charges")) {
|
if (submittedFields.contains("charges")) {
|
||||||
ProductPendingEditFieldMapper.writeChargesToJson(root,
|
ProductPendingEditFieldMapper.writeChargesToJson(root,
|
||||||
this.pendingEditChargeRepository.findByPendingEditId(pendingEdit.getId()));
|
this.pendingEditChargeRepository.findByPendingEditId(pendingEdit.getId()), ShareProductApiConstants.charges_paramname);
|
||||||
}
|
}
|
||||||
if (submittedFields.contains("glArrayMappings")) {
|
if (submittedFields.contains("glArrayMappings")) {
|
||||||
ProductPendingEditFieldMapper.writeGlArrayMappingsToJson(root,
|
ProductPendingEditFieldMapper.writeGlArrayMappingsToJson(root,
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ fineract.security.oauth2.client.registrations.frontend-client.require-authorizat
|
|||||||
fineract.tenant.host=${FINERACT_DEFAULT_TENANTDB_HOSTNAME:localhost}
|
fineract.tenant.host=${FINERACT_DEFAULT_TENANTDB_HOSTNAME:localhost}
|
||||||
fineract.tenant.port=${FINERACT_DEFAULT_TENANTDB_PORT:5432}
|
fineract.tenant.port=${FINERACT_DEFAULT_TENANTDB_PORT:5432}
|
||||||
fineract.tenant.username=${FINERACT_DEFAULT_TENANTDB_UID:postgres}
|
fineract.tenant.username=${FINERACT_DEFAULT_TENANTDB_UID:postgres}
|
||||||
fineract.tenant.password=${FINERACT_DEFAULT_TENANTDB_PWD:Keshav@898966}
|
fineract.tenant.password=${FINERACT_DEFAULT_TENANTDB_PWD:313863}
|
||||||
fineract.tenant.parameters=${FINERACT_DEFAULT_TENANTDB_CONN_PARAMS:}
|
fineract.tenant.parameters=${FINERACT_DEFAULT_TENANTDB_CONN_PARAMS:}
|
||||||
fineract.tenant.timezone=${FINERACT_DEFAULT_TENANTDB_TIMEZONE:Asia/Kolkata}
|
fineract.tenant.timezone=${FINERACT_DEFAULT_TENANTDB_TIMEZONE:Asia/Kolkata}
|
||||||
fineract.tenant.identifier=${FINERACT_DEFAULT_TENANTDB_IDENTIFIER:default}
|
fineract.tenant.identifier=${FINERACT_DEFAULT_TENANTDB_IDENTIFIER:default}
|
||||||
@@ -407,7 +407,7 @@ server.tomcat.mbeanregistry.enabled=${FINERACT_SERVER_TOMCAT_MBEANREGISTRY_ENABL
|
|||||||
spring.datasource.hikari.driverClassName=${FINERACT_HIKARI_DRIVER_SOURCE_CLASS_NAME:org.postgresql.Driver}
|
spring.datasource.hikari.driverClassName=${FINERACT_HIKARI_DRIVER_SOURCE_CLASS_NAME:org.postgresql.Driver}
|
||||||
spring.datasource.hikari.jdbcUrl=${FINERACT_HIKARI_JDBC_URL:jdbc:postgresql://localhost:5432/fineract_tenants}
|
spring.datasource.hikari.jdbcUrl=${FINERACT_HIKARI_JDBC_URL:jdbc:postgresql://localhost:5432/fineract_tenants}
|
||||||
spring.datasource.hikari.username=${FINERACT_HIKARI_USERNAME:postgres}
|
spring.datasource.hikari.username=${FINERACT_HIKARI_USERNAME:postgres}
|
||||||
spring.datasource.hikari.password=${FINERACT_HIKARI_PASSWORD:Keshav@898966}
|
spring.datasource.hikari.password=${FINERACT_HIKARI_PASSWORD:313863}
|
||||||
spring.datasource.hikari.minimumIdle=${FINERACT_HIKARI_MINIMUM_IDLE:3}
|
spring.datasource.hikari.minimumIdle=${FINERACT_HIKARI_MINIMUM_IDLE:3}
|
||||||
spring.datasource.hikari.maximumPoolSize=${FINERACT_HIKARI_MAXIMUM_POOL_SIZE:10}
|
spring.datasource.hikari.maximumPoolSize=${FINERACT_HIKARI_MAXIMUM_POOL_SIZE:10}
|
||||||
spring.datasource.hikari.idleTimeout=${FINERACT_HIKARI_IDLE_TIMEOUT:60000}
|
spring.datasource.hikari.idleTimeout=${FINERACT_HIKARI_IDLE_TIMEOUT:60000}
|
||||||
|
|||||||
+24
-3
@@ -547,10 +547,21 @@ public final class ProductPendingEditFieldMapper {
|
|||||||
|
|
||||||
/** Returns null if "charges" wasn't present at all, else the (possibly empty) list of charge ids. */
|
/** Returns null if "charges" wasn't present at all, else the (possibly empty) list of charge ids. */
|
||||||
public static List<Long> extractChargeIds(final JsonCommand command) {
|
public static List<Long> extractChargeIds(final JsonCommand command) {
|
||||||
if (!command.hasParameter("charges")) {
|
return extractChargeIds(command, "charges");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same as {@link #extractChargeIds(JsonCommand)}, but reads a caller-chosen JSON field instead of the
|
||||||
|
* "charges" every other product type uses. Share products alone submit this array under
|
||||||
|
* {@code chargesSelected} (see {@code ShareProductApiConstants.charges_paramname}) -- calling the
|
||||||
|
* default overload for a share product silently finds nothing, since {@code hasParameter("charges")}
|
||||||
|
* is always false for that payload.
|
||||||
|
*/
|
||||||
|
public static List<Long> extractChargeIds(final JsonCommand command, final String paramName) {
|
||||||
|
if (!command.hasParameter(paramName)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final com.google.gson.JsonElement raw = command.parsedJson().getAsJsonObject().get("charges");
|
final com.google.gson.JsonElement raw = command.parsedJson().getAsJsonObject().get(paramName);
|
||||||
final List<Long> ids = new java.util.ArrayList<>();
|
final List<Long> ids = new java.util.ArrayList<>();
|
||||||
if (raw != null && raw.isJsonArray()) {
|
if (raw != null && raw.isJsonArray()) {
|
||||||
for (final com.google.gson.JsonElement el : raw.getAsJsonArray()) {
|
for (final com.google.gson.JsonElement el : raw.getAsJsonArray()) {
|
||||||
@@ -563,13 +574,23 @@ public final class ProductPendingEditFieldMapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void writeChargesToJson(final JsonObject root, final List<ProductPendingEditCharge> charges) {
|
public static void writeChargesToJson(final JsonObject root, final List<ProductPendingEditCharge> charges) {
|
||||||
|
writeChargesToJson(root, charges, "charges");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same as {@link #writeChargesToJson(JsonObject, List)}, but writes under a caller-chosen field name.
|
||||||
|
* The replayed JSON built here is fed straight back into the same product-update parser the original
|
||||||
|
* submission went through, so the field name must match whatever that product type's own submit
|
||||||
|
* payload used -- {@code chargesSelected} for share products, {@code charges} for every other type.
|
||||||
|
*/
|
||||||
|
public static void writeChargesToJson(final JsonObject root, final List<ProductPendingEditCharge> charges, final String paramName) {
|
||||||
final JsonArray array = new JsonArray();
|
final JsonArray array = new JsonArray();
|
||||||
for (final ProductPendingEditCharge charge : charges) {
|
for (final ProductPendingEditCharge charge : charges) {
|
||||||
final JsonObject entry = new JsonObject();
|
final JsonObject entry = new JsonObject();
|
||||||
entry.addProperty("id", charge.getChargeId());
|
entry.addProperty("id", charge.getChargeId());
|
||||||
array.add(entry);
|
array.add(entry);
|
||||||
}
|
}
|
||||||
root.add("charges", array);
|
root.add(paramName, array);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- accountFacilityLimits (1:many, satellite table) -------------------------------------------
|
// ---- accountFacilityLimits (1:many, satellite table) -------------------------------------------
|
||||||
|
|||||||
+1
-1
@@ -355,7 +355,7 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
|
|||||||
* {@link #savingsOnHoldAmount}'s staff-placed lien, so the two read as genuinely different figures
|
* {@link #savingsOnHoldAmount}'s staff-placed lien, so the two read as genuinely different figures
|
||||||
* rather than one bucket doing double duty. */
|
* rather than one bucket doing double duty. */
|
||||||
@Column(name = "total_savings_amount_on_hold_clearing", scale = 6, precision = 19, nullable = true)
|
@Column(name = "total_savings_amount_on_hold_clearing", scale = 6, precision = 19, nullable = true)
|
||||||
private BigDecimal savingsOnHoldAmountClearing;
|
private BigDecimal savingsOnHoldAmountClearing = BigDecimal.ZERO;
|
||||||
|
|
||||||
@OneToMany(cascade = CascadeType.ALL, mappedBy = "account", orphanRemoval = true, fetch = FetchType.LAZY)
|
@OneToMany(cascade = CascadeType.ALL, mappedBy = "account", orphanRemoval = true, fetch = FetchType.LAZY)
|
||||||
protected List<InteropIdentifier> identifiers = new ArrayList<>();
|
protected List<InteropIdentifier> identifiers = new ArrayList<>();
|
||||||
|
|||||||
Reference in New Issue
Block a user