Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VKT(Backend & Frontend) OPHKIOS-114 Yhteydenottoputki #736

Draft
wants to merge 67 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
d374299
VKT(Backend): support for exam event open and close times
jrkkp Aug 26, 2024
7ac6e98
VKT(Backend & Frontend): support for exam event open and close times
jrkkp Aug 28, 2024
db0dfa8
Merge branch 'dev' into feature/OPHKIOS-100
jrkkp Sep 1, 2024
741cce6
VKT(Backend & Frontend): support for exam event open and close times …
jrkkp Sep 2, 2024
500c71c
VKT(Backend) test fix
jrkkp Sep 3, 2024
a8b40e8
VKT(Backend) test fix
jrkkp Sep 3, 2024
5f847fc
VKT(Backend & Frontend): registration opens front page listing refresh
jrkkp Sep 3, 2024
04574e9
VKT(Backend & Frontend): registration opens front page listing refresh
jrkkp Sep 4, 2024
8e47a77
VKT(Backend & Frontend): some small tweaks and fixes
jrkkp Sep 4, 2024
8e24523
Merge branch 'dev' into feature/OPHKIOS-100
jrkkp Sep 11, 2024
9766618
VKT(Frontend & Backend): review fixes
jrkkp Sep 11, 2024
0b5a5ea
VKT(Frontend): Cypress test fix
jrkkp Sep 11, 2024
c75d6d8
VKT(Backend): unit test fixes
jrkkp Sep 11, 2024
be8dff4
VKT(Frontend): time format localisation fix
jrkkp Sep 12, 2024
ab2a487
VKT(Frontend): oops, cypress fix
jrkkp Sep 12, 2024
679a75a
Merge branch 'dev' into feature/OPHKIOS-100
jrkkp Sep 13, 2024
d445b64
Merge branch 'fix/OPHKIOS-91_further-deps-upgrades' into feature/OPHK…
jrkkp Sep 16, 2024
0fb5eed
VKT(Backend): good and satisfactory level enrollment core functionality
jrkkp Sep 17, 2024
5b119c9
VKT(Backend): good and satisfactory level enrollment payment function…
jrkkp Sep 20, 2024
c5bee41
VKT(Frontend): enrollment appointment grid
jrkkp Sep 23, 2024
c2f6c83
VKT(Frontend): enrollment appointment auth & payment grid
jrkkp Sep 24, 2024
dead010
VKT(Frontend): enrollment appointment continues
jrkkp Sep 25, 2024
8f0eb9b
VKT(Frontend): enrollment appointment continues
jrkkp Sep 29, 2024
4bb8098
VKT(Frontend): enrollment appointment continues
jrkkp Sep 30, 2024
b36b02f
VKT(Frontend & Backend): enrollment appointment continues
jrkkp Oct 1, 2024
1845ba1
VKT(Frontend & Backend): enrollment appointment continues
jrkkp Oct 2, 2024
146cf07
VKT(Frontend & Backend): enrollment appointment continues
jrkkp Oct 3, 2024
b01f9c3
VKT(Frontend): enrollment appointment continues
jrkkp Oct 7, 2024
71c7129
VKT(Backend): Feature flag for good and satisfactory level support
pkoivisto Sep 19, 2024
ee95d47
Remove obsolete comment from package.json
pkoivisto Sep 23, 2024
9a73599
VKT(Frontend): Introduce feature flag for good and satisfactory level
pkoivisto Sep 24, 2024
bfe8576
VKT(Frontend): Refactor: move pages around a bit
pkoivisto Sep 24, 2024
980f431
VKT(Frontend): Include NavigationLinks in Header
pkoivisto Sep 24, 2024
54b90bb
VKT(Frontend): Text and styling changes [deploy]
pkoivisto Sep 24, 2024
5696630
VKT(Frontend): Refactor frontend URL structure [deploy]
pkoivisto Sep 24, 2024
862fa24
VKT(Frontend): Fix Cypress tests [deploy]
pkoivisto Sep 25, 2024
5eb3afc
VKT(Frontend): Always show navigation links [deploy]
pkoivisto Sep 25, 2024
6fac8ee
VKT(Frontend): Fix msw test setup [deploy]
pkoivisto Sep 25, 2024
74b25ed
YKI(Frontend): Remove leftover styling rule
pkoivisto Sep 26, 2024
e1a3fd7
VKT(Frontend): Use grid layout on desktop in header to improve layout…
pkoivisto Sep 26, 2024
f8f3669
SHARED(Frontend): Introduce components for accessible mobile navigati…
pkoivisto Sep 27, 2024
edca07f
VKT(Frontend): Use hamburger menu on mobile for navigation
pkoivisto Sep 27, 2024
ce17f1c
VKT(Frontend): Fix Jest snapshots [deploy]
pkoivisto Sep 27, 2024
56997b8
VKT(Frontend): Drop unused import
pkoivisto Sep 30, 2024
653413d
SHARED(Frontend): Render menu contents with portal to specified DOM e…
pkoivisto Sep 30, 2024
b6cd72e
VKT(Frontend): Take MobileNavigationMenuWithPortal into use [deploy]
pkoivisto Sep 30, 2024
af14621
SHARED:VKT(Frontend): Cut new shared library version, take it into us…
pkoivisto Sep 30, 2024
2f56613
VKT(Frontend): Card layout on mobile should be rows
pkoivisto Sep 30, 2024
ed1d609
VKT(Frontend): Make LanguageFilter a 'common' component by file syste…
pkoivisto Oct 1, 2024
d4e33f2
VKT(Frontend): Style and layout adjustments
pkoivisto Oct 1, 2024
93ffe2a
VKT(Frontend): Foundations for PublicExaminerListing
pkoivisto Oct 1, 2024
8300eee
VKT(Frontend): Public examiner listing with mock data for good and sa…
pkoivisto Oct 3, 2024
d28dcc3
VKT(Backend&Frontend): Get (mocked, fixed) list of examiners to displ…
pkoivisto Oct 3, 2024
b29721d
Merge branch 'dev' into feature/OPHKIOS-107
jrkkp Oct 7, 2024
46cf830
VKT(Frontend & Backend): enrollment appointment continues
jrkkp Oct 7, 2024
140ef60
Merge branch 'feature/OPHKIOS-108' into feature/OPHKIOS-114
jrkkp Oct 8, 2024
70c5a9e
VKT(Frontend): Enrollment appointment contact form begins
jrkkp Oct 8, 2024
13bedb9
VKT(Frontend & Backend): Enrollment appointment contact form continues
jrkkp Oct 10, 2024
4468fa2
VKT(Frontend): Enrollment appointment contact form continues
jrkkp Oct 13, 2024
90e2bd2
VKT(Frontend): Enrollment appointment contact form continues
jrkkp Oct 14, 2024
2100958
VKT(Frontend): Enrollment appointment contact form continues
jrkkp Oct 16, 2024
630c6c0
VKT(Frontend): Enrollment appointment contact form continues
jrkkp Oct 16, 2024
1877df5
VKT(Frontend): Enrollment appointment contact form continues
jrkkp Oct 18, 2024
4a16a49
VKT(Frontend & Backend): Enrollment appointment contact form continues
jrkkp Oct 21, 2024
f0ed902
VKT(Frontend): missing deps
jrkkp Oct 21, 2024
f998f35
VKT(Frontend & Backend): Enrollment appointment contact form continues
jrkkp Oct 21, 2024
986c56d
VKT(Backend): Enrollment appointment contact form continues
jrkkp Oct 22, 2024
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
11 changes: 11 additions & 0 deletions backend/vkt/db/4_init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -375,3 +375,14 @@ SELECT exam_event_id, (SELECT max(person_id) FROM person),
'CANCELED', true,
'[email protected]', '0404040404', null, null, null, null
FROM exam_event;

