Implement organization maker-checker workflows
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s

This commit is contained in:
2026-09-24 17:22:39 +05:30
parent fde2699957
commit 9beae3df28
38 changed files with 1589 additions and 410 deletions
@@ -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";
@@ -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<CurrencyUpdateRequest> 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<CurrencyUpdateResponse> response = commandPipeline.send(command);
return response.get();
return toApiJsonSerializer.serialize(result);
}
}
@@ -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<String> 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();
}
}
@@ -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<PaymentTypeData> 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<PaymentTypeCreateResponse> 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<PaymentTypeUpdateResponse> 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<PaymentTypeDeleteResponse> response = commandPipeline.send(command);
return response.get();
return jsonSerializer.serialize(result);
}
}
@@ -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<PaymentTypeCreateRequest, PaymentTypeCreateResponse> {
@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<PaymentTypeCreateRequest> command) {
return paymentTypeWriteService.createPaymentType(command.getPayload());
@Autowired
public PaymentTypeCreateCommandHandler(final PaymentTypeWriteService paymentTypeWriteService) {
this.paymentTypeWriteService = paymentTypeWriteService;
}
@Transactional
@Override
public PaymentTypeCreateResponse fallback(Command<PaymentTypeCreateRequest> 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();
}
}
@@ -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<PaymentTypeDeleteRequest, PaymentTypeDeleteResponse> {
@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<PaymentTypeDeleteRequest> command) {
return paymentTypeWriteService.deletePaymentType(command.getPayload());
@Autowired
public PaymentTypeDeleteCommandHandler(final PaymentTypeWriteService paymentTypeWriteService) {
this.paymentTypeWriteService = paymentTypeWriteService;
}
@Transactional
@Override
public PaymentTypeDeleteResponse fallback(Command<PaymentTypeDeleteRequest> 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();
}
}
@@ -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<PaymentTypeUpdateRequest, PaymentTypeUpdateResponse> {
@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<PaymentTypeUpdateRequest> command) {
return paymentTypeWriteService.updatePaymentType(command.getPayload());
@Autowired
public PaymentTypeUpdateCommandHandler(final PaymentTypeWriteService paymentTypeWriteService) {
this.paymentTypeWriteService = paymentTypeWriteService;
}
@Transactional
@Override
public PaymentTypeUpdateResponse fallback(Command<PaymentTypeUpdateRequest> 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();
}
}
@@ -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()
@@ -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<WorkingDaysUpdateRequest> 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<WorkingDaysUpdateResponse> response = commandPipeline.send(command);
return response.get();
return toApiJsonSerializer.serialize(result);
}
@GET
@@ -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<WorkingDaysUpdateRequest, WorkingDaysUpdateResponse> {
@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<WorkingDaysUpdateRequest> command) {
WorkingDaysUpdateRequest request = command.getPayload();
Map<String, Object> 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<String, Object> changes = this.workingDaysWritePlatformService.updateWorkingDays(request);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.with(changes) //
.build();
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -26,4 +26,6 @@ public interface FundWritePlatformService {
CommandProcessingResult createFund(JsonCommand command);
CommandProcessingResult updateFund(Long fundId, JsonCommand command);
CommandProcessingResult deleteFund(Long fundId, JsonCommand command);
}
@@ -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.
*/
@@ -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);
}
}
@@ -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() {
@@ -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<String, Object> toResult(final Map<String, Object> row) {
final Map<String, Object> out = new LinkedHashMap<>();
out.put("bankName", row.get("bank_name"));
@@ -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();
}
}
@@ -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();
}
}
@@ -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);
}
@@ -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();
}
}
@@ -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);
}
@@ -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);
}
}
@@ -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<Map<String, Object>> 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<String, Object> 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);
});
}
@@ -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
@@ -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();
}
}
@@ -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();
}
}
@@ -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();
}
}
@@ -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);
}
@@ -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<Map<String, Object>> 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<String, Object> 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.");
}
}
}
@@ -97,15 +97,34 @@ 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'";
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).
*/
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 + ")"
+ " order by p.grouping, coalesce(entity_name, ''), p.code";
}
@@ -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<String, Object> originalValues = data.getOriginalValues();
final Map<String, Object> 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<String, Object> 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<String> resolveRoleNames(final List<String> roleIds) {
final List<String> 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<String> sortNames(final List<String> names) {
final List<String> sorted = new ArrayList<>(names);
sorted.sort(String.CASE_INSENSITIVE_ORDER);
return sorted;
}
private static List<String> toStringList(final Object value) {
final List<String> 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<UserPendingRequestData> {
@@ -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<String> roleNames = new HashSet<>();
final Set<Long> roleIds = new HashSet<>();
if (userToUpdate.getRoles() != null) {
@@ -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}
@@ -116,4 +116,6 @@
<include file="parts/0568_add_settlement_mode_to_clerk_share_withdrawal_request.xml" relativeToChangelogFile="true"/>
<include file="parts/0569_aml_alert_release_stale_under_review.xml" relativeToChangelogFile="true"/>
<include file="parts/0570_add_dd_payable_gl_to_sponsor_bank.xml" relativeToChangelogFile="true"/>
<include file="parts/0571_add_sponsor_dd_gl_mapping_permission.xml" relativeToChangelogFile="true"/>
<include file="parts/0572_add_sponsor_bank_update_permission.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!-- No permission code existed at all for the Sponsor DD GST/GL settings mapping before this -
the endpoint (POST /sponsordd/gl-mapping) was reachable by any authenticated user with no
permission check anywhere in the request path. This adds the one permission the endpoint now
requires, now that it is routed through the generic Maker-Checker command pipeline
(UpdateSponsorDdGlMappingCommandHandler). One code, not a CREATE/UPDATE pair: the underlying
write is a genuine upsert (m_sponsor_dd_gl_mapping is always a single row), so there is no
separate create step to give its own code to. -->
<changeSet id="0571_add_sponsor_dd_gl_mapping_permission" author="cbs-anshul">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'UPDATE_SPONSORDDGLMAPPING'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="UPDATE_SPONSORDDGLMAPPING"/>
<column name="entity_name" value="SPONSORDDGLMAPPING"/>
<column name="action_name" value="UPDATE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
<!-- Granted to the same roles the sibling org-setup permissions (0511) grant to - Super user and
any manager-named role - since configuring the sponsor DD GL/GST mapping is a manager/admin
act, not a teller or cashier one. -->
<changeSet id="0571_grant_sponsor_dd_gl_mapping_permission" author="cbs-anshul">
<sql>
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
)
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!-- CREATE_SPONSORBANK and READ_SPONSORBANK already exist (seeded outside this changelog history -
see the drift noted when CREATE_SPONSORBANK was wired for Maker-Checker), but no UPDATE
permission for this entity has ever existed, so PUT /sponsorbanks/{id} had no permission code
to check anywhere in its request path. This adds it, seeded consistently with the existing
Sponsor Bank permissions (same grouping/entity_name, can_maker_checker off by default - an
administrator opts in via Configure Maker-Checker Tasks, same as CREATE_SPONSORBANK was). -->
<changeSet id="0572_add_sponsor_bank_update_permission" author="cbs-anshul">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'UPDATE_SPONSORBANK'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="UPDATE_SPONSORBANK"/>
<column name="entity_name" value="SPONSORBANK"/>
<column name="action_name" value="UPDATE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
<!-- Granted to every role that already holds CREATE_SPONSORBANK, so whoever could create a sponsor
bank before this change can also still edit one after it - no role loses access it already
had. -->
<changeSet id="0572_grant_sponsor_bank_update_permission" author="cbs-anshul">
<sql>
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
)
</sql>
</changeSet>
</databaseChangeLog>