implemented a Maker-Checker workflow for Products, with approval-based updates and product locking to prevent concurrent edits
This commit is contained in:
+3
@@ -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,
|
||||
|
||||
+49
@@ -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() {}
|
||||
}
|
||||
+29
@@ -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);
|
||||
}
|
||||
}
|
||||
+29
@@ -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);
|
||||
}
|
||||
}
|
||||
+29
@@ -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);
|
||||
}
|
||||
}
|
||||
+5
@@ -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);
|
||||
}
|
||||
|
||||
+55
-1
@@ -42,7 +42,9 @@ 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;
|
||||
@@ -147,6 +149,55 @@ 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.maker_id as makerId, maker.username as makerUsername, "
|
||||
+ "spt.checker_id as checkerId, checker.username as checkerUsername, spt.created_on as createdOn, "
|
||||
+ "spt.checked_on as checkedOn, spt.status as status, spt.reject_reason as rejectReason, "
|
||||
+ "spt.proposed_payload_json as proposedPayloadJson "
|
||||
+ "from m_savings_product_temp spt "
|
||||
+ "join m_current_product cp on cp.id = spt.product_id "
|
||||
+ "join m_appuser maker on maker.id = spt.maker_id "
|
||||
+ "left join m_appuser checker on checker.id = spt.checker_id "
|
||||
+ "where spt.product_type = 'CURRENT' ";
|
||||
|
||||
private static final RowMapper<SavingsProductTempData> PENDING_EDIT_MAPPER = (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 String proposedPayloadJson = rs.getString("proposedPayloadJson");
|
||||
if (proposedPayloadJson != null) {
|
||||
data.setProposedChanges(GSON.fromJson(proposedPayloadJson, new TypeToken<Map<String, Object>>() {}.getType()));
|
||||
}
|
||||
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_on desc";
|
||||
return this.jdbcTemplate.query(sql, PENDING_EDIT_MAPPER);
|
||||
}
|
||||
|
||||
@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, PENDING_EDIT_MAPPER, 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 +248,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_savings_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 +374,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;
|
||||
}
|
||||
|
||||
+17
@@ -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);
|
||||
}
|
||||
|
||||
+132
@@ -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,16 @@ 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.tax.domain.TaxGroup;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -69,6 +80,9 @@ public class CurrentProductWritePlatformServiceJpaRepositoryImpl implements Curr
|
||||
private final CurrentProductFacilityConfigRepository facilityConfigRepository;
|
||||
private final CurrentProductFacilityLimitRepository facilityLimitRepository;
|
||||
private final CurrentProductDepositLimitKycRepository depositLimitKycRepository;
|
||||
private final ProductPendingEditRepository productPendingEditRepository;
|
||||
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 +265,124 @@ 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, command.json());
|
||||
|
||||
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);
|
||||
|
||||
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 JsonCommand replayedCommand = JsonCommand.fromJsonElement(productId,
|
||||
this.fromApiJsonHelper.parse(pendingEdit.getProposedPayloadJson()), 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 CommandSource auditEntry = CommandSource.builder() //
|
||||
.actionName("UPDATE") //
|
||||
.entityName("CURRENTPRODUCT") //
|
||||
.resourceGetUrl("/currentproducts/" + productId) //
|
||||
.resourceId(productId) //
|
||||
.commandAsJson(pendingEdit.getProposedPayloadJson()) //
|
||||
.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) {
|
||||
|
||||
+9
-2
@@ -19,7 +19,10 @@
|
||||
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.currentproduct.data.CurrentProductDataValidator;
|
||||
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductAssembler;
|
||||
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductDepositLimitKycRepository;
|
||||
@@ -56,9 +59,13 @@ public class CurrentProductConfiguration {
|
||||
ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService,
|
||||
CurrentProductFacilityConfigRepository currentFacilityConfigRepository,
|
||||
CurrentProductFacilityLimitRepository currentFacilityLimitRepository,
|
||||
CurrentProductDepositLimitKycRepository currentDepositLimitKycRepository) {
|
||||
CurrentProductDepositLimitKycRepository currentDepositLimitKycRepository,
|
||||
ProductPendingEditRepository productPendingEditRepository,
|
||||
FromJsonHelper fromApiJsonHelper,
|
||||
CommandSourceRepository commandSourceRepository) {
|
||||
return new CurrentProductWritePlatformServiceJpaRepositoryImpl(context, currentProductRepository, currentProductDataValidator,
|
||||
currentProductAssembler, accountMappingWritePlatformService, currentFacilityConfigRepository,
|
||||
currentFacilityLimitRepository, currentDepositLimitKycRepository);
|
||||
currentFacilityLimitRepository, currentDepositLimitKycRepository, productPendingEditRepository, fromApiJsonHelper,
|
||||
commandSourceRepository);
|
||||
}
|
||||
}
|
||||
|
||||
+112
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -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);
|
||||
}
|
||||
|
||||
+74
-3
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+77
-3
@@ -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 })
|
||||
|
||||
+78
-3
@@ -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 })
|
||||
|
||||
+88
-3
@@ -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 })
|
||||
|
||||
+67
-2
@@ -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,7 +37,9 @@ 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;
|
||||
@@ -124,6 +129,60 @@ 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 static final RowMapper<SavingsProductTempData> PENDING_EDIT_MAPPER = (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 String proposedPayloadJson = rs.getString("proposedPayloadJson");
|
||||
if (proposedPayloadJson != null) {
|
||||
data.setProposedChanges(GSON.fromJson(proposedPayloadJson, new TypeToken<Map<String, Object>>() {}.getType()));
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
private static String pendingEditSchema(final String productType) {
|
||||
return "spt.id as tempId, spt.product_id as savingsProductId, " + "sp.name as savingsProductName, spt.maker_id as makerId, "
|
||||
+ "maker.username as makerUsername, spt.checker_id as checkerId, checker.username as checkerUsername, "
|
||||
+ "spt.created_on as createdOn, spt.checked_on as checkedOn, spt.status as status, "
|
||||
+ "spt.reject_reason as rejectReason, spt.proposed_payload_json as proposedPayloadJson "
|
||||
+ "from m_savings_product_temp spt " + "join m_savings_product sp on sp.id = spt.product_id "
|
||||
+ "join m_appuser maker on maker.id = spt.maker_id " + "left join m_appuser checker on checker.id = spt.checker_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_on desc";
|
||||
return this.jdbcTemplate.query(sql, PENDING_EDIT_MAPPER);
|
||||
}
|
||||
|
||||
@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, PENDING_EDIT_MAPPER, 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 +314,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_savings_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 +406,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 +454,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_savings_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 +577,7 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead
|
||||
result.setDeceasedSettlement(deceasedSettlement);
|
||||
result.setAllowDebitTxnAfterDueDate(allowDebitTxnAfterDueDate);
|
||||
result.setWithdrawalFeeForTransfers(withdrawalFeeForTransfers);
|
||||
result.setIsPendingApproval(rs.getBoolean("isPendingApproval"));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
+132
@@ -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,14 @@ 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.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;
|
||||
|
||||
@@ -63,6 +74,9 @@ public class FixedDepositProductWritePlatformServiceJpaRepositoryImpl implements
|
||||
private final DepositProductAssembler depositProductAssembler;
|
||||
private final ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService;
|
||||
private final InterestRateChartAssembler chartAssembler;
|
||||
private final ProductPendingEditRepository productPendingEditRepository;
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
private final CommandSourceRepository commandSourceRepository;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
@@ -150,6 +164,124 @@ 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, command.json());
|
||||
|
||||
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);
|
||||
|
||||
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 JsonCommand replayedCommand = JsonCommand.fromJsonElement(productId,
|
||||
this.fromApiJsonHelper.parse(pendingEdit.getProposedPayloadJson()), 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 CommandSource auditEntry = CommandSource.builder() //
|
||||
.actionName("UPDATE") //
|
||||
.entityName("FIXEDDEPOSITPRODUCT") //
|
||||
.resourceGetUrl("/fixeddepositproducts/" + productId) //
|
||||
.resourceId(productId) //
|
||||
.commandAsJson(pendingEdit.getProposedPayloadJson()) //
|
||||
.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) {
|
||||
|
||||
+132
@@ -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,16 @@ 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.ProductPendingEditRepository;
|
||||
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 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;
|
||||
|
||||
@@ -63,6 +74,9 @@ public class RecurringDepositProductWritePlatformServiceJpaRepositoryImpl implem
|
||||
private final DepositProductAssembler depositProductAssembler;
|
||||
private final ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService;
|
||||
private final InterestRateChartAssembler chartAssembler;
|
||||
private final ProductPendingEditRepository productPendingEditRepository;
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
private final CommandSourceRepository commandSourceRepository;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
@@ -150,6 +164,124 @@ 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, command.json());
|
||||
|
||||
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);
|
||||
|
||||
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 JsonCommand replayedCommand = JsonCommand.fromJsonElement(productId,
|
||||
this.fromApiJsonHelper.parse(pendingEdit.getProposedPayloadJson()), 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 CommandSource auditEntry = CommandSource.builder() //
|
||||
.actionName("UPDATE") //
|
||||
.entityName("RECURRINGDEPOSITPRODUCT") //
|
||||
.resourceGetUrl("/recurringdepositproducts/" + productId) //
|
||||
.resourceId(productId) //
|
||||
.commandAsJson(pendingEdit.getProposedPayloadJson()) //
|
||||
.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) {
|
||||
|
||||
+57
-1
@@ -38,12 +38,14 @@ 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.exception.SavingsProductNoPendingEditException;
|
||||
import org.apache.fineract.portfolio.savings.exception.SavingsProductNotFoundException;
|
||||
import org.apache.fineract.portfolio.tax.data.TaxGroupData;
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
@@ -170,6 +172,57 @@ 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.maker_id as makerId, maker.username as makerUsername, "
|
||||
+ "spt.checker_id as checkerId, checker.username as checkerUsername, spt.created_on as createdOn, "
|
||||
+ "spt.checked_on as checkedOn, spt.status as status, spt.reject_reason as rejectReason, "
|
||||
+ "spt.proposed_payload_json as proposedPayloadJson "
|
||||
+ "from m_savings_product_temp spt "
|
||||
+ "join m_savings_product sp on sp.id = spt.product_id "
|
||||
+ "join m_appuser maker on maker.id = spt.maker_id "
|
||||
+ "left join m_appuser checker on checker.id = spt.checker_id "
|
||||
+ "where spt.product_type = 'SAVINGS' ";
|
||||
|
||||
private static final RowMapper<SavingsProductTempData> PENDING_EDIT_MAPPER = (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 String proposedPayloadJson = rs.getString("proposedPayloadJson");
|
||||
if (proposedPayloadJson != null) {
|
||||
final Map<String, Object> proposedChanges = GSON.fromJson(proposedPayloadJson,
|
||||
new TypeToken<Map<String, Object>>() {}.getType());
|
||||
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_on desc";
|
||||
return this.jdbcTemplate.query(sql, PENDING_EDIT_MAPPER);
|
||||
}
|
||||
|
||||
@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, PENDING_EDIT_MAPPER, 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 +270,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_savings_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 +395,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;
|
||||
}
|
||||
|
||||
+236
@@ -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;
|
||||
@@ -75,6 +96,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 +307,214 @@ 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, command.json());
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Replay through the real update path so charges, GL/accounting mappings and other related
|
||||
// tables are applied exactly as a direct edit would apply them - not hand-copied field by
|
||||
// field. The replay JSON is rebuilt from this row's typed columns/satellite tables (the
|
||||
// manifest records exactly which top-level parameters the maker actually submitted); pending
|
||||
// edits created before this cutover have no manifest rows, so they fall back to replaying the
|
||||
// legacy JSON blob directly.
|
||||
final Set<String> submittedFields = new java.util.HashSet<>(
|
||||
this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId()));
|
||||
final JsonCommand replayedCommand;
|
||||
if (submittedFields.isEmpty()) {
|
||||
replayedCommand = JsonCommand.fromJsonElement(productId, this.fromApiJsonHelper.parse(pendingEdit.getProposedPayloadJson()),
|
||||
this.fromApiJsonHelper);
|
||||
} else {
|
||||
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()));
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every edit submitted through submitEditForApproval() links a CommandSource audit row. This fallback only
|
||||
* matters for pending edits that were created before that linking existed - it backfills one now so approve
|
||||
* and reject always leave an entry in m_portfolio_command_source, with no silent no-op case.
|
||||
*/
|
||||
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 CommandSource auditEntry = CommandSource.builder() //
|
||||
.actionName("UPDATE") //
|
||||
.entityName("SAVINGSPRODUCT") //
|
||||
.resourceGetUrl("/savingsproducts/" + productId) //
|
||||
.resourceId(productId) //
|
||||
.commandAsJson(pendingEdit.getProposedPayloadJson()) //
|
||||
.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) {
|
||||
|
||||
+23
-6
@@ -96,6 +96,8 @@ 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.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;
|
||||
@@ -324,9 +326,12 @@ public class SavingsConfiguration {
|
||||
public FixedDepositProductWritePlatformService fixedDepositProductWritePlatformService(PlatformSecurityContext context,
|
||||
FixedDepositProductRepository fixedDepositProductRepository, DepositProductDataValidator fromApiJsonDataValidator,
|
||||
DepositProductAssembler depositProductAssembler,
|
||||
ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService, InterestRateChartAssembler chartAssembler) {
|
||||
ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService, InterestRateChartAssembler chartAssembler,
|
||||
ProductPendingEditRepository productPendingEditRepository, FromJsonHelper fromApiJsonHelper,
|
||||
CommandSourceRepository commandSourceRepository) {
|
||||
return new FixedDepositProductWritePlatformServiceJpaRepositoryImpl(context, fixedDepositProductRepository,
|
||||
fromApiJsonDataValidator, depositProductAssembler, accountMappingWritePlatformService, chartAssembler);
|
||||
fromApiJsonDataValidator, depositProductAssembler, accountMappingWritePlatformService, chartAssembler,
|
||||
productPendingEditRepository, fromApiJsonHelper, commandSourceRepository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -348,9 +353,12 @@ public class SavingsConfiguration {
|
||||
public RecurringDepositProductWritePlatformService recurringDepositProductWritePlatformService(PlatformSecurityContext context,
|
||||
RecurringDepositProductRepository recurringDepositProductRepository, DepositProductDataValidator fromApiJsonDataValidator,
|
||||
DepositProductAssembler depositProductAssembler,
|
||||
ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService, InterestRateChartAssembler chartAssembler) {
|
||||
ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService, InterestRateChartAssembler chartAssembler,
|
||||
ProductPendingEditRepository productPendingEditRepository, FromJsonHelper fromApiJsonHelper,
|
||||
CommandSourceRepository commandSourceRepository) {
|
||||
return new RecurringDepositProductWritePlatformServiceJpaRepositoryImpl(context, recurringDepositProductRepository,
|
||||
fromApiJsonDataValidator, depositProductAssembler, accountMappingWritePlatformService, chartAssembler);
|
||||
fromApiJsonDataValidator, depositProductAssembler, accountMappingWritePlatformService, chartAssembler,
|
||||
productPendingEditRepository, fromApiJsonHelper, commandSourceRepository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -489,10 +497,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
|
||||
|
||||
+29
@@ -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);
|
||||
}
|
||||
}
|
||||
+29
@@ -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);
|
||||
}
|
||||
}
|
||||
+29
@@ -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);
|
||||
}
|
||||
}
|
||||
+53
@@ -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,9 +48,11 @@ 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;
|
||||
@@ -152,6 +156,55 @@ 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.maker_id as makerId, maker.username as makerUsername, "
|
||||
+ "spt.checker_id as checkerId, checker.username as checkerUsername, spt.created_on as createdOn, "
|
||||
+ "spt.checked_on as checkedOn, spt.status as status, spt.reject_reason as rejectReason, "
|
||||
+ "spt.proposed_payload_json as proposedPayloadJson "
|
||||
+ "from m_savings_product_temp spt "
|
||||
+ "join m_share_product sp on sp.id = spt.product_id "
|
||||
+ "join m_appuser maker on maker.id = spt.maker_id "
|
||||
+ "left join m_appuser checker on checker.id = spt.checker_id "
|
||||
+ "where spt.product_type = 'SHARE' ";
|
||||
|
||||
private static final RowMapper<SavingsProductTempData> PENDING_EDIT_MAPPER = (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 String proposedPayloadJson = rs.getString("proposedPayloadJson");
|
||||
if (proposedPayloadJson != null) {
|
||||
data.setProposedChanges(GSON.fromJson(proposedPayloadJson, new TypeToken<Map<String, Object>>() {}.getType()));
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
@Override
|
||||
public Collection<SavingsProductTempData> retrieveAllPendingEdits() {
|
||||
final String sql = "select " + PENDING_EDIT_SCHEMA + "and spt.status = 'PENDING' order by spt.created_on desc";
|
||||
return this.jdbcTemplate.query(sql, PENDING_EDIT_MAPPER);
|
||||
}
|
||||
|
||||
@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, PENDING_EDIT_MAPPER, new Object[] { productId }); // NOSONAR
|
||||
} catch (final EmptyResultDataAccessException e) {
|
||||
throw new ShareProductNoPendingEditException(productId);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class AllShareProductRowMapper implements RowMapper<ProductData> {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
|
||||
+17
@@ -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);
|
||||
}
|
||||
|
||||
+130
@@ -27,23 +27,35 @@ 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.useradministration.domain.AppUser;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.orm.jpa.JpaSystemException;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@@ -56,6 +68,9 @@ 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 CommandSourceRepository commandSourceRepository;
|
||||
|
||||
@Override
|
||||
public CommandProcessingResult createShareProduct(JsonCommand jsonCommand) {
|
||||
@@ -180,6 +195,121 @@ 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, command.json());
|
||||
|
||||
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);
|
||||
|
||||
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 JsonCommand replayedCommand = JsonCommand.fromJsonElement(productId,
|
||||
this.fromApiJsonHelper.parse(pendingEdit.getProposedPayloadJson()), 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 CommandSource auditEntry = CommandSource.builder() //
|
||||
.actionName("UPDATE") //
|
||||
.entityName("SHAREPRODUCT") //
|
||||
.resourceGetUrl("/products/share/" + productId) //
|
||||
.resourceId(productId) //
|
||||
.commandAsJson(pendingEdit.getProposedPayloadJson()) //
|
||||
.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",
|
||||
|
||||
+8
-2
@@ -24,11 +24,14 @@ 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.shareaccounts.service.ShareAccountReadPlatformService;
|
||||
import org.apache.fineract.portfolio.shareproducts.domain.ShareProductDividentPayOutDetailsRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.shareproducts.domain.ShareProductRepositoryWrapper;
|
||||
@@ -99,10 +102,13 @@ public class ShareProductsConfiguration {
|
||||
ShareProductDividentPayOutDetailsRepositoryWrapper shareProductDividentPayOutDetailsRepository,
|
||||
ShareProductDividendAssembler shareProductDividendAssembler,
|
||||
ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService,
|
||||
BusinessEventNotifierService businessEventNotifierService) {
|
||||
BusinessEventNotifierService businessEventNotifierService,
|
||||
PlatformSecurityContext context,
|
||||
ProductPendingEditRepository productPendingEditRepository,
|
||||
CommandSourceRepository commandSourceRepository) {
|
||||
return new ShareProductWritePlatformServiceJpaRepositoryImpl(repository, serializer, fromApiJsonHelper,
|
||||
shareProductDividentPayOutDetailsRepository, shareProductDividendAssembler, accountMappingWritePlatformService,
|
||||
businessEventNotifierService);
|
||||
businessEventNotifierService, context, productPendingEditRepository, commandSourceRepository);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
@@ -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; }
|
||||
|
||||
+2
@@ -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) {
|
||||
|
||||
+39
@@ -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;
|
||||
}
|
||||
+54
@@ -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;
|
||||
}
|
||||
+79
@@ -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;
|
||||
}
|
||||
+71
@@ -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;
|
||||
}
|
||||
+78
@@ -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;
|
||||
}
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* 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_savings_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 = "maker_id", nullable = false)
|
||||
private AppUser maker;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "checker_id")
|
||||
private AppUser checker;
|
||||
|
||||
@Column(name = "created_on", nullable = false)
|
||||
private LocalDateTime createdOn;
|
||||
|
||||
@Column(name = "checked_on")
|
||||
private LocalDateTime checkedOn;
|
||||
|
||||
@Column(name = "status", nullable = false, length = 20)
|
||||
private String status;
|
||||
|
||||
@Column(name = "reject_reason", length = 500)
|
||||
private String rejectReason;
|
||||
|
||||
@Lob
|
||||
@Column(name = "proposed_payload_json", nullable = false)
|
||||
private String proposedPayloadJson;
|
||||
|
||||
/**
|
||||
* "<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, final String proposedPayloadJson) {
|
||||
this.productType = productType;
|
||||
this.productId = productId;
|
||||
this.maker = maker;
|
||||
this.proposedPayloadJson = proposedPayloadJson;
|
||||
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 String getProposedPayloadJson() {
|
||||
return this.proposedPayloadJson;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
+50
@@ -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;
|
||||
}
|
||||
}
|
||||
+29
@@ -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);
|
||||
}
|
||||
+63
@@ -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;
|
||||
}
|
||||
}
|
||||
+29
@@ -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);
|
||||
}
|
||||
+696
@@ -0,0 +1,696 @@
|
||||
/**
|
||||
* 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.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");
|
||||
}
|
||||
return present;
|
||||
}
|
||||
|
||||
public static void writeCoreFieldsToJson(final JsonObject root, final SavingsCoreEditFields f, final Set<String> submitted) {
|
||||
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());
|
||||
}
|
||||
|
||||
// ---- 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 (!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 (!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) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+70
@@ -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;
|
||||
}
|
||||
}
|
||||
+29
@@ -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);
|
||||
}
|
||||
+66
@@ -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;
|
||||
}
|
||||
}
|
||||
+29
@@ -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);
|
||||
}
|
||||
+32
@@ -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);
|
||||
}
|
||||
+56
@@ -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;
|
||||
}
|
||||
}
|
||||
+34
@@ -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);
|
||||
}
|
||||
+39
@@ -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;
|
||||
}
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.savings.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Embeddable;
|
||||
import jakarta.persistence.Lob;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* Scalar edit fields shared by Savings, Current, Fixed Deposit and Recurring Deposit products
|
||||
* (FD/RD extend the Savings field set). Column names are the snake_case of the JSON API parameter
|
||||
* name each field stores, not the underlying product table's own column name.
|
||||
*/
|
||||
@Embeddable
|
||||
@Getter
|
||||
@Setter
|
||||
public class SavingsCoreEditFields {
|
||||
|
||||
@Column(name = "name")
|
||||
private String name;
|
||||
|
||||
@Column(name = "short_name")
|
||||
private String shortName;
|
||||
|
||||
@Column(name = "description")
|
||||
private String description;
|
||||
|
||||
@Column(name = "currency_code")
|
||||
private String currencyCode;
|
||||
|
||||
@Column(name = "digits_after_decimal")
|
||||
private Integer digitsAfterDecimal;
|
||||
|
||||
@Column(name = "in_multiples_of")
|
||||
private Integer inMultiplesOf;
|
||||
|
||||
@Column(name = "accounting_rule")
|
||||
private Integer accountingRule;
|
||||
|
||||
@Column(name = "nominal_annual_interest_rate")
|
||||
private BigDecimal nominalAnnualInterestRate;
|
||||
|
||||
@Column(name = "interest_compounding_period_type")
|
||||
private Integer interestCompoundingPeriodType;
|
||||
|
||||
@Column(name = "interest_posting_period_type")
|
||||
private Integer interestPostingPeriodType;
|
||||
|
||||
@Column(name = "interest_calculation_type")
|
||||
private Integer interestCalculationType;
|
||||
|
||||
@Column(name = "interest_calculation_days_in_year_type")
|
||||
private Integer interestCalculationDaysInYearType;
|
||||
|
||||
@Column(name = "min_required_opening_balance")
|
||||
private BigDecimal minRequiredOpeningBalance;
|
||||
|
||||
@Column(name = "lockin_period_frequency")
|
||||
private Integer lockinPeriodFrequency;
|
||||
|
||||
@Column(name = "lockin_period_frequency_type")
|
||||
private Integer lockinPeriodFrequencyType;
|
||||
|
||||
@Column(name = "withdrawal_fee_for_transfers")
|
||||
private Boolean withdrawalFeeForTransfers;
|
||||
|
||||
@Column(name = "allow_overdraft")
|
||||
private Boolean allowOverdraft;
|
||||
|
||||
@Column(name = "overdraft_limit")
|
||||
private BigDecimal overdraftLimit;
|
||||
|
||||
@Column(name = "nominal_annual_interest_rate_overdraft")
|
||||
private BigDecimal nominalAnnualInterestRateOverdraft;
|
||||
|
||||
@Column(name = "min_overdraft_for_interest_calculation")
|
||||
private BigDecimal minOverdraftForInterestCalculation;
|
||||
|
||||
@Column(name = "enforce_min_required_balance")
|
||||
private Boolean enforceMinRequiredBalance;
|
||||
|
||||
@Column(name = "min_required_balance")
|
||||
private BigDecimal minRequiredBalance;
|
||||
|
||||
@Column(name = "lien_allowed")
|
||||
private Boolean lienAllowed;
|
||||
|
||||
@Column(name = "max_allowed_lien_limit")
|
||||
private BigDecimal maxAllowedLienLimit;
|
||||
|
||||
@Column(name = "min_balance_for_interest_calculation")
|
||||
private BigDecimal minBalanceForInterestCalculation;
|
||||
|
||||
@Column(name = "with_hold_tax")
|
||||
private Boolean withHoldTax;
|
||||
|
||||
@Column(name = "tax_group_id")
|
||||
private Long taxGroupId;
|
||||
|
||||
@Lob
|
||||
@Column(name = "other_gl_mappings")
|
||||
private String otherGlMappings;
|
||||
|
||||
@Column(name = "is_dormancy_tracking_active")
|
||||
private Boolean isDormancyTrackingActive;
|
||||
|
||||
@Column(name = "days_to_inactive")
|
||||
private Integer daysToInactive;
|
||||
|
||||
@Column(name = "days_to_dormancy")
|
||||
private Integer daysToDormancy;
|
||||
|
||||
@Column(name = "days_to_escheat")
|
||||
private Integer daysToEscheat;
|
||||
|
||||
@Column(name = "account_type")
|
||||
private String accountType;
|
||||
|
||||
@Column(name = "customer_segment")
|
||||
private String customerSegment;
|
||||
|
||||
@Column(name = "product_code")
|
||||
private String productCode;
|
||||
|
||||
@Column(name = "gl_account_id")
|
||||
private Long glAccountId;
|
||||
|
||||
@Column(name = "effective_date")
|
||||
private LocalDate effectiveDate;
|
||||
|
||||
@Column(name = "effective_to")
|
||||
private LocalDate effectiveTo;
|
||||
|
||||
@Column(name = "end_date")
|
||||
private LocalDate endDate;
|
||||
|
||||
@Column(name = "introducer_required")
|
||||
private Boolean introducerRequired;
|
||||
|
||||
@Column(name = "share_linking_enabled")
|
||||
private Boolean shareLinkingEnabled;
|
||||
|
||||
@Column(name = "nomination_facility")
|
||||
private Boolean nominationFacility;
|
||||
|
||||
@Column(name = "dormancy_alert")
|
||||
private Boolean dormancyAlert;
|
||||
|
||||
@Column(name = "interest_calculate_selector")
|
||||
private Integer interestCalculateSelector;
|
||||
|
||||
@Column(name = "average_period")
|
||||
private String averagePeriod;
|
||||
|
||||
@Column(name = "auto_post")
|
||||
private Boolean autoPost;
|
||||
|
||||
@Column(name = "frequency_fix_day")
|
||||
private Integer frequencyFixDay;
|
||||
|
||||
@Column(name = "last_interest_date")
|
||||
private LocalDate lastInterestDate;
|
||||
|
||||
@Column(name = "enable_lockin_period")
|
||||
private Boolean enableLockinPeriod;
|
||||
|
||||
@Column(name = "balance_frequency")
|
||||
private String balanceFrequency;
|
||||
|
||||
@Column(name = "account_closed_zero_balance_in_day_closing")
|
||||
private Boolean accountClosedZeroBalanceInDayClosing;
|
||||
|
||||
@Column(name = "balance_swap_option")
|
||||
private Boolean balanceSwapOption;
|
||||
|
||||
@Column(name = "msme")
|
||||
private Boolean msme;
|
||||
|
||||
@Column(name = "auto_debit_facility")
|
||||
private Boolean autoDebitFacility;
|
||||
|
||||
@Column(name = "loan_facility")
|
||||
private Boolean loanFacility;
|
||||
|
||||
@Column(name = "premature_withdrawal_option")
|
||||
private Boolean prematureWithdrawalOption;
|
||||
|
||||
@Column(name = "tenure_flexibility")
|
||||
private Boolean tenureFlexibility;
|
||||
|
||||
@Column(name = "lien_marked")
|
||||
private Boolean lienMarked;
|
||||
|
||||
@Column(name = "maturity_instructions")
|
||||
private Boolean maturityInstructions;
|
||||
|
||||
@Column(name = "credit_maturity_proceeds")
|
||||
private Boolean creditMaturityProceeds;
|
||||
|
||||
@Column(name = "penalty_grace_period_missed_installments")
|
||||
private Boolean penaltyGracePeriodMissedInstallments;
|
||||
|
||||
@Column(name = "panel_request_applicable")
|
||||
private Boolean panelRequestApplicable;
|
||||
|
||||
@Column(name = "no_penalty_nominee_heir_closure")
|
||||
private Boolean noPenaltyNomineeHeirClosure;
|
||||
|
||||
@Column(name = "deceased_settlement")
|
||||
private Boolean deceasedSettlement;
|
||||
|
||||
@Column(name = "allow_debit_txn_after_due_date")
|
||||
private Boolean allowDebitTxnAfterDueDate;
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.savings.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Embeddable;
|
||||
import java.math.BigDecimal;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Embeddable
|
||||
@Getter
|
||||
@Setter
|
||||
public class ShareOnlyEditFields {
|
||||
|
||||
@Column(name = "total_shares")
|
||||
private Long totalShares;
|
||||
|
||||
@Column(name = "total_shares_issued")
|
||||
private Long totalSharesIssued;
|
||||
|
||||
@Column(name = "unit_price")
|
||||
private BigDecimal unitPrice;
|
||||
|
||||
@Column(name = "minimum_shares")
|
||||
private Long minimumShares;
|
||||
|
||||
@Column(name = "nominal_shares")
|
||||
private Long nominalShares;
|
||||
|
||||
@Column(name = "maximum_shares")
|
||||
private Long maximumShares;
|
||||
|
||||
@Column(name = "allow_dividend_calc_for_inactive_clients")
|
||||
private Boolean allowDividendCalcForInactiveClients;
|
||||
|
||||
@Column(name = "minimum_active_period_for_dividends")
|
||||
private Integer minimumActivePeriodForDividends;
|
||||
|
||||
@Column(name = "minimum_active_period_frequency_type")
|
||||
private Integer minimumActivePeriodFrequencyType;
|
||||
|
||||
@Column(name = "share_lockin_period")
|
||||
private Integer shareLockinPeriod;
|
||||
|
||||
@Column(name = "share_lockin_period_frequency_type")
|
||||
private Integer shareLockinPeriodFrequencyType;
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.savings.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
public class SavingsProductEditPendingApprovalException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public SavingsProductEditPendingApprovalException(final Long productId) {
|
||||
super("error.msg.savingsproduct.edit.pending.approval",
|
||||
"Savings product with identifier " + productId + " already has an edit pending approval", productId);
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.savings.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
public class SavingsProductNoPendingEditException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public SavingsProductNoPendingEditException(final Long productId) {
|
||||
super("error.msg.savingsproduct.no.pending.edit",
|
||||
"Savings product with identifier " + productId + " has no edit pending approval", productId);
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.savings.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
public class SavingsProductSelfApprovalNotAllowedException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public SavingsProductSelfApprovalNotAllowedException(final Long productId) {
|
||||
super("error.msg.savingsproduct.self.approval.not.allowed",
|
||||
"The maker of the pending edit for savings product " + productId + " cannot also approve or reject it", productId);
|
||||
}
|
||||
}
|
||||
+5
@@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.savings.service;
|
||||
import java.util.Collection;
|
||||
import org.apache.fineract.portfolio.savings.DepositAccountType;
|
||||
import org.apache.fineract.portfolio.savings.data.DepositProductData;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsProductTempData;
|
||||
|
||||
public interface DepositProductReadPlatformService {
|
||||
|
||||
@@ -32,4 +33,8 @@ public interface DepositProductReadPlatformService {
|
||||
|
||||
DepositProductData retrieveOneWithChartSlabs(DepositAccountType depositAccountType, Long productId);
|
||||
|
||||
Collection<SavingsProductTempData> retrieveAllPendingEdits(DepositAccountType depositAccountType);
|
||||
|
||||
SavingsProductTempData retrievePendingEdit(DepositAccountType depositAccountType, Long productId);
|
||||
|
||||
}
|
||||
|
||||
+6
@@ -28,4 +28,10 @@ public interface FixedDepositProductWritePlatformService {
|
||||
CommandProcessingResult update(Long productId, JsonCommand command);
|
||||
|
||||
CommandProcessingResult delete(Long productId);
|
||||
|
||||
CommandProcessingResult submitEditForApproval(Long productId, JsonCommand command);
|
||||
|
||||
CommandProcessingResult approveEdit(Long productId);
|
||||
|
||||
CommandProcessingResult rejectEdit(Long productId, String rejectReason);
|
||||
}
|
||||
|
||||
+6
@@ -28,4 +28,10 @@ public interface RecurringDepositProductWritePlatformService {
|
||||
CommandProcessingResult update(Long productId, JsonCommand command);
|
||||
|
||||
CommandProcessingResult delete(Long productId);
|
||||
|
||||
CommandProcessingResult submitEditForApproval(Long productId, JsonCommand command);
|
||||
|
||||
CommandProcessingResult approveEdit(Long productId);
|
||||
|
||||
CommandProcessingResult rejectEdit(Long productId, String rejectReason);
|
||||
}
|
||||
|
||||
+5
@@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.savings.service;
|
||||
|
||||
import java.util.Collection;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsProductData;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsProductTempData;
|
||||
|
||||
public interface SavingsProductReadPlatformService {
|
||||
|
||||
@@ -33,4 +34,8 @@ public interface SavingsProductReadPlatformService {
|
||||
|
||||
SavingsProductData retrieveOne(Long productId);
|
||||
|
||||
Collection<SavingsProductTempData> retrieveAllPendingEdits();
|
||||
|
||||
SavingsProductTempData retrievePendingEdit(Long productId);
|
||||
|
||||
}
|
||||
|
||||
+17
@@ -28,4 +28,21 @@ public interface SavingsProductWritePlatformService {
|
||||
CommandProcessingResult update(Long productId, JsonCommand command);
|
||||
|
||||
CommandProcessingResult delete(Long productId);
|
||||
|
||||
/**
|
||||
* Submits an edit for approval instead of applying it directly. Fails if a pending edit already exists for this
|
||||
* product (that pending row is itself the lock).
|
||||
*/
|
||||
CommandProcessingResult submitEditForApproval(Long productId, JsonCommand command);
|
||||
|
||||
/**
|
||||
* Replays the stored pending edit through the normal update path and marks it approved. Fails if the approving
|
||||
* user is the same user who submitted the edit.
|
||||
*/
|
||||
CommandProcessingResult approveEdit(Long productId);
|
||||
|
||||
/**
|
||||
* Discards the pending edit. Fails if the rejecting user is the same user who submitted the edit.
|
||||
*/
|
||||
CommandProcessingResult rejectEdit(Long productId, String rejectReason);
|
||||
}
|
||||
|
||||
+10
@@ -39,4 +39,14 @@
|
||||
<include file="parts/2014_add_is_mandatory_to_joint_holders.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/2015_add_senior_citizen_to_savings_account.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/2016_add_share_linking_enabled_to_savings_account.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/2017_add_saving_product_temp_table.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/2018_fix_saving_product_temp_lock_constraint.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/2019_add_command_source_link_to_saving_product_temp.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/2020_generalize_product_temp_table.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/2021_add_fd_rd_approval_permissions.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/2022_add_product_checker_permissions.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/2023_fix_product_approval_permissions.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/2025_recreate_savings_product_temp_table.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/2026_add_pending_edit_typed_columns.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/2027_add_pending_edit_satellite_tables.xml" relativeToChangelogFile="true" />
|
||||
</databaseChangeLog>
|
||||
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
<?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 author="fineract" id="2017-1">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<tableExists tableName="m_savings_product_temp" />
|
||||
</not>
|
||||
</preConditions>
|
||||
<createTable tableName="m_savings_product_temp">
|
||||
<column name="id" type="BIGINT" autoIncrement="true">
|
||||
<constraints primaryKey="true" nullable="false" />
|
||||
</column>
|
||||
<column name="savings_product_id" type="BIGINT">
|
||||
<constraints nullable="false" unique="true" uniqueConstraintName="sp_temp_unq_savings_product_id"
|
||||
foreignKeyName="fk_sp_temp_savings_product_id" references="m_savings_product(id)" />
|
||||
</column>
|
||||
<column name="maker_id" type="BIGINT">
|
||||
<constraints nullable="false" foreignKeyName="fk_sp_temp_maker_id" references="m_appuser(id)" />
|
||||
</column>
|
||||
<column name="checker_id" type="BIGINT">
|
||||
<constraints nullable="true" foreignKeyName="fk_sp_temp_checker_id" references="m_appuser(id)" />
|
||||
</column>
|
||||
<column name="created_on" type="DATETIME">
|
||||
<constraints nullable="false" />
|
||||
</column>
|
||||
<column name="checked_on" type="DATETIME">
|
||||
<constraints nullable="true" />
|
||||
</column>
|
||||
<column name="status" type="VARCHAR(20)" defaultValue="PENDING">
|
||||
<constraints nullable="false" />
|
||||
</column>
|
||||
<column name="reject_reason" type="VARCHAR(500)">
|
||||
<constraints nullable="true" />
|
||||
</column>
|
||||
<column name="proposed_payload_json" type="CLOB">
|
||||
<constraints nullable="false" />
|
||||
</column>
|
||||
</createTable>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="2017-2">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'APPROVE_SAVINGSPRODUCT'</sqlCheck>
|
||||
</not>
|
||||
</preConditions>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="organisation" />
|
||||
<column name="code" value="APPROVE_SAVINGSPRODUCT" />
|
||||
<column name="entity_name" value="SAVINGSPRODUCT" />
|
||||
<column name="action_name" value="APPROVE" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="organisation" />
|
||||
<column name="code" value="REJECT_SAVINGSPRODUCT" />
|
||||
<column name="entity_name" value="SAVINGSPRODUCT" />
|
||||
<column name="action_name" value="REJECT" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
<?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">
|
||||
|
||||
<!--
|
||||
The original unique constraint on savings_product_id blocked ANY future edit on a product forever, once a
|
||||
single pending edit had been resolved - because resolved rows are kept (not deleted) for audit history.
|
||||
The lock must only apply while status = PENDING. Unique constraints treat NULL as distinct in both
|
||||
PostgreSQL and MySQL/MariaDB, so a nullable "lock key" column - set to the product id while pending, and
|
||||
cleared back to NULL on approve/reject - gives a correct, cross-database concurrency-safe lock.
|
||||
-->
|
||||
<changeSet author="fineract" id="2018-1">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<columnExists tableName="m_savings_product_temp" columnName="savings_product_id" />
|
||||
<not>
|
||||
<columnExists tableName="m_savings_product_temp" columnName="pending_lock_key" />
|
||||
</not>
|
||||
</preConditions>
|
||||
|
||||
<dropUniqueConstraint tableName="m_savings_product_temp" constraintName="sp_temp_unq_savings_product_id" />
|
||||
|
||||
<addColumn tableName="m_savings_product_temp">
|
||||
<column name="pending_lock_key" type="BIGINT">
|
||||
<constraints nullable="true" />
|
||||
</column>
|
||||
</addColumn>
|
||||
|
||||
<sql>UPDATE m_savings_product_temp SET pending_lock_key = savings_product_id WHERE status = 'PENDING'</sql>
|
||||
|
||||
<addUniqueConstraint tableName="m_savings_product_temp" columnNames="pending_lock_key"
|
||||
constraintName="sp_temp_unq_pending_lock_key" />
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
<?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">
|
||||
|
||||
<!--
|
||||
Links each pending edit to the audit-only m_portfolio_command_source row created for it, so that
|
||||
submit/approve/reject also appear in System -> Audit Trails even though this feature bypasses the native
|
||||
maker-checker execution pipeline.
|
||||
-->
|
||||
<changeSet author="fineract" id="2019-1">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<columnExists tableName="m_savings_product_temp" columnName="command_source_id" />
|
||||
</not>
|
||||
</preConditions>
|
||||
<addColumn tableName="m_savings_product_temp">
|
||||
<column name="command_source_id" type="BIGINT">
|
||||
<constraints nullable="true" foreignKeyName="fk_sp_temp_command_source_id"
|
||||
references="m_portfolio_command_source(id)" />
|
||||
</column>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
<?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">
|
||||
|
||||
<!--
|
||||
Generalizes m_savings_product_temp (kept as one shared table, per requirement) so it can hold pending edits
|
||||
for Current Account Products (m_current_product) and Share Products (m_share_product) as well as Savings
|
||||
Products (m_savings_product) - not just savings. A single BIGINT "product_id" can't carry a normal FK
|
||||
anymore since it now points at three different tables depending on product_type, so the old FK is dropped
|
||||
(application code, not the DB, is now responsible for referential validity here).
|
||||
|
||||
pending_lock_key changes from BIGINT to VARCHAR because a raw numeric id is no longer unique across product
|
||||
types (e.g. a current account product id=3 and a savings product id=3 must not collide). The new format is
|
||||
"<PRODUCT_TYPE>:<product_id>", e.g. "SAVINGS:3" / "CURRENT:3" / "SHARE:3".
|
||||
-->
|
||||
<changeSet author="fineract" id="2020-1">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<columnExists tableName="m_savings_product_temp" columnName="product_type" />
|
||||
</not>
|
||||
</preConditions>
|
||||
|
||||
<addColumn tableName="m_savings_product_temp">
|
||||
<column name="product_type" type="VARCHAR(20)" defaultValue="SAVINGS">
|
||||
<constraints nullable="false" />
|
||||
</column>
|
||||
</addColumn>
|
||||
|
||||
<dropForeignKeyConstraint baseTableName="m_savings_product_temp" constraintName="fk_sp_temp_savings_product_id" />
|
||||
|
||||
<renameColumn tableName="m_savings_product_temp" oldColumnName="savings_product_id" newColumnName="product_id"
|
||||
columnDataType="BIGINT" />
|
||||
|
||||
<dropUniqueConstraint tableName="m_savings_product_temp" constraintName="sp_temp_unq_pending_lock_key" />
|
||||
|
||||
<addColumn tableName="m_savings_product_temp">
|
||||
<column name="pending_lock_key_new" type="VARCHAR(40)">
|
||||
<constraints nullable="true" />
|
||||
</column>
|
||||
</addColumn>
|
||||
<sql dbms="postgresql">UPDATE m_savings_product_temp SET pending_lock_key_new = 'SAVINGS:' || product_id WHERE status = 'PENDING'</sql>
|
||||
<sql dbms="mysql,mariadb">UPDATE m_savings_product_temp SET pending_lock_key_new = CONCAT('SAVINGS:', product_id) WHERE status = 'PENDING'</sql>
|
||||
<dropColumn tableName="m_savings_product_temp" columnName="pending_lock_key" />
|
||||
<renameColumn tableName="m_savings_product_temp" oldColumnName="pending_lock_key_new" newColumnName="pending_lock_key"
|
||||
columnDataType="VARCHAR(40)" />
|
||||
|
||||
<addUniqueConstraint tableName="m_savings_product_temp" columnNames="pending_lock_key"
|
||||
constraintName="sp_temp_unq_pending_lock_key" />
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="2020-2">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'APPROVE_CURRENTPRODUCT'</sqlCheck>
|
||||
</not>
|
||||
</preConditions>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="organisation" />
|
||||
<column name="code" value="APPROVE_CURRENTPRODUCT" />
|
||||
<column name="entity_name" value="CURRENTPRODUCT" />
|
||||
<column name="action_name" value="APPROVE" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="organisation" />
|
||||
<column name="code" value="REJECT_CURRENTPRODUCT" />
|
||||
<column name="entity_name" value="CURRENTPRODUCT" />
|
||||
<column name="action_name" value="REJECT" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="organisation" />
|
||||
<column name="code" value="APPROVE_SHAREPRODUCT" />
|
||||
<column name="entity_name" value="SHAREPRODUCT" />
|
||||
<column name="action_name" value="APPROVE" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="organisation" />
|
||||
<column name="code" value="REJECT_SHAREPRODUCT" />
|
||||
<column name="entity_name" value="SHAREPRODUCT" />
|
||||
<column name="action_name" value="REJECT" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
<?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 author="fineract" id="2021-1">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'APPROVE_FIXEDDEPOSITPRODUCT'</sqlCheck>
|
||||
</not>
|
||||
</preConditions>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="organisation" />
|
||||
<column name="code" value="APPROVE_FIXEDDEPOSITPRODUCT" />
|
||||
<column name="entity_name" value="FIXEDDEPOSITPRODUCT" />
|
||||
<column name="action_name" value="APPROVE" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="organisation" />
|
||||
<column name="code" value="REJECT_FIXEDDEPOSITPRODUCT" />
|
||||
<column name="entity_name" value="FIXEDDEPOSITPRODUCT" />
|
||||
<column name="action_name" value="REJECT" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="organisation" />
|
||||
<column name="code" value="APPROVE_RECURRINGDEPOSITPRODUCT" />
|
||||
<column name="entity_name" value="RECURRINGDEPOSITPRODUCT" />
|
||||
<column name="action_name" value="APPROVE" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="organisation" />
|
||||
<column name="code" value="REJECT_RECURRINGDEPOSITPRODUCT" />
|
||||
<column name="entity_name" value="RECURRINGDEPOSITPRODUCT" />
|
||||
<column name="action_name" value="REJECT" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
<?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">
|
||||
|
||||
<!--
|
||||
AuditReadPlatformServiceImpl.retrieveAllEntriesToBeChecked() (backing GET /v1/makercheckers, the "Checker
|
||||
Inbox" tab) shows a pending m_portfolio_command_source row to a "limited" checker only if their role holds
|
||||
a permission whose action_name is "<ACTION>_CHECKER" and entity_name matches the row's entity_name (code
|
||||
convention: "<ACTION>_<ENTITY>_CHECKER", matching how core Fineract permissions like UPDATE_ROLE_CHECKER
|
||||
are seeded). Our custom pending-approval CommandSource rows for product edits (SAVINGSPRODUCT,
|
||||
CURRENTPRODUCT, SHAREPRODUCT, FIXEDDEPOSITPRODUCT, RECURRINGDEPOSITPRODUCT) already satisfy the base
|
||||
status=AWAITING_APPROVAL filter, but without these permission rows a limited checker's role has nothing to
|
||||
match against, so the rows never show up in Checker Inbox - only in our own Pending Product Edits screen.
|
||||
|
||||
UPDATE_SAVINGSPRODUCT_CHECKER already exists as a native Fineract permission, so each row below is its own
|
||||
changeset with its own precondition - one already-existing code must not block inserting the others.
|
||||
-->
|
||||
<changeSet author="fineract" id="2022-1">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'UPDATE_SAVINGSPRODUCT_CHECKER'</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="organisation" />
|
||||
<column name="code" value="UPDATE_SAVINGSPRODUCT_CHECKER" />
|
||||
<column name="entity_name" value="SAVINGSPRODUCT" />
|
||||
<column name="action_name" value="UPDATE_CHECKER" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="2022-2">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'UPDATE_CURRENTPRODUCT_CHECKER'</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="organisation" />
|
||||
<column name="code" value="UPDATE_CURRENTPRODUCT_CHECKER" />
|
||||
<column name="entity_name" value="CURRENTPRODUCT" />
|
||||
<column name="action_name" value="UPDATE_CHECKER" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="2022-3">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'UPDATE_SHAREPRODUCT_CHECKER'</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="organisation" />
|
||||
<column name="code" value="UPDATE_SHAREPRODUCT_CHECKER" />
|
||||
<column name="entity_name" value="SHAREPRODUCT" />
|
||||
<column name="action_name" value="UPDATE_CHECKER" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="2022-4">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'UPDATE_FIXEDDEPOSITPRODUCT_CHECKER'</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="organisation" />
|
||||
<column name="code" value="UPDATE_FIXEDDEPOSITPRODUCT_CHECKER" />
|
||||
<column name="entity_name" value="FIXEDDEPOSITPRODUCT" />
|
||||
<column name="action_name" value="UPDATE_CHECKER" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="2022-5">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'UPDATE_RECURRINGDEPOSITPRODUCT_CHECKER'</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="organisation" />
|
||||
<column name="code" value="UPDATE_RECURRINGDEPOSITPRODUCT_CHECKER" />
|
||||
<column name="entity_name" value="RECURRINGDEPOSITPRODUCT" />
|
||||
<column name="action_name" value="UPDATE_CHECKER" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
<?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">
|
||||
|
||||
<!--
|
||||
Changesets 2017-2, 2020-2 and 2021-1 used an inverted precondition - `<not><sqlCheck expectedResult="0">...`
|
||||
actually means "run only if the row ALREADY exists", the opposite of the intended idempotent-insert guard.
|
||||
On a fresh database (count=0) that precondition evaluates to false, so onFail="MARK_RAN" silently skipped
|
||||
the insert entirely - the APPROVE_*/REJECT_* permissions for all five product types were marked as applied
|
||||
in databasechangelog but never actually inserted. Those changesets are already recorded and must not be
|
||||
edited (Liquibase would fail on checksum mismatch), so this migration re-inserts the missing rows under
|
||||
fresh changeset ids, each with a correct (non-inverted) precondition and split per row so one already-
|
||||
existing code can never block the others.
|
||||
-->
|
||||
<changeSet author="fineract" id="2023-1">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'APPROVE_SAVINGSPRODUCT'</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="organisation" />
|
||||
<column name="code" value="APPROVE_SAVINGSPRODUCT" />
|
||||
<column name="entity_name" value="SAVINGSPRODUCT" />
|
||||
<column name="action_name" value="APPROVE" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="2023-2">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'REJECT_SAVINGSPRODUCT'</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="organisation" />
|
||||
<column name="code" value="REJECT_SAVINGSPRODUCT" />
|
||||
<column name="entity_name" value="SAVINGSPRODUCT" />
|
||||
<column name="action_name" value="REJECT" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="2023-3">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'APPROVE_CURRENTPRODUCT'</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="organisation" />
|
||||
<column name="code" value="APPROVE_CURRENTPRODUCT" />
|
||||
<column name="entity_name" value="CURRENTPRODUCT" />
|
||||
<column name="action_name" value="APPROVE" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="2023-4">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'REJECT_CURRENTPRODUCT'</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="organisation" />
|
||||
<column name="code" value="REJECT_CURRENTPRODUCT" />
|
||||
<column name="entity_name" value="CURRENTPRODUCT" />
|
||||
<column name="action_name" value="REJECT" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="2023-5">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'APPROVE_SHAREPRODUCT'</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="organisation" />
|
||||
<column name="code" value="APPROVE_SHAREPRODUCT" />
|
||||
<column name="entity_name" value="SHAREPRODUCT" />
|
||||
<column name="action_name" value="APPROVE" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="2023-6">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'REJECT_SHAREPRODUCT'</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="organisation" />
|
||||
<column name="code" value="REJECT_SHAREPRODUCT" />
|
||||
<column name="entity_name" value="SHAREPRODUCT" />
|
||||
<column name="action_name" value="REJECT" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="2023-7">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'APPROVE_FIXEDDEPOSITPRODUCT'</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="organisation" />
|
||||
<column name="code" value="APPROVE_FIXEDDEPOSITPRODUCT" />
|
||||
<column name="entity_name" value="FIXEDDEPOSITPRODUCT" />
|
||||
<column name="action_name" value="APPROVE" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="2023-8">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'REJECT_FIXEDDEPOSITPRODUCT'</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="organisation" />
|
||||
<column name="code" value="REJECT_FIXEDDEPOSITPRODUCT" />
|
||||
<column name="entity_name" value="FIXEDDEPOSITPRODUCT" />
|
||||
<column name="action_name" value="REJECT" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="2023-9">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'APPROVE_RECURRINGDEPOSITPRODUCT'</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="organisation" />
|
||||
<column name="code" value="APPROVE_RECURRINGDEPOSITPRODUCT" />
|
||||
<column name="entity_name" value="RECURRINGDEPOSITPRODUCT" />
|
||||
<column name="action_name" value="APPROVE" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="2023-10">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'REJECT_RECURRINGDEPOSITPRODUCT'</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="organisation" />
|
||||
<column name="code" value="REJECT_RECURRINGDEPOSITPRODUCT" />
|
||||
<column name="entity_name" value="RECURRINGDEPOSITPRODUCT" />
|
||||
<column name="action_name" value="REJECT" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
<?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">
|
||||
|
||||
<!--
|
||||
m_savings_product_temp was dropped by an experimental changeset that was later removed from source without
|
||||
restoring the table. Recreates it with the final shape from the 2017/2018/2019/2020 migrations (guarded so
|
||||
it's a no-op wherever the table already exists) so ProductPendingEditRepository and the isPendingApproval
|
||||
subqueries in the product read services stop failing with "relation does not exist".
|
||||
-->
|
||||
<changeSet author="fineract" id="2025-1">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<tableExists tableName="m_savings_product_temp" />
|
||||
</not>
|
||||
</preConditions>
|
||||
<createTable tableName="m_savings_product_temp">
|
||||
<column name="id" type="BIGINT" autoIncrement="true">
|
||||
<constraints primaryKey="true" nullable="false" />
|
||||
</column>
|
||||
<column name="product_id" type="BIGINT">
|
||||
<constraints nullable="false" />
|
||||
</column>
|
||||
<column name="product_type" type="VARCHAR(20)" defaultValue="SAVINGS">
|
||||
<constraints nullable="false" />
|
||||
</column>
|
||||
<column name="maker_id" type="BIGINT">
|
||||
<constraints nullable="false" foreignKeyName="fk_sp_temp_maker_id" references="m_appuser(id)" />
|
||||
</column>
|
||||
<column name="checker_id" type="BIGINT">
|
||||
<constraints nullable="true" foreignKeyName="fk_sp_temp_checker_id" references="m_appuser(id)" />
|
||||
</column>
|
||||
<column name="created_on" type="DATETIME">
|
||||
<constraints nullable="false" />
|
||||
</column>
|
||||
<column name="checked_on" type="DATETIME">
|
||||
<constraints nullable="true" />
|
||||
</column>
|
||||
<column name="status" type="VARCHAR(20)" defaultValue="PENDING">
|
||||
<constraints nullable="false" />
|
||||
</column>
|
||||
<column name="reject_reason" type="VARCHAR(500)">
|
||||
<constraints nullable="true" />
|
||||
</column>
|
||||
<column name="proposed_payload_json" type="CLOB">
|
||||
<constraints nullable="false" />
|
||||
</column>
|
||||
<column name="pending_lock_key" type="VARCHAR(40)">
|
||||
<constraints nullable="true" unique="true" uniqueConstraintName="sp_temp_unq_pending_lock_key" />
|
||||
</column>
|
||||
<column name="command_source_id" type="BIGINT">
|
||||
<constraints nullable="true" foreignKeyName="fk_sp_temp_command_source_id"
|
||||
references="m_portfolio_command_source(id)" />
|
||||
</column>
|
||||
</createTable>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
<?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">
|
||||
|
||||
<!--
|
||||
Adds typed columns to m_savings_product_temp so a pending product edit is stored as real,
|
||||
individually-inspectable columns instead of only the opaque proposed_payload_json blob.
|
||||
proposed_payload_json is kept (still written) for a safe deprecation window; nothing here
|
||||
removes or alters any existing column. Every changeset is purely additive (addColumn only)
|
||||
and guarded with a correctly-shaped precondition (<not><columnExists .../></not> - a
|
||||
boolean precondition, unlike the historical bug in 2017-2/2020-2 which wrapped a numeric
|
||||
<sqlCheck expectedResult="0"> in <not>, inverting its meaning).
|
||||
|
||||
Columns are grouped by which product type(s) apply them; unrelated columns are simply NULL
|
||||
for a given row's product_type. Names are the snake_case of the JSON API parameter name the
|
||||
column stores, not the source product table's own column name, since this table stores
|
||||
proposed *edits* (parameter values), not a mirror of product-table storage.
|
||||
-->
|
||||
|
||||
<!-- Group 1: fields shared across Savings, Current, FD and RD (FD/RD extend Savings) -->
|
||||
<changeSet author="fineract" id="2026-1">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<columnExists tableName="m_savings_product_temp" columnName="name" />
|
||||
</not>
|
||||
</preConditions>
|
||||
<addColumn tableName="m_savings_product_temp">
|
||||
<column name="name" type="VARCHAR(100)" />
|
||||
<column name="short_name" type="VARCHAR(20)" />
|
||||
<column name="description" type="VARCHAR(500)" />
|
||||
<column name="currency_code" type="VARCHAR(3)" />
|
||||
<column name="digits_after_decimal" type="INTEGER" />
|
||||
<column name="in_multiples_of" type="INTEGER" />
|
||||
<column name="accounting_rule" type="INTEGER" />
|
||||
|
||||
<column name="nominal_annual_interest_rate" type="DECIMAL(19,6)" />
|
||||
<column name="interest_compounding_period_type" type="INTEGER" />
|
||||
<column name="interest_posting_period_type" type="INTEGER" />
|
||||
<column name="interest_calculation_type" type="INTEGER" />
|
||||
<column name="interest_calculation_days_in_year_type" type="INTEGER" />
|
||||
<column name="min_required_opening_balance" type="DECIMAL(19,6)" />
|
||||
<column name="lockin_period_frequency" type="INTEGER" />
|
||||
<column name="lockin_period_frequency_type" type="INTEGER" />
|
||||
<column name="withdrawal_fee_for_transfers" type="BOOLEAN" />
|
||||
<column name="allow_overdraft" type="BOOLEAN" />
|
||||
<column name="overdraft_limit" type="DECIMAL(19,6)" />
|
||||
<column name="nominal_annual_interest_rate_overdraft" type="DECIMAL(19,6)" />
|
||||
<column name="min_overdraft_for_interest_calculation" type="DECIMAL(19,6)" />
|
||||
<column name="enforce_min_required_balance" type="BOOLEAN" />
|
||||
<column name="min_required_balance" type="DECIMAL(19,6)" />
|
||||
<column name="lien_allowed" type="BOOLEAN" />
|
||||
<column name="max_allowed_lien_limit" type="DECIMAL(19,6)" />
|
||||
<column name="min_balance_for_interest_calculation" type="DECIMAL(19,6)" />
|
||||
<column name="with_hold_tax" type="BOOLEAN" />
|
||||
<column name="tax_group_id" type="BIGINT" />
|
||||
<column name="other_gl_mappings" type="CLOB" />
|
||||
<column name="is_dormancy_tracking_active" type="BOOLEAN" />
|
||||
<column name="days_to_inactive" type="INTEGER" />
|
||||
<column name="days_to_dormancy" type="INTEGER" />
|
||||
<column name="days_to_escheat" type="INTEGER" />
|
||||
<column name="account_type" type="VARCHAR(50)" />
|
||||
<column name="customer_segment" type="VARCHAR(50)" />
|
||||
<column name="product_code" type="VARCHAR(20)" />
|
||||
<column name="gl_account_id" type="BIGINT" />
|
||||
<column name="effective_date" type="DATE" />
|
||||
<column name="effective_to" type="DATE" />
|
||||
<column name="end_date" type="DATE" />
|
||||
<column name="introducer_required" type="BOOLEAN" />
|
||||
<column name="share_linking_enabled" type="BOOLEAN" />
|
||||
<column name="nomination_facility" type="BOOLEAN" />
|
||||
<column name="dormancy_alert" type="BOOLEAN" />
|
||||
<column name="interest_calculate_selector" type="INTEGER" />
|
||||
<column name="average_period" type="VARCHAR(20)" />
|
||||
<column name="auto_post" type="BOOLEAN" />
|
||||
<column name="frequency_fix_day" type="INTEGER" />
|
||||
<column name="last_interest_date" type="DATE" />
|
||||
<column name="enable_lockin_period" type="BOOLEAN" />
|
||||
<column name="balance_frequency" type="VARCHAR(50)" />
|
||||
<column name="account_closed_zero_balance_in_day_closing" type="BOOLEAN" />
|
||||
<column name="balance_swap_option" type="BOOLEAN" />
|
||||
<column name="msme" type="BOOLEAN" />
|
||||
<column name="auto_debit_facility" type="BOOLEAN" />
|
||||
<column name="loan_facility" type="BOOLEAN" />
|
||||
<column name="premature_withdrawal_option" type="BOOLEAN" />
|
||||
<column name="tenure_flexibility" type="BOOLEAN" />
|
||||
<column name="lien_marked" type="BOOLEAN" />
|
||||
<column name="maturity_instructions" type="BOOLEAN" />
|
||||
<column name="credit_maturity_proceeds" type="BOOLEAN" />
|
||||
<column name="penalty_grace_period_missed_installments" type="BOOLEAN" />
|
||||
<column name="panel_request_applicable" type="BOOLEAN" />
|
||||
<column name="no_penalty_nominee_heir_closure" type="BOOLEAN" />
|
||||
<column name="deceased_settlement" type="BOOLEAN" />
|
||||
<column name="allow_debit_txn_after_due_date" type="BOOLEAN" />
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
<!-- Group 2: Current-product-only fields -->
|
||||
<changeSet author="fineract" id="2026-2">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<columnExists tableName="m_savings_product_temp" columnName="avg_balance_calc_period" />
|
||||
</not>
|
||||
</preConditions>
|
||||
<addColumn tableName="m_savings_product_temp">
|
||||
<column name="avg_balance_calc_period" type="VARCHAR(10)" />
|
||||
<column name="valid_for_period" type="VARCHAR(10)" />
|
||||
<column name="od_account_closed_zero_balance_in_day_closing" type="BOOLEAN" />
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
<!-- Group 3: Share-product-only fields -->
|
||||
<changeSet author="fineract" id="2026-3">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<columnExists tableName="m_savings_product_temp" columnName="total_shares" />
|
||||
</not>
|
||||
</preConditions>
|
||||
<addColumn tableName="m_savings_product_temp">
|
||||
<column name="total_shares" type="BIGINT" />
|
||||
<column name="total_shares_issued" type="BIGINT" />
|
||||
<column name="unit_price" type="DECIMAL(19,6)" />
|
||||
<column name="minimum_shares" type="BIGINT" />
|
||||
<column name="nominal_shares" type="BIGINT" />
|
||||
<column name="maximum_shares" type="BIGINT" />
|
||||
<column name="allow_dividend_calc_for_inactive_clients" type="BOOLEAN" />
|
||||
<column name="minimum_active_period_for_dividends" type="INTEGER" />
|
||||
<column name="minimum_active_period_frequency_type" type="INTEGER" />
|
||||
<column name="share_lockin_period" type="INTEGER" />
|
||||
<column name="share_lockin_period_frequency_type" type="INTEGER" />
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
<!-- Group 4: Fixed-Deposit-only fields (also used by Recurring Deposit, which extends FD) -->
|
||||
<changeSet author="fineract" id="2026-4">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<columnExists tableName="m_savings_product_temp" columnName="fd_type" />
|
||||
</not>
|
||||
</preConditions>
|
||||
<addColumn tableName="m_savings_product_temp">
|
||||
<column name="fd_type" type="VARCHAR(20)" />
|
||||
<column name="pre_closure_penal_applicable" type="BOOLEAN" />
|
||||
<column name="pre_closure_penal_interest" type="DECIMAL(19,6)" />
|
||||
<column name="pre_closure_penal_interest_on_type_id" type="INTEGER" />
|
||||
<column name="min_deposit_term" type="INTEGER" />
|
||||
<column name="max_deposit_term" type="INTEGER" />
|
||||
<column name="min_deposit_term_type_id" type="INTEGER" />
|
||||
<column name="max_deposit_term_type_id" type="INTEGER" />
|
||||
<column name="in_multiples_of_deposit_term" type="INTEGER" />
|
||||
<column name="in_multiples_of_deposit_term_type_id" type="INTEGER" />
|
||||
<column name="min_deposit_amount" type="DECIMAL(19,6)" />
|
||||
<column name="max_deposit_amount" type="DECIMAL(19,6)" />
|
||||
<column name="deposit_amount" type="DECIMAL(19,6)" />
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
<!-- Group 5: Recurring-Deposit-only fields -->
|
||||
<changeSet author="fineract" id="2026-5">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<columnExists tableName="m_savings_product_temp" columnName="is_mandatory_deposit" />
|
||||
</not>
|
||||
</preConditions>
|
||||
<addColumn tableName="m_savings_product_temp">
|
||||
<column name="is_mandatory_deposit" type="BOOLEAN" />
|
||||
<column name="allow_withdrawal" type="BOOLEAN" />
|
||||
<column name="adjust_advance_towards_future_payments" type="BOOLEAN" />
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
<!-- Group 6: accountFacilityConfig (1:1 today - kept as flat columns, fc_ prefix) -->
|
||||
<changeSet author="fineract" id="2026-6">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<columnExists tableName="m_savings_product_temp" columnName="fc_atm_facility" />
|
||||
</not>
|
||||
</preConditions>
|
||||
<addColumn tableName="m_savings_product_temp">
|
||||
<column name="fc_atm_facility" type="BOOLEAN" />
|
||||
<column name="fc_atm_withdrawal_facility" type="BOOLEAN" />
|
||||
<column name="fc_upi_facility" type="BOOLEAN" />
|
||||
<column name="fc_pos_transactions_facility" type="BOOLEAN" />
|
||||
<column name="fc_cheque_book" type="BOOLEAN" />
|
||||
<column name="fc_imps_facility" type="BOOLEAN" />
|
||||
<column name="fc_sms_alerts" type="BOOLEAN" />
|
||||
<column name="fc_bbps_facility" type="BOOLEAN" />
|
||||
<column name="fc_insurance_cover" type="BOOLEAN" />
|
||||
<column name="fc_bepg_facility" type="BOOLEAN" />
|
||||
<column name="fc_neft_facility" type="BOOLEAN" />
|
||||
<column name="fc_rtgs_facility" type="BOOLEAN" />
|
||||
<column name="fc_inter_bank_total_cap_facility" type="BOOLEAN" />
|
||||
<column name="fc_withdrawal_facility" type="BOOLEAN" />
|
||||
<column name="fc_deposit_facility_kyc" type="BOOLEAN" />
|
||||
<column name="fc_virtual_debit_card" type="BOOLEAN" />
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
<!-- Group 7: depositLimitKyc (1:1 today - kept as flat columns, kyc_ prefix) -->
|
||||
<changeSet author="fineract" id="2026-7">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<columnExists tableName="m_savings_product_temp" columnName="kyc_general_type" />
|
||||
</not>
|
||||
</preConditions>
|
||||
<addColumn tableName="m_savings_product_temp">
|
||||
<column name="kyc_general_type" type="VARCHAR(50)" />
|
||||
<column name="kyc_general_limit" type="VARCHAR(50)" />
|
||||
<column name="kyc_minor_type" type="VARCHAR(50)" />
|
||||
<column name="kyc_minor_limit" type="BIGINT" />
|
||||
<column name="kyc_small_saving_type" type="VARCHAR(50)" />
|
||||
<column name="kyc_small_saving_limit" type="BIGINT" />
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
<!-- Group 8: GL account mapping scalar ids (1:1 today - kept as flat columns, gl_ prefix). No FK
|
||||
constraints - the existing validator already checks GL account validity at submit time. -->
|
||||
<changeSet author="fineract" id="2026-8">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<columnExists tableName="m_savings_product_temp" columnName="gl_savings_reference_account_id" />
|
||||
</not>
|
||||
</preConditions>
|
||||
<addColumn tableName="m_savings_product_temp">
|
||||
<column name="gl_savings_reference_account_id" type="BIGINT" />
|
||||
<column name="gl_savings_control_account_id" type="BIGINT" />
|
||||
<column name="gl_income_from_fee_account_id" type="BIGINT" />
|
||||
<column name="gl_income_from_penalty_account_id" type="BIGINT" />
|
||||
<column name="gl_interest_on_savings_account_id" type="BIGINT" />
|
||||
<column name="gl_transfers_in_suspense_account_id" type="BIGINT" />
|
||||
<column name="gl_overdraft_portfolio_control_id" type="BIGINT" />
|
||||
<column name="gl_income_from_interest_id" type="BIGINT" />
|
||||
<column name="gl_write_off_account_id" type="BIGINT" />
|
||||
<column name="gl_escheat_liability_id" type="BIGINT" />
|
||||
<column name="gl_penalties_receivable_account_id" type="BIGINT" />
|
||||
<column name="gl_interest_receivable_account_id" type="BIGINT" />
|
||||
<column name="gl_fees_receivable_account_id" type="BIGINT" />
|
||||
<column name="gl_interest_payable_account_id" type="BIGINT" />
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
<?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">
|
||||
|
||||
<!--
|
||||
Satellite tables for the genuine one-to-many pieces of a pending product edit (charges,
|
||||
facility limits, GL array mappings, share market prices) plus a "submitted field" manifest
|
||||
that records which top-level JSON parameters were actually present in the maker's request.
|
||||
|
||||
The manifest table is not optional bookkeeping: raw JSON storage implicitly distinguishes
|
||||
"field omitted" / "field explicitly null" / "field has a value", and every existing
|
||||
command.hasParameter(...) branch in the write-services and assemblers depends on that
|
||||
distinction. Typed nullable columns alone can't represent it, so this table is the
|
||||
relational replacement for that information - one row per parameter name that was present
|
||||
at submit time, per pending edit.
|
||||
|
||||
All tables reference m_savings_product_temp(id) - no cascade delete, since pending-edit
|
||||
rows are never hard-deleted (only status-flipped) once resolved.
|
||||
-->
|
||||
|
||||
<changeSet author="fineract" id="2027-1">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<tableExists tableName="m_product_temp_charge" />
|
||||
</not>
|
||||
</preConditions>
|
||||
<createTable tableName="m_product_temp_charge">
|
||||
<column name="id" type="BIGINT" autoIncrement="true">
|
||||
<constraints primaryKey="true" nullable="false" />
|
||||
</column>
|
||||
<column name="pending_edit_id" type="BIGINT">
|
||||
<constraints nullable="false" foreignKeyName="fk_ptc_pending_edit_id" references="m_savings_product_temp(id)" />
|
||||
</column>
|
||||
<column name="charge_id" type="BIGINT">
|
||||
<constraints nullable="false" />
|
||||
</column>
|
||||
</createTable>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="2027-2">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<tableExists tableName="m_product_temp_facility_limit" />
|
||||
</not>
|
||||
</preConditions>
|
||||
<createTable tableName="m_product_temp_facility_limit">
|
||||
<column name="id" type="BIGINT" autoIncrement="true">
|
||||
<constraints primaryKey="true" nullable="false" />
|
||||
</column>
|
||||
<column name="pending_edit_id" type="BIGINT">
|
||||
<constraints nullable="false" foreignKeyName="fk_ptfl_pending_edit_id" references="m_savings_product_temp(id)" />
|
||||
</column>
|
||||
<column name="limit_type" type="VARCHAR(100)">
|
||||
<constraints nullable="false" />
|
||||
</column>
|
||||
<column name="general_limit" type="VARCHAR(50)" />
|
||||
<column name="minor_account_limit" type="VARCHAR(50)" />
|
||||
<column name="small_saving_limit" type="VARCHAR(50)" />
|
||||
</createTable>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="2027-3">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<tableExists tableName="m_product_temp_gl_mapping" />
|
||||
</not>
|
||||
</preConditions>
|
||||
<createTable tableName="m_product_temp_gl_mapping">
|
||||
<column name="id" type="BIGINT" autoIncrement="true">
|
||||
<constraints primaryKey="true" nullable="false" />
|
||||
</column>
|
||||
<column name="pending_edit_id" type="BIGINT">
|
||||
<constraints nullable="false" foreignKeyName="fk_ptgm_pending_edit_id" references="m_savings_product_temp(id)" />
|
||||
</column>
|
||||
<!-- PAYMENT_CHANNEL / FEE / PENALTY -->
|
||||
<column name="mapping_type" type="VARCHAR(30)">
|
||||
<constraints nullable="false" />
|
||||
</column>
|
||||
<column name="payment_type_id" type="BIGINT" />
|
||||
<column name="charge_id" type="BIGINT" />
|
||||
<column name="gl_account_id" type="BIGINT">
|
||||
<constraints nullable="false" />
|
||||
</column>
|
||||
</createTable>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="2027-4">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<tableExists tableName="m_product_temp_share_market_price" />
|
||||
</not>
|
||||
</preConditions>
|
||||
<createTable tableName="m_product_temp_share_market_price">
|
||||
<column name="id" type="BIGINT" autoIncrement="true">
|
||||
<constraints primaryKey="true" nullable="false" />
|
||||
</column>
|
||||
<column name="pending_edit_id" type="BIGINT">
|
||||
<constraints nullable="false" foreignKeyName="fk_ptsmp_pending_edit_id" references="m_savings_product_temp(id)" />
|
||||
</column>
|
||||
<column name="source_id" type="BIGINT" />
|
||||
<column name="delete_flag" type="BOOLEAN" defaultValueBoolean="false">
|
||||
<constraints nullable="false" />
|
||||
</column>
|
||||
<column name="from_date" type="DATE" />
|
||||
<column name="share_value" type="DECIMAL(19,6)" />
|
||||
</createTable>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="fineract" id="2027-5">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<tableExists tableName="m_product_temp_submitted_field" />
|
||||
</not>
|
||||
</preConditions>
|
||||
<createTable tableName="m_product_temp_submitted_field">
|
||||
<column name="id" type="BIGINT" autoIncrement="true">
|
||||
<constraints primaryKey="true" nullable="false" />
|
||||
</column>
|
||||
<column name="pending_edit_id" type="BIGINT">
|
||||
<constraints nullable="false" foreignKeyName="fk_ptsf_pending_edit_id" references="m_savings_product_temp(id)" />
|
||||
</column>
|
||||
<column name="field_name" type="VARCHAR(100)">
|
||||
<constraints nullable="false" />
|
||||
</column>
|
||||
</createTable>
|
||||
<addUniqueConstraint tableName="m_product_temp_submitted_field" columnNames="pending_edit_id, field_name"
|
||||
constraintName="ptsf_unq_pending_edit_field" />
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
Reference in New Issue
Block a user