Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7eabaf2542 | ||
|
|
9beae3df28 | ||
|
|
d2e13cc7b9 | ||
|
|
ecfe8aa76a | ||
|
|
a7cb09f15c | ||
|
|
26d4c241c7 | ||
|
|
dbc2737ada | ||
|
|
f457b9b8c7 | ||
|
|
23ee4f1063 | ||
|
|
bc38216ca3 |
+79
@@ -756,6 +756,14 @@ public class CommandWrapperBuilder {
|
|||||||
return this;
|
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() {
|
public CommandWrapperBuilder createReport() {
|
||||||
this.actionName = "CREATE";
|
this.actionName = "CREATE";
|
||||||
this.entityName = "REPORT";
|
this.entityName = "REPORT";
|
||||||
@@ -884,6 +892,70 @@ public class CommandWrapperBuilder {
|
|||||||
return this;
|
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() {
|
public CommandWrapperBuilder createLoanProduct() {
|
||||||
this.actionName = "CREATE";
|
this.actionName = "CREATE";
|
||||||
this.entityName = "LOANPRODUCT";
|
this.entityName = "LOANPRODUCT";
|
||||||
@@ -3390,6 +3462,13 @@ public class CommandWrapperBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder updateCurrency() {
|
||||||
|
this.actionName = "UPDATE";
|
||||||
|
this.entityName = "CURRENCY";
|
||||||
|
this.href = "/currencies";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public CommandWrapperBuilder updateExternalServiceProperties(final String externalServiceName) {
|
public CommandWrapperBuilder updateExternalServiceProperties(final String externalServiceName) {
|
||||||
this.actionName = "UPDATE";
|
this.actionName = "UPDATE";
|
||||||
this.entityName = "EXTERNALSERVICES";
|
this.entityName = "EXTERNALSERVICES";
|
||||||
|
|||||||
+12
-11
@@ -27,13 +27,14 @@ import jakarta.ws.rs.PUT;
|
|||||||
import jakarta.ws.rs.Path;
|
import jakarta.ws.rs.Path;
|
||||||
import jakarta.ws.rs.Produces;
|
import jakarta.ws.rs.Produces;
|
||||||
import jakarta.ws.rs.core.MediaType;
|
import jakarta.ws.rs.core.MediaType;
|
||||||
import java.util.function.Supplier;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.fineract.command.core.CommandPipeline;
|
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||||
import org.apache.fineract.organisation.monetary.command.CurrencyUpdateCommand;
|
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.CurrencyConfigurationData;
|
||||||
import org.apache.fineract.organisation.monetary.data.CurrencyUpdateRequest;
|
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.apache.fineract.organisation.monetary.service.OrganisationCurrencyReadPlatformService;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@@ -44,7 +45,8 @@ import org.springframework.stereotype.Component;
|
|||||||
public class CurrenciesApiResource {
|
public class CurrenciesApiResource {
|
||||||
|
|
||||||
private final OrganisationCurrencyReadPlatformService readPlatformService;
|
private final OrganisationCurrencyReadPlatformService readPlatformService;
|
||||||
private final CommandPipeline commandPipeline;
|
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||||
|
private final DefaultToApiJsonSerializer<CurrencyUpdateRequest> toApiJsonSerializer;
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
@@ -65,13 +67,12 @@ public class CurrenciesApiResource {
|
|||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
@Operation(summary = "Update Currency Configuration", description = "Updates the list of currencies permitted for use.")
|
@Operation(summary = "Update Currency Configuration", description = "Updates the list of currencies permitted for use.")
|
||||||
public CurrencyUpdateResponse updateCurrencies(@Valid CurrencyUpdateRequest request) {
|
public String updateCurrencies(@Valid final CurrencyUpdateRequest request) {
|
||||||
final var command = new CurrencyUpdateCommand();
|
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 toApiJsonSerializer.serialize(result);
|
||||||
|
|
||||||
return response.get();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+63
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
-29
@@ -31,20 +31,15 @@ import jakarta.ws.rs.Produces;
|
|||||||
import jakarta.ws.rs.QueryParam;
|
import jakarta.ws.rs.QueryParam;
|
||||||
import jakarta.ws.rs.core.MediaType;
|
import jakarta.ws.rs.core.MediaType;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.Supplier;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
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.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.PaymentTypeCreateRequest;
|
||||||
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeCreateResponse;
|
|
||||||
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData;
|
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.PaymentTypeUpdateRequest;
|
||||||
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeUpdateResponse;
|
|
||||||
import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadService;
|
import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadService;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@@ -55,7 +50,7 @@ import org.springframework.stereotype.Component;
|
|||||||
public class PaymentTypeApiResource {
|
public class PaymentTypeApiResource {
|
||||||
|
|
||||||
private final PaymentTypeReadService readPlatformService;
|
private final PaymentTypeReadService readPlatformService;
|
||||||
private final CommandPipeline commandPipeline;
|
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||||
private final DefaultToApiJsonSerializer<PaymentTypeData> jsonSerializer;
|
private final DefaultToApiJsonSerializer<PaymentTypeData> jsonSerializer;
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@@ -79,14 +74,13 @@ public class PaymentTypeApiResource {
|
|||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
@Operation(summary = "Create a Payment Type", description = "Creates a new Payment type")
|
@Operation(summary = "Create a Payment Type", description = "Creates a new Payment type")
|
||||||
public PaymentTypeCreateResponse createPaymentType(PaymentTypeCreateRequest request) {
|
public String createPaymentType(final PaymentTypeCreateRequest request) {
|
||||||
final var command = new PaymentTypeCreateCommand();
|
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 jsonSerializer.serialize(result);
|
||||||
|
|
||||||
return response.get();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PUT
|
@PUT
|
||||||
@@ -94,18 +88,16 @@ public class PaymentTypeApiResource {
|
|||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
@Operation(summary = "Update a Payment Type", description = "Updates a Payment Type")
|
@Operation(summary = "Update a Payment Type", description = "Updates a Payment Type")
|
||||||
public PaymentTypeUpdateResponse updatePaymentType(@PathParam("paymentTypeId") final Long paymentTypeId,
|
public String updatePaymentType(@PathParam("paymentTypeId") final Long paymentTypeId, final PaymentTypeUpdateRequest request) {
|
||||||
final PaymentTypeUpdateRequest request) {
|
|
||||||
|
|
||||||
request.setId(paymentTypeId);
|
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 jsonSerializer.serialize(result);
|
||||||
|
|
||||||
return response.get();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@DELETE
|
@DELETE
|
||||||
@@ -113,14 +105,12 @@ public class PaymentTypeApiResource {
|
|||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
@Operation(summary = "Delete a Payment Type", description = "Deletes payment type")
|
@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 jsonSerializer.serialize(result);
|
||||||
|
|
||||||
return response.get();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-17
@@ -18,33 +18,50 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.portfolio.paymenttype.handler;
|
package org.apache.fineract.portfolio.paymenttype.handler;
|
||||||
|
|
||||||
import io.github.resilience4j.retry.annotation.Retry;
|
import org.apache.fineract.commands.annotation.CommandType;
|
||||||
import lombok.RequiredArgsConstructor;
|
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
import org.apache.fineract.command.core.Command;
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
import org.apache.fineract.command.core.CommandHandler;
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||||
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeCreateRequest;
|
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeCreateRequest;
|
||||||
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeCreateResponse;
|
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeCreateResponse;
|
||||||
import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeWriteService;
|
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;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@Slf4j
|
@Service
|
||||||
@Component
|
@CommandType(entity = "PAYMENTTYPE", action = "CREATE")
|
||||||
@RequiredArgsConstructor
|
public class PaymentTypeCreateCommandHandler implements NewCommandSourceHandler {
|
||||||
public class PaymentTypeCreateCommandHandler implements CommandHandler<PaymentTypeCreateRequest, PaymentTypeCreateResponse> {
|
|
||||||
|
|
||||||
private final PaymentTypeWriteService paymentTypeWriteService;
|
private final PaymentTypeWriteService paymentTypeWriteService;
|
||||||
|
|
||||||
@Retry(name = "commandPaymentTypeCreate", fallbackMethod = "fallback")
|
@Autowired
|
||||||
@Override
|
public PaymentTypeCreateCommandHandler(final PaymentTypeWriteService paymentTypeWriteService) {
|
||||||
@Transactional
|
this.paymentTypeWriteService = paymentTypeWriteService;
|
||||||
public PaymentTypeCreateResponse handle(Command<PaymentTypeCreateRequest> command) {
|
|
||||||
return paymentTypeWriteService.createPaymentType(command.getPayload());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
public PaymentTypeCreateResponse fallback(Command<PaymentTypeCreateRequest> command, Throwable t) {
|
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||||
return CommandHandler.super.fallback(command, t);
|
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-17
@@ -18,33 +18,41 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.portfolio.paymenttype.handler;
|
package org.apache.fineract.portfolio.paymenttype.handler;
|
||||||
|
|
||||||
import io.github.resilience4j.retry.annotation.Retry;
|
import org.apache.fineract.commands.annotation.CommandType;
|
||||||
import lombok.RequiredArgsConstructor;
|
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
import org.apache.fineract.command.core.Command;
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
import org.apache.fineract.command.core.CommandHandler;
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||||
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeDeleteRequest;
|
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeDeleteRequest;
|
||||||
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeDeleteResponse;
|
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeDeleteResponse;
|
||||||
import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeWriteService;
|
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;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@Slf4j
|
@Service
|
||||||
@Component
|
@CommandType(entity = "PAYMENTTYPE", action = "DELETE")
|
||||||
@RequiredArgsConstructor
|
public class PaymentTypeDeleteCommandHandler implements NewCommandSourceHandler {
|
||||||
public class PaymentTypeDeleteCommandHandler implements CommandHandler<PaymentTypeDeleteRequest, PaymentTypeDeleteResponse> {
|
|
||||||
|
|
||||||
private final PaymentTypeWriteService paymentTypeWriteService;
|
private final PaymentTypeWriteService paymentTypeWriteService;
|
||||||
|
|
||||||
@Retry(name = "commandPaymentTypeDelete", fallbackMethod = "fallback")
|
@Autowired
|
||||||
@Override
|
public PaymentTypeDeleteCommandHandler(final PaymentTypeWriteService paymentTypeWriteService) {
|
||||||
@Transactional
|
this.paymentTypeWriteService = paymentTypeWriteService;
|
||||||
public PaymentTypeDeleteResponse handle(Command<PaymentTypeDeleteRequest> command) {
|
|
||||||
return paymentTypeWriteService.deletePaymentType(command.getPayload());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
public PaymentTypeDeleteResponse fallback(Command<PaymentTypeDeleteRequest> command, Throwable t) {
|
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||||
return CommandHandler.super.fallback(command, t);
|
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-17
@@ -18,33 +18,51 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.portfolio.paymenttype.handler;
|
package org.apache.fineract.portfolio.paymenttype.handler;
|
||||||
|
|
||||||
import io.github.resilience4j.retry.annotation.Retry;
|
import org.apache.fineract.commands.annotation.CommandType;
|
||||||
import lombok.RequiredArgsConstructor;
|
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
import org.apache.fineract.command.core.Command;
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
import org.apache.fineract.command.core.CommandHandler;
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||||
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeUpdateRequest;
|
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeUpdateRequest;
|
||||||
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeUpdateResponse;
|
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeUpdateResponse;
|
||||||
import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeWriteService;
|
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;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@Slf4j
|
@Service
|
||||||
@Component
|
@CommandType(entity = "PAYMENTTYPE", action = "UPDATE")
|
||||||
@RequiredArgsConstructor
|
public class PaymentTypeUpdateCommandHandler implements NewCommandSourceHandler {
|
||||||
public class PaymentTypeUpdateCommandHandler implements CommandHandler<PaymentTypeUpdateRequest, PaymentTypeUpdateResponse> {
|
|
||||||
|
|
||||||
private final PaymentTypeWriteService paymentTypeWriteService;
|
private final PaymentTypeWriteService paymentTypeWriteService;
|
||||||
|
|
||||||
@Retry(name = "commandPaymentTypeUpdate", fallbackMethod = "fallback")
|
@Autowired
|
||||||
@Override
|
public PaymentTypeUpdateCommandHandler(final PaymentTypeWriteService paymentTypeWriteService) {
|
||||||
@Transactional
|
this.paymentTypeWriteService = paymentTypeWriteService;
|
||||||
public PaymentTypeUpdateResponse handle(Command<PaymentTypeUpdateRequest> command) {
|
|
||||||
return paymentTypeWriteService.updatePaymentType(command.getPayload());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
public PaymentTypeUpdateResponse fallback(Command<PaymentTypeUpdateRequest> command, Throwable t) {
|
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||||
return CommandHandler.super.fallback(command, t);
|
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-2
@@ -123,6 +123,7 @@ public final class DepositsApiConstants {
|
|||||||
public static final String isSeniorCitizenParamName = "isSeniorCitizen";
|
public static final String isSeniorCitizenParamName = "isSeniorCitizen";
|
||||||
public static final String seniorCitizenInterestRateParamName = "seniorCitizenInterestRate";
|
public static final String seniorCitizenInterestRateParamName = "seniorCitizenInterestRate";
|
||||||
public static final String tenureInMonthsParamName = "tenureInMonths";
|
public static final String tenureInMonthsParamName = "tenureInMonths";
|
||||||
|
public static final String tenureInDaysParamName = "tenureInDays";
|
||||||
public static final String interestCompoundingPeriodInMonthsParamName = "interestCompoundingPeriodInMonths";
|
public static final String interestCompoundingPeriodInMonthsParamName = "interestCompoundingPeriodInMonths";
|
||||||
|
|
||||||
// Preclosure parameters
|
// Preclosure parameters
|
||||||
@@ -142,6 +143,9 @@ public final class DepositsApiConstants {
|
|||||||
public static final String inMultiplesOfDepositTermParamName = "inMultiplesOfDepositTerm";
|
public static final String inMultiplesOfDepositTermParamName = "inMultiplesOfDepositTerm";
|
||||||
public static final String inMultiplesOfDepositTermTypeIdParamName = "inMultiplesOfDepositTermTypeId";
|
public static final String inMultiplesOfDepositTermTypeIdParamName = "inMultiplesOfDepositTermTypeId";
|
||||||
public static final String inMultiplesOfDepositTermType = "inMultiplesOfDepositTermType";
|
public static final String inMultiplesOfDepositTermType = "inMultiplesOfDepositTermType";
|
||||||
|
// Days after maturity a depositor still has to walk in and renew at the original maturity date --
|
||||||
|
// null/0 means this product keeps the instant, same-night auto-renewal every FD product had before.
|
||||||
|
public static final String renewalGracePeriodDaysParamName = "renewalGracePeriodDays";
|
||||||
|
|
||||||
public static final String depositAmountParamName = "depositAmount";
|
public static final String depositAmountParamName = "depositAmount";
|
||||||
public static final String depositMinAmountParamName = "minDepositAmount";
|
public static final String depositMinAmountParamName = "minDepositAmount";
|
||||||
@@ -247,11 +251,13 @@ public final class DepositsApiConstants {
|
|||||||
|
|
||||||
private static final Set<String> DEPOSIT_TERM_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(minDepositTermParamName,
|
private static final Set<String> DEPOSIT_TERM_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(minDepositTermParamName,
|
||||||
maxDepositTermParamName, minDepositTermTypeIdParamName, maxDepositTermTypeIdParamName, inMultiplesOfDepositTermParamName,
|
maxDepositTermParamName, minDepositTermTypeIdParamName, maxDepositTermTypeIdParamName, inMultiplesOfDepositTermParamName,
|
||||||
inMultiplesOfDepositTermTypeIdParamName, depositAmountParamName, depositMinAmountParamName, depositMaxAmountParamName));
|
inMultiplesOfDepositTermTypeIdParamName, depositAmountParamName, depositMinAmountParamName, depositMaxAmountParamName,
|
||||||
|
renewalGracePeriodDaysParamName));
|
||||||
|
|
||||||
private static final Set<String> DEPOSIT_TERM_RESPONSE_DATA_PARAMETERS = new HashSet<>(Arrays.asList(minDepositTermParamName,
|
private static final Set<String> DEPOSIT_TERM_RESPONSE_DATA_PARAMETERS = new HashSet<>(Arrays.asList(minDepositTermParamName,
|
||||||
maxDepositTermParamName, minDepositTermType, maxDepositTermType, inMultiplesOfDepositTermParamName,
|
maxDepositTermParamName, minDepositTermType, maxDepositTermType, inMultiplesOfDepositTermParamName,
|
||||||
inMultiplesOfDepositTermType, depositAmountParamName, depositMinAmountParamName, depositMaxAmountParamName));
|
inMultiplesOfDepositTermType, depositAmountParamName, depositMinAmountParamName, depositMaxAmountParamName,
|
||||||
|
renewalGracePeriodDaysParamName));
|
||||||
|
|
||||||
private static final Set<String> RECURRING_DETAILS_REQUEST_DATA_PARAMETERS = new HashSet<>(
|
private static final Set<String> RECURRING_DETAILS_REQUEST_DATA_PARAMETERS = new HashSet<>(
|
||||||
Arrays.asList(mandatoryRecommendedDepositAmountParamName, isMandatoryDepositParamName, allowWithdrawalParamName,
|
Arrays.asList(mandatoryRecommendedDepositAmountParamName, isMandatoryDepositParamName, allowWithdrawalParamName,
|
||||||
@@ -511,6 +517,7 @@ public final class DepositsApiConstants {
|
|||||||
fixedDepositInterestCalculationData.add(principalAmountParamName);
|
fixedDepositInterestCalculationData.add(principalAmountParamName);
|
||||||
fixedDepositInterestCalculationData.add(annualInterestRateParamName);
|
fixedDepositInterestCalculationData.add(annualInterestRateParamName);
|
||||||
fixedDepositInterestCalculationData.add(tenureInMonthsParamName);
|
fixedDepositInterestCalculationData.add(tenureInMonthsParamName);
|
||||||
|
fixedDepositInterestCalculationData.add(tenureInDaysParamName);
|
||||||
fixedDepositInterestCalculationData.add(interestPostingPeriodInMonthsParamName);
|
fixedDepositInterestCalculationData.add(interestPostingPeriodInMonthsParamName);
|
||||||
fixedDepositInterestCalculationData.add(interestCompoundingPeriodInMonthsParamName);
|
fixedDepositInterestCalculationData.add(interestCompoundingPeriodInMonthsParamName);
|
||||||
// Exact calendar-based calculation params
|
// Exact calendar-based calculation params
|
||||||
|
|||||||
+44
-5
@@ -222,7 +222,10 @@ public class SecurityConfig {
|
|||||||
// currency
|
// currency
|
||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/currencies"))
|
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/currencies"))
|
||||||
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_CURRENCY")
|
.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")
|
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "UPDATE_CURRENCY")
|
||||||
// notes: read
|
// notes: read
|
||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/clients/*/notes"))
|
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/clients/*/notes"))
|
||||||
@@ -355,11 +358,18 @@ public class SecurityConfig {
|
|||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/import/*"))
|
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/import/*"))
|
||||||
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_IMPORT")
|
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_IMPORT")
|
||||||
// payment type
|
// 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/*"))
|
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/paymenttypes/*"))
|
||||||
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_PAYMENTTYPE")
|
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_READ, "READ_PAYMENTTYPE")
|
||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.POST, "/api/*/paymenttypes"))
|
.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")
|
.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")
|
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_PAYMENTTYPE")
|
||||||
// mix: taxonomy
|
// mix: taxonomy
|
||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/mixtaxonomy/*"))
|
.requestMatchers(API_MATCHER.matcher(HttpMethod.GET, "/api/*/mixtaxonomy/*"))
|
||||||
@@ -390,12 +400,41 @@ public class SecurityConfig {
|
|||||||
.requestMatchers(API_MATCHER.matcher(HttpMethod.DELETE, "/api/*/mixreport/*"))
|
.requestMatchers(API_MATCHER.matcher(HttpMethod.DELETE, "/api/*/mixreport/*"))
|
||||||
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_MIX_REPORT")
|
.hasAnyAuthority(ALL_FUNCTIONS, ALL_FUNCTIONS_WRITE, "DELETE_MIX_REPORT")
|
||||||
// working days
|
// 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"))
|
.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"))
|
.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"))
|
.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(HttpMethod.POST, "/api/*/twofactor/validate")).fullyAuthenticated()
|
||||||
.requestMatchers(API_MATCHER.matcher("/api/*/twofactor")).fullyAuthenticated()
|
.requestMatchers(API_MATCHER.matcher("/api/*/twofactor")).fullyAuthenticated()
|
||||||
|
|||||||
+12
-15
@@ -27,15 +27,15 @@ import jakarta.ws.rs.PUT;
|
|||||||
import jakarta.ws.rs.Path;
|
import jakarta.ws.rs.Path;
|
||||||
import jakarta.ws.rs.Produces;
|
import jakarta.ws.rs.Produces;
|
||||||
import jakarta.ws.rs.core.MediaType;
|
import jakarta.ws.rs.core.MediaType;
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.fineract.command.core.CommandPipeline;
|
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||||
import org.apache.fineract.organisation.workingdays.command.WorkingDaysUpdateCommand;
|
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.WorkingDaysData;
|
||||||
import org.apache.fineract.organisation.workingdays.data.WorkingDaysUpdateRequest;
|
import org.apache.fineract.organisation.workingdays.data.WorkingDaysUpdateRequest;
|
||||||
import org.apache.fineract.organisation.workingdays.data.WorkingDaysUpdateRequestValidator;
|
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.apache.fineract.organisation.workingdays.service.WorkingDaysReadPlatformService;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@@ -50,7 +50,8 @@ public class WorkingDaysApiResource {
|
|||||||
|
|
||||||
private final WorkingDaysReadPlatformService workingDaysReadPlatformService;
|
private final WorkingDaysReadPlatformService workingDaysReadPlatformService;
|
||||||
private final WorkingDaysUpdateRequestValidator workingDaysUpdateRequestValidator;
|
private final WorkingDaysUpdateRequestValidator workingDaysUpdateRequestValidator;
|
||||||
private final CommandPipeline commandPipeline;
|
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||||
|
private final DefaultToApiJsonSerializer<WorkingDaysUpdateRequest> toApiJsonSerializer;
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
@@ -65,17 +66,13 @@ public class WorkingDaysApiResource {
|
|||||||
@Produces({ MediaType.APPLICATION_JSON })
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
@Operation(summary = "Update a Working Day", description = "Mandatory Fields\n"
|
@Operation(summary = "Update a Working Day", description = "Mandatory Fields\n"
|
||||||
+ "recurrence,repaymentRescheduleType,extendTermForDailyRepayments,locale")
|
+ "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());
|
return toApiJsonSerializer.serialize(result);
|
||||||
command.setCreatedAt(Instant.now());
|
|
||||||
command.setPayload(request);
|
|
||||||
|
|
||||||
final Supplier<WorkingDaysUpdateResponse> response = commandPipeline.send(command);
|
|
||||||
|
|
||||||
return response.get();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
|
|||||||
+29
-21
@@ -18,38 +18,46 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.organisation.workingdays.handler;
|
package org.apache.fineract.organisation.workingdays.handler;
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import lombok.RequiredArgsConstructor;
|
import org.apache.fineract.commands.annotation.CommandType;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||||
import org.apache.fineract.command.core.Command;
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
import org.apache.fineract.command.core.CommandHandler;
|
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.WorkingDaysUpdateRequest;
|
||||||
import org.apache.fineract.organisation.workingdays.data.WorkingDaysUpdateResponse;
|
|
||||||
import org.apache.fineract.organisation.workingdays.service.WorkingDaysWritePlatformService;
|
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;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@Slf4j
|
@Service
|
||||||
@Component
|
@CommandType(entity = "WORKINGDAYS", action = "UPDATE")
|
||||||
@RequiredArgsConstructor
|
public class UpdateWorkingDaysCommandHandler implements NewCommandSourceHandler {
|
||||||
public class UpdateWorkingDaysCommandHandler implements CommandHandler<WorkingDaysUpdateRequest, WorkingDaysUpdateResponse> {
|
|
||||||
|
|
||||||
private final WorkingDaysWritePlatformService workingDaysWritePlatformService;
|
private final WorkingDaysWritePlatformService workingDaysWritePlatformService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public UpdateWorkingDaysCommandHandler(final WorkingDaysWritePlatformService workingDaysWritePlatformService) {
|
||||||
|
this.workingDaysWritePlatformService = workingDaysWritePlatformService;
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
public WorkingDaysUpdateResponse handle(Command<WorkingDaysUpdateRequest> command) {
|
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||||
WorkingDaysUpdateRequest request = command.getPayload();
|
|
||||||
Map<String, Object> changes = this.workingDaysWritePlatformService.updateWorkingDays(request);
|
|
||||||
if (changes == null) {
|
|
||||||
changes = Collections.emptyMap();
|
|
||||||
}
|
|
||||||
|
|
||||||
return WorkingDaysUpdateResponse.builder().resourceId((Long) changes.get("resourceId")).changes(changes)
|
final WorkingDaysUpdateRequest request = WorkingDaysUpdateRequest.builder()
|
||||||
.recurrence(request.getRecurrence()).repaymentRescheduleType(request.getRepaymentRescheduleType())
|
.recurrence(command.stringValueOfParameterNamed("recurrence"))
|
||||||
.extendTermForDailyRepayments(request.getExtendTermForDailyRepayments())
|
.repaymentRescheduleType(command.integerValueSansLocaleOfParameterNamed("repaymentRescheduleType"))
|
||||||
.extendTermForRepaymentsOnHolidays(request.getExtendTermForRepaymentsOnHolidays()).build();
|
.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();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+97
-6
@@ -418,13 +418,20 @@ public class CashierDepositRequestApiResource {
|
|||||||
txnId = executeGlJournalEntry(glAccountId, contraGlId, glOfficeId, currencyCode,
|
txnId = executeGlJournalEntry(glAccountId, contraGlId, glOfficeId, currencyCode,
|
||||||
amount, ptId, txnDateStr, description, true);
|
amount, ptId, txnDateStr, description, true);
|
||||||
} else {
|
} else {
|
||||||
// Determine real account type by checking which table owns this ID.
|
// Determine real account type by checking which table/column owns this ID.
|
||||||
// Do NOT rely solely on the stored account_type field — old records may have 'SAVINGS'
|
// Do NOT rely solely on the stored account_type field — old records may have 'SAVINGS'
|
||||||
// even for current accounts (submitted before the frontend fix).
|
// even for current accounts (submitted before the frontend fix), and FD/RD accounts share
|
||||||
final boolean isCurrentAccount = isCurrentAccountById(savingsAccountId);
|
// the very same m_savings_account table as a plain savings account, distinguished only by
|
||||||
txnId = isCurrentAccount
|
// deposit_type_enum -- so this is looked up fresh rather than trusted from the request.
|
||||||
? executeCurrentAccountDeposit(savingsAccountId, amount, ptId, txnDateStr, description)
|
final String depositCategory = resolveDepositAccountCategory(savingsAccountId);
|
||||||
: executeSavingsDeposit(savingsAccountId, amount, ptId, txnDateStr, description);
|
final boolean isCurrentAccount = "CURRENT".equals(depositCategory);
|
||||||
|
txnId = switch (depositCategory) {
|
||||||
|
case "CURRENT" -> executeCurrentAccountDeposit(savingsAccountId, amount, ptId, txnDateStr, description);
|
||||||
|
case "FIXED_DEPOSIT" -> executeFixedDepositDeposit(savingsAccountId, amount, ptId, txnDateStr, description);
|
||||||
|
case "RECURRING_DEPOSIT" ->
|
||||||
|
executeRecurringDepositDeposit(savingsAccountId, amount, ptId, txnDateStr, description);
|
||||||
|
default -> executeSavingsDeposit(savingsAccountId, amount, ptId, txnDateStr, description);
|
||||||
|
};
|
||||||
if (!isCurrentAccount && txnId != null) {
|
if (!isCurrentAccount && txnId != null) {
|
||||||
// approve() is one @Transactional block, so an uncaught exception here would roll back
|
// approve() is one @Transactional block, so an uncaught exception here would roll back
|
||||||
// the deposit that was just posted along with it -- every Cheque/DD approval that ever
|
// the deposit that was just posted along with it -- every Cheque/DD approval that ever
|
||||||
@@ -718,6 +725,64 @@ public class CashierDepositRequestApiResource {
|
|||||||
return result.getResourceId() != null ? result.getResourceId().toString() : null;
|
return result.getResourceId() != null ? result.getResourceId().toString() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String executeFixedDepositDeposit(
|
||||||
|
final Long accountId, final BigDecimal amount,
|
||||||
|
final Object paymentTypeId, final String txnDateStr, final String note) {
|
||||||
|
|
||||||
|
final String today = LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||||
|
final String date = txnDateStr != null && !txnDateStr.isBlank() ? formatForFineract(txnDateStr) : today;
|
||||||
|
|
||||||
|
final JsonObject payload = new JsonObject();
|
||||||
|
payload.addProperty("locale", "en");
|
||||||
|
payload.addProperty("dateFormat", "dd MMMM yyyy");
|
||||||
|
payload.addProperty("transactionDate", date);
|
||||||
|
payload.addProperty("transactionAmount", amount);
|
||||||
|
if (paymentTypeId != null) {
|
||||||
|
payload.addProperty("paymentTypeId", ((Number) paymentTypeId).longValue());
|
||||||
|
}
|
||||||
|
if (note != null && !note.isBlank()) {
|
||||||
|
payload.addProperty("note", note);
|
||||||
|
}
|
||||||
|
|
||||||
|
final CommandWrapper commandRequest = new CommandWrapperBuilder()
|
||||||
|
.fixedDepositAccountDeposit(accountId)
|
||||||
|
.withJson(payload.toString())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
final CommandProcessingResult result = voucherService
|
||||||
|
.withOwnVoucher(() -> commandsSourceWritePlatformService.logCommandSource(commandRequest));
|
||||||
|
return result.getResourceId() != null ? result.getResourceId().toString() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String executeRecurringDepositDeposit(
|
||||||
|
final Long accountId, final BigDecimal amount,
|
||||||
|
final Object paymentTypeId, final String txnDateStr, final String note) {
|
||||||
|
|
||||||
|
final String today = LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||||
|
final String date = txnDateStr != null && !txnDateStr.isBlank() ? formatForFineract(txnDateStr) : today;
|
||||||
|
|
||||||
|
final JsonObject payload = new JsonObject();
|
||||||
|
payload.addProperty("locale", "en");
|
||||||
|
payload.addProperty("dateFormat", "dd MMMM yyyy");
|
||||||
|
payload.addProperty("transactionDate", date);
|
||||||
|
payload.addProperty("transactionAmount", amount);
|
||||||
|
if (paymentTypeId != null) {
|
||||||
|
payload.addProperty("paymentTypeId", ((Number) paymentTypeId).longValue());
|
||||||
|
}
|
||||||
|
if (note != null && !note.isBlank()) {
|
||||||
|
payload.addProperty("note", note);
|
||||||
|
}
|
||||||
|
|
||||||
|
final CommandWrapper commandRequest = new CommandWrapperBuilder()
|
||||||
|
.recurringAccountDeposit(accountId)
|
||||||
|
.withJson(payload.toString())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
final CommandProcessingResult result = voucherService
|
||||||
|
.withOwnVoucher(() -> commandsSourceWritePlatformService.logCommandSource(commandRequest));
|
||||||
|
return result.getResourceId() != null ? result.getResourceId().toString() : null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Cheque/DD deposit is credited to the ledger immediately (same as cash) but is not really the
|
* A Cheque/DD deposit is credited to the ledger immediately (same as cash) but is not really the
|
||||||
* branch's own money yet -- it still has to clear. Real-world CBS practice is to hold that amount
|
* branch's own money yet -- it still has to clear. Real-world CBS practice is to hold that amount
|
||||||
@@ -809,6 +874,32 @@ public class CashierDepositRequestApiResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "CURRENT", "FIXED_DEPOSIT", "RECURRING_DEPOSIT" or "SAVINGS" — checked against the database
|
||||||
|
* rather than the request's own account_type string. FD and RD accounts live in the same
|
||||||
|
* m_savings_account table as a plain savings account (deposit_type_enum 200/300 vs 100), so a
|
||||||
|
* plain "not current → savings" check would post an FD/RD deposit through the generic savings
|
||||||
|
* deposit command, which does not accept a term-deposit account id.
|
||||||
|
*/
|
||||||
|
private String resolveDepositAccountCategory(final Long accountId) {
|
||||||
|
if (accountId == null) {
|
||||||
|
return "SAVINGS";
|
||||||
|
}
|
||||||
|
if (isCurrentAccountById(accountId)) {
|
||||||
|
return "CURRENT";
|
||||||
|
}
|
||||||
|
final List<Integer> depositTypes = jdbcTemplate.queryForList(
|
||||||
|
"SELECT deposit_type_enum FROM m_savings_account WHERE id = ?", Integer.class, accountId);
|
||||||
|
final Integer depositType = depositTypes.isEmpty() ? null : depositTypes.get(0);
|
||||||
|
if (Integer.valueOf(200).equals(depositType)) {
|
||||||
|
return "FIXED_DEPOSIT";
|
||||||
|
}
|
||||||
|
if (Integer.valueOf(300).equals(depositType)) {
|
||||||
|
return "RECURRING_DEPOSIT";
|
||||||
|
}
|
||||||
|
return "SAVINGS";
|
||||||
|
}
|
||||||
|
|
||||||
// Parses a Fineract transaction id string to Long.
|
// Parses a Fineract transaction id string to Long.
|
||||||
// Savings/current txn ids are decimal; GL journal entry ids are hex strings.
|
// Savings/current txn ids are decimal; GL journal entry ids are hex strings.
|
||||||
private Long parseTxnIdToLong(final String txnId) {
|
private Long parseTxnIdToLong(final String txnId) {
|
||||||
|
|||||||
+54
@@ -177,6 +177,15 @@ public class ClerkDisbursementRequestApiResource {
|
|||||||
kycRestrictionGuard.check(entry.get("savingsAccountId").getAsLong(),
|
kycRestrictionGuard.check(entry.get("savingsAccountId").getAsLong(),
|
||||||
"Dr".equals(dbCr) ? KycRestrictionGuard.Operation.TRANSFER : KycRestrictionGuard.Operation.CREDIT);
|
"Dr".equals(dbCr) ? KycRestrictionGuard.Operation.TRANSFER : KycRestrictionGuard.Operation.CREDIT);
|
||||||
}
|
}
|
||||||
|
// A Dr leg against a savings account is a withdrawal, and the clerk who raises it should
|
||||||
|
// not learn the account can't pay out until a manager tries to approve it and the whole
|
||||||
|
// request bounces back generic and reversed. Every reason SavingsAccount.withdraw() would
|
||||||
|
// refuse this later -- inactive, closed, or still inside its lock-in period -- is checked
|
||||||
|
// here too, against the same columns, so the clerk sees the real reason at entry time.
|
||||||
|
if ("Dr".equals(dbCr) && "SAVINGS".equals(entryAccountType)
|
||||||
|
&& entry.has("savingsAccountId") && !entry.get("savingsAccountId").isJsonNull()) {
|
||||||
|
validateSavingsWithdrawalEligible(entry.get("savingsAccountId").getAsLong(), entryAmt);
|
||||||
|
}
|
||||||
if ("Dr".equals(dbCr)) {
|
if ("Dr".equals(dbCr)) {
|
||||||
totalDr = totalDr.add(entryAmt);
|
totalDr = totalDr.add(entryAmt);
|
||||||
} else {
|
} else {
|
||||||
@@ -1179,6 +1188,51 @@ public class ClerkDisbursementRequestApiResource {
|
|||||||
.findFirst().orElse(null);
|
.findFirst().orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Savings status_enum for an account still open for business (Fineract's SavingsAccountStatusType.ACTIVE). */
|
||||||
|
private static final int SAVINGS_STATUS_ACTIVE = 300;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The same refusals {@code SavingsAccount.withdraw()} raises at approval time -- account not
|
||||||
|
* active, or still inside its lock-in period -- checked here against the same columns, so the
|
||||||
|
* clerk sees the real reason when the request is raised rather than a generic failure once a
|
||||||
|
* manager tries to approve it. Balance is checked too, against the account's own minimum, since
|
||||||
|
* an entry that can never post is no more usable sitting in the approval queue than one refused
|
||||||
|
* outright.
|
||||||
|
*/
|
||||||
|
private void validateSavingsWithdrawalEligible(final Long savingsAccountId, final BigDecimal amount) {
|
||||||
|
final List<Map<String, Object>> rows = jdbcTemplate.queryForList("""
|
||||||
|
SELECT account_no, status_enum, lockedin_until_date_derived, account_balance_derived,
|
||||||
|
min_required_balance, enforce_min_required_balance
|
||||||
|
FROM m_savings_account WHERE id = ?
|
||||||
|
""", savingsAccountId);
|
||||||
|
if (rows.isEmpty()) {
|
||||||
|
return; // not found here is not this check's job -- the id/type mismatch surfaces elsewhere
|
||||||
|
}
|
||||||
|
final Map<String, Object> row = rows.get(0);
|
||||||
|
final String accountNo = (String) row.get("account_no");
|
||||||
|
final Number statusEnum = (Number) row.get("status_enum");
|
||||||
|
if (statusEnum == null || statusEnum.intValue() != SAVINGS_STATUS_ACTIVE) {
|
||||||
|
throw apiError(422, "Withdrawal not allowed for savings account " + accountNo + ": account is not active.");
|
||||||
|
}
|
||||||
|
final java.sql.Date lockedUntil = (java.sql.Date) row.get("lockedin_until_date_derived");
|
||||||
|
if (lockedUntil != null) {
|
||||||
|
final LocalDate today = LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
|
||||||
|
final LocalDate lockedUntilDate = lockedUntil.toLocalDate();
|
||||||
|
if (today.isBefore(lockedUntilDate)) {
|
||||||
|
throw apiError(422, "Withdrawal not allowed for savings account " + accountNo
|
||||||
|
+ ": account is locked-in until " + lockedUntilDate + ".");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final BigDecimal balance = (BigDecimal) row.get("account_balance_derived");
|
||||||
|
final boolean enforceMin = Boolean.TRUE.equals(row.get("enforce_min_required_balance"));
|
||||||
|
final BigDecimal minRequired = enforceMin && row.get("min_required_balance") != null
|
||||||
|
? (BigDecimal) row.get("min_required_balance") : BigDecimal.ZERO;
|
||||||
|
if (balance != null && balance.subtract(amount).compareTo(minRequired) < 0) {
|
||||||
|
throw apiError(422, "Withdrawal not allowed for savings account " + accountNo + ": available balance ("
|
||||||
|
+ balance.subtract(minRequired) + ") is less than the requested amount (" + amount + ").");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void addDisbursementVoucherLines(final Map<?, ?> record, final JsonArray creditEntries, final Long sourceGlId,
|
private void addDisbursementVoucherLines(final Map<?, ?> record, final JsonArray creditEntries, final Long sourceGlId,
|
||||||
final String requestType) {
|
final String requestType) {
|
||||||
|
|
||||||
|
|||||||
+13
@@ -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.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.ws.rs.Consumes;
|
import jakarta.ws.rs.Consumes;
|
||||||
|
import jakarta.ws.rs.DELETE;
|
||||||
import jakarta.ws.rs.GET;
|
import jakarta.ws.rs.GET;
|
||||||
import jakarta.ws.rs.POST;
|
import jakarta.ws.rs.POST;
|
||||||
import jakarta.ws.rs.PUT;
|
import jakarta.ws.rs.PUT;
|
||||||
@@ -114,4 +115,16 @@ public class FundsApiResource {
|
|||||||
final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||||
return toApiJsonSerializer.serialize(result);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
@@ -55,4 +55,13 @@ final class FundsApiResourceSwagger {
|
|||||||
public Long resourceId;
|
public Long resourceId;
|
||||||
public PutFundsFundIdRequest changes;
|
public PutFundsFundIdRequest changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Schema(description = "DeleteFundsFundIdResponse")
|
||||||
|
public static final class DeleteFundsFundIdResponse {
|
||||||
|
|
||||||
|
private DeleteFundsFundIdResponse() {}
|
||||||
|
|
||||||
|
@Schema(example = "1")
|
||||||
|
public Long resourceId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+47
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
@@ -26,4 +26,6 @@ public interface FundWritePlatformService {
|
|||||||
CommandProcessingResult createFund(JsonCommand command);
|
CommandProcessingResult createFund(JsonCommand command);
|
||||||
|
|
||||||
CommandProcessingResult updateFund(Long fundId, JsonCommand command);
|
CommandProcessingResult updateFund(Long fundId, JsonCommand command);
|
||||||
|
|
||||||
|
CommandProcessingResult deleteFund(Long fundId, JsonCommand command);
|
||||||
}
|
}
|
||||||
|
|||||||
+41
@@ -35,6 +35,7 @@ import org.apache.fineract.portfolio.fund.exception.FundNotFoundException;
|
|||||||
import org.apache.fineract.portfolio.fund.serialization.FundCommandFromApiJsonDeserializer;
|
import org.apache.fineract.portfolio.fund.serialization.FundCommandFromApiJsonDeserializer;
|
||||||
import org.springframework.cache.annotation.CacheEvict;
|
import org.springframework.cache.annotation.CacheEvict;
|
||||||
import org.springframework.dao.DataIntegrityViolationException;
|
import org.springframework.dao.DataIntegrityViolationException;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.orm.jpa.JpaSystemException;
|
import org.springframework.orm.jpa.JpaSystemException;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -45,6 +46,7 @@ public class FundWritePlatformServiceJpaRepositoryImpl implements FundWritePlatf
|
|||||||
private final PlatformSecurityContext context;
|
private final PlatformSecurityContext context;
|
||||||
private final FundCommandFromApiJsonDeserializer fromApiJsonDeserializer;
|
private final FundCommandFromApiJsonDeserializer fromApiJsonDeserializer;
|
||||||
private final FundRepository fundRepository;
|
private final FundRepository fundRepository;
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@Override
|
@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.
|
* Guaranteed to throw an exception no matter what the data integrity issue is.
|
||||||
*/
|
*/
|
||||||
|
|||||||
+2
-2
@@ -42,7 +42,7 @@ public class FundConfiguration {
|
|||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnMissingBean(FundWritePlatformService.class)
|
@ConditionalOnMissingBean(FundWritePlatformService.class)
|
||||||
public FundWritePlatformService fundWritePlatformService(PlatformSecurityContext context,
|
public FundWritePlatformService fundWritePlatformService(PlatformSecurityContext context,
|
||||||
FundCommandFromApiJsonDeserializer fromApiJsonDeserializer, FundRepository fundRepository) {
|
FundCommandFromApiJsonDeserializer fromApiJsonDeserializer, FundRepository fundRepository, JdbcTemplate jdbcTemplate) {
|
||||||
return new FundWritePlatformServiceJpaRepositoryImpl(context, fromApiJsonDeserializer, fundRepository);
|
return new FundWritePlatformServiceJpaRepositoryImpl(context, fromApiJsonDeserializer, fundRepository, jdbcTemplate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-63
@@ -19,6 +19,7 @@
|
|||||||
package org.apache.fineract.portfolio.orgsetup.api;
|
package org.apache.fineract.portfolio.orgsetup.api;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
import jakarta.ws.rs.Consumes;
|
import jakarta.ws.rs.Consumes;
|
||||||
import jakarta.ws.rs.GET;
|
import jakarta.ws.rs.GET;
|
||||||
import jakarta.ws.rs.PUT;
|
import jakarta.ws.rs.PUT;
|
||||||
@@ -27,17 +28,18 @@ import jakarta.ws.rs.PathParam;
|
|||||||
import jakarta.ws.rs.Produces;
|
import jakarta.ws.rs.Produces;
|
||||||
import jakarta.ws.rs.QueryParam;
|
import jakarta.ws.rs.QueryParam;
|
||||||
import jakarta.ws.rs.core.MediaType;
|
import jakarta.ws.rs.core.MediaType;
|
||||||
import java.time.Instant;
|
|
||||||
import java.sql.Timestamp;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import lombok.RequiredArgsConstructor;
|
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.exception.PlatformDataIntegrityException;
|
||||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
import org.apache.fineract.useradministration.domain.AppUser;
|
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@@ -57,6 +59,7 @@ public class BranchProfileApiResource {
|
|||||||
private final JdbcTemplate jdbcTemplate;
|
private final JdbcTemplate jdbcTemplate;
|
||||||
private final PlatformSecurityContext context;
|
private final PlatformSecurityContext context;
|
||||||
private final FromJsonHelper fromJsonHelper;
|
private final FromJsonHelper fromJsonHelper;
|
||||||
|
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||||
private final Gson gson = new Gson();
|
private final Gson gson = new Gson();
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@@ -84,68 +87,23 @@ public class BranchProfileApiResource {
|
|||||||
@PUT
|
@PUT
|
||||||
@Path("{officeId}")
|
@Path("{officeId}")
|
||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
public String update(@PathParam("officeId") final Long officeId, final String body) {
|
public CommandProcessingResult update(@PathParam("officeId") final Long officeId, final String body) {
|
||||||
this.context.authenticatedUser().validateHasPermissionTo("UPDATE_BRANCHPROFILE");
|
// The write itself (office-exists check, branch-code uniqueness check, m_office_profile
|
||||||
|
// upsert) now lives in BranchProfileWritePlatformService, invoked via
|
||||||
final Integer officeExists = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM m_office WHERE id = ?",
|
// UpdateBranchProfileCommandHandler so it goes through the generic Maker-Checker gate. The
|
||||||
Integer.class, officeId);
|
// permission check that used to happen here explicitly is now enforced by SecurityConfig's
|
||||||
if (officeExists == null || officeExists == 0) {
|
// matcher plus the command-source pipeline itself, the same as every other endpoint already
|
||||||
throw new PlatformDataIntegrityException("error.msg.branchprofile.office.not.found",
|
// migrated to this pipeline. A "locale" key is added if the body does not already carry one -
|
||||||
"Office " + officeId + " was not found.");
|
// 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 var root = this.fromJsonHelper.parse(body);
|
||||||
final String branchCode = this.fromJsonHelper.extractStringNamed("branchCode", root);
|
final JsonObject json = root.getAsJsonObject();
|
||||||
final String branchType = this.fromJsonHelper.extractStringNamed("branchType", root);
|
if (!json.has("locale")) {
|
||||||
final String address = this.fromJsonHelper.extractStringNamed("address", root);
|
json.addProperty("locale", "en");
|
||||||
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 CommandWrapper commandRequest = new CommandWrapperBuilder().updateBranchProfile(officeId)
|
||||||
final AppUser currentUser = this.context.authenticatedUser();
|
.withJson(this.gson.toJson(json)).build();
|
||||||
final Integer existing = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM m_office_profile WHERE office_id = ?",
|
return this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String baseSql() {
|
private String baseSql() {
|
||||||
|
|||||||
+16
-104
@@ -25,14 +25,16 @@ import jakarta.ws.rs.PUT;
|
|||||||
import jakarta.ws.rs.Path;
|
import jakarta.ws.rs.Path;
|
||||||
import jakarta.ws.rs.Produces;
|
import jakarta.ws.rs.Produces;
|
||||||
import jakarta.ws.rs.core.MediaType;
|
import jakarta.ws.rs.core.MediaType;
|
||||||
import java.sql.Date;
|
|
||||||
import java.sql.Timestamp;
|
import java.sql.Timestamp;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import lombok.RequiredArgsConstructor;
|
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.core.serialization.FromJsonHelper;
|
||||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
import org.apache.fineract.useradministration.domain.AppUser;
|
import org.apache.fineract.useradministration.domain.AppUser;
|
||||||
@@ -60,6 +62,7 @@ public class InstitutionProfileApiResource {
|
|||||||
private final JdbcTemplate jdbcTemplate;
|
private final JdbcTemplate jdbcTemplate;
|
||||||
private final PlatformSecurityContext context;
|
private final PlatformSecurityContext context;
|
||||||
private final FromJsonHelper fromJsonHelper;
|
private final FromJsonHelper fromJsonHelper;
|
||||||
|
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||||
private final Gson gson = new Gson();
|
private final Gson gson = new Gson();
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@@ -89,100 +92,17 @@ public class InstitutionProfileApiResource {
|
|||||||
|
|
||||||
@PUT
|
@PUT
|
||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
public String update(final String body) {
|
public CommandProcessingResult update(final String body) {
|
||||||
this.context.authenticatedUser().validateHasPermissionTo("UPDATE_INSTITUTIONPROFILE");
|
// The write itself (the JDBC UPDATE of m_institution_profile) now lives in
|
||||||
ensureRowExists();
|
// InstitutionProfileWritePlatformService, invoked via UpdateInstitutionProfileCommandHandler so
|
||||||
|
// it goes through the generic Maker-Checker gate. The permission check that used to happen here
|
||||||
final var root = this.fromJsonHelper.parse(body);
|
// explicitly is now enforced by SecurityConfig's matcher plus the command-source pipeline
|
||||||
final String bankName = this.fromJsonHelper.extractStringNamed("bankName", root);
|
// itself, the same as every other endpoint already migrated to this pipeline (Currency, Working
|
||||||
final String legalName = this.fromJsonHelper.extractStringNamed("legalName", root);
|
// Days, Payment Type). `body` is forwarded to the command as-is - it already carries the
|
||||||
final String shortName = this.fromJsonHelper.extractStringNamed("shortName", root);
|
// `locale` key the frontend sends, which JsonCommand's BigDecimal/date extractors would
|
||||||
final String bankCode = this.fromJsonHelper.extractStringNamed("bankCode", root);
|
// otherwise require.
|
||||||
final String institutionCode = this.fromJsonHelper.extractStringNamed("institutionCode", root);
|
final CommandWrapper commandRequest = new CommandWrapperBuilder().updateInstitutionProfile().withJson(body).build();
|
||||||
final String bankType = this.fromJsonHelper.extractStringNamed("bankType", root);
|
return this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ensureRowExists() {
|
private void ensureRowExists() {
|
||||||
@@ -204,14 +124,6 @@ public class InstitutionProfileApiResource {
|
|||||||
return codes.isEmpty() ? null : codes.get(0);
|
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) {
|
private Map<String, Object> toResult(final Map<String, Object> row) {
|
||||||
final Map<String, Object> out = new LinkedHashMap<>();
|
final Map<String, Object> out = new LinkedHashMap<>();
|
||||||
out.put("bankName", row.get("bank_name"));
|
out.put("bankName", row.get("bank_name"));
|
||||||
|
|||||||
+62
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+88
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.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);
|
||||||
|
}
|
||||||
+103
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
@@ -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);
|
||||||
|
}
|
||||||
+134
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
@@ -284,6 +284,7 @@ public class FixedDepositAccountsApiResource {
|
|||||||
@QueryParam("principalAmount") @Parameter(description = "Deposit principal amount") final BigDecimal principalAmount,
|
@QueryParam("principalAmount") @Parameter(description = "Deposit principal amount") final BigDecimal principalAmount,
|
||||||
@QueryParam("annualInterestRate") @Parameter(description = "Annual interest rate (%)") final BigDecimal annualInterestRate,
|
@QueryParam("annualInterestRate") @Parameter(description = "Annual interest rate (%)") final BigDecimal annualInterestRate,
|
||||||
@QueryParam("tenureInMonths") @Parameter(description = "Tenure in months") final Long tenureInMonths,
|
@QueryParam("tenureInMonths") @Parameter(description = "Tenure in months") final Long tenureInMonths,
|
||||||
|
@QueryParam("tenureInDays") @Parameter(description = "Tenure in days (alternative to tenureInMonths, for sub-month FD Single tenures)") final Long tenureInDays,
|
||||||
@QueryParam("interestCompoundingPeriodInMonths") @Parameter(description = "Compounding period in months (1,3,6,12)") final Long interestCompoundingPeriodInMonths,
|
@QueryParam("interestCompoundingPeriodInMonths") @Parameter(description = "Compounding period in months (1,3,6,12)") final Long interestCompoundingPeriodInMonths,
|
||||||
@QueryParam("interestPostingPeriodInMonths") @Parameter(description = "Posting period in months (1,3,6,12)") final Long interestPostingPeriodInMonths,
|
@QueryParam("interestPostingPeriodInMonths") @Parameter(description = "Posting period in months (1,3,6,12)") final Long interestPostingPeriodInMonths,
|
||||||
@QueryParam("submittedOnDate") @Parameter(description = "Submission date (yyyy-MM-dd) for exact calendar calculation") final String submittedOnDate,
|
@QueryParam("submittedOnDate") @Parameter(description = "Submission date (yyyy-MM-dd) for exact calendar calculation") final String submittedOnDate,
|
||||||
@@ -291,6 +292,9 @@ public class FixedDepositAccountsApiResource {
|
|||||||
HashMap request = new HashMap<>();
|
HashMap request = new HashMap<>();
|
||||||
request.put("annualInterestRate", annualInterestRate);
|
request.put("annualInterestRate", annualInterestRate);
|
||||||
request.put("tenureInMonths", tenureInMonths);
|
request.put("tenureInMonths", tenureInMonths);
|
||||||
|
if (tenureInDays != null) {
|
||||||
|
request.put("tenureInDays", tenureInDays);
|
||||||
|
}
|
||||||
request.put("interestCompoundingPeriodInMonths", interestCompoundingPeriodInMonths);
|
request.put("interestCompoundingPeriodInMonths", interestCompoundingPeriodInMonths);
|
||||||
request.put("interestPostingPeriodInMonths", interestPostingPeriodInMonths);
|
request.put("interestPostingPeriodInMonths", interestPostingPeriodInMonths);
|
||||||
request.put("principalAmount", principalAmount);
|
request.put("principalAmount", principalAmount);
|
||||||
|
|||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* 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.savings.api;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.ws.rs.Consumes;
|
||||||
|
import jakarta.ws.rs.POST;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.PathParam;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.savings.DepositsApiConstants;
|
||||||
|
import org.apache.fineract.portfolio.savings.service.DepositAccountWritePlatformService;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FD Dashboard's Renew action calls this as a pre-step, immediately before submitting the generic
|
||||||
|
* `payment` command (the same one the account's own Account Actions menu already uses) -- so the
|
||||||
|
* Dashboard offers every closure type that Payment does (Transfer to Savings, both Reinvest options,
|
||||||
|
* Partial Reinvest), not just the two {@code /renew} itself supports, while still getting the
|
||||||
|
* gap-period interest credited first when the product's renewal grace window has passed. A no-op
|
||||||
|
* (returns gapInterestAmount = 0) when still within grace, or when no grace is configured at all --
|
||||||
|
* see DepositAccountWritePlatformServiceJpaRepositoryImpl#creditGapInterestIfPastGrace.
|
||||||
|
*/
|
||||||
|
@Path("/v1/fixeddepositaccounts/{accountId}/credit-gap-interest")
|
||||||
|
@Component
|
||||||
|
@Tag(name = "Fixed Deposit Accounts", description = "FD Dashboard: credits gap-period interest before a late renewal.")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class FixedDepositGapInterestApiResource {
|
||||||
|
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final DepositAccountWritePlatformService depositAccountWritePlatformService;
|
||||||
|
private final Gson gson = new Gson();
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Transactional
|
||||||
|
public String creditGapInterest(@PathParam("accountId") final Long accountId) {
|
||||||
|
// Posts a real interest transaction when past grace, so it needs write/update, not just read.
|
||||||
|
this.context.authenticatedUser().validateHasUpdatePermission(DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME);
|
||||||
|
final Map<String, Object> result = this.depositAccountWritePlatformService.creditGapInterestIfPastGrace(accountId);
|
||||||
|
return this.gson.toJson(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
+96
@@ -0,0 +1,96 @@
|
|||||||
|
/**
|
||||||
|
* 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.savings.api;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.savings.DepositsApiConstants;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FD Dashboard's data source: every ACTIVE Fixed Deposit with just the fields the maturity ladder
|
||||||
|
* needs (account/client/product identity, maturity date, deposit and maturity amounts).
|
||||||
|
*
|
||||||
|
* Deliberately not routed through {@link FixedDepositAccountsApiResource#retrieveAll}: that
|
||||||
|
* endpoint's mapper joins m_savings_account to m_client, m_group, m_staff, m_currency,
|
||||||
|
* m_code_value and FIVE separate m_appuser joins (submitted/rejected/withdrawn/approved/activated/
|
||||||
|
* closed-by), plus a correlated subquery against m_savings_account_transaction per row -- all of it
|
||||||
|
* needed for the single-account and full-list screens it serves, none of it needed here. Neither
|
||||||
|
* m_savings_account.deposit_type_enum/status_enum nor
|
||||||
|
* m_deposit_account_term_and_preclosure.maturity_date had an index before this feature (confirmed
|
||||||
|
* directly against pg_indexes), so that query was a full table scan on top of being wide. This
|
||||||
|
* endpoint selects only what the dashboard renders, against the narrow, now-indexed WHERE clause
|
||||||
|
* this feature added (see migration 0571).
|
||||||
|
*/
|
||||||
|
@Path("/v1/fixeddepositaccounts/maturity-dashboard")
|
||||||
|
@Component
|
||||||
|
@Tag(name = "Fixed Deposit Accounts", description = "FD Dashboard: active FDs positioned against their maturity date.")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class FixedDepositMaturityDashboardApiResource {
|
||||||
|
|
||||||
|
/** Fineract's DepositAccountType.FIXED_DEPOSIT. */
|
||||||
|
private static final int FIXED_DEPOSIT = 200;
|
||||||
|
/** SavingsAccountStatusType.ACTIVE. */
|
||||||
|
private static final int ACTIVE = 300;
|
||||||
|
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
private final Gson gson = new Gson();
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public String retrieve() {
|
||||||
|
// Same permission as the single-account/full-list FD endpoints -- this is a narrower read of
|
||||||
|
// the same resource, not a new one, so no new permission is introduced for it.
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME);
|
||||||
|
|
||||||
|
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList("""
|
||||||
|
SELECT sa.id AS "id",
|
||||||
|
sa.account_no AS "accountNo",
|
||||||
|
sa.client_id AS "clientId",
|
||||||
|
c.display_name AS "clientName",
|
||||||
|
sp.name AS "productName",
|
||||||
|
sp.fd_type AS "fdType",
|
||||||
|
TO_CHAR(datp.maturity_date, 'YYYY-MM-DD') AS "maturityDate",
|
||||||
|
datp.deposit_amount AS "depositAmount",
|
||||||
|
datp.maturity_amount AS "maturityAmount",
|
||||||
|
datp.on_account_closure_enum AS "onClosureType",
|
||||||
|
sa.linked_account_id AS "linkedAccountId",
|
||||||
|
dptp.renewal_grace_period_days AS "renewalGracePeriodDays"
|
||||||
|
FROM m_savings_account sa
|
||||||
|
JOIN m_deposit_account_term_and_preclosure datp ON datp.savings_account_id = sa.id
|
||||||
|
JOIN m_savings_product sp ON sp.id = sa.product_id
|
||||||
|
LEFT JOIN m_deposit_product_term_and_preclosure dptp ON dptp.savings_product_id = sp.id
|
||||||
|
LEFT JOIN m_client c ON c.id = sa.client_id
|
||||||
|
WHERE sa.deposit_type_enum = ? AND sa.status_enum = ?
|
||||||
|
ORDER BY datp.maturity_date ASC
|
||||||
|
""", FIXED_DEPOSIT, ACTIVE);
|
||||||
|
|
||||||
|
return this.gson.toJson(rows);
|
||||||
|
}
|
||||||
|
}
|
||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* 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.savings.api;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.ws.rs.Consumes;
|
||||||
|
import jakarta.ws.rs.POST;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.PathParam;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.savings.DepositsApiConstants;
|
||||||
|
import org.apache.fineract.portfolio.savings.service.DepositAccountWritePlatformService;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FD Dashboard's "Renew" action: manually triggers the renewal of a matured FD -- backdated to the
|
||||||
|
* original maturity date within the product's renewal grace window (0 days if none is configured), or
|
||||||
|
* from today with a gap-period interest credit (at the linked Savings account's rate) once the grace
|
||||||
|
* window has passed. The staff's choice of Reinvest Principal + Interest vs Reinvest Principal Only is
|
||||||
|
* made here, at renewal time, via {@code reinvestType} in the request body -- the account's own stored
|
||||||
|
* maturity instruction (if it even has one; new SINGLE/DOUBLE FDs are opened without one) is not
|
||||||
|
* consulted. All the actual renewal and gap-interest logic lives in
|
||||||
|
* DepositAccountWritePlatformServiceJpaRepositoryImpl#renewMaturedFixedDeposit (it needs the same
|
||||||
|
* collaborators updateMaturityDetails() already has); this resource is just the HTTP entry point onto it.
|
||||||
|
*/
|
||||||
|
@Path("/v1/fixeddepositaccounts/{accountId}/renew")
|
||||||
|
@Component
|
||||||
|
@Tag(name = "Fixed Deposit Accounts", description = "FD Dashboard: manual renewal of a matured FD within/after its grace period.")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class FixedDepositRenewalApiResource {
|
||||||
|
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final DepositAccountWritePlatformService depositAccountWritePlatformService;
|
||||||
|
private final Gson gson = new Gson();
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Transactional
|
||||||
|
public String renew(@PathParam("accountId") final Long accountId,
|
||||||
|
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
// A renewal moves money (or credits gap-period interest), so it needs the write/update
|
||||||
|
// permission on this resource, not just read -- unlike the dashboard's own read-only endpoint.
|
||||||
|
this.context.authenticatedUser().validateHasUpdatePermission(DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME);
|
||||||
|
final JsonObject body = (apiRequestBodyAsJson == null || apiRequestBodyAsJson.isBlank()) ? new JsonObject()
|
||||||
|
: JsonParser.parseString(apiRequestBodyAsJson).getAsJsonObject();
|
||||||
|
final Integer reinvestType = (body.has("reinvestType") && !body.get("reinvestType").isJsonNull())
|
||||||
|
? body.get("reinvestType").getAsInt()
|
||||||
|
: null;
|
||||||
|
final Map<String, Object> result = this.depositAccountWritePlatformService.renewMaturedFixedDeposit(accountId, reinvestType);
|
||||||
|
return this.gson.toJson(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-1
@@ -43,6 +43,7 @@ import static org.apache.fineract.portfolio.savings.DepositsApiConstants.preClos
|
|||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.principalAmountParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.principalAmountParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.recurringFrequencyParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.recurringFrequencyParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.recurringFrequencyTypeParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.recurringFrequencyTypeParamName;
|
||||||
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.tenureInDaysParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.tenureInMonthsParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.tenureInMonthsParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transferInterestToSavingsParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transferInterestToSavingsParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transferToSavingsIdParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transferToSavingsIdParamName;
|
||||||
@@ -681,8 +682,16 @@ public class DepositAccountDataValidator {
|
|||||||
baseDataValidator.reset().parameter(annualInterestRateParamName).value(annualInterestRate).notNull()
|
baseDataValidator.reset().parameter(annualInterestRateParamName).value(annualInterestRate).notNull()
|
||||||
.notLessThanMin(BigDecimal.valueOf(0));
|
.notLessThanMin(BigDecimal.valueOf(0));
|
||||||
|
|
||||||
|
// FD Single accounts under 30 days are entered in whole days rather than months (a whole-month
|
||||||
|
// tenure would round down to 0 months); tenureInDays is an alternative to tenureInMonths, not
|
||||||
|
// an addition, so only one of the two needs to be a positive value.
|
||||||
Long tenureInMonths = this.fromApiJsonHelper.extractLongNamed(tenureInMonthsParamName, element);
|
Long tenureInMonths = this.fromApiJsonHelper.extractLongNamed(tenureInMonthsParamName, element);
|
||||||
baseDataValidator.reset().parameter(tenureInMonthsParamName).value(tenureInMonths).notNull().longGreaterThanZero();
|
Long tenureInDays = this.fromApiJsonHelper.extractLongNamed(tenureInDaysParamName, element);
|
||||||
|
if (tenureInDays != null && tenureInDays > 0) {
|
||||||
|
baseDataValidator.reset().parameter(tenureInMonthsParamName).value(tenureInMonths).ignoreIfNull().longGreaterThanZero();
|
||||||
|
} else {
|
||||||
|
baseDataValidator.reset().parameter(tenureInMonthsParamName).value(tenureInMonths).notNull().longGreaterThanZero();
|
||||||
|
}
|
||||||
|
|
||||||
Long interestPostingPeriodInMonths = this.fromApiJsonHelper.extractLongNamed(interestPostingPeriodInMonthsParamName, element);
|
Long interestPostingPeriodInMonths = this.fromApiJsonHelper.extractLongNamed(interestPostingPeriodInMonthsParamName, element);
|
||||||
baseDataValidator.reset().parameter(interestPostingPeriodInMonthsParamName).value(interestPostingPeriodInMonths).notNull()
|
baseDataValidator.reset().parameter(interestPostingPeriodInMonthsParamName).value(interestPostingPeriodInMonths).notNull()
|
||||||
|
|||||||
+28
@@ -355,6 +355,16 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi
|
|||||||
// an in-place renewal has nowhere to send. Left on the original flow rather than guessing.
|
// an in-place renewal has nowhere to send. Left on the original flow rather than guessing.
|
||||||
final BigDecimal maturedBalance = account.getAccountBalance();
|
final BigDecimal maturedBalance = account.getAccountBalance();
|
||||||
final BigDecimal reInvestAmount = account.getDepositAmount();
|
final BigDecimal reInvestAmount = account.getDepositAmount();
|
||||||
|
// Whatever is left after the principal moves into the new deposit is interest the customer
|
||||||
|
// earned and has to actually be paid out somewhere -- it must not just sit on this account,
|
||||||
|
// which is about to be closed. Validated before anything below runs, so a missing target
|
||||||
|
// fails cleanly with nothing done rather than leaving a half-completed reinvestment.
|
||||||
|
final BigDecimal leftoverInterest = maturedBalance.subtract(reInvestAmount);
|
||||||
|
if (leftoverInterest.compareTo(BigDecimal.ZERO) > 0 && toSavingsId == null) {
|
||||||
|
throw new GeneralPlatformDomainRuleException(
|
||||||
|
"error.msg.fixeddepositaccount.reinvest.principal.only.requires.savings.account",
|
||||||
|
"Reinvest Principal Only requires a Savings account to receive the leftover interest.");
|
||||||
|
}
|
||||||
ExternalId externalId = this.externalIdFactory.create();
|
ExternalId externalId = this.externalIdFactory.create();
|
||||||
|
|
||||||
FixedDepositAccount reinvestedDeposit = account.reInvest(reInvestAmount, externalId);
|
FixedDepositAccount reinvestedDeposit = account.reInvest(reInvestAmount, externalId);
|
||||||
@@ -384,6 +394,24 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi
|
|||||||
recordRenewal(account, reinvestedDeposit, maturedBalance, reInvestAmount, closedDate,
|
recordRenewal(account, reinvestedDeposit, maturedBalance, reInvestAmount, closedDate,
|
||||||
FixedDepositRenewalHistory.RENEWAL_TYPE_AUTO, user);
|
FixedDepositRenewalHistory.RENEWAL_TYPE_AUTO, user);
|
||||||
|
|
||||||
|
// Pay out whatever didn't move into the new deposit, then actually close this account --
|
||||||
|
// previously this branch left the leftover interest stranded on an account that was never
|
||||||
|
// closed, silently, because nothing past this point ever referenced toSavingsId at all.
|
||||||
|
if (leftoverInterest.compareTo(BigDecimal.ZERO) > 0) {
|
||||||
|
final SavingsAccount toSavingsAccount = this.depositAccountAssembler.assembleFrom(toSavingsId,
|
||||||
|
DepositAccountType.SAVINGS_DEPOSIT);
|
||||||
|
final boolean isExceptionForBalanceCheck = false;
|
||||||
|
final AccountTransferDTO leftoverTransferDTO = new AccountTransferDTO(closedDate, leftoverInterest,
|
||||||
|
PortfolioAccountType.SAVINGS, PortfolioAccountType.SAVINGS, null, null,
|
||||||
|
"Reinvest Principal Only -- interest paid out", null, fmt, null, null, null, null, null,
|
||||||
|
AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, ExternalId.empty(), null, toSavingsAccount, account,
|
||||||
|
isAccountTransfer, isExceptionForBalanceCheck);
|
||||||
|
this.accountTransfersWritePlatformService.transferFunds(leftoverTransferDTO);
|
||||||
|
updateAlreadyPostedTransactions(existingTransactionIds, account);
|
||||||
|
}
|
||||||
|
account.updateClosedStatus();
|
||||||
|
account.updateOnAccountClosureStatus(onClosureType);
|
||||||
|
|
||||||
} else if (onClosureType.isTransferToSavings()) {
|
} else if (onClosureType.isTransferToSavings()) {
|
||||||
final SavingsAccount toSavingsAccount = this.depositAccountAssembler.assembleFrom(toSavingsId,
|
final SavingsAccount toSavingsAccount = this.depositAccountAssembler.assembleFrom(toSavingsId,
|
||||||
DepositAccountType.SAVINGS_DEPOSIT);
|
DepositAccountType.SAVINGS_DEPOSIT);
|
||||||
|
|||||||
+25
-12
@@ -414,20 +414,33 @@ public class SavingsAccountAssembler {
|
|||||||
// dependency on the shareaccounts domain class) rather than trusted blindly, the same way an
|
// dependency on the shareaccounts domain class) rather than trusted blindly, the same way an
|
||||||
// account number typed for a linked savings/loan account elsewhere in this form is verified
|
// account number typed for a linked savings/loan account elsewhere in this form is verified
|
||||||
// before being accepted.
|
// before being accepted.
|
||||||
|
//
|
||||||
|
// For a term deposit (FD/RD), the product screen labels this same flag "Deposit Account Link" --
|
||||||
|
// it links an existing Savings/Current account (linkedAccountId, set below), not a co-op share
|
||||||
|
// account, so it's validated against that field instead of shareAccountId.
|
||||||
if (Boolean.TRUE.equals(account.getShareLinkingEnabled())) {
|
if (Boolean.TRUE.equals(account.getShareLinkingEnabled())) {
|
||||||
final Long shareAccountId = this.fromApiJsonHelper.extractLongNamed(shareAccountIdParamName, element);
|
final boolean isTermDeposit = account.depositAccountType().isFixedDeposit() || account.depositAccountType().isRecurringDeposit();
|
||||||
if (shareAccountId == null) {
|
if (isTermDeposit) {
|
||||||
throw new GeneralPlatformDomainRuleException("error.msg.savingsaccount.share.account.required",
|
final Long linkedAccountIdForCheck = this.fromApiJsonHelper.extractLongNamed(linkedAccountIdParamName, element);
|
||||||
"A share account must be chosen when Share Linking is enabled.");
|
if (linkedAccountIdForCheck == null) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.savingsaccount.linked.account.required",
|
||||||
|
"A Savings/Current account must be linked when Deposit Account Link is enabled.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final Long shareAccountId = this.fromApiJsonHelper.extractLongNamed(shareAccountIdParamName, element);
|
||||||
|
if (shareAccountId == null) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.savingsaccount.share.account.required",
|
||||||
|
"A share account must be chosen when Share Linking is enabled.");
|
||||||
|
}
|
||||||
|
final Integer matches = this.jdbcTemplate.queryForObject(
|
||||||
|
"SELECT COUNT(*) FROM m_share_account WHERE id = ? AND client_id = ? AND status_enum = 300", Integer.class,
|
||||||
|
shareAccountId, account.clientId());
|
||||||
|
if (matches == null || matches == 0) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.savingsaccount.share.account.not.active.for.client",
|
||||||
|
"Share account " + shareAccountId + " is not an active share account for this client.");
|
||||||
|
}
|
||||||
|
account.setShareAccountId(shareAccountId);
|
||||||
}
|
}
|
||||||
final Integer matches = this.jdbcTemplate.queryForObject(
|
|
||||||
"SELECT COUNT(*) FROM m_share_account WHERE id = ? AND client_id = ? AND status_enum = 300", Integer.class,
|
|
||||||
shareAccountId, account.clientId());
|
|
||||||
if (matches == null || matches == 0) {
|
|
||||||
throw new GeneralPlatformDomainRuleException("error.msg.savingsaccount.share.account.not.active.for.client",
|
|
||||||
"Share account " + shareAccountId + " is not an active share account for this client.");
|
|
||||||
}
|
|
||||||
account.setShareAccountId(shareAccountId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Senior citizen rate — runs after validateDomainRules so it is the final persisted value
|
// Senior citizen rate — runs after validateDomainRules so it is the final persisted value
|
||||||
|
|||||||
+205
-5
@@ -28,6 +28,7 @@ import java.math.BigDecimal;
|
|||||||
import java.math.MathContext;
|
import java.math.MathContext;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
@@ -86,6 +87,7 @@ import org.apache.fineract.portfolio.note.domain.Note;
|
|||||||
import org.apache.fineract.portfolio.note.domain.NoteRepository;
|
import org.apache.fineract.portfolio.note.domain.NoteRepository;
|
||||||
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
|
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
|
||||||
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
|
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
|
||||||
|
import org.apache.fineract.portfolio.savings.DepositAccountOnClosureType;
|
||||||
import org.apache.fineract.portfolio.savings.DepositAccountType;
|
import org.apache.fineract.portfolio.savings.DepositAccountType;
|
||||||
import org.apache.fineract.portfolio.savings.DepositsApiConstants;
|
import org.apache.fineract.portfolio.savings.DepositsApiConstants;
|
||||||
import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType;
|
import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType;
|
||||||
@@ -99,6 +101,7 @@ import org.apache.fineract.portfolio.savings.domain.DepositAccountOnHoldTransact
|
|||||||
import org.apache.fineract.portfolio.savings.domain.DepositAccountOnHoldTransactionRepository;
|
import org.apache.fineract.portfolio.savings.domain.DepositAccountOnHoldTransactionRepository;
|
||||||
import org.apache.fineract.portfolio.savings.domain.DepositAccountRecurringDetail;
|
import org.apache.fineract.portfolio.savings.domain.DepositAccountRecurringDetail;
|
||||||
import org.apache.fineract.portfolio.savings.domain.FixedDepositAccount;
|
import org.apache.fineract.portfolio.savings.domain.FixedDepositAccount;
|
||||||
|
import org.apache.fineract.portfolio.savings.domain.FixedDepositProduct;
|
||||||
import org.apache.fineract.portfolio.savings.domain.RecurringDepositAccount;
|
import org.apache.fineract.portfolio.savings.domain.RecurringDepositAccount;
|
||||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
|
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
|
||||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountCharge;
|
import org.apache.fineract.portfolio.savings.domain.SavingsAccountCharge;
|
||||||
@@ -112,6 +115,7 @@ import org.apache.fineract.portfolio.savings.exception.DepositAccountTransaction
|
|||||||
import org.apache.fineract.portfolio.savings.exception.SavingsAccountTransactionNotFoundException;
|
import org.apache.fineract.portfolio.savings.exception.SavingsAccountTransactionNotFoundException;
|
||||||
import org.apache.fineract.portfolio.savings.exception.TransactionUpdateNotAllowedException;
|
import org.apache.fineract.portfolio.savings.exception.TransactionUpdateNotAllowedException;
|
||||||
import org.apache.fineract.useradministration.domain.AppUser;
|
import org.apache.fineract.useradministration.domain.AppUser;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -142,6 +146,7 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
|
|||||||
private final WorkingDaysRepositoryWrapper workingDaysRepository;
|
private final WorkingDaysRepositoryWrapper workingDaysRepository;
|
||||||
private final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository;
|
private final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository;
|
||||||
private final DenominationBreakdownWriter denominationBreakdownWriter;
|
private final DenominationBreakdownWriter denominationBreakdownWriter;
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
@@ -1512,23 +1517,53 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
|
|||||||
if (depositAccountType.isFixedDeposit()) {
|
if (depositAccountType.isFixedDeposit()) {
|
||||||
((FixedDepositAccount) account).updateMaturityStatus(isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth);
|
((FixedDepositAccount) account).updateMaturityStatus(isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth);
|
||||||
FixedDepositAccount fdAccount = ((FixedDepositAccount) account);
|
FixedDepositAccount fdAccount = ((FixedDepositAccount) account);
|
||||||
|
|
||||||
|
// Post the interest transaction(s) updateMaturityStatus() just created BEFORE any maturity
|
||||||
|
// closure below runs. The closure's own withdrawal (handleWithdrawal -> postJournalEntries)
|
||||||
|
// snapshots "existing" transactions the moment IT starts, which by then already includes
|
||||||
|
// this interest posting -- so its own posting treats the interest as already accounted for
|
||||||
|
// and skips it, while this method's own final postJournalEntries() call (previously relying
|
||||||
|
// on the interest staying out of existingTransactionIds so it would post there instead)
|
||||||
|
// never actually saw it get posted either: for an FD whose entire term is a single posting
|
||||||
|
// period -- interest posts and the account matures in this same run -- that left the
|
||||||
|
// interest transaction with a real ledger balance but no matching GL entry at all, forever.
|
||||||
|
// Posting it now, while it is still the only new thing on the account, gives it exactly one
|
||||||
|
// correct journal entry, and folding it into existingTransactionIds below (the same way
|
||||||
|
// markWithdrawalsAsAlreadyPosted() already does for the withdrawal) keeps the closure branch
|
||||||
|
// and the final call from ever trying to post it again.
|
||||||
|
this.savingAccountRepositoryWrapper.saveAndFlush(account);
|
||||||
|
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds);
|
||||||
|
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
|
||||||
// handle maturity instructions
|
// handle maturity instructions
|
||||||
|
|
||||||
|
// An account opened WITH an explicit maturity instruction (Transfer to Savings or either
|
||||||
|
// Reinvest option) auto-processes here, tonight, exactly as every FD always has -- the
|
||||||
|
// renewal grace period never delays this, no matter what the product has configured. Grace
|
||||||
|
// only matters for an account that was opened with NO instruction at all: that one is never
|
||||||
|
// touched here (isReinvestOnClosure()/isTransferToSavingsOnClosure() are both false for a
|
||||||
|
// null closure type), so it just sits matured until a human renews it from the FD Dashboard
|
||||||
|
// -- see renewMaturedFixedDeposit(), which is where the grace window is actually read.
|
||||||
if (fdAccount.isMatured() && (fdAccount.isReinvestOnClosure() || fdAccount.isTransferToSavingsOnClosure())) {
|
if (fdAccount.isMatured() && (fdAccount.isReinvestOnClosure() || fdAccount.isTransferToSavingsOnClosure())) {
|
||||||
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||||
Map<String, Object> changes = new HashMap<>();
|
Map<String, Object> changes = new HashMap<>();
|
||||||
final AppUser user = context.authenticatedUser();
|
final AppUser user = context.authenticatedUser();
|
||||||
Long toSavingsId = fdAccount.getTransferToSavingsAccountId();
|
Long toSavingsId = fdAccount.getTransferToSavingsAccountId();
|
||||||
|
// Reinvest Principal Only needs a target for the leftover interest (see
|
||||||
|
// DepositAccountDomainServiceJpa#handleFDAccountMaturityClosure) -- the account-opening
|
||||||
|
// form only collects transferToSavingsId when Transfer to Savings itself was chosen, so
|
||||||
|
// an account opened straight into Reinvest Principal Only can reach here with none set.
|
||||||
|
// Falls back to the FD's own Deposit Account Link rather than failing the whole job run.
|
||||||
|
if (toSavingsId == null && fdAccount.isReinvestOnClosure()) {
|
||||||
|
toSavingsId = fdAccount.getLinkedAccountId();
|
||||||
|
}
|
||||||
this.depositAccountDomainService.handleFDAccountMaturityClosure(fdAccount, null, user, fmt, fdAccount.maturityDate(),
|
this.depositAccountDomainService.handleFDAccountMaturityClosure(fdAccount, null, user, fmt, fdAccount.maturityDate(),
|
||||||
fdAccount.getOnAccountClosureId(), toSavingsId, "Apply maturity instructions", changes);
|
fdAccount.getOnAccountClosureId(), toSavingsId, "Apply maturity instructions", changes);
|
||||||
|
|
||||||
// Every maturity-instruction branch takes the balance out of this account through
|
// Every maturity-instruction branch takes the balance out of this account through
|
||||||
// SavingsAccountDomainServiceJpa#handleWithdrawal, which posts that withdrawal's journal
|
// SavingsAccountDomainServiceJpa#handleWithdrawal, which posts that withdrawal's journal
|
||||||
// entries itself. existingTransactionIds was snapshotted at the top of this method --
|
// entries itself. existingTransactionIds was refreshed just above (after the interest was
|
||||||
// before that withdrawal existed -- so the postJournalEntries() call at the end would
|
// posted) -- before that withdrawal existed -- so the postJournalEntries() call at the end
|
||||||
// treat it as new and post it a second time. Interest postings created by
|
// would treat it as new and post it a second time without this.
|
||||||
// updateMaturityStatus() above are still genuinely unposted at this point and must stay
|
|
||||||
// out of the snapshot so that final call can post them exactly once.
|
|
||||||
markWithdrawalsAsAlreadyPosted(account, existingTransactionIds);
|
markWithdrawalsAsAlreadyPosted(account, existingTransactionIds);
|
||||||
|
|
||||||
if (changes.get("reinvestedDepositId") != null) {
|
if (changes.get("reinvestedDepositId") != null) {
|
||||||
@@ -1563,6 +1598,171 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
|
|||||||
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds);
|
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> renewMaturedFixedDeposit(final Long fdAccountId, final Integer reinvestType) {
|
||||||
|
final AppUser user = context.authenticatedUser();
|
||||||
|
final FixedDepositAccount fdAccount = (FixedDepositAccount) this.depositAccountAssembler.assembleFrom(fdAccountId,
|
||||||
|
DepositAccountType.FIXED_DEPOSIT);
|
||||||
|
|
||||||
|
if (!fdAccount.isMatured()) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.fixeddepositaccount.not.matured",
|
||||||
|
"This FD has not matured yet -- nothing to renew.");
|
||||||
|
}
|
||||||
|
// The account's own stored maturity instruction is no longer authoritative -- new SINGLE/DOUBLE
|
||||||
|
// FDs are opened without one at all (see create-fixed-deposit-account.component.ts), because
|
||||||
|
// what happens at maturity is now decided here, by staff, at the moment of renewal.
|
||||||
|
final DepositAccountOnClosureType chosenClosureType = DepositAccountOnClosureType.fromInt(reinvestType);
|
||||||
|
if (!chosenClosureType.isReinvestPrincipalAndInterest() && !chosenClosureType.isReinvestPrincipal()) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.fixeddepositaccount.renewal.reinvest.type.required",
|
||||||
|
"Choose Reinvest Principal + Interest or Reinvest Principal Only.");
|
||||||
|
}
|
||||||
|
// Only Principal pays the leftover interest out to the FD's own linked Savings account -- the
|
||||||
|
// account-opening wizard makes a Deposit Account Link mandatory for SINGLE/DOUBLE FDs, so this
|
||||||
|
// should always be present, but fail clearly rather than silently stranding the interest if not.
|
||||||
|
final Long linkedSavingsAccountId = fdAccount.getLinkedAccountId();
|
||||||
|
if (chosenClosureType.isReinvestPrincipal() && linkedSavingsAccountId == null) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.fixeddepositaccount.renewal.linked.account.required",
|
||||||
|
"This FD has no Deposit Account Link -- link a Savings account to it before renewing with Principal Only, "
|
||||||
|
+ "so the leftover interest has somewhere to go.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renewal grace period only shrinks the backdate window below -- it is no longer a precondition
|
||||||
|
// for whether Renew is allowed at all. A product that never configured one behaves as a 0-day
|
||||||
|
// window: renewing on the maturity date itself backdates, any later renewal pays gap interest.
|
||||||
|
Integer renewalGracePeriodDays = null;
|
||||||
|
if (fdAccount.savingsProduct() instanceof FixedDepositProduct fdProduct && fdProduct.depositProductTermAndPreClosure() != null) {
|
||||||
|
renewalGracePeriodDays = fdProduct.depositProductTermAndPreClosure().depositTermDetail().renewalGracePeriodDays();
|
||||||
|
}
|
||||||
|
if (renewalGracePeriodDays == null || renewalGracePeriodDays < 0) {
|
||||||
|
renewalGracePeriodDays = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
final LocalDate maturityDate = fdAccount.maturityDate();
|
||||||
|
final LocalDate today = DateUtils.getBusinessLocalDate();
|
||||||
|
final LocalDate graceDeadline = maturityDate.plusDays(renewalGracePeriodDays);
|
||||||
|
final boolean withinGrace = !today.isAfter(graceDeadline);
|
||||||
|
|
||||||
|
final Map<String, Object> result = new HashMap<>();
|
||||||
|
BigDecimal gapInterestAmount = null;
|
||||||
|
|
||||||
|
if (!withinGrace) {
|
||||||
|
// Late: the backdated renewal is no longer offered. The gap between the original maturity
|
||||||
|
// date and today is compensated at the linked Savings account's own product rate (simple
|
||||||
|
// interest for that stretch) before the FD renews from today, so the new term's principal
|
||||||
|
// already includes it.
|
||||||
|
gapInterestAmount = creditGapInterest(fdAccount, maturityDate, today);
|
||||||
|
}
|
||||||
|
|
||||||
|
final LocalDate renewalDate = withinGrace ? maturityDate : today;
|
||||||
|
final Map<String, Object> changes = new HashMap<>();
|
||||||
|
final DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||||
|
final Set<Long> existingTransactionIds = new HashSet<>();
|
||||||
|
final Set<Long> existingReversedTransactionIds = new HashSet<>();
|
||||||
|
updateExistingTransactionsDetails(fdAccount, existingTransactionIds, existingReversedTransactionIds);
|
||||||
|
|
||||||
|
this.depositAccountDomainService.handleFDAccountMaturityClosure(fdAccount, null, user, fmt, renewalDate, reinvestType,
|
||||||
|
chosenClosureType.isReinvestPrincipal() ? linkedSavingsAccountId : null,
|
||||||
|
withinGrace ? "Manual renewal within grace period" : "Manual renewal after grace period, gap interest credited at linked savings rate",
|
||||||
|
changes);
|
||||||
|
markWithdrawalsAsAlreadyPosted(fdAccount, existingTransactionIds);
|
||||||
|
this.savingAccountRepositoryWrapper.saveAndFlush(fdAccount);
|
||||||
|
postJournalEntries(fdAccount, existingTransactionIds, existingReversedTransactionIds);
|
||||||
|
|
||||||
|
// REINVEST_PRINCIPAL_ONLY closes the old account and opens a new one (reinvestedDepositId);
|
||||||
|
// REINVEST_PRINCIPAL_AND_INTEREST renews the same account in place -- report the account that
|
||||||
|
// actually carries the new term's maturity date/amount either way.
|
||||||
|
FixedDepositAccount renewedAccount = fdAccount;
|
||||||
|
if (changes.get("reinvestedDepositId") != null) {
|
||||||
|
renewedAccount = (FixedDepositAccount) this.depositAccountAssembler.assembleFrom((Long) changes.get("reinvestedDepositId"),
|
||||||
|
DepositAccountType.FIXED_DEPOSIT);
|
||||||
|
}
|
||||||
|
result.put("renewedAccountId", renewedAccount.getId());
|
||||||
|
result.put("maturityDate", renewedAccount.maturityDate() != null ? renewedAccount.maturityDate().toString() : null);
|
||||||
|
result.put("maturityAmount", renewedAccount.maturityAmount());
|
||||||
|
result.put("backdated", withinGrace);
|
||||||
|
if (gapInterestAmount != null) {
|
||||||
|
result.put("gapInterestAmount", gapInterestAmount);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> creditGapInterestIfPastGrace(final Long fdAccountId) {
|
||||||
|
final FixedDepositAccount fdAccount = (FixedDepositAccount) this.depositAccountAssembler.assembleFrom(fdAccountId,
|
||||||
|
DepositAccountType.FIXED_DEPOSIT);
|
||||||
|
if (!fdAccount.isMatured()) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.fixeddepositaccount.not.matured",
|
||||||
|
"This FD has not matured yet.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer renewalGracePeriodDays = null;
|
||||||
|
if (fdAccount.savingsProduct() instanceof FixedDepositProduct fdProduct && fdProduct.depositProductTermAndPreClosure() != null) {
|
||||||
|
renewalGracePeriodDays = fdProduct.depositProductTermAndPreClosure().depositTermDetail().renewalGracePeriodDays();
|
||||||
|
}
|
||||||
|
if (renewalGracePeriodDays == null || renewalGracePeriodDays < 0) {
|
||||||
|
renewalGracePeriodDays = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
final LocalDate maturityDate = fdAccount.maturityDate();
|
||||||
|
final LocalDate today = DateUtils.getBusinessLocalDate();
|
||||||
|
final boolean withinGrace = !today.isAfter(maturityDate.plusDays(renewalGracePeriodDays));
|
||||||
|
|
||||||
|
final Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("withinGrace", withinGrace);
|
||||||
|
result.put("gapInterestAmount", withinGrace ? BigDecimal.ZERO : creditGapInterest(fdAccount, maturityDate, today));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple interest for the stretch between the FD's own maturity date and today, at the rate of its
|
||||||
|
* linked Savings/Current account's product -- credited onto the FD itself as a genuine
|
||||||
|
* INTEREST_POSTING transaction (not a DEPOSIT, which would wrongly count as cash received in the
|
||||||
|
* cashier/vault reconciliation) before whatever the caller does next (a manual renewal, or the FD
|
||||||
|
* Dashboard's generic Payment command) reads the account's balance. Shared by
|
||||||
|
* {@link #renewMaturedFixedDeposit} and {@link #creditGapInterestIfPastGrace} -- the FD Dashboard's
|
||||||
|
* Renew action now calls the latter as a pre-step before submitting the same generic `payment`
|
||||||
|
* command the account's own Account Actions menu already uses, so every closure type (Transfer to
|
||||||
|
* Savings, both Reinvest options, Partial Reinvest) is available from the Dashboard too, not just the
|
||||||
|
* two this method's sibling endpoint originally supported.
|
||||||
|
*/
|
||||||
|
private BigDecimal creditGapInterest(final FixedDepositAccount fdAccount, final LocalDate maturityDate, final LocalDate today) {
|
||||||
|
final Long linkedAccountId = fdAccount.getLinkedAccountId();
|
||||||
|
if (linkedAccountId == null) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.fixeddepositaccount.renewal.linked.account.required",
|
||||||
|
"This FD's grace period has passed and it has no Deposit Account Link -- link a Savings/Current account to it "
|
||||||
|
+ "before renewing late, so the gap-period interest has a rate to use.");
|
||||||
|
}
|
||||||
|
final BigDecimal linkedAccountRate = this.jdbcTemplate.queryForObject("""
|
||||||
|
SELECT sp.nominal_annual_interest_rate FROM m_savings_account sa
|
||||||
|
JOIN m_savings_product sp ON sp.id = sa.product_id
|
||||||
|
WHERE sa.id = ?
|
||||||
|
""", BigDecimal.class, linkedAccountId);
|
||||||
|
|
||||||
|
final long gapDays = ChronoUnit.DAYS.between(maturityDate, today);
|
||||||
|
final int daysInYear = fdAccount.getInterestCalculationDaysInYearType() == null ? 365
|
||||||
|
: fdAccount.getInterestCalculationDaysInYearType();
|
||||||
|
final MathContext mc = MoneyHelper.getMathContext();
|
||||||
|
final BigDecimal gapInterestAmount = fdAccount.getAccountBalance().multiply(linkedAccountRate, mc)
|
||||||
|
.multiply(BigDecimal.valueOf(gapDays), mc).divide(BigDecimal.valueOf((long) daysInYear * 100), mc)
|
||||||
|
.setScale(2, java.math.RoundingMode.HALF_UP);
|
||||||
|
|
||||||
|
if (gapInterestAmount.compareTo(BigDecimal.ZERO) > 0) {
|
||||||
|
final Set<Long> existingTransactionIds = new HashSet<>();
|
||||||
|
final Set<Long> existingReversedTransactionIds = new HashSet<>();
|
||||||
|
updateExistingTransactionsDetails(fdAccount, existingTransactionIds, existingReversedTransactionIds);
|
||||||
|
|
||||||
|
final SavingsAccountTransaction gapInterestTxn = SavingsAccountTransaction.interestPosting(fdAccount, fdAccount.office(),
|
||||||
|
today, Money.of(fdAccount.getCurrency(), gapInterestAmount), true);
|
||||||
|
fdAccount.addTransaction(gapInterestTxn);
|
||||||
|
fdAccount.updateSavingsAccountSummary(fdAccount.getTransactions());
|
||||||
|
this.savingAccountRepositoryWrapper.saveAndFlush(fdAccount);
|
||||||
|
postJournalEntries(fdAccount, existingTransactionIds, existingReversedTransactionIds);
|
||||||
|
}
|
||||||
|
return gapInterestAmount;
|
||||||
|
}
|
||||||
|
|
||||||
private void updateExistingTransactionsDetails(SavingsAccount account, Set<Long> existingTransactionIds,
|
private void updateExistingTransactionsDetails(SavingsAccount account, Set<Long> existingTransactionIds,
|
||||||
Set<Long> existingReversedTransactionIds) {
|
Set<Long> existingReversedTransactionIds) {
|
||||||
existingTransactionIds.addAll(account.findExistingTransactionIds());
|
existingTransactionIds.addAll(account.findExistingTransactionIds());
|
||||||
|
|||||||
+2
@@ -309,6 +309,7 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead
|
|||||||
sqlBuilder.append("dptp.in_multiples_of_deposit_term_type_enum as inMultiplesOfDepositTermTypeId, ");
|
sqlBuilder.append("dptp.in_multiples_of_deposit_term_type_enum as inMultiplesOfDepositTermTypeId, ");
|
||||||
sqlBuilder.append("dptp.min_deposit_amount as minDepositAmount, dptp.deposit_amount as depositAmount, ");
|
sqlBuilder.append("dptp.min_deposit_amount as minDepositAmount, dptp.deposit_amount as depositAmount, ");
|
||||||
sqlBuilder.append("dptp.max_deposit_amount as maxDepositAmount, ");
|
sqlBuilder.append("dptp.max_deposit_amount as maxDepositAmount, ");
|
||||||
|
sqlBuilder.append("dptp.renewal_grace_period_days as renewalGracePeriodDays, ");
|
||||||
sqlBuilder.append("sp.account_type as accountType, sp.fd_type as fdType, sp.product_code as productCode, ");
|
sqlBuilder.append("sp.account_type as accountType, sp.fd_type as fdType, sp.product_code as productCode, ");
|
||||||
sqlBuilder.append("sp.customer_segment as customerSegment, ");
|
sqlBuilder.append("sp.customer_segment as customerSegment, ");
|
||||||
sqlBuilder.append("sp.gl_account_id as glAccountId, gla.name as glAccountName, ");
|
sqlBuilder.append("sp.gl_account_id as glAccountId, gla.name as glAccountName, ");
|
||||||
@@ -385,6 +386,7 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead
|
|||||||
// CBS Custom Fields
|
// CBS Custom Fields
|
||||||
result.setAccountType(rs.getString("accountType"));
|
result.setAccountType(rs.getString("accountType"));
|
||||||
result.setFdType(rs.getString("fdType"));
|
result.setFdType(rs.getString("fdType"));
|
||||||
|
result.setRenewalGracePeriodDays(JdbcSupport.getInteger(rs, "renewalGracePeriodDays"));
|
||||||
result.setProductCode(rs.getString("productCode"));
|
result.setProductCode(rs.getString("productCode"));
|
||||||
result.setCustomerSegment(rs.getString("customerSegment"));
|
result.setCustomerSegment(rs.getString("customerSegment"));
|
||||||
result.setGlAccountId(JdbcSupport.getLong(rs, "glAccountId"));
|
result.setGlAccountId(JdbcSupport.getLong(rs, "glAccountId"));
|
||||||
|
|||||||
+6
-3
@@ -22,6 +22,7 @@ package org.apache.fineract.portfolio.savings.service;
|
|||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.annualInterestRateParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.annualInterestRateParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.interestCompoundingPeriodInMonthsParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.interestCompoundingPeriodInMonthsParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.principalAmountParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.principalAmountParamName;
|
||||||
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.tenureInDaysParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.tenureInMonthsParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.tenureInMonthsParamName;
|
||||||
|
|
||||||
import com.google.gson.JsonElement;
|
import com.google.gson.JsonElement;
|
||||||
@@ -52,6 +53,7 @@ public class FixedDepositAccountInterestCalculationServiceImpl implements FixedD
|
|||||||
BigDecimal principalAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(principalAmountParamName, element);
|
BigDecimal principalAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(principalAmountParamName, element);
|
||||||
BigDecimal annualInterestRate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(annualInterestRateParamName, element);
|
BigDecimal annualInterestRate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(annualInterestRateParamName, element);
|
||||||
Long tenureInMonths = this.fromApiJsonHelper.extractLongNamed(tenureInMonthsParamName, element);
|
Long tenureInMonths = this.fromApiJsonHelper.extractLongNamed(tenureInMonthsParamName, element);
|
||||||
|
Long tenureInDays = this.fromApiJsonHelper.extractLongNamed(tenureInDaysParamName, element);
|
||||||
Long interestCompoundingPeriodInMonths = this.fromApiJsonHelper.extractLongNamed(interestCompoundingPeriodInMonthsParamName, element);
|
Long interestCompoundingPeriodInMonths = this.fromApiJsonHelper.extractLongNamed(interestCompoundingPeriodInMonthsParamName, element);
|
||||||
|
|
||||||
// Optional params for exact calendar-based calculation
|
// Optional params for exact calendar-based calculation
|
||||||
@@ -62,8 +64,8 @@ public class FixedDepositAccountInterestCalculationServiceImpl implements FixedD
|
|||||||
if (dateStr != null && dateFormat != null) {
|
if (dateStr != null && dateFormat != null) {
|
||||||
submittedOnDate = LocalDate.parse(dateStr, java.time.format.DateTimeFormatter.ofPattern(dateFormat));
|
submittedOnDate = LocalDate.parse(dateStr, java.time.format.DateTimeFormatter.ofPattern(dateFormat));
|
||||||
}
|
}
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {
|
||||||
// intentionally ignored
|
// intentionally ignored
|
||||||
}
|
}
|
||||||
Integer daysInYear = null;
|
Integer daysInYear = null;
|
||||||
final Long daysInYearLong = this.fromApiJsonHelper.extractLongNamed("daysInYear", element);
|
final Long daysInYearLong = this.fromApiJsonHelper.extractLongNamed("daysInYear", element);
|
||||||
@@ -73,7 +75,8 @@ public class FixedDepositAccountInterestCalculationServiceImpl implements FixedD
|
|||||||
BigDecimal maturityAmount;
|
BigDecimal maturityAmount;
|
||||||
if (submittedOnDate != null && daysInYear != null && interestPostingPeriodInMonths != null) {
|
if (submittedOnDate != null && daysInYear != null && interestPostingPeriodInMonths != null) {
|
||||||
// Exact calendar-based calculation — same algorithm as account creation
|
// Exact calendar-based calculation — same algorithm as account creation
|
||||||
final LocalDate maturityDate = submittedOnDate.plusMonths(tenureInMonths);
|
final LocalDate maturityDate = (tenureInDays != null && tenureInDays > 0) ? submittedOnDate.plusDays(tenureInDays)
|
||||||
|
: submittedOnDate.plusMonths(tenureInMonths);
|
||||||
maturityAmount = this.calculateExact(principalAmount, annualInterestRate, submittedOnDate, maturityDate,
|
maturityAmount = this.calculateExact(principalAmount, annualInterestRate, submittedOnDate, maturityDate,
|
||||||
interestCompoundingPeriodInMonths, interestPostingPeriodInMonths, daysInYear);
|
interestCompoundingPeriodInMonths, interestPostingPeriodInMonths, daysInYear);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+2
-2
@@ -297,7 +297,7 @@ public class SavingsConfiguration {
|
|||||||
ConfigurationDomainService configurationDomainService, HolidayRepositoryWrapper holidayRepository,
|
ConfigurationDomainService configurationDomainService, HolidayRepositoryWrapper holidayRepository,
|
||||||
WorkingDaysRepositoryWrapper workingDaysRepository,
|
WorkingDaysRepositoryWrapper workingDaysRepository,
|
||||||
DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository,
|
DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository,
|
||||||
DenominationBreakdownWriter denominationBreakdownWriter
|
DenominationBreakdownWriter denominationBreakdownWriter, JdbcTemplate jdbcTemplate
|
||||||
|
|
||||||
) {
|
) {
|
||||||
return new DepositAccountWritePlatformServiceJpaRepositoryImpl(context, savingAccountRepositoryWrapper,
|
return new DepositAccountWritePlatformServiceJpaRepositoryImpl(context, savingAccountRepositoryWrapper,
|
||||||
@@ -307,7 +307,7 @@ public class SavingsConfiguration {
|
|||||||
chargeRepository, savingsAccountChargeRepository, accountAssociationsReadPlatformService,
|
chargeRepository, savingsAccountChargeRepository, accountAssociationsReadPlatformService,
|
||||||
accountTransfersWritePlatformService, depositAccountReadPlatformService, calendarInstanceRepository,
|
accountTransfersWritePlatformService, depositAccountReadPlatformService, calendarInstanceRepository,
|
||||||
configurationDomainService, holidayRepository, workingDaysRepository, depositAccountOnHoldTransactionRepository,
|
configurationDomainService, holidayRepository, workingDaysRepository, depositAccountOnHoldTransactionRepository,
|
||||||
denominationBreakdownWriter);
|
denominationBreakdownWriter, jdbcTemplate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
+18
-94
@@ -34,23 +34,19 @@ import jakarta.ws.rs.Produces;
|
|||||||
import jakarta.ws.rs.WebApplicationException;
|
import jakarta.ws.rs.WebApplicationException;
|
||||||
import jakarta.ws.rs.core.MediaType;
|
import jakarta.ws.rs.core.MediaType;
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.RequiredArgsConstructor;
|
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.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.portfolio.sponsordd.domain.SponsorBankRepository;
|
||||||
import org.apache.fineract.useradministration.domain.AppUser;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
|
||||||
|
|
||||||
@Path("/v1/sponsorbanks")
|
@Path("/v1/sponsorbanks")
|
||||||
@Component
|
@Component
|
||||||
@@ -59,8 +55,8 @@ import org.apache.fineract.infrastructure.core.service.DateUtils;
|
|||||||
public class SponsorBankApiResource {
|
public class SponsorBankApiResource {
|
||||||
|
|
||||||
private final SponsorBankRepository sponsorBankRepository;
|
private final SponsorBankRepository sponsorBankRepository;
|
||||||
private final PlatformSecurityContext context;
|
|
||||||
private final org.springframework.jdbc.core.JdbcTemplate jdbcTemplate;
|
private final org.springframework.jdbc.core.JdbcTemplate jdbcTemplate;
|
||||||
|
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||||
private final Gson gson = new GsonBuilder().serializeNulls().create();
|
private final Gson gson = new GsonBuilder().serializeNulls().create();
|
||||||
|
|
||||||
/** Mirrors SponsorDDApiResource.safely() - see that class for the full rationale. */
|
/** Mirrors SponsorDDApiResource.safely() - see that class for the full rationale. */
|
||||||
@@ -125,78 +121,19 @@ public class SponsorBankApiResource {
|
|||||||
return names;
|
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
|
@POST
|
||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
@Operation(summary = "Create a Sponsor Bank", description = "Creates a new Sponsor Bank.")
|
@Operation(summary = "Create a Sponsor Bank", description = "Creates a new Sponsor Bank.")
|
||||||
public CommandProcessingResult createSponsorBank(final SponsorBankRequest request) {
|
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(() -> {
|
return safely(() -> {
|
||||||
if (request.getIfscCode() != null && !request.getIfscCode().isBlank()
|
final CommandWrapper commandRequest = new CommandWrapperBuilder().createSponsorBank()
|
||||||
&& !request.getIfscCode().trim().toUpperCase().matches("^[A-Z]{4}0[A-Z0-9]{6}$")) {
|
.withJson(this.gson.toJson(request)).build();
|
||||||
throw new IllegalArgumentException(
|
return this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||||
"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();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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.")
|
@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,
|
public CommandProcessingResult updateSponsorBank(@PathParam("sponsorBankId") final Long sponsorBankId,
|
||||||
final SponsorBankRequest request) {
|
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(() -> {
|
return safely(() -> {
|
||||||
final SponsorBank sponsorBank = this.sponsorBankRepository.findById(sponsorBankId)
|
final CommandWrapper commandRequest = new CommandWrapperBuilder().updateSponsorBank(sponsorBankId)
|
||||||
.orElseThrow(() -> new IllegalArgumentException("Sponsor bank not found."));
|
.withJson(this.gson.toJson(request)).build();
|
||||||
if (request.getIfscCode() != null && !request.getIfscCode().isBlank()
|
return this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||||
&& !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();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+26
-10
@@ -41,6 +41,9 @@ import java.util.Collection;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import lombok.RequiredArgsConstructor;
|
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.CommandProcessingResult;
|
||||||
import org.apache.fineract.portfolio.sponsordd.data.SponsorDDBatchIssueResult;
|
import org.apache.fineract.portfolio.sponsordd.data.SponsorDDBatchIssueResult;
|
||||||
import org.apache.fineract.portfolio.sponsordd.data.SponsorDDCashierDepositOptionData;
|
import org.apache.fineract.portfolio.sponsordd.data.SponsorDDCashierDepositOptionData;
|
||||||
@@ -76,6 +79,7 @@ public class SponsorDDApiResource {
|
|||||||
private final SponsorDDReprintRequestWritePlatformService reprintRequestWritePlatformService;
|
private final SponsorDDReprintRequestWritePlatformService reprintRequestWritePlatformService;
|
||||||
private final SponsorDDRevokeRequestWritePlatformService revokeRequestWritePlatformService;
|
private final SponsorDDRevokeRequestWritePlatformService revokeRequestWritePlatformService;
|
||||||
private final SponsorDDAmendmentRequestWritePlatformService amendmentRequestWritePlatformService;
|
private final SponsorDDAmendmentRequestWritePlatformService amendmentRequestWritePlatformService;
|
||||||
|
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||||
private final PlatformSecurityContext context;
|
private final PlatformSecurityContext context;
|
||||||
private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||||
private final Gson gson = new GsonBuilder().serializeNulls().create();
|
private final Gson gson = new GsonBuilder().serializeNulls().create();
|
||||||
@@ -539,16 +543,28 @@ public class SponsorDDApiResource {
|
|||||||
@Produces({ MediaType.APPLICATION_JSON })
|
@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.")
|
@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) {
|
public CommandProcessingResult upsertGlMapping(final GlMappingRequest request) {
|
||||||
return safely(() -> this.issueRequestWritePlatformService.upsertGlMapping(
|
// Routed through the generic command-source pipeline (CommandWrapperBuilder ->
|
||||||
request.getSponsorBankId(),
|
// logCommandSource() -> UpdateSponsorDdGlMappingCommandHandler) so this genuinely gains
|
||||||
request.getDdPayableGlAccountId(),
|
// Maker-Checker support; the write itself (issueRequestWritePlatformService.upsertGlMapping)
|
||||||
request.getChequeClearingGlAccountId(),
|
// is unchanged and now called from that handler instead of directly from here. The JSON is
|
||||||
request.getCashGlAccountId(),
|
// built by hand (not Gson-serializing `request` directly) purely to include a "locale" key -
|
||||||
request.getChargesIncomeGlAccountId(),
|
// JsonCommand's BigDecimal extractor requires one, and GlMappingRequest has no such field.
|
||||||
request.getCashSuspenseGlAccountId(),
|
return safely(() -> {
|
||||||
request.getGstPercentage(),
|
final JsonObject json = new JsonObject();
|
||||||
request.getGstPayableGlAccountId(),
|
json.addProperty("sponsorBankId", request.getSponsorBankId());
|
||||||
request.getDdLimitAmount()));
|
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
|
@POST
|
||||||
|
|||||||
+62
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+63
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+74
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.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);
|
||||||
|
}
|
||||||
+147
@@ -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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
-1
@@ -97,15 +97,34 @@ public class PermissionReadPlatformServiceImpl implements PermissionReadPlatform
|
|||||||
+ " order by p.grouping, coalesce(entity_name, ''), p.code";
|
+ " 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() {
|
public String makerCheckerablePermissionSchema() {
|
||||||
/*
|
/*
|
||||||
* get all 'Maker-Checkerable' permissions - Maintenance permissions (i.e. exclude the 'special' grouping,
|
* 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"
|
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")
|
+ " from m_permission p " + " where " + sqlGenerator.escape("grouping")
|
||||||
+ " != 'special' and code not like 'READ_%' and code not like '%\\_CHECKER'"
|
+ " != '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";
|
+ " order by p.grouping, coalesce(entity_name, ''), p.code";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+101
-1
@@ -36,7 +36,13 @@ import org.apache.commons.lang3.StringUtils;
|
|||||||
import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
|
import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
|
||||||
import org.apache.fineract.infrastructure.core.serialization.GoogleGsonSerializerHelper;
|
import org.apache.fineract.infrastructure.core.serialization.GoogleGsonSerializerHelper;
|
||||||
import org.apache.fineract.infrastructure.core.service.Page;
|
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.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.UserPendingRequest;
|
||||||
import org.apache.fineract.useradministration.domain.UserPendingRequestRepositoryWrapper;
|
import org.apache.fineract.useradministration.domain.UserPendingRequestRepositoryWrapper;
|
||||||
import org.springframework.dao.EmptyResultDataAccessException;
|
import org.springframework.dao.EmptyResultDataAccessException;
|
||||||
@@ -51,8 +57,13 @@ public class UserPendingReadPlatformServiceImpl implements UserPendingReadPlatfo
|
|||||||
|
|
||||||
private final JdbcTemplate jdbcTemplate;
|
private final JdbcTemplate jdbcTemplate;
|
||||||
private final UserPendingRequestRepositoryWrapper pendingRequestRepositoryWrapper;
|
private final UserPendingRequestRepositoryWrapper pendingRequestRepositoryWrapper;
|
||||||
|
private final OfficeRepositoryWrapper officeRepositoryWrapper;
|
||||||
|
private final StaffRepositoryWrapper staffRepositoryWrapper;
|
||||||
|
private final RoleRepository roleRepository;
|
||||||
private final Gson gson = GoogleGsonSerializerHelper.createSimpleGson();
|
private final Gson gson = GoogleGsonSerializerHelper.createSimpleGson();
|
||||||
|
|
||||||
|
private static final String UNKNOWN_REFERENCE_FORMAT = "Unknown (ID: %s)";
|
||||||
|
|
||||||
private static final String SELECT_SQL = """
|
private static final String SELECT_SQL = """
|
||||||
SELECT r.id AS id,
|
SELECT r.id AS id,
|
||||||
r.user_id AS userId,
|
r.user_id AS userId,
|
||||||
@@ -115,7 +126,96 @@ public class UserPendingReadPlatformServiceImpl implements UserPendingReadPlatfo
|
|||||||
public UserPendingRequestData retrievePendingRequest(final Long id) {
|
public UserPendingRequestData retrievePendingRequest(final Long id) {
|
||||||
final UserPendingRequest request = this.pendingRequestRepositoryWrapper.findOneWithNotFoundDetection(id);
|
final UserPendingRequest request = this.pendingRequestRepositoryWrapper.findOneWithNotFoundDetection(id);
|
||||||
final String sql = SELECT_SQL + " WHERE r.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> {
|
private static class UserPendingRowMapper implements RowMapper<UserPendingRequestData> {
|
||||||
|
|||||||
+1
-1
@@ -162,8 +162,8 @@ public class UserPendingWritePlatformServiceImpl implements UserPendingWritePlat
|
|||||||
originalSnapshot.put("officeId", userToUpdate.getOffice() != null ? userToUpdate.getOffice().getId() : null);
|
originalSnapshot.put("officeId", userToUpdate.getOffice() != null ? userToUpdate.getOffice().getId() : null);
|
||||||
originalSnapshot.put("officeName", userToUpdate.getOffice() != null ? userToUpdate.getOffice().getName() : null);
|
originalSnapshot.put("officeName", userToUpdate.getOffice() != null ? userToUpdate.getOffice().getName() : null);
|
||||||
originalSnapshot.put("staffId", userToUpdate.getStaff() != null ? userToUpdate.getStaff().getId() : 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("passwordNeverExpires", userToUpdate.getPasswordNeverExpires());
|
||||||
originalSnapshot.put("isSelfServiceUser", userToUpdate.isSelfServiceUser());
|
|
||||||
final Set<String> roleNames = new HashSet<>();
|
final Set<String> roleNames = new HashSet<>();
|
||||||
final Set<Long> roleIds = new HashSet<>();
|
final Set<Long> roleIds = new HashSet<>();
|
||||||
if (userToUpdate.getRoles() != null) {
|
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.host=${FINERACT_DEFAULT_TENANTDB_HOSTNAME:localhost}
|
||||||
fineract.tenant.port=${FINERACT_DEFAULT_TENANTDB_PORT:5432}
|
fineract.tenant.port=${FINERACT_DEFAULT_TENANTDB_PORT:5432}
|
||||||
fineract.tenant.username=${FINERACT_DEFAULT_TENANTDB_UID:postgres}
|
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.parameters=${FINERACT_DEFAULT_TENANTDB_CONN_PARAMS:}
|
||||||
fineract.tenant.timezone=${FINERACT_DEFAULT_TENANTDB_TIMEZONE:Asia/Kolkata}
|
fineract.tenant.timezone=${FINERACT_DEFAULT_TENANTDB_TIMEZONE:Asia/Kolkata}
|
||||||
fineract.tenant.identifier=${FINERACT_DEFAULT_TENANTDB_IDENTIFIER:default}
|
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.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.jdbcUrl=${FINERACT_HIKARI_JDBC_URL:jdbc:postgresql://localhost:5432/fineract_tenants}
|
||||||
spring.datasource.hikari.username=${FINERACT_HIKARI_USERNAME:postgres}
|
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.minimumIdle=${FINERACT_HIKARI_MINIMUM_IDLE:3}
|
||||||
spring.datasource.hikari.maximumPoolSize=${FINERACT_HIKARI_MAXIMUM_POOL_SIZE:10}
|
spring.datasource.hikari.maximumPoolSize=${FINERACT_HIKARI_MAXIMUM_POOL_SIZE:10}
|
||||||
spring.datasource.hikari.idleTimeout=${FINERACT_HIKARI_IDLE_TIMEOUT:60000}
|
spring.datasource.hikari.idleTimeout=${FINERACT_HIKARI_IDLE_TIMEOUT:60000}
|
||||||
|
|||||||
@@ -116,4 +116,17 @@
|
|||||||
<include file="parts/0568_add_settlement_mode_to_clerk_share_withdrawal_request.xml" relativeToChangelogFile="true"/>
|
<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/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/0570_add_dd_payable_gl_to_sponsor_bank.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0571_index_savings_account_deposit_type_status_and_maturity_date.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0572_add_renewal_grace_period_to_deposit_product_term.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0573_add_renewal_grace_period_to_saving_product_temp.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0574_add_renewal_grace_period_to_deposit_account_term.xml" relativeToChangelogFile="true"/>
|
||||||
|
<!-- NOTE: numbered 0571/0572 like the dev-latest FD migrations directly above - both were added
|
||||||
|
independently (this Maker-Checker work on feature/bugfix, the FD work on dev-latest) before
|
||||||
|
either branch saw the other's changes, so the numeric prefixes collide. Left as originally
|
||||||
|
named (not renumbered) since only the changelog include list, not the migration files
|
||||||
|
themselves, is in scope for this merge; Liquibase identifies changesets by id+author+file
|
||||||
|
path, not by filename number, so the collision is cosmetic only and does not affect
|
||||||
|
correctness. -->
|
||||||
|
<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>
|
</databaseChangeLog>
|
||||||
|
|||||||
+45
@@ -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>
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
<?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">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
The FD Dashboard's maturity-dashboard endpoint filters m_savings_account by
|
||||||
|
(deposit_type_enum = 200, status_enum = 300) and orders by
|
||||||
|
m_deposit_account_term_and_preclosure.maturity_date. Neither column was indexed, confirmed
|
||||||
|
by querying pg_indexes directly before adding this, so both were full table scans, which
|
||||||
|
only gets slower as the FD book grows. These two indexes are what actually makes that
|
||||||
|
endpoint fast; the endpoint's own SQL was already written lean, but a lean query still scans
|
||||||
|
the whole table without something to seek on.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<changeSet id="0571_index_savings_account_deposit_type_status" author="CBS">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><indexExists indexName="idx_msa_deposit_type_status"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<createIndex tableName="m_savings_account" indexName="idx_msa_deposit_type_status">
|
||||||
|
<column name="deposit_type_enum"/>
|
||||||
|
<column name="status_enum"/>
|
||||||
|
</createIndex>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="0571_index_deposit_account_term_preclosure_maturity_date" author="CBS">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><indexExists indexName="idx_datp_maturity_date"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<createIndex tableName="m_deposit_account_term_and_preclosure" indexName="idx_datp_maturity_date">
|
||||||
|
<column name="maturity_date"/>
|
||||||
|
</createIndex>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
<?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">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Days after maturity a depositor can still walk in and have their FD renewed backdated to the
|
||||||
|
original maturity date, at the currently active rate. Null/0 (every existing product, since
|
||||||
|
this column is brand new) means "no grace period configured", so that product keeps the
|
||||||
|
instant, same-night auto-renewal it always had; see
|
||||||
|
DepositAccountWritePlatformServiceJpaRepositoryImpl#updateMaturityDetails, which only skips
|
||||||
|
the automatic renewal when this is a positive value. So adding this column changes nothing
|
||||||
|
for any product until an admin explicitly sets a grace period on it.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<changeSet id="0572_add_renewal_grace_period_to_deposit_product_term" author="CBS">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><columnExists tableName="m_deposit_product_term_and_preclosure" columnName="renewal_grace_period_days"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<addColumn tableName="m_deposit_product_term_and_preclosure">
|
||||||
|
<column name="renewal_grace_period_days" type="INTEGER"/>
|
||||||
|
</addColumn>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
+44
@@ -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>
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
<?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">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
m_saving_product_temp is a full field-by-field mirror of the product tables, used by the
|
||||||
|
maker-checker "pending product edit" flow (ProductPendingEditFieldMapper /
|
||||||
|
FixedDepositEditFields, an @Embeddable mapped directly onto this table). It is a separate
|
||||||
|
storage path from the live product tables migration 0572 added a column to, so
|
||||||
|
renewalGracePeriodDays needs its own column here too, or a governed FD/RD product edit that
|
||||||
|
sets it silently drops the value: the checker's "Proposed changes" screen never shows it,
|
||||||
|
and approving the edit never carries it forward, even though the value was actually typed in
|
||||||
|
and submitted.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<changeSet id="0573_add_renewal_grace_period_to_saving_product_temp" author="CBS">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><columnExists tableName="m_saving_product_temp" columnName="renewal_grace_period_days"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<addColumn tableName="m_saving_product_temp">
|
||||||
|
<column name="renewal_grace_period_days" type="INTEGER"/>
|
||||||
|
</addColumn>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
<?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">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
DepositTermDetail (the @Embeddable that 0572 added renewal_grace_period_days to) is embedded
|
||||||
|
by BOTH DepositProductTermAndPreClosure (m_deposit_product_term_and_preclosure, the product's
|
||||||
|
own term settings, covered by 0572) AND DepositAccountTermAndPreClosure
|
||||||
|
(m_deposit_account_term_and_preclosure, one row per FD/RD account, created fresh on every
|
||||||
|
account submission). An @Embeddable's columns are expected on every table it's embedded into,
|
||||||
|
so without this column here too, FD account creation fails outright with a Postgres "column
|
||||||
|
renewal_grace_period_days does not exist" error on the INSERT into this table, confirmed by a
|
||||||
|
live devRun stack trace, not a hypothetical. Nullable, no default: existing/new account rows
|
||||||
|
simply don't carry a grace period of their own (the product's value is what actually gates the
|
||||||
|
nightly job and the Renew action), so this column is unused today but has to exist for
|
||||||
|
EclipseLink's generated INSERT/UPDATE statements to succeed at all.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<changeSet id="0574_add_renewal_grace_period_to_deposit_account_term" author="CBS">
|
||||||
|
<preConditions onFail="MARK_RAN">
|
||||||
|
<not><columnExists tableName="m_deposit_account_term_and_preclosure" columnName="renewal_grace_period_days"/></not>
|
||||||
|
</preConditions>
|
||||||
|
<addColumn tableName="m_deposit_account_term_and_preclosure">
|
||||||
|
<column name="renewal_grace_period_days" type="INTEGER"/>
|
||||||
|
</addColumn>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
+4
@@ -496,6 +496,7 @@ public final class FixedDepositProductData extends DepositProductData {
|
|||||||
private Boolean accountClosedZeroBalanceInDayClosing;
|
private Boolean accountClosedZeroBalanceInDayClosing;
|
||||||
private String balanceFrequency;
|
private String balanceFrequency;
|
||||||
private Boolean isPendingApproval;
|
private Boolean isPendingApproval;
|
||||||
|
private Integer renewalGracePeriodDays;
|
||||||
|
|
||||||
// Template option setters
|
// Template option setters
|
||||||
public void setInterestCalculationTypeOption(Collection<org.apache.fineract.infrastructure.core.data.EnumOptionData> v) { this.interestCalculationTypeOption = v; }
|
public void setInterestCalculationTypeOption(Collection<org.apache.fineract.infrastructure.core.data.EnumOptionData> v) { this.interestCalculationTypeOption = v; }
|
||||||
@@ -543,6 +544,7 @@ public final class FixedDepositProductData extends DepositProductData {
|
|||||||
public void setAccountClosedZeroBalanceInDayClosing(Boolean v) { this.accountClosedZeroBalanceInDayClosing = v; }
|
public void setAccountClosedZeroBalanceInDayClosing(Boolean v) { this.accountClosedZeroBalanceInDayClosing = v; }
|
||||||
public void setBalanceFrequency(String v) { this.balanceFrequency = v; }
|
public void setBalanceFrequency(String v) { this.balanceFrequency = v; }
|
||||||
public void setIsPendingApproval(Boolean v) { this.isPendingApproval = v; }
|
public void setIsPendingApproval(Boolean v) { this.isPendingApproval = v; }
|
||||||
|
public void setRenewalGracePeriodDays(Integer v) { this.renewalGracePeriodDays = v; }
|
||||||
|
|
||||||
// CBS field getters
|
// CBS field getters
|
||||||
public String getProductCode() { return productCode; }
|
public String getProductCode() { return productCode; }
|
||||||
@@ -559,6 +561,7 @@ public final class FixedDepositProductData extends DepositProductData {
|
|||||||
public Long getDaysToInactive() { return daysToInactive; }
|
public Long getDaysToInactive() { return daysToInactive; }
|
||||||
public Long getDaysToDormancy() { return daysToDormancy; }
|
public Long getDaysToDormancy() { return daysToDormancy; }
|
||||||
public Long getDaysToEscheat() { return daysToEscheat; }
|
public Long getDaysToEscheat() { return daysToEscheat; }
|
||||||
|
public Integer getRenewalGracePeriodDays() { return renewalGracePeriodDays; }
|
||||||
|
|
||||||
public void copyCbsFieldsFrom(FixedDepositProductData src) {
|
public void copyCbsFieldsFrom(FixedDepositProductData src) {
|
||||||
if (src == null) return;
|
if (src == null) return;
|
||||||
@@ -605,6 +608,7 @@ public final class FixedDepositProductData extends DepositProductData {
|
|||||||
this.accountClosedZeroBalanceInDayClosing = src.accountClosedZeroBalanceInDayClosing;
|
this.accountClosedZeroBalanceInDayClosing = src.accountClosedZeroBalanceInDayClosing;
|
||||||
this.balanceFrequency = src.balanceFrequency;
|
this.balanceFrequency = src.balanceFrequency;
|
||||||
this.withdrawalFeeForTransfers = src.withdrawalFeeForTransfers;
|
this.withdrawalFeeForTransfers = src.withdrawalFeeForTransfers;
|
||||||
|
this.renewalGracePeriodDays = src.renewalGracePeriodDays;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Boolean withdrawalFeeForTransfers;
|
private Boolean withdrawalFeeForTransfers;
|
||||||
|
|||||||
+16
-4
@@ -36,6 +36,7 @@ import static org.apache.fineract.portfolio.savings.DepositsApiConstants.minDepo
|
|||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.preClosurePenalApplicableParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.preClosurePenalApplicableParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.preClosurePenalInterestOnTypeIdParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.preClosurePenalInterestOnTypeIdParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.preClosurePenalInterestParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.preClosurePenalInterestParamName;
|
||||||
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.renewalGracePeriodDaysParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.chargesParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.chargesParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.currencyCodeParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.currencyCodeParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.descriptionParamName;
|
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.descriptionParamName;
|
||||||
@@ -473,8 +474,10 @@ public class DepositProductAssembler {
|
|||||||
final SavingsPeriodFrequencyType inMultiplesOfDepositTermType = (inMultiplesOfDepositTermTypeId == null) ? null
|
final SavingsPeriodFrequencyType inMultiplesOfDepositTermType = (inMultiplesOfDepositTermTypeId == null) ? null
|
||||||
: SavingsPeriodFrequencyType.fromInt(inMultiplesOfDepositTermTypeId);
|
: SavingsPeriodFrequencyType.fromInt(inMultiplesOfDepositTermTypeId);
|
||||||
|
|
||||||
return DepositTermDetail.createFrom(minDepositTerm, maxDepositTerm, minDepositTermType, maxDepositTermType,
|
final DepositTermDetail depositTermDetail = DepositTermDetail.createFrom(minDepositTerm, maxDepositTerm, minDepositTermType,
|
||||||
inMultiplesOfDepositTerm, inMultiplesOfDepositTermType);
|
maxDepositTermType, inMultiplesOfDepositTerm, inMultiplesOfDepositTermType);
|
||||||
|
depositTermDetail.setRenewalGracePeriodDays(command.integerValueOfParameterNamed(renewalGracePeriodDaysParamName));
|
||||||
|
return depositTermDetail;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DepositTermDetail assembleDepositTermDetail(final JsonCommand command, final DepositTermDetail prodDepositTermDetail) {
|
public DepositTermDetail assembleDepositTermDetail(final JsonCommand command, final DepositTermDetail prodDepositTermDetail) {
|
||||||
@@ -531,8 +534,17 @@ public class DepositProductAssembler {
|
|||||||
final SavingsPeriodFrequencyType inMultiplesOfDepositTermType = (inMultiplesOfDepositTermTypeId == null) ? null
|
final SavingsPeriodFrequencyType inMultiplesOfDepositTermType = (inMultiplesOfDepositTermTypeId == null) ? null
|
||||||
: SavingsPeriodFrequencyType.fromInt(inMultiplesOfDepositTermTypeId);
|
: SavingsPeriodFrequencyType.fromInt(inMultiplesOfDepositTermTypeId);
|
||||||
|
|
||||||
return DepositTermDetail.createFrom(minDepositTerm, maxDepositTerm, minDepositTermType, maxDepositTermType,
|
Integer renewalGracePeriodDays = null;
|
||||||
inMultiplesOfDepositTerm, inMultiplesOfDepositTermType);
|
if (command.parameterExists(renewalGracePeriodDaysParamName)) {
|
||||||
|
renewalGracePeriodDays = command.integerValueOfParameterNamed(renewalGracePeriodDaysParamName);
|
||||||
|
} else if (prodDepositTermDetail != null) {
|
||||||
|
renewalGracePeriodDays = prodDepositTermDetail.renewalGracePeriodDays();
|
||||||
|
}
|
||||||
|
|
||||||
|
final DepositTermDetail depositTermDetail = DepositTermDetail.createFrom(minDepositTerm, maxDepositTerm, minDepositTermType,
|
||||||
|
maxDepositTermType, inMultiplesOfDepositTerm, inMultiplesOfDepositTermType);
|
||||||
|
depositTermDetail.setRenewalGracePeriodDays(renewalGracePeriodDays);
|
||||||
|
return depositTermDetail;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DepositRecurringDetail assembleRecurringDetail(final JsonCommand command) {
|
public DepositRecurringDetail assembleRecurringDetail(final JsonCommand command) {
|
||||||
|
|||||||
+27
-2
@@ -24,6 +24,7 @@ import static org.apache.fineract.portfolio.savings.DepositsApiConstants.maxDepo
|
|||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.maxDepositTermTypeIdParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.maxDepositTermTypeIdParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.minDepositTermParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.minDepositTermParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.minDepositTermTypeIdParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.minDepositTermTypeIdParamName;
|
||||||
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.renewalGracePeriodDaysParamName;
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
import jakarta.persistence.Column;
|
||||||
import jakarta.persistence.Embeddable;
|
import jakarta.persistence.Embeddable;
|
||||||
@@ -61,6 +62,14 @@ public class DepositTermDetail {
|
|||||||
@Column(name = "in_multiples_of_deposit_term_type_enum", nullable = true)
|
@Column(name = "in_multiples_of_deposit_term_type_enum", nullable = true)
|
||||||
private Integer inMultiplesOfDepositTermType;
|
private Integer inMultiplesOfDepositTermType;
|
||||||
|
|
||||||
|
// Days after maturity a depositor can still walk in and renew backdated to the original maturity
|
||||||
|
// date, at the currently active rate. Null/0 means this product keeps the instant, same-night
|
||||||
|
// auto-renewal every FD product had before this field existed -- see
|
||||||
|
// DepositAccountWritePlatformServiceJpaRepositoryImpl#updateMaturityDetails, which only skips the
|
||||||
|
// automatic renewal when this is a positive value.
|
||||||
|
@Column(name = "renewal_grace_period_days", nullable = true)
|
||||||
|
private Integer renewalGracePeriodDays;
|
||||||
|
|
||||||
public static DepositTermDetail createFrom(final Integer minDepositTerm, final Integer maxDepositTerm,
|
public static DepositTermDetail createFrom(final Integer minDepositTerm, final Integer maxDepositTerm,
|
||||||
final SavingsPeriodFrequencyType minDepositTermType, final SavingsPeriodFrequencyType maxDepositTermType,
|
final SavingsPeriodFrequencyType minDepositTermType, final SavingsPeriodFrequencyType maxDepositTermType,
|
||||||
final Integer inMultiplesOfDepositTerm, final SavingsPeriodFrequencyType inMultiplesOfDepositTermType) {
|
final Integer inMultiplesOfDepositTerm, final SavingsPeriodFrequencyType inMultiplesOfDepositTermType) {
|
||||||
@@ -123,6 +132,12 @@ public class DepositTermDetail {
|
|||||||
this.inMultiplesOfDepositTermType = newValue;
|
this.inMultiplesOfDepositTermType = newValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (command.isChangeInIntegerParameterNamed(renewalGracePeriodDaysParamName, this.renewalGracePeriodDays)) {
|
||||||
|
final Integer newValue = command.integerValueOfParameterNamed(renewalGracePeriodDaysParamName);
|
||||||
|
actualChanges.put(renewalGracePeriodDaysParamName, newValue);
|
||||||
|
this.renewalGracePeriodDays = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
if (isMinDepositTermGreaterThanMaxDepositTerm()) {
|
if (isMinDepositTermGreaterThanMaxDepositTerm()) {
|
||||||
baseDataValidator.parameter(minDepositTermParamName).value(this.minDepositTerm).failWithCode(".greater.than.maxDepositTerm",
|
baseDataValidator.parameter(minDepositTermParamName).value(this.minDepositTerm).failWithCode(".greater.than.maxDepositTerm",
|
||||||
this.minDepositTerm, this.maxDepositTerm);
|
this.minDepositTerm, this.maxDepositTerm);
|
||||||
@@ -166,6 +181,14 @@ public class DepositTermDetail {
|
|||||||
return this.inMultiplesOfDepositTermType;
|
return this.inMultiplesOfDepositTermType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Integer renewalGracePeriodDays() {
|
||||||
|
return this.renewalGracePeriodDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRenewalGracePeriodDays(final Integer renewalGracePeriodDays) {
|
||||||
|
this.renewalGracePeriodDays = renewalGracePeriodDays;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isDepositBetweenMinAndMax(LocalDate depositStartDate, LocalDate depositEndDate) {
|
public boolean isDepositBetweenMinAndMax(LocalDate depositStartDate, LocalDate depositEndDate) {
|
||||||
return isEqualOrGreaterThanMin(depositStartDate, depositEndDate) && isEqualOrLessThanMax(depositStartDate, depositEndDate);
|
return isEqualOrGreaterThanMin(depositStartDate, depositEndDate) && isEqualOrLessThanMax(depositStartDate, depositEndDate);
|
||||||
}
|
}
|
||||||
@@ -260,7 +283,9 @@ public class DepositTermDetail {
|
|||||||
final SavingsPeriodFrequencyType inMultiplesOfDepositTermType = SavingsPeriodFrequencyType
|
final SavingsPeriodFrequencyType inMultiplesOfDepositTermType = SavingsPeriodFrequencyType
|
||||||
.fromInt(this.inMultiplesOfDepositTermType());
|
.fromInt(this.inMultiplesOfDepositTermType());
|
||||||
|
|
||||||
return DepositTermDetail.createFrom(minDepositTerm, maxDepositTerm, minDepositTermType, maxDepositTermType,
|
final DepositTermDetail copy = DepositTermDetail.createFrom(minDepositTerm, maxDepositTerm, minDepositTermType,
|
||||||
inMultiplesOfDepositTerm, inMultiplesOfDepositTermType);
|
maxDepositTermType, inMultiplesOfDepositTerm, inMultiplesOfDepositTermType);
|
||||||
|
copy.renewalGracePeriodDays = this.renewalGracePeriodDays;
|
||||||
|
return copy;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -68,4 +68,7 @@ public class FixedDepositEditFields {
|
|||||||
|
|
||||||
@Column(name = "deposit_amount")
|
@Column(name = "deposit_amount")
|
||||||
private BigDecimal depositAmount;
|
private BigDecimal depositAmount;
|
||||||
|
|
||||||
|
@Column(name = "renewal_grace_period_days")
|
||||||
|
private Integer renewalGracePeriodDays;
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -942,6 +942,10 @@ public final class ProductPendingEditFieldMapper {
|
|||||||
f.setDepositAmount(command.bigDecimalValueOfParameterNamed("depositAmount"));
|
f.setDepositAmount(command.bigDecimalValueOfParameterNamed("depositAmount"));
|
||||||
present.add("depositAmount");
|
present.add("depositAmount");
|
||||||
}
|
}
|
||||||
|
if (command.hasParameter("renewalGracePeriodDays")) {
|
||||||
|
f.setRenewalGracePeriodDays(command.integerValueOfParameterNamed("renewalGracePeriodDays"));
|
||||||
|
present.add("renewalGracePeriodDays");
|
||||||
|
}
|
||||||
return present;
|
return present;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -962,6 +966,7 @@ public final class ProductPendingEditFieldMapper {
|
|||||||
if (f.getMinDepositAmount() != null) putNumber(root, submitted, "minDepositAmount", f.getMinDepositAmount());
|
if (f.getMinDepositAmount() != null) putNumber(root, submitted, "minDepositAmount", f.getMinDepositAmount());
|
||||||
if (f.getMaxDepositAmount() != null) putNumber(root, submitted, "maxDepositAmount", f.getMaxDepositAmount());
|
if (f.getMaxDepositAmount() != null) putNumber(root, submitted, "maxDepositAmount", f.getMaxDepositAmount());
|
||||||
if (f.getDepositAmount() != null) putNumber(root, submitted, "depositAmount", f.getDepositAmount());
|
if (f.getDepositAmount() != null) putNumber(root, submitted, "depositAmount", f.getDepositAmount());
|
||||||
|
if (f.getRenewalGracePeriodDays() != null) putNumber(root, submitted, "renewalGracePeriodDays", f.getRenewalGracePeriodDays());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Set<String> populateRecurringDepositFields(final JsonCommand command, final RecurringDepositEditFields f) {
|
public static Set<String> populateRecurringDepositFields(final JsonCommand command, final RecurringDepositEditFields f) {
|
||||||
|
|||||||
+22
@@ -92,5 +92,27 @@ public interface DepositAccountWritePlatformService {
|
|||||||
|
|
||||||
void updateMaturityDetails(Long depositAccountId, DepositAccountType depositAccountType);
|
void updateMaturityDetails(Long depositAccountId, DepositAccountType depositAccountType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manually renews a matured FD from the FD Dashboard -- backdated to the original maturity date if
|
||||||
|
* still within the product's renewal grace window (0 days if none is configured), or from today
|
||||||
|
* with a gap-period interest credit (at the linked Savings account's product rate) if not.
|
||||||
|
* {@code reinvestType} is the staff's choice made at renewal time (300 = Reinvest Principal and
|
||||||
|
* Interest, 400 = Reinvest Principal Only -- the rest of the balance is paid to the FD's linked
|
||||||
|
* Savings account); the account's own stored maturity instruction, if any, is not consulted.
|
||||||
|
* Returns a small result map: renewedAccountId, maturityDate, maturityAmount, backdated (boolean),
|
||||||
|
* gapInterestAmount (BigDecimal, present only when backdated is false).
|
||||||
|
*/
|
||||||
|
Map<String, Object> renewMaturedFixedDeposit(Long fdAccountId, Integer reinvestType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Credits gap-period interest (simple interest, at the linked Savings/Current account's product
|
||||||
|
* rate) onto a matured FD if its product's renewal grace window has passed -- a no-op, returning
|
||||||
|
* zero, if still within it. Called as a pre-step before the FD Dashboard's Renew action submits the
|
||||||
|
* generic `payment` command (the same one the account's own Account Actions menu uses), so every
|
||||||
|
* closure type is available from the Dashboard, not just the two {@link #renewMaturedFixedDeposit}
|
||||||
|
* itself supports. Returns {@code withinGrace} (boolean) and {@code gapInterestAmount} (BigDecimal).
|
||||||
|
*/
|
||||||
|
Map<String, Object> creditGapInterestIfPastGrace(Long fdAccountId);
|
||||||
|
|
||||||
SavingsAccountTransaction mandatorySavingsAccountDeposit(SavingsAccountTransactionDTO accountTransactionDTO);
|
SavingsAccountTransaction mandatorySavingsAccountDeposit(SavingsAccountTransactionDTO accountTransactionDTO);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user