-- Insert enrollment appointment
INSERT INTO enrollment_appointment(person_id,
skill_oral, skill_textual, skill_understanding,
partial_exam_speaking, partial_exam_speech_comprehension, partial_exam_writing, partial_exam_reading_comprehension,
status, digital_certificate_consent, email, phone_number, street, postal_code, town, country)
VALUES (SELECT max(person_id) FROM person),
true, true, true,
true, true, true, true,
'COMPLETED', true,
'[email protected]', '0404040404', null, null, null, null;
133 changes: 107 additions & 26 deletions backend/vkt/src/main/java/fi/oph/vkt/api/PublicController.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@

import com.fasterxml.jackson.core.JsonProcessingException;
import fi.oph.vkt.api.dto.PublicEducationDTO;
import fi.oph.vkt.api.dto.PublicEnrollmentAppointmentDTO;
import fi.oph.vkt.api.dto.PublicEnrollmentAppointmentUpdateDTO;
import fi.oph.vkt.api.dto.PublicEnrollmentContactCreateDTO;
import fi.oph.vkt.api.dto.PublicEnrollmentCreateDTO;
import fi.oph.vkt.api.dto.PublicEnrollmentDTO;
import fi.oph.vkt.api.dto.PublicEnrollmentInitialisationDTO;
import fi.oph.vkt.api.dto.PublicExamEventDTO;
import fi.oph.vkt.api.dto.PublicExaminerDTO;
import fi.oph.vkt.api.dto.PublicPersonDTO;
import fi.oph.vkt.api.dto.PublicReservationDTO;
import fi.oph.vkt.model.Enrollment;
import fi.oph.vkt.model.EnrollmentAppointment;
import fi.oph.vkt.model.FeatureFlag;
import fi.oph.vkt.model.Person;
import fi.oph.vkt.model.type.AppLocale;
Expand All @@ -18,14 +23,17 @@
import fi.oph.vkt.service.FeatureFlagService;
import fi.oph.vkt.service.PaymentService;
import fi.oph.vkt.service.PublicAuthService;
import fi.oph.vkt.service.PublicEnrollmentAppointmentService;
import fi.oph.vkt.service.PublicEnrollmentService;
import fi.oph.vkt.service.PublicExamEventService;
import fi.oph.vkt.service.PublicExaminerService;
import fi.oph.vkt.service.PublicPersonService;
import fi.oph.vkt.service.PublicReservationService;
import fi.oph.vkt.service.koski.KoskiService;
import fi.oph.vkt.util.SessionUtil;
import fi.oph.vkt.util.UIRouteUtil;
import fi.oph.vkt.util.exception.APIException;
import fi.oph.vkt.util.exception.APIExceptionType;
import fi.oph.vkt.util.exception.NotFoundException;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
Expand Down Expand Up @@ -66,6 +74,9 @@ public class PublicController {
@Resource
private PublicEnrollmentService publicEnrollmentService;

@Resource
private PublicEnrollmentAppointmentService publicEnrollmentAppointmentService;

@Resource
private PublicExamEventService publicExamEventService;

Expand All @@ -87,11 +98,30 @@ public class PublicController {
@Resource
private FeatureFlagService featureFlagService;

@Resource
private PublicExaminerService publicExaminerService;

@GetMapping(path = "/examEvent")
public List<PublicExamEventDTO> list() {
return publicExamEventService.listExamEvents(ExamLevel.EXCELLENT);
}

@GetMapping(path = "/examiner")
public List<PublicExaminerDTO> listExaminers() {
return publicExaminerService.listExaminers();
}

@PostMapping(path = "/enrollment/examiner/{examinerId:\\d+}")
@ResponseStatus(HttpStatus.CREATED)
public void createEnrollmentContact(@RequestBody @Valid final PublicEnrollmentContactCreateDTO dto) {
publicEnrollmentService.createEnrollmentContact(dto);
}

@GetMapping(path = "/enrollment/examiner/{examinerId:\\d+}")
public PublicExaminerDTO getExaminer(@PathVariable final long examinerId) {
return publicEnrollmentService.getExaminer(examinerId);
}

@PostMapping(path = "/enrollment/reservation/{reservationId:\\d+}")
@ResponseStatus(HttpStatus.CREATED)
public PublicEnrollmentDTO createEnrollment(
Expand Down Expand Up @@ -144,6 +174,32 @@ public PublicExamEventDTO getExamEventInfo(@PathVariable final long examEventId)
return publicExamEventService.getExamEvent(examEventId);
}

@GetMapping(path = "/enrollment/appointment/{enrollmentAppointmentId:\\d+}")
public PublicEnrollmentAppointmentDTO getEnrollmentAppointment(
@PathVariable final long enrollmentAppointmentId,
final HttpSession session
) {
final Person person = publicAuthService.getPersonFromSession(session);

return publicEnrollmentService.getEnrollmentAppointment(enrollmentAppointmentId, person);
}

@PostMapping(path = "/enrollment/appointment/{enrollmentAppointmentId:\\d+}")
@ResponseStatus(HttpStatus.CREATED)
public PublicEnrollmentAppointmentDTO saveEnrollmentAppointment(
@RequestBody @Valid final PublicEnrollmentAppointmentUpdateDTO dto,
@PathVariable final long enrollmentAppointmentId,
final HttpSession session
) {
final Person person = publicAuthService.getPersonFromSession(session);

if (enrollmentAppointmentId != dto.id()) {
throw new APIException(APIExceptionType.APPOINTMENT_ID_MISMATCH);
}

return publicEnrollmentService.saveEnrollmentAppointment(dto, person);
}

@GetMapping(path = "/education")
public List<PublicEducationDTO> getEducation(final HttpSession session) throws JsonProcessingException {
final Person person = publicAuthService.getPersonFromSession(session);
Expand Down Expand Up @@ -180,6 +236,30 @@ public PublicReservationDTO renewReservation(@PathVariable final long reservatio
return publicReservationService.renewReservation(reservationId, person);
}

@GetMapping(path = "/enrollment/appointment/{enrollmentAppointmentId:\\d+}/redirect/{authHash:[a-z0-9\\-]+}")
public void createSessionAndRedirectToEnrollmentAppointment(
final HttpServletResponse httpResponse,
@PathVariable final long enrollmentAppointmentId,
@PathVariable final String authHash,
final HttpSession session
) throws IOException {
try {
final EnrollmentAppointment enrollmentAppointment = publicEnrollmentAppointmentService.getEnrollmentAppointmentByHash(
enrollmentAppointmentId,
authHash
);
SessionUtil.setAppointmentId(session, enrollmentAppointment.getId());

httpResponse.sendRedirect(uiRouteUtil.getEnrollmentAppointmentUrl(enrollmentAppointment.getId()));
} catch (final APIException e) {
LOG.warn("Encountered known error, redirecting to front page. Error:", e);
httpResponse.sendRedirect(uiRouteUtil.getPublicFrontPageUrlWithError(e.getExceptionType()));
} catch (final Exception e) {
LOG.error("Encountered unknown error, redirecting to front page. Error:", e);
httpResponse.sendRedirect(uiRouteUtil.getPublicFrontPageUrlWithGenericError());
}
}

@GetMapping(path = "/examEvent/{examEventId:\\d+}/redirect/{paymentLinkHash:[a-z0-9\\-]+}")
public void createSessionAndRedirectToPreview(
final HttpServletResponse httpResponse,
Expand Down Expand Up @@ -211,47 +291,49 @@ public void deleteReservation(@PathVariable final long reservationId, final Http
publicReservationService.deleteReservation(reservationId, person);
}

@GetMapping(path = "/auth/login/{examEventId:\\d+}/{type:\\w+}")
@GetMapping(path = "/auth/login/{targetId:\\d+}/{type:\\w+}")
public void casLoginRedirect(
final HttpServletResponse httpResponse,
@PathVariable final long examEventId,
@PathVariable final long targetId,
@PathVariable final String type,
@RequestParam final Optional<String> locale,
final HttpSession session
@RequestParam final Optional<String> locale
) throws IOException {
final String casLoginUrl = publicAuthService.createCasLoginUrl(
examEventId,
targetId,
EnrollmentType.fromString(type),
locale.isPresent() ? AppLocale.fromString(locale.get()) : AppLocale.FI
);

if (session != null) {
session.invalidate();
}

httpResponse.sendRedirect(casLoginUrl);
}

@GetMapping(path = "/auth/validate/{examEventId:\\d+}/{type:\\w+}")
@GetMapping(path = "/auth/validate/{targetId:\\d+}/{type:\\w+}")
public void validateTicket(
@RequestParam final String ticket,
@PathVariable final long examEventId,
@PathVariable final long targetId,
@PathVariable final String type,
final HttpSession session,
final HttpServletResponse httpResponse
) throws IOException {
try {
final EnrollmentType enrollmentType = EnrollmentType.fromString(type);
final Person person = publicAuthService.createPersonFromTicket(ticket, examEventId, enrollmentType);
final Person person = publicAuthService.createPersonFromTicket(ticket, targetId, enrollmentType);
SessionUtil.setPersonId(session, person.getId());

if (enrollmentType.equals(EnrollmentType.QUEUE)) {
publicEnrollmentService.initialiseEnrollmentToQueue(examEventId, person);
} else {
publicEnrollmentService.initialiseEnrollment(examEventId, person);
publicEnrollmentService.initialiseEnrollmentToQueue(targetId, person);
} else if (enrollmentType.equals(EnrollmentType.RESERVATION)) {
publicEnrollmentService.initialiseEnrollment(targetId, person);
} else if (enrollmentType.equals(EnrollmentType.APPOINTMENT)) {
final Long appointmentId = SessionUtil.getAppointmentId(session);
publicEnrollmentAppointmentService.savePersonInfo(targetId, appointmentId, person);
}

httpResponse.sendRedirect(uiRouteUtil.getEnrollmentContactDetailsUrl(examEventId));
if (enrollmentType.equals(EnrollmentType.APPOINTMENT)) {
httpResponse.sendRedirect(uiRouteUtil.getEnrollmentAppointmentContactDetailsUrl(targetId));
} else {
httpResponse.sendRedirect(uiRouteUtil.getEnrollmentContactDetailsUrl(targetId));
}
} catch (final APIException e) {
LOG.warn("Encountered known error, redirecting to front page. Error:", e);
httpResponse.sendRedirect(uiRouteUtil.getPublicFrontPageUrlWithError(e.getExceptionType()));
Expand All @@ -266,12 +348,9 @@ public Optional<PublicPersonDTO> authInfo(final HttpSession session) {
if (session == null) {
return Optional.empty();
}

try {
return Optional.of(publicPersonService.getPersonDTO(publicAuthService.getPersonFromSession(session)));
} catch (final NotFoundException e) {
session.invalidate();

return Optional.empty();
}
}
Expand All @@ -286,20 +365,22 @@ public void logout(final HttpSession session, final HttpServletResponse httpResp
httpResponse.sendRedirect(publicAuthService.createCasLogoutUrl());
}

@GetMapping(path = "/payment/create/{enrollmentId:\\d+}/redirect")
@GetMapping(path = "/payment/create/{targetId:\\d+}/{type:\\w+}/redirect")
public void createPaymentAndRedirect(
@PathVariable final Long enrollmentId,
@PathVariable final Long targetId,
@PathVariable final String type,
@RequestParam final Optional<String> locale,
final HttpSession session,
final HttpServletResponse httpResponse
) throws IOException {
try {
final EnrollmentType enrollmentType = EnrollmentType.fromString(type);
final Person person = publicPersonService.getPerson(SessionUtil.getPersonId(session));
final String redirectUrl = paymentService.createPaymentForEnrollment(
enrollmentId,
person,
locale.isPresent() ? AppLocale.fromString(locale.get()) : AppLocale.FI
);
final AppLocale localeOrDefault = locale.isPresent() ? AppLocale.fromString(locale.get()) : AppLocale.FI;

final String redirectUrl = enrollmentType.equals(EnrollmentType.APPOINTMENT)
? paymentService.createPaymentForEnrollmentAppointment(targetId, person, localeOrDefault)
: paymentService.createPaymentForEnrollment(targetId, person, localeOrDefault);

httpResponse.sendRedirect(redirectUrl);
} catch (final APIException e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package fi.oph.vkt.api.dto;

import fi.oph.vkt.model.type.EnrollmentStatus;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Builder;
import lombok.NonNull;

@Builder
public record PublicEnrollmentAppointmentDTO(
@NonNull @NotNull Long id,
@NonNull @NotNull Boolean oralSkill,
@NonNull @NotNull Boolean textualSkill,
@NonNull @NotNull Boolean understandingSkill,
@NonNull @NotNull Boolean speakingPartialExam,
@NonNull @NotNull Boolean speechComprehensionPartialExam,
@NonNull @NotNull Boolean writingPartialExam,
@NonNull @NotNull Boolean readingComprehensionPartialExam,
@NonNull @NotNull EnrollmentStatus status,
String previousEnrollment,
@NonNull @NotNull Boolean digitalCertificateConsent,
@NonNull @NotBlank String email,
String phoneNumber,
String street,
String postalCode,
String town,
String country,
@NonNull @NotNull PublicPersonDTO person
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package fi.oph.vkt.api.dto;

import fi.oph.vkt.util.StringUtil;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Builder;
import lombok.NonNull;

@Builder
public record PublicEnrollmentAppointmentUpdateDTO(
@NotNull long id,
String previousEnrollment,
@NonNull @NotNull Boolean digitalCertificateConsent,
@NonNull @NotBlank String phoneNumber,
@Size(max = 1024) String street,
@Size(max = 1024) String postalCode,
@Size(max = 1024) String town,
@Size(max = 1024) String country
) {
public PublicEnrollmentAppointmentUpdateDTO {
previousEnrollment = StringUtil.sanitize(previousEnrollment);
street = StringUtil.sanitize(street);
postalCode = StringUtil.sanitize(postalCode);
town = StringUtil.sanitize(town);
country = StringUtil.sanitize(country);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package fi.oph.vkt.api.dto;

import fi.oph.vkt.util.StringUtil;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Builder;
import lombok.NonNull;

@Builder
public record PublicEnrollmentContactCreateDTO(
@NonNull @NotNull Boolean oralSkill,
@NonNull @NotNull Boolean textualSkill,
@NonNull @NotNull Boolean understandingSkill,
@NonNull @NotNull Boolean speakingPartialExam,
@NonNull @NotNull Boolean speechComprehensionPartialExam,
@NonNull @NotNull Boolean writingPartialExam,
@NonNull @NotNull Boolean readingComprehensionPartialExam,
@Size(max = 1024) String previousEnrollment,
@Size(max = 255) @NonNull @NotBlank String email,
@Size(max = 255) @NonNull @NotBlank String firstName,
@Size(max = 255) @NonNull @NotBlank String lastName
) {
public PublicEnrollmentContactCreateDTO {
previousEnrollment = StringUtil.sanitize(previousEnrollment);
email = StringUtil.sanitize(email);
firstName = StringUtil.sanitize(firstName);
lastName = StringUtil.sanitize(lastName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package fi.oph.vkt.api.dto;

import fi.oph.vkt.model.type.ExamLanguage;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDate;
import java.util.List;
import lombok.Builder;
import lombok.NonNull;

@Builder
public record PublicExaminerDTO(
@NonNull @NotNull Long id,
@NonNull @NotNull String lastName,
@NonNull @NotNull String firstName,
@NonNull @NotNull List<ExamLanguage> languages,
@NonNull @NotNull List<PublicMunicipalityDTO> municipalities,
@NonNull @NotNull List<LocalDate> examDates
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package fi.oph.vkt.api.dto;

import jakarta.validation.constraints.NotNull;
import lombok.Builder;
import lombok.NonNull;

@Builder
public record PublicMunicipalityDTO(@NonNull @NotNull String fi, @NonNull @NotNull String sv) {}
Loading
Loading