diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/api/CashExchangeApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/api/CashExchangeApiResource.java new file mode 100644 index 000000000..c06a5d346 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/api/CashExchangeApiResource.java @@ -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 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 map = toMap(header); + List> 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 toMap(CashExchange e) { + Map 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 detailToMap(CashExchangeDetail d) { + Map 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; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/domain/CashExchange.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/domain/CashExchange.java new file mode 100644 index 000000000..586772c5b --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/domain/CashExchange.java @@ -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; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/domain/CashExchangeDetail.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/domain/CashExchangeDetail.java new file mode 100644 index 000000000..ad4696058 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/domain/CashExchangeDetail.java @@ -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; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/domain/CashExchangeDetailRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/domain/CashExchangeDetailRepository.java new file mode 100644 index 000000000..4587720c9 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/domain/CashExchangeDetailRepository.java @@ -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 { + List findByExchangeId(Long exchangeId); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/domain/CashExchangeRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/domain/CashExchangeRepository.java new file mode 100644 index 000000000..3cffef6c1 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashexchange/domain/CashExchangeRepository.java @@ -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 { + List findAllByOrderByCreatedAtDesc(); + List findByAccountIdOrderByCreatedAtDesc(Long accountId); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/SavingsTxnDenomination.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/SavingsTxnDenomination.java index 8db21d345..fedd02652 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/SavingsTxnDenomination.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/SavingsTxnDenomination.java @@ -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; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/service/DenominationBreakdownWriter.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/service/DenominationBreakdownWriter.java new file mode 100644 index 000000000..23c4fb3db --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/service/DenominationBreakdownWriter.java @@ -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: + *
    + *
  1. Each item must have a non-null {@code denominationId}.
  2. + *
  3. Each item's {@code count} must be > 0.
  4. + *
  5. Each item's {@code denominationValue} (or {@code denomination}) must be > 0.
  6. + *
  7. The grand total (sum of value × count) must equal {@code expectedAmount}.
  8. + *
+ * 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 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. + *

+ * 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}). + *

+ * 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); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/DepositAccountTransactionDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/DepositAccountTransactionDataValidator.java index fa09ace7b..03b69a1da 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/DepositAccountTransactionDataValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/DepositAccountTransactionDataValidator.java @@ -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 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 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 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 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>() {}.getType(); - this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, DEPOSIT_ACCOUNT_TRANSACTION_REQUEST_DATA_PARAMETERS); - - final List 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 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>() {}.getType(); - this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, - DEPOSIT_ACCOUNT_RECOMMENDED_DEPOSIT_AMOUNT_UPDATE_REQUEST_DATA_PARAMETERS); - - final List 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>() {}.getType(); - this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, - SavingsAccountConstant.SAVINGS_ACCOUNT_ACTIVATION_REQUEST_DATA_PARAMETERS); - - final List 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>() {}.getType(); - this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, - DEPOSIT_ACCOUNT_PRE_MATURE_CALCULATION_REQUEST_DATA_PARAMETERS); - - final List 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>() {}.getType(); - this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, DEPOSIT_ACCOUNT_CLOSE_REQUEST_DATA_PARAMETERS); - - final List 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 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 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 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 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 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 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>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, DEPOSIT_ACCOUNT_TRANSACTION_REQUEST_DATA_PARAMETERS); + + final List 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 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>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, + DEPOSIT_ACCOUNT_RECOMMENDED_DEPOSIT_AMOUNT_UPDATE_REQUEST_DATA_PARAMETERS); + + final List 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>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, + SavingsAccountConstant.SAVINGS_ACCOUNT_ACTIVATION_REQUEST_DATA_PARAMETERS); + + final List 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>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, + DEPOSIT_ACCOUNT_PRE_MATURE_CALCULATION_REQUEST_DATA_PARAMETERS); + + final List 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>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, DEPOSIT_ACCOUNT_CLOSE_REQUEST_DATA_PARAMETERS); + + final List 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 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 dataValidationErrors) { + if (!dataValidationErrors.isEmpty()) { + // + throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", + dataValidationErrors); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountTransactionWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountTransactionWritePlatformServiceImpl.java index cde77dd4d..09b0ec6d5 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountTransactionWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountTransactionWritePlatformServiceImpl.java @@ -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"); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java index 77808a8eb..c5190d7c6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java @@ -1,1401 +1,1419 @@ -/** - * 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.service; - -import static org.apache.fineract.portfolio.savings.DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.amountParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.chargeIdParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.dueAsOfDateParamName; - -import java.math.BigDecimal; -import java.math.MathContext; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.UUID; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService; -import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; -import org.apache.fineract.infrastructure.core.api.JsonCommand; -import org.apache.fineract.infrastructure.core.data.ApiParameterError; -import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; -import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; -import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; -import org.apache.fineract.infrastructure.core.domain.ExternalId; -import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; -import org.apache.fineract.infrastructure.core.exception.PlatformServiceUnavailableException; -import org.apache.fineract.infrastructure.core.service.DateUtils; -import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; -import org.apache.fineract.organisation.holiday.domain.HolidayRepositoryWrapper; -import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper; -import org.apache.fineract.organisation.monetary.domain.Money; -import org.apache.fineract.organisation.monetary.domain.MoneyHelper; -import org.apache.fineract.organisation.office.domain.Office; -import org.apache.fineract.organisation.staff.domain.Staff; -import org.apache.fineract.organisation.workingdays.domain.WorkingDaysRepositoryWrapper; -import org.apache.fineract.portfolio.account.PortfolioAccountType; -import org.apache.fineract.portfolio.account.data.AccountTransferDTO; -import org.apache.fineract.portfolio.account.data.PortfolioAccountData; -import org.apache.fineract.portfolio.account.domain.AccountTransferType; -import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformService; -import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService; -import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService; -import org.apache.fineract.portfolio.calendar.domain.Calendar; -import org.apache.fineract.portfolio.calendar.domain.CalendarEntityType; -import org.apache.fineract.portfolio.calendar.domain.CalendarFrequencyType; -import org.apache.fineract.portfolio.calendar.domain.CalendarInstance; -import org.apache.fineract.portfolio.calendar.domain.CalendarInstanceRepository; -import org.apache.fineract.portfolio.calendar.domain.CalendarType; -import org.apache.fineract.portfolio.calendar.service.CalendarUtils; -import org.apache.fineract.portfolio.charge.domain.Charge; -import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper; -import org.apache.fineract.portfolio.client.domain.Client; -import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; -import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; -import org.apache.fineract.portfolio.group.domain.Group; -import org.apache.fineract.portfolio.group.exception.GroupNotActiveException; -import org.apache.fineract.portfolio.note.domain.Note; -import org.apache.fineract.portfolio.note.domain.NoteRepository; -import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; -import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService; -import org.apache.fineract.portfolio.savings.DepositAccountType; -import org.apache.fineract.portfolio.savings.DepositsApiConstants; -import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType; -import org.apache.fineract.portfolio.savings.SavingsApiConstants; -import org.apache.fineract.portfolio.savings.data.DepositAccountTransactionDataValidator; -import org.apache.fineract.portfolio.savings.data.SavingsAccountChargeDataValidator; -import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionDTO; -import org.apache.fineract.portfolio.savings.domain.DepositAccountAssembler; -import org.apache.fineract.portfolio.savings.domain.DepositAccountDomainService; -import org.apache.fineract.portfolio.savings.domain.DepositAccountOnHoldTransaction; -import org.apache.fineract.portfolio.savings.domain.DepositAccountOnHoldTransactionRepository; -import org.apache.fineract.portfolio.savings.domain.DepositAccountRecurringDetail; -import org.apache.fineract.portfolio.savings.domain.FixedDepositAccount; -import org.apache.fineract.portfolio.savings.domain.RecurringDepositAccount; -import org.apache.fineract.portfolio.savings.domain.SavingsAccount; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountCharge; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountChargeRepositoryWrapper; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountStatusType; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransactionRepository; -import org.apache.fineract.portfolio.savings.exception.DepositAccountTransactionNotAllowedException; -import org.apache.fineract.portfolio.savings.exception.SavingsAccountTransactionNotFoundException; -import org.apache.fineract.portfolio.savings.exception.TransactionUpdateNotAllowedException; -import org.apache.fineract.useradministration.domain.AppUser; -import org.springframework.transaction.annotation.Transactional; - -@Slf4j -@Transactional -@RequiredArgsConstructor -public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements DepositAccountWritePlatformService { - - private final PlatformSecurityContext context; - private final SavingsAccountRepositoryWrapper savingAccountRepositoryWrapper; - private final SavingsAccountTransactionRepository savingsAccountTransactionRepository; - private final DepositAccountAssembler depositAccountAssembler; - private final DepositAccountTransactionDataValidator depositAccountTransactionDataValidator; - private final SavingsAccountChargeDataValidator savingsAccountChargeDataValidator; - private final PaymentDetailWritePlatformService paymentDetailWritePlatformService; - private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper; - private final JournalEntryWritePlatformService journalEntryWritePlatformService; - private final DepositAccountDomainService depositAccountDomainService; - private final NoteRepository noteRepository; - private final AccountTransfersReadPlatformService accountTransfersReadPlatformService; - private final ChargeRepositoryWrapper chargeRepository; - private final SavingsAccountChargeRepositoryWrapper savingsAccountChargeRepository; - private final AccountAssociationsReadPlatformService accountAssociationsReadPlatformService; - private final AccountTransfersWritePlatformService accountTransfersWritePlatformService; - private final DepositAccountReadPlatformService depositAccountReadPlatformService; - private final CalendarInstanceRepository calendarInstanceRepository; - private final ConfigurationDomainService configurationDomainService; - private final HolidayRepositoryWrapper holidayRepository; - private final WorkingDaysRepositoryWrapper workingDaysRepository; - private final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository; - - @Transactional - @Override - public CommandProcessingResult activateFDAccount(final Long savingsId, final JsonCommand command) { - final AppUser user = context.authenticatedUser(); - - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - final boolean postReversals = false; - this.depositAccountTransactionDataValidator.validateActivation(command); - final MathContext mc = MathContext.DECIMAL64; - final FixedDepositAccount account = (FixedDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, - DepositAccountType.FIXED_DEPOSIT); - checkClientOrGroupActive(account); - - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - - final Map changes = account.activate(user, command); - Money activationChargeAmount = getActivationCharge(account); - if (!changes.isEmpty()) { - final Locale locale = command.extractLocale(); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); - Money amountForDeposit = account.activateWithBalance().plus(activationChargeAmount); - if (amountForDeposit.isGreaterThanZero()) { - final PortfolioAccountData portfolioAccountData = this.accountAssociationsReadPlatformService - .retriveSavingsLinkedAssociation(savingsId); - - if (portfolioAccountData == null) { - final PaymentDetail paymentDetail = null; - this.depositAccountDomainService.handleFDDeposit(account, fmt, account.getActivationDate(), - amountForDeposit.getAmount(), paymentDetail); - } else { - final SavingsAccount fromSavingsAccount = null; - boolean isRegularTransaction = false; - final boolean isExceptionForBalanceCheck = false; - final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(account.getActivationDate(), - amountForDeposit.getAmount(), PortfolioAccountType.SAVINGS, PortfolioAccountType.SAVINGS, - portfolioAccountData.getId(), account.getId(), "Account Transfer", locale, fmt, null, null, null, null, null, - AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, ExternalId.empty(), null, account, - fromSavingsAccount, isRegularTransaction, isExceptionForBalanceCheck); - this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); - } - final boolean isInterestTransfer = false; - final LocalDate postInterestOnDate = null; - if (activationChargeAmount.isGreaterThanZero()) { - payActivationCharge(account); - } - if (account.isBeforeLastPostingPeriod(account.getActivationDate(), false)) { - final LocalDate today = DateUtils.getBusinessLocalDate(); - account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, false); - } else { - final LocalDate today = DateUtils.getBusinessLocalDate(); - account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, false, postReversals); - } - } - - final boolean isPreMatureClosure = false; - account.updateMaturityDateAndAmount(mc, isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth); - List depositAccountOnHoldTransactions = null; - if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { - depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository - .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); - } - account.validateAccountBalanceDoesNotBecomeNegative(SavingsAccountTransactionType.PAY_CHARGE.name(), - depositAccountOnHoldTransactions, false); - this.savingAccountRepositoryWrapper.saveAndFlush(account); - } - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); - - return new CommandProcessingResultBuilder() // - .withEntityId(savingsId) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .with(changes) // - .build(); - } - - private Money getActivationCharge(final FixedDepositAccount account) { - Money activationChargeAmount = Money.zero(account.getCurrency()); - for (SavingsAccountCharge savingsAccountCharge : account.charges()) { - if (savingsAccountCharge.isSavingsActivation()) { - activationChargeAmount = activationChargeAmount.plus(savingsAccountCharge.getAmount(account.getCurrency())); - } - } - return activationChargeAmount; - } - - private void payActivationCharge(final FixedDepositAccount account) { - for (SavingsAccountCharge savingsAccountCharge : account.charges()) { - if (savingsAccountCharge.isSavingsActivation()) { - account.payCharge(savingsAccountCharge, savingsAccountCharge.getAmount(account.getCurrency()), account.getActivationDate(), - false, null); - } - } - } - - @Transactional - @Override - public CommandProcessingResult activateRDAccount(final Long savingsId, final JsonCommand command) { - boolean isRegularTransaction = false; - - final AppUser user = this.context.authenticatedUser(); - - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - final boolean postReversals = false; - this.depositAccountTransactionDataValidator.validateActivation(command); - - final RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, - DepositAccountType.RECURRING_DEPOSIT); - checkClientOrGroupActive(account); - - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - - final Map changes = account.activate(user, command); - - if (!changes.isEmpty()) { - final Locale locale = command.extractLocale(); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); - Money amountForDeposit = account.activateWithBalance(); - if (amountForDeposit.isGreaterThanZero()) { - final PortfolioAccountData portfolioAccountData = this.accountAssociationsReadPlatformService - .retriveSavingsLinkedAssociation(savingsId); - if (portfolioAccountData == null) { - this.depositAccountDomainService.handleRDDeposit(account, fmt, account.getActivationDate(), - amountForDeposit.getAmount(), null, isRegularTransaction); - } else { - final boolean isExceptionForBalanceCheck = false; - final SavingsAccount fromSavingsAccount = null; - final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(account.getActivationDate(), - amountForDeposit.getAmount(), PortfolioAccountType.SAVINGS, PortfolioAccountType.SAVINGS, - portfolioAccountData.getId(), account.getId(), "Account Transfer", locale, fmt, null, null, null, null, null, - AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, ExternalId.empty(), null, account, - fromSavingsAccount, isRegularTransaction, isExceptionForBalanceCheck); - this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); - } - } - - final MathContext mc = MathContext.DECIMAL64; - - // submitted and activation date are different then recalculate - // maturity date and schedule - if (!account.accountSubmittedAndActivationOnSameDate()) { - final boolean isPreMatureClosure = false; - final CalendarInstance calendarInstance = this.calendarInstanceRepository.findByEntityIdAndEntityTypeIdAndCalendarTypeId( - savingsId, CalendarEntityType.SAVINGS.getValue(), CalendarType.COLLECTION.getValue()); - - final Calendar calendar = calendarInstance.getCalendar(); - final PeriodFrequencyType frequencyType = CalendarFrequencyType.from(CalendarUtils.getFrequency(calendar.getRecurrence())); - Integer frequency = CalendarUtils.getInterval(calendar.getRecurrence()); - frequency = frequency == -1 ? 1 : frequency; - account.generateSchedule(frequencyType, frequency, calendar); - account.updateMaturityDateAndAmount(mc, isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth); - } - - final LocalDate overdueUptoDate = DateUtils.getBusinessLocalDate(); - account.updateOverduePayments(overdueUptoDate); - final boolean isInterestTransfer = false; - final LocalDate postInterestOnDate = null; - if (account.isBeforeLastPostingPeriod(account.getActivationDate(), false)) { - final LocalDate today = DateUtils.getBusinessLocalDate(); - account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, - postInterestOnDate, false, postReversals); - } else { - final LocalDate today = DateUtils.getBusinessLocalDate(); - account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, false, postReversals); - } - List depositAccountOnHoldTransactions = null; - if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { - depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository - .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); - } - - account.validateAccountBalanceDoesNotBecomeNegative(SavingsAccountTransactionType.PAY_CHARGE.name(), - depositAccountOnHoldTransactions, false); - - this.savingAccountRepositoryWrapper.saveAndFlush(account); - } - - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); - - return new CommandProcessingResultBuilder() // - .withEntityId(savingsId) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .with(changes) // - .build(); - } - - @Transactional - @Override - public CommandProcessingResult depositToFDAccount(final Long savingsId, @SuppressWarnings("unused") final JsonCommand command) { - // this.context.authenticatedUser(); - this.depositAccountTransactionDataValidator.validate(command, DepositAccountType.FIXED_DEPOSIT); - - final FixedDepositAccount account = (FixedDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, - DepositAccountType.FIXED_DEPOSIT); - checkClientOrGroupActive(account); - - final Locale locale = command.extractLocale(); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); - - final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); - final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); - - final Map changes = new LinkedHashMap<>(); - final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); - final SavingsAccountTransaction deposit = this.depositAccountDomainService.handleFDDeposit(account, fmt, transactionDate, - transactionAmount, paymentDetail); - - return new CommandProcessingResultBuilder().withEntityId(deposit.getId()).withOfficeId(account.officeId()) - .withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).with(changes).build(); - - } - - @Transactional - @Override - public CommandProcessingResult updateDepositAmountForRDAccount(Long savingsId, JsonCommand command) { - this.depositAccountTransactionDataValidator.validateDepositAmountUpdate(command); - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - - final BigDecimal mandatoryRecommendedDepositAmount = command - .bigDecimalValueOfParameterNamed(DepositsApiConstants.mandatoryRecommendedDepositAmountParamName); - - final LocalDate depositAmountUpdateEffectiveFromDate = command - .localDateValueOfParameterNamed(DepositsApiConstants.effectiveDateParamName); - - final RecurringDepositAccount recurringDepositAccount = (RecurringDepositAccount) this.depositAccountAssembler - .assembleFrom(savingsId, DepositAccountType.RECURRING_DEPOSIT); - DepositAccountRecurringDetail recurringDetail = recurringDepositAccount.getRecurringDetail(); - Map changes = recurringDetail.updateMandatoryRecommendedDepositAmount(mandatoryRecommendedDepositAmount, - depositAmountUpdateEffectiveFromDate, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); - - return new CommandProcessingResultBuilder() // - .withEntityId(savingsId) // - .withOfficeId(recurringDepositAccount.officeId()) // - .withClientId(recurringDepositAccount.clientId()) // - .withGroupId(recurringDepositAccount.groupId()) // - .withSavingsId(savingsId) // - .with(changes) // - .build(); - } - - @Transactional - @Override - public CommandProcessingResult depositToRDAccount(final Long savingsId, final JsonCommand command) { - boolean isRegularTransaction = true; - - this.depositAccountTransactionDataValidator.validate(command, DepositAccountType.RECURRING_DEPOSIT); - - final RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, - DepositAccountType.RECURRING_DEPOSIT); - checkClientOrGroupActive(account); - - final Locale locale = command.extractLocale(); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); - - final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); - final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); - - final Map changes = new LinkedHashMap<>(); - final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); - final SavingsAccountTransaction deposit = this.depositAccountDomainService.handleRDDeposit(account, fmt, transactionDate, - transactionAmount, paymentDetail, isRegularTransaction); - - return new CommandProcessingResultBuilder() // - .withEntityId(deposit.getId()) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .with(changes) // - .build(); - } - - private Long saveTransactionToGenerateTransactionId(final SavingsAccountTransaction transaction) { - this.savingsAccountTransactionRepository.saveAndFlush(transaction); - return transaction.getId(); - } - - @Transactional - @Override - public CommandProcessingResult withdrawal(final Long savingsId, final JsonCommand command, - final DepositAccountType depositAccountType) { - - boolean isRegularTransaction = true; - - this.depositAccountTransactionDataValidator.validate(command, depositAccountType); - - final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); - final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); - - final Locale locale = command.extractLocale(); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); - - final Map changes = new LinkedHashMap<>(); - final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); - - final SavingsAccount account = this.depositAccountAssembler.assembleFrom(savingsId, depositAccountType); - - checkClientOrGroupActive(account); - - final SavingsAccountTransaction withdrawal = this.depositAccountDomainService.handleWithdrawal(account, fmt, transactionDate, - transactionAmount, paymentDetail, true, isRegularTransaction); - - return new CommandProcessingResultBuilder() // - .withEntityId(withdrawal.getId()) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .with(changes)// - .build(); - } - - @Transactional - @Override - public CommandProcessingResult calculateInterest(final Long savingsId, final DepositAccountType depositAccountType) { - - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - - final SavingsAccount account = this.depositAccountAssembler.assembleFrom(savingsId, depositAccountType); - checkClientOrGroupActive(account); - - final LocalDate today = DateUtils.getBusinessLocalDate(); - final boolean postReversals = false; - final MathContext mc = new MathContext(15, MoneyHelper.getRoundingMode()); - boolean isInterestTransfer = false; - LocalDate postInterestOnDate = null; - account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, false, postReversals); - - this.savingAccountRepositoryWrapper.save(account); - - return new CommandProcessingResultBuilder() // - .withEntityId(savingsId) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .build(); - } - - @Transactional - @Override - public CommandProcessingResult postInterest(final Long savingsId, final DepositAccountType depositAccountType) { - - final SavingsAccount account = this.depositAccountAssembler.assembleFrom(savingsId, depositAccountType); - checkClientOrGroupActive(account); - postInterest(account); - return new CommandProcessingResultBuilder() // - .withEntityId(savingsId) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .build(); - } - - @Transactional - private void postInterest(final SavingsAccount account) { - - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - final LocalDate today = DateUtils.getBusinessLocalDate(); - final MathContext mc = new MathContext(10, MoneyHelper.getRoundingMode()); - boolean isInterestTransfer = false; - LocalDate postInterestOnDate = null; - final boolean postReversals = false; - account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, - postInterestOnDate, false, postReversals); - this.savingAccountRepositoryWrapper.saveAndFlush(account); - - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); - } - - @Override - public CommandProcessingResult undoFDTransaction(final Long savingsId, @SuppressWarnings("unused") final Long transactionId, - @SuppressWarnings("unused") final boolean allowAccountTransferModification) { - - throw new DepositAccountTransactionNotAllowedException(savingsId, "undo", DepositAccountType.FIXED_DEPOSIT); - } - - @Override - public CommandProcessingResult undoRDTransaction(final Long savingsId, final Long transactionId, - final boolean allowAccountTransferModification) { - - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - - final RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, - DepositAccountType.RECURRING_DEPOSIT); - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - - final SavingsAccountTransaction savingsAccountTransaction = this.savingsAccountTransactionRepository - .findOneByIdAndSavingsAccountId(transactionId, savingsId); - if (savingsAccountTransaction == null) { - throw new SavingsAccountTransactionNotFoundException(savingsId, transactionId); - } - - if (!allowAccountTransferModification - && this.accountTransfersReadPlatformService.isAccountTransfer(transactionId, PortfolioAccountType.SAVINGS)) { - throw new PlatformServiceUnavailableException("error.msg.recurring.deposit.account.transfer.transaction.update.not.allowed", - "Recurring deposit account transaction:" + transactionId + " update not allowed as it involves in account transfer", - transactionId); - } - - final LocalDate today = DateUtils.getBusinessLocalDate(); - final MathContext mc = MathContext.DECIMAL64; - - if (account.isNotActive()) { - throwValidationForActiveStatus(SavingsApiConstants.undoTransactionAction); - } - account.undoTransaction(transactionId); - boolean isInterestTransfer = false; - LocalDate postInterestOnDate = null; - checkClientOrGroupActive(account); - final boolean postReversals = false; - if (savingsAccountTransaction.isPostInterestCalculationRequired() - && account.isBeforeLastPostingPeriod(savingsAccountTransaction.getTransactionDate(), false)) { - account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, - postInterestOnDate, false, postReversals); - } else { - account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, false, postReversals); - } - List depositAccountOnHoldTransactions = null; - if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { - depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository - .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); - } - - account.validateAccountBalanceDoesNotBecomeNegative(SavingsApiConstants.undoTransactionAction, depositAccountOnHoldTransactions, - false); - // account.activateAccountBasedOnBalance(); - final boolean isPreMatureClosure = false; - account.updateMaturityDateAndAmount(mc, isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth); - - final LocalDate overdueUptoDate = DateUtils.getBusinessLocalDate(); - - if (savingsAccountTransaction.isDeposit()) { - account.updateScheduleInstallments(); - } - - account.updateOverduePayments(overdueUptoDate); - - this.savingAccountRepositoryWrapper.saveAndFlush(account); - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); - - return new CommandProcessingResultBuilder() // - .withEntityId(savingsId) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .build(); - } - - @Override - public CommandProcessingResult adjustFDTransaction(final Long savingsId, @SuppressWarnings("unused") final Long transactionId, - @SuppressWarnings("unused") final JsonCommand command) { - - throw new DepositAccountTransactionNotAllowedException(savingsId, "modify", DepositAccountType.FIXED_DEPOSIT); - } - - @Override - public CommandProcessingResult adjustRDTransaction(final Long savingsId, final Long transactionId, final JsonCommand command) { - context.authenticatedUser(); - - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate(); - this.depositAccountTransactionDataValidator.validate(command, DepositAccountType.RECURRING_DEPOSIT); - - final SavingsAccountTransaction savingsAccountTransaction = this.savingsAccountTransactionRepository - .findOneByIdAndSavingsAccountId(transactionId, savingsId); - if (savingsAccountTransaction == null) { - throw new SavingsAccountTransactionNotFoundException(savingsId, transactionId); - } - - if ((!savingsAccountTransaction.isDeposit() && !savingsAccountTransaction.isWithdrawal()) - || savingsAccountTransaction.isReversed()) { - throw new TransactionUpdateNotAllowedException(savingsId, transactionId); - } - - if (this.accountTransfersReadPlatformService.isAccountTransfer(transactionId, PortfolioAccountType.SAVINGS)) { - throw new PlatformServiceUnavailableException("error.msg.saving.account.transfer.transaction.update.not.allowed", - "Deposit account transaction:" + transactionId + " update not allowed as it involves in account transfer", - transactionId); - } - - final LocalDate today = DateUtils.getBusinessLocalDate(); - - final RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, - DepositAccountType.RECURRING_DEPOSIT); - if (account.isNotActive()) { - throwValidationForActiveStatus(SavingsApiConstants.adjustTransactionAction); - } - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - - final Locale locale = command.extractLocale(); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); - final LocalDate transactionDate = command.localDateValueOfParameterNamed(SavingsApiConstants.transactionDateParamName); - final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed(SavingsApiConstants.transactionAmountParamName); - final Map changes = new LinkedHashMap<>(); - final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); - - final MathContext mc = new MathContext(10, MoneyHelper.getRoundingMode()); - account.undoTransaction(transactionId); - - SavingsAccountTransaction transaction = null; - Integer accountType = null; - UUID refNo = UUID.randomUUID(); - if (savingsAccountTransaction.isDeposit()) { - final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount, - paymentDetail, null, accountType); - transaction = account.deposit(transactionDTO, false, relaxingDaysConfigForPivotDate, refNo.toString()); - } else { - final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount, - paymentDetail, null, accountType); - transaction = account.withdraw(transactionDTO, true, false, relaxingDaysConfigForPivotDate, refNo.toString()); - } - final Long newtransactionId = saveTransactionToGenerateTransactionId(transaction); - boolean isInterestTransfer = false; - final LocalDate postInterestOnDate = null; - final boolean postReversals = false; - if (account.isBeforeLastPostingPeriod(transactionDate, false) - || account.isBeforeLastPostingPeriod(savingsAccountTransaction.getTransactionDate(), false)) { - account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, - postInterestOnDate, false, postReversals); - } else { - account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, false, postReversals); - } - List depositAccountOnHoldTransactions = null; - if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { - depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository - .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); - } - - account.validateAccountBalanceDoesNotBecomeNegative(SavingsApiConstants.adjustTransactionAction, depositAccountOnHoldTransactions, - false); - account.activateAccountBasedOnBalance(); - - if (savingsAccountTransaction.isDeposit()) { - account.handleScheduleInstallments(savingsAccountTransaction); - } - final LocalDate overdueUptoDate = DateUtils.getBusinessLocalDate(); - account.updateOverduePayments(overdueUptoDate); - - this.savingAccountRepositoryWrapper.saveAndFlush(account); - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); - return new CommandProcessingResultBuilder() // - .withEntityId(newtransactionId) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .with(changes)// - .build(); - } - - /** - * - */ - private void throwValidationForActiveStatus(final String actionName) { - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(SAVINGS_ACCOUNT_RESOURCE_NAME + actionName); - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("account.is.not.active"); - throw new PlatformApiDataValidationException(dataValidationErrors); - } - - private void checkClientOrGroupActive(final SavingsAccount account) { - final Client client = account.getClient(); - if (client != null && client.isNotActive()) { - throw new ClientNotActiveException(client.getId()); - } - final Group group = account.group(); - if (group != null && group.isNotActive()) { - throw new GroupNotActiveException(group.getId()); - } - } - - @Override - public CommandProcessingResult closeFDAccount(final Long savingsId, final JsonCommand command) { - final AppUser user = this.context.authenticatedUser(); - final boolean isPreMatureClose = false; - this.depositAccountTransactionDataValidator.validateClosing(command, DepositAccountType.FIXED_DEPOSIT, isPreMatureClose); - - final Map changes = new LinkedHashMap<>(); - final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); - - final FixedDepositAccount account = (FixedDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, - DepositAccountType.FIXED_DEPOSIT); - checkClientOrGroupActive(account); - - this.depositAccountDomainService.handleFDAccountClosure(account, paymentDetail, user, command, changes); - - final String noteText = command.stringValueOfParameterNamed("note"); - if (StringUtils.isNotBlank(noteText)) { - final Note note = Note.savingNote(account, noteText); - changes.put("note", noteText); - this.noteRepository.save(note); - } - - return new CommandProcessingResultBuilder() // - .withEntityId(savingsId) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .with(changes)// - .build(); - - } - - @Override - public CommandProcessingResult closeRDAccount(final Long savingsId, final JsonCommand command) { - final AppUser user = this.context.authenticatedUser(); - - this.depositAccountTransactionDataValidator.validateClosing(command, DepositAccountType.RECURRING_DEPOSIT, false); - - final Map changes = new LinkedHashMap<>(); - final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); - - final RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, - DepositAccountType.RECURRING_DEPOSIT); - checkClientOrGroupActive(account); - - this.depositAccountDomainService.handleRDAccountClosure(account, paymentDetail, user, command, changes); - - final String noteText = command.stringValueOfParameterNamed("note"); - if (StringUtils.isNotBlank(noteText)) { - final Note note = Note.savingNote(account, noteText); - changes.put("note", noteText); - this.noteRepository.save(note); - } - - return new CommandProcessingResultBuilder() // - .withEntityId(savingsId) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .with(changes)// - .build(); - - } - - @Override - public CommandProcessingResult prematureCloseFDAccount(final Long savingsId, final JsonCommand command) { - final AppUser user = this.context.authenticatedUser(); - - this.depositAccountTransactionDataValidator.validateClosing(command, DepositAccountType.FIXED_DEPOSIT, true); - - final Map changes = new LinkedHashMap<>(); - final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); - - final FixedDepositAccount account = (FixedDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, - DepositAccountType.FIXED_DEPOSIT); - checkClientOrGroupActive(account); - - this.depositAccountDomainService.handleFDAccountPreMatureClosure(account, paymentDetail, user, command, changes); - - final String noteText = command.stringValueOfParameterNamed("note"); - if (StringUtils.isNotBlank(noteText)) { - final Note note = Note.savingNote(account, noteText); - changes.put("note", noteText); - this.noteRepository.save(note); - } - - return new CommandProcessingResultBuilder() // - .withEntityId(savingsId) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .with(changes)// - .build(); - - } - - @Override - public CommandProcessingResult prematureCloseRDAccount(final Long savingsId, final JsonCommand command) { - final AppUser user = this.context.authenticatedUser(); - - this.depositAccountTransactionDataValidator.validateClosing(command, DepositAccountType.RECURRING_DEPOSIT, true); - - final Map changes = new LinkedHashMap<>(); - final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); - - final RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, - DepositAccountType.RECURRING_DEPOSIT); - checkClientOrGroupActive(account); - if (account.maturityDate() == null) { - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME + DepositsApiConstants.preMatureCloseAction); - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("can.not.close.as.premature"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - this.depositAccountDomainService.handleRDAccountPreMatureClosure(account, paymentDetail, user, command, changes); - - final String noteText = command.stringValueOfParameterNamed("note"); - if (StringUtils.isNotBlank(noteText)) { - final Note note = Note.savingNote(account, noteText); - changes.put("note", noteText); - this.noteRepository.save(note); - } - - return new CommandProcessingResultBuilder() // - .withEntityId(savingsId) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .with(changes)// - .build(); - - } - - @Override - public SavingsAccountTransaction initiateSavingsTransfer(final Long accountId, final LocalDate transferDate, - final DepositAccountType depositAccountType) { - context.authenticatedUser(); - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - - final SavingsAccount savingsAccount = this.depositAccountAssembler.assembleFrom(accountId, depositAccountType); - final LocalDate postInterestOnDate = null; - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - updateExistingTransactionsDetails(savingsAccount, existingTransactionIds, existingReversedTransactionIds); - - final SavingsAccountTransaction newTransferTransaction = SavingsAccountTransaction.initiateTransfer(savingsAccount, - savingsAccount.office(), transferDate); - savingsAccount.addTransaction(newTransferTransaction); - savingsAccount.setStatus(SavingsAccountStatusType.TRANSFER_IN_PROGRESS.getValue()); - final MathContext mc = MathContext.DECIMAL64; - boolean isInterestTransfer = false; - final boolean postReversals = false; - savingsAccount.calculateInterestUsing(mc, transferDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, false, postReversals); - - this.savingsAccountTransactionRepository.save(newTransferTransaction); - this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); - - postJournalEntries(savingsAccount, existingTransactionIds, existingReversedTransactionIds); - - return newTransferTransaction; - } - - @Override - public SavingsAccountTransaction withdrawSavingsTransfer(final Long accountId, final LocalDate transferDate, - final DepositAccountType depositAccountType) { - context.authenticatedUser(); - - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - - final SavingsAccount savingsAccount = this.depositAccountAssembler.assembleFrom(accountId, depositAccountType); - - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - updateExistingTransactionsDetails(savingsAccount, existingTransactionIds, existingReversedTransactionIds); - - final SavingsAccountTransaction withdrawtransferTransaction = SavingsAccountTransaction.withdrawTransfer(savingsAccount, - savingsAccount.office(), transferDate); - savingsAccount.addTransaction(withdrawtransferTransaction); - savingsAccount.setStatus(SavingsAccountStatusType.ACTIVE.getValue()); - final boolean postReversals = false; - final MathContext mc = MathContext.DECIMAL64; - boolean isInterestTransfer = false; - LocalDate postInterestOnDate = null; - savingsAccount.calculateInterestUsing(mc, transferDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, false, postReversals); - - this.savingsAccountTransactionRepository.save(withdrawtransferTransaction); - this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); - - postJournalEntries(savingsAccount, existingTransactionIds, existingReversedTransactionIds); - - return withdrawtransferTransaction; - } - - @Override - public void rejectSavingsTransfer(final Long accountId, final DepositAccountType depositAccountType) { - final SavingsAccount savingsAccount = this.depositAccountAssembler.assembleFrom(accountId, depositAccountType); - savingsAccount.setStatus(SavingsAccountStatusType.TRANSFER_ON_HOLD.getValue()); - this.savingAccountRepositoryWrapper.save(savingsAccount); - } - - @Override - public SavingsAccountTransaction acceptSavingsTransfer(final Long accountId, final LocalDate transferDate, - final Office acceptedInOffice, final Staff fieldOfficer, final DepositAccountType depositAccountType) { - context.authenticatedUser(); - - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - - final SavingsAccount savingsAccount = this.depositAccountAssembler.assembleFrom(accountId, depositAccountType); - - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - updateExistingTransactionsDetails(savingsAccount, existingTransactionIds, existingReversedTransactionIds); - - final SavingsAccountTransaction acceptTransferTransaction = SavingsAccountTransaction.approveTransfer(savingsAccount, - acceptedInOffice, transferDate); - savingsAccount.addTransaction(acceptTransferTransaction); - savingsAccount.setStatus(SavingsAccountStatusType.ACTIVE.getValue()); - if (fieldOfficer != null) { - savingsAccount.reassignSavingsOfficer(fieldOfficer, transferDate); - } - boolean isInterestTransfer = false; - final boolean postReversals = false; - LocalDate postInterestOnDate = null; - final MathContext mc = MathContext.DECIMAL64; - savingsAccount.calculateInterestUsing(mc, transferDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, false, postReversals); - - this.savingsAccountTransactionRepository.save(acceptTransferTransaction); - this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); - - postJournalEntries(savingsAccount, existingTransactionIds, existingReversedTransactionIds); - - return acceptTransferTransaction; - } - - @Transactional - @Override - public CommandProcessingResult addSavingsAccountCharge(final JsonCommand command, final DepositAccountType depositAccountType) { - - this.context.authenticatedUser(); - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(SAVINGS_ACCOUNT_RESOURCE_NAME); - - final Long savingsAccountId = command.getSavingsId(); - this.savingsAccountChargeDataValidator.validateAdd(command.json()); - - final SavingsAccount savingsAccount = this.depositAccountAssembler.assembleFrom(savingsAccountId, depositAccountType); - checkClientOrGroupActive(savingsAccount); - - final Locale locale = command.extractLocale(); - final String format = command.dateFormat(); - final DateTimeFormatter fmt = StringUtils.isNotBlank(format) ? DateTimeFormatter.ofPattern(format).withLocale(locale) - : DateTimeFormatter.ofPattern("dd MM yyyy"); - - final Long chargeDefinitionId = command.longValueOfParameterNamed(chargeIdParamName); - final Charge chargeDefinition = this.chargeRepository.findOneWithNotFoundDetection(chargeDefinitionId); - - final SavingsAccountCharge savingsAccountCharge = SavingsAccountCharge.createNewFromJson(savingsAccount, chargeDefinition, command); - - if (savingsAccountCharge.getDueDate() != null) { - // transaction date should not be on a holiday or non working day - if (!this.configurationDomainService.allowTransactionsOnHolidayEnabled() - && this.holidayRepository.isHoliday(savingsAccount.officeId(), savingsAccountCharge.getDueDate())) { - baseDataValidator.reset().parameter(dueAsOfDateParamName).value(savingsAccountCharge.getDueDate().format(fmt)) - .failWithCodeNoParameterAddedToErrorCode("charge.due.date.is.on.holiday"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - if (!this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled() - && !this.workingDaysRepository.isWorkingDay(savingsAccountCharge.getDueDate())) { - baseDataValidator.reset().parameter(dueAsOfDateParamName).value(savingsAccountCharge.getDueDate().format(fmt)) - .failWithCodeNoParameterAddedToErrorCode("charge.due.date.is.a.nonworking.day"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - } - - savingsAccount.addCharge(fmt, savingsAccountCharge, chargeDefinition); - - this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); - - return new CommandProcessingResultBuilder() // - .withEntityId(savingsAccountCharge.getId()) // - .withOfficeId(savingsAccount.officeId()) // - .withClientId(savingsAccount.clientId()) // - .withGroupId(savingsAccount.groupId()) // - .withSavingsId(savingsAccountId) // - .build(); - } - - @Transactional - @Override - public CommandProcessingResult updateSavingsAccountCharge(final JsonCommand command, final DepositAccountType depositAccountType) { - - this.context.authenticatedUser(); - this.savingsAccountChargeDataValidator.validateUpdate(command.json()); - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(SAVINGS_ACCOUNT_RESOURCE_NAME); - - final Long savingsAccountId = command.getSavingsId(); - // SavingsAccount Charge entity - final Long savingsChargeId = command.entityId(); - - final SavingsAccount savingsAccount = this.depositAccountAssembler.assembleFrom(savingsAccountId, depositAccountType); - checkClientOrGroupActive(savingsAccount); - - final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository.findOneWithNotFoundDetection(savingsChargeId, - savingsAccountId); - - final Map changes = savingsAccountCharge.update(command); - - if (savingsAccountCharge.getDueDate() != null) { - final Locale locale = command.extractLocale(); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); - - // transaction date should not be on a holiday or non working day - if (!this.configurationDomainService.allowTransactionsOnHolidayEnabled() - && this.holidayRepository.isHoliday(savingsAccount.officeId(), savingsAccountCharge.getDueDate())) { - baseDataValidator.reset().parameter(dueAsOfDateParamName).value(savingsAccountCharge.getDueDate().format(fmt)) - .failWithCodeNoParameterAddedToErrorCode("charge.due.date.is.on.holiday"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - if (!this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled() - && !this.workingDaysRepository.isWorkingDay(savingsAccountCharge.getDueDate())) { - baseDataValidator.reset().parameter(dueAsOfDateParamName).value(savingsAccountCharge.getDueDate().format(fmt)) - .failWithCodeNoParameterAddedToErrorCode("charge.due.date.is.a.nonworking.day"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - } - - this.savingsAccountChargeRepository.saveAndFlush(savingsAccountCharge); - - return new CommandProcessingResultBuilder() // - .withEntityId(savingsAccountCharge.getId()) // - .withOfficeId(savingsAccountCharge.savingsAccount().officeId()) // - .withClientId(savingsAccountCharge.savingsAccount().clientId()) // - .withGroupId(savingsAccountCharge.savingsAccount().groupId()) // - .withSavingsId(savingsAccountCharge.savingsAccount().getId()) // - .with(changes) // - .build(); - } - - @Transactional - @Override - public CommandProcessingResult waiveCharge(final Long savingsAccountId, final Long savingsAccountChargeId, - @SuppressWarnings("unused") final DepositAccountType depositAccountType) { - context.authenticatedUser(); - - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - - final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository - .findOneWithNotFoundDetection(savingsAccountChargeId, savingsAccountId); - - // Get Savings account from savings charge - final SavingsAccount account = savingsAccountCharge.savingsAccount(); - this.depositAccountAssembler.assignSavingAccountHelpers(account); - - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - - account.waiveCharge(savingsAccountChargeId, false); - boolean isInterestTransfer = false; - LocalDate postInterestOnDate = null; - final MathContext mc = MathContext.DECIMAL64; - final boolean postReversals = false; - if (account.isBeforeLastPostingPeriod(savingsAccountCharge.getDueDate(), false)) { - final LocalDate today = DateUtils.getBusinessLocalDate(); - account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, - postInterestOnDate, false, postReversals); - } else { - final LocalDate today = DateUtils.getBusinessLocalDate(); - account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, false, postReversals); - } - List depositAccountOnHoldTransactions = null; - if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { - depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository - .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); - } - - account.validateAccountBalanceDoesNotBecomeNegative(SavingsApiConstants.waiveChargeTransactionAction, - depositAccountOnHoldTransactions, false); - - this.savingAccountRepositoryWrapper.saveAndFlush(account); - - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); - - return new CommandProcessingResultBuilder() // - .withEntityId(savingsAccountChargeId) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsAccountId) // - .build(); - } - - @Transactional - @Override - public CommandProcessingResult deleteSavingsAccountCharge(final Long savingsAccountId, final Long savingsAccountChargeId, - @SuppressWarnings("unused") final JsonCommand command, final DepositAccountType depositAccountType) { - this.context.authenticatedUser(); - - final SavingsAccount savingsAccount = this.depositAccountAssembler.assembleFrom(savingsAccountId, depositAccountType); - checkClientOrGroupActive(savingsAccount); - final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository - .findOneWithNotFoundDetection(savingsAccountChargeId, savingsAccountId); - - savingsAccount.removeCharge(savingsAccountCharge); - this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); - - return new CommandProcessingResultBuilder() // - .withEntityId(savingsAccountChargeId) // - .withOfficeId(savingsAccount.officeId()) // - .withClientId(savingsAccount.clientId()) // - .withGroupId(savingsAccount.groupId()) // - .withSavingsId(savingsAccountId) // - .build(); - } - - @Override - public CommandProcessingResult payCharge(final Long savingsAccountId, final Long savingsAccountChargeId, final JsonCommand command, - @SuppressWarnings("unused") final DepositAccountType depositAccountType) { - - this.context.authenticatedUser(); - - this.savingsAccountChargeDataValidator.validatePayCharge(command.json()); - final Locale locale = command.extractLocale(); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); - final BigDecimal amountPaid = command.bigDecimalValueOfParameterNamed(amountParamName); - final LocalDate transactionDate = command.localDateValueOfParameterNamed(dueAsOfDateParamName); - - final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository - .findOneWithNotFoundDetection(savingsAccountChargeId, savingsAccountId); - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(SAVINGS_ACCOUNT_RESOURCE_NAME); - - // transaction date should not be on a holiday or non working day - if (!this.configurationDomainService.allowTransactionsOnHolidayEnabled() - && this.holidayRepository.isHoliday(savingsAccountCharge.savingsAccount().officeId(), transactionDate)) { - baseDataValidator.reset().parameter(dueAsOfDateParamName).value(transactionDate.format(fmt)) - .failWithCodeNoParameterAddedToErrorCode("transaction.not.allowed.transaction.date.is.on.holiday"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - if (!this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled() - && !this.workingDaysRepository.isWorkingDay(transactionDate)) { - baseDataValidator.reset().parameter(dueAsOfDateParamName).value(transactionDate.format(fmt)) - .failWithCodeNoParameterAddedToErrorCode("transaction.not.allowed.transaction.date.is.a.nonworking.day"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - this.payCharge(savingsAccountCharge, transactionDate, amountPaid, fmt); - return new CommandProcessingResultBuilder() // - .withEntityId(savingsAccountCharge.getId()) // - .withOfficeId(savingsAccountCharge.savingsAccount().officeId()) // - .withClientId(savingsAccountCharge.savingsAccount().clientId()) // - .withGroupId(savingsAccountCharge.savingsAccount().groupId()) // - .withSavingsId(savingsAccountCharge.savingsAccount().getId()) // - .build(); - - } - - @Transactional - @Override - public void applyChargeDue(final Long savingsAccountChargeId, final Long accountId, - @SuppressWarnings("unused") final DepositAccountType depositAccountType) { - final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository - .findOneWithNotFoundDetection(savingsAccountChargeId, accountId); - // always use current date as transaction date for batch job - final LocalDate transactionDate = DateUtils.getBusinessLocalDate(); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd MM yyyy"); - - while (DateUtils.isBefore(savingsAccountCharge.getDueDate(), transactionDate)) { - payCharge(savingsAccountCharge, transactionDate, savingsAccountCharge.amoutOutstanding(), fmt); - } - } - - @Transactional - private void payCharge(final SavingsAccountCharge savingsAccountCharge, final LocalDate transactionDate, final BigDecimal amountPaid, - final DateTimeFormatter formatter) { - context.authenticatedUser(); - - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - - // Get Savings account from savings charge - final SavingsAccount account = savingsAccountCharge.savingsAccount(); - this.depositAccountAssembler.assignSavingAccountHelpers(account); - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - account.payCharge(savingsAccountCharge, amountPaid, transactionDate, formatter, false, null); - boolean isInterestTransfer = false; - LocalDate postInterestOnDate = null; - final MathContext mc = MathContext.DECIMAL64; - final boolean postReversals = false; - if (account.isBeforeLastPostingPeriod(transactionDate, false)) { - final LocalDate today = DateUtils.getBusinessLocalDate(); - account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, - postInterestOnDate, false, postReversals); - } else { - final LocalDate today = DateUtils.getBusinessLocalDate(); - account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, false, postReversals); - } - List depositAccountOnHoldTransactions = null; - if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { - depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository - .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); - } - - account.validateAccountBalanceDoesNotBecomeNegative("." + SavingsAccountTransactionType.PAY_CHARGE.getCode(), - depositAccountOnHoldTransactions, false); - - this.savingAccountRepositoryWrapper.saveAndFlush(account); - - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); - } - - @Transactional - @Override - public void updateMaturityDetails(Long depositAccountId, DepositAccountType depositAccountType) { - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - final boolean postReversals = false; - final SavingsAccount account = this.depositAccountAssembler.assembleFrom(depositAccountId, depositAccountType); - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - - if (depositAccountType.isFixedDeposit()) { - ((FixedDepositAccount) account).updateMaturityStatus(isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); - FixedDepositAccount fdAccount = ((FixedDepositAccount) account); - // handle maturity instructions - - if (fdAccount.isMatured() && (fdAccount.isReinvestOnClosure() || fdAccount.isTransferToSavingsOnClosure())) { - DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - Map changes = new HashMap<>(); - final AppUser user = context.authenticatedUser(); - Long toSavingsId = fdAccount.getTransferToSavingsAccountId(); - this.depositAccountDomainService.handleFDAccountMaturityClosure(fdAccount, null, user, fmt, fdAccount.maturityDate(), - fdAccount.getOnAccountClosureId(), toSavingsId, "Apply maturity instructions", changes); - - if (changes.get("reinvestedDepositId") != null) { - Long reinvestedDepositId = (Long) changes.get("reinvestedDepositId"); - Money amountForDeposit = account.activateWithBalance(); - final FixedDepositAccount reinvestAccount = (FixedDepositAccount) this.depositAccountAssembler - .assembleFrom(reinvestedDepositId, DepositAccountType.FIXED_DEPOSIT); - Money activationChargeAmount = getActivationCharge(reinvestAccount); - if (activationChargeAmount.isGreaterThanZero()) { - payActivationCharge(reinvestAccount); - amountForDeposit = amountForDeposit.plus(activationChargeAmount); - } - this.depositAccountDomainService.handleFDDeposit(reinvestAccount, fmt, fdAccount.maturityDate(), - amountForDeposit.getAmount(), null); - } - } - } else if (depositAccountType.isRecurringDeposit()) { - ((RecurringDepositAccount) account).updateMaturityStatus(isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postReversals); - } - this.savingAccountRepositoryWrapper.saveAndFlush(account); - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); - } - - private void updateExistingTransactionsDetails(SavingsAccount account, Set existingTransactionIds, - Set existingReversedTransactionIds) { - existingTransactionIds.addAll(account.findExistingTransactionIds()); - existingReversedTransactionIds.addAll(account.findExistingReversedTransactionIds()); - } - - private void postJournalEntries(final SavingsAccount savingsAccount, final Set existingTransactionIds, - final Set existingReversedTransactionIds) { - - boolean isAccountTransfer = false; - final Map accountingBridgeData = savingsAccount.deriveAccountingBridgeData(savingsAccount.getCurrency().getCode(), - existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, false); - this.journalEntryWritePlatformService.createJournalEntriesForSavings(accountingBridgeData); - } - - @Transactional - @Override - public SavingsAccountTransaction mandatorySavingsAccountDeposit(final SavingsAccountTransactionDTO accountTransactionDTO) { - boolean isRegularTransaction = false; - final PaymentDetail paymentDetail = accountTransactionDTO.getPaymentDetail(); - if (paymentDetail != null && paymentDetail.getId() == null) { - this.paymentDetailWritePlatformService.persistPaymentDetail(paymentDetail); - } - if (accountTransactionDTO.getAccountType().equals(DepositAccountType.RECURRING_DEPOSIT.getValue())) { - RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler - .assembleFrom(accountTransactionDTO.getSavingsAccountId(), DepositAccountType.RECURRING_DEPOSIT); - return this.depositAccountDomainService.handleRDDeposit(account, accountTransactionDTO.getFormatter(), - accountTransactionDTO.getTransactionDate(), accountTransactionDTO.getTransactionAmount(), paymentDetail, - isRegularTransaction); - } - SavingsAccount account = null; - if (accountTransactionDTO.getAccountType().equals(DepositAccountType.SAVINGS_DEPOSIT.getValue())) { - account = this.depositAccountAssembler.assembleFrom(accountTransactionDTO.getSavingsAccountId(), - DepositAccountType.SAVINGS_DEPOSIT); - } else { - account = this.depositAccountAssembler.assembleFrom(accountTransactionDTO.getSavingsAccountId(), - DepositAccountType.CURRENT_DEPOSIT); - } - return this.depositAccountDomainService.handleSavingDeposit(account, accountTransactionDTO.getFormatter(), - accountTransactionDTO.getTransactionDate(), accountTransactionDTO.getTransactionAmount(), paymentDetail, - isRegularTransaction); - - } -} +/** + * 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.service; + +import static org.apache.fineract.portfolio.savings.DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.amountParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.chargeIdParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.dueAsOfDateParamName; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService; +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.ApiParameterError; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; +import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.apache.fineract.infrastructure.core.exception.PlatformServiceUnavailableException; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.organisation.holiday.domain.HolidayRepositoryWrapper; +import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.organisation.monetary.domain.MoneyHelper; +import org.apache.fineract.organisation.office.domain.Office; +import org.apache.fineract.organisation.staff.domain.Staff; +import org.apache.fineract.organisation.workingdays.domain.WorkingDaysRepositoryWrapper; +import org.apache.fineract.portfolio.account.PortfolioAccountType; +import org.apache.fineract.portfolio.account.data.AccountTransferDTO; +import org.apache.fineract.portfolio.account.data.PortfolioAccountData; +import org.apache.fineract.portfolio.account.domain.AccountTransferType; +import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformService; +import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService; +import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService; +import org.apache.fineract.portfolio.calendar.domain.Calendar; +import org.apache.fineract.portfolio.calendar.domain.CalendarEntityType; +import org.apache.fineract.portfolio.calendar.domain.CalendarFrequencyType; +import org.apache.fineract.portfolio.calendar.domain.CalendarInstance; +import org.apache.fineract.portfolio.calendar.domain.CalendarInstanceRepository; +import org.apache.fineract.portfolio.calendar.domain.CalendarType; +import org.apache.fineract.portfolio.calendar.service.CalendarUtils; +import org.apache.fineract.portfolio.charge.domain.Charge; +import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper; +import org.apache.fineract.portfolio.client.domain.Client; +import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; +import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; +import org.apache.fineract.portfolio.denominationmaster.service.DenominationBreakdownWriter; +import org.apache.fineract.portfolio.group.domain.Group; +import org.apache.fineract.portfolio.group.exception.GroupNotActiveException; +import org.apache.fineract.portfolio.note.domain.Note; +import org.apache.fineract.portfolio.note.domain.NoteRepository; +import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; +import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService; +import org.apache.fineract.portfolio.savings.DepositAccountType; +import org.apache.fineract.portfolio.savings.DepositsApiConstants; +import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType; +import org.apache.fineract.portfolio.savings.SavingsApiConstants; +import org.apache.fineract.portfolio.savings.data.DepositAccountTransactionDataValidator; +import org.apache.fineract.portfolio.savings.data.SavingsAccountChargeDataValidator; +import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionDTO; +import org.apache.fineract.portfolio.savings.domain.DepositAccountAssembler; +import org.apache.fineract.portfolio.savings.domain.DepositAccountDomainService; +import org.apache.fineract.portfolio.savings.domain.DepositAccountOnHoldTransaction; +import org.apache.fineract.portfolio.savings.domain.DepositAccountOnHoldTransactionRepository; +import org.apache.fineract.portfolio.savings.domain.DepositAccountRecurringDetail; +import org.apache.fineract.portfolio.savings.domain.FixedDepositAccount; +import org.apache.fineract.portfolio.savings.domain.RecurringDepositAccount; +import org.apache.fineract.portfolio.savings.domain.SavingsAccount; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountCharge; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountChargeRepositoryWrapper; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountStatusType; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransactionRepository; +import org.apache.fineract.portfolio.savings.exception.DepositAccountTransactionNotAllowedException; +import org.apache.fineract.portfolio.savings.exception.SavingsAccountTransactionNotFoundException; +import org.apache.fineract.portfolio.savings.exception.TransactionUpdateNotAllowedException; +import org.apache.fineract.useradministration.domain.AppUser; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Transactional +@RequiredArgsConstructor +public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements DepositAccountWritePlatformService { + + private final PlatformSecurityContext context; + private final SavingsAccountRepositoryWrapper savingAccountRepositoryWrapper; + private final SavingsAccountTransactionRepository savingsAccountTransactionRepository; + private final DepositAccountAssembler depositAccountAssembler; + private final DepositAccountTransactionDataValidator depositAccountTransactionDataValidator; + private final SavingsAccountChargeDataValidator savingsAccountChargeDataValidator; + private final PaymentDetailWritePlatformService paymentDetailWritePlatformService; + private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper; + private final JournalEntryWritePlatformService journalEntryWritePlatformService; + private final DepositAccountDomainService depositAccountDomainService; + private final NoteRepository noteRepository; + private final AccountTransfersReadPlatformService accountTransfersReadPlatformService; + private final ChargeRepositoryWrapper chargeRepository; + private final SavingsAccountChargeRepositoryWrapper savingsAccountChargeRepository; + private final AccountAssociationsReadPlatformService accountAssociationsReadPlatformService; + private final AccountTransfersWritePlatformService accountTransfersWritePlatformService; + private final DepositAccountReadPlatformService depositAccountReadPlatformService; + private final CalendarInstanceRepository calendarInstanceRepository; + private final ConfigurationDomainService configurationDomainService; + private final HolidayRepositoryWrapper holidayRepository; + private final WorkingDaysRepositoryWrapper workingDaysRepository; + private final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository; + private final DenominationBreakdownWriter denominationBreakdownWriter; + + @Transactional + @Override + public CommandProcessingResult activateFDAccount(final Long savingsId, final JsonCommand command) { + final AppUser user = context.authenticatedUser(); + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + final boolean postReversals = false; + this.depositAccountTransactionDataValidator.validateActivation(command); + final MathContext mc = MathContext.DECIMAL64; + final FixedDepositAccount account = (FixedDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, + DepositAccountType.FIXED_DEPOSIT); + checkClientOrGroupActive(account); + + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + + final Map changes = account.activate(user, command); + Money activationChargeAmount = getActivationCharge(account); + if (!changes.isEmpty()) { + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); + Money amountForDeposit = account.activateWithBalance().plus(activationChargeAmount); + if (amountForDeposit.isGreaterThanZero()) { + final PortfolioAccountData portfolioAccountData = this.accountAssociationsReadPlatformService + .retriveSavingsLinkedAssociation(savingsId); + + if (portfolioAccountData == null) { + final PaymentDetail paymentDetail = null; + this.depositAccountDomainService.handleFDDeposit(account, fmt, account.getActivationDate(), + amountForDeposit.getAmount(), paymentDetail); + } else { + final SavingsAccount fromSavingsAccount = null; + boolean isRegularTransaction = false; + final boolean isExceptionForBalanceCheck = false; + final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(account.getActivationDate(), + amountForDeposit.getAmount(), PortfolioAccountType.SAVINGS, PortfolioAccountType.SAVINGS, + portfolioAccountData.getId(), account.getId(), "Account Transfer", locale, fmt, null, null, null, null, null, + AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, ExternalId.empty(), null, account, + fromSavingsAccount, isRegularTransaction, isExceptionForBalanceCheck); + this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); + } + final boolean isInterestTransfer = false; + final LocalDate postInterestOnDate = null; + if (activationChargeAmount.isGreaterThanZero()) { + payActivationCharge(account); + } + if (account.isBeforeLastPostingPeriod(account.getActivationDate(), false)) { + final LocalDate today = DateUtils.getBusinessLocalDate(); + account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, false); + } else { + final LocalDate today = DateUtils.getBusinessLocalDate(); + account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, false, postReversals); + } + } + + final boolean isPreMatureClosure = false; + account.updateMaturityDateAndAmount(mc, isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth); + List depositAccountOnHoldTransactions = null; + if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { + depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository + .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); + } + account.validateAccountBalanceDoesNotBecomeNegative(SavingsAccountTransactionType.PAY_CHARGE.name(), + depositAccountOnHoldTransactions, false); + this.savingAccountRepositoryWrapper.saveAndFlush(account); + } + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .with(changes) // + .build(); + } + + private Money getActivationCharge(final FixedDepositAccount account) { + Money activationChargeAmount = Money.zero(account.getCurrency()); + for (SavingsAccountCharge savingsAccountCharge : account.charges()) { + if (savingsAccountCharge.isSavingsActivation()) { + activationChargeAmount = activationChargeAmount.plus(savingsAccountCharge.getAmount(account.getCurrency())); + } + } + return activationChargeAmount; + } + + private void payActivationCharge(final FixedDepositAccount account) { + for (SavingsAccountCharge savingsAccountCharge : account.charges()) { + if (savingsAccountCharge.isSavingsActivation()) { + account.payCharge(savingsAccountCharge, savingsAccountCharge.getAmount(account.getCurrency()), account.getActivationDate(), + false, null); + } + } + } + + @Transactional + @Override + public CommandProcessingResult activateRDAccount(final Long savingsId, final JsonCommand command) { + boolean isRegularTransaction = false; + + final AppUser user = this.context.authenticatedUser(); + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + final boolean postReversals = false; + this.depositAccountTransactionDataValidator.validateActivation(command); + + final RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, + DepositAccountType.RECURRING_DEPOSIT); + checkClientOrGroupActive(account); + + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + + final Map changes = account.activate(user, command); + + if (!changes.isEmpty()) { + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); + Money amountForDeposit = account.activateWithBalance(); + if (amountForDeposit.isGreaterThanZero()) { + final PortfolioAccountData portfolioAccountData = this.accountAssociationsReadPlatformService + .retriveSavingsLinkedAssociation(savingsId); + if (portfolioAccountData == null) { + this.depositAccountDomainService.handleRDDeposit(account, fmt, account.getActivationDate(), + amountForDeposit.getAmount(), null, isRegularTransaction); + } else { + final boolean isExceptionForBalanceCheck = false; + final SavingsAccount fromSavingsAccount = null; + final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(account.getActivationDate(), + amountForDeposit.getAmount(), PortfolioAccountType.SAVINGS, PortfolioAccountType.SAVINGS, + portfolioAccountData.getId(), account.getId(), "Account Transfer", locale, fmt, null, null, null, null, null, + AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, ExternalId.empty(), null, account, + fromSavingsAccount, isRegularTransaction, isExceptionForBalanceCheck); + this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); + } + } + + final MathContext mc = MathContext.DECIMAL64; + + // submitted and activation date are different then recalculate + // maturity date and schedule + if (!account.accountSubmittedAndActivationOnSameDate()) { + final boolean isPreMatureClosure = false; + final CalendarInstance calendarInstance = this.calendarInstanceRepository.findByEntityIdAndEntityTypeIdAndCalendarTypeId( + savingsId, CalendarEntityType.SAVINGS.getValue(), CalendarType.COLLECTION.getValue()); + + final Calendar calendar = calendarInstance.getCalendar(); + final PeriodFrequencyType frequencyType = CalendarFrequencyType.from(CalendarUtils.getFrequency(calendar.getRecurrence())); + Integer frequency = CalendarUtils.getInterval(calendar.getRecurrence()); + frequency = frequency == -1 ? 1 : frequency; + account.generateSchedule(frequencyType, frequency, calendar); + account.updateMaturityDateAndAmount(mc, isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth); + } + + final LocalDate overdueUptoDate = DateUtils.getBusinessLocalDate(); + account.updateOverduePayments(overdueUptoDate); + final boolean isInterestTransfer = false; + final LocalDate postInterestOnDate = null; + if (account.isBeforeLastPostingPeriod(account.getActivationDate(), false)) { + final LocalDate today = DateUtils.getBusinessLocalDate(); + account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, + postInterestOnDate, false, postReversals); + } else { + final LocalDate today = DateUtils.getBusinessLocalDate(); + account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, false, postReversals); + } + List depositAccountOnHoldTransactions = null; + if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { + depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository + .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); + } + + account.validateAccountBalanceDoesNotBecomeNegative(SavingsAccountTransactionType.PAY_CHARGE.name(), + depositAccountOnHoldTransactions, false); + + this.savingAccountRepositoryWrapper.saveAndFlush(account); + } + + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .with(changes) // + .build(); + } + + @Transactional + @Override + public CommandProcessingResult depositToFDAccount(final Long savingsId, @SuppressWarnings("unused") final JsonCommand command) { + // this.context.authenticatedUser(); + this.depositAccountTransactionDataValidator.validate(command, DepositAccountType.FIXED_DEPOSIT); + + final FixedDepositAccount account = (FixedDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, + DepositAccountType.FIXED_DEPOSIT); + checkClientOrGroupActive(account); + + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); + + final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); + final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); + + final Map changes = new LinkedHashMap<>(); + final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); + this.denominationBreakdownWriter.validateBreakdown(command, transactionAmount, "DEPOSIT"); + final SavingsAccountTransaction deposit = this.depositAccountDomainService.handleFDDeposit(account, fmt, transactionDate, + transactionAmount, paymentDetail); + + this.denominationBreakdownWriter.save(command, deposit.getId(), account.getId(), account.getAccountNumber(), + account.clientId(), account.getClient() != null ? account.getClient().getCifNumber() : null, transactionDate, + "DEPOSIT"); + + return new CommandProcessingResultBuilder().withEntityId(deposit.getId()).withOfficeId(account.officeId()) + .withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).with(changes).build(); + + } + + @Transactional + @Override + public CommandProcessingResult updateDepositAmountForRDAccount(Long savingsId, JsonCommand command) { + this.depositAccountTransactionDataValidator.validateDepositAmountUpdate(command); + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + + final BigDecimal mandatoryRecommendedDepositAmount = command + .bigDecimalValueOfParameterNamed(DepositsApiConstants.mandatoryRecommendedDepositAmountParamName); + + final LocalDate depositAmountUpdateEffectiveFromDate = command + .localDateValueOfParameterNamed(DepositsApiConstants.effectiveDateParamName); + + final RecurringDepositAccount recurringDepositAccount = (RecurringDepositAccount) this.depositAccountAssembler + .assembleFrom(savingsId, DepositAccountType.RECURRING_DEPOSIT); + DepositAccountRecurringDetail recurringDetail = recurringDepositAccount.getRecurringDetail(); + Map changes = recurringDetail.updateMandatoryRecommendedDepositAmount(mandatoryRecommendedDepositAmount, + depositAmountUpdateEffectiveFromDate, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsId) // + .withOfficeId(recurringDepositAccount.officeId()) // + .withClientId(recurringDepositAccount.clientId()) // + .withGroupId(recurringDepositAccount.groupId()) // + .withSavingsId(savingsId) // + .with(changes) // + .build(); + } + + @Transactional + @Override + public CommandProcessingResult depositToRDAccount(final Long savingsId, final JsonCommand command) { + boolean isRegularTransaction = true; + + this.depositAccountTransactionDataValidator.validate(command, DepositAccountType.RECURRING_DEPOSIT); + + final RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, + DepositAccountType.RECURRING_DEPOSIT); + checkClientOrGroupActive(account); + + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); + + final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); + final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); + + final Map changes = new LinkedHashMap<>(); + final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); + this.denominationBreakdownWriter.validateBreakdown(command, transactionAmount, "DEPOSIT"); + final SavingsAccountTransaction deposit = this.depositAccountDomainService.handleRDDeposit(account, fmt, transactionDate, + transactionAmount, paymentDetail, isRegularTransaction); + + this.denominationBreakdownWriter.save(command, deposit.getId(), account.getId(), account.getAccountNumber(), + account.clientId(), account.getClient() != null ? account.getClient().getCifNumber() : null, transactionDate, + "DEPOSIT"); + + return new CommandProcessingResultBuilder() // + .withEntityId(deposit.getId()) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .with(changes) // + .build(); + } + + private Long saveTransactionToGenerateTransactionId(final SavingsAccountTransaction transaction) { + this.savingsAccountTransactionRepository.saveAndFlush(transaction); + return transaction.getId(); + } + + @Transactional + @Override + public CommandProcessingResult withdrawal(final Long savingsId, final JsonCommand command, + final DepositAccountType depositAccountType) { + + boolean isRegularTransaction = true; + + this.depositAccountTransactionDataValidator.validate(command, depositAccountType); + + final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); + final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); + + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); + + final Map changes = new LinkedHashMap<>(); + final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); + + final SavingsAccount account = this.depositAccountAssembler.assembleFrom(savingsId, depositAccountType); + + checkClientOrGroupActive(account); + + final SavingsAccountTransaction withdrawal = this.depositAccountDomainService.handleWithdrawal(account, fmt, transactionDate, + transactionAmount, paymentDetail, true, isRegularTransaction); + + // Validate and save denomination breakdown for WITHDRAWAL if provided + 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"); + + return new CommandProcessingResultBuilder() // + .withEntityId(withdrawal.getId()) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .with(changes)// + .build(); + } + + @Transactional + @Override + public CommandProcessingResult calculateInterest(final Long savingsId, final DepositAccountType depositAccountType) { + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + + final SavingsAccount account = this.depositAccountAssembler.assembleFrom(savingsId, depositAccountType); + checkClientOrGroupActive(account); + + final LocalDate today = DateUtils.getBusinessLocalDate(); + final boolean postReversals = false; + final MathContext mc = new MathContext(15, MoneyHelper.getRoundingMode()); + boolean isInterestTransfer = false; + LocalDate postInterestOnDate = null; + account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, false, postReversals); + + this.savingAccountRepositoryWrapper.save(account); + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .build(); + } + + @Transactional + @Override + public CommandProcessingResult postInterest(final Long savingsId, final DepositAccountType depositAccountType) { + + final SavingsAccount account = this.depositAccountAssembler.assembleFrom(savingsId, depositAccountType); + checkClientOrGroupActive(account); + postInterest(account); + return new CommandProcessingResultBuilder() // + .withEntityId(savingsId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .build(); + } + + @Transactional + private void postInterest(final SavingsAccount account) { + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + final LocalDate today = DateUtils.getBusinessLocalDate(); + final MathContext mc = new MathContext(10, MoneyHelper.getRoundingMode()); + boolean isInterestTransfer = false; + LocalDate postInterestOnDate = null; + final boolean postReversals = false; + account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, + postInterestOnDate, false, postReversals); + this.savingAccountRepositoryWrapper.saveAndFlush(account); + + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); + } + + @Override + public CommandProcessingResult undoFDTransaction(final Long savingsId, @SuppressWarnings("unused") final Long transactionId, + @SuppressWarnings("unused") final boolean allowAccountTransferModification) { + + throw new DepositAccountTransactionNotAllowedException(savingsId, "undo", DepositAccountType.FIXED_DEPOSIT); + } + + @Override + public CommandProcessingResult undoRDTransaction(final Long savingsId, final Long transactionId, + final boolean allowAccountTransferModification) { + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + + final RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, + DepositAccountType.RECURRING_DEPOSIT); + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + + final SavingsAccountTransaction savingsAccountTransaction = this.savingsAccountTransactionRepository + .findOneByIdAndSavingsAccountId(transactionId, savingsId); + if (savingsAccountTransaction == null) { + throw new SavingsAccountTransactionNotFoundException(savingsId, transactionId); + } + + if (!allowAccountTransferModification + && this.accountTransfersReadPlatformService.isAccountTransfer(transactionId, PortfolioAccountType.SAVINGS)) { + throw new PlatformServiceUnavailableException("error.msg.recurring.deposit.account.transfer.transaction.update.not.allowed", + "Recurring deposit account transaction:" + transactionId + " update not allowed as it involves in account transfer", + transactionId); + } + + final LocalDate today = DateUtils.getBusinessLocalDate(); + final MathContext mc = MathContext.DECIMAL64; + + if (account.isNotActive()) { + throwValidationForActiveStatus(SavingsApiConstants.undoTransactionAction); + } + account.undoTransaction(transactionId); + boolean isInterestTransfer = false; + LocalDate postInterestOnDate = null; + checkClientOrGroupActive(account); + final boolean postReversals = false; + if (savingsAccountTransaction.isPostInterestCalculationRequired() + && account.isBeforeLastPostingPeriod(savingsAccountTransaction.getTransactionDate(), false)) { + account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, + postInterestOnDate, false, postReversals); + } else { + account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, false, postReversals); + } + List depositAccountOnHoldTransactions = null; + if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { + depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository + .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); + } + + account.validateAccountBalanceDoesNotBecomeNegative(SavingsApiConstants.undoTransactionAction, depositAccountOnHoldTransactions, + false); + // account.activateAccountBasedOnBalance(); + final boolean isPreMatureClosure = false; + account.updateMaturityDateAndAmount(mc, isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth); + + final LocalDate overdueUptoDate = DateUtils.getBusinessLocalDate(); + + if (savingsAccountTransaction.isDeposit()) { + account.updateScheduleInstallments(); + } + + account.updateOverduePayments(overdueUptoDate); + + this.savingAccountRepositoryWrapper.saveAndFlush(account); + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .build(); + } + + @Override + public CommandProcessingResult adjustFDTransaction(final Long savingsId, @SuppressWarnings("unused") final Long transactionId, + @SuppressWarnings("unused") final JsonCommand command) { + + throw new DepositAccountTransactionNotAllowedException(savingsId, "modify", DepositAccountType.FIXED_DEPOSIT); + } + + @Override + public CommandProcessingResult adjustRDTransaction(final Long savingsId, final Long transactionId, final JsonCommand command) { + context.authenticatedUser(); + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate(); + this.depositAccountTransactionDataValidator.validate(command, DepositAccountType.RECURRING_DEPOSIT); + + final SavingsAccountTransaction savingsAccountTransaction = this.savingsAccountTransactionRepository + .findOneByIdAndSavingsAccountId(transactionId, savingsId); + if (savingsAccountTransaction == null) { + throw new SavingsAccountTransactionNotFoundException(savingsId, transactionId); + } + + if ((!savingsAccountTransaction.isDeposit() && !savingsAccountTransaction.isWithdrawal()) + || savingsAccountTransaction.isReversed()) { + throw new TransactionUpdateNotAllowedException(savingsId, transactionId); + } + + if (this.accountTransfersReadPlatformService.isAccountTransfer(transactionId, PortfolioAccountType.SAVINGS)) { + throw new PlatformServiceUnavailableException("error.msg.saving.account.transfer.transaction.update.not.allowed", + "Deposit account transaction:" + transactionId + " update not allowed as it involves in account transfer", + transactionId); + } + + final LocalDate today = DateUtils.getBusinessLocalDate(); + + final RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, + DepositAccountType.RECURRING_DEPOSIT); + if (account.isNotActive()) { + throwValidationForActiveStatus(SavingsApiConstants.adjustTransactionAction); + } + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); + final LocalDate transactionDate = command.localDateValueOfParameterNamed(SavingsApiConstants.transactionDateParamName); + final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed(SavingsApiConstants.transactionAmountParamName); + final Map changes = new LinkedHashMap<>(); + final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); + + final MathContext mc = new MathContext(10, MoneyHelper.getRoundingMode()); + account.undoTransaction(transactionId); + + SavingsAccountTransaction transaction = null; + Integer accountType = null; + UUID refNo = UUID.randomUUID(); + if (savingsAccountTransaction.isDeposit()) { + final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount, + paymentDetail, null, accountType); + transaction = account.deposit(transactionDTO, false, relaxingDaysConfigForPivotDate, refNo.toString()); + } else { + final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount, + paymentDetail, null, accountType); + transaction = account.withdraw(transactionDTO, true, false, relaxingDaysConfigForPivotDate, refNo.toString()); + } + final Long newtransactionId = saveTransactionToGenerateTransactionId(transaction); + boolean isInterestTransfer = false; + final LocalDate postInterestOnDate = null; + final boolean postReversals = false; + if (account.isBeforeLastPostingPeriod(transactionDate, false) + || account.isBeforeLastPostingPeriod(savingsAccountTransaction.getTransactionDate(), false)) { + account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, + postInterestOnDate, false, postReversals); + } else { + account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, false, postReversals); + } + List depositAccountOnHoldTransactions = null; + if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { + depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository + .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); + } + + account.validateAccountBalanceDoesNotBecomeNegative(SavingsApiConstants.adjustTransactionAction, depositAccountOnHoldTransactions, + false); + account.activateAccountBasedOnBalance(); + + if (savingsAccountTransaction.isDeposit()) { + account.handleScheduleInstallments(savingsAccountTransaction); + } + final LocalDate overdueUptoDate = DateUtils.getBusinessLocalDate(); + account.updateOverduePayments(overdueUptoDate); + + this.savingAccountRepositoryWrapper.saveAndFlush(account); + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); + return new CommandProcessingResultBuilder() // + .withEntityId(newtransactionId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .with(changes)// + .build(); + } + + /** + * + */ + private void throwValidationForActiveStatus(final String actionName) { + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(SAVINGS_ACCOUNT_RESOURCE_NAME + actionName); + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("account.is.not.active"); + throw new PlatformApiDataValidationException(dataValidationErrors); + } + + private void checkClientOrGroupActive(final SavingsAccount account) { + final Client client = account.getClient(); + if (client != null && client.isNotActive()) { + throw new ClientNotActiveException(client.getId()); + } + final Group group = account.group(); + if (group != null && group.isNotActive()) { + throw new GroupNotActiveException(group.getId()); + } + } + + @Override + public CommandProcessingResult closeFDAccount(final Long savingsId, final JsonCommand command) { + final AppUser user = this.context.authenticatedUser(); + final boolean isPreMatureClose = false; + this.depositAccountTransactionDataValidator.validateClosing(command, DepositAccountType.FIXED_DEPOSIT, isPreMatureClose); + + final Map changes = new LinkedHashMap<>(); + final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); + + final FixedDepositAccount account = (FixedDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, + DepositAccountType.FIXED_DEPOSIT); + checkClientOrGroupActive(account); + + this.depositAccountDomainService.handleFDAccountClosure(account, paymentDetail, user, command, changes); + + final String noteText = command.stringValueOfParameterNamed("note"); + if (StringUtils.isNotBlank(noteText)) { + final Note note = Note.savingNote(account, noteText); + changes.put("note", noteText); + this.noteRepository.save(note); + } + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .with(changes)// + .build(); + + } + + @Override + public CommandProcessingResult closeRDAccount(final Long savingsId, final JsonCommand command) { + final AppUser user = this.context.authenticatedUser(); + + this.depositAccountTransactionDataValidator.validateClosing(command, DepositAccountType.RECURRING_DEPOSIT, false); + + final Map changes = new LinkedHashMap<>(); + final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); + + final RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, + DepositAccountType.RECURRING_DEPOSIT); + checkClientOrGroupActive(account); + + this.depositAccountDomainService.handleRDAccountClosure(account, paymentDetail, user, command, changes); + + final String noteText = command.stringValueOfParameterNamed("note"); + if (StringUtils.isNotBlank(noteText)) { + final Note note = Note.savingNote(account, noteText); + changes.put("note", noteText); + this.noteRepository.save(note); + } + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .with(changes)// + .build(); + + } + + @Override + public CommandProcessingResult prematureCloseFDAccount(final Long savingsId, final JsonCommand command) { + final AppUser user = this.context.authenticatedUser(); + + this.depositAccountTransactionDataValidator.validateClosing(command, DepositAccountType.FIXED_DEPOSIT, true); + + final Map changes = new LinkedHashMap<>(); + final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); + + final FixedDepositAccount account = (FixedDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, + DepositAccountType.FIXED_DEPOSIT); + checkClientOrGroupActive(account); + + this.depositAccountDomainService.handleFDAccountPreMatureClosure(account, paymentDetail, user, command, changes); + + final String noteText = command.stringValueOfParameterNamed("note"); + if (StringUtils.isNotBlank(noteText)) { + final Note note = Note.savingNote(account, noteText); + changes.put("note", noteText); + this.noteRepository.save(note); + } + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .with(changes)// + .build(); + + } + + @Override + public CommandProcessingResult prematureCloseRDAccount(final Long savingsId, final JsonCommand command) { + final AppUser user = this.context.authenticatedUser(); + + this.depositAccountTransactionDataValidator.validateClosing(command, DepositAccountType.RECURRING_DEPOSIT, true); + + final Map changes = new LinkedHashMap<>(); + final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); + + final RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, + DepositAccountType.RECURRING_DEPOSIT); + checkClientOrGroupActive(account); + if (account.maturityDate() == null) { + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME + DepositsApiConstants.preMatureCloseAction); + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("can.not.close.as.premature"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + this.depositAccountDomainService.handleRDAccountPreMatureClosure(account, paymentDetail, user, command, changes); + + final String noteText = command.stringValueOfParameterNamed("note"); + if (StringUtils.isNotBlank(noteText)) { + final Note note = Note.savingNote(account, noteText); + changes.put("note", noteText); + this.noteRepository.save(note); + } + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .with(changes)// + .build(); + + } + + @Override + public SavingsAccountTransaction initiateSavingsTransfer(final Long accountId, final LocalDate transferDate, + final DepositAccountType depositAccountType) { + context.authenticatedUser(); + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + + final SavingsAccount savingsAccount = this.depositAccountAssembler.assembleFrom(accountId, depositAccountType); + final LocalDate postInterestOnDate = null; + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(savingsAccount, existingTransactionIds, existingReversedTransactionIds); + + final SavingsAccountTransaction newTransferTransaction = SavingsAccountTransaction.initiateTransfer(savingsAccount, + savingsAccount.office(), transferDate); + savingsAccount.addTransaction(newTransferTransaction); + savingsAccount.setStatus(SavingsAccountStatusType.TRANSFER_IN_PROGRESS.getValue()); + final MathContext mc = MathContext.DECIMAL64; + boolean isInterestTransfer = false; + final boolean postReversals = false; + savingsAccount.calculateInterestUsing(mc, transferDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, false, postReversals); + + this.savingsAccountTransactionRepository.save(newTransferTransaction); + this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); + + postJournalEntries(savingsAccount, existingTransactionIds, existingReversedTransactionIds); + + return newTransferTransaction; + } + + @Override + public SavingsAccountTransaction withdrawSavingsTransfer(final Long accountId, final LocalDate transferDate, + final DepositAccountType depositAccountType) { + context.authenticatedUser(); + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + + final SavingsAccount savingsAccount = this.depositAccountAssembler.assembleFrom(accountId, depositAccountType); + + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(savingsAccount, existingTransactionIds, existingReversedTransactionIds); + + final SavingsAccountTransaction withdrawtransferTransaction = SavingsAccountTransaction.withdrawTransfer(savingsAccount, + savingsAccount.office(), transferDate); + savingsAccount.addTransaction(withdrawtransferTransaction); + savingsAccount.setStatus(SavingsAccountStatusType.ACTIVE.getValue()); + final boolean postReversals = false; + final MathContext mc = MathContext.DECIMAL64; + boolean isInterestTransfer = false; + LocalDate postInterestOnDate = null; + savingsAccount.calculateInterestUsing(mc, transferDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, false, postReversals); + + this.savingsAccountTransactionRepository.save(withdrawtransferTransaction); + this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); + + postJournalEntries(savingsAccount, existingTransactionIds, existingReversedTransactionIds); + + return withdrawtransferTransaction; + } + + @Override + public void rejectSavingsTransfer(final Long accountId, final DepositAccountType depositAccountType) { + final SavingsAccount savingsAccount = this.depositAccountAssembler.assembleFrom(accountId, depositAccountType); + savingsAccount.setStatus(SavingsAccountStatusType.TRANSFER_ON_HOLD.getValue()); + this.savingAccountRepositoryWrapper.save(savingsAccount); + } + + @Override + public SavingsAccountTransaction acceptSavingsTransfer(final Long accountId, final LocalDate transferDate, + final Office acceptedInOffice, final Staff fieldOfficer, final DepositAccountType depositAccountType) { + context.authenticatedUser(); + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + + final SavingsAccount savingsAccount = this.depositAccountAssembler.assembleFrom(accountId, depositAccountType); + + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(savingsAccount, existingTransactionIds, existingReversedTransactionIds); + + final SavingsAccountTransaction acceptTransferTransaction = SavingsAccountTransaction.approveTransfer(savingsAccount, + acceptedInOffice, transferDate); + savingsAccount.addTransaction(acceptTransferTransaction); + savingsAccount.setStatus(SavingsAccountStatusType.ACTIVE.getValue()); + if (fieldOfficer != null) { + savingsAccount.reassignSavingsOfficer(fieldOfficer, transferDate); + } + boolean isInterestTransfer = false; + final boolean postReversals = false; + LocalDate postInterestOnDate = null; + final MathContext mc = MathContext.DECIMAL64; + savingsAccount.calculateInterestUsing(mc, transferDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, false, postReversals); + + this.savingsAccountTransactionRepository.save(acceptTransferTransaction); + this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); + + postJournalEntries(savingsAccount, existingTransactionIds, existingReversedTransactionIds); + + return acceptTransferTransaction; + } + + @Transactional + @Override + public CommandProcessingResult addSavingsAccountCharge(final JsonCommand command, final DepositAccountType depositAccountType) { + + this.context.authenticatedUser(); + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(SAVINGS_ACCOUNT_RESOURCE_NAME); + + final Long savingsAccountId = command.getSavingsId(); + this.savingsAccountChargeDataValidator.validateAdd(command.json()); + + final SavingsAccount savingsAccount = this.depositAccountAssembler.assembleFrom(savingsAccountId, depositAccountType); + checkClientOrGroupActive(savingsAccount); + + final Locale locale = command.extractLocale(); + final String format = command.dateFormat(); + final DateTimeFormatter fmt = StringUtils.isNotBlank(format) ? DateTimeFormatter.ofPattern(format).withLocale(locale) + : DateTimeFormatter.ofPattern("dd MM yyyy"); + + final Long chargeDefinitionId = command.longValueOfParameterNamed(chargeIdParamName); + final Charge chargeDefinition = this.chargeRepository.findOneWithNotFoundDetection(chargeDefinitionId); + + final SavingsAccountCharge savingsAccountCharge = SavingsAccountCharge.createNewFromJson(savingsAccount, chargeDefinition, command); + + if (savingsAccountCharge.getDueDate() != null) { + // transaction date should not be on a holiday or non working day + if (!this.configurationDomainService.allowTransactionsOnHolidayEnabled() + && this.holidayRepository.isHoliday(savingsAccount.officeId(), savingsAccountCharge.getDueDate())) { + baseDataValidator.reset().parameter(dueAsOfDateParamName).value(savingsAccountCharge.getDueDate().format(fmt)) + .failWithCodeNoParameterAddedToErrorCode("charge.due.date.is.on.holiday"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + if (!this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled() + && !this.workingDaysRepository.isWorkingDay(savingsAccountCharge.getDueDate())) { + baseDataValidator.reset().parameter(dueAsOfDateParamName).value(savingsAccountCharge.getDueDate().format(fmt)) + .failWithCodeNoParameterAddedToErrorCode("charge.due.date.is.a.nonworking.day"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + } + + savingsAccount.addCharge(fmt, savingsAccountCharge, chargeDefinition); + + this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsAccountCharge.getId()) // + .withOfficeId(savingsAccount.officeId()) // + .withClientId(savingsAccount.clientId()) // + .withGroupId(savingsAccount.groupId()) // + .withSavingsId(savingsAccountId) // + .build(); + } + + @Transactional + @Override + public CommandProcessingResult updateSavingsAccountCharge(final JsonCommand command, final DepositAccountType depositAccountType) { + + this.context.authenticatedUser(); + this.savingsAccountChargeDataValidator.validateUpdate(command.json()); + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(SAVINGS_ACCOUNT_RESOURCE_NAME); + + final Long savingsAccountId = command.getSavingsId(); + // SavingsAccount Charge entity + final Long savingsChargeId = command.entityId(); + + final SavingsAccount savingsAccount = this.depositAccountAssembler.assembleFrom(savingsAccountId, depositAccountType); + checkClientOrGroupActive(savingsAccount); + + final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository.findOneWithNotFoundDetection(savingsChargeId, + savingsAccountId); + + final Map changes = savingsAccountCharge.update(command); + + if (savingsAccountCharge.getDueDate() != null) { + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); + + // transaction date should not be on a holiday or non working day + if (!this.configurationDomainService.allowTransactionsOnHolidayEnabled() + && this.holidayRepository.isHoliday(savingsAccount.officeId(), savingsAccountCharge.getDueDate())) { + baseDataValidator.reset().parameter(dueAsOfDateParamName).value(savingsAccountCharge.getDueDate().format(fmt)) + .failWithCodeNoParameterAddedToErrorCode("charge.due.date.is.on.holiday"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + if (!this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled() + && !this.workingDaysRepository.isWorkingDay(savingsAccountCharge.getDueDate())) { + baseDataValidator.reset().parameter(dueAsOfDateParamName).value(savingsAccountCharge.getDueDate().format(fmt)) + .failWithCodeNoParameterAddedToErrorCode("charge.due.date.is.a.nonworking.day"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + } + + this.savingsAccountChargeRepository.saveAndFlush(savingsAccountCharge); + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsAccountCharge.getId()) // + .withOfficeId(savingsAccountCharge.savingsAccount().officeId()) // + .withClientId(savingsAccountCharge.savingsAccount().clientId()) // + .withGroupId(savingsAccountCharge.savingsAccount().groupId()) // + .withSavingsId(savingsAccountCharge.savingsAccount().getId()) // + .with(changes) // + .build(); + } + + @Transactional + @Override + public CommandProcessingResult waiveCharge(final Long savingsAccountId, final Long savingsAccountChargeId, + @SuppressWarnings("unused") final DepositAccountType depositAccountType) { + context.authenticatedUser(); + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + + final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository + .findOneWithNotFoundDetection(savingsAccountChargeId, savingsAccountId); + + // Get Savings account from savings charge + final SavingsAccount account = savingsAccountCharge.savingsAccount(); + this.depositAccountAssembler.assignSavingAccountHelpers(account); + + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + + account.waiveCharge(savingsAccountChargeId, false); + boolean isInterestTransfer = false; + LocalDate postInterestOnDate = null; + final MathContext mc = MathContext.DECIMAL64; + final boolean postReversals = false; + if (account.isBeforeLastPostingPeriod(savingsAccountCharge.getDueDate(), false)) { + final LocalDate today = DateUtils.getBusinessLocalDate(); + account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, + postInterestOnDate, false, postReversals); + } else { + final LocalDate today = DateUtils.getBusinessLocalDate(); + account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, false, postReversals); + } + List depositAccountOnHoldTransactions = null; + if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { + depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository + .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); + } + + account.validateAccountBalanceDoesNotBecomeNegative(SavingsApiConstants.waiveChargeTransactionAction, + depositAccountOnHoldTransactions, false); + + this.savingAccountRepositoryWrapper.saveAndFlush(account); + + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsAccountChargeId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsAccountId) // + .build(); + } + + @Transactional + @Override + public CommandProcessingResult deleteSavingsAccountCharge(final Long savingsAccountId, final Long savingsAccountChargeId, + @SuppressWarnings("unused") final JsonCommand command, final DepositAccountType depositAccountType) { + this.context.authenticatedUser(); + + final SavingsAccount savingsAccount = this.depositAccountAssembler.assembleFrom(savingsAccountId, depositAccountType); + checkClientOrGroupActive(savingsAccount); + final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository + .findOneWithNotFoundDetection(savingsAccountChargeId, savingsAccountId); + + savingsAccount.removeCharge(savingsAccountCharge); + this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsAccountChargeId) // + .withOfficeId(savingsAccount.officeId()) // + .withClientId(savingsAccount.clientId()) // + .withGroupId(savingsAccount.groupId()) // + .withSavingsId(savingsAccountId) // + .build(); + } + + @Override + public CommandProcessingResult payCharge(final Long savingsAccountId, final Long savingsAccountChargeId, final JsonCommand command, + @SuppressWarnings("unused") final DepositAccountType depositAccountType) { + + this.context.authenticatedUser(); + + this.savingsAccountChargeDataValidator.validatePayCharge(command.json()); + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); + final BigDecimal amountPaid = command.bigDecimalValueOfParameterNamed(amountParamName); + final LocalDate transactionDate = command.localDateValueOfParameterNamed(dueAsOfDateParamName); + + final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository + .findOneWithNotFoundDetection(savingsAccountChargeId, savingsAccountId); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(SAVINGS_ACCOUNT_RESOURCE_NAME); + + // transaction date should not be on a holiday or non working day + if (!this.configurationDomainService.allowTransactionsOnHolidayEnabled() + && this.holidayRepository.isHoliday(savingsAccountCharge.savingsAccount().officeId(), transactionDate)) { + baseDataValidator.reset().parameter(dueAsOfDateParamName).value(transactionDate.format(fmt)) + .failWithCodeNoParameterAddedToErrorCode("transaction.not.allowed.transaction.date.is.on.holiday"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + if (!this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled() + && !this.workingDaysRepository.isWorkingDay(transactionDate)) { + baseDataValidator.reset().parameter(dueAsOfDateParamName).value(transactionDate.format(fmt)) + .failWithCodeNoParameterAddedToErrorCode("transaction.not.allowed.transaction.date.is.a.nonworking.day"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + this.payCharge(savingsAccountCharge, transactionDate, amountPaid, fmt); + return new CommandProcessingResultBuilder() // + .withEntityId(savingsAccountCharge.getId()) // + .withOfficeId(savingsAccountCharge.savingsAccount().officeId()) // + .withClientId(savingsAccountCharge.savingsAccount().clientId()) // + .withGroupId(savingsAccountCharge.savingsAccount().groupId()) // + .withSavingsId(savingsAccountCharge.savingsAccount().getId()) // + .build(); + + } + + @Transactional + @Override + public void applyChargeDue(final Long savingsAccountChargeId, final Long accountId, + @SuppressWarnings("unused") final DepositAccountType depositAccountType) { + final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository + .findOneWithNotFoundDetection(savingsAccountChargeId, accountId); + // always use current date as transaction date for batch job + final LocalDate transactionDate = DateUtils.getBusinessLocalDate(); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd MM yyyy"); + + while (DateUtils.isBefore(savingsAccountCharge.getDueDate(), transactionDate)) { + payCharge(savingsAccountCharge, transactionDate, savingsAccountCharge.amoutOutstanding(), fmt); + } + } + + @Transactional + private void payCharge(final SavingsAccountCharge savingsAccountCharge, final LocalDate transactionDate, final BigDecimal amountPaid, + final DateTimeFormatter formatter) { + context.authenticatedUser(); + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + + // Get Savings account from savings charge + final SavingsAccount account = savingsAccountCharge.savingsAccount(); + this.depositAccountAssembler.assignSavingAccountHelpers(account); + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + account.payCharge(savingsAccountCharge, amountPaid, transactionDate, formatter, false, null); + boolean isInterestTransfer = false; + LocalDate postInterestOnDate = null; + final MathContext mc = MathContext.DECIMAL64; + final boolean postReversals = false; + if (account.isBeforeLastPostingPeriod(transactionDate, false)) { + final LocalDate today = DateUtils.getBusinessLocalDate(); + account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, + postInterestOnDate, false, postReversals); + } else { + final LocalDate today = DateUtils.getBusinessLocalDate(); + account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, false, postReversals); + } + List depositAccountOnHoldTransactions = null; + if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { + depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository + .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); + } + + account.validateAccountBalanceDoesNotBecomeNegative("." + SavingsAccountTransactionType.PAY_CHARGE.getCode(), + depositAccountOnHoldTransactions, false); + + this.savingAccountRepositoryWrapper.saveAndFlush(account); + + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); + } + + @Transactional + @Override + public void updateMaturityDetails(Long depositAccountId, DepositAccountType depositAccountType) { + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + final boolean postReversals = false; + final SavingsAccount account = this.depositAccountAssembler.assembleFrom(depositAccountId, depositAccountType); + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + + if (depositAccountType.isFixedDeposit()) { + ((FixedDepositAccount) account).updateMaturityStatus(isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth); + FixedDepositAccount fdAccount = ((FixedDepositAccount) account); + // handle maturity instructions + + if (fdAccount.isMatured() && (fdAccount.isReinvestOnClosure() || fdAccount.isTransferToSavingsOnClosure())) { + DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + Map changes = new HashMap<>(); + final AppUser user = context.authenticatedUser(); + Long toSavingsId = fdAccount.getTransferToSavingsAccountId(); + this.depositAccountDomainService.handleFDAccountMaturityClosure(fdAccount, null, user, fmt, fdAccount.maturityDate(), + fdAccount.getOnAccountClosureId(), toSavingsId, "Apply maturity instructions", changes); + + if (changes.get("reinvestedDepositId") != null) { + Long reinvestedDepositId = (Long) changes.get("reinvestedDepositId"); + Money amountForDeposit = account.activateWithBalance(); + final FixedDepositAccount reinvestAccount = (FixedDepositAccount) this.depositAccountAssembler + .assembleFrom(reinvestedDepositId, DepositAccountType.FIXED_DEPOSIT); + Money activationChargeAmount = getActivationCharge(reinvestAccount); + if (activationChargeAmount.isGreaterThanZero()) { + payActivationCharge(reinvestAccount); + amountForDeposit = amountForDeposit.plus(activationChargeAmount); + } + this.depositAccountDomainService.handleFDDeposit(reinvestAccount, fmt, fdAccount.maturityDate(), + amountForDeposit.getAmount(), null); + } + } + } else if (depositAccountType.isRecurringDeposit()) { + ((RecurringDepositAccount) account).updateMaturityStatus(isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postReversals); + } + this.savingAccountRepositoryWrapper.saveAndFlush(account); + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); + } + + private void updateExistingTransactionsDetails(SavingsAccount account, Set existingTransactionIds, + Set existingReversedTransactionIds) { + existingTransactionIds.addAll(account.findExistingTransactionIds()); + existingReversedTransactionIds.addAll(account.findExistingReversedTransactionIds()); + } + + private void postJournalEntries(final SavingsAccount savingsAccount, final Set existingTransactionIds, + final Set existingReversedTransactionIds) { + + boolean isAccountTransfer = false; + final Map accountingBridgeData = savingsAccount.deriveAccountingBridgeData(savingsAccount.getCurrency().getCode(), + existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, false); + this.journalEntryWritePlatformService.createJournalEntriesForSavings(accountingBridgeData); + } + + @Transactional + @Override + public SavingsAccountTransaction mandatorySavingsAccountDeposit(final SavingsAccountTransactionDTO accountTransactionDTO) { + boolean isRegularTransaction = false; + final PaymentDetail paymentDetail = accountTransactionDTO.getPaymentDetail(); + if (paymentDetail != null && paymentDetail.getId() == null) { + this.paymentDetailWritePlatformService.persistPaymentDetail(paymentDetail); + } + if (accountTransactionDTO.getAccountType().equals(DepositAccountType.RECURRING_DEPOSIT.getValue())) { + RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler + .assembleFrom(accountTransactionDTO.getSavingsAccountId(), DepositAccountType.RECURRING_DEPOSIT); + return this.depositAccountDomainService.handleRDDeposit(account, accountTransactionDTO.getFormatter(), + accountTransactionDTO.getTransactionDate(), accountTransactionDTO.getTransactionAmount(), paymentDetail, + isRegularTransaction); + } + SavingsAccount account = null; + if (accountTransactionDTO.getAccountType().equals(DepositAccountType.SAVINGS_DEPOSIT.getValue())) { + account = this.depositAccountAssembler.assembleFrom(accountTransactionDTO.getSavingsAccountId(), + DepositAccountType.SAVINGS_DEPOSIT); + } else { + account = this.depositAccountAssembler.assembleFrom(accountTransactionDTO.getSavingsAccountId(), + DepositAccountType.CURRENT_DEPOSIT); + } + return this.depositAccountDomainService.handleSavingDeposit(account, accountTransactionDTO.getFormatter(), + accountTransactionDTO.getTransactionDate(), accountTransactionDTO.getTransactionAmount(), paymentDetail, + isRegularTransaction); + + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java index 7edfbf915..0ee80cdeb 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java @@ -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 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); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java index 2079c29e5..7b2cfdc32 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java @@ -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 diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index ad7639229..7935a1da7 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -319,4 +319,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0012_denomination_master.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0012_denomination_master.xml index aad95f9a8..85fec5075 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0012_denomination_master.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0012_denomination_master.xml @@ -177,4 +177,28 @@ + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0013_cash_exchange.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0013_cash_exchange.xml new file mode 100644 index 000000000..a4d48752a --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0013_cash_exchange.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +