Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
### IntelliJ IDEA ###
.idea/

### Logs ###
backend/**/logs/*
!backend/**/logs/.gitkeep
.idea
68 changes: 0 additions & 68 deletions backend/.idea/workspace.xml

This file was deleted.

6 changes: 6 additions & 0 deletions backend/appointment-service/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@
<groupId>io.confluent</groupId>
<artifactId>kafka-avro-serializer</artifactId>
<version>7.5.1</version>
<exclusions>
<exclusion>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import com.appointment.usecases.ports.in.FindAppointmentByIdUseCase;
import com.appointment.usecases.ports.in.ListAppointmentsUseCase;
import com.appointment.usecases.ports.in.UpdateAppointmentStatusUseCase;
import io.swagger.v3.oas.annotations.Parameter;
import jakarta.validation.Valid;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
Expand Down Expand Up @@ -59,6 +60,7 @@ public AppointmentController(

@PostMapping("/appointment")
public ResponseEntity<ApiResponse<EntityModel<AppointmentResponse>>> create(
@Parameter(example = "11111111-1111-1111-1111-111111111111")
@RequestHeader("Idempotency-Key") String idempotencyKey,
@Valid @RequestBody AppointmentRequest request
) {
Expand All @@ -72,7 +74,7 @@ public ResponseEntity<ApiResponse<PagedModel<EntityModel<AppointmentResponse>>>>
@RequestParam(required = false) Status status,
@PageableDefault(size = 10, sort = "scheduledAt") Pageable pageable
) {
Page<Appointment> page = listAppointmentsUseCase.execute(status, pageable);
Page<Appointment> page = listAppointmentsUseCase.execute(status, AppointmentSortSanitizer.sanitize(pageable));
PagedModel<EntityModel<AppointmentResponse>> pagedModel =
pagedResourcesAssembler.toModel(page, appointmentModelAssembler::toModel);

Expand All @@ -91,7 +93,7 @@ public ResponseEntity<ApiResponse<EntityModel<AppointmentResponse>>> updateStatu
@PathVariable UUID id,
@Valid @RequestBody UpdateAppointmentStatusRequest request
) {
Appointment updated = updateAppointmentStatusUseCase.execute(id, request.status(), request.observation());
Appointment updated = updateAppointmentStatusUseCase.execute(id, request.toStatus(), request.observation());

return ResponseEntity.ok(ApiResponse.of(appointmentModelAssembler.toModel(updated), "Appointment status updated successfully"));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.appointment.adapters.in.controller;

import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;

import java.util.Set;

final class AppointmentSortSanitizer {

private static final Set<String> SORTABLE_PROPERTIES =
Set.of("scheduledAt", "status", "patientName", "createdAt", "updatedAt");

private static final Sort DEFAULT_SORT = Sort.by(Sort.Direction.ASC, "scheduledAt");

private AppointmentSortSanitizer() {
}

static Pageable sanitize(Pageable pageable) {
Sort validOrders = Sort.by(pageable.getSort().stream()
.filter(order -> SORTABLE_PROPERTIES.contains(order.getProperty()))
.toList());

Sort sort = validOrders.isSorted() ? validOrders : DEFAULT_SORT;
return pageable.getSort().equals(sort)
? pageable
: PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), sort);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.appointment.entities.Appointment;
import com.appointment.enums.Status;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Future;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
Expand All @@ -13,13 +14,16 @@
public record AppointmentRequest(
@NotNull
@CPF(message = "${response.error-messages.invalid-cpf}")
@Schema(example = "529.982.247-25")
String patientCpf,

@NotBlank
@Schema(example = "John Doe")
String patientName,

@NotNull
@Future(message = "${response.error-messages.appointment-with-invalid-date}")
@Schema(example = "2030-01-01T10:00:00")
LocalDateTime scheduledAt
) {
public Appointment toEntity(String idempotencyKey) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
package com.appointment.adapters.in.controller.dtos;

import com.appointment.enums.Status;
import com.appointment.frameworks.validation.ValidEnum;
import jakarta.validation.constraints.NotNull;

public record UpdateAppointmentStatusRequest(
@NotNull Status status,
@NotNull
@ValidEnum(enumClass = Status.class)
String status,
String observation
) {
public Status toStatus() {
return Status.valueOf(status);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.appointment.frameworks.config;

import jakarta.validation.MessageInterpolator;
import org.hibernate.validator.messageinterpolation.ParameterMessageInterpolator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Configuration
public class ValidationMessageConfig {

@Bean
public LocalValidatorFactoryBean localValidatorFactoryBean(Environment environment) {
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
factoryBean.setMessageInterpolator(
new PropertyPlaceholderMessageInterpolator(new ParameterMessageInterpolator(), environment)
);
return factoryBean;
}

private static class PropertyPlaceholderMessageInterpolator implements MessageInterpolator {

private static final Pattern PLACEHOLDER = Pattern.compile("\\$\\{([^}]+)}");

private final MessageInterpolator delegate;
private final Environment environment;

PropertyPlaceholderMessageInterpolator(MessageInterpolator delegate, Environment environment) {
this.delegate = delegate;
this.environment = environment;
}

@Override
public String interpolate(String messageTemplate, Context context) {
return delegate.interpolate(resolve(messageTemplate), context);
}

@Override
public String interpolate(String messageTemplate, Context context, Locale locale) {
return delegate.interpolate(resolve(messageTemplate), context, locale);
}

private String resolve(String messageTemplate) {
Matcher matcher = PLACEHOLDER.matcher(messageTemplate);
StringBuilder result = new StringBuilder();
while (matcher.find()) {
String value = environment.getProperty(matcher.group(1));
matcher.appendReplacement(result, Matcher.quoteReplacement(value != null ? value : matcher.group(0)));
}
matcher.appendTail(result);
return result.toString();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
import org.springframework.dao.DataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingRequestHeaderException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.resource.NoResourceFoundException;

import java.util.LinkedHashMap;
Expand Down Expand Up @@ -70,6 +72,18 @@ public ResponseEntity<Void> handleNoResourceFound(NoResourceFoundException ex) {
return ResponseEntity.notFound().build();
}

@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<ApiResponse<Void>> handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException ex) {
ApiResponse<Void> body = ApiResponse.of(null, "Invalid value for parameter: " + ex.getName());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body);
}

@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<ApiResponse<Void>> handleHttpMessageNotReadable(HttpMessageNotReadableException ex) {
ApiResponse<Void> body = ApiResponse.of(null, "Malformed or invalid request body.");
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body);
}

@ExceptionHandler(DataAccessException.class)
public ResponseEntity<ApiResponse<Void>> handleDataAccessException(DataAccessException ex) {
log.error("Database error", ex);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.appointment.frameworks.validation;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

import java.util.Arrays;
import java.util.stream.Collectors;

public class EnumValidator implements ConstraintValidator<ValidEnum, String> {

private Class<? extends Enum<?>> enumClass;

@Override
public void initialize(ValidEnum annotation) {
this.enumClass = annotation.enumClass();
}

@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) {
return true;
}

boolean valid = Arrays.stream(enumClass.getEnumConstants())
.anyMatch(candidate -> candidate.name().equals(value));

if (!valid) {
String allowedValues = Arrays.stream(enumClass.getEnumConstants())
.map(Enum::name)
.collect(Collectors.joining(", "));

context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate("must be one of: " + allowedValues)
.addConstraintViolation();
}

return valid;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.appointment.frameworks.validation;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.RECORD_COMPONENT})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EnumValidator.class)
public @interface ValidEnum {

Class<? extends Enum<?>> enumClass();

String message() default "must be one of the accepted values";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};
}
Loading
Loading