From c1e23080fc1a3bd23d91886eb3778b4b4e695d68 Mon Sep 17 00:00:00 2001 From: Jaanu423 Date: Wed, 17 Jun 2026 18:46:17 +0530 Subject: [PATCH] fix(share-account+clerk): remove cashier collection validation, add NPE guards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ShareAccountWritePlatformServiceJpaRepositoryImpl: - Remove m_share_cashier_collection validation from activateShareAccount() — cashier now enters GL directly via transaction entry workstation - Mark all share account charges as paid on activation instead of posting duplicate GL (cashier already posted DR Cash -> CR Charges GL directly) - Remove unused imports: java.sql.Date, EmptyResultDataAccessException, Collectors - Update createShareAccount() comment to reflect new cashier GL flow ClerkDisbursementRequestApiResource: - Add null guard for clerk_id before cast in approve() — returns 500 with clear message if DB record is missing the field - Add null/blank guard for credit_entries in approve() — returns 400 with clear message instead of NPE on gson.fromJson Co-Authored-By: Claude Sonnet 4.6 --- .../ClerkDisbursementRequestApiResource.java | 9 ++- ...WritePlatformServiceJpaRepositoryImpl.java | 67 ++++--------------- 2 files changed, 20 insertions(+), 56 deletions(-) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/clerkdisbursement/api/ClerkDisbursementRequestApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/clerkdisbursement/api/ClerkDisbursementRequestApiResource.java index 3d8a3f413..ad8d054c7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/clerkdisbursement/api/ClerkDisbursementRequestApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/clerkdisbursement/api/ClerkDisbursementRequestApiResource.java @@ -366,13 +366,20 @@ public class ClerkDisbursementRequestApiResource { if (!"PENDING".equals(record.get("status"))) { throw apiError(409, "Only PENDING requests can be approved. Current status: " + record.get("status")); } - final Long makerId = ((Number) record.get("clerk_id")).longValue(); + final Object clerkIdObj = record.get("clerk_id"); + if (clerkIdObj == null) { + throw apiError(500, "Disbursement request " + id + " has no clerk ID on record."); + } + final Long makerId = ((Number) clerkIdObj).longValue(); if (makerId.equals(currentUser.getId())) { throw apiError(403, "Maker-checker: you cannot approve a disbursement request you submitted yourself."); } final String creditEntriesJson = (String) record.get("credit_entries"); + if (creditEntriesJson == null || creditEntriesJson.isBlank()) { + throw apiError(400, "Disbursement request " + id + " has no credit entries."); + } final JsonArray creditEntries = gson.fromJson(creditEntriesJson, JsonArray.class); final Object ptId = record.get("payment_type_id"); final String description = (String) record.get("description"); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountWritePlatformServiceJpaRepositoryImpl.java index 1ea5985da..cc6664731 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountWritePlatformServiceJpaRepositoryImpl.java @@ -20,7 +20,6 @@ package org.apache.fineract.portfolio.shareaccounts.service; import jakarta.persistence.PersistenceException; import java.math.BigDecimal; -import java.sql.Date; import java.time.LocalDate; import java.util.ArrayList; import java.util.HashMap; @@ -28,7 +27,6 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; @@ -57,7 +55,6 @@ import org.apache.fineract.portfolio.shareaccounts.serialization.ShareAccountDat import org.apache.fineract.portfolio.shareproducts.domain.ShareProduct; import org.apache.fineract.portfolio.shareproducts.domain.ShareProductRepositoryWrapper; import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.orm.jpa.JpaSystemException; @@ -89,11 +86,10 @@ public class ShareAccountWritePlatformServiceJpaRepositoryImpl implements ShareA this.shareAccountRepository.saveAndFlush(account); generateAccountNumber(account); // GL entries are intentionally NOT posted at submission time. - // Cash is physically collected by the cashier AFTER submission via the - // /cashier/sharecollection endpoint, which posts: - // Dr Cash at Vault → Cr Share Sundry (share amount) - // Cr Income from Fees (charge amount) - // Posting GL here would cause a duplicate entry when cashier collects. + // Cashier collects cash and enters GL directly in the transaction entry + // workstation: DR Cash → CR Share Sundry GL (share amount) and + // DR Cash → CR Charges GL (charge amount). The approval step posts the + // share purchase GL (DR Share Sundry → CR Share Capital) via Fineract accounting. businessEventNotifierService.notifyPostBusinessEvent(new ShareAccountCreateBusinessEvent(account)); @@ -355,59 +351,20 @@ public class ShareAccountWritePlatformServiceJpaRepositoryImpl implements ShareA public CommandProcessingResult activateShareAccount(Long accountId, JsonCommand jsonCommand) { try { - // ── Validate: verified cashier collection record must exist ──────── - // Checks m_share_cashier_collection for a record where: - // 1. share_account_id = exactly this account (no cross-account match) - // 2. total_amount > 0 (real cash was received, not a zero entry) - // 3. collection_date within MAX_COLLECTION_AGE_DAYS (not a stale record) - final int maxAgeDays = 30; - final Date oldestValidDate = Date.valueOf(LocalDate.now().minusDays(maxAgeDays)); - - final Map collectionRecord; - try { - collectionRecord = jdbcTemplate.queryForMap( - "SELECT share_account_id, collected_by_name, collection_date, " - + " total_amount, gl_transaction_id " - + "FROM m_share_cashier_collection " - + "WHERE share_account_id = ? " - + " AND total_amount > 0 " - + " AND collection_date >= ?", - accountId, oldestValidDate); - } catch (EmptyResultDataAccessException e) { - throw new org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException( - "error.msg.share.activation.cashier.collection.required", - "Share account " + accountId + " cannot be activated: " - + "no cashier cash-collection record found within the last " - + maxAgeDays + " days. " - + "Please complete the Share Collection step in the Cashier menu first."); - } - - // Query BEFORE activation which charges were already paid via cashier collection. - // Cashier collection sets is_paid_derived = true; activation would otherwise - // post a duplicate Dr Cash → Cr Income entry for those same charges. - final Set cashierPaidChargeIds = new HashSet<>(jdbcTemplate.queryForList( - "SELECT id FROM m_share_account_charge " - + "WHERE account_id = ? AND is_paid_derived = true", - Long.class, accountId)); - ShareAccount account = this.shareAccountRepository.findOneWithNotFoundDetection(accountId); Map changes = this.accountDataSerializer.validateAndActivate(jsonCommand, account); if (!changes.isEmpty()) { this.shareAccountRepository.save(account); } - // Filter out charge transactions whose charges were already collected - // by the cashier — prevents duplicate GL entries on activation. - Set chargesForGL = account.getChargeTransactions().stream() - .filter(txn -> txn.getChargesPaidBy() == null - || txn.getChargesPaidBy().stream() - .noneMatch(cpb -> cashierPaidChargeIds.contains(cpb.getShareChargeId()))) - .collect(Collectors.toSet()); - - if (!chargesForGL.isEmpty()) { - this.journalEntryWritePlatformService - .createJournalEntriesForShares(populateJournalEntries(account, chargesForGL)); - } + // Cashier collected cash and entered GL directly via the transaction entry + // workstation (DR Cash → CR Share Sundry GL for share amount, and DR Cash → CR + // Charges GL for charges). Mark charges as paid to reflect cashier's collection + // without posting duplicate GL entries — the approval step already posted the + // share purchase GL (DR Share Sundry → CR Share Capital) via Fineract accounting. + jdbcTemplate.update( + "UPDATE m_share_account_charge SET is_paid_derived = true WHERE account_id = ?", + accountId); return new CommandProcessingResultBuilder() // .withCommandId(jsonCommand.commandId()) //