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:
co-authored by
Claude Opus 4.7
parent
ee915e395a
commit
9a30ede278
+199
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+57
@@ -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;
|
||||||
|
}
|
||||||
+46
@@ -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;
|
||||||
|
}
|
||||||
+10
@@ -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);
|
||||||
|
}
|
||||||
+11
@@ -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);
|
||||||
|
}
|
||||||
+12
@@ -78,4 +78,16 @@ public class SavingsTxnDenomination {
|
|||||||
|
|
||||||
@Column(name = "created_at")
|
@Column(name = "created_at")
|
||||||
private LocalDateTime createdAt;
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
+232
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+257
-257
@@ -1,257 +1,257 @@
|
|||||||
/**
|
/**
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
* or more contributor license agreements. See the NOTICE file
|
* or more contributor license agreements. See the NOTICE file
|
||||||
* distributed with this work for additional information
|
* distributed with this work for additional information
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
* to you under the Apache License, Version 2.0 (the
|
* to you under the Apache License, Version 2.0 (the
|
||||||
* "License"); you may not use this file except in compliance
|
* "License"); you may not use this file except in compliance
|
||||||
* with the License. You may obtain a copy of the License at
|
* with the License. You may obtain a copy of the License at
|
||||||
*
|
*
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
*
|
*
|
||||||
* Unless required by applicable law or agreed to in writing,
|
* Unless required by applicable law or agreed to in writing,
|
||||||
* software distributed under the License is distributed on an
|
* software distributed under the License is distributed on an
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
* KIND, either express or implied. See the License for the
|
* KIND, either express or implied. See the License for the
|
||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
package org.apache.fineract.portfolio.savings.data;
|
package org.apache.fineract.portfolio.savings.data;
|
||||||
|
|
||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.activatedOnDateParamName;
|
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.bankNumberParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.checkNumberParamName;
|
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.closedOnDateParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.onAccountClosureIdParamName;
|
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.paymentTypeIdParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.receiptNumberParamName;
|
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.routingCodeParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.toSavingsAccountIdParamName;
|
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.transactionAccountNumberParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transactionAmountParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transactionAmountParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transactionDateParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transactionDateParamName;
|
||||||
|
|
||||||
import com.google.gson.JsonElement;
|
import com.google.gson.JsonElement;
|
||||||
import com.google.gson.reflect.TypeToken;
|
import com.google.gson.reflect.TypeToken;
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||||
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||||
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
|
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
|
||||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||||
import org.apache.fineract.portfolio.savings.DepositAccountOnClosureType;
|
import org.apache.fineract.portfolio.savings.DepositAccountOnClosureType;
|
||||||
import org.apache.fineract.portfolio.savings.DepositAccountType;
|
import org.apache.fineract.portfolio.savings.DepositAccountType;
|
||||||
import org.apache.fineract.portfolio.savings.DepositsApiConstants;
|
import org.apache.fineract.portfolio.savings.DepositsApiConstants;
|
||||||
import org.apache.fineract.portfolio.savings.SavingsApiConstants;
|
import org.apache.fineract.portfolio.savings.SavingsApiConstants;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class DepositAccountTransactionDataValidator {
|
public class DepositAccountTransactionDataValidator {
|
||||||
|
|
||||||
private final FromJsonHelper fromApiJsonHelper;
|
private final FromJsonHelper fromApiJsonHelper;
|
||||||
|
|
||||||
private static final Set<String> DEPOSIT_ACCOUNT_TRANSACTION_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(
|
private static final Set<String> DEPOSIT_ACCOUNT_TRANSACTION_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(
|
||||||
DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, transactionDateParamName,
|
DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, transactionDateParamName,
|
||||||
transactionAmountParamName, paymentTypeIdParamName, transactionAccountNumberParamName, checkNumberParamName,
|
transactionAmountParamName, paymentTypeIdParamName, transactionAccountNumberParamName, checkNumberParamName,
|
||||||
routingCodeParamName, receiptNumberParamName, bankNumberParamName, DepositsApiConstants.amountParamName,
|
routingCodeParamName, receiptNumberParamName, bankNumberParamName, DepositsApiConstants.amountParamName,
|
||||||
DepositsApiConstants.accountIdParamName, DepositsApiConstants.dateParamName, DepositsApiConstants.submittedOnDateParamName,
|
DepositsApiConstants.accountIdParamName, DepositsApiConstants.dateParamName, DepositsApiConstants.submittedOnDateParamName,
|
||||||
DepositsApiConstants.lienTransaction, DepositsApiConstants.isManualTransaction, DepositsApiConstants.chargesPaidByData,
|
DepositsApiConstants.lienTransaction, DepositsApiConstants.isManualTransaction, DepositsApiConstants.chargesPaidByData,
|
||||||
DepositsApiConstants.accountNoParamName, DepositsApiConstants.noteParamName));
|
DepositsApiConstants.accountNoParamName, DepositsApiConstants.noteParamName, "denominationBreakdown"));
|
||||||
|
|
||||||
private static final Set<String> DEPOSIT_ACCOUNT_RECOMMENDED_DEPOSIT_AMOUNT_UPDATE_REQUEST_DATA_PARAMETERS = new HashSet<>(
|
private static final Set<String> DEPOSIT_ACCOUNT_RECOMMENDED_DEPOSIT_AMOUNT_UPDATE_REQUEST_DATA_PARAMETERS = new HashSet<>(
|
||||||
Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName,
|
Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName,
|
||||||
DepositsApiConstants.mandatoryRecommendedDepositAmountParamName, DepositsApiConstants.effectiveDateParamName));
|
DepositsApiConstants.mandatoryRecommendedDepositAmountParamName, DepositsApiConstants.effectiveDateParamName));
|
||||||
|
|
||||||
private static final Set<String> DEPOSIT_ACCOUNT_CLOSE_REQUEST_DATA_PARAMETERS = new HashSet<>(
|
private static final Set<String> DEPOSIT_ACCOUNT_CLOSE_REQUEST_DATA_PARAMETERS = new HashSet<>(
|
||||||
Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, closedOnDateParamName,
|
Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, closedOnDateParamName,
|
||||||
DepositsApiConstants.noteParamName, onAccountClosureIdParamName, paymentTypeIdParamName,
|
DepositsApiConstants.noteParamName, onAccountClosureIdParamName, paymentTypeIdParamName,
|
||||||
transactionAccountNumberParamName, checkNumberParamName, routingCodeParamName, receiptNumberParamName,
|
transactionAccountNumberParamName, checkNumberParamName, routingCodeParamName, receiptNumberParamName,
|
||||||
bankNumberParamName, DepositsApiConstants.transferDescriptionParamName, toSavingsAccountIdParamName));
|
bankNumberParamName, DepositsApiConstants.transferDescriptionParamName, toSavingsAccountIdParamName));
|
||||||
|
|
||||||
private static final Set<String> DEPOSIT_ACCOUNT_PRE_MATURE_CALCULATION_REQUEST_DATA_PARAMETERS = new HashSet<>(
|
private static final Set<String> DEPOSIT_ACCOUNT_PRE_MATURE_CALCULATION_REQUEST_DATA_PARAMETERS = new HashSet<>(
|
||||||
Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, closedOnDateParamName));
|
Arrays.asList(DepositsApiConstants.localeParamName, DepositsApiConstants.dateFormatParamName, closedOnDateParamName));
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public DepositAccountTransactionDataValidator(final FromJsonHelper fromApiJsonHelper) {
|
public DepositAccountTransactionDataValidator(final FromJsonHelper fromApiJsonHelper) {
|
||||||
this.fromApiJsonHelper = fromApiJsonHelper;
|
this.fromApiJsonHelper = fromApiJsonHelper;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void validate(final JsonCommand command, DepositAccountType depositAccountType) {
|
public void validate(final JsonCommand command, DepositAccountType depositAccountType) {
|
||||||
|
|
||||||
final String json = command.json();
|
final String json = command.json();
|
||||||
|
|
||||||
if (StringUtils.isBlank(json)) {
|
if (StringUtils.isBlank(json)) {
|
||||||
throw new InvalidJsonException();
|
throw new InvalidJsonException();
|
||||||
}
|
}
|
||||||
|
|
||||||
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
||||||
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, DEPOSIT_ACCOUNT_TRANSACTION_REQUEST_DATA_PARAMETERS);
|
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, DEPOSIT_ACCOUNT_TRANSACTION_REQUEST_DATA_PARAMETERS);
|
||||||
|
|
||||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||||
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
|
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
|
||||||
.resource(depositAccountType.resourceName());
|
.resource(depositAccountType.resourceName());
|
||||||
|
|
||||||
final JsonElement element = command.parsedJson();
|
final JsonElement element = command.parsedJson();
|
||||||
|
|
||||||
final LocalDate transactionDate = this.fromApiJsonHelper.extractLocalDateNamed(transactionDateParamName, element);
|
final LocalDate transactionDate = this.fromApiJsonHelper.extractLocalDateNamed(transactionDateParamName, element);
|
||||||
baseDataValidator.reset().parameter(transactionDateParamName).value(transactionDate).notNull();
|
baseDataValidator.reset().parameter(transactionDateParamName).value(transactionDate).notNull();
|
||||||
|
|
||||||
final BigDecimal transactionAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(transactionAmountParamName, element);
|
final BigDecimal transactionAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(transactionAmountParamName, element);
|
||||||
baseDataValidator.reset().parameter(transactionAmountParamName).value(transactionAmount).notNull().positiveAmount();
|
baseDataValidator.reset().parameter(transactionAmountParamName).value(transactionAmount).notNull().positiveAmount();
|
||||||
|
|
||||||
// Validate all string payment detail fields for max length
|
// Validate all string payment detail fields for max length
|
||||||
final Integer paymentTypeId = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(paymentTypeIdParamName, element);
|
final Integer paymentTypeId = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(paymentTypeIdParamName, element);
|
||||||
baseDataValidator.reset().parameter(paymentTypeIdParamName).value(paymentTypeId).ignoreIfNull().integerGreaterThanZero();
|
baseDataValidator.reset().parameter(paymentTypeIdParamName).value(paymentTypeId).ignoreIfNull().integerGreaterThanZero();
|
||||||
final Set<String> paymentDetailParameters = new HashSet<>(Arrays.asList(transactionAccountNumberParamName, checkNumberParamName,
|
final Set<String> paymentDetailParameters = new HashSet<>(Arrays.asList(transactionAccountNumberParamName, checkNumberParamName,
|
||||||
routingCodeParamName, receiptNumberParamName, bankNumberParamName));
|
routingCodeParamName, receiptNumberParamName, bankNumberParamName));
|
||||||
for (final String paymentDetailParameterName : paymentDetailParameters) {
|
for (final String paymentDetailParameterName : paymentDetailParameters) {
|
||||||
final String paymentDetailParameterValue = this.fromApiJsonHelper.extractStringNamed(paymentDetailParameterName, element);
|
final String paymentDetailParameterValue = this.fromApiJsonHelper.extractStringNamed(paymentDetailParameterName, element);
|
||||||
baseDataValidator.reset().parameter(paymentDetailParameterName).value(paymentDetailParameterValue).ignoreIfNull()
|
baseDataValidator.reset().parameter(paymentDetailParameterName).value(paymentDetailParameterValue).ignoreIfNull()
|
||||||
.notExceedingLengthOf(50);
|
.notExceedingLengthOf(50);
|
||||||
}
|
}
|
||||||
|
|
||||||
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void validateDepositAmountUpdate(final JsonCommand command) {
|
public void validateDepositAmountUpdate(final JsonCommand command) {
|
||||||
final String json = command.json();
|
final String json = command.json();
|
||||||
|
|
||||||
if (StringUtils.isBlank(json)) {
|
if (StringUtils.isBlank(json)) {
|
||||||
throw new InvalidJsonException();
|
throw new InvalidJsonException();
|
||||||
}
|
}
|
||||||
|
|
||||||
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
||||||
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json,
|
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json,
|
||||||
DEPOSIT_ACCOUNT_RECOMMENDED_DEPOSIT_AMOUNT_UPDATE_REQUEST_DATA_PARAMETERS);
|
DEPOSIT_ACCOUNT_RECOMMENDED_DEPOSIT_AMOUNT_UPDATE_REQUEST_DATA_PARAMETERS);
|
||||||
|
|
||||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||||
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
|
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
|
||||||
.resource(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME);
|
.resource(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME);
|
||||||
|
|
||||||
final JsonElement element = command.parsedJson();
|
final JsonElement element = command.parsedJson();
|
||||||
|
|
||||||
final LocalDate effectiveDate = this.fromApiJsonHelper.extractLocalDateNamed(DepositsApiConstants.effectiveDateParamName, element);
|
final LocalDate effectiveDate = this.fromApiJsonHelper.extractLocalDateNamed(DepositsApiConstants.effectiveDateParamName, element);
|
||||||
baseDataValidator.reset().parameter(DepositsApiConstants.effectiveDateParamName).value(effectiveDate).notNull();
|
baseDataValidator.reset().parameter(DepositsApiConstants.effectiveDateParamName).value(effectiveDate).notNull();
|
||||||
|
|
||||||
final BigDecimal mandatoryRecommendedDepositAmount = this.fromApiJsonHelper
|
final BigDecimal mandatoryRecommendedDepositAmount = this.fromApiJsonHelper
|
||||||
.extractBigDecimalWithLocaleNamed(DepositsApiConstants.mandatoryRecommendedDepositAmountParamName, element);
|
.extractBigDecimalWithLocaleNamed(DepositsApiConstants.mandatoryRecommendedDepositAmountParamName, element);
|
||||||
baseDataValidator.reset().parameter(DepositsApiConstants.mandatoryRecommendedDepositAmountParamName)
|
baseDataValidator.reset().parameter(DepositsApiConstants.mandatoryRecommendedDepositAmountParamName)
|
||||||
.value(mandatoryRecommendedDepositAmount).notNull().positiveAmount();
|
.value(mandatoryRecommendedDepositAmount).notNull().positiveAmount();
|
||||||
|
|
||||||
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void validateActivation(final JsonCommand command) {
|
public void validateActivation(final JsonCommand command) {
|
||||||
final String json = command.json();
|
final String json = command.json();
|
||||||
|
|
||||||
if (StringUtils.isBlank(json)) {
|
if (StringUtils.isBlank(json)) {
|
||||||
throw new InvalidJsonException();
|
throw new InvalidJsonException();
|
||||||
}
|
}
|
||||||
|
|
||||||
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
||||||
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json,
|
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json,
|
||||||
SavingsAccountConstant.SAVINGS_ACCOUNT_ACTIVATION_REQUEST_DATA_PARAMETERS);
|
SavingsAccountConstant.SAVINGS_ACCOUNT_ACTIVATION_REQUEST_DATA_PARAMETERS);
|
||||||
|
|
||||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||||
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
|
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
|
||||||
.resource(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME);
|
.resource(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME);
|
||||||
|
|
||||||
final JsonElement element = command.parsedJson();
|
final JsonElement element = command.parsedJson();
|
||||||
|
|
||||||
final LocalDate activationDate = this.fromApiJsonHelper.extractLocalDateNamed(activatedOnDateParamName, element);
|
final LocalDate activationDate = this.fromApiJsonHelper.extractLocalDateNamed(activatedOnDateParamName, element);
|
||||||
baseDataValidator.reset().parameter(activatedOnDateParamName).value(activationDate).notNull();
|
baseDataValidator.reset().parameter(activatedOnDateParamName).value(activationDate).notNull();
|
||||||
|
|
||||||
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void validatePreMatureAmountCalculation(final String json, final DepositAccountType depositAccountType) {
|
public void validatePreMatureAmountCalculation(final String json, final DepositAccountType depositAccountType) {
|
||||||
|
|
||||||
if (StringUtils.isBlank(json)) {
|
if (StringUtils.isBlank(json)) {
|
||||||
throw new InvalidJsonException();
|
throw new InvalidJsonException();
|
||||||
}
|
}
|
||||||
|
|
||||||
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
||||||
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json,
|
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json,
|
||||||
DEPOSIT_ACCOUNT_PRE_MATURE_CALCULATION_REQUEST_DATA_PARAMETERS);
|
DEPOSIT_ACCOUNT_PRE_MATURE_CALCULATION_REQUEST_DATA_PARAMETERS);
|
||||||
|
|
||||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||||
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
|
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
|
||||||
.resource(depositAccountType.resourceName());
|
.resource(depositAccountType.resourceName());
|
||||||
|
|
||||||
final JsonElement element = this.fromApiJsonHelper.parse(json);
|
final JsonElement element = this.fromApiJsonHelper.parse(json);
|
||||||
|
|
||||||
final LocalDate closeDate = this.fromApiJsonHelper.extractLocalDateNamed(closedOnDateParamName, element);
|
final LocalDate closeDate = this.fromApiJsonHelper.extractLocalDateNamed(closedOnDateParamName, element);
|
||||||
baseDataValidator.reset().parameter(closedOnDateParamName).value(closeDate).notNull();
|
baseDataValidator.reset().parameter(closedOnDateParamName).value(closeDate).notNull();
|
||||||
|
|
||||||
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void validateClosing(final JsonCommand command, DepositAccountType depositAccountType, final boolean isPreMatureClose) {
|
public void validateClosing(final JsonCommand command, DepositAccountType depositAccountType, final boolean isPreMatureClose) {
|
||||||
final String json = command.json();
|
final String json = command.json();
|
||||||
|
|
||||||
if (StringUtils.isBlank(json)) {
|
if (StringUtils.isBlank(json)) {
|
||||||
throw new InvalidJsonException();
|
throw new InvalidJsonException();
|
||||||
}
|
}
|
||||||
|
|
||||||
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
||||||
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, DEPOSIT_ACCOUNT_CLOSE_REQUEST_DATA_PARAMETERS);
|
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, DEPOSIT_ACCOUNT_CLOSE_REQUEST_DATA_PARAMETERS);
|
||||||
|
|
||||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||||
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
|
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
|
||||||
.resource(depositAccountType.resourceName());
|
.resource(depositAccountType.resourceName());
|
||||||
|
|
||||||
final JsonElement element = command.parsedJson();
|
final JsonElement element = command.parsedJson();
|
||||||
|
|
||||||
final LocalDate activationDate = this.fromApiJsonHelper.extractLocalDateNamed(closedOnDateParamName, element);
|
final LocalDate activationDate = this.fromApiJsonHelper.extractLocalDateNamed(closedOnDateParamName, element);
|
||||||
baseDataValidator.reset().parameter(closedOnDateParamName).value(activationDate).notNull();
|
baseDataValidator.reset().parameter(closedOnDateParamName).value(activationDate).notNull();
|
||||||
|
|
||||||
final Integer onAccountClosureId = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(onAccountClosureIdParamName, element);
|
final Integer onAccountClosureId = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(onAccountClosureIdParamName, element);
|
||||||
baseDataValidator.reset().parameter(onAccountClosureIdParamName).value(onAccountClosureId).notBlank()
|
baseDataValidator.reset().parameter(onAccountClosureIdParamName).value(onAccountClosureId).notBlank()
|
||||||
.isOneOfTheseValues(DepositAccountOnClosureType.integerValues());
|
.isOneOfTheseValues(DepositAccountOnClosureType.integerValues());
|
||||||
|
|
||||||
if (onAccountClosureId != null) {
|
if (onAccountClosureId != null) {
|
||||||
final DepositAccountOnClosureType accountOnClosureType = DepositAccountOnClosureType.fromInt(onAccountClosureId);
|
final DepositAccountOnClosureType accountOnClosureType = DepositAccountOnClosureType.fromInt(onAccountClosureId);
|
||||||
if (accountOnClosureType.isTransferToSavings()) {
|
if (accountOnClosureType.isTransferToSavings()) {
|
||||||
final Long toSavingsAccountId = this.fromApiJsonHelper.extractLongNamed(toSavingsAccountIdParamName, element);
|
final Long toSavingsAccountId = this.fromApiJsonHelper.extractLongNamed(toSavingsAccountIdParamName, element);
|
||||||
baseDataValidator.reset().parameter(toSavingsAccountIdParamName).value(toSavingsAccountId)
|
baseDataValidator.reset().parameter(toSavingsAccountIdParamName).value(toSavingsAccountId)
|
||||||
.cantBeBlankWhenParameterProvidedIs(onAccountClosureIdParamName,
|
.cantBeBlankWhenParameterProvidedIs(onAccountClosureIdParamName,
|
||||||
DepositAccountOnClosureType.fromInt(onAccountClosureId).getCode());
|
DepositAccountOnClosureType.fromInt(onAccountClosureId).getCode());
|
||||||
} else if (accountOnClosureType.isReinvest() && isPreMatureClose) {
|
} else if (accountOnClosureType.isReinvest() && isPreMatureClose) {
|
||||||
baseDataValidator.reset().parameter(onAccountClosureIdParamName).value(onAccountClosureId)
|
baseDataValidator.reset().parameter(onAccountClosureIdParamName).value(onAccountClosureId)
|
||||||
.failWithCode("reinvest.not.allowed", "Re-Invest is not supported for account pre mature close");
|
.failWithCode("reinvest.not.allowed", "Re-Invest is not supported for account pre mature close");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate all string payment detail fields for max length
|
// Validate all string payment detail fields for max length
|
||||||
final Integer paymentTypeId = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(paymentTypeIdParamName, element);
|
final Integer paymentTypeId = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(paymentTypeIdParamName, element);
|
||||||
baseDataValidator.reset().parameter(paymentTypeIdParamName).value(paymentTypeId).ignoreIfNull().integerGreaterThanZero();
|
baseDataValidator.reset().parameter(paymentTypeIdParamName).value(paymentTypeId).ignoreIfNull().integerGreaterThanZero();
|
||||||
final Set<String> paymentDetailParameters = new HashSet<>(Arrays.asList(transactionAccountNumberParamName, checkNumberParamName,
|
final Set<String> paymentDetailParameters = new HashSet<>(Arrays.asList(transactionAccountNumberParamName, checkNumberParamName,
|
||||||
routingCodeParamName, receiptNumberParamName, bankNumberParamName));
|
routingCodeParamName, receiptNumberParamName, bankNumberParamName));
|
||||||
for (final String paymentDetailParameterName : paymentDetailParameters) {
|
for (final String paymentDetailParameterName : paymentDetailParameters) {
|
||||||
final String paymentDetailParameterValue = this.fromApiJsonHelper.extractStringNamed(paymentDetailParameterName, element);
|
final String paymentDetailParameterValue = this.fromApiJsonHelper.extractStringNamed(paymentDetailParameterName, element);
|
||||||
baseDataValidator.reset().parameter(paymentDetailParameterName).value(paymentDetailParameterValue).ignoreIfNull()
|
baseDataValidator.reset().parameter(paymentDetailParameterName).value(paymentDetailParameterValue).ignoreIfNull()
|
||||||
.notExceedingLengthOf(50);
|
.notExceedingLengthOf(50);
|
||||||
}
|
}
|
||||||
|
|
||||||
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
|
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
|
||||||
if (!dataValidationErrors.isEmpty()) {
|
if (!dataValidationErrors.isEmpty()) {
|
||||||
//
|
//
|
||||||
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
|
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
|
||||||
dataValidationErrors);
|
dataValidationErrors);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
@@ -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.currentaccount.service.CurrentAccountTransactionWritePlatformService;
|
||||||
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProduct;
|
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProduct;
|
||||||
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductRepository;
|
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductRepository;
|
||||||
|
import org.apache.fineract.portfolio.denominationmaster.service.DenominationBreakdownWriter;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -60,6 +61,7 @@ public class CurrentAccountTransactionWritePlatformServiceImpl implements Curren
|
|||||||
private final CurrentProductRepository currentProductRepository;
|
private final CurrentProductRepository currentProductRepository;
|
||||||
private final ProductToGLAccountMappingRepository productToGLAccountMappingRepository;
|
private final ProductToGLAccountMappingRepository productToGLAccountMappingRepository;
|
||||||
private final JournalEntryRepository journalEntryRepository;
|
private final JournalEntryRepository journalEntryRepository;
|
||||||
|
private final DenominationBreakdownWriter denominationBreakdownWriter;
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
@@ -74,6 +76,7 @@ public class CurrentAccountTransactionWritePlatformServiceImpl implements Curren
|
|||||||
|
|
||||||
final BigDecimal amount = command.bigDecimalValueOfParameterNamed("transactionAmount");
|
final BigDecimal amount = command.bigDecimalValueOfParameterNamed("transactionAmount");
|
||||||
validatePositiveAmount(amount, accountId, "deposit");
|
validatePositiveAmount(amount, accountId, "deposit");
|
||||||
|
this.denominationBreakdownWriter.validateBreakdown(command, amount, "DEPOSIT");
|
||||||
|
|
||||||
final String refNo = command.stringValueOfParameterNamed("accountNumber");
|
final String refNo = command.stringValueOfParameterNamed("accountNumber");
|
||||||
|
|
||||||
@@ -85,6 +88,11 @@ public class CurrentAccountTransactionWritePlatformServiceImpl implements Curren
|
|||||||
|
|
||||||
currentAccountTransactionRepository.saveAndFlush(txn);
|
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.setAccountBalanceDerived(newBalance);
|
||||||
account.setTotalDepositsDerived(safeBalance(account.getTotalDepositsDerived()).add(amount));
|
account.setTotalDepositsDerived(safeBalance(account.getTotalDepositsDerived()).add(amount));
|
||||||
currentAccountRepository.save(account);
|
currentAccountRepository.save(account);
|
||||||
@@ -135,6 +143,13 @@ public class CurrentAccountTransactionWritePlatformServiceImpl implements Curren
|
|||||||
account.setTotalWithdrawalsDerived(safeBalance(account.getTotalWithdrawalsDerived()).add(amount));
|
account.setTotalWithdrawalsDerived(safeBalance(account.getTotalWithdrawalsDerived()).add(amount));
|
||||||
currentAccountRepository.save(account);
|
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)
|
// Withdrawal GL: DR SAVINGS_CONTROL (liability down), CR SAVINGS_REFERENCE (cash out)
|
||||||
postTransactionGl(account, txn, JournalEntryType.CREDIT, JournalEntryType.DEBIT, "Current Account Withdrawal");
|
postTransactionGl(account, txn, JournalEntryType.CREDIT, JournalEntryType.DEBIT, "Current Account Withdrawal");
|
||||||
|
|
||||||
|
|||||||
+1419
-1401
File diff suppressed because it is too large
Load Diff
+13
-36
@@ -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.account.service.AccountTransfersReadPlatformService;
|
||||||
import org.apache.fineract.portfolio.charge.domain.Charge;
|
import org.apache.fineract.portfolio.charge.domain.Charge;
|
||||||
import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper;
|
import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper;
|
||||||
import org.apache.fineract.portfolio.denominationmaster.domain.SavingsTxnDenomination;
|
import org.apache.fineract.portfolio.denominationmaster.service.DenominationBreakdownWriter;
|
||||||
import org.apache.fineract.portfolio.denominationmaster.domain.SavingsTxnDenominationRepository;
|
|
||||||
import org.apache.fineract.portfolio.charge.domain.ChargeTimeType;
|
import org.apache.fineract.portfolio.charge.domain.ChargeTimeType;
|
||||||
import org.apache.fineract.portfolio.client.domain.Client;
|
import org.apache.fineract.portfolio.client.domain.Client;
|
||||||
import org.apache.fineract.portfolio.client.exception.ClientNotActiveException;
|
import org.apache.fineract.portfolio.client.exception.ClientNotActiveException;
|
||||||
@@ -170,7 +169,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
|
|||||||
private final SavingsAccountInterestPostingService savingsAccountInterestPostingService;
|
private final SavingsAccountInterestPostingService savingsAccountInterestPostingService;
|
||||||
private final ErrorHandler errorHandler;
|
private final ErrorHandler errorHandler;
|
||||||
private final FacilityChargeAutoApplyService facilityChargeAutoApplyService;
|
private final FacilityChargeAutoApplyService facilityChargeAutoApplyService;
|
||||||
private final SavingsTxnDenominationRepository savingsTxnDenominationRepository;
|
private final DenominationBreakdownWriter denominationBreakdownWriter;
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
@@ -329,6 +328,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
|
|||||||
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
|
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
|
||||||
|
|
||||||
this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transactionDate, account);
|
this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transactionDate, account);
|
||||||
|
this.denominationBreakdownWriter.validateBreakdown(command, transactionAmount, "DEPOSIT");
|
||||||
|
|
||||||
final Map<String, Object> changes = new LinkedHashMap<>();
|
final Map<String, Object> changes = new LinkedHashMap<>();
|
||||||
final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes);
|
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,
|
final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(account, fmt, transactionDate,
|
||||||
transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill);
|
transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill);
|
||||||
|
|
||||||
// Save denomination breakdown if provided
|
// Save denomination breakdown for DEPOSIT via shared writer
|
||||||
if (command.parameterExists("denominationBreakdown")) {
|
this.denominationBreakdownWriter.save(command, deposit.getId(), account.getId(), account.getAccountNumber(),
|
||||||
final JsonArray denominationBreakdown = command.arrayOfParameterNamed("denominationBreakdown");
|
account.clientId(), account.getClient() != null ? account.getClient().getCifNumber() : null,
|
||||||
if (denominationBreakdown != null && denominationBreakdown.size() > 0) {
|
transactionDate, "DEPOSIT");
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isGsim && (deposit.getId() != null)) {
|
if (isGsim && (deposit.getId() != null)) {
|
||||||
|
|
||||||
@@ -450,9 +422,14 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
|
|||||||
BigDecimal currentBalance = gsim.getParentDeposit().subtract(transactionAmount);
|
BigDecimal currentBalance = gsim.getParentDeposit().subtract(transactionAmount);
|
||||||
gsim.setParentDeposit(currentBalance);
|
gsim.setParentDeposit(currentBalance);
|
||||||
gsimRepository.save(gsim);
|
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");
|
final String noteText = command.stringValueOfParameterNamed("note");
|
||||||
if (StringUtils.isNotBlank(noteText)) {
|
if (StringUtils.isNotBlank(noteText)) {
|
||||||
final Note note = Note.savingsTransactionNote(account, withdrawal, noteText);
|
final Note note = Note.savingsTransactionNote(account, withdrawal, noteText);
|
||||||
|
|||||||
+7
-4
@@ -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.DepositAccountPreMatureCalculationPlatformServiceImpl;
|
||||||
import org.apache.fineract.portfolio.savings.service.DepositAccountReadPlatformService;
|
import org.apache.fineract.portfolio.savings.service.DepositAccountReadPlatformService;
|
||||||
import org.apache.fineract.portfolio.savings.service.DepositAccountReadPlatformServiceImpl;
|
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.DepositAccountWritePlatformService;
|
||||||
import org.apache.fineract.portfolio.savings.service.DepositAccountWritePlatformServiceJpaRepositoryImpl;
|
import org.apache.fineract.portfolio.savings.service.DepositAccountWritePlatformServiceJpaRepositoryImpl;
|
||||||
import org.apache.fineract.portfolio.savings.service.DepositApplicationProcessWritePlatformService;
|
import org.apache.fineract.portfolio.savings.service.DepositApplicationProcessWritePlatformService;
|
||||||
@@ -268,7 +269,8 @@ public class SavingsConfiguration {
|
|||||||
DepositAccountReadPlatformService depositAccountReadPlatformService, CalendarInstanceRepository calendarInstanceRepository,
|
DepositAccountReadPlatformService depositAccountReadPlatformService, CalendarInstanceRepository calendarInstanceRepository,
|
||||||
ConfigurationDomainService configurationDomainService, HolidayRepositoryWrapper holidayRepository,
|
ConfigurationDomainService configurationDomainService, HolidayRepositoryWrapper holidayRepository,
|
||||||
WorkingDaysRepositoryWrapper workingDaysRepository,
|
WorkingDaysRepositoryWrapper workingDaysRepository,
|
||||||
DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository
|
DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository,
|
||||||
|
DenominationBreakdownWriter denominationBreakdownWriter
|
||||||
|
|
||||||
) {
|
) {
|
||||||
return new DepositAccountWritePlatformServiceJpaRepositoryImpl(context, savingAccountRepositoryWrapper,
|
return new DepositAccountWritePlatformServiceJpaRepositoryImpl(context, savingAccountRepositoryWrapper,
|
||||||
@@ -277,7 +279,8 @@ public class SavingsConfiguration {
|
|||||||
journalEntryWritePlatformService, depositAccountDomainService, noteRepository, accountTransfersReadPlatformService,
|
journalEntryWritePlatformService, depositAccountDomainService, noteRepository, accountTransfersReadPlatformService,
|
||||||
chargeRepository, savingsAccountChargeRepository, accountAssociationsReadPlatformService,
|
chargeRepository, savingsAccountChargeRepository, accountAssociationsReadPlatformService,
|
||||||
accountTransfersWritePlatformService, depositAccountReadPlatformService, calendarInstanceRepository,
|
accountTransfersWritePlatformService, depositAccountReadPlatformService, calendarInstanceRepository,
|
||||||
configurationDomainService, holidayRepository, workingDaysRepository, depositAccountOnHoldTransactionRepository);
|
configurationDomainService, holidayRepository, workingDaysRepository, depositAccountOnHoldTransactionRepository,
|
||||||
|
denominationBreakdownWriter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@@ -426,7 +429,7 @@ public class SavingsConfiguration {
|
|||||||
StandingInstructionRepository standingInstructionRepository, BusinessEventNotifierService businessEventNotifierService,
|
StandingInstructionRepository standingInstructionRepository, BusinessEventNotifierService businessEventNotifierService,
|
||||||
GSIMRepositoy gsimRepository, SavingsAccountInterestPostingService savingsAccountInterestPostingService,
|
GSIMRepositoy gsimRepository, SavingsAccountInterestPostingService savingsAccountInterestPostingService,
|
||||||
ErrorHandler errorHandler, FacilityChargeAutoApplyService facilityChargeAutoApplyService,
|
ErrorHandler errorHandler, FacilityChargeAutoApplyService facilityChargeAutoApplyService,
|
||||||
SavingsTxnDenominationRepository savingsTxnDenominationRepository) {
|
DenominationBreakdownWriter denominationBreakdownWriter) {
|
||||||
return new SavingsAccountWritePlatformServiceJpaRepositoryImpl(context, fromApiJsonDeserializer, savingAccountRepositoryWrapper,
|
return new SavingsAccountWritePlatformServiceJpaRepositoryImpl(context, fromApiJsonDeserializer, savingAccountRepositoryWrapper,
|
||||||
staffRepository, savingsAccountTransactionRepository, savingAccountAssembler, savingsAccountTransactionDataValidator,
|
staffRepository, savingsAccountTransactionRepository, savingAccountAssembler, savingsAccountTransactionDataValidator,
|
||||||
savingsAccountChargeDataValidator, paymentDetailWritePlatformService, journalEntryWritePlatformService,
|
savingsAccountChargeDataValidator, paymentDetailWritePlatformService, journalEntryWritePlatformService,
|
||||||
@@ -434,7 +437,7 @@ public class SavingsConfiguration {
|
|||||||
chargeRepository, savingsAccountChargeRepository, holidayRepository, workingDaysRepository, configurationDomainService,
|
chargeRepository, savingsAccountChargeRepository, holidayRepository, workingDaysRepository, configurationDomainService,
|
||||||
depositAccountOnHoldTransactionRepository, entityDatatableChecksWritePlatformService, appuserRepository,
|
depositAccountOnHoldTransactionRepository, entityDatatableChecksWritePlatformService, appuserRepository,
|
||||||
standingInstructionRepository, businessEventNotifierService, gsimRepository, savingsAccountInterestPostingService,
|
standingInstructionRepository, businessEventNotifierService, gsimRepository, savingsAccountInterestPostingService,
|
||||||
errorHandler, facilityChargeAutoApplyService, savingsTxnDenominationRepository);
|
errorHandler, facilityChargeAutoApplyService, denominationBreakdownWriter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
@@ -319,4 +319,5 @@
|
|||||||
<include file="parts/0300_rename_fd_base_interest_rate_tables_to_cbs.xml" relativeToChangelogFile="true"/>
|
<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/0301_add_current_account_to_transfer_details.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0012_denomination_master.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0012_denomination_master.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0013_cash_exchange.xml" relativeToChangelogFile="true"/>
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|||||||
+24
@@ -177,4 +177,28 @@
|
|||||||
</addColumn>
|
</addColumn>
|
||||||
</changeSet>
|
</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>
|
</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>
|
||||||
Reference in New Issue
Block a user