FINERACT-2238: New command processing - Business Date Management (org.apache.fineract.infrastructure.businessdate)

This commit is contained in:
viktorpavlenko
2025-06-26 00:49:05 +02:00
committed by Aleksandar Vidakovic
parent d5958f36c7
commit bb46397daa
81 changed files with 1385 additions and 1099 deletions
@@ -20,30 +20,26 @@ package org.apache.fineract.infrastructure.businessdate.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.HeaderParam;
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.List;
import java.util.UUID;
import java.util.function.Supplier;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
import org.apache.fineract.infrastructure.businessdate.data.BusinessDateData;
import org.apache.fineract.infrastructure.businessdate.data.request.BusinessDateRequest;
import org.apache.fineract.command.core.CommandPipeline;
import org.apache.fineract.infrastructure.businessdate.command.BusinessDateUpdateCommand;
import org.apache.fineract.infrastructure.businessdate.data.BusinessDateResponse;
import org.apache.fineract.infrastructure.businessdate.data.BusinessDateUpdateRequest;
import org.apache.fineract.infrastructure.businessdate.service.BusinessDateReadPlatformService;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.springframework.stereotype.Component;
@RequiredArgsConstructor
@@ -52,19 +48,14 @@ import org.springframework.stereotype.Component;
@Tag(name = "Business Date Management", description = "Business date management enables you to set up, fetch and adjust organisation business dates")
public class BusinessDateApiResource {
private static final String BUSINESS_DATE = "BUSINESS_DATE";
private final PlatformSecurityContext securityContext;
private final DefaultToApiJsonSerializer<BusinessDateData> jsonSerializer;
private final BusinessDateReadPlatformService readPlatformService;
private final PortfolioCommandSourceWritePlatformService commandWritePlatformService;
private final CommandPipeline commandPipeline;
@GET
@Consumes({ MediaType.TEXT_HTML, MediaType.APPLICATION_JSON })
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "List all business dates", description = "")
public List<BusinessDateData> getBusinessDates() {
securityContext.authenticatedUser().validateHasReadPermission(BUSINESS_DATE);
public List<BusinessDateResponse> getBusinessDates() {
return this.readPlatformService.findAll();
}
@@ -73,8 +64,7 @@ public class BusinessDateApiResource {
@Consumes({ MediaType.TEXT_HTML, MediaType.APPLICATION_JSON })
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Retrieve a specific Business date", description = "")
public BusinessDateData getBusinessDate(@PathParam("type") @Parameter(description = "type") final String type) {
securityContext.authenticatedUser().validateHasReadPermission(BUSINESS_DATE);
public BusinessDateResponse getBusinessDate(@PathParam("type") @Parameter(description = "type") final String type) {
return this.readPlatformService.findByType(type);
}
@@ -82,14 +72,19 @@ public class BusinessDateApiResource {
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Update Business Date", description = "")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = BusinessDateRequest.class)))
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = BusinessDateApiResourceSwagger.BusinessDateResponse.class))) })
public CommandProcessingResult updateBusinessDate(BusinessDateRequest businessDateRequest) {
securityContext.authenticatedUser().validateHasUpdatePermission(BUSINESS_DATE);
final CommandWrapper commandRequest = new CommandWrapperBuilder().updateBusinessDate()
.withJson(jsonSerializer.serialize(businessDateRequest)).build();
return commandWritePlatformService.logCommandSource(commandRequest);
public BusinessDateResponse updateBusinessDate(@HeaderParam("Idempotency-Key") String idempotencyKey,
@Valid BusinessDateUpdateRequest request) {
final var command = new BusinessDateUpdateCommand();
command.setId(UUID.randomUUID());
command.setIdempotencyKey(idempotencyKey);
command.setCreatedAt(DateUtils.getAuditOffsetDateTime());
command.setPayload(request);
final Supplier<BusinessDateResponse> response = commandPipeline.send(command);
return response.get();
}
}
@@ -1,41 +0,0 @@
/**
* 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.infrastructure.businessdate.api;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.Map;
final class BusinessDateApiResourceSwagger {
private BusinessDateApiResourceSwagger() {
}
@Schema(description = "BusinessDateResponse")
public static final class BusinessDateResponse {
@Schema(example = "1")
public Long commandId;
public Map<String, Object> changes;
private BusinessDateResponse() {
}
}
}
@@ -16,17 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.businessdate.data.request;
package org.apache.fineract.infrastructure.businessdate.command;
import java.io.Serial;
import java.io.Serializable;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.fineract.command.core.Command;
import org.apache.fineract.infrastructure.businessdate.data.BusinessDateUpdateRequest;
public record BusinessDateRequest(String dateFormat, String type, String date, String locale) implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
public static BusinessDateRequest ofNull() {
return new BusinessDateRequest(null, null, null, null);
}
}
@Data
@EqualsAndHashCode(callSuper = true)
public class BusinessDateUpdateCommand extends Command<BusinessDateUpdateRequest> {}
@@ -21,24 +21,43 @@ package org.apache.fineract.infrastructure.businessdate.data;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.Map;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
@Builder
@Data
@NoArgsConstructor
@Accessors(chain = true)
public class BusinessDateData implements Serializable {
@AllArgsConstructor
public class BusinessDateResponse implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private String description;
private String type;
private LocalDate date;
public static BusinessDateData instance(BusinessDateType businessDateType, LocalDate value) {
return new BusinessDateData().setType(businessDateType.getName()).setDescription(businessDateType.getDescription()).setDate(value);
private String description;
private BusinessDateType type;
private LocalDate date;
private Map<BusinessDateType, LocalDate> changes;
public void addChange(final BusinessDateType businessDateType, final LocalDate date) {
if (this.changes == null) {
this.changes = new HashMap<>();
}
changes.put(businessDateType, date);
}
public void addAllChanges(final Map<BusinessDateType, LocalDate> changes) {
if (changes == null || changes.isEmpty()) {
return;
}
for (final Map.Entry<BusinessDateType, LocalDate> entry : changes.entrySet()) {
addChange(entry.getKey(), entry.getValue());
}
}
}
@@ -0,0 +1,52 @@
/**
* 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.infrastructure.businessdate.data;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.io.Serial;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.validation.constraints.LocalDate;
import org.apache.fineract.validation.constraints.Locale;
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
@LocalDate(dateField = "date", formatField = "dateFormat", localeField = "locale")
public class BusinessDateUpdateRequest implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@NotBlank(message = "{org.apache.fineract.businessdate.date-format.not-blank}")
private String dateFormat;
@NotNull(message = "{org.apache.fineract.businessdate.type.not-null}")
private BusinessDateType type;
@NotBlank(message = "{org.apache.fineract.businessdate.date.not-blank}")
private String date;
@NotBlank(message = "{org.apache.fineract.businessdate.locale.not-blank}")
@Locale
private String locale;
}
@@ -18,26 +18,26 @@
*/
package org.apache.fineract.infrastructure.businessdate.handler;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.command.core.Command;
import org.apache.fineract.command.core.CommandHandler;
import org.apache.fineract.infrastructure.businessdate.data.BusinessDateResponse;
import org.apache.fineract.infrastructure.businessdate.data.BusinessDateUpdateRequest;
import org.apache.fineract.infrastructure.businessdate.service.BusinessDateWritePlatformService;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.springframework.stereotype.Service;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Component
@RequiredArgsConstructor
@Service
@CommandType(entity = "BUSINESS_DATE", action = "UPDATE")
public class BusinessDateUpdateHandler implements NewCommandSourceHandler {
public class BusinessDateUpdateHandler implements CommandHandler<BusinessDateUpdateRequest, BusinessDateResponse> {
private final BusinessDateWritePlatformService businessDateWritePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(@NotNull final JsonCommand command) {
return businessDateWritePlatformService.updateBusinessDate(command);
public BusinessDateResponse handle(Command<BusinessDateUpdateRequest> command) {
return businessDateWritePlatformService.updateBusinessDate(command.getPayload());
}
}
@@ -19,7 +19,7 @@
package org.apache.fineract.infrastructure.businessdate.mapper;
import java.util.List;
import org.apache.fineract.infrastructure.businessdate.data.BusinessDateData;
import org.apache.fineract.infrastructure.businessdate.data.BusinessDateResponse;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDate;
import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig;
import org.mapstruct.Mapper;
@@ -29,8 +29,8 @@ import org.mapstruct.Mappings;
@Mapper(config = MapstructMapperConfig.class)
public interface BusinessDateMapper {
@Mappings({ @Mapping(target = "description", source = "source.type.description") })
BusinessDateData map(BusinessDate source);
@Mappings({ @Mapping(target = "description", source = "source.type.description"), @Mapping(target = "changes", ignore = true) })
BusinessDateResponse map(BusinessDate source);
List<BusinessDateData> map(List<BusinessDate> sources);
List<BusinessDateResponse> map(List<BusinessDate> sources);
}
@@ -0,0 +1,35 @@
/**
* 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.infrastructure.businessdate.mapper;
import org.apache.fineract.infrastructure.businessdate.data.BusinessDateResponse;
import org.apache.fineract.infrastructure.businessdate.data.BusinessDateUpdateRequest;
import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper(config = MapstructMapperConfig.class)
public interface BusinessDateUpdateRequestMapper {
@Mapping(target = "description", expression = "java(source.getType().getDescription())")
@Mapping(source = "type", target = "type")
@Mapping(target = "date", expression = "java(org.apache.fineract.infrastructure.core.service.DateUtils.toLocalDate(source.getLocale(), source.getDate(), source.getDateFormat()))")
@Mapping(target = "changes", ignore = true)
BusinessDateResponse map(BusinessDateUpdateRequest source);
}
@@ -21,14 +21,14 @@ package org.apache.fineract.infrastructure.businessdate.service;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.List;
import org.apache.fineract.infrastructure.businessdate.data.BusinessDateData;
import org.apache.fineract.infrastructure.businessdate.data.BusinessDateResponse;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
public interface BusinessDateReadPlatformService {
List<BusinessDateData> findAll();
List<BusinessDateResponse> findAll();
BusinessDateData findByType(String type);
BusinessDateResponse findByType(String type);
HashMap<BusinessDateType, LocalDate> getBusinessDates();
}
@@ -24,7 +24,7 @@ import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.businessdate.data.BusinessDateData;
import org.apache.fineract.infrastructure.businessdate.data.BusinessDateResponse;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDate;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateRepository;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
@@ -40,17 +40,17 @@ import org.springframework.stereotype.Service;
public class BusinessDateReadPlatformServiceImpl implements BusinessDateReadPlatformService {
private final BusinessDateRepository repository;
private final BusinessDateMapper mapper;
private final BusinessDateMapper businessDatemapper;
private final ConfigurationDomainService configurationDomainService;
@Override
public List<BusinessDateData> findAll() {
public List<BusinessDateResponse> findAll() {
List<BusinessDate> businessDateList = repository.findAll();
return mapper.map(businessDateList);
return businessDatemapper.map(businessDateList);
}
@Override
public BusinessDateData findByType(String type) {
public BusinessDateResponse findByType(String type) {
BusinessDateType businessDateType;
try {
businessDateType = BusinessDateType.valueOf(type);
@@ -63,7 +63,7 @@ public class BusinessDateReadPlatformServiceImpl implements BusinessDateReadPlat
log.error("Business date with the provided type cannot be found {}", type);
throw BusinessDateNotFoundException.notFound(type);
}
return mapper.map(businessDate.get());
return businessDatemapper.map(businessDate.get());
}
@Override
@@ -73,9 +73,9 @@ public class BusinessDateReadPlatformServiceImpl implements BusinessDateReadPlat
businessDateMap.put(BusinessDateType.BUSINESS_DATE, tenantDate);
businessDateMap.put(BusinessDateType.COB_DATE, tenantDate);
if (configurationDomainService.isBusinessDateEnabled()) {
final List<BusinessDateData> businessDateDataList = this.findAll();
for (BusinessDateData businessDateData : businessDateDataList) {
businessDateMap.put(BusinessDateType.valueOf(businessDateData.getType()), businessDateData.getDate());
final List<BusinessDateResponse> businessDateResponseList = this.findAll();
for (BusinessDateResponse businessDateResponse : businessDateResponseList) {
businessDateMap.put(businessDateResponse.getType(), businessDateResponse.getDate());
}
}
return businessDateMap;
@@ -18,19 +18,14 @@
*/
package org.apache.fineract.infrastructure.businessdate.service;
import java.util.Map;
import org.apache.fineract.infrastructure.businessdate.data.BusinessDateData;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.businessdate.data.BusinessDateResponse;
import org.apache.fineract.infrastructure.businessdate.data.BusinessDateUpdateRequest;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
public interface BusinessDateWritePlatformService {
CommandProcessingResult updateBusinessDate(JsonCommand command);
BusinessDateResponse updateBusinessDate(BusinessDateUpdateRequest request);
void adjustDate(BusinessDateData data, Map<String, Object> changes);
void increaseCOBDateByOneDay() throws JobExecutionException;
void increaseBusinessDateByOneDay() throws JobExecutionException;
void increaseDateByTypeByOneDay(BusinessDateType businessDateType) throws JobExecutionException;
}
@@ -18,27 +18,21 @@
*/
package org.apache.fineract.infrastructure.businessdate.service;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.businessdate.data.BusinessDateData;
import org.apache.fineract.infrastructure.businessdate.data.BusinessDateResponse;
import org.apache.fineract.infrastructure.businessdate.data.BusinessDateUpdateRequest;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDate;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateRepository;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.businessdate.exception.BusinessDateActionException;
import org.apache.fineract.infrastructure.businessdate.validator.BusinessDateDataParserAndValidator;
import org.apache.fineract.infrastructure.businessdate.mapper.BusinessDateUpdateRequestMapper;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@@ -50,54 +44,30 @@ import org.springframework.stereotype.Service;
@RequiredArgsConstructor
public class BusinessDateWritePlatformServiceImpl implements BusinessDateWritePlatformService {
private final BusinessDateDataParserAndValidator dataValidator;
private final BusinessDateRepository repository;
private final ConfigurationDomainService configurationDomainService;
private final BusinessDateUpdateRequestMapper updateRequestMapper;
@Override
public CommandProcessingResult updateBusinessDate(@NotNull final JsonCommand command) {
BusinessDateData data = dataValidator.validateAndParseUpdate(command);
Map<String, Object> changes = new HashMap<>();
adjustDate(data, changes);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).with(changes).build();
public BusinessDateResponse updateBusinessDate(BusinessDateUpdateRequest request) {
BusinessDateResponse businessDateDto = updateRequestMapper.map(request);
adjustDate(businessDateDto);
return businessDateDto;
}
@Override
public void adjustDate(BusinessDateData data, Map<String, Object> changes) {
boolean isCOBDateAdjustmentEnabled = configurationDomainService.isCOBDateAdjustmentEnabled();
boolean isBusinessDateEnabled = configurationDomainService.isBusinessDateEnabled();
if (!isBusinessDateEnabled) {
log.error("Business date functionality is not enabled!");
throw new BusinessDateActionException("business.date.is.not.enabled", "Business date functionality is not enabled");
}
updateOrCreateBusinessDate(data.getType(), data.getDate(), changes);
if (isCOBDateAdjustmentEnabled && BusinessDateType.BUSINESS_DATE.name().equals(data.getType())) {
updateOrCreateBusinessDate(BusinessDateType.COB_DATE.getName(), data.getDate().minus(1, ChronoUnit.DAYS), changes);
}
}
@Override
public void increaseCOBDateByOneDay() throws JobExecutionException {
increaseDateByTypeByOneDay(BusinessDateType.COB_DATE);
}
@Override
public void increaseBusinessDateByOneDay() throws JobExecutionException {
increaseDateByTypeByOneDay(BusinessDateType.BUSINESS_DATE);
}
private void increaseDateByTypeByOneDay(BusinessDateType businessDateType) throws JobExecutionException {
Map<String, Object> changes = new HashMap<>();
public void increaseDateByTypeByOneDay(BusinessDateType businessDateType) throws JobExecutionException {
Optional<BusinessDate> businessDateEntity = repository.findByType(businessDateType);
List<Throwable> exceptions = new ArrayList<>();
LocalDate businessDate = businessDateEntity.map(BusinessDate::getDate).orElse(DateUtils.getLocalDateOfTenant());
businessDate = businessDate.plusDays(1);
try {
BusinessDateData businessDateData = BusinessDateData.instance(businessDateType, businessDate);
adjustDate(businessDateData, changes);
BusinessDateResponse response = BusinessDateResponse.builder().type(businessDateType)
.description(businessDateType.getDescription()).date(businessDate).build();
adjustDate(response);
} catch (final PlatformApiDataValidationException e) {
final List<ApiParameterError> errors = e.getErrors();
for (final ApiParameterError error : errors) {
@@ -116,27 +86,44 @@ public class BusinessDateWritePlatformServiceImpl implements BusinessDateWritePl
}
}
private void updateOrCreateBusinessDate(String type, LocalDate newDate, Map<String, Object> changes) {
BusinessDateType businessDateType = BusinessDateType.valueOf(type);
private void adjustDate(BusinessDateResponse response) {
boolean isCOBDateAdjustmentEnabled = configurationDomainService.isCOBDateAdjustmentEnabled();
boolean isBusinessDateEnabled = configurationDomainService.isBusinessDateEnabled();
if (!isBusinessDateEnabled) {
log.error("Business date functionality is not enabled!");
throw new BusinessDateActionException("business.date.is.not.enabled", "Business date functionality is not enabled");
}
updateOrCreateBusinessDate(response);
if (isCOBDateAdjustmentEnabled && BusinessDateType.BUSINESS_DATE.equals(response.getType())) {
BusinessDateResponse res = BusinessDateResponse.builder().type(BusinessDateType.COB_DATE)
.description(BusinessDateType.COB_DATE.getDescription()).date(response.getDate().minusDays(1)).build();
updateOrCreateBusinessDate(res);
response.addAllChanges(res.getChanges());
}
}
private void updateOrCreateBusinessDate(BusinessDateResponse businessDateDto) {
BusinessDateType businessDateType = businessDateDto.getType();
Optional<BusinessDate> businessDate = repository.findByType(businessDateType);
if (businessDate.isEmpty()) {
BusinessDate newBusinessDate = BusinessDate.instance(businessDateType, newDate);
BusinessDate newBusinessDate = BusinessDate.instance(businessDateType, businessDateDto.getDate());
repository.save(newBusinessDate);
changes.put(type, newBusinessDate.getDate());
businessDateDto.addChange(businessDateType, newBusinessDate.getDate());
} else {
updateBusinessDate(businessDate.get(), newDate, changes);
updateBusinessDate(businessDate.get(), businessDateDto);
}
}
private void updateBusinessDate(BusinessDate businessDate, LocalDate newDate, Map<String, Object> changes) {
LocalDate oldDate = businessDate.getDate();
if (DateUtils.isEqual(oldDate, newDate)) {
private void updateBusinessDate(BusinessDate businessDate, BusinessDateResponse businessDateDto) {
if (DateUtils.isEqual(businessDate.getDate(), businessDateDto.getDate())) {
return;
}
businessDate.setDate(newDate);
businessDate.setDate(businessDateDto.getDate());
repository.save(businessDate);
changes.put(businessDate.getType().name(), newDate);
businessDateDto.addChange(businessDate.getType(), businessDateDto.getDate());
}
}
@@ -1,107 +0,0 @@
/**
* 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.infrastructure.businessdate.validator;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.businessdate.data.BusinessDateData;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.springframework.stereotype.Component;
@Slf4j
@RequiredArgsConstructor
@Component
public class BusinessDateDataParserAndValidator {
private final FromJsonHelper jsonHelper;
public BusinessDateData validateAndParseUpdate(@NotNull final JsonCommand command) {
final DataValidatorBuilder dataValidator = new DataValidatorBuilder(new ArrayList<>()).resource("businessdate.update");
JsonObject element = extractJsonObject(command);
BusinessDateData result = validateAndParseUpdate(dataValidator, element, jsonHelper);
throwExceptionIfValidationWarningsExist(dataValidator);
return result;
}
private JsonObject extractJsonObject(JsonCommand command) {
String json = command.json();
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final JsonElement element = jsonHelper.parse(json);
return element.getAsJsonObject();
}
private void throwExceptionIfValidationWarningsExist(DataValidatorBuilder dataValidator) {
if (dataValidator.hasError()) {
log.error("Business date - Validation errors: {}", dataValidator.getDataValidationErrors());
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
dataValidator.getDataValidationErrors());
}
}
private BusinessDateData validateAndParseUpdate(final DataValidatorBuilder dataValidator, JsonObject element,
FromJsonHelper jsonHelper) {
if (element == null) {
return null;
}
jsonHelper.checkForUnsupportedParameters(element, List.of("type", "date", "dateFormat", "locale"));
String businessDateTypeName = jsonHelper.extractStringNamed("type", element);
final String localeValue = jsonHelper.extractStringNamed("locale", element);
final String dateFormat = jsonHelper.extractDateFormatParameter(element);
final String dateValue = jsonHelper.extractStringNamed("date", element);
dataValidator.reset().parameter("type").value(businessDateTypeName).notBlank();
dataValidator.reset().parameter("locale").value(localeValue).notBlank();
dataValidator.reset().parameter("dateFormat").value(dateFormat).notBlank();
dataValidator.reset().parameter("date").value(dateValue).notBlank();
if (dataValidator.hasError()) {
return null;
}
Locale locale = jsonHelper.extractLocaleParameter(element);
BusinessDateType type;
try {
type = BusinessDateType.valueOf(businessDateTypeName);
} catch (IllegalArgumentException e) {
dataValidator.reset().parameter("type").failWithCode("Invalid Business Type value: `" + businessDateTypeName + "`");
return null;
}
LocalDate date = jsonHelper.extractLocalDateNamed("date", element, dateFormat, locale);
dataValidator.reset().parameter("date").value(date).notNull();
return dataValidator.hasError() ? null : BusinessDateData.instance(type, date);
}
}
@@ -0,0 +1,57 @@
/**
* 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.infrastructure.core.exceptionmapper;
import jakarta.validation.ConstraintViolationException;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.data.ApiGlobalErrorResponse;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
import org.springframework.stereotype.Component;
@Slf4j
@Provider
@Component
public class JakartaValidationExceptionMapper implements FineractExceptionMapper, ExceptionMapper<ConstraintViolationException> {
@Override
public Response toResponse(final ConstraintViolationException exception) {
log.warn("Exception occurred", ErrorHandler.findMostSpecificException(exception));
final ApiGlobalErrorResponse dataValidationErrorResponse = ApiGlobalErrorResponse.badClientRequest(exception.getMessage(),
exception.getLocalizedMessage(), getApiParameterErrors(exception));
return Response.status(Status.BAD_REQUEST).entity(dataValidationErrorResponse).type(MediaType.APPLICATION_JSON).build();
}
@Override
public int errorCode() {
return 2002;
}
private List<ApiParameterError> getApiParameterErrors(final ConstraintViolationException exception) {
return exception.getConstraintViolations().stream().map(violation -> ApiParameterError
.parameterError(violation.getMessageTemplate(), violation.getMessage(), violation.getPropertyPath().toString())).toList();
}
}
@@ -28,7 +28,9 @@ import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAccessor;
@@ -128,18 +130,10 @@ public final class DateUtils {
return compare(first, second, truncate) == 0;
}
public static boolean isEqualTenantDateTime(LocalDateTime dateTime) {
return isEqualTenantDateTime(dateTime, null);
}
public static boolean isEqualTenantDateTime(LocalDateTime dateTime, ChronoUnit truncate) {
return isEqual(dateTime, getLocalDateTimeOfTenant(), truncate);
}
public static boolean isEqualSystemDateTime(LocalDateTime dateTime) {
return isEqualSystemDateTime(dateTime, null);
}
public static boolean isEqualSystemDateTime(LocalDateTime dateTime, ChronoUnit truncate) {
return isEqual(dateTime, getLocalDateTimeOfSystem(), truncate);
}
@@ -152,22 +146,6 @@ public final class DateUtils {
return compare(first, second, truncate) < 0;
}
public static boolean isBeforeTenantDateTime(LocalDateTime dateTime) {
return isBeforeTenantDateTime(dateTime, null);
}
public static boolean isBeforeTenantDateTime(LocalDateTime dateTime, ChronoUnit truncate) {
return isBefore(dateTime, getLocalDateTimeOfTenant(), truncate);
}
public static boolean isBeforeSystemDateTime(LocalDateTime dateTime) {
return isBeforeSystemDateTime(dateTime, null);
}
public static boolean isBeforeSystemDateTime(LocalDateTime dateTime, ChronoUnit truncate) {
return isBefore(dateTime, getLocalDateTimeOfSystem(), truncate);
}
public static boolean isAfter(LocalDateTime first, LocalDateTime second) {
return isAfter(first, second, null);
}
@@ -184,10 +162,6 @@ public final class DateUtils {
return isAfter(dateTime, getLocalDateTimeOfTenant(), truncate);
}
public static boolean isAfterSystemDateTime(LocalDateTime dateTime) {
return isAfterSystemDateTime(dateTime, null);
}
public static boolean isAfterSystemDateTime(LocalDateTime dateTime, ChronoUnit truncate) {
return isAfter(dateTime, getLocalDateTimeOfSystem(), truncate);
}
@@ -228,10 +202,6 @@ public final class DateUtils {
return compare(first, second, truncate) == 0;
}
public static boolean isEqualTenantDateTime(OffsetDateTime dateTime) {
return isEqualTenantDateTime(dateTime, null);
}
public static boolean isEqualTenantDateTime(OffsetDateTime dateTime, ChronoUnit truncate) {
return isEqual(dateTime, getOffsetDateTimeOfTenant(), truncate);
}
@@ -244,10 +214,6 @@ public final class DateUtils {
return compare(first, second, truncate) < 0;
}
public static boolean isBeforeTenantDateTime(OffsetDateTime dateTime) {
return isBeforeTenantDateTime(dateTime, null);
}
public static boolean isBeforeTenantDateTime(OffsetDateTime dateTime, ChronoUnit truncate) {
return isBefore(dateTime, getOffsetDateTimeOfTenant(), truncate);
}
@@ -260,10 +226,6 @@ public final class DateUtils {
return compare(first, second, truncate) > 0;
}
public static boolean isAfterTenantDateTime(OffsetDateTime dateTime) {
return isAfterTenantDateTime(dateTime, null);
}
public static boolean isAfterTenantDateTime(OffsetDateTime dateTime, ChronoUnit truncate) {
return isAfter(dateTime, getOffsetDateTimeOfTenant(), truncate);
}
@@ -274,10 +236,6 @@ public final class DateUtils {
return ThreadLocalContextUtil.getBusinessDate();
}
public static int compareToBusinessDate(LocalDate date) {
return compare(date, getBusinessLocalDate());
}
public static boolean isEqualTenantDate(LocalDate date) {
return isEqual(date, getLocalDateOfTenant());
}
@@ -286,10 +244,6 @@ public final class DateUtils {
return isBefore(date, getLocalDateOfTenant());
}
public static boolean isAfterTenantDate(LocalDate date) {
return isAfter(date, getLocalDateOfTenant());
}
public static boolean isEqualBusinessDate(LocalDate date) {
return isEqual(date, getBusinessLocalDate());
}
@@ -306,10 +260,6 @@ public final class DateUtils {
return isAfterBusinessDate(localDate);
}
public static boolean isDateInThePast(final LocalDate localDate) {
return isBeforeBusinessDate(localDate);
}
public static boolean isDateInRangeFromInclusiveToExclusive(final LocalDate fromInclusive, final LocalDate upToNotInclusive,
final LocalDate target) {
return (DateUtils.isEqual(target, fromInclusive) || DateUtils.isAfter(target, fromInclusive))
@@ -399,6 +349,16 @@ public final class DateUtils {
}
}
public static LocalDate toLocalDate(String local, String date, String dateFormat) {
Locale locale = Locale.forLanguageTag(local);
String patternISO = dateFormat.replace("y", "u");
DateTimeFormatter formatter = new DateTimeFormatterBuilder().parseCaseInsensitive().parseLenient().appendPattern(patternISO)
.optionalStart().appendPattern(" HH:mm:ss").optionalEnd().parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
.parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0).parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0).toFormatter(locale)
.withResolverStyle(ResolverStyle.STRICT);
return LocalDateTime.parse(date, formatter).toLocalDate();
}
public static String format(LocalDate date) {
return format(date, null);
}