diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index af0fb4ddd..0f2140efd 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -756,6 +756,14 @@ public class CommandWrapperBuilder { return this; } + public CommandWrapperBuilder deleteFund(final Long fundId) { + this.actionName = "DELETE"; + this.entityName = "FUND"; + this.entityId = fundId; + this.href = "/funds/" + fundId; + return this; + } + public CommandWrapperBuilder createReport() { this.actionName = "CREATE"; this.entityName = "REPORT"; @@ -884,6 +892,70 @@ public class CommandWrapperBuilder { return this; } + public CommandWrapperBuilder createPaymentType() { + this.actionName = "CREATE"; + this.entityName = "PAYMENTTYPE"; + this.entityId = null; + this.href = "/paymenttypes/template"; + return this; + } + + public CommandWrapperBuilder updatePaymentType(final Long paymentTypeId) { + this.actionName = "UPDATE"; + this.entityName = "PAYMENTTYPE"; + this.entityId = paymentTypeId; + this.href = "/paymenttypes/" + paymentTypeId; + return this; + } + + public CommandWrapperBuilder deletePaymentType(final Long paymentTypeId) { + this.actionName = "DELETE"; + this.entityName = "PAYMENTTYPE"; + this.entityId = paymentTypeId; + this.href = "/paymenttypes/" + paymentTypeId; + return this; + } + + public CommandWrapperBuilder createSponsorBank() { + this.actionName = "CREATE"; + this.entityName = "SPONSORBANK"; + this.entityId = null; + this.href = "/sponsorbanks"; + return this; + } + + public CommandWrapperBuilder updateSponsorBank(final Long sponsorBankId) { + this.actionName = "UPDATE"; + this.entityName = "SPONSORBANK"; + this.entityId = sponsorBankId; + this.href = "/sponsorbanks/" + sponsorBankId; + return this; + } + + public CommandWrapperBuilder updateSponsorDdGlMapping() { + this.actionName = "UPDATE"; + this.entityName = "SPONSORDDGLMAPPING"; + this.entityId = null; + this.href = "/sponsordd/gl-mapping"; + return this; + } + + public CommandWrapperBuilder updateInstitutionProfile() { + this.actionName = "UPDATE"; + this.entityName = "INSTITUTIONPROFILE"; + this.entityId = null; + this.href = "/institutionprofile"; + return this; + } + + public CommandWrapperBuilder updateBranchProfile(final Long officeId) { + this.actionName = "UPDATE"; + this.entityName = "BRANCHPROFILE"; + this.entityId = officeId; + this.href = "/branchprofile/" + officeId; + return this; + } + public CommandWrapperBuilder createLoanProduct() { this.actionName = "CREATE"; this.entityName = "LOANPRODUCT"; @@ -3390,6 +3462,13 @@ public class CommandWrapperBuilder { return this; } + public CommandWrapperBuilder updateCurrency() { + this.actionName = "UPDATE"; + this.entityName = "CURRENCY"; + this.href = "/currencies"; + return this; + } + public CommandWrapperBuilder updateExternalServiceProperties(final String externalServiceName) { this.actionName = "UPDATE"; this.entityName = "EXTERNALSERVICES"; @@ -4730,4 +4809,36 @@ public class CommandWrapperBuilder { this.href = "/loans/" + loanId + "/originators/" + originatorId; return this; } + + public CommandWrapperBuilder createSurvey() { + this.actionName = "CREATE"; + this.entityName = "SURVEY"; + this.entityId = null; + this.href = "/surveys"; + return this; + } + + public CommandWrapperBuilder updateSurvey(final Long surveyId) { + this.actionName = "UPDATE"; + this.entityName = "SURVEY"; + this.entityId = surveyId; + this.href = "/surveys/" + surveyId; + return this; + } + + public CommandWrapperBuilder activateSurvey(final Long surveyId) { + this.actionName = "ACTIVATE"; + this.entityName = "SURVEY"; + this.entityId = surveyId; + this.href = "/surveys/" + surveyId; + return this; + } + + public CommandWrapperBuilder deactivateSurvey(final Long surveyId) { + this.actionName = "DEACTIVATE"; + this.entityName = "SURVEY"; + this.entityId = surveyId; + this.href = "/surveys/" + surveyId; + return this; + } } diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/api/CurrenciesApiResource.java b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/api/CurrenciesApiResource.java index ff3ac4e61..953541927 100644 --- a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/api/CurrenciesApiResource.java +++ b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/api/CurrenciesApiResource.java @@ -27,13 +27,14 @@ import jakarta.ws.rs.PUT; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; -import java.util.function.Supplier; import lombok.RequiredArgsConstructor; -import org.apache.fineract.command.core.CommandPipeline; -import org.apache.fineract.organisation.monetary.command.CurrencyUpdateCommand; +import org.apache.fineract.commands.domain.CommandWrapper; +import org.apache.fineract.commands.service.CommandWrapperBuilder; +import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; import org.apache.fineract.organisation.monetary.data.CurrencyConfigurationData; import org.apache.fineract.organisation.monetary.data.CurrencyUpdateRequest; -import org.apache.fineract.organisation.monetary.data.CurrencyUpdateResponse; import org.apache.fineract.organisation.monetary.service.OrganisationCurrencyReadPlatformService; import org.springframework.stereotype.Component; @@ -44,7 +45,8 @@ import org.springframework.stereotype.Component; public class CurrenciesApiResource { private final OrganisationCurrencyReadPlatformService readPlatformService; - private final CommandPipeline commandPipeline; + private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; + private final DefaultToApiJsonSerializer toApiJsonSerializer; @GET @Consumes({ MediaType.APPLICATION_JSON }) @@ -65,13 +67,12 @@ public class CurrenciesApiResource { @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) @Operation(summary = "Update Currency Configuration", description = "Updates the list of currencies permitted for use.") - public CurrencyUpdateResponse updateCurrencies(@Valid CurrencyUpdateRequest request) { - final var command = new CurrencyUpdateCommand(); + public String updateCurrencies(@Valid final CurrencyUpdateRequest request) { + final CommandWrapper commandRequest = new CommandWrapperBuilder().updateCurrency() + .withJson(toApiJsonSerializer.serialize(request)).build(); - command.setPayload(request); + final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - final Supplier response = commandPipeline.send(command); - - return response.get(); + return toApiJsonSerializer.serialize(result); } } diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/handler/UpdateCurrencyCommandHandler.java b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/handler/UpdateCurrencyCommandHandler.java new file mode 100644 index 000000000..d4aa3a9e1 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/handler/UpdateCurrencyCommandHandler.java @@ -0,0 +1,63 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.organisation.monetary.handler; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.organisation.monetary.data.CurrencyUpdateRequest; +import org.apache.fineract.organisation.monetary.data.CurrencyUpdateResponse; +import org.apache.fineract.organisation.monetary.service.CurrencyWritePlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@CommandType(entity = "CURRENCY", action = "UPDATE") +public class UpdateCurrencyCommandHandler implements NewCommandSourceHandler { + + private final CurrencyWritePlatformService writePlatformService; + + @Autowired + public UpdateCurrencyCommandHandler(final CurrencyWritePlatformService writePlatformService) { + this.writePlatformService = writePlatformService; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + + final String[] currencyCodes = command.arrayValueOfParameterNamed("currencies"); + final List currencies = currencyCodes == null ? List.of() : Arrays.asList(currencyCodes); + + final CurrencyUpdateRequest request = CurrencyUpdateRequest.builder().currencies(currencies).build(); + + final CurrencyUpdateResponse response = this.writePlatformService.updateAllowedCurrencies(request); + + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .with(Map.of("currencies", response.getCurrencies())) // + .build(); + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/paymenttype/api/PaymentTypeApiResource.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/paymenttype/api/PaymentTypeApiResource.java index 4be438b08..6ca135657 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/paymenttype/api/PaymentTypeApiResource.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/paymenttype/api/PaymentTypeApiResource.java @@ -31,20 +31,15 @@ import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import java.util.List; -import java.util.function.Supplier; import lombok.RequiredArgsConstructor; -import org.apache.fineract.command.core.CommandPipeline; +import org.apache.fineract.commands.domain.CommandWrapper; +import org.apache.fineract.commands.service.CommandWrapperBuilder; +import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; -import org.apache.fineract.portfolio.paymenttype.command.PaymentTypeCreateCommand; -import org.apache.fineract.portfolio.paymenttype.command.PaymentTypeDeleteCommand; -import org.apache.fineract.portfolio.paymenttype.command.PaymentTypeUpdateCommand; import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeCreateRequest; -import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeCreateResponse; import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; -import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeDeleteRequest; -import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeDeleteResponse; import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeUpdateRequest; -import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeUpdateResponse; import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadService; import org.springframework.stereotype.Component; @@ -55,7 +50,7 @@ import org.springframework.stereotype.Component; public class PaymentTypeApiResource { private final PaymentTypeReadService readPlatformService; - private final CommandPipeline commandPipeline; + private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; private final DefaultToApiJsonSerializer jsonSerializer; @GET @@ -79,14 +74,13 @@ public class PaymentTypeApiResource { @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) @Operation(summary = "Create a Payment Type", description = "Creates a new Payment type") - public PaymentTypeCreateResponse createPaymentType(PaymentTypeCreateRequest request) { - final var command = new PaymentTypeCreateCommand(); + public String createPaymentType(final PaymentTypeCreateRequest request) { + final CommandWrapper commandRequest = new CommandWrapperBuilder().createPaymentType() + .withJson(jsonSerializer.serialize(request)).build(); - command.setPayload(request); + final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - final Supplier response = commandPipeline.send(command); - - return response.get(); + return jsonSerializer.serialize(result); } @PUT @@ -94,18 +88,16 @@ public class PaymentTypeApiResource { @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) @Operation(summary = "Update a Payment Type", description = "Updates a Payment Type") - public PaymentTypeUpdateResponse updatePaymentType(@PathParam("paymentTypeId") final Long paymentTypeId, - final PaymentTypeUpdateRequest request) { + public String updatePaymentType(@PathParam("paymentTypeId") final Long paymentTypeId, final PaymentTypeUpdateRequest request) { request.setId(paymentTypeId); - final var command = new PaymentTypeUpdateCommand(); + final CommandWrapper commandRequest = new CommandWrapperBuilder().updatePaymentType(paymentTypeId) + .withJson(jsonSerializer.serialize(request)).build(); - command.setPayload(request); + final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - final Supplier response = commandPipeline.send(command); - - return response.get(); + return jsonSerializer.serialize(result); } @DELETE @@ -113,14 +105,12 @@ public class PaymentTypeApiResource { @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) @Operation(summary = "Delete a Payment Type", description = "Deletes payment type") - public PaymentTypeDeleteResponse deleteCode(@PathParam("paymentTypeId") final Long paymentTypeId) { + public String deleteCode(@PathParam("paymentTypeId") final Long paymentTypeId) { - final var command = new PaymentTypeDeleteCommand(); + final CommandWrapper commandRequest = new CommandWrapperBuilder().deletePaymentType(paymentTypeId).build(); - command.setPayload(PaymentTypeDeleteRequest.builder().id(paymentTypeId).build()); + final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - final Supplier response = commandPipeline.send(command); - - return response.get(); + return jsonSerializer.serialize(result); } } diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/paymenttype/handler/PaymentTypeCreateCommandHandler.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/paymenttype/handler/PaymentTypeCreateCommandHandler.java index e7604f6e8..292c6bed3 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/paymenttype/handler/PaymentTypeCreateCommandHandler.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/paymenttype/handler/PaymentTypeCreateCommandHandler.java @@ -18,33 +18,50 @@ */ package org.apache.fineract.portfolio.paymenttype.handler; -import io.github.resilience4j.retry.annotation.Retry; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.fineract.command.core.Command; -import org.apache.fineract.command.core.CommandHandler; +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeCreateRequest; import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeCreateResponse; import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeWriteService; -import org.springframework.stereotype.Component; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -@Slf4j -@Component -@RequiredArgsConstructor -public class PaymentTypeCreateCommandHandler implements CommandHandler { +@Service +@CommandType(entity = "PAYMENTTYPE", action = "CREATE") +public class PaymentTypeCreateCommandHandler implements NewCommandSourceHandler { private final PaymentTypeWriteService paymentTypeWriteService; - @Retry(name = "commandPaymentTypeCreate", fallbackMethod = "fallback") - @Override - @Transactional - public PaymentTypeCreateResponse handle(Command command) { - return paymentTypeWriteService.createPaymentType(command.getPayload()); + @Autowired + public PaymentTypeCreateCommandHandler(final PaymentTypeWriteService paymentTypeWriteService) { + this.paymentTypeWriteService = paymentTypeWriteService; } + @Transactional @Override - public PaymentTypeCreateResponse fallback(Command command, Throwable t) { - return CommandHandler.super.fallback(command, t); + public CommandProcessingResult processCommand(final JsonCommand command) { + + final Boolean isSystemDefined = command.booleanObjectValueOfParameterNamed("isSystemDefined"); + + final PaymentTypeCreateRequest request = PaymentTypeCreateRequest.builder() + .name(command.stringValueOfParameterNamed("name")) + .description(command.stringValueOfParameterNamed("description")) + .isCashPayment(command.booleanObjectValueOfParameterNamed("isCashPayment")) + .position(command.longValueOfParameterNamed("position")) + .codeName(command.stringValueOfParameterNamed("codeName")) + .isSystemDefined(isSystemDefined != null ? isSystemDefined : Boolean.FALSE) + .build(); + + final PaymentTypeCreateResponse response = this.paymentTypeWriteService.createPaymentType(request); + + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(response.getResourceId()) // + .build(); } + } diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/paymenttype/handler/PaymentTypeDeleteCommandHandler.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/paymenttype/handler/PaymentTypeDeleteCommandHandler.java index 9c1e9d050..8e8cdc4c6 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/paymenttype/handler/PaymentTypeDeleteCommandHandler.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/paymenttype/handler/PaymentTypeDeleteCommandHandler.java @@ -18,33 +18,41 @@ */ package org.apache.fineract.portfolio.paymenttype.handler; -import io.github.resilience4j.retry.annotation.Retry; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.fineract.command.core.Command; -import org.apache.fineract.command.core.CommandHandler; +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeDeleteRequest; import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeDeleteResponse; import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeWriteService; -import org.springframework.stereotype.Component; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -@Slf4j -@Component -@RequiredArgsConstructor -public class PaymentTypeDeleteCommandHandler implements CommandHandler { +@Service +@CommandType(entity = "PAYMENTTYPE", action = "DELETE") +public class PaymentTypeDeleteCommandHandler implements NewCommandSourceHandler { private final PaymentTypeWriteService paymentTypeWriteService; - @Retry(name = "commandPaymentTypeDelete", fallbackMethod = "fallback") - @Override - @Transactional - public PaymentTypeDeleteResponse handle(Command command) { - return paymentTypeWriteService.deletePaymentType(command.getPayload()); + @Autowired + public PaymentTypeDeleteCommandHandler(final PaymentTypeWriteService paymentTypeWriteService) { + this.paymentTypeWriteService = paymentTypeWriteService; } + @Transactional @Override - public PaymentTypeDeleteResponse fallback(Command command, Throwable t) { - return CommandHandler.super.fallback(command, t); + public CommandProcessingResult processCommand(final JsonCommand command) { + + final PaymentTypeDeleteRequest request = PaymentTypeDeleteRequest.builder().id(command.entityId()).build(); + + final PaymentTypeDeleteResponse response = this.paymentTypeWriteService.deletePaymentType(request); + + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(response.getResourceId()) // + .build(); } + } diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/paymenttype/handler/PaymentTypeUpdateCommandHandler.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/paymenttype/handler/PaymentTypeUpdateCommandHandler.java index 41914c9e5..cc3f50854 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/paymenttype/handler/PaymentTypeUpdateCommandHandler.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/paymenttype/handler/PaymentTypeUpdateCommandHandler.java @@ -18,33 +18,51 @@ */ package org.apache.fineract.portfolio.paymenttype.handler; -import io.github.resilience4j.retry.annotation.Retry; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.fineract.command.core.Command; -import org.apache.fineract.command.core.CommandHandler; +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeUpdateRequest; import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeUpdateResponse; import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeWriteService; -import org.springframework.stereotype.Component; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -@Slf4j -@Component -@RequiredArgsConstructor -public class PaymentTypeUpdateCommandHandler implements CommandHandler { +@Service +@CommandType(entity = "PAYMENTTYPE", action = "UPDATE") +public class PaymentTypeUpdateCommandHandler implements NewCommandSourceHandler { private final PaymentTypeWriteService paymentTypeWriteService; - @Retry(name = "commandPaymentTypeUpdate", fallbackMethod = "fallback") - @Override - @Transactional - public PaymentTypeUpdateResponse handle(Command command) { - return paymentTypeWriteService.updatePaymentType(command.getPayload()); + @Autowired + public PaymentTypeUpdateCommandHandler(final PaymentTypeWriteService paymentTypeWriteService) { + this.paymentTypeWriteService = paymentTypeWriteService; } + @Transactional @Override - public PaymentTypeUpdateResponse fallback(Command command, Throwable t) { - return CommandHandler.super.fallback(command, t); + public CommandProcessingResult processCommand(final JsonCommand command) { + + final Boolean isSystemDefined = command.booleanObjectValueOfParameterNamed("isSystemDefined"); + + final PaymentTypeUpdateRequest request = PaymentTypeUpdateRequest.builder() + .id(command.entityId()) + .name(command.stringValueOfParameterNamed("name")) + .description(command.stringValueOfParameterNamed("description")) + .isCashPayment(command.booleanObjectValueOfParameterNamed("isCashPayment")) + .position(command.longValueOfParameterNamed("position")) + .codeName(command.stringValueOfParameterNamed("codeName")) + .isSystemDefined(isSystemDefined != null ? isSystemDefined : Boolean.FALSE) + .build(); + + final PaymentTypeUpdateResponse response = this.paymentTypeWriteService.updatePaymentType(request); + + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(response.getResourceId()) // + .build(); } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/api/LoanCOBCatchUpApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/cob/api/LoanCOBCatchUpApiResource.java index f29866125..448ed615d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/api/LoanCOBCatchUpApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/api/LoanCOBCatchUpApiResource.java @@ -35,6 +35,7 @@ import org.apache.fineract.cob.data.OldestCOBProcessedLoanDTO; import org.apache.fineract.cob.service.LoanCOBCatchUpService; import org.apache.fineract.infrastructure.core.exception.JobIsNotFoundOrNotEnabledException; import org.apache.fineract.infrastructure.jobs.service.JobName; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.springframework.stereotype.Component; @Path("/v1/loans") @@ -44,6 +45,7 @@ import org.springframework.stereotype.Component; public class LoanCOBCatchUpApiResource { private final Optional loanCOBCatchUpServiceOp; + private final PlatformSecurityContext context; @GET @Path("oldest-cob-closed") @@ -64,6 +66,7 @@ public class LoanCOBCatchUpApiResource { @ApiResponse(responseCode = "202", description = "Catch Up has been started") @ApiResponse(responseCode = "400", description = "Catch Up is already running") public Response executeLoanCOBCatchUp() { + this.context.authenticatedUser().validateHasPermissionTo("EXECUTE_LOANCOBCATCHUP"); return loanCOBCatchUpServiceOp.map(loanCOBCatchUpService -> { if (loanCOBCatchUpService.isCatchUpRunning().isCatchUpRunning()) { return Response.status(Response.Status.BAD_REQUEST).build(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java index b918422a1..4e739e895 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java @@ -222,7 +222,10 @@ public class SecurityConfig { // currency .requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/currencies")) .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_CURRENCY") - .requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/currencies")) + // the actual currency-update endpoint is PUT (CurrenciesApiResource has no @POST method) - + // this matcher previously targeted POST, which meant PUT fell through to the generic + // fullyAuthenticated() catch-all with no permission check at all. + .requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/currencies")) .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_CURRENCY") // notes: read .requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/clients/*/notes")) @@ -355,11 +358,18 @@ public class SecurityConfig { .requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/import/*")) .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_IMPORT") // payment type + // NOTE: POST previously required "UPDATE_PAYMENTTYPE" (wrong authority for create), with a + // second, unreachable matcher for the same POST pattern requiring "DELETE_PAYMENTTYPE" - and + // no PUT/DELETE matcher existed at all, so those requests fell through to the generic + // fullyAuthenticated() catch-all below with no permission check. Corrected to one matcher + // per method, each requiring its own correct permission code. .requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/paymenttypes/*")) .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_PAYMENTTYPE") .requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/paymenttypes")) + .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "CREATE_PAYMENTTYPE") + .requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/paymenttypes/*")) .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_PAYMENTTYPE") - .requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/paymenttypes")) + .requestMatchers(API_MATCHER.matcher(HttpMethod.DELETE, "/api/*/paymenttypes/*")) .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_PAYMENTTYPE") // mix: taxonomy .requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/mixtaxonomy/*")) @@ -390,12 +400,41 @@ public class SecurityConfig { .requestMatchers(API_MATCHER.matcher(HttpMethod.DELETE, "/api/*/mixreport/*")) .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_MIX_REPORT") // working days + // NOTE: previously "READ_WORKING_DAYS"/"UPDATE_WORKING_DAYS" (with an underscore between + // WORKING and DAYS), which did not match the actual seeded permission codes + // (READ_WORKINGDAYS / UPDATE_WORKINGDAYS) - corrected to the real codes. .requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/workingdays")) - .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_WORKING_DAYS") + .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_WORKINGDAYS") .requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/workingdays/template")) - .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_WORKING_DAYS") + .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_WORKINGDAYS") .requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/workingdays")) - .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_WORKING_DAYS") + .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_WORKINGDAYS") + // sponsor banks + // NOTE: no matcher previously existed at all for this path - POST/PUT both fell + // through to the generic fullyAuthenticated() catch-all below with no permission + // check whatsoever. + .requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/sponsorbanks")) + .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "CREATE_SPONSORBANK") + .requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/sponsorbanks/*")) + .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_SPONSORBANK") + // sponsor DD GST/GL settings + // NOTE: no matcher previously existed at all for this path either. gl-mapping is a + // genuine upsert (one call both creates and updates the single settings row), so it is + // covered by one UPDATE_SPONSORDDGLMAPPING matcher rather than a separate CREATE one. + .requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/sponsordd/gl-mapping")) + .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_SPONSORDDGLMAPPING") + // bank profile (institution profile) + // NOTE: no matcher previously existed at all for this path - the PUT relied solely on + // the in-resource validateHasPermissionTo() check, which this change replaces with the + // command-source pipeline. Added here for the same defense-in-depth already applied to + // Currency/Payment Type/Working Days. + .requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/institutionprofile")) + .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_INSTITUTIONPROFILE") + // branch profile + // NOTE: no matcher previously existed at all for this path either, same rationale as + // institution profile above. + .requestMatchers(API_MATCHER.matcher(HttpMethod.PUT, "/api/*/branchprofile/*")) + .hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_BRANCHPROFILE") .requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/twofactor/validate")).fullyAuthenticated() .requestMatchers(API_MATCHER.matcher("/api/*/twofactor")).fullyAuthenticated() diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/workingdays/api/WorkingDaysApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/workingdays/api/WorkingDaysApiResource.java index d58dd0bfa..7134ee9ba 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/organisation/workingdays/api/WorkingDaysApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/workingdays/api/WorkingDaysApiResource.java @@ -27,15 +27,15 @@ import jakarta.ws.rs.PUT; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; -import java.time.Instant; -import java.util.function.Supplier; import lombok.RequiredArgsConstructor; -import org.apache.fineract.command.core.CommandPipeline; -import org.apache.fineract.organisation.workingdays.command.WorkingDaysUpdateCommand; +import org.apache.fineract.commands.domain.CommandWrapper; +import org.apache.fineract.commands.service.CommandWrapperBuilder; +import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; import org.apache.fineract.organisation.workingdays.data.WorkingDaysData; import org.apache.fineract.organisation.workingdays.data.WorkingDaysUpdateRequest; import org.apache.fineract.organisation.workingdays.data.WorkingDaysUpdateRequestValidator; -import org.apache.fineract.organisation.workingdays.data.WorkingDaysUpdateResponse; import org.apache.fineract.organisation.workingdays.service.WorkingDaysReadPlatformService; import org.springframework.stereotype.Component; @@ -50,7 +50,8 @@ public class WorkingDaysApiResource { private final WorkingDaysReadPlatformService workingDaysReadPlatformService; private final WorkingDaysUpdateRequestValidator workingDaysUpdateRequestValidator; - private final CommandPipeline commandPipeline; + private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; + private final DefaultToApiJsonSerializer toApiJsonSerializer; @GET @Consumes({ MediaType.APPLICATION_JSON }) @@ -65,17 +66,13 @@ public class WorkingDaysApiResource { @Produces({ MediaType.APPLICATION_JSON }) @Operation(summary = "Update a Working Day", description = "Mandatory Fields\n" + "recurrence,repaymentRescheduleType,extendTermForDailyRepayments,locale") - public WorkingDaysUpdateResponse update(@Valid WorkingDaysUpdateRequest request) { + public String update(@Valid final WorkingDaysUpdateRequest request) { + final CommandWrapper commandRequest = new CommandWrapperBuilder().updateWorkingDays() + .withJson(toApiJsonSerializer.serialize(request)).build(); - final var command = new WorkingDaysUpdateCommand(); + final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest); - command.setCommandId(System.currentTimeMillis()); - command.setCreatedAt(Instant.now()); - command.setPayload(request); - - final Supplier response = commandPipeline.send(command); - - return response.get(); + return toApiJsonSerializer.serialize(result); } @GET diff --git a/fineract-provider/src/main/java/org/apache/fineract/organisation/workingdays/handler/UpdateWorkingDaysCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/organisation/workingdays/handler/UpdateWorkingDaysCommandHandler.java index 2528d818f..821516099 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/organisation/workingdays/handler/UpdateWorkingDaysCommandHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/organisation/workingdays/handler/UpdateWorkingDaysCommandHandler.java @@ -18,38 +18,46 @@ */ package org.apache.fineract.organisation.workingdays.handler; -import java.util.Collections; import java.util.Map; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.fineract.command.core.Command; -import org.apache.fineract.command.core.CommandHandler; +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; import org.apache.fineract.organisation.workingdays.data.WorkingDaysUpdateRequest; -import org.apache.fineract.organisation.workingdays.data.WorkingDaysUpdateResponse; import org.apache.fineract.organisation.workingdays.service.WorkingDaysWritePlatformService; -import org.springframework.stereotype.Component; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -@Slf4j -@Component -@RequiredArgsConstructor -public class UpdateWorkingDaysCommandHandler implements CommandHandler { +@Service +@CommandType(entity = "WORKINGDAYS", action = "UPDATE") +public class UpdateWorkingDaysCommandHandler implements NewCommandSourceHandler { private final WorkingDaysWritePlatformService workingDaysWritePlatformService; + @Autowired + public UpdateWorkingDaysCommandHandler(final WorkingDaysWritePlatformService workingDaysWritePlatformService) { + this.workingDaysWritePlatformService = workingDaysWritePlatformService; + } + @Transactional @Override - public WorkingDaysUpdateResponse handle(Command command) { - WorkingDaysUpdateRequest request = command.getPayload(); - Map changes = this.workingDaysWritePlatformService.updateWorkingDays(request); - if (changes == null) { - changes = Collections.emptyMap(); - } + public CommandProcessingResult processCommand(final JsonCommand command) { - return WorkingDaysUpdateResponse.builder().resourceId((Long) changes.get("resourceId")).changes(changes) - .recurrence(request.getRecurrence()).repaymentRescheduleType(request.getRepaymentRescheduleType()) - .extendTermForDailyRepayments(request.getExtendTermForDailyRepayments()) - .extendTermForRepaymentsOnHolidays(request.getExtendTermForRepaymentsOnHolidays()).build(); + final WorkingDaysUpdateRequest request = WorkingDaysUpdateRequest.builder() + .recurrence(command.stringValueOfParameterNamed("recurrence")) + .repaymentRescheduleType(command.integerValueSansLocaleOfParameterNamed("repaymentRescheduleType")) + .extendTermForDailyRepayments(command.booleanObjectValueOfParameterNamed("extendTermForDailyRepayments")) + .extendTermForRepaymentsOnHolidays(command.booleanObjectValueOfParameterNamed("extendTermForRepaymentsOnHolidays")) + .build(); + + final Map changes = this.workingDaysWritePlatformService.updateWorkingDays(request); + + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .with(changes) // + .build(); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/fund/api/FundsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/fund/api/FundsApiResource.java index 94d7dd76e..3b6706fe2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/fund/api/FundsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/fund/api/FundsApiResource.java @@ -26,6 +26,7 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.PUT; @@ -114,4 +115,16 @@ public class FundsApiResource { final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest); return toApiJsonSerializer.serialize(result); } + + @DELETE + @Path("{fundId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Delete a Fund", description = "Deletes a fund outright. Fails if the fund is still referenced by any loan or loan product.") + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = FundsApiResourceSwagger.DeleteFundsFundIdResponse.class))) + public String deleteFund(@PathParam("fundId") @Parameter(description = "fundId") final Long fundId) { + final CommandWrapper commandRequest = new CommandWrapperBuilder().deleteFund(fundId).build(); + final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest); + return toApiJsonSerializer.serialize(result); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/fund/api/FundsApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/fund/api/FundsApiResourceSwagger.java index 746a65c35..0756f2c92 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/fund/api/FundsApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/fund/api/FundsApiResourceSwagger.java @@ -55,4 +55,13 @@ final class FundsApiResourceSwagger { public Long resourceId; public PutFundsFundIdRequest changes; } + + @Schema(description = "DeleteFundsFundIdResponse") + public static final class DeleteFundsFundIdResponse { + + private DeleteFundsFundIdResponse() {} + + @Schema(example = "1") + public Long resourceId; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/fund/handler/DeleteFundCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/fund/handler/DeleteFundCommandHandler.java new file mode 100644 index 000000000..34765a911 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/fund/handler/DeleteFundCommandHandler.java @@ -0,0 +1,47 @@ +/** + * 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.fund.handler; + +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.fund.service.FundWritePlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@CommandType(entity = "FUND", action = "DELETE") +public class DeleteFundCommandHandler implements NewCommandSourceHandler { + + private final FundWritePlatformService writePlatformService; + + @Autowired + public DeleteFundCommandHandler(final FundWritePlatformService writePlatformService) { + this.writePlatformService = writePlatformService; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + + return this.writePlatformService.deleteFund(command.entityId(), command); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/fund/service/FundWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/fund/service/FundWritePlatformService.java index 3eeddd82b..3d2d8a5be 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/fund/service/FundWritePlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/fund/service/FundWritePlatformService.java @@ -26,4 +26,6 @@ public interface FundWritePlatformService { CommandProcessingResult createFund(JsonCommand command); CommandProcessingResult updateFund(Long fundId, JsonCommand command); + + CommandProcessingResult deleteFund(Long fundId, JsonCommand command); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/fund/service/FundWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/fund/service/FundWritePlatformServiceJpaRepositoryImpl.java index 550e8aaa9..b6e7232db 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/fund/service/FundWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/fund/service/FundWritePlatformServiceJpaRepositoryImpl.java @@ -35,6 +35,7 @@ import org.apache.fineract.portfolio.fund.exception.FundNotFoundException; import org.apache.fineract.portfolio.fund.serialization.FundCommandFromApiJsonDeserializer; import org.springframework.cache.annotation.CacheEvict; import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.orm.jpa.JpaSystemException; import org.springframework.transaction.annotation.Transactional; @@ -45,6 +46,7 @@ public class FundWritePlatformServiceJpaRepositoryImpl implements FundWritePlatf private final PlatformSecurityContext context; private final FundCommandFromApiJsonDeserializer fromApiJsonDeserializer; private final FundRepository fundRepository; + private final JdbcTemplate jdbcTemplate; @Transactional @Override @@ -99,6 +101,45 @@ public class FundWritePlatformServiceJpaRepositoryImpl implements FundWritePlatf } } + @Transactional + @Override + @CacheEvict(value = "funds", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier().concat('fn')") + public CommandProcessingResult deleteFund(final Long fundId, final JsonCommand command) { + + this.context.authenticatedUser(); + + final Fund fund = this.fundRepository.findById(fundId).orElseThrow(() -> new FundNotFoundException(fundId)); + + requireNoneReference(fundId, "m_loan", "fund_id", "loan", "loans", + "Reassign or remove the loans using this fund before deleting it."); + requireNoneReference(fundId, "m_product_loan", "fund_id", "loan product", "loan products", + "Remove this fund from the loan products using it before deleting it."); + + try { + this.fundRepository.delete(fund); + this.fundRepository.flush(); + } catch (final JpaSystemException | DataIntegrityViolationException dve) { + throw ErrorHandler.getMappable(dve, "error.msg.fund.cannot.delete.has.references", + "This fund cannot be deleted: other records still reference it."); + } catch (final PersistenceException dve) { + throw ErrorHandler.getMappable(dve, "error.msg.fund.cannot.delete.has.references", + "This fund cannot be deleted: other records still reference it."); + } + + return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(fundId).build(); + } + + private void requireNoneReference(final Long fundId, final String table, final String column, final String singular, + final String plural, final String remedy) { + final Long count = this.jdbcTemplate.queryForObject("SELECT COUNT(*) FROM " + table + " WHERE " + column + " = ?", Long.class, + fundId); + if (count != null && count > 0) { + final String noun = count == 1 ? singular : plural; + throw new PlatformDataIntegrityException("error.msg.fund.cannot.delete.still.referenced", + "This fund cannot be deleted: it is still referenced by " + count + " " + noun + ". " + remedy, "fundId", fundId); + } + } + /* * Guaranteed to throw an exception no matter what the data integrity issue is. */ diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/fund/starter/FundConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/fund/starter/FundConfiguration.java index dfc9dae3a..450e0fcf9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/fund/starter/FundConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/fund/starter/FundConfiguration.java @@ -42,7 +42,7 @@ public class FundConfiguration { @Bean @ConditionalOnMissingBean(FundWritePlatformService.class) public FundWritePlatformService fundWritePlatformService(PlatformSecurityContext context, - FundCommandFromApiJsonDeserializer fromApiJsonDeserializer, FundRepository fundRepository) { - return new FundWritePlatformServiceJpaRepositoryImpl(context, fromApiJsonDeserializer, fundRepository); + FundCommandFromApiJsonDeserializer fromApiJsonDeserializer, FundRepository fundRepository, JdbcTemplate jdbcTemplate) { + return new FundWritePlatformServiceJpaRepositoryImpl(context, fromApiJsonDeserializer, fundRepository, jdbcTemplate); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/orgsetup/api/BranchProfileApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/orgsetup/api/BranchProfileApiResource.java index 55f7dd7e1..7c4ee8fcb 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/orgsetup/api/BranchProfileApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/orgsetup/api/BranchProfileApiResource.java @@ -19,6 +19,7 @@ package org.apache.fineract.portfolio.orgsetup.api; import com.google.gson.Gson; +import com.google.gson.JsonObject; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; import jakarta.ws.rs.PUT; @@ -27,17 +28,18 @@ import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; -import java.time.Instant; -import java.sql.Timestamp; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; 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.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; -import org.apache.fineract.useradministration.domain.AppUser; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; @@ -57,6 +59,7 @@ public class BranchProfileApiResource { private final JdbcTemplate jdbcTemplate; private final PlatformSecurityContext context; private final FromJsonHelper fromJsonHelper; + private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; private final Gson gson = new Gson(); @GET @@ -84,68 +87,23 @@ public class BranchProfileApiResource { @PUT @Path("{officeId}") @Consumes({ MediaType.APPLICATION_JSON }) - public String update(@PathParam("officeId") final Long officeId, final String body) { - this.context.authenticatedUser().validateHasPermissionTo("UPDATE_BRANCHPROFILE"); - - final Integer officeExists = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM m_office WHERE id = ?", - Integer.class, officeId); - if (officeExists == null || officeExists == 0) { - throw new PlatformDataIntegrityException("error.msg.branchprofile.office.not.found", - "Office " + officeId + " was not found."); - } - + public CommandProcessingResult update(@PathParam("officeId") final Long officeId, final String body) { + // The write itself (office-exists check, branch-code uniqueness check, m_office_profile + // upsert) now lives in BranchProfileWritePlatformService, invoked via + // UpdateBranchProfileCommandHandler so it goes through the generic Maker-Checker gate. The + // permission check that used to happen here explicitly is now enforced by SecurityConfig's + // matcher plus the command-source pipeline itself, the same as every other endpoint already + // migrated to this pipeline. A "locale" key is added if the body does not already carry one - + // JsonCommand's BigDecimal extractor (for latitude/longitude) requires it, and the frontend's + // payload does not currently send one. final var root = this.fromJsonHelper.parse(body); - final String branchCode = this.fromJsonHelper.extractStringNamed("branchCode", root); - final String branchType = this.fromJsonHelper.extractStringNamed("branchType", root); - final String address = this.fromJsonHelper.extractStringNamed("address", root); - final String pincode = this.fromJsonHelper.extractStringNamed("pincode", root); - final String contactPhone = this.fromJsonHelper.extractStringNamed("contactPhone", root); - final String contactEmail = this.fromJsonHelper.extractStringNamed("contactEmail", root); - final String workingHoursStart = this.fromJsonHelper.extractStringNamed("workingHoursStart", root); - final String workingHoursEnd = this.fromJsonHelper.extractStringNamed("workingHoursEnd", root); - final String workingDays = this.fromJsonHelper.extractStringNamed("workingDays", root); - final String ifscCode = this.fromJsonHelper.extractStringNamed("ifscCode", root); - final String micrCode = this.fromJsonHelper.extractStringNamed("micrCode", root); - final java.math.BigDecimal latitude = this.fromJsonHelper.extractBigDecimalWithLocaleNamed("latitude", root); - final java.math.BigDecimal longitude = this.fromJsonHelper.extractBigDecimalWithLocaleNamed("longitude", root); - - if (branchCode != null && !branchCode.isBlank()) { - final Integer dupe = jdbcTemplate.queryForObject( - "SELECT COUNT(*) FROM m_office_profile WHERE branch_code = ? AND office_id <> ?", Integer.class, - branchCode, officeId); - if (dupe != null && dupe > 0) { - throw new PlatformDataIntegrityException("error.msg.branchprofile.code.duplicate", - "Branch code '" + branchCode + "' is already used by another branch."); - } + final JsonObject json = root.getAsJsonObject(); + if (!json.has("locale")) { + json.addProperty("locale", "en"); } - - final AppUser currentUser = this.context.authenticatedUser(); - final Integer existing = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM m_office_profile WHERE office_id = ?", - Integer.class, officeId); - if (existing != null && existing > 0) { - jdbcTemplate.update(""" - UPDATE m_office_profile - SET branch_code = ?, branch_type = ?, address = ?, pincode = ?, contact_phone = ?, contact_email = ?, - working_hours_start = ?, working_hours_end = ?, working_days = ?, - ifsc_code = ?, micr_code = ?, latitude = ?, longitude = ?, - updated_by_username = ?, updated_at = ? - WHERE office_id = ? - """, branchCode, branchType, address, pincode, contactPhone, contactEmail, workingHoursStart, - workingHoursEnd, workingDays, ifscCode, micrCode, latitude, longitude, currentUser.getUsername(), - Timestamp.from(Instant.now()), officeId); - } else { - jdbcTemplate.update(""" - INSERT INTO m_office_profile - (office_id, branch_code, branch_type, address, pincode, contact_phone, contact_email, - working_hours_start, working_hours_end, working_days, ifsc_code, micr_code, - latitude, longitude, updated_by_username, updated_at) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) - """, officeId, branchCode, branchType, address, pincode, contactPhone, contactEmail, workingHoursStart, - workingHoursEnd, workingDays, ifscCode, micrCode, latitude, longitude, currentUser.getUsername(), - Timestamp.from(Instant.now())); - } - - return list(officeId); + final CommandWrapper commandRequest = new CommandWrapperBuilder().updateBranchProfile(officeId) + .withJson(this.gson.toJson(json)).build(); + return this.commandsSourceWritePlatformService.logCommandSource(commandRequest); } private String baseSql() { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/orgsetup/api/InstitutionProfileApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/orgsetup/api/InstitutionProfileApiResource.java index 7bcf6fc7d..a13e9a84d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/orgsetup/api/InstitutionProfileApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/orgsetup/api/InstitutionProfileApiResource.java @@ -25,14 +25,16 @@ import jakarta.ws.rs.PUT; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; -import java.sql.Date; import java.sql.Timestamp; import java.time.Instant; -import java.time.LocalDate; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; 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.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.useradministration.domain.AppUser; @@ -60,6 +62,7 @@ public class InstitutionProfileApiResource { private final JdbcTemplate jdbcTemplate; private final PlatformSecurityContext context; private final FromJsonHelper fromJsonHelper; + private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; private final Gson gson = new Gson(); @GET @@ -89,100 +92,17 @@ public class InstitutionProfileApiResource { @PUT @Consumes({ MediaType.APPLICATION_JSON }) - public String update(final String body) { - this.context.authenticatedUser().validateHasPermissionTo("UPDATE_INSTITUTIONPROFILE"); - ensureRowExists(); - - final var root = this.fromJsonHelper.parse(body); - final String bankName = this.fromJsonHelper.extractStringNamed("bankName", root); - final String legalName = this.fromJsonHelper.extractStringNamed("legalName", root); - final String shortName = this.fromJsonHelper.extractStringNamed("shortName", root); - final String bankCode = this.fromJsonHelper.extractStringNamed("bankCode", root); - final String institutionCode = this.fromJsonHelper.extractStringNamed("institutionCode", root); - final String bankType = this.fromJsonHelper.extractStringNamed("bankType", root); - final String ownershipType = this.fromJsonHelper.extractStringNamed("ownershipType", root); - final String parentInstitutionName = this.fromJsonHelper.extractStringNamed("parentInstitutionName", root); - final String registrationNumber = this.fromJsonHelper.extractStringNamed("registrationNumber", root); - final String licenseNumber = this.fromJsonHelper.extractStringNamed("licenseNumber", root); - final LocalDate establishedDate = parseDate(this.fromJsonHelper.extractStringNamed("establishedDate", root)); - final String status = this.fromJsonHelper.extractStringNamed("status", root); - final String country = this.fromJsonHelper.extractStringNamed("country", root); - final String regulatoryAuthority = this.fromJsonHelper.extractStringNamed("regulatoryAuthority", root); - final LocalDate registrationDate = parseDate(this.fromJsonHelper.extractStringNamed("registrationDate", root)); - final String licenseStatus = this.fromJsonHelper.extractStringNamed("licenseStatus", root); - final LocalDate licenseExpiryDate = parseDate(this.fromJsonHelper.extractStringNamed("licenseExpiryDate", root)); - final String swiftBic = this.fromJsonHelper.extractStringNamed("swiftBic", root); - final String addressLine1 = this.fromJsonHelper.extractStringNamed("addressLine1", root); - final String addressLine2 = this.fromJsonHelper.extractStringNamed("addressLine2", root); - final String city = this.fromJsonHelper.extractStringNamed("city", root); - final String district = this.fromJsonHelper.extractStringNamed("district", root); - final String state = this.fromJsonHelper.extractStringNamed("state", root); - final String pincode = this.fromJsonHelper.extractStringNamed("pincode", root); - final Boolean headOfficeSame = this.fromJsonHelper.parameterExists("headOfficeSameAsRegistered", root) - ? this.fromJsonHelper.extractBooleanNamed("headOfficeSameAsRegistered", root) : Boolean.TRUE; - final String headOfficeAddress = this.fromJsonHelper.extractStringNamed("headOfficeAddress", root); - final Boolean communicationSame = this.fromJsonHelper.parameterExists("communicationSameAsRegistered", root) - ? this.fromJsonHelper.extractBooleanNamed("communicationSameAsRegistered", root) : Boolean.TRUE; - final String communicationAddress = this.fromJsonHelper.extractStringNamed("communicationAddress", root); - final String contactPhone = this.fromJsonHelper.extractStringNamed("contactPhone", root); - final String contactEmail = this.fromJsonHelper.extractStringNamed("contactEmail", root); - final String customerCareNumber = this.fromJsonHelper.extractStringNamed("customerCareNumber", root); - final String grievanceEmail = this.fromJsonHelper.extractStringNamed("grievanceEmail", root); - final String website = this.fromJsonHelper.extractStringNamed("website", root); - final String pan = this.fromJsonHelper.extractStringNamed("pan", root); - final String tan = this.fromJsonHelper.extractStringNamed("tan", root); - final String gstin = this.fromJsonHelper.extractStringNamed("gstin", root); - final String statementHeaderName = this.fromJsonHelper.extractStringNamed("statementHeaderName", root); - final String passbookHeaderName = this.fromJsonHelper.extractStringNamed("passbookHeaderName", root); - final String receiptHeaderName = this.fromJsonHelper.extractStringNamed("receiptHeaderName", root); - final String termsAndConditions = this.fromJsonHelper.extractStringNamed("termsAndConditions", root); - final String authorizedPersonName = this.fromJsonHelper.extractStringNamed("authorizedPersonName", root); - final String authorizedPersonDesignation = this.fromJsonHelper.extractStringNamed("authorizedPersonDesignation", - root); - final String authorizedPersonContact = this.fromJsonHelper.extractStringNamed("authorizedPersonContact", root); - final String authorizedPersonEmail = this.fromJsonHelper.extractStringNamed("authorizedPersonEmail", root); - - // A status flip is stamped automatically the first time it happens, rather than asking the - // user to also remember to set a date — the previous row's own status is what "the first - // time" is judged against. - final String previousStatus = jdbcTemplate.queryForObject("SELECT status FROM m_institution_profile WHERE id = 1", - String.class); - final boolean turningActive = "ACTIVE".equalsIgnoreCase(status) && !"ACTIVE".equalsIgnoreCase(previousStatus); - final boolean turningInactive = "INACTIVE".equalsIgnoreCase(status) && !"INACTIVE".equalsIgnoreCase(previousStatus); - final LocalDate today = LocalDate.now(); - - final AppUser currentUser = this.context.authenticatedUser(); - jdbcTemplate.update(""" - UPDATE m_institution_profile - SET bank_name = ?, legal_name = ?, short_name = ?, bank_code = ?, institution_code = ?, - bank_type = ?, ownership_type = ?, parent_institution_name = ?, - registration_number = ?, license_number = ?, established_date = ?, status = ?, country = ?, - regulatory_authority = ?, registration_date = ?, license_status = ?, license_expiry_date = ?, - swift_bic = ?, - address_line1 = ?, address_line2 = ?, city = ?, district = ?, state = ?, pincode = ?, - head_office_same_as_registered = ?, head_office_address = ?, - communication_same_as_registered = ?, communication_address = ?, - contact_phone = ?, contact_email = ?, customer_care_number = ?, grievance_email = ?, - website = ?, pan = ?, tan = ?, gstin = ?, - statement_header_name = ?, passbook_header_name = ?, receipt_header_name = ?, - terms_and_conditions = ?, - authorized_person_name = ?, authorized_person_designation = ?, authorized_person_contact = ?, - authorized_person_email = ?, - activation_date = CASE WHEN ? THEN ? ELSE activation_date END, - deactivation_date = CASE WHEN ? THEN ? ELSE deactivation_date END, - updated_by_username = ?, updated_at = ? - WHERE id = 1 - """, bankName, legalName, shortName, bankCode, institutionCode, bankType, ownershipType, - parentInstitutionName, registrationNumber, licenseNumber, toSqlDate(establishedDate), status, country, - regulatoryAuthority, toSqlDate(registrationDate), licenseStatus, toSqlDate(licenseExpiryDate), swiftBic, - addressLine1, addressLine2, city, district, state, pincode, headOfficeSame, headOfficeAddress, - communicationSame, communicationAddress, contactPhone, contactEmail, customerCareNumber, grievanceEmail, - website, pan, tan, gstin, statementHeaderName, passbookHeaderName, receiptHeaderName, termsAndConditions, - authorizedPersonName, authorizedPersonDesignation, authorizedPersonContact, authorizedPersonEmail, - turningActive, toSqlDate(today), turningInactive, toSqlDate(today), currentUser.getUsername(), - Timestamp.from(Instant.now())); - - return get(); + public CommandProcessingResult update(final String body) { + // The write itself (the JDBC UPDATE of m_institution_profile) now lives in + // InstitutionProfileWritePlatformService, invoked via UpdateInstitutionProfileCommandHandler so + // it goes through the generic Maker-Checker gate. The permission check that used to happen here + // explicitly is now enforced by SecurityConfig's matcher plus the command-source pipeline + // itself, the same as every other endpoint already migrated to this pipeline (Currency, Working + // Days, Payment Type). `body` is forwarded to the command as-is - it already carries the + // `locale` key the frontend sends, which JsonCommand's BigDecimal/date extractors would + // otherwise require. + final CommandWrapper commandRequest = new CommandWrapperBuilder().updateInstitutionProfile().withJson(body).build(); + return this.commandsSourceWritePlatformService.logCommandSource(commandRequest); } private void ensureRowExists() { @@ -204,14 +124,6 @@ public class InstitutionProfileApiResource { return codes.isEmpty() ? null : codes.get(0); } - private LocalDate parseDate(final String raw) { - return (raw != null && !raw.isBlank()) ? LocalDate.parse(raw) : null; - } - - private Date toSqlDate(final LocalDate d) { - return d == null ? null : Date.valueOf(d); - } - private Map toResult(final Map row) { final Map out = new LinkedHashMap<>(); out.put("bankName", row.get("bank_name")); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/orgsetup/handler/UpdateBranchProfileCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/orgsetup/handler/UpdateBranchProfileCommandHandler.java new file mode 100644 index 000000000..e4b3bb09e --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/orgsetup/handler/UpdateBranchProfileCommandHandler.java @@ -0,0 +1,62 @@ +/** + * 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.orgsetup.handler; + +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.portfolio.orgsetup.service.BranchProfileWritePlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@CommandType(entity = "BRANCHPROFILE", action = "UPDATE") +public class UpdateBranchProfileCommandHandler implements NewCommandSourceHandler { + + private final BranchProfileWritePlatformService branchProfileWritePlatformService; + + @Autowired + public UpdateBranchProfileCommandHandler(final BranchProfileWritePlatformService branchProfileWritePlatformService) { + this.branchProfileWritePlatformService = branchProfileWritePlatformService; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + + final Long officeId = command.entityId(); + + final CommandProcessingResult result = this.branchProfileWritePlatformService.updateBranchProfile(officeId, + command.stringValueOfParameterNamed("branchCode"), command.stringValueOfParameterNamed("branchType"), + command.stringValueOfParameterNamed("address"), command.stringValueOfParameterNamed("pincode"), + command.stringValueOfParameterNamed("contactPhone"), command.stringValueOfParameterNamed("contactEmail"), + command.stringValueOfParameterNamed("workingHoursStart"), + command.stringValueOfParameterNamed("workingHoursEnd"), command.stringValueOfParameterNamed("workingDays"), + command.stringValueOfParameterNamed("ifscCode"), command.stringValueOfParameterNamed("micrCode"), + command.bigDecimalValueOfParameterNamed("latitude"), command.bigDecimalValueOfParameterNamed("longitude")); + + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(result.getResourceId()) // + .build(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/orgsetup/handler/UpdateInstitutionProfileCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/orgsetup/handler/UpdateInstitutionProfileCommandHandler.java new file mode 100644 index 000000000..2d7270d4f --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/orgsetup/handler/UpdateInstitutionProfileCommandHandler.java @@ -0,0 +1,88 @@ +/** + * 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.orgsetup.handler; + +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.portfolio.orgsetup.service.InstitutionProfileWritePlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@CommandType(entity = "INSTITUTIONPROFILE", action = "UPDATE") +public class UpdateInstitutionProfileCommandHandler implements NewCommandSourceHandler { + + private final InstitutionProfileWritePlatformService institutionProfileWritePlatformService; + + @Autowired + public UpdateInstitutionProfileCommandHandler( + final InstitutionProfileWritePlatformService institutionProfileWritePlatformService) { + this.institutionProfileWritePlatformService = institutionProfileWritePlatformService; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + + final Boolean headOfficeSame = command.parameterExists("headOfficeSameAsRegistered") + ? command.booleanObjectValueOfParameterNamed("headOfficeSameAsRegistered") : Boolean.TRUE; + final Boolean communicationSame = command.parameterExists("communicationSameAsRegistered") + ? command.booleanObjectValueOfParameterNamed("communicationSameAsRegistered") : Boolean.TRUE; + + final CommandProcessingResult result = this.institutionProfileWritePlatformService.updateInstitutionProfile( + command.stringValueOfParameterNamed("bankName"), command.stringValueOfParameterNamed("legalName"), + command.stringValueOfParameterNamed("shortName"), command.stringValueOfParameterNamed("bankCode"), + command.stringValueOfParameterNamed("institutionCode"), command.stringValueOfParameterNamed("bankType"), + command.stringValueOfParameterNamed("ownershipType"), + command.stringValueOfParameterNamed("parentInstitutionName"), + command.stringValueOfParameterNamed("registrationNumber"), + command.stringValueOfParameterNamed("licenseNumber"), command.stringValueOfParameterNamed("establishedDate"), + command.stringValueOfParameterNamed("status"), command.stringValueOfParameterNamed("country"), + command.stringValueOfParameterNamed("regulatoryAuthority"), + command.stringValueOfParameterNamed("registrationDate"), + command.stringValueOfParameterNamed("licenseStatus"), + command.stringValueOfParameterNamed("licenseExpiryDate"), command.stringValueOfParameterNamed("swiftBic"), + command.stringValueOfParameterNamed("addressLine1"), command.stringValueOfParameterNamed("addressLine2"), + command.stringValueOfParameterNamed("city"), command.stringValueOfParameterNamed("district"), + command.stringValueOfParameterNamed("state"), command.stringValueOfParameterNamed("pincode"), headOfficeSame, + command.stringValueOfParameterNamed("headOfficeAddress"), communicationSame, + command.stringValueOfParameterNamed("communicationAddress"), + command.stringValueOfParameterNamed("contactPhone"), command.stringValueOfParameterNamed("contactEmail"), + command.stringValueOfParameterNamed("customerCareNumber"), + command.stringValueOfParameterNamed("grievanceEmail"), command.stringValueOfParameterNamed("website"), + command.stringValueOfParameterNamed("pan"), command.stringValueOfParameterNamed("tan"), + command.stringValueOfParameterNamed("gstin"), command.stringValueOfParameterNamed("statementHeaderName"), + command.stringValueOfParameterNamed("passbookHeaderName"), + command.stringValueOfParameterNamed("receiptHeaderName"), + command.stringValueOfParameterNamed("termsAndConditions"), + command.stringValueOfParameterNamed("authorizedPersonName"), + command.stringValueOfParameterNamed("authorizedPersonDesignation"), + command.stringValueOfParameterNamed("authorizedPersonContact"), + command.stringValueOfParameterNamed("authorizedPersonEmail")); + + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(result.getResourceId()) // + .build(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/orgsetup/service/BranchProfileWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/orgsetup/service/BranchProfileWritePlatformService.java new file mode 100644 index 000000000..f80ff95a3 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/orgsetup/service/BranchProfileWritePlatformService.java @@ -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.orgsetup.service; + +import java.math.BigDecimal; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; + +public interface BranchProfileWritePlatformService { + + CommandProcessingResult updateBranchProfile(Long officeId, String branchCode, String branchType, String address, + String pincode, String contactPhone, String contactEmail, String workingHoursStart, String workingHoursEnd, + String workingDays, String ifscCode, String micrCode, BigDecimal latitude, BigDecimal longitude); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/orgsetup/service/BranchProfileWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/orgsetup/service/BranchProfileWritePlatformServiceImpl.java new file mode 100644 index 000000000..386b9b61a --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/orgsetup/service/BranchProfileWritePlatformServiceImpl.java @@ -0,0 +1,103 @@ +/** + * 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.orgsetup.service; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.time.Instant; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.useradministration.domain.AppUser; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * Moved verbatim out of {@code BranchProfileApiResource.update()} so the write can be invoked from a + * {@code NewCommandSourceHandler} for Maker-Checker support. The resource's own {@code list()} (read) + * method is untouched. + */ +@Service +public class BranchProfileWritePlatformServiceImpl implements BranchProfileWritePlatformService { + + private final JdbcTemplate jdbcTemplate; + private final PlatformSecurityContext context; + + @Autowired + public BranchProfileWritePlatformServiceImpl(final JdbcTemplate jdbcTemplate, final PlatformSecurityContext context) { + this.jdbcTemplate = jdbcTemplate; + this.context = context; + } + + @Transactional + @Override + public CommandProcessingResult updateBranchProfile(final Long officeId, final String branchCode, final String branchType, + final String address, final String pincode, final String contactPhone, final String contactEmail, + final String workingHoursStart, final String workingHoursEnd, final String workingDays, final String ifscCode, + final String micrCode, final BigDecimal latitude, final BigDecimal longitude) { + + final Integer officeExists = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM m_office WHERE id = ?", Integer.class, + officeId); + if (officeExists == null || officeExists == 0) { + throw new PlatformDataIntegrityException("error.msg.branchprofile.office.not.found", + "Office " + officeId + " was not found."); + } + + if (branchCode != null && !branchCode.isBlank()) { + final Integer dupe = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM m_office_profile WHERE branch_code = ? AND office_id <> ?", Integer.class, + branchCode, officeId); + if (dupe != null && dupe > 0) { + throw new PlatformDataIntegrityException("error.msg.branchprofile.code.duplicate", + "Branch code '" + branchCode + "' is already used by another branch."); + } + } + + final AppUser currentUser = this.context.authenticatedUser(); + final Integer existing = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM m_office_profile WHERE office_id = ?", + Integer.class, officeId); + if (existing != null && existing > 0) { + jdbcTemplate.update(""" + UPDATE m_office_profile + SET branch_code = ?, branch_type = ?, address = ?, pincode = ?, contact_phone = ?, contact_email = ?, + working_hours_start = ?, working_hours_end = ?, working_days = ?, + ifsc_code = ?, micr_code = ?, latitude = ?, longitude = ?, + updated_by_username = ?, updated_at = ? + WHERE office_id = ? + """, branchCode, branchType, address, pincode, contactPhone, contactEmail, workingHoursStart, + workingHoursEnd, workingDays, ifscCode, micrCode, latitude, longitude, currentUser.getUsername(), + Timestamp.from(Instant.now()), officeId); + } else { + jdbcTemplate.update(""" + INSERT INTO m_office_profile + (office_id, branch_code, branch_type, address, pincode, contact_phone, contact_email, + working_hours_start, working_hours_end, working_days, ifsc_code, micr_code, + latitude, longitude, updated_by_username, updated_at) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + """, officeId, branchCode, branchType, address, pincode, contactPhone, contactEmail, workingHoursStart, + workingHoursEnd, workingDays, ifscCode, micrCode, latitude, longitude, currentUser.getUsername(), + Timestamp.from(Instant.now())); + } + + return new CommandProcessingResultBuilder().withEntityId(officeId).build(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/orgsetup/service/InstitutionProfileWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/orgsetup/service/InstitutionProfileWritePlatformService.java new file mode 100644 index 000000000..4a544c5c2 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/orgsetup/service/InstitutionProfileWritePlatformService.java @@ -0,0 +1,36 @@ +/** + * 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.orgsetup.service; + +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; + +public interface InstitutionProfileWritePlatformService { + + CommandProcessingResult updateInstitutionProfile(String bankName, String legalName, String shortName, String bankCode, + String institutionCode, String bankType, String ownershipType, String parentInstitutionName, + String registrationNumber, String licenseNumber, String establishedDate, String status, String country, + String regulatoryAuthority, String registrationDate, String licenseStatus, String licenseExpiryDate, + String swiftBic, String addressLine1, String addressLine2, String city, String district, String state, + String pincode, Boolean headOfficeSameAsRegistered, String headOfficeAddress, + Boolean communicationSameAsRegistered, String communicationAddress, String contactPhone, String contactEmail, + String customerCareNumber, String grievanceEmail, String website, String pan, String tan, String gstin, + String statementHeaderName, String passbookHeaderName, String receiptHeaderName, String termsAndConditions, + String authorizedPersonName, String authorizedPersonDesignation, String authorizedPersonContact, + String authorizedPersonEmail); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/orgsetup/service/InstitutionProfileWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/orgsetup/service/InstitutionProfileWritePlatformServiceImpl.java new file mode 100644 index 000000000..44b1209bf --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/orgsetup/service/InstitutionProfileWritePlatformServiceImpl.java @@ -0,0 +1,134 @@ +/** + * 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.orgsetup.service; + +import java.sql.Date; +import java.sql.Timestamp; +import java.time.Instant; +import java.time.LocalDate; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.useradministration.domain.AppUser; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * Moved verbatim out of {@code InstitutionProfileApiResource.update()} so the write can be invoked + * from a {@code NewCommandSourceHandler} for Maker-Checker support. {@code ensureRowExists()}, + * {@code parseDate()} and {@code toSqlDate()} are copies of the resource's own private helpers rather + * than shared ones - the resource's untouched {@code get()} method keeps its own copies so this change + * does not have to touch that unrelated read path at all. + */ +@Service +public class InstitutionProfileWritePlatformServiceImpl implements InstitutionProfileWritePlatformService { + + private final JdbcTemplate jdbcTemplate; + private final PlatformSecurityContext context; + + @Autowired + public InstitutionProfileWritePlatformServiceImpl(final JdbcTemplate jdbcTemplate, final PlatformSecurityContext context) { + this.jdbcTemplate = jdbcTemplate; + this.context = context; + } + + @Transactional + @Override + public CommandProcessingResult updateInstitutionProfile(final String bankName, final String legalName, + final String shortName, final String bankCode, final String institutionCode, final String bankType, + final String ownershipType, final String parentInstitutionName, final String registrationNumber, + final String licenseNumber, final String establishedDate, final String status, final String country, + final String regulatoryAuthority, final String registrationDate, final String licenseStatus, + final String licenseExpiryDate, final String swiftBic, final String addressLine1, final String addressLine2, + final String city, final String district, final String state, final String pincode, + final Boolean headOfficeSameAsRegistered, final String headOfficeAddress, + final Boolean communicationSameAsRegistered, final String communicationAddress, final String contactPhone, + final String contactEmail, final String customerCareNumber, final String grievanceEmail, final String website, + final String pan, final String tan, final String gstin, final String statementHeaderName, + final String passbookHeaderName, final String receiptHeaderName, final String termsAndConditions, + final String authorizedPersonName, final String authorizedPersonDesignation, final String authorizedPersonContact, + final String authorizedPersonEmail) { + + ensureRowExists(); + + // A status flip is stamped automatically the first time it happens, rather than asking the + // user to also remember to set a date — the previous row's own status is what "the first + // time" is judged against. + final String previousStatus = jdbcTemplate.queryForObject("SELECT status FROM m_institution_profile WHERE id = 1", + String.class); + final boolean turningActive = "ACTIVE".equalsIgnoreCase(status) && !"ACTIVE".equalsIgnoreCase(previousStatus); + final boolean turningInactive = "INACTIVE".equalsIgnoreCase(status) && !"INACTIVE".equalsIgnoreCase(previousStatus); + final LocalDate today = LocalDate.now(); + + final AppUser currentUser = this.context.authenticatedUser(); + jdbcTemplate.update(""" + UPDATE m_institution_profile + SET bank_name = ?, legal_name = ?, short_name = ?, bank_code = ?, institution_code = ?, + bank_type = ?, ownership_type = ?, parent_institution_name = ?, + registration_number = ?, license_number = ?, established_date = ?, status = ?, country = ?, + regulatory_authority = ?, registration_date = ?, license_status = ?, license_expiry_date = ?, + swift_bic = ?, + address_line1 = ?, address_line2 = ?, city = ?, district = ?, state = ?, pincode = ?, + head_office_same_as_registered = ?, head_office_address = ?, + communication_same_as_registered = ?, communication_address = ?, + contact_phone = ?, contact_email = ?, customer_care_number = ?, grievance_email = ?, + website = ?, pan = ?, tan = ?, gstin = ?, + statement_header_name = ?, passbook_header_name = ?, receipt_header_name = ?, + terms_and_conditions = ?, + authorized_person_name = ?, authorized_person_designation = ?, authorized_person_contact = ?, + authorized_person_email = ?, + activation_date = CASE WHEN ? THEN ? ELSE activation_date END, + deactivation_date = CASE WHEN ? THEN ? ELSE deactivation_date END, + updated_by_username = ?, updated_at = ? + WHERE id = 1 + """, bankName, legalName, shortName, bankCode, institutionCode, bankType, ownershipType, + parentInstitutionName, registrationNumber, licenseNumber, toSqlDate(parseDate(establishedDate)), status, country, + regulatoryAuthority, toSqlDate(parseDate(registrationDate)), licenseStatus, toSqlDate(parseDate(licenseExpiryDate)), + swiftBic, addressLine1, addressLine2, city, district, state, pincode, headOfficeSameAsRegistered, + headOfficeAddress, communicationSameAsRegistered, communicationAddress, contactPhone, contactEmail, + customerCareNumber, grievanceEmail, website, pan, tan, gstin, statementHeaderName, passbookHeaderName, + receiptHeaderName, termsAndConditions, authorizedPersonName, authorizedPersonDesignation, + authorizedPersonContact, authorizedPersonEmail, turningActive, toSqlDate(today), turningInactive, + toSqlDate(today), currentUser.getUsername(), Timestamp.from(Instant.now())); + + return new CommandProcessingResultBuilder().withEntityId(1L).build(); + } + + private void ensureRowExists() { + final Integer count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM m_institution_profile WHERE id = 1", + Integer.class); + if (count == null || count == 0) { + final AppUser currentUser = this.context.authenticatedUser(); + jdbcTemplate.update(""" + INSERT INTO m_institution_profile (id, status, created_by_username, created_at) + VALUES (1, 'ACTIVE', ?, ?) + """, currentUser.getUsername(), Timestamp.from(Instant.now())); + } + } + + private LocalDate parseDate(final String raw) { + return (raw != null && !raw.isBlank()) ? LocalDate.parse(raw) : null; + } + + private Date toSqlDate(final LocalDate d) { + return d == null ? null : Date.valueOf(d); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/sponsordd/api/SponsorBankApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/sponsordd/api/SponsorBankApiResource.java index a78c88407..b5325ad94 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/sponsordd/api/SponsorBankApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/sponsordd/api/SponsorBankApiResource.java @@ -34,23 +34,19 @@ import jakarta.ws.rs.Produces; import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; -import java.time.LocalDateTime; import java.util.Collection; -import java.util.List; import java.util.Map; import java.util.function.Supplier; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; 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.data.CommandProcessingResult; -import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; -import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; -import org.apache.fineract.portfolio.sponsordd.domain.SponsorBank; import org.apache.fineract.portfolio.sponsordd.domain.SponsorBankRepository; -import org.apache.fineract.useradministration.domain.AppUser; import org.springframework.stereotype.Component; -import org.apache.fineract.infrastructure.core.service.DateUtils; @Path("/v1/sponsorbanks") @Component @@ -59,8 +55,8 @@ import org.apache.fineract.infrastructure.core.service.DateUtils; public class SponsorBankApiResource { private final SponsorBankRepository sponsorBankRepository; - private final PlatformSecurityContext context; private final org.springframework.jdbc.core.JdbcTemplate jdbcTemplate; + private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; private final Gson gson = new GsonBuilder().serializeNulls().create(); /** Mirrors SponsorDDApiResource.safely() - see that class for the full rationale. */ @@ -125,78 +121,19 @@ public class SponsorBankApiResource { return names; } - /** - * Rejects a payable account that cannot serve as one. - * - * A DD payable is money the bank owes the sponsor until the draft is presented, so it has to be - * a liability account, and it has to accept postings. Catching that here means an administrator - * is told while choosing the account, rather than a clerk discovering it when a draft fails. - */ - private void validateDdPayableGlAccount(final Long glAccountId) { - if (glAccountId == null) { - return; - } - final List> rows = this.jdbcTemplate.queryForList( - "SELECT gl_code, name, classification_enum, account_usage, disabled FROM acc_gl_account WHERE id = ?", - glAccountId); - if (rows.isEmpty()) { - throw new IllegalArgumentException("The selected DD Payable GL account does not exist."); - } - final Map gl = rows.get(0); - final String label = gl.get("gl_code") + " · " + gl.get("name"); - // 2 is LIABILITY in Fineract's GLAccountType enum. - if (((Number) gl.get("classification_enum")).intValue() != 2) { - throw new IllegalArgumentException( - "DD Payable must be a LIABILITY account — a draft is money owed to the sponsor bank until it is presented. " - + label + " is not a liability account."); - } - // 2 is HEADER in Fineract's GLAccountUsage enum; nothing can be posted to a header. - if (((Number) gl.get("account_usage")).intValue() == 2) { - throw new IllegalArgumentException("DD Payable must be a detail account, not a header. Pick a postable account under " - + label + "."); - } - // Read through both shapes: the column comes back as a Boolean on some drivers and as a - // 0/1 Number on others, and a plain Boolean cast would throw on the latter. - final Object disabled = gl.get("disabled"); - final boolean isDisabled = disabled instanceof Boolean b ? b - : disabled instanceof Number n && n.intValue() != 0; - if (isDisabled) { - throw new IllegalArgumentException("The selected DD Payable GL account (" + label + ") is disabled."); - } - } - @POST @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) @Operation(summary = "Create a Sponsor Bank", description = "Creates a new Sponsor Bank.") public CommandProcessingResult createSponsorBank(final SponsorBankRequest request) { + // The actual validation/persistence now lives in SponsorBankWritePlatformService, invoked via + // a NewCommandSourceHandler so the write goes through the generic Maker-Checker gate - see + // CreateSponsorBankCommandHandler. safely() is kept around the call so a validation failure + // still comes back as the same 400 it always has, rather than an unmapped 500. return safely(() -> { - if (request.getIfscCode() != null && !request.getIfscCode().isBlank() - && !request.getIfscCode().trim().toUpperCase().matches("^[A-Z]{4}0[A-Z0-9]{6}$")) { - throw new IllegalArgumentException( - "IFSC Code must be 11 characters: 4 letters, then '0', then 6 alphanumeric characters (e.g. SBIN0001234)."); - } - if (request.getName() == null || request.getName().isBlank()) { - throw new IllegalArgumentException("Sponsor Bank Name is required."); - } - validateDdPayableGlAccount(request.getDdPayableGlAccountId()); - final AppUser currentUser = this.context.authenticatedUser(); - // Set field by field rather than through the all-args constructor: that constructor is - // positional, so adding a column silently shifts every argument after it, and the shift - // type-checks whenever two neighbours share a type. - final SponsorBank sponsorBank = new SponsorBank(); - sponsorBank.setName(request.getName().trim()); - sponsorBank.setDescription(request.getDescription()); - sponsorBank.setIfscCode(request.getIfscCode() != null && !request.getIfscCode().isBlank() - ? request.getIfscCode().trim().toUpperCase() : null); - sponsorBank.setBankCode(request.getBankCode()); - sponsorBank.setSwiftCode(request.getSwiftCode() != null && !request.getSwiftCode().isBlank() - ? request.getSwiftCode().trim().toUpperCase() : null); - sponsorBank.setDdPayableGlAccountId(request.getDdPayableGlAccountId()); - sponsorBank.setCreatedBy(currentUser); - sponsorBank.setCreatedDate(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant())); - this.sponsorBankRepository.saveAndFlush(sponsorBank); - return new CommandProcessingResultBuilder().withEntityId(sponsorBank.getId()).build(); + final CommandWrapper commandRequest = new CommandWrapperBuilder().createSponsorBank() + .withJson(this.gson.toJson(request)).build(); + return this.commandsSourceWritePlatformService.logCommandSource(commandRequest); }); } @@ -214,27 +151,14 @@ public class SponsorBankApiResource { @Operation(summary = "Update a Sponsor Bank", description = "Updates a sponsor bank's details and the DD Payable GL account its drafts are credited to.") public CommandProcessingResult updateSponsorBank(@PathParam("sponsorBankId") final Long sponsorBankId, final SponsorBankRequest request) { + // The actual validation/persistence now lives in SponsorBankWritePlatformService, invoked via + // a NewCommandSourceHandler so the write goes through the generic Maker-Checker gate - see + // UpdateSponsorBankCommandHandler. safely() is kept around the call so a validation failure + // still comes back as the same 400 it always has, rather than an unmapped 500. return safely(() -> { - final SponsorBank sponsorBank = this.sponsorBankRepository.findById(sponsorBankId) - .orElseThrow(() -> new IllegalArgumentException("Sponsor bank not found.")); - if (request.getIfscCode() != null && !request.getIfscCode().isBlank() - && !request.getIfscCode().trim().toUpperCase().matches("^[A-Z]{4}0[A-Z0-9]{6}$")) { - throw new IllegalArgumentException( - "IFSC Code must be 11 characters: 4 letters, then '0', then 6 alphanumeric characters (e.g. SBIN0001234)."); - } - validateDdPayableGlAccount(request.getDdPayableGlAccountId()); - if (request.getName() != null && !request.getName().isBlank()) { - sponsorBank.setName(request.getName().trim()); - } - sponsorBank.setDescription(request.getDescription()); - sponsorBank.setIfscCode(request.getIfscCode() != null && !request.getIfscCode().isBlank() - ? request.getIfscCode().trim().toUpperCase() : null); - sponsorBank.setBankCode(request.getBankCode()); - sponsorBank.setSwiftCode(request.getSwiftCode() != null && !request.getSwiftCode().isBlank() - ? request.getSwiftCode().trim().toUpperCase() : null); - sponsorBank.setDdPayableGlAccountId(request.getDdPayableGlAccountId()); - this.sponsorBankRepository.saveAndFlush(sponsorBank); - return new CommandProcessingResultBuilder().withEntityId(sponsorBank.getId()).build(); + final CommandWrapper commandRequest = new CommandWrapperBuilder().updateSponsorBank(sponsorBankId) + .withJson(this.gson.toJson(request)).build(); + return this.commandsSourceWritePlatformService.logCommandSource(commandRequest); }); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/sponsordd/api/SponsorDDApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/sponsordd/api/SponsorDDApiResource.java index dfa5cba14..664ba319a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/sponsordd/api/SponsorDDApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/sponsordd/api/SponsorDDApiResource.java @@ -41,6 +41,9 @@ import java.util.Collection; import java.util.List; import java.util.function.Supplier; 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.data.CommandProcessingResult; import org.apache.fineract.portfolio.sponsordd.data.SponsorDDBatchIssueResult; import org.apache.fineract.portfolio.sponsordd.data.SponsorDDCashierDepositOptionData; @@ -76,6 +79,7 @@ public class SponsorDDApiResource { private final SponsorDDReprintRequestWritePlatformService reprintRequestWritePlatformService; private final SponsorDDRevokeRequestWritePlatformService revokeRequestWritePlatformService; private final SponsorDDAmendmentRequestWritePlatformService amendmentRequestWritePlatformService; + private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; private final PlatformSecurityContext context; private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); private final Gson gson = new GsonBuilder().serializeNulls().create(); @@ -539,16 +543,28 @@ public class SponsorDDApiResource { @Produces({ MediaType.APPLICATION_JSON }) @Operation(summary = "Configure a Sponsor DD GL mapping", description = "Creates or updates the DD Payable / cheque-clearing GL accounts for a sponsor bank / payment type.") public CommandProcessingResult upsertGlMapping(final GlMappingRequest request) { - return safely(() -> this.issueRequestWritePlatformService.upsertGlMapping( - request.getSponsorBankId(), - request.getDdPayableGlAccountId(), - request.getChequeClearingGlAccountId(), - request.getCashGlAccountId(), - request.getChargesIncomeGlAccountId(), - request.getCashSuspenseGlAccountId(), - request.getGstPercentage(), - request.getGstPayableGlAccountId(), - request.getDdLimitAmount())); + // Routed through the generic command-source pipeline (CommandWrapperBuilder -> + // logCommandSource() -> UpdateSponsorDdGlMappingCommandHandler) so this genuinely gains + // Maker-Checker support; the write itself (issueRequestWritePlatformService.upsertGlMapping) + // is unchanged and now called from that handler instead of directly from here. The JSON is + // built by hand (not Gson-serializing `request` directly) purely to include a "locale" key - + // JsonCommand's BigDecimal extractor requires one, and GlMappingRequest has no such field. + return safely(() -> { + final JsonObject json = new JsonObject(); + json.addProperty("sponsorBankId", request.getSponsorBankId()); + json.addProperty("ddPayableGlAccountId", request.getDdPayableGlAccountId()); + json.addProperty("chequeClearingGlAccountId", request.getChequeClearingGlAccountId()); + json.addProperty("cashGlAccountId", request.getCashGlAccountId()); + json.addProperty("chargesIncomeGlAccountId", request.getChargesIncomeGlAccountId()); + json.addProperty("cashSuspenseGlAccountId", request.getCashSuspenseGlAccountId()); + json.addProperty("gstPercentage", request.getGstPercentage()); + json.addProperty("gstPayableGlAccountId", request.getGstPayableGlAccountId()); + json.addProperty("ddLimitAmount", request.getDdLimitAmount()); + json.addProperty("locale", "en"); + final CommandWrapper commandRequest = new CommandWrapperBuilder().updateSponsorDdGlMapping() + .withJson(this.gson.toJson(json)).build(); + return this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + }); } @POST diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/sponsordd/handler/CreateSponsorBankCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/sponsordd/handler/CreateSponsorBankCommandHandler.java new file mode 100644 index 000000000..0cc96b316 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/sponsordd/handler/CreateSponsorBankCommandHandler.java @@ -0,0 +1,62 @@ +/** + * 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.sponsordd.handler; + +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.portfolio.sponsordd.api.SponsorBankApiResource.SponsorBankRequest; +import org.apache.fineract.portfolio.sponsordd.service.SponsorBankWritePlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@CommandType(entity = "SPONSORBANK", action = "CREATE") +public class CreateSponsorBankCommandHandler implements NewCommandSourceHandler { + + private final SponsorBankWritePlatformService sponsorBankWritePlatformService; + + @Autowired + public CreateSponsorBankCommandHandler(final SponsorBankWritePlatformService sponsorBankWritePlatformService) { + this.sponsorBankWritePlatformService = sponsorBankWritePlatformService; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + + final SponsorBankRequest request = new SponsorBankRequest(); + request.setName(command.stringValueOfParameterNamed("name")); + request.setDescription(command.stringValueOfParameterNamed("description")); + request.setIfscCode(command.stringValueOfParameterNamed("ifscCode")); + request.setBankCode(command.stringValueOfParameterNamed("bankCode")); + request.setSwiftCode(command.stringValueOfParameterNamed("swiftCode")); + request.setDdPayableGlAccountId(command.longValueOfParameterNamed("ddPayableGlAccountId")); + + final CommandProcessingResult result = this.sponsorBankWritePlatformService.createSponsorBank(request); + + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(result.getResourceId()) // + .build(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/sponsordd/handler/UpdateSponsorBankCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/sponsordd/handler/UpdateSponsorBankCommandHandler.java new file mode 100644 index 000000000..0b5ecc00d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/sponsordd/handler/UpdateSponsorBankCommandHandler.java @@ -0,0 +1,63 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.sponsordd.handler; + +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.portfolio.sponsordd.api.SponsorBankApiResource.SponsorBankRequest; +import org.apache.fineract.portfolio.sponsordd.service.SponsorBankWritePlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@CommandType(entity = "SPONSORBANK", action = "UPDATE") +public class UpdateSponsorBankCommandHandler implements NewCommandSourceHandler { + + private final SponsorBankWritePlatformService sponsorBankWritePlatformService; + + @Autowired + public UpdateSponsorBankCommandHandler(final SponsorBankWritePlatformService sponsorBankWritePlatformService) { + this.sponsorBankWritePlatformService = sponsorBankWritePlatformService; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + + final SponsorBankRequest request = new SponsorBankRequest(); + request.setName(command.stringValueOfParameterNamed("name")); + request.setDescription(command.stringValueOfParameterNamed("description")); + request.setIfscCode(command.stringValueOfParameterNamed("ifscCode")); + request.setBankCode(command.stringValueOfParameterNamed("bankCode")); + request.setSwiftCode(command.stringValueOfParameterNamed("swiftCode")); + request.setDdPayableGlAccountId(command.longValueOfParameterNamed("ddPayableGlAccountId")); + + final CommandProcessingResult result = this.sponsorBankWritePlatformService.updateSponsorBank(command.entityId(), + request); + + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(result.getResourceId()) // + .build(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/sponsordd/handler/UpdateSponsorDdGlMappingCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/sponsordd/handler/UpdateSponsorDdGlMappingCommandHandler.java new file mode 100644 index 000000000..6960faeae --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/sponsordd/handler/UpdateSponsorDdGlMappingCommandHandler.java @@ -0,0 +1,74 @@ +/** + * 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.sponsordd.handler; + +import java.math.BigDecimal; +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.portfolio.sponsordd.service.SponsorDDIssueRequestWritePlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * The underlying write ({@link SponsorDDIssueRequestWritePlatformService#upsertGlMapping}) is a + * genuine upsert - one call creates the single {@code m_sponsor_dd_gl_mapping} row the first time and + * updates it every time after - so there is exactly one command/permission here rather than a + * separate CREATE and UPDATE, matching that reality instead of inventing a split the endpoint has + * never had. + */ +@Service +@CommandType(entity = "SPONSORDDGLMAPPING", action = "UPDATE") +public class UpdateSponsorDdGlMappingCommandHandler implements NewCommandSourceHandler { + + private final SponsorDDIssueRequestWritePlatformService issueRequestWritePlatformService; + + @Autowired + public UpdateSponsorDdGlMappingCommandHandler( + final SponsorDDIssueRequestWritePlatformService issueRequestWritePlatformService) { + this.issueRequestWritePlatformService = issueRequestWritePlatformService; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + + final Long sponsorBankId = command.longValueOfParameterNamed("sponsorBankId"); + final Long ddPayableGlAccountId = command.longValueOfParameterNamed("ddPayableGlAccountId"); + final Long chequeClearingGlAccountId = command.longValueOfParameterNamed("chequeClearingGlAccountId"); + final Long cashGlAccountId = command.longValueOfParameterNamed("cashGlAccountId"); + final Long chargesIncomeGlAccountId = command.longValueOfParameterNamed("chargesIncomeGlAccountId"); + final Long cashSuspenseGlAccountId = command.longValueOfParameterNamed("cashSuspenseGlAccountId"); + final BigDecimal gstPercentage = command.bigDecimalValueOfParameterNamed("gstPercentage"); + final Long gstPayableGlAccountId = command.longValueOfParameterNamed("gstPayableGlAccountId"); + final BigDecimal ddLimitAmount = command.bigDecimalValueOfParameterNamed("ddLimitAmount"); + + final CommandProcessingResult result = this.issueRequestWritePlatformService.upsertGlMapping(sponsorBankId, + ddPayableGlAccountId, chequeClearingGlAccountId, cashGlAccountId, chargesIncomeGlAccountId, + cashSuspenseGlAccountId, gstPercentage, gstPayableGlAccountId, ddLimitAmount); + + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(result.getResourceId()) // + .build(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/sponsordd/service/SponsorBankWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/sponsordd/service/SponsorBankWritePlatformService.java new file mode 100644 index 000000000..9078614c8 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/sponsordd/service/SponsorBankWritePlatformService.java @@ -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.sponsordd.service; + +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.sponsordd.api.SponsorBankApiResource.SponsorBankRequest; + +public interface SponsorBankWritePlatformService { + + CommandProcessingResult createSponsorBank(SponsorBankRequest request); + + CommandProcessingResult updateSponsorBank(Long sponsorBankId, SponsorBankRequest request); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/sponsordd/service/SponsorBankWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/sponsordd/service/SponsorBankWritePlatformServiceImpl.java new file mode 100644 index 000000000..04f3fd57e --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/sponsordd/service/SponsorBankWritePlatformServiceImpl.java @@ -0,0 +1,147 @@ +/** + * 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.sponsordd.service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.sponsordd.api.SponsorBankApiResource.SponsorBankRequest; +import org.apache.fineract.portfolio.sponsordd.domain.SponsorBank; +import org.apache.fineract.portfolio.sponsordd.domain.SponsorBankRepository; +import org.apache.fineract.useradministration.domain.AppUser; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * Moved verbatim out of {@code SponsorBankApiResource.createSponsorBank()} and + * {@code updateSponsorBank()} so both writes can be invoked from a {@code NewCommandSourceHandler} + * for Maker-Checker support. {@link #validateDdPayableGlAccount(Long)} is a copy of the resource's + * own (now-unused) validator, kept here since both CREATE and UPDATE rely on it. + */ +@Service +public class SponsorBankWritePlatformServiceImpl implements SponsorBankWritePlatformService { + + private final SponsorBankRepository sponsorBankRepository; + private final PlatformSecurityContext context; + private final JdbcTemplate jdbcTemplate; + + @Autowired + public SponsorBankWritePlatformServiceImpl(final SponsorBankRepository sponsorBankRepository, + final PlatformSecurityContext context, final JdbcTemplate jdbcTemplate) { + this.sponsorBankRepository = sponsorBankRepository; + this.context = context; + this.jdbcTemplate = jdbcTemplate; + } + + @Transactional + @Override + public CommandProcessingResult createSponsorBank(final SponsorBankRequest request) { + if (request.getIfscCode() != null && !request.getIfscCode().isBlank() + && !request.getIfscCode().trim().toUpperCase().matches("^[A-Z]{4}0[A-Z0-9]{6}$")) { + throw new IllegalArgumentException( + "IFSC Code must be 11 characters: 4 letters, then '0', then 6 alphanumeric characters (e.g. SBIN0001234)."); + } + if (request.getName() == null || request.getName().isBlank()) { + throw new IllegalArgumentException("Sponsor Bank Name is required."); + } + validateDdPayableGlAccount(request.getDdPayableGlAccountId()); + final AppUser currentUser = this.context.authenticatedUser(); + // Set field by field rather than through the all-args constructor: that constructor is + // positional, so adding a column silently shifts every argument after it, and the shift + // type-checks whenever two neighbours share a type. + final SponsorBank sponsorBank = new SponsorBank(); + sponsorBank.setName(request.getName().trim()); + sponsorBank.setDescription(request.getDescription()); + sponsorBank.setIfscCode(request.getIfscCode() != null && !request.getIfscCode().isBlank() + ? request.getIfscCode().trim().toUpperCase() : null); + sponsorBank.setBankCode(request.getBankCode()); + sponsorBank.setSwiftCode(request.getSwiftCode() != null && !request.getSwiftCode().isBlank() + ? request.getSwiftCode().trim().toUpperCase() : null); + sponsorBank.setDdPayableGlAccountId(request.getDdPayableGlAccountId()); + sponsorBank.setCreatedBy(currentUser); + sponsorBank.setCreatedDate(LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant())); + this.sponsorBankRepository.saveAndFlush(sponsorBank); + return new CommandProcessingResultBuilder().withEntityId(sponsorBank.getId()).build(); + } + + @Transactional + @Override + public CommandProcessingResult updateSponsorBank(final Long sponsorBankId, final SponsorBankRequest request) { + final SponsorBank sponsorBank = this.sponsorBankRepository.findById(sponsorBankId) + .orElseThrow(() -> new IllegalArgumentException("Sponsor bank not found.")); + if (request.getIfscCode() != null && !request.getIfscCode().isBlank() + && !request.getIfscCode().trim().toUpperCase().matches("^[A-Z]{4}0[A-Z0-9]{6}$")) { + throw new IllegalArgumentException( + "IFSC Code must be 11 characters: 4 letters, then '0', then 6 alphanumeric characters (e.g. SBIN0001234)."); + } + validateDdPayableGlAccount(request.getDdPayableGlAccountId()); + if (request.getName() != null && !request.getName().isBlank()) { + sponsorBank.setName(request.getName().trim()); + } + sponsorBank.setDescription(request.getDescription()); + sponsorBank.setIfscCode(request.getIfscCode() != null && !request.getIfscCode().isBlank() + ? request.getIfscCode().trim().toUpperCase() : null); + sponsorBank.setBankCode(request.getBankCode()); + sponsorBank.setSwiftCode(request.getSwiftCode() != null && !request.getSwiftCode().isBlank() + ? request.getSwiftCode().trim().toUpperCase() : null); + sponsorBank.setDdPayableGlAccountId(request.getDdPayableGlAccountId()); + this.sponsorBankRepository.saveAndFlush(sponsorBank); + return new CommandProcessingResultBuilder().withEntityId(sponsorBank.getId()).build(); + } + + /** Copied from {@code SponsorBankApiResource} - see that class for the full rationale. */ + private void validateDdPayableGlAccount(final Long glAccountId) { + if (glAccountId == null) { + return; + } + final List> rows = this.jdbcTemplate.queryForList( + "SELECT gl_code, name, classification_enum, account_usage, disabled FROM acc_gl_account WHERE id = ?", + glAccountId); + if (rows.isEmpty()) { + throw new IllegalArgumentException("The selected DD Payable GL account does not exist."); + } + final Map gl = rows.get(0); + final String label = gl.get("gl_code") + " · " + gl.get("name"); + // 2 is LIABILITY in Fineract's GLAccountType enum. + if (((Number) gl.get("classification_enum")).intValue() != 2) { + throw new IllegalArgumentException( + "DD Payable must be a LIABILITY account — a draft is money owed to the sponsor bank until it is presented. " + + label + " is not a liability account."); + } + // 2 is HEADER in Fineract's GLAccountUsage enum; nothing can be posted to a header. + if (((Number) gl.get("account_usage")).intValue() == 2) { + throw new IllegalArgumentException("DD Payable must be a detail account, not a header. Pick a postable account under " + + label + "."); + } + // Read through both shapes: the column comes back as a Boolean on some drivers and as a + // 0/1 Number on others, and a plain Boolean cast would throw on the latter. + final Object disabled = gl.get("disabled"); + final boolean isDisabled = disabled instanceof Boolean b ? b + : disabled instanceof Number n && n.intValue() != 0; + if (isDisabled) { + throw new IllegalArgumentException("The selected DD Payable GL account (" + label + ") is disabled."); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/spm/api/SpmApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/spm/api/SpmApiResource.java index c8c6ff120..cce2ef2d2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/spm/api/SpmApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/spm/api/SpmApiResource.java @@ -18,7 +18,6 @@ */ package org.apache.fineract.spm.api; -import com.google.gson.Gson; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; @@ -37,10 +36,14 @@ import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; 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.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException; +import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.spm.data.SurveyData; import org.apache.fineract.spm.domain.Survey; @@ -57,6 +60,8 @@ public class SpmApiResource { private final PlatformSecurityContext securityContext; private final SpmService spmService; + private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; + private final DefaultToApiJsonSerializer toApiJsonSerializer; @GET @Consumes({ MediaType.APPLICATION_JSON }) @@ -103,12 +108,10 @@ public class SpmApiResource { @Operation(summary = "Create a Survey", description = "Adds a new survey to collect client related data.\n" + "\n" + "Mandatory Fields\n" + "\n" + "countryCode, key, name, questions, responses, sequenceNo, text, description") @ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") }) - public String createSurvey(@Parameter(description = "Create survey") final SurveyData surveyData) { - this.securityContext.authenticatedUser(); - final Survey survey = SurveyMapper.map(surveyData, new Survey()); - this.spmService.createSurvey(survey); - return getResponse(survey.getId()); - + public String createSurvey(@Parameter(description = "Create survey") final String apiRequestBodyAsJson) { + final CommandWrapper commandRequest = new CommandWrapperBuilder().createSurvey().withJson(apiRequestBodyAsJson).build(); + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + return this.toApiJsonSerializer.serialize(result); } @PUT @@ -116,12 +119,10 @@ public class SpmApiResource { @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) @Transactional - public String editSurvey(@PathParam("id") final Long id, final SurveyData surveyData) { - this.securityContext.authenticatedUser(); - final Survey surveyToUpdate = this.spmService.findById(id); - final Survey survey = SurveyMapper.map(surveyData, surveyToUpdate); - this.spmService.updateSurvey(survey); - return getResponse(survey.getId()); + public String editSurvey(@PathParam("id") final Long id, final String apiRequestBodyAsJson) { + final CommandWrapper commandRequest = new CommandWrapperBuilder().updateSurvey(id).withJson(apiRequestBodyAsJson).build(); + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + return this.toApiJsonSerializer.serialize(result); } @POST @@ -131,22 +132,17 @@ public class SpmApiResource { @Transactional @Operation(summary = "Deactivate Survey", description = "") @ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") }) - public void activateOrDeactivateSurvey(@PathParam("id") final Long id, @QueryParam("command") final String command) { - this.securityContext.authenticatedUser(); + public String activateOrDeactivateSurvey(@PathParam("id") final Long id, @QueryParam("command") final String command) { + final CommandWrapperBuilder builder = new CommandWrapperBuilder(); + final CommandWrapper commandRequest; if (command != null && command.equalsIgnoreCase("activate")) { - this.spmService.activateSurvey(id); + commandRequest = builder.activateSurvey(id).build(); } else if (command != null && command.equalsIgnoreCase("deactivate")) { - this.spmService.deactivateSurvey(id); + commandRequest = builder.deactivateSurvey(id).build(); } else { throw new UnrecognizedQueryParamException("command", command); } - - } - - private String getResponse(Long id) { - Gson gson = new Gson(); - HashMap response = new HashMap<>(); - response.put("resourceId", id); - return gson.toJson(response); + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + return this.toApiJsonSerializer.serialize(result); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/spm/handler/ActivateSurveyCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/spm/handler/ActivateSurveyCommandHandler.java new file mode 100644 index 000000000..bae02b9da --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/spm/handler/ActivateSurveyCommandHandler.java @@ -0,0 +1,53 @@ +/** + * 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.spm.handler; + +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.spm.service.SpmService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@CommandType(entity = "SURVEY", action = "ACTIVATE") +public class ActivateSurveyCommandHandler implements NewCommandSourceHandler { + + private final SpmService spmService; + + @Autowired + public ActivateSurveyCommandHandler(final SpmService spmService) { + this.spmService = spmService; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + + this.spmService.activateSurvey(command.entityId()); + + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(command.entityId()) // + .build(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/spm/handler/CreateSurveyCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/spm/handler/CreateSurveyCommandHandler.java new file mode 100644 index 000000000..06c2e162f --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/spm/handler/CreateSurveyCommandHandler.java @@ -0,0 +1,71 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.spm.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.core.JsonProcessingException; +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; +import org.apache.fineract.spm.data.SurveyData; +import org.apache.fineract.spm.domain.Survey; +import org.apache.fineract.spm.service.SpmService; +import org.apache.fineract.spm.util.SurveyMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@CommandType(entity = "SURVEY", action = "CREATE") +public class CreateSurveyCommandHandler implements NewCommandSourceHandler { + + private final SpmService spmService; + private final ObjectMapper objectMapper; + + @Autowired + public CreateSurveyCommandHandler(final SpmService spmService, final ObjectMapper objectMapper) { + this.spmService = spmService; + this.objectMapper = objectMapper; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + + final SurveyData surveyData = readSurveyData(command); + final Survey survey = SurveyMapper.map(surveyData, new Survey()); + this.spmService.createSurvey(survey); + + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(survey.getId()) // + .build(); + } + + private SurveyData readSurveyData(final JsonCommand command) { + try { + return this.objectMapper.readValue(command.json(), SurveyData.class); + } catch (final JsonProcessingException e) { + throw new PlatformDataIntegrityException("error.msg.survey.invalid.json", "Invalid survey payload: " + e.getMessage()); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/spm/handler/DeactivateSurveyCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/spm/handler/DeactivateSurveyCommandHandler.java new file mode 100644 index 000000000..3d9e4bfdc --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/spm/handler/DeactivateSurveyCommandHandler.java @@ -0,0 +1,53 @@ +/** + * 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.spm.handler; + +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.spm.service.SpmService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@CommandType(entity = "SURVEY", action = "DEACTIVATE") +public class DeactivateSurveyCommandHandler implements NewCommandSourceHandler { + + private final SpmService spmService; + + @Autowired + public DeactivateSurveyCommandHandler(final SpmService spmService) { + this.spmService = spmService; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + + this.spmService.deactivateSurvey(command.entityId()); + + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(command.entityId()) // + .build(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/spm/handler/UpdateSurveyCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/spm/handler/UpdateSurveyCommandHandler.java new file mode 100644 index 000000000..9452e818f --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/spm/handler/UpdateSurveyCommandHandler.java @@ -0,0 +1,72 @@ +/** + * 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.spm.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.core.JsonProcessingException; +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; +import org.apache.fineract.spm.data.SurveyData; +import org.apache.fineract.spm.domain.Survey; +import org.apache.fineract.spm.service.SpmService; +import org.apache.fineract.spm.util.SurveyMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@CommandType(entity = "SURVEY", action = "UPDATE") +public class UpdateSurveyCommandHandler implements NewCommandSourceHandler { + + private final SpmService spmService; + private final ObjectMapper objectMapper; + + @Autowired + public UpdateSurveyCommandHandler(final SpmService spmService, final ObjectMapper objectMapper) { + this.spmService = spmService; + this.objectMapper = objectMapper; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + + final SurveyData surveyData = readSurveyData(command); + final Survey surveyToUpdate = this.spmService.findById(command.entityId()); + final Survey survey = SurveyMapper.map(surveyData, surveyToUpdate); + this.spmService.updateSurvey(survey); + + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(survey.getId()) // + .build(); + } + + private SurveyData readSurveyData(final JsonCommand command) { + try { + return this.objectMapper.readValue(command.json(), SurveyData.class); + } catch (final JsonProcessingException e) { + throw new PlatformDataIntegrityException("error.msg.survey.invalid.json", "Invalid survey payload: " + e.getMessage()); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/useradministration/service/PermissionReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/useradministration/service/PermissionReadPlatformServiceImpl.java index 9554f8cdb..17475b1fd 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/useradministration/service/PermissionReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/useradministration/service/PermissionReadPlatformServiceImpl.java @@ -97,15 +97,50 @@ public class PermissionReadPlatformServiceImpl implements PermissionReadPlatform + " order by p.grouping, coalesce(entity_name, ''), p.code"; } + /** + * Permission codes whose approval workflow is a bespoke, unconditional pending-request mechanism + * (m_user_pending_request / m_share_account_pending_edit / m_current_account_pending_edit / + * m_client_pending_edit) that never reads {@code can_maker_checker} - verified by inspecting every + * write-service on this path (UserPendingWritePlatformServiceImpl, ShareAccountEditWritePlatformServiceJpaRepositoryImpl, + * CurrentAccountEditWritePlatformServiceJpaRepositoryImpl, and the client edit equivalent): none of + * them consult ConfigurationDomainService in any way. Toggling this checkbox on the Configure + * Maker-Checker Tasks screen therefore has zero effect on these tasks in either direction, so they + * are excluded here rather than presented as configurable when they are not. CREATE_USER/UPDATE_USER + * remain governed by the separate global "maker-checker" configuration switch (System -> + * Configurations), not by this per-task flag. + */ + private static final String DEAD_MAKER_CHECKER_CODES = "'CREATE_USER','UPDATE_USER'," + + "'SUBMITEDIT_SHAREACCOUNT','APPROVEEDIT_SHAREACCOUNT','REJECTEDIT_SHAREACCOUNT'," + + "'SUBMITEDIT_CURRENTACCOUNT','APPROVEEDIT_CURRENTACCOUNT','REJECTEDIT_CURRENTACCOUNT'," + + "'SUBMITEDIT_CLIENT','APPROVEEDIT_CLIENT','REJECTEDIT_CLIENT'"; + + /* + * Unlike DEAD_MAKER_CHECKER_CODES above, these codes ARE read by the live maker-checker pipeline + * (CommandSourceService.isMakerCheckerEnabledForTask()) - they are excluded from this screen for a + * different reason: UPDATE_PERMISSION governs this very "Configure Maker Checker Tasks" screen + * (toggling which tasks require maker-checker), and UPDATE_CONFIGURATION governs the global + * maker-checker on/off switch and other System -> Configurations settings. If either were flipped + * to can_maker_checker=true and the approving checker.user lacked CHECKER_SUPER_USER, an admin + * could lock themselves out of ever approving further changes to maker-checker configuration + * itself, with no ordinary way to reverse it. Excluding them here only hides them from this + * screen; it does not touch their can_maker_checker value or their normal enforcement. + */ + private static final String SELF_REFERENTIAL_MAKER_CHECKER_CODES = "'UPDATE_PERMISSION','UPDATE_CONFIGURATION'"; + public String makerCheckerablePermissionSchema() { /* * get all 'Maker-Checkerable' permissions - Maintenance permissions (i.e. exclude the 'special' grouping, - * the READ permissions and the CHECKER permissions + * the READ permissions and the CHECKER permissions), and exclude permission codes whose approval + * workflow does not actually read can_maker_checker (see DEAD_MAKER_CHECKER_CODES above), as well as + * codes that control maker-checker configuration itself (see SELF_REFERENTIAL_MAKER_CHECKER_CODES + * above) to avoid a self-lockout. */ return "select p.grouping, p.code, p.entity_name as entityName, p.action_name as actionName, p.can_maker_checker as selected" + " from m_permission p " + " where " + sqlGenerator.escape("grouping") + " != 'special' and code not like 'READ_%' and code not like '%\\_CHECKER'" + + " and code not in (" + DEAD_MAKER_CHECKER_CODES + ")" + + " and code not in (" + SELF_REFERENTIAL_MAKER_CHECKER_CODES + ")" + " order by p.grouping, coalesce(entity_name, ''), p.code"; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/useradministration/service/UserPendingReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/useradministration/service/UserPendingReadPlatformServiceImpl.java index 08d2f1038..e767db338 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/useradministration/service/UserPendingReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/useradministration/service/UserPendingReadPlatformServiceImpl.java @@ -36,7 +36,13 @@ import org.apache.commons.lang3.StringUtils; import org.apache.fineract.infrastructure.core.domain.JdbcSupport; import org.apache.fineract.infrastructure.core.serialization.GoogleGsonSerializerHelper; import org.apache.fineract.infrastructure.core.service.Page; +import org.apache.fineract.organisation.office.domain.Office; +import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper; +import org.apache.fineract.organisation.staff.domain.Staff; +import org.apache.fineract.organisation.staff.domain.StaffRepositoryWrapper; import org.apache.fineract.useradministration.data.UserPendingRequestData; +import org.apache.fineract.useradministration.domain.Role; +import org.apache.fineract.useradministration.domain.RoleRepository; import org.apache.fineract.useradministration.domain.UserPendingRequest; import org.apache.fineract.useradministration.domain.UserPendingRequestRepositoryWrapper; import org.springframework.dao.EmptyResultDataAccessException; @@ -51,8 +57,13 @@ public class UserPendingReadPlatformServiceImpl implements UserPendingReadPlatfo private final JdbcTemplate jdbcTemplate; private final UserPendingRequestRepositoryWrapper pendingRequestRepositoryWrapper; + private final OfficeRepositoryWrapper officeRepositoryWrapper; + private final StaffRepositoryWrapper staffRepositoryWrapper; + private final RoleRepository roleRepository; private final Gson gson = GoogleGsonSerializerHelper.createSimpleGson(); + private static final String UNKNOWN_REFERENCE_FORMAT = "Unknown (ID: %s)"; + private static final String SELECT_SQL = """ SELECT r.id AS id, r.user_id AS userId, @@ -115,7 +126,96 @@ public class UserPendingReadPlatformServiceImpl implements UserPendingReadPlatfo public UserPendingRequestData retrievePendingRequest(final Long id) { final UserPendingRequest request = this.pendingRequestRepositoryWrapper.findOneWithNotFoundDetection(id); final String sql = SELECT_SQL + " WHERE r.id = ?"; - return this.jdbcTemplate.queryForObject(sql, new UserPendingDetailRowMapper(), id); + final UserPendingRequestData data = this.jdbcTemplate.queryForObject(sql, new UserPendingDetailRowMapper(), id); + resolveDisplayNames(data); + return data; + } + + /** + * Resolves office/staff/role ids on the pending-user comparison payload to human-readable names, and folds + * the point-in-time original-side name fields (officeName/staffName/roleNames) into their corresponding + * id-keyed rows so the compare dialog shows one row per field instead of a redundant name-only row. + */ + private void resolveDisplayNames(final UserPendingRequestData data) { + final Map originalValues = data.getOriginalValues(); + final Map proposedValues = data.getProposedValues(); + + // CREATE_USER requests have no original snapshot - nothing to fold on that side. + if (originalValues != null && !originalValues.isEmpty()) { + foldNameIntoId(originalValues, "officeName", "officeId"); + foldNameIntoId(originalValues, "staffName", "staffId"); + if (originalValues.containsKey("roleNames")) { + originalValues.put("roles", sortNames(toStringList(originalValues.get("roleNames")))); + originalValues.remove("roleNames"); + } + } + + if (proposedValues != null && !proposedValues.isEmpty()) { + if (proposedValues.get("officeId") instanceof Number officeIdNum) { + proposedValues.put("officeId", resolveOfficeName(officeIdNum.longValue())); + } + if (proposedValues.get("staffId") instanceof Number staffIdNum) { + proposedValues.put("staffId", resolveStaffName(staffIdNum.longValue())); + } + if (proposedValues.get("roles") instanceof Collection rolesCollection) { + proposedValues.put("roles", sortNames(resolveRoleNames(toStringList(rolesCollection)))); + } + } + } + + private static void foldNameIntoId(final Map values, final String nameKey, final String idKey) { + if (values.containsKey(nameKey)) { + values.put(idKey, values.get(nameKey)); + values.remove(nameKey); + } + } + + private String resolveOfficeName(final Long officeId) { + try { + final Office office = this.officeRepositoryWrapper.findOneWithNotFoundDetection(officeId); + return office.getName(); + } catch (final RuntimeException e) { + return String.format(UNKNOWN_REFERENCE_FORMAT, officeId); + } + } + + private String resolveStaffName(final Long staffId) { + try { + final Staff staff = this.staffRepositoryWrapper.findOneWithNotFoundDetection(staffId); + return staff.displayName(); + } catch (final RuntimeException e) { + return String.format(UNKNOWN_REFERENCE_FORMAT, staffId); + } + } + + private List resolveRoleNames(final List roleIds) { + final List names = new ArrayList<>(); + for (final String roleIdStr : roleIds) { + try { + final Long roleId = Long.valueOf(roleIdStr); + final Role role = this.roleRepository.findById(roleId).orElse(null); + names.add(role != null ? role.getName() : String.format(UNKNOWN_REFERENCE_FORMAT, roleId)); + } catch (final NumberFormatException e) { + names.add(roleIdStr); + } + } + return names; + } + + private static List sortNames(final List names) { + final List sorted = new ArrayList<>(names); + sorted.sort(String.CASE_INSENSITIVE_ORDER); + return sorted; + } + + private static List toStringList(final Object value) { + final List result = new ArrayList<>(); + if (value instanceof Collection collection) { + for (final Object item : collection) { + result.add(String.valueOf(item)); + } + } + return result; } private static class UserPendingRowMapper implements RowMapper { diff --git a/fineract-provider/src/main/java/org/apache/fineract/useradministration/service/UserPendingWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/useradministration/service/UserPendingWritePlatformServiceImpl.java index 00c235802..3806b6b24 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/useradministration/service/UserPendingWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/useradministration/service/UserPendingWritePlatformServiceImpl.java @@ -162,8 +162,8 @@ public class UserPendingWritePlatformServiceImpl implements UserPendingWritePlat originalSnapshot.put("officeId", userToUpdate.getOffice() != null ? userToUpdate.getOffice().getId() : null); originalSnapshot.put("officeName", userToUpdate.getOffice() != null ? userToUpdate.getOffice().getName() : null); originalSnapshot.put("staffId", userToUpdate.getStaff() != null ? userToUpdate.getStaff().getId() : null); + originalSnapshot.put("staffName", userToUpdate.getStaff() != null ? userToUpdate.getStaff().displayName() : null); originalSnapshot.put("passwordNeverExpires", userToUpdate.getPasswordNeverExpires()); - originalSnapshot.put("isSelfServiceUser", userToUpdate.isSelfServiceUser()); final Set roleNames = new HashSet<>(); final Set roleIds = new HashSet<>(); if (userToUpdate.getRoles() != null) { diff --git a/fineract-provider/src/main/resources/application.properties b/fineract-provider/src/main/resources/application.properties index 020cdbd2f..b6ac7ca48 100644 --- a/fineract-provider/src/main/resources/application.properties +++ b/fineract-provider/src/main/resources/application.properties @@ -46,7 +46,7 @@ fineract.security.oauth2.client.registrations.frontend-client.require-authorizat fineract.tenant.host=${FINERACT_DEFAULT_TENANTDB_HOSTNAME:localhost} fineract.tenant.port=${FINERACT_DEFAULT_TENANTDB_PORT:5432} fineract.tenant.username=${FINERACT_DEFAULT_TENANTDB_UID:postgres} -fineract.tenant.password=${FINERACT_DEFAULT_TENANTDB_PWD:Keshav@898966} +fineract.tenant.password=${FINERACT_DEFAULT_TENANTDB_PWD:anshul@9009} fineract.tenant.parameters=${FINERACT_DEFAULT_TENANTDB_CONN_PARAMS:} fineract.tenant.timezone=${FINERACT_DEFAULT_TENANTDB_TIMEZONE:Asia/Kolkata} fineract.tenant.identifier=${FINERACT_DEFAULT_TENANTDB_IDENTIFIER:default} @@ -415,7 +415,7 @@ server.tomcat.mbeanregistry.enabled=${FINERACT_SERVER_TOMCAT_MBEANREGISTRY_ENABL spring.datasource.hikari.driverClassName=${FINERACT_HIKARI_DRIVER_SOURCE_CLASS_NAME:org.postgresql.Driver} spring.datasource.hikari.jdbcUrl=${FINERACT_HIKARI_JDBC_URL:jdbc:postgresql://localhost:5432/fineract_tenants} spring.datasource.hikari.username=${FINERACT_HIKARI_USERNAME:postgres} -spring.datasource.hikari.password=${FINERACT_HIKARI_PASSWORD:Keshav@898966} +spring.datasource.hikari.password=${FINERACT_HIKARI_PASSWORD:anshul@9009} spring.datasource.hikari.minimumIdle=${FINERACT_HIKARI_MINIMUM_IDLE:3} spring.datasource.hikari.maximumPoolSize=${FINERACT_HIKARI_MAXIMUM_POOL_SIZE:10} spring.datasource.hikari.idleTimeout=${FINERACT_HIKARI_IDLE_TIMEOUT:60000} diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml index 7727710c9..d1385c84d 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml @@ -120,6 +120,15 @@ + + + @@ -136,5 +145,15 @@ + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0571_add_sponsor_dd_gl_mapping_permission.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0571_add_sponsor_dd_gl_mapping_permission.xml new file mode 100644 index 000000000..3f5a9552a --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0571_add_sponsor_dd_gl_mapping_permission.xml @@ -0,0 +1,45 @@ + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code = 'UPDATE_SPONSORDDGLMAPPING' + + + + + + + + + + + + + + INSERT INTO m_role_permission (role_id, permission_id) + SELECT r.id, p.id + FROM m_role r + CROSS JOIN m_permission p + WHERE p.code = 'UPDATE_SPONSORDDGLMAPPING' + AND (LOWER(r.name) LIKE '%manager%' OR LOWER(r.name) = 'super user') + AND NOT EXISTS ( + SELECT 1 FROM m_role_permission rp + WHERE rp.role_id = r.id AND rp.permission_id = p.id + ) + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0572_add_sponsor_bank_update_permission.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0572_add_sponsor_bank_update_permission.xml new file mode 100644 index 000000000..1006408c2 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0572_add_sponsor_bank_update_permission.xml @@ -0,0 +1,44 @@ + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code = 'UPDATE_SPONSORBANK' + + + + + + + + + + + + + + INSERT INTO m_role_permission (role_id, permission_id) + SELECT rp.role_id, p.id + FROM m_role_permission rp + JOIN m_permission existing ON existing.id = rp.permission_id AND existing.code = 'CREATE_SPONSORBANK' + CROSS JOIN m_permission p + WHERE p.code = 'UPDATE_SPONSORBANK' + AND NOT EXISTS ( + SELECT 1 FROM m_role_permission rp2 + WHERE rp2.role_id = rp.role_id AND rp2.permission_id = p.id + ) + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0583_add_loan_originator_checker_permissions.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0583_add_loan_originator_checker_permissions.xml new file mode 100644 index 000000000..9b771834e --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0583_add_loan_originator_checker_permissions.xml @@ -0,0 +1,79 @@ + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code = 'CREATE_LOAN_ORIGINATOR_CHECKER' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO m_role_permission (role_id, permission_id) + SELECT rp.role_id, checker.id + FROM m_role_permission rp + JOIN m_permission base ON base.id = rp.permission_id + JOIN m_permission checker ON checker.code = base.code || '_CHECKER' + WHERE base.code IN ('CREATE_LOAN_ORIGINATOR', 'UPDATE_LOAN_ORIGINATOR', 'DELETE_LOAN_ORIGINATOR', + 'ATTACH_LOAN_ORIGINATOR', 'DETACH_LOAN_ORIGINATOR') + AND NOT EXISTS ( + SELECT 1 FROM m_role_permission rp2 + WHERE rp2.role_id = rp.role_id AND rp2.permission_id = checker.id + ) + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0584_add_manage_survey_permissions.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0584_add_manage_survey_permissions.xml new file mode 100644 index 000000000..68e5330ab --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0584_add_manage_survey_permissions.xml @@ -0,0 +1,68 @@ + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code = 'CREATE_SURVEY' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO m_role_permission (role_id, permission_id) + SELECT r.id, p.id + FROM m_role r + JOIN m_permission p ON p.code IN ('CREATE_SURVEY', 'UPDATE_SURVEY', 'ACTIVATE_SURVEY', 'DEACTIVATE_SURVEY') + WHERE (LOWER(r.name) LIKE '%manager%' OR LOWER(r.name) = 'super user') + AND NOT EXISTS ( + SELECT 1 FROM m_role_permission rp2 + WHERE rp2.role_id = r.id AND rp2.permission_id = p.id + ) + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0585_add_loan_cob_catchup_permission.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0585_add_loan_cob_catchup_permission.xml new file mode 100644 index 000000000..9cbdd9553 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0585_add_loan_cob_catchup_permission.xml @@ -0,0 +1,44 @@ + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code = 'EXECUTE_LOANCOBCATCHUP' + + + + + + + + + + + + + + INSERT INTO m_role_permission (role_id, permission_id) + SELECT r.id, p.id + FROM m_role r + JOIN m_permission p ON p.code = 'EXECUTE_LOANCOBCATCHUP' + WHERE (LOWER(r.name) LIKE '%manager%' OR LOWER(r.name) = 'super user') + AND NOT EXISTS ( + SELECT 1 FROM m_role_permission rp2 + WHERE rp2.role_id = r.id AND rp2.permission_id = p.id + ) + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0586_add_system_module_checker_permissions.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0586_add_system_module_checker_permissions.xml new file mode 100644 index 000000000..150bffec5 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0586_add_system_module_checker_permissions.xml @@ -0,0 +1,107 @@ + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code = 'CREATE_ACCOUNTNUMBERFORMAT_CHECKER' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO m_role_permission (role_id, permission_id) + SELECT rp.role_id, checker.id + FROM m_role_permission rp + JOIN m_permission base ON base.id = rp.permission_id + JOIN m_permission checker ON checker.code = base.code || '_CHECKER' + WHERE base.code IN ('CREATE_ACCOUNTNUMBERFORMAT', 'UPDATE_ACCOUNTNUMBERFORMAT', 'DELETE_ACCOUNTNUMBERFORMAT', + 'CREATE_ENTITYMAPPING', 'UPDATE_ENTITYMAPPING', 'DELETE_ENTITYMAPPING', + 'CREATE_USERAGREEMENT', 'UPDATE_BATCH_BUSINESS_STEP') + AND NOT EXISTS ( + SELECT 1 FROM m_role_permission rp2 + WHERE rp2.role_id = rp.role_id AND rp2.permission_id = checker.id + ) + + + +