Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8724c54d9b | ||
|
|
5c0a482f49 | ||
|
|
0e07e60328 | ||
|
|
e227e8ae0f | ||
|
|
231919b62b |
+62
@@ -57,14 +57,23 @@ public class MobileLookupServiceImpl implements MobileLookupService {
|
||||
"9752123456", "9752345678", "9752987654"
|
||||
);
|
||||
|
||||
/** Demo numbers that always return DISCONNECTED (found on the MNRL) without calling the external API. */
|
||||
private static final Set<String> DEMO_DISCONNECTED_NUMBERS = Set.of("9000000001", "9000000002");
|
||||
|
||||
/** Demo numbers that always return SUSPECTED_AND_DISCONNECTED without calling the external API. */
|
||||
private static final Set<String> DEMO_SUSPECTED_AND_DISCONNECTED_NUMBERS = Set.of("9000000003");
|
||||
|
||||
private final String baseUrl;
|
||||
private final boolean enabled;
|
||||
private final RestTemplate restTemplate;
|
||||
private final MobileLookupLogRepository logRepository;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
public MobileLookupServiceImpl(@Value("${fineract.mobile-lookup.base-url}") String baseUrl,
|
||||
@Value("${fineract.mobile-lookup.enabled:true}") boolean enabled,
|
||||
MobileLookupLogRepository logRepository, JdbcTemplate jdbcTemplate) {
|
||||
this.baseUrl = baseUrl;
|
||||
this.enabled = enabled;
|
||||
this.restTemplate = new RestTemplate();
|
||||
this.logRepository = logRepository;
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
@@ -103,6 +112,39 @@ public class MobileLookupServiceImpl implements MobileLookupService {
|
||||
return MobileLookupResult.clean(logId, trimmed);
|
||||
}
|
||||
|
||||
// Demo numbers — return DISCONNECTED (on the MNRL) without calling the external API
|
||||
if (isDemoDisconnectedNumber(trimmed)) {
|
||||
LOG.info("Demo disconnected number detected: {}", trimmed);
|
||||
final MobileLookupDetailData disconnectedDetail = MobileLookupDetailData.forDisconnected(
|
||||
"Number revoked and listed on MNRL (demo data)", "DEMO");
|
||||
final Long logId = saveLog(trimmed, "DISCONNECTED", false, true,
|
||||
"Number is DISCONNECTED — found in MNRL records", null, source, clientId);
|
||||
return MobileLookupResult.disconnected(logId, trimmed, disconnectedDetail);
|
||||
}
|
||||
|
||||
// Demo numbers — return SUSPECTED_AND_DISCONNECTED without calling the external API
|
||||
if (isDemoSuspectedAndDisconnectedNumber(trimmed)) {
|
||||
LOG.info("Demo suspected+disconnected number detected: {}", trimmed);
|
||||
final MobileLookupDetailData suspectedDetail = MobileLookupDetailData.forSuspected(
|
||||
"active", "Demo DIP suspected number", "DEMO");
|
||||
final MobileLookupDetailData disconnectedDetail = MobileLookupDetailData.forDisconnected(
|
||||
"Number revoked and listed on MNRL (demo data)", "DEMO");
|
||||
final Long logId = saveLog(trimmed, "SUSPECTED_AND_DISCONNECTED", true, true,
|
||||
"Number is SUSPECTED and DISCONNECTED — found in both DIP and MNRL records", null, source, clientId);
|
||||
return MobileLookupResult.suspectedAndDisconnected(logId, trimmed, suspectedDetail, disconnectedDetail);
|
||||
}
|
||||
|
||||
// Lookup switched off by config (fineract.mobile-lookup.enabled=false) -- skip the external
|
||||
// call entirely rather than fail with a connection error. Logged distinctly from a genuine
|
||||
// CLEAN result so it can never be mistaken for one in an audit trail.
|
||||
if (!enabled) {
|
||||
LOG.info("DIP/MNRL lookup is disabled by configuration — skipping check for {}", trimmed);
|
||||
final Long logId = saveLog(trimmed, MobileLookupLog.STATUS_CLEAN, false, false,
|
||||
"DIP/MNRL lookup is disabled by configuration — the number was not actually checked.",
|
||||
"{\"provider\":\"LOOKUP_DISABLED_BY_CONFIG\"}", source, clientId);
|
||||
return MobileLookupResult.clean(logId, trimmed);
|
||||
}
|
||||
|
||||
final String url = baseUrl + "/api/v1/lookup/check?mobileNumber=" + trimmed;
|
||||
|
||||
LOG.info("Calling DIP/MNRL lookup API: {}", url);
|
||||
@@ -402,6 +444,26 @@ public class MobileLookupServiceImpl implements MobileLookupService {
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isDemoDisconnectedNumber(final String mobileNumber) {
|
||||
if (DEMO_DISCONNECTED_NUMBERS.contains(mobileNumber)) {
|
||||
return true;
|
||||
}
|
||||
if (mobileNumber.startsWith("91") && mobileNumber.length() > 10) {
|
||||
return DEMO_DISCONNECTED_NUMBERS.contains(mobileNumber.substring(2));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isDemoSuspectedAndDisconnectedNumber(final String mobileNumber) {
|
||||
if (DEMO_SUSPECTED_AND_DISCONNECTED_NUMBERS.contains(mobileNumber)) {
|
||||
return true;
|
||||
}
|
||||
if (mobileNumber.startsWith("91") && mobileNumber.length() > 10) {
|
||||
return DEMO_SUSPECTED_AND_DISCONNECTED_NUMBERS.contains(mobileNumber.substring(2));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String getStringOrNull(JsonObject json, String key) {
|
||||
JsonElement el = json.get(key);
|
||||
return (el != null && !el.isJsonNull()) ? el.getAsString() : null;
|
||||
|
||||
+69
@@ -18,6 +18,11 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.savings.api;
|
||||
|
||||
import static org.apache.fineract.infrastructure.documentmanagement.api.DocumentApiConstants.DOCUMENT_API_PARAM_DESCRIPTION;
|
||||
import static org.apache.fineract.infrastructure.documentmanagement.api.DocumentApiConstants.DOCUMENT_API_PARAM_FILE;
|
||||
import static org.apache.fineract.infrastructure.documentmanagement.api.DocumentApiConstants.DOCUMENT_API_PARAM_NAME;
|
||||
import static org.springframework.http.HttpHeaders.CONTENT_LENGTH;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
@@ -26,6 +31,7 @@ import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.DELETE;
|
||||
import jakarta.ws.rs.DefaultValue;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.HeaderParam;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.PUT;
|
||||
import jakarta.ws.rs.Path;
|
||||
@@ -34,9 +40,11 @@ import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.Context;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import java.io.InputStream;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
@@ -50,6 +58,10 @@ import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamE
|
||||
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
|
||||
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.api.DocumentApiResource;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentCreateResponse;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentData;
|
||||
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentDeleteResponse;
|
||||
import org.apache.fineract.infrastructure.security.exception.NoAuthorizationException;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.infrastructure.security.service.SqlValidator;
|
||||
@@ -63,6 +75,9 @@ import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformS
|
||||
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountEditWritePlatformService;
|
||||
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountWritePlatformService;
|
||||
import org.apache.fineract.portfolio.savings.service.CurrentAccountTemplateReadPlatformService;
|
||||
import org.glassfish.jersey.media.multipart.FormDataBodyPart;
|
||||
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
|
||||
import org.glassfish.jersey.media.multipart.FormDataParam;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Path("/v1/currentaccounts")
|
||||
@@ -84,6 +99,10 @@ public class CurrentAccountsApiResource {
|
||||
private final CurrentAccountEditWritePlatformService currentAccountEditWritePlatformService;
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
private final DefaultToApiJsonSerializer<CurrentAccountPendingEditData> pendingEditApiJsonSerializer;
|
||||
private final DocumentApiResource documentApiResource;
|
||||
|
||||
/** Matches the {@code entityType} current-account document requests are stored/looked up under. */
|
||||
private static final String DOCUMENT_ENTITY_TYPE = "currentaccounts";
|
||||
|
||||
@GET
|
||||
@Path("template")
|
||||
@@ -337,4 +356,54 @@ public class CurrentAccountsApiResource {
|
||||
return this.toApiJsonSerializer.serialize(result);
|
||||
}
|
||||
|
||||
// ── Documents ────────────────────────────────────────────────────────────
|
||||
// The generic /v1/{entityType}/{entityId}/documents resource (DocumentApiResource) never gets a
|
||||
// chance to handle current-account requests: this class's own literal /v1/currentaccounts path
|
||||
// is matched first for anything under {accountId}, and it has no "documents" method, so the
|
||||
// request dead-ends in a 404 instead of falling through. These delegate straight to the same
|
||||
// DocumentApiResource so current accounts get identical document-management behaviour.
|
||||
|
||||
@GET
|
||||
@Path("{accountId}/documents")
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "List documents attached to a current account")
|
||||
@ApiResponse(responseCode = "200", description = "OK")
|
||||
public List<DocumentData> retrieveDocuments(@PathParam("accountId") final Long accountId) {
|
||||
return documentApiResource.retrieveAllDocuments(DOCUMENT_ENTITY_TYPE, accountId);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{accountId}/documents/{documentId}/attachment")
|
||||
@Produces({ MediaType.APPLICATION_OCTET_STREAM })
|
||||
@Operation(summary = "Download a document attached to a current account")
|
||||
@ApiResponse(responseCode = "200", description = "Not Shown: The corresponding Binary file")
|
||||
public Response downloadDocument(@PathParam("accountId") final Long accountId, @PathParam("documentId") final Long documentId) {
|
||||
return documentApiResource.downloadFile(DOCUMENT_ENTITY_TYPE, accountId, documentId);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("{accountId}/documents")
|
||||
@Consumes({ MediaType.MULTIPART_FORM_DATA })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Attach a document to a current account")
|
||||
@ApiResponse(responseCode = "200", description = "Not Shown (multi-part form data)")
|
||||
public DocumentCreateResponse createDocument(@PathParam("accountId") final Long accountId,
|
||||
@HeaderParam(CONTENT_LENGTH) final Long fileSize, @FormDataParam(DOCUMENT_API_PARAM_FILE) final InputStream inputStream,
|
||||
@FormDataParam(DOCUMENT_API_PARAM_FILE) final FormDataContentDisposition fileDetails,
|
||||
@FormDataParam(DOCUMENT_API_PARAM_FILE) final FormDataBodyPart filePart,
|
||||
@FormDataParam(DOCUMENT_API_PARAM_NAME) final String name,
|
||||
@FormDataParam(DOCUMENT_API_PARAM_DESCRIPTION) final String description) {
|
||||
return documentApiResource.createDocument(DOCUMENT_ENTITY_TYPE, accountId, fileSize, inputStream, fileDetails, filePart, name,
|
||||
description);
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("{accountId}/documents/{documentId}")
|
||||
@Operation(summary = "Remove a document attached to a current account")
|
||||
@ApiResponse(responseCode = "200", description = "OK")
|
||||
public DocumentDeleteResponse deleteDocument(@PathParam("accountId") final Long accountId,
|
||||
@PathParam("documentId") final Long documentId) {
|
||||
return documentApiResource.deleteDocument(DOCUMENT_ENTITY_TYPE, accountId, documentId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+4
-1
@@ -435,7 +435,10 @@ public class FixedDepositAccount extends SavingsAccount {
|
||||
|
||||
@Override
|
||||
public Money activateWithBalance() {
|
||||
return Money.of(this.currency, this.accountTermAndPreClosure.depositAmount());
|
||||
// Activation never deposits money -- funding happens strictly through a separate deposit
|
||||
// transaction (posted by a clerk) after the account is already active, regardless of the
|
||||
// deposit amount configured on the account's term/pre-closure details.
|
||||
return Money.zero(this.currency);
|
||||
}
|
||||
|
||||
public void close(final AppUser currentUser, final JsonCommand command, final Map<String, Object> actualChanges) {
|
||||
|
||||
+4
-1
@@ -552,7 +552,10 @@ public class RecurringDepositAccount extends SavingsAccount {
|
||||
|
||||
@Override
|
||||
public Money activateWithBalance() {
|
||||
return Money.of(this.currency, this.minRequiredOpeningBalance);
|
||||
// Activation never deposits money -- funding happens strictly through a separate deposit
|
||||
// transaction (posted by a clerk) after the account is already active, regardless of any
|
||||
// minimum required opening balance the product has configured.
|
||||
return Money.zero(this.currency);
|
||||
}
|
||||
|
||||
protected void processAccountUponActivation(final DateTimeFormatter fmt, final boolean postReversals,
|
||||
|
||||
+4
-13
@@ -19,7 +19,6 @@
|
||||
package org.apache.fineract.portfolio.savings.domain;
|
||||
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.accountFacilityConfigParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.accountNoParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.accountStatusParamName;
|
||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.guardianClientIdParamName;
|
||||
@@ -455,18 +454,10 @@ public class SavingsAccountAssembler {
|
||||
account.setLinkedAccountType(this.fromApiJsonHelper.extractStringNamed(linkedAccountTypeParamName, element));
|
||||
account.setLinkedAccountNo(this.fromApiJsonHelper.extractStringNamed(linkedAccountNoParamName, element));
|
||||
|
||||
// Account Facility Config
|
||||
if (element.getAsJsonObject().has(accountFacilityConfigParamName)) {
|
||||
final JsonElement configEl = element.getAsJsonObject().get(accountFacilityConfigParamName);
|
||||
if (configEl != null && !configEl.isJsonNull() && configEl.isJsonObject()) {
|
||||
SavingsAccountFacilityConfig facilityConfig = account.getFacilityConfig();
|
||||
if (facilityConfig == null) {
|
||||
facilityConfig = new SavingsAccountFacilityConfig(account);
|
||||
account.setFacilityConfig(facilityConfig);
|
||||
}
|
||||
facilityConfig.updateFrom(configEl.getAsJsonObject());
|
||||
}
|
||||
}
|
||||
// Every digital facility starts disabled at account opening, regardless of anything the
|
||||
// request sends or the product defaults to -- they're switched on deliberately, one at a
|
||||
// time, later, through the account's own Edit Nominee & Features screen.
|
||||
account.setFacilityConfig(new SavingsAccountFacilityConfig(account));
|
||||
|
||||
// Joint holders
|
||||
if (element.getAsJsonObject().has(jointHoldersParamName)) {
|
||||
|
||||
+3
-11
@@ -133,18 +133,10 @@ public class CurrentAccountTransactionWritePlatformServiceImpl implements Curren
|
||||
"Insufficient available balance. Available: " + available + ", Requested: " + amount, accountId);
|
||||
}
|
||||
|
||||
// Minimum required balance validation
|
||||
// Minimum required balance is not enforced -- a withdrawal must never be blocked for
|
||||
// taking the account below its configured minimum, only for exceeding the available
|
||||
// balance (checked above).
|
||||
final BigDecimal newBalance = currentBalance.subtract(amount);
|
||||
if (account.isEnforceMinRequiredBalance() && account.getMinRequiredBalance() != null
|
||||
&& account.getMinRequiredBalance().compareTo(BigDecimal.ZERO) > 0) {
|
||||
if (newBalance.compareTo(account.getMinRequiredBalance()) < 0) {
|
||||
throw new GeneralPlatformDomainRuleException(
|
||||
"error.msg.current.account.min.required.balance.breached",
|
||||
"Withdrawal of " + amount + " would breach the minimum required balance of "
|
||||
+ account.getMinRequiredBalance() + ". Balance after withdrawal would be " + newBalance,
|
||||
accountId);
|
||||
}
|
||||
}
|
||||
|
||||
final String refNo = command.stringValueOfParameterNamed("accountNumber");
|
||||
final CurrentAccountTransaction txn = CurrentAccountTransaction.withdrawal(account, office, amount, txnDate, refNo);
|
||||
|
||||
+11
-151
@@ -227,12 +227,10 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri
|
||||
clientNomineeWritePlatformService.saveNomineeIfPresent(clientId, accountNo, "CB", json.get("nominee"));
|
||||
}
|
||||
|
||||
// Facility config — UI sends "accountFacilityConfig", internal uses "facilityConfig"
|
||||
JsonObject fcJson = json.getAsJsonObject("accountFacilityConfig");
|
||||
if (fcJson == null) fcJson = json.getAsJsonObject("facilityConfig");
|
||||
if (fcJson != null) {
|
||||
account.setFacilityConfig(assembleFacilityConfig(account, fcJson));
|
||||
}
|
||||
// Every digital facility starts disabled at account opening, regardless of anything the
|
||||
// request sends or the product defaults to -- they're switched on deliberately, one at a
|
||||
// time, later, through the account's own Edit Nominee & Features screen.
|
||||
account.setFacilityConfig(CurrentAccountFacilityConfig.createFor(account));
|
||||
|
||||
// Joint holders
|
||||
if (json.has("jointHolders") && json.get("jointHolders").isJsonArray()) {
|
||||
@@ -294,61 +292,16 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri
|
||||
final JsonObject json = command.parsedJson().getAsJsonObject();
|
||||
LocalDate activatedOnDate = parseDate(getString(json, "activatedOnDate"), getString(json, "dateFormat"), getString(json, "locale"));
|
||||
if (activatedOnDate == null) activatedOnDate = LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
// Validate minimum opening balance before activation
|
||||
java.math.BigDecimal openingBalanceValue = java.math.BigDecimal.ZERO;
|
||||
if (json.has("openingBalance") && !json.get("openingBalance").isJsonNull()) {
|
||||
try { openingBalanceValue = json.get("openingBalance").getAsBigDecimal(); } catch (Exception ignored) {
|
||||
// intentionally ignored
|
||||
}
|
||||
}
|
||||
final java.math.BigDecimal minRequired = account.getMinRequiredOpeningBalance();
|
||||
if (minRequired != null && minRequired.compareTo(java.math.BigDecimal.ZERO) > 0
|
||||
&& openingBalanceValue.compareTo(minRequired) < 0) {
|
||||
throw new org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException(
|
||||
java.util.List.of(org.apache.fineract.infrastructure.core.data.ApiParameterError.parameterError(
|
||||
"openingBalance",
|
||||
"Opening balance (" + openingBalanceValue + ") is less than minimum required opening balance (" + minRequired + ")",
|
||||
"validation.msg.currentaccount.openingBalance.less.than.min.required",
|
||||
openingBalanceValue, minRequired)));
|
||||
}
|
||||
|
||||
account.activate(activatedOnDate, user);
|
||||
applyFacilityChargesOnActivation(account);
|
||||
|
||||
java.math.BigDecimal currentBalance = java.math.BigDecimal.ZERO;
|
||||
final java.util.List<org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransaction> depositTxns = new java.util.ArrayList<>();
|
||||
final java.util.List<ChargeGlEntry> chargeEntries = new java.util.ArrayList<>();
|
||||
|
||||
// Opening balance deposit transaction
|
||||
if (openingBalanceValue.compareTo(java.math.BigDecimal.ZERO) > 0) {
|
||||
try {
|
||||
final org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransaction deposit =
|
||||
org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransaction
|
||||
.deposit(account, account.getOffice(), openingBalanceValue, activatedOnDate, "OPENING-BALANCE");
|
||||
deposit.setRunningBalanceDerived(openingBalanceValue);
|
||||
account.setTotalDepositsDerived(openingBalanceValue);
|
||||
currentAccountTransactionRepository.saveAndFlush(deposit);
|
||||
currentBalance = openingBalanceValue;
|
||||
depositTxns.add(deposit);
|
||||
log.info("Opening balance deposit {} created for account id={}", openingBalanceValue, accountId);
|
||||
} catch (Exception e) {
|
||||
log.warn("Could not process openingBalance for account id={}: {}", accountId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-deduct activation charges (chargeTimeEnum=3) from opening balance
|
||||
final java.util.List<org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountCharge> activationCharges =
|
||||
account.getCharges().stream()
|
||||
.filter(c -> c.getChargeTimeEnum() != null && c.getChargeTimeEnum() == 3 && !c.isPaid())
|
||||
.toList();
|
||||
currentBalance = collectChargesAndPost(account, activationCharges, currentBalance, activatedOnDate, chargeEntries);
|
||||
|
||||
account.setAccountBalanceDerived(currentBalance);
|
||||
// Activation posts no transaction of its own -- no opening-balance deposit, no
|
||||
// digital-facility charges applied or collected, regardless of what the account's facility
|
||||
// config or product defaults say. Funding and any facility charges happen strictly
|
||||
// afterwards, through separate transactions a clerk processes once the account is already
|
||||
// active.
|
||||
currentAccountRepository.saveAndFlush(account);
|
||||
|
||||
// Post GL journal entries — deposits in batch, each charge individually
|
||||
postGlEntries(account, depositTxns, chargeEntries);
|
||||
|
||||
return new CommandProcessingResultBuilder().withEntityId(accountId).build();
|
||||
}
|
||||
|
||||
@@ -637,78 +590,6 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri
|
||||
}
|
||||
}
|
||||
|
||||
private void applyFacilityChargesOnActivation(final CurrentAccount account) {
|
||||
// Use account-level facility config — reflects what was selected at account opening time
|
||||
org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountFacilityConfig config =
|
||||
account.getFacilityConfig();
|
||||
|
||||
log.info("[FacilityCharge] Activating account id={}, facilityConfig={}",
|
||||
account.getId(), config != null ? "FOUND" : "NULL");
|
||||
|
||||
if (config == null) {
|
||||
// Fallback: use product-level facility config
|
||||
log.info("[FacilityCharge] No account-level config, falling back to product id={}", account.getProductId());
|
||||
config = currentProductRepository.findById(account.getProductId())
|
||||
.map(p -> {
|
||||
final var pc = p.getFacilityConfig();
|
||||
if (pc == null) {
|
||||
log.info("[FacilityCharge] Product has no facility config either");
|
||||
return null;
|
||||
}
|
||||
// Map product config to account config fields inline
|
||||
org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountFacilityConfig ac =
|
||||
new org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountFacilityConfig();
|
||||
ac.setAtmFacility(Boolean.TRUE.equals(pc.getAtmFacility()));
|
||||
ac.setAtmWithdrawalFacility(Boolean.TRUE.equals(pc.getAtmWithdrawalFacility()));
|
||||
ac.setUpiFacility(Boolean.TRUE.equals(pc.getUpiFacility()));
|
||||
ac.setPosTransactionsFacility(Boolean.TRUE.equals(pc.getPosTransactionsFacility()));
|
||||
ac.setChequeBook(Boolean.TRUE.equals(pc.getChequeBook()));
|
||||
ac.setImpsFacility(Boolean.TRUE.equals(pc.getImpsFacility()));
|
||||
ac.setSmsAlerts(Boolean.TRUE.equals(pc.getSmsAlerts()));
|
||||
ac.setBbpsFacility(Boolean.TRUE.equals(pc.getBbpsFacility()));
|
||||
ac.setInsuranceCover(Boolean.TRUE.equals(pc.getInsuranceCover()));
|
||||
ac.setBepgFacility(Boolean.TRUE.equals(pc.getBepgFacility()));
|
||||
ac.setNeftFacility(Boolean.TRUE.equals(pc.getNeftFacility()));
|
||||
ac.setRtgsFacility(Boolean.TRUE.equals(pc.getRtgsFacility()));
|
||||
ac.setInterBankTotalCapFacility(Boolean.TRUE.equals(pc.getInterBankTotalCapFacility()));
|
||||
ac.setWithdrawalFacility(Boolean.TRUE.equals(pc.getWithdrawalFacility()));
|
||||
ac.setDepositFacilityKyc(Boolean.TRUE.equals(pc.getDepositFacilityKyc()));
|
||||
ac.setVirtualDebitCard(Boolean.TRUE.equals(pc.getVirtualDebitCard()));
|
||||
return ac;
|
||||
}).orElse(null);
|
||||
}
|
||||
|
||||
if (config == null) {
|
||||
log.warn("[FacilityCharge] No facility config found for account id={} — no charges applied", account.getId());
|
||||
return;
|
||||
}
|
||||
|
||||
final org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountFacilityConfig fc = config;
|
||||
log.info("[FacilityCharge] Config found — ATM={} UPI={} NEFT={} RTGS={} CHEQUE={} SMS={} IMPs={} POS={} BBPS={} INS={} BEPG={} IBCAP={} WD={} KYC={} VCARD={} ATM_WD={}",
|
||||
fc.isAtmFacility(), fc.isUpiFacility(), fc.isNeftFacility(), fc.isRtgsFacility(),
|
||||
fc.isChequeBook(), fc.isSmsAlerts(), fc.isImpsFacility(), fc.isPosTransactionsFacility(),
|
||||
fc.isBbpsFacility(), fc.isInsuranceCover(), fc.isBepgFacility(), fc.isInterBankTotalCapFacility(),
|
||||
fc.isWithdrawalFacility(), fc.isDepositFacilityKyc(), fc.isVirtualDebitCard(), fc.isAtmWithdrawalFacility());
|
||||
applyMapped(account, fc.isAtmFacility(), "ATM_FACILITY");
|
||||
applyMapped(account, fc.isAtmWithdrawalFacility(), "ATM_WITHDRAWAL");
|
||||
applyMapped(account, fc.isUpiFacility(), "UPI_FACILITY");
|
||||
applyMapped(account, fc.isPosTransactionsFacility(), "POS_TRANSACTIONS");
|
||||
// Cheque book: no longer charged at activation — the per-leaf issuance charge is now deducted
|
||||
// when a cheque book is actually issued (ChequeBookIssuanceChargeService), not just for having
|
||||
// the facility enabled.
|
||||
applyMapped(account, fc.isImpsFacility(), "IMPS_FACILITY");
|
||||
applyMapped(account, fc.isSmsAlerts(), "SMS_ALERTS");
|
||||
applyMapped(account, fc.isBbpsFacility(), "BBPS_FACILITY");
|
||||
applyMapped(account, fc.isInsuranceCover(), "INSURANCE_COVER");
|
||||
applyMapped(account, fc.isBepgFacility(), "BEPG_FACILITY");
|
||||
applyMapped(account, fc.isNeftFacility(), "NEFT_FACILITY");
|
||||
applyMapped(account, fc.isRtgsFacility(), "RTGS_FACILITY");
|
||||
applyMapped(account, fc.isInterBankTotalCapFacility(),"INTER_BANK_TOTAL_CAP");
|
||||
applyMapped(account, fc.isWithdrawalFacility(), "WITHDRAWAL_FACILITY");
|
||||
applyMapped(account, fc.isDepositFacilityKyc(), "DEPOSIT_FACILITY_KYC");
|
||||
applyMapped(account, fc.isVirtualDebitCard(), "VIRTUAL_DEBIT_CARD");
|
||||
}
|
||||
|
||||
// Wrapper to carry a charge transaction together with its charge entity for GL posting
|
||||
private record ChargeGlEntry(
|
||||
org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransaction txn,
|
||||
@@ -717,9 +598,8 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri
|
||||
/**
|
||||
* Marks each of the given (unpaid) charges as paid, deducts its amount from {@code runningBalance}, creates a
|
||||
* {@code CurrentAccountTransaction.chargeDeduction(...)} row for it, and records a {@link ChargeGlEntry} for GL
|
||||
* posting. Shared by {@link #activate(Long, JsonCommand)} (activation-time charges) and
|
||||
* {@link #applyAndCollectFacilityCharges(Long, java.util.Set)} (edit-approval-time charges) so the
|
||||
* charge-collection logic isn't duplicated. Returns the resulting balance.
|
||||
* posting. Used by {@link #applyAndCollectFacilityCharges(Long, java.util.Set)} (edit-approval-time charges) --
|
||||
* activation itself no longer collects any charges. Returns the resulting balance.
|
||||
*/
|
||||
private java.math.BigDecimal collectChargesAndPost(final CurrentAccount account,
|
||||
final java.util.List<org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountCharge> charges,
|
||||
@@ -875,24 +755,4 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri
|
||||
}
|
||||
}
|
||||
|
||||
private void applyMapped(final CurrentAccount account, final boolean enabled, final String facilityType) {
|
||||
if (!enabled) {
|
||||
log.debug("[FacilityCharge] {} disabled — skip", facilityType);
|
||||
return;
|
||||
}
|
||||
final var mappingOpt = facilityChargeMappingRepository.findByFacilityType(facilityType);
|
||||
if (mappingOpt.isEmpty()) {
|
||||
log.warn("[FacilityCharge] {} enabled but NO mapping found in m_facility_charge_mapping", facilityType);
|
||||
return;
|
||||
}
|
||||
final org.apache.fineract.portfolio.charge.domain.Charge charge = mappingOpt.get().getCharge();
|
||||
if (charge == null) {
|
||||
log.warn("[FacilityCharge] {} mapping exists but charge_id is NULL", facilityType);
|
||||
return;
|
||||
}
|
||||
log.info("[FacilityCharge] Applying charge '{}' (id={}) for facility {}", charge.getName(), charge.getId(), facilityType);
|
||||
account.getCharges().add(
|
||||
org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountCharge
|
||||
.createFromFacility(account, charge));
|
||||
}
|
||||
}
|
||||
|
||||
+40
-6
@@ -172,8 +172,14 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
|
||||
|
||||
if (portfolioAccountData == null) {
|
||||
final PaymentDetail paymentDetail = null;
|
||||
this.depositAccountDomainService.handleFDDeposit(account, fmt, account.getActivationDate(),
|
||||
amountForDeposit.getAmount(), paymentDetail);
|
||||
final SavingsAccountTransaction depositTransaction = this.depositAccountDomainService.handleFDDeposit(account, fmt,
|
||||
account.getActivationDate(), amountForDeposit.getAmount(), paymentDetail);
|
||||
// handleFDDeposit() already posts this transaction's own journal entries internally --
|
||||
// mark it as "existing" so the postJournalEntries(...) call below (which covers other
|
||||
// activation-time changes, e.g. an activation charge payment) does not post it again.
|
||||
if (depositTransaction != null && depositTransaction.getId() != null) {
|
||||
existingTransactionIds.add(depositTransaction.getId());
|
||||
}
|
||||
} else {
|
||||
final SavingsAccount fromSavingsAccount = null;
|
||||
boolean isRegularTransaction = false;
|
||||
@@ -202,8 +208,17 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
|
||||
}
|
||||
|
||||
final boolean isPreMatureClosure = false;
|
||||
account.updateMaturityDateAndAmount(mc, isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd,
|
||||
financialYearBeginningMonth);
|
||||
if (amountForDeposit.isGreaterThanZero()) {
|
||||
account.updateMaturityDateAndAmount(mc, isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd,
|
||||
financialYearBeginningMonth);
|
||||
} else {
|
||||
// No deposit has actually landed on the account yet -- project maturity using the
|
||||
// configured deposit amount (the same approach used for the pre-activation preview and
|
||||
// for reinvested FDs) instead of the account's real, still-empty transaction history,
|
||||
// which would otherwise price in zero interest.
|
||||
account.updateMaturityDateAndAmountBeforeAccountActivation(mc, isPreMatureClosure,
|
||||
isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth);
|
||||
}
|
||||
List<DepositAccountOnHoldTransaction> depositAccountOnHoldTransactions = null;
|
||||
if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) {
|
||||
depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository
|
||||
@@ -275,8 +290,14 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
|
||||
final PortfolioAccountData portfolioAccountData = this.accountAssociationsReadPlatformService
|
||||
.retriveSavingsLinkedAssociation(savingsId);
|
||||
if (portfolioAccountData == null) {
|
||||
this.depositAccountDomainService.handleRDDeposit(account, fmt, account.getActivationDate(),
|
||||
amountForDeposit.getAmount(), null, isRegularTransaction);
|
||||
final SavingsAccountTransaction depositTransaction = this.depositAccountDomainService.handleRDDeposit(account, fmt,
|
||||
account.getActivationDate(), amountForDeposit.getAmount(), null, isRegularTransaction);
|
||||
// handleRDDeposit() already posts this transaction's own journal entries internally --
|
||||
// mark it as "existing" so the postJournalEntries(...) call below (which covers other
|
||||
// activation-time changes) does not post it again.
|
||||
if (depositTransaction != null && depositTransaction.getId() != null) {
|
||||
existingTransactionIds.add(depositTransaction.getId());
|
||||
}
|
||||
} else {
|
||||
final boolean isExceptionForBalanceCheck = false;
|
||||
final SavingsAccount fromSavingsAccount = null;
|
||||
@@ -368,6 +389,19 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
|
||||
final SavingsAccountTransaction deposit = this.depositAccountDomainService.handleFDDeposit(account, fmt, transactionDate,
|
||||
transactionAmount, paymentDetail);
|
||||
|
||||
// The account may have been activated with no funds yet (opening deposit is now a separate,
|
||||
// later step) -- in that case its maturity amount was only ever projected off the configured
|
||||
// deposit amount, with zero real transactions. Now that money has actually landed, recompute
|
||||
// maturity date/amount off the account's real transaction history so it reflects reality.
|
||||
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
|
||||
.isSavingsInterestPostingAtCurrentPeriodEnd();
|
||||
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
|
||||
final MathContext mc = MathContext.DECIMAL64;
|
||||
final boolean isPreMatureClosure = false;
|
||||
account.updateMaturityDateAndAmount(mc, isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd,
|
||||
financialYearBeginningMonth);
|
||||
this.savingAccountRepositoryWrapper.saveAndFlush(account);
|
||||
|
||||
this.denominationBreakdownWriter.save(command, deposit.getId(), account.getId(), account.getAccountNumber(),
|
||||
account.clientId(), account.getClient() != null ? account.getClient().getCifNumber() : null, transactionDate,
|
||||
"DEPOSIT");
|
||||
|
||||
+7
-1
@@ -220,10 +220,16 @@ public class SavingsAccountEditWritePlatformServiceJpaRepositoryImpl implements
|
||||
this.savingsAccountWritePlatformService.releaseAmount(savingsId, pendingEdit.getHoldTransactionId());
|
||||
}
|
||||
|
||||
// 2) Post the real charge(s) for newly-enabled facilities, today.
|
||||
// 2) Post the real charge(s) for newly-enabled facilities, today -- and post the matching
|
||||
// GL journal entries for those charge transactions (collectCharges() only books them
|
||||
// against the account itself; it does not touch the general ledger).
|
||||
if (!newlyEnabledFacilityTypes.isEmpty()) {
|
||||
final Set<Long> existingTransactionIds = new HashSet<>(account.findExistingTransactionIds());
|
||||
final Set<Long> existingReversedTransactionIds = new HashSet<>(account.findExistingReversedTransactionIds());
|
||||
final var chargesAdded = this.facilityChargeAutoApplyService.applyChargesForFacilities(account, newlyEnabledFacilityTypes);
|
||||
this.facilityChargeAutoApplyService.collectCharges(account, chargesAdded, DateUtils.getBusinessLocalDate());
|
||||
this.savingsAccountWritePlatformService.postJournalEntriesForSavingsAccount(account, existingTransactionIds,
|
||||
existingReversedTransactionIds, false);
|
||||
}
|
||||
|
||||
// 3) Apply the facility-config toggles to the real row.
|
||||
|
||||
+13
-27
@@ -97,7 +97,6 @@ import org.apache.fineract.portfolio.note.domain.Note;
|
||||
import org.apache.fineract.portfolio.note.domain.NoteRepository;
|
||||
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
|
||||
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
|
||||
import org.apache.fineract.portfolio.savings.service.FacilityChargeAutoApplyService;
|
||||
import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType;
|
||||
import org.apache.fineract.portfolio.savings.SavingsApiConstants;
|
||||
import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues;
|
||||
@@ -167,7 +166,6 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
|
||||
private final GSIMRepositoy gsimRepository;
|
||||
private final SavingsAccountInterestPostingService savingsAccountInterestPostingService;
|
||||
private final ErrorHandler errorHandler;
|
||||
private final FacilityChargeAutoApplyService facilityChargeAutoApplyService;
|
||||
private final DenominationBreakdownWriter denominationBreakdownWriter;
|
||||
|
||||
@Transactional
|
||||
@@ -203,23 +201,9 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
|
||||
|
||||
final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false);
|
||||
|
||||
// Validate minimum opening balance
|
||||
BigDecimal openingBalanceForValidation = BigDecimal.ZERO;
|
||||
if (command.parameterExists("openingBalance")) {
|
||||
final BigDecimal ob = command.bigDecimalValueOfParameterNamed("openingBalance");
|
||||
if (ob != null) openingBalanceForValidation = ob;
|
||||
}
|
||||
final BigDecimal minRequiredOpening = account.getMinRequiredOpeningBalance();
|
||||
if (minRequiredOpening != null && minRequiredOpening.compareTo(BigDecimal.ZERO) > 0
|
||||
&& openingBalanceForValidation.compareTo(minRequiredOpening) < 0) {
|
||||
final List<ApiParameterError> errors = new ArrayList<>();
|
||||
errors.add(ApiParameterError.parameterError(
|
||||
"openingBalance",
|
||||
"Opening balance (" + openingBalanceForValidation + ") is less than minimum required opening balance (" + minRequiredOpening + ")",
|
||||
"validation.msg.savingsaccount.openingBalance.less.than.min.required",
|
||||
openingBalanceForValidation, minRequiredOpening));
|
||||
throw new PlatformApiDataValidationException(errors);
|
||||
}
|
||||
// Minimum required opening balance is not enforced -- activation must never be blocked
|
||||
// for not meeting it; the account can be funded afterwards by whatever deposit the
|
||||
// customer brings (cash or a transfer), on their own timeline.
|
||||
|
||||
checkClientOrGroupActive(account);
|
||||
final Set<Long> existingTransactionIds = new HashSet<>();
|
||||
@@ -236,14 +220,10 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
|
||||
final Locale locale = command.extractLocale();
|
||||
final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale);
|
||||
processPostActiveActions(account, fmt, existingTransactionIds, existingReversedTransactionIds);
|
||||
// Auto-apply facility charges based on product facility config
|
||||
facilityChargeAutoApplyService.applyFacilityChargesOnActivation(account);
|
||||
this.savingAccountRepositoryWrapper.saveAndFlush(account);
|
||||
// Collect any newly added facility activation charges (after flush so amountOutstanding is set)
|
||||
// payCharge() internally adds transactions to account.transactions via handleChargeTransactions
|
||||
// Do NOT add to existingTransactionIds — postJournalEntries must include them for GL posting
|
||||
facilityChargeAutoApplyService.collectActivationCharges(account);
|
||||
this.savingAccountRepositoryWrapper.saveAndFlush(account);
|
||||
// Activation posts no transaction of its own -- digital-facility charges are no longer
|
||||
// auto-applied and auto-collected here. Whatever facilities are turned on later get
|
||||
// charged (if at all) through the normal charge/collection flow at that time, not as a
|
||||
// side effect of activation.
|
||||
}
|
||||
|
||||
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false);
|
||||
@@ -1521,6 +1501,12 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
|
||||
this.journalEntryWritePlatformService.createJournalEntriesForSavings(accountingBridgeData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postJournalEntriesForSavingsAccount(final SavingsAccount account, final Set<Long> existingTransactionIds,
|
||||
final Set<Long> existingReversedTransactionIds, final boolean backdatedTxnsAllowedTill) {
|
||||
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, backdatedTxnsAllowedTill);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandProcessingResult inactivateCharge(final Long savingsAccountId, final Long savingsAccountChargeId) {
|
||||
|
||||
|
||||
+2
-3
@@ -463,8 +463,7 @@ public class SavingsConfiguration {
|
||||
EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService, AppUserRepositoryWrapper appuserRepository,
|
||||
StandingInstructionRepository standingInstructionRepository, BusinessEventNotifierService businessEventNotifierService,
|
||||
GSIMRepositoy gsimRepository, SavingsAccountInterestPostingService savingsAccountInterestPostingService,
|
||||
ErrorHandler errorHandler, FacilityChargeAutoApplyService facilityChargeAutoApplyService,
|
||||
DenominationBreakdownWriter denominationBreakdownWriter) {
|
||||
ErrorHandler errorHandler, DenominationBreakdownWriter denominationBreakdownWriter) {
|
||||
return new SavingsAccountWritePlatformServiceJpaRepositoryImpl(context, fromApiJsonDeserializer, savingAccountRepositoryWrapper,
|
||||
staffRepository, savingsAccountTransactionRepository, savingAccountAssembler, savingsAccountTransactionDataValidator,
|
||||
savingsAccountChargeDataValidator, paymentDetailWritePlatformService, journalEntryWritePlatformService,
|
||||
@@ -472,7 +471,7 @@ public class SavingsConfiguration {
|
||||
chargeRepository, savingsAccountChargeRepository, holidayRepository, workingDaysRepository, configurationDomainService,
|
||||
depositAccountOnHoldTransactionRepository, entityDatatableChecksWritePlatformService, appuserRepository,
|
||||
standingInstructionRepository, businessEventNotifierService, gsimRepository, savingsAccountInterestPostingService,
|
||||
errorHandler, facilityChargeAutoApplyService, denominationBreakdownWriter);
|
||||
errorHandler, denominationBreakdownWriter);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -688,6 +688,13 @@ fineract.openproject.api-token=${FINERACT_OPENPROJECT_API_TOKEN:57dd9f933f113c72
|
||||
# │ DIP/MNRL provider. Do not ship the mock. │
|
||||
# └──────────────────────────────────────────────────────────────────────────┘
|
||||
fineract.mobile-lookup.base-url=${FINERACT_MOBILE_LOOKUP_BASE_URL:http://127.0.0.1:8090}
|
||||
# Set to false to skip the DIP/MNRL check entirely (no HTTP call at all, no need to run the mock
|
||||
# service) -- every number is then logged as CLEAN but stamped "LOOKUP_DISABLED_BY_CONFIG" so it is
|
||||
# never mistaken for a real pass.
|
||||
# Currently OFF by default for local work so nobody needs the mock service running. Set
|
||||
# FINERACT_MOBILE_LOOKUP_ENABLED=true (or flip the default below) before UAT/production, where the
|
||||
# check must actually run.
|
||||
fineract.mobile-lookup.enabled=${FINERACT_MOBILE_LOOKUP_ENABLED:false}
|
||||
|
||||
# ── Login OTP (2FA) — uses LincPay credentials above ─────────────────────────
|
||||
fineract.security.otp.lincpay.generate-path=${FINERACT_OTP_LINCPAY_GENERATE_PATH:/mobile/generate-otp}
|
||||
|
||||
+7
-4
@@ -3008,7 +3008,10 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
|
||||
}
|
||||
|
||||
public Money activateWithBalance() {
|
||||
return Money.of(this.currency, this.minRequiredOpeningBalance);
|
||||
// Activation never deposits money -- funding happens strictly through a separate deposit
|
||||
// transaction (posted by a clerk) after the account is already active, regardless of any
|
||||
// minimum required opening balance the product has configured.
|
||||
return Money.zero(this.currency);
|
||||
}
|
||||
|
||||
public void approveAndActivateApplication(final LocalDate appliedonDate, final AppUser appliedBy) {
|
||||
@@ -3700,10 +3703,10 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
|
||||
}
|
||||
|
||||
private Money minRequiredBalanceDerived(final MonetaryCurrency currency) {
|
||||
// Minimum required balance is not enforced -- withdrawals (and every other balance check
|
||||
// that goes through this) must never be blocked by a configured minimum balance. Overdraft
|
||||
// headroom is a separate concept and still applies.
|
||||
Money minReqBalance = Money.zero(currency);
|
||||
if (this.enforceMinRequiredBalance) {
|
||||
minReqBalance = minReqBalance.plus(this.minRequiredBalance);
|
||||
}
|
||||
if (this.allowOverdraft) {
|
||||
minReqBalance = minReqBalance.minus(this.overdraftLimit);
|
||||
}
|
||||
|
||||
+5
-1
@@ -160,7 +160,11 @@ public class FacilityChargeAutoApplyService {
|
||||
}
|
||||
}
|
||||
if (!transactions.isEmpty()) {
|
||||
savingsAccountTransactionRepository.saveAll(transactions);
|
||||
// Flush immediately (not just save) so each transaction gets its DB-generated id right
|
||||
// away -- the caller posts GL journal entries for these transactions straight after this
|
||||
// call returns, and that requires a real id on each one, not one that only appears at
|
||||
// end-of-transaction commit.
|
||||
savingsAccountTransactionRepository.saveAllAndFlush(transactions);
|
||||
account.recalculateBalancesAfterFacilityCharges(transactionDate);
|
||||
}
|
||||
return transactions;
|
||||
|
||||
+3
@@ -79,6 +79,9 @@ public interface SavingsAccountWritePlatformService {
|
||||
void processPostActiveActions(SavingsAccount account, DateTimeFormatter fmt, Set<Long> existingTransactionIds,
|
||||
Set<Long> existingReversedTransactionIds);
|
||||
|
||||
void postJournalEntriesForSavingsAccount(SavingsAccount account, Set<Long> existingTransactionIds,
|
||||
Set<Long> existingReversedTransactionIds, boolean backdatedTxnsAllowedTill);
|
||||
|
||||
CommandProcessingResult modifyWithHoldTax(Long savingsAccountId, JsonCommand command);
|
||||
|
||||
void setSubStatusInactive(Long savingsId);
|
||||
|
||||
Reference in New Issue
Block a user