merge(release): merge feature/minor-update into release/share-account-module

Combines election module + UAT catch-up migrations (0338-0367) with
minor-update changes: soft delete for tellers (0338), share linking
for current account (0339), terminate flag for savings/current (0340).
Resolved changelog conflict by appending minor-update entries after 0367.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jaanu423
2026-07-09 13:43:03 +05:30
co-authored by Claude Sonnet 4.6
125 changed files with 7194 additions and 167 deletions
@@ -76,6 +76,9 @@ public class Teller extends AbstractPersistableCustom<Long> {
@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<Cashier> cashiers;
@@ -161,4 +164,12 @@ public class Teller extends AbstractPersistableCustom<Long> {
this.office.getId();
this.cashiers.size();
}
public void delete() {
this.deleted = true;
}
public boolean isDeleted() {
return this.deleted;
}
}
@@ -31,4 +31,6 @@ import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface TellerRepository extends JpaRepository<Teller, Long>, JpaSpecificationExecutor<Teller> {
Collection<Teller> findTellerByOfficeId(Long officeId);
long countByDeletedFalse();
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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<Integer, CommandProcessingResultType> 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;
}
}
@@ -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;
}
@@ -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);
@@ -224,7 +224,7 @@ public final class DepositsApiConstants {
* Deposit Product Parameters
*/
private static final Set<String> 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,
@@ -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;
@@ -159,6 +159,9 @@ public final class SavingsProductData implements Serializable {
@Getter @Setter private Map<String, Object> depositLimitKyc;
@Getter @Setter private Map<String, List<Map<String, Object>>> 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,
@@ -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<String, Object> proposedChanges;
public SavingsProductTempData() {}
}
@@ -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());
}
}
@@ -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;
}
@@ -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;
}
}
@@ -37,6 +37,31 @@ public class CurrentAccountNumberGenerator {
private final JdbcTemplate jdbcTemplate;
private String getNextSequence() {
final java.util.List<String> 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;
}
}
@@ -40,4 +40,6 @@ public interface CurrentAccountWritePlatformService {
CommandProcessingResult modifyApplication(Long accountId, JsonCommand command);
CommandProcessingResult deleteApplication(Long accountId);
CommandProcessingResult terminateAccount(Long accountId, JsonCommand command);
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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<SavingsProductData> retrieveAllForLookup();
SavingsProductData retrieveOne(Long productId);
Collection<SavingsProductTempData> retrieveAllPendingEdits();
SavingsProductTempData retrievePendingEdit(Long productId);
}
@@ -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<SavingsProductTempData> 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<ProductPendingEdit> opt = this.productPendingEditRepository.findById(tempId);
if (opt.isPresent()) {
final ProductPendingEdit pendingEdit = opt.get();
final Set<String> submittedFields = new java.util.HashSet<>(
this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId()));
final Map<String, Object> proposedChanges = ProductPendingEditFieldMapper.reconstructProposedChangesMap(
pendingEdit, submittedFields, this.pendingEditChargeRepository, this.pendingEditGlMappingRepository,
this.pendingEditFacilityLimitRepository, GSON);
data.setProposedChanges(proposedChanges);
}
return data;
};
}
@Override
public Collection<SavingsProductTempData> 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<SavingsProductData> {
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;
}
@@ -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);
}
@@ -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<String> submitted = ProductPendingEditFieldMapper.populateCoreFields(command, pendingEdit.getCoreFields());
final java.util.Set<String> 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<Long> 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<ProductPendingEditFacilityLimit> facilityLimits = ProductPendingEditFieldMapper.extractFacilityLimits(command, pendingEdit);
if (facilityLimits != null) {
this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, "accountFacilityLimits"));
this.pendingEditFacilityLimitRepository.saveAll(facilityLimits);
}
final List<ProductPendingEditGlMapping> 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<String> 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<CommandSource> existing = this.commandSourceRepository.findById(pendingEdit.getCommandSourceId());
if (existing.isPresent()) {
return existing.get();
}
}
final Set<String> 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) {
@@ -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);
}
}
@@ -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
}
@@ -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<Cashier> 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();
}
/*
@@ -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<CommandProcessingResult> 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);
}
}
@@ -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);
}
}
@@ -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);
}
@@ -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.");
};
}
}
@@ -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<String> 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<ProductData> toApiJsonSerializer;
private final PlatformSecurityContext platformSecurityContext;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final ShareProductWritePlatformService shareProductWritePlatformService;
private final FromJsonHelper fromApiJsonHelper;
private final DefaultToApiJsonSerializer<SavingsProductTempData> 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<SavingsProductTempData> 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);
}
}
@@ -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<String> getResponseDataParams();
Collection<ProductData> retrieveAllForLookup();
Collection<SavingsProductTempData> retrieveAllPendingEdits();
SavingsProductTempData retrievePendingEdit(Long productId);
}
@@ -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);
}
@@ -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<SavingsProductTempData> 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<SavingsProductTempData> 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);
}
@@ -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<SavingsProductTempData> 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<SavingsProductTempData> 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 })
@@ -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<LocalDate>) (src, typeOfSrc, ctx) -> new JsonPrimitive(src.toString()))
.create();
private final RecurringDepositProductWritePlatformService recurringDepositProductWritePlatformService;
private final FromJsonHelper fromApiJsonHelper;
private final DefaultToApiJsonSerializer<SavingsProductTempData> 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<SavingsProductTempData> 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 })
@@ -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<SavingsProductTempData> 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<SavingsProductTempData> 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 })
@@ -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<Lon
return actualChanges;
}
@PostLoad
public void postLoad() {
if (this.recurringDetail == null) {
this.recurringDetail = new DepositRecurringDetail();
}
}
public DepositRecurringDetail recurringDetail() {
if (this.recurringDetail == null) {
this.recurringDetail = new DepositRecurringDetail();
}
return this.recurringDetail;
}
@@ -42,6 +42,8 @@ import static org.apache.fineract.portfolio.savings.SavingsApiConstants.specialC
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.weakerSectionParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nominationFacilityParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.autoDebitFacilityParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.introducerRequiredParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.shareLinkingEnabledParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.isSeniorCitizenParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.seniorCitizenInterestRateParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.allowOverdraftParamName;
@@ -395,6 +397,16 @@ public class SavingsAccountAssembler {
if (command.parameterExists(autoDebitFacilityParamName)) {
account.setAutoDebitFacility(this.fromApiJsonHelper.extractBooleanNamed(autoDebitFacilityParamName, element));
}
if (command.parameterExists(introducerRequiredParamName)) {
account.setIntroducerRequired(this.fromApiJsonHelper.extractBooleanNamed(introducerRequiredParamName, element));
} else if (account.savingsProduct() != null) {
account.setIntroducerRequired(account.savingsProduct().isIntroducerRequired());
}
if (command.parameterExists(shareLinkingEnabledParamName)) {
account.setShareLinkingEnabled(this.fromApiJsonHelper.extractBooleanNamed(shareLinkingEnabledParamName, element));
} else if (account.savingsProduct() != null) {
account.setShareLinkingEnabled(account.savingsProduct().isShareLinkingEnabled());
}
// Senior citizen rate — runs after validateDomainRules so it is the final persisted value
if (command.parameterExists(isSeniorCitizenParamName)) {
@@ -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.savings.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.currentaccount.service.CurrentAccountWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CURRENTACCOUNT", action = "TERMINATE")
public class CurrentAccountTerminateCommandHandler implements NewCommandSourceHandler {
private final CurrentAccountWritePlatformService writePlatformService;
@Autowired
public CurrentAccountTerminateCommandHandler(final CurrentAccountWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.terminateAccount(command.entityId(), command);
}
}
@@ -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.savings.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.savings.service.SavingsAccountWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "FIXEDDEPOSITACCOUNT", action = "TERMINATE")
public class FixedDepositAccountTerminateCommandHandler implements NewCommandSourceHandler {
private final SavingsAccountWritePlatformService writePlatformService;
@Autowired
public FixedDepositAccountTerminateCommandHandler(final SavingsAccountWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.terminate(command.entityId(), command);
}
}
@@ -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.savings.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.savings.service.SavingsAccountWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "RECURRINGDEPOSITACCOUNT", action = "TERMINATE")
public class RecurringDepositAccountTerminateCommandHandler implements NewCommandSourceHandler {
private final SavingsAccountWritePlatformService writePlatformService;
@Autowired
public RecurringDepositAccountTerminateCommandHandler(final SavingsAccountWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.terminate(command.entityId(), command);
}
}
@@ -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.savings.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.savings.service.SavingsAccountWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "SAVINGSACCOUNT", action = "TERMINATE")
public class SavingsAccountTerminateCommandHandler implements NewCommandSourceHandler {
private final SavingsAccountWritePlatformService writePlatformService;
@Autowired
public SavingsAccountTerminateCommandHandler(final SavingsAccountWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.terminate(command.entityId(), command);
}
}
@@ -22,7 +22,7 @@ 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.savings.service.SavingsAccountWritePlatformService;
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -31,16 +31,16 @@ import org.springframework.transaction.annotation.Transactional;
@CommandType(entity = "CURRENTACCOUNT", action = "ACTIVATE")
public class ActivateCurrentAccountCommandHandler implements NewCommandSourceHandler {
private final SavingsAccountWritePlatformService writePlatformService;
private final CurrentAccountWritePlatformService writePlatformService;
@Autowired
public ActivateCurrentAccountCommandHandler(final SavingsAccountWritePlatformService writePlatformService) {
public ActivateCurrentAccountCommandHandler(final CurrentAccountWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.activate(command.getSavingsId(), command);
return this.writePlatformService.activate(command.entityId(), command);
}
}
@@ -22,7 +22,7 @@ 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.savings.service.SavingsAccountWritePlatformService;
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -31,16 +31,16 @@ import org.springframework.transaction.annotation.Transactional;
@CommandType(entity = "CURRENTACCOUNT", action = "CLOSE")
public class CloseSavingsCurrentAccountCommandHandler implements NewCommandSourceHandler {
private final SavingsAccountWritePlatformService writePlatformService;
private final CurrentAccountWritePlatformService writePlatformService;
@Autowired
public CloseSavingsCurrentAccountCommandHandler(final SavingsAccountWritePlatformService writePlatformService) {
public CloseSavingsCurrentAccountCommandHandler(final CurrentAccountWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.close(command.getSavingsId(), command);
return this.writePlatformService.closeAccount(command.entityId(), command);
}
}
@@ -22,7 +22,7 @@ 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.savings.service.SavingsApplicationProcessWritePlatformService;
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -31,16 +31,16 @@ import org.springframework.transaction.annotation.Transactional;
@CommandType(entity = "CURRENTACCOUNT", action = "APPROVE")
public class CurrentAccountApplicationApprovalCommandHandler implements NewCommandSourceHandler {
private final SavingsApplicationProcessWritePlatformService applicationProcessService;
private final CurrentAccountWritePlatformService writePlatformService;
@Autowired
public CurrentAccountApplicationApprovalCommandHandler(final SavingsApplicationProcessWritePlatformService applicationProcessService) {
this.applicationProcessService = applicationProcessService;
public CurrentAccountApplicationApprovalCommandHandler(final CurrentAccountWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.applicationProcessService.approveApplication(command.getSavingsId(), command);
return this.writePlatformService.approveApplication(command.entityId(), command);
}
}
@@ -22,7 +22,7 @@ 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.savings.service.SavingsApplicationProcessWritePlatformService;
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -31,16 +31,16 @@ import org.springframework.transaction.annotation.Transactional;
@CommandType(entity = "CURRENTACCOUNT", action = "APPROVALUNDO")
public class CurrentAccountApplicationApprovalUndoCommandHandler implements NewCommandSourceHandler {
private final SavingsApplicationProcessWritePlatformService applicationProcessService;
private final CurrentAccountWritePlatformService writePlatformService;
@Autowired
public CurrentAccountApplicationApprovalUndoCommandHandler(final SavingsApplicationProcessWritePlatformService applicationProcessService) {
this.applicationProcessService = applicationProcessService;
public CurrentAccountApplicationApprovalUndoCommandHandler(final CurrentAccountWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.applicationProcessService.undoApplicationApproval(command.getSavingsId(), command);
return this.writePlatformService.undoApproval(command.entityId(), command);
}
}
@@ -22,7 +22,7 @@ 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.savings.service.SavingsApplicationProcessWritePlatformService;
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -31,16 +31,16 @@ import org.springframework.transaction.annotation.Transactional;
@CommandType(entity = "CURRENTACCOUNT", action = "DELETE")
public class CurrentAccountApplicationDeletionCommandHandler implements NewCommandSourceHandler {
private final SavingsApplicationProcessWritePlatformService applicationProcessService;
private final CurrentAccountWritePlatformService writePlatformService;
@Autowired
public CurrentAccountApplicationDeletionCommandHandler(final SavingsApplicationProcessWritePlatformService applicationProcessService) {
this.applicationProcessService = applicationProcessService;
public CurrentAccountApplicationDeletionCommandHandler(final CurrentAccountWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.applicationProcessService.deleteApplication(command.getSavingsId());
return this.writePlatformService.deleteApplication(command.entityId());
}
}
@@ -22,7 +22,7 @@ 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.savings.service.SavingsApplicationProcessWritePlatformService;
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -31,16 +31,16 @@ import org.springframework.transaction.annotation.Transactional;
@CommandType(entity = "CURRENTACCOUNT", action = "UPDATE")
public class CurrentAccountApplicationModificationCommandHandler implements NewCommandSourceHandler {
private final SavingsApplicationProcessWritePlatformService applicationProcessService;
private final CurrentAccountWritePlatformService writePlatformService;
@Autowired
public CurrentAccountApplicationModificationCommandHandler(final SavingsApplicationProcessWritePlatformService applicationProcessService) {
this.applicationProcessService = applicationProcessService;
public CurrentAccountApplicationModificationCommandHandler(final CurrentAccountWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.applicationProcessService.modifyApplication(command.getSavingsId(), command);
return this.writePlatformService.modifyApplication(command.entityId(), command);
}
}
@@ -22,7 +22,7 @@ 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.savings.service.SavingsApplicationProcessWritePlatformService;
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -31,16 +31,16 @@ import org.springframework.transaction.annotation.Transactional;
@CommandType(entity = "CURRENTACCOUNT", action = "REJECT")
public class CurrentAccountApplicationRejectedCommandHandler implements NewCommandSourceHandler {
private final SavingsApplicationProcessWritePlatformService applicationProcessService;
private final CurrentAccountWritePlatformService writePlatformService;
@Autowired
public CurrentAccountApplicationRejectedCommandHandler(final SavingsApplicationProcessWritePlatformService applicationProcessService) {
this.applicationProcessService = applicationProcessService;
public CurrentAccountApplicationRejectedCommandHandler(final CurrentAccountWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.applicationProcessService.rejectApplication(command.getSavingsId(), command);
return this.writePlatformService.rejectApplication(command.entityId(), command);
}
}
@@ -22,7 +22,7 @@ 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.savings.service.SavingsApplicationProcessWritePlatformService;
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -31,16 +31,16 @@ import org.springframework.transaction.annotation.Transactional;
@CommandType(entity = "CURRENTACCOUNT", action = "CREATE")
public class CurrentAccountApplicationSubmittalCommandHandler implements NewCommandSourceHandler {
private final SavingsApplicationProcessWritePlatformService applicationProcessService;
private final CurrentAccountWritePlatformService writePlatformService;
@Autowired
public CurrentAccountApplicationSubmittalCommandHandler(final SavingsApplicationProcessWritePlatformService applicationProcessService) {
this.applicationProcessService = applicationProcessService;
public CurrentAccountApplicationSubmittalCommandHandler(final CurrentAccountWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.applicationProcessService.submitApplication(command);
return this.writePlatformService.submitApplication(command);
}
}
@@ -22,7 +22,7 @@ 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.savings.service.SavingsApplicationProcessWritePlatformService;
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -31,16 +31,16 @@ import org.springframework.transaction.annotation.Transactional;
@CommandType(entity = "CURRENTACCOUNT", action = "WITHDRAW")
public class CurrentAccountApplicationWithdrawnCommandHandler implements NewCommandSourceHandler {
private final SavingsApplicationProcessWritePlatformService applicationProcessService;
private final CurrentAccountWritePlatformService writePlatformService;
@Autowired
public CurrentAccountApplicationWithdrawnCommandHandler(final SavingsApplicationProcessWritePlatformService applicationProcessService) {
this.applicationProcessService = applicationProcessService;
public CurrentAccountApplicationWithdrawnCommandHandler(final CurrentAccountWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.applicationProcessService.applicantWithdrawsFromApplication(command.getSavingsId(), command);
return this.writePlatformService.withdrawApplication(command.entityId(), command);
}
}
@@ -297,12 +297,21 @@ public class CurrentAccountReadPlatformServiceImpl implements CurrentAccountRead
hasWhere = true;
}
if (StringUtils.isNotBlank(status)) {
final Integer statusEnum = resolveStatusEnum(status);
if (statusEnum != null) {
if ("terminated".equalsIgnoreCase(status)) {
sqlBuilder.append(hasWhere ? " AND " : " WHERE ");
sqlBuilder.append("ca.status_enum = ?");
params.add(statusEnum);
sqlBuilder.append("ca.terminate_true = true");
hasWhere = true;
} else {
final Integer statusEnum = resolveStatusEnum(status);
if (statusEnum != null) {
sqlBuilder.append(hasWhere ? " AND " : " WHERE ");
sqlBuilder.append("ca.status_enum = ?");
params.add(statusEnum);
hasWhere = true;
if (statusEnum == 100) {
sqlBuilder.append(" AND (ca.terminate_true = false OR ca.terminate_true IS NULL)");
}
}
}
}
if (fromDate != null) {
@@ -356,6 +365,7 @@ public class CurrentAccountReadPlatformServiceImpl implements CurrentAccountRead
case "withdrawn" -> 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<Map<String, Object>> {
@Override
public Map<String, Object> mapRow(ResultSet rs, int rowNum) throws SQLException {
@@ -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());
@@ -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<String, Object> 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"));
@@ -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<DepositProductData> 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<SavingsProductTempData> 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<ProductPendingEdit> opt = this.productPendingEditRepository.findById(tempId);
if (opt.isPresent()) {
final ProductPendingEdit pendingEdit = opt.get();
final Set<String> submittedFields = new java.util.HashSet<>(
this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId()));
final Map<String, Object> 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<SavingsProductTempData> 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<DepositProductData> {
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;
}
}
@@ -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<String> submitted = ProductPendingEditFieldMapper.populateCoreFields(command, pendingEdit.getCoreFields());
final java.util.Set<String> 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<Long> 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<ProductPendingEditGlMapping> 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<String> 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<CommandSource> existing = this.commandSourceRepository.findById(pendingEdit.getCommandSourceId());
if (existing.isPresent()) {
return existing.get();
}
}
final Set<String> 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) {
@@ -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<String> submitted = ProductPendingEditFieldMapper.populateCoreFields(command, pendingEdit.getCoreFields());
final java.util.Set<String> fdSubmitted = ProductPendingEditFieldMapper.populateFixedDepositFields(command, pendingEdit.getFixedDepositFields());
final java.util.Set<String> 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<Long> 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<ProductPendingEditGlMapping> 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<String> 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<CommandSource> existing = this.commandSourceRepository.findById(pendingEdit.getCommandSourceId());
if (existing.isPresent()) {
return existing.get();
}
}
final Set<String> 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) {
@@ -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");
@@ -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<String, Object> 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();
@@ -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<List<Map<String, Object>>>() {}.getType();
private static final Type OTHER_GL_MAPPINGS_MAP_TYPE = new TypeToken<Map<String, List<Map<String, Object>>>>() {}.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<SavingsProductTempData> 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<ProductPendingEdit> opt = this.productPendingEditRepository.findById(tempId);
if (opt.isPresent()) {
final ProductPendingEdit pendingEdit = opt.get();
final Set<String> submittedFields = new java.util.HashSet<>(
this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId()));
final Map<String, Object> proposedChanges = ProductPendingEditFieldMapper.reconstructProposedChangesMap(
pendingEdit, submittedFields, this.pendingEditChargeRepository, this.pendingEditGlMappingRepository, null, GSON);
data.setProposedChanges(proposedChanges);
}
return data;
};
}
@Override
public Collection<SavingsProductTempData> 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<SavingsProductData> {
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;
}
@@ -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<String> 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<Long> 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<ProductPendingEditFacilityLimit> facilityLimits = ProductPendingEditFieldMapper.extractFacilityLimits(command,
pendingEdit);
if (facilityLimits != null) {
this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, "accountFacilityLimits"));
this.pendingEditFacilityLimitRepository.saveAll(facilityLimits);
}
final List<ProductPendingEditGlMapping> 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<String> 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<CommandSource> existing = this.commandSourceRepository.findById(pendingEdit.getCommandSourceId());
if (existing.isPresent()) {
return existing.get();
}
}
final Set<String> 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) {
@@ -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
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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<SavingsProductTempData> 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<ProductPendingEdit> opt = this.productPendingEditRepository.findById(tempId);
if (opt.isPresent()) {
final ProductPendingEdit pendingEdit = opt.get();
final Set<String> submittedFields = new java.util.HashSet<>(
this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId()));
final Map<String, Object> proposedChanges = ProductPendingEditFieldMapper.reconstructProposedChangesMap(
pendingEdit, submittedFields, this.pendingEditChargeRepository, this.pendingEditGlMappingRepository, null, GSON);
data.setProposedChanges(proposedChanges);
}
return data;
};
}
@Override
public Collection<SavingsProductTempData> 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<ProductData> {
@SuppressWarnings("unused")
@@ -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);
}
@@ -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<String> 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<Long> 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<ProductPendingEditGlMapping> 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<String> 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<CommandSource> existing = this.commandSourceRepository.findById(pendingEdit.getCommandSourceId());
if (existing.isPresent()) {
return existing.get();
}
}
final Set<String> 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",
@@ -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);
}
}
@@ -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}
@@ -394,4 +394,7 @@
<include file="parts/0364_catchup_current_product_tables.xml" relativeToChangelogFile="true"/>
<include file="parts/0366_catchup_clerk_withdrawal_token.xml" relativeToChangelogFile="true"/>
<include file="parts/0367_catchup_missing_tables.xml" relativeToChangelogFile="true"/>
<include file="parts/0338_add_soft_delete_to_tellers.xml" relativeToChangelogFile="true"/>
<include file="parts/0339_add_share_linking_enabled_to_current_account.xml" relativeToChangelogFile="true"/>
<include file="parts/0340_add_terminate_true_to_savings_and_current_account.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet id="0338_add_soft_delete_to_tellers" author="fineract">
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_tellers"/>
<not><columnExists tableName="m_tellers" columnName="is_deleted"/></not>
</and>
</preConditions>
<addColumn tableName="m_tellers">
<column name="is_deleted" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet id="0339_add_share_linking_enabled_to_current_account" author="CBS">
<preConditions onFail="MARK_RAN">
<tableExists tableName="m_current_account"/>
<not><columnExists tableName="m_current_account" columnName="is_linking_enable"/></not>
</preConditions>
<addColumn tableName="m_current_account">
<column name="is_linking_enable" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet id="0340_1" author="fineract">
<addColumn tableName="m_savings_account">
<column name="terminate_true" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
<changeSet id="0340_2" author="fineract">
<addColumn tableName="m_current_account">
<column name="terminate_true" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -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<org.apache.fineract.infrastructure.core.data.EnumOptionData> 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; }
@@ -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<EnumOptionData> v) { this.interestCalculationTypeOption = v; }
public void setPeriodFrequencyOptions(Collection<EnumOptionData> 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) {
@@ -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
@@ -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;
}
@@ -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<L
return actualChanges;
}
@PostLoad
public void postLoad() {
if (this.preClosureDetail == null) {
this.preClosureDetail = new DepositPreClosureDetail();
}
if (this.depositTermDetail == null) {
this.depositTermDetail = new DepositTermDetail();
}
}
public DepositPreClosureDetail depositPreClosureDetail() {
if (this.preClosureDetail == null) {
this.preClosureDetail = new DepositPreClosureDetail();
}
return this.preClosureDetail;
}
public DepositTermDetail depositTermDetail() {
if (this.depositTermDetail == null) {
this.depositTermDetail = new DepositTermDetail();
}
return this.depositTermDetail;
}
@@ -0,0 +1,54 @@
/**
* 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;
/**
* Mirrors the "depositLimitKyc" JSON object - a 1:1 relationship today, so kept flat here. Note:
* the real JSON shape is nested ({@code {general: {kycType, limit}, minor: {...}, smallSaving:
* {...}}}), not flat siblings - the replay step is responsible for re-nesting these columns back
* into that shape before handing off to the unchanged assembler.
*/
@Embeddable
@Getter
@Setter
public class DepositLimitKycEditFields {
@Column(name = "kyc_general_type")
private String generalType;
@Column(name = "kyc_general_limit")
private String generalLimit;
@Column(name = "kyc_minor_type")
private String minorType;
@Column(name = "kyc_minor_limit")
private Long minorLimit;
@Column(name = "kyc_small_saving_type")
private String smallSavingType;
@Column(name = "kyc_small_saving_limit")
private Long smallSavingLimit;
}
@@ -0,0 +1,79 @@
/**
* 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;
/** Mirrors the "accountFacilityConfig" JSON object - a 1:1 relationship today, so kept flat here. */
@Embeddable
@Getter
@Setter
public class FacilityConfigEditFields {
@Column(name = "fc_atm_facility")
private Boolean atmFacility;
@Column(name = "fc_atm_withdrawal_facility")
private Boolean atmWithdrawalFacility;
@Column(name = "fc_upi_facility")
private Boolean upiFacility;
@Column(name = "fc_pos_transactions_facility")
private Boolean posTransactionsFacility;
@Column(name = "fc_cheque_book")
private Boolean chequeBook;
@Column(name = "fc_imps_facility")
private Boolean impsFacility;
@Column(name = "fc_sms_alerts")
private Boolean smsAlerts;
@Column(name = "fc_bbps_facility")
private Boolean bbpsFacility;
@Column(name = "fc_insurance_cover")
private Boolean insuranceCover;
@Column(name = "fc_bepg_facility")
private Boolean bepgFacility;
@Column(name = "fc_neft_facility")
private Boolean neftFacility;
@Column(name = "fc_rtgs_facility")
private Boolean rtgsFacility;
@Column(name = "fc_inter_bank_total_cap_facility")
private Boolean interBankTotalCapFacility;
@Column(name = "fc_withdrawal_facility")
private Boolean withdrawalFacility;
@Column(name = "fc_deposit_facility_kyc")
private Boolean depositFacilityKyc;
@Column(name = "fc_virtual_debit_card")
private Boolean virtualDebitCard;
}
@@ -0,0 +1,71 @@
/**
* 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;
/** Also used by Recurring Deposit, which extends Fixed Deposit. */
@Embeddable
@Getter
@Setter
public class FixedDepositEditFields {
@Column(name = "fd_type")
private String fdType;
@Column(name = "pre_closure_penal_applicable")
private Boolean preClosurePenalApplicable;
@Column(name = "pre_closure_penal_interest")
private BigDecimal preClosurePenalInterest;
@Column(name = "pre_closure_penal_interest_on_type_id")
private Integer preClosurePenalInterestOnTypeId;
@Column(name = "min_deposit_term")
private Integer minDepositTerm;
@Column(name = "max_deposit_term")
private Integer maxDepositTerm;
@Column(name = "min_deposit_term_type_id")
private Integer minDepositTermTypeId;
@Column(name = "max_deposit_term_type_id")
private Integer maxDepositTermTypeId;
@Column(name = "in_multiples_of_deposit_term")
private Integer inMultiplesOfDepositTerm;
@Column(name = "in_multiples_of_deposit_term_type_id")
private Integer inMultiplesOfDepositTermTypeId;
@Column(name = "min_deposit_amount")
private BigDecimal minDepositAmount;
@Column(name = "max_deposit_amount")
private BigDecimal maxDepositAmount;
@Column(name = "deposit_amount")
private BigDecimal depositAmount;
}
@@ -0,0 +1,78 @@
/**
* 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;
/**
* Scalar (1:1) GL account mapping ids. No FK constraints to the accounting GL table - the
* existing validator already checks GL account validity at submit time. The 3 array-shaped GL
* mappings (payment-channel/fee/penalty to income account) live in m_product_temp_gl_mapping
* instead, since those are genuinely one-to-many.
*/
@Embeddable
@Getter
@Setter
public class GlAccountMappingEditFields {
@Column(name = "gl_savings_reference_account_id")
private Long savingsReferenceAccountId;
@Column(name = "gl_savings_control_account_id")
private Long savingsControlAccountId;
@Column(name = "gl_income_from_fee_account_id")
private Long incomeFromFeeAccountId;
@Column(name = "gl_income_from_penalty_account_id")
private Long incomeFromPenaltyAccountId;
@Column(name = "gl_interest_on_savings_account_id")
private Long interestOnSavingsAccountId;
@Column(name = "gl_transfers_in_suspense_account_id")
private Long transfersInSuspenseAccountId;
@Column(name = "gl_overdraft_portfolio_control_id")
private Long overdraftPortfolioControlId;
@Column(name = "gl_income_from_interest_id")
private Long incomeFromInterestId;
@Column(name = "gl_write_off_account_id")
private Long writeOffAccountId;
@Column(name = "gl_escheat_liability_id")
private Long escheatLiabilityId;
@Column(name = "gl_penalties_receivable_account_id")
private Long penaltiesReceivableAccountId;
@Column(name = "gl_interest_receivable_account_id")
private Long interestReceivableAccountId;
@Column(name = "gl_fees_receivable_account_id")
private Long feesReceivableAccountId;
@Column(name = "gl_interest_payable_account_id")
private Long interestPayableAccountId;
}
@@ -0,0 +1,204 @@
/**
* 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.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.Lob;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
import lombok.Getter;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.useradministration.domain.AppUser;
/**
* Holds a single pending product edit, shared across product types (Savings, Current Account, Share) in one table
* (m_savings_product_temp). Presence of a row here for a given (productType, productId) with
* {@code status = PENDING} is itself the lock - no separate "is locked" column anywhere. The full edit request body
* is kept in {@link #proposedPayloadJson} so approval can replay it through each product's normal update path
* (covering charges, GL/accounting mappings and other related tables, not just scalar columns).
*/
@Entity
@Table(name = "m_saving_product_temp")
@Getter
@Setter
public class ProductPendingEdit extends AbstractPersistableCustom<Long> {
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;
/**
* "<productType>:<productId>" 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;
}
}
@@ -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<Long> {
@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;
}
}
@@ -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<ProductPendingEditCharge, Long> {
List<ProductPendingEditCharge> findByPendingEditId(Long pendingEditId);
}
@@ -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<Long> {
@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;
}
}
@@ -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<ProductPendingEditFacilityLimit, Long> {
List<ProductPendingEditFacilityLimit> findByPendingEditId(Long pendingEditId);
}
@@ -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<String> populateCoreFields(final JsonCommand command, final SavingsCoreEditFields f) {
final Set<String> 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<String> 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<Long> extractChargeIds(final JsonCommand command) {
if (!command.hasParameter("charges")) {
return null;
}
final com.google.gson.JsonElement raw = command.parsedJson().getAsJsonObject().get("charges");
final List<Long> 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<ProductPendingEditCharge> 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<ProductPendingEditFacilityLimit> 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<ProductPendingEditFacilityLimit> 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<ProductPendingEditFacilityLimit> 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<ProductPendingEditGlMapping> extractGlArrayMappings(final JsonCommand command, final ProductPendingEdit edit) {
final List<ProductPendingEditGlMapping> 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<ProductPendingEditGlMapping> 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<ProductPendingEditGlMapping> 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<ProductPendingEditGlMapping> 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<String> submitted, final String name, final String value) {
if (submitted.contains(name)) {
root.addProperty(name, value);
}
}
private static void putNumber(final JsonObject root, final Set<String> submitted, final String name, final Number value) {
if (submitted.contains(name)) {
root.addProperty(name, value);
}
}
private static void putBoolean(final JsonObject root, final Set<String> submitted, final String name, final Boolean value) {
if (submitted.contains(name)) {
root.addProperty(name, value);
}
}
private static void putDate(final JsonObject root, final Set<String> 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<String> populateCurrentOnlyFields(final JsonCommand command, final CurrentOnlyEditFields f) {
final Set<String> 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<String> 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<String> populateShareOnlyFields(final JsonCommand command, final ShareOnlyEditFields f) {
final Set<String> 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<String> 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<String> populateFixedDepositFields(final JsonCommand command, final FixedDepositEditFields f) {
final Set<String> 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<String> 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<String> populateRecurringDepositFields(final JsonCommand command, final RecurringDepositEditFields f) {
final Set<String> 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<String> 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<String, Object> reconstructProposedChangesMap(
final ProductPendingEdit pendingEdit,
final Set<String> 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<java.util.Map<String, Object>>() {}.getType());
}
}
@@ -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<Long> {
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;
}
}
@@ -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<ProductPendingEditGlMapping, Long> {
List<ProductPendingEditGlMapping> findByPendingEditId(Long pendingEditId);
}
@@ -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<Long> {
@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;
}
}
@@ -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<ProductPendingEditMarketPrice, Long> {
List<ProductPendingEditMarketPrice> findByPendingEditId(Long pendingEditId);
}
@@ -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<ProductPendingEdit, Long> {
Optional<ProductPendingEdit> findByProductTypeAndProductIdAndStatus(String productType, Long productId, String status);
List<ProductPendingEdit> findByProductTypeAndStatus(String productType, String status);
}
@@ -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<Long> {
@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;
}
}
@@ -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<ProductPendingEditSubmittedField, Long> {
List<ProductPendingEditSubmittedField> findByPendingEditId(Long pendingEditId);
@Query("SELECT f.fieldName FROM ProductPendingEditSubmittedField f WHERE f.pendingEdit.id = :pendingEditId")
List<String> findFieldNamesByPendingEditId(@Param("pendingEditId") Long pendingEditId);
}
@@ -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;
}
@@ -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<Long>
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<SavingsAccountJointHolder> 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<Long>
@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<Long>
@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<String, Object> terminate() {
final Map<String, Object> 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<Long>
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; }
@@ -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);
}

Some files were not shown because too many files have changed in this diff Show More