diff --git a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/Teller.java b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/Teller.java index 91e22d10e..e41e7bc27 100644 --- a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/Teller.java +++ b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/Teller.java @@ -76,6 +76,9 @@ public class Teller extends AbstractPersistableCustom { @Column(name = "state", nullable = false) private Integer status; + @Column(name = "is_deleted", nullable = false) + private boolean deleted = false; + @OneToMany(mappedBy = "teller", fetch = FetchType.LAZY) private Set cashiers; @@ -161,4 +164,12 @@ public class Teller extends AbstractPersistableCustom { this.office.getId(); this.cashiers.size(); } + + public void delete() { + this.deleted = true; + } + + public boolean isDeleted() { + return this.deleted; + } } diff --git a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/TellerRepository.java b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/TellerRepository.java index 2fdea2a68..6b4c43e63 100644 --- a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/TellerRepository.java +++ b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/TellerRepository.java @@ -31,4 +31,6 @@ import org.springframework.data.jpa.repository.JpaSpecificationExecutor; public interface TellerRepository extends JpaRepository, JpaSpecificationExecutor { Collection findTellerByOfficeId(Long officeId); + + long countByDeletedFalse(); } diff --git a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/TellerRepositoryWrapper.java b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/TellerRepositoryWrapper.java index f0b335d1e..494c4069c 100644 --- a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/TellerRepositoryWrapper.java +++ b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/domain/TellerRepositoryWrapper.java @@ -36,6 +36,9 @@ public class TellerRepositoryWrapper { @Transactional(readOnly = true) public Teller findOneWithNotFoundDetection(final Long id) { final Teller teller = this.repository.findById(id).orElseThrow(() -> new TellerNotFoundException(id)); + if (teller.isDeleted()) { + throw new TellerNotFoundException(id); + } teller.initializeLazyCollections(); return teller; } @@ -51,4 +54,8 @@ public class TellerRepositoryWrapper { public void delete(final Teller teller) { this.repository.delete(teller); } + + public boolean existsAny() { + return this.repository.countByDeletedFalse() > 0; + } } diff --git a/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/exception/TellerAlreadyExistsException.java b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/exception/TellerAlreadyExistsException.java new file mode 100644 index 000000000..a465aa44d --- /dev/null +++ b/fineract-branch/src/main/java/org/apache/fineract/organisation/teller/exception/TellerAlreadyExistsException.java @@ -0,0 +1,31 @@ +/** + * 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.organisation.teller.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class TellerAlreadyExistsException extends AbstractPlatformDomainRuleException { + + private static final String ERROR_MESSAGE_CODE = "error.msg.teller.already.exists"; + private static final String DEFAULT_ERROR_MESSAGE = "Cannot create teller as a teller is already present."; + + public TellerAlreadyExistsException() { + super(ERROR_MESSAGE_CODE, DEFAULT_ERROR_MESSAGE); + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/domain/CommandProcessingResultType.java b/fineract-core/src/main/java/org/apache/fineract/commands/domain/CommandProcessingResultType.java index 17f70dfe4..224490cc7 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/domain/CommandProcessingResultType.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/domain/CommandProcessingResultType.java @@ -34,7 +34,8 @@ public enum CommandProcessingResultType { AWAITING_APPROVAL(2, "commandProcessingResultType.awaiting.approval"), // REJECTED(3, "commandProcessingResultType.rejected"), // UNDER_PROCESSING(4, "commandProcessingResultType.underProcessing"), // - ERROR(5, "commandProcessingResultType.error"); + ERROR(5, "commandProcessingResultType.error"), // + TERMINATE(6, "commandProcessingResultType.terminate"); private static final Map BY_ID = Arrays.stream(values()) .collect(Collectors.toMap(CommandProcessingResultType::getValue, v -> v)); @@ -59,4 +60,8 @@ public enum CommandProcessingResultType { public boolean isRejected() { return this == REJECTED; } + + public boolean isTerminate() { + return this == TERMINATE; + } } diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index b16b574c1..b95df2d30 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -1611,6 +1611,14 @@ public class CommandWrapperBuilder { return this; } + public CommandWrapperBuilder terminateAccount(final Long accountId) { + this.actionName = "TERMINATE"; + this.entityName = "ACCOUNT"; + this.entityId = accountId; + this.href = "/accounts/" + accountId + "/terminate"; + return this; + } + public CommandWrapperBuilder approveGSIMAccountApplication(final Long accountId) { this.actionName = "APPROVE"; this.entityName = "GSIMACCOUNT"; @@ -3565,7 +3573,7 @@ public class CommandWrapperBuilder { this.actionName = "ACTIVATE"; this.entityName = "CURRENTACCOUNT"; this.savingsId = accountId; - this.entityId = null; + this.entityId = accountId; this.href = "/currentaccounts/" + accountId + "?command=activate"; return this; } diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/SynchronousCommandProcessingService.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/SynchronousCommandProcessingService.java index a13835b62..ea09a0776 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/SynchronousCommandProcessingService.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/SynchronousCommandProcessingService.java @@ -20,6 +20,7 @@ package org.apache.fineract.commands.service; import static org.apache.fineract.commands.domain.CommandProcessingResultType.ERROR; import static org.apache.fineract.commands.domain.CommandProcessingResultType.PROCESSED; +import static org.apache.fineract.commands.domain.CommandProcessingResultType.TERMINATE; import static org.apache.http.HttpStatus.SC_OK; import com.google.gson.Gson; @@ -181,7 +182,7 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer currentSource.setResultStatusCode(SC_OK); currentSource.updateForAudit(result); currentSource.setResult(toApiResultJsonSerializer.serializeResult(result)); - currentSource.setStatus(PROCESSED); + currentSource.setStatus("TERMINATE".equals(wrapper.getActionName()) ? TERMINATE : PROCESSED); // Return saved command source return commandSourceService.saveResultSameTransaction(currentSource); diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/DepositsApiConstants.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/DepositsApiConstants.java index 88ae1c759..078d9aaf4 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/DepositsApiConstants.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/DepositsApiConstants.java @@ -224,7 +224,7 @@ public final class DepositsApiConstants { * Deposit Product Parameters */ private static final Set DEPOSIT_PRODUCT_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(localeParamName, - monthDayFormatParamName, nameParamName, shortNameParamName, descriptionParamName, currencyCodeParamName, + dateFormatParamName, monthDayFormatParamName, nameParamName, shortNameParamName, descriptionParamName, currencyCodeParamName, digitsAfterDecimalParamName, inMultiplesOfParamName, nominalAnnualInterestRateParamName, interestCompoundingPeriodTypeParamName, interestPostingPeriodTypeParamName, interestCalculationTypeParamName, interestCalculationDaysInYearTypeParamName, lockinPeriodFrequencyParamName, lockinPeriodFrequencyTypeParamName, accountingRuleParamName, chargesParamName, diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountData.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountData.java index 544d0f35f..def3e27f7 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountData.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountData.java @@ -170,6 +170,7 @@ public final class SavingsAccountData implements Serializable { private Boolean balanceSwapOption; private Boolean msme; private Boolean autoDebitFacility; + private Boolean terminateTrue; private String avgBalanceCalcPeriod; private String validForPeriod; private Boolean odAccountClosedZeroBalanceInDayClosing; diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductData.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductData.java index 72380a6d8..0c74cde3c 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductData.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductData.java @@ -159,6 +159,9 @@ public final class SavingsProductData implements Serializable { @Getter @Setter private Map depositLimitKyc; @Getter @Setter private Map>> otherGlMappingsMap; + // True when a row exists in m_saving_product_temp with status = PENDING for this product - that row is the lock. + @Getter @Setter private Boolean isPendingApproval; + public static SavingsProductData template(final CurrencyData currency, final EnumOptionData interestCompoundingPeriodType, final EnumOptionData interestPostingPeriodType, final EnumOptionData interestCalculationType, final EnumOptionData interestCalculationDaysInYearType, final EnumOptionData accountingRule, diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductTempData.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductTempData.java new file mode 100644 index 000000000..563388c89 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsProductTempData.java @@ -0,0 +1,49 @@ +/** + * 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.io.Serializable; +import java.time.LocalDateTime; +import java.util.Map; +import lombok.Getter; +import lombok.Setter; + +/** + * Read-side view of a single pending Savings Product edit row (m_savings_product_temp), for the Approvals -> + * Pending Savings Product Edits screen. + */ +@Getter +@Setter +public class SavingsProductTempData implements Serializable { + + private Long id; + private Long savingsProductId; + private String savingsProductName; + private Long makerId; + private String makerUsername; + private Long checkerId; + private String checkerUsername; + private LocalDateTime createdOn; + private LocalDateTime checkedOn; + private String status; + private String rejectReason; + private Map proposedChanges; + + public SavingsProductTempData() {} +} diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountStatusType.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountStatusType.java index ffe2fefaf..ef61b14de 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountStatusType.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountStatusType.java @@ -36,7 +36,8 @@ public enum SavingsAccountStatusType { REJECTED(500, "savingsAccountStatusType.rejected"), // CLOSED(600, "savingsAccountStatusType.closed"), // PRE_MATURE_CLOSURE(700, "savingsAccountStatusType.pre.mature.closure"), // - MATURED(800, "savingsAccountStatusType.matured"); // + MATURED(800, "savingsAccountStatusType.matured"), // + TERMINATED(900, "savingsAccountStatusType.terminated"); // private final Integer value; private final String code; @@ -53,6 +54,7 @@ public enum SavingsAccountStatusType { case 600 -> SavingsAccountStatusType.CLOSED; case 700 -> SavingsAccountStatusType.PRE_MATURE_CLOSURE; case 800 -> SavingsAccountStatusType.MATURED; + case 900 -> SavingsAccountStatusType.TERMINATED; default -> SavingsAccountStatusType.INVALID; }; } @@ -113,4 +115,8 @@ public enum SavingsAccountStatusType { public boolean isPreMatureClosure() { return this.value.equals(SavingsAccountStatusType.PRE_MATURE_CLOSURE.getValue()); } + + public boolean isTerminated() { + return this.value.equals(SavingsAccountStatusType.TERMINATED.getValue()); + } } diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsEnumerations.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsEnumerations.java index 7f4cab97b..57e025a06 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsEnumerations.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsEnumerations.java @@ -285,6 +285,11 @@ public final class SavingsEnumerations { SavingsAccountStatusType.MATURED.getCode(), "Matured", submittedAndPendingApproval, isApproved, isRejected, isWithdrawnByApplicant, isActive, isClosed, isPrematureClosed, isTransferInProgress, isTransferOnHold, matured); break; + case TERMINATED: + optionData = new SavingsAccountStatusEnumData(SavingsAccountStatusType.TERMINATED.getValue().longValue(), + SavingsAccountStatusType.TERMINATED.getCode(), "Terminated", submittedAndPendingApproval, isApproved, isRejected, + isWithdrawnByApplicant, isActive, isClosed, isPrematureClosed, isTransferInProgress, isTransferOnHold, matured); + break; } return optionData; } diff --git a/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/domain/CurrentAccount.java b/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/domain/CurrentAccount.java index 9ec8a520c..38bf40627 100644 --- a/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/domain/CurrentAccount.java +++ b/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/domain/CurrentAccount.java @@ -39,6 +39,7 @@ import org.apache.fineract.infrastructure.core.domain.AbstractAuditableCustom; import org.apache.fineract.organisation.office.domain.Office; import org.apache.fineract.organisation.staff.domain.Staff; import org.apache.fineract.portfolio.client.domain.Client; +import org.apache.fineract.useradministration.domain.AppUser; @Entity @Table(name = "m_current_account", uniqueConstraints = { @@ -102,6 +103,30 @@ public class CurrentAccount extends AbstractAuditableCustom { @Column(name = "closedon_date") private LocalDate closedOnDate; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "submittedon_userid") + private AppUser submittedOnByUser; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "approvedon_userid") + private AppUser approvedOnByUser; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "activatedon_userid") + private AppUser activatedOnByUser; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "rejectedon_userid") + private AppUser rejectedOnByUser; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "withdrawnon_userid") + private AppUser withdrawnOnByUser; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "closedon_userid") + private AppUser closedOnByUser; + // Currency (copied from product at account creation) @Column(name = "currency_code", nullable = false, length = 3) private String currencyCode; @@ -304,6 +329,12 @@ public class CurrentAccount extends AbstractAuditableCustom { @Column(name = "od_account_closed_zero_balance_in_day_closing") private boolean odAccountClosedZeroBalanceInDayClosing = false; + @Column(name = "is_linking_enable", nullable = false) + private boolean shareLinkingEnabled = false; + + @Column(name = "terminate_true", nullable = false, columnDefinition = "boolean default false") + private boolean terminateTrue = false; + // Relationships @OneToMany(mappedBy = "currentAccount", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) @@ -373,29 +404,34 @@ public class CurrentAccount extends AbstractAuditableCustom { public void updateAccountNo(String accountNo) { this.accountNo = accountNo; } - public void approve(LocalDate approvedOnDate) { + public void approve(LocalDate approvedOnDate, AppUser approvedOnByUser) { this.statusEnum = 200; this.approvedOnDate = approvedOnDate; + this.approvedOnByUser = approvedOnByUser; } - public void activate(LocalDate activatedOnDate) { + public void activate(LocalDate activatedOnDate, AppUser activatedOnByUser) { this.statusEnum = 300; this.activatedOnDate = activatedOnDate; + this.activatedOnByUser = activatedOnByUser; } - public void reject(LocalDate rejectedOnDate) { + public void reject(LocalDate rejectedOnDate, AppUser rejectedOnByUser) { this.statusEnum = 500; this.rejectedOnDate = rejectedOnDate; + this.rejectedOnByUser = rejectedOnByUser; } - public void withdraw(LocalDate withdrawnOnDate) { + public void withdraw(LocalDate withdrawnOnDate, AppUser withdrawnOnByUser) { this.statusEnum = 400; this.withdrawnOnDate = withdrawnOnDate; + this.withdrawnOnByUser = withdrawnOnByUser; } - public void close(LocalDate closedOnDate) { + public void close(LocalDate closedOnDate, AppUser closedOnByUser) { this.statusEnum = 600; this.closedOnDate = closedOnDate; + this.closedOnByUser = closedOnByUser; } public void setMinRequiredOpeningBalance(java.math.BigDecimal val) { this.minRequiredOpeningBalance = val; } @@ -406,4 +442,15 @@ public class CurrentAccount extends AbstractAuditableCustom { public void setNominalAnnualInterestRateOverdraft(java.math.BigDecimal val) { this.nominalAnnualInterestRateOverdraft = val; } public void setWithdrawalFeeForTransfer(boolean val) { this.withdrawalFeeForTransfer = val; } public void setMinOverdraftForInterestCalculation(java.math.BigDecimal val) { this.minOverdraftForInterestCalculation = val; } + + public boolean isShareLinkingEnabled() { return shareLinkingEnabled; } + public void setShareLinkingEnabled(boolean shareLinkingEnabled) { this.shareLinkingEnabled = shareLinkingEnabled; } + + public boolean isTerminateTrue() { return terminateTrue; } + public void setTerminateTrue(boolean terminateTrue) { this.terminateTrue = terminateTrue; } + + public void terminate() { + this.terminateTrue = true; + this.statusEnum = 900; + } } diff --git a/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/service/CurrentAccountNumberGenerator.java b/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/service/CurrentAccountNumberGenerator.java index 5b34709bf..ca585310b 100644 --- a/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/service/CurrentAccountNumberGenerator.java +++ b/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/service/CurrentAccountNumberGenerator.java @@ -37,6 +37,31 @@ public class CurrentAccountNumberGenerator { private final JdbcTemplate jdbcTemplate; + private String getNextSequence() { + final java.util.List accountNos = this.jdbcTemplate.query( + "SELECT account_no FROM m_current_account WHERE account_no IS NOT NULL", + (rs, rowNum) -> rs.getString(1)); + + long maxVal = 0; + for (String accNo : accountNos) { + if (accNo != null && accNo.length() >= 5) { + String suffix = accNo.substring(accNo.length() - 5); + try { + long val = Long.parseLong(suffix); + if (val > maxVal) { + maxVal = val; + } + } catch (NumberFormatException ignored) {} + } + } + + String nextSeq = StringUtils.leftPad(String.valueOf(maxVal + 1), 5, '0'); + if (nextSeq.length() > 5) { + nextSeq = nextSeq.substring(nextSeq.length() - 5); + } + return nextSeq; + } + public String generate(CurrentAccount account) { Long productId = account.getProductId(); @@ -59,11 +84,10 @@ public class CurrentAccountNumberGenerator { String officeId = StringUtils.leftPad(account.getOffice().getId().toString(), 2, '0'); if (officeId.length() > 2) officeId = officeId.substring(officeId.length() - 2); - // Account ID — 5 digits - String accountId = StringUtils.leftPad(account.getId().toString(), 5, '0'); - if (accountId.length() > 5) accountId = accountId.substring(accountId.length() - 5); + // Next sequence from existing account numbers + String nextSeq = getNextSequence(); - return CBS_PREFIX + glCode + officeId + accountId; + return CBS_PREFIX + glCode + officeId + nextSeq; } public String preview(Long productId, Long officeId) { @@ -80,13 +104,10 @@ public class CurrentAccountNumberGenerator { String officeStr = StringUtils.leftPad(officeId != null ? officeId.toString() : "0", 2, '0'); if (officeStr.length() > 2) officeStr = officeStr.substring(officeStr.length() - 2); - // Next account ID estimate from m_current_account - Long maxId = jdbcTemplate.query( - "SELECT MAX(id) FROM m_current_account", - rs -> rs.next() ? rs.getLong(1) : 0L); - String nextSeq = StringUtils.leftPad(String.valueOf((maxId != null ? maxId : 0L) + 1), 5, '0'); - if (nextSeq.length() > 5) nextSeq = nextSeq.substring(nextSeq.length() - 5); + // Next sequence from existing account numbers + String nextSeq = getNextSequence(); return CBS_PREFIX + glCode + officeStr + nextSeq; } + } diff --git a/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/service/CurrentAccountWritePlatformService.java b/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/service/CurrentAccountWritePlatformService.java index c3c1a85f9..2556aaf46 100644 --- a/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/service/CurrentAccountWritePlatformService.java +++ b/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/service/CurrentAccountWritePlatformService.java @@ -40,4 +40,6 @@ public interface CurrentAccountWritePlatformService { CommandProcessingResult modifyApplication(Long accountId, JsonCommand command); CommandProcessingResult deleteApplication(Long accountId); + + CommandProcessingResult terminateAccount(Long accountId, JsonCommand command); } diff --git a/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentproduct/exception/CurrentProductEditPendingApprovalException.java b/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentproduct/exception/CurrentProductEditPendingApprovalException.java new file mode 100644 index 000000000..a8862e301 --- /dev/null +++ b/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentproduct/exception/CurrentProductEditPendingApprovalException.java @@ -0,0 +1,29 @@ +/** + * 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.currentproduct.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class CurrentProductEditPendingApprovalException extends AbstractPlatformDomainRuleException { + + public CurrentProductEditPendingApprovalException(final Long productId) { + super("error.msg.currentproduct.edit.pending.approval", + "Current product with identifier " + productId + " already has an edit pending approval", productId); + } +} diff --git a/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentproduct/exception/CurrentProductNoPendingEditException.java b/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentproduct/exception/CurrentProductNoPendingEditException.java new file mode 100644 index 000000000..a24e442f8 --- /dev/null +++ b/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentproduct/exception/CurrentProductNoPendingEditException.java @@ -0,0 +1,29 @@ +/** + * 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.currentproduct.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class CurrentProductNoPendingEditException extends AbstractPlatformDomainRuleException { + + public CurrentProductNoPendingEditException(final Long productId) { + super("error.msg.currentproduct.no.pending.edit", + "Current product with identifier " + productId + " has no edit pending approval", productId); + } +} diff --git a/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentproduct/exception/CurrentProductSelfApprovalNotAllowedException.java b/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentproduct/exception/CurrentProductSelfApprovalNotAllowedException.java new file mode 100644 index 000000000..4a5c72a86 --- /dev/null +++ b/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentproduct/exception/CurrentProductSelfApprovalNotAllowedException.java @@ -0,0 +1,29 @@ +/** + * 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.currentproduct.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class CurrentProductSelfApprovalNotAllowedException extends AbstractPlatformDomainRuleException { + + public CurrentProductSelfApprovalNotAllowedException(final Long productId) { + super("error.msg.currentproduct.self.approval.not.allowed", + "The maker of the pending edit for current product " + productId + " cannot also approve or reject it", productId); + } +} diff --git a/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentproduct/service/CurrentProductReadPlatformService.java b/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentproduct/service/CurrentProductReadPlatformService.java index 83e9bbd3a..929d2e12f 100644 --- a/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentproduct/service/CurrentProductReadPlatformService.java +++ b/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentproduct/service/CurrentProductReadPlatformService.java @@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.currentproduct.service; import java.util.Collection; import org.apache.fineract.portfolio.savings.data.SavingsProductData; +import org.apache.fineract.portfolio.savings.data.SavingsProductTempData; public interface CurrentProductReadPlatformService { @@ -28,4 +29,8 @@ public interface CurrentProductReadPlatformService { Collection retrieveAllForLookup(); SavingsProductData retrieveOne(Long productId); + + Collection retrieveAllPendingEdits(); + + SavingsProductTempData retrievePendingEdit(Long productId); } diff --git a/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentproduct/service/CurrentProductReadPlatformServiceImpl.java b/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentproduct/service/CurrentProductReadPlatformServiceImpl.java index 0075a381a..08aca6829 100644 --- a/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentproduct/service/CurrentProductReadPlatformServiceImpl.java +++ b/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentproduct/service/CurrentProductReadPlatformServiceImpl.java @@ -42,13 +42,23 @@ import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilit import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilityConfigRepository; import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilityLimit; import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilityLimitRepository; +import org.apache.fineract.portfolio.currentproduct.exception.CurrentProductNoPendingEditException; import org.apache.fineract.portfolio.savings.data.SavingsProductData; +import org.apache.fineract.portfolio.savings.data.SavingsProductTempData; import org.apache.fineract.portfolio.savings.exception.SavingsProductNotFoundException; import org.apache.fineract.portfolio.savings.service.SavingsEnumerations; import org.apache.fineract.portfolio.tax.data.TaxGroupData; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEdit; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedFieldRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditChargeRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMappingRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFacilityLimitRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFieldMapper; +import java.util.Set; @RequiredArgsConstructor public class CurrentProductReadPlatformServiceImpl implements CurrentProductReadPlatformService { @@ -58,6 +68,11 @@ public class CurrentProductReadPlatformServiceImpl implements CurrentProductRead private final CurrentProductFacilityConfigRepository facilityConfigRepository; private final CurrentProductFacilityLimitRepository facilityLimitRepository; private final CurrentProductDepositLimitKycRepository depositLimitKycRepository; + private final ProductPendingEditRepository productPendingEditRepository; + private final ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository; + private final ProductPendingEditChargeRepository pendingEditChargeRepository; + private final ProductPendingEditGlMappingRepository pendingEditGlMappingRepository; + private final ProductPendingEditFacilityLimitRepository pendingEditFacilityLimitRepository; private final CurrentProductMapper currentProductRowMapper = new CurrentProductMapper(); private final CurrentProductLookupMapper currentProductLookupsRowMapper = new CurrentProductLookupMapper(); @@ -147,6 +162,64 @@ public class CurrentProductReadPlatformServiceImpl implements CurrentProductRead } } + private static final String PENDING_EDIT_SCHEMA = "spt.id as tempId, spt.product_id as savingsProductId, " + + "cp.name as savingsProductName, spt.createdby_id as makerId, maker.username as makerUsername, " + + "spt.lastmodifiedby_id as checkerId, checker.username as checkerUsername, spt.created_date as createdOn, " + + "spt.lastmodified_date as checkedOn, spt.status as status, spt.reject_reason as rejectReason " + + "from m_saving_product_temp spt " + + "join m_current_product cp on cp.id = spt.product_id " + + "join m_appuser maker on maker.id = spt.createdby_id " + + "left join m_appuser checker on checker.id = spt.lastmodifiedby_id " + + "where spt.product_type = 'CURRENT' "; + + private RowMapper getPendingEditRowMapper() { + return (rs, rowNum) -> { + final SavingsProductTempData data = new SavingsProductTempData(); + data.setId(rs.getLong("tempId")); + data.setSavingsProductId(rs.getLong("savingsProductId")); + data.setSavingsProductName(rs.getString("savingsProductName")); + data.setMakerId(rs.getLong("makerId")); + data.setMakerUsername(rs.getString("makerUsername")); + data.setCheckerId(JdbcSupport.getLong(rs, "checkerId")); + data.setCheckerUsername(rs.getString("checkerUsername")); + data.setCreatedOn(rs.getTimestamp("createdOn") == null ? null : rs.getTimestamp("createdOn").toLocalDateTime()); + data.setCheckedOn(rs.getTimestamp("checkedOn") == null ? null : rs.getTimestamp("checkedOn").toLocalDateTime()); + data.setStatus(rs.getString("status")); + data.setRejectReason(rs.getString("rejectReason")); + + final Long tempId = data.getId(); + final java.util.Optional opt = this.productPendingEditRepository.findById(tempId); + if (opt.isPresent()) { + final ProductPendingEdit pendingEdit = opt.get(); + final Set submittedFields = new java.util.HashSet<>( + this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId())); + final Map proposedChanges = ProductPendingEditFieldMapper.reconstructProposedChangesMap( + pendingEdit, submittedFields, this.pendingEditChargeRepository, this.pendingEditGlMappingRepository, + this.pendingEditFacilityLimitRepository, GSON); + data.setProposedChanges(proposedChanges); + } + return data; + }; + } + + @Override + public Collection retrieveAllPendingEdits() { + this.context.authenticatedUser(); + final String sql = "select " + PENDING_EDIT_SCHEMA + "and spt.status = 'PENDING' order by spt.created_date desc"; + return this.jdbcTemplate.query(sql, getPendingEditRowMapper()); + } + + @Override + public SavingsProductTempData retrievePendingEdit(final Long productId) { + this.context.authenticatedUser(); + final String sql = "select " + PENDING_EDIT_SCHEMA + "and spt.status = 'PENDING' and spt.product_id = ?"; + try { + return this.jdbcTemplate.queryForObject(sql, getPendingEditRowMapper(), new Object[] { productId }); // NOSONAR + } catch (final EmptyResultDataAccessException e) { + throw new CurrentProductNoPendingEditException(productId); + } + } + private static final class CurrentProductMapper implements RowMapper { private final String schemaSql; @@ -197,7 +270,9 @@ public class CurrentProductReadPlatformServiceImpl implements CurrentProductRead sqlBuilder.append("cp.account_closed_zero_balance_in_day_closing as accountClosedZeroBalanceInDayClosing, "); sqlBuilder.append("cp.balance_swap_option as balanceSwapOption, cp.msme as msme, cp.auto_debit_facility as autoDebitFacility, "); sqlBuilder.append("cp.avg_balance_calc_period as avgBalanceCalcPeriod, cp.valid_for_period as validForPeriod, "); - sqlBuilder.append("cp.od_account_closed_zero_balance_in_day_closing as odAccountClosedZeroBalanceInDayClosing "); + sqlBuilder.append("cp.od_account_closed_zero_balance_in_day_closing as odAccountClosedZeroBalanceInDayClosing, "); + sqlBuilder.append("exists(select 1 from m_saving_product_temp spt where spt.product_id = cp.id " + + "and spt.product_type = 'CURRENT' and spt.status = 'PENDING') as isPendingApproval "); sqlBuilder.append("from m_current_product cp "); sqlBuilder.append("join m_currency curr on curr.code = cp.currency_code "); @@ -321,6 +396,7 @@ public class CurrentProductReadPlatformServiceImpl implements CurrentProductRead productData.setAvgBalanceCalcPeriod(rs.getString("avgBalanceCalcPeriod")); productData.setValidForPeriod(rs.getString("validForPeriod")); productData.setOdAccountClosedZeroBalanceInDayClosing(rs.getBoolean("odAccountClosedZeroBalanceInDayClosing")); + productData.setIsPendingApproval(rs.getBoolean("isPendingApproval")); return productData; } diff --git a/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentproduct/service/CurrentProductWritePlatformService.java b/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentproduct/service/CurrentProductWritePlatformService.java index ba2f63af1..9c3accc66 100644 --- a/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentproduct/service/CurrentProductWritePlatformService.java +++ b/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentproduct/service/CurrentProductWritePlatformService.java @@ -28,4 +28,21 @@ public interface CurrentProductWritePlatformService { CommandProcessingResult update(Long productId, JsonCommand command); CommandProcessingResult delete(Long productId); + + /** + * Submits an edit for approval instead of applying it directly. Fails if a pending edit already exists for this + * product (that pending row is itself the lock). + */ + CommandProcessingResult submitEditForApproval(Long productId, JsonCommand command); + + /** + * Replays the stored pending edit through the normal update path and marks it approved. Fails if the approving + * user is the same user who submitted the edit. + */ + CommandProcessingResult approveEdit(Long productId); + + /** + * Discards the pending edit. Fails if the rejecting user is the same user who submitted the edit. + */ + CommandProcessingResult rejectEdit(Long productId, String rejectReason); } diff --git a/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentproduct/service/CurrentProductWritePlatformServiceJpaRepositoryImpl.java b/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentproduct/service/CurrentProductWritePlatformServiceJpaRepositoryImpl.java index eed0dfb31..7ef44cd2c 100644 --- a/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentproduct/service/CurrentProductWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentproduct/service/CurrentProductWritePlatformServiceJpaRepositoryImpl.java @@ -31,6 +31,9 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.fineract.accounting.producttoaccountmapping.service.ProductToGLAccountMappingWritePlatformService; +import org.apache.fineract.commands.domain.CommandProcessingResultType; +import org.apache.fineract.commands.domain.CommandSource; +import org.apache.fineract.commands.domain.CommandSourceRepository; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; @@ -38,6 +41,8 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuild import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; import org.apache.fineract.infrastructure.core.exception.ErrorHandler; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.portfolio.charge.domain.Charge; import org.apache.fineract.portfolio.currentproduct.data.CurrentProductDataValidator; @@ -50,10 +55,26 @@ import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilit import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilityLimit; import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilityLimitRepository; import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductRepository; +import org.apache.fineract.portfolio.currentproduct.exception.CurrentProductEditPendingApprovalException; +import org.apache.fineract.portfolio.currentproduct.exception.CurrentProductNoPendingEditException; +import org.apache.fineract.portfolio.currentproduct.exception.CurrentProductSelfApprovalNotAllowedException; import org.apache.fineract.portfolio.savings.DepositsApiConstants; import org.apache.fineract.portfolio.savings.DepositAccountType; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEdit; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditRepository; import org.apache.fineract.portfolio.savings.exception.SavingsProductNotFoundException; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedField; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedFieldRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditCharge; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditChargeRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFacilityLimit; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFacilityLimitRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMapping; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMappingRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFieldMapper; +import com.google.gson.JsonObject; import org.apache.fineract.portfolio.tax.domain.TaxGroup; +import org.apache.fineract.useradministration.domain.AppUser; import org.springframework.dao.DataAccessException; import org.springframework.transaction.annotation.Transactional; @@ -61,6 +82,7 @@ import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor public class CurrentProductWritePlatformServiceJpaRepositoryImpl implements CurrentProductWritePlatformService { + private static final com.google.gson.Gson GSON = new com.google.gson.Gson(); private final PlatformSecurityContext context; private final CurrentProductRepository currentProductRepository; private final CurrentProductDataValidator fromApiJsonDataValidator; @@ -69,6 +91,13 @@ public class CurrentProductWritePlatformServiceJpaRepositoryImpl implements Curr private final CurrentProductFacilityConfigRepository facilityConfigRepository; private final CurrentProductFacilityLimitRepository facilityLimitRepository; private final CurrentProductDepositLimitKycRepository depositLimitKycRepository; + private final ProductPendingEditRepository productPendingEditRepository; + private final ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository; + private final ProductPendingEditChargeRepository pendingEditChargeRepository; + private final ProductPendingEditFacilityLimitRepository pendingEditFacilityLimitRepository; + private final ProductPendingEditGlMappingRepository pendingEditGlMappingRepository; + private final FromJsonHelper fromApiJsonHelper; + private final CommandSourceRepository commandSourceRepository; private void handleDataIntegrityIssues(final JsonCommand command, final Throwable realCause, final Exception dae) { String msgCode = "error.msg." + DepositsApiConstants.CURRENT_PRODUCT_RESOURCE_NAME; @@ -251,6 +280,222 @@ public class CurrentProductWritePlatformServiceJpaRepositoryImpl implements Curr } } + @Transactional + @Override + public CommandProcessingResult submitEditForApproval(final Long productId, final JsonCommand command) { + + final AppUser maker = this.context.authenticatedUser(); + final CurrentProduct product = this.currentProductRepository.findById(productId) + .orElseThrow(() -> new SavingsProductNotFoundException(productId)); + + if (this.productPendingEditRepository + .findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_CURRENT, productId, ProductPendingEdit.STATUS_PENDING) + .isPresent()) { + throw new CurrentProductEditPendingApprovalException(productId); + } + + this.fromApiJsonDataValidator.validateForUpdate(command.json(), product); + + final ProductPendingEdit pendingEdit = new ProductPendingEdit(ProductPendingEdit.TYPE_CURRENT, productId, maker); + + final java.util.Set submitted = ProductPendingEditFieldMapper.populateCoreFields(command, pendingEdit.getCoreFields()); + final java.util.Set currentOnlySubmitted = ProductPendingEditFieldMapper.populateCurrentOnlyFields(command, pendingEdit.getCurrentOnlyFields()); + final boolean facilityConfigSubmitted = ProductPendingEditFieldMapper.populateFacilityConfig(command, pendingEdit.getFacilityConfigFields()); + final boolean depositLimitKycSubmitted = ProductPendingEditFieldMapper.populateDepositLimitKyc(command, pendingEdit.getDepositLimitKycFields()); + final boolean glMappingSubmitted = ProductPendingEditFieldMapper.populateGlAccountMapping(command, pendingEdit.getGlAccountMappingFields()); + + if (!currentOnlySubmitted.isEmpty()) { + submitted.add("avgBalanceCalcPeriod"); + submitted.add("validForPeriod"); + submitted.add("odAccountClosedZeroBalanceInDayClosing"); + } + if (facilityConfigSubmitted) { + submitted.add("accountFacilityConfig"); + } + if (depositLimitKycSubmitted) { + submitted.add("depositLimitKyc"); + } + if (glMappingSubmitted) { + submitted.add("glAccountMapping"); + } + + final CommandSource auditEntry = CommandSource.builder() // + .actionName("UPDATE") // + .entityName("CURRENTPRODUCT") // + .resourceGetUrl("/currentproducts/" + productId) // + .resourceId(productId) // + .commandAsJson(command.json()) // + .maker(maker) // + .madeOnDate(DateUtils.getAuditOffsetDateTime()) // + .status(CommandProcessingResultType.AWAITING_APPROVAL.getValue()) // + .idempotencyKey(java.util.UUID.randomUUID().toString()) // + .sanitized(false) // + .build(); + this.commandSourceRepository.saveAndFlush(auditEntry); + pendingEdit.setCommandSourceId(auditEntry.getId()); + + this.productPendingEditRepository.saveAndFlush(pendingEdit); + + for (final String fieldName : submitted) { + this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, fieldName)); + } + + final List chargeIds = ProductPendingEditFieldMapper.extractChargeIds(command); + if (chargeIds != null) { + this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, "charges")); + for (final Long chargeId : chargeIds) { + this.pendingEditChargeRepository.save(new ProductPendingEditCharge(pendingEdit, chargeId)); + } + } + + final List facilityLimits = ProductPendingEditFieldMapper.extractFacilityLimits(command, pendingEdit); + if (facilityLimits != null) { + this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, "accountFacilityLimits")); + this.pendingEditFacilityLimitRepository.saveAll(facilityLimits); + } + + final List glMappings = ProductPendingEditFieldMapper.extractGlArrayMappings(command, pendingEdit); + if (glMappings != null) { + this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, "glArrayMappings")); + this.pendingEditGlMappingRepository.saveAll(glMappings); + } + + return new CommandProcessingResultBuilder() // + .withEntityId(product.getId()) // + .build(); + } + + @Transactional + @Override + public CommandProcessingResult approveEdit(final Long productId) { + + final AppUser checker = this.context.authenticatedUser(); + final ProductPendingEdit pendingEdit = this.productPendingEditRepository + .findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_CURRENT, productId, ProductPendingEdit.STATUS_PENDING) + .orElseThrow(() -> new CurrentProductNoPendingEditException(productId)); + + if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) { + throw new CurrentProductSelfApprovalNotAllowedException(productId); + } + + final Set submittedFields = new java.util.HashSet<>( + this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId())); + final JsonObject root = new JsonObject(); + root.addProperty("locale", "en"); + root.addProperty("dateFormat", "yyyy-MM-dd"); + ProductPendingEditFieldMapper.writeCoreFieldsToJson(root, pendingEdit.getCoreFields(), submittedFields); + ProductPendingEditFieldMapper.writeCurrentOnlyFieldsToJson(root, pendingEdit.getCurrentOnlyFields(), submittedFields); + ProductPendingEditFieldMapper.writeFacilityConfigToJson(root, pendingEdit.getFacilityConfigFields(), + submittedFields.contains("accountFacilityConfig")); + ProductPendingEditFieldMapper.writeDepositLimitKycToJson(root, pendingEdit.getDepositLimitKycFields(), + submittedFields.contains("depositLimitKyc")); + if (submittedFields.contains("glAccountMapping")) { + ProductPendingEditFieldMapper.writeGlAccountMappingToJson(root, pendingEdit.getGlAccountMappingFields()); + } + if (submittedFields.contains("charges")) { + ProductPendingEditFieldMapper.writeChargesToJson(root, + this.pendingEditChargeRepository.findByPendingEditId(pendingEdit.getId())); + } + if (submittedFields.contains("accountFacilityLimits")) { + ProductPendingEditFieldMapper.writeFacilityLimitsToJson(root, + this.pendingEditFacilityLimitRepository.findByPendingEditId(pendingEdit.getId())); + } + if (submittedFields.contains("glArrayMappings")) { + ProductPendingEditFieldMapper.writeGlArrayMappingsToJson(root, + this.pendingEditGlMappingRepository.findByPendingEditId(pendingEdit.getId())); + } + final JsonCommand replayedCommand = JsonCommand.fromJsonElement(productId, root, this.fromApiJsonHelper); + final CommandProcessingResult result = update(productId, replayedCommand); + + pendingEdit.markApproved(checker); + this.productPendingEditRepository.saveAndFlush(pendingEdit); + + final CommandSource auditEntry = findOrCreateAuditEntry(pendingEdit, productId); + auditEntry.markAsChecked(checker); + auditEntry.updateForAudit(result); + this.commandSourceRepository.saveAndFlush(auditEntry); + + return result; + } + + private CommandSource findOrCreateAuditEntry(final ProductPendingEdit pendingEdit, final Long productId) { + if (pendingEdit.getCommandSourceId() != null) { + final java.util.Optional existing = this.commandSourceRepository.findById(pendingEdit.getCommandSourceId()); + if (existing.isPresent()) { + return existing.get(); + } + } + final Set submittedFields = new java.util.HashSet<>( + this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId())); + final JsonObject root = new JsonObject(); + root.addProperty("locale", "en"); + root.addProperty("dateFormat", "yyyy-MM-dd"); + ProductPendingEditFieldMapper.writeCoreFieldsToJson(root, pendingEdit.getCoreFields(), submittedFields); + ProductPendingEditFieldMapper.writeCurrentOnlyFieldsToJson(root, pendingEdit.getCurrentOnlyFields(), submittedFields); + ProductPendingEditFieldMapper.writeFacilityConfigToJson(root, pendingEdit.getFacilityConfigFields(), + submittedFields.contains("accountFacilityConfig")); + ProductPendingEditFieldMapper.writeDepositLimitKycToJson(root, pendingEdit.getDepositLimitKycFields(), + submittedFields.contains("depositLimitKyc")); + if (submittedFields.contains("glAccountMapping")) { + ProductPendingEditFieldMapper.writeGlAccountMappingToJson(root, pendingEdit.getGlAccountMappingFields()); + } + if (submittedFields.contains("charges")) { + ProductPendingEditFieldMapper.writeChargesToJson(root, + this.pendingEditChargeRepository.findByPendingEditId(pendingEdit.getId())); + } + if (submittedFields.contains("accountFacilityLimits")) { + ProductPendingEditFieldMapper.writeFacilityLimitsToJson(root, + this.pendingEditFacilityLimitRepository.findByPendingEditId(pendingEdit.getId())); + } + if (submittedFields.contains("glArrayMappings")) { + ProductPendingEditFieldMapper.writeGlArrayMappingsToJson(root, + this.pendingEditGlMappingRepository.findByPendingEditId(pendingEdit.getId())); + } + final String commandJson = GSON.toJson(root); + + final CommandSource auditEntry = CommandSource.builder() // + .actionName("UPDATE") // + .entityName("CURRENTPRODUCT") // + .resourceGetUrl("/currentproducts/" + productId) // + .resourceId(productId) // + .commandAsJson(commandJson) // + .maker(pendingEdit.getMaker()) // + .madeOnDate(DateUtils.getAuditOffsetDateTime()) // + .status(CommandProcessingResultType.AWAITING_APPROVAL.getValue()) // + .idempotencyKey(java.util.UUID.randomUUID().toString()) // + .sanitized(false) // + .build(); + this.commandSourceRepository.saveAndFlush(auditEntry); + pendingEdit.setCommandSourceId(auditEntry.getId()); + this.productPendingEditRepository.saveAndFlush(pendingEdit); + return auditEntry; + } + + @Transactional + @Override + public CommandProcessingResult rejectEdit(final Long productId, final String rejectReason) { + + final AppUser checker = this.context.authenticatedUser(); + final ProductPendingEdit pendingEdit = this.productPendingEditRepository + .findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_CURRENT, productId, ProductPendingEdit.STATUS_PENDING) + .orElseThrow(() -> new CurrentProductNoPendingEditException(productId)); + + if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) { + throw new CurrentProductSelfApprovalNotAllowedException(productId); + } + + pendingEdit.markRejected(checker, rejectReason); + this.productPendingEditRepository.saveAndFlush(pendingEdit); + + final CommandSource auditEntry = findOrCreateAuditEntry(pendingEdit, productId); + auditEntry.markAsRejected(checker); + this.commandSourceRepository.saveAndFlush(auditEntry); + + return new CommandProcessingResultBuilder() // + .withEntityId(productId) // + .build(); + } + @Transactional @Override public CommandProcessingResult delete(final Long productId) { diff --git a/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentproduct/starter/CurrentProductConfiguration.java b/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentproduct/starter/CurrentProductConfiguration.java index 27afeccfe..8b7af3502 100644 --- a/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentproduct/starter/CurrentProductConfiguration.java +++ b/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentproduct/starter/CurrentProductConfiguration.java @@ -19,7 +19,14 @@ package org.apache.fineract.portfolio.currentproduct.starter; import org.apache.fineract.accounting.producttoaccountmapping.service.ProductToGLAccountMappingWritePlatformService; +import org.apache.fineract.commands.domain.CommandSourceRepository; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedFieldRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditChargeRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFacilityLimitRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMappingRepository; import org.apache.fineract.portfolio.currentproduct.data.CurrentProductDataValidator; import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductAssembler; import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductDepositLimitKycRepository; @@ -43,9 +50,17 @@ public class CurrentProductConfiguration { public CurrentProductReadPlatformService currentProductReadPlatformService(PlatformSecurityContext context, JdbcTemplate jdbcTemplate, CurrentProductFacilityConfigRepository currentFacilityConfigRepository, CurrentProductFacilityLimitRepository currentFacilityLimitRepository, - CurrentProductDepositLimitKycRepository currentDepositLimitKycRepository) { + CurrentProductDepositLimitKycRepository currentDepositLimitKycRepository, + ProductPendingEditRepository productPendingEditRepository, + ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository, + ProductPendingEditChargeRepository pendingEditChargeRepository, + ProductPendingEditGlMappingRepository pendingEditGlMappingRepository, + ProductPendingEditFacilityLimitRepository pendingEditFacilityLimitRepository) { return new CurrentProductReadPlatformServiceImpl(context, jdbcTemplate, currentFacilityConfigRepository, - currentFacilityLimitRepository, currentDepositLimitKycRepository); + currentFacilityLimitRepository, currentDepositLimitKycRepository, + productPendingEditRepository, pendingEditSubmittedFieldRepository, + pendingEditChargeRepository, pendingEditGlMappingRepository, + pendingEditFacilityLimitRepository); } @Bean @@ -56,9 +71,18 @@ public class CurrentProductConfiguration { ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService, CurrentProductFacilityConfigRepository currentFacilityConfigRepository, CurrentProductFacilityLimitRepository currentFacilityLimitRepository, - CurrentProductDepositLimitKycRepository currentDepositLimitKycRepository) { + CurrentProductDepositLimitKycRepository currentDepositLimitKycRepository, + ProductPendingEditRepository productPendingEditRepository, + ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository, + ProductPendingEditChargeRepository pendingEditChargeRepository, + ProductPendingEditFacilityLimitRepository pendingEditFacilityLimitRepository, + ProductPendingEditGlMappingRepository pendingEditGlMappingRepository, + FromJsonHelper fromApiJsonHelper, + CommandSourceRepository commandSourceRepository) { return new CurrentProductWritePlatformServiceJpaRepositoryImpl(context, currentProductRepository, currentProductDataValidator, currentProductAssembler, accountMappingWritePlatformService, currentFacilityConfigRepository, - currentFacilityLimitRepository, currentDepositLimitKycRepository); + currentFacilityLimitRepository, currentDepositLimitKycRepository, productPendingEditRepository, + pendingEditSubmittedFieldRepository, pendingEditChargeRepository, pendingEditFacilityLimitRepository, + pendingEditGlMappingRepository, fromApiJsonHelper, commandSourceRepository); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/teller/service/TellerManagementReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/teller/service/TellerManagementReadPlatformServiceImpl.java index aa694751a..2f05b3d4a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/organisation/teller/service/TellerManagementReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/teller/service/TellerManagementReadPlatformServiceImpl.java @@ -120,7 +120,7 @@ public class TellerManagementReadPlatformServiceImpl implements TellerManagement try { final TellerMapper tm = new TellerMapper(); - final String sql = "select " + tm.schema() + " where t.id = ?"; + final String sql = "select " + tm.schema() + " where t.id = ? and t.is_deleted = false"; return this.jdbcTemplate.queryForObject(sql, tm, new Object[] { tellerId }); // NOSONAR } catch (final EmptyResultDataAccessException e) { @@ -198,7 +198,7 @@ public class TellerManagementReadPlatformServiceImpl implements TellerManagement hierarchySearchString = hierarchy + "%"; } final TellerMapper tm = new TellerMapper(); - final String sql = "select " + tm.schema() + "where o.hierarchy like ? order by o.hierarchy"; + final String sql = "select " + tm.schema() + "where o.hierarchy like ? and t.is_deleted = false order by o.hierarchy"; return this.jdbcTemplate.query(sql, tm, new Object[] { hierarchySearchString }); // NOSONAR } diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/teller/service/TellerWritePlatformServiceJpaImpl.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/teller/service/TellerWritePlatformServiceJpaImpl.java index 0561cca45..a2475b4c2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/organisation/teller/service/TellerWritePlatformServiceJpaImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/teller/service/TellerWritePlatformServiceJpaImpl.java @@ -46,6 +46,7 @@ import org.apache.fineract.organisation.teller.domain.Teller; import org.apache.fineract.organisation.teller.domain.TellerRepositoryWrapper; import org.apache.fineract.organisation.teller.exception.CashierExistForTellerException; import org.apache.fineract.organisation.teller.exception.CashierNotFoundException; +import org.apache.fineract.organisation.teller.exception.TellerAlreadyExistsException; import org.apache.fineract.organisation.teller.serialization.TellerCommandFromApiJsonDeserializer; import org.apache.fineract.useradministration.domain.AppUser; import org.springframework.dao.DataIntegrityViolationException; @@ -71,6 +72,10 @@ public class TellerWritePlatformServiceJpaImpl implements TellerWritePlatformSer try { this.context.authenticatedUser(); + if (this.tellerRepositoryWrapper.existsAny()) { + throw new TellerAlreadyExistsException(); + } + final Long officeId = command.longValueOfParameterNamed("officeId"); this.fromApiJsonDeserializer.validateForCreateAndUpdateTeller(command.json()); @@ -152,22 +157,12 @@ public class TellerWritePlatformServiceJpaImpl implements TellerWritePlatformSer @Override @Transactional public CommandProcessingResult deleteTeller(Long tellerId) { - // TODO Auto-generated method stub - Teller teller = tellerRepositoryWrapper.findOneWithNotFoundDetection(tellerId); - Set isTellerIdPresentInCashier = teller.getCashiers(); - - for (final Cashier tellerIdInCashier : isTellerIdPresentInCashier) { - if (tellerIdInCashier.getTeller().getId().toString().equalsIgnoreCase(tellerId.toString())) { - throw new CashierExistForTellerException(tellerId); - } - - } - tellerRepositoryWrapper.delete(teller); + teller.delete(); + tellerRepositoryWrapper.saveAndFlush(teller); return new CommandProcessingResultBuilder() // .withEntityId(teller.getId()) // .build(); - } /* diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/api/AccountTerminateApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/api/AccountTerminateApiResource.java new file mode 100644 index 000000000..3128afbef --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/api/AccountTerminateApiResource.java @@ -0,0 +1,80 @@ +/** + * 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.account.api; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.commands.domain.CommandWrapper; +import org.apache.fineract.commands.service.CommandWrapperBuilder; +import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import com.google.gson.JsonElement; +import org.springframework.stereotype.Component; + +@Path("/v1/accounts/{accountId}/terminate") +@Component +@Tag(name = "Account Terminate", description = "Unified terminate endpoint for Savings, Current, FD and RD accounts.") +@RequiredArgsConstructor +public class AccountTerminateApiResource { + + private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; + private final DefaultToApiJsonSerializer toApiJsonSerializer; + private final FromJsonHelper fromJsonHelper; + + @POST + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Terminate an account", description = "Terminates a Savings, Current, Fixed Deposit or Recurring Deposit account. Pass accountType in the request body: SAVINGS | CURRENT | FD | RD.") + @ApiResponse(responseCode = "200", description = "OK") + public String terminate(@PathParam("accountId") final Long accountId, + final String apiRequestBodyAsJson) { + + final String jsonBody = StringUtils.isBlank(apiRequestBodyAsJson) ? "{}" : apiRequestBodyAsJson; + + final JsonElement root = this.fromJsonHelper.parse(jsonBody); + final String accountType = this.fromJsonHelper.extractStringNamed("accountType", root); + String entityName = "SAVINGSACCOUNT"; + if ("CURRENT".equalsIgnoreCase(accountType)) { + entityName = "CURRENTACCOUNT"; + } else if ("FD".equalsIgnoreCase(accountType)) { + entityName = "FIXEDDEPOSITACCOUNT"; + } else if ("RD".equalsIgnoreCase(accountType)) { + entityName = "RECURRINGDEPOSITACCOUNT"; + } + + final CommandWrapper commandRequest = new CommandWrapperBuilder() + .withJson(jsonBody) + .terminateAccount(accountId) + .withEntityName(entityName) + .build(); + final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + return toApiJsonSerializer.serialize(result); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/handler/AccountTerminateCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/handler/AccountTerminateCommandHandler.java new file mode 100644 index 000000000..9f6a2553c --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/handler/AccountTerminateCommandHandler.java @@ -0,0 +1,46 @@ +/** + * 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.account.handler; + +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.account.service.AccountTerminateWritePlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@CommandType(entity = "ACCOUNT", action = "TERMINATE") +public class AccountTerminateCommandHandler implements NewCommandSourceHandler { + + private final AccountTerminateWritePlatformService writePlatformService; + + @Autowired + public AccountTerminateCommandHandler(final AccountTerminateWritePlatformService writePlatformService) { + this.writePlatformService = writePlatformService; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + return this.writePlatformService.terminate(command.entityId(), command); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTerminateWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTerminateWritePlatformService.java new file mode 100644 index 000000000..b75bc4787 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTerminateWritePlatformService.java @@ -0,0 +1,27 @@ +/** + * 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.account.service; + +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; + +public interface AccountTerminateWritePlatformService { + + CommandProcessingResult terminate(Long accountId, JsonCommand command); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTerminateWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTerminateWritePlatformServiceImpl.java new file mode 100644 index 000000000..045d24ced --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTerminateWritePlatformServiceImpl.java @@ -0,0 +1,66 @@ +/** + * 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.account.service; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; +import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountWritePlatformService; +import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * Routes terminate to the correct account service based on the + * "accountType" field in the JSON body: + * SAVINGS → m_savings_account (depositType = savings) + * FD → m_savings_account (depositType = 200) + * RD → m_savings_account (depositType = 300) + * CURRENT → m_current_account + */ +@Service +@RequiredArgsConstructor +public class AccountTerminateWritePlatformServiceImpl implements AccountTerminateWritePlatformService { + + private static final String PARAM_ACCOUNT_TYPE = "accountType"; + + private final SavingsAccountWritePlatformService savingsAccountWritePlatformService; + private final CurrentAccountWritePlatformService currentAccountWritePlatformService; + + @Override + @Transactional + public CommandProcessingResult terminate(final Long accountId, final JsonCommand command) { + final String accountType = command.stringValueOfParameterNamed(PARAM_ACCOUNT_TYPE); + + if (accountType == null || accountType.isBlank()) { + throw new PlatformDataIntegrityException( + "error.msg.account.terminate.accountType.required", + "accountType is required: SAVINGS, CURRENT, FD or RD"); + } + + return switch (accountType.toUpperCase()) { + case "CURRENT" -> currentAccountWritePlatformService.terminateAccount(accountId, command); + case "SAVINGS", "FD", "RD" -> savingsAccountWritePlatformService.terminate(accountId, command); + default -> throw new PlatformDataIntegrityException( + "error.msg.account.terminate.accountType.invalid", + "Unknown accountType '" + accountType + "'. Expected SAVINGS, CURRENT, FD or RD."); + }; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/products/api/ProductsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/products/api/ProductsApiResource.java index 17708a967..fb62d7a65 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/products/api/ProductsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/products/api/ProductsApiResource.java @@ -37,20 +37,26 @@ import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.UriInfo; +import java.util.Collection; import lombok.RequiredArgsConstructor; import org.apache.fineract.commands.domain.CommandWrapper; import org.apache.fineract.commands.service.CommandWrapperBuilder; import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper; +import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.exception.ResourceNotFoundException; import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.service.Page; +import org.apache.fineract.infrastructure.security.exception.NoAuthorizationException; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.portfolio.products.constants.ProductsApiConstants; import org.apache.fineract.portfolio.products.data.ProductData; import org.apache.fineract.portfolio.products.service.ShareProductReadPlatformService; +import org.apache.fineract.portfolio.savings.data.SavingsProductTempData; +import org.apache.fineract.portfolio.shareproducts.service.ShareProductWritePlatformService; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; @@ -61,11 +67,26 @@ import org.springframework.stereotype.Component; @RequiredArgsConstructor public class ProductsApiResource { + private static final String SHARE_PRODUCT_TYPE = "share"; + /** + * These product types have their own dedicated endpoints (/savingsproducts/{id}, /currentproducts/{id}, + * /fixeddepositproducts/{id}, /recurringdepositproducts/{id}) which correctly route edits through + * submitEditForApproval - the pending-edit row is the lock. This generic /v1/products/{type}/{productId} PUT + * falls through to the native command-processing pipeline for any type not special-cased above, which would + * bypass that lock entirely for these types. The Angular frontend never calls this generic path for them (only + * for "share"), so blocking them here closes the bypass without removing any real functionality. + */ + private static final java.util.Set PRODUCT_TYPES_WITH_DEDICATED_LOCKED_ENDPOINT = java.util.Set.of("savings", "current", + "fixeddeposit", "recurringdeposit"); + private final ApplicationContext applicationContext; private final ApiRequestParameterHelper apiRequestParameterHelper; private final DefaultToApiJsonSerializer toApiJsonSerializer; private final PlatformSecurityContext platformSecurityContext; private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; + private final ShareProductWritePlatformService shareProductWritePlatformService; + private final FromJsonHelper fromApiJsonHelper; + private final DefaultToApiJsonSerializer tempDataApiJsonSerializer; @GET @Path("template") @@ -171,9 +192,100 @@ public class ProductsApiResource { @PathParam("productId") @Parameter(description = "productId") final Long productId, @Parameter(hidden = true) final String apiRequestBodyAsJson) { this.platformSecurityContext.authenticatedUser(); + + if (SHARE_PRODUCT_TYPE.equalsIgnoreCase(productType)) { + // Bypasses the native command-source/maker-checker pipeline by design, same as Savings/Current + // Products: edits are held in m_savings_product_temp (a pending row there is itself the lock) and + // only applied to m_share_product on approval - see the pending/approve/reject endpoints below. + final JsonCommand command = JsonCommand.fromJsonElement(productId, this.fromApiJsonHelper.parse(apiRequestBodyAsJson), + this.fromApiJsonHelper); + final CommandProcessingResult result = this.shareProductWritePlatformService.submitEditForApproval(productId, command); + return this.toApiJsonSerializer.serialize(result); + } + + if (PRODUCT_TYPES_WITH_DEDICATED_LOCKED_ENDPOINT.contains(productType.toLowerCase(java.util.Locale.ROOT))) { + throw new ResourceNotFoundException("error.msg.product.generic.update.not.supported", + "Updating a " + productType + " product through this generic endpoint is not supported - use its dedicated endpoint", + new Object[] { productType }); + } + final CommandWrapper commandRequest = new CommandWrapperBuilder().updateProduct(productType, productId) .withJson(apiRequestBodyAsJson).build(); final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); return this.toApiJsonSerializer.serialize(result); } + + @GET + @Path("pending") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "List Share Products pending edit approval") + public String retrieveAllPending(@PathParam("type") @Parameter(description = "type") final String productType, + @Context final UriInfo uriInfo) { + this.platformSecurityContext.authenticatedUser(); + try { + String serviceName = productType + ProductsApiConstants.READPLATFORM_NAME; + ShareProductReadPlatformService service = (ShareProductReadPlatformService) this.applicationContext.getBean(serviceName); + final Collection pendingEdits = service.retrieveAllPendingEdits(); + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.tempDataApiJsonSerializer.serialize(settings, pendingEdits); + } catch (BeansException e) { + throw new ResourceNotFoundException(e); + } + } + + @GET + @Path("{productId}/pending") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve the pending edit for a Share Product") + public String retrievePending(@PathParam("type") @Parameter(description = "type") final String productType, + @PathParam("productId") @Parameter(description = "productId") final Long productId, @Context final UriInfo uriInfo) { + this.platformSecurityContext.authenticatedUser(); + try { + String serviceName = productType + ProductsApiConstants.READPLATFORM_NAME; + ShareProductReadPlatformService service = (ShareProductReadPlatformService) this.applicationContext.getBean(serviceName); + final SavingsProductTempData pendingEdit = service.retrievePendingEdit(productId); + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.tempDataApiJsonSerializer.serialize(settings, pendingEdit); + } catch (BeansException e) { + throw new ResourceNotFoundException(e); + } + } + + @POST + @Path("{productId}/approve") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Approve a pending Share Product edit") + public String approvePending(@PathParam("type") @Parameter(description = "type") final String productType, + @PathParam("productId") @Parameter(description = "productId") final Long productId) { + if (this.platformSecurityContext.authenticatedUser().hasNotPermissionForAnyOf("APPROVE_SHAREPRODUCT")) { + throw new NoAuthorizationException("User has no authority to approve share products"); + } + final CommandProcessingResult result = this.shareProductWritePlatformService.approveEdit(productId); + return this.toApiJsonSerializer.serialize(result); + } + + @POST + @Path("{productId}/reject") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Reject a pending Share Product edit") + public String rejectPending(@PathParam("type") @Parameter(description = "type") final String productType, + @PathParam("productId") @Parameter(description = "productId") final Long productId, + @Parameter(hidden = true) final String apiRequestBodyAsJson) { + if (this.platformSecurityContext.authenticatedUser().hasNotPermissionForAnyOf("REJECT_SHAREPRODUCT")) { + throw new NoAuthorizationException("User has no authority to reject share products"); + } + String rejectReason = null; + if (apiRequestBodyAsJson != null && !apiRequestBodyAsJson.isBlank()) { + final var parsed = this.fromApiJsonHelper.parse(apiRequestBodyAsJson); + if (parsed != null && parsed.isJsonObject() && parsed.getAsJsonObject().has("rejectReason")) { + rejectReason = parsed.getAsJsonObject().get("rejectReason").getAsString(); + } + } + final CommandProcessingResult result = this.shareProductWritePlatformService.rejectEdit(productId, rejectReason); + return this.toApiJsonSerializer.serialize(result); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/products/service/ShareProductReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/products/service/ShareProductReadPlatformService.java index 9c9d15ee2..84d9b6289 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/products/service/ShareProductReadPlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/products/service/ShareProductReadPlatformService.java @@ -22,6 +22,7 @@ import java.util.Collection; import java.util.Set; import org.apache.fineract.infrastructure.core.service.Page; import org.apache.fineract.portfolio.products.data.ProductData; +import org.apache.fineract.portfolio.savings.data.SavingsProductTempData; public interface ShareProductReadPlatformService { @@ -34,4 +35,8 @@ public interface ShareProductReadPlatformService { Set getResponseDataParams(); Collection retrieveAllForLookup(); + + Collection retrieveAllPendingEdits(); + + SavingsProductTempData retrievePendingEdit(Long productId); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/CurrentAccountsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/CurrentAccountsApiResource.java index ccaf3ff11..a0310f85a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/CurrentAccountsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/CurrentAccountsApiResource.java @@ -53,6 +53,9 @@ import org.apache.fineract.infrastructure.security.service.SqlValidator; import org.apache.fineract.portfolio.savings.DepositsApiConstants; import org.apache.fineract.portfolio.savings.data.SavingsAccountData; import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountReadPlatformService; +import org.apache.fineract.commands.domain.CommandWrapper; +import org.apache.fineract.commands.service.CommandWrapperBuilder; +import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountWritePlatformService; import org.apache.fineract.portfolio.savings.service.CurrentAccountTemplateReadPlatformService; import org.springframework.stereotype.Component; @@ -72,6 +75,7 @@ public class CurrentAccountsApiResource { private final SqlValidator sqlValidator; private final CurrentAccountWritePlatformService currentAccountWritePlatformService; private final CurrentAccountReadPlatformService currentAccountReadPlatformService; + private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; @GET @Path("template") @@ -125,10 +129,11 @@ public class CurrentAccountsApiResource { @Operation(summary = "Submit new current account application") @ApiResponse(responseCode = "200", description = "OK") public String submitApplication(@Parameter(hidden = true) final String apiRequestBodyAsJson) { - final org.apache.fineract.infrastructure.core.api.JsonCommand command = - org.apache.fineract.infrastructure.core.api.JsonCommand.fromJsonElement(null, - JsonParser.parseString(apiRequestBodyAsJson != null ? apiRequestBodyAsJson : "{}")); - final CommandProcessingResult result = currentAccountWritePlatformService.submitApplication(command); + final CommandWrapper commandRequest = new CommandWrapperBuilder() // + .createCurrentAccount() // + .withJson(apiRequestBodyAsJson) // + .build(); + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); return toApiJsonSerializer.serialize(result); } @@ -157,11 +162,11 @@ public class CurrentAccountsApiResource { public String update(@PathParam("accountId") final Long accountId, @Parameter(hidden = true) final String apiRequestBodyAsJson, @QueryParam("command") final String commandParam) { - - final org.apache.fineract.infrastructure.core.api.JsonCommand command = - org.apache.fineract.infrastructure.core.api.JsonCommand.fromJsonElement(null, - JsonParser.parseString(apiRequestBodyAsJson != null ? apiRequestBodyAsJson : "{}")); - final CommandProcessingResult result = currentAccountWritePlatformService.modifyApplication(accountId, command); + final CommandWrapper commandRequest = new CommandWrapperBuilder() // + .updateCurrentAccount(accountId) // + .withJson(apiRequestBodyAsJson) // + .build(); + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); return toApiJsonSerializer.serialize(result); } @@ -180,30 +185,29 @@ public class CurrentAccountsApiResource { jsonApiRequest = "{}"; } - final org.apache.fineract.infrastructure.core.api.JsonCommand command = - org.apache.fineract.infrastructure.core.api.JsonCommand.fromJsonElement(null, - JsonParser.parseString(jsonApiRequest)); + final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(jsonApiRequest); - CommandProcessingResult result = null; + CommandWrapper commandRequest = null; if (is(commandParam, "reject")) { - result = currentAccountWritePlatformService.rejectApplication(accountId, command); + commandRequest = builder.rejectCurrentAccountApplication(accountId).build(); } else if (is(commandParam, "withdrawnByApplicant")) { - result = currentAccountWritePlatformService.withdrawApplication(accountId, command); + commandRequest = builder.withdrawCurrentAccountApplication(accountId).build(); } else if (is(commandParam, "approve")) { - result = currentAccountWritePlatformService.approveApplication(accountId, command); + commandRequest = builder.approveCurrentAccountApplication(accountId).build(); } else if (is(commandParam, "undoapproval")) { - result = currentAccountWritePlatformService.undoApproval(accountId, command); + commandRequest = builder.undoCurrentAccountApplication(accountId).build(); } else if (is(commandParam, "activate")) { - result = currentAccountWritePlatformService.activate(accountId, command); + commandRequest = builder.currentAccountActivation(accountId).build(); } else if (is(commandParam, "close")) { - result = currentAccountWritePlatformService.closeAccount(accountId, command); + commandRequest = builder.closeCurrentAccountApplication(accountId).build(); } - if (result == null) { + if (commandRequest == null) { throw new UnrecognizedQueryParamException("command", commandParam, new Object[] { "reject", "withdrawnByApplicant", "approve", "undoapproval", "activate", "close" }); } + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); return toApiJsonSerializer.serialize(result); } @@ -214,7 +218,10 @@ public class CurrentAccountsApiResource { @Operation(summary = "Delete a current account application") @ApiResponse(responseCode = "200", description = "OK") public String delete(@PathParam("accountId") final Long accountId) { - final CommandProcessingResult result = currentAccountWritePlatformService.deleteApplication(accountId); + final CommandWrapper commandRequest = new CommandWrapperBuilder() // + .deleteCurrentAccount(accountId) // + .build(); + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); return toApiJsonSerializer.serialize(result); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/CurrentProductsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/CurrentProductsApiResource.java index 06082b591..9cbe38744 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/CurrentProductsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/CurrentProductsApiResource.java @@ -53,10 +53,13 @@ import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDoma import org.apache.fineract.infrastructure.codes.data.CodeValueData; import org.apache.fineract.infrastructure.codes.service.CodeValueReadPlatformService; import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper; +import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.EnumOptionData; import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.security.exception.NoAuthorizationException; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.organisation.monetary.service.CurrencyReadPlatformService; @@ -71,7 +74,9 @@ import org.apache.fineract.portfolio.savings.SavingsInterestCalculationDaysInYea import org.apache.fineract.portfolio.savings.SavingsInterestCalculationType; import org.apache.fineract.portfolio.savings.SavingsPostingInterestPeriodType; import org.apache.fineract.portfolio.savings.data.SavingsProductData; +import org.apache.fineract.portfolio.savings.data.SavingsProductTempData; import org.apache.fineract.portfolio.currentproduct.service.CurrentProductReadPlatformService; +import org.apache.fineract.portfolio.currentproduct.service.CurrentProductWritePlatformService; import org.apache.fineract.portfolio.savings.service.SavingsDropdownReadPlatformService; import org.apache.fineract.portfolio.savings.service.SavingsEnumerations; import org.apache.fineract.portfolio.tax.data.TaxGroupData; @@ -101,6 +106,9 @@ public class CurrentProductsApiResource { private final ConfigurationDomainService configurationDomainService; private final CodeValueReadPlatformService codeValueReadPlatformService; private final JdbcTemplate jdbcTemplate; + private final CurrentProductWritePlatformService currentProductWritePlatformService; + private final FromJsonHelper fromApiJsonHelper; + private final DefaultToApiJsonSerializer tempDataApiJsonSerializer; @POST @@ -122,9 +130,72 @@ public class CurrentProductsApiResource { @ApiResponse(responseCode = "200", description = "OK") public String update(@PathParam("productId") final Long productId, @Parameter(hidden = true) final String apiRequestBodyAsJson) { - final CommandWrapper commandRequest = new CommandWrapperBuilder().updateCurrentProduct(productId).withJson(apiRequestBodyAsJson) - .build(); - final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + // Bypasses the native command-source/maker-checker pipeline by design: edits are held in + // m_savings_product_temp (a pending row there is itself the lock) and only applied to m_current_product on + // approval - see /currentproducts/{productId}/approve and /reject below. + this.context.authenticatedUser().validateHasUpdatePermission(DepositsApiConstants.CURRENT_PRODUCT_RESOURCE_NAME); + + final JsonCommand command = JsonCommand.fromJsonElement(productId, this.fromApiJsonHelper.parse(apiRequestBodyAsJson), + this.fromApiJsonHelper); + + final CommandProcessingResult result = this.currentProductWritePlatformService.submitEditForApproval(productId, command); + return toApiJsonSerializer.serialize(result); + } + + @GET + @Path("pending") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "List Current Products pending edit approval") + public String retrieveAllPending(@Context final UriInfo uriInfo) { + this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.CURRENT_PRODUCT_RESOURCE_NAME); + final Collection pendingEdits = this.savingProductReadPlatformService.retrieveAllPendingEdits(); + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.tempDataApiJsonSerializer.serialize(settings, pendingEdits); + } + + @GET + @Path("{productId}/pending") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve the pending edit for a Current Product") + public String retrievePending(@PathParam("productId") final Long productId, @Context final UriInfo uriInfo) { + this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.CURRENT_PRODUCT_RESOURCE_NAME); + final SavingsProductTempData pendingEdit = this.savingProductReadPlatformService.retrievePendingEdit(productId); + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.tempDataApiJsonSerializer.serialize(settings, pendingEdit); + } + + @POST + @Path("{productId}/approve") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Approve a pending Current Product edit") + public String approvePending(@PathParam("productId") final Long productId) { + if (this.context.authenticatedUser().hasNotPermissionForAnyOf("APPROVE_CURRENTPRODUCT")) { + throw new NoAuthorizationException("User has no authority to approve current products"); + } + final CommandProcessingResult result = this.currentProductWritePlatformService.approveEdit(productId); + return toApiJsonSerializer.serialize(result); + } + + @POST + @Path("{productId}/reject") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Reject a pending Current Product edit") + public String rejectPending(@PathParam("productId") final Long productId, @Parameter(hidden = true) final String apiRequestBodyAsJson) { + if (this.context.authenticatedUser().hasNotPermissionForAnyOf("REJECT_CURRENTPRODUCT")) { + throw new NoAuthorizationException("User has no authority to reject current products"); + } + String rejectReason = null; + if (apiRequestBodyAsJson != null && !apiRequestBodyAsJson.isBlank()) { + final var parsed = this.fromApiJsonHelper.parse(apiRequestBodyAsJson); + if (parsed != null && parsed.isJsonObject() && parsed.getAsJsonObject().has("rejectReason")) { + rejectReason = parsed.getAsJsonObject().get("rejectReason").getAsString(); + } + } + final CommandProcessingResult result = this.currentProductWritePlatformService.rejectEdit(productId, rejectReason); return toApiJsonSerializer.serialize(result); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositProductsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositProductsApiResource.java index 9347d544b..a2ed17e02 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositProductsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositProductsApiResource.java @@ -53,14 +53,17 @@ import org.apache.fineract.commands.domain.CommandWrapper; import org.apache.fineract.commands.service.CommandWrapperBuilder; import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper; +import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import java.util.Locale; import org.apache.fineract.infrastructure.core.data.EnumOptionData; import org.apache.fineract.portfolio.savings.SavingsApiConstants; import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.codes.data.CodeValueData; import org.apache.fineract.infrastructure.codes.service.CodeValueReadPlatformService; +import org.apache.fineract.infrastructure.security.exception.NoAuthorizationException; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.organisation.monetary.service.CurrencyReadPlatformService; @@ -78,8 +81,10 @@ import org.apache.fineract.portfolio.savings.SavingsInterestCalculationDaysInYea import org.apache.fineract.portfolio.savings.SavingsInterestCalculationType; import org.apache.fineract.portfolio.savings.SavingsPostingInterestPeriodType; import org.apache.fineract.portfolio.savings.data.FixedDepositProductData; +import org.apache.fineract.portfolio.savings.data.SavingsProductTempData; import org.apache.fineract.portfolio.savings.service.DepositProductReadPlatformService; import org.apache.fineract.portfolio.savings.service.DepositsDropdownReadPlatformService; +import org.apache.fineract.portfolio.savings.service.FixedDepositProductWritePlatformService; import org.apache.fineract.portfolio.savings.service.SavingsDropdownReadPlatformService; import org.apache.fineract.portfolio.savings.service.SavingsEnumerations; import org.apache.fineract.portfolio.tax.data.TaxGroupData; @@ -116,6 +121,9 @@ public class FixedDepositProductsApiResource { private final TaxReadPlatformService taxReadPlatformService; private final CodeValueReadPlatformService codeValueReadPlatformService; private final org.springframework.jdbc.core.JdbcTemplate jdbcTemplate; + private final FixedDepositProductWritePlatformService fixedDepositProductWritePlatformService; + private final FromJsonHelper fromApiJsonHelper; + private final DefaultToApiJsonSerializer tempDataApiJsonSerializer; @POST @Consumes({ MediaType.APPLICATION_JSON }) @@ -151,15 +159,81 @@ public class FixedDepositProductsApiResource { public String update(@PathParam("productId") @Parameter(description = "productId") final Long productId, @Parameter(hidden = true) final String apiRequestBodyAsJson) { - final CommandWrapper commandRequest = new CommandWrapperBuilder().updateFixedDepositProduct(productId) - .withJson(apiRequestBodyAsJson).build(); + // Bypasses the native command-source/maker-checker pipeline by design: edits are held in + // m_savings_product_temp (a pending row there is itself the lock) and only applied to m_savings_product on + // approval - see /fixeddepositproducts/{productId}/approve and /reject below. + this.context.authenticatedUser().validateHasUpdatePermission(DepositsApiConstants.FIXED_DEPOSIT_PRODUCT_RESOURCE_NAME); - final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + final JsonCommand command = JsonCommand.fromJsonElement(productId, this.fromApiJsonHelper.parse(apiRequestBodyAsJson), + this.fromApiJsonHelper); + + final CommandProcessingResult result = this.fixedDepositProductWritePlatformService.submitEditForApproval(productId, command); return this.toApiJsonSerializer.serialize(result); } + @GET + @Path("pending") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "List Fixed Deposit Products pending edit approval") + public String retrieveAllPending(@Context final UriInfo uriInfo) { + this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.FIXED_DEPOSIT_PRODUCT_RESOURCE_NAME); + final Collection pendingEdits = this.depositProductReadPlatformService + .retrieveAllPendingEdits(DepositAccountType.FIXED_DEPOSIT); + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.tempDataApiJsonSerializer.serialize(settings, pendingEdits); + } + + @GET + @Path("{productId}/pending") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve the pending edit for a Fixed Deposit Product") + public String retrievePending(@PathParam("productId") @Parameter(description = "productId") final Long productId, + @Context final UriInfo uriInfo) { + this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.FIXED_DEPOSIT_PRODUCT_RESOURCE_NAME); + final SavingsProductTempData pendingEdit = this.depositProductReadPlatformService + .retrievePendingEdit(DepositAccountType.FIXED_DEPOSIT, productId); + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.tempDataApiJsonSerializer.serialize(settings, pendingEdit); + } + + @POST + @Path("{productId}/approve") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Approve a pending Fixed Deposit Product edit") + public String approvePending(@PathParam("productId") @Parameter(description = "productId") final Long productId) { + if (this.context.authenticatedUser().hasNotPermissionForAnyOf("APPROVE_FIXEDDEPOSITPRODUCT")) { + throw new NoAuthorizationException("User has no authority to approve fixed deposit products"); + } + final CommandProcessingResult result = this.fixedDepositProductWritePlatformService.approveEdit(productId); + return this.toApiJsonSerializer.serialize(result); + } + + @POST + @Path("{productId}/reject") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Reject a pending Fixed Deposit Product edit") + public String rejectPending(@PathParam("productId") @Parameter(description = "productId") final Long productId, + @Parameter(hidden = true) final String apiRequestBodyAsJson) { + if (this.context.authenticatedUser().hasNotPermissionForAnyOf("REJECT_FIXEDDEPOSITPRODUCT")) { + throw new NoAuthorizationException("User has no authority to reject fixed deposit products"); + } + String rejectReason = null; + if (apiRequestBodyAsJson != null && !apiRequestBodyAsJson.isBlank()) { + final var parsed = this.fromApiJsonHelper.parse(apiRequestBodyAsJson); + if (parsed != null && parsed.isJsonObject() && parsed.getAsJsonObject().has("rejectReason")) { + rejectReason = parsed.getAsJsonObject().get("rejectReason").getAsString(); + } + } + final CommandProcessingResult result = this.fixedDepositProductWritePlatformService.rejectEdit(productId, rejectReason); + return this.toApiJsonSerializer.serialize(result); + } + @GET @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/RecurringDepositProductsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/RecurringDepositProductsApiResource.java index 4b0e6d305..28eb2581e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/RecurringDepositProductsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/RecurringDepositProductsApiResource.java @@ -55,10 +55,13 @@ import org.apache.fineract.commands.domain.CommandWrapper; import org.apache.fineract.commands.service.CommandWrapperBuilder; import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper; +import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.EnumOptionData; import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.security.exception.NoAuthorizationException; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.organisation.monetary.service.CurrencyReadPlatformService; @@ -85,8 +88,10 @@ import com.google.gson.JsonPrimitive; import com.google.gson.JsonSerializer; import java.time.LocalDate; import org.apache.fineract.portfolio.savings.data.RecurringDepositProductData; +import org.apache.fineract.portfolio.savings.data.SavingsProductTempData; import org.apache.fineract.portfolio.savings.service.DepositProductReadPlatformService; import org.apache.fineract.portfolio.savings.service.DepositsDropdownReadPlatformService; +import org.apache.fineract.portfolio.savings.service.RecurringDepositProductWritePlatformService; import org.apache.fineract.portfolio.savings.service.SavingsDropdownReadPlatformService; import org.apache.fineract.portfolio.savings.service.SavingsEnumerations; import org.apache.fineract.portfolio.tax.data.TaxGroupData; @@ -126,6 +131,9 @@ public class RecurringDepositProductsApiResource { .registerTypeAdapter(LocalDate.class, (JsonSerializer) (src, typeOfSrc, ctx) -> new JsonPrimitive(src.toString())) .create(); + private final RecurringDepositProductWritePlatformService recurringDepositProductWritePlatformService; + private final FromJsonHelper fromApiJsonHelper; + private final DefaultToApiJsonSerializer tempDataApiJsonSerializer; @POST @Consumes({ MediaType.APPLICATION_JSON }) @@ -158,15 +166,82 @@ public class RecurringDepositProductsApiResource { public String update(@PathParam("productId") @Parameter(description = "productId") final Long productId, @Parameter(hidden = true) final String apiRequestBodyAsJson) { - final CommandWrapper commandRequest = new CommandWrapperBuilder().updateRecurringDepositProduct(productId) - .withJson(apiRequestBodyAsJson).build(); + // Bypasses the native command-source/maker-checker pipeline by design: edits are held in + // m_savings_product_temp (a pending row there is itself the lock) and only applied to m_savings_product on + // approval - see /recurringdepositproducts/{productId}/approve and /reject below. + this.context.authenticatedUser().validateHasUpdatePermission(DepositsApiConstants.RECURRING_DEPOSIT_PRODUCT_RESOURCE_NAME); - final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + final JsonCommand command = JsonCommand.fromJsonElement(productId, this.fromApiJsonHelper.parse(apiRequestBodyAsJson), + this.fromApiJsonHelper); + + final CommandProcessingResult result = this.recurringDepositProductWritePlatformService.submitEditForApproval(productId, + command); return this.toApiJsonSerializer.serialize(result); } + @GET + @Path("pending") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "List Recurring Deposit Products pending edit approval") + public String retrieveAllPending(@Context final UriInfo uriInfo) { + this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.RECURRING_DEPOSIT_PRODUCT_RESOURCE_NAME); + final Collection pendingEdits = this.depositProductReadPlatformService + .retrieveAllPendingEdits(DepositAccountType.RECURRING_DEPOSIT); + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.tempDataApiJsonSerializer.serialize(settings, pendingEdits); + } + + @GET + @Path("{productId}/pending") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve the pending edit for a Recurring Deposit Product") + public String retrievePending(@PathParam("productId") @Parameter(description = "productId") final Long productId, + @Context final UriInfo uriInfo) { + this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.RECURRING_DEPOSIT_PRODUCT_RESOURCE_NAME); + final SavingsProductTempData pendingEdit = this.depositProductReadPlatformService + .retrievePendingEdit(DepositAccountType.RECURRING_DEPOSIT, productId); + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.tempDataApiJsonSerializer.serialize(settings, pendingEdit); + } + + @POST + @Path("{productId}/approve") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Approve a pending Recurring Deposit Product edit") + public String approvePending(@PathParam("productId") @Parameter(description = "productId") final Long productId) { + if (this.context.authenticatedUser().hasNotPermissionForAnyOf("APPROVE_RECURRINGDEPOSITPRODUCT")) { + throw new NoAuthorizationException("User has no authority to approve recurring deposit products"); + } + final CommandProcessingResult result = this.recurringDepositProductWritePlatformService.approveEdit(productId); + return this.toApiJsonSerializer.serialize(result); + } + + @POST + @Path("{productId}/reject") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Reject a pending Recurring Deposit Product edit") + public String rejectPending(@PathParam("productId") @Parameter(description = "productId") final Long productId, + @Parameter(hidden = true) final String apiRequestBodyAsJson) { + if (this.context.authenticatedUser().hasNotPermissionForAnyOf("REJECT_RECURRINGDEPOSITPRODUCT")) { + throw new NoAuthorizationException("User has no authority to reject recurring deposit products"); + } + String rejectReason = null; + if (apiRequestBodyAsJson != null && !apiRequestBodyAsJson.isBlank()) { + final var parsed = this.fromApiJsonHelper.parse(apiRequestBodyAsJson); + if (parsed != null && parsed.isJsonObject() && parsed.getAsJsonObject().has("rejectReason")) { + rejectReason = parsed.getAsJsonObject().get("rejectReason").getAsString(); + } + } + final CommandProcessingResult result = this.recurringDepositProductWritePlatformService.rejectEdit(productId, rejectReason); + return this.toApiJsonSerializer.serialize(result); + } + @GET @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsProductsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsProductsApiResource.java index 90b49c5f1..06362799b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsProductsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsProductsApiResource.java @@ -59,8 +59,11 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.EnumOptionData; import org.apache.fineract.infrastructure.codes.data.CodeValueData; import org.apache.fineract.infrastructure.codes.service.CodeValueReadPlatformService; +import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.security.exception.NoAuthorizationException; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.organisation.monetary.service.CurrencyReadPlatformService; @@ -74,9 +77,11 @@ import org.apache.fineract.portfolio.savings.SavingsInterestCalculationDaysInYea import org.apache.fineract.portfolio.savings.SavingsInterestCalculationType; import org.apache.fineract.portfolio.savings.SavingsPostingInterestPeriodType; import org.apache.fineract.portfolio.savings.data.SavingsProductData; +import org.apache.fineract.portfolio.savings.data.SavingsProductTempData; import org.apache.fineract.portfolio.savings.service.SavingsDropdownReadPlatformService; import org.apache.fineract.portfolio.savings.service.SavingsEnumerations; import org.apache.fineract.portfolio.savings.service.SavingsProductReadPlatformService; +import org.apache.fineract.portfolio.savings.service.SavingsProductWritePlatformService; import org.apache.fineract.portfolio.tax.data.TaxGroupData; import org.apache.fineract.portfolio.tax.service.TaxReadPlatformService; import org.springframework.jdbc.core.JdbcTemplate; @@ -105,6 +110,9 @@ public class SavingsProductsApiResource { private final TaxReadPlatformService taxReadPlatformService; private final ConfigurationDomainService configurationDomainService; private final JdbcTemplate jdbcTemplate; + private final SavingsProductWritePlatformService savingsProductWritePlatformService; + private final FromJsonHelper fromApiJsonHelper; + private final DefaultToApiJsonSerializer tempDataApiJsonSerializer; @POST @@ -135,15 +143,92 @@ public class SavingsProductsApiResource { public String update(@PathParam("productId") @Parameter(description = "productId") final Long productId, @Parameter(hidden = true) final String apiRequestBodyAsJson) { - final CommandWrapper commandRequest = new CommandWrapperBuilder().updateSavingProduct(productId).withJson(apiRequestBodyAsJson) - .build(); + // Bypasses the native command-source/maker-checker pipeline by design: edits are held in + // m_savings_product_temp (a pending row there is itself the lock) and only applied to m_savings_product on + // approval - see /savingsproducts/{productId}/approve and /reject below. + this.context.authenticatedUser().validateHasUpdatePermission(SavingsApiConstants.SAVINGS_PRODUCT_RESOURCE_NAME); - final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + final JsonCommand command = JsonCommand.fromJsonElement(productId, this.fromApiJsonHelper.parse(apiRequestBodyAsJson), + this.fromApiJsonHelper); + + final CommandProcessingResult result = this.savingsProductWritePlatformService.submitEditForApproval(productId, command); return this.toApiJsonSerializer.serialize(result); } + @GET + @Path("pending") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "List Savings Products pending edit approval", description = "Lists all savings product edits currently awaiting checker approval") + public String retrieveAllPending(@Context final UriInfo uriInfo) { + + this.context.authenticatedUser().validateHasReadPermission(SavingsApiConstants.SAVINGS_PRODUCT_RESOURCE_NAME); + + final Collection pendingEdits = this.savingProductReadPlatformService.retrieveAllPendingEdits(); + + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.tempDataApiJsonSerializer.serialize(settings, pendingEdits); + } + + @GET + @Path("{productId}/pending") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve the pending edit for a Savings Product", description = "Retrieves the draft awaiting checker approval for this product, if any") + public String retrievePending(@PathParam("productId") @Parameter(description = "productId") final Long productId, + @Context final UriInfo uriInfo) { + + this.context.authenticatedUser().validateHasReadPermission(SavingsApiConstants.SAVINGS_PRODUCT_RESOURCE_NAME); + + final SavingsProductTempData pendingEdit = this.savingProductReadPlatformService.retrievePendingEdit(productId); + + final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.tempDataApiJsonSerializer.serialize(settings, pendingEdit); + } + + @POST + @Path("{productId}/approve") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Approve a pending Savings Product edit", description = "Replays the pending edit and applies it to the Savings Product. The maker of the edit cannot also approve it.") + public String approvePending(@PathParam("productId") @Parameter(description = "productId") final Long productId) { + + if (this.context.authenticatedUser().hasNotPermissionForAnyOf("APPROVE_SAVINGSPRODUCT")) { + throw new NoAuthorizationException("User has no authority to approve savings products"); + } + + final CommandProcessingResult result = this.savingsProductWritePlatformService.approveEdit(productId); + + return this.toApiJsonSerializer.serialize(result); + } + + @POST + @Path("{productId}/reject") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Reject a pending Savings Product edit", description = "Discards the pending edit without applying it. The maker of the edit cannot also reject it.") + public String rejectPending(@PathParam("productId") @Parameter(description = "productId") final Long productId, + @Parameter(hidden = true) final String apiRequestBodyAsJson) { + + if (this.context.authenticatedUser().hasNotPermissionForAnyOf("REJECT_SAVINGSPRODUCT")) { + throw new NoAuthorizationException("User has no authority to reject savings products"); + } + + String rejectReason = null; + if (apiRequestBodyAsJson != null && !apiRequestBodyAsJson.isBlank()) { + final var parsed = this.fromApiJsonHelper.parse(apiRequestBodyAsJson); + if (parsed != null && parsed.isJsonObject() && parsed.getAsJsonObject().has("rejectReason")) { + rejectReason = parsed.getAsJsonObject().get("rejectReason").getAsString(); + } + } + + final CommandProcessingResult result = this.savingsProductWritePlatformService.rejectEdit(productId, rejectReason); + + return this.toApiJsonSerializer.serialize(result); + } + @GET @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountRecurringDetail.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountRecurringDetail.java index 6f659759d..3c209996b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountRecurringDetail.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountRecurringDetail.java @@ -25,6 +25,7 @@ import jakarta.persistence.Embedded; import jakarta.persistence.Entity; import jakarta.persistence.JoinColumn; import jakarta.persistence.OneToOne; +import jakarta.persistence.PostLoad; import jakarta.persistence.Table; import java.math.BigDecimal; import java.math.MathContext; @@ -135,7 +136,17 @@ public class DepositAccountRecurringDetail extends AbstractPersistableCustom 400; case "rejected" -> 500; case "closed" -> 600; + case "terminated" -> 900; default -> { try { yield Integer.parseInt(status); @@ -389,6 +399,7 @@ public class CurrentAccountReadPlatformServiceImpl implements CurrentAccountRead // status int statusEnum = toInt(row.get("status_enum"), 100); out.put("status", buildStatus(statusEnum)); + out.put("terminateTrue", toBoolean(row.get("terminate_true"))); // subStatus int subStatusEnum = toInt(row.get("sub_status_enum"), 0); @@ -443,6 +454,7 @@ public class CurrentAccountReadPlatformServiceImpl implements CurrentAccountRead out.put("msme", row.get("msme")); out.put("autoDebitFacility", row.get("auto_debit_facility")); out.put("balanceSwapOption", row.get("balance_swap_option")); + out.put("shareLinkingEnabled", row.get("is_linking_enable")); out.put("establishmentDate", row.get("establishment_date")); out.put("startOperationDate", row.get("start_operation_date")); out.put("nonIndividualClient", row.get("is_non_individual_client")); @@ -501,6 +513,13 @@ public class CurrentAccountReadPlatformServiceImpl implements CurrentAccountRead s.put("approved", false); s.put("active", false); s.put("rejected", false); s.put("withdrawnByApplicant", false); s.put("closed", true); } + case 900 -> { + s.put("code", "savingsAccountStatusType.terminated"); + s.put("value", "Terminated"); + s.put("submittedAndPendingApproval", false); + s.put("approved", false); s.put("active", false); + s.put("rejected", false); s.put("withdrawnByApplicant", false); s.put("closed", false); + } default -> { s.put("code", "savingsAccountStatusType.invalid"); s.put("value", "Invalid"); @@ -658,6 +677,14 @@ public class CurrentAccountReadPlatformServiceImpl implements CurrentAccountRead try { return new BigDecimal(val.toString().trim()); } catch (Exception e) { return null; } } + private boolean toBoolean(Object val) { + if (val == null) return false; + if (val instanceof Boolean) return (Boolean) val; + if (val instanceof Number) return ((Number) val).intValue() == 1; + String s = val.toString().trim(); + return "true".equalsIgnoreCase(s) || "1".equals(s); + } + private static class FlatRowMapper implements RowMapper> { @Override public Map mapRow(ResultSet rs, int rowNum) throws SQLException { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountTemplateReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountTemplateReadPlatformServiceImpl.java index 75f6c875d..a881d39e4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountTemplateReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountTemplateReadPlatformServiceImpl.java @@ -30,7 +30,7 @@ import org.apache.fineract.infrastructure.codes.data.CodeValueData; import org.apache.fineract.infrastructure.codes.service.CodeValueReadPlatformService; import org.apache.fineract.infrastructure.core.data.EnumOptionData; import org.apache.fineract.portfolio.savings.SavingsApiConstants; -import org.apache.fineract.portfolio.account.service.AccountNumberGenerator; +import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountNumberGenerator; import org.apache.fineract.infrastructure.core.domain.JdbcSupport; import org.apache.fineract.infrastructure.dataqueries.data.DatatableData; import org.apache.fineract.infrastructure.dataqueries.data.EntityTables; @@ -80,7 +80,7 @@ public class CurrentAccountTemplateReadPlatformServiceImpl implements CurrentAcc private final SavingsDropdownReadPlatformService dropdownReadPlatformService; private final ChargeReadPlatformService chargeReadPlatformService; private final EntityDatatableChecksReadService entityDatatableChecksReadService; - private final AccountNumberGenerator accountNumberGenerator; + private final CurrentAccountNumberGenerator currentAccountNumberGenerator; private final CodeValueReadPlatformService codeValueReadPlatformService; public CurrentAccountTemplateReadPlatformServiceImpl(final PlatformSecurityContext context, final JdbcTemplate jdbcTemplate, @@ -91,7 +91,7 @@ public class CurrentAccountTemplateReadPlatformServiceImpl implements CurrentAcc final ChargeReadPlatformService chargeReadPlatformService, final EntityDatatableChecksReadService entityDatatableChecksReadService, final ColumnValidator columnValidator, - final AccountNumberGenerator accountNumberGenerator, + final CurrentAccountNumberGenerator currentAccountNumberGenerator, final CodeValueReadPlatformService codeValueReadPlatformService) { this.context = context; this.jdbcTemplate = jdbcTemplate; @@ -102,7 +102,7 @@ public class CurrentAccountTemplateReadPlatformServiceImpl implements CurrentAcc this.dropdownReadPlatformService = dropdownReadPlatformService; this.chargeReadPlatformService = chargeReadPlatformService; this.entityDatatableChecksReadService = entityDatatableChecksReadService; - this.accountNumberGenerator = accountNumberGenerator; + this.currentAccountNumberGenerator = currentAccountNumberGenerator; this.codeValueReadPlatformService = codeValueReadPlatformService; } @@ -231,7 +231,7 @@ public class CurrentAccountTemplateReadPlatformServiceImpl implements CurrentAcc productData.getOdAccountClosedZeroBalanceInDayClosing() != null && productData.getOdAccountClosedZeroBalanceInDayClosing()); // Generate preview account number using current product's own GL account ID try { - final String preview = accountNumberGenerator.previewAccountNumberByGlAccountId(productData.getGlAccountId(), officeId); + final String preview = currentAccountNumberGenerator.preview(productId, officeId); template.setPreviewAccountNo(preview); } catch (final Exception ex) { log.warn("Failed to generate preview account number for productId={}: {}", productId, ex.getMessage()); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountWritePlatformServiceImpl.java index 3f87c8cf1..cf9f24db0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/CurrentAccountWritePlatformServiceImpl.java @@ -45,6 +45,7 @@ import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountWriteP import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountNumberGenerator; import org.apache.fineract.portfolio.currentproduct.domain.CurrentProduct; import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductRepository; +import org.apache.fineract.useradministration.domain.AppUser; import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForSavings; import org.apache.fineract.accounting.common.AccountingRuleType; import org.apache.fineract.accounting.glaccount.domain.GLAccount; @@ -127,6 +128,7 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri boolean balanceSwapOption = json.has("balanceSwapOption") && !json.get("balanceSwapOption").isJsonNull() ? json.get("balanceSwapOption").getAsBoolean() : product.isBalanceSwapOption(); + final AppUser user = context.authenticatedUser(); final CurrentAccount account = CurrentAccount.createNew( client, productId, office, null, "TEMP", submittedOnDate, accountTypeEnum, product.currency().getCode(), product.currency().getDigitsAfterDecimal(), @@ -139,6 +141,11 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri product.getCustomerSegment(), product.isAutoPost(), product.getInterestCalculateSelector(), product.getAveragePeriod(), product.getAvgBalanceCalcPeriod()); + account.setSubmittedOnByUser(user); + + boolean shareLinkingEnabled = json.has("shareLinkingEnabled") && !json.get("shareLinkingEnabled").isJsonNull() + ? json.get("shareLinkingEnabled").getAsBoolean() : product.isShareLinkingEnabled(); + account.setShareLinkingEnabled(shareLinkingEnabled); // Save CBS account type (descriptive name: "Standard Account", "Basic Account", etc.) if (cbsAccountType != null) account.setCbsAccountType(cbsAccountType); @@ -244,6 +251,9 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri throw e; } + log.info("Current Account created successfully: [id={}, accountNo={}, clientId={}, officeId={}, submittedOnDate={}]", + account.getId(), account.getAccountNo(), clientId, office.getId(), submittedOnDate); + return new CommandProcessingResultBuilder() .withEntityId(account.getId()) .withClientId(clientId) @@ -254,12 +264,12 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri @Override @Transactional public CommandProcessingResult approveApplication(final Long accountId, final JsonCommand command) { - context.authenticatedUser(); + final AppUser user = context.authenticatedUser(); final CurrentAccount account = findAccount(accountId); final JsonObject json = command.parsedJson().getAsJsonObject(); LocalDate approvedOnDate = parseDate(getString(json, "approvedOnDate"), getString(json, "dateFormat"), getString(json, "locale")); if (approvedOnDate == null) approvedOnDate = LocalDate.now(); - account.approve(approvedOnDate); + account.approve(approvedOnDate, user); currentAccountRepository.saveAndFlush(account); return new CommandProcessingResultBuilder().withEntityId(accountId).build(); } @@ -271,6 +281,7 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri final CurrentAccount account = findAccount(accountId); account.setStatusEnum(100); account.setApprovedOnDate(null); + account.setApprovedOnByUser(null); currentAccountRepository.saveAndFlush(account); return new CommandProcessingResultBuilder().withEntityId(accountId).build(); } @@ -278,7 +289,7 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri @Override @Transactional public CommandProcessingResult activate(final Long accountId, final JsonCommand command) { - context.authenticatedUser(); + final AppUser user = context.authenticatedUser(); final CurrentAccount account = findAccount(accountId); final JsonObject json = command.parsedJson().getAsJsonObject(); LocalDate activatedOnDate = parseDate(getString(json, "activatedOnDate"), getString(json, "dateFormat"), getString(json, "locale")); @@ -299,7 +310,7 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri openingBalanceValue, minRequired))); } - account.activate(activatedOnDate); + account.activate(activatedOnDate, user); applyFacilityChargesOnActivation(account); java.math.BigDecimal currentBalance = java.math.BigDecimal.ZERO; @@ -356,12 +367,12 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri @Override @Transactional public CommandProcessingResult rejectApplication(final Long accountId, final JsonCommand command) { - context.authenticatedUser(); + final AppUser user = context.authenticatedUser(); final CurrentAccount account = findAccount(accountId); final JsonObject json = command.parsedJson().getAsJsonObject(); LocalDate rejectedOnDate = parseDate(getString(json, "rejectedOnDate"), getString(json, "dateFormat"), getString(json, "locale")); if (rejectedOnDate == null) rejectedOnDate = LocalDate.now(); - account.reject(rejectedOnDate); + account.reject(rejectedOnDate, user); currentAccountRepository.saveAndFlush(account); return new CommandProcessingResultBuilder().withEntityId(accountId).build(); } @@ -369,12 +380,12 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri @Override @Transactional public CommandProcessingResult withdrawApplication(final Long accountId, final JsonCommand command) { - context.authenticatedUser(); + final AppUser user = context.authenticatedUser(); final CurrentAccount account = findAccount(accountId); final JsonObject json = command.parsedJson().getAsJsonObject(); LocalDate withdrawnOnDate = parseDate(getString(json, "withdrawnOnDate"), getString(json, "dateFormat"), getString(json, "locale")); if (withdrawnOnDate == null) withdrawnOnDate = LocalDate.now(); - account.withdraw(withdrawnOnDate); + account.withdraw(withdrawnOnDate, user); currentAccountRepository.saveAndFlush(account); return new CommandProcessingResultBuilder().withEntityId(accountId).build(); } @@ -382,12 +393,12 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri @Override @Transactional public CommandProcessingResult closeAccount(final Long accountId, final JsonCommand command) { - context.authenticatedUser(); + final AppUser user = context.authenticatedUser(); final CurrentAccount account = findAccount(accountId); final JsonObject json = command.parsedJson().getAsJsonObject(); LocalDate closedOnDate = parseDate(getString(json, "closedOnDate"), getString(json, "dateFormat"), getString(json, "locale")); if (closedOnDate == null) closedOnDate = LocalDate.now(); - account.close(closedOnDate); + account.close(closedOnDate, user); currentAccountRepository.saveAndFlush(account); return new CommandProcessingResultBuilder().withEntityId(accountId).build(); } @@ -399,6 +410,19 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri final CurrentAccount account = findAccount(accountId); final JsonObject json = command.parsedJson().getAsJsonObject(); + if (account.getStatusEnum() != null) { + if (account.getStatusEnum() == 500) { + account.setStatusEnum(100); + account.setRejectedOnDate(null); + account.setRejectedOnByUser(null); + } else if (account.getStatusEnum() == 400) { + account.setStatusEnum(100); + account.setWithdrawnOnDate(null); + account.setWithdrawnOnByUser(null); + } + } + + if (json.has("externalId") && !json.get("externalId").isJsonNull()) account.setExternalId(json.get("externalId").getAsString()); if (json.has("accountStatus") && !json.get("accountStatus").isJsonNull()) @@ -422,6 +446,8 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri account.setBalanceSwapOption(json.get("balanceSwapOption").getAsBoolean()); if (json.has("autoDebitFacility") && !json.get("autoDebitFacility").isJsonNull()) account.setAutoDebitFacility(json.get("autoDebitFacility").getAsBoolean()); + if (json.has("shareLinkingEnabled") && !json.get("shareLinkingEnabled").isJsonNull()) + account.setShareLinkingEnabled(json.get("shareLinkingEnabled").getAsBoolean()); if (json.has("introducerClientId") && !json.get("introducerClientId").isJsonNull()) account.setIntroducerClientId(json.get("introducerClientId").getAsLong()); if (json.has("introducerName") && !json.get("introducerName").isJsonNull()) @@ -484,6 +510,30 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri return new CommandProcessingResultBuilder().withEntityId(accountId).build(); } + @Override + @Transactional + public CommandProcessingResult terminateAccount(final Long accountId, final JsonCommand command) { + context.authenticatedUser(); + final CurrentAccount account = findAccount(accountId); + account.terminate(); + currentAccountRepository.saveAndFlush(account); + + final java.util.Map changes = new java.util.LinkedHashMap<>(); + changes.put("terminateTrue", true); + changes.put("status", 900); + + final Long officeId = account.getOffice() != null ? account.getOffice().getId() : null; + final Long clientId = account.getClient() != null ? account.getClient().getId() : null; + + return new CommandProcessingResultBuilder() + .withEntityId(accountId) + .withOfficeId(officeId) + .withClientId(clientId) + .withSavingsId(accountId) + .with(changes) + .build(); + } + // ─── helpers ────────────────────────────────────────────────────────────── private CurrentAccount findAccount(Long accountId) { @@ -492,7 +542,10 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri } private CurrentAccountFacilityConfig assembleFacilityConfig(CurrentAccount account, JsonObject fcJson) { - final CurrentAccountFacilityConfig cfg = CurrentAccountFacilityConfig.createFor(account); + CurrentAccountFacilityConfig cfg = account.getFacilityConfig(); + if (cfg == null) { + cfg = CurrentAccountFacilityConfig.createFor(account); + } cfg.setAtmFacility(getBool(fcJson, "atmFacility")); cfg.setAtmWithdrawalFacility(getBool(fcJson, "atmWithdrawalFacility")); cfg.setUpiFacility(getBool(fcJson, "upiFacility")); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositProductReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositProductReadPlatformServiceImpl.java index 9a2cc2852..2be58645f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositProductReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositProductReadPlatformServiceImpl.java @@ -18,10 +18,13 @@ */ package org.apache.fineract.portfolio.savings.service; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; import java.math.BigDecimal; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Collection; +import java.util.Map; import lombok.RequiredArgsConstructor; import org.apache.fineract.accounting.common.AccountingEnumerations; import org.apache.fineract.infrastructure.core.data.EnumOptionData; @@ -34,11 +37,20 @@ import org.apache.fineract.portfolio.savings.DepositAccountType; import org.apache.fineract.portfolio.savings.data.DepositProductData; import org.apache.fineract.portfolio.savings.data.FixedDepositProductData; import org.apache.fineract.portfolio.savings.data.RecurringDepositProductData; +import org.apache.fineract.portfolio.savings.data.SavingsProductTempData; import org.apache.fineract.portfolio.savings.exception.FixedDepositProductNotFoundException; +import org.apache.fineract.portfolio.savings.exception.SavingsProductNoPendingEditException; import org.apache.fineract.portfolio.tax.data.TaxGroupData; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEdit; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedFieldRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditChargeRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMappingRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFieldMapper; +import java.util.Set; @RequiredArgsConstructor public class DepositProductReadPlatformServiceImpl implements DepositProductReadPlatformService { @@ -49,6 +61,10 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead private final PlatformSecurityContext context; private final JdbcTemplate jdbcTemplate; private final InterestRateChartReadPlatformService chartReadPlatformService; + private final ProductPendingEditRepository productPendingEditRepository; + private final ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository; + private final ProductPendingEditChargeRepository pendingEditChargeRepository; + private final ProductPendingEditGlMappingRepository pendingEditGlMappingRepository; @Override public Collection retrieveAll(final DepositAccountType depositAccountType) { @@ -124,6 +140,69 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead return null; } + private static final Gson GSON = new Gson(); + + private static String productPendingType(final DepositAccountType depositAccountType) { + return depositAccountType.isFixedDeposit() ? "FD" : "RD"; + } + + private RowMapper getPendingEditRowMapper() { + return (rs, rowNum) -> { + final SavingsProductTempData data = new SavingsProductTempData(); + data.setId(rs.getLong("tempId")); + data.setSavingsProductId(rs.getLong("savingsProductId")); + data.setSavingsProductName(rs.getString("savingsProductName")); + data.setMakerId(rs.getLong("makerId")); + data.setMakerUsername(rs.getString("makerUsername")); + data.setCheckerId(JdbcSupport.getLong(rs, "checkerId")); + data.setCheckerUsername(rs.getString("checkerUsername")); + data.setCreatedOn(rs.getTimestamp("createdOn") == null ? null : rs.getTimestamp("createdOn").toLocalDateTime()); + data.setCheckedOn(rs.getTimestamp("checkedOn") == null ? null : rs.getTimestamp("checkedOn").toLocalDateTime()); + data.setStatus(rs.getString("status")); + data.setRejectReason(rs.getString("rejectReason")); + + final Long tempId = data.getId(); + final java.util.Optional opt = this.productPendingEditRepository.findById(tempId); + if (opt.isPresent()) { + final ProductPendingEdit pendingEdit = opt.get(); + final Set submittedFields = new java.util.HashSet<>( + this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId())); + final Map proposedChanges = ProductPendingEditFieldMapper.reconstructProposedChangesMap( + pendingEdit, submittedFields, this.pendingEditChargeRepository, this.pendingEditGlMappingRepository, null, GSON); + data.setProposedChanges(proposedChanges); + } + return data; + }; + } + + private static String pendingEditSchema(final String productType) { + return "spt.id as tempId, spt.product_id as savingsProductId, " + "sp.name as savingsProductName, spt.createdby_id as makerId, " + + "maker.username as makerUsername, spt.lastmodifiedby_id as checkerId, checker.username as checkerUsername, " + + "spt.created_date as createdOn, spt.lastmodified_date as checkedOn, spt.status as status, " + + "spt.reject_reason as rejectReason " + + "from m_saving_product_temp spt " + "join m_savings_product sp on sp.id = spt.product_id " + + "join m_appuser maker on maker.id = spt.createdby_id " + "left join m_appuser checker on checker.id = spt.lastmodifiedby_id " + + "where spt.product_type = '" + productType + "' "; + } + + @Override + public Collection retrieveAllPendingEdits(final DepositAccountType depositAccountType) { + final String sql = "select " + pendingEditSchema(productPendingType(depositAccountType)) + + "and spt.status = 'PENDING' order by spt.created_date desc"; + return this.jdbcTemplate.query(sql, getPendingEditRowMapper()); + } + + @Override + public SavingsProductTempData retrievePendingEdit(final DepositAccountType depositAccountType, final Long productId) { + final String sql = "select " + pendingEditSchema(productPendingType(depositAccountType)) + + "and spt.status = 'PENDING' and spt.product_id = ?"; + try { + return this.jdbcTemplate.queryForObject(sql, getPendingEditRowMapper(), new Object[] { productId }); // NOSONAR + } catch (final EmptyResultDataAccessException e) { + throw new SavingsProductNoPendingEditException(productId); + } + } + private abstract static class DepositProductMapper implements RowMapper { private final String schemaSql; @@ -255,7 +334,9 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead sqlBuilder.append("sp.no_penalty_nominee_heir_closure as noPenaltyNomineeHeirClosure, "); sqlBuilder.append("sp.deceased_settlement as deceasedSettlement, "); sqlBuilder.append("sp.allow_debit_txn_after_due_date as allowDebitTxnAfterDueDate, "); - sqlBuilder.append("sp.withdrawal_fee_for_transfer as withdrawalFeeForTransfers "); + sqlBuilder.append("sp.withdrawal_fee_for_transfer as withdrawalFeeForTransfers, "); + sqlBuilder.append("exists(select 1 from m_saving_product_temp spt where spt.product_id = sp.id " + + "and spt.product_type = 'FD' and spt.status = 'PENDING') as isPendingApproval "); sqlBuilder.append("from m_savings_product sp "); sqlBuilder.append("left join m_deposit_product_term_and_preclosure dptp on sp.id=dptp.savings_product_id "); sqlBuilder.append("join m_currency curr on curr.code = sp.currency_code "); @@ -345,6 +426,7 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead result.setDeceasedSettlement(rs.getBoolean("deceasedSettlement")); result.setAllowDebitTxnAfterDueDate(rs.getBoolean("allowDebitTxnAfterDueDate")); result.setWithdrawalFeeForTransfers(rs.getBoolean("withdrawalFeeForTransfers")); + result.setIsPendingApproval(rs.getBoolean("isPendingApproval")); return result; } } @@ -392,7 +474,9 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead sqlBuilder.append("sp.no_penalty_nominee_heir_closure as noPenaltyNomineeHeirClosure, "); sqlBuilder.append("sp.deceased_settlement as deceasedSettlement, "); sqlBuilder.append("sp.allow_debit_txn_after_due_date as allowDebitTxnAfterDueDate, "); - sqlBuilder.append("sp.withdrawal_fee_for_transfer as withdrawalFeeForTransfers "); + sqlBuilder.append("sp.withdrawal_fee_for_transfer as withdrawalFeeForTransfers, "); + sqlBuilder.append("exists(select 1 from m_saving_product_temp spt where spt.product_id = sp.id " + + "and spt.product_type = 'RD' and spt.status = 'PENDING') as isPendingApproval "); sqlBuilder.append("from m_savings_product sp "); sqlBuilder.append("left join m_deposit_product_term_and_preclosure dptp on sp.id=dptp.savings_product_id "); sqlBuilder.append("left join m_deposit_product_recurring_detail dprd on sp.id=dprd.savings_product_id "); @@ -513,6 +597,7 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead result.setDeceasedSettlement(deceasedSettlement); result.setAllowDebitTxnAfterDueDate(allowDebitTxnAfterDueDate); result.setWithdrawalFeeForTransfers(withdrawalFeeForTransfers); + result.setIsPendingApproval(rs.getBoolean("isPendingApproval")); return result; } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/FixedDepositProductWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/FixedDepositProductWritePlatformServiceJpaRepositoryImpl.java index a05c538ab..301e3bbcf 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/FixedDepositProductWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/FixedDepositProductWritePlatformServiceJpaRepositoryImpl.java @@ -32,6 +32,9 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.fineract.accounting.producttoaccountmapping.service.ProductToGLAccountMappingWritePlatformService; +import org.apache.fineract.commands.domain.CommandProcessingResultType; +import org.apache.fineract.commands.domain.CommandSource; +import org.apache.fineract.commands.domain.CommandSourceRepository; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; @@ -39,6 +42,8 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuild import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; import org.apache.fineract.infrastructure.core.exception.ErrorHandler; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.portfolio.charge.domain.Charge; import org.apache.fineract.portfolio.interestratechart.service.InterestRateChartAssembler; @@ -48,8 +53,22 @@ import org.apache.fineract.portfolio.savings.data.DepositProductDataValidator; import org.apache.fineract.portfolio.savings.domain.DepositProductAssembler; import org.apache.fineract.portfolio.savings.domain.FixedDepositProduct; import org.apache.fineract.portfolio.savings.domain.FixedDepositProductRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEdit; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedField; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedFieldRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditCharge; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditChargeRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMapping; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMappingRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFieldMapper; +import com.google.gson.JsonObject; import org.apache.fineract.portfolio.savings.exception.FixedDepositProductNotFoundException; +import org.apache.fineract.portfolio.savings.exception.SavingsProductEditPendingApprovalException; +import org.apache.fineract.portfolio.savings.exception.SavingsProductNoPendingEditException; +import org.apache.fineract.portfolio.savings.exception.SavingsProductSelfApprovalNotAllowedException; import org.apache.fineract.portfolio.tax.domain.TaxGroup; +import org.apache.fineract.useradministration.domain.AppUser; import org.springframework.dao.DataAccessException; import org.springframework.transaction.annotation.Transactional; @@ -57,12 +76,19 @@ import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor public class FixedDepositProductWritePlatformServiceJpaRepositoryImpl implements FixedDepositProductWritePlatformService { + private static final com.google.gson.Gson GSON = new com.google.gson.Gson(); private final PlatformSecurityContext context; private final FixedDepositProductRepository fixedDepositProductRepository; private final DepositProductDataValidator fromApiJsonDataValidator; private final DepositProductAssembler depositProductAssembler; private final ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService; private final InterestRateChartAssembler chartAssembler; + private final ProductPendingEditRepository productPendingEditRepository; + private final ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository; + private final ProductPendingEditChargeRepository pendingEditChargeRepository; + private final ProductPendingEditGlMappingRepository pendingEditGlMappingRepository; + private final FromJsonHelper fromApiJsonHelper; + private final CommandSourceRepository commandSourceRepository; @Transactional @Override @@ -94,10 +120,16 @@ public class FixedDepositProductWritePlatformServiceJpaRepositoryImpl implements @Transactional @Override public CommandProcessingResult update(final Long productId, final JsonCommand command) { + return updateInternal(productId, command, true); + } + + private CommandProcessingResult updateInternal(final Long productId, final JsonCommand command, final boolean validate) { try { this.context.authenticatedUser(); - this.fromApiJsonDataValidator.validateForFixedDepositUpdate(command.json()); + if (validate) { + this.fromApiJsonDataValidator.validateForFixedDepositUpdate(command.json()); + } final FixedDepositProduct product = this.fixedDepositProductRepository.findById(productId) .orElseThrow(() -> new FixedDepositProductNotFoundException(productId)); @@ -150,6 +182,190 @@ public class FixedDepositProductWritePlatformServiceJpaRepositoryImpl implements } } + @Transactional + @Override + public CommandProcessingResult submitEditForApproval(final Long productId, final JsonCommand command) { + + final AppUser maker = this.context.authenticatedUser(); + final FixedDepositProduct product = this.fixedDepositProductRepository.findById(productId) + .orElseThrow(() -> new FixedDepositProductNotFoundException(productId)); + + if (this.productPendingEditRepository + .findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_FD, productId, ProductPendingEdit.STATUS_PENDING) + .isPresent()) { + throw new SavingsProductEditPendingApprovalException(productId); + } + + this.fromApiJsonDataValidator.validateForFixedDepositUpdate(command.json()); + + final ProductPendingEdit pendingEdit = new ProductPendingEdit(ProductPendingEdit.TYPE_FD, productId, maker); + + final java.util.Set submitted = ProductPendingEditFieldMapper.populateCoreFields(command, pendingEdit.getCoreFields()); + final java.util.Set fdSubmitted = ProductPendingEditFieldMapper.populateFixedDepositFields(command, pendingEdit.getFixedDepositFields()); + final boolean glMappingSubmitted = ProductPendingEditFieldMapper.populateGlAccountMapping(command, pendingEdit.getGlAccountMappingFields()); + + if (!fdSubmitted.isEmpty()) { + submitted.addAll(fdSubmitted); + } + if (glMappingSubmitted) { + submitted.add("glAccountMapping"); + } + + final CommandSource auditEntry = CommandSource.builder() // + .actionName("UPDATE") // + .entityName("FIXEDDEPOSITPRODUCT") // + .resourceGetUrl("/fixeddepositproducts/" + productId) // + .resourceId(productId) // + .commandAsJson(command.json()) // + .maker(maker) // + .madeOnDate(DateUtils.getAuditOffsetDateTime()) // + .status(CommandProcessingResultType.AWAITING_APPROVAL.getValue()) // + .idempotencyKey(java.util.UUID.randomUUID().toString()) // + .sanitized(false) // + .build(); + this.commandSourceRepository.saveAndFlush(auditEntry); + pendingEdit.setCommandSourceId(auditEntry.getId()); + + this.productPendingEditRepository.saveAndFlush(pendingEdit); + + for (final String fieldName : submitted) { + this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, fieldName)); + } + + final List chargeIds = ProductPendingEditFieldMapper.extractChargeIds(command); + if (chargeIds != null) { + this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, "charges")); + for (final Long chargeId : chargeIds) { + this.pendingEditChargeRepository.save(new ProductPendingEditCharge(pendingEdit, chargeId)); + } + } + + final List glMappings = ProductPendingEditFieldMapper.extractGlArrayMappings(command, pendingEdit); + if (glMappings != null) { + this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, "glArrayMappings")); + this.pendingEditGlMappingRepository.saveAll(glMappings); + } + + return new CommandProcessingResultBuilder() // + .withEntityId(product.getId()) // + .build(); + } + + @Transactional + @Override + public CommandProcessingResult approveEdit(final Long productId) { + + final AppUser checker = this.context.authenticatedUser(); + final ProductPendingEdit pendingEdit = this.productPendingEditRepository + .findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_FD, productId, ProductPendingEdit.STATUS_PENDING) + .orElseThrow(() -> new SavingsProductNoPendingEditException(productId)); + + if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) { + throw new SavingsProductSelfApprovalNotAllowedException(productId); + } + + final Set submittedFields = new java.util.HashSet<>( + this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId())); + final JsonObject root = new JsonObject(); + root.addProperty("locale", "en"); + root.addProperty("dateFormat", "yyyy-MM-dd"); + ProductPendingEditFieldMapper.writeCoreFieldsToJson(root, pendingEdit.getCoreFields(), submittedFields); + ProductPendingEditFieldMapper.writeFixedDepositFieldsToJson(root, pendingEdit.getFixedDepositFields(), submittedFields); + if (submittedFields.contains("glAccountMapping")) { + ProductPendingEditFieldMapper.writeGlAccountMappingToJson(root, pendingEdit.getGlAccountMappingFields()); + } + if (submittedFields.contains("charges")) { + ProductPendingEditFieldMapper.writeChargesToJson(root, + this.pendingEditChargeRepository.findByPendingEditId(pendingEdit.getId())); + } + if (submittedFields.contains("glArrayMappings")) { + ProductPendingEditFieldMapper.writeGlArrayMappingsToJson(root, + this.pendingEditGlMappingRepository.findByPendingEditId(pendingEdit.getId())); + } + final JsonCommand replayedCommand = JsonCommand.fromJsonElement(productId, root, this.fromApiJsonHelper); + final CommandProcessingResult result = updateInternal(productId, replayedCommand, false); + + pendingEdit.markApproved(checker); + this.productPendingEditRepository.saveAndFlush(pendingEdit); + + final CommandSource auditEntry = findOrCreateAuditEntry(pendingEdit, productId); + auditEntry.markAsChecked(checker); + auditEntry.updateForAudit(result); + this.commandSourceRepository.saveAndFlush(auditEntry); + + return result; + } + + private CommandSource findOrCreateAuditEntry(final ProductPendingEdit pendingEdit, final Long productId) { + if (pendingEdit.getCommandSourceId() != null) { + final java.util.Optional existing = this.commandSourceRepository.findById(pendingEdit.getCommandSourceId()); + if (existing.isPresent()) { + return existing.get(); + } + } + final Set submittedFields = new java.util.HashSet<>( + this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId())); + final JsonObject root = new JsonObject(); + root.addProperty("locale", "en"); + root.addProperty("dateFormat", "yyyy-MM-dd"); + ProductPendingEditFieldMapper.writeCoreFieldsToJson(root, pendingEdit.getCoreFields(), submittedFields); + ProductPendingEditFieldMapper.writeFixedDepositFieldsToJson(root, pendingEdit.getFixedDepositFields(), submittedFields); + if (submittedFields.contains("glAccountMapping")) { + ProductPendingEditFieldMapper.writeGlAccountMappingToJson(root, pendingEdit.getGlAccountMappingFields()); + } + if (submittedFields.contains("charges")) { + ProductPendingEditFieldMapper.writeChargesToJson(root, + this.pendingEditChargeRepository.findByPendingEditId(pendingEdit.getId())); + } + if (submittedFields.contains("glArrayMappings")) { + ProductPendingEditFieldMapper.writeGlArrayMappingsToJson(root, + this.pendingEditGlMappingRepository.findByPendingEditId(pendingEdit.getId())); + } + final String commandJson = GSON.toJson(root); + + final CommandSource auditEntry = CommandSource.builder() // + .actionName("UPDATE") // + .entityName("FIXEDDEPOSITPRODUCT") // + .resourceGetUrl("/fixeddepositproducts/" + productId) // + .resourceId(productId) // + .commandAsJson(commandJson) // + .maker(pendingEdit.getMaker()) // + .madeOnDate(DateUtils.getAuditOffsetDateTime()) // + .status(CommandProcessingResultType.AWAITING_APPROVAL.getValue()) // + .idempotencyKey(java.util.UUID.randomUUID().toString()) // + .sanitized(false) // + .build(); + this.commandSourceRepository.saveAndFlush(auditEntry); + pendingEdit.setCommandSourceId(auditEntry.getId()); + this.productPendingEditRepository.saveAndFlush(pendingEdit); + return auditEntry; + } + + @Transactional + @Override + public CommandProcessingResult rejectEdit(final Long productId, final String rejectReason) { + + final AppUser checker = this.context.authenticatedUser(); + final ProductPendingEdit pendingEdit = this.productPendingEditRepository + .findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_FD, productId, ProductPendingEdit.STATUS_PENDING) + .orElseThrow(() -> new SavingsProductNoPendingEditException(productId)); + + if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) { + throw new SavingsProductSelfApprovalNotAllowedException(productId); + } + + pendingEdit.markRejected(checker, rejectReason); + this.productPendingEditRepository.saveAndFlush(pendingEdit); + + final CommandSource auditEntry = findOrCreateAuditEntry(pendingEdit, productId); + auditEntry.markAsRejected(checker); + this.commandSourceRepository.saveAndFlush(auditEntry); + + return new CommandProcessingResultBuilder() // + .withEntityId(productId) // + .build(); + } + @Transactional @Override public CommandProcessingResult delete(final Long productId) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/RecurringDepositProductWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/RecurringDepositProductWritePlatformServiceJpaRepositoryImpl.java index 354c92cdf..3abffe3ad 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/RecurringDepositProductWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/RecurringDepositProductWritePlatformServiceJpaRepositoryImpl.java @@ -32,6 +32,9 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.fineract.accounting.producttoaccountmapping.service.ProductToGLAccountMappingWritePlatformService; +import org.apache.fineract.commands.domain.CommandProcessingResultType; +import org.apache.fineract.commands.domain.CommandSource; +import org.apache.fineract.commands.domain.CommandSourceRepository; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; @@ -39,6 +42,8 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuild import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; import org.apache.fineract.infrastructure.core.exception.ErrorHandler; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.portfolio.charge.domain.Charge; import org.apache.fineract.portfolio.interestratechart.service.InterestRateChartAssembler; @@ -46,10 +51,24 @@ import org.apache.fineract.portfolio.savings.DepositAccountType; import org.apache.fineract.portfolio.savings.SavingsApiConstants; import org.apache.fineract.portfolio.savings.data.DepositProductDataValidator; import org.apache.fineract.portfolio.savings.domain.DepositProductAssembler; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEdit; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditCharge; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditChargeRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFieldMapper; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMapping; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMappingRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedField; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedFieldRepository; import org.apache.fineract.portfolio.savings.domain.RecurringDepositProduct; import org.apache.fineract.portfolio.savings.domain.RecurringDepositProductRepository; import org.apache.fineract.portfolio.savings.exception.RecurringDepositProductNotFoundException; +import com.google.gson.JsonObject; +import org.apache.fineract.portfolio.savings.exception.SavingsProductEditPendingApprovalException; +import org.apache.fineract.portfolio.savings.exception.SavingsProductNoPendingEditException; +import org.apache.fineract.portfolio.savings.exception.SavingsProductSelfApprovalNotAllowedException; import org.apache.fineract.portfolio.tax.domain.TaxGroup; +import org.apache.fineract.useradministration.domain.AppUser; import org.springframework.dao.DataAccessException; import org.springframework.transaction.annotation.Transactional; @@ -57,12 +76,19 @@ import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor public class RecurringDepositProductWritePlatformServiceJpaRepositoryImpl implements RecurringDepositProductWritePlatformService { + private static final com.google.gson.Gson GSON = new com.google.gson.Gson(); private final PlatformSecurityContext context; private final RecurringDepositProductRepository recurringDepositProductRepository; private final DepositProductDataValidator fromApiJsonDataValidator; private final DepositProductAssembler depositProductAssembler; private final ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService; private final InterestRateChartAssembler chartAssembler; + private final ProductPendingEditRepository productPendingEditRepository; + private final ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository; + private final ProductPendingEditChargeRepository pendingEditChargeRepository; + private final ProductPendingEditGlMappingRepository pendingEditGlMappingRepository; + private final FromJsonHelper fromApiJsonHelper; + private final CommandSourceRepository commandSourceRepository; @Transactional @Override @@ -94,10 +120,16 @@ public class RecurringDepositProductWritePlatformServiceJpaRepositoryImpl implem @Transactional @Override public CommandProcessingResult update(final Long productId, final JsonCommand command) { + return updateInternal(productId, command, true); + } + + private CommandProcessingResult updateInternal(final Long productId, final JsonCommand command, final boolean validate) { try { this.context.authenticatedUser(); - this.fromApiJsonDataValidator.validateForRecurringDepositUpdate(command.json()); + if (validate) { + this.fromApiJsonDataValidator.validateForRecurringDepositUpdate(command.json()); + } final RecurringDepositProduct product = this.recurringDepositProductRepository.findById(productId) .orElseThrow(() -> new RecurringDepositProductNotFoundException(productId)); @@ -150,6 +182,197 @@ public class RecurringDepositProductWritePlatformServiceJpaRepositoryImpl implem } } + @Transactional + @Override + public CommandProcessingResult submitEditForApproval(final Long productId, final JsonCommand command) { + + final AppUser maker = this.context.authenticatedUser(); + final RecurringDepositProduct product = this.recurringDepositProductRepository.findById(productId) + .orElseThrow(() -> new RecurringDepositProductNotFoundException(productId)); + + if (this.productPendingEditRepository + .findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_RD, productId, ProductPendingEdit.STATUS_PENDING) + .isPresent()) { + throw new SavingsProductEditPendingApprovalException(productId); + } + + this.fromApiJsonDataValidator.validateForRecurringDepositUpdate(command.json()); + + final ProductPendingEdit pendingEdit = new ProductPendingEdit(ProductPendingEdit.TYPE_RD, productId, maker); + + final java.util.Set submitted = ProductPendingEditFieldMapper.populateCoreFields(command, pendingEdit.getCoreFields()); + final java.util.Set fdSubmitted = ProductPendingEditFieldMapper.populateFixedDepositFields(command, pendingEdit.getFixedDepositFields()); + final java.util.Set rdSubmitted = ProductPendingEditFieldMapper.populateRecurringDepositFields(command, pendingEdit.getRecurringDepositFields()); + final boolean glMappingSubmitted = ProductPendingEditFieldMapper.populateGlAccountMapping(command, pendingEdit.getGlAccountMappingFields()); + + if (!fdSubmitted.isEmpty()) { + submitted.addAll(fdSubmitted); + } + if (!rdSubmitted.isEmpty()) { + submitted.addAll(rdSubmitted); + } + if (glMappingSubmitted) { + submitted.add("glAccountMapping"); + } + + final CommandSource auditEntry = CommandSource.builder() // + .actionName("UPDATE") // + .entityName("RECURRINGDEPOSITPRODUCT") // + .resourceGetUrl("/recurringdepositproducts/" + productId) // + .resourceId(productId) // + .commandAsJson(command.json()) // + .maker(maker) // + .madeOnDate(DateUtils.getAuditOffsetDateTime()) // + .status(CommandProcessingResultType.AWAITING_APPROVAL.getValue()) // + .idempotencyKey(java.util.UUID.randomUUID().toString()) // + .sanitized(false) // + .build(); + this.commandSourceRepository.saveAndFlush(auditEntry); + pendingEdit.setCommandSourceId(auditEntry.getId()); + + this.productPendingEditRepository.saveAndFlush(pendingEdit); + + for (final String fieldName : submitted) { + this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, fieldName)); + } + + final List chargeIds = ProductPendingEditFieldMapper.extractChargeIds(command); + if (chargeIds != null) { + this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, "charges")); + for (final Long chargeId : chargeIds) { + this.pendingEditChargeRepository.save(new ProductPendingEditCharge(pendingEdit, chargeId)); + } + } + + final List glMappings = ProductPendingEditFieldMapper.extractGlArrayMappings(command, pendingEdit); + if (glMappings != null) { + this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, "glArrayMappings")); + this.pendingEditGlMappingRepository.saveAll(glMappings); + } + + return new CommandProcessingResultBuilder() // + .withEntityId(product.getId()) // + .build(); + } + + @Transactional + @Override + public CommandProcessingResult approveEdit(final Long productId) { + + final AppUser checker = this.context.authenticatedUser(); + final ProductPendingEdit pendingEdit = this.productPendingEditRepository + .findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_RD, productId, ProductPendingEdit.STATUS_PENDING) + .orElseThrow(() -> new SavingsProductNoPendingEditException(productId)); + + if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) { + throw new SavingsProductSelfApprovalNotAllowedException(productId); + } + + final Set submittedFields = new java.util.HashSet<>( + this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId())); + submittedFields.remove("fdType"); + final JsonObject root = new JsonObject(); + root.addProperty("locale", "en"); + root.addProperty("dateFormat", "yyyy-MM-dd"); + ProductPendingEditFieldMapper.writeCoreFieldsToJson(root, pendingEdit.getCoreFields(), submittedFields); + ProductPendingEditFieldMapper.writeFixedDepositFieldsToJson(root, pendingEdit.getFixedDepositFields(), submittedFields); + ProductPendingEditFieldMapper.writeRecurringDepositFieldsToJson(root, pendingEdit.getRecurringDepositFields(), submittedFields); + if (submittedFields.contains("glAccountMapping")) { + ProductPendingEditFieldMapper.writeGlAccountMappingToJson(root, pendingEdit.getGlAccountMappingFields()); + } + if (submittedFields.contains("charges")) { + ProductPendingEditFieldMapper.writeChargesToJson(root, + this.pendingEditChargeRepository.findByPendingEditId(pendingEdit.getId())); + } + if (submittedFields.contains("glArrayMappings")) { + ProductPendingEditFieldMapper.writeGlArrayMappingsToJson(root, + this.pendingEditGlMappingRepository.findByPendingEditId(pendingEdit.getId())); + } + final JsonCommand replayedCommand = JsonCommand.fromJsonElement(productId, root, this.fromApiJsonHelper); + final CommandProcessingResult result = updateInternal(productId, replayedCommand, false); + + pendingEdit.markApproved(checker); + this.productPendingEditRepository.saveAndFlush(pendingEdit); + + final CommandSource auditEntry = findOrCreateAuditEntry(pendingEdit, productId); + auditEntry.markAsChecked(checker); + auditEntry.updateForAudit(result); + this.commandSourceRepository.saveAndFlush(auditEntry); + + return result; + } + + private CommandSource findOrCreateAuditEntry(final ProductPendingEdit pendingEdit, final Long productId) { + if (pendingEdit.getCommandSourceId() != null) { + final java.util.Optional existing = this.commandSourceRepository.findById(pendingEdit.getCommandSourceId()); + if (existing.isPresent()) { + return existing.get(); + } + } + final Set submittedFields = new java.util.HashSet<>( + this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId())); + final JsonObject root = new JsonObject(); + root.addProperty("locale", "en"); + root.addProperty("dateFormat", "yyyy-MM-dd"); + ProductPendingEditFieldMapper.writeCoreFieldsToJson(root, pendingEdit.getCoreFields(), submittedFields); + ProductPendingEditFieldMapper.writeFixedDepositFieldsToJson(root, pendingEdit.getFixedDepositFields(), submittedFields); + ProductPendingEditFieldMapper.writeRecurringDepositFieldsToJson(root, pendingEdit.getRecurringDepositFields(), submittedFields); + if (submittedFields.contains("glAccountMapping")) { + ProductPendingEditFieldMapper.writeGlAccountMappingToJson(root, pendingEdit.getGlAccountMappingFields()); + } + if (submittedFields.contains("charges")) { + ProductPendingEditFieldMapper.writeChargesToJson(root, + this.pendingEditChargeRepository.findByPendingEditId(pendingEdit.getId())); + } + if (submittedFields.contains("glArrayMappings")) { + ProductPendingEditFieldMapper.writeGlArrayMappingsToJson(root, + this.pendingEditGlMappingRepository.findByPendingEditId(pendingEdit.getId())); + } + final String commandJson = GSON.toJson(root); + + final CommandSource auditEntry = CommandSource.builder() // + .actionName("UPDATE") // + .entityName("RECURRINGDEPOSITPRODUCT") // + .resourceGetUrl("/recurringdepositproducts/" + productId) // + .resourceId(productId) // + .commandAsJson(commandJson) // + .maker(pendingEdit.getMaker()) // + .madeOnDate(DateUtils.getAuditOffsetDateTime()) // + .status(CommandProcessingResultType.AWAITING_APPROVAL.getValue()) // + .idempotencyKey(java.util.UUID.randomUUID().toString()) // + .sanitized(false) // + .build(); + this.commandSourceRepository.saveAndFlush(auditEntry); + pendingEdit.setCommandSourceId(auditEntry.getId()); + this.productPendingEditRepository.saveAndFlush(pendingEdit); + return auditEntry; + } + + @Transactional + @Override + public CommandProcessingResult rejectEdit(final Long productId, final String rejectReason) { + + final AppUser checker = this.context.authenticatedUser(); + final ProductPendingEdit pendingEdit = this.productPendingEditRepository + .findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_RD, productId, ProductPendingEdit.STATUS_PENDING) + .orElseThrow(() -> new SavingsProductNoPendingEditException(productId)); + + if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) { + throw new SavingsProductSelfApprovalNotAllowedException(productId); + } + + pendingEdit.markRejected(checker, rejectReason); + this.productPendingEditRepository.saveAndFlush(pendingEdit); + + final CommandSource auditEntry = findOrCreateAuditEntry(pendingEdit, productId); + auditEntry.markAsRejected(checker); + this.commandSourceRepository.saveAndFlush(auditEntry); + + return new CommandProcessingResultBuilder() // + .withEntityId(productId) // + .build(); + } + @Transactional @Override public CommandProcessingResult delete(final Long productId) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java index 886b7b959..471e82962 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java @@ -172,10 +172,17 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead if (searchParameters != null) { if (StringUtils.isNotBlank(searchParameters.getStatus())) { - final Integer statusEnum = resolveStatusEnum(searchParameters.getStatus()); - if (statusEnum != null) { - sqlBuilder.append(" and sa.status_enum = ?"); - objectArray[arrayPos++] = statusEnum; + if ("terminated".equalsIgnoreCase(searchParameters.getStatus())) { + sqlBuilder.append(" and sa.terminate_true = true"); + } else { + final Integer statusEnum = resolveStatusEnum(searchParameters.getStatus()); + if (statusEnum != null) { + sqlBuilder.append(" and sa.status_enum = ?"); + objectArray[arrayPos++] = statusEnum; + if (statusEnum == 100) { + sqlBuilder.append(" and (sa.terminate_true = false or sa.terminate_true is null)"); + } + } } } @@ -248,6 +255,7 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead case "withdrawn" -> 400; case "rejected" -> 500; case "closed" -> 600; + case "terminated" -> 900; default -> { try { yield Integer.parseInt(status); @@ -863,6 +871,8 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead sqlBuilder.append("sp.msme as msme, "); sqlBuilder.append("COALESCE(sa.nomination_facility, sp.nomination_facility) as nominationFacility, "); sqlBuilder.append("COALESCE(sa.auto_debit_facility, sp.auto_debit_facility) as autoDebitFacility, "); + sqlBuilder.append("COALESCE(sa.introducer_required, sp.introducer_required) as introducerRequired, "); + sqlBuilder.append("COALESCE(sa.share_linking_enabled, sp.share_linking_enabled) as shareLinkingEnabled, "); // Facility config fields sqlBuilder.append("safc.atm_facility as atmFacility, "); sqlBuilder.append("safc.atm_withdrawal_facility as atmWithdrawalFacility, "); @@ -879,7 +889,8 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead sqlBuilder.append("safc.inter_bank_total_cap_facility as interBankTotalCapFacility, "); sqlBuilder.append("safc.withdrawal_facility as withdrawalFacility, "); sqlBuilder.append("safc.deposit_facility_kyc as depositFacilityKyc, "); - sqlBuilder.append("safc.virtual_debit_card as virtualDebitCard "); + sqlBuilder.append("safc.virtual_debit_card as virtualDebitCard, "); + sqlBuilder.append("sa.terminate_true as terminateTrue "); sqlBuilder.append("from m_savings_account sa "); sqlBuilder.append("join m_savings_product sp ON sa.product_id = sp.id "); sqlBuilder.append("join m_currency curr on curr.code = sa.currency_code "); @@ -1118,6 +1129,9 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead accountData.setMsme(rs.getObject("msme") != null ? rs.getBoolean("msme") : null); accountData.setNominationFacility(rs.getObject("nominationFacility") != null ? rs.getBoolean("nominationFacility") : null); accountData.setAutoDebitFacility(rs.getObject("autoDebitFacility") != null ? rs.getBoolean("autoDebitFacility") : null); + accountData.setIntroducerRequired(rs.getObject("introducerRequired") != null ? rs.getBoolean("introducerRequired") : null); + accountData.setShareLinkingEnabled(rs.getObject("shareLinkingEnabled") != null ? rs.getBoolean("shareLinkingEnabled") : null); + accountData.setTerminateTrue(rs.getObject("terminateTrue") != null ? rs.getBoolean("terminateTrue") : null); // Nominee as parsed map final String nomineeDataStr = rs.getString("nomineeData"); 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 74bed6cfd..6ec884712 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 @@ -1770,6 +1770,19 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi .withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).with(changes).build(); } + @Override + @Transactional + public CommandProcessingResult terminate(final Long savingsId, final JsonCommand command) { + this.context.authenticatedUser(); + final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); + final Map changes = account.terminate(); + 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(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsProductReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsProductReadPlatformServiceImpl.java index c1a702c84..2663310e8 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsProductReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsProductReadPlatformServiceImpl.java @@ -38,17 +38,26 @@ import org.apache.fineract.infrastructure.security.service.PlatformSecurityConte import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.portfolio.savings.DepositAccountType; import org.apache.fineract.portfolio.savings.data.SavingsProductData; +import org.apache.fineract.portfolio.savings.data.SavingsProductTempData; import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityConfig; import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityLimit; import org.apache.fineract.portfolio.savings.domain.SavingsProductDepositLimitKyc; import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityConfigRepository; import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityLimitRepository; import org.apache.fineract.portfolio.savings.domain.SavingsProductDepositLimitKycRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEdit; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedFieldRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditChargeRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMappingRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFieldMapper; +import org.apache.fineract.portfolio.savings.exception.SavingsProductNoPendingEditException; import org.apache.fineract.portfolio.savings.exception.SavingsProductNotFoundException; import org.apache.fineract.portfolio.tax.data.TaxGroupData; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; +import java.util.Set; @RequiredArgsConstructor public class SavingsProductReadPlatformServiceImpl implements SavingsProductReadPlatformService { @@ -61,6 +70,10 @@ public class SavingsProductReadPlatformServiceImpl implements SavingsProductRead private final SavingsProductFacilityConfigRepository facilityConfigRepository; private final SavingsProductFacilityLimitRepository facilityLimitRepository; private final SavingsProductDepositLimitKycRepository depositLimitKycRepository; + private final ProductPendingEditRepository productPendingEditRepository; + private final ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository; + private final ProductPendingEditChargeRepository pendingEditChargeRepository; + private final ProductPendingEditGlMappingRepository pendingEditGlMappingRepository; private static final Gson GSON = new Gson(); private static final Type OTHER_GL_MAPPINGS_TYPE = new TypeToken>>() {}.getType(); private static final Type OTHER_GL_MAPPINGS_MAP_TYPE = new TypeToken>>>() {}.getType(); @@ -170,6 +183,63 @@ public class SavingsProductReadPlatformServiceImpl implements SavingsProductRead } } + private static final String PENDING_EDIT_SCHEMA = "spt.id as tempId, spt.product_id as savingsProductId, " + + "sp.name as savingsProductName, spt.createdby_id as makerId, maker.username as makerUsername, " + + "spt.lastmodifiedby_id as checkerId, checker.username as checkerUsername, spt.created_date as createdOn, " + + "spt.lastmodified_date as checkedOn, spt.status as status, spt.reject_reason as rejectReason " + + "from m_saving_product_temp spt " + + "join m_savings_product sp on sp.id = spt.product_id " + + "join m_appuser maker on maker.id = spt.createdby_id " + + "left join m_appuser checker on checker.id = spt.lastmodifiedby_id " + + "where spt.product_type = 'SAVINGS' "; + + private RowMapper getPendingEditRowMapper() { + return (rs, rowNum) -> { + final SavingsProductTempData data = new SavingsProductTempData(); + data.setId(rs.getLong("tempId")); + data.setSavingsProductId(rs.getLong("savingsProductId")); + data.setSavingsProductName(rs.getString("savingsProductName")); + data.setMakerId(rs.getLong("makerId")); + data.setMakerUsername(rs.getString("makerUsername")); + data.setCheckerId(JdbcSupport.getLong(rs, "checkerId")); + data.setCheckerUsername(rs.getString("checkerUsername")); + data.setCreatedOn(rs.getTimestamp("createdOn") == null ? null : rs.getTimestamp("createdOn").toLocalDateTime()); + data.setCheckedOn(rs.getTimestamp("checkedOn") == null ? null : rs.getTimestamp("checkedOn").toLocalDateTime()); + data.setStatus(rs.getString("status")); + data.setRejectReason(rs.getString("rejectReason")); + + final Long tempId = data.getId(); + final java.util.Optional opt = this.productPendingEditRepository.findById(tempId); + if (opt.isPresent()) { + final ProductPendingEdit pendingEdit = opt.get(); + final Set submittedFields = new java.util.HashSet<>( + this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId())); + final Map proposedChanges = ProductPendingEditFieldMapper.reconstructProposedChangesMap( + pendingEdit, submittedFields, this.pendingEditChargeRepository, this.pendingEditGlMappingRepository, null, GSON); + data.setProposedChanges(proposedChanges); + } + return data; + }; + } + + @Override + public Collection retrieveAllPendingEdits() { + this.context.authenticatedUser(); + final String sql = "select " + PENDING_EDIT_SCHEMA + "and spt.status = 'PENDING' order by spt.created_date desc"; + return this.jdbcTemplate.query(sql, getPendingEditRowMapper()); + } + + @Override + public SavingsProductTempData retrievePendingEdit(final Long productId) { + this.context.authenticatedUser(); + final String sql = "select " + PENDING_EDIT_SCHEMA + "and spt.status = 'PENDING' and spt.product_id = ?"; + try { + return this.jdbcTemplate.queryForObject(sql, getPendingEditRowMapper(), new Object[] { productId }); // NOSONAR + } catch (final EmptyResultDataAccessException e) { + throw new SavingsProductNoPendingEditException(productId); + } + } + private static final class SavingProductMapper implements RowMapper { private final String schemaSql; @@ -217,7 +287,9 @@ public class SavingsProductReadPlatformServiceImpl implements SavingsProductRead sqlBuilder.append("sp.auto_post as autoPost, sp.frequency_fix_day as frequencyFixDay, sp.last_interest_date as lastInterestDate, "); sqlBuilder.append("sp.enable_lockin_period as enableLockinPeriod, "); sqlBuilder.append("sp.balance_frequency as balanceFrequency, "); - sqlBuilder.append("sp.account_closed_zero_balance_in_day_closing as accountClosedZeroBalanceInDayClosing "); + sqlBuilder.append("sp.account_closed_zero_balance_in_day_closing as accountClosedZeroBalanceInDayClosing, "); + sqlBuilder.append("exists(select 1 from m_saving_product_temp spt where spt.product_id = sp.id " + + "and spt.product_type = 'SAVINGS' and spt.status = 'PENDING') as isPendingApproval "); sqlBuilder.append("from m_savings_product sp "); sqlBuilder.append("join m_currency curr on curr.code = sp.currency_code "); @@ -340,6 +412,7 @@ public class SavingsProductReadPlatformServiceImpl implements SavingsProductRead productData.setEnableLockinPeriod(rs.getBoolean("enableLockinPeriod")); productData.setBalanceFrequency(rs.getString("balanceFrequency")); productData.setAccountClosedZeroBalanceInDayClosing(rs.getBoolean("accountClosedZeroBalanceInDayClosing")); + productData.setIsPendingApproval(rs.getBoolean("isPendingApproval")); return productData; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsProductWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsProductWritePlatformServiceJpaRepositoryImpl.java index b2aa0832a..5a2ee8187 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsProductWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsProductWritePlatformServiceJpaRepositoryImpl.java @@ -46,6 +46,11 @@ import org.apache.fineract.infrastructure.security.service.PlatformSecurityConte import org.apache.fineract.portfolio.charge.domain.Charge; import org.apache.fineract.portfolio.savings.DepositAccountType; import org.apache.fineract.portfolio.savings.SavingsApiConstants; +import org.apache.fineract.commands.domain.CommandProcessingResultType; +import org.apache.fineract.commands.domain.CommandSource; +import org.apache.fineract.commands.domain.CommandSourceRepository; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.portfolio.savings.data.SavingsProductDataValidator; import org.apache.fineract.portfolio.savings.domain.SavingsProductDepositLimitKyc; import org.apache.fineract.portfolio.savings.domain.SavingsProductDepositLimitKycRepository; @@ -56,8 +61,24 @@ import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityLimitR import org.apache.fineract.portfolio.savings.domain.SavingsProduct; import org.apache.fineract.portfolio.savings.domain.SavingsProductRepository; import org.apache.fineract.portfolio.savings.domain.SavingsProductAssembler; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEdit; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditCharge; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditChargeRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFacilityLimit; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFacilityLimitRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMapping; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMappingRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedField; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedFieldRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFieldMapper; +import com.google.gson.JsonObject; +import org.apache.fineract.portfolio.savings.exception.SavingsProductEditPendingApprovalException; +import org.apache.fineract.portfolio.savings.exception.SavingsProductNoPendingEditException; import org.apache.fineract.portfolio.savings.exception.SavingsProductNotFoundException; +import org.apache.fineract.portfolio.savings.exception.SavingsProductSelfApprovalNotAllowedException; import org.apache.fineract.portfolio.tax.domain.TaxGroup; +import org.apache.fineract.useradministration.domain.AppUser; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.transaction.annotation.Transactional; @@ -66,6 +87,7 @@ import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor public class SavingsProductWritePlatformServiceJpaRepositoryImpl implements SavingsProductWritePlatformService { + private static final com.google.gson.Gson GSON = new com.google.gson.Gson(); private final PlatformSecurityContext context; private final SavingsProductRepository savingProductRepository; private final SavingsProductDataValidator fromApiJsonDataValidator; @@ -75,6 +97,13 @@ public class SavingsProductWritePlatformServiceJpaRepositoryImpl implements Savi private final SavingsProductFacilityConfigRepository facilityConfigRepository; private final SavingsProductFacilityLimitRepository facilityLimitRepository; private final SavingsProductDepositLimitKycRepository depositLimitKycRepository; + private final ProductPendingEditRepository productPendingEditRepository; + private final FromJsonHelper fromApiJsonHelper; + private final CommandSourceRepository commandSourceRepository; + private final ProductPendingEditChargeRepository pendingEditChargeRepository; + private final ProductPendingEditFacilityLimitRepository pendingEditFacilityLimitRepository; + private final ProductPendingEditGlMappingRepository pendingEditGlMappingRepository; + private final ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository; /* * Guaranteed to throw an exception no matter what the data integrity issue is. @@ -279,6 +308,224 @@ public class SavingsProductWritePlatformServiceJpaRepositoryImpl implements Savi } } + @Transactional + @Override + public CommandProcessingResult submitEditForApproval(final Long productId, final JsonCommand command) { + + final AppUser maker = this.context.authenticatedUser(); + final SavingsProduct product = this.savingProductRepository.findById(productId) + .orElseThrow(() -> new SavingsProductNotFoundException(productId)); + + if (this.productPendingEditRepository + .findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_SAVINGS, productId, ProductPendingEdit.STATUS_PENDING) + .isPresent()) { + throw new SavingsProductEditPendingApprovalException(productId); + } + + // Reuses the same validator the direct-update path uses, so bad data can't sit "pending" undetected. + this.fromApiJsonDataValidator.validateForUpdate(command.json(), product); + + final ProductPendingEdit pendingEdit = new ProductPendingEdit(ProductPendingEdit.TYPE_SAVINGS, productId, maker); + + // Typed-column storage: extract every recognised field into the pending edit's own columns + // (plus satellite rows for the 1:many pieces) instead of relying solely on the JSON blob + // above. proposedPayloadJson is still populated for now (safe deprecation window) but is no + // longer what approveEdit replays from - see approveEdit(). + final java.util.Set submitted = ProductPendingEditFieldMapper.populateCoreFields(command, pendingEdit.getCoreFields()); + final boolean facilityConfigSubmitted = ProductPendingEditFieldMapper.populateFacilityConfig(command, + pendingEdit.getFacilityConfigFields()); + final boolean depositLimitKycSubmitted = ProductPendingEditFieldMapper.populateDepositLimitKyc(command, + pendingEdit.getDepositLimitKycFields()); + final boolean glMappingSubmitted = ProductPendingEditFieldMapper.populateGlAccountMapping(command, + pendingEdit.getGlAccountMappingFields()); + if (facilityConfigSubmitted) { + submitted.add("accountFacilityConfig"); + } + if (depositLimitKycSubmitted) { + submitted.add("depositLimitKyc"); + } + if (glMappingSubmitted) { + submitted.add("glAccountMapping"); + } + + // Audit-only entry so this submission is visible in System -> Audit Trails even though it bypasses the + // native command-processing/maker-checker pipeline (the real gate is m_savings_product_temp itself). + final CommandSource auditEntry = CommandSource.builder() // + .actionName("UPDATE") // + .entityName("SAVINGSPRODUCT") // + .resourceGetUrl("/savingsproducts/" + productId) // + .resourceId(productId) // + .commandAsJson(command.json()) // + .maker(maker) // + .madeOnDate(DateUtils.getAuditOffsetDateTime()) // + .status(CommandProcessingResultType.AWAITING_APPROVAL.getValue()) // + .idempotencyKey(java.util.UUID.randomUUID().toString()) // + .sanitized(false) // + .build(); + this.commandSourceRepository.saveAndFlush(auditEntry); + pendingEdit.setCommandSourceId(auditEntry.getId()); + + this.productPendingEditRepository.saveAndFlush(pendingEdit); + + for (final String fieldName : submitted) { + this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, fieldName)); + } + + final List chargeIds = ProductPendingEditFieldMapper.extractChargeIds(command); + if (chargeIds != null) { + this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, "charges")); + for (final Long chargeId : chargeIds) { + this.pendingEditChargeRepository.save(new ProductPendingEditCharge(pendingEdit, chargeId)); + } + } + + final List facilityLimits = ProductPendingEditFieldMapper.extractFacilityLimits(command, + pendingEdit); + if (facilityLimits != null) { + this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, "accountFacilityLimits")); + this.pendingEditFacilityLimitRepository.saveAll(facilityLimits); + } + + final List glMappings = ProductPendingEditFieldMapper.extractGlArrayMappings(command, pendingEdit); + if (glMappings != null) { + this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, "glArrayMappings")); + this.pendingEditGlMappingRepository.saveAll(glMappings); + } + + return new CommandProcessingResultBuilder() // + .withEntityId(product.getId()) // + .build(); + } + + @Transactional + @Override + public CommandProcessingResult approveEdit(final Long productId) { + + final AppUser checker = this.context.authenticatedUser(); + final ProductPendingEdit pendingEdit = this.productPendingEditRepository + .findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_SAVINGS, productId, ProductPendingEdit.STATUS_PENDING) + .orElseThrow(() -> new SavingsProductNoPendingEditException(productId)); + + if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) { + throw new SavingsProductSelfApprovalNotAllowedException(productId); + } + + final Set submittedFields = new java.util.HashSet<>( + this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId())); + final JsonObject root = new JsonObject(); + root.addProperty("locale", "en"); + root.addProperty("dateFormat", "yyyy-MM-dd"); + ProductPendingEditFieldMapper.writeCoreFieldsToJson(root, pendingEdit.getCoreFields(), submittedFields); + ProductPendingEditFieldMapper.writeFacilityConfigToJson(root, pendingEdit.getFacilityConfigFields(), + submittedFields.contains("accountFacilityConfig")); + ProductPendingEditFieldMapper.writeDepositLimitKycToJson(root, pendingEdit.getDepositLimitKycFields(), + submittedFields.contains("depositLimitKyc")); + if (submittedFields.contains("glAccountMapping")) { + ProductPendingEditFieldMapper.writeGlAccountMappingToJson(root, pendingEdit.getGlAccountMappingFields()); + } + if (submittedFields.contains("charges")) { + ProductPendingEditFieldMapper.writeChargesToJson(root, + this.pendingEditChargeRepository.findByPendingEditId(pendingEdit.getId())); + } + if (submittedFields.contains("accountFacilityLimits")) { + ProductPendingEditFieldMapper.writeFacilityLimitsToJson(root, + this.pendingEditFacilityLimitRepository.findByPendingEditId(pendingEdit.getId())); + } + if (submittedFields.contains("glArrayMappings")) { + ProductPendingEditFieldMapper.writeGlArrayMappingsToJson(root, + this.pendingEditGlMappingRepository.findByPendingEditId(pendingEdit.getId())); + } + final JsonCommand replayedCommand = JsonCommand.fromJsonElement(productId, root, this.fromApiJsonHelper); + final CommandProcessingResult result = update(productId, replayedCommand); + + pendingEdit.markApproved(checker); + this.productPendingEditRepository.saveAndFlush(pendingEdit); + + final CommandSource auditEntry = findOrCreateAuditEntry(pendingEdit, productId); + auditEntry.markAsChecked(checker); + auditEntry.updateForAudit(result); + this.commandSourceRepository.saveAndFlush(auditEntry); + + return result; + } + + private CommandSource findOrCreateAuditEntry(final ProductPendingEdit pendingEdit, final Long productId) { + if (pendingEdit.getCommandSourceId() != null) { + final java.util.Optional existing = this.commandSourceRepository.findById(pendingEdit.getCommandSourceId()); + if (existing.isPresent()) { + return existing.get(); + } + } + final Set submittedFields = new java.util.HashSet<>( + this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId())); + final JsonObject root = new JsonObject(); + root.addProperty("locale", "en"); + root.addProperty("dateFormat", "yyyy-MM-dd"); + ProductPendingEditFieldMapper.writeCoreFieldsToJson(root, pendingEdit.getCoreFields(), submittedFields); + ProductPendingEditFieldMapper.writeFacilityConfigToJson(root, pendingEdit.getFacilityConfigFields(), + submittedFields.contains("accountFacilityConfig")); + ProductPendingEditFieldMapper.writeDepositLimitKycToJson(root, pendingEdit.getDepositLimitKycFields(), + submittedFields.contains("depositLimitKyc")); + if (submittedFields.contains("glAccountMapping")) { + ProductPendingEditFieldMapper.writeGlAccountMappingToJson(root, pendingEdit.getGlAccountMappingFields()); + } + if (submittedFields.contains("charges")) { + ProductPendingEditFieldMapper.writeChargesToJson(root, + this.pendingEditChargeRepository.findByPendingEditId(pendingEdit.getId())); + } + if (submittedFields.contains("accountFacilityLimits")) { + ProductPendingEditFieldMapper.writeFacilityLimitsToJson(root, + this.pendingEditFacilityLimitRepository.findByPendingEditId(pendingEdit.getId())); + } + if (submittedFields.contains("glArrayMappings")) { + ProductPendingEditFieldMapper.writeGlArrayMappingsToJson(root, + this.pendingEditGlMappingRepository.findByPendingEditId(pendingEdit.getId())); + } + final String commandJson = GSON.toJson(root); + + final CommandSource auditEntry = CommandSource.builder() // + .actionName("UPDATE") // + .entityName("SAVINGSPRODUCT") // + .resourceGetUrl("/savingsproducts/" + productId) // + .resourceId(productId) // + .commandAsJson(commandJson) // + .maker(pendingEdit.getMaker()) // + .madeOnDate(DateUtils.getAuditOffsetDateTime()) // + .status(CommandProcessingResultType.AWAITING_APPROVAL.getValue()) // + .idempotencyKey(java.util.UUID.randomUUID().toString()) // + .sanitized(false) // + .build(); + this.commandSourceRepository.saveAndFlush(auditEntry); + pendingEdit.setCommandSourceId(auditEntry.getId()); + this.productPendingEditRepository.saveAndFlush(pendingEdit); + return auditEntry; + } + + @Transactional + @Override + public CommandProcessingResult rejectEdit(final Long productId, final String rejectReason) { + + final AppUser checker = this.context.authenticatedUser(); + final ProductPendingEdit pendingEdit = this.productPendingEditRepository + .findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_SAVINGS, productId, ProductPendingEdit.STATUS_PENDING) + .orElseThrow(() -> new SavingsProductNoPendingEditException(productId)); + + if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) { + throw new SavingsProductSelfApprovalNotAllowedException(productId); + } + + pendingEdit.markRejected(checker, rejectReason); + this.productPendingEditRepository.saveAndFlush(pendingEdit); + + final CommandSource auditEntry = findOrCreateAuditEntry(pendingEdit, productId); + auditEntry.markAsRejected(checker); + this.commandSourceRepository.saveAndFlush(auditEntry); + + return new CommandProcessingResultBuilder() // + .withEntityId(productId) // + .build(); + } + @Transactional @Override public CommandProcessingResult delete(final Long productId) { 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 7b2cfdc32..b5fa180bc 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 @@ -47,6 +47,7 @@ import org.apache.fineract.portfolio.account.domain.AccountAssociationsRepositor import org.apache.fineract.portfolio.account.domain.StandingInstructionRepository; import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformService; import org.apache.fineract.portfolio.account.service.AccountNumberGenerator; +import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountNumberGenerator; import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService; import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService; import org.apache.fineract.portfolio.calendar.domain.CalendarInstanceRepository; @@ -95,6 +96,11 @@ import org.apache.fineract.portfolio.savings.domain.SavingsProductAssembler; import org.apache.fineract.portfolio.savings.domain.SavingsProductDepositLimitKycRepository; import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityConfig; import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityConfigRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedFieldRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditChargeRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMappingRepository; +import org.apache.fineract.commands.domain.CommandSourceRepository; import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityLimit; import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityLimitRepository; import org.apache.fineract.portfolio.savings.domain.SavingsProductRepository; @@ -308,8 +314,14 @@ public class SavingsConfiguration { @Bean @ConditionalOnMissingBean(DepositProductReadPlatformService.class) public DepositProductReadPlatformService depositProductReadPlatformService(PlatformSecurityContext context, JdbcTemplate jdbcTemplate, - InterestRateChartReadPlatformService interestRateChartReadPlatformService) { - return new DepositProductReadPlatformServiceImpl(context, jdbcTemplate, interestRateChartReadPlatformService); + InterestRateChartReadPlatformService interestRateChartReadPlatformService, + ProductPendingEditRepository productPendingEditRepository, + ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository, + ProductPendingEditChargeRepository pendingEditChargeRepository, + ProductPendingEditGlMappingRepository pendingEditGlMappingRepository) { + return new DepositProductReadPlatformServiceImpl(context, jdbcTemplate, interestRateChartReadPlatformService, + productPendingEditRepository, pendingEditSubmittedFieldRepository, + pendingEditChargeRepository, pendingEditGlMappingRepository); } @Bean @@ -323,9 +335,17 @@ public class SavingsConfiguration { public FixedDepositProductWritePlatformService fixedDepositProductWritePlatformService(PlatformSecurityContext context, FixedDepositProductRepository fixedDepositProductRepository, DepositProductDataValidator fromApiJsonDataValidator, DepositProductAssembler depositProductAssembler, - ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService, InterestRateChartAssembler chartAssembler) { + ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService, InterestRateChartAssembler chartAssembler, + ProductPendingEditRepository productPendingEditRepository, + ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository, + ProductPendingEditChargeRepository pendingEditChargeRepository, + ProductPendingEditGlMappingRepository pendingEditGlMappingRepository, + FromJsonHelper fromApiJsonHelper, + CommandSourceRepository commandSourceRepository) { return new FixedDepositProductWritePlatformServiceJpaRepositoryImpl(context, fixedDepositProductRepository, - fromApiJsonDataValidator, depositProductAssembler, accountMappingWritePlatformService, chartAssembler); + fromApiJsonDataValidator, depositProductAssembler, accountMappingWritePlatformService, chartAssembler, + productPendingEditRepository, pendingEditSubmittedFieldRepository, pendingEditChargeRepository, + pendingEditGlMappingRepository, fromApiJsonHelper, commandSourceRepository); } @Bean @@ -347,9 +367,17 @@ public class SavingsConfiguration { public RecurringDepositProductWritePlatformService recurringDepositProductWritePlatformService(PlatformSecurityContext context, RecurringDepositProductRepository recurringDepositProductRepository, DepositProductDataValidator fromApiJsonDataValidator, DepositProductAssembler depositProductAssembler, - ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService, InterestRateChartAssembler chartAssembler) { + ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService, InterestRateChartAssembler chartAssembler, + ProductPendingEditRepository productPendingEditRepository, + ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository, + ProductPendingEditChargeRepository pendingEditChargeRepository, + ProductPendingEditGlMappingRepository pendingEditGlMappingRepository, + FromJsonHelper fromApiJsonHelper, + CommandSourceRepository commandSourceRepository) { return new RecurringDepositProductWritePlatformServiceJpaRepositoryImpl(context, recurringDepositProductRepository, - fromApiJsonDataValidator, depositProductAssembler, accountMappingWritePlatformService, chartAssembler); + fromApiJsonDataValidator, depositProductAssembler, accountMappingWritePlatformService, chartAssembler, + productPendingEditRepository, pendingEditSubmittedFieldRepository, pendingEditChargeRepository, + pendingEditGlMappingRepository, fromApiJsonHelper, commandSourceRepository); } @Bean @@ -404,11 +432,11 @@ public class SavingsConfiguration { GroupReadPlatformService groupReadPlatformService, CurrentProductReadPlatformService currentProductReadPlatformService, StaffReadPlatformService staffReadPlatformService, SavingsDropdownReadPlatformService dropdownReadPlatformService, ChargeReadPlatformService chargeReadPlatformService, EntityDatatableChecksReadService entityDatatableChecksReadService, - ColumnValidator columnValidator, AccountNumberGenerator accountNumberGenerator, + ColumnValidator columnValidator, CurrentAccountNumberGenerator currentAccountNumberGenerator, CodeValueReadPlatformService codeValueReadPlatformService) { return new CurrentAccountTemplateReadPlatformServiceImpl(context, jdbcTemplate, clientReadPlatformService, groupReadPlatformService, currentProductReadPlatformService, staffReadPlatformService, dropdownReadPlatformService, chargeReadPlatformService, - entityDatatableChecksReadService, columnValidator, accountNumberGenerator, codeValueReadPlatformService); + entityDatatableChecksReadService, columnValidator, currentAccountNumberGenerator, codeValueReadPlatformService); } @Bean @@ -472,9 +500,15 @@ public class SavingsConfiguration { @ConditionalOnMissingBean(SavingsProductReadPlatformService.class) public SavingsProductReadPlatformService savingsProductReadPlatformService(PlatformSecurityContext context, JdbcTemplate jdbcTemplate, FineractEntityAccessUtil fineractEntityAccessUtil, SavingsProductFacilityConfigRepository facilityConfigRepository, - SavingsProductFacilityLimitRepository facilityLimitRepository, SavingsProductDepositLimitKycRepository depositLimitKycRepository) { + SavingsProductFacilityLimitRepository facilityLimitRepository, SavingsProductDepositLimitKycRepository depositLimitKycRepository, + ProductPendingEditRepository productPendingEditRepository, + ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository, + ProductPendingEditChargeRepository pendingEditChargeRepository, + ProductPendingEditGlMappingRepository pendingEditGlMappingRepository) { return new SavingsProductReadPlatformServiceImpl(context, jdbcTemplate, fineractEntityAccessUtil, facilityConfigRepository, - facilityLimitRepository, depositLimitKycRepository); + facilityLimitRepository, depositLimitKycRepository, + productPendingEditRepository, pendingEditSubmittedFieldRepository, + pendingEditChargeRepository, pendingEditGlMappingRepository); } @Bean @@ -488,10 +522,19 @@ public class SavingsConfiguration { FineractEntityAccessUtil fineractEntityAccessUtil, SavingsProductFacilityConfigRepository facilityConfigRepository, SavingsProductFacilityLimitRepository facilityLimitRepository, - SavingsProductDepositLimitKycRepository depositLimitKycRepository) { + SavingsProductDepositLimitKycRepository depositLimitKycRepository, + ProductPendingEditRepository productPendingEditRepository, + FromJsonHelper fromApiJsonHelper, + CommandSourceRepository commandSourceRepository, + org.apache.fineract.portfolio.savings.domain.ProductPendingEditChargeRepository pendingEditChargeRepository, + org.apache.fineract.portfolio.savings.domain.ProductPendingEditFacilityLimitRepository pendingEditFacilityLimitRepository, + org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMappingRepository pendingEditGlMappingRepository, + org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository) { return new SavingsProductWritePlatformServiceJpaRepositoryImpl(context, savingProductRepository, fromApiJsonDataValidator, savingsProductAssembler, accountMappingWritePlatformService, fineractEntityAccessUtil, facilityConfigRepository, - facilityLimitRepository, depositLimitKycRepository); + facilityLimitRepository, depositLimitKycRepository, productPendingEditRepository, fromApiJsonHelper, + commandSourceRepository, pendingEditChargeRepository, pendingEditFacilityLimitRepository, pendingEditGlMappingRepository, + pendingEditSubmittedFieldRepository); } @Bean diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareproducts/exception/ShareProductEditPendingApprovalException.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareproducts/exception/ShareProductEditPendingApprovalException.java new file mode 100644 index 000000000..a607901b7 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareproducts/exception/ShareProductEditPendingApprovalException.java @@ -0,0 +1,29 @@ +/** + * 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.shareproducts.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class ShareProductEditPendingApprovalException extends AbstractPlatformDomainRuleException { + + public ShareProductEditPendingApprovalException(final Long productId) { + super("error.msg.shareproduct.edit.pending.approval", + "Share product with identifier " + productId + " already has an edit pending approval", productId); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareproducts/exception/ShareProductNoPendingEditException.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareproducts/exception/ShareProductNoPendingEditException.java new file mode 100644 index 000000000..9b25a9dc6 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareproducts/exception/ShareProductNoPendingEditException.java @@ -0,0 +1,29 @@ +/** + * 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.shareproducts.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class ShareProductNoPendingEditException extends AbstractPlatformDomainRuleException { + + public ShareProductNoPendingEditException(final Long productId) { + super("error.msg.shareproduct.no.pending.edit", "Share product with identifier " + productId + " has no edit pending approval", + productId); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareproducts/exception/ShareProductSelfApprovalNotAllowedException.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareproducts/exception/ShareProductSelfApprovalNotAllowedException.java new file mode 100644 index 000000000..1f727cf8c --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareproducts/exception/ShareProductSelfApprovalNotAllowedException.java @@ -0,0 +1,29 @@ +/** + * 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.shareproducts.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class ShareProductSelfApprovalNotAllowedException extends AbstractPlatformDomainRuleException { + + public ShareProductSelfApprovalNotAllowedException(final Long productId) { + super("error.msg.shareproduct.self.approval.not.allowed", + "The maker of the pending edit for share product " + productId + " cannot also approve or reject it", productId); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareproducts/service/ShareProductReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareproducts/service/ShareProductReadPlatformServiceImpl.java index 7b3ac1274..a9525484e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareproducts/service/ShareProductReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareproducts/service/ShareProductReadPlatformServiceImpl.java @@ -18,6 +18,8 @@ */ package org.apache.fineract.portfolio.shareproducts.service; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; import java.math.BigDecimal; import java.sql.Date; import java.sql.ResultSet; @@ -46,12 +48,20 @@ import org.apache.fineract.portfolio.charge.service.ChargeReadPlatformService; import org.apache.fineract.portfolio.products.data.ProductData; import org.apache.fineract.portfolio.products.exception.ProductNotFoundException; import org.apache.fineract.portfolio.products.service.ShareProductReadPlatformService; +import org.apache.fineract.portfolio.savings.data.SavingsProductTempData; import org.apache.fineract.portfolio.shareaccounts.service.SharesEnumerations; import org.apache.fineract.portfolio.shareproducts.data.ShareProductData; import org.apache.fineract.portfolio.shareproducts.data.ShareProductMarketPriceData; +import org.apache.fineract.portfolio.shareproducts.exception.ShareProductNoPendingEditException; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEdit; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedFieldRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditChargeRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMappingRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFieldMapper; @RequiredArgsConstructor public class ShareProductReadPlatformServiceImpl implements ShareProductReadPlatformService { @@ -62,6 +72,10 @@ public class ShareProductReadPlatformServiceImpl implements ShareProductReadPlat private final ShareProductDropdownReadPlatformService shareProductDropdownReadPlatformService; private final AccountingDropdownReadPlatformService accountingDropdownReadPlatformService; private final ProductToGLAccountMappingReadPlatformService accountMappingReadPlatformService; + private final ProductPendingEditRepository productPendingEditRepository; + private final ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository; + private final ProductPendingEditChargeRepository pendingEditChargeRepository; + private final ProductPendingEditGlMappingRepository pendingEditGlMappingRepository; private final PaginationHelper shareProductDataPaginationHelper; private final DatabaseSpecificSQLGenerator sqlGenerator; @@ -152,6 +166,63 @@ public class ShareProductReadPlatformServiceImpl implements ShareProductReadPlat return null; } + private static final Gson GSON = new Gson(); + + private static final String PENDING_EDIT_SCHEMA = "spt.id as tempId, spt.product_id as savingsProductId, " + + "sp.name as savingsProductName, spt.createdby_id as makerId, maker.username as makerUsername, " + + "spt.lastmodifiedby_id as checkerId, checker.username as checkerUsername, spt.created_date as createdOn, " + + "spt.lastmodified_date as checkedOn, spt.status as status, spt.reject_reason as rejectReason " + + "from m_saving_product_temp spt " + + "join m_share_product sp on sp.id = spt.product_id " + + "join m_appuser maker on maker.id = spt.createdby_id " + + "left join m_appuser checker on checker.id = spt.lastmodifiedby_id " + + "where spt.product_type = 'SHARE' "; + + private RowMapper getPendingEditRowMapper() { + return (rs, rowNum) -> { + final SavingsProductTempData data = new SavingsProductTempData(); + data.setId(rs.getLong("tempId")); + data.setSavingsProductId(rs.getLong("savingsProductId")); + data.setSavingsProductName(rs.getString("savingsProductName")); + data.setMakerId(rs.getLong("makerId")); + data.setMakerUsername(rs.getString("makerUsername")); + data.setCheckerId(JdbcSupport.getLong(rs, "checkerId")); + data.setCheckerUsername(rs.getString("checkerUsername")); + data.setCreatedOn(rs.getTimestamp("createdOn") == null ? null : rs.getTimestamp("createdOn").toLocalDateTime()); + data.setCheckedOn(rs.getTimestamp("checkedOn") == null ? null : rs.getTimestamp("checkedOn").toLocalDateTime()); + data.setStatus(rs.getString("status")); + data.setRejectReason(rs.getString("rejectReason")); + + final Long tempId = data.getId(); + final java.util.Optional opt = this.productPendingEditRepository.findById(tempId); + if (opt.isPresent()) { + final ProductPendingEdit pendingEdit = opt.get(); + final Set submittedFields = new java.util.HashSet<>( + this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId())); + final Map proposedChanges = ProductPendingEditFieldMapper.reconstructProposedChangesMap( + pendingEdit, submittedFields, this.pendingEditChargeRepository, this.pendingEditGlMappingRepository, null, GSON); + data.setProposedChanges(proposedChanges); + } + return data; + }; + } + + @Override + public Collection retrieveAllPendingEdits() { + final String sql = "select " + PENDING_EDIT_SCHEMA + "and spt.status = 'PENDING' order by spt.created_date desc"; + return this.jdbcTemplate.query(sql, getPendingEditRowMapper()); + } + + @Override + public SavingsProductTempData retrievePendingEdit(final Long productId) { + final String sql = "select " + PENDING_EDIT_SCHEMA + "and spt.status = 'PENDING' and spt.product_id = ?"; + try { + return this.jdbcTemplate.queryForObject(sql, getPendingEditRowMapper(), new Object[] { productId }); // NOSONAR + } catch (final EmptyResultDataAccessException e) { + throw new ShareProductNoPendingEditException(productId); + } + } + private static final class AllShareProductRowMapper implements RowMapper { @SuppressWarnings("unused") diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareproducts/service/ShareProductWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareproducts/service/ShareProductWritePlatformService.java index 7bb811165..17ecd2b93 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareproducts/service/ShareProductWritePlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareproducts/service/ShareProductWritePlatformService.java @@ -32,4 +32,21 @@ public interface ShareProductWritePlatformService { CommandProcessingResult approveShareProductDividend(Long PayOutDetailId); CommandProcessingResult deleteShareProductDividend(Long PayOutDetailId); + + /** + * Submits an edit for approval instead of applying it directly. Fails if a pending edit already exists for this + * product (that pending row is itself the lock). + */ + CommandProcessingResult submitEditForApproval(Long productId, JsonCommand command); + + /** + * Replays the stored pending edit through the normal update path and marks it approved. Fails if the approving + * user is the same user who submitted the edit. + */ + CommandProcessingResult approveEdit(Long productId); + + /** + * Discards the pending edit. Fails if the rejecting user is the same user who submitted the edit. + */ + CommandProcessingResult rejectEdit(Long productId, String rejectReason); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareproducts/service/ShareProductWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareproducts/service/ShareProductWritePlatformServiceJpaRepositoryImpl.java index 229106864..e3714d10d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareproducts/service/ShareProductWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareproducts/service/ShareProductWritePlatformServiceJpaRepositoryImpl.java @@ -22,33 +22,56 @@ import com.google.gson.JsonElement; import jakarta.persistence.PersistenceException; import java.math.BigDecimal; import java.time.LocalDate; +import java.util.List; import java.util.Map; +import java.util.Set; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.fineract.accounting.producttoaccountmapping.service.ProductToGLAccountMappingWritePlatformService; +import org.apache.fineract.commands.domain.CommandProcessingResultType; +import org.apache.fineract.commands.domain.CommandSource; +import org.apache.fineract.commands.domain.CommandSourceRepository; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; import org.apache.fineract.infrastructure.core.exception.ErrorHandler; import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.event.business.domain.share.ShareProductDividentsCreateBusinessEvent; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEdit; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditRepository; import org.apache.fineract.portfolio.shareproducts.constants.ShareProductApiConstants; import org.apache.fineract.portfolio.shareproducts.domain.ShareProduct; import org.apache.fineract.portfolio.shareproducts.domain.ShareProductDividendPayOutDetails; import org.apache.fineract.portfolio.shareproducts.domain.ShareProductDividentPayOutDetailsRepositoryWrapper; import org.apache.fineract.portfolio.shareproducts.domain.ShareProductRepositoryWrapper; import org.apache.fineract.portfolio.shareproducts.exception.DividentProcessingException; +import org.apache.fineract.portfolio.shareproducts.exception.ShareProductEditPendingApprovalException; +import org.apache.fineract.portfolio.shareproducts.exception.ShareProductNoPendingEditException; +import org.apache.fineract.portfolio.shareproducts.exception.ShareProductSelfApprovalNotAllowedException; import org.apache.fineract.portfolio.shareproducts.serialization.ShareProductDataSerializer; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedField; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedFieldRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditCharge; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditChargeRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMapping; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMappingRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFieldMapper; +import com.google.gson.JsonObject; +import org.apache.fineract.useradministration.domain.AppUser; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.orm.jpa.JpaSystemException; +import org.springframework.transaction.annotation.Transactional; @Slf4j @RequiredArgsConstructor public class ShareProductWritePlatformServiceJpaRepositoryImpl implements ShareProductWritePlatformService { + private static final com.google.gson.Gson GSON = new com.google.gson.Gson(); private final ShareProductRepositoryWrapper repository; private final ShareProductDataSerializer serializer; private final FromJsonHelper fromApiJsonHelper; @@ -56,6 +79,12 @@ public class ShareProductWritePlatformServiceJpaRepositoryImpl implements ShareP private final ShareProductDividendAssembler shareProductDividendAssembler; private final ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService; private final BusinessEventNotifierService businessEventNotifierService; + private final PlatformSecurityContext context; + private final ProductPendingEditRepository productPendingEditRepository; + private final ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository; + private final ProductPendingEditChargeRepository pendingEditChargeRepository; + private final ProductPendingEditGlMappingRepository pendingEditGlMappingRepository; + private final CommandSourceRepository commandSourceRepository; @Override public CommandProcessingResult createShareProduct(JsonCommand jsonCommand) { @@ -180,6 +209,178 @@ public class ShareProductWritePlatformServiceJpaRepositoryImpl implements ShareP } } + @Transactional + @Override + public CommandProcessingResult submitEditForApproval(final Long productId, final JsonCommand command) { + + final AppUser maker = this.context.authenticatedUser(); + final ShareProduct product = this.repository.findOneWithNotFoundDetection(productId); + + if (this.productPendingEditRepository + .findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_SHARE, productId, ProductPendingEdit.STATUS_PENDING) + .isPresent()) { + throw new ShareProductEditPendingApprovalException(productId); + } + + final ProductPendingEdit pendingEdit = new ProductPendingEdit(ProductPendingEdit.TYPE_SHARE, productId, maker); + + final java.util.Set submitted = ProductPendingEditFieldMapper.populateShareOnlyFields(command, pendingEdit.getShareOnlyFields()); + final boolean glMappingSubmitted = ProductPendingEditFieldMapper.populateGlAccountMapping(command, pendingEdit.getGlAccountMappingFields()); + if (glMappingSubmitted) { + submitted.add("glAccountMapping"); + } + + final CommandSource auditEntry = CommandSource.builder() // + .actionName("UPDATE") // + .entityName("SHAREPRODUCT") // + .resourceGetUrl("/products/share/" + productId) // + .resourceId(productId) // + .commandAsJson(command.json()) // + .maker(maker) // + .madeOnDate(DateUtils.getAuditOffsetDateTime()) // + .status(CommandProcessingResultType.AWAITING_APPROVAL.getValue()) // + .idempotencyKey(java.util.UUID.randomUUID().toString()) // + .sanitized(false) // + .build(); + this.commandSourceRepository.saveAndFlush(auditEntry); + pendingEdit.setCommandSourceId(auditEntry.getId()); + + this.productPendingEditRepository.saveAndFlush(pendingEdit); + + for (final String fieldName : submitted) { + this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, fieldName)); + } + + final List chargeIds = ProductPendingEditFieldMapper.extractChargeIds(command); + if (chargeIds != null) { + this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, "charges")); + for (final Long chargeId : chargeIds) { + this.pendingEditChargeRepository.save(new ProductPendingEditCharge(pendingEdit, chargeId)); + } + } + + final List glMappings = ProductPendingEditFieldMapper.extractGlArrayMappings(command, pendingEdit); + if (glMappings != null) { + this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, "glArrayMappings")); + this.pendingEditGlMappingRepository.saveAll(glMappings); + } + + return new CommandProcessingResultBuilder() // + .withEntityId(product.getId()) // + .build(); + } + + @Transactional + @Override + public CommandProcessingResult approveEdit(final Long productId) { + + final AppUser checker = this.context.authenticatedUser(); + final ProductPendingEdit pendingEdit = this.productPendingEditRepository + .findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_SHARE, productId, ProductPendingEdit.STATUS_PENDING) + .orElseThrow(() -> new ShareProductNoPendingEditException(productId)); + + if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) { + throw new ShareProductSelfApprovalNotAllowedException(productId); + } + + final Set submittedFields = new java.util.HashSet<>( + this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId())); + final JsonObject root = new JsonObject(); + root.addProperty("locale", "en"); + ProductPendingEditFieldMapper.writeShareOnlyFieldsToJson(root, pendingEdit.getShareOnlyFields(), submittedFields); + if (submittedFields.contains("glAccountMapping")) { + ProductPendingEditFieldMapper.writeGlAccountMappingToJson(root, pendingEdit.getGlAccountMappingFields()); + } + if (submittedFields.contains("charges")) { + ProductPendingEditFieldMapper.writeChargesToJson(root, + this.pendingEditChargeRepository.findByPendingEditId(pendingEdit.getId())); + } + if (submittedFields.contains("glArrayMappings")) { + ProductPendingEditFieldMapper.writeGlArrayMappingsToJson(root, + this.pendingEditGlMappingRepository.findByPendingEditId(pendingEdit.getId())); + } + final JsonCommand replayedCommand = JsonCommand.fromJsonElement(productId, root, this.fromApiJsonHelper); + final CommandProcessingResult result = updateProduct(productId, replayedCommand); + + pendingEdit.markApproved(checker); + this.productPendingEditRepository.saveAndFlush(pendingEdit); + + final CommandSource auditEntry = findOrCreateAuditEntry(pendingEdit, productId); + auditEntry.markAsChecked(checker); + auditEntry.updateForAudit(result); + this.commandSourceRepository.saveAndFlush(auditEntry); + + return result; + } + + private CommandSource findOrCreateAuditEntry(final ProductPendingEdit pendingEdit, final Long productId) { + if (pendingEdit.getCommandSourceId() != null) { + final java.util.Optional existing = this.commandSourceRepository.findById(pendingEdit.getCommandSourceId()); + if (existing.isPresent()) { + return existing.get(); + } + } + final Set submittedFields = new java.util.HashSet<>( + this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId())); + final JsonObject root = new JsonObject(); + root.addProperty("locale", "en"); + ProductPendingEditFieldMapper.writeShareOnlyFieldsToJson(root, pendingEdit.getShareOnlyFields(), submittedFields); + if (submittedFields.contains("glAccountMapping")) { + ProductPendingEditFieldMapper.writeGlAccountMappingToJson(root, pendingEdit.getGlAccountMappingFields()); + } + if (submittedFields.contains("charges")) { + ProductPendingEditFieldMapper.writeChargesToJson(root, + this.pendingEditChargeRepository.findByPendingEditId(pendingEdit.getId())); + } + if (submittedFields.contains("glArrayMappings")) { + ProductPendingEditFieldMapper.writeGlArrayMappingsToJson(root, + this.pendingEditGlMappingRepository.findByPendingEditId(pendingEdit.getId())); + } + final String commandJson = GSON.toJson(root); + + final CommandSource auditEntry = CommandSource.builder() // + .actionName("UPDATE") // + .entityName("SHAREPRODUCT") // + .resourceGetUrl("/products/share/" + productId) // + .resourceId(productId) // + .commandAsJson(commandJson) // + .maker(pendingEdit.getMaker()) // + .madeOnDate(DateUtils.getAuditOffsetDateTime()) // + .status(CommandProcessingResultType.AWAITING_APPROVAL.getValue()) // + .idempotencyKey(java.util.UUID.randomUUID().toString()) // + .sanitized(false) // + .build(); + this.commandSourceRepository.saveAndFlush(auditEntry); + pendingEdit.setCommandSourceId(auditEntry.getId()); + this.productPendingEditRepository.saveAndFlush(pendingEdit); + return auditEntry; + } + + @Transactional + @Override + public CommandProcessingResult rejectEdit(final Long productId, final String rejectReason) { + + final AppUser checker = this.context.authenticatedUser(); + final ProductPendingEdit pendingEdit = this.productPendingEditRepository + .findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_SHARE, productId, ProductPendingEdit.STATUS_PENDING) + .orElseThrow(() -> new ShareProductNoPendingEditException(productId)); + + if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) { + throw new ShareProductSelfApprovalNotAllowedException(productId); + } + + pendingEdit.markRejected(checker, rejectReason); + this.productPendingEditRepository.saveAndFlush(pendingEdit); + + final CommandSource auditEntry = findOrCreateAuditEntry(pendingEdit, productId); + auditEntry.markAsRejected(checker); + this.commandSourceRepository.saveAndFlush(auditEntry); + + return new CommandProcessingResultBuilder() // + .withEntityId(productId) // + .build(); + } + private void handleDataIntegrityIssues(final Exception e) { log.error("Unknown data integrity issue with resource", e); throw ErrorHandler.getMappable(e, "error.msg.shareproduct.unknown.data.integrity.issue", diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareproducts/starter/ShareProductsConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareproducts/starter/ShareProductsConfiguration.java index 2cbe6469d..0134e629d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareproducts/starter/ShareProductsConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareproducts/starter/ShareProductsConfiguration.java @@ -24,11 +24,17 @@ import org.apache.fineract.accounting.producttoaccountmapping.service.ProductToG import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.service.PaginationHelper; import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; +import org.apache.fineract.commands.domain.CommandSourceRepository; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.infrastructure.security.utils.ColumnValidator; import org.apache.fineract.organisation.monetary.service.CurrencyReadPlatformService; import org.apache.fineract.portfolio.charge.service.ChargeReadPlatformService; import org.apache.fineract.portfolio.products.service.ShareProductReadPlatformService; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedFieldRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditChargeRepository; +import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMappingRepository; import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountReadPlatformService; import org.apache.fineract.portfolio.shareproducts.domain.ShareProductDividentPayOutDetailsRepositoryWrapper; import org.apache.fineract.portfolio.shareproducts.domain.ShareProductRepositoryWrapper; @@ -86,9 +92,15 @@ public class ShareProductsConfiguration { ShareProductDropdownReadPlatformService shareProductDropdownReadPlatformService, AccountingDropdownReadPlatformService accountingDropdownReadPlatformService, ProductToGLAccountMappingReadPlatformService accountMappingReadPlatformService, - PaginationHelper shareProductDataPaginationHelper, DatabaseSpecificSQLGenerator sqlGenerator) { + PaginationHelper shareProductDataPaginationHelper, DatabaseSpecificSQLGenerator sqlGenerator, + ProductPendingEditRepository productPendingEditRepository, + ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository, + ProductPendingEditChargeRepository pendingEditChargeRepository, + ProductPendingEditGlMappingRepository pendingEditGlMappingRepository) { return new ShareProductReadPlatformServiceImpl(jdbcTemplate, currencyReadPlatformService, chargeReadPlatformService, shareProductDropdownReadPlatformService, accountingDropdownReadPlatformService, accountMappingReadPlatformService, + productPendingEditRepository, pendingEditSubmittedFieldRepository, + pendingEditChargeRepository, pendingEditGlMappingRepository, shareProductDataPaginationHelper, sqlGenerator); } @@ -99,10 +111,17 @@ public class ShareProductsConfiguration { ShareProductDividentPayOutDetailsRepositoryWrapper shareProductDividentPayOutDetailsRepository, ShareProductDividendAssembler shareProductDividendAssembler, ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService, - BusinessEventNotifierService businessEventNotifierService) { + BusinessEventNotifierService businessEventNotifierService, + PlatformSecurityContext context, + ProductPendingEditRepository productPendingEditRepository, + ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository, + ProductPendingEditChargeRepository pendingEditChargeRepository, + ProductPendingEditGlMappingRepository pendingEditGlMappingRepository, + CommandSourceRepository commandSourceRepository) { return new ShareProductWritePlatformServiceJpaRepositoryImpl(repository, serializer, fromApiJsonHelper, shareProductDividentPayOutDetailsRepository, shareProductDividendAssembler, accountMappingWritePlatformService, - businessEventNotifierService); + businessEventNotifierService, context, productPendingEditRepository, pendingEditSubmittedFieldRepository, + pendingEditChargeRepository, pendingEditGlMappingRepository, commandSourceRepository); } } diff --git a/fineract-provider/src/main/resources/application.properties b/fineract-provider/src/main/resources/application.properties index 7f1c08b22..a1b2caaf0 100644 --- a/fineract-provider/src/main/resources/application.properties +++ b/fineract-provider/src/main/resources/application.properties @@ -46,7 +46,7 @@ fineract.security.oauth2.client.registrations.frontend-client.require-authorizat fineract.tenant.host=${FINERACT_DEFAULT_TENANTDB_HOSTNAME:localhost} fineract.tenant.port=${FINERACT_DEFAULT_TENANTDB_PORT:5432} fineract.tenant.username=${FINERACT_DEFAULT_TENANTDB_UID:postgres} -fineract.tenant.password=${FINERACT_DEFAULT_TENANTDB_PWD:root} +fineract.tenant.password=${FINERACT_DEFAULT_TENANTDB_PWD:Keshav@898966} fineract.tenant.parameters=${FINERACT_DEFAULT_TENANTDB_CONN_PARAMS:} fineract.tenant.timezone=${FINERACT_DEFAULT_TENANTDB_TIMEZONE:Asia/Kolkata} fineract.tenant.identifier=${FINERACT_DEFAULT_TENANTDB_IDENTIFIER:default} @@ -407,7 +407,7 @@ server.tomcat.mbeanregistry.enabled=${FINERACT_SERVER_TOMCAT_MBEANREGISTRY_ENABL spring.datasource.hikari.driverClassName=${FINERACT_HIKARI_DRIVER_SOURCE_CLASS_NAME:org.postgresql.Driver} spring.datasource.hikari.jdbcUrl=${FINERACT_HIKARI_JDBC_URL:jdbc:postgresql://localhost:5432/fineract_tenants} spring.datasource.hikari.username=${FINERACT_HIKARI_USERNAME:postgres} -spring.datasource.hikari.password=${FINERACT_HIKARI_PASSWORD:313863} +spring.datasource.hikari.password=${FINERACT_HIKARI_PASSWORD:Keshav@898966} spring.datasource.hikari.minimumIdle=${FINERACT_HIKARI_MINIMUM_IDLE:3} spring.datasource.hikari.maximumPoolSize=${FINERACT_HIKARI_MAXIMUM_POOL_SIZE:10} spring.datasource.hikari.idleTimeout=${FINERACT_HIKARI_IDLE_TIMEOUT:60000} 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 6d8925647..8629886a9 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 @@ -394,4 +394,7 @@ + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0338_add_soft_delete_to_tellers.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0338_add_soft_delete_to_tellers.xml new file mode 100644 index 000000000..f544cbb71 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0338_add_soft_delete_to_tellers.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0339_add_share_linking_enabled_to_current_account.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0339_add_share_linking_enabled_to_current_account.xml new file mode 100644 index 000000000..0f70ede28 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0339_add_share_linking_enabled_to_current_account.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0340_add_terminate_true_to_savings_and_current_account.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0340_add_terminate_true_to_savings_and_current_account.xml new file mode 100644 index 000000000..677356c90 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0340_add_terminate_true_to_savings_and_current_account.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/FixedDepositProductData.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/FixedDepositProductData.java index 4da5b1860..53c2e9199 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/FixedDepositProductData.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/FixedDepositProductData.java @@ -495,6 +495,7 @@ public final class FixedDepositProductData extends DepositProductData { private Boolean allowDebitTxnAfterDueDate; private Boolean accountClosedZeroBalanceInDayClosing; private String balanceFrequency; + private Boolean isPendingApproval; // Template option setters public void setInterestCalculationTypeOption(Collection v) { this.interestCalculationTypeOption = v; } @@ -541,6 +542,7 @@ public final class FixedDepositProductData extends DepositProductData { public void setAllowDebitTxnAfterDueDate(Boolean v) { this.allowDebitTxnAfterDueDate = v; } public void setAccountClosedZeroBalanceInDayClosing(Boolean v) { this.accountClosedZeroBalanceInDayClosing = v; } public void setBalanceFrequency(String v) { this.balanceFrequency = v; } + public void setIsPendingApproval(Boolean v) { this.isPendingApproval = v; } // CBS field getters public String getProductCode() { return productCode; } diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/RecurringDepositProductData.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/RecurringDepositProductData.java index 62ae76c63..3c6537c83 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/RecurringDepositProductData.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/RecurringDepositProductData.java @@ -98,6 +98,7 @@ public final class RecurringDepositProductData extends DepositProductData { private Boolean deceasedSettlement; private Boolean allowDebitTxnAfterDueDate; private Boolean withdrawalFeeForTransfers; + private Boolean isPendingApproval; public void setInterestCalculationTypeOption(Collection v) { this.interestCalculationTypeOption = v; } public void setPeriodFrequencyOptions(Collection v) { this.periodFrequencyOptions = v; } @@ -140,6 +141,7 @@ public final class RecurringDepositProductData extends DepositProductData { public void setDeceasedSettlement(Boolean v) { this.deceasedSettlement = v; } public void setAllowDebitTxnAfterDueDate(Boolean v) { this.allowDebitTxnAfterDueDate = v; } public void setWithdrawalFeeForTransfers(Boolean v) { this.withdrawalFeeForTransfers = v; } + public void setIsPendingApproval(Boolean v) { this.isPendingApproval = v; } /** Copy all CBS custom fields from source into this instance. */ public void copyCbsFieldsFrom(final RecurringDepositProductData src) { 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 85b32925e..599ac65c7 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 @@ -48,7 +48,7 @@ public class SavingsAccountConstant extends SavingsApiConstants { introducerIdParamName, introducerNameParamName, introducerCifNumberParamName, nomineeParamName, "nominees", linkedAccountIdParamName, linkedAccountTypeParamName, linkedAccountNoParamName, accountFacilityConfigParamName, jointHoldersParamName, - balanceSwapOptionParamName, msmeParamName, autoDebitFacilityParamName)); + balanceSwapOptionParamName, msmeParamName, autoDebitFacilityParamName, introducerRequiredParamName, shareLinkingEnabledParamName)); /** * These parameters will match the class level parameters of {@link SavingsAccountData}. Where possible, we try to diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/CurrentOnlyEditFields.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/CurrentOnlyEditFields.java new file mode 100644 index 000000000..0f3e8922b --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/CurrentOnlyEditFields.java @@ -0,0 +1,39 @@ +/** + * 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.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.Getter; +import lombok.Setter; + +@Embeddable +@Getter +@Setter +public class CurrentOnlyEditFields { + + @Column(name = "avg_balance_calc_period") + private String avgBalanceCalcPeriod; + + @Column(name = "valid_for_period") + private String validForPeriod; + + @Column(name = "od_account_closed_zero_balance_in_day_closing") + private Boolean odAccountClosedZeroBalanceInDayClosing; +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountTermAndPreClosure.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountTermAndPreClosure.java index 31240765d..eb2fc6022 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountTermAndPreClosure.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountTermAndPreClosure.java @@ -33,6 +33,7 @@ import jakarta.persistence.Embedded; import jakarta.persistence.Entity; import jakarta.persistence.JoinColumn; import jakarta.persistence.OneToOne; +import jakarta.persistence.PostLoad; import jakarta.persistence.Table; import java.math.BigDecimal; import java.time.LocalDate; @@ -181,11 +182,27 @@ public class DepositAccountTermAndPreClosure extends AbstractPersistableCustom { + + public static final String STATUS_PENDING = "PENDING"; + public static final String STATUS_APPROVED = "APPROVED"; + public static final String STATUS_REJECTED = "REJECTED"; + + public static final String TYPE_SAVINGS = "SAVINGS"; + public static final String TYPE_CURRENT = "CURRENT"; + public static final String TYPE_SHARE = "SHARE"; + public static final String TYPE_FD = "FD"; + public static final String TYPE_RD = "RD"; + + @Column(name = "product_id", nullable = false) + private Long productId; + + @Column(name = "product_type", nullable = false, length = 20) + private String productType; + + @ManyToOne + @JoinColumn(name = "createdby_id", nullable = false) + private AppUser maker; + + @ManyToOne + @JoinColumn(name = "lastmodifiedby_id") + private AppUser checker; + + @Column(name = "created_date", nullable = false) + private LocalDateTime createdOn; + + @Column(name = "lastmodified_date") + private LocalDateTime checkedOn; + + @Column(name = "status", nullable = false, length = 20) + private String status; + + @Column(name = "reject_reason", length = 500) + private String rejectReason; + + + + /** + * ":" while PENDING, NULL otherwise - the real DB-level lock. Unique constraints treat + * NULL as distinct in both PostgreSQL and MySQL/MariaDB, so only one PENDING row per (productType, productId) + * can ever exist, while resolved rows are kept indefinitely for audit history without blocking future edits. + */ + @Column(name = "pending_lock_key", length = 40) + private String pendingLockKey; + + /** Links to the audit-only m_portfolio_command_source row created for this edit's submit/approve/reject. */ + @Column(name = "command_source_id") + private Long commandSourceId; + + /** + * Typed replacement for proposedPayloadJson - one embeddable group per concern, all mapped + * onto this same physical table. proposedPayloadJson is still populated (kept for a safe + * deprecation window) but a product type's write-service uses these typed columns, plus the + * satellite tables (ProductPendingEditCharge/FacilityLimit/GlMapping/MarketPrice) and the + * ProductPendingEditSubmittedField manifest, to build and replay an edit once its cutover + * (see SavingsProductWritePlatformServiceJpaRepositoryImpl and siblings) is complete. + */ + @Embedded + private SavingsCoreEditFields coreFields = new SavingsCoreEditFields(); + + @Embedded + private CurrentOnlyEditFields currentOnlyFields = new CurrentOnlyEditFields(); + + @Embedded + private ShareOnlyEditFields shareOnlyFields = new ShareOnlyEditFields(); + + @Embedded + private FixedDepositEditFields fixedDepositFields = new FixedDepositEditFields(); + + @Embedded + private RecurringDepositEditFields recurringDepositFields = new RecurringDepositEditFields(); + + @Embedded + private FacilityConfigEditFields facilityConfigFields = new FacilityConfigEditFields(); + + @Embedded + private DepositLimitKycEditFields depositLimitKycFields = new DepositLimitKycEditFields(); + + @Embedded + private GlAccountMappingEditFields glAccountMappingFields = new GlAccountMappingEditFields(); + + protected ProductPendingEdit() {} + + public ProductPendingEdit(final String productType, final Long productId, final AppUser maker) { + this.productType = productType; + this.productId = productId; + this.maker = maker; + this.createdOn = LocalDateTime.now(); + this.status = STATUS_PENDING; + this.pendingLockKey = lockKeyFor(productType, productId); + } + + public static String lockKeyFor(final String productType, final Long productId) { + return productType + ":" + productId; + } + + public Long getProductId() { + return this.productId; + } + + public String getProductType() { + return this.productType; + } + + public AppUser getMaker() { + return this.maker; + } + + public AppUser getChecker() { + return this.checker; + } + + public LocalDateTime getCreatedOn() { + return this.createdOn; + } + + public LocalDateTime getCheckedOn() { + return this.checkedOn; + } + + public String getStatus() { + return this.status; + } + + public String getRejectReason() { + return this.rejectReason; + } + + + + public Long getCommandSourceId() { + return this.commandSourceId; + } + + public void setCommandSourceId(final Long commandSourceId) { + this.commandSourceId = commandSourceId; + } + + public boolean isPending() { + return STATUS_PENDING.equals(this.status); + } + + public void markApproved(final AppUser checker) { + this.status = STATUS_APPROVED; + this.checker = checker; + this.checkedOn = LocalDateTime.now(); + this.pendingLockKey = null; + } + + public void markRejected(final AppUser checker, final String reason) { + this.status = STATUS_REJECTED; + this.checker = checker; + this.checkedOn = LocalDateTime.now(); + this.rejectReason = reason; + this.pendingLockKey = null; + } +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditCharge.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditCharge.java new file mode 100644 index 000000000..b88d6c14b --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditCharge.java @@ -0,0 +1,50 @@ +/** + * 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.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; + +/** One row per charge id proposed to be attached to the product, mirroring the "charges" array. */ +@Entity +@Table(name = "m_product_temp_charge") +@Getter +@Setter +@NoArgsConstructor +public class ProductPendingEditCharge extends AbstractPersistableCustom { + + @ManyToOne + @JoinColumn(name = "pending_edit_id", nullable = false) + private ProductPendingEdit pendingEdit; + + @Column(name = "charge_id", nullable = false) + private Long chargeId; + + public ProductPendingEditCharge(final ProductPendingEdit pendingEdit, final Long chargeId) { + this.pendingEdit = pendingEdit; + this.chargeId = chargeId; + } +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditChargeRepository.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditChargeRepository.java new file mode 100644 index 000000000..0a0688b49 --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditChargeRepository.java @@ -0,0 +1,29 @@ +/** + * 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.domain; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ProductPendingEditChargeRepository extends JpaRepository { + + List findByPendingEditId(Long pendingEditId); +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditFacilityLimit.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditFacilityLimit.java new file mode 100644 index 000000000..55a0f271e --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditFacilityLimit.java @@ -0,0 +1,63 @@ +/** + * 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.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; + +/** One row per entry of the "accountFacilityLimits" array. Values can be "UNLIMITED"/"N/A"/numeric-as-string. */ +@Entity +@Table(name = "m_product_temp_facility_limit") +@Getter +@Setter +@NoArgsConstructor +public class ProductPendingEditFacilityLimit extends AbstractPersistableCustom { + + @ManyToOne + @JoinColumn(name = "pending_edit_id", nullable = false) + private ProductPendingEdit pendingEdit; + + @Column(name = "limit_type", nullable = false) + private String limitType; + + @Column(name = "general_limit") + private String generalLimit; + + @Column(name = "minor_account_limit") + private String minorAccountLimit; + + @Column(name = "small_saving_limit") + private String smallSavingLimit; + + public ProductPendingEditFacilityLimit(final ProductPendingEdit pendingEdit, final String limitType, final String generalLimit, + final String minorAccountLimit, final String smallSavingLimit) { + this.pendingEdit = pendingEdit; + this.limitType = limitType; + this.generalLimit = generalLimit; + this.minorAccountLimit = minorAccountLimit; + this.smallSavingLimit = smallSavingLimit; + } +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditFacilityLimitRepository.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditFacilityLimitRepository.java new file mode 100644 index 000000000..1b6577a37 --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditFacilityLimitRepository.java @@ -0,0 +1,29 @@ +/** + * 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.domain; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ProductPendingEditFacilityLimitRepository extends JpaRepository { + + List findByPendingEditId(Long pendingEditId); +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditFieldMapper.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditFieldMapper.java new file mode 100644 index 000000000..030703976 --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditFieldMapper.java @@ -0,0 +1,996 @@ +/** + * 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.domain; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.apache.fineract.infrastructure.core.api.JsonCommand; + +/** + * Shared submit-time extraction / approve-time reconstruction logic for the scalar and 1:1 + * "container" fields that Savings, Current, Fixed Deposit and Recurring Deposit products all have + * in common (FD/RD extend the Savings field set). Each populateXxx method reads present JSON + * parameters into the corresponding typed embeddable, returning the set of parameter names it + * found (which the caller must persist as {@link ProductPendingEditSubmittedField} rows - this is + * how the manifest preserves "field omitted" vs "field explicitly null" vs "field has a value"). + * Each writeXxxToJson method is the exact inverse, used at approval time to rebuild a JsonObject + * equivalent to what the maker originally submitted, before handing it to the unchanged + * update()/updateProduct() methods via JsonCommand.fromJsonElement(...). + * + * Share Product's field set is largely disjoint from this shared core and is mapped separately. + */ +public final class ProductPendingEditFieldMapper { + + private ProductPendingEditFieldMapper() {} + + // ---- Savings/Current/FD/RD shared core fields ---------------------------------------------- + + public static Set populateCoreFields(final JsonCommand command, final SavingsCoreEditFields f) { + final Set present = new HashSet<>(); + + if (command.hasParameter("name")) { + f.setName(command.stringValueOfParameterNamed("name")); + present.add("name"); + } + if (command.hasParameter("shortName")) { + f.setShortName(command.stringValueOfParameterNamed("shortName")); + present.add("shortName"); + } + if (command.hasParameter("description")) { + f.setDescription(command.stringValueOfParameterNamed("description")); + present.add("description"); + } + if (command.hasParameter("currencyCode")) { + f.setCurrencyCode(command.stringValueOfParameterNamed("currencyCode")); + present.add("currencyCode"); + } + if (command.hasParameter("digitsAfterDecimal")) { + f.setDigitsAfterDecimal(command.integerValueOfParameterNamed("digitsAfterDecimal")); + present.add("digitsAfterDecimal"); + } + if (command.hasParameter("inMultiplesOf")) { + f.setInMultiplesOf(command.integerValueOfParameterNamed("inMultiplesOf")); + present.add("inMultiplesOf"); + } + if (command.hasParameter("accountingRule")) { + f.setAccountingRule(command.integerValueOfParameterNamed("accountingRule")); + present.add("accountingRule"); + } + if (command.hasParameter("nominalAnnualInterestRate")) { + f.setNominalAnnualInterestRate(command.bigDecimalValueOfParameterNamed("nominalAnnualInterestRate")); + present.add("nominalAnnualInterestRate"); + } + if (command.hasParameter("interestCompoundingPeriodType")) { + f.setInterestCompoundingPeriodType(command.integerValueOfParameterNamed("interestCompoundingPeriodType")); + present.add("interestCompoundingPeriodType"); + } + if (command.hasParameter("interestPostingPeriodType")) { + f.setInterestPostingPeriodType(command.integerValueOfParameterNamed("interestPostingPeriodType")); + present.add("interestPostingPeriodType"); + } + if (command.hasParameter("interestCalculationType")) { + f.setInterestCalculationType(command.integerValueOfParameterNamed("interestCalculationType")); + present.add("interestCalculationType"); + } + if (command.hasParameter("interestCalculationDaysInYearType")) { + f.setInterestCalculationDaysInYearType(command.integerValueOfParameterNamed("interestCalculationDaysInYearType")); + present.add("interestCalculationDaysInYearType"); + } + if (command.hasParameter("minRequiredOpeningBalance")) { + f.setMinRequiredOpeningBalance(command.bigDecimalValueOfParameterNamed("minRequiredOpeningBalance")); + present.add("minRequiredOpeningBalance"); + } + if (command.hasParameter("lockinPeriodFrequency")) { + f.setLockinPeriodFrequency(command.integerValueOfParameterNamed("lockinPeriodFrequency")); + present.add("lockinPeriodFrequency"); + } + if (command.hasParameter("lockinPeriodFrequencyType")) { + f.setLockinPeriodFrequencyType(command.integerValueOfParameterNamed("lockinPeriodFrequencyType")); + present.add("lockinPeriodFrequencyType"); + } + if (command.hasParameter("withdrawalFeeForTransfers")) { + f.setWithdrawalFeeForTransfers(command.booleanObjectValueOfParameterNamed("withdrawalFeeForTransfers")); + present.add("withdrawalFeeForTransfers"); + } + if (command.hasParameter("allowOverdraft")) { + f.setAllowOverdraft(command.booleanObjectValueOfParameterNamed("allowOverdraft")); + present.add("allowOverdraft"); + } + if (command.hasParameter("overdraftLimit")) { + f.setOverdraftLimit(command.bigDecimalValueOfParameterNamed("overdraftLimit")); + present.add("overdraftLimit"); + } + if (command.hasParameter("nominalAnnualInterestRateOverdraft")) { + f.setNominalAnnualInterestRateOverdraft(command.bigDecimalValueOfParameterNamed("nominalAnnualInterestRateOverdraft")); + present.add("nominalAnnualInterestRateOverdraft"); + } + if (command.hasParameter("minOverdraftForInterestCalculation")) { + f.setMinOverdraftForInterestCalculation(command.bigDecimalValueOfParameterNamed("minOverdraftForInterestCalculation")); + present.add("minOverdraftForInterestCalculation"); + } + if (command.hasParameter("enforceMinRequiredBalance")) { + f.setEnforceMinRequiredBalance(command.booleanObjectValueOfParameterNamed("enforceMinRequiredBalance")); + present.add("enforceMinRequiredBalance"); + } + if (command.hasParameter("minRequiredBalance")) { + f.setMinRequiredBalance(command.bigDecimalValueOfParameterNamed("minRequiredBalance")); + present.add("minRequiredBalance"); + } + if (command.hasParameter("lienAllowed")) { + f.setLienAllowed(command.booleanObjectValueOfParameterNamed("lienAllowed")); + present.add("lienAllowed"); + } + if (command.hasParameter("maxAllowedLienLimit")) { + f.setMaxAllowedLienLimit(command.bigDecimalValueOfParameterNamed("maxAllowedLienLimit")); + present.add("maxAllowedLienLimit"); + } + if (command.hasParameter("minBalanceForInterestCalculation")) { + f.setMinBalanceForInterestCalculation(command.bigDecimalValueOfParameterNamed("minBalanceForInterestCalculation")); + present.add("minBalanceForInterestCalculation"); + } + if (command.hasParameter("withHoldTax")) { + f.setWithHoldTax(command.booleanObjectValueOfParameterNamed("withHoldTax")); + present.add("withHoldTax"); + } + if (command.hasParameter("taxGroupId")) { + f.setTaxGroupId(command.longValueOfParameterNamed("taxGroupId")); + present.add("taxGroupId"); + } + if (command.hasParameter("otherGlMappings")) { + f.setOtherGlMappings(command.stringValueOfParameterNamed("otherGlMappings")); + present.add("otherGlMappings"); + } + if (command.hasParameter("isDormancyTrackingActive")) { + f.setIsDormancyTrackingActive(command.booleanObjectValueOfParameterNamed("isDormancyTrackingActive")); + present.add("isDormancyTrackingActive"); + } + if (command.hasParameter("daysToInactive")) { + f.setDaysToInactive(command.integerValueOfParameterNamed("daysToInactive")); + present.add("daysToInactive"); + } + if (command.hasParameter("daysToDormancy")) { + f.setDaysToDormancy(command.integerValueOfParameterNamed("daysToDormancy")); + present.add("daysToDormancy"); + } + if (command.hasParameter("daysToEscheat")) { + f.setDaysToEscheat(command.integerValueOfParameterNamed("daysToEscheat")); + present.add("daysToEscheat"); + } + if (command.hasParameter("accountType")) { + f.setAccountType(command.stringValueOfParameterNamed("accountType")); + present.add("accountType"); + } + if (command.hasParameter("customerSegment")) { + f.setCustomerSegment(command.stringValueOfParameterNamed("customerSegment")); + present.add("customerSegment"); + } + if (command.hasParameter("productCode")) { + f.setProductCode(command.stringValueOfParameterNamed("productCode")); + present.add("productCode"); + } + if (command.hasParameter("glAccountId")) { + f.setGlAccountId(command.longValueOfParameterNamed("glAccountId")); + present.add("glAccountId"); + } + if (command.hasParameter("effectiveDate")) { + f.setEffectiveDate(command.localDateValueOfParameterNamed("effectiveDate")); + present.add("effectiveDate"); + } + if (command.hasParameter("effectiveTo")) { + f.setEffectiveTo(command.localDateValueOfParameterNamed("effectiveTo")); + present.add("effectiveTo"); + } + if (command.hasParameter("endDate")) { + f.setEndDate(command.localDateValueOfParameterNamed("endDate")); + present.add("endDate"); + } + if (command.hasParameter("introducerRequired")) { + f.setIntroducerRequired(command.booleanObjectValueOfParameterNamed("introducerRequired")); + present.add("introducerRequired"); + } + if (command.hasParameter("shareLinkingEnabled")) { + f.setShareLinkingEnabled(command.booleanObjectValueOfParameterNamed("shareLinkingEnabled")); + present.add("shareLinkingEnabled"); + } + if (command.hasParameter("nominationFacility")) { + f.setNominationFacility(command.booleanObjectValueOfParameterNamed("nominationFacility")); + present.add("nominationFacility"); + } + if (command.hasParameter("dormancyAlert")) { + f.setDormancyAlert(command.booleanObjectValueOfParameterNamed("dormancyAlert")); + present.add("dormancyAlert"); + } + if (command.hasParameter("interestCalculateSelector")) { + f.setInterestCalculateSelector(command.integerValueOfParameterNamed("interestCalculateSelector")); + present.add("interestCalculateSelector"); + } + if (command.hasParameter("averagePeriod")) { + f.setAveragePeriod(command.stringValueOfParameterNamed("averagePeriod")); + present.add("averagePeriod"); + } + if (command.hasParameter("autoPost")) { + f.setAutoPost(command.booleanObjectValueOfParameterNamed("autoPost")); + present.add("autoPost"); + } + if (command.hasParameter("frequencyFixDay")) { + f.setFrequencyFixDay(command.integerValueOfParameterNamed("frequencyFixDay")); + present.add("frequencyFixDay"); + } + if (command.hasParameter("lastInterestDate")) { + f.setLastInterestDate(command.localDateValueOfParameterNamed("lastInterestDate")); + present.add("lastInterestDate"); + } + if (command.hasParameter("enableLockinPeriod")) { + f.setEnableLockinPeriod(command.booleanObjectValueOfParameterNamed("enableLockinPeriod")); + present.add("enableLockinPeriod"); + } + if (command.hasParameter("balanceFrequency")) { + f.setBalanceFrequency(command.stringValueOfParameterNamed("balanceFrequency")); + present.add("balanceFrequency"); + } + if (command.hasParameter("accountClosedZeroBalanceInDayClosing")) { + f.setAccountClosedZeroBalanceInDayClosing(command.booleanObjectValueOfParameterNamed("accountClosedZeroBalanceInDayClosing")); + present.add("accountClosedZeroBalanceInDayClosing"); + } + if (command.hasParameter("balanceSwapOption")) { + f.setBalanceSwapOption(command.booleanObjectValueOfParameterNamed("balanceSwapOption")); + present.add("balanceSwapOption"); + } + if (command.hasParameter("msme")) { + f.setMsme(command.booleanObjectValueOfParameterNamed("msme")); + present.add("msme"); + } + if (command.hasParameter("autoDebitFacility")) { + f.setAutoDebitFacility(command.booleanObjectValueOfParameterNamed("autoDebitFacility")); + present.add("autoDebitFacility"); + } + if (command.hasParameter("loanFacility")) { + f.setLoanFacility(command.booleanObjectValueOfParameterNamed("loanFacility")); + present.add("loanFacility"); + } + if (command.hasParameter("prematureWithdrawalOption")) { + f.setPrematureWithdrawalOption(command.booleanObjectValueOfParameterNamed("prematureWithdrawalOption")); + present.add("prematureWithdrawalOption"); + } + if (command.hasParameter("tenureFlexibility")) { + f.setTenureFlexibility(command.booleanObjectValueOfParameterNamed("tenureFlexibility")); + present.add("tenureFlexibility"); + } + if (command.hasParameter("lienMarked")) { + f.setLienMarked(command.booleanObjectValueOfParameterNamed("lienMarked")); + present.add("lienMarked"); + } + if (command.hasParameter("maturityInstructions")) { + f.setMaturityInstructions(command.booleanObjectValueOfParameterNamed("maturityInstructions")); + present.add("maturityInstructions"); + } + if (command.hasParameter("creditMaturityProceeds")) { + f.setCreditMaturityProceeds(command.booleanObjectValueOfParameterNamed("creditMaturityProceeds")); + present.add("creditMaturityProceeds"); + } + if (command.hasParameter("penaltyGracePeriodMissedInstallments")) { + f.setPenaltyGracePeriodMissedInstallments(command.booleanObjectValueOfParameterNamed("penaltyGracePeriodMissedInstallments")); + present.add("penaltyGracePeriodMissedInstallments"); + } + if (command.hasParameter("panelRequestApplicable")) { + f.setPanelRequestApplicable(command.booleanObjectValueOfParameterNamed("panelRequestApplicable")); + present.add("panelRequestApplicable"); + } + if (command.hasParameter("noPenaltyNomineeHeirClosure")) { + f.setNoPenaltyNomineeHeirClosure(command.booleanObjectValueOfParameterNamed("noPenaltyNomineeHeirClosure")); + present.add("noPenaltyNomineeHeirClosure"); + } + if (command.hasParameter("deceasedSettlement")) { + f.setDeceasedSettlement(command.booleanObjectValueOfParameterNamed("deceasedSettlement")); + present.add("deceasedSettlement"); + } + if (command.hasParameter("allowDebitTxnAfterDueDate")) { + f.setAllowDebitTxnAfterDueDate(command.booleanObjectValueOfParameterNamed("allowDebitTxnAfterDueDate")); + present.add("allowDebitTxnAfterDueDate"); + } + if (command.hasParameter("depositTypeEnum")) { + f.setDepositTypeEnum(command.integerValueOfParameterNamed("depositTypeEnum")); + present.add("depositTypeEnum"); + } + if (command.hasParameter("withdrawalFeeAmount")) { + f.setWithdrawalFeeAmount(command.bigDecimalValueOfParameterNamed("withdrawalFeeAmount")); + present.add("withdrawalFeeAmount"); + } + if (command.hasParameter("withdrawalFeeTypeEnum")) { + f.setWithdrawalFeeTypeEnum(command.integerValueOfParameterNamed("withdrawalFeeTypeEnum")); + present.add("withdrawalFeeTypeEnum"); + } + return present; + } + + public static void writeCoreFieldsToJson(final JsonObject root, final SavingsCoreEditFields f, final Set submitted) { + if (f == null) { + return; + } + putString(root, submitted, "name", f.getName()); + putString(root, submitted, "shortName", f.getShortName()); + putString(root, submitted, "description", f.getDescription()); + putString(root, submitted, "currencyCode", f.getCurrencyCode()); + putNumber(root, submitted, "digitsAfterDecimal", f.getDigitsAfterDecimal()); + putNumber(root, submitted, "inMultiplesOf", f.getInMultiplesOf()); + putNumber(root, submitted, "accountingRule", f.getAccountingRule()); + putNumber(root, submitted, "nominalAnnualInterestRate", f.getNominalAnnualInterestRate()); + putNumber(root, submitted, "interestCompoundingPeriodType", f.getInterestCompoundingPeriodType()); + putNumber(root, submitted, "interestPostingPeriodType", f.getInterestPostingPeriodType()); + putNumber(root, submitted, "interestCalculationType", f.getInterestCalculationType()); + putNumber(root, submitted, "interestCalculationDaysInYearType", f.getInterestCalculationDaysInYearType()); + putNumber(root, submitted, "minRequiredOpeningBalance", f.getMinRequiredOpeningBalance()); + putNumber(root, submitted, "lockinPeriodFrequency", f.getLockinPeriodFrequency()); + putNumber(root, submitted, "lockinPeriodFrequencyType", f.getLockinPeriodFrequencyType()); + putBoolean(root, submitted, "withdrawalFeeForTransfers", f.getWithdrawalFeeForTransfers()); + putBoolean(root, submitted, "allowOverdraft", f.getAllowOverdraft()); + putNumber(root, submitted, "overdraftLimit", f.getOverdraftLimit()); + putNumber(root, submitted, "nominalAnnualInterestRateOverdraft", f.getNominalAnnualInterestRateOverdraft()); + putNumber(root, submitted, "minOverdraftForInterestCalculation", f.getMinOverdraftForInterestCalculation()); + putBoolean(root, submitted, "enforceMinRequiredBalance", f.getEnforceMinRequiredBalance()); + putNumber(root, submitted, "minRequiredBalance", f.getMinRequiredBalance()); + putBoolean(root, submitted, "lienAllowed", f.getLienAllowed()); + putNumber(root, submitted, "maxAllowedLienLimit", f.getMaxAllowedLienLimit()); + putNumber(root, submitted, "minBalanceForInterestCalculation", f.getMinBalanceForInterestCalculation()); + putBoolean(root, submitted, "withHoldTax", f.getWithHoldTax()); + putNumber(root, submitted, "taxGroupId", f.getTaxGroupId()); + putString(root, submitted, "otherGlMappings", f.getOtherGlMappings()); + putBoolean(root, submitted, "isDormancyTrackingActive", f.getIsDormancyTrackingActive()); + putNumber(root, submitted, "daysToInactive", f.getDaysToInactive()); + putNumber(root, submitted, "daysToDormancy", f.getDaysToDormancy()); + putNumber(root, submitted, "daysToEscheat", f.getDaysToEscheat()); + putString(root, submitted, "accountType", f.getAccountType()); + putString(root, submitted, "customerSegment", f.getCustomerSegment()); + putString(root, submitted, "productCode", f.getProductCode()); + putNumber(root, submitted, "glAccountId", f.getGlAccountId()); + putDate(root, submitted, "effectiveDate", f.getEffectiveDate()); + putDate(root, submitted, "effectiveTo", f.getEffectiveTo()); + putDate(root, submitted, "endDate", f.getEndDate()); + putBoolean(root, submitted, "introducerRequired", f.getIntroducerRequired()); + putBoolean(root, submitted, "shareLinkingEnabled", f.getShareLinkingEnabled()); + putBoolean(root, submitted, "nominationFacility", f.getNominationFacility()); + putBoolean(root, submitted, "dormancyAlert", f.getDormancyAlert()); + putNumber(root, submitted, "interestCalculateSelector", f.getInterestCalculateSelector()); + putString(root, submitted, "averagePeriod", f.getAveragePeriod()); + putBoolean(root, submitted, "autoPost", f.getAutoPost()); + putNumber(root, submitted, "frequencyFixDay", f.getFrequencyFixDay()); + putDate(root, submitted, "lastInterestDate", f.getLastInterestDate()); + putBoolean(root, submitted, "enableLockinPeriod", f.getEnableLockinPeriod()); + putString(root, submitted, "balanceFrequency", f.getBalanceFrequency()); + putBoolean(root, submitted, "accountClosedZeroBalanceInDayClosing", f.getAccountClosedZeroBalanceInDayClosing()); + putBoolean(root, submitted, "balanceSwapOption", f.getBalanceSwapOption()); + putBoolean(root, submitted, "msme", f.getMsme()); + putBoolean(root, submitted, "autoDebitFacility", f.getAutoDebitFacility()); + putBoolean(root, submitted, "loanFacility", f.getLoanFacility()); + putBoolean(root, submitted, "prematureWithdrawalOption", f.getPrematureWithdrawalOption()); + putBoolean(root, submitted, "tenureFlexibility", f.getTenureFlexibility()); + putBoolean(root, submitted, "lienMarked", f.getLienMarked()); + putBoolean(root, submitted, "maturityInstructions", f.getMaturityInstructions()); + putBoolean(root, submitted, "creditMaturityProceeds", f.getCreditMaturityProceeds()); + putBoolean(root, submitted, "penaltyGracePeriodMissedInstallments", f.getPenaltyGracePeriodMissedInstallments()); + putBoolean(root, submitted, "panelRequestApplicable", f.getPanelRequestApplicable()); + putBoolean(root, submitted, "noPenaltyNomineeHeirClosure", f.getNoPenaltyNomineeHeirClosure()); + putBoolean(root, submitted, "deceasedSettlement", f.getDeceasedSettlement()); + putBoolean(root, submitted, "allowDebitTxnAfterDueDate", f.getAllowDebitTxnAfterDueDate()); + putNumber(root, submitted, "depositTypeEnum", f.getDepositTypeEnum()); + putNumber(root, submitted, "withdrawalFeeAmount", f.getWithdrawalFeeAmount()); + putNumber(root, submitted, "withdrawalFeeTypeEnum", f.getWithdrawalFeeTypeEnum()); + } + + // ---- accountFacilityConfig (1:1 container) -------------------------------------------------- + + public static boolean populateFacilityConfig(final JsonCommand command, final FacilityConfigEditFields f) { + if (!command.hasParameter("accountFacilityConfig")) { + return false; + } + f.setAtmFacility(booleanOf(command, "accountFacilityConfig", "atmFacility")); + f.setAtmWithdrawalFacility(booleanOf(command, "accountFacilityConfig", "atmWithdrawalFacility")); + f.setUpiFacility(booleanOf(command, "accountFacilityConfig", "upiFacility")); + f.setPosTransactionsFacility(booleanOf(command, "accountFacilityConfig", "posTransactionsFacility")); + f.setChequeBook(booleanOf(command, "accountFacilityConfig", "chequeBook")); + f.setImpsFacility(booleanOf(command, "accountFacilityConfig", "impsFacility")); + f.setSmsAlerts(booleanOf(command, "accountFacilityConfig", "smsAlerts")); + f.setBbpsFacility(booleanOf(command, "accountFacilityConfig", "bbpsFacility")); + f.setInsuranceCover(booleanOf(command, "accountFacilityConfig", "insuranceCover")); + f.setBepgFacility(booleanOf(command, "accountFacilityConfig", "bepgFacility")); + f.setNeftFacility(booleanOf(command, "accountFacilityConfig", "neftFacility")); + f.setRtgsFacility(booleanOf(command, "accountFacilityConfig", "rtgsFacility")); + f.setInterBankTotalCapFacility(booleanOf(command, "accountFacilityConfig", "interBankTotalCapFacility")); + f.setWithdrawalFacility(booleanOf(command, "accountFacilityConfig", "withdrawalFacility")); + f.setDepositFacilityKyc(booleanOf(command, "accountFacilityConfig", "depositFacilityKyc")); + f.setVirtualDebitCard(booleanOf(command, "accountFacilityConfig", "virtualDebitCard")); + return true; + } + + public static void writeFacilityConfigToJson(final JsonObject root, final FacilityConfigEditFields f, final boolean submitted) { + if (f == null || !submitted) { + return; + } + final JsonObject config = new JsonObject(); + config.addProperty("atmFacility", f.getAtmFacility()); + config.addProperty("atmWithdrawalFacility", f.getAtmWithdrawalFacility()); + config.addProperty("upiFacility", f.getUpiFacility()); + config.addProperty("posTransactionsFacility", f.getPosTransactionsFacility()); + config.addProperty("chequeBook", f.getChequeBook()); + config.addProperty("impsFacility", f.getImpsFacility()); + config.addProperty("smsAlerts", f.getSmsAlerts()); + config.addProperty("bbpsFacility", f.getBbpsFacility()); + config.addProperty("insuranceCover", f.getInsuranceCover()); + config.addProperty("bepgFacility", f.getBepgFacility()); + config.addProperty("neftFacility", f.getNeftFacility()); + config.addProperty("rtgsFacility", f.getRtgsFacility()); + config.addProperty("interBankTotalCapFacility", f.getInterBankTotalCapFacility()); + config.addProperty("withdrawalFacility", f.getWithdrawalFacility()); + config.addProperty("depositFacilityKyc", f.getDepositFacilityKyc()); + config.addProperty("virtualDebitCard", f.getVirtualDebitCard()); + root.add("accountFacilityConfig", config); + } + + // ---- depositLimitKyc (1:1 container, nested general/minor/smallSaving shape) ---------------- + + public static boolean populateDepositLimitKyc(final JsonCommand command, final DepositLimitKycEditFields f) { + if (!command.hasParameter("depositLimitKyc")) { + return false; + } + final com.google.gson.JsonElement raw = command.parsedJson().getAsJsonObject().get("depositLimitKyc"); + if (raw == null || !raw.isJsonObject()) { + return true; + } + final JsonObject kyc = raw.getAsJsonObject(); + if (kyc.has("general") && kyc.get("general").isJsonObject()) { + final JsonObject general = kyc.getAsJsonObject("general"); + f.setGeneralType(stringOrNull(general, "kycType")); + f.setGeneralLimit(stringOrNull(general, "limit")); + } + if (kyc.has("minor") && kyc.get("minor").isJsonObject()) { + final JsonObject minor = kyc.getAsJsonObject("minor"); + f.setMinorType(stringOrNull(minor, "kycType")); + final String minorLimit = stringOrNull(minor, "limit"); + f.setMinorLimit(minorLimit == null ? null : Long.valueOf(minorLimit)); + } + if (kyc.has("smallSaving") && kyc.get("smallSaving").isJsonObject()) { + final JsonObject smallSaving = kyc.getAsJsonObject("smallSaving"); + f.setSmallSavingType(stringOrNull(smallSaving, "kycType")); + final String smallSavingLimit = stringOrNull(smallSaving, "limit"); + f.setSmallSavingLimit(smallSavingLimit == null ? null : Long.valueOf(smallSavingLimit)); + } + return true; + } + + public static void writeDepositLimitKycToJson(final JsonObject root, final DepositLimitKycEditFields f, final boolean submitted) { + if (f == null || !submitted) { + return; + } + final JsonObject kyc = new JsonObject(); + final JsonObject general = new JsonObject(); + general.addProperty("kycType", f.getGeneralType()); + general.addProperty("limit", f.getGeneralLimit()); + kyc.add("general", general); + final JsonObject minor = new JsonObject(); + minor.addProperty("kycType", f.getMinorType()); + if (f.getMinorLimit() != null) { + minor.addProperty("limit", f.getMinorLimit()); + } + kyc.add("minor", minor); + final JsonObject smallSaving = new JsonObject(); + smallSaving.addProperty("kycType", f.getSmallSavingType()); + if (f.getSmallSavingLimit() != null) { + smallSaving.addProperty("limit", f.getSmallSavingLimit()); + } + kyc.add("smallSaving", smallSaving); + root.add("depositLimitKyc", kyc); + } + + // ---- GL scalar account mapping (1:1) ---------------------------------------------------------- + + public static boolean populateGlAccountMapping(final JsonCommand command, final GlAccountMappingEditFields f) { + boolean any = false; + any |= setIfPresent(command, "savingsReferenceAccountId", f::setSavingsReferenceAccountId); + any |= setIfPresent(command, "savingsControlAccountId", f::setSavingsControlAccountId); + any |= setIfPresent(command, "incomeFromFeeAccountId", f::setIncomeFromFeeAccountId); + any |= setIfPresent(command, "incomeFromPenaltyAccountId", f::setIncomeFromPenaltyAccountId); + any |= setIfPresent(command, "interestOnSavingsAccountId", f::setInterestOnSavingsAccountId); + any |= setIfPresent(command, "transfersInSuspenseAccountId", f::setTransfersInSuspenseAccountId); + any |= setIfPresent(command, "overdraftPortfolioControlId", f::setOverdraftPortfolioControlId); + any |= setIfPresent(command, "incomeFromInterestId", f::setIncomeFromInterestId); + any |= setIfPresent(command, "writeOffAccountId", f::setWriteOffAccountId); + any |= setIfPresent(command, "escheatLiabilityId", f::setEscheatLiabilityId); + any |= setIfPresent(command, "penaltiesReceivableAccountId", f::setPenaltiesReceivableAccountId); + any |= setIfPresent(command, "interestReceivableAccountId", f::setInterestReceivableAccountId); + any |= setIfPresent(command, "feesReceivableAccountId", f::setFeesReceivableAccountId); + any |= setIfPresent(command, "interestPayableAccountId", f::setInterestPayableAccountId); + return any; + } + + public static void writeGlAccountMappingToJson(final JsonObject root, final GlAccountMappingEditFields f) { + if (f == null) { + return; + } + putNumber(root, "savingsReferenceAccountId", f.getSavingsReferenceAccountId()); + putNumber(root, "savingsControlAccountId", f.getSavingsControlAccountId()); + putNumber(root, "incomeFromFeeAccountId", f.getIncomeFromFeeAccountId()); + putNumber(root, "incomeFromPenaltyAccountId", f.getIncomeFromPenaltyAccountId()); + putNumber(root, "interestOnSavingsAccountId", f.getInterestOnSavingsAccountId()); + putNumber(root, "transfersInSuspenseAccountId", f.getTransfersInSuspenseAccountId()); + putNumber(root, "overdraftPortfolioControlId", f.getOverdraftPortfolioControlId()); + putNumber(root, "incomeFromInterestId", f.getIncomeFromInterestId()); + putNumber(root, "writeOffAccountId", f.getWriteOffAccountId()); + putNumber(root, "escheatLiabilityId", f.getEscheatLiabilityId()); + putNumber(root, "penaltiesReceivableAccountId", f.getPenaltiesReceivableAccountId()); + putNumber(root, "interestReceivableAccountId", f.getInterestReceivableAccountId()); + putNumber(root, "feesReceivableAccountId", f.getFeesReceivableAccountId()); + putNumber(root, "interestPayableAccountId", f.getInterestPayableAccountId()); + } + + // ---- charges (1:many, satellite table) -------------------------------------------------------- + + /** Returns null if "charges" wasn't present at all, else the (possibly empty) list of charge ids. */ + public static List extractChargeIds(final JsonCommand command) { + if (!command.hasParameter("charges")) { + return null; + } + final com.google.gson.JsonElement raw = command.parsedJson().getAsJsonObject().get("charges"); + final List ids = new java.util.ArrayList<>(); + if (raw != null && raw.isJsonArray()) { + for (final com.google.gson.JsonElement el : raw.getAsJsonArray()) { + if (el.isJsonObject() && el.getAsJsonObject().has("id")) { + ids.add(el.getAsJsonObject().get("id").getAsLong()); + } + } + } + return ids; + } + + public static void writeChargesToJson(final JsonObject root, final List charges) { + final JsonArray array = new JsonArray(); + for (final ProductPendingEditCharge charge : charges) { + final JsonObject entry = new JsonObject(); + entry.addProperty("id", charge.getChargeId()); + array.add(entry); + } + root.add("charges", array); + } + + // ---- accountFacilityLimits (1:many, satellite table) ------------------------------------------- + + public static List extractFacilityLimits(final JsonCommand command, final ProductPendingEdit edit) { + if (!command.hasParameter("accountFacilityLimits")) { + return null; + } + final com.google.gson.JsonElement raw = command.parsedJson().getAsJsonObject().get("accountFacilityLimits"); + final List limits = new java.util.ArrayList<>(); + if (raw != null && raw.isJsonArray()) { + for (final com.google.gson.JsonElement el : raw.getAsJsonArray()) { + final JsonObject o = el.getAsJsonObject(); + limits.add(new ProductPendingEditFacilityLimit(edit, stringOrNull(o, "limitType"), stringOrNull(o, "generalLimit"), + stringOrNull(o, "minorAccountLimit"), stringOrNull(o, "smallSavingLimit"))); + } + } + return limits; + } + + public static void writeFacilityLimitsToJson(final JsonObject root, final List limits) { + final JsonArray array = new JsonArray(); + for (final ProductPendingEditFacilityLimit limit : limits) { + final JsonObject entry = new JsonObject(); + entry.addProperty("limitType", limit.getLimitType()); + entry.addProperty("generalLimit", limit.getGeneralLimit()); + entry.addProperty("minorAccountLimit", limit.getMinorAccountLimit()); + entry.addProperty("smallSavingLimit", limit.getSmallSavingLimit()); + array.add(entry); + } + root.add("accountFacilityLimits", array); + } + + // ---- GL array mappings (1:many, satellite table) ----------------------------------------------- + + public static List extractGlArrayMappings(final JsonCommand command, final ProductPendingEdit edit) { + final List mappings = new java.util.ArrayList<>(); + boolean any = false; + any |= extractGlArray(command, edit, "paymentChannelToFundSourceMappings", ProductPendingEditGlMapping.TYPE_PAYMENT_CHANNEL, + "paymentTypeId", mappings); + any |= extractGlArray(command, edit, "feeToIncomeAccountMappings", ProductPendingEditGlMapping.TYPE_FEE, "chargeId", mappings); + any |= extractGlArray(command, edit, "penaltyToIncomeAccountMappings", ProductPendingEditGlMapping.TYPE_PENALTY, "chargeId", + mappings); + return any ? mappings : null; + } + + private static boolean extractGlArray(final JsonCommand command, final ProductPendingEdit edit, final String paramName, + final String mappingType, final String idFieldName, final List out) { + if (!command.hasParameter(paramName)) { + return false; + } + final com.google.gson.JsonElement raw = command.parsedJson().getAsJsonObject().get(paramName); + if (raw != null && raw.isJsonArray()) { + for (final com.google.gson.JsonElement el : raw.getAsJsonArray()) { + final JsonObject o = el.getAsJsonObject(); + final Long idFieldValue = o.has(idFieldName) ? o.get(idFieldName).getAsLong() : null; + final Long glAccountId = o.has("glAccountId") ? o.get("glAccountId").getAsLong() : null; + final Long paymentTypeId = "paymentTypeId".equals(idFieldName) ? idFieldValue : null; + final Long chargeId = "chargeId".equals(idFieldName) ? idFieldValue : null; + out.add(new ProductPendingEditGlMapping(edit, mappingType, paymentTypeId, chargeId, glAccountId)); + } + } + return true; + } + + public static void writeGlArrayMappingsToJson(final JsonObject root, final List mappings) { + writeGlArrayGroup(root, mappings, ProductPendingEditGlMapping.TYPE_PAYMENT_CHANNEL, "paymentChannelToFundSourceMappings", + "paymentTypeId"); + writeGlArrayGroup(root, mappings, ProductPendingEditGlMapping.TYPE_FEE, "feeToIncomeAccountMappings", "chargeId"); + writeGlArrayGroup(root, mappings, ProductPendingEditGlMapping.TYPE_PENALTY, "penaltyToIncomeAccountMappings", "chargeId"); + } + + private static void writeGlArrayGroup(final JsonObject root, final List mappings, final String type, + final String paramName, final String idFieldName) { + final JsonArray array = new JsonArray(); + for (final ProductPendingEditGlMapping mapping : mappings) { + if (!type.equals(mapping.getMappingType())) { + continue; + } + final JsonObject entry = new JsonObject(); + final Long idFieldValue = "paymentTypeId".equals(idFieldName) ? mapping.getPaymentTypeId() : mapping.getChargeId(); + entry.addProperty(idFieldName, idFieldValue); + entry.addProperty("glAccountId", mapping.getGlAccountId()); + array.add(entry); + } + root.add(paramName, array); + } + + // ---- small helpers ----------------------------------------------------------------------------- + + private static boolean booleanOf(final JsonCommand command, final String containerName, final String fieldName) { + final com.google.gson.JsonElement raw = command.parsedJson().getAsJsonObject().get(containerName); + if (raw == null || !raw.isJsonObject()) { + return false; + } + final JsonObject container = raw.getAsJsonObject(); + return container.has(fieldName) && !container.get(fieldName).isJsonNull() && container.get(fieldName).getAsBoolean(); + } + + private static String stringOrNull(final JsonObject o, final String field) { + return (o.has(field) && !o.get(field).isJsonNull()) ? o.get(field).getAsString() : null; + } + + private interface LongSetter { + + void set(Long value); + } + + private static boolean setIfPresent(final JsonCommand command, final String paramName, final LongSetter setter) { + if (!command.hasParameter(paramName)) { + return false; + } + setter.set(command.longValueOfParameterNamed(paramName)); + return true; + } + + private static void putString(final JsonObject root, final Set submitted, final String name, final String value) { + if (submitted.contains(name)) { + root.addProperty(name, value); + } + } + + private static void putNumber(final JsonObject root, final Set submitted, final String name, final Number value) { + if (submitted.contains(name)) { + root.addProperty(name, value); + } + } + + private static void putBoolean(final JsonObject root, final Set submitted, final String name, final Boolean value) { + if (submitted.contains(name)) { + root.addProperty(name, value); + } + } + + private static void putDate(final JsonObject root, final Set submitted, final String name, final LocalDate value) { + if (submitted.contains(name)) { + root.addProperty(name, value == null ? null : value.toString()); + } + } + + private static void putNumber(final JsonObject root, final String name, final Number value) { + if (value != null) { + root.addProperty(name, value); + } + } + + public static Set populateCurrentOnlyFields(final JsonCommand command, final CurrentOnlyEditFields f) { + final Set present = new java.util.HashSet<>(); + if (command.hasParameter("avgBalanceCalcPeriod")) { + f.setAvgBalanceCalcPeriod(command.stringValueOfParameterNamed("avgBalanceCalcPeriod")); + present.add("avgBalanceCalcPeriod"); + } + if (command.hasParameter("validForPeriod")) { + f.setValidForPeriod(command.stringValueOfParameterNamed("validForPeriod")); + present.add("validForPeriod"); + } + if (command.hasParameter("odAccountClosedZeroBalanceInDayClosing")) { + f.setOdAccountClosedZeroBalanceInDayClosing(command.booleanObjectValueOfParameterNamed("odAccountClosedZeroBalanceInDayClosing")); + present.add("odAccountClosedZeroBalanceInDayClosing"); + } + return present; + } + + public static void writeCurrentOnlyFieldsToJson(final JsonObject root, final CurrentOnlyEditFields f, final Set submitted) { + if (f == null) { + return; + } + putString(root, submitted, "avgBalanceCalcPeriod", f.getAvgBalanceCalcPeriod()); + putString(root, submitted, "validForPeriod", f.getValidForPeriod()); + putBoolean(root, submitted, "odAccountClosedZeroBalanceInDayClosing", f.getOdAccountClosedZeroBalanceInDayClosing()); + } + + public static Set populateShareOnlyFields(final JsonCommand command, final ShareOnlyEditFields f) { + final Set present = new java.util.HashSet<>(); + if (command.hasParameter("totalShares")) { + f.setTotalShares(command.longValueOfParameterNamed("totalShares")); + present.add("totalShares"); + } + if (command.hasParameter("totalSharesIssued") || command.hasParameter("issuedShares")) { + final Long val = command.hasParameter("totalSharesIssued") ? command.longValueOfParameterNamed("totalSharesIssued") : command.longValueOfParameterNamed("issuedShares"); + f.setTotalSharesIssued(val); + present.add("totalSharesIssued"); + } + if (command.hasParameter("unitPrice")) { + f.setUnitPrice(command.bigDecimalValueOfParameterNamed("unitPrice")); + present.add("unitPrice"); + } + if (command.hasParameter("minimumShares") || command.hasParameter("minimumClientShares")) { + final Long val = command.hasParameter("minimumShares") ? command.longValueOfParameterNamed("minimumShares") : command.longValueOfParameterNamed("minimumClientShares"); + f.setMinimumShares(val); + present.add("minimumShares"); + } + if (command.hasParameter("nominalShares") || command.hasParameter("nominalClientShares")) { + final Long val = command.hasParameter("nominalShares") ? command.longValueOfParameterNamed("nominalShares") : command.longValueOfParameterNamed("nominalClientShares"); + f.setNominalShares(val); + present.add("nominalShares"); + } + if (command.hasParameter("maximumShares") || command.hasParameter("maximumClientShares")) { + final Long val = command.hasParameter("maximumShares") ? command.longValueOfParameterNamed("maximumShares") : command.longValueOfParameterNamed("maximumClientShares"); + f.setMaximumShares(val); + present.add("maximumShares"); + } + if (command.hasParameter("allowDividendCalcForInactiveClients") || command.hasParameter("allowDividendsInactiveClients")) { + final Boolean val = command.hasParameter("allowDividendCalcForInactiveClients") ? command.booleanObjectValueOfParameterNamed("allowDividendCalcForInactiveClients") : command.booleanObjectValueOfParameterNamed("allowDividendsInactiveClients"); + f.setAllowDividendCalcForInactiveClients(val); + present.add("allowDividendCalcForInactiveClients"); + } + if (command.hasParameter("minimumActivePeriodForDividends") || command.hasParameter("minimumActivePeriodFrequency")) { + final Integer val = command.hasParameter("minimumActivePeriodForDividends") ? command.integerValueOfParameterNamed("minimumActivePeriodForDividends") : command.integerValueOfParameterNamed("minimumActivePeriodFrequency"); + f.setMinimumActivePeriodForDividends(val); + present.add("minimumActivePeriodForDividends"); + } + if (command.hasParameter("minimumActivePeriodFrequencyType") || command.hasParameter("minimumActivePeriodFrequencyEnum")) { + final Integer val = command.hasParameter("minimumActivePeriodFrequencyType") ? command.integerValueOfParameterNamed("minimumActivePeriodFrequencyType") : command.integerValueOfParameterNamed("minimumActivePeriodFrequencyEnum"); + f.setMinimumActivePeriodFrequencyType(val); + present.add("minimumActivePeriodFrequencyType"); + } + if (command.hasParameter("shareLockinPeriod")) { + f.setShareLockinPeriod(command.integerValueOfParameterNamed("shareLockinPeriod")); + present.add("shareLockinPeriod"); + } + if (command.hasParameter("shareLockinPeriodFrequencyType")) { + f.setShareLockinPeriodFrequencyType(command.integerValueOfParameterNamed("shareLockinPeriodFrequencyType")); + present.add("shareLockinPeriodFrequencyType"); + } + if (command.hasParameter("externalId")) { + f.setExternalId(command.stringValueOfParameterNamed("externalId")); + present.add("externalId"); + } + if (command.hasParameter("startDate")) { + f.setStartDate(command.localDateValueOfParameterNamed("startDate")); + present.add("startDate"); + } + if (command.hasParameter("totalsubscribedShares")) { + f.setTotalsubscribedShares(command.longValueOfParameterNamed("totalsubscribedShares")); + present.add("totalsubscribedShares"); + } + if (command.hasParameter("capitalAmount") || command.hasParameter("shareCapital")) { + final BigDecimal val = command.hasParameter("capitalAmount") ? command.bigDecimalValueOfParameterNamed("capitalAmount") : command.bigDecimalValueOfParameterNamed("shareCapital"); + f.setCapitalAmount(val); + present.add("shareCapital"); + } + return present; + } + + public static void writeShareOnlyFieldsToJson(final JsonObject root, final ShareOnlyEditFields f, final Set submitted) { + if (f == null) { + return; + } + putNumber(root, submitted, "totalShares", f.getTotalShares()); + putNumber(root, submitted, "totalSharesIssued", f.getTotalSharesIssued()); + putNumber(root, submitted, "unitPrice", f.getUnitPrice()); + putNumber(root, submitted, "minimumShares", f.getMinimumShares()); + putNumber(root, submitted, "nominalShares", f.getNominalShares()); + putNumber(root, submitted, "maximumShares", f.getMaximumShares()); + putBoolean(root, submitted, "allowDividendCalcForInactiveClients", f.getAllowDividendCalcForInactiveClients()); + putNumber(root, submitted, "minimumActivePeriodForDividends", f.getMinimumActivePeriodForDividends()); + putNumber(root, submitted, "minimumActivePeriodFrequencyType", f.getMinimumActivePeriodFrequencyType()); + putNumber(root, submitted, "shareLockinPeriod", f.getShareLockinPeriod()); + putNumber(root, submitted, "shareLockinPeriodFrequencyType", f.getShareLockinPeriodFrequencyType()); + putString(root, submitted, "externalId", f.getExternalId()); + putDate(root, submitted, "startDate", f.getStartDate()); + putNumber(root, submitted, "totalsubscribedShares", f.getTotalsubscribedShares()); + if (submitted.contains("capitalAmount") || submitted.contains("shareCapital")) { + if (f.getCapitalAmount() != null) { + root.addProperty("shareCapital", f.getCapitalAmount()); + } + } + } + + public static Set populateFixedDepositFields(final JsonCommand command, final FixedDepositEditFields f) { + final Set present = new java.util.HashSet<>(); + if (command.hasParameter("fdType")) { + f.setFdType(command.stringValueOfParameterNamed("fdType")); + present.add("fdType"); + } + if (command.hasParameter("preClosurePenalApplicable")) { + f.setPreClosurePenalApplicable(command.booleanObjectValueOfParameterNamed("preClosurePenalApplicable")); + present.add("preClosurePenalApplicable"); + } + if (command.hasParameter("preClosurePenalInterest")) { + f.setPreClosurePenalInterest(command.bigDecimalValueOfParameterNamed("preClosurePenalInterest")); + present.add("preClosurePenalInterest"); + } + if (command.hasParameter("preClosurePenalInterestOnTypeId")) { + f.setPreClosurePenalInterestOnTypeId(command.integerValueOfParameterNamed("preClosurePenalInterestOnTypeId")); + present.add("preClosurePenalInterestOnTypeId"); + } + if (command.hasParameter("minDepositTerm")) { + f.setMinDepositTerm(command.integerValueOfParameterNamed("minDepositTerm")); + present.add("minDepositTerm"); + } + if (command.hasParameter("maxDepositTerm")) { + f.setMaxDepositTerm(command.integerValueOfParameterNamed("maxDepositTerm")); + present.add("maxDepositTerm"); + } + if (command.hasParameter("minDepositTermTypeId")) { + f.setMinDepositTermTypeId(command.integerValueOfParameterNamed("minDepositTermTypeId")); + present.add("minDepositTermTypeId"); + } + if (command.hasParameter("maxDepositTermTypeId")) { + f.setMaxDepositTermTypeId(command.integerValueOfParameterNamed("maxDepositTermTypeId")); + present.add("maxDepositTermTypeId"); + } + if (command.hasParameter("inMultiplesOfDepositTerm")) { + f.setInMultiplesOfDepositTerm(command.integerValueOfParameterNamed("inMultiplesOfDepositTerm")); + present.add("inMultiplesOfDepositTerm"); + } + if (command.hasParameter("inMultiplesOfDepositTermTypeId")) { + f.setInMultiplesOfDepositTermTypeId(command.integerValueOfParameterNamed("inMultiplesOfDepositTermTypeId")); + present.add("inMultiplesOfDepositTermTypeId"); + } + if (command.hasParameter("minDepositAmount")) { + f.setMinDepositAmount(command.bigDecimalValueOfParameterNamed("minDepositAmount")); + present.add("minDepositAmount"); + } + if (command.hasParameter("maxDepositAmount")) { + f.setMaxDepositAmount(command.bigDecimalValueOfParameterNamed("maxDepositAmount")); + present.add("maxDepositAmount"); + } + if (command.hasParameter("depositAmount")) { + f.setDepositAmount(command.bigDecimalValueOfParameterNamed("depositAmount")); + present.add("depositAmount"); + } + return present; + } + + public static void writeFixedDepositFieldsToJson(final JsonObject root, final FixedDepositEditFields f, final Set submitted) { + if (f == null) { + return; + } + if (f.getFdType() != null) putString(root, submitted, "fdType", f.getFdType()); + if (f.getPreClosurePenalApplicable() != null) putBoolean(root, submitted, "preClosurePenalApplicable", f.getPreClosurePenalApplicable()); + if (f.getPreClosurePenalInterest() != null) putNumber(root, submitted, "preClosurePenalInterest", f.getPreClosurePenalInterest()); + if (f.getPreClosurePenalInterestOnTypeId() != null) putNumber(root, submitted, "preClosurePenalInterestOnTypeId", f.getPreClosurePenalInterestOnTypeId()); + if (f.getMinDepositTerm() != null) putNumber(root, submitted, "minDepositTerm", f.getMinDepositTerm()); + if (f.getMaxDepositTerm() != null) putNumber(root, submitted, "maxDepositTerm", f.getMaxDepositTerm()); + if (f.getMinDepositTermTypeId() != null) putNumber(root, submitted, "minDepositTermTypeId", f.getMinDepositTermTypeId()); + if (f.getMaxDepositTermTypeId() != null) putNumber(root, submitted, "maxDepositTermTypeId", f.getMaxDepositTermTypeId()); + if (f.getInMultiplesOfDepositTerm() != null) putNumber(root, submitted, "inMultiplesOfDepositTerm", f.getInMultiplesOfDepositTerm()); + if (f.getInMultiplesOfDepositTermTypeId() != null) putNumber(root, submitted, "inMultiplesOfDepositTermTypeId", f.getInMultiplesOfDepositTermTypeId()); + if (f.getMinDepositAmount() != null) putNumber(root, submitted, "minDepositAmount", f.getMinDepositAmount()); + if (f.getMaxDepositAmount() != null) putNumber(root, submitted, "maxDepositAmount", f.getMaxDepositAmount()); + if (f.getDepositAmount() != null) putNumber(root, submitted, "depositAmount", f.getDepositAmount()); + } + + public static Set populateRecurringDepositFields(final JsonCommand command, final RecurringDepositEditFields f) { + final Set present = new java.util.HashSet<>(); + if (command.hasParameter("isMandatoryDeposit")) { + f.setIsMandatoryDeposit(command.booleanObjectValueOfParameterNamed("isMandatoryDeposit")); + present.add("isMandatoryDeposit"); + } + if (command.hasParameter("allowWithdrawal")) { + f.setAllowWithdrawal(command.booleanObjectValueOfParameterNamed("allowWithdrawal")); + present.add("allowWithdrawal"); + } + if (command.hasParameter("adjustAdvanceTowardsFuturePayments")) { + f.setAdjustAdvanceTowardsFuturePayments(command.booleanObjectValueOfParameterNamed("adjustAdvanceTowardsFuturePayments")); + present.add("adjustAdvanceTowardsFuturePayments"); + } + return present; + } + + public static void writeRecurringDepositFieldsToJson(final JsonObject root, final RecurringDepositEditFields f, final Set submitted) { + if (f == null) { + return; + } + if (f.getIsMandatoryDeposit() != null) putBoolean(root, submitted, "isMandatoryDeposit", f.getIsMandatoryDeposit()); + if (f.getAllowWithdrawal() != null) putBoolean(root, submitted, "allowWithdrawal", f.getAllowWithdrawal()); + if (f.getAdjustAdvanceTowardsFuturePayments() != null) putBoolean(root, submitted, "adjustAdvanceTowardsFuturePayments", f.getAdjustAdvanceTowardsFuturePayments()); + } + + public static java.util.Map reconstructProposedChangesMap( + final ProductPendingEdit pendingEdit, + final Set submittedFields, + final ProductPendingEditChargeRepository pendingEditChargeRepository, + final ProductPendingEditGlMappingRepository pendingEditGlMappingRepository, + final ProductPendingEditFacilityLimitRepository pendingEditFacilityLimitRepository, + final com.google.gson.Gson gson) { + final JsonObject root = new JsonObject(); + root.addProperty("locale", "en"); + root.addProperty("dateFormat", "yyyy-MM-dd"); + + final String productType = pendingEdit.getProductType(); + if ((ProductPendingEdit.TYPE_SAVINGS.equals(productType) || ProductPendingEdit.TYPE_CURRENT.equals(productType) || ProductPendingEdit.TYPE_FD.equals(productType) || ProductPendingEdit.TYPE_RD.equals(productType)) && pendingEdit.getCoreFields() != null) { + writeCoreFieldsToJson(root, pendingEdit.getCoreFields(), submittedFields); + } + if (ProductPendingEdit.TYPE_CURRENT.equals(productType) && pendingEdit.getCurrentOnlyFields() != null) { + writeCurrentOnlyFieldsToJson(root, pendingEdit.getCurrentOnlyFields(), submittedFields); + } + if (ProductPendingEdit.TYPE_SAVINGS.equals(productType) || ProductPendingEdit.TYPE_CURRENT.equals(productType)) { + if (submittedFields.contains("accountFacilityConfig") && pendingEdit.getFacilityConfigFields() != null) { + writeFacilityConfigToJson(root, pendingEdit.getFacilityConfigFields(), true); + } + if (submittedFields.contains("depositLimitKyc") && pendingEdit.getDepositLimitKycFields() != null) { + writeDepositLimitKycToJson(root, pendingEdit.getDepositLimitKycFields(), true); + } + if (submittedFields.contains("accountFacilityLimits") && pendingEditFacilityLimitRepository != null) { + writeFacilityLimitsToJson(root, pendingEditFacilityLimitRepository.findByPendingEditId(pendingEdit.getId())); + } + } + if ((ProductPendingEdit.TYPE_FD.equals(productType) || ProductPendingEdit.TYPE_RD.equals(productType)) && pendingEdit.getFixedDepositFields() != null) { + writeFixedDepositFieldsToJson(root, pendingEdit.getFixedDepositFields(), submittedFields); + } + if (ProductPendingEdit.TYPE_RD.equals(productType) && pendingEdit.getRecurringDepositFields() != null) { + writeRecurringDepositFieldsToJson(root, pendingEdit.getRecurringDepositFields(), submittedFields); + } + if (ProductPendingEdit.TYPE_SHARE.equals(productType) && pendingEdit.getShareOnlyFields() != null) { + writeShareOnlyFieldsToJson(root, pendingEdit.getShareOnlyFields(), submittedFields); + } + if (submittedFields.contains("glAccountMapping") && pendingEdit.getGlAccountMappingFields() != null) { + writeGlAccountMappingToJson(root, pendingEdit.getGlAccountMappingFields()); + } + if (submittedFields.contains("charges") && pendingEditChargeRepository != null) { + writeChargesToJson(root, pendingEditChargeRepository.findByPendingEditId(pendingEdit.getId())); + } + if (submittedFields.contains("glArrayMappings") && pendingEditGlMappingRepository != null) { + writeGlArrayMappingsToJson(root, pendingEditGlMappingRepository.findByPendingEditId(pendingEdit.getId())); + } + + final String json = gson.toJson(root); + return gson.fromJson(json, new com.google.gson.reflect.TypeToken>() {}.getType()); + } +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditGlMapping.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditGlMapping.java new file mode 100644 index 000000000..0943f008d --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditGlMapping.java @@ -0,0 +1,70 @@ +/** + * 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.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; + +/** + * One row per entry of the 3 array-shaped GL mappings (paymentChannelToFundSourceMappings, + * feeToIncomeAccountMappings, penaltyToIncomeAccountMappings), distinguished by mappingType. + */ +@Entity +@Table(name = "m_product_temp_gl_mapping") +@Getter +@Setter +@NoArgsConstructor +public class ProductPendingEditGlMapping extends AbstractPersistableCustom { + + public static final String TYPE_PAYMENT_CHANNEL = "PAYMENT_CHANNEL"; + public static final String TYPE_FEE = "FEE"; + public static final String TYPE_PENALTY = "PENALTY"; + + @ManyToOne + @JoinColumn(name = "pending_edit_id", nullable = false) + private ProductPendingEdit pendingEdit; + + @Column(name = "mapping_type", nullable = false) + private String mappingType; + + @Column(name = "payment_type_id") + private Long paymentTypeId; + + @Column(name = "charge_id") + private Long chargeId; + + @Column(name = "gl_account_id", nullable = false) + private Long glAccountId; + + public ProductPendingEditGlMapping(final ProductPendingEdit pendingEdit, final String mappingType, final Long paymentTypeId, + final Long chargeId, final Long glAccountId) { + this.pendingEdit = pendingEdit; + this.mappingType = mappingType; + this.paymentTypeId = paymentTypeId; + this.chargeId = chargeId; + this.glAccountId = glAccountId; + } +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditGlMappingRepository.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditGlMappingRepository.java new file mode 100644 index 000000000..12c2c0f9c --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditGlMappingRepository.java @@ -0,0 +1,29 @@ +/** + * 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.domain; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ProductPendingEditGlMappingRepository extends JpaRepository { + + List findByPendingEditId(Long pendingEditId); +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditMarketPrice.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditMarketPrice.java new file mode 100644 index 000000000..6dc2955be --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditMarketPrice.java @@ -0,0 +1,66 @@ +/** + * 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.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import java.time.LocalDate; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; + +/** One row per entry of Share Product's "marketPrice" array (create/update/delete-by-id semantics). */ +@Entity +@Table(name = "m_product_temp_share_market_price") +@Getter +@Setter +@NoArgsConstructor +public class ProductPendingEditMarketPrice extends AbstractPersistableCustom { + + @ManyToOne + @JoinColumn(name = "pending_edit_id", nullable = false) + private ProductPendingEdit pendingEdit; + + /** id of the existing m_share_product_market_price row this entry updates/deletes, null if new. */ + @Column(name = "source_id") + private Long sourceId; + + @Column(name = "delete_flag", nullable = false) + private boolean deleteFlag; + + @Column(name = "from_date") + private LocalDate fromDate; + + @Column(name = "share_value") + private BigDecimal shareValue; + + public ProductPendingEditMarketPrice(final ProductPendingEdit pendingEdit, final Long sourceId, final boolean deleteFlag, + final LocalDate fromDate, final BigDecimal shareValue) { + this.pendingEdit = pendingEdit; + this.sourceId = sourceId; + this.deleteFlag = deleteFlag; + this.fromDate = fromDate; + this.shareValue = shareValue; + } +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditMarketPriceRepository.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditMarketPriceRepository.java new file mode 100644 index 000000000..3bb8982c3 --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditMarketPriceRepository.java @@ -0,0 +1,29 @@ +/** + * 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.domain; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ProductPendingEditMarketPriceRepository extends JpaRepository { + + List findByPendingEditId(Long pendingEditId); +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditRepository.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditRepository.java new file mode 100644 index 000000000..4c4646bc7 --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditRepository.java @@ -0,0 +1,32 @@ +/** + * 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.domain; + +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ProductPendingEditRepository extends JpaRepository { + + Optional findByProductTypeAndProductIdAndStatus(String productType, Long productId, String status); + + List findByProductTypeAndStatus(String productType, String status); +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditSubmittedField.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditSubmittedField.java new file mode 100644 index 000000000..24a0f1469 --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditSubmittedField.java @@ -0,0 +1,56 @@ +/** + * 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.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; + +/** + * Records which top-level JSON parameter names were actually present in the maker's original + * request. Typed nullable columns can't by themselves distinguish "field omitted" from "field + * explicitly sent as null" from "field has a value" - and every existing command.hasParameter(...) + * branch in the write-services and assemblers depends on that distinction - so this table is the + * relational replacement for the information a raw JSON blob used to carry implicitly. + */ +@Entity +@Table(name = "m_product_temp_submitted_field") +@Getter +@Setter +@NoArgsConstructor +public class ProductPendingEditSubmittedField extends AbstractPersistableCustom { + + @ManyToOne + @JoinColumn(name = "pending_edit_id", nullable = false) + private ProductPendingEdit pendingEdit; + + @Column(name = "field_name", nullable = false) + private String fieldName; + + public ProductPendingEditSubmittedField(final ProductPendingEdit pendingEdit, final String fieldName) { + this.pendingEdit = pendingEdit; + this.fieldName = fieldName; + } +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditSubmittedFieldRepository.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditSubmittedFieldRepository.java new file mode 100644 index 000000000..a2cf887bd --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ProductPendingEditSubmittedFieldRepository.java @@ -0,0 +1,34 @@ +/** + * 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.domain; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface ProductPendingEditSubmittedFieldRepository extends JpaRepository { + + List findByPendingEditId(Long pendingEditId); + + @Query("SELECT f.fieldName FROM ProductPendingEditSubmittedField f WHERE f.pendingEdit.id = :pendingEditId") + List findFieldNamesByPendingEditId(@Param("pendingEditId") Long pendingEditId); +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/RecurringDepositEditFields.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/RecurringDepositEditFields.java new file mode 100644 index 000000000..489cc3a3e --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/RecurringDepositEditFields.java @@ -0,0 +1,39 @@ +/** + * 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.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.Getter; +import lombok.Setter; + +@Embeddable +@Getter +@Setter +public class RecurringDepositEditFields { + + @Column(name = "is_mandatory_deposit") + private Boolean isMandatoryDeposit; + + @Column(name = "allow_withdrawal") + private Boolean allowWithdrawal; + + @Column(name = "adjust_advance_towards_future_payments") + private Boolean adjustAdvanceTowardsFuturePayments; +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java index aa728ff7f..a3d54240d 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java @@ -34,6 +34,8 @@ import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nominalA import static org.apache.fineract.portfolio.savings.SavingsApiConstants.overdraftLimitParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.withHoldTaxParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.withdrawalFeeForTransfersParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.introducerRequiredParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.shareLinkingEnabledParamName; import com.google.gson.JsonArray; import jakarta.persistence.CascadeType; @@ -1852,6 +1854,230 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom baseDataValidator.reset().parameter(withHoldTaxParamName).failWithCode("not.supported.for.this.account"); } } + + if (command.isChangeInBooleanParameterNamed(introducerRequiredParamName, this.introducerRequired)) { + final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(introducerRequiredParamName); + actualChanges.put(introducerRequiredParamName, newValue); + this.introducerRequired = newValue; + } + + if (command.isChangeInBooleanParameterNamed(shareLinkingEnabledParamName, this.shareLinkingEnabled)) { + final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(shareLinkingEnabledParamName); + actualChanges.put(shareLinkingEnabledParamName, newValue); + this.shareLinkingEnabled = newValue; + } + + // CBS extended fields mapping during modify application + final String accountTypeParam = "accountType"; + if (command.isChangeInStringParameterNamed(accountTypeParam, this.cbsAccountType)) { + final String newValue = command.stringValueOfParameterNamed(accountTypeParam); + actualChanges.put(accountTypeParam, newValue); + this.cbsAccountType = newValue; + } + + final String customerSegmentParam = "customerSegment"; + if (command.isChangeInStringParameterNamed(customerSegmentParam, this.customerSegment)) { + final String newValue = command.stringValueOfParameterNamed(customerSegmentParam); + actualChanges.put(customerSegmentParam, newValue); + this.customerSegment = newValue; + } + + final String accountStatusParam = "accountStatus"; + if (command.isChangeInStringParameterNamed(accountStatusParam, this.accountStatus)) { + final String newValue = command.stringValueOfParameterNamed(accountStatusParam); + actualChanges.put(accountStatusParam, newValue); + this.accountStatus = newValue; + } + + final String modeOfOperationParam = "modeOfOperation"; + if (command.isChangeInStringParameterNamed(modeOfOperationParam, this.modeOfOperation)) { + final String newValue = command.stringValueOfParameterNamed(modeOfOperationParam); + actualChanges.put(modeOfOperationParam, newValue); + this.modeOfOperation = newValue; + } + + final String specialCategoryParam = "specialCategory"; + if (command.isChangeInStringParameterNamed(specialCategoryParam, this.specialCategory)) { + final String newValue = command.stringValueOfParameterNamed(specialCategoryParam); + actualChanges.put(specialCategoryParam, newValue); + this.specialCategory = newValue; + } + + final String weakerSectionParam = "weakerSection"; + if (command.isChangeInStringParameterNamed(weakerSectionParam, this.weakerSection)) { + final String newValue = command.stringValueOfParameterNamed(weakerSectionParam); + actualChanges.put(weakerSectionParam, newValue); + this.weakerSection = newValue; + } + + final String nominationFacilityParam = "nominationFacility"; + if (command.isChangeInBooleanParameterNamed(nominationFacilityParam, this.nominationFacility)) { + final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(nominationFacilityParam); + actualChanges.put(nominationFacilityParam, newValue); + this.nominationFacility = newValue; + } + + final String autoDebitFacilityParam = "autoDebitFacility"; + if (command.isChangeInBooleanParameterNamed(autoDebitFacilityParam, this.autoDebitFacility)) { + final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(autoDebitFacilityParam); + actualChanges.put(autoDebitFacilityParam, newValue); + this.autoDebitFacility = newValue; + } + + final String isSeniorCitizenParam = "isSeniorCitizen"; + if (command.isChangeInBooleanParameterNamed(isSeniorCitizenParam, this.isSeniorCitizen)) { + final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(isSeniorCitizenParam); + actualChanges.put(isSeniorCitizenParam, newValue); + this.isSeniorCitizen = newValue; + } + + final String seniorCitizenInterestRateParam = "seniorCitizenInterestRate"; + if (command.isChangeInBigDecimalParameterNamed(seniorCitizenInterestRateParam, this.seniorCitizenInterestRate)) { + final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(seniorCitizenInterestRateParam); + actualChanges.put(seniorCitizenInterestRateParam, newValue); + this.seniorCitizenInterestRate = newValue; + if (newValue != null && newValue.compareTo(BigDecimal.ZERO) > 0) { + updateNominalAnnualInterestRate(newValue); + } + } + + final String guardianNameParam = "guardianName"; + if (command.isChangeInStringParameterNamed(guardianNameParam, this.guardianName)) { + final String newValue = command.stringValueOfParameterNamed(guardianNameParam); + actualChanges.put(guardianNameParam, newValue); + this.guardianName = newValue; + } + + final String guardianCifNumberParam = "guardianCifNumber"; + if (command.isChangeInStringParameterNamed(guardianCifNumberParam, this.guardianCifNumber)) { + final String newValue = command.stringValueOfParameterNamed(guardianCifNumberParam); + actualChanges.put(guardianCifNumberParam, newValue); + this.guardianCifNumber = newValue; + } + + final String guardianRelationshipParam = "guardianRelationship"; + if (command.isChangeInStringParameterNamed(guardianRelationshipParam, this.guardianRelationship)) { + final String newValue = command.stringValueOfParameterNamed(guardianRelationshipParam); + actualChanges.put(guardianRelationshipParam, newValue); + this.guardianRelationship = newValue; + } + + final String minorDateOfBirthParam = "minorDateOfBirth"; + if (command.isChangeInLocalDateParameterNamed(minorDateOfBirthParam, this.minorDateOfBirth)) { + final LocalDate newValue = command.localDateValueOfParameterNamed(minorDateOfBirthParam); + actualChanges.put(minorDateOfBirthParam, newValue); + this.minorDateOfBirth = newValue; + } + + final String guardianClientIdParam = "guardianClientId"; + if (command.isChangeInLongParameterNamed(guardianClientIdParam, this.guardianClientId)) { + final Long newValue = command.longValueOfParameterNamed(guardianClientIdParam); + actualChanges.put(guardianClientIdParam, newValue); + this.guardianClientId = newValue; + } + + final String introducerIdParam = "introducerId"; + if (command.isChangeInLongParameterNamed(introducerIdParam, this.introducerClientId)) { + final Long newValue = command.longValueOfParameterNamed(introducerIdParam); + actualChanges.put(introducerIdParam, newValue); + this.introducerClientId = newValue; + } + + final String introducerNameParam = "introducerName"; + if (command.isChangeInStringParameterNamed(introducerNameParam, this.introducerName)) { + final String newValue = command.stringValueOfParameterNamed(introducerNameParam); + actualChanges.put(introducerNameParam, newValue); + this.introducerName = newValue; + } + + final String introducerCifNumberParam = "introducerCifNumber"; + if (command.isChangeInStringParameterNamed(introducerCifNumberParam, this.introducerCifNumber)) { + final String newValue = command.stringValueOfParameterNamed(introducerCifNumberParam); + actualChanges.put(introducerCifNumberParam, newValue); + this.introducerCifNumber = newValue; + } + + final String linkedAccountIdParam = "linkedAccountId"; + if (command.isChangeInLongParameterNamed(linkedAccountIdParam, this.linkedAccountId)) { + final Long newValue = command.longValueOfParameterNamed(linkedAccountIdParam); + actualChanges.put(linkedAccountIdParam, newValue); + this.linkedAccountId = newValue; + } + + final String linkedAccountTypeParam = "linkedAccountType"; + if (command.isChangeInStringParameterNamed(linkedAccountTypeParam, this.linkedAccountType)) { + final String newValue = command.stringValueOfParameterNamed(linkedAccountTypeParam); + actualChanges.put(linkedAccountTypeParam, newValue); + this.linkedAccountType = newValue; + } + + final String linkedAccountNoParam = "linkedAccountNo"; + if (command.isChangeInStringParameterNamed(linkedAccountNoParam, this.linkedAccountNo)) { + final String newValue = command.stringValueOfParameterNamed(linkedAccountNoParam); + actualChanges.put(linkedAccountNoParam, newValue); + this.linkedAccountNo = newValue; + } + + final String accountFacilityConfigParam = "accountFacilityConfig"; + if (command.hasParameter(accountFacilityConfigParam)) { + final com.google.gson.JsonElement parsedJson = command.parsedJson(); + if (parsedJson != null && parsedJson.isJsonObject()) { + final com.google.gson.JsonObject jsonObj = parsedJson.getAsJsonObject(); + if (jsonObj.has(accountFacilityConfigParam) && !jsonObj.get(accountFacilityConfigParam).isJsonNull() + && jsonObj.get(accountFacilityConfigParam).isJsonObject()) { + final com.google.gson.JsonObject configObj = jsonObj.get(accountFacilityConfigParam).getAsJsonObject(); + if (this.facilityConfig == null) { + this.facilityConfig = new SavingsAccountFacilityConfig(this); + } + this.facilityConfig.updateFrom(configObj); + actualChanges.put(accountFacilityConfigParam, configObj.toString()); + } + } + } + + final String jointHoldersParam = "jointHolders"; + if (command.hasParameter(jointHoldersParam)) { + final com.google.gson.JsonElement parsedJson = command.parsedJson(); + if (parsedJson != null && parsedJson.isJsonObject()) { + final com.google.gson.JsonObject jsonObj = parsedJson.getAsJsonObject(); + if (jsonObj.has(jointHoldersParam) && !jsonObj.get(jointHoldersParam).isJsonNull() + && jsonObj.get(jointHoldersParam).isJsonArray()) { + final com.google.gson.JsonArray holdersArray = jsonObj.get(jointHoldersParam).getAsJsonArray(); + final List holders = new ArrayList<>(); + for (int i = 0; i < holdersArray.size(); i++) { + final com.google.gson.JsonObject h = holdersArray.get(i).getAsJsonObject(); + final Long clientId = h.has("clientId") && !h.get("clientId").isJsonNull() + ? h.get("clientId").getAsLong() : null; + final String cifNumber = h.has("cifNumber") && !h.get("cifNumber").isJsonNull() + ? h.get("cifNumber").getAsString() : null; + final String displayName = h.has("displayName") && !h.get("displayName").isJsonNull() + ? h.get("displayName").getAsString() : null; + final BigDecimal sharePercentage = h.has("sharePercentage") && !h.get("sharePercentage").isJsonNull() + ? h.get("sharePercentage").getAsBigDecimal() : null; + final boolean isPrimary = h.has("isPrimary") && h.get("isPrimary").getAsBoolean(); + String signingAuthority = h.has("signingAuthority") && !h.get("signingAuthority").isJsonNull() + ? h.get("signingAuthority").getAsString() : null; + String role = h.has("role") && !h.get("role").isJsonNull() + ? h.get("role").getAsString() : null; + + if (signingAuthority == null || signingAuthority.trim().isEmpty()) { + signingAuthority = isPrimary ? "Mandatory" : "Optional"; + } + if (role == null || role.trim().isEmpty()) { + role = isPrimary ? "Primary Account Holder" : "Co-Holder"; + } + + final boolean isMandatory = h.has("isMandatory") && h.get("isMandatory").getAsBoolean(); + holders.add(new SavingsAccountJointHolder(this, clientId, cifNumber, displayName, + sharePercentage, signingAuthority, role, isPrimary, isMandatory)); + } + this.jointHolders.clear(); + this.jointHolders.addAll(holders); + actualChanges.put(jointHoldersParam, holdersArray.toString()); + } + } + } + validateLockinDetails(baseDataValidator); esnureOverdraftLimitsSetForOverdraftAccounts(); } @@ -3972,6 +4198,12 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom @Column(name = "linked_account_no", length = 50) private String linkedAccountNo; + @Column(name = "introducer_required", nullable = false, columnDefinition = "boolean default false") + private Boolean introducerRequired = false; + + @Column(name = "share_linking_enabled", nullable = false, columnDefinition = "boolean default false") + private Boolean shareLinkingEnabled = false; + @Column(name = "nomination_facility", nullable = false, columnDefinition = "boolean default false") private Boolean nominationFacility = false; @@ -3984,6 +4216,21 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom @Column(name = "senior_citizen_interest_rate", scale = 6, precision = 19, nullable = true) private BigDecimal seniorCitizenInterestRate; + @Column(name = "terminate_true", nullable = false, columnDefinition = "boolean default false") + private boolean terminateTrue = false; + + public boolean isTerminateTrue() { return terminateTrue; } + public void setTerminateTrue(boolean terminateTrue) { this.terminateTrue = terminateTrue; } + + public Map terminate() { + final Map actualChanges = new LinkedHashMap<>(); + this.terminateTrue = true; + this.status = SavingsAccountStatusType.TERMINATED.getValue(); + actualChanges.put("terminateTrue", true); + actualChanges.put("status", SavingsAccountStatusType.TERMINATED.getValue()); + return actualChanges; + } + @jakarta.persistence.OneToOne(cascade = jakarta.persistence.CascadeType.ALL, mappedBy = "savingsAccount", orphanRemoval = true, fetch = FetchType.LAZY) private SavingsAccountFacilityConfig facilityConfig; @@ -4057,6 +4304,12 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom public Boolean getAutoDebitFacility() { return autoDebitFacility; } public void setAutoDebitFacility(Boolean v) { this.autoDebitFacility = v; } + public Boolean getIntroducerRequired() { return introducerRequired; } + public void setIntroducerRequired(Boolean v) { this.introducerRequired = v; } + + public Boolean getShareLinkingEnabled() { return shareLinkingEnabled; } + public void setShareLinkingEnabled(Boolean v) { this.shareLinkingEnabled = v; } + public boolean isSeniorCitizen() { return isSeniorCitizen; } public void setSeniorCitizen(boolean v) { this.isSeniorCitizen = v; } diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountCharge.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountCharge.java index 93127dfed..3e5b80748 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountCharge.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountCharge.java @@ -815,16 +815,20 @@ public class SavingsAccountCharge extends AbstractAuditableWithUTCDateTimeCustom return nextDueLocalDate; } + public Integer getFeeInterval() { + return this.feeInterval == null ? 1 : this.feeInterval; + } + private LocalDate calculateNextDueDate(final LocalDate date) { LocalDate nextDueLocalDate = null; if (isAnnualFee()) { nextDueLocalDate = date.withMonth(this.feeOnMonth).plusYears(1); nextDueLocalDate = setDayOfMonth(nextDueLocalDate); } else if (isMonthlyFee()) { - nextDueLocalDate = date.plusMonths(this.feeInterval); + nextDueLocalDate = date.plusMonths(getFeeInterval()); nextDueLocalDate = setDayOfMonth(nextDueLocalDate); } else if (isWeeklyFee()) { - nextDueLocalDate = date.plusWeeks(this.feeInterval); + nextDueLocalDate = date.plusWeeks(getFeeInterval()); nextDueLocalDate = setDayOfWeek(nextDueLocalDate); } return nextDueLocalDate; @@ -857,10 +861,10 @@ public class SavingsAccountCharge extends AbstractAuditableWithUTCDateTimeCustom nextDueLocalDate = nextDueLocalDate.withMonth(this.feeOnMonth).minusYears(1); nextDueLocalDate = setDayOfMonth(nextDueLocalDate); } else if (isMonthlyFee()) { - nextDueLocalDate = nextDueLocalDate.minusMonths(this.feeInterval); + nextDueLocalDate = nextDueLocalDate.minusMonths(getFeeInterval()); nextDueLocalDate = setDayOfMonth(nextDueLocalDate); } else if (isWeeklyFee()) { - nextDueLocalDate = nextDueLocalDate.minusDays(7 * this.feeInterval); + nextDueLocalDate = nextDueLocalDate.minusDays(7 * getFeeInterval()); nextDueLocalDate = setDayOfWeek(nextDueLocalDate); } diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsCoreEditFields.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsCoreEditFields.java new file mode 100644 index 000000000..b531290e5 --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsCoreEditFields.java @@ -0,0 +1,243 @@ +/** + * 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.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.Lob; +import java.math.BigDecimal; +import java.time.LocalDate; +import lombok.Getter; +import lombok.Setter; + +/** + * Scalar edit fields shared by Savings, Current, Fixed Deposit and Recurring Deposit products + * (FD/RD extend the Savings field set). Column names are the snake_case of the JSON API parameter + * name each field stores, not the underlying product table's own column name. + */ +@Embeddable +@Getter +@Setter +public class SavingsCoreEditFields { + + @Column(name = "name") + private String name; + + @Column(name = "short_name") + private String shortName; + + @Column(name = "description") + private String description; + + @Column(name = "currency_code") + private String currencyCode; + + @Column(name = "currency_digits") + private Integer digitsAfterDecimal; + + @Column(name = "currency_multiplesof") + private Integer inMultiplesOf; + + @Column(name = "accounting_type") + private Integer accountingRule; + + @Column(name = "nominal_annual_interest_rate") + private BigDecimal nominalAnnualInterestRate; + + @Column(name = "interest_compounding_period_enum") + private Integer interestCompoundingPeriodType; + + @Column(name = "interest_posting_period_enum") + private Integer interestPostingPeriodType; + + @Column(name = "interest_calculation_type_enum") + private Integer interestCalculationType; + + @Column(name = "interest_calculation_days_in_year_type_enum") + private Integer interestCalculationDaysInYearType; + + @Column(name = "min_required_opening_balance") + private BigDecimal minRequiredOpeningBalance; + + @Column(name = "lockin_period_frequency") + private Integer lockinPeriodFrequency; + + @Column(name = "lockin_period_frequency_enum") + private Integer lockinPeriodFrequencyType; + + @Column(name = "withdrawal_fee_for_transfer") + private Boolean withdrawalFeeForTransfers; + + @Column(name = "allow_overdraft") + private Boolean allowOverdraft; + + @Column(name = "overdraft_limit") + private BigDecimal overdraftLimit; + + @Column(name = "nominal_annual_interest_rate_overdraft") + private BigDecimal nominalAnnualInterestRateOverdraft; + + @Column(name = "min_overdraft_for_interest_calculation") + private BigDecimal minOverdraftForInterestCalculation; + + @Column(name = "enforce_min_required_balance") + private Boolean enforceMinRequiredBalance; + + @Column(name = "min_required_balance") + private BigDecimal minRequiredBalance; + + @Column(name = "is_lien_allowed") + private Boolean lienAllowed; + + @Column(name = "max_allowed_lien_limit") + private BigDecimal maxAllowedLienLimit; + + @Column(name = "min_balance_for_interest_calculation") + private BigDecimal minBalanceForInterestCalculation; + + @Column(name = "withhold_tax") + private Boolean withHoldTax; + + @Column(name = "tax_group_id") + private Long taxGroupId; + + @Lob + @Column(name = "other_gl_mappings") + private String otherGlMappings; + + @Column(name = "is_dormancy_tracking_active") + private Boolean isDormancyTrackingActive; + + @Column(name = "days_to_inactive") + private Integer daysToInactive; + + @Column(name = "days_to_dormancy") + private Integer daysToDormancy; + + @Column(name = "days_to_escheat") + private Integer daysToEscheat; + + @Column(name = "account_type") + private String accountType; + + @Column(name = "customer_segment") + private String customerSegment; + + @Column(name = "product_code") + private String productCode; + + @Column(name = "gl_account_id") + private Long glAccountId; + + @Column(name = "effective_date") + private LocalDate effectiveDate; + + @Column(name = "effective_to") + private LocalDate effectiveTo; + + @Column(name = "end_date") + private LocalDate endDate; + + @Column(name = "introducer_required") + private Boolean introducerRequired; + + @Column(name = "share_linking_enabled") + private Boolean shareLinkingEnabled; + + @Column(name = "nomination_facility") + private Boolean nominationFacility; + + @Column(name = "dormancy_alert") + private Boolean dormancyAlert; + + @Column(name = "interest_calculate_selector") + private Integer interestCalculateSelector; + + @Column(name = "average_period") + private String averagePeriod; + + @Column(name = "auto_post") + private Boolean autoPost; + + @Column(name = "frequency_fix_day") + private Integer frequencyFixDay; + + @Column(name = "last_interest_date") + private LocalDate lastInterestDate; + + @Column(name = "enable_lockin_period") + private Boolean enableLockinPeriod; + + @Column(name = "balance_frequency") + private String balanceFrequency; + + @Column(name = "account_closed_zero_balance_in_day_closing") + private Boolean accountClosedZeroBalanceInDayClosing; + + @Column(name = "balance_swap_option") + private Boolean balanceSwapOption; + + @Column(name = "msme") + private Boolean msme; + + @Column(name = "auto_debit_facility") + private Boolean autoDebitFacility; + + @Column(name = "loan_facility") + private Boolean loanFacility; + + @Column(name = "premature_withdrawal_option") + private Boolean prematureWithdrawalOption; + + @Column(name = "tenure_flexibility") + private Boolean tenureFlexibility; + + @Column(name = "lien_marked") + private Boolean lienMarked; + + @Column(name = "maturity_instructions") + private Boolean maturityInstructions; + + @Column(name = "credit_maturity_proceeds") + private Boolean creditMaturityProceeds; + + @Column(name = "penalty_grace_period_missed_installments") + private Boolean penaltyGracePeriodMissedInstallments; + + @Column(name = "panel_request_applicable") + private Boolean panelRequestApplicable; + + @Column(name = "no_penalty_nominee_heir_closure") + private Boolean noPenaltyNomineeHeirClosure; + + @Column(name = "deceased_settlement") + private Boolean deceasedSettlement; + + @Column(name = "allow_debit_txn_after_due_date") + private Boolean allowDebitTxnAfterDueDate; + + @Column(name = "deposit_type_enum") + private Integer depositTypeEnum; + + @Column(name = "withdrawal_fee_amount") + private BigDecimal withdrawalFeeAmount; + + @Column(name = "withdrawal_fee_type_enum") + private Integer withdrawalFeeTypeEnum; +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ShareOnlyEditFields.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ShareOnlyEditFields.java new file mode 100644 index 000000000..2a614fe95 --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/ShareOnlyEditFields.java @@ -0,0 +1,76 @@ +/** + * 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.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import java.math.BigDecimal; +import lombok.Getter; +import lombok.Setter; + +@Embeddable +@Getter +@Setter +public class ShareOnlyEditFields { + + @Column(name = "total_shares") + private Long totalShares; + + @Column(name = "issued_shares") + private Long totalSharesIssued; + + @Column(name = "unit_price") + private BigDecimal unitPrice; + + @Column(name = "minimum_client_shares") + private Long minimumShares; + + @Column(name = "nominal_client_shares") + private Long nominalShares; + + @Column(name = "maximum_client_shares") + private Long maximumShares; + + @Column(name = "allow_dividends_inactive_clients") + private Boolean allowDividendCalcForInactiveClients; + + @Column(name = "minimum_active_period_frequency") + private Integer minimumActivePeriodForDividends; + + @Column(name = "minimum_active_period_frequency_enum") + private Integer minimumActivePeriodFrequencyType; + + @Column(name = "share_lockin_period") + private Integer shareLockinPeriod; + + @Column(name = "share_lockin_period_frequency_type") + private Integer shareLockinPeriodFrequencyType; + + @Column(name = "external_id") + private String externalId; + + @Column(name = "start_date") + private java.time.LocalDate startDate; + + @Column(name = "totalsubscribed_shares") + private Long totalsubscribedShares; + + @Column(name = "capital_amount") + private BigDecimal capitalAmount; +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsProductEditPendingApprovalException.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsProductEditPendingApprovalException.java new file mode 100644 index 000000000..ccb526231 --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsProductEditPendingApprovalException.java @@ -0,0 +1,29 @@ +/** + * 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.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class SavingsProductEditPendingApprovalException extends AbstractPlatformDomainRuleException { + + public SavingsProductEditPendingApprovalException(final Long productId) { + super("error.msg.savingsproduct.edit.pending.approval", + "Savings product with identifier " + productId + " already has an edit pending approval", productId); + } +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsProductNoPendingEditException.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsProductNoPendingEditException.java new file mode 100644 index 000000000..11c5bb2e0 --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsProductNoPendingEditException.java @@ -0,0 +1,29 @@ +/** + * 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.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class SavingsProductNoPendingEditException extends AbstractPlatformDomainRuleException { + + public SavingsProductNoPendingEditException(final Long productId) { + super("error.msg.savingsproduct.no.pending.edit", + "Savings product with identifier " + productId + " has no edit pending approval", productId); + } +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsProductSelfApprovalNotAllowedException.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsProductSelfApprovalNotAllowedException.java new file mode 100644 index 000000000..47dcdf844 --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/SavingsProductSelfApprovalNotAllowedException.java @@ -0,0 +1,29 @@ +/** + * 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.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class SavingsProductSelfApprovalNotAllowedException extends AbstractPlatformDomainRuleException { + + public SavingsProductSelfApprovalNotAllowedException(final Long productId) { + super("error.msg.savingsproduct.self.approval.not.allowed", + "The maker of the pending edit for savings product " + productId + " cannot also approve or reject it", productId); + } +} diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/DepositProductReadPlatformService.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/DepositProductReadPlatformService.java index 76a8c5937..ba906bc70 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/DepositProductReadPlatformService.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/DepositProductReadPlatformService.java @@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.savings.service; import java.util.Collection; import org.apache.fineract.portfolio.savings.DepositAccountType; import org.apache.fineract.portfolio.savings.data.DepositProductData; +import org.apache.fineract.portfolio.savings.data.SavingsProductTempData; public interface DepositProductReadPlatformService { @@ -32,4 +33,8 @@ public interface DepositProductReadPlatformService { DepositProductData retrieveOneWithChartSlabs(DepositAccountType depositAccountType, Long productId); + Collection retrieveAllPendingEdits(DepositAccountType depositAccountType); + + SavingsProductTempData retrievePendingEdit(DepositAccountType depositAccountType, Long productId); + } diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/FixedDepositProductWritePlatformService.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/FixedDepositProductWritePlatformService.java index 92ca7cd51..49a54be66 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/FixedDepositProductWritePlatformService.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/FixedDepositProductWritePlatformService.java @@ -28,4 +28,10 @@ public interface FixedDepositProductWritePlatformService { CommandProcessingResult update(Long productId, JsonCommand command); CommandProcessingResult delete(Long productId); + + CommandProcessingResult submitEditForApproval(Long productId, JsonCommand command); + + CommandProcessingResult approveEdit(Long productId); + + CommandProcessingResult rejectEdit(Long productId, String rejectReason); } diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/RecurringDepositProductWritePlatformService.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/RecurringDepositProductWritePlatformService.java index 81a5a2914..09e3d3375 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/RecurringDepositProductWritePlatformService.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/RecurringDepositProductWritePlatformService.java @@ -28,4 +28,10 @@ public interface RecurringDepositProductWritePlatformService { CommandProcessingResult update(Long productId, JsonCommand command); CommandProcessingResult delete(Long productId); + + CommandProcessingResult submitEditForApproval(Long productId, JsonCommand command); + + CommandProcessingResult approveEdit(Long productId); + + CommandProcessingResult rejectEdit(Long productId, String rejectReason); } diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformService.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformService.java index 8b2979591..e6daf5577 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformService.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformService.java @@ -118,4 +118,6 @@ public interface SavingsAccountWritePlatformService { CommandProcessingResult gsimDeposit(Long gsimId, JsonCommand command); CommandProcessingResult bulkGSIMClose(Long gsimId, JsonCommand command); + + CommandProcessingResult terminate(Long savingsId, JsonCommand command); } diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsProductReadPlatformService.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsProductReadPlatformService.java index 3f5e2e402..e5ca40ba2 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsProductReadPlatformService.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsProductReadPlatformService.java @@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.savings.service; import java.util.Collection; import org.apache.fineract.portfolio.savings.data.SavingsProductData; +import org.apache.fineract.portfolio.savings.data.SavingsProductTempData; public interface SavingsProductReadPlatformService { @@ -33,4 +34,8 @@ public interface SavingsProductReadPlatformService { SavingsProductData retrieveOne(Long productId); + Collection retrieveAllPendingEdits(); + + SavingsProductTempData retrievePendingEdit(Long productId); + } diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsProductWritePlatformService.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsProductWritePlatformService.java index 0617cc3be..37c640fde 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsProductWritePlatformService.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsProductWritePlatformService.java @@ -28,4 +28,21 @@ public interface SavingsProductWritePlatformService { CommandProcessingResult update(Long productId, JsonCommand command); CommandProcessingResult delete(Long productId); + + /** + * Submits an edit for approval instead of applying it directly. Fails if a pending edit already exists for this + * product (that pending row is itself the lock). + */ + CommandProcessingResult submitEditForApproval(Long productId, JsonCommand command); + + /** + * Replays the stored pending edit through the normal update path and marks it approved. Fails if the approving + * user is the same user who submitted the edit. + */ + CommandProcessingResult approveEdit(Long productId); + + /** + * Discards the pending edit. Fails if the rejecting user is the same user who submitted the edit. + */ + CommandProcessingResult rejectEdit(Long productId, String rejectReason); } diff --git a/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/module-changelog-master.xml b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/module-changelog-master.xml index f8c193c0b..c717feeaa 100644 --- a/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/module-changelog-master.xml +++ b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/module-changelog-master.xml @@ -38,4 +38,17 @@ + + + + + + + + + + + + + diff --git a/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2016_add_share_linking_enabled_to_savings_account.xml b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2016_add_share_linking_enabled_to_savings_account.xml new file mode 100644 index 000000000..54f7cdbb2 --- /dev/null +++ b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2016_add_share_linking_enabled_to_savings_account.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2017_add_saving_product_temp_table.xml b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2017_add_saving_product_temp_table.xml new file mode 100644 index 000000000..a1b9befbc --- /dev/null +++ b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2017_add_saving_product_temp_table.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code = 'APPROVE_SAVINGSPRODUCT' + + + + + + + + + + + + + + + + + + diff --git a/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2018_fix_saving_product_temp_lock_constraint.xml b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2018_fix_saving_product_temp_lock_constraint.xml new file mode 100644 index 000000000..4efb0d2f7 --- /dev/null +++ b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2018_fix_saving_product_temp_lock_constraint.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + UPDATE m_savings_product_temp SET pending_lock_key = savings_product_id WHERE status = 'PENDING' + + + + diff --git a/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2019_add_command_source_link_to_saving_product_temp.xml b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2019_add_command_source_link_to_saving_product_temp.xml new file mode 100644 index 000000000..44a9e7d89 --- /dev/null +++ b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2019_add_command_source_link_to_saving_product_temp.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + diff --git a/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2020_generalize_product_temp_table.xml b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2020_generalize_product_temp_table.xml new file mode 100644 index 000000000..0892f7f79 --- /dev/null +++ b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2020_generalize_product_temp_table.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + UPDATE m_savings_product_temp SET pending_lock_key_new = 'SAVINGS:' || product_id WHERE status = 'PENDING' + UPDATE m_savings_product_temp SET pending_lock_key_new = CONCAT('SAVINGS:', product_id) WHERE status = 'PENDING' + + + + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code = 'APPROVE_CURRENTPRODUCT' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2021_add_fd_rd_approval_permissions.xml b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2021_add_fd_rd_approval_permissions.xml new file mode 100644 index 000000000..8ae87c1d2 --- /dev/null +++ b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2021_add_fd_rd_approval_permissions.xml @@ -0,0 +1,61 @@ + + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code = 'APPROVE_FIXEDDEPOSITPRODUCT' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2022_add_product_checker_permissions.xml b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2022_add_product_checker_permissions.xml new file mode 100644 index 000000000..dbeb8f43b --- /dev/null +++ b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2022_add_product_checker_permissions.xml @@ -0,0 +1,103 @@ + + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code = 'UPDATE_SAVINGSPRODUCT_CHECKER' + + + + + + + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code = 'UPDATE_CURRENTPRODUCT_CHECKER' + + + + + + + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code = 'UPDATE_SHAREPRODUCT_CHECKER' + + + + + + + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code = 'UPDATE_FIXEDDEPOSITPRODUCT_CHECKER' + + + + + + + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code = 'UPDATE_RECURRINGDEPOSITPRODUCT_CHECKER' + + + + + + + + + + diff --git a/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2023_fix_product_approval_permissions.xml b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2023_fix_product_approval_permissions.xml new file mode 100644 index 000000000..44b9945a9 --- /dev/null +++ b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2023_fix_product_approval_permissions.xml @@ -0,0 +1,165 @@ + + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code = 'APPROVE_SAVINGSPRODUCT' + + + + + + + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code = 'REJECT_SAVINGSPRODUCT' + + + + + + + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code = 'APPROVE_CURRENTPRODUCT' + + + + + + + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code = 'REJECT_CURRENTPRODUCT' + + + + + + + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code = 'APPROVE_SHAREPRODUCT' + + + + + + + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code = 'REJECT_SHAREPRODUCT' + + + + + + + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code = 'APPROVE_FIXEDDEPOSITPRODUCT' + + + + + + + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code = 'REJECT_FIXEDDEPOSITPRODUCT' + + + + + + + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code = 'APPROVE_RECURRINGDEPOSITPRODUCT' + + + + + + + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code = 'REJECT_RECURRINGDEPOSITPRODUCT' + + + + + + + + + + diff --git a/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2025_recreate_savings_product_temp_table.xml b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2025_recreate_savings_product_temp_table.xml new file mode 100644 index 000000000..69ff8cab2 --- /dev/null +++ b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2025_recreate_savings_product_temp_table.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2026_add_pending_edit_typed_columns.xml b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2026_add_pending_edit_typed_columns.xml new file mode 100644 index 000000000..b54bb8458 --- /dev/null +++ b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2026_add_pending_edit_typed_columns.xml @@ -0,0 +1,261 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2027_add_pending_edit_satellite_tables.xml b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2027_add_pending_edit_satellite_tables.xml new file mode 100644 index 000000000..003966ce9 --- /dev/null +++ b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2027_add_pending_edit_satellite_tables.xml @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2028_update_saving_product_temp_table.xml b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2028_update_saving_product_temp_table.xml new file mode 100644 index 000000000..c6bfea24a --- /dev/null +++ b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2028_update_saving_product_temp_table.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2029_remove_proposed_payload_json.xml b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2029_remove_proposed_payload_json.xml new file mode 100644 index 000000000..19a3970fe --- /dev/null +++ b/fineract-savings/src/main/resources/db/changelog/tenant/module/savings/parts/parts/2029_remove_proposed_payload_json.xml @@ -0,0 +1,30 @@ + + + + + + + + + + +