diff --git a/pom.xml b/pom.xml
index 622e927..5cbeab4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -41,6 +41,10 @@
org.springframework.boot
spring-boot-starter-web
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
org.springframework.boot
spring-boot-starter-data-jpa
diff --git a/src/main/java/eu/ztsh/wymiana/validation/Adult.java b/src/main/java/eu/ztsh/wymiana/validation/Adult.java
new file mode 100644
index 0000000..affba90
--- /dev/null
+++ b/src/main/java/eu/ztsh/wymiana/validation/Adult.java
@@ -0,0 +1,24 @@
+package eu.ztsh.wymiana.validation;
+
+import jakarta.validation.Constraint;
+import jakarta.validation.Payload;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+@Target({ FIELD, PARAMETER })
+@Retention(RUNTIME)
+@Constraint(validatedBy = AdultValidator.class)
+@Documented
+public @interface Adult {
+ String message() default "{jakarta.validation.constraints.Adult.message}";
+
+ Class>[] groups() default { };
+
+ Class extends Payload>[] payload() default { };
+}
diff --git a/src/main/java/eu/ztsh/wymiana/validation/AdultValidator.java b/src/main/java/eu/ztsh/wymiana/validation/AdultValidator.java
new file mode 100644
index 0000000..47e2255
--- /dev/null
+++ b/src/main/java/eu/ztsh/wymiana/validation/AdultValidator.java
@@ -0,0 +1,42 @@
+package eu.ztsh.wymiana.validation;
+
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorContext;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+public class AdultValidator implements ConstraintValidator {
+
+ private final Pattern pattern = Pattern.compile("\\d{11}");
+ private final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyyMMdd");
+
+ @Override
+ public boolean isValid(String value, ConstraintValidatorContext context) {
+ if (!pattern.matcher(value).matches()) {
+ return false;
+ }
+ var datePart = Arrays.stream(value.substring(0, 6).split("")).map(Integer::parseInt).toArray(Integer[]::new);
+ final String prefix;
+ if (datePart[2] > 1) {
+ datePart[2] = (datePart[2] - 2);
+ prefix = "20";
+ } else {
+ prefix = "19";
+ }
+ var dateStamp = prefix.concat(Arrays.stream(datePart).map(Objects::toString).collect(Collectors.joining()));
+ try {
+ return LocalDate.parse(dateStamp, dtf)
+ .plusYears(18)
+ .isBefore(LocalDate.now(context.getClockProvider().getClock()));
+ } catch (DateTimeParseException exception) {
+ return false;
+ }
+ }
+
+}
diff --git a/src/main/java/eu/ztsh/wymiana/web/model/UserCreateRequest.java b/src/main/java/eu/ztsh/wymiana/web/model/UserCreateRequest.java
new file mode 100644
index 0000000..32d884e
--- /dev/null
+++ b/src/main/java/eu/ztsh/wymiana/web/model/UserCreateRequest.java
@@ -0,0 +1,13 @@
+package eu.ztsh.wymiana.web.model;
+
+import eu.ztsh.wymiana.validation.Adult;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotNull;
+
+public record UserCreateRequest(
+ @NotNull String name,
+ @NotNull String surname,
+ @Adult String pesel,
+ @Min(0) int pln) {
+
+}
diff --git a/src/test/java/eu/ztsh/wymiana/validation/AdultValidatorTest.java b/src/test/java/eu/ztsh/wymiana/validation/AdultValidatorTest.java
new file mode 100644
index 0000000..ca24718
--- /dev/null
+++ b/src/test/java/eu/ztsh/wymiana/validation/AdultValidatorTest.java
@@ -0,0 +1,58 @@
+package eu.ztsh.wymiana.validation;
+
+import jakarta.validation.ConstraintValidatorContext;
+import org.hibernate.validator.internal.engine.DefaultClockProvider;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class AdultValidatorTest {
+
+ private static AdultValidator validator;
+ private static ConstraintValidatorContext validatorContext;
+
+ @BeforeAll
+ static void prepare() {
+ validator = new AdultValidator();
+ validatorContext = Mockito.mock(ConstraintValidatorContext.class);
+ Mockito.when(validatorContext.getClockProvider()).thenReturn(DefaultClockProvider.INSTANCE);
+ }
+
+ @Test
+ @DisplayName("No digits in PESEL")
+ void invalidPatternTest() {
+ assertThat(call("notAPesel")).isFalse();
+ }
+
+ @Test
+ @DisplayName("Not an adult")
+ void notAnAdultTest() {
+ assertThat(call("24242400000")).isFalse();
+ }
+
+ @Test
+ @DisplayName("Adult")
+ void adultTest() {
+ assertThat(call("88010100000")).isTrue();
+ }
+
+ @Test
+ @DisplayName("Elderly person")
+ void seniorTest() {
+ assertThat(call("00010100000")).isTrue();
+ }
+
+ @Test
+ @DisplayName("Invalid date")
+ void notAValidDateTest() {
+ assertThat(call("00919100000")).isFalse();
+ }
+
+ private boolean call(String value) {
+ return validator.isValid(value, validatorContext);
+ }
+
+}