table changes on Maker-Checker workflow for Products, with approval-based updates and product locking to prevent concurrent edits

This commit is contained in:
keshav898966
2026-07-06 17:49:39 +05:30
parent b0bfa386ce
commit 9980310c9e
21 changed files with 1175 additions and 232 deletions
@@ -3573,7 +3573,7 @@ public class CommandWrapperBuilder {
this.actionName = "ACTIVATE";
this.entityName = "CURRENTACCOUNT";
this.savingsId = accountId;
this.entityId = null;
this.entityId = accountId;
this.href = "/currentaccounts/" + accountId + "?command=activate";
return this;
}
@@ -224,7 +224,7 @@ public final class DepositsApiConstants {
* Deposit Product Parameters
*/
private static final Set<String> DEPOSIT_PRODUCT_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(localeParamName,
monthDayFormatParamName, nameParamName, shortNameParamName, descriptionParamName, currencyCodeParamName,
dateFormatParamName, monthDayFormatParamName, nameParamName, shortNameParamName, descriptionParamName, currencyCodeParamName,
digitsAfterDecimalParamName, inMultiplesOfParamName, nominalAnnualInterestRateParamName, interestCompoundingPeriodTypeParamName,
interestPostingPeriodTypeParamName, interestCalculationTypeParamName, interestCalculationDaysInYearTypeParamName,
lockinPeriodFrequencyParamName, lockinPeriodFrequencyTypeParamName, accountingRuleParamName, chargesParamName,
@@ -51,6 +51,14 @@ import org.apache.fineract.portfolio.tax.data.TaxGroupData;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEdit;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedFieldRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditChargeRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMappingRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFacilityLimitRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFieldMapper;
import java.util.Set;
@RequiredArgsConstructor
public class CurrentProductReadPlatformServiceImpl implements CurrentProductReadPlatformService {
@@ -60,6 +68,11 @@ public class CurrentProductReadPlatformServiceImpl implements CurrentProductRead
private final CurrentProductFacilityConfigRepository facilityConfigRepository;
private final CurrentProductFacilityLimitRepository facilityLimitRepository;
private final CurrentProductDepositLimitKycRepository depositLimitKycRepository;
private final ProductPendingEditRepository productPendingEditRepository;
private final ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository;
private final ProductPendingEditChargeRepository pendingEditChargeRepository;
private final ProductPendingEditGlMappingRepository pendingEditGlMappingRepository;
private final ProductPendingEditFacilityLimitRepository pendingEditFacilityLimitRepository;
private final CurrentProductMapper currentProductRowMapper = new CurrentProductMapper();
private final CurrentProductLookupMapper currentProductLookupsRowMapper = new CurrentProductLookupMapper();
@@ -150,41 +163,50 @@ 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 "
+ "cp.name as savingsProductName, spt.createdby_id as makerId, maker.username as makerUsername, "
+ "spt.lastmodifiedby_id as checkerId, checker.username as checkerUsername, spt.created_date as createdOn, "
+ "spt.lastmodified_date as checkedOn, spt.status as status, spt.reject_reason as rejectReason "
+ "from m_saving_product_temp spt "
+ "join m_current_product cp on cp.id = spt.product_id "
+ "join m_appuser maker on maker.id = spt.maker_id "
+ "left join m_appuser checker on checker.id = spt.checker_id "
+ "join m_appuser maker on maker.id = spt.createdby_id "
+ "left join m_appuser checker on checker.id = spt.lastmodifiedby_id "
+ "where spt.product_type = 'CURRENT' ";
private 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 RowMapper<SavingsProductTempData> getPendingEditRowMapper() {
return (rs, rowNum) -> {
final SavingsProductTempData data = new SavingsProductTempData();
data.setId(rs.getLong("tempId"));
data.setSavingsProductId(rs.getLong("savingsProductId"));
data.setSavingsProductName(rs.getString("savingsProductName"));
data.setMakerId(rs.getLong("makerId"));
data.setMakerUsername(rs.getString("makerUsername"));
data.setCheckerId(JdbcSupport.getLong(rs, "checkerId"));
data.setCheckerUsername(rs.getString("checkerUsername"));
data.setCreatedOn(rs.getTimestamp("createdOn") == null ? null : rs.getTimestamp("createdOn").toLocalDateTime());
data.setCheckedOn(rs.getTimestamp("checkedOn") == null ? null : rs.getTimestamp("checkedOn").toLocalDateTime());
data.setStatus(rs.getString("status"));
data.setRejectReason(rs.getString("rejectReason"));
final Long tempId = data.getId();
final java.util.Optional<ProductPendingEdit> opt = this.productPendingEditRepository.findById(tempId);
if (opt.isPresent()) {
final ProductPendingEdit pendingEdit = opt.get();
final Set<String> submittedFields = new java.util.HashSet<>(
this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId()));
final Map<String, Object> proposedChanges = ProductPendingEditFieldMapper.reconstructProposedChangesMap(
pendingEdit, submittedFields, this.pendingEditChargeRepository, this.pendingEditGlMappingRepository,
this.pendingEditFacilityLimitRepository, GSON);
data.setProposedChanges(proposedChanges);
}
return data;
};
}
@Override
public Collection<SavingsProductTempData> retrieveAllPendingEdits() {
this.context.authenticatedUser();
final String sql = "select " + PENDING_EDIT_SCHEMA + "and spt.status = 'PENDING' order by spt.created_on desc";
return this.jdbcTemplate.query(sql, PENDING_EDIT_MAPPER);
final String sql = "select " + PENDING_EDIT_SCHEMA + "and spt.status = 'PENDING' order by spt.created_date desc";
return this.jdbcTemplate.query(sql, getPendingEditRowMapper());
}
@Override
@@ -192,7 +214,7 @@ public class CurrentProductReadPlatformServiceImpl implements CurrentProductRead
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
return this.jdbcTemplate.queryForObject(sql, getPendingEditRowMapper(), new Object[] { productId }); // NOSONAR
} catch (final EmptyResultDataAccessException e) {
throw new CurrentProductNoPendingEditException(productId);
}
@@ -249,7 +271,7 @@ public class CurrentProductReadPlatformServiceImpl implements CurrentProductRead
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("exists(select 1 from m_savings_product_temp spt where spt.product_id = cp.id "
sqlBuilder.append("exists(select 1 from m_saving_product_temp spt where spt.product_id = cp.id "
+ "and spt.product_type = 'CURRENT' and spt.status = 'PENDING') as isPendingApproval ");
sqlBuilder.append("from m_current_product cp ");
@@ -63,6 +63,16 @@ import org.apache.fineract.portfolio.savings.DepositAccountType;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEdit;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditRepository;
import org.apache.fineract.portfolio.savings.exception.SavingsProductNotFoundException;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedField;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedFieldRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditCharge;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditChargeRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFacilityLimit;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFacilityLimitRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMapping;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMappingRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFieldMapper;
import com.google.gson.JsonObject;
import org.apache.fineract.portfolio.tax.domain.TaxGroup;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.dao.DataAccessException;
@@ -72,6 +82,7 @@ import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
public class CurrentProductWritePlatformServiceJpaRepositoryImpl implements CurrentProductWritePlatformService {
private static final com.google.gson.Gson GSON = new com.google.gson.Gson();
private final PlatformSecurityContext context;
private final CurrentProductRepository currentProductRepository;
private final CurrentProductDataValidator fromApiJsonDataValidator;
@@ -81,6 +92,10 @@ public class CurrentProductWritePlatformServiceJpaRepositoryImpl implements Curr
private final CurrentProductFacilityLimitRepository facilityLimitRepository;
private final CurrentProductDepositLimitKycRepository depositLimitKycRepository;
private final ProductPendingEditRepository productPendingEditRepository;
private final ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository;
private final ProductPendingEditChargeRepository pendingEditChargeRepository;
private final ProductPendingEditFacilityLimitRepository pendingEditFacilityLimitRepository;
private final ProductPendingEditGlMappingRepository pendingEditGlMappingRepository;
private final FromJsonHelper fromApiJsonHelper;
private final CommandSourceRepository commandSourceRepository;
@@ -281,7 +296,28 @@ public class CurrentProductWritePlatformServiceJpaRepositoryImpl implements Curr
this.fromApiJsonDataValidator.validateForUpdate(command.json(), product);
final ProductPendingEdit pendingEdit = new ProductPendingEdit(ProductPendingEdit.TYPE_CURRENT, productId, maker, command.json());
final ProductPendingEdit pendingEdit = new ProductPendingEdit(ProductPendingEdit.TYPE_CURRENT, productId, maker);
final java.util.Set<String> submitted = ProductPendingEditFieldMapper.populateCoreFields(command, pendingEdit.getCoreFields());
final java.util.Set<String> currentOnlySubmitted = ProductPendingEditFieldMapper.populateCurrentOnlyFields(command, pendingEdit.getCurrentOnlyFields());
final boolean facilityConfigSubmitted = ProductPendingEditFieldMapper.populateFacilityConfig(command, pendingEdit.getFacilityConfigFields());
final boolean depositLimitKycSubmitted = ProductPendingEditFieldMapper.populateDepositLimitKyc(command, pendingEdit.getDepositLimitKycFields());
final boolean glMappingSubmitted = ProductPendingEditFieldMapper.populateGlAccountMapping(command, pendingEdit.getGlAccountMappingFields());
if (!currentOnlySubmitted.isEmpty()) {
submitted.add("avgBalanceCalcPeriod");
submitted.add("validForPeriod");
submitted.add("odAccountClosedZeroBalanceInDayClosing");
}
if (facilityConfigSubmitted) {
submitted.add("accountFacilityConfig");
}
if (depositLimitKycSubmitted) {
submitted.add("depositLimitKyc");
}
if (glMappingSubmitted) {
submitted.add("glAccountMapping");
}
final CommandSource auditEntry = CommandSource.builder() //
.actionName("UPDATE") //
@@ -300,6 +336,30 @@ public class CurrentProductWritePlatformServiceJpaRepositoryImpl implements Curr
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();
@@ -318,8 +378,33 @@ public class CurrentProductWritePlatformServiceJpaRepositoryImpl implements Curr
throw new CurrentProductSelfApprovalNotAllowedException(productId);
}
final JsonCommand replayedCommand = JsonCommand.fromJsonElement(productId,
this.fromApiJsonHelper.parse(pendingEdit.getProposedPayloadJson()), this.fromApiJsonHelper);
final Set<String> submittedFields = new java.util.HashSet<>(
this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId()));
final JsonObject root = new JsonObject();
root.addProperty("locale", "en");
root.addProperty("dateFormat", "yyyy-MM-dd");
ProductPendingEditFieldMapper.writeCoreFieldsToJson(root, pendingEdit.getCoreFields(), submittedFields);
ProductPendingEditFieldMapper.writeCurrentOnlyFieldsToJson(root, pendingEdit.getCurrentOnlyFields(), submittedFields);
ProductPendingEditFieldMapper.writeFacilityConfigToJson(root, pendingEdit.getFacilityConfigFields(),
submittedFields.contains("accountFacilityConfig"));
ProductPendingEditFieldMapper.writeDepositLimitKycToJson(root, pendingEdit.getDepositLimitKycFields(),
submittedFields.contains("depositLimitKyc"));
if (submittedFields.contains("glAccountMapping")) {
ProductPendingEditFieldMapper.writeGlAccountMappingToJson(root, pendingEdit.getGlAccountMappingFields());
}
if (submittedFields.contains("charges")) {
ProductPendingEditFieldMapper.writeChargesToJson(root,
this.pendingEditChargeRepository.findByPendingEditId(pendingEdit.getId()));
}
if (submittedFields.contains("accountFacilityLimits")) {
ProductPendingEditFieldMapper.writeFacilityLimitsToJson(root,
this.pendingEditFacilityLimitRepository.findByPendingEditId(pendingEdit.getId()));
}
if (submittedFields.contains("glArrayMappings")) {
ProductPendingEditFieldMapper.writeGlArrayMappingsToJson(root,
this.pendingEditGlMappingRepository.findByPendingEditId(pendingEdit.getId()));
}
final JsonCommand replayedCommand = JsonCommand.fromJsonElement(productId, root, this.fromApiJsonHelper);
final CommandProcessingResult result = update(productId, replayedCommand);
pendingEdit.markApproved(checker);
@@ -340,12 +425,40 @@ public class CurrentProductWritePlatformServiceJpaRepositoryImpl implements Curr
return existing.get();
}
}
final Set<String> submittedFields = new java.util.HashSet<>(
this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId()));
final JsonObject root = new JsonObject();
root.addProperty("locale", "en");
root.addProperty("dateFormat", "yyyy-MM-dd");
ProductPendingEditFieldMapper.writeCoreFieldsToJson(root, pendingEdit.getCoreFields(), submittedFields);
ProductPendingEditFieldMapper.writeCurrentOnlyFieldsToJson(root, pendingEdit.getCurrentOnlyFields(), submittedFields);
ProductPendingEditFieldMapper.writeFacilityConfigToJson(root, pendingEdit.getFacilityConfigFields(),
submittedFields.contains("accountFacilityConfig"));
ProductPendingEditFieldMapper.writeDepositLimitKycToJson(root, pendingEdit.getDepositLimitKycFields(),
submittedFields.contains("depositLimitKyc"));
if (submittedFields.contains("glAccountMapping")) {
ProductPendingEditFieldMapper.writeGlAccountMappingToJson(root, pendingEdit.getGlAccountMappingFields());
}
if (submittedFields.contains("charges")) {
ProductPendingEditFieldMapper.writeChargesToJson(root,
this.pendingEditChargeRepository.findByPendingEditId(pendingEdit.getId()));
}
if (submittedFields.contains("accountFacilityLimits")) {
ProductPendingEditFieldMapper.writeFacilityLimitsToJson(root,
this.pendingEditFacilityLimitRepository.findByPendingEditId(pendingEdit.getId()));
}
if (submittedFields.contains("glArrayMappings")) {
ProductPendingEditFieldMapper.writeGlArrayMappingsToJson(root,
this.pendingEditGlMappingRepository.findByPendingEditId(pendingEdit.getId()));
}
final String commandJson = GSON.toJson(root);
final CommandSource auditEntry = CommandSource.builder() //
.actionName("UPDATE") //
.entityName("CURRENTPRODUCT") //
.resourceGetUrl("/currentproducts/" + productId) //
.resourceId(productId) //
.commandAsJson(pendingEdit.getProposedPayloadJson()) //
.commandAsJson(commandJson) //
.maker(pendingEdit.getMaker()) //
.madeOnDate(DateUtils.getAuditOffsetDateTime()) //
.status(CommandProcessingResultType.AWAITING_APPROVAL.getValue()) //
@@ -23,6 +23,10 @@ import org.apache.fineract.commands.domain.CommandSourceRepository;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedFieldRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditChargeRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFacilityLimitRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMappingRepository;
import org.apache.fineract.portfolio.currentproduct.data.CurrentProductDataValidator;
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductAssembler;
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductDepositLimitKycRepository;
@@ -46,9 +50,17 @@ public class CurrentProductConfiguration {
public CurrentProductReadPlatformService currentProductReadPlatformService(PlatformSecurityContext context,
JdbcTemplate jdbcTemplate, CurrentProductFacilityConfigRepository currentFacilityConfigRepository,
CurrentProductFacilityLimitRepository currentFacilityLimitRepository,
CurrentProductDepositLimitKycRepository currentDepositLimitKycRepository) {
CurrentProductDepositLimitKycRepository currentDepositLimitKycRepository,
ProductPendingEditRepository productPendingEditRepository,
ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository,
ProductPendingEditChargeRepository pendingEditChargeRepository,
ProductPendingEditGlMappingRepository pendingEditGlMappingRepository,
ProductPendingEditFacilityLimitRepository pendingEditFacilityLimitRepository) {
return new CurrentProductReadPlatformServiceImpl(context, jdbcTemplate, currentFacilityConfigRepository,
currentFacilityLimitRepository, currentDepositLimitKycRepository);
currentFacilityLimitRepository, currentDepositLimitKycRepository,
productPendingEditRepository, pendingEditSubmittedFieldRepository,
pendingEditChargeRepository, pendingEditGlMappingRepository,
pendingEditFacilityLimitRepository);
}
@Bean
@@ -61,11 +73,16 @@ public class CurrentProductConfiguration {
CurrentProductFacilityLimitRepository currentFacilityLimitRepository,
CurrentProductDepositLimitKycRepository currentDepositLimitKycRepository,
ProductPendingEditRepository productPendingEditRepository,
ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository,
ProductPendingEditChargeRepository pendingEditChargeRepository,
ProductPendingEditFacilityLimitRepository pendingEditFacilityLimitRepository,
ProductPendingEditGlMappingRepository pendingEditGlMappingRepository,
FromJsonHelper fromApiJsonHelper,
CommandSourceRepository commandSourceRepository) {
return new CurrentProductWritePlatformServiceJpaRepositoryImpl(context, currentProductRepository, currentProductDataValidator,
currentProductAssembler, accountMappingWritePlatformService, currentFacilityConfigRepository,
currentFacilityLimitRepository, currentDepositLimitKycRepository, productPendingEditRepository, fromApiJsonHelper,
commandSourceRepository);
currentFacilityLimitRepository, currentDepositLimitKycRepository, productPendingEditRepository,
pendingEditSubmittedFieldRepository, pendingEditChargeRepository, pendingEditFacilityLimitRepository,
pendingEditGlMappingRepository, fromApiJsonHelper, commandSourceRepository);
}
}
@@ -44,6 +44,13 @@ import org.apache.fineract.portfolio.tax.data.TaxGroupData;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEdit;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedFieldRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditChargeRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMappingRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFieldMapper;
import java.util.Set;
@RequiredArgsConstructor
public class DepositProductReadPlatformServiceImpl implements DepositProductReadPlatformService {
@@ -54,6 +61,10 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead
private final PlatformSecurityContext context;
private final JdbcTemplate jdbcTemplate;
private final InterestRateChartReadPlatformService chartReadPlatformService;
private final ProductPendingEditRepository productPendingEditRepository;
private final ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository;
private final ProductPendingEditChargeRepository pendingEditChargeRepository;
private final ProductPendingEditGlMappingRepository pendingEditGlMappingRepository;
@Override
public Collection<DepositProductData> retrieveAll(final DepositAccountType depositAccountType) {
@@ -135,41 +146,50 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead
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 RowMapper<SavingsProductTempData> getPendingEditRowMapper() {
return (rs, rowNum) -> {
final SavingsProductTempData data = new SavingsProductTempData();
data.setId(rs.getLong("tempId"));
data.setSavingsProductId(rs.getLong("savingsProductId"));
data.setSavingsProductName(rs.getString("savingsProductName"));
data.setMakerId(rs.getLong("makerId"));
data.setMakerUsername(rs.getString("makerUsername"));
data.setCheckerId(JdbcSupport.getLong(rs, "checkerId"));
data.setCheckerUsername(rs.getString("checkerUsername"));
data.setCreatedOn(rs.getTimestamp("createdOn") == null ? null : rs.getTimestamp("createdOn").toLocalDateTime());
data.setCheckedOn(rs.getTimestamp("checkedOn") == null ? null : rs.getTimestamp("checkedOn").toLocalDateTime());
data.setStatus(rs.getString("status"));
data.setRejectReason(rs.getString("rejectReason"));
final Long tempId = data.getId();
final java.util.Optional<ProductPendingEdit> opt = this.productPendingEditRepository.findById(tempId);
if (opt.isPresent()) {
final ProductPendingEdit pendingEdit = opt.get();
final Set<String> submittedFields = new java.util.HashSet<>(
this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId()));
final Map<String, Object> proposedChanges = ProductPendingEditFieldMapper.reconstructProposedChangesMap(
pendingEdit, submittedFields, this.pendingEditChargeRepository, this.pendingEditGlMappingRepository, null, GSON);
data.setProposedChanges(proposedChanges);
}
return data;
};
}
private static String pendingEditSchema(final String productType) {
return "spt.id as tempId, spt.product_id as savingsProductId, " + "sp.name as savingsProductName, spt.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 "
return "spt.id as tempId, spt.product_id as savingsProductId, " + "sp.name as savingsProductName, spt.createdby_id as makerId, "
+ "maker.username as makerUsername, spt.lastmodifiedby_id as checkerId, checker.username as checkerUsername, "
+ "spt.created_date as createdOn, spt.lastmodified_date as checkedOn, spt.status as status, "
+ "spt.reject_reason as rejectReason "
+ "from m_saving_product_temp spt " + "join m_savings_product sp on sp.id = spt.product_id "
+ "join m_appuser maker on maker.id = spt.createdby_id " + "left join m_appuser checker on checker.id = spt.lastmodifiedby_id "
+ "where spt.product_type = '" + productType + "' ";
}
@Override
public Collection<SavingsProductTempData> retrieveAllPendingEdits(final DepositAccountType depositAccountType) {
final String sql = "select " + pendingEditSchema(productPendingType(depositAccountType))
+ "and spt.status = 'PENDING' order by spt.created_on desc";
return this.jdbcTemplate.query(sql, PENDING_EDIT_MAPPER);
+ "and spt.status = 'PENDING' order by spt.created_date desc";
return this.jdbcTemplate.query(sql, getPendingEditRowMapper());
}
@Override
@@ -177,7 +197,7 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead
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
return this.jdbcTemplate.queryForObject(sql, getPendingEditRowMapper(), new Object[] { productId }); // NOSONAR
} catch (final EmptyResultDataAccessException e) {
throw new SavingsProductNoPendingEditException(productId);
}
@@ -315,7 +335,7 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead
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("exists(select 1 from m_savings_product_temp spt where spt.product_id = sp.id "
sqlBuilder.append("exists(select 1 from m_saving_product_temp spt where spt.product_id = sp.id "
+ "and spt.product_type = 'FD' and spt.status = 'PENDING') as isPendingApproval ");
sqlBuilder.append("from m_savings_product sp ");
sqlBuilder.append("left join m_deposit_product_term_and_preclosure dptp on sp.id=dptp.savings_product_id ");
@@ -455,7 +475,7 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead
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("exists(select 1 from m_savings_product_temp spt where spt.product_id = sp.id "
sqlBuilder.append("exists(select 1 from m_saving_product_temp spt where spt.product_id = sp.id "
+ "and spt.product_type = 'RD' and spt.status = 'PENDING') as isPendingApproval ");
sqlBuilder.append("from m_savings_product sp ");
sqlBuilder.append("left join m_deposit_product_term_and_preclosure dptp on sp.id=dptp.savings_product_id ");
@@ -55,6 +55,14 @@ import org.apache.fineract.portfolio.savings.domain.FixedDepositProduct;
import org.apache.fineract.portfolio.savings.domain.FixedDepositProductRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEdit;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedField;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedFieldRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditCharge;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditChargeRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMapping;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMappingRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFieldMapper;
import com.google.gson.JsonObject;
import org.apache.fineract.portfolio.savings.exception.FixedDepositProductNotFoundException;
import org.apache.fineract.portfolio.savings.exception.SavingsProductEditPendingApprovalException;
import org.apache.fineract.portfolio.savings.exception.SavingsProductNoPendingEditException;
@@ -68,6 +76,7 @@ import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
public class FixedDepositProductWritePlatformServiceJpaRepositoryImpl implements FixedDepositProductWritePlatformService {
private static final com.google.gson.Gson GSON = new com.google.gson.Gson();
private final PlatformSecurityContext context;
private final FixedDepositProductRepository fixedDepositProductRepository;
private final DepositProductDataValidator fromApiJsonDataValidator;
@@ -75,6 +84,9 @@ public class FixedDepositProductWritePlatformServiceJpaRepositoryImpl implements
private final ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService;
private final InterestRateChartAssembler chartAssembler;
private final ProductPendingEditRepository productPendingEditRepository;
private final ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository;
private final ProductPendingEditChargeRepository pendingEditChargeRepository;
private final ProductPendingEditGlMappingRepository pendingEditGlMappingRepository;
private final FromJsonHelper fromApiJsonHelper;
private final CommandSourceRepository commandSourceRepository;
@@ -108,10 +120,16 @@ public class FixedDepositProductWritePlatformServiceJpaRepositoryImpl implements
@Transactional
@Override
public CommandProcessingResult update(final Long productId, final JsonCommand command) {
return updateInternal(productId, command, true);
}
private CommandProcessingResult updateInternal(final Long productId, final JsonCommand command, final boolean validate) {
try {
this.context.authenticatedUser();
this.fromApiJsonDataValidator.validateForFixedDepositUpdate(command.json());
if (validate) {
this.fromApiJsonDataValidator.validateForFixedDepositUpdate(command.json());
}
final FixedDepositProduct product = this.fixedDepositProductRepository.findById(productId)
.orElseThrow(() -> new FixedDepositProductNotFoundException(productId));
@@ -180,7 +198,18 @@ public class FixedDepositProductWritePlatformServiceJpaRepositoryImpl implements
this.fromApiJsonDataValidator.validateForFixedDepositUpdate(command.json());
final ProductPendingEdit pendingEdit = new ProductPendingEdit(ProductPendingEdit.TYPE_FD, productId, maker, command.json());
final ProductPendingEdit pendingEdit = new ProductPendingEdit(ProductPendingEdit.TYPE_FD, productId, maker);
final java.util.Set<String> submitted = ProductPendingEditFieldMapper.populateCoreFields(command, pendingEdit.getCoreFields());
final java.util.Set<String> fdSubmitted = ProductPendingEditFieldMapper.populateFixedDepositFields(command, pendingEdit.getFixedDepositFields());
final boolean glMappingSubmitted = ProductPendingEditFieldMapper.populateGlAccountMapping(command, pendingEdit.getGlAccountMappingFields());
if (!fdSubmitted.isEmpty()) {
submitted.addAll(fdSubmitted);
}
if (glMappingSubmitted) {
submitted.add("glAccountMapping");
}
final CommandSource auditEntry = CommandSource.builder() //
.actionName("UPDATE") //
@@ -199,6 +228,24 @@ public class FixedDepositProductWritePlatformServiceJpaRepositoryImpl implements
this.productPendingEditRepository.saveAndFlush(pendingEdit);
for (final String fieldName : submitted) {
this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, fieldName));
}
final List<Long> chargeIds = ProductPendingEditFieldMapper.extractChargeIds(command);
if (chargeIds != null) {
this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, "charges"));
for (final Long chargeId : chargeIds) {
this.pendingEditChargeRepository.save(new ProductPendingEditCharge(pendingEdit, chargeId));
}
}
final List<ProductPendingEditGlMapping> glMappings = ProductPendingEditFieldMapper.extractGlArrayMappings(command, pendingEdit);
if (glMappings != null) {
this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, "glArrayMappings"));
this.pendingEditGlMappingRepository.saveAll(glMappings);
}
return new CommandProcessingResultBuilder() //
.withEntityId(product.getId()) //
.build();
@@ -217,9 +264,26 @@ public class FixedDepositProductWritePlatformServiceJpaRepositoryImpl implements
throw new SavingsProductSelfApprovalNotAllowedException(productId);
}
final JsonCommand replayedCommand = JsonCommand.fromJsonElement(productId,
this.fromApiJsonHelper.parse(pendingEdit.getProposedPayloadJson()), this.fromApiJsonHelper);
final CommandProcessingResult result = update(productId, replayedCommand);
final Set<String> submittedFields = new java.util.HashSet<>(
this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId()));
final JsonObject root = new JsonObject();
root.addProperty("locale", "en");
root.addProperty("dateFormat", "yyyy-MM-dd");
ProductPendingEditFieldMapper.writeCoreFieldsToJson(root, pendingEdit.getCoreFields(), submittedFields);
ProductPendingEditFieldMapper.writeFixedDepositFieldsToJson(root, pendingEdit.getFixedDepositFields(), submittedFields);
if (submittedFields.contains("glAccountMapping")) {
ProductPendingEditFieldMapper.writeGlAccountMappingToJson(root, pendingEdit.getGlAccountMappingFields());
}
if (submittedFields.contains("charges")) {
ProductPendingEditFieldMapper.writeChargesToJson(root,
this.pendingEditChargeRepository.findByPendingEditId(pendingEdit.getId()));
}
if (submittedFields.contains("glArrayMappings")) {
ProductPendingEditFieldMapper.writeGlArrayMappingsToJson(root,
this.pendingEditGlMappingRepository.findByPendingEditId(pendingEdit.getId()));
}
final JsonCommand replayedCommand = JsonCommand.fromJsonElement(productId, root, this.fromApiJsonHelper);
final CommandProcessingResult result = updateInternal(productId, replayedCommand, false);
pendingEdit.markApproved(checker);
this.productPendingEditRepository.saveAndFlush(pendingEdit);
@@ -239,12 +303,32 @@ public class FixedDepositProductWritePlatformServiceJpaRepositoryImpl implements
return existing.get();
}
}
final Set<String> submittedFields = new java.util.HashSet<>(
this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId()));
final JsonObject root = new JsonObject();
root.addProperty("locale", "en");
root.addProperty("dateFormat", "yyyy-MM-dd");
ProductPendingEditFieldMapper.writeCoreFieldsToJson(root, pendingEdit.getCoreFields(), submittedFields);
ProductPendingEditFieldMapper.writeFixedDepositFieldsToJson(root, pendingEdit.getFixedDepositFields(), submittedFields);
if (submittedFields.contains("glAccountMapping")) {
ProductPendingEditFieldMapper.writeGlAccountMappingToJson(root, pendingEdit.getGlAccountMappingFields());
}
if (submittedFields.contains("charges")) {
ProductPendingEditFieldMapper.writeChargesToJson(root,
this.pendingEditChargeRepository.findByPendingEditId(pendingEdit.getId()));
}
if (submittedFields.contains("glArrayMappings")) {
ProductPendingEditFieldMapper.writeGlArrayMappingsToJson(root,
this.pendingEditGlMappingRepository.findByPendingEditId(pendingEdit.getId()));
}
final String commandJson = GSON.toJson(root);
final CommandSource auditEntry = CommandSource.builder() //
.actionName("UPDATE") //
.entityName("FIXEDDEPOSITPRODUCT") //
.resourceGetUrl("/fixeddepositproducts/" + productId) //
.resourceId(productId) //
.commandAsJson(pendingEdit.getProposedPayloadJson()) //
.commandAsJson(commandJson) //
.maker(pendingEdit.getMaker()) //
.madeOnDate(DateUtils.getAuditOffsetDateTime()) //
.status(CommandProcessingResultType.AWAITING_APPROVAL.getValue()) //
@@ -52,10 +52,18 @@ import org.apache.fineract.portfolio.savings.SavingsApiConstants;
import org.apache.fineract.portfolio.savings.data.DepositProductDataValidator;
import org.apache.fineract.portfolio.savings.domain.DepositProductAssembler;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEdit;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditCharge;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditChargeRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFieldMapper;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMapping;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMappingRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedField;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedFieldRepository;
import org.apache.fineract.portfolio.savings.domain.RecurringDepositProduct;
import org.apache.fineract.portfolio.savings.domain.RecurringDepositProductRepository;
import org.apache.fineract.portfolio.savings.exception.RecurringDepositProductNotFoundException;
import com.google.gson.JsonObject;
import org.apache.fineract.portfolio.savings.exception.SavingsProductEditPendingApprovalException;
import org.apache.fineract.portfolio.savings.exception.SavingsProductNoPendingEditException;
import org.apache.fineract.portfolio.savings.exception.SavingsProductSelfApprovalNotAllowedException;
@@ -68,6 +76,7 @@ import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
public class RecurringDepositProductWritePlatformServiceJpaRepositoryImpl implements RecurringDepositProductWritePlatformService {
private static final com.google.gson.Gson GSON = new com.google.gson.Gson();
private final PlatformSecurityContext context;
private final RecurringDepositProductRepository recurringDepositProductRepository;
private final DepositProductDataValidator fromApiJsonDataValidator;
@@ -75,6 +84,9 @@ public class RecurringDepositProductWritePlatformServiceJpaRepositoryImpl implem
private final ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService;
private final InterestRateChartAssembler chartAssembler;
private final ProductPendingEditRepository productPendingEditRepository;
private final ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository;
private final ProductPendingEditChargeRepository pendingEditChargeRepository;
private final ProductPendingEditGlMappingRepository pendingEditGlMappingRepository;
private final FromJsonHelper fromApiJsonHelper;
private final CommandSourceRepository commandSourceRepository;
@@ -108,10 +120,16 @@ public class RecurringDepositProductWritePlatformServiceJpaRepositoryImpl implem
@Transactional
@Override
public CommandProcessingResult update(final Long productId, final JsonCommand command) {
return updateInternal(productId, command, true);
}
private CommandProcessingResult updateInternal(final Long productId, final JsonCommand command, final boolean validate) {
try {
this.context.authenticatedUser();
this.fromApiJsonDataValidator.validateForRecurringDepositUpdate(command.json());
if (validate) {
this.fromApiJsonDataValidator.validateForRecurringDepositUpdate(command.json());
}
final RecurringDepositProduct product = this.recurringDepositProductRepository.findById(productId)
.orElseThrow(() -> new RecurringDepositProductNotFoundException(productId));
@@ -180,7 +198,22 @@ public class RecurringDepositProductWritePlatformServiceJpaRepositoryImpl implem
this.fromApiJsonDataValidator.validateForRecurringDepositUpdate(command.json());
final ProductPendingEdit pendingEdit = new ProductPendingEdit(ProductPendingEdit.TYPE_RD, productId, maker, command.json());
final ProductPendingEdit pendingEdit = new ProductPendingEdit(ProductPendingEdit.TYPE_RD, productId, maker);
final java.util.Set<String> submitted = ProductPendingEditFieldMapper.populateCoreFields(command, pendingEdit.getCoreFields());
final java.util.Set<String> fdSubmitted = ProductPendingEditFieldMapper.populateFixedDepositFields(command, pendingEdit.getFixedDepositFields());
final java.util.Set<String> rdSubmitted = ProductPendingEditFieldMapper.populateRecurringDepositFields(command, pendingEdit.getRecurringDepositFields());
final boolean glMappingSubmitted = ProductPendingEditFieldMapper.populateGlAccountMapping(command, pendingEdit.getGlAccountMappingFields());
if (!fdSubmitted.isEmpty()) {
submitted.addAll(fdSubmitted);
}
if (!rdSubmitted.isEmpty()) {
submitted.addAll(rdSubmitted);
}
if (glMappingSubmitted) {
submitted.add("glAccountMapping");
}
final CommandSource auditEntry = CommandSource.builder() //
.actionName("UPDATE") //
@@ -199,6 +232,24 @@ public class RecurringDepositProductWritePlatformServiceJpaRepositoryImpl implem
this.productPendingEditRepository.saveAndFlush(pendingEdit);
for (final String fieldName : submitted) {
this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, fieldName));
}
final List<Long> chargeIds = ProductPendingEditFieldMapper.extractChargeIds(command);
if (chargeIds != null) {
this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, "charges"));
for (final Long chargeId : chargeIds) {
this.pendingEditChargeRepository.save(new ProductPendingEditCharge(pendingEdit, chargeId));
}
}
final List<ProductPendingEditGlMapping> glMappings = ProductPendingEditFieldMapper.extractGlArrayMappings(command, pendingEdit);
if (glMappings != null) {
this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, "glArrayMappings"));
this.pendingEditGlMappingRepository.saveAll(glMappings);
}
return new CommandProcessingResultBuilder() //
.withEntityId(product.getId()) //
.build();
@@ -217,9 +268,28 @@ public class RecurringDepositProductWritePlatformServiceJpaRepositoryImpl implem
throw new SavingsProductSelfApprovalNotAllowedException(productId);
}
final JsonCommand replayedCommand = JsonCommand.fromJsonElement(productId,
this.fromApiJsonHelper.parse(pendingEdit.getProposedPayloadJson()), this.fromApiJsonHelper);
final CommandProcessingResult result = update(productId, replayedCommand);
final Set<String> submittedFields = new java.util.HashSet<>(
this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId()));
submittedFields.remove("fdType");
final JsonObject root = new JsonObject();
root.addProperty("locale", "en");
root.addProperty("dateFormat", "yyyy-MM-dd");
ProductPendingEditFieldMapper.writeCoreFieldsToJson(root, pendingEdit.getCoreFields(), submittedFields);
ProductPendingEditFieldMapper.writeFixedDepositFieldsToJson(root, pendingEdit.getFixedDepositFields(), submittedFields);
ProductPendingEditFieldMapper.writeRecurringDepositFieldsToJson(root, pendingEdit.getRecurringDepositFields(), submittedFields);
if (submittedFields.contains("glAccountMapping")) {
ProductPendingEditFieldMapper.writeGlAccountMappingToJson(root, pendingEdit.getGlAccountMappingFields());
}
if (submittedFields.contains("charges")) {
ProductPendingEditFieldMapper.writeChargesToJson(root,
this.pendingEditChargeRepository.findByPendingEditId(pendingEdit.getId()));
}
if (submittedFields.contains("glArrayMappings")) {
ProductPendingEditFieldMapper.writeGlArrayMappingsToJson(root,
this.pendingEditGlMappingRepository.findByPendingEditId(pendingEdit.getId()));
}
final JsonCommand replayedCommand = JsonCommand.fromJsonElement(productId, root, this.fromApiJsonHelper);
final CommandProcessingResult result = updateInternal(productId, replayedCommand, false);
pendingEdit.markApproved(checker);
this.productPendingEditRepository.saveAndFlush(pendingEdit);
@@ -239,12 +309,33 @@ public class RecurringDepositProductWritePlatformServiceJpaRepositoryImpl implem
return existing.get();
}
}
final Set<String> submittedFields = new java.util.HashSet<>(
this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId()));
final JsonObject root = new JsonObject();
root.addProperty("locale", "en");
root.addProperty("dateFormat", "yyyy-MM-dd");
ProductPendingEditFieldMapper.writeCoreFieldsToJson(root, pendingEdit.getCoreFields(), submittedFields);
ProductPendingEditFieldMapper.writeFixedDepositFieldsToJson(root, pendingEdit.getFixedDepositFields(), submittedFields);
ProductPendingEditFieldMapper.writeRecurringDepositFieldsToJson(root, pendingEdit.getRecurringDepositFields(), submittedFields);
if (submittedFields.contains("glAccountMapping")) {
ProductPendingEditFieldMapper.writeGlAccountMappingToJson(root, pendingEdit.getGlAccountMappingFields());
}
if (submittedFields.contains("charges")) {
ProductPendingEditFieldMapper.writeChargesToJson(root,
this.pendingEditChargeRepository.findByPendingEditId(pendingEdit.getId()));
}
if (submittedFields.contains("glArrayMappings")) {
ProductPendingEditFieldMapper.writeGlArrayMappingsToJson(root,
this.pendingEditGlMappingRepository.findByPendingEditId(pendingEdit.getId()));
}
final String commandJson = GSON.toJson(root);
final CommandSource auditEntry = CommandSource.builder() //
.actionName("UPDATE") //
.entityName("RECURRINGDEPOSITPRODUCT") //
.resourceGetUrl("/recurringdepositproducts/" + productId) //
.resourceId(productId) //
.commandAsJson(pendingEdit.getProposedPayloadJson()) //
.commandAsJson(commandJson) //
.maker(pendingEdit.getMaker()) //
.madeOnDate(DateUtils.getAuditOffsetDateTime()) //
.status(CommandProcessingResultType.AWAITING_APPROVAL.getValue()) //
@@ -45,12 +45,19 @@ import org.apache.fineract.portfolio.savings.domain.SavingsProductDepositLimitKy
import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityConfigRepository;
import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityLimitRepository;
import org.apache.fineract.portfolio.savings.domain.SavingsProductDepositLimitKycRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEdit;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedFieldRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditChargeRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMappingRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFieldMapper;
import org.apache.fineract.portfolio.savings.exception.SavingsProductNoPendingEditException;
import org.apache.fineract.portfolio.savings.exception.SavingsProductNotFoundException;
import org.apache.fineract.portfolio.tax.data.TaxGroupData;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import java.util.Set;
@RequiredArgsConstructor
public class SavingsProductReadPlatformServiceImpl implements SavingsProductReadPlatformService {
@@ -63,6 +70,10 @@ public class SavingsProductReadPlatformServiceImpl implements SavingsProductRead
private final SavingsProductFacilityConfigRepository facilityConfigRepository;
private final SavingsProductFacilityLimitRepository facilityLimitRepository;
private final SavingsProductDepositLimitKycRepository depositLimitKycRepository;
private final ProductPendingEditRepository productPendingEditRepository;
private final ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository;
private final ProductPendingEditChargeRepository pendingEditChargeRepository;
private final ProductPendingEditGlMappingRepository pendingEditGlMappingRepository;
private static final Gson GSON = new Gson();
private static final Type OTHER_GL_MAPPINGS_TYPE = new TypeToken<List<Map<String, Object>>>() {}.getType();
private static final Type OTHER_GL_MAPPINGS_MAP_TYPE = new TypeToken<Map<String, List<Map<String, Object>>>>() {}.getType();
@@ -173,43 +184,49 @@ 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 "
+ "sp.name as savingsProductName, spt.createdby_id as makerId, maker.username as makerUsername, "
+ "spt.lastmodifiedby_id as checkerId, checker.username as checkerUsername, spt.created_date as createdOn, "
+ "spt.lastmodified_date as checkedOn, spt.status as status, spt.reject_reason as rejectReason "
+ "from m_saving_product_temp spt "
+ "join m_savings_product sp on sp.id = spt.product_id "
+ "join m_appuser maker on maker.id = spt.maker_id "
+ "left join m_appuser checker on checker.id = spt.checker_id "
+ "join m_appuser maker on maker.id = spt.createdby_id "
+ "left join m_appuser checker on checker.id = spt.lastmodifiedby_id "
+ "where spt.product_type = 'SAVINGS' ";
private 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;
};
private RowMapper<SavingsProductTempData> getPendingEditRowMapper() {
return (rs, rowNum) -> {
final SavingsProductTempData data = new SavingsProductTempData();
data.setId(rs.getLong("tempId"));
data.setSavingsProductId(rs.getLong("savingsProductId"));
data.setSavingsProductName(rs.getString("savingsProductName"));
data.setMakerId(rs.getLong("makerId"));
data.setMakerUsername(rs.getString("makerUsername"));
data.setCheckerId(JdbcSupport.getLong(rs, "checkerId"));
data.setCheckerUsername(rs.getString("checkerUsername"));
data.setCreatedOn(rs.getTimestamp("createdOn") == null ? null : rs.getTimestamp("createdOn").toLocalDateTime());
data.setCheckedOn(rs.getTimestamp("checkedOn") == null ? null : rs.getTimestamp("checkedOn").toLocalDateTime());
data.setStatus(rs.getString("status"));
data.setRejectReason(rs.getString("rejectReason"));
final Long tempId = data.getId();
final java.util.Optional<ProductPendingEdit> opt = this.productPendingEditRepository.findById(tempId);
if (opt.isPresent()) {
final ProductPendingEdit pendingEdit = opt.get();
final Set<String> submittedFields = new java.util.HashSet<>(
this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId()));
final Map<String, Object> proposedChanges = ProductPendingEditFieldMapper.reconstructProposedChangesMap(
pendingEdit, submittedFields, this.pendingEditChargeRepository, this.pendingEditGlMappingRepository, null, GSON);
data.setProposedChanges(proposedChanges);
}
return data;
};
}
@Override
public Collection<SavingsProductTempData> retrieveAllPendingEdits() {
this.context.authenticatedUser();
final String sql = "select " + PENDING_EDIT_SCHEMA + "and spt.status = 'PENDING' order by spt.created_on desc";
return this.jdbcTemplate.query(sql, PENDING_EDIT_MAPPER);
final String sql = "select " + PENDING_EDIT_SCHEMA + "and spt.status = 'PENDING' order by spt.created_date desc";
return this.jdbcTemplate.query(sql, getPendingEditRowMapper());
}
@Override
@@ -217,7 +234,7 @@ public class SavingsProductReadPlatformServiceImpl implements SavingsProductRead
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
return this.jdbcTemplate.queryForObject(sql, getPendingEditRowMapper(), new Object[] { productId }); // NOSONAR
} catch (final EmptyResultDataAccessException e) {
throw new SavingsProductNoPendingEditException(productId);
}
@@ -271,7 +288,7 @@ public class SavingsProductReadPlatformServiceImpl implements SavingsProductRead
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("exists(select 1 from m_savings_product_temp spt where spt.product_id = sp.id "
sqlBuilder.append("exists(select 1 from m_saving_product_temp spt where spt.product_id = sp.id "
+ "and spt.product_type = 'SAVINGS' and spt.status = 'PENDING') as isPendingApproval ");
sqlBuilder.append("from m_savings_product sp ");
@@ -87,6 +87,7 @@ import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
public class SavingsProductWritePlatformServiceJpaRepositoryImpl implements SavingsProductWritePlatformService {
private static final com.google.gson.Gson GSON = new com.google.gson.Gson();
private final PlatformSecurityContext context;
private final SavingsProductRepository savingProductRepository;
private final SavingsProductDataValidator fromApiJsonDataValidator;
@@ -324,7 +325,7 @@ public class SavingsProductWritePlatformServiceJpaRepositoryImpl implements Savi
// 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());
final ProductPendingEdit pendingEdit = new ProductPendingEdit(ProductPendingEdit.TYPE_SAVINGS, productId, maker);
// Typed-column storage: extract every recognised field into the pending edit's own columns
// (plus satellite rows for the 1:many pieces) instead of relying solely on the JSON blob
@@ -409,44 +410,32 @@ public class SavingsProductWritePlatformServiceJpaRepositoryImpl implements Savi
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 JsonObject root = new JsonObject();
root.addProperty("locale", "en");
root.addProperty("dateFormat", "yyyy-MM-dd");
ProductPendingEditFieldMapper.writeCoreFieldsToJson(root, pendingEdit.getCoreFields(), submittedFields);
ProductPendingEditFieldMapper.writeFacilityConfigToJson(root, pendingEdit.getFacilityConfigFields(),
submittedFields.contains("accountFacilityConfig"));
ProductPendingEditFieldMapper.writeDepositLimitKycToJson(root, pendingEdit.getDepositLimitKycFields(),
submittedFields.contains("depositLimitKyc"));
if (submittedFields.contains("glAccountMapping")) {
ProductPendingEditFieldMapper.writeGlAccountMappingToJson(root, pendingEdit.getGlAccountMappingFields());
}
if (submittedFields.contains("charges")) {
ProductPendingEditFieldMapper.writeChargesToJson(root,
this.pendingEditChargeRepository.findByPendingEditId(pendingEdit.getId()));
}
if (submittedFields.contains("accountFacilityLimits")) {
ProductPendingEditFieldMapper.writeFacilityLimitsToJson(root,
this.pendingEditFacilityLimitRepository.findByPendingEditId(pendingEdit.getId()));
}
if (submittedFields.contains("glArrayMappings")) {
ProductPendingEditFieldMapper.writeGlArrayMappingsToJson(root,
this.pendingEditGlMappingRepository.findByPendingEditId(pendingEdit.getId()));
}
final JsonCommand replayedCommand = JsonCommand.fromJsonElement(productId, root, this.fromApiJsonHelper);
final CommandProcessingResult result = update(productId, replayedCommand);
pendingEdit.markApproved(checker);
@@ -460,11 +449,6 @@ public class SavingsProductWritePlatformServiceJpaRepositoryImpl implements Savi
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());
@@ -472,12 +456,39 @@ public class SavingsProductWritePlatformServiceJpaRepositoryImpl implements Savi
return existing.get();
}
}
final Set<String> submittedFields = new java.util.HashSet<>(
this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId()));
final JsonObject root = new JsonObject();
root.addProperty("locale", "en");
root.addProperty("dateFormat", "yyyy-MM-dd");
ProductPendingEditFieldMapper.writeCoreFieldsToJson(root, pendingEdit.getCoreFields(), submittedFields);
ProductPendingEditFieldMapper.writeFacilityConfigToJson(root, pendingEdit.getFacilityConfigFields(),
submittedFields.contains("accountFacilityConfig"));
ProductPendingEditFieldMapper.writeDepositLimitKycToJson(root, pendingEdit.getDepositLimitKycFields(),
submittedFields.contains("depositLimitKyc"));
if (submittedFields.contains("glAccountMapping")) {
ProductPendingEditFieldMapper.writeGlAccountMappingToJson(root, pendingEdit.getGlAccountMappingFields());
}
if (submittedFields.contains("charges")) {
ProductPendingEditFieldMapper.writeChargesToJson(root,
this.pendingEditChargeRepository.findByPendingEditId(pendingEdit.getId()));
}
if (submittedFields.contains("accountFacilityLimits")) {
ProductPendingEditFieldMapper.writeFacilityLimitsToJson(root,
this.pendingEditFacilityLimitRepository.findByPendingEditId(pendingEdit.getId()));
}
if (submittedFields.contains("glArrayMappings")) {
ProductPendingEditFieldMapper.writeGlArrayMappingsToJson(root,
this.pendingEditGlMappingRepository.findByPendingEditId(pendingEdit.getId()));
}
final String commandJson = GSON.toJson(root);
final CommandSource auditEntry = CommandSource.builder() //
.actionName("UPDATE") //
.entityName("SAVINGSPRODUCT") //
.resourceGetUrl("/savingsproducts/" + productId) //
.resourceId(productId) //
.commandAsJson(pendingEdit.getProposedPayloadJson()) //
.commandAsJson(commandJson) //
.maker(pendingEdit.getMaker()) //
.madeOnDate(DateUtils.getAuditOffsetDateTime()) //
.status(CommandProcessingResultType.AWAITING_APPROVAL.getValue()) //
@@ -97,6 +97,9 @@ import org.apache.fineract.portfolio.savings.domain.SavingsProductDepositLimitKy
import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityConfig;
import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityConfigRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedFieldRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditChargeRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMappingRepository;
import org.apache.fineract.commands.domain.CommandSourceRepository;
import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityLimit;
import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityLimitRepository;
@@ -311,8 +314,14 @@ public class SavingsConfiguration {
@Bean
@ConditionalOnMissingBean(DepositProductReadPlatformService.class)
public DepositProductReadPlatformService depositProductReadPlatformService(PlatformSecurityContext context, JdbcTemplate jdbcTemplate,
InterestRateChartReadPlatformService interestRateChartReadPlatformService) {
return new DepositProductReadPlatformServiceImpl(context, jdbcTemplate, interestRateChartReadPlatformService);
InterestRateChartReadPlatformService interestRateChartReadPlatformService,
ProductPendingEditRepository productPendingEditRepository,
ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository,
ProductPendingEditChargeRepository pendingEditChargeRepository,
ProductPendingEditGlMappingRepository pendingEditGlMappingRepository) {
return new DepositProductReadPlatformServiceImpl(context, jdbcTemplate, interestRateChartReadPlatformService,
productPendingEditRepository, pendingEditSubmittedFieldRepository,
pendingEditChargeRepository, pendingEditGlMappingRepository);
}
@Bean
@@ -327,11 +336,16 @@ public class SavingsConfiguration {
FixedDepositProductRepository fixedDepositProductRepository, DepositProductDataValidator fromApiJsonDataValidator,
DepositProductAssembler depositProductAssembler,
ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService, InterestRateChartAssembler chartAssembler,
ProductPendingEditRepository productPendingEditRepository, FromJsonHelper fromApiJsonHelper,
ProductPendingEditRepository productPendingEditRepository,
ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository,
ProductPendingEditChargeRepository pendingEditChargeRepository,
ProductPendingEditGlMappingRepository pendingEditGlMappingRepository,
FromJsonHelper fromApiJsonHelper,
CommandSourceRepository commandSourceRepository) {
return new FixedDepositProductWritePlatformServiceJpaRepositoryImpl(context, fixedDepositProductRepository,
fromApiJsonDataValidator, depositProductAssembler, accountMappingWritePlatformService, chartAssembler,
productPendingEditRepository, fromApiJsonHelper, commandSourceRepository);
productPendingEditRepository, pendingEditSubmittedFieldRepository, pendingEditChargeRepository,
pendingEditGlMappingRepository, fromApiJsonHelper, commandSourceRepository);
}
@Bean
@@ -354,11 +368,16 @@ public class SavingsConfiguration {
RecurringDepositProductRepository recurringDepositProductRepository, DepositProductDataValidator fromApiJsonDataValidator,
DepositProductAssembler depositProductAssembler,
ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService, InterestRateChartAssembler chartAssembler,
ProductPendingEditRepository productPendingEditRepository, FromJsonHelper fromApiJsonHelper,
ProductPendingEditRepository productPendingEditRepository,
ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository,
ProductPendingEditChargeRepository pendingEditChargeRepository,
ProductPendingEditGlMappingRepository pendingEditGlMappingRepository,
FromJsonHelper fromApiJsonHelper,
CommandSourceRepository commandSourceRepository) {
return new RecurringDepositProductWritePlatformServiceJpaRepositoryImpl(context, recurringDepositProductRepository,
fromApiJsonDataValidator, depositProductAssembler, accountMappingWritePlatformService, chartAssembler,
productPendingEditRepository, fromApiJsonHelper, commandSourceRepository);
productPendingEditRepository, pendingEditSubmittedFieldRepository, pendingEditChargeRepository,
pendingEditGlMappingRepository, fromApiJsonHelper, commandSourceRepository);
}
@Bean
@@ -481,9 +500,15 @@ public class SavingsConfiguration {
@ConditionalOnMissingBean(SavingsProductReadPlatformService.class)
public SavingsProductReadPlatformService savingsProductReadPlatformService(PlatformSecurityContext context, JdbcTemplate jdbcTemplate,
FineractEntityAccessUtil fineractEntityAccessUtil, SavingsProductFacilityConfigRepository facilityConfigRepository,
SavingsProductFacilityLimitRepository facilityLimitRepository, SavingsProductDepositLimitKycRepository depositLimitKycRepository) {
SavingsProductFacilityLimitRepository facilityLimitRepository, SavingsProductDepositLimitKycRepository depositLimitKycRepository,
ProductPendingEditRepository productPendingEditRepository,
ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository,
ProductPendingEditChargeRepository pendingEditChargeRepository,
ProductPendingEditGlMappingRepository pendingEditGlMappingRepository) {
return new SavingsProductReadPlatformServiceImpl(context, jdbcTemplate, fineractEntityAccessUtil, facilityConfigRepository,
facilityLimitRepository, depositLimitKycRepository);
facilityLimitRepository, depositLimitKycRepository,
productPendingEditRepository, pendingEditSubmittedFieldRepository,
pendingEditChargeRepository, pendingEditGlMappingRepository);
}
@Bean
@@ -56,6 +56,12 @@ import org.apache.fineract.portfolio.shareproducts.exception.ShareProductNoPendi
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEdit;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedFieldRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditChargeRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMappingRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFieldMapper;
@RequiredArgsConstructor
public class ShareProductReadPlatformServiceImpl implements ShareProductReadPlatformService {
@@ -66,6 +72,10 @@ public class ShareProductReadPlatformServiceImpl implements ShareProductReadPlat
private final ShareProductDropdownReadPlatformService shareProductDropdownReadPlatformService;
private final AccountingDropdownReadPlatformService accountingDropdownReadPlatformService;
private final ProductToGLAccountMappingReadPlatformService accountMappingReadPlatformService;
private final ProductPendingEditRepository productPendingEditRepository;
private final ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository;
private final ProductPendingEditChargeRepository pendingEditChargeRepository;
private final ProductPendingEditGlMappingRepository pendingEditGlMappingRepository;
private final PaginationHelper shareProductDataPaginationHelper;
private final DatabaseSpecificSQLGenerator sqlGenerator;
@@ -159,47 +169,55 @@ public class ShareProductReadPlatformServiceImpl implements ShareProductReadPlat
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 "
+ "sp.name as savingsProductName, spt.createdby_id as makerId, maker.username as makerUsername, "
+ "spt.lastmodifiedby_id as checkerId, checker.username as checkerUsername, spt.created_date as createdOn, "
+ "spt.lastmodified_date as checkedOn, spt.status as status, spt.reject_reason as rejectReason "
+ "from m_saving_product_temp spt "
+ "join m_share_product sp on sp.id = spt.product_id "
+ "join m_appuser maker on maker.id = spt.maker_id "
+ "left join m_appuser checker on checker.id = spt.checker_id "
+ "join m_appuser maker on maker.id = spt.createdby_id "
+ "left join m_appuser checker on checker.id = spt.lastmodifiedby_id "
+ "where spt.product_type = 'SHARE' ";
private 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 RowMapper<SavingsProductTempData> getPendingEditRowMapper() {
return (rs, rowNum) -> {
final SavingsProductTempData data = new SavingsProductTempData();
data.setId(rs.getLong("tempId"));
data.setSavingsProductId(rs.getLong("savingsProductId"));
data.setSavingsProductName(rs.getString("savingsProductName"));
data.setMakerId(rs.getLong("makerId"));
data.setMakerUsername(rs.getString("makerUsername"));
data.setCheckerId(JdbcSupport.getLong(rs, "checkerId"));
data.setCheckerUsername(rs.getString("checkerUsername"));
data.setCreatedOn(rs.getTimestamp("createdOn") == null ? null : rs.getTimestamp("createdOn").toLocalDateTime());
data.setCheckedOn(rs.getTimestamp("checkedOn") == null ? null : rs.getTimestamp("checkedOn").toLocalDateTime());
data.setStatus(rs.getString("status"));
data.setRejectReason(rs.getString("rejectReason"));
final Long tempId = data.getId();
final java.util.Optional<ProductPendingEdit> opt = this.productPendingEditRepository.findById(tempId);
if (opt.isPresent()) {
final ProductPendingEdit pendingEdit = opt.get();
final Set<String> submittedFields = new java.util.HashSet<>(
this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId()));
final Map<String, Object> proposedChanges = ProductPendingEditFieldMapper.reconstructProposedChangesMap(
pendingEdit, submittedFields, this.pendingEditChargeRepository, this.pendingEditGlMappingRepository, null, GSON);
data.setProposedChanges(proposedChanges);
}
return data;
};
}
@Override
public Collection<SavingsProductTempData> retrieveAllPendingEdits() {
final String sql = "select " + PENDING_EDIT_SCHEMA + "and spt.status = 'PENDING' order by spt.created_on desc";
return this.jdbcTemplate.query(sql, PENDING_EDIT_MAPPER);
final String sql = "select " + PENDING_EDIT_SCHEMA + "and spt.status = 'PENDING' order by spt.created_date desc";
return this.jdbcTemplate.query(sql, getPendingEditRowMapper());
}
@Override
public SavingsProductTempData retrievePendingEdit(final Long productId) {
final String sql = "select " + PENDING_EDIT_SCHEMA + "and spt.status = 'PENDING' and spt.product_id = ?";
try {
return this.jdbcTemplate.queryForObject(sql, PENDING_EDIT_MAPPER, new Object[] { productId }); // NOSONAR
return this.jdbcTemplate.queryForObject(sql, getPendingEditRowMapper(), new Object[] { productId }); // NOSONAR
} catch (final EmptyResultDataAccessException e) {
throw new ShareProductNoPendingEditException(productId);
}
@@ -22,7 +22,9 @@ import com.google.gson.JsonElement;
import jakarta.persistence.PersistenceException;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
@@ -52,6 +54,14 @@ import org.apache.fineract.portfolio.shareproducts.exception.ShareProductEditPen
import org.apache.fineract.portfolio.shareproducts.exception.ShareProductNoPendingEditException;
import org.apache.fineract.portfolio.shareproducts.exception.ShareProductSelfApprovalNotAllowedException;
import org.apache.fineract.portfolio.shareproducts.serialization.ShareProductDataSerializer;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedField;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedFieldRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditCharge;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditChargeRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMapping;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMappingRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFieldMapper;
import com.google.gson.JsonObject;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.orm.jpa.JpaSystemException;
@@ -61,6 +71,7 @@ import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
public class ShareProductWritePlatformServiceJpaRepositoryImpl implements ShareProductWritePlatformService {
private static final com.google.gson.Gson GSON = new com.google.gson.Gson();
private final ShareProductRepositoryWrapper repository;
private final ShareProductDataSerializer serializer;
private final FromJsonHelper fromApiJsonHelper;
@@ -70,6 +81,9 @@ public class ShareProductWritePlatformServiceJpaRepositoryImpl implements ShareP
private final BusinessEventNotifierService businessEventNotifierService;
private final PlatformSecurityContext context;
private final ProductPendingEditRepository productPendingEditRepository;
private final ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository;
private final ProductPendingEditChargeRepository pendingEditChargeRepository;
private final ProductPendingEditGlMappingRepository pendingEditGlMappingRepository;
private final CommandSourceRepository commandSourceRepository;
@Override
@@ -208,7 +222,13 @@ public class ShareProductWritePlatformServiceJpaRepositoryImpl implements ShareP
throw new ShareProductEditPendingApprovalException(productId);
}
final ProductPendingEdit pendingEdit = new ProductPendingEdit(ProductPendingEdit.TYPE_SHARE, productId, maker, command.json());
final ProductPendingEdit pendingEdit = new ProductPendingEdit(ProductPendingEdit.TYPE_SHARE, productId, maker);
final java.util.Set<String> submitted = ProductPendingEditFieldMapper.populateShareOnlyFields(command, pendingEdit.getShareOnlyFields());
final boolean glMappingSubmitted = ProductPendingEditFieldMapper.populateGlAccountMapping(command, pendingEdit.getGlAccountMappingFields());
if (glMappingSubmitted) {
submitted.add("glAccountMapping");
}
final CommandSource auditEntry = CommandSource.builder() //
.actionName("UPDATE") //
@@ -227,6 +247,24 @@ public class ShareProductWritePlatformServiceJpaRepositoryImpl implements ShareP
this.productPendingEditRepository.saveAndFlush(pendingEdit);
for (final String fieldName : submitted) {
this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, fieldName));
}
final List<Long> chargeIds = ProductPendingEditFieldMapper.extractChargeIds(command);
if (chargeIds != null) {
this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, "charges"));
for (final Long chargeId : chargeIds) {
this.pendingEditChargeRepository.save(new ProductPendingEditCharge(pendingEdit, chargeId));
}
}
final List<ProductPendingEditGlMapping> glMappings = ProductPendingEditFieldMapper.extractGlArrayMappings(command, pendingEdit);
if (glMappings != null) {
this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, "glArrayMappings"));
this.pendingEditGlMappingRepository.saveAll(glMappings);
}
return new CommandProcessingResultBuilder() //
.withEntityId(product.getId()) //
.build();
@@ -245,8 +283,23 @@ public class ShareProductWritePlatformServiceJpaRepositoryImpl implements ShareP
throw new ShareProductSelfApprovalNotAllowedException(productId);
}
final JsonCommand replayedCommand = JsonCommand.fromJsonElement(productId,
this.fromApiJsonHelper.parse(pendingEdit.getProposedPayloadJson()), this.fromApiJsonHelper);
final Set<String> submittedFields = new java.util.HashSet<>(
this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId()));
final JsonObject root = new JsonObject();
root.addProperty("locale", "en");
ProductPendingEditFieldMapper.writeShareOnlyFieldsToJson(root, pendingEdit.getShareOnlyFields(), submittedFields);
if (submittedFields.contains("glAccountMapping")) {
ProductPendingEditFieldMapper.writeGlAccountMappingToJson(root, pendingEdit.getGlAccountMappingFields());
}
if (submittedFields.contains("charges")) {
ProductPendingEditFieldMapper.writeChargesToJson(root,
this.pendingEditChargeRepository.findByPendingEditId(pendingEdit.getId()));
}
if (submittedFields.contains("glArrayMappings")) {
ProductPendingEditFieldMapper.writeGlArrayMappingsToJson(root,
this.pendingEditGlMappingRepository.findByPendingEditId(pendingEdit.getId()));
}
final JsonCommand replayedCommand = JsonCommand.fromJsonElement(productId, root, this.fromApiJsonHelper);
final CommandProcessingResult result = updateProduct(productId, replayedCommand);
pendingEdit.markApproved(checker);
@@ -267,12 +320,30 @@ public class ShareProductWritePlatformServiceJpaRepositoryImpl implements ShareP
return existing.get();
}
}
final Set<String> submittedFields = new java.util.HashSet<>(
this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId()));
final JsonObject root = new JsonObject();
root.addProperty("locale", "en");
ProductPendingEditFieldMapper.writeShareOnlyFieldsToJson(root, pendingEdit.getShareOnlyFields(), submittedFields);
if (submittedFields.contains("glAccountMapping")) {
ProductPendingEditFieldMapper.writeGlAccountMappingToJson(root, pendingEdit.getGlAccountMappingFields());
}
if (submittedFields.contains("charges")) {
ProductPendingEditFieldMapper.writeChargesToJson(root,
this.pendingEditChargeRepository.findByPendingEditId(pendingEdit.getId()));
}
if (submittedFields.contains("glArrayMappings")) {
ProductPendingEditFieldMapper.writeGlArrayMappingsToJson(root,
this.pendingEditGlMappingRepository.findByPendingEditId(pendingEdit.getId()));
}
final String commandJson = GSON.toJson(root);
final CommandSource auditEntry = CommandSource.builder() //
.actionName("UPDATE") //
.entityName("SHAREPRODUCT") //
.resourceGetUrl("/products/share/" + productId) //
.resourceId(productId) //
.commandAsJson(pendingEdit.getProposedPayloadJson()) //
.commandAsJson(commandJson) //
.maker(pendingEdit.getMaker()) //
.madeOnDate(DateUtils.getAuditOffsetDateTime()) //
.status(CommandProcessingResultType.AWAITING_APPROVAL.getValue()) //
@@ -32,6 +32,9 @@ import org.apache.fineract.organisation.monetary.service.CurrencyReadPlatformSer
import org.apache.fineract.portfolio.charge.service.ChargeReadPlatformService;
import org.apache.fineract.portfolio.products.service.ShareProductReadPlatformService;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedFieldRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditChargeRepository;
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMappingRepository;
import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountReadPlatformService;
import org.apache.fineract.portfolio.shareproducts.domain.ShareProductDividentPayOutDetailsRepositoryWrapper;
import org.apache.fineract.portfolio.shareproducts.domain.ShareProductRepositoryWrapper;
@@ -89,9 +92,15 @@ public class ShareProductsConfiguration {
ShareProductDropdownReadPlatformService shareProductDropdownReadPlatformService,
AccountingDropdownReadPlatformService accountingDropdownReadPlatformService,
ProductToGLAccountMappingReadPlatformService accountMappingReadPlatformService,
PaginationHelper shareProductDataPaginationHelper, DatabaseSpecificSQLGenerator sqlGenerator) {
PaginationHelper shareProductDataPaginationHelper, DatabaseSpecificSQLGenerator sqlGenerator,
ProductPendingEditRepository productPendingEditRepository,
ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository,
ProductPendingEditChargeRepository pendingEditChargeRepository,
ProductPendingEditGlMappingRepository pendingEditGlMappingRepository) {
return new ShareProductReadPlatformServiceImpl(jdbcTemplate, currencyReadPlatformService, chargeReadPlatformService,
shareProductDropdownReadPlatformService, accountingDropdownReadPlatformService, accountMappingReadPlatformService,
productPendingEditRepository, pendingEditSubmittedFieldRepository,
pendingEditChargeRepository, pendingEditGlMappingRepository,
shareProductDataPaginationHelper, sqlGenerator);
}
@@ -105,10 +114,14 @@ public class ShareProductsConfiguration {
BusinessEventNotifierService businessEventNotifierService,
PlatformSecurityContext context,
ProductPendingEditRepository productPendingEditRepository,
ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository,
ProductPendingEditChargeRepository pendingEditChargeRepository,
ProductPendingEditGlMappingRepository pendingEditGlMappingRepository,
CommandSourceRepository commandSourceRepository) {
return new ShareProductWritePlatformServiceJpaRepositoryImpl(repository, serializer, fromApiJsonHelper,
shareProductDividentPayOutDetailsRepository, shareProductDividendAssembler, accountMappingWritePlatformService,
businessEventNotifierService, context, productPendingEditRepository, commandSourceRepository);
businessEventNotifierService, context, productPendingEditRepository, pendingEditSubmittedFieldRepository,
pendingEditChargeRepository, pendingEditGlMappingRepository, commandSourceRepository);
}
}
@@ -39,7 +39,7 @@ import org.apache.fineract.useradministration.domain.AppUser;
* (covering charges, GL/accounting mappings and other related tables, not just scalar columns).
*/
@Entity
@Table(name = "m_savings_product_temp")
@Table(name = "m_saving_product_temp")
@Getter
@Setter
public class ProductPendingEdit extends AbstractPersistableCustom<Long> {
@@ -61,17 +61,17 @@ public class ProductPendingEdit extends AbstractPersistableCustom<Long> {
private String productType;
@ManyToOne
@JoinColumn(name = "maker_id", nullable = false)
@JoinColumn(name = "createdby_id", nullable = false)
private AppUser maker;
@ManyToOne
@JoinColumn(name = "checker_id")
@JoinColumn(name = "lastmodifiedby_id")
private AppUser checker;
@Column(name = "created_on", nullable = false)
@Column(name = "created_date", nullable = false)
private LocalDateTime createdOn;
@Column(name = "checked_on")
@Column(name = "lastmodified_date")
private LocalDateTime checkedOn;
@Column(name = "status", nullable = false, length = 20)
@@ -80,9 +80,7 @@ public class ProductPendingEdit extends AbstractPersistableCustom<Long> {
@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
@@ -130,11 +128,10 @@ public class ProductPendingEdit extends AbstractPersistableCustom<Long> {
protected ProductPendingEdit() {}
public ProductPendingEdit(final String productType, final Long productId, final AppUser maker, final String proposedPayloadJson) {
public ProductPendingEdit(final String productType, final Long productId, final AppUser maker) {
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);
@@ -176,9 +173,7 @@ public class ProductPendingEdit extends AbstractPersistableCustom<Long> {
return this.rejectReason;
}
public String getProposedPayloadJson() {
return this.proposedPayloadJson;
}
public Long getCommandSourceId() {
return this.commandSourceId;
@@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.savings.domain;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.HashSet;
import java.util.List;
@@ -308,10 +309,25 @@ public final class ProductPendingEditFieldMapper {
f.setAllowDebitTxnAfterDueDate(command.booleanObjectValueOfParameterNamed("allowDebitTxnAfterDueDate"));
present.add("allowDebitTxnAfterDueDate");
}
if (command.hasParameter("depositTypeEnum")) {
f.setDepositTypeEnum(command.integerValueOfParameterNamed("depositTypeEnum"));
present.add("depositTypeEnum");
}
if (command.hasParameter("withdrawalFeeAmount")) {
f.setWithdrawalFeeAmount(command.bigDecimalValueOfParameterNamed("withdrawalFeeAmount"));
present.add("withdrawalFeeAmount");
}
if (command.hasParameter("withdrawalFeeTypeEnum")) {
f.setWithdrawalFeeTypeEnum(command.integerValueOfParameterNamed("withdrawalFeeTypeEnum"));
present.add("withdrawalFeeTypeEnum");
}
return present;
}
public static void writeCoreFieldsToJson(final JsonObject root, final SavingsCoreEditFields f, final Set<String> submitted) {
if (f == null) {
return;
}
putString(root, submitted, "name", f.getName());
putString(root, submitted, "shortName", f.getShortName());
putString(root, submitted, "description", f.getDescription());
@@ -377,6 +393,9 @@ public final class ProductPendingEditFieldMapper {
putBoolean(root, submitted, "noPenaltyNomineeHeirClosure", f.getNoPenaltyNomineeHeirClosure());
putBoolean(root, submitted, "deceasedSettlement", f.getDeceasedSettlement());
putBoolean(root, submitted, "allowDebitTxnAfterDueDate", f.getAllowDebitTxnAfterDueDate());
putNumber(root, submitted, "depositTypeEnum", f.getDepositTypeEnum());
putNumber(root, submitted, "withdrawalFeeAmount", f.getWithdrawalFeeAmount());
putNumber(root, submitted, "withdrawalFeeTypeEnum", f.getWithdrawalFeeTypeEnum());
}
// ---- accountFacilityConfig (1:1 container) --------------------------------------------------
@@ -405,7 +424,7 @@ public final class ProductPendingEditFieldMapper {
}
public static void writeFacilityConfigToJson(final JsonObject root, final FacilityConfigEditFields f, final boolean submitted) {
if (!submitted) {
if (f == null || !submitted) {
return;
}
final JsonObject config = new JsonObject();
@@ -460,7 +479,7 @@ public final class ProductPendingEditFieldMapper {
}
public static void writeDepositLimitKycToJson(final JsonObject root, final DepositLimitKycEditFields f, final boolean submitted) {
if (!submitted) {
if (f == null || !submitted) {
return;
}
final JsonObject kyc = new JsonObject();
@@ -505,6 +524,9 @@ public final class ProductPendingEditFieldMapper {
}
public static void writeGlAccountMappingToJson(final JsonObject root, final GlAccountMappingEditFields f) {
if (f == null) {
return;
}
putNumber(root, "savingsReferenceAccountId", f.getSavingsReferenceAccountId());
putNumber(root, "savingsControlAccountId", f.getSavingsControlAccountId());
putNumber(root, "incomeFromFeeAccountId", f.getIncomeFromFeeAccountId());
@@ -693,4 +715,282 @@ public final class ProductPendingEditFieldMapper {
root.addProperty(name, value);
}
}
public static Set<String> populateCurrentOnlyFields(final JsonCommand command, final CurrentOnlyEditFields f) {
final Set<String> present = new java.util.HashSet<>();
if (command.hasParameter("avgBalanceCalcPeriod")) {
f.setAvgBalanceCalcPeriod(command.stringValueOfParameterNamed("avgBalanceCalcPeriod"));
present.add("avgBalanceCalcPeriod");
}
if (command.hasParameter("validForPeriod")) {
f.setValidForPeriod(command.stringValueOfParameterNamed("validForPeriod"));
present.add("validForPeriod");
}
if (command.hasParameter("odAccountClosedZeroBalanceInDayClosing")) {
f.setOdAccountClosedZeroBalanceInDayClosing(command.booleanObjectValueOfParameterNamed("odAccountClosedZeroBalanceInDayClosing"));
present.add("odAccountClosedZeroBalanceInDayClosing");
}
return present;
}
public static void writeCurrentOnlyFieldsToJson(final JsonObject root, final CurrentOnlyEditFields f, final Set<String> submitted) {
if (f == null) {
return;
}
putString(root, submitted, "avgBalanceCalcPeriod", f.getAvgBalanceCalcPeriod());
putString(root, submitted, "validForPeriod", f.getValidForPeriod());
putBoolean(root, submitted, "odAccountClosedZeroBalanceInDayClosing", f.getOdAccountClosedZeroBalanceInDayClosing());
}
public static Set<String> populateShareOnlyFields(final JsonCommand command, final ShareOnlyEditFields f) {
final Set<String> present = new java.util.HashSet<>();
if (command.hasParameter("totalShares")) {
f.setTotalShares(command.longValueOfParameterNamed("totalShares"));
present.add("totalShares");
}
if (command.hasParameter("totalSharesIssued") || command.hasParameter("issuedShares")) {
final Long val = command.hasParameter("totalSharesIssued") ? command.longValueOfParameterNamed("totalSharesIssued") : command.longValueOfParameterNamed("issuedShares");
f.setTotalSharesIssued(val);
present.add("totalSharesIssued");
}
if (command.hasParameter("unitPrice")) {
f.setUnitPrice(command.bigDecimalValueOfParameterNamed("unitPrice"));
present.add("unitPrice");
}
if (command.hasParameter("minimumShares") || command.hasParameter("minimumClientShares")) {
final Long val = command.hasParameter("minimumShares") ? command.longValueOfParameterNamed("minimumShares") : command.longValueOfParameterNamed("minimumClientShares");
f.setMinimumShares(val);
present.add("minimumShares");
}
if (command.hasParameter("nominalShares") || command.hasParameter("nominalClientShares")) {
final Long val = command.hasParameter("nominalShares") ? command.longValueOfParameterNamed("nominalShares") : command.longValueOfParameterNamed("nominalClientShares");
f.setNominalShares(val);
present.add("nominalShares");
}
if (command.hasParameter("maximumShares") || command.hasParameter("maximumClientShares")) {
final Long val = command.hasParameter("maximumShares") ? command.longValueOfParameterNamed("maximumShares") : command.longValueOfParameterNamed("maximumClientShares");
f.setMaximumShares(val);
present.add("maximumShares");
}
if (command.hasParameter("allowDividendCalcForInactiveClients") || command.hasParameter("allowDividendsInactiveClients")) {
final Boolean val = command.hasParameter("allowDividendCalcForInactiveClients") ? command.booleanObjectValueOfParameterNamed("allowDividendCalcForInactiveClients") : command.booleanObjectValueOfParameterNamed("allowDividendsInactiveClients");
f.setAllowDividendCalcForInactiveClients(val);
present.add("allowDividendCalcForInactiveClients");
}
if (command.hasParameter("minimumActivePeriodForDividends") || command.hasParameter("minimumActivePeriodFrequency")) {
final Integer val = command.hasParameter("minimumActivePeriodForDividends") ? command.integerValueOfParameterNamed("minimumActivePeriodForDividends") : command.integerValueOfParameterNamed("minimumActivePeriodFrequency");
f.setMinimumActivePeriodForDividends(val);
present.add("minimumActivePeriodForDividends");
}
if (command.hasParameter("minimumActivePeriodFrequencyType") || command.hasParameter("minimumActivePeriodFrequencyEnum")) {
final Integer val = command.hasParameter("minimumActivePeriodFrequencyType") ? command.integerValueOfParameterNamed("minimumActivePeriodFrequencyType") : command.integerValueOfParameterNamed("minimumActivePeriodFrequencyEnum");
f.setMinimumActivePeriodFrequencyType(val);
present.add("minimumActivePeriodFrequencyType");
}
if (command.hasParameter("shareLockinPeriod")) {
f.setShareLockinPeriod(command.integerValueOfParameterNamed("shareLockinPeriod"));
present.add("shareLockinPeriod");
}
if (command.hasParameter("shareLockinPeriodFrequencyType")) {
f.setShareLockinPeriodFrequencyType(command.integerValueOfParameterNamed("shareLockinPeriodFrequencyType"));
present.add("shareLockinPeriodFrequencyType");
}
if (command.hasParameter("externalId")) {
f.setExternalId(command.stringValueOfParameterNamed("externalId"));
present.add("externalId");
}
if (command.hasParameter("startDate")) {
f.setStartDate(command.localDateValueOfParameterNamed("startDate"));
present.add("startDate");
}
if (command.hasParameter("totalsubscribedShares")) {
f.setTotalsubscribedShares(command.longValueOfParameterNamed("totalsubscribedShares"));
present.add("totalsubscribedShares");
}
if (command.hasParameter("capitalAmount") || command.hasParameter("shareCapital")) {
final BigDecimal val = command.hasParameter("capitalAmount") ? command.bigDecimalValueOfParameterNamed("capitalAmount") : command.bigDecimalValueOfParameterNamed("shareCapital");
f.setCapitalAmount(val);
present.add("shareCapital");
}
return present;
}
public static void writeShareOnlyFieldsToJson(final JsonObject root, final ShareOnlyEditFields f, final Set<String> submitted) {
if (f == null) {
return;
}
putNumber(root, submitted, "totalShares", f.getTotalShares());
putNumber(root, submitted, "totalSharesIssued", f.getTotalSharesIssued());
putNumber(root, submitted, "unitPrice", f.getUnitPrice());
putNumber(root, submitted, "minimumShares", f.getMinimumShares());
putNumber(root, submitted, "nominalShares", f.getNominalShares());
putNumber(root, submitted, "maximumShares", f.getMaximumShares());
putBoolean(root, submitted, "allowDividendCalcForInactiveClients", f.getAllowDividendCalcForInactiveClients());
putNumber(root, submitted, "minimumActivePeriodForDividends", f.getMinimumActivePeriodForDividends());
putNumber(root, submitted, "minimumActivePeriodFrequencyType", f.getMinimumActivePeriodFrequencyType());
putNumber(root, submitted, "shareLockinPeriod", f.getShareLockinPeriod());
putNumber(root, submitted, "shareLockinPeriodFrequencyType", f.getShareLockinPeriodFrequencyType());
putString(root, submitted, "externalId", f.getExternalId());
putDate(root, submitted, "startDate", f.getStartDate());
putNumber(root, submitted, "totalsubscribedShares", f.getTotalsubscribedShares());
if (submitted.contains("capitalAmount") || submitted.contains("shareCapital")) {
if (f.getCapitalAmount() != null) {
root.addProperty("shareCapital", f.getCapitalAmount());
}
}
}
public static Set<String> populateFixedDepositFields(final JsonCommand command, final FixedDepositEditFields f) {
final Set<String> present = new java.util.HashSet<>();
if (command.hasParameter("fdType")) {
f.setFdType(command.stringValueOfParameterNamed("fdType"));
present.add("fdType");
}
if (command.hasParameter("preClosurePenalApplicable")) {
f.setPreClosurePenalApplicable(command.booleanObjectValueOfParameterNamed("preClosurePenalApplicable"));
present.add("preClosurePenalApplicable");
}
if (command.hasParameter("preClosurePenalInterest")) {
f.setPreClosurePenalInterest(command.bigDecimalValueOfParameterNamed("preClosurePenalInterest"));
present.add("preClosurePenalInterest");
}
if (command.hasParameter("preClosurePenalInterestOnTypeId")) {
f.setPreClosurePenalInterestOnTypeId(command.integerValueOfParameterNamed("preClosurePenalInterestOnTypeId"));
present.add("preClosurePenalInterestOnTypeId");
}
if (command.hasParameter("minDepositTerm")) {
f.setMinDepositTerm(command.integerValueOfParameterNamed("minDepositTerm"));
present.add("minDepositTerm");
}
if (command.hasParameter("maxDepositTerm")) {
f.setMaxDepositTerm(command.integerValueOfParameterNamed("maxDepositTerm"));
present.add("maxDepositTerm");
}
if (command.hasParameter("minDepositTermTypeId")) {
f.setMinDepositTermTypeId(command.integerValueOfParameterNamed("minDepositTermTypeId"));
present.add("minDepositTermTypeId");
}
if (command.hasParameter("maxDepositTermTypeId")) {
f.setMaxDepositTermTypeId(command.integerValueOfParameterNamed("maxDepositTermTypeId"));
present.add("maxDepositTermTypeId");
}
if (command.hasParameter("inMultiplesOfDepositTerm")) {
f.setInMultiplesOfDepositTerm(command.integerValueOfParameterNamed("inMultiplesOfDepositTerm"));
present.add("inMultiplesOfDepositTerm");
}
if (command.hasParameter("inMultiplesOfDepositTermTypeId")) {
f.setInMultiplesOfDepositTermTypeId(command.integerValueOfParameterNamed("inMultiplesOfDepositTermTypeId"));
present.add("inMultiplesOfDepositTermTypeId");
}
if (command.hasParameter("minDepositAmount")) {
f.setMinDepositAmount(command.bigDecimalValueOfParameterNamed("minDepositAmount"));
present.add("minDepositAmount");
}
if (command.hasParameter("maxDepositAmount")) {
f.setMaxDepositAmount(command.bigDecimalValueOfParameterNamed("maxDepositAmount"));
present.add("maxDepositAmount");
}
if (command.hasParameter("depositAmount")) {
f.setDepositAmount(command.bigDecimalValueOfParameterNamed("depositAmount"));
present.add("depositAmount");
}
return present;
}
public static void writeFixedDepositFieldsToJson(final JsonObject root, final FixedDepositEditFields f, final Set<String> submitted) {
if (f == null) {
return;
}
if (f.getFdType() != null) putString(root, submitted, "fdType", f.getFdType());
if (f.getPreClosurePenalApplicable() != null) putBoolean(root, submitted, "preClosurePenalApplicable", f.getPreClosurePenalApplicable());
if (f.getPreClosurePenalInterest() != null) putNumber(root, submitted, "preClosurePenalInterest", f.getPreClosurePenalInterest());
if (f.getPreClosurePenalInterestOnTypeId() != null) putNumber(root, submitted, "preClosurePenalInterestOnTypeId", f.getPreClosurePenalInterestOnTypeId());
if (f.getMinDepositTerm() != null) putNumber(root, submitted, "minDepositTerm", f.getMinDepositTerm());
if (f.getMaxDepositTerm() != null) putNumber(root, submitted, "maxDepositTerm", f.getMaxDepositTerm());
if (f.getMinDepositTermTypeId() != null) putNumber(root, submitted, "minDepositTermTypeId", f.getMinDepositTermTypeId());
if (f.getMaxDepositTermTypeId() != null) putNumber(root, submitted, "maxDepositTermTypeId", f.getMaxDepositTermTypeId());
if (f.getInMultiplesOfDepositTerm() != null) putNumber(root, submitted, "inMultiplesOfDepositTerm", f.getInMultiplesOfDepositTerm());
if (f.getInMultiplesOfDepositTermTypeId() != null) putNumber(root, submitted, "inMultiplesOfDepositTermTypeId", f.getInMultiplesOfDepositTermTypeId());
if (f.getMinDepositAmount() != null) putNumber(root, submitted, "minDepositAmount", f.getMinDepositAmount());
if (f.getMaxDepositAmount() != null) putNumber(root, submitted, "maxDepositAmount", f.getMaxDepositAmount());
if (f.getDepositAmount() != null) putNumber(root, submitted, "depositAmount", f.getDepositAmount());
}
public static Set<String> populateRecurringDepositFields(final JsonCommand command, final RecurringDepositEditFields f) {
final Set<String> present = new java.util.HashSet<>();
if (command.hasParameter("isMandatoryDeposit")) {
f.setIsMandatoryDeposit(command.booleanObjectValueOfParameterNamed("isMandatoryDeposit"));
present.add("isMandatoryDeposit");
}
if (command.hasParameter("allowWithdrawal")) {
f.setAllowWithdrawal(command.booleanObjectValueOfParameterNamed("allowWithdrawal"));
present.add("allowWithdrawal");
}
if (command.hasParameter("adjustAdvanceTowardsFuturePayments")) {
f.setAdjustAdvanceTowardsFuturePayments(command.booleanObjectValueOfParameterNamed("adjustAdvanceTowardsFuturePayments"));
present.add("adjustAdvanceTowardsFuturePayments");
}
return present;
}
public static void writeRecurringDepositFieldsToJson(final JsonObject root, final RecurringDepositEditFields f, final Set<String> submitted) {
if (f == null) {
return;
}
if (f.getIsMandatoryDeposit() != null) putBoolean(root, submitted, "isMandatoryDeposit", f.getIsMandatoryDeposit());
if (f.getAllowWithdrawal() != null) putBoolean(root, submitted, "allowWithdrawal", f.getAllowWithdrawal());
if (f.getAdjustAdvanceTowardsFuturePayments() != null) putBoolean(root, submitted, "adjustAdvanceTowardsFuturePayments", f.getAdjustAdvanceTowardsFuturePayments());
}
public static java.util.Map<String, Object> reconstructProposedChangesMap(
final ProductPendingEdit pendingEdit,
final Set<String> submittedFields,
final ProductPendingEditChargeRepository pendingEditChargeRepository,
final ProductPendingEditGlMappingRepository pendingEditGlMappingRepository,
final ProductPendingEditFacilityLimitRepository pendingEditFacilityLimitRepository,
final com.google.gson.Gson gson) {
final JsonObject root = new JsonObject();
root.addProperty("locale", "en");
root.addProperty("dateFormat", "yyyy-MM-dd");
final String productType = pendingEdit.getProductType();
if ((ProductPendingEdit.TYPE_SAVINGS.equals(productType) || ProductPendingEdit.TYPE_CURRENT.equals(productType) || ProductPendingEdit.TYPE_FD.equals(productType) || ProductPendingEdit.TYPE_RD.equals(productType)) && pendingEdit.getCoreFields() != null) {
writeCoreFieldsToJson(root, pendingEdit.getCoreFields(), submittedFields);
}
if (ProductPendingEdit.TYPE_CURRENT.equals(productType) && pendingEdit.getCurrentOnlyFields() != null) {
writeCurrentOnlyFieldsToJson(root, pendingEdit.getCurrentOnlyFields(), submittedFields);
}
if (ProductPendingEdit.TYPE_SAVINGS.equals(productType) || ProductPendingEdit.TYPE_CURRENT.equals(productType)) {
if (submittedFields.contains("accountFacilityConfig") && pendingEdit.getFacilityConfigFields() != null) {
writeFacilityConfigToJson(root, pendingEdit.getFacilityConfigFields(), true);
}
if (submittedFields.contains("depositLimitKyc") && pendingEdit.getDepositLimitKycFields() != null) {
writeDepositLimitKycToJson(root, pendingEdit.getDepositLimitKycFields(), true);
}
if (submittedFields.contains("accountFacilityLimits") && pendingEditFacilityLimitRepository != null) {
writeFacilityLimitsToJson(root, pendingEditFacilityLimitRepository.findByPendingEditId(pendingEdit.getId()));
}
}
if ((ProductPendingEdit.TYPE_FD.equals(productType) || ProductPendingEdit.TYPE_RD.equals(productType)) && pendingEdit.getFixedDepositFields() != null) {
writeFixedDepositFieldsToJson(root, pendingEdit.getFixedDepositFields(), submittedFields);
}
if (ProductPendingEdit.TYPE_RD.equals(productType) && pendingEdit.getRecurringDepositFields() != null) {
writeRecurringDepositFieldsToJson(root, pendingEdit.getRecurringDepositFields(), submittedFields);
}
if (ProductPendingEdit.TYPE_SHARE.equals(productType) && pendingEdit.getShareOnlyFields() != null) {
writeShareOnlyFieldsToJson(root, pendingEdit.getShareOnlyFields(), submittedFields);
}
if (submittedFields.contains("glAccountMapping") && pendingEdit.getGlAccountMappingFields() != null) {
writeGlAccountMappingToJson(root, pendingEdit.getGlAccountMappingFields());
}
if (submittedFields.contains("charges") && pendingEditChargeRepository != null) {
writeChargesToJson(root, pendingEditChargeRepository.findByPendingEditId(pendingEdit.getId()));
}
if (submittedFields.contains("glArrayMappings") && pendingEditGlMappingRepository != null) {
writeGlArrayMappingsToJson(root, pendingEditGlMappingRepository.findByPendingEditId(pendingEdit.getId()));
}
final String json = gson.toJson(root);
return gson.fromJson(json, new com.google.gson.reflect.TypeToken<java.util.Map<String, Object>>() {}.getType());
}
}
@@ -48,28 +48,28 @@ public class SavingsCoreEditFields {
@Column(name = "currency_code")
private String currencyCode;
@Column(name = "digits_after_decimal")
@Column(name = "currency_digits")
private Integer digitsAfterDecimal;
@Column(name = "in_multiples_of")
@Column(name = "currency_multiplesof")
private Integer inMultiplesOf;
@Column(name = "accounting_rule")
@Column(name = "accounting_type")
private Integer accountingRule;
@Column(name = "nominal_annual_interest_rate")
private BigDecimal nominalAnnualInterestRate;
@Column(name = "interest_compounding_period_type")
@Column(name = "interest_compounding_period_enum")
private Integer interestCompoundingPeriodType;
@Column(name = "interest_posting_period_type")
@Column(name = "interest_posting_period_enum")
private Integer interestPostingPeriodType;
@Column(name = "interest_calculation_type")
@Column(name = "interest_calculation_type_enum")
private Integer interestCalculationType;
@Column(name = "interest_calculation_days_in_year_type")
@Column(name = "interest_calculation_days_in_year_type_enum")
private Integer interestCalculationDaysInYearType;
@Column(name = "min_required_opening_balance")
@@ -78,10 +78,10 @@ public class SavingsCoreEditFields {
@Column(name = "lockin_period_frequency")
private Integer lockinPeriodFrequency;
@Column(name = "lockin_period_frequency_type")
@Column(name = "lockin_period_frequency_enum")
private Integer lockinPeriodFrequencyType;
@Column(name = "withdrawal_fee_for_transfers")
@Column(name = "withdrawal_fee_for_transfer")
private Boolean withdrawalFeeForTransfers;
@Column(name = "allow_overdraft")
@@ -102,7 +102,7 @@ public class SavingsCoreEditFields {
@Column(name = "min_required_balance")
private BigDecimal minRequiredBalance;
@Column(name = "lien_allowed")
@Column(name = "is_lien_allowed")
private Boolean lienAllowed;
@Column(name = "max_allowed_lien_limit")
@@ -111,7 +111,7 @@ public class SavingsCoreEditFields {
@Column(name = "min_balance_for_interest_calculation")
private BigDecimal minBalanceForInterestCalculation;
@Column(name = "with_hold_tax")
@Column(name = "withhold_tax")
private Boolean withHoldTax;
@Column(name = "tax_group_id")
@@ -231,4 +231,13 @@ public class SavingsCoreEditFields {
@Column(name = "allow_debit_txn_after_due_date")
private Boolean allowDebitTxnAfterDueDate;
@Column(name = "deposit_type_enum")
private Integer depositTypeEnum;
@Column(name = "withdrawal_fee_amount")
private BigDecimal withdrawalFeeAmount;
@Column(name = "withdrawal_fee_type_enum")
private Integer withdrawalFeeTypeEnum;
}
@@ -32,28 +32,28 @@ public class ShareOnlyEditFields {
@Column(name = "total_shares")
private Long totalShares;
@Column(name = "total_shares_issued")
@Column(name = "issued_shares")
private Long totalSharesIssued;
@Column(name = "unit_price")
private BigDecimal unitPrice;
@Column(name = "minimum_shares")
@Column(name = "minimum_client_shares")
private Long minimumShares;
@Column(name = "nominal_shares")
@Column(name = "nominal_client_shares")
private Long nominalShares;
@Column(name = "maximum_shares")
@Column(name = "maximum_client_shares")
private Long maximumShares;
@Column(name = "allow_dividend_calc_for_inactive_clients")
@Column(name = "allow_dividends_inactive_clients")
private Boolean allowDividendCalcForInactiveClients;
@Column(name = "minimum_active_period_for_dividends")
@Column(name = "minimum_active_period_frequency")
private Integer minimumActivePeriodForDividends;
@Column(name = "minimum_active_period_frequency_type")
@Column(name = "minimum_active_period_frequency_enum")
private Integer minimumActivePeriodFrequencyType;
@Column(name = "share_lockin_period")
@@ -61,4 +61,16 @@ public class ShareOnlyEditFields {
@Column(name = "share_lockin_period_frequency_type")
private Integer shareLockinPeriodFrequencyType;
@Column(name = "external_id")
private String externalId;
@Column(name = "start_date")
private java.time.LocalDate startDate;
@Column(name = "totalsubscribed_shares")
private Long totalsubscribedShares;
@Column(name = "capital_amount")
private BigDecimal capitalAmount;
}
@@ -49,4 +49,6 @@
<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" />
<include file="parts/2028_update_saving_product_temp_table.xml" relativeToChangelogFile="true" />
<include file="parts/2029_remove_proposed_payload_json.xml" relativeToChangelogFile="true" />
</databaseChangeLog>
@@ -0,0 +1,93 @@
<?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">
<!-- 1. Rename table m_savings_product_temp to m_saving_product_temp -->
<changeSet author="fineract" id="2028-1">
<preConditions onFail="MARK_RAN">
<tableExists tableName="m_savings_product_temp" />
</preConditions>
<renameTable oldTableName="m_savings_product_temp" newTableName="m_saving_product_temp" />
</changeSet>
<!-- 2. Recreate unique constraint on pending_lock_key if needed -->
<changeSet author="fineract" id="2028-2">
<preConditions onFail="MARK_RAN">
<tableExists tableName="m_saving_product_temp" />
</preConditions>
<dropUniqueConstraint tableName="m_saving_product_temp" constraintName="sp_temp_unq_pending_lock_key" />
<addUniqueConstraint tableName="m_saving_product_temp" columnNames="pending_lock_key" constraintName="sp_temp_unq_pending_lock_key" />
</changeSet>
<!-- 3. Rename columns to match exact user comparative list names -->
<changeSet author="fineract" id="2028-rename-core-columns">
<preConditions onFail="MARK_RAN">
<tableExists tableName="m_saving_product_temp" />
</preConditions>
<!-- Auditing -->
<renameColumn tableName="m_saving_product_temp" oldColumnName="maker_id" newColumnName="createdby_id" columnDataType="BIGINT" />
<renameColumn tableName="m_saving_product_temp" oldColumnName="checker_id" newColumnName="lastmodifiedby_id" columnDataType="BIGINT" />
<renameColumn tableName="m_saving_product_temp" oldColumnName="created_on" newColumnName="created_date" columnDataType="DATETIME" />
<renameColumn tableName="m_saving_product_temp" oldColumnName="checked_on" newColumnName="lastmodified_date" columnDataType="DATETIME" />
<!-- Core fields -->
<renameColumn tableName="m_saving_product_temp" oldColumnName="digits_after_decimal" newColumnName="currency_digits" columnDataType="INTEGER" />
<renameColumn tableName="m_saving_product_temp" oldColumnName="in_multiples_of" newColumnName="currency_multiplesof" columnDataType="INTEGER" />
<renameColumn tableName="m_saving_product_temp" oldColumnName="accounting_rule" newColumnName="accounting_type" columnDataType="INTEGER" />
<renameColumn tableName="m_saving_product_temp" oldColumnName="interest_compounding_period_type" newColumnName="interest_compounding_period_enum" columnDataType="INTEGER" />
<renameColumn tableName="m_saving_product_temp" oldColumnName="interest_posting_period_type" newColumnName="interest_posting_period_enum" columnDataType="INTEGER" />
<renameColumn tableName="m_saving_product_temp" oldColumnName="interest_calculation_type" newColumnName="interest_calculation_type_enum" columnDataType="INTEGER" />
<renameColumn tableName="m_saving_product_temp" oldColumnName="interest_calculation_days_in_year_type" newColumnName="interest_calculation_days_in_year_type_enum" columnDataType="INTEGER" />
<renameColumn tableName="m_saving_product_temp" oldColumnName="lockin_period_frequency_type" newColumnName="lockin_period_frequency_enum" columnDataType="INTEGER" />
<renameColumn tableName="m_saving_product_temp" oldColumnName="withdrawal_fee_for_transfers" newColumnName="withdrawal_fee_for_transfer" columnDataType="BOOLEAN" />
<renameColumn tableName="m_saving_product_temp" oldColumnName="lien_allowed" newColumnName="is_lien_allowed" columnDataType="BOOLEAN" />
<renameColumn tableName="m_saving_product_temp" oldColumnName="with_hold_tax" newColumnName="withhold_tax" columnDataType="BOOLEAN" />
<!-- Share fields -->
<renameColumn tableName="m_saving_product_temp" oldColumnName="total_shares_issued" newColumnName="issued_shares" columnDataType="BIGINT" />
<renameColumn tableName="m_saving_product_temp" oldColumnName="minimum_shares" newColumnName="minimum_client_shares" columnDataType="BIGINT" />
<renameColumn tableName="m_saving_product_temp" oldColumnName="nominal_shares" newColumnName="nominal_client_shares" columnDataType="BIGINT" />
<renameColumn tableName="m_saving_product_temp" oldColumnName="maximum_shares" newColumnName="maximum_client_shares" columnDataType="BIGINT" />
<renameColumn tableName="m_saving_product_temp" oldColumnName="minimum_active_period_for_dividends" newColumnName="minimum_active_period_frequency" columnDataType="INTEGER" />
<renameColumn tableName="m_saving_product_temp" oldColumnName="minimum_active_period_frequency_type" newColumnName="minimum_active_period_frequency_enum" columnDataType="INTEGER" />
<renameColumn tableName="m_saving_product_temp" oldColumnName="allow_dividend_calc_for_inactive_clients" newColumnName="allow_dividends_inactive_clients" columnDataType="BOOLEAN" />
</changeSet>
<!-- 4. Add the completely new columns to m_saving_product_temp -->
<changeSet author="fineract" id="2028-add-missing-columns">
<preConditions onFail="MARK_RAN">
<tableExists tableName="m_saving_product_temp" />
</preConditions>
<addColumn tableName="m_saving_product_temp">
<!-- If column does not already exist, it will be added. Guard is implicit via preCondition or Liquibase will skip. -->
<column name="deposit_type_enum" type="SMALLINT" />
<column name="withdrawal_fee_amount" type="DECIMAL(19, 6)" />
<column name="withdrawal_fee_type_enum" type="SMALLINT" />
<column name="external_id" type="VARCHAR(100)" />
<column name="start_date" type="DATE" />
<column name="totalsubscribed_shares" type="BIGINT" />
<column name="capital_amount" type="DECIMAL(20, 2)" />
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,30 @@
<?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="2029-remove-proposed-payload-json">
<preConditions onFail="MARK_RAN">
<columnExists tableName="m_saving_product_temp" columnName="proposed_payload_json" />
</preConditions>
<dropColumn tableName="m_saving_product_temp" columnName="proposed_payload_json" />
</changeSet>
</databaseChangeLog>