diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/PublicController.java b/backend/vkt/src/main/java/fi/oph/vkt/api/PublicController.java index bd04cf760..f46656219 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/api/PublicController.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/PublicController.java @@ -9,6 +9,7 @@ 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; @@ -18,6 +19,7 @@ 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.PublicPersonService; @@ -66,6 +68,9 @@ public class PublicController { @Resource private PublicEnrollmentService publicEnrollmentService; + @Resource + private PublicEnrollmentAppointmentService publicEnrollmentAppointmentService; + @Resource private PublicExamEventService publicExamEventService; @@ -180,6 +185,30 @@ public PublicReservationDTO renewReservation(@PathVariable final long reservatio return publicReservationService.renewReservation(reservationId, person); } + @GetMapping(path = "/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.setPersonId(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, @@ -211,16 +240,16 @@ 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 locale, final HttpSession session ) throws IOException { final String casLoginUrl = publicAuthService.createCasLoginUrl( - examEventId, + targetId, EnrollmentType.fromString(type), locale.isPresent() ? AppLocale.fromString(locale.get()) : AppLocale.FI ); @@ -232,26 +261,32 @@ public void casLoginRedirect( 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)) { + publicEnrollmentAppointmentService.savePersonInfo(targetId, person); } - httpResponse.sendRedirect(uiRouteUtil.getEnrollmentContactDetailsUrl(examEventId)); + if (enrollmentType.equals(EnrollmentType.APPOINTMENT)) { + httpResponse.sendRedirect(uiRouteUtil.getEnrollmentAppointmentUrl(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())); diff --git a/backend/vkt/src/main/java/fi/oph/vkt/model/EnrollmentAppointment.java b/backend/vkt/src/main/java/fi/oph/vkt/model/EnrollmentAppointment.java new file mode 100644 index 000000000..24d177545 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/model/EnrollmentAppointment.java @@ -0,0 +1,87 @@ +package fi.oph.vkt.model; + +import fi.oph.vkt.model.type.EnrollmentStatus; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import jakarta.validation.constraints.Size; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +@Table(name = "enrollment") +public class EnrollmentAppointment extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "enrollment_appointment_id", nullable = false) + private long id; + + @Column(name = "skill_oral") + private boolean oralSkill; + + @Column(name = "skill_textual") + private boolean textualSkill; + + @Column(name = "skill_understanding") + private boolean understandingSkill; + + @Column(name = "partial_exam_speaking") + private boolean speakingPartialExam; + + @Column(name = "partial_exam_speech_comprehension") + private boolean speechComprehensionPartialExam; + + @Column(name = "partial_exam_writing") + private boolean writingPartialExam; + + @Column(name = "partial_exam_reading_comprehension") + private boolean readingComprehensionPartialExam; + + @Column(name = "digital_certificate_consent") + private boolean digitalCertificateConsent; + + @Column(name = "email") + private String email; + + @Column(name = "phone_number") + private String phoneNumber; + + @Column(name = "street") + private String street; + + @Column(name = "postal_code") + private String postalCode; + + @Column(name = "town") + private String town; + + @Column(name = "country") + private String country; + + @Size(max = 255) + @Column(name = "auth_hash", unique = true) + private String authHash; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "person_id", referencedColumnName = "person_id") + private Person person; + + @OneToMany(mappedBy = "enrollmentAppointment") + private List payments = new ArrayList<>(); +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/model/Payment.java b/backend/vkt/src/main/java/fi/oph/vkt/model/Payment.java index a1ea43d1c..d10709091 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/model/Payment.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/model/Payment.java @@ -27,10 +27,14 @@ public class Payment extends BaseEntity { @Column(name = "payment_id", nullable = false) private long id; - @ManyToOne(fetch = FetchType.LAZY, optional = false) - @JoinColumn(name = "enrollment_id", referencedColumnName = "enrollment_id", nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "enrollment_id", referencedColumnName = "enrollment_id") private Enrollment enrollment; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "enrollment_appointment_id", referencedColumnName = "enrollment_appointment_id") + private EnrollmentAppointment enrollmentAppointment; + @Column(name = "amount", nullable = false) private int amount; diff --git a/backend/vkt/src/main/java/fi/oph/vkt/model/type/EnrollmentType.java b/backend/vkt/src/main/java/fi/oph/vkt/model/type/EnrollmentType.java index 5336881c7..a313245ff 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/model/type/EnrollmentType.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/model/type/EnrollmentType.java @@ -2,6 +2,7 @@ public enum EnrollmentType { RESERVATION("reservation"), + APPOINTMENT("appointment"), QUEUE("queue"); private final String text; diff --git a/backend/vkt/src/main/java/fi/oph/vkt/repository/EnrollmentAppointmentRepository.java b/backend/vkt/src/main/java/fi/oph/vkt/repository/EnrollmentAppointmentRepository.java new file mode 100644 index 000000000..41e47c33b --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/repository/EnrollmentAppointmentRepository.java @@ -0,0 +1,17 @@ +package fi.oph.vkt.repository; + +import fi.oph.vkt.api.dto.FreeEnrollmentDetails; +import fi.oph.vkt.model.Enrollment; +import fi.oph.vkt.model.EnrollmentAppointment; +import fi.oph.vkt.model.ExamEvent; +import fi.oph.vkt.model.Person; +import fi.oph.vkt.model.type.EnrollmentStatus; +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +@Repository +public interface EnrollmentAppointmentRepository extends BaseRepository { + Optional findByIdAndAuthHash(final long id, final String paymentLinkHash); +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicAuthService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicAuthService.java index 524da8011..1071f6a34 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicAuthService.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicAuthService.java @@ -42,10 +42,10 @@ public class PublicAuthService { private final CasTicketRepository casTicketRepository; private final CasSessionMappingStorage sessionMappingStorage; - public String createCasLoginUrl(final long examEventId, final EnrollmentType type, final AppLocale appLocale) { + public String createCasLoginUrl(final long targetId, final EnrollmentType type, final AppLocale appLocale) { final String casLoginUrl = environment.getRequiredProperty("app.cas-oppija.login-url"); final String casServiceUrl = URLEncoder.encode( - String.format(environment.getRequiredProperty("app.cas-oppija.service-url"), examEventId, type), + String.format(environment.getRequiredProperty("app.cas-oppija.service-url"), targetId, type), StandardCharsets.UTF_8 ); return casLoginUrl + "?service=" + casServiceUrl + "&locale=" + appLocale.name().toLowerCase(); diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentAppointmentService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentAppointmentService.java new file mode 100644 index 000000000..b22e4156e --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentAppointmentService.java @@ -0,0 +1,63 @@ +package fi.oph.vkt.service; + +import fi.oph.vkt.api.dto.FreeEnrollmentAttachmentDTO; +import fi.oph.vkt.api.dto.FreeEnrollmentDetails; +import fi.oph.vkt.api.dto.FreeEnrollmentDetailsDTO; +import fi.oph.vkt.api.dto.PublicEducationDTO; +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.PublicFreeEnrollmentBasisDTO; +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.ExamEvent; +import fi.oph.vkt.model.FeatureFlag; +import fi.oph.vkt.model.FreeEnrollment; +import fi.oph.vkt.model.Person; +import fi.oph.vkt.model.Reservation; +import fi.oph.vkt.model.UploadedFileAttachment; +import fi.oph.vkt.model.type.EnrollmentStatus; +import fi.oph.vkt.model.type.FreeEnrollmentSource; +import fi.oph.vkt.model.type.FreeEnrollmentType; +import fi.oph.vkt.repository.EnrollmentAppointmentRepository; +import fi.oph.vkt.repository.EnrollmentRepository; +import fi.oph.vkt.repository.ExamEventRepository; +import fi.oph.vkt.repository.FreeEnrollmentRepository; +import fi.oph.vkt.repository.ReservationRepository; +import fi.oph.vkt.repository.UploadedFileAttachmentRepository; +import fi.oph.vkt.service.aws.S3Service; +import fi.oph.vkt.service.koski.KoskiService; +import fi.oph.vkt.util.EnrollmentUtil; +import fi.oph.vkt.util.ExamEventUtil; +import fi.oph.vkt.util.PersonUtil; +import fi.oph.vkt.util.exception.APIException; +import fi.oph.vkt.util.exception.APIExceptionType; +import fi.oph.vkt.util.exception.NotFoundException; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.apache.commons.io.FilenameUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class PublicEnrollmentAppointmentService extends AbstractEnrollmentService { + + private final EnrollmentAppointmentRepository enrollmentAppointmentRepository; + + public EnrollmentAppointment getEnrollmentAppointmentByHash( + final long enrollmentAppointmentId, + final String authHash + ) { + return enrollmentAppointmentRepository.findByIdAndAuthHash(enrollmentAppointmentId, authHash).orElseThrow(); + } + + public void savePersonInfo(long targetId, Person person) {} +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/util/UIRouteUtil.java b/backend/vkt/src/main/java/fi/oph/vkt/util/UIRouteUtil.java index 027d520a9..18ba970b3 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/util/UIRouteUtil.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/util/UIRouteUtil.java @@ -30,4 +30,8 @@ public String getPublicFrontPageUrlWithError(final APIExceptionType exceptionTyp private String getPublicBaseUrl() { return environment.getRequiredProperty("app.base-url.public"); } + + public String getEnrollmentAppointmentUrl(final long enrollmentAppointmentId) { + return String.format("%s/ota-yhteytta/%s/tunnistaudu", getPublicBaseUrl(), enrollmentAppointmentId); + } } diff --git a/backend/vkt/src/main/resources/db/changelog/db.changelog-1.0.xml b/backend/vkt/src/main/resources/db/changelog/db.changelog-1.0.xml index 694e56614..7301f26a7 100644 --- a/backend/vkt/src/main/resources/db/changelog/db.changelog-1.0.xml +++ b/backend/vkt/src/main/resources/db/changelog/db.changelog-1.0.xml @@ -859,4 +859,32 @@ ALTER TABLE exam_event ADD COLUMN registration_opens TIMESTAMP WITH TIME ZONE; + + + + + + + + + + + + + + + + + + + + + + + + + + + +