diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/api/DenominationMasterApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/api/DenominationMasterApiResource.java new file mode 100644 index 000000000..36166bffa --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/api/DenominationMasterApiResource.java @@ -0,0 +1,232 @@ +package org.apache.fineract.portfolio.denominationmaster.api; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +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.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +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.denominationmaster.domain.DenominationMaster; +import org.apache.fineract.portfolio.denominationmaster.domain.DenominationMasterAudit; +import org.apache.fineract.portfolio.denominationmaster.domain.DenominationMasterAuditRepository; +import org.apache.fineract.portfolio.denominationmaster.domain.DenominationMasterRepository; +import org.apache.fineract.useradministration.domain.AppUser; +import org.springframework.stereotype.Component; + +@Path("/v1/denominationmaster") +@Component +@RequiredArgsConstructor +@Consumes({ MediaType.APPLICATION_JSON }) +@Produces({ MediaType.APPLICATION_JSON }) +public class DenominationMasterApiResource { + + private final DenominationMasterRepository repository; + private final DenominationMasterAuditRepository auditRepository; + private final PlatformSecurityContext context; + private final Gson gson = new Gson(); + + @GET + public String getAll() { + List list = repository.findByDeletedFalseOrderByKindDescDenominationDesc(); + return gson.toJson(list.stream().map(this::toMap).collect(Collectors.toList())); + } + + @GET + @Path("active") + public String getActive() { + List list = repository.findByIsActiveTrueAndDeletedFalseOrderByKindDescDenominationDesc(); + return gson.toJson(list.stream().map(this::toMap).collect(Collectors.toList())); + } + + @GET + @Path("deleted") + public String getDeleted() { + List list = repository.findByDeletedTrueOrderByKindDescDenominationDesc(); + return gson.toJson(list.stream().map(this::toMap).collect(Collectors.toList())); + } + + @GET + @Path("audit") + public String getAllAudit() { + List list = auditRepository.findAllByOrderByPerformedOnDesc(); + return gson.toJson(list.stream().map(this::auditToMap).collect(Collectors.toList())); + } + + @GET + @Path("{id}/audit") + public String getAuditForDenomination(@PathParam("id") Long id) { + List list = auditRepository.findByDenominationIdOrderByPerformedOnDesc(id); + return gson.toJson(list.stream().map(this::auditToMap).collect(Collectors.toList())); + } + + @POST + public String create(String body) { + JsonObject json = JsonParser.parseString(body).getAsJsonObject(); + final int denomination = json.get("denomination").getAsInt(); + final String kind = json.get("kind").getAsString(); + if (repository.existsByDenominationAndKindAndDeletedFalse(denomination, kind)) { + throw new PlatformDataIntegrityException("error.msg.denomination.duplicate", + "A " + kind + " of denomination " + denomination + " already exists.", "denomination", denomination); + } + DenominationMaster dm = DenominationMaster.create(denomination, kind, + json.has("description") ? json.get("description").getAsString() : null, + json.has("isActive") ? json.get("isActive").getAsBoolean() : true, + json.has("introducedOn") ? LocalDate.parse(json.get("introducedOn").getAsString()) : null, null); + DenominationMaster saved = repository.save(dm); + writeAudit("CREATE", saved, null); + JsonObject result = new JsonObject(); + result.addProperty("resourceId", saved.getId()); + return gson.toJson(result); + } + + @PUT + @Path("{id}") + public String update(@PathParam("id") Long id, @QueryParam("command") String command, String body) { + DenominationMaster dm = repository.findById(id).orElseThrow(() -> new RuntimeException("Denomination not found: " + id)); + + if ("activate".equals(command)) { + dm.setIsActive(true); + dm.setDiscontinuedOn(null); + repository.save(dm); + writeAudit("ACTIVATE", dm, null); + } else if ("deactivate".equals(command)) { + dm.setIsActive(false); + dm.setDiscontinuedOn(LocalDate.now()); + repository.save(dm); + writeAudit("DEACTIVATE", dm, null); + } else if ("restore".equals(command)) { + if (repository.existsByDenominationAndKindAndDeletedFalseAndIdNot(dm.getDenomination(), dm.getKind(), id)) { + throw new PlatformDataIntegrityException("error.msg.denomination.duplicate.restore", + "Cannot restore: a " + dm.getKind() + " of denomination " + dm.getDenomination() + " already exists.", + "denomination", dm.getDenomination()); + } + dm.setDeleted(false); + repository.save(dm); + writeAudit("RESTORE", dm, null); + } else { + JsonObject json = JsonParser.parseString(body).getAsJsonObject(); + final int effectiveDenomination = json.has("denomination") ? json.get("denomination").getAsInt() : dm.getDenomination(); + final String effectiveKind = json.has("kind") ? json.get("kind").getAsString() : dm.getKind(); + if (repository.existsByDenominationAndKindAndDeletedFalseAndIdNot(effectiveDenomination, effectiveKind, id)) { + throw new PlatformDataIntegrityException("error.msg.denomination.duplicate", + "A " + effectiveKind + " of denomination " + effectiveDenomination + " already exists.", "denomination", + effectiveDenomination); + } + List changes = new ArrayList<>(); + if (json.has("denomination")) { + int newVal = json.get("denomination").getAsInt(); + if (!java.util.Objects.equals(dm.getDenomination(), newVal)) { + changes.add("denomination: " + dm.getDenomination() + " -> " + newVal); + } + dm.setDenomination(newVal); + } + if (json.has("kind")) { + String newVal = json.get("kind").getAsString(); + if (!java.util.Objects.equals(dm.getKind(), newVal)) { + changes.add("kind: " + dm.getKind() + " -> " + newVal); + } + dm.setKind(newVal); + } + if (json.has("description")) { + String newVal = json.get("description").getAsString(); + if (!java.util.Objects.equals(dm.getDescription(), newVal)) { + changes.add("description: " + dm.getDescription() + " -> " + newVal); + } + dm.setDescription(newVal); + } + if (json.has("isActive")) { + boolean newVal = json.get("isActive").getAsBoolean(); + if (!java.util.Objects.equals(dm.getIsActive(), newVal)) { + changes.add("isActive: " + dm.getIsActive() + " -> " + newVal); + } + dm.setIsActive(newVal); + } + if (json.has("introducedOn")) { + LocalDate newVal = LocalDate.parse(json.get("introducedOn").getAsString()); + if (!java.util.Objects.equals(dm.getIntroducedOn(), newVal)) { + changes.add("introducedOn: " + dm.getIntroducedOn() + " -> " + newVal); + } + dm.setIntroducedOn(newVal); + } + repository.save(dm); + writeAudit("UPDATE", dm, changes.isEmpty() ? null : String.join("; ", changes)); + } + JsonObject result = new JsonObject(); + result.addProperty("resourceId", id); + return gson.toJson(result); + } + + @DELETE + @Path("{id}") + public String delete(@PathParam("id") Long id) { + DenominationMaster dm = repository.findById(id).orElseThrow(() -> new RuntimeException("Denomination not found: " + id)); + // soft delete: flag the row instead of removing it, so it can be restored and history is preserved + dm.setDeleted(true); + dm.setIsActive(false); + repository.save(dm); + writeAudit("DELETE", dm, null); + JsonObject result = new JsonObject(); + result.addProperty("resourceId", id); + return gson.toJson(result); + } + + private void writeAudit(final String action, final DenominationMaster dm, final String changes) { + Long userId = null; + String username = null; + try { + AppUser user = this.context.authenticatedUser(); + if (user != null) { + userId = user.getId(); + username = user.getUsername(); + } + } catch (Exception e) { + // audit should never block the main operation if the user cannot be resolved + } + auditRepository.save(DenominationMasterAudit.of(action, dm, changes, userId, username)); + } + + private java.util.Map toMap(DenominationMaster dm) { + java.util.Map map = new java.util.LinkedHashMap<>(); + map.put("id", dm.getId()); + map.put("denomination", dm.getDenomination()); + map.put("kind", dm.getKind()); + map.put("description", dm.getDescription()); + map.put("isActive", dm.getIsActive()); + map.put("introducedOn", dm.getIntroducedOn() != null ? dm.getIntroducedOn().toString() : null); + map.put("discontinuedOn", dm.getDiscontinuedOn() != null ? dm.getDiscontinuedOn().toString() : null); + map.put("deleted", dm.getDeleted()); + return map; + } + + private java.util.Map auditToMap(DenominationMasterAudit a) { + java.util.Map map = new java.util.LinkedHashMap<>(); + map.put("id", a.getId()); + map.put("denominationId", a.getDenominationId()); + map.put("action", a.getAction()); + map.put("denomination", a.getDenomination()); + map.put("kind", a.getKind()); + map.put("description", a.getDescription()); + map.put("isActive", a.getIsActive()); + map.put("introducedOn", a.getIntroducedOn() != null ? a.getIntroducedOn().toString() : null); + map.put("discontinuedOn", a.getDiscontinuedOn() != null ? a.getDiscontinuedOn().toString() : null); + map.put("changes", a.getChanges()); + map.put("performedBy", a.getPerformedBy()); + map.put("performedByUsername", a.getPerformedByUsername()); + map.put("performedOn", a.getPerformedOn() != null ? a.getPerformedOn().toString() : null); + return map; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/DenominationMaster.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/DenominationMaster.java new file mode 100644 index 000000000..fcd83ea30 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/DenominationMaster.java @@ -0,0 +1,62 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. + */ +package org.apache.fineract.portfolio.denominationmaster.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.time.LocalDate; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "m_denomination_master") +@Getter +@Setter +@NoArgsConstructor +public class DenominationMaster { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "denomination", nullable = false) + private Integer denomination; + + @Column(name = "kind", nullable = false, length = 10) + private String kind; + + @Column(name = "description", length = 100) + private String description; + + @Column(name = "is_active", nullable = false) + private Boolean isActive = true; + + @Column(name = "introduced_on") + private LocalDate introducedOn; + + @Column(name = "discontinued_on") + private LocalDate discontinuedOn; + + @Column(name = "deleted", nullable = false) + private Boolean deleted = false; + + public static DenominationMaster create(Integer denomination, String kind, String description, + Boolean isActive, LocalDate introducedOn, LocalDate discontinuedOn) { + DenominationMaster dm = new DenominationMaster(); + dm.denomination = denomination; + dm.kind = kind; + dm.description = description; + dm.isActive = isActive != null ? isActive : true; + dm.introducedOn = introducedOn; + dm.discontinuedOn = discontinuedOn; + dm.deleted = false; + return dm; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/DenominationMasterAudit.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/DenominationMasterAudit.java new file mode 100644 index 000000000..1e9d05c1a --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/DenominationMasterAudit.java @@ -0,0 +1,83 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. + */ +package org.apache.fineract.portfolio.denominationmaster.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.time.LocalDate; +import java.time.LocalDateTime; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "m_denomination_master_audit") +@Getter +@Setter +@NoArgsConstructor +public class DenominationMasterAudit { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "denomination_id") + private Long denominationId; + + @Column(name = "action", nullable = false, length = 20) + private String action; + + @Column(name = "denomination") + private Integer denomination; + + @Column(name = "kind", length = 10) + private String kind; + + @Column(name = "description", length = 100) + private String description; + + @Column(name = "is_active") + private Boolean isActive; + + @Column(name = "introduced_on") + private LocalDate introducedOn; + + @Column(name = "discontinued_on") + private LocalDate discontinuedOn; + + @Column(name = "changes", length = 1000) + private String changes; + + @Column(name = "performed_by") + private Long performedBy; + + @Column(name = "performed_by_username", length = 100) + private String performedByUsername; + + @Column(name = "performed_on", nullable = false) + private LocalDateTime performedOn; + + public static DenominationMasterAudit of(final String action, final DenominationMaster dm, final String changes, + final Long performedBy, final String performedByUsername) { + final DenominationMasterAudit a = new DenominationMasterAudit(); + a.action = action; + a.denominationId = dm.getId(); + a.denomination = dm.getDenomination(); + a.kind = dm.getKind(); + a.description = dm.getDescription(); + a.isActive = dm.getIsActive(); + a.introducedOn = dm.getIntroducedOn(); + a.discontinuedOn = dm.getDiscontinuedOn(); + a.changes = changes; + a.performedBy = performedBy; + a.performedByUsername = performedByUsername; + a.performedOn = LocalDateTime.now(); + return a; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/DenominationMasterAuditRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/DenominationMasterAuditRepository.java new file mode 100644 index 000000000..a66dcbc32 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/DenominationMasterAuditRepository.java @@ -0,0 +1,17 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. + */ +package org.apache.fineract.portfolio.denominationmaster.domain; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface DenominationMasterAuditRepository extends JpaRepository { + + List findByDenominationIdOrderByPerformedOnDesc(Long denominationId); + + List findAllByOrderByPerformedOnDesc(); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/DenominationMasterRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/DenominationMasterRepository.java new file mode 100644 index 000000000..1b93d2024 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/DenominationMasterRepository.java @@ -0,0 +1,15 @@ +package org.apache.fineract.portfolio.denominationmaster.domain; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface DenominationMasterRepository extends JpaRepository { + List findByDeletedFalseOrderByKindDescDenominationDesc(); + List findByIsActiveTrueAndDeletedFalseOrderByKindDescDenominationDesc(); + List findByDeletedTrueOrderByKindDescDenominationDesc(); + + boolean existsByDenominationAndKindAndDeletedFalse(Integer denomination, String kind); + boolean existsByDenominationAndKindAndDeletedFalseAndIdNot(Integer denomination, String kind, Long id); +} 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 new file mode 100644 index 000000000..8db21d345 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/SavingsTxnDenomination.java @@ -0,0 +1,81 @@ +/** + * 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.denominationmaster.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.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "m_savings_txn_denomination") +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SavingsTxnDenomination { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "txn_id", nullable = false) + private Long txnId; + + @Column(name = "savings_account_id", nullable = false) + private Long savingsAccountId; + + @Column(name = "account_no", length = 50) + private String accountNo; + + @Column(name = "client_id") + private Long clientId; + + @Column(name = "cif_no", length = 50) + private String cifNo; + + @Column(name = "denomination_id", nullable = false) + 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, precision = 19, scale = 6) + private BigDecimal totalAmount; + + @Column(name = "txn_date") + private LocalDate txnDate; + + @Column(name = "created_at") + private LocalDateTime createdAt; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/SavingsTxnDenominationRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/SavingsTxnDenominationRepository.java new file mode 100644 index 000000000..b8a14a0aa --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/domain/SavingsTxnDenominationRepository.java @@ -0,0 +1,15 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. + */ +package org.apache.fineract.portfolio.denominationmaster.domain; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface SavingsTxnDenominationRepository extends JpaRepository { + List findByTxnId(Long txnId); + List findBySavingsAccountId(Long savingsAccountId); +} 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 53248d7d2..7edfbf915 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 @@ -1,1955 +1,1992 @@ -/** - * 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.SavingsApiConstants.SAVINGS_ACCOUNT_CHARGE_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 static org.apache.fineract.portfolio.savings.SavingsApiConstants.lienAllowedParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.transactionAmountParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.transactionDateParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.withHoldTaxParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.withdrawBalanceParamName; - -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import io.github.resilience4j.retry.annotation.Retry; -import java.math.BigDecimal; -import java.math.MathContext; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -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.exception.ErrorHandler; -import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; -import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; -import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; -import org.apache.fineract.infrastructure.core.exception.PlatformServiceUnavailableException; -import org.apache.fineract.infrastructure.core.service.DateUtils; -import org.apache.fineract.infrastructure.core.service.MathUtil; -import org.apache.fineract.infrastructure.dataqueries.data.EntityTables; -import org.apache.fineract.infrastructure.dataqueries.data.StatusEnum; -import org.apache.fineract.infrastructure.dataqueries.service.EntityDatatableChecksWritePlatformService; -import org.apache.fineract.infrastructure.event.business.domain.savings.SavingsActivateBusinessEvent; -import org.apache.fineract.infrastructure.event.business.domain.savings.SavingsCloseBusinessEvent; -import org.apache.fineract.infrastructure.event.business.domain.savings.SavingsPostInterestBusinessEvent; -import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; -import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; -import org.apache.fineract.organisation.holiday.domain.HolidayRepositoryWrapper; -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.staff.domain.StaffRepositoryWrapper; -import org.apache.fineract.organisation.workingdays.domain.WorkingDaysRepositoryWrapper; -import org.apache.fineract.portfolio.account.PortfolioAccountType; -import org.apache.fineract.portfolio.account.domain.AccountTransferStandingInstruction; -import org.apache.fineract.portfolio.account.domain.StandingInstructionRepository; -import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus; -import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformService; -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.charge.domain.ChargeTimeType; -import org.apache.fineract.portfolio.client.domain.Client; -import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; -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.service.FacilityChargeAutoApplyService; -import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType; -import org.apache.fineract.portfolio.savings.SavingsApiConstants; -import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues; -import org.apache.fineract.portfolio.savings.data.SavingsAccountChargeDataValidator; -import org.apache.fineract.portfolio.savings.data.SavingsAccountData; -import org.apache.fineract.portfolio.savings.data.SavingsAccountDataValidator; -import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionDTO; -import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; -import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionDataValidator; -import org.apache.fineract.portfolio.savings.domain.DepositAccountOnHoldTransaction; -import org.apache.fineract.portfolio.savings.domain.DepositAccountOnHoldTransactionRepository; -import org.apache.fineract.portfolio.savings.domain.GSIMRepositoy; -import org.apache.fineract.portfolio.savings.domain.GroupSavingsIndividualMonitoring; -import org.apache.fineract.portfolio.savings.domain.SavingsAccount; -import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; -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.PostInterestAsOnDateException; -import org.apache.fineract.portfolio.savings.exception.PostInterestAsOnDateException.PostInterestAsOnExceptionType; -import org.apache.fineract.portfolio.savings.exception.PostInterestClosingDateException; -import org.apache.fineract.portfolio.savings.exception.SavingsAccountClosingNotAllowedException; -import org.apache.fineract.portfolio.savings.exception.SavingsAccountTransactionNotFoundException; -import org.apache.fineract.portfolio.savings.exception.SavingsOfficerAssignmentException; -import org.apache.fineract.portfolio.savings.exception.SavingsOfficerUnassignmentException; -import org.apache.fineract.portfolio.savings.exception.TransactionUpdateNotAllowedException; -import org.apache.fineract.portfolio.transfer.api.TransferApiConstants; -import org.apache.fineract.useradministration.domain.AppUser; -import org.apache.fineract.useradministration.domain.AppUserRepositoryWrapper; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.CollectionUtils; - -@Slf4j -@RequiredArgsConstructor -public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements SavingsAccountWritePlatformService { - - private final PlatformSecurityContext context; - private final SavingsAccountDataValidator fromApiJsonDeserializer; - private final SavingsAccountRepositoryWrapper savingAccountRepositoryWrapper; - private final StaffRepositoryWrapper staffRepository; - private final SavingsAccountTransactionRepository savingsAccountTransactionRepository; - private final SavingsAccountAssembler savingAccountAssembler; - private final SavingsAccountTransactionDataValidator savingsAccountTransactionDataValidator; - private final SavingsAccountChargeDataValidator savingsAccountChargeDataValidator; - private final PaymentDetailWritePlatformService paymentDetailWritePlatformService; - private final JournalEntryWritePlatformService journalEntryWritePlatformService; - private final SavingsAccountDomainService savingsAccountDomainService; - private final NoteRepository noteRepository; - private final AccountTransfersReadPlatformService accountTransfersReadPlatformService; - private final AccountAssociationsReadPlatformService accountAssociationsReadPlatformService; - private final ChargeRepositoryWrapper chargeRepository; - private final SavingsAccountChargeRepositoryWrapper savingsAccountChargeRepository; - private final HolidayRepositoryWrapper holidayRepository; - private final WorkingDaysRepositoryWrapper workingDaysRepository; - private final ConfigurationDomainService configurationDomainService; - private final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository; - private final EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService; - private final AppUserRepositoryWrapper appuserRepository; - private final StandingInstructionRepository standingInstructionRepository; - private final BusinessEventNotifierService businessEventNotifierService; - private final GSIMRepositoy gsimRepository; - private final SavingsAccountInterestPostingService savingsAccountInterestPostingService; - private final ErrorHandler errorHandler; - private final FacilityChargeAutoApplyService facilityChargeAutoApplyService; - - @Transactional - @Override - public CommandProcessingResult gsimActivate(final Long gsimId, final JsonCommand command) { - - Long parentSavingId = gsimId; - GroupSavingsIndividualMonitoring parentSavings = gsimRepository.findById(parentSavingId).orElseThrow(); - List childSavings = this.savingAccountRepositoryWrapper.findByGsimId(gsimId); - - CommandProcessingResult result = null; - int count = 0; - for (SavingsAccount account : childSavings) { - result = activate(account.getId(), command); - if (result != null) { - count++; - if (count == parentSavings.getChildAccountsCount()) { - parentSavings.setSavingsStatus(SavingsAccountStatusType.ACTIVE.getValue()); - gsimRepository.save(parentSavings); - } - } - } - return result; - } - - @Transactional - @Override - public CommandProcessingResult activate(final Long savingsId, final JsonCommand command) { - - final AppUser user = this.context.authenticatedUser(); - - this.savingsAccountTransactionDataValidator.validateActivation(command); - - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); - - // Validate minimum opening balance - BigDecimal openingBalanceForValidation = BigDecimal.ZERO; - if (command.parameterExists("openingBalance")) { - final BigDecimal ob = command.bigDecimalValueOfParameterNamed("openingBalance"); - if (ob != null) openingBalanceForValidation = ob; - } - final BigDecimal minRequiredOpening = account.getMinRequiredOpeningBalance(); - if (minRequiredOpening != null && minRequiredOpening.compareTo(BigDecimal.ZERO) > 0 - && openingBalanceForValidation.compareTo(minRequiredOpening) < 0) { - final List errors = new ArrayList<>(); - errors.add(ApiParameterError.parameterError( - "openingBalance", - "Opening balance (" + openingBalanceForValidation + ") is less than minimum required opening balance (" + minRequiredOpening + ")", - "validation.msg.savingsaccount.openingBalance.less.than.min.required", - openingBalanceForValidation, minRequiredOpening)); - throw new PlatformApiDataValidationException(errors); - } - - checkClientOrGroupActive(account); - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - - final Map changes = account.activate(user, command); - - entityDatatableChecksWritePlatformService.runTheCheckForProduct(savingsId, EntityTables.SAVINGS.getName(), - StatusEnum.ACTIVATE.getValue(), EntityTables.SAVINGS.getForeignKeyColumnNameOnDatatable(), account.productId()); - - if (!changes.isEmpty()) { - final Locale locale = command.extractLocale(); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); - processPostActiveActions(account, fmt, existingTransactionIds, existingReversedTransactionIds); - // Auto-apply facility charges based on product facility config - facilityChargeAutoApplyService.applyFacilityChargesOnActivation(account); - this.savingAccountRepositoryWrapper.saveAndFlush(account); - // Collect any newly added facility activation charges (after flush so amountOutstanding is set) - // payCharge() internally adds transactions to account.transactions via handleChargeTransactions - // Do NOT add to existingTransactionIds — postJournalEntries must include them for GL posting - facilityChargeAutoApplyService.collectActivationCharges(account); - this.savingAccountRepositoryWrapper.saveAndFlush(account); - } - - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false); - businessEventNotifierService.notifyPostBusinessEvent(new SavingsActivateBusinessEvent(account)); - - return new CommandProcessingResultBuilder() // - .withEntityId(savingsId) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .with(changes) // - .build(); - } - - @Override - public void processPostActiveActions(final SavingsAccount account, final DateTimeFormatter fmt, final Set existingTransactionIds, - final Set existingReversedTransactionIds) { - getAppUserIfPresent(); - - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - - Money amountForDeposit = account.activateWithBalance(); - boolean isRegularTransaction = false; - if (amountForDeposit.isGreaterThanZero()) { - boolean isAccountTransfer = false; - this.savingsAccountDomainService.handleDeposit(account, fmt, account.getActivationDate(), amountForDeposit.getAmount(), null, - isAccountTransfer, isRegularTransaction, false); - - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - } - - account.processAccountUponActivation(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); - } - - @Transactional - @Override - public CommandProcessingResult gsimDeposit(final Long gsimId, final JsonCommand command) { - JsonArray savingsArray = command.arrayOfParameterNamed("savingsArray"); - - CommandProcessingResult result = null; - for (JsonElement element : savingsArray) { - result = deposit(element.getAsJsonObject().get("childAccountId").getAsLong(), - JsonCommand.fromExistingCommand(command, element)); - } - return result; - } - - @Transactional - @Override - public CommandProcessingResult deposit(final Long savingsId, final JsonCommand command) { - this.context.authenticatedUser(); - - this.savingsAccountTransactionDataValidator.validate(command); - boolean isGsim = false; - - final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); - - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill); - - if (account.getGsim() != null) { - isGsim = true; - log.debug("is gsim"); - } - 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"); - - this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transactionDate, account); - - final Map changes = new LinkedHashMap<>(); - final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); - boolean isAccountTransfer = false; - boolean isRegularTransaction = true; - final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(account, fmt, transactionDate, - transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill); - - if (isGsim && (deposit.getId() != null)) { - - log.debug("Deposit account has been created: {} ", deposit); - - GroupSavingsIndividualMonitoring gsim = gsimRepository.findById(account.getGsim().getId()).orElseThrow(); - log.debug("parent deposit : {} ", gsim.getParentDeposit()); - log.debug("child account : {} ", savingsId); - BigDecimal currentBalance = gsim.getParentDeposit(); - BigDecimal newBalance = currentBalance.add(transactionAmount); - gsim.setParentDeposit(newBalance); - gsimRepository.save(gsim); - log.debug("balance after making deposit : {} ", - gsimRepository.findById(account.getGsim().getId()).orElseThrow().getParentDeposit()); - - } - - final String noteText = command.stringValueOfParameterNamed("note"); - if (StringUtils.isNotBlank(noteText)) { - final Note note = Note.savingsTransactionNote(account, deposit, noteText); - this.noteRepository.save(note); - } - - 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) { - - this.savingsAccountTransactionDataValidator.validate(command); - - boolean isGsim = false; - - 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 boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); - - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill); - - if (account.getGsim() != null) { - isGsim = true; - } - checkClientOrGroupActive(account); - - this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transactionDate, account); - - final boolean isAccountTransfer = false; - final boolean isRegularTransaction = true; - final boolean isApplyWithdrawFee = true; - final boolean isInterestTransfer = false; - final boolean isWithdrawBalance = false; - final SavingsTransactionBooleanValues transactionBooleanValues = new SavingsTransactionBooleanValues(isAccountTransfer, - isRegularTransaction, isApplyWithdrawFee, isInterestTransfer, isWithdrawBalance); - final SavingsAccountTransaction withdrawal = this.savingsAccountDomainService.handleWithdrawal(account, fmt, transactionDate, - transactionAmount, paymentDetail, transactionBooleanValues, backdatedTxnsAllowedTill); - - if (isGsim && (withdrawal.getId() != null)) { - GroupSavingsIndividualMonitoring gsim = gsimRepository.findById(account.getGsim().getId()).orElseThrow(); - BigDecimal currentBalance = gsim.getParentDeposit().subtract(transactionAmount); - gsim.setParentDeposit(currentBalance); - gsimRepository.save(gsim); - - } - - final String noteText = command.stringValueOfParameterNamed("note"); - if (StringUtils.isNotBlank(noteText)) { - final Note note = Note.savingsTransactionNote(account, withdrawal, noteText); - this.noteRepository.save(note); - } - - return new CommandProcessingResultBuilder() // - .withEntityId(withdrawal.getId()) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .with(changes)// - .build(); - } - - @Transactional - @Override - public CommandProcessingResult applyAnnualFee(final Long savingsAccountChargeId, final Long accountId) { - getAppUserIfPresent(); - - final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository - .findOneWithNotFoundDetection(savingsAccountChargeId, accountId); - - final LocalDate currentDate = DateUtils.getBusinessLocalDate(); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd MM yyyy").withZone(DateUtils.getDateTimeZoneOfTenant()); - - while (DateUtils.isBefore(savingsAccountCharge.getDueDate(), currentDate)) { - this.payCharge(savingsAccountCharge, savingsAccountCharge.getDueDate(), savingsAccountCharge.amount(), fmt, false); - } - - 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 CommandProcessingResult calculateInterest(final Long savingsId) { - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - - final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill); - checkClientOrGroupActive(account); - - final LocalDate today = DateUtils.getBusinessLocalDate(); - final MathContext mc = new MathContext(15, MoneyHelper.getRoundingMode()); - boolean isInterestTransfer = false; - final LocalDate postInterestOnDate = null; - boolean postReversals = false; - account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals); - - if (backdatedTxnsAllowedTill) { - this.savingsAccountTransactionRepository.saveAll(account.getSavingsAccountTransactionsWithPivotConfig()); - } - - this.savingAccountRepositoryWrapper.save(account); - - return new CommandProcessingResultBuilder() // - .withEntityId(savingsId) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .build(); - } - - @Override - @Transactional - public CommandProcessingResult postInterest(final JsonCommand command) { - Long savingsId = command.getSavingsId(); - final boolean postInterestAs = command.booleanPrimitiveValueOfParameterNamed("isPostInterestAsOn"); - final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); - - final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill); - checkClientOrGroupActive(account); - - this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transactionDate, account); - - if (postInterestAs) { - if (transactionDate == null) { - throw new PostInterestAsOnDateException(PostInterestAsOnExceptionType.VALID_DATE); - } - if (DateUtils.isBefore(transactionDate, account.accountSubmittedOrActivationDate())) { - throw new PostInterestAsOnDateException(PostInterestAsOnExceptionType.ACTIVATION_DATE); - } - - List savingTransactions = null; - if (backdatedTxnsAllowedTill) { - savingTransactions = account.getSavingsAccountTransactionsWithPivotConfig(); - } else { - savingTransactions = account.getTransactions(); - } - - for (SavingsAccountTransaction savingTransaction : savingTransactions) { - if (DateUtils.isBefore(transactionDate, savingTransaction.getDateOf())) { - throw new PostInterestAsOnDateException(PostInterestAsOnExceptionType.LAST_TRANSACTION_DATE); - } - } - - if (DateUtils.isDateInTheFuture(transactionDate)) { - throw new PostInterestAsOnDateException(PostInterestAsOnExceptionType.FUTURE_DATE); - } - } - postInterest(account, postInterestAs, transactionDate, backdatedTxnsAllowedTill); - - businessEventNotifierService.notifyPostBusinessEvent(new SavingsPostInterestBusinessEvent(account)); - return new CommandProcessingResultBuilder() // - .withEntityId(savingsId) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .build(); - } - - @Transactional - @Override - public void postInterest(final SavingsAccount account, final boolean postInterestAs, final LocalDate transactionDate, - final boolean backdatedTxnsAllowedTill) { - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - - if (account.getNominalAnnualInterestRate().compareTo(BigDecimal.ZERO) > 0 - || (account.allowOverdraft() && account.getNominalAnnualInterestRateOverdraft().compareTo(BigDecimal.ZERO) > 0)) { - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - - if (backdatedTxnsAllowedTill) { - updateSavingsTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - } else { - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - } - - final LocalDate today = DateUtils.getBusinessLocalDate(); - final MathContext mc = new MathContext(10, MoneyHelper.getRoundingMode()); - boolean isInterestTransfer = false; - LocalDate postInterestOnDate = null; - if (postInterestAs) { - postInterestOnDate = transactionDate; - } - boolean postReversals = false; - account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, - postInterestOnDate, backdatedTxnsAllowedTill, postReversals); - - if (!backdatedTxnsAllowedTill) { - List transactions = account.getTransactions(); - for (SavingsAccountTransaction accountTransaction : transactions) { - if (accountTransaction.getId() == null) { - this.savingsAccountTransactionRepository.save(accountTransaction); - } - } - } - - if (backdatedTxnsAllowedTill) { - this.savingsAccountTransactionRepository.saveAll(account.getSavingsAccountTransactionsWithPivotConfig()); - } - - this.savingAccountRepositoryWrapper.saveAndFlush(account); - - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, backdatedTxnsAllowedTill); - } - } - - @Transactional - @Override - @Retry(name = "postInterest", fallbackMethod = "fallbackPostInterest") - public SavingsAccountData postInterest(SavingsAccountData savingsAccountData, final boolean postInterestAs, - final LocalDate transactionDate, final boolean backdatedTxnsAllowedTill) { - - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - - if (MathUtil.isGreaterThanZero(savingsAccountData.getNominalAnnualInterestRate()) || (savingsAccountData.isAllowOverdraft() - && MathUtil.isGreaterThanZero(savingsAccountData.getNominalAnnualInterestRateOverdraft()))) { - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - updateExistingTransactionsDetails(savingsAccountData, existingTransactionIds, existingReversedTransactionIds); - - final LocalDate today = DateUtils.getBusinessLocalDate(); - final MathContext mc = new MathContext(10, MoneyHelper.getRoundingMode()); - boolean isInterestTransfer = false; - LocalDate postInterestOnDate = null; - if (postInterestAs) { - postInterestOnDate = transactionDate; - } - - savingsAccountData = this.savingsAccountInterestPostingService.postInterest(mc, today, isInterestTransfer, - isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, - savingsAccountData); - - if (!backdatedTxnsAllowedTill) { - List transactions = savingsAccountData.getSavingsAccountTransactionData(); - for (SavingsAccountTransactionData accountTransaction : transactions) { - if (accountTransaction.getId() == null) { - savingsAccountData.setNewSavingsAccountTransactionData(accountTransaction); - selectAccountId(accountTransaction, savingsAccountData); - } - } - } - savingsAccountData.setExistingTransactionIds(existingTransactionIds); - savingsAccountData.setExistingReversedTransactionIds(existingReversedTransactionIds); - } - return savingsAccountData; - } - - public void selectAccountId(SavingsAccountTransactionData accountTransaction, SavingsAccountData savingsAccountData) { - SavingsAccountTransactionType transactionType = SavingsAccountTransactionType - .fromInt(accountTransaction.getTransactionType().getId().intValue()); - if (transactionType.isOverDraftInterestPosting()) { - if (MathUtil.isGreaterThanZero(accountTransaction.getRunningBalance())) { - accountTransaction.setAccountDebit(savingsAccountData.getGlAccountIdForSavingsControl()); - accountTransaction.setAccountCredit(savingsAccountData.getGlAccountIdForInterestReceivable()); - } else { - accountTransaction.setAccountDebit(savingsAccountData.getGlAccountIdForOverdraftPorfolio()); - accountTransaction.setAccountCredit(savingsAccountData.getGlAccountIdForInterestReceivable()); - } - } else { - accountTransaction.setAccountDebit(savingsAccountData.getGlAccountIdForInterestPayable()); - accountTransaction.setAccountCredit(savingsAccountData.getGlAccountIdForSavingsControl()); - } - } - - @Override - public CommandProcessingResult reverseTransaction(final Long savingsId, final Long transactionId, - final boolean allowAccountTransferModification, final JsonCommand command) { - - final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); - final boolean isBulk = command.booleanPrimitiveValueOfParameterNamed("isBulk"); - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill); - - 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.saving.account.transfer.transaction.update.not.allowed", - "Savings account transaction:" + transactionId + " update not allowed as it involves in account transfer", - transactionId); - } - - if (!account.allowModify()) { - throw new PlatformServiceUnavailableException("error.msg.saving.account.transaction.update.not.allowed", - "Savings account transaction:" + transactionId + " update not allowed for this savings type", transactionId); - } - if (account.isNotActive()) { - throwValidationForActiveStatus(SavingsApiConstants.undoTransactionAction); - } - SavingsAccountTransaction reversal = null; - Long reversalId = null; - checkClientOrGroupActive(account); - List savingsAccountTransactions = null; - if (isBulk) { - String transactionRefNo = savingsAccountTransaction.getRefNo(); - savingsAccountTransactions = this.savingsAccountTransactionRepository.findByRefNo(transactionRefNo); - reversal = this.savingsAccountDomainService.handleReversal(account, savingsAccountTransactions, backdatedTxnsAllowedTill); - } else { - reversal = this.savingsAccountDomainService.handleReversal(account, Collections.singletonList(savingsAccountTransaction), - backdatedTxnsAllowedTill); - } - reversalId = reversal.getId(); - return new CommandProcessingResultBuilder() // - .withEntityId(reversalId) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .build(); - } - - @Override - public CommandProcessingResult undoTransaction(final Long savingsId, final Long transactionId, - final boolean allowAccountTransferModification) { - - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); - 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); - } - - this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(savingsAccountTransaction.getTransactionDate(), - account); - - if (!allowAccountTransferModification - && this.accountTransfersReadPlatformService.isAccountTransfer(transactionId, PortfolioAccountType.SAVINGS)) { - throw new PlatformServiceUnavailableException("error.msg.saving.account.transfer.transaction.update.not.allowed", - "Savings account transaction:" + transactionId + " update not allowed as it involves in account transfer", - transactionId); - } - - if (!account.allowModify()) { - throw new PlatformServiceUnavailableException("error.msg.saving.account.transaction.update.not.allowed", - "Savings account transaction:" + transactionId + " update not allowed for this savings type", transactionId); - } - - final LocalDate today = DateUtils.getBusinessLocalDate(); - final MathContext mc = new MathContext(15, MoneyHelper.getRoundingMode()); - - if (account.isNotActive()) { - throwValidationForActiveStatus(SavingsApiConstants.undoTransactionAction); - } - account.undoTransaction(transactionId); - - // undoing transaction is withdrawal then undo withdrawal fee - // transaction if any - if (savingsAccountTransaction.isWithdrawal()) { - final SavingsAccountTransaction nextSavingsAccountTransaction = this.savingsAccountTransactionRepository - .findOneByIdAndSavingsAccountId(transactionId + 1, savingsId); - if (nextSavingsAccountTransaction != null && nextSavingsAccountTransaction.isWithdrawalFeeAndNotReversed()) { - account.undoTransaction(transactionId + 1); - } - } - boolean isInterestTransfer = false; - LocalDate postInterestOnDate = null; - boolean postReversals = false; - checkClientOrGroupActive(account); - 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(); - this.savingAccountRepositoryWrapper.saveAndFlush(account); - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false); - return new CommandProcessingResultBuilder() // - .withEntityId(savingsId) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .build(); - } - - @Override - public CommandProcessingResult adjustSavingsTransaction(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(); - 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", - "Savings account transaction:" + transactionId + " update not allowed as it involves in account transfer", - transactionId); - } - - this.savingsAccountTransactionDataValidator.validate(command); - - final LocalDate today = DateUtils.getBusinessLocalDate(); - - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); - - if (account.isNotActive()) { - throwValidationForActiveStatus(SavingsApiConstants.adjustTransactionAction); - } - if (!account.allowModify()) { - throw new PlatformServiceUnavailableException("error.msg.saving.account.transaction.update.not.allowed", - "Savings account transaction:" + transactionId + " update not allowed for this savings type", transactionId); - } - 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); - - // for undo withdrawal fee - final SavingsAccountTransaction nextSavingsAccountTransaction = this.savingsAccountTransactionRepository - .findOneByIdAndSavingsAccountId(transactionId + 1, savingsId); - if (nextSavingsAccountTransaction != null && nextSavingsAccountTransaction.isWithdrawalFeeAndNotReversed()) { - account.undoTransaction(transactionId + 1); - } - - SavingsAccountTransaction transaction = null; - boolean isInterestTransfer = false; - Integer accountType = null; - final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount, - paymentDetail, null, accountType); - UUID refNo = UUID.randomUUID(); - if (savingsAccountTransaction.isDeposit()) { - transaction = account.deposit(transactionDTO, false, relaxingDaysConfigForPivotDate, refNo.toString()); - } else { - transaction = account.withdraw(transactionDTO, true, false, relaxingDaysConfigForPivotDate, refNo.toString()); - } - final Long newtransactionId = saveTransactionToGenerateTransactionId(transaction); - final LocalDate postInterestOnDate = null; - 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(); - - this.savingAccountRepositoryWrapper.saveAndFlush(account); - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false); - 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) { - if (client.isNotActive()) { - throw new ClientNotActiveException(client.getId()); - } - } - final Group group = account.group(); - if (group != null) { - if (group.isNotActive()) { - throw new GroupNotActiveException(group.getId()); - } - } - } - - @Override - public CommandProcessingResult bulkGSIMClose(final Long gsimId, final JsonCommand command) { - - final Long parentSavingId = gsimId; - GroupSavingsIndividualMonitoring parentSavings = gsimRepository.findById(parentSavingId).orElseThrow(); - List childSavings = this.savingAccountRepositoryWrapper.findByGsimId(gsimId); - - CommandProcessingResult result = null; - int count = 0; - for (SavingsAccount account : childSavings) { - result = close(account.getId(), command); - - if (result != null) { - count++; - if (count == parentSavings.getChildAccountsCount()) { - parentSavings.setSavingsStatus(SavingsAccountStatusType.CLOSED.getValue()); - gsimRepository.save(parentSavings); - } - } - } - return result; - } - - @Override - public CommandProcessingResult close(final Long savingsId, final JsonCommand command) { - final AppUser user = this.context.authenticatedUser(); - - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); - this.savingsAccountTransactionDataValidator.validateClosing(command, account); - - final boolean isLinkedWithAnyActiveLoan = this.accountAssociationsReadPlatformService.isLinkedWithAnyActiveAccount(savingsId); - - if (isLinkedWithAnyActiveLoan) { - final String defaultUserMessage = "Closing savings account with id:" + savingsId - + " is not allowed, since it is linked with one of the active accounts"; - throw new SavingsAccountClosingNotAllowedException("linked", defaultUserMessage, savingsId); - } - - entityDatatableChecksWritePlatformService.runTheCheckForProduct(savingsId, EntityTables.SAVINGS.getName(), - StatusEnum.CLOSE.getValue(), EntityTables.SAVINGS.getForeignKeyColumnNameOnDatatable(), account.productId()); - - final boolean isWithdrawBalance = command.booleanPrimitiveValueOfParameterNamed(withdrawBalanceParamName); - - final Locale locale = command.extractLocale(); - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); - final LocalDate closedDate = command.localDateValueOfParameterNamed(SavingsApiConstants.closedOnDateParamName); - final boolean isPostInterest = command.booleanPrimitiveValueOfParameterNamed(SavingsApiConstants.postInterestValidationOnClosure); - // postInterest(account,closedDate,flag); - if (isPostInterest) { - boolean postInterestOnClosingDate = false; - List savingTransactions = account.getTransactions(); - for (SavingsAccountTransaction savingTransaction : savingTransactions) { - if (savingTransaction.isInterestPosting() && savingTransaction.isNotReversed() - && DateUtils.isEqual(closedDate, savingTransaction.getTransactionDate())) { - postInterestOnClosingDate = true; - break; - } - } - if (!postInterestOnClosingDate) { - throw new PostInterestClosingDateException(); - } - } - - final Map changes = new LinkedHashMap<>(); - - if (isWithdrawBalance && account.getSummary().getAccountBalance(account.getCurrency()).isGreaterThanZero()) { - - final BigDecimal transactionAmount = account.getSummary().getAccountBalance(); - - final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); - - final boolean isAccountTransfer = false; - final boolean isRegularTransaction = true; - final boolean isApplyWithdrawFee = false; - final boolean isInterestTransfer = false; - final SavingsTransactionBooleanValues transactionBooleanValues = new SavingsTransactionBooleanValues(isAccountTransfer, - isRegularTransaction, isApplyWithdrawFee, isInterestTransfer, isWithdrawBalance); - - this.savingsAccountDomainService.handleWithdrawal(account, fmt, closedDate, transactionAmount, paymentDetail, - transactionBooleanValues, false); - - } - - final Map accountChanges = account.close(user, command); - changes.putAll(accountChanges); - if (!changes.isEmpty()) { - this.savingAccountRepositoryWrapper.save(account); - 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); - } - - } - - businessEventNotifierService.notifyPostBusinessEvent(new SavingsCloseBusinessEvent(account)); - // disable all standing orders linked to the savings account - disableStandingInstructionsLinkedToClosedSavings(account); - return new CommandProcessingResultBuilder() // - .withEntityId(savingsId) // - .withOfficeId(account.officeId()) // - .withClientId(account.clientId()) // - .withGroupId(account.groupId()) // - .withSavingsId(savingsId) // - .with(changes) // - .build(); - } - - @Override - public SavingsAccountTransaction initiateSavingsTransfer(final SavingsAccount savingsAccount, final LocalDate transferDate) { - getAppUserIfPresent(); - - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - - validateTransactionsForTransfer(savingsAccount, transferDate); - - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - this.savingAccountAssembler.setHelpers(savingsAccount); - 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; - LocalDate postInterestOnDate = null; - 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, false); - - return newTransferTransaction; - } - - @Override - public SavingsAccountTransaction withdrawSavingsTransfer(final SavingsAccount savingsAccount, final LocalDate transferDate) { - getAppUserIfPresent(); - - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - this.savingAccountAssembler.setHelpers(savingsAccount); - 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 MathContext mc = MathContext.DECIMAL64; - boolean isInterestTransfer = false; - LocalDate postInterestOnDate = null; - boolean postReversals = false; - savingsAccount.calculateInterestUsing(mc, transferDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, false, postReversals); - - this.savingsAccountTransactionRepository.save(withdrawtransferTransaction); - this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); - - postJournalEntries(savingsAccount, existingTransactionIds, existingReversedTransactionIds, false); - - return withdrawtransferTransaction; - } - - @Override - public void rejectSavingsTransfer(final SavingsAccount savingsAccount) { - this.savingAccountAssembler.setHelpers(savingsAccount); - savingsAccount.setStatus(SavingsAccountStatusType.TRANSFER_ON_HOLD.getValue()); - this.savingAccountRepositoryWrapper.save(savingsAccount); - } - - @Override - public SavingsAccountTransaction acceptSavingsTransfer(final SavingsAccount savingsAccount, final LocalDate transferDate, - final Office acceptedInOffice, final Staff fieldOfficer) { - getAppUserIfPresent(); - - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - this.savingAccountAssembler.setHelpers(savingsAccount); - 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 MathContext mc = MathContext.DECIMAL64; - LocalDate postInterestOnDate = null; - boolean postReversals = false; - savingsAccount.calculateInterestUsing(mc, transferDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, false, postReversals); - - this.savingsAccountTransactionRepository.save(acceptTransferTransaction); - this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); - - postJournalEntries(savingsAccount, existingTransactionIds, existingReversedTransactionIds, false); - - return acceptTransferTransaction; - } - - @Transactional - @Override - public CommandProcessingResult addSavingsAccountCharge(final JsonCommand command) { - 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.savingAccountAssembler.assembleFrom(savingsAccountId, false); - 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); - - Integer chargeTimeType = chargeDefinition.getChargeTimeType(); - LocalDate dueAsOfDateParam = command.localDateValueOfParameterNamed(dueAsOfDateParamName); - if ((chargeTimeType.equals(ChargeTimeType.WITHDRAWAL_FEE.getValue()) - || chargeTimeType.equals(ChargeTimeType.SAVINGS_NOACTIVITY_FEE.getValue())) && dueAsOfDateParam != null) { - baseDataValidator.reset().parameter(dueAsOfDateParamName).value(dueAsOfDateParam.format(fmt)) - .failWithCodeNoParameterAddedToErrorCode( - "charge.due.date.is.invalid.for." + ChargeTimeType.fromInt(chargeTimeType).getCode()); - } - final SavingsAccountCharge savingsAccountCharge = SavingsAccountCharge.createNewFromJson(savingsAccount, chargeDefinition, command); - - if (chargeDefinition.isEnableFreeWithdrawal()) { - savingsAccountCharge.setFreeWithdrawalCount(0); - } - - 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 (!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.savingsAccountChargeRepository.save(savingsAccountCharge); - 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) { - - 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.savingAccountAssembler.assembleFrom(savingsAccountId, false); - 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) { - 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(); - - final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); - - this.savingAccountAssembler.loadTransactionsToSavingsAccount(account, backdatedTxnsAllowedTill); - - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - - if (backdatedTxnsAllowedTill) { - updateSavingsTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - } else { - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - } - - account.waiveCharge(savingsAccountChargeId, backdatedTxnsAllowedTill); - - boolean isInterestTransfer = false; - LocalDate postInterestOnDate = null; - final MathContext mc = MathContext.DECIMAL64; - boolean postReversals = false; - if (account.isBeforeLastPostingPeriod(savingsAccountCharge.getDueDate(), backdatedTxnsAllowedTill)) { - final LocalDate today = DateUtils.getBusinessLocalDate(); - account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, - postInterestOnDate, backdatedTxnsAllowedTill, postReversals); - } else { - final LocalDate today = DateUtils.getBusinessLocalDate(); - account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals); - } - - List depositAccountOnHoldTransactions = null; - if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { - depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository - .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); - } - - account.validateAccountBalanceDoesNotBecomeNegative(SavingsApiConstants.waiveChargeTransactionAction, - depositAccountOnHoldTransactions, backdatedTxnsAllowedTill); - - if (backdatedTxnsAllowedTill) { - this.savingsAccountTransactionRepository.saveAll(account.getSavingsAccountTransactionsWithPivotConfig()); - } - - this.savingAccountRepositoryWrapper.saveAndFlush(account); - - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, backdatedTxnsAllowedTill); - 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) { - this.context.authenticatedUser(); - - final SavingsAccount savingsAccount = this.savingAccountAssembler.assembleFrom(savingsAccountId, false); - 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) { - 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); - } - } - - final boolean backdatedTxnsAllowedTill = false; - - SavingsAccountTransaction chargeTransaction = this.payCharge(savingsAccountCharge, transactionDate, amountPaid, fmt, - backdatedTxnsAllowedTill); - - final String noteText = command.stringValueOfParameterNamed("note"); - if (StringUtils.isNotBlank(noteText)) { - final Note note = Note.savingsTransactionNote(savingsAccountCharge.savingsAccount(), chargeTransaction, noteText); - this.noteRepository.save(note); - } - - 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) { - // always use current date as transaction date for batch job - final LocalDate transactionDate = DateUtils.getBusinessLocalDate(); - final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository - .findOneWithNotFoundDetection(savingsAccountChargeId, accountId); - - final DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd MM yyyy").withZone(DateUtils.getDateTimeZoneOfTenant()); - - while (savingsAccountCharge.isNotFullyPaid() && DateUtils.isBefore(savingsAccountCharge.getDueDate(), transactionDate)) { - payCharge(savingsAccountCharge, transactionDate, savingsAccountCharge.amoutOutstanding(), fmt, false); - } - } - - @SuppressWarnings("unused") - public SavingsAccountData fallbackPostInterest(SavingsAccountData savingsAccountData, boolean postInterestAs, LocalDate transactionDate, - boolean backdatedTxnsAllowedTill, Throwable t) { - // NOTE: allow caller to catch the exceptions - throw org.apache.fineract.infrastructure.core.exception.ErrorHandler.getMappable(t, null, null, "savings.postinterest"); - } - - @Transactional - private SavingsAccountTransaction payCharge(final SavingsAccountCharge savingsAccountCharge, final LocalDate transactionDate, - final BigDecimal amountPaid, final DateTimeFormatter formatter, final boolean backdatedTxnsAllowedTill) { - final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService - .isSavingsInterestPostingAtCurrentPeriodEnd(); - final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); - - // Get Savings account from savings charge - final SavingsAccount account = savingsAccountCharge.savingsAccount(); - this.savingAccountAssembler.assignSavingAccountHelpers(account); - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - Pageable sortedByDateAndIdDesc = PageRequest.of(0, 1, Sort.by("dateOf", "id").descending()); - - List savingsAccountTransaction = this.savingsAccountTransactionRepository - .findBySavingsAccountIdAndLessThanDateOfAndReversedIsFalse(account.getId(), transactionDate, sortedByDateAndIdDesc); - - account.validateAccountBalanceDoesNotViolateOverdraft(savingsAccountTransaction, amountPaid); - - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - SavingsAccountTransaction chargeTransaction = account.payCharge(savingsAccountCharge, amountPaid, transactionDate, formatter, - backdatedTxnsAllowedTill, null); - boolean isInterestTransfer = false; - LocalDate postInterestOnDate = null; - final MathContext mc = MathContext.DECIMAL64; - boolean postReversals = false; - if (account.isBeforeLastPostingPeriod(transactionDate, backdatedTxnsAllowedTill)) { - final LocalDate today = DateUtils.getBusinessLocalDate(); - account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, - postInterestOnDate, isInterestTransfer, postReversals); - } else { - final LocalDate today = DateUtils.getBusinessLocalDate(); - account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, - financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals); - } - List depositAccountOnHoldTransactions = null; - - if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { - depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository - .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); - } - - account.validateAccountBalanceDoesNotBecomeNegative("." + SavingsAccountTransactionType.PAY_CHARGE.getCode(), - depositAccountOnHoldTransactions, backdatedTxnsAllowedTill); - - saveTransactionToGenerateTransactionId(chargeTransaction); - - this.savingAccountRepositoryWrapper.saveAndFlush(account); - - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, backdatedTxnsAllowedTill); - - return chargeTransaction; - } - - private void updateExistingTransactionsDetails(SavingsAccount account, Set existingTransactionIds, - Set existingReversedTransactionIds) { - existingTransactionIds.addAll(account.findExistingTransactionIds()); - existingReversedTransactionIds.addAll(account.findExistingReversedTransactionIds()); - } - - private void updateExistingTransactionsDetails(SavingsAccountData account, Set existingTransactionIds, - Set existingReversedTransactionIds) { - existingTransactionIds.addAll(account.findCurrentTransactionIdsWithPivotDateConfig()); - existingReversedTransactionIds.addAll(account.findCurrentReversedTransactionIdsWithPivotDateConfig()); - } - - private void updateSavingsTransactionsDetails(SavingsAccount account, Set existingTransactionIds, - Set existingReversedTransactionIds) { - existingTransactionIds.addAll(account.findCurrentTransactionIdsWithPivotDateConfig()); - existingReversedTransactionIds.addAll(account.findCurrentReversedTransactionIdsWithPivotDateConfig()); - } - - private void postJournalEntries(final SavingsAccount savingsAccount, final Set existingTransactionIds, - final Set existingReversedTransactionIds, final boolean backdatedTxnsAllowedTill) { - - boolean isAccountTransfer = false; - final Map accountingBridgeData = savingsAccount.deriveAccountingBridgeData(savingsAccount.getCurrency().getCode(), - existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill); - this.journalEntryWritePlatformService.createJournalEntriesForSavings(accountingBridgeData); - } - - @Override - public CommandProcessingResult inactivateCharge(final Long savingsAccountId, final Long savingsAccountChargeId) { - - this.context.authenticatedUser(); - - final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository - .findOneWithNotFoundDetection(savingsAccountChargeId, savingsAccountId); - - final SavingsAccount account = savingsAccountCharge.savingsAccount(); - this.savingAccountAssembler.assignSavingAccountHelpers(account); - - final LocalDate inactivationOnDate = DateUtils.getBusinessLocalDate(); - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) - .resource(SAVINGS_ACCOUNT_CHARGE_RESOURCE_NAME); - - // Only recurring fees are allowed to inactivate - if (!savingsAccountCharge.isRecurringFee()) { - baseDataValidator.reset().parameter(null).value(savingsAccountCharge.getId()) - .failWithCodeNoParameterAddedToErrorCode("charge.inactivation.allowed.only.for.recurring.charges"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - - } else { - final LocalDate nextDueDate = savingsAccountCharge.getNextDueDateFrom(inactivationOnDate); - - if (savingsAccountCharge.isChargeIsDue(nextDueDate)) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("inactivation.of.charge.not.allowed.when.charge.is.due"); - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } else if (savingsAccountCharge.isChargeIsOverPaid(nextDueDate)) { - - final List chargePayments = new ArrayList<>(); - SavingsAccountCharge updatedCharge = savingsAccountCharge; - do { - chargePayments.clear(); - for (SavingsAccountTransaction transaction : account.getTransactions()) { - if (transaction.isPayCharge() && transaction.isNotReversed() - && transaction.isPaymentForCurrentCharge(savingsAccountCharge)) { - chargePayments.add(transaction); - } - } - // Reverse the excess payments of charge transactions - SavingsAccountTransaction lastChargePayment = getLastChargePayment(chargePayments); - this.undoTransaction(savingsAccountCharge.savingsAccount().getId(), lastChargePayment.getId(), false); - updatedCharge = account.getUpdatedChargeDetails(savingsAccountCharge); - } while (updatedCharge.isChargeIsOverPaid(nextDueDate)); - } - account.inactivateCharge(savingsAccountCharge, inactivationOnDate); - } - - return new CommandProcessingResultBuilder() // - .withEntityId(savingsAccountCharge.getId()) // - .withOfficeId(savingsAccountCharge.savingsAccount().officeId()) // - .withClientId(savingsAccountCharge.savingsAccount().clientId()) // - .withGroupId(savingsAccountCharge.savingsAccount().groupId()) // - .withSavingsId(savingsAccountCharge.savingsAccount().getId()) // - .build(); - } - - private SavingsAccountTransaction getLastChargePayment(final List chargePayments) { - if (!CollectionUtils.isEmpty(chargePayments)) { - return chargePayments.get(chargePayments.size() - 1); - } - return null; - } - - @Transactional - @Override - public CommandProcessingResult assignFieldOfficer(Long savingsAccountId, JsonCommand command) { - this.context.authenticatedUser(); - final Map actualChanges = new LinkedHashMap<>(5); - - Staff fromSavingsOfficer = null; - Staff toSavingsOfficer = null; - this.fromApiJsonDeserializer.validateForAssignSavingsOfficer(command.json()); - - final SavingsAccount savingsForUpdate = this.savingAccountRepositoryWrapper.findOneWithNotFoundDetection(savingsAccountId); - final Long fromSavingsOfficerId = command.longValueOfParameterNamed("fromSavingsOfficerId"); - final Long toSavingsOfficerId = command.longValueOfParameterNamed("toSavingsOfficerId"); - final LocalDate dateOfSavingsOfficerAssignment = command.localDateValueOfParameterNamed("assignmentDate"); - - if (fromSavingsOfficerId != null) { - fromSavingsOfficer = this.staffRepository.findByOfficeHierarchyWithNotFoundDetection(fromSavingsOfficerId, - savingsForUpdate.office().getHierarchy()); - } - if (toSavingsOfficerId != null) { - toSavingsOfficer = this.staffRepository.findByOfficeHierarchyWithNotFoundDetection(toSavingsOfficerId, - savingsForUpdate.office().getHierarchy()); - actualChanges.put("toSavingsOfficerId", toSavingsOfficer.getId()); - } - if (!savingsForUpdate.hasSavingsOfficer(fromSavingsOfficer)) { - throw new SavingsOfficerAssignmentException(savingsAccountId, fromSavingsOfficerId); - } - - savingsForUpdate.reassignSavingsOfficer(toSavingsOfficer, dateOfSavingsOfficerAssignment); - - this.savingAccountRepositoryWrapper.saveAndFlush(savingsForUpdate); - - return new CommandProcessingResultBuilder() // - .withCommandId(command.commandId()) // - .withOfficeId(savingsForUpdate.officeId()) // - .withEntityId(savingsForUpdate.getId()) // - .withSavingsId(savingsAccountId) // - .with(actualChanges) // - .build(); - } - - @Transactional - @Override - public CommandProcessingResult unassignFieldOfficer(Long savingsAccountId, JsonCommand command) { - - this.context.authenticatedUser(); - - final Map actualChanges = new LinkedHashMap<>(5); - this.fromApiJsonDeserializer.validateForUnAssignSavingsOfficer(command.json()); - - final SavingsAccount savingsForUpdate = this.savingAccountRepositoryWrapper.findOneWithNotFoundDetection(savingsAccountId); - if (savingsForUpdate.getSavingsOfficer() == null) { - throw new SavingsOfficerUnassignmentException(savingsAccountId); - } - - final LocalDate dateOfSavingsOfficerUnassigned = command.localDateValueOfParameterNamed("unassignedDate"); - - savingsForUpdate.removeSavingsOfficer(dateOfSavingsOfficerUnassigned); - - this.savingAccountRepositoryWrapper.saveAndFlush(savingsForUpdate); - - actualChanges.put("toSavingsOfficerId", null); - - return new CommandProcessingResultBuilder() // - .withCommandId(command.commandId()) // - .withOfficeId(savingsForUpdate.officeId()) // - .withEntityId(savingsForUpdate.getId()) // - .withSavingsId(savingsAccountId) // - .with(actualChanges) // - .build(); - } - - @Override - public CommandProcessingResult modifyWithHoldTax(Long savingsAccountId, JsonCommand command) { - final Map actualChanges = new HashMap<>(1); - final SavingsAccount savingsForUpdate = this.savingAccountRepositoryWrapper.findOneWithNotFoundDetection(savingsAccountId); - if (command.isChangeInBooleanParameterNamed(withHoldTaxParamName, savingsForUpdate.withHoldTax())) { - final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(withHoldTaxParamName); - actualChanges.put(withHoldTaxParamName, newValue); - savingsForUpdate.setWithHoldTax(newValue); - if (savingsForUpdate.getTaxGroup() == null) { - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("account"); - baseDataValidator.reset().parameter(withHoldTaxParamName).failWithCode("not.supported"); - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - - return new CommandProcessingResultBuilder() // - .withCommandId(command.commandId()) // - .withEntityId(savingsAccountId) // - .withSavingsId(savingsAccountId) // - .with(actualChanges) // - .build(); - } - - @Override - public void setSubStatusInactive(Long savingsId) { - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - account.setSubStatusInactive(false); - this.savingAccountRepositoryWrapper.saveAndFlush(account); - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false); - } - - @Override - public void setSubStatusDormant(Long savingsId) { - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); - account.setSubStatusDormant(); - this.savingAccountRepositoryWrapper.saveAndFlush(account); - } - - @Override - public void escheat(Long savingsId) { - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); - final Set existingTransactionIds = new HashSet<>(); - final Set existingReversedTransactionIds = new HashSet<>(); - updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); - account.escheat(appuserRepository.fetchSystemUser()); - this.savingAccountRepositoryWrapper.saveAndFlush(account); - postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false); - } - - private AppUser getAppUserIfPresent() { - AppUser user = null; - if (this.context != null) { - user = this.context.getAuthenticatedUserIfPresent(); - } - return user; - } - - /** - * Disable all standing instructions linked to the savings account if the status is "closed" - * - * @param savingsAccount - * -- the savings account object - * - **/ - @Transactional - private void disableStandingInstructionsLinkedToClosedSavings(final SavingsAccount savingsAccount) { - if (savingsAccount != null && savingsAccount.isClosed()) { - final Integer standingInstructionStatus = StandingInstructionStatus.ACTIVE.getValue(); - final Collection accountTransferStandingInstructions = this.standingInstructionRepository - .findBySavingsAccountAndStatus(savingsAccount, standingInstructionStatus); - - if (!accountTransferStandingInstructions.isEmpty()) { - for (AccountTransferStandingInstruction accountTransferStandingInstruction : accountTransferStandingInstructions) { - accountTransferStandingInstruction.updateStatus(StandingInstructionStatus.DISABLED.getValue()); - this.standingInstructionRepository.save(accountTransferStandingInstruction); - } - } - } - } - - @Override - public CommandProcessingResult blockAccount(final Long savingsId, final JsonCommand command) { - - this.context.authenticatedUser(); - - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); - checkClientOrGroupActive(account); - - final Map changes = account.block(); - - final String reasonForBlock = command.stringValueOfParameterNamed(SavingsApiConstants.reasonForBlockParamName); - validateReasonForHold(reasonForBlock); - account.updateReason(reasonForBlock); - - if (!changes.isEmpty()) { - - this.savingAccountRepositoryWrapper.save(account); - } - return new CommandProcessingResultBuilder().withEntityId(savingsId).withOfficeId(account.officeId()) - .withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).with(changes).build(); - } - - @Override - public CommandProcessingResult unblockAccount(final Long savingsId) { - this.context.authenticatedUser(); - - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); - checkClientOrGroupActive(account); - - final Map changes = account.unblock(); - - account.updateReason(null); - - if (!changes.isEmpty()) { - - this.savingAccountRepositoryWrapper.save(account); - } - return new CommandProcessingResultBuilder().withEntityId(savingsId).withOfficeId(account.officeId()) - .withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).with(changes).build(); - } - - @Transactional - @Override - public CommandProcessingResult holdAmount(final Long savingsId, final JsonCommand command) { - - final AppUser submittedBy = this.context.authenticatedUser(); - final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill); - final LocalDate transactionDate = command.localDateValueOfParameterNamed(transactionDateParamName); - final boolean lienAllowed = command.booleanPrimitiveValueOfParameterNamed(lienAllowedParamName); - - checkClientOrGroupActive(account); - - final BigDecimal amount = command.bigDecimalValueOfParameterNamed(transactionAmountParamName); - - Money runningBalance = Money.of(account.getCurrency(), account.getAccountBalance()); - if (account.getSavingsHoldAmount() != null) { - runningBalance = runningBalance.minus(account.getSavingsHoldAmount()).minus(amount); - } else { - runningBalance = runningBalance.minus(amount); - } - - this.savingsAccountTransactionDataValidator.validateHoldAndAssembleForm(command.json(), account, submittedBy, - backdatedTxnsAllowedTill); - SavingsAccountTransaction transaction = this.savingsAccountDomainService.handleHold(account, amount, transactionDate, lienAllowed); - account.holdAmount(amount); - transaction.setRunningBalance(runningBalance); - - final String reasonForBlock = command.stringValueOfParameterNamed(SavingsApiConstants.reasonForBlockParamName); - transaction.updateReason(reasonForBlock); - - account.getAccountBalance(); - this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transaction.getTransactionDate(), account); - - this.savingsAccountTransactionRepository.saveAndFlush(transaction); - - if (backdatedTxnsAllowedTill) { - // Check again whether transactions are modified - this.savingsAccountTransactionRepository.saveAll(account.getSavingsAccountTransactionsWithPivotConfig()); - } - - this.savingAccountRepositoryWrapper.saveAndFlush(account); - - return new CommandProcessingResultBuilder().withEntityId(transaction.getId()).withOfficeId(account.officeId()) - .withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).build(); - } - - @Transactional - @Override - public CommandProcessingResult releaseAmount(final Long savingsId, final Long savingsTransactionId) { - context.authenticatedUser(); - SavingsAccountTransaction holdTransaction = this.savingsAccountTransactionRepository - .findOneByIdAndSavingsAccountId(savingsTransactionId, savingsId); - - holdTransaction.updateReason(null); - - final SavingsAccountTransaction transaction = this.savingsAccountTransactionDataValidator - .validateReleaseAmountAndAssembleForm(holdTransaction); - - final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill); - checkClientOrGroupActive(account); - - Money runningBalance = Money.of(account.getCurrency(), account.getAccountBalance()); - - Money savingsOnHold = Money.of(account.getCurrency(), account.getSavingsHoldAmount()); - - runningBalance = runningBalance.minus(savingsOnHold); - - runningBalance = runningBalance.plus(transaction.getAmount()); - transaction.setRunningBalance(runningBalance); - - this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transaction.getTransactionDate(), account); - account.releaseOnHoldAmount(transaction.getAmount()); - - this.savingsAccountTransactionRepository.saveAndFlush(transaction); - holdTransaction.updateReleaseId(transaction.getId()); - - if (backdatedTxnsAllowedTill) { - this.savingsAccountTransactionRepository.saveAll(account.getSavingsAccountTransactionsWithPivotConfig()); - } else { - account.addTransaction(transaction); - } - - this.savingAccountRepositoryWrapper.save(account); - - return new CommandProcessingResultBuilder().withEntityId(transaction.getId()).withOfficeId(account.officeId()) - .withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(account.getId()).build(); - } - - @Transactional - @Override - public CommandProcessingResult blockCredits(final Long savingsId, final JsonCommand command) { - this.context.authenticatedUser(); - - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); - checkClientOrGroupActive(account); - - final String reasonForBlock = command.stringValueOfParameterNamed(SavingsApiConstants.reasonForBlockParamName); - validateReasonForHold(reasonForBlock); - account.updateReason(reasonForBlock); - - final Map changes = account.blockCredits(account.getSubStatus()); - if (!changes.isEmpty()) { - - this.savingAccountRepositoryWrapper.save(account); - } - return new CommandProcessingResultBuilder().withEntityId(savingsId).withOfficeId(account.officeId()) - .withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).with(changes).build(); - } - - @Transactional - @Override - public CommandProcessingResult unblockCredits(final Long savingsId) { - this.context.authenticatedUser(); - - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); - checkClientOrGroupActive(account); - account.updateReason(null); - final Map changes = account.unblockCredits(); - if (!changes.isEmpty()) { - - this.savingAccountRepositoryWrapper.save(account); - } - return new CommandProcessingResultBuilder().withEntityId(savingsId).withOfficeId(account.officeId()) - .withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).with(changes).build(); - } - - @Transactional - @Override - public CommandProcessingResult blockDebits(final Long savingsId, final JsonCommand command) { - this.context.authenticatedUser(); - - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); - checkClientOrGroupActive(account); - - final String reasonForBlock = command.stringValueOfParameterNamed(SavingsApiConstants.reasonForBlockParamName); - validateReasonForHold(reasonForBlock); - account.updateReason(reasonForBlock); - - final Map changes = account.blockDebits(account.getSubStatus()); - if (!changes.isEmpty()) { - - this.savingAccountRepositoryWrapper.save(account); - } - return new CommandProcessingResultBuilder().withEntityId(savingsId).withOfficeId(account.officeId()) - .withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).with(changes).build(); - } - - @Transactional - @Override - public CommandProcessingResult unblockDebits(final Long savingsId) { - this.context.authenticatedUser(); - - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); - checkClientOrGroupActive(account); - - account.updateReason(null); - - final Map changes = account.unblockDebits(); - if (!changes.isEmpty()) { - - this.savingAccountRepositoryWrapper.save(account); - } - return new CommandProcessingResultBuilder().withEntityId(savingsId).withOfficeId(account.officeId()) - .withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).with(changes).build(); - } - - private void validateTransactionsForTransfer(final SavingsAccount savingsAccount, final LocalDate transferDate) { - for (SavingsAccountTransaction transaction : savingsAccount.getTransactions()) { - if ((DateUtils.isEqual(transferDate, transaction.getTransactionDate()) - && DateUtils.isEqual(transferDate, transaction.getSubmittedOnDate())) - || DateUtils.isBefore(transferDate, transaction.getTransactionDate())) { - throw new GeneralPlatformDomainRuleException(TransferApiConstants.transferClientSavingsException, - TransferApiConstants.transferClientSavingsException, transaction.getTransactionDate(), transferDate); - } - } - } - - private void validateReasonForHold(String reasonForBlock) { - if (StringUtils.isBlank(reasonForBlock)) { - throw new PlatformDataIntegrityException("Reason For Block is Mandatory", "error.msg.reason.for.block.mandatory"); - } - } -} +/** + * 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.SavingsApiConstants.SAVINGS_ACCOUNT_CHARGE_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 static org.apache.fineract.portfolio.savings.SavingsApiConstants.lienAllowedParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.transactionAmountParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.transactionDateParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.withHoldTaxParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.withdrawBalanceParamName; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import io.github.resilience4j.retry.annotation.Retry; +import java.math.BigDecimal; +import java.math.MathContext; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +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.exception.ErrorHandler; +import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; +import org.apache.fineract.infrastructure.core.exception.PlatformServiceUnavailableException; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.MathUtil; +import org.apache.fineract.infrastructure.dataqueries.data.EntityTables; +import org.apache.fineract.infrastructure.dataqueries.data.StatusEnum; +import org.apache.fineract.infrastructure.dataqueries.service.EntityDatatableChecksWritePlatformService; +import org.apache.fineract.infrastructure.event.business.domain.savings.SavingsActivateBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.savings.SavingsCloseBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.savings.SavingsPostInterestBusinessEvent; +import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.organisation.holiday.domain.HolidayRepositoryWrapper; +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.staff.domain.StaffRepositoryWrapper; +import org.apache.fineract.organisation.workingdays.domain.WorkingDaysRepositoryWrapper; +import org.apache.fineract.portfolio.account.PortfolioAccountType; +import org.apache.fineract.portfolio.account.domain.AccountTransferStandingInstruction; +import org.apache.fineract.portfolio.account.domain.StandingInstructionRepository; +import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus; +import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformService; +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.charge.domain.ChargeTimeType; +import org.apache.fineract.portfolio.client.domain.Client; +import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; +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.service.FacilityChargeAutoApplyService; +import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType; +import org.apache.fineract.portfolio.savings.SavingsApiConstants; +import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues; +import org.apache.fineract.portfolio.savings.data.SavingsAccountChargeDataValidator; +import org.apache.fineract.portfolio.savings.data.SavingsAccountData; +import org.apache.fineract.portfolio.savings.data.SavingsAccountDataValidator; +import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionDTO; +import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; +import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionDataValidator; +import org.apache.fineract.portfolio.savings.domain.DepositAccountOnHoldTransaction; +import org.apache.fineract.portfolio.savings.domain.DepositAccountOnHoldTransactionRepository; +import org.apache.fineract.portfolio.savings.domain.GSIMRepositoy; +import org.apache.fineract.portfolio.savings.domain.GroupSavingsIndividualMonitoring; +import org.apache.fineract.portfolio.savings.domain.SavingsAccount; +import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; +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.PostInterestAsOnDateException; +import org.apache.fineract.portfolio.savings.exception.PostInterestAsOnDateException.PostInterestAsOnExceptionType; +import org.apache.fineract.portfolio.savings.exception.PostInterestClosingDateException; +import org.apache.fineract.portfolio.savings.exception.SavingsAccountClosingNotAllowedException; +import org.apache.fineract.portfolio.savings.exception.SavingsAccountTransactionNotFoundException; +import org.apache.fineract.portfolio.savings.exception.SavingsOfficerAssignmentException; +import org.apache.fineract.portfolio.savings.exception.SavingsOfficerUnassignmentException; +import org.apache.fineract.portfolio.savings.exception.TransactionUpdateNotAllowedException; +import org.apache.fineract.portfolio.transfer.api.TransferApiConstants; +import org.apache.fineract.useradministration.domain.AppUser; +import org.apache.fineract.useradministration.domain.AppUserRepositoryWrapper; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + +@Slf4j +@RequiredArgsConstructor +public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements SavingsAccountWritePlatformService { + + private final PlatformSecurityContext context; + private final SavingsAccountDataValidator fromApiJsonDeserializer; + private final SavingsAccountRepositoryWrapper savingAccountRepositoryWrapper; + private final StaffRepositoryWrapper staffRepository; + private final SavingsAccountTransactionRepository savingsAccountTransactionRepository; + private final SavingsAccountAssembler savingAccountAssembler; + private final SavingsAccountTransactionDataValidator savingsAccountTransactionDataValidator; + private final SavingsAccountChargeDataValidator savingsAccountChargeDataValidator; + private final PaymentDetailWritePlatformService paymentDetailWritePlatformService; + private final JournalEntryWritePlatformService journalEntryWritePlatformService; + private final SavingsAccountDomainService savingsAccountDomainService; + private final NoteRepository noteRepository; + private final AccountTransfersReadPlatformService accountTransfersReadPlatformService; + private final AccountAssociationsReadPlatformService accountAssociationsReadPlatformService; + private final ChargeRepositoryWrapper chargeRepository; + private final SavingsAccountChargeRepositoryWrapper savingsAccountChargeRepository; + private final HolidayRepositoryWrapper holidayRepository; + private final WorkingDaysRepositoryWrapper workingDaysRepository; + private final ConfigurationDomainService configurationDomainService; + private final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository; + private final EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService; + private final AppUserRepositoryWrapper appuserRepository; + private final StandingInstructionRepository standingInstructionRepository; + private final BusinessEventNotifierService businessEventNotifierService; + private final GSIMRepositoy gsimRepository; + private final SavingsAccountInterestPostingService savingsAccountInterestPostingService; + private final ErrorHandler errorHandler; + private final FacilityChargeAutoApplyService facilityChargeAutoApplyService; + private final SavingsTxnDenominationRepository savingsTxnDenominationRepository; + + @Transactional + @Override + public CommandProcessingResult gsimActivate(final Long gsimId, final JsonCommand command) { + + Long parentSavingId = gsimId; + GroupSavingsIndividualMonitoring parentSavings = gsimRepository.findById(parentSavingId).orElseThrow(); + List childSavings = this.savingAccountRepositoryWrapper.findByGsimId(gsimId); + + CommandProcessingResult result = null; + int count = 0; + for (SavingsAccount account : childSavings) { + result = activate(account.getId(), command); + if (result != null) { + count++; + if (count == parentSavings.getChildAccountsCount()) { + parentSavings.setSavingsStatus(SavingsAccountStatusType.ACTIVE.getValue()); + gsimRepository.save(parentSavings); + } + } + } + return result; + } + + @Transactional + @Override + public CommandProcessingResult activate(final Long savingsId, final JsonCommand command) { + + final AppUser user = this.context.authenticatedUser(); + + this.savingsAccountTransactionDataValidator.validateActivation(command); + + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); + + // Validate minimum opening balance + BigDecimal openingBalanceForValidation = BigDecimal.ZERO; + if (command.parameterExists("openingBalance")) { + final BigDecimal ob = command.bigDecimalValueOfParameterNamed("openingBalance"); + if (ob != null) openingBalanceForValidation = ob; + } + final BigDecimal minRequiredOpening = account.getMinRequiredOpeningBalance(); + if (minRequiredOpening != null && minRequiredOpening.compareTo(BigDecimal.ZERO) > 0 + && openingBalanceForValidation.compareTo(minRequiredOpening) < 0) { + final List errors = new ArrayList<>(); + errors.add(ApiParameterError.parameterError( + "openingBalance", + "Opening balance (" + openingBalanceForValidation + ") is less than minimum required opening balance (" + minRequiredOpening + ")", + "validation.msg.savingsaccount.openingBalance.less.than.min.required", + openingBalanceForValidation, minRequiredOpening)); + throw new PlatformApiDataValidationException(errors); + } + + checkClientOrGroupActive(account); + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + + final Map changes = account.activate(user, command); + + entityDatatableChecksWritePlatformService.runTheCheckForProduct(savingsId, EntityTables.SAVINGS.getName(), + StatusEnum.ACTIVATE.getValue(), EntityTables.SAVINGS.getForeignKeyColumnNameOnDatatable(), account.productId()); + + if (!changes.isEmpty()) { + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); + processPostActiveActions(account, fmt, existingTransactionIds, existingReversedTransactionIds); + // Auto-apply facility charges based on product facility config + facilityChargeAutoApplyService.applyFacilityChargesOnActivation(account); + this.savingAccountRepositoryWrapper.saveAndFlush(account); + // Collect any newly added facility activation charges (after flush so amountOutstanding is set) + // payCharge() internally adds transactions to account.transactions via handleChargeTransactions + // Do NOT add to existingTransactionIds — postJournalEntries must include them for GL posting + facilityChargeAutoApplyService.collectActivationCharges(account); + this.savingAccountRepositoryWrapper.saveAndFlush(account); + } + + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false); + businessEventNotifierService.notifyPostBusinessEvent(new SavingsActivateBusinessEvent(account)); + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .with(changes) // + .build(); + } + + @Override + public void processPostActiveActions(final SavingsAccount account, final DateTimeFormatter fmt, final Set existingTransactionIds, + final Set existingReversedTransactionIds) { + getAppUserIfPresent(); + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + + Money amountForDeposit = account.activateWithBalance(); + boolean isRegularTransaction = false; + if (amountForDeposit.isGreaterThanZero()) { + boolean isAccountTransfer = false; + this.savingsAccountDomainService.handleDeposit(account, fmt, account.getActivationDate(), amountForDeposit.getAmount(), null, + isAccountTransfer, isRegularTransaction, false); + + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + } + + account.processAccountUponActivation(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); + } + + @Transactional + @Override + public CommandProcessingResult gsimDeposit(final Long gsimId, final JsonCommand command) { + JsonArray savingsArray = command.arrayOfParameterNamed("savingsArray"); + + CommandProcessingResult result = null; + for (JsonElement element : savingsArray) { + result = deposit(element.getAsJsonObject().get("childAccountId").getAsLong(), + JsonCommand.fromExistingCommand(command, element)); + } + return result; + } + + @Transactional + @Override + public CommandProcessingResult deposit(final Long savingsId, final JsonCommand command) { + this.context.authenticatedUser(); + + this.savingsAccountTransactionDataValidator.validate(command); + boolean isGsim = false; + + final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); + + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill); + + if (account.getGsim() != null) { + isGsim = true; + log.debug("is gsim"); + } + 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"); + + this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transactionDate, account); + + final Map changes = new LinkedHashMap<>(); + final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); + boolean isAccountTransfer = false; + boolean isRegularTransaction = true; + 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); + } + } + } + + if (isGsim && (deposit.getId() != null)) { + + log.debug("Deposit account has been created: {} ", deposit); + + GroupSavingsIndividualMonitoring gsim = gsimRepository.findById(account.getGsim().getId()).orElseThrow(); + log.debug("parent deposit : {} ", gsim.getParentDeposit()); + log.debug("child account : {} ", savingsId); + BigDecimal currentBalance = gsim.getParentDeposit(); + BigDecimal newBalance = currentBalance.add(transactionAmount); + gsim.setParentDeposit(newBalance); + gsimRepository.save(gsim); + log.debug("balance after making deposit : {} ", + gsimRepository.findById(account.getGsim().getId()).orElseThrow().getParentDeposit()); + + } + + final String noteText = command.stringValueOfParameterNamed("note"); + if (StringUtils.isNotBlank(noteText)) { + final Note note = Note.savingsTransactionNote(account, deposit, noteText); + this.noteRepository.save(note); + } + + 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) { + + this.savingsAccountTransactionDataValidator.validate(command); + + boolean isGsim = false; + + 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 boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); + + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill); + + if (account.getGsim() != null) { + isGsim = true; + } + checkClientOrGroupActive(account); + + this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transactionDate, account); + + final boolean isAccountTransfer = false; + final boolean isRegularTransaction = true; + final boolean isApplyWithdrawFee = true; + final boolean isInterestTransfer = false; + final boolean isWithdrawBalance = false; + final SavingsTransactionBooleanValues transactionBooleanValues = new SavingsTransactionBooleanValues(isAccountTransfer, + isRegularTransaction, isApplyWithdrawFee, isInterestTransfer, isWithdrawBalance); + final SavingsAccountTransaction withdrawal = this.savingsAccountDomainService.handleWithdrawal(account, fmt, transactionDate, + transactionAmount, paymentDetail, transactionBooleanValues, backdatedTxnsAllowedTill); + + if (isGsim && (withdrawal.getId() != null)) { + GroupSavingsIndividualMonitoring gsim = gsimRepository.findById(account.getGsim().getId()).orElseThrow(); + BigDecimal currentBalance = gsim.getParentDeposit().subtract(transactionAmount); + gsim.setParentDeposit(currentBalance); + gsimRepository.save(gsim); + + } + + final String noteText = command.stringValueOfParameterNamed("note"); + if (StringUtils.isNotBlank(noteText)) { + final Note note = Note.savingsTransactionNote(account, withdrawal, noteText); + this.noteRepository.save(note); + } + + return new CommandProcessingResultBuilder() // + .withEntityId(withdrawal.getId()) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .with(changes)// + .build(); + } + + @Transactional + @Override + public CommandProcessingResult applyAnnualFee(final Long savingsAccountChargeId, final Long accountId) { + getAppUserIfPresent(); + + final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository + .findOneWithNotFoundDetection(savingsAccountChargeId, accountId); + + final LocalDate currentDate = DateUtils.getBusinessLocalDate(); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd MM yyyy").withZone(DateUtils.getDateTimeZoneOfTenant()); + + while (DateUtils.isBefore(savingsAccountCharge.getDueDate(), currentDate)) { + this.payCharge(savingsAccountCharge, savingsAccountCharge.getDueDate(), savingsAccountCharge.amount(), fmt, false); + } + + 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 CommandProcessingResult calculateInterest(final Long savingsId) { + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + + final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill); + checkClientOrGroupActive(account); + + final LocalDate today = DateUtils.getBusinessLocalDate(); + final MathContext mc = new MathContext(15, MoneyHelper.getRoundingMode()); + boolean isInterestTransfer = false; + final LocalDate postInterestOnDate = null; + boolean postReversals = false; + account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals); + + if (backdatedTxnsAllowedTill) { + this.savingsAccountTransactionRepository.saveAll(account.getSavingsAccountTransactionsWithPivotConfig()); + } + + this.savingAccountRepositoryWrapper.save(account); + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .build(); + } + + @Override + @Transactional + public CommandProcessingResult postInterest(final JsonCommand command) { + Long savingsId = command.getSavingsId(); + final boolean postInterestAs = command.booleanPrimitiveValueOfParameterNamed("isPostInterestAsOn"); + final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); + + final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill); + checkClientOrGroupActive(account); + + this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transactionDate, account); + + if (postInterestAs) { + if (transactionDate == null) { + throw new PostInterestAsOnDateException(PostInterestAsOnExceptionType.VALID_DATE); + } + if (DateUtils.isBefore(transactionDate, account.accountSubmittedOrActivationDate())) { + throw new PostInterestAsOnDateException(PostInterestAsOnExceptionType.ACTIVATION_DATE); + } + + List savingTransactions = null; + if (backdatedTxnsAllowedTill) { + savingTransactions = account.getSavingsAccountTransactionsWithPivotConfig(); + } else { + savingTransactions = account.getTransactions(); + } + + for (SavingsAccountTransaction savingTransaction : savingTransactions) { + if (DateUtils.isBefore(transactionDate, savingTransaction.getDateOf())) { + throw new PostInterestAsOnDateException(PostInterestAsOnExceptionType.LAST_TRANSACTION_DATE); + } + } + + if (DateUtils.isDateInTheFuture(transactionDate)) { + throw new PostInterestAsOnDateException(PostInterestAsOnExceptionType.FUTURE_DATE); + } + } + postInterest(account, postInterestAs, transactionDate, backdatedTxnsAllowedTill); + + businessEventNotifierService.notifyPostBusinessEvent(new SavingsPostInterestBusinessEvent(account)); + return new CommandProcessingResultBuilder() // + .withEntityId(savingsId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .build(); + } + + @Transactional + @Override + public void postInterest(final SavingsAccount account, final boolean postInterestAs, final LocalDate transactionDate, + final boolean backdatedTxnsAllowedTill) { + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + + if (account.getNominalAnnualInterestRate().compareTo(BigDecimal.ZERO) > 0 + || (account.allowOverdraft() && account.getNominalAnnualInterestRateOverdraft().compareTo(BigDecimal.ZERO) > 0)) { + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + + if (backdatedTxnsAllowedTill) { + updateSavingsTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + } else { + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + } + + final LocalDate today = DateUtils.getBusinessLocalDate(); + final MathContext mc = new MathContext(10, MoneyHelper.getRoundingMode()); + boolean isInterestTransfer = false; + LocalDate postInterestOnDate = null; + if (postInterestAs) { + postInterestOnDate = transactionDate; + } + boolean postReversals = false; + account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, + postInterestOnDate, backdatedTxnsAllowedTill, postReversals); + + if (!backdatedTxnsAllowedTill) { + List transactions = account.getTransactions(); + for (SavingsAccountTransaction accountTransaction : transactions) { + if (accountTransaction.getId() == null) { + this.savingsAccountTransactionRepository.save(accountTransaction); + } + } + } + + if (backdatedTxnsAllowedTill) { + this.savingsAccountTransactionRepository.saveAll(account.getSavingsAccountTransactionsWithPivotConfig()); + } + + this.savingAccountRepositoryWrapper.saveAndFlush(account); + + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, backdatedTxnsAllowedTill); + } + } + + @Transactional + @Override + @Retry(name = "postInterest", fallbackMethod = "fallbackPostInterest") + public SavingsAccountData postInterest(SavingsAccountData savingsAccountData, final boolean postInterestAs, + final LocalDate transactionDate, final boolean backdatedTxnsAllowedTill) { + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + + if (MathUtil.isGreaterThanZero(savingsAccountData.getNominalAnnualInterestRate()) || (savingsAccountData.isAllowOverdraft() + && MathUtil.isGreaterThanZero(savingsAccountData.getNominalAnnualInterestRateOverdraft()))) { + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(savingsAccountData, existingTransactionIds, existingReversedTransactionIds); + + final LocalDate today = DateUtils.getBusinessLocalDate(); + final MathContext mc = new MathContext(10, MoneyHelper.getRoundingMode()); + boolean isInterestTransfer = false; + LocalDate postInterestOnDate = null; + if (postInterestAs) { + postInterestOnDate = transactionDate; + } + + savingsAccountData = this.savingsAccountInterestPostingService.postInterest(mc, today, isInterestTransfer, + isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, + savingsAccountData); + + if (!backdatedTxnsAllowedTill) { + List transactions = savingsAccountData.getSavingsAccountTransactionData(); + for (SavingsAccountTransactionData accountTransaction : transactions) { + if (accountTransaction.getId() == null) { + savingsAccountData.setNewSavingsAccountTransactionData(accountTransaction); + selectAccountId(accountTransaction, savingsAccountData); + } + } + } + savingsAccountData.setExistingTransactionIds(existingTransactionIds); + savingsAccountData.setExistingReversedTransactionIds(existingReversedTransactionIds); + } + return savingsAccountData; + } + + public void selectAccountId(SavingsAccountTransactionData accountTransaction, SavingsAccountData savingsAccountData) { + SavingsAccountTransactionType transactionType = SavingsAccountTransactionType + .fromInt(accountTransaction.getTransactionType().getId().intValue()); + if (transactionType.isOverDraftInterestPosting()) { + if (MathUtil.isGreaterThanZero(accountTransaction.getRunningBalance())) { + accountTransaction.setAccountDebit(savingsAccountData.getGlAccountIdForSavingsControl()); + accountTransaction.setAccountCredit(savingsAccountData.getGlAccountIdForInterestReceivable()); + } else { + accountTransaction.setAccountDebit(savingsAccountData.getGlAccountIdForOverdraftPorfolio()); + accountTransaction.setAccountCredit(savingsAccountData.getGlAccountIdForInterestReceivable()); + } + } else { + accountTransaction.setAccountDebit(savingsAccountData.getGlAccountIdForInterestPayable()); + accountTransaction.setAccountCredit(savingsAccountData.getGlAccountIdForSavingsControl()); + } + } + + @Override + public CommandProcessingResult reverseTransaction(final Long savingsId, final Long transactionId, + final boolean allowAccountTransferModification, final JsonCommand command) { + + final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); + final boolean isBulk = command.booleanPrimitiveValueOfParameterNamed("isBulk"); + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill); + + 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.saving.account.transfer.transaction.update.not.allowed", + "Savings account transaction:" + transactionId + " update not allowed as it involves in account transfer", + transactionId); + } + + if (!account.allowModify()) { + throw new PlatformServiceUnavailableException("error.msg.saving.account.transaction.update.not.allowed", + "Savings account transaction:" + transactionId + " update not allowed for this savings type", transactionId); + } + if (account.isNotActive()) { + throwValidationForActiveStatus(SavingsApiConstants.undoTransactionAction); + } + SavingsAccountTransaction reversal = null; + Long reversalId = null; + checkClientOrGroupActive(account); + List savingsAccountTransactions = null; + if (isBulk) { + String transactionRefNo = savingsAccountTransaction.getRefNo(); + savingsAccountTransactions = this.savingsAccountTransactionRepository.findByRefNo(transactionRefNo); + reversal = this.savingsAccountDomainService.handleReversal(account, savingsAccountTransactions, backdatedTxnsAllowedTill); + } else { + reversal = this.savingsAccountDomainService.handleReversal(account, Collections.singletonList(savingsAccountTransaction), + backdatedTxnsAllowedTill); + } + reversalId = reversal.getId(); + return new CommandProcessingResultBuilder() // + .withEntityId(reversalId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .build(); + } + + @Override + public CommandProcessingResult undoTransaction(final Long savingsId, final Long transactionId, + final boolean allowAccountTransferModification) { + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); + 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); + } + + this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(savingsAccountTransaction.getTransactionDate(), + account); + + if (!allowAccountTransferModification + && this.accountTransfersReadPlatformService.isAccountTransfer(transactionId, PortfolioAccountType.SAVINGS)) { + throw new PlatformServiceUnavailableException("error.msg.saving.account.transfer.transaction.update.not.allowed", + "Savings account transaction:" + transactionId + " update not allowed as it involves in account transfer", + transactionId); + } + + if (!account.allowModify()) { + throw new PlatformServiceUnavailableException("error.msg.saving.account.transaction.update.not.allowed", + "Savings account transaction:" + transactionId + " update not allowed for this savings type", transactionId); + } + + final LocalDate today = DateUtils.getBusinessLocalDate(); + final MathContext mc = new MathContext(15, MoneyHelper.getRoundingMode()); + + if (account.isNotActive()) { + throwValidationForActiveStatus(SavingsApiConstants.undoTransactionAction); + } + account.undoTransaction(transactionId); + + // undoing transaction is withdrawal then undo withdrawal fee + // transaction if any + if (savingsAccountTransaction.isWithdrawal()) { + final SavingsAccountTransaction nextSavingsAccountTransaction = this.savingsAccountTransactionRepository + .findOneByIdAndSavingsAccountId(transactionId + 1, savingsId); + if (nextSavingsAccountTransaction != null && nextSavingsAccountTransaction.isWithdrawalFeeAndNotReversed()) { + account.undoTransaction(transactionId + 1); + } + } + boolean isInterestTransfer = false; + LocalDate postInterestOnDate = null; + boolean postReversals = false; + checkClientOrGroupActive(account); + 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(); + this.savingAccountRepositoryWrapper.saveAndFlush(account); + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false); + return new CommandProcessingResultBuilder() // + .withEntityId(savingsId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .build(); + } + + @Override + public CommandProcessingResult adjustSavingsTransaction(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(); + 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", + "Savings account transaction:" + transactionId + " update not allowed as it involves in account transfer", + transactionId); + } + + this.savingsAccountTransactionDataValidator.validate(command); + + final LocalDate today = DateUtils.getBusinessLocalDate(); + + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); + + if (account.isNotActive()) { + throwValidationForActiveStatus(SavingsApiConstants.adjustTransactionAction); + } + if (!account.allowModify()) { + throw new PlatformServiceUnavailableException("error.msg.saving.account.transaction.update.not.allowed", + "Savings account transaction:" + transactionId + " update not allowed for this savings type", transactionId); + } + 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); + + // for undo withdrawal fee + final SavingsAccountTransaction nextSavingsAccountTransaction = this.savingsAccountTransactionRepository + .findOneByIdAndSavingsAccountId(transactionId + 1, savingsId); + if (nextSavingsAccountTransaction != null && nextSavingsAccountTransaction.isWithdrawalFeeAndNotReversed()) { + account.undoTransaction(transactionId + 1); + } + + SavingsAccountTransaction transaction = null; + boolean isInterestTransfer = false; + Integer accountType = null; + final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount, + paymentDetail, null, accountType); + UUID refNo = UUID.randomUUID(); + if (savingsAccountTransaction.isDeposit()) { + transaction = account.deposit(transactionDTO, false, relaxingDaysConfigForPivotDate, refNo.toString()); + } else { + transaction = account.withdraw(transactionDTO, true, false, relaxingDaysConfigForPivotDate, refNo.toString()); + } + final Long newtransactionId = saveTransactionToGenerateTransactionId(transaction); + final LocalDate postInterestOnDate = null; + 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(); + + this.savingAccountRepositoryWrapper.saveAndFlush(account); + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false); + 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) { + if (client.isNotActive()) { + throw new ClientNotActiveException(client.getId()); + } + } + final Group group = account.group(); + if (group != null) { + if (group.isNotActive()) { + throw new GroupNotActiveException(group.getId()); + } + } + } + + @Override + public CommandProcessingResult bulkGSIMClose(final Long gsimId, final JsonCommand command) { + + final Long parentSavingId = gsimId; + GroupSavingsIndividualMonitoring parentSavings = gsimRepository.findById(parentSavingId).orElseThrow(); + List childSavings = this.savingAccountRepositoryWrapper.findByGsimId(gsimId); + + CommandProcessingResult result = null; + int count = 0; + for (SavingsAccount account : childSavings) { + result = close(account.getId(), command); + + if (result != null) { + count++; + if (count == parentSavings.getChildAccountsCount()) { + parentSavings.setSavingsStatus(SavingsAccountStatusType.CLOSED.getValue()); + gsimRepository.save(parentSavings); + } + } + } + return result; + } + + @Override + public CommandProcessingResult close(final Long savingsId, final JsonCommand command) { + final AppUser user = this.context.authenticatedUser(); + + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); + this.savingsAccountTransactionDataValidator.validateClosing(command, account); + + final boolean isLinkedWithAnyActiveLoan = this.accountAssociationsReadPlatformService.isLinkedWithAnyActiveAccount(savingsId); + + if (isLinkedWithAnyActiveLoan) { + final String defaultUserMessage = "Closing savings account with id:" + savingsId + + " is not allowed, since it is linked with one of the active accounts"; + throw new SavingsAccountClosingNotAllowedException("linked", defaultUserMessage, savingsId); + } + + entityDatatableChecksWritePlatformService.runTheCheckForProduct(savingsId, EntityTables.SAVINGS.getName(), + StatusEnum.CLOSE.getValue(), EntityTables.SAVINGS.getForeignKeyColumnNameOnDatatable(), account.productId()); + + final boolean isWithdrawBalance = command.booleanPrimitiveValueOfParameterNamed(withdrawBalanceParamName); + + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); + final LocalDate closedDate = command.localDateValueOfParameterNamed(SavingsApiConstants.closedOnDateParamName); + final boolean isPostInterest = command.booleanPrimitiveValueOfParameterNamed(SavingsApiConstants.postInterestValidationOnClosure); + // postInterest(account,closedDate,flag); + if (isPostInterest) { + boolean postInterestOnClosingDate = false; + List savingTransactions = account.getTransactions(); + for (SavingsAccountTransaction savingTransaction : savingTransactions) { + if (savingTransaction.isInterestPosting() && savingTransaction.isNotReversed() + && DateUtils.isEqual(closedDate, savingTransaction.getTransactionDate())) { + postInterestOnClosingDate = true; + break; + } + } + if (!postInterestOnClosingDate) { + throw new PostInterestClosingDateException(); + } + } + + final Map changes = new LinkedHashMap<>(); + + if (isWithdrawBalance && account.getSummary().getAccountBalance(account.getCurrency()).isGreaterThanZero()) { + + final BigDecimal transactionAmount = account.getSummary().getAccountBalance(); + + final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); + + final boolean isAccountTransfer = false; + final boolean isRegularTransaction = true; + final boolean isApplyWithdrawFee = false; + final boolean isInterestTransfer = false; + final SavingsTransactionBooleanValues transactionBooleanValues = new SavingsTransactionBooleanValues(isAccountTransfer, + isRegularTransaction, isApplyWithdrawFee, isInterestTransfer, isWithdrawBalance); + + this.savingsAccountDomainService.handleWithdrawal(account, fmt, closedDate, transactionAmount, paymentDetail, + transactionBooleanValues, false); + + } + + final Map accountChanges = account.close(user, command); + changes.putAll(accountChanges); + if (!changes.isEmpty()) { + this.savingAccountRepositoryWrapper.save(account); + 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); + } + + } + + businessEventNotifierService.notifyPostBusinessEvent(new SavingsCloseBusinessEvent(account)); + // disable all standing orders linked to the savings account + disableStandingInstructionsLinkedToClosedSavings(account); + return new CommandProcessingResultBuilder() // + .withEntityId(savingsId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .with(changes) // + .build(); + } + + @Override + public SavingsAccountTransaction initiateSavingsTransfer(final SavingsAccount savingsAccount, final LocalDate transferDate) { + getAppUserIfPresent(); + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + + validateTransactionsForTransfer(savingsAccount, transferDate); + + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + this.savingAccountAssembler.setHelpers(savingsAccount); + 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; + LocalDate postInterestOnDate = null; + 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, false); + + return newTransferTransaction; + } + + @Override + public SavingsAccountTransaction withdrawSavingsTransfer(final SavingsAccount savingsAccount, final LocalDate transferDate) { + getAppUserIfPresent(); + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + this.savingAccountAssembler.setHelpers(savingsAccount); + 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 MathContext mc = MathContext.DECIMAL64; + boolean isInterestTransfer = false; + LocalDate postInterestOnDate = null; + boolean postReversals = false; + savingsAccount.calculateInterestUsing(mc, transferDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, false, postReversals); + + this.savingsAccountTransactionRepository.save(withdrawtransferTransaction); + this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); + + postJournalEntries(savingsAccount, existingTransactionIds, existingReversedTransactionIds, false); + + return withdrawtransferTransaction; + } + + @Override + public void rejectSavingsTransfer(final SavingsAccount savingsAccount) { + this.savingAccountAssembler.setHelpers(savingsAccount); + savingsAccount.setStatus(SavingsAccountStatusType.TRANSFER_ON_HOLD.getValue()); + this.savingAccountRepositoryWrapper.save(savingsAccount); + } + + @Override + public SavingsAccountTransaction acceptSavingsTransfer(final SavingsAccount savingsAccount, final LocalDate transferDate, + final Office acceptedInOffice, final Staff fieldOfficer) { + getAppUserIfPresent(); + + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + this.savingAccountAssembler.setHelpers(savingsAccount); + 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 MathContext mc = MathContext.DECIMAL64; + LocalDate postInterestOnDate = null; + boolean postReversals = false; + savingsAccount.calculateInterestUsing(mc, transferDate, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, false, postReversals); + + this.savingsAccountTransactionRepository.save(acceptTransferTransaction); + this.savingAccountRepositoryWrapper.saveAndFlush(savingsAccount); + + postJournalEntries(savingsAccount, existingTransactionIds, existingReversedTransactionIds, false); + + return acceptTransferTransaction; + } + + @Transactional + @Override + public CommandProcessingResult addSavingsAccountCharge(final JsonCommand command) { + 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.savingAccountAssembler.assembleFrom(savingsAccountId, false); + 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); + + Integer chargeTimeType = chargeDefinition.getChargeTimeType(); + LocalDate dueAsOfDateParam = command.localDateValueOfParameterNamed(dueAsOfDateParamName); + if ((chargeTimeType.equals(ChargeTimeType.WITHDRAWAL_FEE.getValue()) + || chargeTimeType.equals(ChargeTimeType.SAVINGS_NOACTIVITY_FEE.getValue())) && dueAsOfDateParam != null) { + baseDataValidator.reset().parameter(dueAsOfDateParamName).value(dueAsOfDateParam.format(fmt)) + .failWithCodeNoParameterAddedToErrorCode( + "charge.due.date.is.invalid.for." + ChargeTimeType.fromInt(chargeTimeType).getCode()); + } + final SavingsAccountCharge savingsAccountCharge = SavingsAccountCharge.createNewFromJson(savingsAccount, chargeDefinition, command); + + if (chargeDefinition.isEnableFreeWithdrawal()) { + savingsAccountCharge.setFreeWithdrawalCount(0); + } + + 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 (!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.savingsAccountChargeRepository.save(savingsAccountCharge); + 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) { + + 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.savingAccountAssembler.assembleFrom(savingsAccountId, false); + 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) { + 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(); + + final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); + + this.savingAccountAssembler.loadTransactionsToSavingsAccount(account, backdatedTxnsAllowedTill); + + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + + if (backdatedTxnsAllowedTill) { + updateSavingsTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + } else { + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + } + + account.waiveCharge(savingsAccountChargeId, backdatedTxnsAllowedTill); + + boolean isInterestTransfer = false; + LocalDate postInterestOnDate = null; + final MathContext mc = MathContext.DECIMAL64; + boolean postReversals = false; + if (account.isBeforeLastPostingPeriod(savingsAccountCharge.getDueDate(), backdatedTxnsAllowedTill)) { + final LocalDate today = DateUtils.getBusinessLocalDate(); + account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, + postInterestOnDate, backdatedTxnsAllowedTill, postReversals); + } else { + final LocalDate today = DateUtils.getBusinessLocalDate(); + account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals); + } + + List depositAccountOnHoldTransactions = null; + if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { + depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository + .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); + } + + account.validateAccountBalanceDoesNotBecomeNegative(SavingsApiConstants.waiveChargeTransactionAction, + depositAccountOnHoldTransactions, backdatedTxnsAllowedTill); + + if (backdatedTxnsAllowedTill) { + this.savingsAccountTransactionRepository.saveAll(account.getSavingsAccountTransactionsWithPivotConfig()); + } + + this.savingAccountRepositoryWrapper.saveAndFlush(account); + + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, backdatedTxnsAllowedTill); + 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) { + this.context.authenticatedUser(); + + final SavingsAccount savingsAccount = this.savingAccountAssembler.assembleFrom(savingsAccountId, false); + 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) { + 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); + } + } + + final boolean backdatedTxnsAllowedTill = false; + + SavingsAccountTransaction chargeTransaction = this.payCharge(savingsAccountCharge, transactionDate, amountPaid, fmt, + backdatedTxnsAllowedTill); + + final String noteText = command.stringValueOfParameterNamed("note"); + if (StringUtils.isNotBlank(noteText)) { + final Note note = Note.savingsTransactionNote(savingsAccountCharge.savingsAccount(), chargeTransaction, noteText); + this.noteRepository.save(note); + } + + 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) { + // always use current date as transaction date for batch job + final LocalDate transactionDate = DateUtils.getBusinessLocalDate(); + final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository + .findOneWithNotFoundDetection(savingsAccountChargeId, accountId); + + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd MM yyyy").withZone(DateUtils.getDateTimeZoneOfTenant()); + + while (savingsAccountCharge.isNotFullyPaid() && DateUtils.isBefore(savingsAccountCharge.getDueDate(), transactionDate)) { + payCharge(savingsAccountCharge, transactionDate, savingsAccountCharge.amoutOutstanding(), fmt, false); + } + } + + @SuppressWarnings("unused") + public SavingsAccountData fallbackPostInterest(SavingsAccountData savingsAccountData, boolean postInterestAs, LocalDate transactionDate, + boolean backdatedTxnsAllowedTill, Throwable t) { + // NOTE: allow caller to catch the exceptions + throw org.apache.fineract.infrastructure.core.exception.ErrorHandler.getMappable(t, null, null, "savings.postinterest"); + } + + @Transactional + private SavingsAccountTransaction payCharge(final SavingsAccountCharge savingsAccountCharge, final LocalDate transactionDate, + final BigDecimal amountPaid, final DateTimeFormatter formatter, final boolean backdatedTxnsAllowedTill) { + final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService + .isSavingsInterestPostingAtCurrentPeriodEnd(); + final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); + + // Get Savings account from savings charge + final SavingsAccount account = savingsAccountCharge.savingsAccount(); + this.savingAccountAssembler.assignSavingAccountHelpers(account); + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + Pageable sortedByDateAndIdDesc = PageRequest.of(0, 1, Sort.by("dateOf", "id").descending()); + + List savingsAccountTransaction = this.savingsAccountTransactionRepository + .findBySavingsAccountIdAndLessThanDateOfAndReversedIsFalse(account.getId(), transactionDate, sortedByDateAndIdDesc); + + account.validateAccountBalanceDoesNotViolateOverdraft(savingsAccountTransaction, amountPaid); + + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + SavingsAccountTransaction chargeTransaction = account.payCharge(savingsAccountCharge, amountPaid, transactionDate, formatter, + backdatedTxnsAllowedTill, null); + boolean isInterestTransfer = false; + LocalDate postInterestOnDate = null; + final MathContext mc = MathContext.DECIMAL64; + boolean postReversals = false; + if (account.isBeforeLastPostingPeriod(transactionDate, backdatedTxnsAllowedTill)) { + final LocalDate today = DateUtils.getBusinessLocalDate(); + account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, + postInterestOnDate, isInterestTransfer, postReversals); + } else { + final LocalDate today = DateUtils.getBusinessLocalDate(); + account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, + financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals); + } + List depositAccountOnHoldTransactions = null; + + if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { + depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository + .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); + } + + account.validateAccountBalanceDoesNotBecomeNegative("." + SavingsAccountTransactionType.PAY_CHARGE.getCode(), + depositAccountOnHoldTransactions, backdatedTxnsAllowedTill); + + saveTransactionToGenerateTransactionId(chargeTransaction); + + this.savingAccountRepositoryWrapper.saveAndFlush(account); + + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, backdatedTxnsAllowedTill); + + return chargeTransaction; + } + + private void updateExistingTransactionsDetails(SavingsAccount account, Set existingTransactionIds, + Set existingReversedTransactionIds) { + existingTransactionIds.addAll(account.findExistingTransactionIds()); + existingReversedTransactionIds.addAll(account.findExistingReversedTransactionIds()); + } + + private void updateExistingTransactionsDetails(SavingsAccountData account, Set existingTransactionIds, + Set existingReversedTransactionIds) { + existingTransactionIds.addAll(account.findCurrentTransactionIdsWithPivotDateConfig()); + existingReversedTransactionIds.addAll(account.findCurrentReversedTransactionIdsWithPivotDateConfig()); + } + + private void updateSavingsTransactionsDetails(SavingsAccount account, Set existingTransactionIds, + Set existingReversedTransactionIds) { + existingTransactionIds.addAll(account.findCurrentTransactionIdsWithPivotDateConfig()); + existingReversedTransactionIds.addAll(account.findCurrentReversedTransactionIdsWithPivotDateConfig()); + } + + private void postJournalEntries(final SavingsAccount savingsAccount, final Set existingTransactionIds, + final Set existingReversedTransactionIds, final boolean backdatedTxnsAllowedTill) { + + boolean isAccountTransfer = false; + final Map accountingBridgeData = savingsAccount.deriveAccountingBridgeData(savingsAccount.getCurrency().getCode(), + existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill); + this.journalEntryWritePlatformService.createJournalEntriesForSavings(accountingBridgeData); + } + + @Override + public CommandProcessingResult inactivateCharge(final Long savingsAccountId, final Long savingsAccountChargeId) { + + this.context.authenticatedUser(); + + final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository + .findOneWithNotFoundDetection(savingsAccountChargeId, savingsAccountId); + + final SavingsAccount account = savingsAccountCharge.savingsAccount(); + this.savingAccountAssembler.assignSavingAccountHelpers(account); + + final LocalDate inactivationOnDate = DateUtils.getBusinessLocalDate(); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(SAVINGS_ACCOUNT_CHARGE_RESOURCE_NAME); + + // Only recurring fees are allowed to inactivate + if (!savingsAccountCharge.isRecurringFee()) { + baseDataValidator.reset().parameter(null).value(savingsAccountCharge.getId()) + .failWithCodeNoParameterAddedToErrorCode("charge.inactivation.allowed.only.for.recurring.charges"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + + } else { + final LocalDate nextDueDate = savingsAccountCharge.getNextDueDateFrom(inactivationOnDate); + + if (savingsAccountCharge.isChargeIsDue(nextDueDate)) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("inactivation.of.charge.not.allowed.when.charge.is.due"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } else if (savingsAccountCharge.isChargeIsOverPaid(nextDueDate)) { + + final List chargePayments = new ArrayList<>(); + SavingsAccountCharge updatedCharge = savingsAccountCharge; + do { + chargePayments.clear(); + for (SavingsAccountTransaction transaction : account.getTransactions()) { + if (transaction.isPayCharge() && transaction.isNotReversed() + && transaction.isPaymentForCurrentCharge(savingsAccountCharge)) { + chargePayments.add(transaction); + } + } + // Reverse the excess payments of charge transactions + SavingsAccountTransaction lastChargePayment = getLastChargePayment(chargePayments); + this.undoTransaction(savingsAccountCharge.savingsAccount().getId(), lastChargePayment.getId(), false); + updatedCharge = account.getUpdatedChargeDetails(savingsAccountCharge); + } while (updatedCharge.isChargeIsOverPaid(nextDueDate)); + } + account.inactivateCharge(savingsAccountCharge, inactivationOnDate); + } + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsAccountCharge.getId()) // + .withOfficeId(savingsAccountCharge.savingsAccount().officeId()) // + .withClientId(savingsAccountCharge.savingsAccount().clientId()) // + .withGroupId(savingsAccountCharge.savingsAccount().groupId()) // + .withSavingsId(savingsAccountCharge.savingsAccount().getId()) // + .build(); + } + + private SavingsAccountTransaction getLastChargePayment(final List chargePayments) { + if (!CollectionUtils.isEmpty(chargePayments)) { + return chargePayments.get(chargePayments.size() - 1); + } + return null; + } + + @Transactional + @Override + public CommandProcessingResult assignFieldOfficer(Long savingsAccountId, JsonCommand command) { + this.context.authenticatedUser(); + final Map actualChanges = new LinkedHashMap<>(5); + + Staff fromSavingsOfficer = null; + Staff toSavingsOfficer = null; + this.fromApiJsonDeserializer.validateForAssignSavingsOfficer(command.json()); + + final SavingsAccount savingsForUpdate = this.savingAccountRepositoryWrapper.findOneWithNotFoundDetection(savingsAccountId); + final Long fromSavingsOfficerId = command.longValueOfParameterNamed("fromSavingsOfficerId"); + final Long toSavingsOfficerId = command.longValueOfParameterNamed("toSavingsOfficerId"); + final LocalDate dateOfSavingsOfficerAssignment = command.localDateValueOfParameterNamed("assignmentDate"); + + if (fromSavingsOfficerId != null) { + fromSavingsOfficer = this.staffRepository.findByOfficeHierarchyWithNotFoundDetection(fromSavingsOfficerId, + savingsForUpdate.office().getHierarchy()); + } + if (toSavingsOfficerId != null) { + toSavingsOfficer = this.staffRepository.findByOfficeHierarchyWithNotFoundDetection(toSavingsOfficerId, + savingsForUpdate.office().getHierarchy()); + actualChanges.put("toSavingsOfficerId", toSavingsOfficer.getId()); + } + if (!savingsForUpdate.hasSavingsOfficer(fromSavingsOfficer)) { + throw new SavingsOfficerAssignmentException(savingsAccountId, fromSavingsOfficerId); + } + + savingsForUpdate.reassignSavingsOfficer(toSavingsOfficer, dateOfSavingsOfficerAssignment); + + this.savingAccountRepositoryWrapper.saveAndFlush(savingsForUpdate); + + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withOfficeId(savingsForUpdate.officeId()) // + .withEntityId(savingsForUpdate.getId()) // + .withSavingsId(savingsAccountId) // + .with(actualChanges) // + .build(); + } + + @Transactional + @Override + public CommandProcessingResult unassignFieldOfficer(Long savingsAccountId, JsonCommand command) { + + this.context.authenticatedUser(); + + final Map actualChanges = new LinkedHashMap<>(5); + this.fromApiJsonDeserializer.validateForUnAssignSavingsOfficer(command.json()); + + final SavingsAccount savingsForUpdate = this.savingAccountRepositoryWrapper.findOneWithNotFoundDetection(savingsAccountId); + if (savingsForUpdate.getSavingsOfficer() == null) { + throw new SavingsOfficerUnassignmentException(savingsAccountId); + } + + final LocalDate dateOfSavingsOfficerUnassigned = command.localDateValueOfParameterNamed("unassignedDate"); + + savingsForUpdate.removeSavingsOfficer(dateOfSavingsOfficerUnassigned); + + this.savingAccountRepositoryWrapper.saveAndFlush(savingsForUpdate); + + actualChanges.put("toSavingsOfficerId", null); + + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withOfficeId(savingsForUpdate.officeId()) // + .withEntityId(savingsForUpdate.getId()) // + .withSavingsId(savingsAccountId) // + .with(actualChanges) // + .build(); + } + + @Override + public CommandProcessingResult modifyWithHoldTax(Long savingsAccountId, JsonCommand command) { + final Map actualChanges = new HashMap<>(1); + final SavingsAccount savingsForUpdate = this.savingAccountRepositoryWrapper.findOneWithNotFoundDetection(savingsAccountId); + if (command.isChangeInBooleanParameterNamed(withHoldTaxParamName, savingsForUpdate.withHoldTax())) { + final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(withHoldTaxParamName); + actualChanges.put(withHoldTaxParamName, newValue); + savingsForUpdate.setWithHoldTax(newValue); + if (savingsForUpdate.getTaxGroup() == null) { + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("account"); + baseDataValidator.reset().parameter(withHoldTaxParamName).failWithCode("not.supported"); + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(savingsAccountId) // + .withSavingsId(savingsAccountId) // + .with(actualChanges) // + .build(); + } + + @Override + public void setSubStatusInactive(Long savingsId) { + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + account.setSubStatusInactive(false); + this.savingAccountRepositoryWrapper.saveAndFlush(account); + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false); + } + + @Override + public void setSubStatusDormant(Long savingsId) { + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); + account.setSubStatusDormant(); + this.savingAccountRepositoryWrapper.saveAndFlush(account); + } + + @Override + public void escheat(Long savingsId) { + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + account.escheat(appuserRepository.fetchSystemUser()); + this.savingAccountRepositoryWrapper.saveAndFlush(account); + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false); + } + + private AppUser getAppUserIfPresent() { + AppUser user = null; + if (this.context != null) { + user = this.context.getAuthenticatedUserIfPresent(); + } + return user; + } + + /** + * Disable all standing instructions linked to the savings account if the status is "closed" + * + * @param savingsAccount + * -- the savings account object + * + **/ + @Transactional + private void disableStandingInstructionsLinkedToClosedSavings(final SavingsAccount savingsAccount) { + if (savingsAccount != null && savingsAccount.isClosed()) { + final Integer standingInstructionStatus = StandingInstructionStatus.ACTIVE.getValue(); + final Collection accountTransferStandingInstructions = this.standingInstructionRepository + .findBySavingsAccountAndStatus(savingsAccount, standingInstructionStatus); + + if (!accountTransferStandingInstructions.isEmpty()) { + for (AccountTransferStandingInstruction accountTransferStandingInstruction : accountTransferStandingInstructions) { + accountTransferStandingInstruction.updateStatus(StandingInstructionStatus.DISABLED.getValue()); + this.standingInstructionRepository.save(accountTransferStandingInstruction); + } + } + } + } + + @Override + public CommandProcessingResult blockAccount(final Long savingsId, final JsonCommand command) { + + this.context.authenticatedUser(); + + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); + checkClientOrGroupActive(account); + + final Map changes = account.block(); + + final String reasonForBlock = command.stringValueOfParameterNamed(SavingsApiConstants.reasonForBlockParamName); + validateReasonForHold(reasonForBlock); + account.updateReason(reasonForBlock); + + if (!changes.isEmpty()) { + + this.savingAccountRepositoryWrapper.save(account); + } + return new CommandProcessingResultBuilder().withEntityId(savingsId).withOfficeId(account.officeId()) + .withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).with(changes).build(); + } + + @Override + public CommandProcessingResult unblockAccount(final Long savingsId) { + this.context.authenticatedUser(); + + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); + checkClientOrGroupActive(account); + + final Map changes = account.unblock(); + + account.updateReason(null); + + if (!changes.isEmpty()) { + + this.savingAccountRepositoryWrapper.save(account); + } + return new CommandProcessingResultBuilder().withEntityId(savingsId).withOfficeId(account.officeId()) + .withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).with(changes).build(); + } + + @Transactional + @Override + public CommandProcessingResult holdAmount(final Long savingsId, final JsonCommand command) { + + final AppUser submittedBy = this.context.authenticatedUser(); + final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill); + final LocalDate transactionDate = command.localDateValueOfParameterNamed(transactionDateParamName); + final boolean lienAllowed = command.booleanPrimitiveValueOfParameterNamed(lienAllowedParamName); + + checkClientOrGroupActive(account); + + final BigDecimal amount = command.bigDecimalValueOfParameterNamed(transactionAmountParamName); + + Money runningBalance = Money.of(account.getCurrency(), account.getAccountBalance()); + if (account.getSavingsHoldAmount() != null) { + runningBalance = runningBalance.minus(account.getSavingsHoldAmount()).minus(amount); + } else { + runningBalance = runningBalance.minus(amount); + } + + this.savingsAccountTransactionDataValidator.validateHoldAndAssembleForm(command.json(), account, submittedBy, + backdatedTxnsAllowedTill); + SavingsAccountTransaction transaction = this.savingsAccountDomainService.handleHold(account, amount, transactionDate, lienAllowed); + account.holdAmount(amount); + transaction.setRunningBalance(runningBalance); + + final String reasonForBlock = command.stringValueOfParameterNamed(SavingsApiConstants.reasonForBlockParamName); + transaction.updateReason(reasonForBlock); + + account.getAccountBalance(); + this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transaction.getTransactionDate(), account); + + this.savingsAccountTransactionRepository.saveAndFlush(transaction); + + if (backdatedTxnsAllowedTill) { + // Check again whether transactions are modified + this.savingsAccountTransactionRepository.saveAll(account.getSavingsAccountTransactionsWithPivotConfig()); + } + + this.savingAccountRepositoryWrapper.saveAndFlush(account); + + return new CommandProcessingResultBuilder().withEntityId(transaction.getId()).withOfficeId(account.officeId()) + .withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).build(); + } + + @Transactional + @Override + public CommandProcessingResult releaseAmount(final Long savingsId, final Long savingsTransactionId) { + context.authenticatedUser(); + SavingsAccountTransaction holdTransaction = this.savingsAccountTransactionRepository + .findOneByIdAndSavingsAccountId(savingsTransactionId, savingsId); + + holdTransaction.updateReason(null); + + final SavingsAccountTransaction transaction = this.savingsAccountTransactionDataValidator + .validateReleaseAmountAndAssembleForm(holdTransaction); + + final boolean backdatedTxnsAllowedTill = this.savingAccountAssembler.getPivotConfigStatus(); + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill); + checkClientOrGroupActive(account); + + Money runningBalance = Money.of(account.getCurrency(), account.getAccountBalance()); + + Money savingsOnHold = Money.of(account.getCurrency(), account.getSavingsHoldAmount()); + + runningBalance = runningBalance.minus(savingsOnHold); + + runningBalance = runningBalance.plus(transaction.getAmount()); + transaction.setRunningBalance(runningBalance); + + this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transaction.getTransactionDate(), account); + account.releaseOnHoldAmount(transaction.getAmount()); + + this.savingsAccountTransactionRepository.saveAndFlush(transaction); + holdTransaction.updateReleaseId(transaction.getId()); + + if (backdatedTxnsAllowedTill) { + this.savingsAccountTransactionRepository.saveAll(account.getSavingsAccountTransactionsWithPivotConfig()); + } else { + account.addTransaction(transaction); + } + + this.savingAccountRepositoryWrapper.save(account); + + return new CommandProcessingResultBuilder().withEntityId(transaction.getId()).withOfficeId(account.officeId()) + .withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(account.getId()).build(); + } + + @Transactional + @Override + public CommandProcessingResult blockCredits(final Long savingsId, final JsonCommand command) { + this.context.authenticatedUser(); + + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); + checkClientOrGroupActive(account); + + final String reasonForBlock = command.stringValueOfParameterNamed(SavingsApiConstants.reasonForBlockParamName); + validateReasonForHold(reasonForBlock); + account.updateReason(reasonForBlock); + + final Map changes = account.blockCredits(account.getSubStatus()); + if (!changes.isEmpty()) { + + this.savingAccountRepositoryWrapper.save(account); + } + return new CommandProcessingResultBuilder().withEntityId(savingsId).withOfficeId(account.officeId()) + .withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).with(changes).build(); + } + + @Transactional + @Override + public CommandProcessingResult unblockCredits(final Long savingsId) { + this.context.authenticatedUser(); + + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); + checkClientOrGroupActive(account); + account.updateReason(null); + final Map changes = account.unblockCredits(); + if (!changes.isEmpty()) { + + this.savingAccountRepositoryWrapper.save(account); + } + return new CommandProcessingResultBuilder().withEntityId(savingsId).withOfficeId(account.officeId()) + .withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).with(changes).build(); + } + + @Transactional + @Override + public CommandProcessingResult blockDebits(final Long savingsId, final JsonCommand command) { + this.context.authenticatedUser(); + + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); + checkClientOrGroupActive(account); + + final String reasonForBlock = command.stringValueOfParameterNamed(SavingsApiConstants.reasonForBlockParamName); + validateReasonForHold(reasonForBlock); + account.updateReason(reasonForBlock); + + final Map changes = account.blockDebits(account.getSubStatus()); + if (!changes.isEmpty()) { + + this.savingAccountRepositoryWrapper.save(account); + } + return new CommandProcessingResultBuilder().withEntityId(savingsId).withOfficeId(account.officeId()) + .withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).with(changes).build(); + } + + @Transactional + @Override + public CommandProcessingResult unblockDebits(final Long savingsId) { + this.context.authenticatedUser(); + + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); + checkClientOrGroupActive(account); + + account.updateReason(null); + + final Map changes = account.unblockDebits(); + if (!changes.isEmpty()) { + + this.savingAccountRepositoryWrapper.save(account); + } + return new CommandProcessingResultBuilder().withEntityId(savingsId).withOfficeId(account.officeId()) + .withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).with(changes).build(); + } + + private void validateTransactionsForTransfer(final SavingsAccount savingsAccount, final LocalDate transferDate) { + for (SavingsAccountTransaction transaction : savingsAccount.getTransactions()) { + if ((DateUtils.isEqual(transferDate, transaction.getTransactionDate()) + && DateUtils.isEqual(transferDate, transaction.getSubmittedOnDate())) + || DateUtils.isBefore(transferDate, transaction.getTransactionDate())) { + throw new GeneralPlatformDomainRuleException(TransferApiConstants.transferClientSavingsException, + TransferApiConstants.transferClientSavingsException, transaction.getTransactionDate(), transferDate); + } + } + } + + private void validateReasonForHold(String reasonForBlock) { + if (StringUtils.isBlank(reasonForBlock)) { + throw new PlatformDataIntegrityException("Reason For Block is Mandatory", "error.msg.reason.for.block.mandatory"); + } + } +} 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 ef4d08f58..2079c29e5 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 @@ -98,6 +98,7 @@ import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityConfig import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityLimit; import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityLimitRepository; import org.apache.fineract.portfolio.savings.domain.SavingsProductRepository; +import org.apache.fineract.portfolio.denominationmaster.domain.SavingsTxnDenominationRepository; import org.apache.fineract.portfolio.savings.service.FacilityChargeAutoApplyService; import org.apache.fineract.portfolio.savings.service.DepositAccountInterestRateChartReadPlatformService; import org.apache.fineract.portfolio.savings.service.DepositAccountInterestRateChartReadPlatformServiceImpl; @@ -424,7 +425,8 @@ public class SavingsConfiguration { EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService, AppUserRepositoryWrapper appuserRepository, StandingInstructionRepository standingInstructionRepository, BusinessEventNotifierService businessEventNotifierService, GSIMRepositoy gsimRepository, SavingsAccountInterestPostingService savingsAccountInterestPostingService, - ErrorHandler errorHandler, FacilityChargeAutoApplyService facilityChargeAutoApplyService) { + ErrorHandler errorHandler, FacilityChargeAutoApplyService facilityChargeAutoApplyService, + SavingsTxnDenominationRepository savingsTxnDenominationRepository) { return new SavingsAccountWritePlatformServiceJpaRepositoryImpl(context, fromApiJsonDeserializer, savingAccountRepositoryWrapper, staffRepository, savingsAccountTransactionRepository, savingAccountAssembler, savingsAccountTransactionDataValidator, savingsAccountChargeDataValidator, paymentDetailWritePlatformService, journalEntryWritePlatformService, @@ -432,7 +434,7 @@ public class SavingsConfiguration { chargeRepository, savingsAccountChargeRepository, holidayRepository, workingDaysRepository, configurationDomainService, depositAccountOnHoldTransactionRepository, entityDatatableChecksWritePlatformService, appuserRepository, standingInstructionRepository, businessEventNotifierService, gsimRepository, savingsAccountInterestPostingService, - errorHandler, facilityChargeAutoApplyService); + errorHandler, facilityChargeAutoApplyService, savingsTxnDenominationRepository); } @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 e640e4ace..ad7639229 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 @@ -318,4 +318,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 new file mode 100644 index 000000000..aad95f9a8 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0012_denomination_master.xml @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountConstant.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountConstant.java index 6cad12d87..85b32925e 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountConstant.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountConstant.java @@ -1,82 +1,83 @@ -/** - * 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 java.util.Arrays; -import java.util.HashSet; -import java.util.Set; -import org.apache.fineract.portfolio.savings.SavingsApiConstants; - -public class SavingsAccountConstant extends SavingsApiConstants { - - /** - * These parameters will match the class level parameters of {@link SavingsProductData}. Where possible, we try to - * get response parameters to match those of request parameters. - */ - protected static final Set SAVINGS_ACCOUNT_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(localeParamName, - dateFormatParamName, monthDayFormatParamName, staffIdParamName, isGSIM, isParentAccount, accountNoParamName, - externalIdParamName, clientIdParamName, groupIdParamName, productIdParamName, fieldOfficerIdParamName, submittedOnDateParamName, - nominalAnnualInterestRateParamName, interestCompoundingPeriodTypeParamName, interestPostingPeriodTypeParamName, - interestCalculationTypeParamName, interestCalculationDaysInYearTypeParamName, minRequiredOpeningBalanceParamName, - lockinPeriodFrequencyParamName, lockinPeriodFrequencyTypeParamName, - // withdrawalFeeAmountParamName, withdrawalFeeTypeParamName, - withdrawalFeeForTransfersParamName, feeAmountParamName, feeOnMonthDayParamName, chargesParamName, allowOverdraftParamName, - overdraftLimitParamName, minRequiredBalanceParamName, enforceMinRequiredBalanceParamName, lienAllowedParamName, - maxAllowedLienLimitParamName, nominalAnnualInterestRateOverdraftParamName, minOverdraftForInterestCalculationParamName, - withHoldTaxParamName, datatables, gsimApplicationId, gsimLastApplication, - // CBS Account fields - "accountType", "customerSegment", accountStatusParamName, modeOfOperationParamName, - specialCategoryParamName, weakerSectionParamName, - guardianNameParamName, guardianCifNumberParamName, guardianRelationshipParamName, - guardianClientIdParamName, minorDateOfBirthParamName, - introducerIdParamName, introducerNameParamName, introducerCifNumberParamName, - nomineeParamName, "nominees", linkedAccountIdParamName, linkedAccountTypeParamName, linkedAccountNoParamName, - accountFacilityConfigParamName, jointHoldersParamName, - balanceSwapOptionParamName, msmeParamName, autoDebitFacilityParamName)); - - /** - * These parameters will match the class level parameters of {@link SavingsAccountData}. Where possible, we try to - * get response parameters to match those of request parameters. - */ - - protected static final Set SAVINGS_ACCOUNT_TRANSACTION_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(localeParamName, - dateFormatParamName, transactionDateParamName, transactionAmountParamName, paymentTypeIdParamName, - transactionAccountNumberParamName, checkNumberParamName, routingCodeParamName, receiptNumberParamName, bankNumberParamName, - retailEntriesParamName, childAccountIdParamName, noteParamName, amountParamName, dateParamName, isManualTransaction, - lienTransaction, chargesPaidByData, submittedOnDateParamName, accountIdParamName, accountNoParamName)); - - protected static final Set SAVINGS_ACCOUNT_TRANSACTION_RESPONSE_DATA_PARAMETERS = new HashSet<>( - Arrays.asList(idParamName, accountNoParamName)); - - protected static final Set SAVINGS_ACCOUNT_ACTIVATION_REQUEST_DATA_PARAMETERS = new HashSet<>( - Arrays.asList(localeParamName, dateFormatParamName, activatedOnDateParamName, "openingBalance")); - - protected static final Set SAVINGS_ACCOUNT_CLOSE_REQUEST_DATA_PARAMETERS = new HashSet<>( - Arrays.asList(localeParamName, dateFormatParamName, closedOnDateParamName, noteParamName, paymentTypeIdParamName, - withdrawBalanceParamName, transactionAccountNumberParamName, checkNumberParamName, routingCodeParamName, - receiptNumberParamName, bankNumberParamName, postInterestValidationOnClosure)); - - protected static final Set SAVINGS_ACCOUNT_CHARGES_ADD_REQUEST_DATA_PARAMETERS = new HashSet<>( - Arrays.asList(chargeIdParamName, amountParamName, dueAsOfDateParamName, dateFormatParamName, localeParamName, - feeOnMonthDayParamName, monthDayFormatParamName, feeIntervalParamName)); - - protected static final Set SAVINGS_ACCOUNT_CHARGES_PAY_CHARGE_REQUEST_DATA_PARAMETERS = new HashSet<>( - Arrays.asList(amountParamName, dueAsOfDateParamName, dateFormatParamName, localeParamName, noteParamName)); - -} +/** + * 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 java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import org.apache.fineract.portfolio.savings.SavingsApiConstants; + +public class SavingsAccountConstant extends SavingsApiConstants { + + /** + * These parameters will match the class level parameters of {@link SavingsProductData}. Where possible, we try to + * get response parameters to match those of request parameters. + */ + protected static final Set SAVINGS_ACCOUNT_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(localeParamName, + dateFormatParamName, monthDayFormatParamName, staffIdParamName, isGSIM, isParentAccount, accountNoParamName, + externalIdParamName, clientIdParamName, groupIdParamName, productIdParamName, fieldOfficerIdParamName, submittedOnDateParamName, + nominalAnnualInterestRateParamName, interestCompoundingPeriodTypeParamName, interestPostingPeriodTypeParamName, + interestCalculationTypeParamName, interestCalculationDaysInYearTypeParamName, minRequiredOpeningBalanceParamName, + lockinPeriodFrequencyParamName, lockinPeriodFrequencyTypeParamName, + // withdrawalFeeAmountParamName, withdrawalFeeTypeParamName, + withdrawalFeeForTransfersParamName, feeAmountParamName, feeOnMonthDayParamName, chargesParamName, allowOverdraftParamName, + overdraftLimitParamName, minRequiredBalanceParamName, enforceMinRequiredBalanceParamName, lienAllowedParamName, + maxAllowedLienLimitParamName, nominalAnnualInterestRateOverdraftParamName, minOverdraftForInterestCalculationParamName, + withHoldTaxParamName, datatables, gsimApplicationId, gsimLastApplication, + // CBS Account fields + "accountType", "customerSegment", accountStatusParamName, modeOfOperationParamName, + specialCategoryParamName, weakerSectionParamName, + guardianNameParamName, guardianCifNumberParamName, guardianRelationshipParamName, + guardianClientIdParamName, minorDateOfBirthParamName, + introducerIdParamName, introducerNameParamName, introducerCifNumberParamName, + nomineeParamName, "nominees", linkedAccountIdParamName, linkedAccountTypeParamName, linkedAccountNoParamName, + accountFacilityConfigParamName, jointHoldersParamName, + balanceSwapOptionParamName, msmeParamName, autoDebitFacilityParamName)); + + /** + * These parameters will match the class level parameters of {@link SavingsAccountData}. Where possible, we try to + * get response parameters to match those of request parameters. + */ + + protected static final Set SAVINGS_ACCOUNT_TRANSACTION_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(localeParamName, + dateFormatParamName, transactionDateParamName, transactionAmountParamName, paymentTypeIdParamName, + transactionAccountNumberParamName, checkNumberParamName, routingCodeParamName, receiptNumberParamName, bankNumberParamName, + retailEntriesParamName, childAccountIdParamName, noteParamName, amountParamName, dateParamName, isManualTransaction, + lienTransaction, chargesPaidByData, submittedOnDateParamName, accountIdParamName, accountNoParamName, + "denominationBreakdown")); + + protected static final Set SAVINGS_ACCOUNT_TRANSACTION_RESPONSE_DATA_PARAMETERS = new HashSet<>( + Arrays.asList(idParamName, accountNoParamName)); + + protected static final Set SAVINGS_ACCOUNT_ACTIVATION_REQUEST_DATA_PARAMETERS = new HashSet<>( + Arrays.asList(localeParamName, dateFormatParamName, activatedOnDateParamName, "openingBalance")); + + protected static final Set SAVINGS_ACCOUNT_CLOSE_REQUEST_DATA_PARAMETERS = new HashSet<>( + Arrays.asList(localeParamName, dateFormatParamName, closedOnDateParamName, noteParamName, paymentTypeIdParamName, + withdrawBalanceParamName, transactionAccountNumberParamName, checkNumberParamName, routingCodeParamName, + receiptNumberParamName, bankNumberParamName, postInterestValidationOnClosure)); + + protected static final Set SAVINGS_ACCOUNT_CHARGES_ADD_REQUEST_DATA_PARAMETERS = new HashSet<>( + Arrays.asList(chargeIdParamName, amountParamName, dueAsOfDateParamName, dateFormatParamName, localeParamName, + feeOnMonthDayParamName, monthDayFormatParamName, feeIntervalParamName)); + + protected static final Set SAVINGS_ACCOUNT_CHARGES_PAY_CHARGE_REQUEST_DATA_PARAMETERS = new HashSet<>( + Arrays.asList(amountParamName, dueAsOfDateParamName, dateFormatParamName, localeParamName, noteParamName)); + +}