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
@@ -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);
}
}