implemented a Maker-Checker workflow for Products, with approval-based updates and product locking to prevent concurrent edits
This commit is contained in:
+112
@@ -37,20 +37,26 @@ import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.Context;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import java.util.Collection;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||
import org.apache.fineract.commands.service.CommandWrapperBuilder;
|
||||
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
|
||||
import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.exception.ResourceNotFoundException;
|
||||
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
|
||||
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.core.service.Page;
|
||||
import org.apache.fineract.infrastructure.security.exception.NoAuthorizationException;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.products.constants.ProductsApiConstants;
|
||||
import org.apache.fineract.portfolio.products.data.ProductData;
|
||||
import org.apache.fineract.portfolio.products.service.ShareProductReadPlatformService;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsProductTempData;
|
||||
import org.apache.fineract.portfolio.shareproducts.service.ShareProductWritePlatformService;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -61,11 +67,26 @@ import org.springframework.stereotype.Component;
|
||||
@RequiredArgsConstructor
|
||||
public class ProductsApiResource {
|
||||
|
||||
private static final String SHARE_PRODUCT_TYPE = "share";
|
||||
/**
|
||||
* These product types have their own dedicated endpoints (/savingsproducts/{id}, /currentproducts/{id},
|
||||
* /fixeddepositproducts/{id}, /recurringdepositproducts/{id}) which correctly route edits through
|
||||
* submitEditForApproval - the pending-edit row is the lock. This generic /v1/products/{type}/{productId} PUT
|
||||
* falls through to the native command-processing pipeline for any type not special-cased above, which would
|
||||
* bypass that lock entirely for these types. The Angular frontend never calls this generic path for them (only
|
||||
* for "share"), so blocking them here closes the bypass without removing any real functionality.
|
||||
*/
|
||||
private static final java.util.Set<String> PRODUCT_TYPES_WITH_DEDICATED_LOCKED_ENDPOINT = java.util.Set.of("savings", "current",
|
||||
"fixeddeposit", "recurringdeposit");
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
private final ApiRequestParameterHelper apiRequestParameterHelper;
|
||||
private final DefaultToApiJsonSerializer<ProductData> toApiJsonSerializer;
|
||||
private final PlatformSecurityContext platformSecurityContext;
|
||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||
private final ShareProductWritePlatformService shareProductWritePlatformService;
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
private final DefaultToApiJsonSerializer<SavingsProductTempData> tempDataApiJsonSerializer;
|
||||
|
||||
@GET
|
||||
@Path("template")
|
||||
@@ -171,9 +192,100 @@ public class ProductsApiResource {
|
||||
@PathParam("productId") @Parameter(description = "productId") final Long productId,
|
||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
this.platformSecurityContext.authenticatedUser();
|
||||
|
||||
if (SHARE_PRODUCT_TYPE.equalsIgnoreCase(productType)) {
|
||||
// Bypasses the native command-source/maker-checker pipeline by design, same as Savings/Current
|
||||
// Products: edits are held in m_savings_product_temp (a pending row there is itself the lock) and
|
||||
// only applied to m_share_product on approval - see the pending/approve/reject endpoints below.
|
||||
final JsonCommand command = JsonCommand.fromJsonElement(productId, this.fromApiJsonHelper.parse(apiRequestBodyAsJson),
|
||||
this.fromApiJsonHelper);
|
||||
final CommandProcessingResult result = this.shareProductWritePlatformService.submitEditForApproval(productId, command);
|
||||
return this.toApiJsonSerializer.serialize(result);
|
||||
}
|
||||
|
||||
if (PRODUCT_TYPES_WITH_DEDICATED_LOCKED_ENDPOINT.contains(productType.toLowerCase(java.util.Locale.ROOT))) {
|
||||
throw new ResourceNotFoundException("error.msg.product.generic.update.not.supported",
|
||||
"Updating a " + productType + " product through this generic endpoint is not supported - use its dedicated endpoint",
|
||||
new Object[] { productType });
|
||||
}
|
||||
|
||||
final CommandWrapper commandRequest = new CommandWrapperBuilder().updateProduct(productType, productId)
|
||||
.withJson(apiRequestBodyAsJson).build();
|
||||
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||
return this.toApiJsonSerializer.serialize(result);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("pending")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "List Share Products pending edit approval")
|
||||
public String retrieveAllPending(@PathParam("type") @Parameter(description = "type") final String productType,
|
||||
@Context final UriInfo uriInfo) {
|
||||
this.platformSecurityContext.authenticatedUser();
|
||||
try {
|
||||
String serviceName = productType + ProductsApiConstants.READPLATFORM_NAME;
|
||||
ShareProductReadPlatformService service = (ShareProductReadPlatformService) this.applicationContext.getBean(serviceName);
|
||||
final Collection<SavingsProductTempData> pendingEdits = service.retrieveAllPendingEdits();
|
||||
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
||||
return this.tempDataApiJsonSerializer.serialize(settings, pendingEdits);
|
||||
} catch (BeansException e) {
|
||||
throw new ResourceNotFoundException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{productId}/pending")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Retrieve the pending edit for a Share Product")
|
||||
public String retrievePending(@PathParam("type") @Parameter(description = "type") final String productType,
|
||||
@PathParam("productId") @Parameter(description = "productId") final Long productId, @Context final UriInfo uriInfo) {
|
||||
this.platformSecurityContext.authenticatedUser();
|
||||
try {
|
||||
String serviceName = productType + ProductsApiConstants.READPLATFORM_NAME;
|
||||
ShareProductReadPlatformService service = (ShareProductReadPlatformService) this.applicationContext.getBean(serviceName);
|
||||
final SavingsProductTempData pendingEdit = service.retrievePendingEdit(productId);
|
||||
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
||||
return this.tempDataApiJsonSerializer.serialize(settings, pendingEdit);
|
||||
} catch (BeansException e) {
|
||||
throw new ResourceNotFoundException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("{productId}/approve")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Approve a pending Share Product edit")
|
||||
public String approvePending(@PathParam("type") @Parameter(description = "type") final String productType,
|
||||
@PathParam("productId") @Parameter(description = "productId") final Long productId) {
|
||||
if (this.platformSecurityContext.authenticatedUser().hasNotPermissionForAnyOf("APPROVE_SHAREPRODUCT")) {
|
||||
throw new NoAuthorizationException("User has no authority to approve share products");
|
||||
}
|
||||
final CommandProcessingResult result = this.shareProductWritePlatformService.approveEdit(productId);
|
||||
return this.toApiJsonSerializer.serialize(result);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("{productId}/reject")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Reject a pending Share Product edit")
|
||||
public String rejectPending(@PathParam("type") @Parameter(description = "type") final String productType,
|
||||
@PathParam("productId") @Parameter(description = "productId") final Long productId,
|
||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
if (this.platformSecurityContext.authenticatedUser().hasNotPermissionForAnyOf("REJECT_SHAREPRODUCT")) {
|
||||
throw new NoAuthorizationException("User has no authority to reject share products");
|
||||
}
|
||||
String rejectReason = null;
|
||||
if (apiRequestBodyAsJson != null && !apiRequestBodyAsJson.isBlank()) {
|
||||
final var parsed = this.fromApiJsonHelper.parse(apiRequestBodyAsJson);
|
||||
if (parsed != null && parsed.isJsonObject() && parsed.getAsJsonObject().has("rejectReason")) {
|
||||
rejectReason = parsed.getAsJsonObject().get("rejectReason").getAsString();
|
||||
}
|
||||
}
|
||||
final CommandProcessingResult result = this.shareProductWritePlatformService.rejectEdit(productId, rejectReason);
|
||||
return this.toApiJsonSerializer.serialize(result);
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -22,6 +22,7 @@ import java.util.Collection;
|
||||
import java.util.Set;
|
||||
import org.apache.fineract.infrastructure.core.service.Page;
|
||||
import org.apache.fineract.portfolio.products.data.ProductData;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsProductTempData;
|
||||
|
||||
public interface ShareProductReadPlatformService {
|
||||
|
||||
@@ -34,4 +35,8 @@ public interface ShareProductReadPlatformService {
|
||||
Set<String> getResponseDataParams();
|
||||
|
||||
Collection<ProductData> retrieveAllForLookup();
|
||||
|
||||
Collection<SavingsProductTempData> retrieveAllPendingEdits();
|
||||
|
||||
SavingsProductTempData retrievePendingEdit(Long productId);
|
||||
}
|
||||
|
||||
+74
-3
@@ -53,10 +53,13 @@ import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDoma
|
||||
import org.apache.fineract.infrastructure.codes.data.CodeValueData;
|
||||
import org.apache.fineract.infrastructure.codes.service.CodeValueReadPlatformService;
|
||||
import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
|
||||
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
|
||||
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.security.exception.NoAuthorizationException;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.organisation.monetary.data.CurrencyData;
|
||||
import org.apache.fineract.organisation.monetary.service.CurrencyReadPlatformService;
|
||||
@@ -71,7 +74,9 @@ import org.apache.fineract.portfolio.savings.SavingsInterestCalculationDaysInYea
|
||||
import org.apache.fineract.portfolio.savings.SavingsInterestCalculationType;
|
||||
import org.apache.fineract.portfolio.savings.SavingsPostingInterestPeriodType;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsProductData;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsProductTempData;
|
||||
import org.apache.fineract.portfolio.currentproduct.service.CurrentProductReadPlatformService;
|
||||
import org.apache.fineract.portfolio.currentproduct.service.CurrentProductWritePlatformService;
|
||||
import org.apache.fineract.portfolio.savings.service.SavingsDropdownReadPlatformService;
|
||||
import org.apache.fineract.portfolio.savings.service.SavingsEnumerations;
|
||||
import org.apache.fineract.portfolio.tax.data.TaxGroupData;
|
||||
@@ -101,6 +106,9 @@ public class CurrentProductsApiResource {
|
||||
private final ConfigurationDomainService configurationDomainService;
|
||||
private final CodeValueReadPlatformService codeValueReadPlatformService;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final CurrentProductWritePlatformService currentProductWritePlatformService;
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
private final DefaultToApiJsonSerializer<SavingsProductTempData> tempDataApiJsonSerializer;
|
||||
|
||||
|
||||
@POST
|
||||
@@ -122,9 +130,72 @@ public class CurrentProductsApiResource {
|
||||
@ApiResponse(responseCode = "200", description = "OK")
|
||||
public String update(@PathParam("productId") final Long productId,
|
||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
final CommandWrapper commandRequest = new CommandWrapperBuilder().updateCurrentProduct(productId).withJson(apiRequestBodyAsJson)
|
||||
.build();
|
||||
final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||
// Bypasses the native command-source/maker-checker pipeline by design: edits are held in
|
||||
// m_savings_product_temp (a pending row there is itself the lock) and only applied to m_current_product on
|
||||
// approval - see /currentproducts/{productId}/approve and /reject below.
|
||||
this.context.authenticatedUser().validateHasUpdatePermission(DepositsApiConstants.CURRENT_PRODUCT_RESOURCE_NAME);
|
||||
|
||||
final JsonCommand command = JsonCommand.fromJsonElement(productId, this.fromApiJsonHelper.parse(apiRequestBodyAsJson),
|
||||
this.fromApiJsonHelper);
|
||||
|
||||
final CommandProcessingResult result = this.currentProductWritePlatformService.submitEditForApproval(productId, command);
|
||||
return toApiJsonSerializer.serialize(result);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("pending")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "List Current Products pending edit approval")
|
||||
public String retrieveAllPending(@Context final UriInfo uriInfo) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.CURRENT_PRODUCT_RESOURCE_NAME);
|
||||
final Collection<SavingsProductTempData> pendingEdits = this.savingProductReadPlatformService.retrieveAllPendingEdits();
|
||||
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
||||
return this.tempDataApiJsonSerializer.serialize(settings, pendingEdits);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{productId}/pending")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Retrieve the pending edit for a Current Product")
|
||||
public String retrievePending(@PathParam("productId") final Long productId, @Context final UriInfo uriInfo) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.CURRENT_PRODUCT_RESOURCE_NAME);
|
||||
final SavingsProductTempData pendingEdit = this.savingProductReadPlatformService.retrievePendingEdit(productId);
|
||||
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
||||
return this.tempDataApiJsonSerializer.serialize(settings, pendingEdit);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("{productId}/approve")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Approve a pending Current Product edit")
|
||||
public String approvePending(@PathParam("productId") final Long productId) {
|
||||
if (this.context.authenticatedUser().hasNotPermissionForAnyOf("APPROVE_CURRENTPRODUCT")) {
|
||||
throw new NoAuthorizationException("User has no authority to approve current products");
|
||||
}
|
||||
final CommandProcessingResult result = this.currentProductWritePlatformService.approveEdit(productId);
|
||||
return toApiJsonSerializer.serialize(result);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("{productId}/reject")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Reject a pending Current Product edit")
|
||||
public String rejectPending(@PathParam("productId") final Long productId, @Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
if (this.context.authenticatedUser().hasNotPermissionForAnyOf("REJECT_CURRENTPRODUCT")) {
|
||||
throw new NoAuthorizationException("User has no authority to reject current products");
|
||||
}
|
||||
String rejectReason = null;
|
||||
if (apiRequestBodyAsJson != null && !apiRequestBodyAsJson.isBlank()) {
|
||||
final var parsed = this.fromApiJsonHelper.parse(apiRequestBodyAsJson);
|
||||
if (parsed != null && parsed.isJsonObject() && parsed.getAsJsonObject().has("rejectReason")) {
|
||||
rejectReason = parsed.getAsJsonObject().get("rejectReason").getAsString();
|
||||
}
|
||||
}
|
||||
final CommandProcessingResult result = this.currentProductWritePlatformService.rejectEdit(productId, rejectReason);
|
||||
return toApiJsonSerializer.serialize(result);
|
||||
}
|
||||
|
||||
|
||||
+77
-3
@@ -53,14 +53,17 @@ import org.apache.fineract.commands.domain.CommandWrapper;
|
||||
import org.apache.fineract.commands.service.CommandWrapperBuilder;
|
||||
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
|
||||
import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import java.util.Locale;
|
||||
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
|
||||
import org.apache.fineract.portfolio.savings.SavingsApiConstants;
|
||||
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
|
||||
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.codes.data.CodeValueData;
|
||||
import org.apache.fineract.infrastructure.codes.service.CodeValueReadPlatformService;
|
||||
import org.apache.fineract.infrastructure.security.exception.NoAuthorizationException;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.organisation.monetary.data.CurrencyData;
|
||||
import org.apache.fineract.organisation.monetary.service.CurrencyReadPlatformService;
|
||||
@@ -78,8 +81,10 @@ import org.apache.fineract.portfolio.savings.SavingsInterestCalculationDaysInYea
|
||||
import org.apache.fineract.portfolio.savings.SavingsInterestCalculationType;
|
||||
import org.apache.fineract.portfolio.savings.SavingsPostingInterestPeriodType;
|
||||
import org.apache.fineract.portfolio.savings.data.FixedDepositProductData;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsProductTempData;
|
||||
import org.apache.fineract.portfolio.savings.service.DepositProductReadPlatformService;
|
||||
import org.apache.fineract.portfolio.savings.service.DepositsDropdownReadPlatformService;
|
||||
import org.apache.fineract.portfolio.savings.service.FixedDepositProductWritePlatformService;
|
||||
import org.apache.fineract.portfolio.savings.service.SavingsDropdownReadPlatformService;
|
||||
import org.apache.fineract.portfolio.savings.service.SavingsEnumerations;
|
||||
import org.apache.fineract.portfolio.tax.data.TaxGroupData;
|
||||
@@ -116,6 +121,9 @@ public class FixedDepositProductsApiResource {
|
||||
private final TaxReadPlatformService taxReadPlatformService;
|
||||
private final CodeValueReadPlatformService codeValueReadPlatformService;
|
||||
private final org.springframework.jdbc.core.JdbcTemplate jdbcTemplate;
|
||||
private final FixedDepositProductWritePlatformService fixedDepositProductWritePlatformService;
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
private final DefaultToApiJsonSerializer<SavingsProductTempData> tempDataApiJsonSerializer;
|
||||
|
||||
@POST
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@@ -151,15 +159,81 @@ public class FixedDepositProductsApiResource {
|
||||
public String update(@PathParam("productId") @Parameter(description = "productId") final Long productId,
|
||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
|
||||
final CommandWrapper commandRequest = new CommandWrapperBuilder().updateFixedDepositProduct(productId)
|
||||
.withJson(apiRequestBodyAsJson).build();
|
||||
// Bypasses the native command-source/maker-checker pipeline by design: edits are held in
|
||||
// m_savings_product_temp (a pending row there is itself the lock) and only applied to m_savings_product on
|
||||
// approval - see /fixeddepositproducts/{productId}/approve and /reject below.
|
||||
this.context.authenticatedUser().validateHasUpdatePermission(DepositsApiConstants.FIXED_DEPOSIT_PRODUCT_RESOURCE_NAME);
|
||||
|
||||
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||
final JsonCommand command = JsonCommand.fromJsonElement(productId, this.fromApiJsonHelper.parse(apiRequestBodyAsJson),
|
||||
this.fromApiJsonHelper);
|
||||
|
||||
final CommandProcessingResult result = this.fixedDepositProductWritePlatformService.submitEditForApproval(productId, command);
|
||||
|
||||
return this.toApiJsonSerializer.serialize(result);
|
||||
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("pending")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "List Fixed Deposit Products pending edit approval")
|
||||
public String retrieveAllPending(@Context final UriInfo uriInfo) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.FIXED_DEPOSIT_PRODUCT_RESOURCE_NAME);
|
||||
final Collection<SavingsProductTempData> pendingEdits = this.depositProductReadPlatformService
|
||||
.retrieveAllPendingEdits(DepositAccountType.FIXED_DEPOSIT);
|
||||
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
||||
return this.tempDataApiJsonSerializer.serialize(settings, pendingEdits);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{productId}/pending")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Retrieve the pending edit for a Fixed Deposit Product")
|
||||
public String retrievePending(@PathParam("productId") @Parameter(description = "productId") final Long productId,
|
||||
@Context final UriInfo uriInfo) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.FIXED_DEPOSIT_PRODUCT_RESOURCE_NAME);
|
||||
final SavingsProductTempData pendingEdit = this.depositProductReadPlatformService
|
||||
.retrievePendingEdit(DepositAccountType.FIXED_DEPOSIT, productId);
|
||||
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
||||
return this.tempDataApiJsonSerializer.serialize(settings, pendingEdit);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("{productId}/approve")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Approve a pending Fixed Deposit Product edit")
|
||||
public String approvePending(@PathParam("productId") @Parameter(description = "productId") final Long productId) {
|
||||
if (this.context.authenticatedUser().hasNotPermissionForAnyOf("APPROVE_FIXEDDEPOSITPRODUCT")) {
|
||||
throw new NoAuthorizationException("User has no authority to approve fixed deposit products");
|
||||
}
|
||||
final CommandProcessingResult result = this.fixedDepositProductWritePlatformService.approveEdit(productId);
|
||||
return this.toApiJsonSerializer.serialize(result);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("{productId}/reject")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Reject a pending Fixed Deposit Product edit")
|
||||
public String rejectPending(@PathParam("productId") @Parameter(description = "productId") final Long productId,
|
||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
if (this.context.authenticatedUser().hasNotPermissionForAnyOf("REJECT_FIXEDDEPOSITPRODUCT")) {
|
||||
throw new NoAuthorizationException("User has no authority to reject fixed deposit products");
|
||||
}
|
||||
String rejectReason = null;
|
||||
if (apiRequestBodyAsJson != null && !apiRequestBodyAsJson.isBlank()) {
|
||||
final var parsed = this.fromApiJsonHelper.parse(apiRequestBodyAsJson);
|
||||
if (parsed != null && parsed.isJsonObject() && parsed.getAsJsonObject().has("rejectReason")) {
|
||||
rejectReason = parsed.getAsJsonObject().get("rejectReason").getAsString();
|
||||
}
|
||||
}
|
||||
final CommandProcessingResult result = this.fixedDepositProductWritePlatformService.rejectEdit(productId, rejectReason);
|
||||
return this.toApiJsonSerializer.serialize(result);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
|
||||
+78
-3
@@ -55,10 +55,13 @@ import org.apache.fineract.commands.domain.CommandWrapper;
|
||||
import org.apache.fineract.commands.service.CommandWrapperBuilder;
|
||||
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
|
||||
import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
|
||||
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
|
||||
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.security.exception.NoAuthorizationException;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.organisation.monetary.data.CurrencyData;
|
||||
import org.apache.fineract.organisation.monetary.service.CurrencyReadPlatformService;
|
||||
@@ -85,8 +88,10 @@ import com.google.gson.JsonPrimitive;
|
||||
import com.google.gson.JsonSerializer;
|
||||
import java.time.LocalDate;
|
||||
import org.apache.fineract.portfolio.savings.data.RecurringDepositProductData;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsProductTempData;
|
||||
import org.apache.fineract.portfolio.savings.service.DepositProductReadPlatformService;
|
||||
import org.apache.fineract.portfolio.savings.service.DepositsDropdownReadPlatformService;
|
||||
import org.apache.fineract.portfolio.savings.service.RecurringDepositProductWritePlatformService;
|
||||
import org.apache.fineract.portfolio.savings.service.SavingsDropdownReadPlatformService;
|
||||
import org.apache.fineract.portfolio.savings.service.SavingsEnumerations;
|
||||
import org.apache.fineract.portfolio.tax.data.TaxGroupData;
|
||||
@@ -126,6 +131,9 @@ public class RecurringDepositProductsApiResource {
|
||||
.registerTypeAdapter(LocalDate.class,
|
||||
(JsonSerializer<LocalDate>) (src, typeOfSrc, ctx) -> new JsonPrimitive(src.toString()))
|
||||
.create();
|
||||
private final RecurringDepositProductWritePlatformService recurringDepositProductWritePlatformService;
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
private final DefaultToApiJsonSerializer<SavingsProductTempData> tempDataApiJsonSerializer;
|
||||
|
||||
@POST
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@@ -158,15 +166,82 @@ public class RecurringDepositProductsApiResource {
|
||||
public String update(@PathParam("productId") @Parameter(description = "productId") final Long productId,
|
||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
|
||||
final CommandWrapper commandRequest = new CommandWrapperBuilder().updateRecurringDepositProduct(productId)
|
||||
.withJson(apiRequestBodyAsJson).build();
|
||||
// Bypasses the native command-source/maker-checker pipeline by design: edits are held in
|
||||
// m_savings_product_temp (a pending row there is itself the lock) and only applied to m_savings_product on
|
||||
// approval - see /recurringdepositproducts/{productId}/approve and /reject below.
|
||||
this.context.authenticatedUser().validateHasUpdatePermission(DepositsApiConstants.RECURRING_DEPOSIT_PRODUCT_RESOURCE_NAME);
|
||||
|
||||
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||
final JsonCommand command = JsonCommand.fromJsonElement(productId, this.fromApiJsonHelper.parse(apiRequestBodyAsJson),
|
||||
this.fromApiJsonHelper);
|
||||
|
||||
final CommandProcessingResult result = this.recurringDepositProductWritePlatformService.submitEditForApproval(productId,
|
||||
command);
|
||||
|
||||
return this.toApiJsonSerializer.serialize(result);
|
||||
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("pending")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "List Recurring Deposit Products pending edit approval")
|
||||
public String retrieveAllPending(@Context final UriInfo uriInfo) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.RECURRING_DEPOSIT_PRODUCT_RESOURCE_NAME);
|
||||
final Collection<SavingsProductTempData> pendingEdits = this.depositProductReadPlatformService
|
||||
.retrieveAllPendingEdits(DepositAccountType.RECURRING_DEPOSIT);
|
||||
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
||||
return this.tempDataApiJsonSerializer.serialize(settings, pendingEdits);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{productId}/pending")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Retrieve the pending edit for a Recurring Deposit Product")
|
||||
public String retrievePending(@PathParam("productId") @Parameter(description = "productId") final Long productId,
|
||||
@Context final UriInfo uriInfo) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.RECURRING_DEPOSIT_PRODUCT_RESOURCE_NAME);
|
||||
final SavingsProductTempData pendingEdit = this.depositProductReadPlatformService
|
||||
.retrievePendingEdit(DepositAccountType.RECURRING_DEPOSIT, productId);
|
||||
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
||||
return this.tempDataApiJsonSerializer.serialize(settings, pendingEdit);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("{productId}/approve")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Approve a pending Recurring Deposit Product edit")
|
||||
public String approvePending(@PathParam("productId") @Parameter(description = "productId") final Long productId) {
|
||||
if (this.context.authenticatedUser().hasNotPermissionForAnyOf("APPROVE_RECURRINGDEPOSITPRODUCT")) {
|
||||
throw new NoAuthorizationException("User has no authority to approve recurring deposit products");
|
||||
}
|
||||
final CommandProcessingResult result = this.recurringDepositProductWritePlatformService.approveEdit(productId);
|
||||
return this.toApiJsonSerializer.serialize(result);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("{productId}/reject")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Reject a pending Recurring Deposit Product edit")
|
||||
public String rejectPending(@PathParam("productId") @Parameter(description = "productId") final Long productId,
|
||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
if (this.context.authenticatedUser().hasNotPermissionForAnyOf("REJECT_RECURRINGDEPOSITPRODUCT")) {
|
||||
throw new NoAuthorizationException("User has no authority to reject recurring deposit products");
|
||||
}
|
||||
String rejectReason = null;
|
||||
if (apiRequestBodyAsJson != null && !apiRequestBodyAsJson.isBlank()) {
|
||||
final var parsed = this.fromApiJsonHelper.parse(apiRequestBodyAsJson);
|
||||
if (parsed != null && parsed.isJsonObject() && parsed.getAsJsonObject().has("rejectReason")) {
|
||||
rejectReason = parsed.getAsJsonObject().get("rejectReason").getAsString();
|
||||
}
|
||||
}
|
||||
final CommandProcessingResult result = this.recurringDepositProductWritePlatformService.rejectEdit(productId, rejectReason);
|
||||
return this.toApiJsonSerializer.serialize(result);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
|
||||
+88
-3
@@ -59,8 +59,11 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
|
||||
import org.apache.fineract.infrastructure.codes.data.CodeValueData;
|
||||
import org.apache.fineract.infrastructure.codes.service.CodeValueReadPlatformService;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
|
||||
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.security.exception.NoAuthorizationException;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.organisation.monetary.data.CurrencyData;
|
||||
import org.apache.fineract.organisation.monetary.service.CurrencyReadPlatformService;
|
||||
@@ -74,9 +77,11 @@ import org.apache.fineract.portfolio.savings.SavingsInterestCalculationDaysInYea
|
||||
import org.apache.fineract.portfolio.savings.SavingsInterestCalculationType;
|
||||
import org.apache.fineract.portfolio.savings.SavingsPostingInterestPeriodType;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsProductData;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsProductTempData;
|
||||
import org.apache.fineract.portfolio.savings.service.SavingsDropdownReadPlatformService;
|
||||
import org.apache.fineract.portfolio.savings.service.SavingsEnumerations;
|
||||
import org.apache.fineract.portfolio.savings.service.SavingsProductReadPlatformService;
|
||||
import org.apache.fineract.portfolio.savings.service.SavingsProductWritePlatformService;
|
||||
import org.apache.fineract.portfolio.tax.data.TaxGroupData;
|
||||
import org.apache.fineract.portfolio.tax.service.TaxReadPlatformService;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
@@ -105,6 +110,9 @@ public class SavingsProductsApiResource {
|
||||
private final TaxReadPlatformService taxReadPlatformService;
|
||||
private final ConfigurationDomainService configurationDomainService;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final SavingsProductWritePlatformService savingsProductWritePlatformService;
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
private final DefaultToApiJsonSerializer<SavingsProductTempData> tempDataApiJsonSerializer;
|
||||
|
||||
|
||||
@POST
|
||||
@@ -135,15 +143,92 @@ public class SavingsProductsApiResource {
|
||||
public String update(@PathParam("productId") @Parameter(description = "productId") final Long productId,
|
||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
|
||||
final CommandWrapper commandRequest = new CommandWrapperBuilder().updateSavingProduct(productId).withJson(apiRequestBodyAsJson)
|
||||
.build();
|
||||
// Bypasses the native command-source/maker-checker pipeline by design: edits are held in
|
||||
// m_savings_product_temp (a pending row there is itself the lock) and only applied to m_savings_product on
|
||||
// approval - see /savingsproducts/{productId}/approve and /reject below.
|
||||
this.context.authenticatedUser().validateHasUpdatePermission(SavingsApiConstants.SAVINGS_PRODUCT_RESOURCE_NAME);
|
||||
|
||||
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||
final JsonCommand command = JsonCommand.fromJsonElement(productId, this.fromApiJsonHelper.parse(apiRequestBodyAsJson),
|
||||
this.fromApiJsonHelper);
|
||||
|
||||
final CommandProcessingResult result = this.savingsProductWritePlatformService.submitEditForApproval(productId, command);
|
||||
|
||||
return this.toApiJsonSerializer.serialize(result);
|
||||
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("pending")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "List Savings Products pending edit approval", description = "Lists all savings product edits currently awaiting checker approval")
|
||||
public String retrieveAllPending(@Context final UriInfo uriInfo) {
|
||||
|
||||
this.context.authenticatedUser().validateHasReadPermission(SavingsApiConstants.SAVINGS_PRODUCT_RESOURCE_NAME);
|
||||
|
||||
final Collection<SavingsProductTempData> pendingEdits = this.savingProductReadPlatformService.retrieveAllPendingEdits();
|
||||
|
||||
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
||||
return this.tempDataApiJsonSerializer.serialize(settings, pendingEdits);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{productId}/pending")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Retrieve the pending edit for a Savings Product", description = "Retrieves the draft awaiting checker approval for this product, if any")
|
||||
public String retrievePending(@PathParam("productId") @Parameter(description = "productId") final Long productId,
|
||||
@Context final UriInfo uriInfo) {
|
||||
|
||||
this.context.authenticatedUser().validateHasReadPermission(SavingsApiConstants.SAVINGS_PRODUCT_RESOURCE_NAME);
|
||||
|
||||
final SavingsProductTempData pendingEdit = this.savingProductReadPlatformService.retrievePendingEdit(productId);
|
||||
|
||||
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
||||
return this.tempDataApiJsonSerializer.serialize(settings, pendingEdit);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("{productId}/approve")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Approve a pending Savings Product edit", description = "Replays the pending edit and applies it to the Savings Product. The maker of the edit cannot also approve it.")
|
||||
public String approvePending(@PathParam("productId") @Parameter(description = "productId") final Long productId) {
|
||||
|
||||
if (this.context.authenticatedUser().hasNotPermissionForAnyOf("APPROVE_SAVINGSPRODUCT")) {
|
||||
throw new NoAuthorizationException("User has no authority to approve savings products");
|
||||
}
|
||||
|
||||
final CommandProcessingResult result = this.savingsProductWritePlatformService.approveEdit(productId);
|
||||
|
||||
return this.toApiJsonSerializer.serialize(result);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("{productId}/reject")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Reject a pending Savings Product edit", description = "Discards the pending edit without applying it. The maker of the edit cannot also reject it.")
|
||||
public String rejectPending(@PathParam("productId") @Parameter(description = "productId") final Long productId,
|
||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
|
||||
if (this.context.authenticatedUser().hasNotPermissionForAnyOf("REJECT_SAVINGSPRODUCT")) {
|
||||
throw new NoAuthorizationException("User has no authority to reject savings products");
|
||||
}
|
||||
|
||||
String rejectReason = null;
|
||||
if (apiRequestBodyAsJson != null && !apiRequestBodyAsJson.isBlank()) {
|
||||
final var parsed = this.fromApiJsonHelper.parse(apiRequestBodyAsJson);
|
||||
if (parsed != null && parsed.isJsonObject() && parsed.getAsJsonObject().has("rejectReason")) {
|
||||
rejectReason = parsed.getAsJsonObject().get("rejectReason").getAsString();
|
||||
}
|
||||
}
|
||||
|
||||
final CommandProcessingResult result = this.savingsProductWritePlatformService.rejectEdit(productId, rejectReason);
|
||||
|
||||
return this.toApiJsonSerializer.serialize(result);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
|
||||
+67
-2
@@ -18,10 +18,13 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.savings.service;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import java.math.BigDecimal;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.accounting.common.AccountingEnumerations;
|
||||
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
|
||||
@@ -34,7 +37,9 @@ import org.apache.fineract.portfolio.savings.DepositAccountType;
|
||||
import org.apache.fineract.portfolio.savings.data.DepositProductData;
|
||||
import org.apache.fineract.portfolio.savings.data.FixedDepositProductData;
|
||||
import org.apache.fineract.portfolio.savings.data.RecurringDepositProductData;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsProductTempData;
|
||||
import org.apache.fineract.portfolio.savings.exception.FixedDepositProductNotFoundException;
|
||||
import org.apache.fineract.portfolio.savings.exception.SavingsProductNoPendingEditException;
|
||||
import org.apache.fineract.portfolio.tax.data.TaxGroupData;
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
@@ -124,6 +129,60 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead
|
||||
return null;
|
||||
}
|
||||
|
||||
private static final Gson GSON = new Gson();
|
||||
|
||||
private static String productPendingType(final DepositAccountType depositAccountType) {
|
||||
return depositAccountType.isFixedDeposit() ? "FD" : "RD";
|
||||
}
|
||||
|
||||
private static final RowMapper<SavingsProductTempData> PENDING_EDIT_MAPPER = (rs, rowNum) -> {
|
||||
final SavingsProductTempData data = new SavingsProductTempData();
|
||||
data.setId(rs.getLong("tempId"));
|
||||
data.setSavingsProductId(rs.getLong("savingsProductId"));
|
||||
data.setSavingsProductName(rs.getString("savingsProductName"));
|
||||
data.setMakerId(rs.getLong("makerId"));
|
||||
data.setMakerUsername(rs.getString("makerUsername"));
|
||||
data.setCheckerId(JdbcSupport.getLong(rs, "checkerId"));
|
||||
data.setCheckerUsername(rs.getString("checkerUsername"));
|
||||
data.setCreatedOn(rs.getTimestamp("createdOn") == null ? null : rs.getTimestamp("createdOn").toLocalDateTime());
|
||||
data.setCheckedOn(rs.getTimestamp("checkedOn") == null ? null : rs.getTimestamp("checkedOn").toLocalDateTime());
|
||||
data.setStatus(rs.getString("status"));
|
||||
data.setRejectReason(rs.getString("rejectReason"));
|
||||
final String proposedPayloadJson = rs.getString("proposedPayloadJson");
|
||||
if (proposedPayloadJson != null) {
|
||||
data.setProposedChanges(GSON.fromJson(proposedPayloadJson, new TypeToken<Map<String, Object>>() {}.getType()));
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
private static String pendingEditSchema(final String productType) {
|
||||
return "spt.id as tempId, spt.product_id as savingsProductId, " + "sp.name as savingsProductName, spt.maker_id as makerId, "
|
||||
+ "maker.username as makerUsername, spt.checker_id as checkerId, checker.username as checkerUsername, "
|
||||
+ "spt.created_on as createdOn, spt.checked_on as checkedOn, spt.status as status, "
|
||||
+ "spt.reject_reason as rejectReason, spt.proposed_payload_json as proposedPayloadJson "
|
||||
+ "from m_savings_product_temp spt " + "join m_savings_product sp on sp.id = spt.product_id "
|
||||
+ "join m_appuser maker on maker.id = spt.maker_id " + "left join m_appuser checker on checker.id = spt.checker_id "
|
||||
+ "where spt.product_type = '" + productType + "' ";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<SavingsProductTempData> retrieveAllPendingEdits(final DepositAccountType depositAccountType) {
|
||||
final String sql = "select " + pendingEditSchema(productPendingType(depositAccountType))
|
||||
+ "and spt.status = 'PENDING' order by spt.created_on desc";
|
||||
return this.jdbcTemplate.query(sql, PENDING_EDIT_MAPPER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SavingsProductTempData retrievePendingEdit(final DepositAccountType depositAccountType, final Long productId) {
|
||||
final String sql = "select " + pendingEditSchema(productPendingType(depositAccountType))
|
||||
+ "and spt.status = 'PENDING' and spt.product_id = ?";
|
||||
try {
|
||||
return this.jdbcTemplate.queryForObject(sql, PENDING_EDIT_MAPPER, new Object[] { productId }); // NOSONAR
|
||||
} catch (final EmptyResultDataAccessException e) {
|
||||
throw new SavingsProductNoPendingEditException(productId);
|
||||
}
|
||||
}
|
||||
|
||||
private abstract static class DepositProductMapper implements RowMapper<DepositProductData> {
|
||||
|
||||
private final String schemaSql;
|
||||
@@ -255,7 +314,9 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead
|
||||
sqlBuilder.append("sp.no_penalty_nominee_heir_closure as noPenaltyNomineeHeirClosure, ");
|
||||
sqlBuilder.append("sp.deceased_settlement as deceasedSettlement, ");
|
||||
sqlBuilder.append("sp.allow_debit_txn_after_due_date as allowDebitTxnAfterDueDate, ");
|
||||
sqlBuilder.append("sp.withdrawal_fee_for_transfer as withdrawalFeeForTransfers ");
|
||||
sqlBuilder.append("sp.withdrawal_fee_for_transfer as withdrawalFeeForTransfers, ");
|
||||
sqlBuilder.append("exists(select 1 from m_savings_product_temp spt where spt.product_id = sp.id "
|
||||
+ "and spt.product_type = 'FD' and spt.status = 'PENDING') as isPendingApproval ");
|
||||
sqlBuilder.append("from m_savings_product sp ");
|
||||
sqlBuilder.append("left join m_deposit_product_term_and_preclosure dptp on sp.id=dptp.savings_product_id ");
|
||||
sqlBuilder.append("join m_currency curr on curr.code = sp.currency_code ");
|
||||
@@ -345,6 +406,7 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead
|
||||
result.setDeceasedSettlement(rs.getBoolean("deceasedSettlement"));
|
||||
result.setAllowDebitTxnAfterDueDate(rs.getBoolean("allowDebitTxnAfterDueDate"));
|
||||
result.setWithdrawalFeeForTransfers(rs.getBoolean("withdrawalFeeForTransfers"));
|
||||
result.setIsPendingApproval(rs.getBoolean("isPendingApproval"));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -392,7 +454,9 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead
|
||||
sqlBuilder.append("sp.no_penalty_nominee_heir_closure as noPenaltyNomineeHeirClosure, ");
|
||||
sqlBuilder.append("sp.deceased_settlement as deceasedSettlement, ");
|
||||
sqlBuilder.append("sp.allow_debit_txn_after_due_date as allowDebitTxnAfterDueDate, ");
|
||||
sqlBuilder.append("sp.withdrawal_fee_for_transfer as withdrawalFeeForTransfers ");
|
||||
sqlBuilder.append("sp.withdrawal_fee_for_transfer as withdrawalFeeForTransfers, ");
|
||||
sqlBuilder.append("exists(select 1 from m_savings_product_temp spt where spt.product_id = sp.id "
|
||||
+ "and spt.product_type = 'RD' and spt.status = 'PENDING') as isPendingApproval ");
|
||||
sqlBuilder.append("from m_savings_product sp ");
|
||||
sqlBuilder.append("left join m_deposit_product_term_and_preclosure dptp on sp.id=dptp.savings_product_id ");
|
||||
sqlBuilder.append("left join m_deposit_product_recurring_detail dprd on sp.id=dprd.savings_product_id ");
|
||||
@@ -513,6 +577,7 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead
|
||||
result.setDeceasedSettlement(deceasedSettlement);
|
||||
result.setAllowDebitTxnAfterDueDate(allowDebitTxnAfterDueDate);
|
||||
result.setWithdrawalFeeForTransfers(withdrawalFeeForTransfers);
|
||||
result.setIsPendingApproval(rs.getBoolean("isPendingApproval"));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
+132
@@ -32,6 +32,9 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.apache.fineract.accounting.producttoaccountmapping.service.ProductToGLAccountMappingWritePlatformService;
|
||||
import org.apache.fineract.commands.domain.CommandProcessingResultType;
|
||||
import org.apache.fineract.commands.domain.CommandSource;
|
||||
import org.apache.fineract.commands.domain.CommandSourceRepository;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
@@ -39,6 +42,8 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuild
|
||||
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.charge.domain.Charge;
|
||||
import org.apache.fineract.portfolio.interestratechart.service.InterestRateChartAssembler;
|
||||
@@ -48,8 +53,14 @@ import org.apache.fineract.portfolio.savings.data.DepositProductDataValidator;
|
||||
import org.apache.fineract.portfolio.savings.domain.DepositProductAssembler;
|
||||
import org.apache.fineract.portfolio.savings.domain.FixedDepositProduct;
|
||||
import org.apache.fineract.portfolio.savings.domain.FixedDepositProductRepository;
|
||||
import org.apache.fineract.portfolio.savings.domain.ProductPendingEdit;
|
||||
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditRepository;
|
||||
import org.apache.fineract.portfolio.savings.exception.FixedDepositProductNotFoundException;
|
||||
import org.apache.fineract.portfolio.savings.exception.SavingsProductEditPendingApprovalException;
|
||||
import org.apache.fineract.portfolio.savings.exception.SavingsProductNoPendingEditException;
|
||||
import org.apache.fineract.portfolio.savings.exception.SavingsProductSelfApprovalNotAllowedException;
|
||||
import org.apache.fineract.portfolio.tax.domain.TaxGroup;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -63,6 +74,9 @@ public class FixedDepositProductWritePlatformServiceJpaRepositoryImpl implements
|
||||
private final DepositProductAssembler depositProductAssembler;
|
||||
private final ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService;
|
||||
private final InterestRateChartAssembler chartAssembler;
|
||||
private final ProductPendingEditRepository productPendingEditRepository;
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
private final CommandSourceRepository commandSourceRepository;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
@@ -150,6 +164,124 @@ public class FixedDepositProductWritePlatformServiceJpaRepositoryImpl implements
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult submitEditForApproval(final Long productId, final JsonCommand command) {
|
||||
|
||||
final AppUser maker = this.context.authenticatedUser();
|
||||
final FixedDepositProduct product = this.fixedDepositProductRepository.findById(productId)
|
||||
.orElseThrow(() -> new FixedDepositProductNotFoundException(productId));
|
||||
|
||||
if (this.productPendingEditRepository
|
||||
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_FD, productId, ProductPendingEdit.STATUS_PENDING)
|
||||
.isPresent()) {
|
||||
throw new SavingsProductEditPendingApprovalException(productId);
|
||||
}
|
||||
|
||||
this.fromApiJsonDataValidator.validateForFixedDepositUpdate(command.json());
|
||||
|
||||
final ProductPendingEdit pendingEdit = new ProductPendingEdit(ProductPendingEdit.TYPE_FD, productId, maker, command.json());
|
||||
|
||||
final CommandSource auditEntry = CommandSource.builder() //
|
||||
.actionName("UPDATE") //
|
||||
.entityName("FIXEDDEPOSITPRODUCT") //
|
||||
.resourceGetUrl("/fixeddepositproducts/" + productId) //
|
||||
.resourceId(productId) //
|
||||
.commandAsJson(command.json()) //
|
||||
.maker(maker) //
|
||||
.madeOnDate(DateUtils.getAuditOffsetDateTime()) //
|
||||
.status(CommandProcessingResultType.AWAITING_APPROVAL.getValue()) //
|
||||
.idempotencyKey(java.util.UUID.randomUUID().toString()) //
|
||||
.sanitized(false) //
|
||||
.build();
|
||||
this.commandSourceRepository.saveAndFlush(auditEntry);
|
||||
pendingEdit.setCommandSourceId(auditEntry.getId());
|
||||
|
||||
this.productPendingEditRepository.saveAndFlush(pendingEdit);
|
||||
|
||||
return new CommandProcessingResultBuilder() //
|
||||
.withEntityId(product.getId()) //
|
||||
.build();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult approveEdit(final Long productId) {
|
||||
|
||||
final AppUser checker = this.context.authenticatedUser();
|
||||
final ProductPendingEdit pendingEdit = this.productPendingEditRepository
|
||||
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_FD, productId, ProductPendingEdit.STATUS_PENDING)
|
||||
.orElseThrow(() -> new SavingsProductNoPendingEditException(productId));
|
||||
|
||||
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
||||
throw new SavingsProductSelfApprovalNotAllowedException(productId);
|
||||
}
|
||||
|
||||
final JsonCommand replayedCommand = JsonCommand.fromJsonElement(productId,
|
||||
this.fromApiJsonHelper.parse(pendingEdit.getProposedPayloadJson()), this.fromApiJsonHelper);
|
||||
final CommandProcessingResult result = update(productId, replayedCommand);
|
||||
|
||||
pendingEdit.markApproved(checker);
|
||||
this.productPendingEditRepository.saveAndFlush(pendingEdit);
|
||||
|
||||
final CommandSource auditEntry = findOrCreateAuditEntry(pendingEdit, productId);
|
||||
auditEntry.markAsChecked(checker);
|
||||
auditEntry.updateForAudit(result);
|
||||
this.commandSourceRepository.saveAndFlush(auditEntry);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private CommandSource findOrCreateAuditEntry(final ProductPendingEdit pendingEdit, final Long productId) {
|
||||
if (pendingEdit.getCommandSourceId() != null) {
|
||||
final java.util.Optional<CommandSource> existing = this.commandSourceRepository.findById(pendingEdit.getCommandSourceId());
|
||||
if (existing.isPresent()) {
|
||||
return existing.get();
|
||||
}
|
||||
}
|
||||
final CommandSource auditEntry = CommandSource.builder() //
|
||||
.actionName("UPDATE") //
|
||||
.entityName("FIXEDDEPOSITPRODUCT") //
|
||||
.resourceGetUrl("/fixeddepositproducts/" + productId) //
|
||||
.resourceId(productId) //
|
||||
.commandAsJson(pendingEdit.getProposedPayloadJson()) //
|
||||
.maker(pendingEdit.getMaker()) //
|
||||
.madeOnDate(DateUtils.getAuditOffsetDateTime()) //
|
||||
.status(CommandProcessingResultType.AWAITING_APPROVAL.getValue()) //
|
||||
.idempotencyKey(java.util.UUID.randomUUID().toString()) //
|
||||
.sanitized(false) //
|
||||
.build();
|
||||
this.commandSourceRepository.saveAndFlush(auditEntry);
|
||||
pendingEdit.setCommandSourceId(auditEntry.getId());
|
||||
this.productPendingEditRepository.saveAndFlush(pendingEdit);
|
||||
return auditEntry;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult rejectEdit(final Long productId, final String rejectReason) {
|
||||
|
||||
final AppUser checker = this.context.authenticatedUser();
|
||||
final ProductPendingEdit pendingEdit = this.productPendingEditRepository
|
||||
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_FD, productId, ProductPendingEdit.STATUS_PENDING)
|
||||
.orElseThrow(() -> new SavingsProductNoPendingEditException(productId));
|
||||
|
||||
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
||||
throw new SavingsProductSelfApprovalNotAllowedException(productId);
|
||||
}
|
||||
|
||||
pendingEdit.markRejected(checker, rejectReason);
|
||||
this.productPendingEditRepository.saveAndFlush(pendingEdit);
|
||||
|
||||
final CommandSource auditEntry = findOrCreateAuditEntry(pendingEdit, productId);
|
||||
auditEntry.markAsRejected(checker);
|
||||
this.commandSourceRepository.saveAndFlush(auditEntry);
|
||||
|
||||
return new CommandProcessingResultBuilder() //
|
||||
.withEntityId(productId) //
|
||||
.build();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult delete(final Long productId) {
|
||||
|
||||
+132
@@ -32,6 +32,9 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.apache.fineract.accounting.producttoaccountmapping.service.ProductToGLAccountMappingWritePlatformService;
|
||||
import org.apache.fineract.commands.domain.CommandProcessingResultType;
|
||||
import org.apache.fineract.commands.domain.CommandSource;
|
||||
import org.apache.fineract.commands.domain.CommandSourceRepository;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
@@ -39,6 +42,8 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuild
|
||||
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.charge.domain.Charge;
|
||||
import org.apache.fineract.portfolio.interestratechart.service.InterestRateChartAssembler;
|
||||
@@ -46,10 +51,16 @@ import org.apache.fineract.portfolio.savings.DepositAccountType;
|
||||
import org.apache.fineract.portfolio.savings.SavingsApiConstants;
|
||||
import org.apache.fineract.portfolio.savings.data.DepositProductDataValidator;
|
||||
import org.apache.fineract.portfolio.savings.domain.DepositProductAssembler;
|
||||
import org.apache.fineract.portfolio.savings.domain.ProductPendingEdit;
|
||||
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditRepository;
|
||||
import org.apache.fineract.portfolio.savings.domain.RecurringDepositProduct;
|
||||
import org.apache.fineract.portfolio.savings.domain.RecurringDepositProductRepository;
|
||||
import org.apache.fineract.portfolio.savings.exception.RecurringDepositProductNotFoundException;
|
||||
import org.apache.fineract.portfolio.savings.exception.SavingsProductEditPendingApprovalException;
|
||||
import org.apache.fineract.portfolio.savings.exception.SavingsProductNoPendingEditException;
|
||||
import org.apache.fineract.portfolio.savings.exception.SavingsProductSelfApprovalNotAllowedException;
|
||||
import org.apache.fineract.portfolio.tax.domain.TaxGroup;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -63,6 +74,9 @@ public class RecurringDepositProductWritePlatformServiceJpaRepositoryImpl implem
|
||||
private final DepositProductAssembler depositProductAssembler;
|
||||
private final ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService;
|
||||
private final InterestRateChartAssembler chartAssembler;
|
||||
private final ProductPendingEditRepository productPendingEditRepository;
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
private final CommandSourceRepository commandSourceRepository;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
@@ -150,6 +164,124 @@ public class RecurringDepositProductWritePlatformServiceJpaRepositoryImpl implem
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult submitEditForApproval(final Long productId, final JsonCommand command) {
|
||||
|
||||
final AppUser maker = this.context.authenticatedUser();
|
||||
final RecurringDepositProduct product = this.recurringDepositProductRepository.findById(productId)
|
||||
.orElseThrow(() -> new RecurringDepositProductNotFoundException(productId));
|
||||
|
||||
if (this.productPendingEditRepository
|
||||
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_RD, productId, ProductPendingEdit.STATUS_PENDING)
|
||||
.isPresent()) {
|
||||
throw new SavingsProductEditPendingApprovalException(productId);
|
||||
}
|
||||
|
||||
this.fromApiJsonDataValidator.validateForRecurringDepositUpdate(command.json());
|
||||
|
||||
final ProductPendingEdit pendingEdit = new ProductPendingEdit(ProductPendingEdit.TYPE_RD, productId, maker, command.json());
|
||||
|
||||
final CommandSource auditEntry = CommandSource.builder() //
|
||||
.actionName("UPDATE") //
|
||||
.entityName("RECURRINGDEPOSITPRODUCT") //
|
||||
.resourceGetUrl("/recurringdepositproducts/" + productId) //
|
||||
.resourceId(productId) //
|
||||
.commandAsJson(command.json()) //
|
||||
.maker(maker) //
|
||||
.madeOnDate(DateUtils.getAuditOffsetDateTime()) //
|
||||
.status(CommandProcessingResultType.AWAITING_APPROVAL.getValue()) //
|
||||
.idempotencyKey(java.util.UUID.randomUUID().toString()) //
|
||||
.sanitized(false) //
|
||||
.build();
|
||||
this.commandSourceRepository.saveAndFlush(auditEntry);
|
||||
pendingEdit.setCommandSourceId(auditEntry.getId());
|
||||
|
||||
this.productPendingEditRepository.saveAndFlush(pendingEdit);
|
||||
|
||||
return new CommandProcessingResultBuilder() //
|
||||
.withEntityId(product.getId()) //
|
||||
.build();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult approveEdit(final Long productId) {
|
||||
|
||||
final AppUser checker = this.context.authenticatedUser();
|
||||
final ProductPendingEdit pendingEdit = this.productPendingEditRepository
|
||||
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_RD, productId, ProductPendingEdit.STATUS_PENDING)
|
||||
.orElseThrow(() -> new SavingsProductNoPendingEditException(productId));
|
||||
|
||||
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
||||
throw new SavingsProductSelfApprovalNotAllowedException(productId);
|
||||
}
|
||||
|
||||
final JsonCommand replayedCommand = JsonCommand.fromJsonElement(productId,
|
||||
this.fromApiJsonHelper.parse(pendingEdit.getProposedPayloadJson()), this.fromApiJsonHelper);
|
||||
final CommandProcessingResult result = update(productId, replayedCommand);
|
||||
|
||||
pendingEdit.markApproved(checker);
|
||||
this.productPendingEditRepository.saveAndFlush(pendingEdit);
|
||||
|
||||
final CommandSource auditEntry = findOrCreateAuditEntry(pendingEdit, productId);
|
||||
auditEntry.markAsChecked(checker);
|
||||
auditEntry.updateForAudit(result);
|
||||
this.commandSourceRepository.saveAndFlush(auditEntry);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private CommandSource findOrCreateAuditEntry(final ProductPendingEdit pendingEdit, final Long productId) {
|
||||
if (pendingEdit.getCommandSourceId() != null) {
|
||||
final java.util.Optional<CommandSource> existing = this.commandSourceRepository.findById(pendingEdit.getCommandSourceId());
|
||||
if (existing.isPresent()) {
|
||||
return existing.get();
|
||||
}
|
||||
}
|
||||
final CommandSource auditEntry = CommandSource.builder() //
|
||||
.actionName("UPDATE") //
|
||||
.entityName("RECURRINGDEPOSITPRODUCT") //
|
||||
.resourceGetUrl("/recurringdepositproducts/" + productId) //
|
||||
.resourceId(productId) //
|
||||
.commandAsJson(pendingEdit.getProposedPayloadJson()) //
|
||||
.maker(pendingEdit.getMaker()) //
|
||||
.madeOnDate(DateUtils.getAuditOffsetDateTime()) //
|
||||
.status(CommandProcessingResultType.AWAITING_APPROVAL.getValue()) //
|
||||
.idempotencyKey(java.util.UUID.randomUUID().toString()) //
|
||||
.sanitized(false) //
|
||||
.build();
|
||||
this.commandSourceRepository.saveAndFlush(auditEntry);
|
||||
pendingEdit.setCommandSourceId(auditEntry.getId());
|
||||
this.productPendingEditRepository.saveAndFlush(pendingEdit);
|
||||
return auditEntry;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult rejectEdit(final Long productId, final String rejectReason) {
|
||||
|
||||
final AppUser checker = this.context.authenticatedUser();
|
||||
final ProductPendingEdit pendingEdit = this.productPendingEditRepository
|
||||
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_RD, productId, ProductPendingEdit.STATUS_PENDING)
|
||||
.orElseThrow(() -> new SavingsProductNoPendingEditException(productId));
|
||||
|
||||
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
||||
throw new SavingsProductSelfApprovalNotAllowedException(productId);
|
||||
}
|
||||
|
||||
pendingEdit.markRejected(checker, rejectReason);
|
||||
this.productPendingEditRepository.saveAndFlush(pendingEdit);
|
||||
|
||||
final CommandSource auditEntry = findOrCreateAuditEntry(pendingEdit, productId);
|
||||
auditEntry.markAsRejected(checker);
|
||||
this.commandSourceRepository.saveAndFlush(auditEntry);
|
||||
|
||||
return new CommandProcessingResultBuilder() //
|
||||
.withEntityId(productId) //
|
||||
.build();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult delete(final Long productId) {
|
||||
|
||||
+57
-1
@@ -38,12 +38,14 @@ import org.apache.fineract.infrastructure.security.service.PlatformSecurityConte
|
||||
import org.apache.fineract.organisation.monetary.data.CurrencyData;
|
||||
import org.apache.fineract.portfolio.savings.DepositAccountType;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsProductData;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsProductTempData;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityConfig;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityLimit;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsProductDepositLimitKyc;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityConfigRepository;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityLimitRepository;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsProductDepositLimitKycRepository;
|
||||
import org.apache.fineract.portfolio.savings.exception.SavingsProductNoPendingEditException;
|
||||
import org.apache.fineract.portfolio.savings.exception.SavingsProductNotFoundException;
|
||||
import org.apache.fineract.portfolio.tax.data.TaxGroupData;
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
@@ -170,6 +172,57 @@ public class SavingsProductReadPlatformServiceImpl implements SavingsProductRead
|
||||
}
|
||||
}
|
||||
|
||||
private static final String PENDING_EDIT_SCHEMA = "spt.id as tempId, spt.product_id as savingsProductId, "
|
||||
+ "sp.name as savingsProductName, spt.maker_id as makerId, maker.username as makerUsername, "
|
||||
+ "spt.checker_id as checkerId, checker.username as checkerUsername, spt.created_on as createdOn, "
|
||||
+ "spt.checked_on as checkedOn, spt.status as status, spt.reject_reason as rejectReason, "
|
||||
+ "spt.proposed_payload_json as proposedPayloadJson "
|
||||
+ "from m_savings_product_temp spt "
|
||||
+ "join m_savings_product sp on sp.id = spt.product_id "
|
||||
+ "join m_appuser maker on maker.id = spt.maker_id "
|
||||
+ "left join m_appuser checker on checker.id = spt.checker_id "
|
||||
+ "where spt.product_type = 'SAVINGS' ";
|
||||
|
||||
private static final RowMapper<SavingsProductTempData> PENDING_EDIT_MAPPER = (rs, rowNum) -> {
|
||||
final SavingsProductTempData data = new SavingsProductTempData();
|
||||
data.setId(rs.getLong("tempId"));
|
||||
data.setSavingsProductId(rs.getLong("savingsProductId"));
|
||||
data.setSavingsProductName(rs.getString("savingsProductName"));
|
||||
data.setMakerId(rs.getLong("makerId"));
|
||||
data.setMakerUsername(rs.getString("makerUsername"));
|
||||
data.setCheckerId(JdbcSupport.getLong(rs, "checkerId"));
|
||||
data.setCheckerUsername(rs.getString("checkerUsername"));
|
||||
data.setCreatedOn(rs.getTimestamp("createdOn") == null ? null : rs.getTimestamp("createdOn").toLocalDateTime());
|
||||
data.setCheckedOn(rs.getTimestamp("checkedOn") == null ? null : rs.getTimestamp("checkedOn").toLocalDateTime());
|
||||
data.setStatus(rs.getString("status"));
|
||||
data.setRejectReason(rs.getString("rejectReason"));
|
||||
final String proposedPayloadJson = rs.getString("proposedPayloadJson");
|
||||
if (proposedPayloadJson != null) {
|
||||
final Map<String, Object> proposedChanges = GSON.fromJson(proposedPayloadJson,
|
||||
new TypeToken<Map<String, Object>>() {}.getType());
|
||||
data.setProposedChanges(proposedChanges);
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
@Override
|
||||
public Collection<SavingsProductTempData> retrieveAllPendingEdits() {
|
||||
this.context.authenticatedUser();
|
||||
final String sql = "select " + PENDING_EDIT_SCHEMA + "and spt.status = 'PENDING' order by spt.created_on desc";
|
||||
return this.jdbcTemplate.query(sql, PENDING_EDIT_MAPPER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SavingsProductTempData retrievePendingEdit(final Long productId) {
|
||||
this.context.authenticatedUser();
|
||||
final String sql = "select " + PENDING_EDIT_SCHEMA + "and spt.status = 'PENDING' and spt.product_id = ?";
|
||||
try {
|
||||
return this.jdbcTemplate.queryForObject(sql, PENDING_EDIT_MAPPER, new Object[] { productId }); // NOSONAR
|
||||
} catch (final EmptyResultDataAccessException e) {
|
||||
throw new SavingsProductNoPendingEditException(productId);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SavingProductMapper implements RowMapper<SavingsProductData> {
|
||||
|
||||
private final String schemaSql;
|
||||
@@ -217,7 +270,9 @@ public class SavingsProductReadPlatformServiceImpl implements SavingsProductRead
|
||||
sqlBuilder.append("sp.auto_post as autoPost, sp.frequency_fix_day as frequencyFixDay, sp.last_interest_date as lastInterestDate, ");
|
||||
sqlBuilder.append("sp.enable_lockin_period as enableLockinPeriod, ");
|
||||
sqlBuilder.append("sp.balance_frequency as balanceFrequency, ");
|
||||
sqlBuilder.append("sp.account_closed_zero_balance_in_day_closing as accountClosedZeroBalanceInDayClosing ");
|
||||
sqlBuilder.append("sp.account_closed_zero_balance_in_day_closing as accountClosedZeroBalanceInDayClosing, ");
|
||||
sqlBuilder.append("exists(select 1 from m_savings_product_temp spt where spt.product_id = sp.id "
|
||||
+ "and spt.product_type = 'SAVINGS' and spt.status = 'PENDING') as isPendingApproval ");
|
||||
|
||||
sqlBuilder.append("from m_savings_product sp ");
|
||||
sqlBuilder.append("join m_currency curr on curr.code = sp.currency_code ");
|
||||
@@ -340,6 +395,7 @@ public class SavingsProductReadPlatformServiceImpl implements SavingsProductRead
|
||||
productData.setEnableLockinPeriod(rs.getBoolean("enableLockinPeriod"));
|
||||
productData.setBalanceFrequency(rs.getString("balanceFrequency"));
|
||||
productData.setAccountClosedZeroBalanceInDayClosing(rs.getBoolean("accountClosedZeroBalanceInDayClosing"));
|
||||
productData.setIsPendingApproval(rs.getBoolean("isPendingApproval"));
|
||||
|
||||
return productData;
|
||||
}
|
||||
|
||||
+236
@@ -46,6 +46,11 @@ import org.apache.fineract.infrastructure.security.service.PlatformSecurityConte
|
||||
import org.apache.fineract.portfolio.charge.domain.Charge;
|
||||
import org.apache.fineract.portfolio.savings.DepositAccountType;
|
||||
import org.apache.fineract.portfolio.savings.SavingsApiConstants;
|
||||
import org.apache.fineract.commands.domain.CommandProcessingResultType;
|
||||
import org.apache.fineract.commands.domain.CommandSource;
|
||||
import org.apache.fineract.commands.domain.CommandSourceRepository;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsProductDataValidator;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsProductDepositLimitKyc;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsProductDepositLimitKycRepository;
|
||||
@@ -56,8 +61,24 @@ import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityLimitR
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsProduct;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsProductRepository;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsProductAssembler;
|
||||
import org.apache.fineract.portfolio.savings.domain.ProductPendingEdit;
|
||||
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditRepository;
|
||||
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditCharge;
|
||||
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditChargeRepository;
|
||||
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFacilityLimit;
|
||||
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFacilityLimitRepository;
|
||||
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMapping;
|
||||
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMappingRepository;
|
||||
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedField;
|
||||
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedFieldRepository;
|
||||
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditFieldMapper;
|
||||
import com.google.gson.JsonObject;
|
||||
import org.apache.fineract.portfolio.savings.exception.SavingsProductEditPendingApprovalException;
|
||||
import org.apache.fineract.portfolio.savings.exception.SavingsProductNoPendingEditException;
|
||||
import org.apache.fineract.portfolio.savings.exception.SavingsProductNotFoundException;
|
||||
import org.apache.fineract.portfolio.savings.exception.SavingsProductSelfApprovalNotAllowedException;
|
||||
import org.apache.fineract.portfolio.tax.domain.TaxGroup;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -75,6 +96,13 @@ public class SavingsProductWritePlatformServiceJpaRepositoryImpl implements Savi
|
||||
private final SavingsProductFacilityConfigRepository facilityConfigRepository;
|
||||
private final SavingsProductFacilityLimitRepository facilityLimitRepository;
|
||||
private final SavingsProductDepositLimitKycRepository depositLimitKycRepository;
|
||||
private final ProductPendingEditRepository productPendingEditRepository;
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
private final CommandSourceRepository commandSourceRepository;
|
||||
private final ProductPendingEditChargeRepository pendingEditChargeRepository;
|
||||
private final ProductPendingEditFacilityLimitRepository pendingEditFacilityLimitRepository;
|
||||
private final ProductPendingEditGlMappingRepository pendingEditGlMappingRepository;
|
||||
private final ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository;
|
||||
|
||||
/*
|
||||
* Guaranteed to throw an exception no matter what the data integrity issue is.
|
||||
@@ -279,6 +307,214 @@ public class SavingsProductWritePlatformServiceJpaRepositoryImpl implements Savi
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult submitEditForApproval(final Long productId, final JsonCommand command) {
|
||||
|
||||
final AppUser maker = this.context.authenticatedUser();
|
||||
final SavingsProduct product = this.savingProductRepository.findById(productId)
|
||||
.orElseThrow(() -> new SavingsProductNotFoundException(productId));
|
||||
|
||||
if (this.productPendingEditRepository
|
||||
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_SAVINGS, productId, ProductPendingEdit.STATUS_PENDING)
|
||||
.isPresent()) {
|
||||
throw new SavingsProductEditPendingApprovalException(productId);
|
||||
}
|
||||
|
||||
// Reuses the same validator the direct-update path uses, so bad data can't sit "pending" undetected.
|
||||
this.fromApiJsonDataValidator.validateForUpdate(command.json(), product);
|
||||
|
||||
final ProductPendingEdit pendingEdit = new ProductPendingEdit(ProductPendingEdit.TYPE_SAVINGS, productId, maker, command.json());
|
||||
|
||||
// Typed-column storage: extract every recognised field into the pending edit's own columns
|
||||
// (plus satellite rows for the 1:many pieces) instead of relying solely on the JSON blob
|
||||
// above. proposedPayloadJson is still populated for now (safe deprecation window) but is no
|
||||
// longer what approveEdit replays from - see approveEdit().
|
||||
final java.util.Set<String> submitted = ProductPendingEditFieldMapper.populateCoreFields(command, pendingEdit.getCoreFields());
|
||||
final boolean facilityConfigSubmitted = ProductPendingEditFieldMapper.populateFacilityConfig(command,
|
||||
pendingEdit.getFacilityConfigFields());
|
||||
final boolean depositLimitKycSubmitted = ProductPendingEditFieldMapper.populateDepositLimitKyc(command,
|
||||
pendingEdit.getDepositLimitKycFields());
|
||||
final boolean glMappingSubmitted = ProductPendingEditFieldMapper.populateGlAccountMapping(command,
|
||||
pendingEdit.getGlAccountMappingFields());
|
||||
if (facilityConfigSubmitted) {
|
||||
submitted.add("accountFacilityConfig");
|
||||
}
|
||||
if (depositLimitKycSubmitted) {
|
||||
submitted.add("depositLimitKyc");
|
||||
}
|
||||
if (glMappingSubmitted) {
|
||||
submitted.add("glAccountMapping");
|
||||
}
|
||||
|
||||
// Audit-only entry so this submission is visible in System -> Audit Trails even though it bypasses the
|
||||
// native command-processing/maker-checker pipeline (the real gate is m_savings_product_temp itself).
|
||||
final CommandSource auditEntry = CommandSource.builder() //
|
||||
.actionName("UPDATE") //
|
||||
.entityName("SAVINGSPRODUCT") //
|
||||
.resourceGetUrl("/savingsproducts/" + productId) //
|
||||
.resourceId(productId) //
|
||||
.commandAsJson(command.json()) //
|
||||
.maker(maker) //
|
||||
.madeOnDate(DateUtils.getAuditOffsetDateTime()) //
|
||||
.status(CommandProcessingResultType.AWAITING_APPROVAL.getValue()) //
|
||||
.idempotencyKey(java.util.UUID.randomUUID().toString()) //
|
||||
.sanitized(false) //
|
||||
.build();
|
||||
this.commandSourceRepository.saveAndFlush(auditEntry);
|
||||
pendingEdit.setCommandSourceId(auditEntry.getId());
|
||||
|
||||
this.productPendingEditRepository.saveAndFlush(pendingEdit);
|
||||
|
||||
for (final String fieldName : submitted) {
|
||||
this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, fieldName));
|
||||
}
|
||||
|
||||
final List<Long> chargeIds = ProductPendingEditFieldMapper.extractChargeIds(command);
|
||||
if (chargeIds != null) {
|
||||
this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, "charges"));
|
||||
for (final Long chargeId : chargeIds) {
|
||||
this.pendingEditChargeRepository.save(new ProductPendingEditCharge(pendingEdit, chargeId));
|
||||
}
|
||||
}
|
||||
|
||||
final List<ProductPendingEditFacilityLimit> facilityLimits = ProductPendingEditFieldMapper.extractFacilityLimits(command,
|
||||
pendingEdit);
|
||||
if (facilityLimits != null) {
|
||||
this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, "accountFacilityLimits"));
|
||||
this.pendingEditFacilityLimitRepository.saveAll(facilityLimits);
|
||||
}
|
||||
|
||||
final List<ProductPendingEditGlMapping> glMappings = ProductPendingEditFieldMapper.extractGlArrayMappings(command, pendingEdit);
|
||||
if (glMappings != null) {
|
||||
this.pendingEditSubmittedFieldRepository.save(new ProductPendingEditSubmittedField(pendingEdit, "glArrayMappings"));
|
||||
this.pendingEditGlMappingRepository.saveAll(glMappings);
|
||||
}
|
||||
|
||||
return new CommandProcessingResultBuilder() //
|
||||
.withEntityId(product.getId()) //
|
||||
.build();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult approveEdit(final Long productId) {
|
||||
|
||||
final AppUser checker = this.context.authenticatedUser();
|
||||
final ProductPendingEdit pendingEdit = this.productPendingEditRepository
|
||||
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_SAVINGS, productId, ProductPendingEdit.STATUS_PENDING)
|
||||
.orElseThrow(() -> new SavingsProductNoPendingEditException(productId));
|
||||
|
||||
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
||||
throw new SavingsProductSelfApprovalNotAllowedException(productId);
|
||||
}
|
||||
|
||||
// Replay through the real update path so charges, GL/accounting mappings and other related
|
||||
// tables are applied exactly as a direct edit would apply them - not hand-copied field by
|
||||
// field. The replay JSON is rebuilt from this row's typed columns/satellite tables (the
|
||||
// manifest records exactly which top-level parameters the maker actually submitted); pending
|
||||
// edits created before this cutover have no manifest rows, so they fall back to replaying the
|
||||
// legacy JSON blob directly.
|
||||
final Set<String> submittedFields = new java.util.HashSet<>(
|
||||
this.pendingEditSubmittedFieldRepository.findFieldNamesByPendingEditId(pendingEdit.getId()));
|
||||
final JsonCommand replayedCommand;
|
||||
if (submittedFields.isEmpty()) {
|
||||
replayedCommand = JsonCommand.fromJsonElement(productId, this.fromApiJsonHelper.parse(pendingEdit.getProposedPayloadJson()),
|
||||
this.fromApiJsonHelper);
|
||||
} else {
|
||||
final JsonObject root = new JsonObject();
|
||||
root.addProperty("locale", "en");
|
||||
root.addProperty("dateFormat", "yyyy-MM-dd");
|
||||
ProductPendingEditFieldMapper.writeCoreFieldsToJson(root, pendingEdit.getCoreFields(), submittedFields);
|
||||
ProductPendingEditFieldMapper.writeFacilityConfigToJson(root, pendingEdit.getFacilityConfigFields(),
|
||||
submittedFields.contains("accountFacilityConfig"));
|
||||
ProductPendingEditFieldMapper.writeDepositLimitKycToJson(root, pendingEdit.getDepositLimitKycFields(),
|
||||
submittedFields.contains("depositLimitKyc"));
|
||||
if (submittedFields.contains("glAccountMapping")) {
|
||||
ProductPendingEditFieldMapper.writeGlAccountMappingToJson(root, pendingEdit.getGlAccountMappingFields());
|
||||
}
|
||||
if (submittedFields.contains("charges")) {
|
||||
ProductPendingEditFieldMapper.writeChargesToJson(root,
|
||||
this.pendingEditChargeRepository.findByPendingEditId(pendingEdit.getId()));
|
||||
}
|
||||
if (submittedFields.contains("accountFacilityLimits")) {
|
||||
ProductPendingEditFieldMapper.writeFacilityLimitsToJson(root,
|
||||
this.pendingEditFacilityLimitRepository.findByPendingEditId(pendingEdit.getId()));
|
||||
}
|
||||
if (submittedFields.contains("glArrayMappings")) {
|
||||
ProductPendingEditFieldMapper.writeGlArrayMappingsToJson(root,
|
||||
this.pendingEditGlMappingRepository.findByPendingEditId(pendingEdit.getId()));
|
||||
}
|
||||
replayedCommand = JsonCommand.fromJsonElement(productId, root, this.fromApiJsonHelper);
|
||||
}
|
||||
final CommandProcessingResult result = update(productId, replayedCommand);
|
||||
|
||||
pendingEdit.markApproved(checker);
|
||||
this.productPendingEditRepository.saveAndFlush(pendingEdit);
|
||||
|
||||
final CommandSource auditEntry = findOrCreateAuditEntry(pendingEdit, productId);
|
||||
auditEntry.markAsChecked(checker);
|
||||
auditEntry.updateForAudit(result);
|
||||
this.commandSourceRepository.saveAndFlush(auditEntry);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every edit submitted through submitEditForApproval() links a CommandSource audit row. This fallback only
|
||||
* matters for pending edits that were created before that linking existed - it backfills one now so approve
|
||||
* and reject always leave an entry in m_portfolio_command_source, with no silent no-op case.
|
||||
*/
|
||||
private CommandSource findOrCreateAuditEntry(final ProductPendingEdit pendingEdit, final Long productId) {
|
||||
if (pendingEdit.getCommandSourceId() != null) {
|
||||
final java.util.Optional<CommandSource> existing = this.commandSourceRepository.findById(pendingEdit.getCommandSourceId());
|
||||
if (existing.isPresent()) {
|
||||
return existing.get();
|
||||
}
|
||||
}
|
||||
final CommandSource auditEntry = CommandSource.builder() //
|
||||
.actionName("UPDATE") //
|
||||
.entityName("SAVINGSPRODUCT") //
|
||||
.resourceGetUrl("/savingsproducts/" + productId) //
|
||||
.resourceId(productId) //
|
||||
.commandAsJson(pendingEdit.getProposedPayloadJson()) //
|
||||
.maker(pendingEdit.getMaker()) //
|
||||
.madeOnDate(DateUtils.getAuditOffsetDateTime()) //
|
||||
.status(CommandProcessingResultType.AWAITING_APPROVAL.getValue()) //
|
||||
.idempotencyKey(java.util.UUID.randomUUID().toString()) //
|
||||
.sanitized(false) //
|
||||
.build();
|
||||
this.commandSourceRepository.saveAndFlush(auditEntry);
|
||||
pendingEdit.setCommandSourceId(auditEntry.getId());
|
||||
this.productPendingEditRepository.saveAndFlush(pendingEdit);
|
||||
return auditEntry;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult rejectEdit(final Long productId, final String rejectReason) {
|
||||
|
||||
final AppUser checker = this.context.authenticatedUser();
|
||||
final ProductPendingEdit pendingEdit = this.productPendingEditRepository
|
||||
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_SAVINGS, productId, ProductPendingEdit.STATUS_PENDING)
|
||||
.orElseThrow(() -> new SavingsProductNoPendingEditException(productId));
|
||||
|
||||
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
||||
throw new SavingsProductSelfApprovalNotAllowedException(productId);
|
||||
}
|
||||
|
||||
pendingEdit.markRejected(checker, rejectReason);
|
||||
this.productPendingEditRepository.saveAndFlush(pendingEdit);
|
||||
|
||||
final CommandSource auditEntry = findOrCreateAuditEntry(pendingEdit, productId);
|
||||
auditEntry.markAsRejected(checker);
|
||||
this.commandSourceRepository.saveAndFlush(auditEntry);
|
||||
|
||||
return new CommandProcessingResultBuilder() //
|
||||
.withEntityId(productId) //
|
||||
.build();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult delete(final Long productId) {
|
||||
|
||||
+23
-6
@@ -96,6 +96,8 @@ import org.apache.fineract.portfolio.savings.domain.SavingsProductAssembler;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsProductDepositLimitKycRepository;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityConfig;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityConfigRepository;
|
||||
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditRepository;
|
||||
import org.apache.fineract.commands.domain.CommandSourceRepository;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityLimit;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityLimitRepository;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsProductRepository;
|
||||
@@ -324,9 +326,12 @@ public class SavingsConfiguration {
|
||||
public FixedDepositProductWritePlatformService fixedDepositProductWritePlatformService(PlatformSecurityContext context,
|
||||
FixedDepositProductRepository fixedDepositProductRepository, DepositProductDataValidator fromApiJsonDataValidator,
|
||||
DepositProductAssembler depositProductAssembler,
|
||||
ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService, InterestRateChartAssembler chartAssembler) {
|
||||
ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService, InterestRateChartAssembler chartAssembler,
|
||||
ProductPendingEditRepository productPendingEditRepository, FromJsonHelper fromApiJsonHelper,
|
||||
CommandSourceRepository commandSourceRepository) {
|
||||
return new FixedDepositProductWritePlatformServiceJpaRepositoryImpl(context, fixedDepositProductRepository,
|
||||
fromApiJsonDataValidator, depositProductAssembler, accountMappingWritePlatformService, chartAssembler);
|
||||
fromApiJsonDataValidator, depositProductAssembler, accountMappingWritePlatformService, chartAssembler,
|
||||
productPendingEditRepository, fromApiJsonHelper, commandSourceRepository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -348,9 +353,12 @@ public class SavingsConfiguration {
|
||||
public RecurringDepositProductWritePlatformService recurringDepositProductWritePlatformService(PlatformSecurityContext context,
|
||||
RecurringDepositProductRepository recurringDepositProductRepository, DepositProductDataValidator fromApiJsonDataValidator,
|
||||
DepositProductAssembler depositProductAssembler,
|
||||
ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService, InterestRateChartAssembler chartAssembler) {
|
||||
ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService, InterestRateChartAssembler chartAssembler,
|
||||
ProductPendingEditRepository productPendingEditRepository, FromJsonHelper fromApiJsonHelper,
|
||||
CommandSourceRepository commandSourceRepository) {
|
||||
return new RecurringDepositProductWritePlatformServiceJpaRepositoryImpl(context, recurringDepositProductRepository,
|
||||
fromApiJsonDataValidator, depositProductAssembler, accountMappingWritePlatformService, chartAssembler);
|
||||
fromApiJsonDataValidator, depositProductAssembler, accountMappingWritePlatformService, chartAssembler,
|
||||
productPendingEditRepository, fromApiJsonHelper, commandSourceRepository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -489,10 +497,19 @@ public class SavingsConfiguration {
|
||||
FineractEntityAccessUtil fineractEntityAccessUtil,
|
||||
SavingsProductFacilityConfigRepository facilityConfigRepository,
|
||||
SavingsProductFacilityLimitRepository facilityLimitRepository,
|
||||
SavingsProductDepositLimitKycRepository depositLimitKycRepository) {
|
||||
SavingsProductDepositLimitKycRepository depositLimitKycRepository,
|
||||
ProductPendingEditRepository productPendingEditRepository,
|
||||
FromJsonHelper fromApiJsonHelper,
|
||||
CommandSourceRepository commandSourceRepository,
|
||||
org.apache.fineract.portfolio.savings.domain.ProductPendingEditChargeRepository pendingEditChargeRepository,
|
||||
org.apache.fineract.portfolio.savings.domain.ProductPendingEditFacilityLimitRepository pendingEditFacilityLimitRepository,
|
||||
org.apache.fineract.portfolio.savings.domain.ProductPendingEditGlMappingRepository pendingEditGlMappingRepository,
|
||||
org.apache.fineract.portfolio.savings.domain.ProductPendingEditSubmittedFieldRepository pendingEditSubmittedFieldRepository) {
|
||||
return new SavingsProductWritePlatformServiceJpaRepositoryImpl(context, savingProductRepository, fromApiJsonDataValidator,
|
||||
savingsProductAssembler, accountMappingWritePlatformService, fineractEntityAccessUtil, facilityConfigRepository,
|
||||
facilityLimitRepository, depositLimitKycRepository);
|
||||
facilityLimitRepository, depositLimitKycRepository, productPendingEditRepository, fromApiJsonHelper,
|
||||
commandSourceRepository, pendingEditChargeRepository, pendingEditFacilityLimitRepository, pendingEditGlMappingRepository,
|
||||
pendingEditSubmittedFieldRepository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.shareproducts.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
public class ShareProductEditPendingApprovalException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public ShareProductEditPendingApprovalException(final Long productId) {
|
||||
super("error.msg.shareproduct.edit.pending.approval",
|
||||
"Share product with identifier " + productId + " already has an edit pending approval", productId);
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.shareproducts.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
public class ShareProductNoPendingEditException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public ShareProductNoPendingEditException(final Long productId) {
|
||||
super("error.msg.shareproduct.no.pending.edit", "Share product with identifier " + productId + " has no edit pending approval",
|
||||
productId);
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.shareproducts.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
public class ShareProductSelfApprovalNotAllowedException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public ShareProductSelfApprovalNotAllowedException(final Long productId) {
|
||||
super("error.msg.shareproduct.self.approval.not.allowed",
|
||||
"The maker of the pending edit for share product " + productId + " cannot also approve or reject it", productId);
|
||||
}
|
||||
}
|
||||
+53
@@ -18,6 +18,8 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.shareproducts.service;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import java.math.BigDecimal;
|
||||
import java.sql.Date;
|
||||
import java.sql.ResultSet;
|
||||
@@ -46,9 +48,11 @@ import org.apache.fineract.portfolio.charge.service.ChargeReadPlatformService;
|
||||
import org.apache.fineract.portfolio.products.data.ProductData;
|
||||
import org.apache.fineract.portfolio.products.exception.ProductNotFoundException;
|
||||
import org.apache.fineract.portfolio.products.service.ShareProductReadPlatformService;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsProductTempData;
|
||||
import org.apache.fineract.portfolio.shareaccounts.service.SharesEnumerations;
|
||||
import org.apache.fineract.portfolio.shareproducts.data.ShareProductData;
|
||||
import org.apache.fineract.portfolio.shareproducts.data.ShareProductMarketPriceData;
|
||||
import org.apache.fineract.portfolio.shareproducts.exception.ShareProductNoPendingEditException;
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
@@ -152,6 +156,55 @@ public class ShareProductReadPlatformServiceImpl implements ShareProductReadPlat
|
||||
return null;
|
||||
}
|
||||
|
||||
private static final Gson GSON = new Gson();
|
||||
|
||||
private static final String PENDING_EDIT_SCHEMA = "spt.id as tempId, spt.product_id as savingsProductId, "
|
||||
+ "sp.name as savingsProductName, spt.maker_id as makerId, maker.username as makerUsername, "
|
||||
+ "spt.checker_id as checkerId, checker.username as checkerUsername, spt.created_on as createdOn, "
|
||||
+ "spt.checked_on as checkedOn, spt.status as status, spt.reject_reason as rejectReason, "
|
||||
+ "spt.proposed_payload_json as proposedPayloadJson "
|
||||
+ "from m_savings_product_temp spt "
|
||||
+ "join m_share_product sp on sp.id = spt.product_id "
|
||||
+ "join m_appuser maker on maker.id = spt.maker_id "
|
||||
+ "left join m_appuser checker on checker.id = spt.checker_id "
|
||||
+ "where spt.product_type = 'SHARE' ";
|
||||
|
||||
private static final RowMapper<SavingsProductTempData> PENDING_EDIT_MAPPER = (rs, rowNum) -> {
|
||||
final SavingsProductTempData data = new SavingsProductTempData();
|
||||
data.setId(rs.getLong("tempId"));
|
||||
data.setSavingsProductId(rs.getLong("savingsProductId"));
|
||||
data.setSavingsProductName(rs.getString("savingsProductName"));
|
||||
data.setMakerId(rs.getLong("makerId"));
|
||||
data.setMakerUsername(rs.getString("makerUsername"));
|
||||
data.setCheckerId(JdbcSupport.getLong(rs, "checkerId"));
|
||||
data.setCheckerUsername(rs.getString("checkerUsername"));
|
||||
data.setCreatedOn(rs.getTimestamp("createdOn") == null ? null : rs.getTimestamp("createdOn").toLocalDateTime());
|
||||
data.setCheckedOn(rs.getTimestamp("checkedOn") == null ? null : rs.getTimestamp("checkedOn").toLocalDateTime());
|
||||
data.setStatus(rs.getString("status"));
|
||||
data.setRejectReason(rs.getString("rejectReason"));
|
||||
final String proposedPayloadJson = rs.getString("proposedPayloadJson");
|
||||
if (proposedPayloadJson != null) {
|
||||
data.setProposedChanges(GSON.fromJson(proposedPayloadJson, new TypeToken<Map<String, Object>>() {}.getType()));
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
@Override
|
||||
public Collection<SavingsProductTempData> retrieveAllPendingEdits() {
|
||||
final String sql = "select " + PENDING_EDIT_SCHEMA + "and spt.status = 'PENDING' order by spt.created_on desc";
|
||||
return this.jdbcTemplate.query(sql, PENDING_EDIT_MAPPER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SavingsProductTempData retrievePendingEdit(final Long productId) {
|
||||
final String sql = "select " + PENDING_EDIT_SCHEMA + "and spt.status = 'PENDING' and spt.product_id = ?";
|
||||
try {
|
||||
return this.jdbcTemplate.queryForObject(sql, PENDING_EDIT_MAPPER, new Object[] { productId }); // NOSONAR
|
||||
} catch (final EmptyResultDataAccessException e) {
|
||||
throw new ShareProductNoPendingEditException(productId);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class AllShareProductRowMapper implements RowMapper<ProductData> {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
|
||||
+17
@@ -32,4 +32,21 @@ public interface ShareProductWritePlatformService {
|
||||
CommandProcessingResult approveShareProductDividend(Long PayOutDetailId);
|
||||
|
||||
CommandProcessingResult deleteShareProductDividend(Long PayOutDetailId);
|
||||
|
||||
/**
|
||||
* Submits an edit for approval instead of applying it directly. Fails if a pending edit already exists for this
|
||||
* product (that pending row is itself the lock).
|
||||
*/
|
||||
CommandProcessingResult submitEditForApproval(Long productId, JsonCommand command);
|
||||
|
||||
/**
|
||||
* Replays the stored pending edit through the normal update path and marks it approved. Fails if the approving
|
||||
* user is the same user who submitted the edit.
|
||||
*/
|
||||
CommandProcessingResult approveEdit(Long productId);
|
||||
|
||||
/**
|
||||
* Discards the pending edit. Fails if the rejecting user is the same user who submitted the edit.
|
||||
*/
|
||||
CommandProcessingResult rejectEdit(Long productId, String rejectReason);
|
||||
}
|
||||
|
||||
+130
@@ -27,23 +27,35 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.apache.fineract.accounting.producttoaccountmapping.service.ProductToGLAccountMappingWritePlatformService;
|
||||
import org.apache.fineract.commands.domain.CommandProcessingResultType;
|
||||
import org.apache.fineract.commands.domain.CommandSource;
|
||||
import org.apache.fineract.commands.domain.CommandSourceRepository;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.share.ShareProductDividentsCreateBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.savings.domain.ProductPendingEdit;
|
||||
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditRepository;
|
||||
import org.apache.fineract.portfolio.shareproducts.constants.ShareProductApiConstants;
|
||||
import org.apache.fineract.portfolio.shareproducts.domain.ShareProduct;
|
||||
import org.apache.fineract.portfolio.shareproducts.domain.ShareProductDividendPayOutDetails;
|
||||
import org.apache.fineract.portfolio.shareproducts.domain.ShareProductDividentPayOutDetailsRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.shareproducts.domain.ShareProductRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.shareproducts.exception.DividentProcessingException;
|
||||
import org.apache.fineract.portfolio.shareproducts.exception.ShareProductEditPendingApprovalException;
|
||||
import org.apache.fineract.portfolio.shareproducts.exception.ShareProductNoPendingEditException;
|
||||
import org.apache.fineract.portfolio.shareproducts.exception.ShareProductSelfApprovalNotAllowedException;
|
||||
import org.apache.fineract.portfolio.shareproducts.serialization.ShareProductDataSerializer;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.orm.jpa.JpaSystemException;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@@ -56,6 +68,9 @@ public class ShareProductWritePlatformServiceJpaRepositoryImpl implements ShareP
|
||||
private final ShareProductDividendAssembler shareProductDividendAssembler;
|
||||
private final ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService;
|
||||
private final BusinessEventNotifierService businessEventNotifierService;
|
||||
private final PlatformSecurityContext context;
|
||||
private final ProductPendingEditRepository productPendingEditRepository;
|
||||
private final CommandSourceRepository commandSourceRepository;
|
||||
|
||||
@Override
|
||||
public CommandProcessingResult createShareProduct(JsonCommand jsonCommand) {
|
||||
@@ -180,6 +195,121 @@ public class ShareProductWritePlatformServiceJpaRepositoryImpl implements ShareP
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult submitEditForApproval(final Long productId, final JsonCommand command) {
|
||||
|
||||
final AppUser maker = this.context.authenticatedUser();
|
||||
final ShareProduct product = this.repository.findOneWithNotFoundDetection(productId);
|
||||
|
||||
if (this.productPendingEditRepository
|
||||
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_SHARE, productId, ProductPendingEdit.STATUS_PENDING)
|
||||
.isPresent()) {
|
||||
throw new ShareProductEditPendingApprovalException(productId);
|
||||
}
|
||||
|
||||
final ProductPendingEdit pendingEdit = new ProductPendingEdit(ProductPendingEdit.TYPE_SHARE, productId, maker, command.json());
|
||||
|
||||
final CommandSource auditEntry = CommandSource.builder() //
|
||||
.actionName("UPDATE") //
|
||||
.entityName("SHAREPRODUCT") //
|
||||
.resourceGetUrl("/products/share/" + productId) //
|
||||
.resourceId(productId) //
|
||||
.commandAsJson(command.json()) //
|
||||
.maker(maker) //
|
||||
.madeOnDate(DateUtils.getAuditOffsetDateTime()) //
|
||||
.status(CommandProcessingResultType.AWAITING_APPROVAL.getValue()) //
|
||||
.idempotencyKey(java.util.UUID.randomUUID().toString()) //
|
||||
.sanitized(false) //
|
||||
.build();
|
||||
this.commandSourceRepository.saveAndFlush(auditEntry);
|
||||
pendingEdit.setCommandSourceId(auditEntry.getId());
|
||||
|
||||
this.productPendingEditRepository.saveAndFlush(pendingEdit);
|
||||
|
||||
return new CommandProcessingResultBuilder() //
|
||||
.withEntityId(product.getId()) //
|
||||
.build();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult approveEdit(final Long productId) {
|
||||
|
||||
final AppUser checker = this.context.authenticatedUser();
|
||||
final ProductPendingEdit pendingEdit = this.productPendingEditRepository
|
||||
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_SHARE, productId, ProductPendingEdit.STATUS_PENDING)
|
||||
.orElseThrow(() -> new ShareProductNoPendingEditException(productId));
|
||||
|
||||
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
||||
throw new ShareProductSelfApprovalNotAllowedException(productId);
|
||||
}
|
||||
|
||||
final JsonCommand replayedCommand = JsonCommand.fromJsonElement(productId,
|
||||
this.fromApiJsonHelper.parse(pendingEdit.getProposedPayloadJson()), this.fromApiJsonHelper);
|
||||
final CommandProcessingResult result = updateProduct(productId, replayedCommand);
|
||||
|
||||
pendingEdit.markApproved(checker);
|
||||
this.productPendingEditRepository.saveAndFlush(pendingEdit);
|
||||
|
||||
final CommandSource auditEntry = findOrCreateAuditEntry(pendingEdit, productId);
|
||||
auditEntry.markAsChecked(checker);
|
||||
auditEntry.updateForAudit(result);
|
||||
this.commandSourceRepository.saveAndFlush(auditEntry);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private CommandSource findOrCreateAuditEntry(final ProductPendingEdit pendingEdit, final Long productId) {
|
||||
if (pendingEdit.getCommandSourceId() != null) {
|
||||
final java.util.Optional<CommandSource> existing = this.commandSourceRepository.findById(pendingEdit.getCommandSourceId());
|
||||
if (existing.isPresent()) {
|
||||
return existing.get();
|
||||
}
|
||||
}
|
||||
final CommandSource auditEntry = CommandSource.builder() //
|
||||
.actionName("UPDATE") //
|
||||
.entityName("SHAREPRODUCT") //
|
||||
.resourceGetUrl("/products/share/" + productId) //
|
||||
.resourceId(productId) //
|
||||
.commandAsJson(pendingEdit.getProposedPayloadJson()) //
|
||||
.maker(pendingEdit.getMaker()) //
|
||||
.madeOnDate(DateUtils.getAuditOffsetDateTime()) //
|
||||
.status(CommandProcessingResultType.AWAITING_APPROVAL.getValue()) //
|
||||
.idempotencyKey(java.util.UUID.randomUUID().toString()) //
|
||||
.sanitized(false) //
|
||||
.build();
|
||||
this.commandSourceRepository.saveAndFlush(auditEntry);
|
||||
pendingEdit.setCommandSourceId(auditEntry.getId());
|
||||
this.productPendingEditRepository.saveAndFlush(pendingEdit);
|
||||
return auditEntry;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult rejectEdit(final Long productId, final String rejectReason) {
|
||||
|
||||
final AppUser checker = this.context.authenticatedUser();
|
||||
final ProductPendingEdit pendingEdit = this.productPendingEditRepository
|
||||
.findByProductTypeAndProductIdAndStatus(ProductPendingEdit.TYPE_SHARE, productId, ProductPendingEdit.STATUS_PENDING)
|
||||
.orElseThrow(() -> new ShareProductNoPendingEditException(productId));
|
||||
|
||||
if (pendingEdit.getMaker().getId().equals(checker.getId()) && !checker.isCheckerSuperUser()) {
|
||||
throw new ShareProductSelfApprovalNotAllowedException(productId);
|
||||
}
|
||||
|
||||
pendingEdit.markRejected(checker, rejectReason);
|
||||
this.productPendingEditRepository.saveAndFlush(pendingEdit);
|
||||
|
||||
final CommandSource auditEntry = findOrCreateAuditEntry(pendingEdit, productId);
|
||||
auditEntry.markAsRejected(checker);
|
||||
this.commandSourceRepository.saveAndFlush(auditEntry);
|
||||
|
||||
return new CommandProcessingResultBuilder() //
|
||||
.withEntityId(productId) //
|
||||
.build();
|
||||
}
|
||||
|
||||
private void handleDataIntegrityIssues(final Exception e) {
|
||||
log.error("Unknown data integrity issue with resource", e);
|
||||
throw ErrorHandler.getMappable(e, "error.msg.shareproduct.unknown.data.integrity.issue",
|
||||
|
||||
+8
-2
@@ -24,11 +24,14 @@ import org.apache.fineract.accounting.producttoaccountmapping.service.ProductToG
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.core.service.PaginationHelper;
|
||||
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
|
||||
import org.apache.fineract.commands.domain.CommandSourceRepository;
|
||||
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.infrastructure.security.utils.ColumnValidator;
|
||||
import org.apache.fineract.organisation.monetary.service.CurrencyReadPlatformService;
|
||||
import org.apache.fineract.portfolio.charge.service.ChargeReadPlatformService;
|
||||
import org.apache.fineract.portfolio.products.service.ShareProductReadPlatformService;
|
||||
import org.apache.fineract.portfolio.savings.domain.ProductPendingEditRepository;
|
||||
import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountReadPlatformService;
|
||||
import org.apache.fineract.portfolio.shareproducts.domain.ShareProductDividentPayOutDetailsRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.shareproducts.domain.ShareProductRepositoryWrapper;
|
||||
@@ -99,10 +102,13 @@ public class ShareProductsConfiguration {
|
||||
ShareProductDividentPayOutDetailsRepositoryWrapper shareProductDividentPayOutDetailsRepository,
|
||||
ShareProductDividendAssembler shareProductDividendAssembler,
|
||||
ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService,
|
||||
BusinessEventNotifierService businessEventNotifierService) {
|
||||
BusinessEventNotifierService businessEventNotifierService,
|
||||
PlatformSecurityContext context,
|
||||
ProductPendingEditRepository productPendingEditRepository,
|
||||
CommandSourceRepository commandSourceRepository) {
|
||||
return new ShareProductWritePlatformServiceJpaRepositoryImpl(repository, serializer, fromApiJsonHelper,
|
||||
shareProductDividentPayOutDetailsRepository, shareProductDividendAssembler, accountMappingWritePlatformService,
|
||||
businessEventNotifierService);
|
||||
businessEventNotifierService, context, productPendingEditRepository, commandSourceRepository);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user