fix(share-account+clerk): remove cashier collection validation, add NPE guards
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
7354da8c1e
commit
c1e23080fc
+8
-1
@@ -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");
|
||||
|
||||
+12
-55
@@ -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<String, Object> 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<Long> 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<String, Object> 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<ShareAccountTransaction> 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()) //
|
||||
|
||||
Reference in New Issue
Block a user