feat: denomination save+validation for all deposit types + cash exchange backend

- DenominationBreakdownWriter helper: validates breakdown total == amount and
  persists per-denomination rows; wired into current, FD and RD deposit flows
  (savings already had inline save) so all 4 deposit types now record the
  cash denomination breakdown.
- CIF on denomination rows now sourced from the client (getCifNumber) instead
  of the account external id.
- Allow denominationBreakdown param in the FD/RD transaction validator.
- Cash Exchange backend: m_cash_exchange + m_cash_exchange_detail tables,
  entities, repositories and JAX-RS API with received==given validation;
  registered the 0013 migration in changelog-tenant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jaanu423
2026-05-27 18:36:00 +05:30
co-authored by Claude Opus 4.7
parent ee915e395a
commit 9a30ede278
15 changed files with 2355 additions and 1698 deletions
@@ -0,0 +1,199 @@
package org.apache.fineract.portfolio.cashexchange.api;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.cashexchange.domain.CashExchange;
import org.apache.fineract.portfolio.cashexchange.domain.CashExchangeDetail;
import org.apache.fineract.portfolio.cashexchange.domain.CashExchangeDetailRepository;
import org.apache.fineract.portfolio.cashexchange.domain.CashExchangeRepository;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.stereotype.Component;
@Path("/v1/cashexchange")
@Component
@RequiredArgsConstructor
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public class CashExchangeApiResource {
private final CashExchangeRepository repository;
private final CashExchangeDetailRepository detailRepository;
private final PlatformSecurityContext context;
private final Gson gson = new Gson();
@POST
public String create(String body) {
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
final Long accountId = json.has("accountId") && !json.get("accountId").isJsonNull() ? json.get("accountId").getAsLong() : null;
final String accountNo = json.has("accountNo") && !json.get("accountNo").isJsonNull() ? json.get("accountNo").getAsString()
: null;
final Long clientId = json.has("clientId") && !json.get("clientId").isJsonNull() ? json.get("clientId").getAsLong() : null;
final LocalDate exchangeDate = json.has("exchangeDate") && !json.get("exchangeDate").isJsonNull()
? LocalDate.parse(json.get("exchangeDate").getAsString())
: null;
final String note = json.has("note") && !json.get("note").isJsonNull() ? json.get("note").getAsString() : null;
final JsonArray received = json.has("received") && json.get("received").isJsonArray() ? json.getAsJsonArray("received")
: new JsonArray();
final JsonArray given = json.has("given") && json.get("given").isJsonArray() ? json.getAsJsonArray("given") : new JsonArray();
final BigDecimal receivedTotal = sumTotal(received);
final BigDecimal givenTotal = sumTotal(given);
if (!hasPositiveCount(received) || !hasPositiveCount(given)) {
throw new PlatformDataIntegrityException("error.msg.cashexchange.empty",
"Both received and given denominations are required.");
}
if (receivedTotal.compareTo(givenTotal) != 0) {
throw new PlatformDataIntegrityException("error.msg.cashexchange.mismatch",
"Received total (" + receivedTotal + ") must equal given total (" + givenTotal + ").");
}
if (receivedTotal.compareTo(BigDecimal.ZERO) <= 0) {
throw new PlatformDataIntegrityException("error.msg.cashexchange.empty",
"Both received and given denominations are required.");
}
CashExchange header = new CashExchange();
header.setAccountId(accountId);
header.setAccountNo(accountNo);
header.setClientId(clientId);
header.setExchangeAmount(receivedTotal);
header.setExchangeDate(exchangeDate);
header.setNote(note);
header.setCreatedAt(LocalDateTime.now());
try {
AppUser user = this.context.authenticatedUser();
if (user != null) {
header.setPerformedBy(user.getId());
header.setPerformedByUsername(user.getUsername());
}
} catch (Exception e) {
// resolving the current user must never block recording the exchange
}
CashExchange saved = repository.save(header);
saveDetails(saved.getId(), "RECEIVED", received);
saveDetails(saved.getId(), "GIVEN", given);
JsonObject result = new JsonObject();
result.addProperty("resourceId", saved.getId());
return gson.toJson(result);
}
@GET
public String getAll(@QueryParam("accountId") Long accountId) {
List<CashExchange> list = accountId != null ? repository.findByAccountIdOrderByCreatedAtDesc(accountId)
: repository.findAllByOrderByCreatedAtDesc();
return gson.toJson(list.stream().map(this::toMap).collect(Collectors.toList()));
}
@GET
@Path("{id}")
public String getOne(@PathParam("id") Long id) {
CashExchange header = repository.findById(id)
.orElseThrow(() -> new RuntimeException("Cash exchange not found: " + id));
Map<String, Object> map = toMap(header);
List<Map<String, Object>> details = detailRepository.findByExchangeId(id).stream().map(this::detailToMap)
.collect(Collectors.toList());
map.put("details", details);
return gson.toJson(map);
}
private BigDecimal sumTotal(JsonArray items) {
BigDecimal total = BigDecimal.ZERO;
for (JsonElement el : items) {
JsonObject item = el.getAsJsonObject();
int value = item.has("denominationValue") && !item.get("denominationValue").isJsonNull()
? item.get("denominationValue").getAsInt()
: 0;
int count = item.has("count") && !item.get("count").isJsonNull() ? item.get("count").getAsInt() : 0;
if (count > 0) {
total = total.add(BigDecimal.valueOf((long) value * count));
}
}
return total;
}
private boolean hasPositiveCount(JsonArray items) {
for (JsonElement el : items) {
JsonObject item = el.getAsJsonObject();
int count = item.has("count") && !item.get("count").isJsonNull() ? item.get("count").getAsInt() : 0;
if (count > 0) {
return true;
}
}
return false;
}
private void saveDetails(Long exchangeId, String direction, JsonArray items) {
for (JsonElement el : items) {
JsonObject item = el.getAsJsonObject();
int count = item.has("count") && !item.get("count").isJsonNull() ? item.get("count").getAsInt() : 0;
if (count <= 0) {
continue;
}
int value = item.has("denominationValue") && !item.get("denominationValue").isJsonNull()
? item.get("denominationValue").getAsInt()
: 0;
Long denominationId = item.has("denominationId") && !item.get("denominationId").isJsonNull()
? item.get("denominationId").getAsLong()
: null;
CashExchangeDetail detail = new CashExchangeDetail();
detail.setExchangeId(exchangeId);
detail.setDirection(direction);
detail.setDenominationId(denominationId);
detail.setDenominationValue(value);
detail.setCount(count);
detail.setTotalAmount(BigDecimal.valueOf((long) value * count));
detailRepository.save(detail);
}
}
private Map<String, Object> toMap(CashExchange e) {
Map<String, Object> map = new LinkedHashMap<>();
map.put("id", e.getId());
map.put("accountId", e.getAccountId());
map.put("accountNo", e.getAccountNo());
map.put("clientId", e.getClientId());
map.put("exchangeAmount", e.getExchangeAmount());
map.put("exchangeDate", e.getExchangeDate() != null ? e.getExchangeDate().toString() : null);
map.put("note", e.getNote());
map.put("performedBy", e.getPerformedBy());
map.put("performedByUsername", e.getPerformedByUsername());
map.put("createdAt", e.getCreatedAt() != null ? e.getCreatedAt().toString() : null);
return map;
}
private Map<String, Object> detailToMap(CashExchangeDetail d) {
Map<String, Object> map = new LinkedHashMap<>();
map.put("id", d.getId());
map.put("direction", d.getDirection());
map.put("denominationId", d.getDenominationId());
map.put("denominationValue", d.getDenominationValue());
map.put("count", d.getCount());
map.put("totalAmount", d.getTotalAmount());
return map;
}
}
@@ -0,0 +1,57 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.
*/
package org.apache.fineract.portfolio.cashexchange.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Table(name = "m_cash_exchange")
@Getter
@Setter
@NoArgsConstructor
public class CashExchange {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "account_id")
private Long accountId;
@Column(name = "account_no", length = 50)
private String accountNo;
@Column(name = "client_id")
private Long clientId;
@Column(name = "exchange_amount", nullable = false)
private BigDecimal exchangeAmount;
@Column(name = "exchange_date")
private LocalDate exchangeDate;
@Column(name = "note", length = 500)
private String note;
@Column(name = "performed_by")
private Long performedBy;
@Column(name = "performed_by_username", length = 100)
private String performedByUsername;
@Column(name = "created_at")
private LocalDateTime createdAt;
}
@@ -0,0 +1,46 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.
*/
package org.apache.fineract.portfolio.cashexchange.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Table(name = "m_cash_exchange_detail")
@Getter
@Setter
@NoArgsConstructor
public class CashExchangeDetail {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "exchange_id", nullable = false)
private Long exchangeId;
@Column(name = "direction", nullable = false, length = 10)
private String direction;
@Column(name = "denomination_id")
private Long denominationId;
@Column(name = "denomination_value", nullable = false)
private Integer denominationValue;
@Column(name = "count", nullable = false)
private Integer count;
@Column(name = "total_amount", nullable = false)
private BigDecimal totalAmount;
}
@@ -0,0 +1,10 @@
package org.apache.fineract.portfolio.cashexchange.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface CashExchangeDetailRepository extends JpaRepository<CashExchangeDetail, Long> {
List<CashExchangeDetail> findByExchangeId(Long exchangeId);
}
@@ -0,0 +1,11 @@
package org.apache.fineract.portfolio.cashexchange.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface CashExchangeRepository extends JpaRepository<CashExchange, Long> {
List<CashExchange> findAllByOrderByCreatedAtDesc();
List<CashExchange> findByAccountIdOrderByCreatedAtDesc(Long accountId);
}
@@ -78,4 +78,16 @@ public class SavingsTxnDenomination {
@Column(name = "created_at")
private LocalDateTime createdAt;
/** "DEPOSIT" or "WITHDRAWAL" — identifies the direction of the cash flow. */
@Column(name = "txn_type", nullable = false, length = 20)
private String txnType;
/** ID of the logged-in teller / officer who performed the transaction. */
@Column(name = "officer_id")
private Long officerId;
/** Username of the teller / officer who performed the transaction. */
@Column(name = "officer_username", length = 100)
private String officerUsername;
}
@@ -0,0 +1,232 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.
*/
package org.apache.fineract.portfolio.denominationmaster.service;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.denominationmaster.domain.SavingsTxnDenomination;
import org.apache.fineract.portfolio.denominationmaster.domain.SavingsTxnDenominationRepository;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.stereotype.Service;
/**
* Validates and persists the per-transaction cash denomination breakdown sent by the UI
* on both deposits and withdrawals.
*/
@Service
@RequiredArgsConstructor
public class DenominationBreakdownWriter {
private final SavingsTxnDenominationRepository repository;
private final PlatformSecurityContext context;
// ── Public API ────────────────────────────────────────────────────────────
/**
* Full validation of the denomination breakdown:
* <ol>
* <li>Each item must have a non-null {@code denominationId}.</li>
* <li>Each item's {@code count} must be > 0.</li>
* <li>Each item's {@code denominationValue} (or {@code denomination}) must be > 0.</li>
* <li>The grand total (sum of value × count) must equal {@code expectedAmount}.</li>
* </ol>
* No-op when no breakdown is provided.
*
* @param command the JSON command containing the denominationBreakdown array
* @param expectedAmount the transaction amount that the breakdown must sum to
* @param txnType "DEPOSIT" or "WITHDRAWAL" — used in error messages
*/
public void validateBreakdown(final JsonCommand command, final BigDecimal expectedAmount, final String txnType) {
if (command == null || !command.parameterExists("denominationBreakdown")) {
return;
}
final JsonArray breakdown = command.arrayOfParameterNamed("denominationBreakdown");
if (breakdown == null || breakdown.size() == 0) {
return;
}
final List<ApiParameterError> errors = new ArrayList<>();
final DataValidatorBuilder validator = new DataValidatorBuilder(errors).resource("denominationBreakdown");
BigDecimal computedTotal = BigDecimal.ZERO;
final String label = (txnType != null && !txnType.isBlank()) ? txnType.toUpperCase() : "TRANSACTION";
for (int i = 0; i < breakdown.size(); i++) {
final JsonObject obj = breakdown.get(i).getAsJsonObject();
final String itemPrefix = "denominationBreakdown[" + i + "]";
// 1. denominationId is required
if (!obj.has("denominationId") || obj.get("denominationId").isJsonNull()) {
validator.reset().parameter(itemPrefix + ".denominationId")
.value(null)
.failWithCode("required", "denominationId is required for each denomination item.");
continue; // cannot compute further without ID
}
final long denominationId = obj.get("denominationId").getAsLong();
if (denominationId <= 0) {
validator.reset().parameter(itemPrefix + ".denominationId")
.value(denominationId)
.failWithCode("invalid", "denominationId must be a positive number.");
}
// 2. count is required and must be > 0
if (!obj.has("count") || obj.get("count").isJsonNull()) {
validator.reset().parameter(itemPrefix + ".count")
.value(null)
.failWithCode("required", "count is required for each denomination item.");
continue;
}
final int count = obj.get("count").getAsInt();
if (count <= 0) {
// count = 0 is silently skipped (UI may send zero for unselected rows)
continue;
}
// 3. denominationValue (or denomination) must be present and > 0
int denominationValue = 0;
if (obj.has("denominationValue") && !obj.get("denominationValue").isJsonNull()) {
denominationValue = obj.get("denominationValue").getAsInt();
} else if (obj.has("denomination") && !obj.get("denomination").isJsonNull()) {
denominationValue = obj.get("denomination").getAsInt();
}
if (denominationValue <= 0) {
validator.reset().parameter(itemPrefix + ".denominationValue")
.value(denominationValue)
.failWithCode("invalid",
"denominationValue must be greater than zero for denomination item " + i + ".");
continue;
}
// 4. Accumulate total: value × count
computedTotal = computedTotal.add(BigDecimal.valueOf((long) denominationValue * count));
}
// Throw item-level validation errors first
if (!errors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist",
"Denomination breakdown validation errors exist.", errors);
}
// 5. Grand total must match the transaction amount
if (expectedAmount != null && computedTotal.compareTo(expectedAmount) != 0) {
throw new PlatformDataIntegrityException(
"error.msg.denomination.total.mismatch",
label + " denomination breakdown total (" + computedTotal
+ ") does not match the transaction amount (" + expectedAmount
+ "). Please re-check the note/coin counts.");
}
}
/**
* Convenience wrapper kept for backward-compatibility with any callers that do not
* yet pass a {@code txnType}. Delegates to {@link #validateBreakdown}.
*/
public void validateTotal(final JsonCommand command, final BigDecimal expectedAmount) {
validateBreakdown(command, expectedAmount, "TRANSACTION");
}
/**
* Persist each valid denomination row for the given transaction.
* <p>
* Items with {@code count <= 0} are silently skipped (UI sends zero for
* unselected rows). Items missing {@code denominationId} are also skipped
* (they should have already been rejected by {@link #validateBreakdown}).
* <p>
* The logged-in officer (teller) is automatically resolved from the
* security context; if resolution fails (e.g. batch job) the officer fields
* are left {@code null}.
*
* @param command the JSON command containing the denominationBreakdown array
* @param txnId ID of the saved savings / current / RD / FD transaction
* @param savingsAccountId savings / current / RD / FD account ID
* @param accountNo account number string
* @param clientId client ID (may be null for group accounts)
* @param cifNo CIF number (may be null)
* @param txnDate transaction date
* @param txnType "DEPOSIT" or "WITHDRAWAL"
*/
public void save(final JsonCommand command, final Long txnId, final Long savingsAccountId, final String accountNo,
final Long clientId, final String cifNo, final LocalDate txnDate, final String txnType) {
if (command == null || txnId == null || !command.parameterExists("denominationBreakdown")) {
return;
}
final JsonArray breakdown = command.arrayOfParameterNamed("denominationBreakdown");
if (breakdown == null || breakdown.size() == 0) {
return;
}
// Resolve the officer / teller from the security context
Long officerId = null;
String officerUsername = null;
try {
final AppUser user = this.context.authenticatedUser();
if (user != null) {
officerId = user.getId();
officerUsername = user.getUsername();
}
} catch (final Exception ignored) {
// If user cannot be resolved (e.g. batch / async job), leave officer fields null
}
final String resolvedTxnType = (txnType != null && !txnType.isBlank()) ? txnType.toUpperCase() : "DEPOSIT";
for (final JsonElement entry : breakdown) {
final JsonObject obj = entry.getAsJsonObject();
// denominationId is mandatory (NOT NULL in DB)
if (!obj.has("denominationId") || obj.get("denominationId").isJsonNull() || !obj.has("count")) {
continue;
}
final int count = obj.get("count").getAsInt();
if (count <= 0) {
continue; // zero-count rows sent by UI are silently ignored
}
final long denominationId = obj.get("denominationId").getAsLong();
// Resolve denomination face value
int denominationValue = 0;
if (obj.has("denominationValue") && !obj.get("denominationValue").isJsonNull()) {
denominationValue = obj.get("denominationValue").getAsInt();
} else if (obj.has("denomination") && !obj.get("denomination").isJsonNull()) {
denominationValue = obj.get("denomination").getAsInt();
}
final BigDecimal lineTotal = BigDecimal.valueOf((long) denominationValue * count);
final SavingsTxnDenomination row = SavingsTxnDenomination.builder()
.txnId(txnId)
.savingsAccountId(savingsAccountId)
.accountNo(accountNo)
.clientId(clientId)
.cifNo(cifNo)
.denominationId(denominationId)
.denominationValue(denominationValue)
.count(count)
.totalAmount(lineTotal)
.txnDate(txnDate)
.createdAt(LocalDateTime.now())
.txnType(resolvedTxnType)
.officerId(officerId)
.officerUsername(officerUsername)
.build();
this.repository.save(row);
}
}
}
@@ -1,257 +1,257 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.savings.data;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.activatedOnDateParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.bankNumberParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.checkNumberParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.closedOnDateParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.onAccountClosureIdParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.paymentTypeIdParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.receiptNumberParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.routingCodeParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.toSavingsAccountIdParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transactionAccountNumberParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transactionAmountParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transactionDateParamName;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.portfolio.savings.DepositAccountOnClosureType;
import org.apache.fineract.portfolio.savings.DepositAccountType;
import org.apache.fineract.portfolio.savings.DepositsApiConstants;
import org.apache.fineract.portfolio.savings.SavingsApiConstants;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class DepositAccountTransactionDataValidator {
private final FromJsonHelper fromApiJsonHelper;
private static final Set<String> DEPOSIT_ACCOUNT_TRANSACTION_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(
DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, transactionDateParamName,
transactionAmountParamName, paymentTypeIdParamName, transactionAccountNumberParamName, checkNumberParamName,
routingCodeParamName, receiptNumberParamName, bankNumberParamName, DepositsApiConstants.amountParamName,
DepositsApiConstants.accountIdParamName, DepositsApiConstants.dateParamName, DepositsApiConstants.submittedOnDateParamName,
DepositsApiConstants.lienTransaction, DepositsApiConstants.isManualTransaction, DepositsApiConstants.chargesPaidByData,
DepositsApiConstants.accountNoParamName, DepositsApiConstants.noteParamName));
private static final Set<String> DEPOSIT_ACCOUNT_RECOMMENDED_DEPOSIT_AMOUNT_UPDATE_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName,
DepositsApiConstants.mandatoryRecommendedDepositAmountParamName, DepositsApiConstants.effectiveDateParamName));
private static final Set<String> DEPOSIT_ACCOUNT_CLOSE_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, closedOnDateParamName,
DepositsApiConstants.noteParamName, onAccountClosureIdParamName, paymentTypeIdParamName,
transactionAccountNumberParamName, checkNumberParamName, routingCodeParamName, receiptNumberParamName,
bankNumberParamName, DepositsApiConstants.transferDescriptionParamName, toSavingsAccountIdParamName));
private static final Set<String> DEPOSIT_ACCOUNT_PRE_MATURE_CALCULATION_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, closedOnDateParamName));
@Autowired
public DepositAccountTransactionDataValidator(final FromJsonHelper fromApiJsonHelper) {
this.fromApiJsonHelper = fromApiJsonHelper;
}
public void validate(final JsonCommand command, DepositAccountType depositAccountType) {
final String json = command.json();
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, DEPOSIT_ACCOUNT_TRANSACTION_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(depositAccountType.resourceName());
final JsonElement element = command.parsedJson();
final LocalDate transactionDate = this.fromApiJsonHelper.extractLocalDateNamed(transactionDateParamName, element);
baseDataValidator.reset().parameter(transactionDateParamName).value(transactionDate).notNull();
final BigDecimal transactionAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(transactionAmountParamName, element);
baseDataValidator.reset().parameter(transactionAmountParamName).value(transactionAmount).notNull().positiveAmount();
// Validate all string payment detail fields for max length
final Integer paymentTypeId = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(paymentTypeIdParamName, element);
baseDataValidator.reset().parameter(paymentTypeIdParamName).value(paymentTypeId).ignoreIfNull().integerGreaterThanZero();
final Set<String> paymentDetailParameters = new HashSet<>(Arrays.asList(transactionAccountNumberParamName, checkNumberParamName,
routingCodeParamName, receiptNumberParamName, bankNumberParamName));
for (final String paymentDetailParameterName : paymentDetailParameters) {
final String paymentDetailParameterValue = this.fromApiJsonHelper.extractStringNamed(paymentDetailParameterName, element);
baseDataValidator.reset().parameter(paymentDetailParameterName).value(paymentDetailParameterValue).ignoreIfNull()
.notExceedingLengthOf(50);
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateDepositAmountUpdate(final JsonCommand command) {
final String json = command.json();
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json,
DEPOSIT_ACCOUNT_RECOMMENDED_DEPOSIT_AMOUNT_UPDATE_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME);
final JsonElement element = command.parsedJson();
final LocalDate effectiveDate = this.fromApiJsonHelper.extractLocalDateNamed(DepositsApiConstants.effectiveDateParamName, element);
baseDataValidator.reset().parameter(DepositsApiConstants.effectiveDateParamName).value(effectiveDate).notNull();
final BigDecimal mandatoryRecommendedDepositAmount = this.fromApiJsonHelper
.extractBigDecimalWithLocaleNamed(DepositsApiConstants.mandatoryRecommendedDepositAmountParamName, element);
baseDataValidator.reset().parameter(DepositsApiConstants.mandatoryRecommendedDepositAmountParamName)
.value(mandatoryRecommendedDepositAmount).notNull().positiveAmount();
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateActivation(final JsonCommand command) {
final String json = command.json();
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json,
SavingsAccountConstant.SAVINGS_ACCOUNT_ACTIVATION_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME);
final JsonElement element = command.parsedJson();
final LocalDate activationDate = this.fromApiJsonHelper.extractLocalDateNamed(activatedOnDateParamName, element);
baseDataValidator.reset().parameter(activatedOnDateParamName).value(activationDate).notNull();
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validatePreMatureAmountCalculation(final String json, final DepositAccountType depositAccountType) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json,
DEPOSIT_ACCOUNT_PRE_MATURE_CALCULATION_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(depositAccountType.resourceName());
final JsonElement element = this.fromApiJsonHelper.parse(json);
final LocalDate closeDate = this.fromApiJsonHelper.extractLocalDateNamed(closedOnDateParamName, element);
baseDataValidator.reset().parameter(closedOnDateParamName).value(closeDate).notNull();
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateClosing(final JsonCommand command, DepositAccountType depositAccountType, final boolean isPreMatureClose) {
final String json = command.json();
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, DEPOSIT_ACCOUNT_CLOSE_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(depositAccountType.resourceName());
final JsonElement element = command.parsedJson();
final LocalDate activationDate = this.fromApiJsonHelper.extractLocalDateNamed(closedOnDateParamName, element);
baseDataValidator.reset().parameter(closedOnDateParamName).value(activationDate).notNull();
final Integer onAccountClosureId = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(onAccountClosureIdParamName, element);
baseDataValidator.reset().parameter(onAccountClosureIdParamName).value(onAccountClosureId).notBlank()
.isOneOfTheseValues(DepositAccountOnClosureType.integerValues());
if (onAccountClosureId != null) {
final DepositAccountOnClosureType accountOnClosureType = DepositAccountOnClosureType.fromInt(onAccountClosureId);
if (accountOnClosureType.isTransferToSavings()) {
final Long toSavingsAccountId = this.fromApiJsonHelper.extractLongNamed(toSavingsAccountIdParamName, element);
baseDataValidator.reset().parameter(toSavingsAccountIdParamName).value(toSavingsAccountId)
.cantBeBlankWhenParameterProvidedIs(onAccountClosureIdParamName,
DepositAccountOnClosureType.fromInt(onAccountClosureId).getCode());
} else if (accountOnClosureType.isReinvest() && isPreMatureClose) {
baseDataValidator.reset().parameter(onAccountClosureIdParamName).value(onAccountClosureId)
.failWithCode("reinvest.not.allowed", "Re-Invest is not supported for account pre mature close");
}
}
// Validate all string payment detail fields for max length
final Integer paymentTypeId = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(paymentTypeIdParamName, element);
baseDataValidator.reset().parameter(paymentTypeIdParamName).value(paymentTypeId).ignoreIfNull().integerGreaterThanZero();
final Set<String> paymentDetailParameters = new HashSet<>(Arrays.asList(transactionAccountNumberParamName, checkNumberParamName,
routingCodeParamName, receiptNumberParamName, bankNumberParamName));
for (final String paymentDetailParameterName : paymentDetailParameters) {
final String paymentDetailParameterValue = this.fromApiJsonHelper.extractStringNamed(paymentDetailParameterName, element);
baseDataValidator.reset().parameter(paymentDetailParameterName).value(paymentDetailParameterValue).ignoreIfNull()
.notExceedingLengthOf(50);
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
if (!dataValidationErrors.isEmpty()) {
//
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
dataValidationErrors);
}
}
}
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.savings.data;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.activatedOnDateParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.bankNumberParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.checkNumberParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.closedOnDateParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.onAccountClosureIdParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.paymentTypeIdParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.receiptNumberParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.routingCodeParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.toSavingsAccountIdParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transactionAccountNumberParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transactionAmountParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transactionDateParamName;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.portfolio.savings.DepositAccountOnClosureType;
import org.apache.fineract.portfolio.savings.DepositAccountType;
import org.apache.fineract.portfolio.savings.DepositsApiConstants;
import org.apache.fineract.portfolio.savings.SavingsApiConstants;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class DepositAccountTransactionDataValidator {
private final FromJsonHelper fromApiJsonHelper;
private static final Set<String> DEPOSIT_ACCOUNT_TRANSACTION_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(
DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, transactionDateParamName,
transactionAmountParamName, paymentTypeIdParamName, transactionAccountNumberParamName, checkNumberParamName,
routingCodeParamName, receiptNumberParamName, bankNumberParamName, DepositsApiConstants.amountParamName,
DepositsApiConstants.accountIdParamName, DepositsApiConstants.dateParamName, DepositsApiConstants.submittedOnDateParamName,
DepositsApiConstants.lienTransaction, DepositsApiConstants.isManualTransaction, DepositsApiConstants.chargesPaidByData,
DepositsApiConstants.accountNoParamName, DepositsApiConstants.noteParamName, "denominationBreakdown"));
private static final Set<String> DEPOSIT_ACCOUNT_RECOMMENDED_DEPOSIT_AMOUNT_UPDATE_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName,
DepositsApiConstants.mandatoryRecommendedDepositAmountParamName, DepositsApiConstants.effectiveDateParamName));
private static final Set<String> DEPOSIT_ACCOUNT_CLOSE_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, closedOnDateParamName,
DepositsApiConstants.noteParamName, onAccountClosureIdParamName, paymentTypeIdParamName,
transactionAccountNumberParamName, checkNumberParamName, routingCodeParamName, receiptNumberParamName,
bankNumberParamName, DepositsApiConstants.transferDescriptionParamName, toSavingsAccountIdParamName));
private static final Set<String> DEPOSIT_ACCOUNT_PRE_MATURE_CALCULATION_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, closedOnDateParamName));
@Autowired
public DepositAccountTransactionDataValidator(final FromJsonHelper fromApiJsonHelper) {
this.fromApiJsonHelper = fromApiJsonHelper;
}
public void validate(final JsonCommand command, DepositAccountType depositAccountType) {
final String json = command.json();
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, DEPOSIT_ACCOUNT_TRANSACTION_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(depositAccountType.resourceName());
final JsonElement element = command.parsedJson();
final LocalDate transactionDate = this.fromApiJsonHelper.extractLocalDateNamed(transactionDateParamName, element);
baseDataValidator.reset().parameter(transactionDateParamName).value(transactionDate).notNull();
final BigDecimal transactionAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(transactionAmountParamName, element);
baseDataValidator.reset().parameter(transactionAmountParamName).value(transactionAmount).notNull().positiveAmount();
// Validate all string payment detail fields for max length
final Integer paymentTypeId = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(paymentTypeIdParamName, element);
baseDataValidator.reset().parameter(paymentTypeIdParamName).value(paymentTypeId).ignoreIfNull().integerGreaterThanZero();
final Set<String> paymentDetailParameters = new HashSet<>(Arrays.asList(transactionAccountNumberParamName, checkNumberParamName,
routingCodeParamName, receiptNumberParamName, bankNumberParamName));
for (final String paymentDetailParameterName : paymentDetailParameters) {
final String paymentDetailParameterValue = this.fromApiJsonHelper.extractStringNamed(paymentDetailParameterName, element);
baseDataValidator.reset().parameter(paymentDetailParameterName).value(paymentDetailParameterValue).ignoreIfNull()
.notExceedingLengthOf(50);
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateDepositAmountUpdate(final JsonCommand command) {
final String json = command.json();
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json,
DEPOSIT_ACCOUNT_RECOMMENDED_DEPOSIT_AMOUNT_UPDATE_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME);
final JsonElement element = command.parsedJson();
final LocalDate effectiveDate = this.fromApiJsonHelper.extractLocalDateNamed(DepositsApiConstants.effectiveDateParamName, element);
baseDataValidator.reset().parameter(DepositsApiConstants.effectiveDateParamName).value(effectiveDate).notNull();
final BigDecimal mandatoryRecommendedDepositAmount = this.fromApiJsonHelper
.extractBigDecimalWithLocaleNamed(DepositsApiConstants.mandatoryRecommendedDepositAmountParamName, element);
baseDataValidator.reset().parameter(DepositsApiConstants.mandatoryRecommendedDepositAmountParamName)
.value(mandatoryRecommendedDepositAmount).notNull().positiveAmount();
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateActivation(final JsonCommand command) {
final String json = command.json();
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json,
SavingsAccountConstant.SAVINGS_ACCOUNT_ACTIVATION_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME);
final JsonElement element = command.parsedJson();
final LocalDate activationDate = this.fromApiJsonHelper.extractLocalDateNamed(activatedOnDateParamName, element);
baseDataValidator.reset().parameter(activatedOnDateParamName).value(activationDate).notNull();
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validatePreMatureAmountCalculation(final String json, final DepositAccountType depositAccountType) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json,
DEPOSIT_ACCOUNT_PRE_MATURE_CALCULATION_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(depositAccountType.resourceName());
final JsonElement element = this.fromApiJsonHelper.parse(json);
final LocalDate closeDate = this.fromApiJsonHelper.extractLocalDateNamed(closedOnDateParamName, element);
baseDataValidator.reset().parameter(closedOnDateParamName).value(closeDate).notNull();
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateClosing(final JsonCommand command, DepositAccountType depositAccountType, final boolean isPreMatureClose) {
final String json = command.json();
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, DEPOSIT_ACCOUNT_CLOSE_REQUEST_DATA_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(depositAccountType.resourceName());
final JsonElement element = command.parsedJson();
final LocalDate activationDate = this.fromApiJsonHelper.extractLocalDateNamed(closedOnDateParamName, element);
baseDataValidator.reset().parameter(closedOnDateParamName).value(activationDate).notNull();
final Integer onAccountClosureId = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(onAccountClosureIdParamName, element);
baseDataValidator.reset().parameter(onAccountClosureIdParamName).value(onAccountClosureId).notBlank()
.isOneOfTheseValues(DepositAccountOnClosureType.integerValues());
if (onAccountClosureId != null) {
final DepositAccountOnClosureType accountOnClosureType = DepositAccountOnClosureType.fromInt(onAccountClosureId);
if (accountOnClosureType.isTransferToSavings()) {
final Long toSavingsAccountId = this.fromApiJsonHelper.extractLongNamed(toSavingsAccountIdParamName, element);
baseDataValidator.reset().parameter(toSavingsAccountIdParamName).value(toSavingsAccountId)
.cantBeBlankWhenParameterProvidedIs(onAccountClosureIdParamName,
DepositAccountOnClosureType.fromInt(onAccountClosureId).getCode());
} else if (accountOnClosureType.isReinvest() && isPreMatureClose) {
baseDataValidator.reset().parameter(onAccountClosureIdParamName).value(onAccountClosureId)
.failWithCode("reinvest.not.allowed", "Re-Invest is not supported for account pre mature close");
}
}
// Validate all string payment detail fields for max length
final Integer paymentTypeId = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(paymentTypeIdParamName, element);
baseDataValidator.reset().parameter(paymentTypeIdParamName).value(paymentTypeId).ignoreIfNull().integerGreaterThanZero();
final Set<String> paymentDetailParameters = new HashSet<>(Arrays.asList(transactionAccountNumberParamName, checkNumberParamName,
routingCodeParamName, receiptNumberParamName, bankNumberParamName));
for (final String paymentDetailParameterName : paymentDetailParameters) {
final String paymentDetailParameterValue = this.fromApiJsonHelper.extractStringNamed(paymentDetailParameterName, element);
baseDataValidator.reset().parameter(paymentDetailParameterName).value(paymentDetailParameterValue).ignoreIfNull()
.notExceedingLengthOf(50);
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
if (!dataValidationErrors.isEmpty()) {
//
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
dataValidationErrors);
}
}
}
@@ -43,6 +43,7 @@ import org.apache.fineract.portfolio.currentaccount.exception.CurrentAccountNotF
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountTransactionWritePlatformService;
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProduct;
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductRepository;
import org.apache.fineract.portfolio.denominationmaster.service.DenominationBreakdownWriter;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -60,6 +61,7 @@ public class CurrentAccountTransactionWritePlatformServiceImpl implements Curren
private final CurrentProductRepository currentProductRepository;
private final ProductToGLAccountMappingRepository productToGLAccountMappingRepository;
private final JournalEntryRepository journalEntryRepository;
private final DenominationBreakdownWriter denominationBreakdownWriter;
@Transactional
@Override
@@ -74,6 +76,7 @@ public class CurrentAccountTransactionWritePlatformServiceImpl implements Curren
final BigDecimal amount = command.bigDecimalValueOfParameterNamed("transactionAmount");
validatePositiveAmount(amount, accountId, "deposit");
this.denominationBreakdownWriter.validateBreakdown(command, amount, "DEPOSIT");
final String refNo = command.stringValueOfParameterNamed("accountNumber");
@@ -85,6 +88,11 @@ public class CurrentAccountTransactionWritePlatformServiceImpl implements Curren
currentAccountTransactionRepository.saveAndFlush(txn);
this.denominationBreakdownWriter.save(command, txn.getId(), account.getId(), account.getAccountNo(),
account.getClient() != null ? account.getClient().getId() : null,
account.getClient() != null ? account.getClient().getCifNumber() : null, txnDate,
"DEPOSIT");
account.setAccountBalanceDerived(newBalance);
account.setTotalDepositsDerived(safeBalance(account.getTotalDepositsDerived()).add(amount));
currentAccountRepository.save(account);
@@ -135,6 +143,13 @@ public class CurrentAccountTransactionWritePlatformServiceImpl implements Curren
account.setTotalWithdrawalsDerived(safeBalance(account.getTotalWithdrawalsDerived()).add(amount));
currentAccountRepository.save(account);
// Save denomination breakdown for WITHDRAWAL if provided
this.denominationBreakdownWriter.validateBreakdown(command, amount, "WITHDRAWAL");
this.denominationBreakdownWriter.save(command, txn.getId(), account.getId(), account.getAccountNo(),
account.getClient() != null ? account.getClient().getId() : null,
account.getClient() != null ? account.getClient().getCifNumber() : null, txnDate,
"WITHDRAWAL");
// Withdrawal GL: DR SAVINGS_CONTROL (liability down), CR SAVINGS_REFERENCE (cash out)
postTransactionGl(account, txn, JournalEntryType.CREDIT, JournalEntryType.DEBIT, "Current Account Withdrawal");
@@ -88,8 +88,7 @@ import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlat
import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService;
import org.apache.fineract.portfolio.charge.domain.Charge;
import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper;
import org.apache.fineract.portfolio.denominationmaster.domain.SavingsTxnDenomination;
import org.apache.fineract.portfolio.denominationmaster.domain.SavingsTxnDenominationRepository;
import org.apache.fineract.portfolio.denominationmaster.service.DenominationBreakdownWriter;
import org.apache.fineract.portfolio.charge.domain.ChargeTimeType;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.exception.ClientNotActiveException;
@@ -170,7 +169,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
private final SavingsAccountInterestPostingService savingsAccountInterestPostingService;
private final ErrorHandler errorHandler;
private final FacilityChargeAutoApplyService facilityChargeAutoApplyService;
private final SavingsTxnDenominationRepository savingsTxnDenominationRepository;
private final DenominationBreakdownWriter denominationBreakdownWriter;
@Transactional
@Override
@@ -329,6 +328,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transactionDate, account);
this.denominationBreakdownWriter.validateBreakdown(command, transactionAmount, "DEPOSIT");
final Map<String, Object> changes = new LinkedHashMap<>();
final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes);
@@ -337,38 +337,10 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(account, fmt, transactionDate,
transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill);
// Save denomination breakdown if provided
if (command.parameterExists("denominationBreakdown")) {
final JsonArray denominationBreakdown = command.arrayOfParameterNamed("denominationBreakdown");
if (denominationBreakdown != null && denominationBreakdown.size() > 0) {
final Long clientId = account.clientId();
final String accountNo = account.getAccountNumber();
final String cifNo = account.getExternalId() != null ? account.getExternalId().getValue() : null;
for (JsonElement entry : denominationBreakdown) {
final com.google.gson.JsonObject obj = entry.getAsJsonObject();
if (!obj.has("denominationId") || !obj.has("count")) continue;
final long denominationId = obj.get("denominationId").getAsLong();
final int count = obj.get("count").getAsInt();
if (count <= 0) continue;
final int denominationValue = obj.has("denominationValue") ? obj.get("denominationValue").getAsInt() : 0;
final BigDecimal total = BigDecimal.valueOf((long) denominationValue * count);
final SavingsTxnDenomination row = SavingsTxnDenomination.builder()
.txnId(deposit.getId())
.savingsAccountId(account.getId())
.accountNo(accountNo)
.clientId(clientId)
.cifNo(cifNo)
.denominationId(denominationId)
.denominationValue(denominationValue)
.count(count)
.totalAmount(total)
.txnDate(transactionDate)
.createdAt(LocalDateTime.now())
.build();
this.savingsTxnDenominationRepository.save(row);
}
}
}
// Save denomination breakdown for DEPOSIT via shared writer
this.denominationBreakdownWriter.save(command, deposit.getId(), account.getId(), account.getAccountNumber(),
account.clientId(), account.getClient() != null ? account.getClient().getCifNumber() : null,
transactionDate, "DEPOSIT");
if (isGsim && (deposit.getId() != null)) {
@@ -450,9 +422,14 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
BigDecimal currentBalance = gsim.getParentDeposit().subtract(transactionAmount);
gsim.setParentDeposit(currentBalance);
gsimRepository.save(gsim);
}
// Validate and save denomination breakdown for WITHDRAWAL via shared writer
this.denominationBreakdownWriter.validateBreakdown(command, transactionAmount, "WITHDRAWAL");
this.denominationBreakdownWriter.save(command, withdrawal.getId(), account.getId(), account.getAccountNumber(),
account.clientId(), account.getClient() != null ? account.getClient().getCifNumber() : null,
transactionDate, "WITHDRAWAL");
final String noteText = command.stringValueOfParameterNamed("note");
if (StringUtils.isNotBlank(noteText)) {
final Note note = Note.savingsTransactionNote(account, withdrawal, noteText);
@@ -108,6 +108,7 @@ import org.apache.fineract.portfolio.savings.service.DepositAccountPreMatureCalc
import org.apache.fineract.portfolio.savings.service.DepositAccountPreMatureCalculationPlatformServiceImpl;
import org.apache.fineract.portfolio.savings.service.DepositAccountReadPlatformService;
import org.apache.fineract.portfolio.savings.service.DepositAccountReadPlatformServiceImpl;
import org.apache.fineract.portfolio.denominationmaster.service.DenominationBreakdownWriter;
import org.apache.fineract.portfolio.savings.service.DepositAccountWritePlatformService;
import org.apache.fineract.portfolio.savings.service.DepositAccountWritePlatformServiceJpaRepositoryImpl;
import org.apache.fineract.portfolio.savings.service.DepositApplicationProcessWritePlatformService;
@@ -268,7 +269,8 @@ public class SavingsConfiguration {
DepositAccountReadPlatformService depositAccountReadPlatformService, CalendarInstanceRepository calendarInstanceRepository,
ConfigurationDomainService configurationDomainService, HolidayRepositoryWrapper holidayRepository,
WorkingDaysRepositoryWrapper workingDaysRepository,
DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository
DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository,
DenominationBreakdownWriter denominationBreakdownWriter
) {
return new DepositAccountWritePlatformServiceJpaRepositoryImpl(context, savingAccountRepositoryWrapper,
@@ -277,7 +279,8 @@ public class SavingsConfiguration {
journalEntryWritePlatformService, depositAccountDomainService, noteRepository, accountTransfersReadPlatformService,
chargeRepository, savingsAccountChargeRepository, accountAssociationsReadPlatformService,
accountTransfersWritePlatformService, depositAccountReadPlatformService, calendarInstanceRepository,
configurationDomainService, holidayRepository, workingDaysRepository, depositAccountOnHoldTransactionRepository);
configurationDomainService, holidayRepository, workingDaysRepository, depositAccountOnHoldTransactionRepository,
denominationBreakdownWriter);
}
@Bean
@@ -426,7 +429,7 @@ public class SavingsConfiguration {
StandingInstructionRepository standingInstructionRepository, BusinessEventNotifierService businessEventNotifierService,
GSIMRepositoy gsimRepository, SavingsAccountInterestPostingService savingsAccountInterestPostingService,
ErrorHandler errorHandler, FacilityChargeAutoApplyService facilityChargeAutoApplyService,
SavingsTxnDenominationRepository savingsTxnDenominationRepository) {
DenominationBreakdownWriter denominationBreakdownWriter) {
return new SavingsAccountWritePlatformServiceJpaRepositoryImpl(context, fromApiJsonDeserializer, savingAccountRepositoryWrapper,
staffRepository, savingsAccountTransactionRepository, savingAccountAssembler, savingsAccountTransactionDataValidator,
savingsAccountChargeDataValidator, paymentDetailWritePlatformService, journalEntryWritePlatformService,
@@ -434,7 +437,7 @@ public class SavingsConfiguration {
chargeRepository, savingsAccountChargeRepository, holidayRepository, workingDaysRepository, configurationDomainService,
depositAccountOnHoldTransactionRepository, entityDatatableChecksWritePlatformService, appuserRepository,
standingInstructionRepository, businessEventNotifierService, gsimRepository, savingsAccountInterestPostingService,
errorHandler, facilityChargeAutoApplyService, savingsTxnDenominationRepository);
errorHandler, facilityChargeAutoApplyService, denominationBreakdownWriter);
}
@Bean
@@ -319,4 +319,5 @@
<include file="parts/0300_rename_fd_base_interest_rate_tables_to_cbs.xml" relativeToChangelogFile="true"/>
<include file="parts/0301_add_current_account_to_transfer_details.xml" relativeToChangelogFile="true"/>
<include file="parts/0012_denomination_master.xml" relativeToChangelogFile="true"/>
<include file="parts/0013_cash_exchange.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -177,4 +177,28 @@
</addColumn>
</changeSet>
<!-- =====================================================================
Migration: Add txn_type, officer_id, officer_username to
m_savings_txn_denomination.
txn_type distinguishes DEPOSIT from WITHDRAWAL.
officer_id / officer_username record who performed the transaction.
===================================================================== -->
<changeSet id="savings_txn_denomination_add_txn_type_officer_001" author="CBS">
<addColumn tableName="m_savings_txn_denomination">
<column name="txn_type" type="VARCHAR(20)" defaultValue="DEPOSIT">
<constraints nullable="false"/>
</column>
<column name="officer_id" type="BIGINT"/>
<column name="officer_username" type="VARCHAR(100)"/>
</addColumn>
<createIndex tableName="m_savings_txn_denomination"
indexName="idx_savings_txn_denom_txntype">
<column name="txn_type"/>
</createIndex>
<createIndex tableName="m_savings_txn_denomination"
indexName="idx_savings_txn_denom_officer">
<column name="officer_id"/>
</createIndex>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet id="cash_exchange_table_001" author="CBS">
<createTable tableName="m_cash_exchange">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="account_id" type="BIGINT"/>
<column name="account_no" type="VARCHAR(50)"/>
<column name="client_id" type="BIGINT"/>
<column name="exchange_amount" type="DECIMAL(19,6)">
<constraints nullable="false"/>
</column>
<column name="exchange_date" type="DATE"/>
<column name="note" type="VARCHAR(500)"/>
<column name="performed_by" type="BIGINT"/>
<column name="performed_by_username" type="VARCHAR(100)"/>
<column name="created_at" type="TIMESTAMP" defaultValueComputed="CURRENT_TIMESTAMP"/>
</createTable>
</changeSet>
<changeSet id="cash_exchange_detail_table_001" author="CBS">
<createTable tableName="m_cash_exchange_detail">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="exchange_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="direction" type="VARCHAR(10)">
<constraints nullable="false"/>
</column>
<column name="denomination_id" type="BIGINT"/>
<column name="denomination_value" type="INT">
<constraints nullable="false"/>
</column>
<column name="count" type="INT">
<constraints nullable="false"/>
</column>
<column name="total_amount" type="DECIMAL(19,6)">
<constraints nullable="false"/>
</column>
</createTable>
<createIndex tableName="m_cash_exchange_detail" indexName="idx_cash_exchange_detail_eid">
<column name="exchange_id"/>
</createIndex>
</changeSet>
</databaseChangeLog>