diff --git a/backend/vkt/db/1_tables.sql b/backend/vkt/db/1_tables.sql index 427064f6e..f2ae4ee67 100644 --- a/backend/vkt/db/1_tables.sql +++ b/backend/vkt/db/1_tables.sql @@ -1,663 +1,1279 @@ --- --- PostgreSQL database dump --- - --- Dumped from database version 12.9 (Debian 12.9-1.pgdg110+1) --- Dumped by pg_dump version 14.7 (Homebrew) - -SET statement_timeout = 0; -SET lock_timeout = 0; -SET idle_in_transaction_session_timeout = 0; -SET client_encoding = 'UTF8'; -SET standard_conforming_strings = on; -SELECT pg_catalog.set_config('search_path', '', false); -SET check_function_bodies = false; -SET xmloption = content; -SET client_min_messages = warning; -SET row_security = off; - -SET default_tablespace = ''; - -SET default_table_access_method = heap; - --- --- Name: email; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE public.email ( - email_id bigint NOT NULL, - version integer DEFAULT 0 NOT NULL, - created_by text, - modified_by text, - deleted_by text, - created_at timestamp with time zone DEFAULT now() NOT NULL, - modified_at timestamp with time zone DEFAULT now() NOT NULL, - deleted_at timestamp with time zone, - email_type character varying(255) NOT NULL, - recipient_name text NOT NULL, - recipient_address text NOT NULL, - subject text NOT NULL, - body text NOT NULL, - sent_at timestamp with time zone, - ext_id text, - error text -); - - -ALTER TABLE public.email OWNER TO postgres; - --- --- Name: email_attachment; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE public.email_attachment ( - email_attachment_id bigint NOT NULL, - version integer DEFAULT 0 NOT NULL, - created_by text, - modified_by text, - deleted_by text, - created_at timestamp with time zone DEFAULT now() NOT NULL, - modified_at timestamp with time zone DEFAULT now() NOT NULL, - deleted_at timestamp with time zone, - email_id bigint NOT NULL, - name character varying(255) NOT NULL, - content_type character varying(255) NOT NULL, - data bytea NOT NULL -); - - -ALTER TABLE public.email_attachment OWNER TO postgres; - --- --- Name: email_attachment_email_attachment_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres --- - -ALTER TABLE public.email_attachment ALTER COLUMN email_attachment_id ADD GENERATED BY DEFAULT AS IDENTITY ( - SEQUENCE NAME public.email_attachment_email_attachment_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1 -); - - --- --- Name: email_email_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres --- - -ALTER TABLE public.email ALTER COLUMN email_id ADD GENERATED BY DEFAULT AS IDENTITY ( - SEQUENCE NAME public.email_email_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1 -); - - --- --- Name: email_type; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE public.email_type ( - name character varying(255) NOT NULL -); - - -ALTER TABLE public.email_type OWNER TO postgres; - --- --- Name: enrollment; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE public.enrollment ( - enrollment_id bigint NOT NULL, - version integer DEFAULT 0 NOT NULL, - created_by text, - modified_by text, - deleted_by text, - created_at timestamp with time zone DEFAULT now() NOT NULL, - modified_at timestamp with time zone DEFAULT now() NOT NULL, - deleted_at timestamp with time zone, - exam_event_id bigint NOT NULL, - person_id bigint NOT NULL, - skill_oral boolean NOT NULL, - skill_textual boolean NOT NULL, - skill_understanding boolean NOT NULL, - partial_exam_speaking boolean NOT NULL, - partial_exam_speech_comprehension boolean NOT NULL, - partial_exam_writing boolean NOT NULL, - partial_exam_reading_comprehension boolean NOT NULL, - status character varying(255) NOT NULL, - previous_enrollment text, - digital_certificate_consent boolean NOT NULL, - email text NOT NULL, - phone_number text NOT NULL, - street text, - postal_code text, - town text, - country text, - payment_link_hash character varying(255), - payment_link_expires_at timestamp with time zone -); - - -ALTER TABLE public.enrollment OWNER TO postgres; - --- --- Name: enrollment_enrollment_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres --- - -ALTER TABLE public.enrollment ALTER COLUMN enrollment_id ADD GENERATED BY DEFAULT AS IDENTITY ( - SEQUENCE NAME public.enrollment_enrollment_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1 -); - - --- --- Name: enrollment_status; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE public.enrollment_status ( - name character varying(255) NOT NULL -); - - -ALTER TABLE public.enrollment_status OWNER TO postgres; - --- --- Name: exam_event; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE public.exam_event ( - exam_event_id bigint NOT NULL, - version integer DEFAULT 0 NOT NULL, - created_by text, - modified_by text, - deleted_by text, - created_at timestamp with time zone DEFAULT now() NOT NULL, - modified_at timestamp with time zone DEFAULT now() NOT NULL, - deleted_at timestamp with time zone, - language character varying(10) NOT NULL, - level character varying(255) NOT NULL, - date date NOT NULL, - registration_closes date NOT NULL, - is_hidden boolean NOT NULL, - max_participants integer NOT NULL, - CONSTRAINT ck_exam_event_max_participants CHECK ((max_participants >= 0)), - CONSTRAINT ck_exam_event_registration_closes CHECK ((registration_closes <= date)) -); - - -ALTER TABLE public.exam_event OWNER TO postgres; - --- --- Name: exam_event_exam_event_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres --- - -ALTER TABLE public.exam_event ALTER COLUMN exam_event_id ADD GENERATED BY DEFAULT AS IDENTITY ( - SEQUENCE NAME public.exam_event_exam_event_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1 -); - - --- --- Name: exam_language; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE public.exam_language ( - name character varying(10) NOT NULL -); - - -ALTER TABLE public.exam_language OWNER TO postgres; - --- --- Name: exam_level; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE public.exam_level ( - name character varying(255) NOT NULL -); - - -ALTER TABLE public.exam_level OWNER TO postgres; - --- --- Name: payment; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE public.payment ( - payment_id bigint NOT NULL, - version integer DEFAULT 0 NOT NULL, - created_by text, - modified_by text, - deleted_by text, - created_at timestamp with time zone DEFAULT now() NOT NULL, - modified_at timestamp with time zone DEFAULT now() NOT NULL, - deleted_at timestamp with time zone, - enrollment_id bigint NOT NULL, - amount integer NOT NULL, - transaction_id text, - reference text, - payment_url text, - payment_status text -); - - -ALTER TABLE public.payment OWNER TO postgres; - --- --- Name: payment_payment_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres --- - -ALTER TABLE public.payment ALTER COLUMN payment_id ADD GENERATED BY DEFAULT AS IDENTITY ( - SEQUENCE NAME public.payment_payment_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1 -); - - --- --- Name: person; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE public.person ( - person_id bigint NOT NULL, - version integer DEFAULT 0 NOT NULL, - created_by text, - modified_by text, - deleted_by text, - created_at timestamp with time zone DEFAULT now() NOT NULL, - modified_at timestamp with time zone DEFAULT now() NOT NULL, - deleted_at timestamp with time zone, - last_name text NOT NULL, - first_name text NOT NULL, - oid character varying(255), - other_identifier character varying(1024), - latest_identified_at timestamp with time zone NOT NULL -); - - -ALTER TABLE public.person OWNER TO postgres; - --- --- Name: person_person_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres --- - -ALTER TABLE public.person ALTER COLUMN person_id ADD GENERATED BY DEFAULT AS IDENTITY ( - SEQUENCE NAME public.person_person_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1 -); - - --- --- Name: reservation; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE public.reservation ( - reservation_id bigint NOT NULL, - version integer DEFAULT 0 NOT NULL, - created_by text, - modified_by text, - deleted_by text, - created_at timestamp with time zone DEFAULT now() NOT NULL, - modified_at timestamp with time zone DEFAULT now() NOT NULL, - deleted_at timestamp with time zone, - exam_event_id bigint NOT NULL, - person_id bigint NOT NULL, - expires_at timestamp with time zone NOT NULL, - renewed_at timestamp with time zone -); - - -ALTER TABLE public.reservation OWNER TO postgres; - --- --- Name: reservation_reservation_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres --- - -ALTER TABLE public.reservation ALTER COLUMN reservation_id ADD GENERATED BY DEFAULT AS IDENTITY ( - SEQUENCE NAME public.reservation_reservation_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1 -); - - --- --- Name: shedlock; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE public.shedlock ( - name character varying(64) NOT NULL, - lock_until timestamp without time zone NOT NULL, - locked_at timestamp without time zone NOT NULL, - locked_by character varying(255) NOT NULL -); - - -ALTER TABLE public.shedlock OWNER TO postgres; - --- --- Name: spring_session; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE public.spring_session ( - primary_id character(36) NOT NULL, - session_id character(36) NOT NULL, - creation_time bigint NOT NULL, - last_access_time bigint NOT NULL, - max_inactive_interval integer NOT NULL, - expiry_time bigint NOT NULL, - principal_name character varying(100) -); - - -ALTER TABLE public.spring_session OWNER TO postgres; - --- --- Name: spring_session_attributes; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE public.spring_session_attributes ( - session_primary_id character(36) NOT NULL, - attribute_name character varying(200) NOT NULL, - attribute_bytes bytea NOT NULL -); - - -ALTER TABLE public.spring_session_attributes OWNER TO postgres; - --- --- Name: email_attachment email_attachment_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.email_attachment - ADD CONSTRAINT email_attachment_pkey PRIMARY KEY (email_attachment_id); - - --- --- Name: email email_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.email - ADD CONSTRAINT email_pkey PRIMARY KEY (email_id); - - --- --- Name: email_type email_type_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.email_type - ADD CONSTRAINT email_type_pkey PRIMARY KEY (name); - - --- --- Name: enrollment enrollment_payment_link_hash_uniq_idx; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.enrollment - ADD CONSTRAINT enrollment_payment_link_hash_uniq_idx UNIQUE (payment_link_hash); - - --- --- Name: enrollment enrollment_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.enrollment - ADD CONSTRAINT enrollment_pkey PRIMARY KEY (enrollment_id); - - --- --- Name: enrollment_status enrollment_status_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.enrollment_status - ADD CONSTRAINT enrollment_status_pkey PRIMARY KEY (name); - - --- --- Name: exam_event exam_event_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.exam_event - ADD CONSTRAINT exam_event_pkey PRIMARY KEY (exam_event_id); - - --- --- Name: exam_language exam_language_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.exam_language - ADD CONSTRAINT exam_language_pkey PRIMARY KEY (name); - - --- --- Name: exam_level exam_level_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.exam_level - ADD CONSTRAINT exam_level_pkey PRIMARY KEY (name); - - --- --- Name: payment payment_id_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.payment - ADD CONSTRAINT payment_id_pkey PRIMARY KEY (payment_id); - - --- --- Name: person person_oid_uniq_idx; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.person - ADD CONSTRAINT person_oid_uniq_idx UNIQUE (oid); - - --- --- Name: person person_other_id_uniq_idx; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.person - ADD CONSTRAINT person_other_id_uniq_idx UNIQUE (other_identifier); - - --- --- Name: person person_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.person - ADD CONSTRAINT person_pkey PRIMARY KEY (person_id); - - --- --- Name: reservation reservation_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.reservation - ADD CONSTRAINT reservation_pkey PRIMARY KEY (reservation_id); - - --- --- Name: shedlock shedlock_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.shedlock - ADD CONSTRAINT shedlock_pkey PRIMARY KEY (name); - - --- --- Name: spring_session_attributes spring_session_attributes_pk; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.spring_session_attributes - ADD CONSTRAINT spring_session_attributes_pk PRIMARY KEY (session_primary_id, attribute_name); - - --- --- Name: spring_session spring_session_id_idx; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.spring_session - ADD CONSTRAINT spring_session_id_idx UNIQUE (session_id); - - --- --- Name: spring_session spring_session_pk; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.spring_session - ADD CONSTRAINT spring_session_pk PRIMARY KEY (primary_id); - - --- --- Name: enrollment uk_enrollment_exam_event_person; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.enrollment - ADD CONSTRAINT uk_enrollment_exam_event_person UNIQUE (exam_event_id, person_id); - - --- --- Name: exam_event uk_exam_event_language_level_date; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.exam_event - ADD CONSTRAINT uk_exam_event_language_level_date UNIQUE (language, level, date); - - --- --- Name: reservation uk_reservation_exam_event_person; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.reservation - ADD CONSTRAINT uk_reservation_exam_event_person UNIQUE (exam_event_id, person_id); - - --- --- Name: spring_session_expires_idx; Type: INDEX; Schema: public; Owner: postgres --- - -CREATE INDEX spring_session_expires_idx ON public.spring_session USING btree (expiry_time); - - --- --- Name: spring_session_principal_idx; Type: INDEX; Schema: public; Owner: postgres --- - -CREATE INDEX spring_session_principal_idx ON public.spring_session USING btree (principal_name); - - --- --- Name: email_attachment fk_email_attachment_email; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.email_attachment - ADD CONSTRAINT fk_email_attachment_email FOREIGN KEY (email_id) REFERENCES public.email(email_id); - - --- --- Name: email fk_email_email_type; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.email - ADD CONSTRAINT fk_email_email_type FOREIGN KEY (email_type) REFERENCES public.email_type(name); - - --- --- Name: enrollment fk_enrollment_exam_event; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.enrollment - ADD CONSTRAINT fk_enrollment_exam_event FOREIGN KEY (exam_event_id) REFERENCES public.exam_event(exam_event_id); - - --- --- Name: enrollment fk_enrollment_person; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.enrollment - ADD CONSTRAINT fk_enrollment_person FOREIGN KEY (person_id) REFERENCES public.person(person_id); - - --- --- Name: enrollment fk_enrollment_status; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.enrollment - ADD CONSTRAINT fk_enrollment_status FOREIGN KEY (status) REFERENCES public.enrollment_status(name); - - --- --- Name: exam_event fk_exam_event_language; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.exam_event - ADD CONSTRAINT fk_exam_event_language FOREIGN KEY (language) REFERENCES public.exam_language(name); - - --- --- Name: exam_event fk_exam_event_level; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.exam_event - ADD CONSTRAINT fk_exam_event_level FOREIGN KEY (level) REFERENCES public.exam_level(name); - - --- --- Name: payment fk_payment_enrollment; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.payment - ADD CONSTRAINT fk_payment_enrollment FOREIGN KEY (enrollment_id) REFERENCES public.enrollment(enrollment_id); - - --- --- Name: reservation fk_reservation_exam_event; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.reservation - ADD CONSTRAINT fk_reservation_exam_event FOREIGN KEY (exam_event_id) REFERENCES public.exam_event(exam_event_id); - - --- --- Name: reservation fk_reservation_person; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.reservation - ADD CONSTRAINT fk_reservation_person FOREIGN KEY (person_id) REFERENCES public.person(person_id); - - --- --- Name: spring_session_attributes spring_session_attributes_fk; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.spring_session_attributes - ADD CONSTRAINT spring_session_attributes_fk FOREIGN KEY (session_primary_id) REFERENCES public.spring_session(primary_id) ON DELETE CASCADE; - - --- --- PostgreSQL database dump complete --- - +-- +-- PostgreSQL database dump +-- + +-- Dumped from database version 16.3 +-- Dumped by pg_dump version 16.3 + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: cas_ticket; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.cas_ticket ( + session_id character varying(255) NOT NULL, + ticket character varying(255) NOT NULL, + created_at timestamp with time zone NOT NULL +); + + +ALTER TABLE public.cas_ticket OWNER TO postgres; + +-- +-- Name: email; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.email ( + email_id bigint NOT NULL, + version integer DEFAULT 0 NOT NULL, + created_by text, + modified_by text, + deleted_by text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + modified_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + email_type character varying(255) NOT NULL, + recipient_name text NOT NULL, + recipient_address text NOT NULL, + subject text NOT NULL, + body text NOT NULL, + sent_at timestamp with time zone, + ext_id text, + error text +); + + +ALTER TABLE public.email OWNER TO postgres; + +-- +-- Name: email_attachment; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.email_attachment ( + email_attachment_id bigint NOT NULL, + version integer DEFAULT 0 NOT NULL, + created_by text, + modified_by text, + deleted_by text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + modified_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + email_id bigint NOT NULL, + name character varying(255) NOT NULL, + content_type character varying(255) NOT NULL, + data bytea NOT NULL +); + + +ALTER TABLE public.email_attachment OWNER TO postgres; + +-- +-- Name: email_attachment_email_attachment_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +ALTER TABLE public.email_attachment ALTER COLUMN email_attachment_id ADD GENERATED BY DEFAULT AS IDENTITY ( + SEQUENCE NAME public.email_attachment_email_attachment_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + +-- +-- Name: email_email_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +ALTER TABLE public.email ALTER COLUMN email_id ADD GENERATED BY DEFAULT AS IDENTITY ( + SEQUENCE NAME public.email_email_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + +-- +-- Name: email_type; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.email_type ( + name character varying(255) NOT NULL +); + + +ALTER TABLE public.email_type OWNER TO postgres; + +-- +-- Name: enrollment; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.enrollment ( + enrollment_id bigint NOT NULL, + version integer DEFAULT 0 NOT NULL, + created_by text, + modified_by text, + deleted_by text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + modified_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + exam_event_id bigint NOT NULL, + person_id bigint NOT NULL, + skill_oral boolean NOT NULL, + skill_textual boolean NOT NULL, + skill_understanding boolean NOT NULL, + partial_exam_speaking boolean NOT NULL, + partial_exam_speech_comprehension boolean NOT NULL, + partial_exam_writing boolean NOT NULL, + partial_exam_reading_comprehension boolean NOT NULL, + status character varying(255) NOT NULL, + previous_enrollment text, + digital_certificate_consent boolean NOT NULL, + email text NOT NULL, + phone_number text NOT NULL, + street text, + postal_code text, + town text, + country text, + payment_link_hash character varying(255), + payment_link_expires_at timestamp with time zone, + free_enrollment bigint, + is_queued boolean +); + + +ALTER TABLE public.enrollment OWNER TO postgres; + +-- +-- Name: enrollment_appointment; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.enrollment_appointment ( + enrollment_appointment_id bigint NOT NULL, + version integer DEFAULT 0 NOT NULL, + created_by text, + modified_by text, + deleted_by text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + modified_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + skill_oral boolean NOT NULL, + skill_textual boolean NOT NULL, + skill_understanding boolean NOT NULL, + partial_exam_speaking boolean NOT NULL, + partial_exam_speech_comprehension boolean NOT NULL, + partial_exam_writing boolean NOT NULL, + partial_exam_reading_comprehension boolean NOT NULL, + status character varying(255) NOT NULL, + previous_enrollment_date date, + digital_certificate_consent boolean NOT NULL, + auth_hash text, + email text, + phone_number text, + first_name text, + last_name text, + street text, + postal_code text, + town text, + country text, + person_id bigint, + payment_link_hash text, + auth_hash_expires timestamp with time zone, + auth_hash_sent timestamp with time zone, + previous_enrollment text, + message text, + grade_id bigint, + examiner_id bigint, + examiner_exam_event_id bigint +); + + +ALTER TABLE public.enrollment_appointment OWNER TO postgres; + +-- +-- Name: enrollment_appointment_enrollment_appointment_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +ALTER TABLE public.enrollment_appointment ALTER COLUMN enrollment_appointment_id ADD GENERATED BY DEFAULT AS IDENTITY ( + SEQUENCE NAME public.enrollment_appointment_enrollment_appointment_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + +-- +-- Name: enrollment_enrollment_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +ALTER TABLE public.enrollment ALTER COLUMN enrollment_id ADD GENERATED BY DEFAULT AS IDENTITY ( + SEQUENCE NAME public.enrollment_enrollment_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + +-- +-- Name: enrollment_grade; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.enrollment_grade ( + grade_id bigint NOT NULL, + version integer DEFAULT 0 NOT NULL, + created_by text, + modified_by text, + deleted_by text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + modified_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + speaking_grade text, + speech_comprehension_grade text, + writing_grade text, + comprehension_grade text, + speaking_comment text, + speech_comprehension_comment text, + writing_comment text, + comprehension_comment text +); + + +ALTER TABLE public.enrollment_grade OWNER TO postgres; + +-- +-- Name: enrollment_grade_grade_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +ALTER TABLE public.enrollment_grade ALTER COLUMN grade_id ADD GENERATED BY DEFAULT AS IDENTITY ( + SEQUENCE NAME public.enrollment_grade_grade_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + +-- +-- Name: enrollment_status; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.enrollment_status ( + name character varying(255) NOT NULL +); + + +ALTER TABLE public.enrollment_status OWNER TO postgres; + +-- +-- Name: exam_event; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.exam_event ( + exam_event_id bigint NOT NULL, + version integer DEFAULT 0 NOT NULL, + created_by text, + modified_by text, + deleted_by text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + modified_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + language character varying(10) NOT NULL, + level character varying(255) NOT NULL, + date date NOT NULL, + registration_closes timestamp with time zone NOT NULL, + is_hidden boolean NOT NULL, + max_participants integer NOT NULL, + registration_opens timestamp with time zone NOT NULL, + CONSTRAINT ck_exam_event_max_participants CHECK ((max_participants >= 0)) +); + + +ALTER TABLE public.exam_event OWNER TO postgres; + +-- +-- Name: exam_event_exam_event_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +ALTER TABLE public.exam_event ALTER COLUMN exam_event_id ADD GENERATED BY DEFAULT AS IDENTITY ( + SEQUENCE NAME public.exam_event_exam_event_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + +-- +-- Name: exam_language; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.exam_language ( + name character varying(10) NOT NULL +); + + +ALTER TABLE public.exam_language OWNER TO postgres; + +-- +-- Name: exam_level; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.exam_level ( + name character varying(255) NOT NULL +); + + +ALTER TABLE public.exam_level OWNER TO postgres; + +-- +-- Name: examiner; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.examiner ( + examiner_id bigint NOT NULL, + version integer DEFAULT 0 NOT NULL, + created_by text, + modified_by text, + deleted_by text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + modified_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + oid character varying(255) NOT NULL, + email character varying(255) NOT NULL, + phone_number character varying(255) NOT NULL, + last_name text NOT NULL, + first_name text NOT NULL, + nickname text NOT NULL, + exam_language_finnish boolean NOT NULL, + exam_language_swedish boolean NOT NULL, + is_public boolean NOT NULL +); + + +ALTER TABLE public.examiner OWNER TO postgres; + +-- +-- Name: examiner_exam_event; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.examiner_exam_event ( + examiner_exam_event_id bigint NOT NULL, + version integer DEFAULT 0 NOT NULL, + created_by text, + modified_by text, + deleted_by text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + modified_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + date date NOT NULL, + language character varying(10) NOT NULL, + examiner_id bigint NOT NULL, + is_hidden boolean NOT NULL, + registration_closes timestamp with time zone, + max_participants integer, + municipality_id bigint NOT NULL, + location text NOT NULL +); + + +ALTER TABLE public.examiner_exam_event OWNER TO postgres; + +-- +-- Name: examiner_exam_event_examiner_exam_event_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +ALTER TABLE public.examiner_exam_event ALTER COLUMN examiner_exam_event_id ADD GENERATED BY DEFAULT AS IDENTITY ( + SEQUENCE NAME public.examiner_exam_event_examiner_exam_event_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + +-- +-- Name: examiner_examiner_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +ALTER TABLE public.examiner ALTER COLUMN examiner_id ADD GENERATED BY DEFAULT AS IDENTITY ( + SEQUENCE NAME public.examiner_examiner_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + +-- +-- Name: examiner_municipality; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.examiner_municipality ( + examiner_municipality_id bigint NOT NULL, + municipality_id bigint NOT NULL, + examiner_id bigint NOT NULL +); + + +ALTER TABLE public.examiner_municipality OWNER TO postgres; + +-- +-- Name: examiner_municipality_examiner_municipality_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +ALTER TABLE public.examiner_municipality ALTER COLUMN examiner_municipality_id ADD GENERATED BY DEFAULT AS IDENTITY ( + SEQUENCE NAME public.examiner_municipality_examiner_municipality_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + +-- +-- Name: free_enrollment; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.free_enrollment ( + free_enrollment_id bigint NOT NULL, + version integer DEFAULT 0 NOT NULL, + created_by text, + modified_by text, + deleted_by text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + modified_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + person_id bigint NOT NULL, + source character varying(255) NOT NULL, + type character varying(255) NOT NULL, + approved boolean, + comment text +); + + +ALTER TABLE public.free_enrollment OWNER TO postgres; + +-- +-- Name: free_enrollment_free_enrollment_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +ALTER TABLE public.free_enrollment ALTER COLUMN free_enrollment_id ADD GENERATED BY DEFAULT AS IDENTITY ( + SEQUENCE NAME public.free_enrollment_free_enrollment_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + +-- +-- Name: koski_educations; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.koski_educations ( + koski_educations_id bigint NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + free_enrollment_id bigint, + exam_event_id bigint NOT NULL, + matriculation_exam boolean NOT NULL, + higher_education_concluded boolean NOT NULL, + higher_education_enrolled boolean NOT NULL, + eb boolean NOT NULL, + dia boolean NOT NULL, + other boolean NOT NULL +); + + +ALTER TABLE public.koski_educations OWNER TO postgres; + +-- +-- Name: koski_educations_koski_educations_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +ALTER TABLE public.koski_educations ALTER COLUMN koski_educations_id ADD GENERATED BY DEFAULT AS IDENTITY ( + SEQUENCE NAME public.koski_educations_koski_educations_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + +-- +-- Name: municipality; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.municipality ( + municipality_id bigint NOT NULL, + version integer DEFAULT 0 NOT NULL, + created_by text, + modified_by text, + deleted_by text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + modified_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + code character varying(255) NOT NULL, + name_fi character varying(255) NOT NULL, + name_sv character varying(255) NOT NULL +); + + +ALTER TABLE public.municipality OWNER TO postgres; + +-- +-- Name: municipality_municipality_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +ALTER TABLE public.municipality ALTER COLUMN municipality_id ADD GENERATED BY DEFAULT AS IDENTITY ( + SEQUENCE NAME public.municipality_municipality_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + +-- +-- Name: payment; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.payment ( + payment_id bigint NOT NULL, + version integer DEFAULT 0 NOT NULL, + created_by text, + modified_by text, + deleted_by text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + modified_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + enrollment_id bigint, + amount integer NOT NULL, + transaction_id text, + reference text, + payment_url text, + payment_status text, + refunded_at timestamp with time zone, + enrollment_appointment_id bigint +); + + +ALTER TABLE public.payment OWNER TO postgres; + +-- +-- Name: payment_payment_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +ALTER TABLE public.payment ALTER COLUMN payment_id ADD GENERATED BY DEFAULT AS IDENTITY ( + SEQUENCE NAME public.payment_payment_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + +-- +-- Name: person; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.person ( + person_id bigint NOT NULL, + version integer DEFAULT 0 NOT NULL, + created_by text, + modified_by text, + deleted_by text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + modified_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + last_name text NOT NULL, + first_name text NOT NULL, + oid character varying(255), + other_identifier character varying(1024), + latest_identified_at timestamp with time zone NOT NULL, + uuid uuid NOT NULL +); + + +ALTER TABLE public.person OWNER TO postgres; + +-- +-- Name: person_person_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +ALTER TABLE public.person ALTER COLUMN person_id ADD GENERATED BY DEFAULT AS IDENTITY ( + SEQUENCE NAME public.person_person_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + +-- +-- Name: reservation; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.reservation ( + reservation_id bigint NOT NULL, + version integer DEFAULT 0 NOT NULL, + created_by text, + modified_by text, + deleted_by text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + modified_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + exam_event_id bigint NOT NULL, + person_id bigint NOT NULL, + expires_at timestamp with time zone NOT NULL, + renewed_at timestamp with time zone +); + + +ALTER TABLE public.reservation OWNER TO postgres; + +-- +-- Name: reservation_reservation_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +ALTER TABLE public.reservation ALTER COLUMN reservation_id ADD GENERATED BY DEFAULT AS IDENTITY ( + SEQUENCE NAME public.reservation_reservation_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + +-- +-- Name: shedlock; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.shedlock ( + name character varying(64) NOT NULL, + lock_until timestamp without time zone NOT NULL, + locked_at timestamp without time zone NOT NULL, + locked_by character varying(255) NOT NULL +); + + +ALTER TABLE public.shedlock OWNER TO postgres; + +-- +-- Name: spring_session; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.spring_session ( + primary_id character(36) NOT NULL, + session_id character(36) NOT NULL, + creation_time bigint NOT NULL, + last_access_time bigint NOT NULL, + max_inactive_interval integer NOT NULL, + expiry_time bigint NOT NULL, + principal_name character varying(100) +); + + +ALTER TABLE public.spring_session OWNER TO postgres; + +-- +-- Name: spring_session_attributes; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.spring_session_attributes ( + session_primary_id character(36) NOT NULL, + attribute_name character varying(200) NOT NULL, + attribute_bytes bytea NOT NULL +); + + +ALTER TABLE public.spring_session_attributes OWNER TO postgres; + +-- +-- Name: uploaded_file_attachment; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.uploaded_file_attachment ( + attachment_id bigint NOT NULL, + version integer DEFAULT 0 NOT NULL, + created_by text, + modified_by text, + deleted_by text, + created_at timestamp with time zone DEFAULT now() NOT NULL, + modified_at timestamp with time zone DEFAULT now() NOT NULL, + deleted_at timestamp with time zone, + free_enrollment_id bigint NOT NULL, + key character varying(255) NOT NULL, + filename text NOT NULL, + size integer NOT NULL +); + + +ALTER TABLE public.uploaded_file_attachment OWNER TO postgres; + +-- +-- Name: uploaded_file_attachment_attachment_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +ALTER TABLE public.uploaded_file_attachment ALTER COLUMN attachment_id ADD GENERATED BY DEFAULT AS IDENTITY ( + SEQUENCE NAME public.uploaded_file_attachment_attachment_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + +-- +-- Name: cas_ticket cas_ticket_session_id_uniq_idx; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.cas_ticket + ADD CONSTRAINT cas_ticket_session_id_uniq_idx UNIQUE (session_id); + + +-- +-- Name: cas_ticket cas_ticket_ticket_uniq_idx; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.cas_ticket + ADD CONSTRAINT cas_ticket_ticket_uniq_idx UNIQUE (ticket); + + +-- +-- Name: email_attachment email_attachment_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.email_attachment + ADD CONSTRAINT email_attachment_pkey PRIMARY KEY (email_attachment_id); + + +-- +-- Name: email email_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.email + ADD CONSTRAINT email_pkey PRIMARY KEY (email_id); + + +-- +-- Name: email_type email_type_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.email_type + ADD CONSTRAINT email_type_pkey PRIMARY KEY (name); + + +-- +-- Name: enrollment_appointment enrollment_appointment_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.enrollment_appointment + ADD CONSTRAINT enrollment_appointment_pkey PRIMARY KEY (enrollment_appointment_id); + + +-- +-- Name: enrollment enrollment_payment_link_hash_uniq_idx; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.enrollment + ADD CONSTRAINT enrollment_payment_link_hash_uniq_idx UNIQUE (payment_link_hash); + + +-- +-- Name: enrollment enrollment_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.enrollment + ADD CONSTRAINT enrollment_pkey PRIMARY KEY (enrollment_id); + + +-- +-- Name: enrollment_status enrollment_status_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.enrollment_status + ADD CONSTRAINT enrollment_status_pkey PRIMARY KEY (name); + + +-- +-- Name: exam_event exam_event_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.exam_event + ADD CONSTRAINT exam_event_pkey PRIMARY KEY (exam_event_id); + + +-- +-- Name: exam_language exam_language_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.exam_language + ADD CONSTRAINT exam_language_pkey PRIMARY KEY (name); + + +-- +-- Name: exam_level exam_level_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.exam_level + ADD CONSTRAINT exam_level_pkey PRIMARY KEY (name); + + +-- +-- Name: examiner_exam_event examiner_exam_event_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.examiner_exam_event + ADD CONSTRAINT examiner_exam_event_pkey PRIMARY KEY (examiner_exam_event_id); + + +-- +-- Name: examiner_municipality examiner_municipality_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.examiner_municipality + ADD CONSTRAINT examiner_municipality_pkey PRIMARY KEY (examiner_municipality_id); + + +-- +-- Name: examiner examiner_oid_uniq_idx; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.examiner + ADD CONSTRAINT examiner_oid_uniq_idx UNIQUE (oid); + + +-- +-- Name: examiner examiner_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.examiner + ADD CONSTRAINT examiner_pkey PRIMARY KEY (examiner_id); + + +-- +-- Name: free_enrollment free_enrollment_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.free_enrollment + ADD CONSTRAINT free_enrollment_pkey PRIMARY KEY (free_enrollment_id); + + +-- +-- Name: enrollment_grade grade_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.enrollment_grade + ADD CONSTRAINT grade_pkey PRIMARY KEY (grade_id); + + +-- +-- Name: koski_educations koski_educations_free_enrollment_id_key; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.koski_educations + ADD CONSTRAINT koski_educations_free_enrollment_id_key UNIQUE (free_enrollment_id); + + +-- +-- Name: koski_educations koski_educations_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.koski_educations + ADD CONSTRAINT koski_educations_pkey PRIMARY KEY (koski_educations_id); + + +-- +-- Name: municipality municipality_code_uniq_idx; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.municipality + ADD CONSTRAINT municipality_code_uniq_idx UNIQUE (code); + + +-- +-- Name: municipality municipality_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.municipality + ADD CONSTRAINT municipality_pkey PRIMARY KEY (municipality_id); + + +-- +-- Name: payment payment_id_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.payment + ADD CONSTRAINT payment_id_pkey PRIMARY KEY (payment_id); + + +-- +-- Name: person person_oid_uniq_idx; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.person + ADD CONSTRAINT person_oid_uniq_idx UNIQUE (oid); + + +-- +-- Name: person person_other_id_uniq_idx; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.person + ADD CONSTRAINT person_other_id_uniq_idx UNIQUE (other_identifier); + + +-- +-- Name: person person_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.person + ADD CONSTRAINT person_pkey PRIMARY KEY (person_id); + + +-- +-- Name: person person_uuid_key; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.person + ADD CONSTRAINT person_uuid_key UNIQUE (uuid); + + +-- +-- Name: reservation reservation_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.reservation + ADD CONSTRAINT reservation_pkey PRIMARY KEY (reservation_id); + + +-- +-- Name: shedlock shedlock_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.shedlock + ADD CONSTRAINT shedlock_pkey PRIMARY KEY (name); + + +-- +-- Name: spring_session_attributes spring_session_attributes_pk; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.spring_session_attributes + ADD CONSTRAINT spring_session_attributes_pk PRIMARY KEY (session_primary_id, attribute_name); + + +-- +-- Name: spring_session spring_session_id_idx; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.spring_session + ADD CONSTRAINT spring_session_id_idx UNIQUE (session_id); + + +-- +-- Name: spring_session spring_session_pk; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.spring_session + ADD CONSTRAINT spring_session_pk PRIMARY KEY (primary_id); + + +-- +-- Name: enrollment uk_enrollment_exam_event_person; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.enrollment + ADD CONSTRAINT uk_enrollment_exam_event_person UNIQUE (exam_event_id, person_id); + + +-- +-- Name: exam_event uk_exam_event_language_level_date; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.exam_event + ADD CONSTRAINT uk_exam_event_language_level_date UNIQUE (language, level, date); + + +-- +-- Name: examiner_municipality uk_examiner_municipality_examiner_id_municipality_id; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.examiner_municipality + ADD CONSTRAINT uk_examiner_municipality_examiner_id_municipality_id UNIQUE (examiner_id, municipality_id); + + +-- +-- Name: reservation uk_reservation_exam_event_person; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.reservation + ADD CONSTRAINT uk_reservation_exam_event_person UNIQUE (exam_event_id, person_id); + + +-- +-- Name: uploaded_file_attachment uploaded_file_attachment_key_uniq_idx; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.uploaded_file_attachment + ADD CONSTRAINT uploaded_file_attachment_key_uniq_idx UNIQUE (key); + + +-- +-- Name: uploaded_file_attachment uploaded_file_attachment_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.uploaded_file_attachment + ADD CONSTRAINT uploaded_file_attachment_pkey PRIMARY KEY (attachment_id); + + +-- +-- Name: spring_session_expires_idx; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX spring_session_expires_idx ON public.spring_session USING btree (expiry_time); + + +-- +-- Name: spring_session_principal_idx; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX spring_session_principal_idx ON public.spring_session USING btree (principal_name); + + +-- +-- Name: email_attachment fk_email_attachment_email; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.email_attachment + ADD CONSTRAINT fk_email_attachment_email FOREIGN KEY (email_id) REFERENCES public.email(email_id); + + +-- +-- Name: email fk_email_email_type; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.email + ADD CONSTRAINT fk_email_email_type FOREIGN KEY (email_type) REFERENCES public.email_type(name); + + +-- +-- Name: enrollment_appointment fk_enrollment_appointment_examiner_exam_event_id; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.enrollment_appointment + ADD CONSTRAINT fk_enrollment_appointment_examiner_exam_event_id FOREIGN KEY (examiner_exam_event_id) REFERENCES public.examiner_exam_event(examiner_exam_event_id); + + +-- +-- Name: enrollment_appointment fk_enrollment_appointment_examiner_id; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.enrollment_appointment + ADD CONSTRAINT fk_enrollment_appointment_examiner_id FOREIGN KEY (examiner_id) REFERENCES public.examiner(examiner_id); + + +-- +-- Name: enrollment_appointment fk_enrollment_appointment_grade_id; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.enrollment_appointment + ADD CONSTRAINT fk_enrollment_appointment_grade_id FOREIGN KEY (grade_id) REFERENCES public.enrollment_grade(grade_id); + + +-- +-- Name: enrollment fk_enrollment_exam_event; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.enrollment + ADD CONSTRAINT fk_enrollment_exam_event FOREIGN KEY (exam_event_id) REFERENCES public.exam_event(exam_event_id); + + +-- +-- Name: enrollment fk_enrollment_free_enrollment; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.enrollment + ADD CONSTRAINT fk_enrollment_free_enrollment FOREIGN KEY (free_enrollment) REFERENCES public.free_enrollment(free_enrollment_id); + + +-- +-- Name: enrollment fk_enrollment_person; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.enrollment + ADD CONSTRAINT fk_enrollment_person FOREIGN KEY (person_id) REFERENCES public.person(person_id); + + +-- +-- Name: enrollment fk_enrollment_status; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.enrollment + ADD CONSTRAINT fk_enrollment_status FOREIGN KEY (status) REFERENCES public.enrollment_status(name); + + +-- +-- Name: exam_event fk_exam_event_language; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.exam_event + ADD CONSTRAINT fk_exam_event_language FOREIGN KEY (language) REFERENCES public.exam_language(name); + + +-- +-- Name: exam_event fk_exam_event_level; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.exam_event + ADD CONSTRAINT fk_exam_event_level FOREIGN KEY (level) REFERENCES public.exam_level(name); + + +-- +-- Name: examiner_exam_event fk_examiner_exam_event_examiner_id; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.examiner_exam_event + ADD CONSTRAINT fk_examiner_exam_event_examiner_id FOREIGN KEY (examiner_id) REFERENCES public.examiner(examiner_id); + + +-- +-- Name: examiner_exam_event fk_examiner_exam_event_language; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.examiner_exam_event + ADD CONSTRAINT fk_examiner_exam_event_language FOREIGN KEY (language) REFERENCES public.exam_language(name); + + +-- +-- Name: examiner_exam_event fk_examiner_exam_event_municipality_id; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.examiner_exam_event + ADD CONSTRAINT fk_examiner_exam_event_municipality_id FOREIGN KEY (municipality_id) REFERENCES public.municipality(municipality_id); + + +-- +-- Name: examiner_municipality fk_examiner_municipality_examiner; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.examiner_municipality + ADD CONSTRAINT fk_examiner_municipality_examiner FOREIGN KEY (examiner_id) REFERENCES public.examiner(examiner_id); + + +-- +-- Name: examiner_municipality fk_examiner_municipality_municipality; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.examiner_municipality + ADD CONSTRAINT fk_examiner_municipality_municipality FOREIGN KEY (municipality_id) REFERENCES public.municipality(municipality_id); + + +-- +-- Name: free_enrollment fk_free_enrollment_person; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.free_enrollment + ADD CONSTRAINT fk_free_enrollment_person FOREIGN KEY (person_id) REFERENCES public.person(person_id); + + +-- +-- Name: koski_educations fk_koski_educations_exam_event; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.koski_educations + ADD CONSTRAINT fk_koski_educations_exam_event FOREIGN KEY (exam_event_id) REFERENCES public.exam_event(exam_event_id); + + +-- +-- Name: koski_educations fk_koski_educations_free_enrollment; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.koski_educations + ADD CONSTRAINT fk_koski_educations_free_enrollment FOREIGN KEY (free_enrollment_id) REFERENCES public.free_enrollment(free_enrollment_id); + + +-- +-- Name: payment fk_payment_enrollment; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.payment + ADD CONSTRAINT fk_payment_enrollment FOREIGN KEY (enrollment_id) REFERENCES public.enrollment(enrollment_id); + + +-- +-- Name: payment fk_payment_enrollment_appointment_id; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.payment + ADD CONSTRAINT fk_payment_enrollment_appointment_id FOREIGN KEY (enrollment_appointment_id) REFERENCES public.enrollment_appointment(enrollment_appointment_id); + + +-- +-- Name: reservation fk_reservation_exam_event; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.reservation + ADD CONSTRAINT fk_reservation_exam_event FOREIGN KEY (exam_event_id) REFERENCES public.exam_event(exam_event_id); + + +-- +-- Name: reservation fk_reservation_person; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.reservation + ADD CONSTRAINT fk_reservation_person FOREIGN KEY (person_id) REFERENCES public.person(person_id); + + +-- +-- Name: uploaded_file_attachment fk_uploaded_file_attachment_free_enrollment; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.uploaded_file_attachment + ADD CONSTRAINT fk_uploaded_file_attachment_free_enrollment FOREIGN KEY (free_enrollment_id) REFERENCES public.free_enrollment(free_enrollment_id); + + +-- +-- Name: spring_session_attributes spring_session_attributes_fk; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.spring_session_attributes + ADD CONSTRAINT spring_session_attributes_fk FOREIGN KEY (session_primary_id) REFERENCES public.spring_session(primary_id) ON DELETE CASCADE; + + +-- +-- PostgreSQL database dump complete +-- + diff --git a/backend/vkt/db/2_tables_data.sql b/backend/vkt/db/2_tables_data.sql index b96e48ef1..efa0a81e1 100644 --- a/backend/vkt/db/2_tables_data.sql +++ b/backend/vkt/db/2_tables_data.sql @@ -1,65 +1,66 @@ --- --- PostgreSQL database dump --- - --- Dumped from database version 12.9 (Debian 12.9-1.pgdg110+1) --- Dumped by pg_dump version 14.7 (Homebrew) - -SET statement_timeout = 0; -SET lock_timeout = 0; -SET idle_in_transaction_session_timeout = 0; -SET client_encoding = 'UTF8'; -SET standard_conforming_strings = on; -SELECT pg_catalog.set_config('search_path', '', false); -SET check_function_bodies = false; -SET xmloption = content; -SET client_min_messages = warning; -SET row_security = off; - --- --- Data for Name: email_type; Type: TABLE DATA; Schema: public; Owner: postgres --- - -COPY public.email_type (name) FROM stdin; -ENROLLMENT_CONFIRMATION -ENROLLMENT_TO_QUEUE_CONFIRMATION -\. - - --- --- Data for Name: enrollment_status; Type: TABLE DATA; Schema: public; Owner: postgres --- - -COPY public.enrollment_status (name) FROM stdin; -PAID -QUEUED -CANCELED -EXPECTING_PAYMENT_UNFINISHED_ENROLLMENT -CANCELED_UNFINISHED_ENROLLMENT -SHIFTED_FROM_QUEUE -\. - - --- --- Data for Name: exam_language; Type: TABLE DATA; Schema: public; Owner: postgres --- - -COPY public.exam_language (name) FROM stdin; -FI -SV -\. - - --- --- Data for Name: exam_level; Type: TABLE DATA; Schema: public; Owner: postgres --- - -COPY public.exam_level (name) FROM stdin; -EXCELLENT -\. - - --- --- PostgreSQL database dump complete --- - +-- +-- PostgreSQL database dump +-- + +-- Dumped from database version 16.3 +-- Dumped by pg_dump version 16.3 + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +-- +-- Data for Name: email_type; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY public.email_type (name) FROM stdin; +ENROLLMENT_CONFIRMATION +ENROLLMENT_TO_QUEUE_CONFIRMATION +\. + + +-- +-- Data for Name: enrollment_status; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY public.enrollment_status (name) FROM stdin; +QUEUED +CANCELED +EXPECTING_PAYMENT_UNFINISHED_ENROLLMENT +CANCELED_UNFINISHED_ENROLLMENT +COMPLETED +AWAITING_PAYMENT +AWAITING_APPROVAL +\. + + +-- +-- Data for Name: exam_language; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY public.exam_language (name) FROM stdin; +FI +SV +\. + + +-- +-- Data for Name: exam_level; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY public.exam_level (name) FROM stdin; +EXCELLENT +\. + + +-- +-- PostgreSQL database dump complete +-- + diff --git a/backend/vkt/db/3_liquibase.sql b/backend/vkt/db/3_liquibase.sql index d7ee01675..eae6d25a6 100644 --- a/backend/vkt/db/3_liquibase.sql +++ b/backend/vkt/db/3_liquibase.sql @@ -1,114 +1,125 @@ --- --- PostgreSQL database dump --- - --- Dumped from database version 12.9 (Debian 12.9-1.pgdg110+1) --- Dumped by pg_dump version 14.7 (Homebrew) - -SET statement_timeout = 0; -SET lock_timeout = 0; -SET idle_in_transaction_session_timeout = 0; -SET client_encoding = 'UTF8'; -SET standard_conforming_strings = on; -SELECT pg_catalog.set_config('search_path', '', false); -SET check_function_bodies = false; -SET xmloption = content; -SET client_min_messages = warning; -SET row_security = off; - -SET default_tablespace = ''; - -SET default_table_access_method = heap; - --- --- Name: databasechangelog; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE public.databasechangelog ( - id character varying(255) NOT NULL, - author character varying(255) NOT NULL, - filename character varying(255) NOT NULL, - dateexecuted timestamp without time zone NOT NULL, - orderexecuted integer NOT NULL, - exectype character varying(10) NOT NULL, - md5sum character varying(35), - description character varying(255), - comments character varying(255), - tag character varying(255), - liquibase character varying(20), - contexts character varying(255), - labels character varying(255), - deployment_id character varying(10) -); - - -ALTER TABLE public.databasechangelog OWNER TO postgres; - --- --- Name: databasechangeloglock; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE public.databasechangeloglock ( - id integer NOT NULL, - locked boolean NOT NULL, - lockgranted timestamp without time zone, - lockedby character varying(255) -); - - -ALTER TABLE public.databasechangeloglock OWNER TO postgres; - --- --- Data for Name: databasechangelog; Type: TABLE DATA; Schema: public; Owner: postgres --- - -COPY public.databasechangelog (id, author, filename, dateexecuted, orderexecuted, exectype, md5sum, description, comments, tag, liquibase, contexts, labels, deployment_id) FROM stdin; -2022-09-14-create-table-exam_language mikhuttu migrations.xml 2022-09-28 13:03:11.518594 1 EXECUTED 8:17db073b8f9d1a3557a6147981fd256e createTable tableName=exam_language; insert tableName=exam_language; insert tableName=exam_language \N 4.9.1 \N \N 4370191374 -2022-09-14-create-table-exam_level mikhuttu migrations.xml 2022-09-28 13:03:11.529341 2 EXECUTED 8:e8ac8e8266550d06f311340105eb558e createTable tableName=exam_level; insert tableName=exam_level \N 4.9.1 \N \N 4370191374 -2022-09-14-create-table-exam_event mikhuttu migrations.xml 2022-09-28 13:03:11.567242 3 EXECUTED 8:78e5d79871764de23d08742187c4be83 createTable tableName=exam_event; addForeignKeyConstraint baseTableName=exam_event, constraintName=fk_exam_event_language, referencedTableName=exam_language; addForeignKeyConstraint baseTableName=exam_event, constraintName=fk_exam_event_level, ref... \N 4.9.1 \N \N 4370191374 -2022-09-19-create-table-person mikhuttu migrations.xml 2022-09-28 13:03:11.580327 4 EXECUTED 8:1218e1e61ba39ea9793c670923ee5a80 createTable tableName=person; addUniqueConstraint constraintName=uk_person_onr_id, tableName=person \N 4.9.1 \N \N 4370191374 -2022-09-19-create-table-enrollment_status mikhuttu migrations.xml 2022-09-28 13:03:11.593403 5 EXECUTED 8:cb6dc3009864c99347b4097cd3d777a1 createTable tableName=enrollment_status; insert tableName=enrollment_status; insert tableName=enrollment_status; insert tableName=enrollment_status; insert tableName=enrollment_status \N 4.9.1 \N \N 4370191374 -2022-09-19-create-table-enrollment mikhuttu migrations.xml 2022-09-28 13:03:11.613202 6 EXECUTED 8:edba4d8326701eb21ede9eb4afb6c628 createTable tableName=enrollment; addForeignKeyConstraint baseTableName=enrollment, constraintName=fk_enrollment_exam_event, referencedTableName=exam_event; addForeignKeyConstraint baseTableName=enrollment, constraintName=fk_enrollment_person, ref... \N 4.9.1 \N \N 4370191374 -2022-10-12-change-person-columns mikhuttu migrations.xml 2022-10-12 18:42:43.132484 7 EXECUTED 8:d27f77bcff4989716b0be1fb83639ccf dropColumn tableName=person; addColumn tableName=person; addUniqueConstraint constraintName=uk_person_identity_number, tableName=person \N 4.9.1 \N \N 5600162993 -2022-10-28-create-table-reservation terova migrations.xml 2022-11-16 16:17:55.348633 8 EXECUTED 8:c4a64bee6d57de586b962e6b334cacbf createTable tableName=reservation; addForeignKeyConstraint baseTableName=reservation, constraintName=fk_reservation_exam_event, referencedTableName=exam_event; addForeignKeyConstraint baseTableName=reservation, constraintName=fk_reservation_person... \N 4.9.1 \N \N 8615475122 -2022-11-17-move_columns_from_person_to_enrollment terova migrations.xml 2022-11-17 12:16:38.355334 9 EXECUTED 8:5a6e66e03fe569e12cab0910180472d2 addColumn tableName=enrollment; dropColumn tableName=person \N 4.9.1 \N \N 8680198271 -2022-11-17-rename_exam_event_visible_to_hidden terova migrations.xml 2022-11-17 14:08:19.109749 10 EXECUTED 8:a8eaf66284e6fa36e931c01302dfcd90 renameColumn newColumnName=is_hidden, oldColumnName=is_visible, tableName=exam_event; sql \N 4.9.1 \N \N 8686899038 -2022-12-06-create-shedlock-table terova migrations.xml 2022-12-06 14:21:45.036739 11 EXECUTED 8:8d3e6aaeff0d8838d329ebbaa95bc096 createTable tableName=shedlock \N 4.9.1 \N \N 0336504848 -2022-12-06-add-enum-email_type terova migrations.xml 2022-12-06 14:21:45.065383 12 EXECUTED 8:9d2dd6c5fb47e67ba50c6cf0db4edd47 createTable tableName=email_type; insert tableName=email_type \N 4.9.1 \N \N 0336504848 -2022-12-06-create-email-table terova migrations.xml 2022-12-06 14:21:45.096787 13 EXECUTED 8:fc290ff4700b729ac568057c7dd6c211 createTable tableName=email; addForeignKeyConstraint baseTableName=email, constraintName=fk_email_email_type, referencedTableName=email_type \N 4.9.1 \N \N 0336504848 -2022-12-06-create-email_attachment-table terova migrations.xml 2022-12-06 18:42:00.87481 14 EXECUTED 8:27ead2667c986a4fb6325d9d93238151 createTable tableName=email_attachment; addForeignKeyConstraint baseTableName=email_attachment, constraintName=fk_email_attachment_email, referencedTableName=email \N 4.9.1 \N \N 0352120682 -2023-01-18-modify_enrollment-table_previous-enrollment-date mikhuttu migrations.xml 2023-05-29 09:17:57.498963 15 EXECUTED 8:b5bb9828cdc9f6349c1e92af6da50bcd modifyDataType columnName=previous_enrollment_date, tableName=enrollment; renameColumn newColumnName=previous_enrollment, oldColumnName=previous_enrollment_date, tableName=enrollment \N 4.9.1 \N \N 5351877372 -2023-02-03-add_reservation-table_renewed_at jrkkp migrations.xml 2023-05-29 09:17:57.503008 16 EXECUTED 8:e9cd840a8006f4a136fac6e259048628 addColumn tableName=reservation \N 4.9.1 \N \N 5351877372 -2023-03-20-add_spring_session_table jrkkp migrations.xml 2023-05-29 09:17:57.520006 17 EXECUTED 8:74529b81aca9ec770312c7017e0c594d createTable tableName=spring_session; createIndex indexName=spring_session_expires_idx, tableName=spring_session; createIndex indexName=spring_session_principal_idx, tableName=spring_session; createTable tableName=spring_session_attributes; addPri... \N 4.9.1 \N \N 5351877372 -2023-04-11-add_person_oid jrkkp migrations.xml 2023-05-29 09:17:57.530923 18 EXECUTED 8:5b0623c9012a91e8f39b129672804bde addColumn tableName=person; dropNotNullConstraint columnName=identity_number, tableName=person \N 4.9.1 \N \N 5351877372 -2023-05-03-add-enrollment-to-queue-confirmation-email_type mikhuttu migrations.xml 2023-05-29 09:17:57.534001 19 EXECUTED 8:4502d8b0afd0a1b88b7649fa2db135f4 insert tableName=email_type \N 4.9.1 \N \N 5351877372 -2023-05-25-payment-table jrkkp migrations.xml 2023-05-29 09:17:57.545296 20 EXECUTED 8:1869b55cb1a464dee967a5b832c80003 createTable tableName=payment; insert tableName=enrollment_status; insert tableName=enrollment_status; addForeignKeyConstraint baseTableName=payment, constraintName=fk_payment_enrollment, referencedTableName=enrollment \N 4.9.1 \N \N 5351877372 -2023-05-30-modify_payment-table_amount mikhuttu migrations.xml 2023-05-30 13:31:02.784706 21 EXECUTED 8:a1e8d1377da2bc6360f9971c8b052cb4 modifyDataType columnName=amount, tableName=payment \N 4.9.1 \N \N 5453462712 -2023-06-01-enrollment-payment-link-hash jrkkp migrations.xml 2023-06-01 13:53:01.472105 22 EXECUTED 8:0fc928a86fa41527372d8e8af21e813b addColumn tableName=enrollment \N 4.9.1 \N \N 5627581378 -2023-06-02-rename-enrollment-status-EXPECTING_PAYMENT mikhuttu migrations.xml 2023-06-02 08:38:57.704377 23 EXECUTED 8:a35dc4c9e0d0d241f4293f0b2ba16224 insert tableName=enrollment_status; sql; sql \N 4.9.1 \N \N 5695137627 -2023-06-16-person-latest-identified-at mikhuttu migrations.xml 2023-06-16 09:46:36.462511 24 EXECUTED 8:51d5c16082a3fd83108e9c40e7ae78e6 addColumn tableName=person; sql; addNotNullConstraint columnName=latest_identified_at, tableName=person \N 4.20.0 \N \N 6908796433 -2023-06-29-remove-person-identity_number mikhuttu migrations.xml 2023-06-29 09:32:48.572192 25 EXECUTED 8:5b23bce4f54b5583b757ad6bb81c612d dropColumn columnName=identity_number, tableName=person; dropColumn columnName=date_of_birth, tableName=person \N 4.20.0 \N \N 8031168558 -\. - - --- --- Data for Name: databasechangeloglock; Type: TABLE DATA; Schema: public; Owner: postgres --- - -COPY public.databasechangeloglock (id, locked, lockgranted, lockedby) FROM stdin; -1 f \N \N -\. - - --- --- Name: databasechangeloglock databasechangeloglock_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.databasechangeloglock - ADD CONSTRAINT databasechangeloglock_pkey PRIMARY KEY (id); - - --- --- PostgreSQL database dump complete --- - +-- +-- PostgreSQL database dump +-- + +-- Dumped from database version 16.3 +-- Dumped by pg_dump version 16.3 + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: databasechangelog; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.databasechangelog ( + id character varying(255) NOT NULL, + author character varying(255) NOT NULL, + filename character varying(255) NOT NULL, + dateexecuted timestamp without time zone NOT NULL, + orderexecuted integer NOT NULL, + exectype character varying(10) NOT NULL, + md5sum character varying(35), + description character varying(255), + comments character varying(255), + tag character varying(255), + liquibase character varying(20), + contexts character varying(255), + labels character varying(255), + deployment_id character varying(10) +); + + +ALTER TABLE public.databasechangelog OWNER TO postgres; + +-- +-- Name: databasechangeloglock; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.databasechangeloglock ( + id integer NOT NULL, + locked boolean NOT NULL, + lockgranted timestamp without time zone, + lockedby character varying(255) +); + + +ALTER TABLE public.databasechangeloglock OWNER TO postgres; + +-- +-- Data for Name: databasechangelog; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY public.databasechangelog (id, author, filename, dateexecuted, orderexecuted, exectype, md5sum, description, comments, tag, liquibase, contexts, labels, deployment_id) FROM stdin; +2022-09-14-create-table-exam_language mikhuttu migrations.xml 2022-09-28 13:03:11.518594 1 EXECUTED 9:01b3c2b6d1b07e777b86ea211569ce34 createTable tableName=exam_language; insert tableName=exam_language; insert tableName=exam_language \N 4.9.1 \N \N 4370191374 +2022-09-14-create-table-exam_level mikhuttu migrations.xml 2022-09-28 13:03:11.529341 2 EXECUTED 9:6b045bdd49b5a6b645f7df1821fd99a8 createTable tableName=exam_level; insert tableName=exam_level \N 4.9.1 \N \N 4370191374 +2022-09-14-create-table-exam_event mikhuttu migrations.xml 2022-09-28 13:03:11.567242 3 EXECUTED 9:469ab52e24b6778fc65a208c183333df createTable tableName=exam_event; addForeignKeyConstraint baseTableName=exam_event, constraintName=fk_exam_event_language, referencedTableName=exam_language; addForeignKeyConstraint baseTableName=exam_event, constraintName=fk_exam_event_level, ref... \N 4.9.1 \N \N 4370191374 +2022-09-19-create-table-person mikhuttu migrations.xml 2022-09-28 13:03:11.580327 4 EXECUTED 9:b6d90b721004da05d10471b1252976e4 createTable tableName=person; addUniqueConstraint constraintName=uk_person_onr_id, tableName=person \N 4.9.1 \N \N 4370191374 +2022-09-19-create-table-enrollment_status mikhuttu migrations.xml 2022-09-28 13:03:11.593403 5 EXECUTED 9:3d0c7bf5f1331255ce5b1ccda9c349b1 createTable tableName=enrollment_status; insert tableName=enrollment_status; insert tableName=enrollment_status; insert tableName=enrollment_status; insert tableName=enrollment_status \N 4.9.1 \N \N 4370191374 +2022-09-19-create-table-enrollment mikhuttu migrations.xml 2022-09-28 13:03:11.613202 6 EXECUTED 9:abacfedebf8b66d765ed003d8f1051c4 createTable tableName=enrollment; addForeignKeyConstraint baseTableName=enrollment, constraintName=fk_enrollment_exam_event, referencedTableName=exam_event; addForeignKeyConstraint baseTableName=enrollment, constraintName=fk_enrollment_person, ref... \N 4.9.1 \N \N 4370191374 +2022-10-12-change-person-columns mikhuttu migrations.xml 2022-10-12 18:42:43.132484 7 EXECUTED 9:64c7f869d68907e4e75c5640170abff0 dropColumn tableName=person; addColumn tableName=person; addUniqueConstraint constraintName=uk_person_identity_number, tableName=person \N 4.9.1 \N \N 5600162993 +2022-10-28-create-table-reservation terova migrations.xml 2022-11-16 16:17:55.348633 8 EXECUTED 9:5a7ec529509bd086e2b22c8090009985 createTable tableName=reservation; addForeignKeyConstraint baseTableName=reservation, constraintName=fk_reservation_exam_event, referencedTableName=exam_event; addForeignKeyConstraint baseTableName=reservation, constraintName=fk_reservation_person... \N 4.9.1 \N \N 8615475122 +2022-11-17-move_columns_from_person_to_enrollment terova migrations.xml 2022-11-17 12:16:38.355334 9 EXECUTED 9:6d847e8743a2b08a08c5be28f6180ac5 addColumn tableName=enrollment; dropColumn tableName=person \N 4.9.1 \N \N 8680198271 +2022-11-17-rename_exam_event_visible_to_hidden terova migrations.xml 2022-11-17 14:08:19.109749 10 EXECUTED 9:cd1240669368efdbe8d5de4e26924cd7 renameColumn newColumnName=is_hidden, oldColumnName=is_visible, tableName=exam_event; sql \N 4.9.1 \N \N 8686899038 +2022-12-06-create-shedlock-table terova migrations.xml 2022-12-06 14:21:45.036739 11 EXECUTED 9:d5d845196153f4afe595382eaf208376 createTable tableName=shedlock \N 4.9.1 \N \N 0336504848 +2022-12-06-add-enum-email_type terova migrations.xml 2022-12-06 14:21:45.065383 12 EXECUTED 9:3a40f8d772c317519fdaba844e162bdb createTable tableName=email_type; insert tableName=email_type \N 4.9.1 \N \N 0336504848 +2022-12-06-create-email-table terova migrations.xml 2022-12-06 14:21:45.096787 13 EXECUTED 9:d556eb6481d5aff882b485908596202f createTable tableName=email; addForeignKeyConstraint baseTableName=email, constraintName=fk_email_email_type, referencedTableName=email_type \N 4.9.1 \N \N 0336504848 +2022-12-06-create-email_attachment-table terova migrations.xml 2022-12-06 18:42:00.87481 14 EXECUTED 9:4c7f2fe87d7688bf0654a3e6f62a9746 createTable tableName=email_attachment; addForeignKeyConstraint baseTableName=email_attachment, constraintName=fk_email_attachment_email, referencedTableName=email \N 4.9.1 \N \N 0352120682 +2023-01-18-modify_enrollment-table_previous-enrollment-date mikhuttu migrations.xml 2023-05-29 09:17:57.498963 15 EXECUTED 9:3c10af2ffb29d968230e15e621c3ba6d modifyDataType columnName=previous_enrollment_date, tableName=enrollment; renameColumn newColumnName=previous_enrollment, oldColumnName=previous_enrollment_date, tableName=enrollment \N 4.9.1 \N \N 5351877372 +2023-02-03-add_reservation-table_renewed_at jrkkp migrations.xml 2023-05-29 09:17:57.503008 16 EXECUTED 9:40b3879c568d7328d1944869f9421376 addColumn tableName=reservation \N 4.9.1 \N \N 5351877372 +2023-03-20-add_spring_session_table jrkkp migrations.xml 2023-05-29 09:17:57.520006 17 EXECUTED 9:9484e3d16fe0c70adca4187e9a2b453b createTable tableName=spring_session; createIndex indexName=spring_session_expires_idx, tableName=spring_session; createIndex indexName=spring_session_principal_idx, tableName=spring_session; createTable tableName=spring_session_attributes; addPri... \N 4.9.1 \N \N 5351877372 +2023-04-11-add_person_oid jrkkp migrations.xml 2023-05-29 09:17:57.530923 18 EXECUTED 9:4904dd1ca41b61cfa4a9ff05ffde1963 addColumn tableName=person; dropNotNullConstraint columnName=identity_number, tableName=person \N 4.9.1 \N \N 5351877372 +2023-05-03-add-enrollment-to-queue-confirmation-email_type mikhuttu migrations.xml 2023-05-29 09:17:57.534001 19 EXECUTED 9:5cf2bfd776d0ee74ff115ecf82793fc0 insert tableName=email_type \N 4.9.1 \N \N 5351877372 +2023-05-25-payment-table jrkkp migrations.xml 2023-05-29 09:17:57.545296 20 EXECUTED 9:ca4457716b237266c83dbd9e07528ae5 createTable tableName=payment; insert tableName=enrollment_status; insert tableName=enrollment_status; addForeignKeyConstraint baseTableName=payment, constraintName=fk_payment_enrollment, referencedTableName=enrollment \N 4.9.1 \N \N 5351877372 +2023-05-30-modify_payment-table_amount mikhuttu migrations.xml 2023-05-30 13:31:02.784706 21 EXECUTED 9:00ea657910e9e5e2fe442fe3af4b8511 modifyDataType columnName=amount, tableName=payment \N 4.9.1 \N \N 5453462712 +2023-06-01-enrollment-payment-link-hash jrkkp migrations.xml 2023-06-01 13:53:01.472105 22 EXECUTED 9:f67baa7a2e918bb953e82f030af5c78b addColumn tableName=enrollment \N 4.9.1 \N \N 5627581378 +2023-06-02-rename-enrollment-status-EXPECTING_PAYMENT mikhuttu migrations.xml 2023-06-02 08:38:57.704377 23 EXECUTED 9:d8ace0bf20260b36e2c2f0c6833f2ffe insert tableName=enrollment_status; sql; sql \N 4.9.1 \N \N 5695137627 +2023-06-16-person-latest-identified-at mikhuttu migrations.xml 2023-06-16 09:46:36.462511 24 EXECUTED 9:7151d4c1022ac9e13ea88c9c7774e579 addColumn tableName=person; sql; addNotNullConstraint columnName=latest_identified_at, tableName=person \N 4.20.0 \N \N 6908796433 +2023-06-29-remove-person-identity_number mikhuttu migrations.xml 2023-06-29 09:32:48.572192 25 EXECUTED 9:687bbb81a5282eccdb83443935421340 dropColumn columnName=identity_number, tableName=person; dropColumn columnName=date_of_birth, tableName=person \N 4.20.0 \N \N 8031168558 +2023-08-03-payment-add-refunded jrkkp migrations.xml 2024-11-13 16:41:53.710139 26 EXECUTED 9:a5197d8152163f63193f3ef194e246aa addColumn tableName=payment \N 4.29.2 \N \N 1508913633 +2024-04-08-cas-session-ticket jrkkp migrations.xml 2024-11-13 16:41:53.721318 27 EXECUTED 9:b6cfacfeeaaa34091bd253a2317bea3f createTable tableName=cas_ticket \N 4.29.2 \N \N 1508913633 +2024-05-31-free-enrollment lket migrations.xml 2024-11-13 16:41:53.758089 28 EXECUTED 9:804d47bd31f027ba706ff62fd67412b3 createTable tableName=free_enrollment; addForeignKeyConstraint baseTableName=free_enrollment, constraintName=fk_free_enrollment_person, referencedTableName=person; addColumn tableName=enrollment; addForeignKeyConstraint baseTableName=enrollment, c... \N 4.29.2 \N \N 1508913633 +2024-06-18-add-uuid-to-person-postgres pkoivisto migrations.xml 2024-11-13 16:41:53.768474 29 EXECUTED 9:72ab649e66366bb6a154ca0ed767d919 addColumn tableName=person; sql; addNotNullConstraint columnName=uuid, tableName=person \N 4.29.2 \N \N 1508913633 +2024-08-14-add-is-queued-to-enrollment jrkkp migrations.xml 2024-11-13 16:41:53.777316 30 EXECUTED 9:0c6f186a9f15e2e395cd24d1652bd239 addColumn tableName=enrollment; sql; sql \N 4.29.2 \N \N 1508913633 +2024-08-27-all-koski-educations pkoivisto migrations.xml 2024-11-13 16:41:53.789024 31 EXECUTED 9:aee4a31a7eb021b38755e24b0c30070f createTable tableName=koski_educations; addForeignKeyConstraint baseTableName=koski_educations, constraintName=fk_koski_educations_free_enrollment, referencedTableName=free_enrollment; addForeignKeyConstraint baseTableName=koski_educations, constr... \N 4.29.2 \N \N 1508913633 +2024-09-11-registration-open-and-close-times-psql jrkkp migrations.xml 2024-11-13 16:41:53.803381 32 EXECUTED 9:27e10d6992e13a438d8945de985d1fa7 sql \N 4.29.2 \N \N 1508913633 +2024-10-04-examiner-and-municipality-tables pkoivisto migrations.xml 2024-11-13 16:41:53.827798 33 EXECUTED 9:17684f1fcc2b8eeda7ed7db0865b421a createTable tableName=examiner; createTable tableName=municipality; createTable tableName=examiner_municipality; addUniqueConstraint constraintName=uk_examiner_municipality_examiner_id_municipality_id, tableName=examiner_municipality; addForeignKe... \N 4.29.2 \N \N 1508913633 +2024-11-07-new-examiner_exam_event-table pkoivisto migrations.xml 2024-11-13 16:41:53.841918 34 EXECUTED 9:cf0bb0d78f0f4adddd3d7147692d1eed createTable tableName=examiner_exam_event; addForeignKeyConstraint baseTableName=examiner_exam_event, constraintName=fk_examiner_exam_event_examiner_id, referencedTableName=examiner; addForeignKeyConstraint baseTableName=examiner_exam_event, const... \N 4.29.2 \N \N 1508913633 +2024-11-11-grade-table jrkkp migrations.xml 2024-11-13 16:41:53.850133 35 EXECUTED 9:cb045c4425bef824bbdb452db81ec27c createTable tableName=enrollment_grade \N 4.29.2 \N \N 1508913633 +2024-11-13-enrollment_appointment jrkkp migrations.xml 2024-11-13 16:41:53.869661 36 EXECUTED 9:dbd365573e74ec0e96178c2e44d37575 createTable tableName=enrollment_appointment; addForeignKeyConstraint baseTableName=enrollment_appointment, constraintName=fk_enrollment_appointment_grade_id, referencedTableName=enrollment_grade; addForeignKeyConstraint baseTableName=enrollment_a... \N 4.29.2 \N \N 1508913633 +\. + + +-- +-- Data for Name: databasechangeloglock; Type: TABLE DATA; Schema: public; Owner: postgres +-- + +COPY public.databasechangeloglock (id, locked, lockgranted, lockedby) FROM stdin; +1 f \N \N +\. + + +-- +-- Name: databasechangeloglock databasechangeloglock_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.databasechangeloglock + ADD CONSTRAINT databasechangeloglock_pkey PRIMARY KEY (id); + + +-- +-- PostgreSQL database dump complete +-- + diff --git a/backend/vkt/db/4_init.sql b/backend/vkt/db/4_init.sql index a0315efbe..43446d6c7 100644 --- a/backend/vkt/db/4_init.sql +++ b/backend/vkt/db/4_init.sql @@ -4,143 +4,156 @@ TRUNCATE TABLE person CASCADE; -- Insert exam events INSERT INTO exam_event - (language, level, date, registration_closes, is_hidden, max_participants) + (language, level, date, registration_opens, registration_closes, is_hidden, max_participants) VALUES ( 'FI', 'EXCELLENT', NOW() - INTERVAL '1 MONTHS', + NOW() - INTERVAL '2 WEEKS', NOW() - INTERVAL '2 MONTHS', false, 10 ); INSERT INTO exam_event - (language, level, date, registration_closes, is_hidden, max_participants) + (language, level, date, registration_opens, registration_closes, is_hidden, max_participants) VALUES ( 'SV', 'EXCELLENT', NOW() - INTERVAL '1 MONTHS', + NOW() - INTERVAL '2 WEEKS', NOW() - INTERVAL '2 MONTHS', false, 10 ); INSERT INTO exam_event - (language, level, date, registration_closes, is_hidden, max_participants) + (language, level, date, registration_opens, registration_closes, is_hidden, max_participants) VALUES ( 'FI', 'EXCELLENT', NOW(), NOW(), + NOW(), false, 10 ); INSERT INTO exam_event - (language, level, date, registration_closes, is_hidden, max_participants) + (language, level, date, registration_opens, registration_closes, is_hidden, max_participants) VALUES ( 'SV', 'EXCELLENT', NOW(), NOW(), + NOW(), false, 10 ); INSERT INTO exam_event - (language, level, date, registration_closes, is_hidden, max_participants) + (language, level, date, registration_opens, registration_closes, is_hidden, max_participants) VALUES ( 'FI', 'EXCELLENT', NOW() + INTERVAL '1 WEEK', + NOW() - INTERVAL '1 DAY', NOW(), false, 10 ); INSERT INTO exam_event - (language, level, date, registration_closes, is_hidden, max_participants) + (language, level, date, registration_opens, registration_closes, is_hidden, max_participants) VALUES ( 'FI', 'EXCELLENT', NOW() + INTERVAL '3 MONTHS', + NOW() + INTERVAL '1 DAY', NOW() + INTERVAL '2 MONTHS', false, 10 ); INSERT INTO exam_event - (language, level, date, registration_closes, is_hidden, max_participants) + (language, level, date, registration_opens, registration_closes, is_hidden, max_participants) VALUES ( 'SV', 'EXCELLENT', NOW() + INTERVAL '3 MONTHS', + NOW(), NOW() + INTERVAL '2 MONTHS', false, 10 ); INSERT INTO exam_event - (language, level, date, registration_closes, is_hidden, max_participants) + (language, level, date, registration_opens, registration_closes, is_hidden, max_participants) VALUES ( 'FI', 'EXCELLENT', NOW() + INTERVAL '5 MONTHS', + NOW(), NOW() + INTERVAL '4 MONTHS', false, 10 ); INSERT INTO exam_event - (language, level, date, registration_closes, is_hidden, max_participants) + (language, level, date, registration_opens, registration_closes, is_hidden, max_participants) VALUES ( 'FI', 'EXCELLENT', NOW() + INTERVAL '9 MONTHS', + NOW(), NOW() + INTERVAL '8 MONTHS', false, 10 ); INSERT INTO exam_event - (language, level, date, registration_closes, is_hidden, max_participants) + (language, level, date, registration_opens, registration_closes, is_hidden, max_participants) VALUES ( 'FI', 'EXCELLENT', NOW() + INTERVAL '12 MONTHS', + NOW(), NOW() + INTERVAL '11 MONTHS', false, 10 ); INSERT INTO exam_event - (language, level, date, registration_closes, is_hidden, max_participants) + (language, level, date, registration_opens, registration_closes, is_hidden, max_participants) VALUES ( 'SV', 'EXCELLENT', NOW() + INTERVAL '12 MONTHS', + NOW(), NOW() + INTERVAL '11 MONTHS', false, 10 ); INSERT INTO exam_event - (language, level, date, registration_closes, is_hidden, max_participants) + (language, level, date, registration_opens, registration_closes, is_hidden, max_participants) VALUES ( 'FI', 'EXCELLENT', NOW() + INTERVAL '24 MONTHS', + NOW(), NOW() + INTERVAL '22 MONTHS', false, 10 ); INSERT INTO exam_event - (language, level, date, registration_closes, is_hidden, max_participants) + (language, level, date, registration_opens, registration_closes, is_hidden, max_participants) VALUES ( 'SV', 'EXCELLENT', NOW() + INTERVAL '24 MONTHS', + NOW(), NOW() + INTERVAL '22 MONTHS', false, 10 @@ -149,29 +162,31 @@ VALUES ( -- Special exam events INSERT INTO exam_event - (language, level, date, registration_closes, is_hidden, max_participants) + (language, level, date, registration_opens, registration_closes, is_hidden, max_participants) VALUES ( 'SV', 'EXCELLENT', NOW() + INTERVAL '5 WEEKS', + NOW(), NOW() + INTERVAL '4 WEEKS', false, 8 ); INSERT INTO exam_event - (language, level, date, registration_closes, is_hidden, max_participants) + (language, level, date, registration_opens, registration_closes, is_hidden, max_participants) VALUES ( 'SV', 'EXCELLENT', NOW() + INTERVAL '2 WEEKS', + NOW(), NOW() + INTERVAL '1 WEEK', true, 10 ); -- Insert persons -INSERT INTO person(last_name, first_name, oid, other_identifier, latest_identified_at) +INSERT INTO person(last_name, first_name, oid, other_identifier, latest_identified_at, uuid) SELECT last_names[mod(i, array_length(last_names, 1)) + 1], first_names[mod(i, array_length(first_names, 1)) + 1], @@ -179,7 +194,8 @@ SELECT WHEN 0 THEN NULL ELSE '1.2.246.init-' || i::text END, CASE mod(i, 7) WHEN 0 THEN 'FI/init-' || i::text END, - NOW() + NOW(), + gen_random_uuid() FROM generate_series(1, 22) i, (SELECT ('{Anneli, Ella, Hanna, Iiris, Liisa, Maria, Ninni, Viivi, Sointu, Jaakko, Lasse, Kyösti, ' || 'Markku, Kristian, Mikael, Nooa, Otto, Olli}')::text[] AS first_names) AS first_name_table, @@ -195,7 +211,7 @@ INSERT INTO enrollment(exam_event_id, person_id, SELECT (SELECT exam_event_id FROM exam_event ORDER BY exam_event_id DESC LIMIT 1 OFFSET 1), person_id, true, true, true, true, true, true, true, - 'PAID', true, + 'COMPLETED', true, 'person' || person_id::text || '@example.invalid', '+35840' || (1000000 + person_id)::text, CASE mod(person_id, 5) @@ -225,7 +241,7 @@ INSERT INTO enrollment(exam_event_id, person_id, SELECT (SELECT exam_event_id FROM exam_event ORDER BY exam_event_id DESC LIMIT 1 OFFSET 2), person_id, true, true, true, true, true, true, true, - 'PAID', true, + 'COMPLETED', true, 'person' || person_id::text || '@example.invalid', '+35840' || (1000000 + person_id)::text, CASE mod(person_id, 5) @@ -285,7 +301,7 @@ INSERT INTO enrollment(exam_event_id, person_id, SELECT (SELECT exam_event_id FROM exam_event ORDER BY exam_event_id DESC LIMIT 1 OFFSET 3), person_id, true, true, true, true, true, true, true, - 'PAID', true, + 'COMPLETED', true, 'person' || person_id::text || '@example.invalid', '+35840' || (1000000 + person_id)::text, CASE mod(person_id, 5) @@ -312,7 +328,7 @@ INSERT INTO enrollment(exam_event_id, person_id, SELECT (SELECT exam_event_id FROM exam_event ORDER BY exam_event_id DESC LIMIT 1 OFFSET 3), person_id, true, true, true, true, true, true, true, - 'SHIFTED_FROM_QUEUE', true, + 'AWAITING_PAYMENT', true, 'person' || person_id::text || '@example.invalid', '+35840' || (1000000 + person_id)::text, CASE mod(person_id, 5) @@ -343,7 +359,7 @@ INSERT INTO enrollment(exam_event_id, person_id, SELECT (SELECT exam_event_id FROM exam_event ORDER BY exam_event_id DESC LIMIT 1 OFFSET 4), person_id, true, true, true, true, true, true, true, - 'PAID', true, + 'COMPLETED', true, 'person' || person_id::text || '@example.invalid', '+35840' || (1000000 + person_id)::text, CASE mod(person_id, 5) @@ -375,3 +391,180 @@ SELECT exam_event_id, (SELECT max(person_id) FROM person), 'CANCELED', true, 'foo@bar.invalid', '0404040404', null, null, null, null FROM exam_event; + +-- Insert municipality +INSERT INTO municipality(version, code, name_fi, name_sv) +VALUES (1, '564', 'Oulu', 'Uleåborg'); + +-- Insert municipality +INSERT INTO municipality(version, code, name_fi, name_sv) +VALUES (1, '837', 'Tampere', 'Tammerfors'); + +-- Insert examiner +INSERT INTO examiner(version, oid, email, phone_number, last_name, first_name, nickname, exam_language_finnish, exam_language_swedish, is_public) +VALUES (1, '1.2.246.562.10.10000000001', 'examiner@example.invalid', '04040404040', 'Tessilä', 'Testi', 'Tessa', true, true, true); + +-- Insert municipality +INSERT INTO examiner_municipality(municipality_id, examiner_id) +VALUES (1, 1), (2, 1); + +-- insert examiner_exam_event +INSERT INTO examiner_exam_event(version, date, language, examiner_id, is_hidden, registration_closes, max_participants, municipality_id, location) +VALUES ( + 1, + NOW() + INTERVAL '5 weeks', + 'FI', + 1, + false, + NOW() + INTERVAL '2 weeks', + 10, + 1, + 'tylypahka' +); + +-- insert examiner_exam_event +INSERT INTO examiner_exam_event(version, date, language, examiner_id, is_hidden, registration_closes, max_participants, municipality_id, location) +VALUES ( + 1, + NOW() + INTERVAL '2 weeks', + 'FI', + 1, + false, + NOW() + INTERVAL '1 weeks', + 10, + 2, + 'tammerkosken silta' +); + +-- insert past examiner_exam_event +INSERT INTO examiner_exam_event(version, date, language, examiner_id, is_hidden, registration_closes, max_participants, municipality_id, location) +VALUES ( + 1, + NOW() - INTERVAL '2 weeks', + 'FI', + 1, + false, + NOW() - INTERVAL '1 weeks', + 10, + 2, + 'Testimaa' +); + +-- insert past examiner_exam_event +INSERT INTO examiner_exam_event(version, date, language, examiner_id, is_hidden, registration_closes, max_participants, municipality_id, location) +VALUES ( + 1, + NOW() - INTERVAL '3 weeks', + 'FI', + 1, + false, + NOW() - INTERVAL '2 weeks', + 10, + 2, + 'Mordor' +); + +-- Insert enrollment appointment +INSERT INTO enrollment_appointment(person_id, examiner_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, + first_name, last_name, message, previous_enrollment, + auth_hash, auth_hash_expires, auth_hash_sent) +VALUES (null, 1, + true, true, true, + true, true, true, true, + 'CONTACT_CREATED', false, + 'foo@bar.invalid', '0404040404', null, null, null, null, + 'Teppo', 'Testaaja', 'Tämä on viesti', 'Edellinen ilmoittautuminen vuonna 1999', + null, null, null); + +-- Insert enrollment appointment +INSERT INTO enrollment_appointment(person_id, examiner_id, examiner_exam_event_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, first_name, last_name, + auth_hash, auth_hash_expires, auth_hash_sent) +VALUES (1, 1, 1, + true, true, true, + true, true, true, true, + 'WAITING_AUTHENTICATION', false, + 'foo@bar.invalid', '0404040404', null, null, null, null, + 'Teppo', 'Testinen', + '922c2089-83a8-4163-8180-d8b675ff5337', NOW() + INTERVAL '3 days', NOW()); + +-- Insert multiple enrollment appointments for person 2 +INSERT INTO enrollment_appointment(person_id, examiner_id, examiner_exam_event_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, first_name, last_name, + auth_hash, auth_hash_expires, auth_hash_sent) +VALUES (2, 1, 1, + false, true, false, + false, false, true, true, + 'COMPLETED', false, + 'test@test.invalid', '0401234504', null, null, null, null, + 'Anneli', 'Annikkinen', + '123c2089-83a8-4163-8180-d8b675ff5337', NOW() - INTERVAL '3 days', NOW() - INTERVAL '6 days'); + +INSERT INTO payment(version, enrollment_id, amount, transaction_id, reference, payment_url, + payment_status, refunded_at, enrollment_appointment_id) +VALUES (2, null, 51400, '78b29334-a283-11ef-88a1-bf672bd574b1', '7676156682', + 'https://pay.paytrail.com/pay/78b29334-a283-11ef-88a1-bf672bd574b1', 'OK', + null, 3); + +-- Insert multiple enrollment appointments for person 2 +INSERT INTO enrollment_appointment(person_id, examiner_id, examiner_exam_event_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, first_name, last_name, + auth_hash, auth_hash_expires, auth_hash_sent, created_at) +VALUES (2, 1, 3, + true, true, true, + true, true, true, true, + 'COMPLETED', false, + 'test@test.invalid', '0401234504', null, null, null, null, + 'Anneli', 'Annikkinen', + '55523089-83a8-4163-8180-d8b675ff5337', NOW() - INTERVAL '13 days', NOW() - INTERVAL '16 days', + NOW() - INTERVAL '17 days'); + +INSERT INTO payment(version, enrollment_id, amount, transaction_id, reference, payment_url, + payment_status, refunded_at, enrollment_appointment_id) +VALUES (3, null, 51400, '12345634-a283-11ef-88a1-bf672bd574b1', '9676156682', + 'https://pay.paytrail.com/pay/12345634-a283-11ef-88a1-bf672bd574b1', 'OK', + null, 4); + +-- Insert multiple enrollment appointments for person 2 +INSERT INTO enrollment_appointment(person_id, examiner_id, examiner_exam_event_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, first_name, last_name, + auth_hash, auth_hash_expires, auth_hash_sent, created_at) +VALUES (2, 1, 4, + false, true, false, + false, false, false, true, + 'COMPLETED', false, + 'test@test.invalid', '0401234504', null, null, null, null, + 'Anneli', 'Annikkinen', + '44423089-83a8-4163-8180-d8b675ff5337', NOW() - INTERVAL '13 days', NOW() - INTERVAL '16 days', + NOW() - INTERVAL '23 days'); + +INSERT INTO payment(version, enrollment_id, amount, transaction_id, reference, payment_url, + payment_status, refunded_at, enrollment_appointment_id) +VALUES (3, null, 51400, '99995634-a283-11ef-88a1-bf672bd574b1', '0006156682', + 'https://pay.paytrail.com/pay/99945634-a283-11ef-88a1-bf672bd574b1', 'OK', + null, 5); + +-- Insert enrollment appointment +INSERT INTO enrollment_appointment(person_id, examiner_id, examiner_exam_event_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, first_name, last_name, + auth_hash, auth_hash_expires, auth_hash_sent) +VALUES (3, 1, 1, + true, true, true, + true, true, true, true, + 'CANCELED', false, + 'bar@test.invalid', '0501234504', null, null, null, null, + 'Marja-Liisa', 'Testaaja', + '233c2129-83a8-4163-8180-d8b675ff5337', NOW() + INTERVAL '3 days', NOW()); diff --git a/backend/vkt/deps-tree.txt b/backend/vkt/deps-tree.txt new file mode 100644 index 000000000..f3585fa40 --- /dev/null +++ b/backend/vkt/deps-tree.txt @@ -0,0 +1,275 @@ +[INFO] Scanning for projects... +[INFO] +[INFO] -----------------------------< fi.oph:vkt >----------------------------- +[INFO] Building vkt 0.0.1-SNAPSHOT +[INFO] --------------------------------[ jar ]--------------------------------- +Downloading from oph-sade-artifactory: https://artifactory.opintopolku.fi/artifactory/oph-sade-snapshot-local/fi/vm/sade/java-utils/java-properties/0.1.0-SNAPSHOT/maven-metadata.xml +Downloading from github: https://maven.pkg.github.com/Opetushallitus/java-utils/fi/vm/sade/java-utils/java-properties/0.1.0-SNAPSHOT/maven-metadata.xml +Progress (1): 1.0 kB Downloaded from oph-sade-artifactory: https://artifactory.opintopolku.fi/artifactory/oph-sade-snapshot-local/fi/vm/sade/java-utils/java-properties/0.1.0-SNAPSHOT/maven-metadata.xml (1.0 kB at 2.7 kB/s) +[WARNING] Could not transfer metadata fi.vm.sade.java-utils:java-properties:0.1.0-SNAPSHOT/maven-metadata.xml from/to github (https://maven.pkg.github.com/Opetushallitus/java-utils): authentication failed for https://maven.pkg.github.com/Opetushallitus/java-utils/fi/vm/sade/java-utils/java-properties/0.1.0-SNAPSHOT/maven-metadata.xml, status: 401 Unauthorized +[WARNING] fi.vm.sade.java-utils:java-properties:0.1.0-SNAPSHOT/maven-metadata.xmlfailed to transfer from https://maven.pkg.github.com/Opetushallitus/java-utils during a previous attempt. This failure was cached in the local repository and resolution will not be reattempted until the update interval of github has elapsed or updates are forced. Original error: Could not transfer metadata fi.vm.sade.java-utils:java-properties:0.1.0-SNAPSHOT/maven-metadata.xml from/to github (https://maven.pkg.github.com/Opetushallitus/java-utils): authentication failed for https://maven.pkg.github.com/Opetushallitus/java-utils/fi/vm/sade/java-utils/java-properties/0.1.0-SNAPSHOT/maven-metadata.xml, status: 401 Unauthorized +Downloading from oph-sade-artifactory: https://artifactory.opintopolku.fi/artifactory/oph-sade-snapshot-local/fi/vm/sade/java-utils/httpclient/0.4.0-SNAPSHOT/maven-metadata.xml +Downloading from github: https://maven.pkg.github.com/Opetushallitus/java-utils/fi/vm/sade/java-utils/httpclient/0.4.0-SNAPSHOT/maven-metadata.xml +Progress (1): 999 B Downloaded from oph-sade-artifactory: https://artifactory.opintopolku.fi/artifactory/oph-sade-snapshot-local/fi/vm/sade/java-utils/httpclient/0.4.0-SNAPSHOT/maven-metadata.xml (999 B at 7.3 kB/s) +[WARNING] Could not transfer metadata fi.vm.sade.java-utils:httpclient:0.4.0-SNAPSHOT/maven-metadata.xml from/to github (https://maven.pkg.github.com/Opetushallitus/java-utils): authentication failed for https://maven.pkg.github.com/Opetushallitus/java-utils/fi/vm/sade/java-utils/httpclient/0.4.0-SNAPSHOT/maven-metadata.xml, status: 401 Unauthorized +[WARNING] fi.vm.sade.java-utils:httpclient:0.4.0-SNAPSHOT/maven-metadata.xmlfailed to transfer from https://maven.pkg.github.com/Opetushallitus/java-utils during a previous attempt. This failure was cached in the local repository and resolution will not be reattempted until the update interval of github has elapsed or updates are forced. Original error: Could not transfer metadata fi.vm.sade.java-utils:httpclient:0.4.0-SNAPSHOT/maven-metadata.xml from/to github (https://maven.pkg.github.com/Opetushallitus/java-utils): authentication failed for https://maven.pkg.github.com/Opetushallitus/java-utils/fi/vm/sade/java-utils/httpclient/0.4.0-SNAPSHOT/maven-metadata.xml, status: 401 Unauthorized +Downloading from github: https://maven.pkg.github.com/Opetushallitus/java-utils/fi/vm/sade/java-utils/java-legacy-cas/0.5.1-SNAPSHOT/maven-metadata.xml +Downloading from oph-sade-artifactory: https://artifactory.opintopolku.fi/artifactory/oph-sade-snapshot-local/fi/vm/sade/java-utils/java-legacy-cas/0.5.1-SNAPSHOT/maven-metadata.xml +Progress (1): 1.0 kB Downloaded from oph-sade-artifactory: https://artifactory.opintopolku.fi/artifactory/oph-sade-snapshot-local/fi/vm/sade/java-utils/java-legacy-cas/0.5.1-SNAPSHOT/maven-metadata.xml (1.0 kB at 7.4 kB/s) +[WARNING] Could not transfer metadata fi.vm.sade.java-utils:java-legacy-cas:0.5.1-SNAPSHOT/maven-metadata.xml from/to github (https://maven.pkg.github.com/Opetushallitus/java-utils): authentication failed for https://maven.pkg.github.com/Opetushallitus/java-utils/fi/vm/sade/java-utils/java-legacy-cas/0.5.1-SNAPSHOT/maven-metadata.xml, status: 401 Unauthorized +[WARNING] fi.vm.sade.java-utils:java-legacy-cas:0.5.1-SNAPSHOT/maven-metadata.xmlfailed to transfer from https://maven.pkg.github.com/Opetushallitus/java-utils during a previous attempt. This failure was cached in the local repository and resolution will not be reattempted until the update interval of github has elapsed or updates are forced. Original error: Could not transfer metadata fi.vm.sade.java-utils:java-legacy-cas:0.5.1-SNAPSHOT/maven-metadata.xml from/to github (https://maven.pkg.github.com/Opetushallitus/java-utils): authentication failed for https://maven.pkg.github.com/Opetushallitus/java-utils/fi/vm/sade/java-utils/java-legacy-cas/0.5.1-SNAPSHOT/maven-metadata.xml, status: 401 Unauthorized +Downloading from github: https://maven.pkg.github.com/Opetushallitus/java-utils/fi/vm/sade/java-utils/httpclient/0.4.1-SNAPSHOT/maven-metadata.xml +Downloading from oph-sade-artifactory: https://artifactory.opintopolku.fi/artifactory/oph-sade-snapshot-local/fi/vm/sade/java-utils/httpclient/0.4.1-SNAPSHOT/maven-metadata.xml +Progress (1): 999 B Downloaded from oph-sade-artifactory: https://artifactory.opintopolku.fi/artifactory/oph-sade-snapshot-local/fi/vm/sade/java-utils/httpclient/0.4.1-SNAPSHOT/maven-metadata.xml (999 B at 7.6 kB/s) +[WARNING] Could not transfer metadata fi.vm.sade.java-utils:httpclient:0.4.1-SNAPSHOT/maven-metadata.xml from/to github (https://maven.pkg.github.com/Opetushallitus/java-utils): authentication failed for https://maven.pkg.github.com/Opetushallitus/java-utils/fi/vm/sade/java-utils/httpclient/0.4.1-SNAPSHOT/maven-metadata.xml, status: 401 Unauthorized +[WARNING] fi.vm.sade.java-utils:httpclient:0.4.1-SNAPSHOT/maven-metadata.xmlfailed to transfer from https://maven.pkg.github.com/Opetushallitus/java-utils during a previous attempt. This failure was cached in the local repository and resolution will not be reattempted until the update interval of github has elapsed or updates are forced. Original error: Could not transfer metadata fi.vm.sade.java-utils:httpclient:0.4.1-SNAPSHOT/maven-metadata.xml from/to github (https://maven.pkg.github.com/Opetushallitus/java-utils): authentication failed for https://maven.pkg.github.com/Opetushallitus/java-utils/fi/vm/sade/java-utils/httpclient/0.4.1-SNAPSHOT/maven-metadata.xml, status: 401 Unauthorized +[INFO] +[INFO] --- maven-dependency-plugin:3.6.1:tree (default-cli) @ vkt --- +[INFO] fi.oph:vkt:jar:0.0.1-SNAPSHOT +[INFO] +- com.github.jhonnymertz:java-wkhtmltopdf-wrapper:jar:1.3.1-RELEASE:compile +[INFO] | +- org.apache.commons:commons-lang3:jar:3.14.0:compile +[INFO] | \- org.slf4j:slf4j-api:jar:2.0.13:compile +[INFO] +- org.springframework.session:spring-session-jdbc:jar:3.3.1:compile +[INFO] | +- org.springframework.session:spring-session-core:jar:3.3.1:compile +[INFO] | | \- org.springframework:spring-jcl:jar:6.1.11:compile +[INFO] | +- org.springframework:spring-context:jar:6.1.11:compile +[INFO] | \- org.springframework:spring-jdbc:jar:6.1.11:compile +[INFO] | \- org.springframework:spring-tx:jar:6.1.11:compile +[INFO] +- com.fasterxml.jackson.dataformat:jackson-dataformat-xml:jar:2.17.2:compile +[INFO] | +- com.fasterxml.jackson.core:jackson-core:jar:2.17.2:compile +[INFO] | +- com.fasterxml.jackson.core:jackson-annotations:jar:2.17.2:compile +[INFO] | +- com.fasterxml.jackson.core:jackson-databind:jar:2.17.2:compile +[INFO] | \- org.codehaus.woodstox:stax2-api:jar:4.2.2:compile +[INFO] +- com.fasterxml.woodstox:woodstox-core:jar:6.7.0:compile +[INFO] +- io.netty:netty-resolver-dns-native-macos:jar:osx-aarch_64:4.1.112.Final:runtime +[INFO] | \- io.netty:netty-resolver-dns-classes-macos:jar:4.1.111.Final:compile +[INFO] | +- io.netty:netty-common:jar:4.1.111.Final:compile +[INFO] | +- io.netty:netty-resolver-dns:jar:4.1.111.Final:compile +[INFO] | | \- io.netty:netty-codec-dns:jar:4.1.111.Final:compile +[INFO] | \- io.netty:netty-transport-native-unix-common:jar:4.1.111.Final:compile +[INFO] +- commons-io:commons-io:jar:2.16.1:compile +[INFO] +- software.amazon.awssdk:s3:jar:2.25.66:compile +[INFO] | +- software.amazon.awssdk:aws-xml-protocol:jar:2.25.66:compile +[INFO] | | \- software.amazon.awssdk:aws-query-protocol:jar:2.25.66:compile +[INFO] | +- software.amazon.awssdk:protocol-core:jar:2.25.66:compile +[INFO] | +- software.amazon.awssdk:arns:jar:2.25.66:compile +[INFO] | +- software.amazon.awssdk:profiles:jar:2.25.66:compile +[INFO] | +- software.amazon.awssdk:crt-core:jar:2.25.66:compile +[INFO] | +- software.amazon.awssdk:http-auth:jar:2.25.66:compile +[INFO] | +- software.amazon.awssdk:identity-spi:jar:2.25.66:compile +[INFO] | +- software.amazon.awssdk:http-auth-spi:jar:2.25.66:compile +[INFO] | | \- org.reactivestreams:reactive-streams:jar:1.0.4:compile +[INFO] | +- software.amazon.awssdk:http-auth-aws:jar:2.25.66:compile +[INFO] | +- software.amazon.awssdk:checksums:jar:2.25.66:compile +[INFO] | +- software.amazon.awssdk:checksums-spi:jar:2.25.66:compile +[INFO] | +- software.amazon.awssdk:sdk-core:jar:2.25.66:compile +[INFO] | +- software.amazon.awssdk:auth:jar:2.25.66:compile +[INFO] | | \- software.amazon.eventstream:eventstream:jar:1.0.1:compile +[INFO] | +- software.amazon.awssdk:http-client-spi:jar:2.25.66:compile +[INFO] | +- software.amazon.awssdk:regions:jar:2.25.66:compile +[INFO] | +- software.amazon.awssdk:annotations:jar:2.25.66:compile +[INFO] | +- software.amazon.awssdk:utils:jar:2.25.66:compile +[INFO] | +- software.amazon.awssdk:aws-core:jar:2.25.66:compile +[INFO] | +- software.amazon.awssdk:metrics-spi:jar:2.25.66:compile +[INFO] | +- software.amazon.awssdk:json-utils:jar:2.25.66:compile +[INFO] | | \- software.amazon.awssdk:third-party-jackson-core:jar:2.25.66:compile +[INFO] | +- software.amazon.awssdk:endpoints-spi:jar:2.25.66:compile +[INFO] | +- software.amazon.awssdk:apache-client:jar:2.25.66:runtime +[INFO] | | +- org.apache.httpcomponents:httpcore:jar:4.4.16:compile +[INFO] | | \- commons-codec:commons-codec:jar:1.16.1:compile +[INFO] | \- software.amazon.awssdk:netty-nio-client:jar:2.25.66:runtime +[INFO] | +- io.netty:netty-codec-http:jar:4.1.111.Final:compile +[INFO] | +- io.netty:netty-codec-http2:jar:4.1.111.Final:compile +[INFO] | +- io.netty:netty-codec:jar:4.1.111.Final:compile +[INFO] | +- io.netty:netty-transport:jar:4.1.111.Final:compile +[INFO] | +- io.netty:netty-buffer:jar:4.1.111.Final:compile +[INFO] | +- io.netty:netty-transport-classes-epoll:jar:4.1.111.Final:compile +[INFO] | \- io.netty:netty-resolver:jar:4.1.111.Final:compile +[INFO] +- tel.schich:aws-s3-post-object-presigner:jar:0.1.1:compile +[INFO] | \- com.google.code.gson:gson:jar:2.10.1:compile +[INFO] +- org.springframework.boot:spring-boot-starter-actuator:jar:3.3.2:compile +[INFO] | +- org.springframework.boot:spring-boot-starter:jar:3.3.2:compile +[INFO] | | +- org.springframework.boot:spring-boot-starter-logging:jar:3.3.2:compile +[INFO] | | | +- ch.qos.logback:logback-classic:jar:1.5.6:compile +[INFO] | | | | \- ch.qos.logback:logback-core:jar:1.5.6:compile +[INFO] | | | +- org.apache.logging.log4j:log4j-to-slf4j:jar:2.23.1:compile +[INFO] | | | \- org.slf4j:jul-to-slf4j:jar:2.0.13:compile +[INFO] | | \- jakarta.annotation:jakarta.annotation-api:jar:2.1.1:compile +[INFO] | +- org.springframework.boot:spring-boot-actuator-autoconfigure:jar:3.3.2:compile +[INFO] | | +- org.springframework.boot:spring-boot-actuator:jar:3.3.2:compile +[INFO] | | \- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:jar:2.17.2:compile +[INFO] | +- io.micrometer:micrometer-observation:jar:1.13.2:compile +[INFO] | | \- io.micrometer:micrometer-commons:jar:1.13.2:compile +[INFO] | \- io.micrometer:micrometer-jakarta9:jar:1.13.2:compile +[INFO] | \- io.micrometer:micrometer-core:jar:1.13.2:compile +[INFO] | +- org.hdrhistogram:HdrHistogram:jar:2.2.2:runtime +[INFO] | \- org.latencyutils:LatencyUtils:jar:2.0.3:runtime +[INFO] +- org.springframework.boot:spring-boot-starter-data-jpa:jar:3.3.2:compile +[INFO] | +- org.springframework.boot:spring-boot-starter-aop:jar:3.3.2:compile +[INFO] | | \- org.aspectj:aspectjweaver:jar:1.9.22.1:compile +[INFO] | +- org.springframework.boot:spring-boot-starter-jdbc:jar:3.3.2:compile +[INFO] | | \- com.zaxxer:HikariCP:jar:5.1.0:compile +[INFO] | +- org.hibernate.orm:hibernate-core:jar:6.5.2.Final:compile +[INFO] | | +- jakarta.persistence:jakarta.persistence-api:jar:3.1.0:compile +[INFO] | | +- jakarta.transaction:jakarta.transaction-api:jar:2.0.1:compile +[INFO] | | +- org.jboss.logging:jboss-logging:jar:3.5.3.Final:compile +[INFO] | | +- org.hibernate.common:hibernate-commons-annotations:jar:6.0.6.Final:runtime +[INFO] | | +- io.smallrye:jandex:jar:3.1.2:runtime +[INFO] | | +- com.fasterxml:classmate:jar:1.7.0:compile +[INFO] | | +- net.bytebuddy:byte-buddy:jar:1.14.18:runtime +[INFO] | | +- org.glassfish.jaxb:jaxb-runtime:jar:4.0.5:runtime +[INFO] | | | \- org.glassfish.jaxb:jaxb-core:jar:4.0.5:runtime +[INFO] | | | +- org.eclipse.angus:angus-activation:jar:2.0.2:runtime +[INFO] | | | +- org.glassfish.jaxb:txw2:jar:4.0.5:runtime +[INFO] | | | \- com.sun.istack:istack-commons-runtime:jar:4.1.2:runtime +[INFO] | | +- jakarta.inject:jakarta.inject-api:jar:2.0.1:runtime +[INFO] | | \- org.antlr:antlr4-runtime:jar:4.13.0:compile +[INFO] | +- org.springframework.data:spring-data-jpa:jar:3.3.2:compile +[INFO] | | +- org.springframework.data:spring-data-commons:jar:3.3.2:compile +[INFO] | | \- org.springframework:spring-orm:jar:6.1.11:compile +[INFO] | \- org.springframework:spring-aspects:jar:6.1.11:compile +[INFO] +- org.springframework.boot:spring-boot-starter-security:jar:3.3.2:compile +[INFO] | \- org.springframework:spring-aop:jar:6.1.11:compile +[INFO] +- org.springframework.security:spring-security-config:jar:6.3.3:compile +[INFO] | +- org.springframework.security:spring-security-core:jar:6.3.1:compile +[INFO] | | \- org.springframework.security:spring-security-crypto:jar:6.3.1:compile +[INFO] | +- org.springframework:spring-beans:jar:6.1.11:compile +[INFO] | \- org.springframework:spring-core:jar:6.1.11:compile +[INFO] +- org.springframework.security:spring-security-web:jar:6.3.3:compile +[INFO] | +- org.springframework:spring-expression:jar:6.1.11:compile +[INFO] | \- org.springframework:spring-web:jar:6.1.11:compile +[INFO] +- org.springframework.boot:spring-boot-starter-web:jar:3.3.2:compile +[INFO] | +- org.springframework.boot:spring-boot-starter-json:jar:3.3.2:compile +[INFO] | | +- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:jar:2.17.2:compile +[INFO] | | \- com.fasterxml.jackson.module:jackson-module-parameter-names:jar:2.17.2:compile +[INFO] | +- org.springframework.boot:spring-boot-starter-tomcat:jar:3.3.2:compile +[INFO] | | +- org.apache.tomcat.embed:tomcat-embed-core:jar:10.1.26:compile +[INFO] | | \- org.apache.tomcat.embed:tomcat-embed-websocket:jar:10.1.26:compile +[INFO] | \- org.springframework:spring-webmvc:jar:6.1.11:compile +[INFO] +- org.springframework.boot:spring-boot-starter-thymeleaf:jar:3.3.2:compile +[INFO] | \- org.thymeleaf:thymeleaf-spring6:jar:3.1.2.RELEASE:compile +[INFO] | \- org.thymeleaf:thymeleaf:jar:3.1.2.RELEASE:compile +[INFO] | +- org.attoparser:attoparser:jar:2.0.7.RELEASE:compile +[INFO] | \- org.unbescape:unbescape:jar:1.1.6.RELEASE:compile +[INFO] +- org.springframework.boot:spring-boot-starter-webflux:jar:3.3.2:compile +[INFO] | +- org.springframework.boot:spring-boot-starter-reactor-netty:jar:3.3.2:compile +[INFO] | | \- io.projectreactor.netty:reactor-netty-http:jar:1.1.21:compile +[INFO] | | +- io.netty:netty-resolver-dns-native-macos:jar:osx-x86_64:4.1.111.Final:compile +[INFO] | | \- io.projectreactor.netty:reactor-netty-core:jar:1.1.21:compile +[INFO] | \- org.springframework:spring-webflux:jar:6.1.11:compile +[INFO] | \- io.projectreactor:reactor-core:jar:3.6.8:compile +[INFO] +- org.springframework.boot:spring-boot-starter-validation:jar:3.3.2:compile +[INFO] | +- org.apache.tomcat.embed:tomcat-embed-el:jar:10.1.26:compile +[INFO] | \- org.hibernate.validator:hibernate-validator:jar:8.0.1.Final:compile +[INFO] | \- jakarta.validation:jakarta.validation-api:jar:3.0.2:compile +[INFO] +- org.liquibase:liquibase-core:jar:4.29.1:compile +[INFO] | +- com.opencsv:opencsv:jar:5.9:compile +[INFO] | +- org.yaml:snakeyaml:jar:2.2:compile +[INFO] | +- javax.xml.bind:jaxb-api:jar:2.3.1:compile +[INFO] | +- org.apache.commons:commons-collections4:jar:4.4:compile +[INFO] | \- org.apache.commons:commons-text:jar:1.12.0:compile +[INFO] +- org.projectlombok:lombok:jar:1.18.34:compile +[INFO] +- net.javacrumbs.shedlock:shedlock-spring:jar:4.48.0:compile +[INFO] | \- net.javacrumbs.shedlock:shedlock-core:jar:4.48.0:compile +[INFO] +- net.javacrumbs.shedlock:shedlock-provider-jdbc-template:jar:4.48.0:compile +[INFO] +- org.springdoc:springdoc-openapi-starter-webmvc-ui:jar:2.6.0:compile +[INFO] | +- org.springdoc:springdoc-openapi-starter-webmvc-api:jar:2.6.0:compile +[INFO] | | \- org.springdoc:springdoc-openapi-starter-common:jar:2.6.0:compile +[INFO] | | \- io.swagger.core.v3:swagger-core-jakarta:jar:2.2.22:compile +[INFO] | | +- io.swagger.core.v3:swagger-annotations-jakarta:jar:2.2.22:compile +[INFO] | | +- io.swagger.core.v3:swagger-models-jakarta:jar:2.2.22:compile +[INFO] | | \- com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:jar:2.17.2:compile +[INFO] | \- org.webjars:swagger-ui:jar:5.17.14:compile +[INFO] +- org.springframework.boot:spring-boot-devtools:jar:3.3.2:runtime +[INFO] | +- org.springframework.boot:spring-boot:jar:3.3.2:compile +[INFO] | \- org.springframework.boot:spring-boot-autoconfigure:jar:3.3.2:compile +[INFO] +- org.postgresql:postgresql:jar:42.7.3:runtime +[INFO] | \- org.checkerframework:checker-qual:jar:3.42.0:runtime +[INFO] +- org.springframework.boot:spring-boot-starter-test:jar:3.3.2:test +[INFO] | +- org.springframework.boot:spring-boot-test:jar:3.3.2:test +[INFO] | +- org.springframework.boot:spring-boot-test-autoconfigure:jar:3.3.2:test +[INFO] | +- com.jayway.jsonpath:json-path:jar:2.9.0:test +[INFO] | +- jakarta.xml.bind:jakarta.xml.bind-api:jar:4.0.2:compile +[INFO] | | \- jakarta.activation:jakarta.activation-api:jar:2.1.3:compile +[INFO] | +- org.assertj:assertj-core:jar:3.25.3:test +[INFO] | +- org.awaitility:awaitility:jar:4.2.1:test +[INFO] | +- org.hamcrest:hamcrest:jar:2.2:test +[INFO] | +- org.junit.jupiter:junit-jupiter:jar:5.10.3:test +[INFO] | | +- org.junit.jupiter:junit-jupiter-api:jar:5.10.3:test +[INFO] | | | +- org.opentest4j:opentest4j:jar:1.3.0:test +[INFO] | | | +- org.junit.platform:junit-platform-commons:jar:1.10.3:test +[INFO] | | | \- org.apiguardian:apiguardian-api:jar:1.1.2:test +[INFO] | | +- org.junit.jupiter:junit-jupiter-params:jar:5.10.3:test +[INFO] | | \- org.junit.jupiter:junit-jupiter-engine:jar:5.10.3:test +[INFO] | | \- org.junit.platform:junit-platform-engine:jar:1.10.3:test +[INFO] | +- org.mockito:mockito-core:jar:5.11.0:test +[INFO] | | +- net.bytebuddy:byte-buddy-agent:jar:1.14.18:test +[INFO] | | \- org.objenesis:objenesis:jar:3.3:test +[INFO] | +- org.mockito:mockito-junit-jupiter:jar:5.11.0:test +[INFO] | +- org.skyscreamer:jsonassert:jar:1.5.3:test +[INFO] | | \- com.vaadin.external.google:android-json:jar:0.0.20131108.vaadin1:test +[INFO] | +- org.springframework:spring-test:jar:6.1.11:test +[INFO] | \- org.xmlunit:xmlunit-core:jar:2.9.1:test +[INFO] +- org.springframework.security:spring-security-test:jar:6.3.1:test +[INFO] +- org.hsqldb:hsqldb:jar:2.7.3:test +[INFO] +- com.squareup.okhttp3:mockwebserver:jar:4.12.0:test +[INFO] | +- com.squareup.okhttp3:okhttp:jar:4.12.0:test +[INFO] | | \- com.squareup.okio:okio:jar:3.6.0:test +[INFO] | | \- com.squareup.okio:okio-jvm:jar:3.6.0:test +[INFO] | | \- org.jetbrains.kotlin:kotlin-stdlib-common:jar:1.9.24:test +[INFO] | +- junit:junit:jar:4.13.2:test +[INFO] | | \- org.hamcrest:hamcrest-core:jar:2.2:test +[INFO] | \- org.jetbrains.kotlin:kotlin-stdlib-jdk8:jar:1.9.24:test +[INFO] | +- org.jetbrains.kotlin:kotlin-stdlib:jar:1.9.24:test +[INFO] | | \- org.jetbrains:annotations:jar:13.0:test +[INFO] | \- org.jetbrains.kotlin:kotlin-stdlib-jdk7:jar:1.9.24:test +[INFO] +- org.jsoup:jsoup:jar:1.18.1:compile +[INFO] +- net.minidev:json-smart:jar:2.5.1:compile +[INFO] | \- net.minidev:accessors-smart:jar:2.5.1:compile +[INFO] | \- org.ow2.asm:asm:jar:9.6:compile +[INFO] +- fi.vm.sade.java-utils:opintopolku-cas-servlet-filter:jar:0.1.2-SNAPSHOT:compile +[INFO] | +- fi.vm.sade.java-utils:java-properties:jar:0.1.0-SNAPSHOT:compile +[INFO] | \- org.springframework.security:spring-security-cas:jar:6.3.1:compile +[INFO] | \- org.apereo.cas.client:cas-client-core:jar:4.0.4:compile +[INFO] | +- com.nimbusds:nimbus-jose-jwt:jar:9.37.3:compile +[INFO] | | \- com.github.stephenc.jcip:jcip-annotations:jar:1.0-1:compile +[INFO] | \- org.bouncycastle:bcpkix-jdk15on:jar:1.70:compile +[INFO] | +- org.bouncycastle:bcprov-jdk15on:jar:1.70:compile +[INFO] | \- org.bouncycastle:bcutil-jdk15on:jar:1.70:compile +[INFO] +- fi.vm.sade.java-utils:java-cas:jar:1.1.1-SNAPSHOT:compile +[INFO] | +- org.asynchttpclient:async-http-client:jar:2.12.3:compile +[INFO] | | +- org.asynchttpclient:async-http-client-netty-utils:jar:2.12.3:compile +[INFO] | | +- io.netty:netty-codec-socks:jar:4.1.111.Final:compile +[INFO] | | +- io.netty:netty-handler-proxy:jar:4.1.111.Final:compile +[INFO] | | +- io.netty:netty-transport-native-epoll:jar:linux-x86_64:4.1.111.Final:compile +[INFO] | | +- io.netty:netty-transport-native-kqueue:jar:osx-x86_64:4.1.111.Final:compile +[INFO] | | | \- io.netty:netty-transport-classes-kqueue:jar:4.1.111.Final:compile +[INFO] | | +- com.typesafe.netty:netty-reactive-streams:jar:2.0.4:compile +[INFO] | | \- com.sun.activation:jakarta.activation:jar:1.2.2:compile +[INFO] | +- io.netty:netty-handler:jar:4.1.111.Final:compile +[INFO] | \- org.slf4j:slf4j-simple:jar:2.0.13:compile +[INFO] +- fi.vm.sade.java-utils:opintopolku-user-details-service:jar:0.3.0-SNAPSHOT:compile +[INFO] | \- fi.vm.sade.java-utils:httpclient:jar:0.4.0-SNAPSHOT:compile +[INFO] +- fi.vm.sade.java-utils:java-http:jar:0.6.1-SNAPSHOT:compile +[INFO] | +- fi.vm.sade.java-utils:java-legacy-cas:jar:0.5.1-SNAPSHOT:compile +[INFO] | | \- commons-lang:commons-lang:jar:2.6:compile +[INFO] | +- org.apache.httpcomponents:httpclient:jar:4.5.13:compile +[INFO] | \- org.apache.httpcomponents:httpclient-cache:jar:4.5.13:compile +[INFO] +- fi.vm.sade.kayttooikeus:kayttooikeus-api:jar:1.0.2-SNAPSHOT:compile +[INFO] +- fi.vm.sade:auditlogger:jar:9.2.1-SNAPSHOT:compile +[INFO] | \- com.tananaev:json-patch:jar:1.2:compile +[INFO] \- org.apache.poi:poi-ooxml:jar:5.3.0:compile +[INFO] +- org.apache.poi:poi:jar:5.3.0:compile +[INFO] | +- org.apache.commons:commons-math3:jar:3.6.1:compile +[INFO] | \- com.zaxxer:SparseBitSet:jar:1.3:compile +[INFO] +- org.apache.poi:poi-ooxml-lite:jar:5.3.0:compile +[INFO] +- org.apache.xmlbeans:xmlbeans:jar:5.2.1:compile +[INFO] +- org.apache.commons:commons-compress:jar:1.26.2:compile +[INFO] +- com.github.virtuald:curvesapi:jar:1.08:compile +[INFO] \- org.apache.logging.log4j:log4j-api:jar:2.23.1:compile +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD SUCCESS +[INFO] ------------------------------------------------------------------------ +[INFO] Total time: 1.449 s +[INFO] Finished at: 2024-10-15T19:10:00+03:00 +[INFO] ------------------------------------------------------------------------ diff --git a/backend/vkt/scripts/koodisto_municipalities.sh b/backend/vkt/scripts/koodisto_municipalities.sh new file mode 100755 index 000000000..69750ce17 --- /dev/null +++ b/backend/vkt/scripts/koodisto_municipalities.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +# Fetch municipalities from koodisto, save returned json for backend to use, generate frontend localisation files. + +BACKEND_PATH=../src/main/resources/koodisto +BACKEND_KOODISTO_FILE=${BACKEND_PATH}/koodisto_kunnat.json +mkdir -p $BACKEND_PATH +FRONTEND_PATH=../../../frontend/packages/vkt/public/i18n + +mkdir -p $FRONTEND_PATH +function fetch_and_transform_koodisto_results_for_backend_use() { + koodistoURL="https://virkailija.opintopolku.fi/koodisto-service/rest/json/kunta/koodi" + # Read municipalities, filter out expired ones, filter out irrelevant codes (198,199,999), pick and transform relevant fields + jq_extract_cmd="[.[] | select(.voimassaLoppuPvm | type == \"null\") | select(.koodiArvo != \"198\" and .koodiArvo != \"199\" and .koodiArvo != \"999\")|{koodiUri, resourceUri, versio, koodiArvo, voimassaAlkuPvm, paivitysPvm, fi: (.metadata[] | select(.kieli == \"FI\") | .nimi), sv: (.metadata[] | select(.kieli == \"SV\") | .nimi)} ]" + curl -H "Caller-Id:kehittaja-vkt" "$koodistoURL" | jq -c "$jq_extract_cmd" > $BACKEND_KOODISTO_FILE +} + +function extract_frontend_localisation() { + lang=$1 + locale=$2 + jq_extract_cmd="[.[] | {key: .koodiArvo, value: .${lang} }] | sort_by(.key) | from_entries" + jq_obj_wrap_cmd='. | {vkt:{koodisto:{municipalities:.}}}' + output="${FRONTEND_PATH}/${locale}/koodisto_municipalities.json" + echo "Command for jq: $jq_extract_cmd" + echo "Outputting to: $output" + jq "$jq_extract_cmd" $BACKEND_KOODISTO_FILE | jq "$jq_obj_wrap_cmd" >"${output}" + echo "ok" +} + +fetch_and_transform_koodisto_results_for_backend_use +extract_frontend_localisation 'fi' 'fi-FI' +extract_frontend_localisation 'sv' 'sv-SE' diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/IndexController.java b/backend/vkt/src/main/java/fi/oph/vkt/api/IndexController.java index af3d00b40..16bc8e752 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/api/IndexController.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/IndexController.java @@ -1,20 +1,15 @@ package fi.oph.vkt.api; import fi.oph.vkt.service.aws.S3Config; -import fi.oph.vkt.util.exception.NotFoundException; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; import java.security.SecureRandom; import java.util.Base64; import java.util.Map; -import org.springframework.http.HttpStatus; import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.servlet.ModelAndView; -import org.springframework.web.servlet.resource.NoResourceFoundException; @Controller @RequestMapping(value = "/") @@ -62,6 +57,11 @@ public ModelAndView index(final HttpServletResponse response) { // Map to everything which has no suffix, i.e. matches to "/foo/bar" but not to "/foo/bar.js" @GetMapping( path = { + // Examiner URLs may end with path segment containing dots (OIDs). + // Pass through requests to /vkt/tv/*, as no static assets are served from that path. + "tv/*", + // For local development + "vkt/tv/*", "{path:[^.]*}", "*/{path:[^.]*}", "*/*/{path:[^.]*}", 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..fd8a9979e 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 @@ -2,30 +2,39 @@ 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; +import fi.oph.vkt.model.type.EnrollmentAppointmentStatus; import fi.oph.vkt.model.type.EnrollmentType; import fi.oph.vkt.model.type.ExamLevel; import fi.oph.vkt.model.type.FreeEnrollmentType; 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; @@ -66,6 +75,9 @@ public class PublicController { @Resource private PublicEnrollmentService publicEnrollmentService; + @Resource + private PublicEnrollmentAppointmentService publicEnrollmentAppointmentService; + @Resource private PublicExamEventService publicExamEventService; @@ -87,11 +99,33 @@ public class PublicController { @Resource private FeatureFlagService featureFlagService; + @Resource + private PublicExaminerService publicExaminerService; + @GetMapping(path = "/examEvent") public List list() { return publicExamEventService.listExamEvents(ExamLevel.EXCELLENT); } + @GetMapping(path = "/examiner") + public List listExaminers() { + return publicExaminerService.listExaminers(); + } + + @PostMapping(path = "/enrollment/examiner/{examinerId:\\d+}") + @ResponseStatus(HttpStatus.CREATED) + public void createEnrollmentContact( + @RequestBody @Valid final PublicEnrollmentContactCreateDTO dto, + @PathVariable final long examinerId + ) throws IOException, InterruptedException { + publicEnrollmentService.createEnrollmentContact(dto, examinerId); + } + + @GetMapping(path = "/examiner/{examinerId:\\d+}") + public PublicExaminerDTO getExaminer(@PathVariable final long examinerId) { + return publicExaminerService.getExaminer(examinerId); + } + @PostMapping(path = "/enrollment/reservation/{reservationId:\\d+}") @ResponseStatus(HttpStatus.CREATED) public PublicEnrollmentDTO createEnrollment( @@ -144,6 +178,35 @@ 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 Long appointmentId = SessionUtil.getAppointmentId(session); + if (appointmentId != enrollmentAppointmentId) { + throw new APIException(APIExceptionType.APPOINTMENT_ID_MISMATCH); + } + + return publicEnrollmentService.getEnrollmentAppointment(enrollmentAppointmentId); + } + + @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 getEducation(final HttpSession session) throws JsonProcessingException { final Person person = publicAuthService.getPersonFromSession(session); @@ -180,6 +243,59 @@ 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 = "/enrollment/appointment/{enrollmentAppointmentId:\\d+}/redirectPayment/{paymentLinkHash:[a-z0-9\\-]+}" + ) + public void createPaymentHashRedirectToPaytrail( + final HttpServletResponse httpResponse, + @PathVariable final long enrollmentAppointmentId, + @PathVariable final String paymentLinkHash + ) throws IOException { + try { + final EnrollmentAppointment enrollment = publicEnrollmentService.getEnrollmentAppointmentByIdAndPaymentLink( + enrollmentAppointmentId, + paymentLinkHash + ); + final String redirectUrl = paymentService.createPaymentForEnrollmentAppointment( + enrollment.getId(), + enrollment.getPerson(), + AppLocale.FI + ); + + httpResponse.sendRedirect(redirectUrl); + } 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,47 +327,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 locale, - final HttpSession session + @RequestParam final Optional 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())); @@ -266,12 +384,9 @@ public Optional 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(); } } @@ -286,20 +401,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 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) { diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/clerk/ClerkEnrollmentController.java b/backend/vkt/src/main/java/fi/oph/vkt/api/clerk/ClerkEnrollmentController.java index 82bca461d..651f016fd 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/api/clerk/ClerkEnrollmentController.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/clerk/ClerkEnrollmentController.java @@ -2,7 +2,6 @@ import static org.springframework.http.MediaType.ALL_VALUE; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; -import static org.springframework.http.MediaType.APPLICATION_PDF_VALUE; import fi.oph.vkt.api.dto.clerk.ClerkEnrollmentDTO; import fi.oph.vkt.api.dto.clerk.ClerkEnrollmentMoveDTO; @@ -13,19 +12,12 @@ import fi.oph.vkt.service.ClerkEnrollmentService; import fi.oph.vkt.service.FeatureFlagService; import fi.oph.vkt.service.aws.S3Service; -import fi.oph.vkt.service.receipt.ReceiptData; -import fi.oph.vkt.service.receipt.ReceiptRenderer; -import fi.oph.vkt.util.LocalisationUtil; import io.swagger.v3.oas.annotations.Operation; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; -import java.io.ByteArrayInputStream; import java.io.IOException; -import java.util.Locale; -import org.springframework.core.io.InputStreamResource; import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -48,9 +40,6 @@ public class ClerkEnrollmentController { @Resource private ClerkEnrollmentService clerkEnrollmentService; - @Resource - private ReceiptRenderer receiptRenderer; - @Resource private FeatureFlagService featureFlagService; @@ -92,6 +81,7 @@ public ClerkPaymentLinkDTO createPaymentLink(@PathVariable final long enrollment return clerkEnrollmentService.createPaymentLink(enrollmentId); } + /* // TODO: this is currently an unused endpoint @GetMapping(path = "/{enrollmentId:\\d+}/receipt", consumes = ALL_VALUE, produces = APPLICATION_PDF_VALUE) @Operation(tags = TAG_ENROLLMENT, summary = "Download payment PDF") @@ -109,6 +99,7 @@ public ResponseEntity downloadReceipt( final ByteArrayInputStream bis = new ByteArrayInputStream(receiptRenderer.getReceiptPdfBytes(receiptData, locale)); return ResponseEntity.ok().body(new InputStreamResource(bis)); } + */ @PostMapping(path = "/{enrollmentId:\\d+}/refreshKoskiEducationDetails", consumes = ALL_VALUE, produces = ALL_VALUE) @Operation(tags = TAG_ENROLLMENT, summary = "Refresh education details from KOSKI") diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/clerk/ClerkExamEventController.java b/backend/vkt/src/main/java/fi/oph/vkt/api/clerk/ClerkExamEventController.java index 90b0c00ae..8b2a2f9a6 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/api/clerk/ClerkExamEventController.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/clerk/ClerkExamEventController.java @@ -6,6 +6,7 @@ import fi.oph.vkt.api.dto.clerk.ClerkExamEventDTO; import fi.oph.vkt.api.dto.clerk.ClerkExamEventListDTO; import fi.oph.vkt.api.dto.clerk.ClerkExamEventUpdateDTO; +import fi.oph.vkt.model.type.ExamLevel; import fi.oph.vkt.service.ClerkExamEventService; import io.swagger.v3.oas.annotations.Operation; import jakarta.annotation.Resource; @@ -33,7 +34,7 @@ public class ClerkExamEventController { @GetMapping @Operation(tags = TAG_EXAM_EVENT, summary = "List all exam events") public List list() { - return clerkExamEventService.list(); + return clerkExamEventService.list(ExamLevel.EXCELLENT); } @GetMapping(path = "/{examEventId:\\d+}") diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/clerk/ClerkExaminerController.java b/backend/vkt/src/main/java/fi/oph/vkt/api/clerk/ClerkExaminerController.java new file mode 100644 index 000000000..628606c00 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/clerk/ClerkExaminerController.java @@ -0,0 +1,27 @@ +package fi.oph.vkt.api.clerk; + +import fi.oph.vkt.api.dto.examiner.ExaminerDetailsDTO; +import fi.oph.vkt.service.ClerkExaminerService; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.annotation.Resource; +import java.util.List; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping(value = "/api/v1/clerk/examiner", produces = MediaType.APPLICATION_JSON_VALUE) +public class ClerkExaminerController { + + private static final String TAG_CLERK_EXAMINER = "Examiner API for OPH clerk users"; + + @Resource + private ClerkExaminerService clerkExaminerService; + + @GetMapping + @Operation(tags = TAG_CLERK_EXAMINER, summary = "List examiner details") + public List listExaminers() { + return clerkExaminerService.listExaminers(); + } +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/clerk/ClerkUserController.java b/backend/vkt/src/main/java/fi/oph/vkt/api/clerk/ClerkUserController.java index cf1454e15..2aa87e4e1 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/api/clerk/ClerkUserController.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/clerk/ClerkUserController.java @@ -1,7 +1,11 @@ package fi.oph.vkt.api.clerk; import fi.oph.vkt.api.dto.clerk.ClerkUserDTO; +import fi.oph.vkt.config.Constants; +import fi.oph.vkt.util.AuthorizationUtil; import org.springframework.http.MediaType; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -13,7 +17,10 @@ public class ClerkUserController { @GetMapping(path = "") public ClerkUserDTO currentClerkUser() { - final String oid = SecurityContextHolder.getContext().getAuthentication().getName(); - return ClerkUserDTO.builder().oid(oid).build(); + final Authentication authn = SecurityContextHolder.getContext().getAuthentication(); + final boolean isAdmin = AuthorizationUtil.hasRole(authn, Constants.APP_ADMIN_ROLE); + final boolean isExaminer = AuthorizationUtil.hasRole(authn, Constants.APP_TV_ROLE); + final String oid = authn.getName(); + return ClerkUserDTO.builder().oid(oid).isAdmin(isAdmin).isExaminer(isExaminer).build(); } } diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/EnrollmentDTOCommonFields.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/EnrollmentDTOCommonFields.java index a80bd6255..d69f44e56 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/EnrollmentDTOCommonFields.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/EnrollmentDTOCommonFields.java @@ -1,13 +1,6 @@ package fi.oph.vkt.api.dto; -public interface EnrollmentDTOCommonFields { - Boolean oralSkill(); - Boolean textualSkill(); - Boolean understandingSkill(); - Boolean speakingPartialExam(); - Boolean speechComprehensionPartialExam(); - Boolean writingPartialExam(); - Boolean readingComprehensionPartialExam(); +public interface EnrollmentDTOCommonFields extends EnrollmentDTOSkillFields { String previousEnrollment(); Boolean digitalCertificateConsent(); String email(); diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/EnrollmentDTOSkillFields.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/EnrollmentDTOSkillFields.java new file mode 100644 index 000000000..5e202a38a --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/EnrollmentDTOSkillFields.java @@ -0,0 +1,11 @@ +package fi.oph.vkt.api.dto; + +public interface EnrollmentDTOSkillFields { + Boolean oralSkill(); + Boolean textualSkill(); + Boolean understandingSkill(); + Boolean speakingPartialExam(); + Boolean speechComprehensionPartialExam(); + Boolean writingPartialExam(); + Boolean readingComprehensionPartialExam(); +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/EnrollmentGradeDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/EnrollmentGradeDTO.java new file mode 100644 index 000000000..f69425417 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/EnrollmentGradeDTO.java @@ -0,0 +1,10 @@ +package fi.oph.vkt.api.dto; + +import fi.oph.vkt.model.type.EnrollmentGradeType; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Builder; +import lombok.NonNull; + +@Builder +public record EnrollmentGradeDTO(@NonNull @NotNull EnrollmentGradeType grade, @Size(max = 1024) String comment) {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/MunicipalityDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/MunicipalityDTO.java new file mode 100644 index 000000000..0a68340c2 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/MunicipalityDTO.java @@ -0,0 +1,8 @@ +package fi.oph.vkt.api.dto; + +import jakarta.validation.constraints.NotNull; +import lombok.Builder; +import lombok.NonNull; + +@Builder +public record MunicipalityDTO(@NonNull @NotNull String code) {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicAppointmentExamDateDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicAppointmentExamDateDTO.java new file mode 100644 index 000000000..9a9cdd01c --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicAppointmentExamDateDTO.java @@ -0,0 +1,15 @@ +package fi.oph.vkt.api.dto; + +import fi.oph.vkt.model.type.ExamLanguage; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDate; +import lombok.Builder; +import lombok.NonNull; + +@Builder +public record PublicAppointmentExamDateDTO( + @NonNull @NotNull LocalDate date, + @NonNull @NotNull String location, + @NonNull @NotNull ExamLanguage language, + @NonNull PublicExaminerNameDTO examiner +) {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicEnrollmentAppointmentDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicEnrollmentAppointmentDTO.java new file mode 100644 index 000000000..7e0a69187 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicEnrollmentAppointmentDTO.java @@ -0,0 +1,31 @@ +package fi.oph.vkt.api.dto; + +import fi.oph.vkt.model.type.EnrollmentAppointmentStatus; +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 EnrollmentAppointmentStatus status, + String previousEnrollment, + @NonNull @NotNull Boolean digitalCertificateConsent, + @NonNull @NotBlank String email, + String phoneNumber, + String street, + String postalCode, + String town, + String country, + PublicPersonDTO person, + @NonNull @NotNull PublicAppointmentExamDateDTO examEvent +) {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicEnrollmentAppointmentUpdateDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicEnrollmentAppointmentUpdateDTO.java new file mode 100644 index 000000000..d17732e92 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicEnrollmentAppointmentUpdateDTO.java @@ -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); + } +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicEnrollmentContactCreateDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicEnrollmentContactCreateDTO.java new file mode 100644 index 000000000..c45956157 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicEnrollmentContactCreateDTO.java @@ -0,0 +1,29 @@ +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 isFullExam, + @Size(max = 1024) String partialExamSelection, + @NonNull @NotNull Boolean hasPreviousEnrollment, + @Size(max = 10240) String message, + @Size(max = 255) @NonNull @NotBlank String phoneNumber, + @Size(max = 255) @NonNull @NotBlank String email, + @Size(max = 255) @NonNull @NotBlank String firstName, + @Size(max = 255) @NonNull @NotBlank String lastName +) { + public PublicEnrollmentContactCreateDTO { + partialExamSelection = StringUtil.sanitize(partialExamSelection); + message = StringUtil.sanitize(message); + phoneNumber = StringUtil.sanitize(phoneNumber); + email = StringUtil.sanitize(email); + firstName = StringUtil.sanitize(firstName); + lastName = StringUtil.sanitize(lastName); + } +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicExaminerDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicExaminerDTO.java new file mode 100644 index 000000000..82b77c0fa --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicExaminerDTO.java @@ -0,0 +1,17 @@ +package fi.oph.vkt.api.dto; + +import fi.oph.vkt.model.type.ExamLanguage; +import jakarta.validation.constraints.NotNull; +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 languages, + @NonNull @NotNull List municipalities, + @NonNull @NotNull List examDates +) {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicExaminerExamDateDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicExaminerExamDateDTO.java new file mode 100644 index 000000000..11b88af28 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicExaminerExamDateDTO.java @@ -0,0 +1,8 @@ +package fi.oph.vkt.api.dto; + +import java.time.LocalDate; +import lombok.Builder; +import lombok.NonNull; + +@Builder +public record PublicExaminerExamDateDTO(@NonNull LocalDate examDate, @NonNull Boolean isFull) {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicExaminerNameDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicExaminerNameDTO.java new file mode 100644 index 000000000..cc4927480 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicExaminerNameDTO.java @@ -0,0 +1,8 @@ +package fi.oph.vkt.api.dto; + +import jakarta.validation.constraints.NotNull; +import lombok.Builder; +import lombok.NonNull; + +@Builder +public record PublicExaminerNameDTO(@NonNull @NotNull String name) {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicMunicipalityDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicMunicipalityDTO.java new file mode 100644 index 000000000..14d40cb70 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/PublicMunicipalityDTO.java @@ -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) {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkEnrollmentContactRequestDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkEnrollmentContactRequestDTO.java new file mode 100644 index 000000000..9d9bb7fba --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkEnrollmentContactRequestDTO.java @@ -0,0 +1,24 @@ +package fi.oph.vkt.api.dto.clerk; + +import fi.oph.vkt.model.type.EnrollmentAppointmentStatus; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDateTime; +import lombok.Builder; +import lombok.NonNull; + +@Builder +public record ClerkEnrollmentContactRequestDTO( + @NonNull @NotNull Long id, + @NonNull @NotNull Integer version, + @NonNull @NotNull LocalDateTime enrollmentTime, + Boolean isFullExam, + String partialExamSelection, + @NonNull @NotNull EnrollmentAppointmentStatus status, + @NonNull @NotNull Boolean hasPreviousEnrollment, + @NonNull @NotBlank String phoneNumber, + @NonNull @NotBlank String email, + @NonNull @NotBlank String firstName, + @NonNull @NotBlank String lastName, + String message +) {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkUserDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkUserDTO.java index 6ed7d3e1b..e28d3834d 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkUserDTO.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/clerk/ClerkUserDTO.java @@ -4,4 +4,4 @@ import lombok.NonNull; @Builder -public record ClerkUserDTO(@NonNull String oid) {} +public record ClerkUserDTO(@NonNull String oid, @NonNull Boolean isAdmin, @NonNull Boolean isExaminer) {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerAuthLinkDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerAuthLinkDTO.java new file mode 100644 index 000000000..362b91382 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerAuthLinkDTO.java @@ -0,0 +1,7 @@ +package fi.oph.vkt.api.dto.examiner; + +import java.time.LocalDateTime; +import lombok.Builder; + +@Builder +public record ExaminerAuthLinkDTO(String url, LocalDateTime expiresAt, LocalDateTime sentAt) {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerContactRequestDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerContactRequestDTO.java new file mode 100644 index 000000000..b0c456bc2 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerContactRequestDTO.java @@ -0,0 +1,9 @@ +package fi.oph.vkt.api.dto.examiner; + +import jakarta.validation.constraints.NotEmpty; +import java.util.List; +import lombok.Builder; +import lombok.NonNull; + +@Builder +public record ExaminerContactRequestDTO(@NonNull Long id, @NonNull String lastName, @NonNull String firstName) {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerDetailsDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerDetailsDTO.java new file mode 100644 index 000000000..e456cc43e --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerDetailsDTO.java @@ -0,0 +1,24 @@ +package fi.oph.vkt.api.dto.examiner; + +import fi.oph.vkt.api.dto.MunicipalityDTO; +import jakarta.validation.constraints.NotEmpty; +import java.util.List; +import lombok.Builder; +import lombok.NonNull; + +@Builder +public record ExaminerDetailsDTO( + @NonNull Long id, + @NonNull Integer version, + @NonNull String oid, + @NonNull String email, + @NonNull String phoneNumber, + @NonNull String lastName, + @NonNull String firstName, + @NonNull Boolean examLanguageFinnish, + @NonNull Boolean examLanguageSwedish, + @NonNull Boolean isPublic, + @NonNull @NotEmpty List municipalities, + @NonNull List examEvents, + @NonNull List contactRequests +) {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerDetailsInitDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerDetailsInitDTO.java new file mode 100644 index 000000000..544fc847a --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerDetailsInitDTO.java @@ -0,0 +1,7 @@ +package fi.oph.vkt.api.dto.examiner; + +import lombok.Builder; +import lombok.NonNull; + +@Builder +public record ExaminerDetailsInitDTO(@NonNull String oid, @NonNull String lastName, @NonNull String firstName) {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerDetailsUpsertDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerDetailsUpsertDTO.java new file mode 100644 index 000000000..4be29ef15 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerDetailsUpsertDTO.java @@ -0,0 +1,17 @@ +package fi.oph.vkt.api.dto.examiner; + +import fi.oph.vkt.api.dto.MunicipalityDTO; +import jakarta.validation.constraints.NotEmpty; +import java.util.List; +import lombok.Builder; +import lombok.NonNull; + +@Builder +public record ExaminerDetailsUpsertDTO( + @NonNull String email, + @NonNull String phoneNumber, + @NonNull Boolean examLanguageFinnish, + @NonNull Boolean examLanguageSwedish, + @NonNull Boolean isPublic, + @NonNull @NotEmpty List municipalities +) {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerEnrollmentAppointmentDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerEnrollmentAppointmentDTO.java new file mode 100644 index 000000000..678caba97 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerEnrollmentAppointmentDTO.java @@ -0,0 +1,41 @@ +package fi.oph.vkt.api.dto.examiner; + +import fi.oph.vkt.api.dto.EnrollmentDTOSkillFields; +import fi.oph.vkt.api.dto.clerk.ClerkPaymentDTO; +import fi.oph.vkt.model.type.EnrollmentAppointmentStatus; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.util.List; +import lombok.Builder; +import lombok.NonNull; + +@Builder +public record ExaminerEnrollmentAppointmentDTO( + @NonNull @NotNull Long id, + @NonNull @NotNull Integer version, + @NonNull @NotNull LocalDateTime enrollmentTime, + @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 EnrollmentAppointmentStatus status, + @NonNull @NotNull Boolean hasPreviousEnrollment, + String previousEnrollment, + @NonNull @NotBlank String email, + String phoneNumber, + String street, + String postalCode, + String town, + String country, + @NonNull @NotBlank String firstName, + @NonNull @NotBlank String lastName, + @NonNull @NotNull List payments, + ExaminerExamEventDTO examEvent, + ExaminerAuthLinkDTO authLink, + String paymentLinkUrl +) + implements EnrollmentDTOSkillFields {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerEnrollmentAppointmentHistoryDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerEnrollmentAppointmentHistoryDTO.java new file mode 100644 index 000000000..9795fbeaf --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerEnrollmentAppointmentHistoryDTO.java @@ -0,0 +1,25 @@ +package fi.oph.vkt.api.dto.examiner; + +import fi.oph.vkt.api.dto.EnrollmentDTOSkillFields; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDateTime; +import lombok.Builder; +import lombok.NonNull; + +@Builder +public record ExaminerEnrollmentAppointmentHistoryDTO( + @NonNull @NotNull LocalDateTime enrollmentTime, + @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, + String previousEnrollment, + ExaminerExamEventDTO examEvent, + ExaminerEnrollmentGradesDTO grades, + @NonNull @NotBlank String examinerName +) + implements EnrollmentDTOSkillFields {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerEnrollmentAppointmentUpdateDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerEnrollmentAppointmentUpdateDTO.java new file mode 100644 index 000000000..4885108b9 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerEnrollmentAppointmentUpdateDTO.java @@ -0,0 +1,47 @@ +package fi.oph.vkt.api.dto.examiner; + +import fi.oph.vkt.api.dto.EnrollmentDTOSkillFields; +import fi.oph.vkt.api.dto.clerk.ClerkPaymentDTO; +import fi.oph.vkt.util.StringUtil; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.util.List; +import lombok.Builder; +import lombok.NonNull; + +@Builder +public record ExaminerEnrollmentAppointmentUpdateDTO( + @NonNull @NotNull Long id, + @NonNull @NotNull Integer version, + @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 Boolean hasPreviousEnrollment, + String previousEnrollment, + @NonNull @NotBlank String email, + @NonNull @NotBlank String firstName, + @NonNull @NotBlank String lastName, + String phoneNumber, + @Size(max = 255) String street, + @Size(max = 255) String postalCode, + @Size(max = 255) String town, + @Size(max = 255) String country, + @NonNull @NotNull List payments, + Long examEvent +) + implements EnrollmentDTOSkillFields { + public ExaminerEnrollmentAppointmentUpdateDTO { + previousEnrollment = StringUtil.sanitize(previousEnrollment); + email = StringUtil.sanitize(email); + phoneNumber = StringUtil.sanitize(phoneNumber); + street = StringUtil.sanitize(street); + postalCode = StringUtil.sanitize(postalCode); + town = StringUtil.sanitize(town); + country = StringUtil.sanitize(country); + } +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerEnrollmentGradesDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerEnrollmentGradesDTO.java new file mode 100644 index 000000000..77ede664b --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerEnrollmentGradesDTO.java @@ -0,0 +1,15 @@ +package fi.oph.vkt.api.dto.examiner; + +import fi.oph.vkt.api.dto.EnrollmentGradeDTO; +import jakarta.validation.constraints.NotNull; +import lombok.Builder; +import lombok.NonNull; + +@Builder +public record ExaminerEnrollmentGradesDTO( + @NonNull @NotNull Integer version, + EnrollmentGradeDTO speakingPartialExam, + EnrollmentGradeDTO speechComprehensionPartialExam, + EnrollmentGradeDTO writingPartialExam, + EnrollmentGradeDTO readingComprehensionPartialExam +) {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerExamEventDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerExamEventDTO.java new file mode 100644 index 000000000..bf423a2c2 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerExamEventDTO.java @@ -0,0 +1,26 @@ +package fi.oph.vkt.api.dto.examiner; + +import fi.oph.vkt.api.dto.MunicipalityDTO; +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 ExaminerExamEventDTO( + @NonNull @NotNull Long id, + @NonNull @NotNull Integer version, + @NonNull @NotNull ExamLanguage language, + @NonNull @NotNull LocalDate date, + @NonNull @NotNull Boolean isHidden, + @NonNull @NotNull MunicipalityDTO municipality, + String location, + String examTime, + String otherInformation, + LocalDate registrationCloses, + Long maxParticipants, + @NonNull @NotNull List enrollments +) + implements ExaminerExamEventDTOCommonFields {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerExamEventDTOCommonFields.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerExamEventDTOCommonFields.java new file mode 100644 index 000000000..3c9dfda53 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerExamEventDTOCommonFields.java @@ -0,0 +1,17 @@ +package fi.oph.vkt.api.dto.examiner; + +import fi.oph.vkt.api.dto.MunicipalityDTO; +import fi.oph.vkt.model.type.ExamLanguage; +import java.time.LocalDate; + +public interface ExaminerExamEventDTOCommonFields { + ExamLanguage language(); + LocalDate date(); + Boolean isHidden(); + MunicipalityDTO municipality(); + String location(); + String examTime(); + String otherInformation(); + LocalDate registrationCloses(); + Long maxParticipants(); +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerExamEventUpsertDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerExamEventUpsertDTO.java new file mode 100644 index 000000000..5b8d49c45 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerExamEventUpsertDTO.java @@ -0,0 +1,23 @@ +package fi.oph.vkt.api.dto.examiner; + +import fi.oph.vkt.api.dto.MunicipalityDTO; +import fi.oph.vkt.model.type.ExamLanguage; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDate; +import lombok.Builder; +import lombok.NonNull; + +@Builder +public record ExaminerExamEventUpsertDTO( + Long id, + @NonNull @NotNull ExamLanguage language, + @NonNull @NotNull LocalDate date, + @NonNull @NotNull Boolean isHidden, + @NonNull @NotNull MunicipalityDTO municipality, + String location, + String examTime, + String otherInformation, + LocalDate registrationCloses, + Long maxParticipants +) + implements ExaminerExamEventDTOCommonFields {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerUserDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerUserDTO.java new file mode 100644 index 000000000..6ff4d0727 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/dto/examiner/ExaminerUserDTO.java @@ -0,0 +1,7 @@ +package fi.oph.vkt.api.dto.examiner; + +import lombok.Builder; +import lombok.NonNull; + +@Builder +public record ExaminerUserDTO(@NonNull String oid) {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/examiner/ExaminerDetailsController.java b/backend/vkt/src/main/java/fi/oph/vkt/api/examiner/ExaminerDetailsController.java new file mode 100644 index 000000000..503024294 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/examiner/ExaminerDetailsController.java @@ -0,0 +1,41 @@ +package fi.oph.vkt.api.examiner; + +import fi.oph.vkt.api.dto.examiner.ExaminerDetailsDTO; +import fi.oph.vkt.api.dto.examiner.ExaminerDetailsInitDTO; +import fi.oph.vkt.api.dto.examiner.ExaminerDetailsUpsertDTO; +import fi.oph.vkt.service.ExaminerDetailsService; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.annotation.Resource; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping(value = "/api/v1/tv/{oid}", produces = MediaType.APPLICATION_JSON_VALUE) +public class ExaminerDetailsController { + + private static final String TAG_EXAMINER = "Examiner details API"; + + @Resource + private ExaminerDetailsService examinerDetailsService; + + @GetMapping + @Operation(tags = TAG_EXAMINER, summary = "Get examiner details") + public ExaminerDetailsDTO getExaminerDetails(@PathVariable("oid") String oid) { + return examinerDetailsService.getExaminer(oid); + } + + @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) + @Operation(tags = TAG_EXAMINER, summary = "Create or update examiner") + public ExaminerDetailsDTO upsertExaminer( + @PathVariable("oid") String oid, + @RequestBody ExaminerDetailsUpsertDTO examinerDetailsUpsertDTO + ) { + return examinerDetailsService.upsertExaminer(oid, examinerDetailsUpsertDTO); + } + + @GetMapping(path = "/init") + @Operation(tags = TAG_EXAMINER, summary = "Get personal data needed for initializing examiner details") + public ExaminerDetailsInitDTO getInitialExaminerDetails(@PathVariable("oid") String oid) { + return examinerDetailsService.getInitialExaminerPersonalData(oid); + } +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/examiner/ExaminerEnrollmentController.java b/backend/vkt/src/main/java/fi/oph/vkt/api/examiner/ExaminerEnrollmentController.java new file mode 100644 index 000000000..9bac623b0 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/examiner/ExaminerEnrollmentController.java @@ -0,0 +1,119 @@ +package fi.oph.vkt.api.examiner; + +import static org.springframework.http.MediaType.ALL_VALUE; + +import fi.oph.vkt.api.dto.clerk.ClerkEnrollmentContactRequestDTO; +import fi.oph.vkt.api.dto.examiner.ExaminerEnrollmentAppointmentDTO; +import fi.oph.vkt.api.dto.examiner.ExaminerEnrollmentAppointmentHistoryDTO; +import fi.oph.vkt.api.dto.examiner.ExaminerEnrollmentAppointmentUpdateDTO; +import fi.oph.vkt.api.dto.examiner.ExaminerEnrollmentGradesDTO; +import fi.oph.vkt.service.ExaminerEnrollmentService; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import java.io.IOException; +import java.util.List; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping(value = "/api/v1/tv/{oid}/enrollment", produces = MediaType.APPLICATION_JSON_VALUE) +public class ExaminerEnrollmentController { + + @Resource + private ExaminerEnrollmentService examinerEnrollmentService; + + private static final String TAG_ENROLLMENT = "Examiner enrollment API"; + + @PutMapping(path = "/appointment/{enrollmentAppointmentId:\\d+}") + @Operation(tags = TAG_ENROLLMENT, summary = "Update enrollment appointment") + public ExaminerEnrollmentAppointmentDTO updateEnrollmentAppointment( + @PathVariable String oid, + @PathVariable Long enrollmentAppointmentId, + @RequestBody @Valid final ExaminerEnrollmentAppointmentUpdateDTO dto + ) { + return examinerEnrollmentService.updateAppointment(oid, enrollmentAppointmentId, dto); + } + + @DeleteMapping(path = "/appointment/{enrollmentAppointmentId:\\d+}", consumes = ALL_VALUE) + @Operation(tags = TAG_ENROLLMENT, summary = "Cancel enrollment appointment") + public void cancelEnrollmentAppointment( + @PathVariable final String oid, + @PathVariable final long enrollmentAppointmentId + ) { + examinerEnrollmentService.cancelEnrollmentAppointment(oid, enrollmentAppointmentId); + } + + @GetMapping(path = "/contact/{enrollmentContactId:\\d+}", consumes = ALL_VALUE) + @Operation(tags = TAG_ENROLLMENT, summary = "Get enrollment contact request") + public ClerkEnrollmentContactRequestDTO getEnrollmentContactRequest( + @PathVariable final String oid, + @PathVariable final long enrollmentContactId + ) { + return examinerEnrollmentService.getEnrollmentContactRequest(oid, enrollmentContactId); + } + + @DeleteMapping(path = "/contact/{enrollmentContactId:\\d+}", consumes = ALL_VALUE) + @Operation(tags = TAG_ENROLLMENT, summary = "Delete enrollment contact request") + public void deleteEnrollmentContactRequest( + @PathVariable final String oid, + @PathVariable final long enrollmentContactId + ) { + examinerEnrollmentService.deleteEnrollmentContactRequest(oid, enrollmentContactId); + } + + @PostMapping(path = "/contact/{enrollmentContactId:\\d+}/convertToAppointment", consumes = ALL_VALUE) + @Operation(tags = TAG_ENROLLMENT, summary = "Convert enrollment contact request to enrollment appointment") + public ExaminerEnrollmentAppointmentDTO enrollmentContactRequestToAppointment( + @PathVariable final String oid, + @PathVariable final long enrollmentContactId + ) { + return examinerEnrollmentService.convertToAppointment(oid, enrollmentContactId); + } + + @GetMapping(path = "/appointment/{enrollmentAppointmentId:\\d+}", consumes = ALL_VALUE) + @Operation(tags = TAG_ENROLLMENT, summary = "Get enrollment appointment") + public ExaminerEnrollmentAppointmentDTO getEnrollmentAppointment( + @PathVariable final String oid, + @PathVariable final long enrollmentAppointmentId + ) { + return examinerEnrollmentService.getEnrollmentAppointment(oid, enrollmentAppointmentId); + } + + @GetMapping(path = "/appointment/{enrollmentAppointmentId:\\d+}/history", consumes = ALL_VALUE) + @Operation(tags = TAG_ENROLLMENT, summary = "Get enrollment history for enrolled person") + public List getEnrollmentAppointmentHistory( + @PathVariable final String oid, + @PathVariable final long enrollmentAppointmentId + ) { + return examinerEnrollmentService.getEnrollmentAppointmentHistory(oid, enrollmentAppointmentId); + } + + @PostMapping(path = "/appointment/{enrollmentAppointmentId:\\d+}/sendAuthLink", consumes = ALL_VALUE) + @Operation(tags = TAG_ENROLLMENT, summary = "Send enrollment appointment auth link") + public ExaminerEnrollmentAppointmentDTO sendEnrollmentAppointmentLink( + @PathVariable final String oid, + @PathVariable final long enrollmentAppointmentId + ) throws IOException, InterruptedException { + return examinerEnrollmentService.sendEnrollmentAppointmentLink(oid, enrollmentAppointmentId); + } + + @PutMapping(path = "/appointment/{enrollmentAppointmentId:\\d+}/grades") + @Operation(tags = TAG_ENROLLMENT, summary = "Update enrollment appointment grades") + public ExaminerEnrollmentGradesDTO upsertEnrollmentAppointmentGrades( + @PathVariable String oid, + @RequestBody @Valid final ExaminerEnrollmentGradesDTO dto, + @PathVariable final long enrollmentAppointmentId + ) { + return examinerEnrollmentService.upsertAppointmentGrades(oid, enrollmentAppointmentId, dto); + } + + @GetMapping(path = "/appointment/{enrollmentAppointmentId:\\d+}/grades") + @Operation(tags = TAG_ENROLLMENT, summary = "Get enrollment appointment grades") + public ExaminerEnrollmentGradesDTO getEnrollmentAppointmentGrades( + @PathVariable String oid, + @PathVariable final long enrollmentAppointmentId + ) { + return examinerEnrollmentService.getAppointmentGrades(oid, enrollmentAppointmentId); + } +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/examiner/ExaminerExamEventController.java b/backend/vkt/src/main/java/fi/oph/vkt/api/examiner/ExaminerExamEventController.java new file mode 100644 index 000000000..56b81a688 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/examiner/ExaminerExamEventController.java @@ -0,0 +1,58 @@ +package fi.oph.vkt.api.examiner; + +import fi.oph.vkt.api.dto.examiner.ExaminerExamEventDTO; +import fi.oph.vkt.api.dto.examiner.ExaminerExamEventUpsertDTO; +import fi.oph.vkt.service.ExaminerExamEventService; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.annotation.Resource; +import java.util.List; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.view.document.AbstractXlsxView; + +@RestController +@RequestMapping(value = "/api/v1/tv/{oid}/examEvent", produces = MediaType.APPLICATION_JSON_VALUE) +public class ExaminerExamEventController { + + @Resource + private ExaminerExamEventService examinerExamEventService; + + private static final String TAG_EXAMINER_EXAM_EVENT = "Exam event API for examiners"; + + @GetMapping + @Operation(tags = TAG_EXAMINER_EXAM_EVENT, summary = "List all exam events") + public List list(@PathVariable String oid) { + return examinerExamEventService.list(oid); + } + + @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) + @Operation(tags = TAG_EXAMINER_EXAM_EVENT, summary = "Create exam event") + public ExaminerExamEventDTO createExamEvent( + @PathVariable String oid, + @RequestBody ExaminerExamEventUpsertDTO examinerExamEventDTO + ) { + return examinerExamEventService.createExamEvent(oid, examinerExamEventDTO); + } + + @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, path = "/{examEventId:\\d+}") + @Operation(tags = TAG_EXAMINER_EXAM_EVENT, summary = "Update exam event") + public ExaminerExamEventDTO updateExamEvent( + @PathVariable String oid, + @PathVariable Long examEventId, + @RequestBody ExaminerExamEventUpsertDTO examinerExamEventDTO + ) { + return examinerExamEventService.updateExamEvent(oid, examEventId, examinerExamEventDTO); + } + + @GetMapping(path = "/{examEventId:\\d+}") + @Operation(tags = TAG_EXAMINER_EXAM_EVENT, summary = "Get exam event and enrollments") + public ExaminerExamEventDTO getExamEvent(@PathVariable final String oid, @PathVariable final long examEventId) { + return examinerExamEventService.getExamEvent(oid, examEventId); + } + + @GetMapping(value = "/{examEventId:\\d+}/excel") + @Operation(tags = TAG_EXAMINER_EXAM_EVENT, summary = "Download excel of enrollments to exam event") + public AbstractXlsxView getExamEventExcel(@PathVariable final String oid, @PathVariable final long examEventId) { + return examinerExamEventService.getExamEventExcel(oid, examEventId); + } +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/api/examiner/ExaminerUserController.java b/backend/vkt/src/main/java/fi/oph/vkt/api/examiner/ExaminerUserController.java new file mode 100644 index 000000000..e3e2fe73b --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/api/examiner/ExaminerUserController.java @@ -0,0 +1,19 @@ +package fi.oph.vkt.api.examiner; + +import fi.oph.vkt.api.dto.examiner.ExaminerUserDTO; +import org.springframework.http.MediaType; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping(value = "/api/v1/tv/user", produces = MediaType.APPLICATION_JSON_VALUE) +public class ExaminerUserController { + + @GetMapping(path = "") + public ExaminerUserDTO currentExaminerUser() { + final String oid = SecurityContextHolder.getContext().getAuthentication().getName(); + return ExaminerUserDTO.builder().oid(oid).build(); + } +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/audit/AuditService.java b/backend/vkt/src/main/java/fi/oph/vkt/audit/AuditService.java index d1d01c839..66ccee241 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/audit/AuditService.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/audit/AuditService.java @@ -31,6 +31,10 @@ public void logById(final VktOperation operation, final long id) { log(operation, new Target.Builder().setField("id", Long.toString(id)).build(), Changes.EMPTY); } + public void logById(final VktOperation operation, final String id) { + log(operation, new Target.Builder().setField("id", id).build(), Changes.EMPTY); + } + public void logUpdate(final VktOperation operation, final long id, final T dtoBefore, final T dtoAfter) { log( operation, diff --git a/backend/vkt/src/main/java/fi/oph/vkt/audit/VktOperation.java b/backend/vkt/src/main/java/fi/oph/vkt/audit/VktOperation.java index acca194a6..06398cef9 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/audit/VktOperation.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/audit/VktOperation.java @@ -13,4 +13,13 @@ public enum VktOperation implements Operation { UPDATE_ENROLLMENT_PAYMENT_LINK, MOVE_ENROLLMENT, REFUND_PAYMENT, + LIST_EXAMINER_DETAILS, + GET_EXAMINER_INITIAL_DETAILS, + VIEW_EXAMINER_CONTACT_REQUEST, + VIEW_EXAMINER_ENROLLMENT, + DELETE_EXAMINER_CONTACT_REQUEST, + VIEW_EXAMINER_ENROLLMENT_HISTORY, + CANCEL_ENROLLMENT_APPOINTMENT, + VIEW_EXAMINER_ENROLLMENT_GRADES, + CONVERT_EXAMINER_CONTACT_REQUEST_TO_APPOINTMENT, } diff --git a/backend/vkt/src/main/java/fi/oph/vkt/config/AppConfig.java b/backend/vkt/src/main/java/fi/oph/vkt/config/AppConfig.java index b7beaea64..8d86f2df7 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/config/AppConfig.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/config/AppConfig.java @@ -9,7 +9,13 @@ import fi.oph.vkt.service.email.sender.EmailSender; import fi.oph.vkt.service.email.sender.EmailSenderNoOp; import fi.oph.vkt.service.email.sender.EmailSenderViestintapalvelu; +import fi.oph.vkt.service.onr.OnrOperationApi; +import fi.oph.vkt.service.onr.OnrOperationApiImpl; +import fi.oph.vkt.service.onr.mock.MockOnrOperationApiImpl; import fi.oph.vkt.util.UUIDSource; +import fi.vm.sade.javautils.nio.cas.CasClient; +import fi.vm.sade.javautils.nio.cas.CasClientBuilder; +import fi.vm.sade.javautils.nio.cas.CasConfig; import java.time.Duration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -143,6 +149,34 @@ public AwsCredentialsProvider defaultAwsCredentialsProvider() { return ContainerCredentialsProvider.builder().build(); } + @Bean + @Profile("dev") + public OnrOperationApi onrOperationApiMock() { + LOG.warn("OnrOperationApiMock in use"); + return new MockOnrOperationApiImpl(); + } + + @Bean + @Profile("!dev") + public OnrOperationApi onrOperationApiImpl( + @Value("${app.onr.service-url}") final String onrServiceUrl, + @Value("${cas.url}") final String casUrl, + @Value("${app.onr.cas.username}") final String casUsername, + @Value("${app.onr.cas.password}") final String casPassword + ) { + LOG.info("onrServiceUrl: {}", onrServiceUrl); + final CasConfig casConfig = CasConfig.SpringSessionCasConfig( + casUsername, + casPassword, + casUrl, + onrServiceUrl, + Constants.CALLER_ID, + Constants.CALLER_ID + ); + final CasClient casClient = CasClientBuilder.build(casConfig); + return new OnrOperationApiImpl(casClient, onrServiceUrl); + } + private static WebClient.Builder webClientBuilderWithCallerId(final String connectionProviderName) { ConnectionProvider connectionProvider = ConnectionProvider .builder(connectionProviderName) diff --git a/backend/vkt/src/main/java/fi/oph/vkt/config/Constants.java b/backend/vkt/src/main/java/fi/oph/vkt/config/Constants.java index 2c4d313a4..7d34fb6dc 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/config/Constants.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/config/Constants.java @@ -5,7 +5,8 @@ public class Constants { public static final String CALLER_ID = "1.2.246.562.10.00000000001.vkt"; public static final String EMAIL_SENDER_NAME = "Valtionhallinnon kielitutkinnot | Opetushallitus"; public static final String SERVICENAME = "vkt"; - public static final String APP_ROLE = "APP_VKT"; + public static final String APP_ADMIN_ROLE = "APP_VKT_PAAKAYTTAJA"; + public static final String APP_TV_ROLE = "APP_VKT_TUTKINNON_VASTAANOTTAJA"; // For now, no containers are run in untuva during nighttime // Daily at 9:00 diff --git a/backend/vkt/src/main/java/fi/oph/vkt/config/ControllerExceptionAdvice.java b/backend/vkt/src/main/java/fi/oph/vkt/config/ControllerExceptionAdvice.java index 620a8d397..a9b2e9316 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/config/ControllerExceptionAdvice.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/config/ControllerExceptionAdvice.java @@ -50,7 +50,7 @@ public ResponseEntity handleOptimisticLockException(final OptimisticLock @ExceptionHandler(NoResourceFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) - public ResponseEntity handleNoResourceFoundException(final NotFoundException ex) { + public ResponseEntity handleNoResourceFoundException(final NoResourceFoundException ex) { LOG.error("NoResourceFoundException: " + ex.getMessage()); return notFound(); } diff --git a/backend/vkt/src/main/java/fi/oph/vkt/config/security/WebSecurityConfig.java b/backend/vkt/src/main/java/fi/oph/vkt/config/security/WebSecurityConfig.java index 4bca28468..cc3841ff8 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/config/security/WebSecurityConfig.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/config/security/WebSecurityConfig.java @@ -1,9 +1,10 @@ package fi.oph.vkt.config.security; import fi.oph.vkt.config.Constants; +import fi.oph.vkt.util.AuthorizationUtil; import fi.oph.vkt.util.OpintopolkuCasAuthenticationFilter; import fi.vm.sade.javautils.kayttooikeusclient.OphUserDetailsServiceImpl; -import org.apereo.cas.client.session.HashMapBackedSessionMappingStorage; +import java.util.Map; import org.apereo.cas.client.session.SessionMappingStorage; import org.apereo.cas.client.session.SingleSignOutFilter; import org.apereo.cas.client.validation.Cas20ProxyTicketValidator; @@ -14,6 +15,8 @@ import org.springframework.context.annotation.Profile; import org.springframework.core.env.Environment; import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.cas.ServiceProperties; import org.springframework.security.cas.authentication.CasAuthenticationProvider; import org.springframework.security.cas.web.CasAuthenticationEntryPoint; @@ -21,7 +24,9 @@ import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.core.Authentication; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.access.intercept.RequestAuthorizationContext; import org.springframework.security.web.csrf.CookieCsrfTokenRepository; import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @@ -151,11 +156,34 @@ public SecurityFilterChain filterChain( } public static HttpSecurity commonConfig(final HttpSecurity httpSecurity) throws Exception { + final AuthorizationManager examinerApiAuthorizationManager = + ( + (authenticationSupplier, object) -> { + Authentication authentication = authenticationSupplier.get(); + if (AuthorizationUtil.hasRole(authentication, Constants.APP_ADMIN_ROLE)) { + return new AuthorizationDecision(true); + } else if (AuthorizationUtil.hasRole(authentication, Constants.APP_TV_ROLE)) { + final Map requestVariables = object.getVariables(); + final String expectedOid = requestVariables.get("oid"); + if (expectedOid != null && expectedOid.equals(authentication.getName())) { + return new AuthorizationDecision(true); + } + } + return new AuthorizationDecision(false); + } + ); + return configCsrf(httpSecurity) .authorizeHttpRequests(registry -> registry + .requestMatchers("/api/v1/clerk/user") + .hasAnyRole(Constants.APP_ADMIN_ROLE, Constants.APP_TV_ROLE) .requestMatchers("/api/v1/clerk/**", "/virkailija/**", "/virkailija") - .hasRole(Constants.APP_ROLE) + .hasRole(Constants.APP_ADMIN_ROLE) + .requestMatchers("/api/v1/tv/{oid}/**") + .access(examinerApiAuthorizationManager) + .requestMatchers("/api/v1/tv/**", "/tv/**", "/tv") + .hasAnyRole(Constants.APP_ADMIN_ROLE, Constants.APP_TV_ROLE) .requestMatchers("/", "/**") .permitAll() .anyRequest() diff --git a/backend/vkt/src/main/java/fi/oph/vkt/config/security/WebSecurityConfigDev.java b/backend/vkt/src/main/java/fi/oph/vkt/config/security/WebSecurityConfigDev.java index 282906d3e..6055b6c0a 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/config/security/WebSecurityConfigDev.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/config/security/WebSecurityConfigDev.java @@ -1,46 +1,46 @@ package fi.oph.vkt.config.security; import fi.oph.vkt.config.Constants; -import java.util.Collection; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.Properties; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; +import java.util.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; -import org.springframework.core.log.LogMessage; -import org.springframework.security.access.AccessDeniedException; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.SpringSecurityCoreVersion; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsPasswordService; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.security.core.userdetails.memory.UserAttribute; -import org.springframework.security.core.userdetails.memory.UserAttributeEditor; -import org.springframework.security.provisioning.UserDetailsManager; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; -import org.springframework.util.Assert; @Profile("dev") @Configuration @EnableWebSecurity public class WebSecurityConfigDev { + private final Map usernameToOid = Map.of( + "clerk", + "1.2.246.562.10.00000000001", + "user", + "1.2.246.562.10.99999999991", + "tv1", + "1.2.246.562.10.10000000001", + "tv2", + "1.2.246.562.10.20000000002", + "tv3", + "1.2.246.562.10.30000000003", + "tv4", + "1.2.246.562.10.40000000004" + ); + + private static UserDetails getUserWithRole(final String username, final String role) { + return User.withDefaultPasswordEncoder().username(username).password(username).roles(role).build(); + } + private static final Logger LOG = LoggerFactory.getLogger(WebSecurityConfigDev.class); @Value("${dev.web.security.off:false}") @@ -82,213 +82,40 @@ public UserDetailsService userDetailsService() { if (devWebSecurityOff) { return new InMemoryUserDetailsManager(); } - final UserDetails user = User.withDefaultPasswordEncoder().username("user").password("user").roles("USER").build(); - - final UserDetails clerk = User - .withDefaultPasswordEncoder() - .username("clerk") - .password("clerk") - .roles(Constants.APP_ROLE) - .build(); + List usersList = List.of( + getUserWithRole("user", "USER"), + getUserWithRole("clerk", Constants.APP_ADMIN_ROLE), + getUserWithRole("tv1", Constants.APP_TV_ROLE), + getUserWithRole("tv2", Constants.APP_TV_ROLE), + getUserWithRole("tv3", Constants.APP_TV_ROLE), + getUserWithRole("tv4", Constants.APP_TV_ROLE) + ); // AuditUtil resolves current username as Oid, and will throw exception if username is not Oid. Therefore, let - // authenticated users to have Oid as username. - return new InMemoryUserDetailsManager(user, clerk) { + // authenticated users have Oid as username. + return new InMemoryUserDetailsManager(usersList) { @Override public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException { - final UserDetails user = this.users.get(username.toLowerCase()); - if (user == null) { + final Optional user = usersList + .stream() + .filter(u -> u.getUsername().equals(username.toLowerCase())) + .findFirst(); + if (user.isEmpty()) { throw new UsernameNotFoundException(username); + } else { + final UserDetails foundUser = user.get(); + final String oid = usernameToOid.get(foundUser.getUsername()); + return new User( + oid, + foundUser.getPassword(), + foundUser.isEnabled(), + foundUser.isAccountNonExpired(), + foundUser.isCredentialsNonExpired(), + foundUser.isAccountNonLocked(), + foundUser.getAuthorities() + ); } - final String oid = Objects.equals("clerk", username) - ? "1.2.246.562.10.00000000001" - : "1.2.246.562.10.99999999991"; - return new User( - oid, - user.getPassword(), - user.isEnabled(), - user.isAccountNonExpired(), - user.isCredentialsNonExpired(), - user.isAccountNonLocked(), - user.getAuthorities() - ); } }; } - - /** - * Below is copy paste of Spring InMemoryUserDetailsManager and related classes. Only change is that 'users' property is - * changed from private to protected. - */ - - private static class InMemoryUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService { - - protected final Log logger = LogFactory.getLog(getClass()); - - protected final Map users = new HashMap<>(); - - private AuthenticationManager authenticationManager; - - public InMemoryUserDetailsManager() {} - - public InMemoryUserDetailsManager(Collection users) { - for (UserDetails user : users) { - createUser(user); - } - } - - public InMemoryUserDetailsManager(UserDetails... users) { - for (UserDetails user : users) { - createUser(user); - } - } - - public InMemoryUserDetailsManager(Properties users) { - Enumeration names = users.propertyNames(); - UserAttributeEditor editor = new UserAttributeEditor(); - while (names.hasMoreElements()) { - String name = (String) names.nextElement(); - editor.setAsText(users.getProperty(name)); - UserAttribute attr = (UserAttribute) editor.getValue(); - createUser(createUserDetails(name, attr)); - } - } - - private User createUserDetails(String name, UserAttribute attr) { - return new User(name, attr.getPassword(), attr.isEnabled(), true, true, true, attr.getAuthorities()); - } - - @Override - public void createUser(UserDetails user) { - Assert.isTrue(!userExists(user.getUsername()), "user should not exist"); - this.users.put(user.getUsername().toLowerCase(), new MutableUser(user)); - } - - @Override - public void deleteUser(String username) { - this.users.remove(username.toLowerCase()); - } - - @Override - public void updateUser(UserDetails user) { - Assert.isTrue(userExists(user.getUsername()), "user should exist"); - this.users.put(user.getUsername().toLowerCase(), new MutableUser(user)); - } - - @Override - public boolean userExists(String username) { - return this.users.containsKey(username.toLowerCase()); - } - - @Override - public void changePassword(String oldPassword, String newPassword) { - Authentication currentUser = SecurityContextHolder.getContext().getAuthentication(); - if (currentUser == null) { - // This would indicate bad coding somewhere - throw new AccessDeniedException( - "Can't change password as no Authentication object found in context " + "for current user." - ); - } - String username = currentUser.getName(); - this.logger.debug(LogMessage.format("Changing password for user '%s'", username)); - // If an authentication manager has been set, re-authenticate the user with the - // supplied password. - if (this.authenticationManager != null) { - this.logger.debug(LogMessage.format("Reauthenticating user '%s' for password change request.", username)); - this.authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, oldPassword)); - } else { - this.logger.debug("No authentication manager set. Password won't be re-checked."); - } - MutableUserDetails user = this.users.get(username); - Assert.state(user != null, "Current user doesn't exist in database."); - user.setPassword(newPassword); - } - - @Override - public UserDetails updatePassword(UserDetails user, String newPassword) { - String username = user.getUsername(); - MutableUserDetails mutableUser = this.users.get(username.toLowerCase()); - mutableUser.setPassword(newPassword); - return mutableUser; - } - - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - UserDetails user = this.users.get(username.toLowerCase()); - if (user == null) { - throw new UsernameNotFoundException(username); - } - return new User( - user.getUsername(), - user.getPassword(), - user.isEnabled(), - user.isAccountNonExpired(), - user.isCredentialsNonExpired(), - user.isAccountNonLocked(), - user.getAuthorities() - ); - } - - public void setAuthenticationManager(AuthenticationManager authenticationManager) { - this.authenticationManager = authenticationManager; - } - } - - private interface MutableUserDetails extends UserDetails { - void setPassword(String password); - } - - private static class MutableUser implements MutableUserDetails { - - private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; - - private String password; - - private final UserDetails delegate; - - MutableUser(UserDetails user) { - this.delegate = user; - this.password = user.getPassword(); - } - - @Override - public String getPassword() { - return this.password; - } - - @Override - public void setPassword(String password) { - this.password = password; - } - - @Override - public Collection getAuthorities() { - return this.delegate.getAuthorities(); - } - - @Override - public String getUsername() { - return this.delegate.getUsername(); - } - - @Override - public boolean isAccountNonExpired() { - return this.delegate.isAccountNonExpired(); - } - - @Override - public boolean isAccountNonLocked() { - return this.delegate.isAccountNonLocked(); - } - - @Override - public boolean isCredentialsNonExpired() { - return this.delegate.isCredentialsNonExpired(); - } - - @Override - public boolean isEnabled() { - return this.delegate.isEnabled(); - } - } } diff --git a/backend/vkt/src/main/java/fi/oph/vkt/model/EmailType.java b/backend/vkt/src/main/java/fi/oph/vkt/model/EmailType.java index 19ad2e8e3..71143d13c 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/model/EmailType.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/model/EmailType.java @@ -3,4 +3,7 @@ public enum EmailType { ENROLLMENT_CONFIRMATION, ENROLLMENT_TO_QUEUE_CONFIRMATION, + ENROLLMENT_APPOINTMENT_AUTH_LINK, + ENROLLMENT_CONTACT_REQUEST, + ENROLLMENT_APPOINTMENT_CONFIRMATION, } diff --git a/backend/vkt/src/main/java/fi/oph/vkt/model/Enrollment.java b/backend/vkt/src/main/java/fi/oph/vkt/model/Enrollment.java index f02d1e44a..0bca1b717 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/model/Enrollment.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/model/Enrollment.java @@ -25,34 +25,13 @@ @Setter @Entity @Table(name = "enrollment") -public class Enrollment extends BaseEntity { +public class Enrollment extends EnrollmentCommon { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "enrollment_id", nullable = false) private long id; - @Column(name = "skill_oral", nullable = false) - private boolean oralSkill; - - @Column(name = "skill_textual", nullable = false) - private boolean textualSkill; - - @Column(name = "skill_understanding", nullable = false) - private boolean understandingSkill; - - @Column(name = "partial_exam_speaking", nullable = false) - private boolean speakingPartialExam; - - @Column(name = "partial_exam_speech_comprehension", nullable = false) - private boolean speechComprehensionPartialExam; - - @Column(name = "partial_exam_writing", nullable = false) - private boolean writingPartialExam; - - @Column(name = "partial_exam_reading_comprehension", nullable = false) - private boolean readingComprehensionPartialExam; - @Column(name = "status", nullable = false) @Enumerated(value = EnumType.STRING) private EnrollmentStatus status; @@ -95,10 +74,6 @@ public class Enrollment extends BaseEntity { @JoinColumn(name = "exam_event_id", referencedColumnName = "exam_event_id", nullable = false) private ExamEvent examEvent; - @ManyToOne(fetch = FetchType.LAZY, optional = false) - @JoinColumn(name = "person_id", referencedColumnName = "person_id", nullable = false) - private Person person; - @OneToMany(mappedBy = "enrollment") private List payments = new ArrayList<>(); 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..4837c23f1 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/model/EnrollmentAppointment.java @@ -0,0 +1,106 @@ +package fi.oph.vkt.model; + +import fi.oph.vkt.model.type.EnrollmentAppointmentStatus; +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_appointment") +public class EnrollmentAppointment extends EnrollmentCommon { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "enrollment_appointment_id", nullable = false) + private long id; + + @Column(name = "status", nullable = false) + @Enumerated(value = EnumType.STRING) + private EnrollmentAppointmentStatus status; + + @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; + + @Column(name = "first_name") + private String firstName; + + @Column(name = "last_name") + private String lastName; + + @Column(name = "partial_exam_selection") + private String partialExamSelection; + + @Column(name = "has_previous_enrollment", nullable = false) + private boolean hasPreviousEnrollment; + + @Column(name = "previous_enrollment") + private String previousEnrollment; + + @Column(name = "message") + private String message; + + @Size(max = 255) + @Column(name = "payment_link_hash", unique = true) + private String paymentLinkHash; + + @Size(max = 255) + @Column(name = "auth_hash", unique = true) + private String authHash; + + @Column(name = "auth_hash_expires", nullable = false) + private LocalDateTime expiresAt; + + @Column(name = "auth_hash_sent", nullable = false) + private LocalDateTime sentAt; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "grade_id", referencedColumnName = "grade_id") + private EnrollmentGrade grade; + + @OneToMany(mappedBy = "enrollmentAppointment") + private List payments = new ArrayList<>(); + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "examiner_id", referencedColumnName = "examiner_id") + private Examiner examiner; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "examiner_exam_event_id", referencedColumnName = "examiner_exam_event_id") + private ExaminerExamEvent examinerExamEvent; +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/model/EnrollmentCommon.java b/backend/vkt/src/main/java/fi/oph/vkt/model/EnrollmentCommon.java new file mode 100644 index 000000000..801e7825f --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/model/EnrollmentCommon.java @@ -0,0 +1,40 @@ +package fi.oph.vkt.model; + +import jakarta.persistence.Column; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@MappedSuperclass +public class EnrollmentCommon extends BaseEntity { + + @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; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "person_id", referencedColumnName = "person_id", nullable = false) + private Person person; +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/model/EnrollmentGrade.java b/backend/vkt/src/main/java/fi/oph/vkt/model/EnrollmentGrade.java new file mode 100644 index 000000000..b01db18cb --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/model/EnrollmentGrade.java @@ -0,0 +1,51 @@ +package fi.oph.vkt.model; + +import fi.oph.vkt.model.type.EnrollmentGradeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +@Table(name = "enrollment_grade") +public class EnrollmentGrade extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "grade_id", nullable = false) + private long id; + + @Column(name = "speaking_grade") + private EnrollmentGradeType speakingPartialExamGrade; + + @Column(name = "speech_comprehension_grade") + private EnrollmentGradeType speechComprehensionPartialExamGrade; + + @Column(name = "writing_grade") + private EnrollmentGradeType writingPartialExamGrade; + + @Column(name = "comprehension_grade") + private EnrollmentGradeType readingComprehensionPartialExamGrade; + + @Column(name = "speaking_comment") + private String speakingPartialExamComment; + + @Column(name = "speech_comprehension_comment") + private String speechComprehensionPartialExamComment; + + @Column(name = "writing_comment") + private String writingPartialExamComment; + + @Column(name = "comprehension_comment") + private String readingComprehensionPartialExamComment; + + @OneToOne(mappedBy = "grade") + private EnrollmentAppointment enrollmentAppointment; +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/model/ExamEvent.java b/backend/vkt/src/main/java/fi/oph/vkt/model/ExamEvent.java index 129cfef49..c69a99308 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/model/ExamEvent.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/model/ExamEvent.java @@ -2,16 +2,7 @@ import fi.oph.vkt.model.type.ExamLanguage; import fi.oph.vkt.model.type.ExamLevel; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.OneToMany; -import jakarta.persistence.OrderBy; -import jakarta.persistence.Table; +import jakarta.persistence.*; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; @@ -23,33 +14,23 @@ @Setter @Entity @Table(name = "exam_event") -public class ExamEvent extends BaseEntity { +public class ExamEvent extends ExamEventCommon { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "exam_event_id", nullable = false) private long id; - @Column(name = "language", nullable = false) - @Enumerated(value = EnumType.STRING) - private ExamLanguage language; - @Column(name = "level", nullable = false) @Enumerated(value = EnumType.STRING) private ExamLevel level; - @Column(name = "date", nullable = false) - private LocalDate date; - @Column(name = "registration_opens", nullable = false) private LocalDateTime registrationOpens; @Column(name = "registration_closes", nullable = false) private LocalDateTime registrationCloses; - @Column(name = "is_hidden", nullable = false) - private boolean isHidden; - @Column(name = "max_participants", nullable = false) private long maxParticipants; diff --git a/backend/vkt/src/main/java/fi/oph/vkt/model/ExamEventCommon.java b/backend/vkt/src/main/java/fi/oph/vkt/model/ExamEventCommon.java new file mode 100644 index 000000000..f4d439368 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/model/ExamEventCommon.java @@ -0,0 +1,26 @@ +package fi.oph.vkt.model; + +import fi.oph.vkt.model.type.ExamLanguage; +import jakarta.persistence.Column; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.MappedSuperclass; +import java.time.LocalDate; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@MappedSuperclass +public class ExamEventCommon extends BaseEntity { + + @Column(name = "language", nullable = false) + @Enumerated(value = EnumType.STRING) + private ExamLanguage language; + + @Column(name = "date", nullable = false) + private LocalDate date; + + @Column(name = "is_hidden", nullable = false) + private boolean isHidden; +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/model/Examiner.java b/backend/vkt/src/main/java/fi/oph/vkt/model/Examiner.java new file mode 100644 index 000000000..b83a613a1 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/model/Examiner.java @@ -0,0 +1,61 @@ +package fi.oph.vkt.model; + +import jakarta.persistence.*; +import jakarta.validation.constraints.Size; +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +@Table(name = "examiner") +public class Examiner extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "examiner_id", nullable = false) + private long id; + + @Size(max = 255) + @Column(name = "oid", unique = true, nullable = false) + private String oid; + + @Size(max = 255) + @Column(name = "email", nullable = false) + private String email; + + @Size(max = 255) + @Column(name = "phone_number", nullable = false) + private String phoneNumber; + + @Column(name = "last_name", nullable = false) + private String lastName; + + @Column(name = "first_name", nullable = false) + private String firstName; + + @Column(name = "nickname", nullable = false) + private String nickname; + + @Column(name = "exam_language_finnish", nullable = false) + private boolean examLanguageFinnish; + + @Column(name = "exam_language_swedish", nullable = false) + private boolean examLanguageSwedish; + + @OneToMany(mappedBy = "examiner") + private List examEvents = new ArrayList<>(); + + @Column(name = "is_public", nullable = false) + private boolean isPublic; + + @ManyToMany + @JoinTable( + name = "examiner_municipality", + joinColumns = @JoinColumn(name = "examiner_id", referencedColumnName = "examiner_id"), + inverseJoinColumns = @JoinColumn(name = "municipality_id") + ) + private List municipalities = new ArrayList<>(); +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/model/ExaminerExamEvent.java b/backend/vkt/src/main/java/fi/oph/vkt/model/ExaminerExamEvent.java new file mode 100644 index 000000000..1f7d04b3a --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/model/ExaminerExamEvent.java @@ -0,0 +1,46 @@ +package fi.oph.vkt.model; + +import jakarta.persistence.*; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +@Table(name = "examiner_exam_event") +public class ExaminerExamEvent extends ExamEventCommon { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "examiner_exam_event_id", nullable = false) + private long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "examiner_id", referencedColumnName = "examiner_id") + private Examiner examiner; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "municipality_id", referencedColumnName = "municipality_id", nullable = false) + private Municipality municipality; + + @Column(name = "registration_closes") + private LocalDate registrationCloses; + + @Column(name = "max_participants") + private Long maxParticipants; + + @Column(name = "location") + private String location; + + @Column(name = "other_information") + private String otherInformation; + + @Column(name = "exam_time") + private String examTime; + + @OneToMany(mappedBy = "examinerExamEvent") + private List enrollments = new ArrayList<>(); +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/model/FeatureFlag.java b/backend/vkt/src/main/java/fi/oph/vkt/model/FeatureFlag.java index 4a95dbaed..0f58cd21c 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/model/FeatureFlag.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/model/FeatureFlag.java @@ -1,7 +1,8 @@ package fi.oph.vkt.model; public enum FeatureFlag { - FREE_ENROLLMENT_FOR_HIGHEST_LEVEL_ALLOWED("freeEnrollmentAllowed"); + FREE_ENROLLMENT_FOR_HIGHEST_LEVEL_ALLOWED("freeEnrollmentAllowed"), + GOOD_AND_SATISFACTORY_LEVEL("goodAndSatisfactoryLevel"); private final String propertyKey; diff --git a/backend/vkt/src/main/java/fi/oph/vkt/model/Municipality.java b/backend/vkt/src/main/java/fi/oph/vkt/model/Municipality.java new file mode 100644 index 000000000..536d9c3ab --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/model/Municipality.java @@ -0,0 +1,31 @@ +package fi.oph.vkt.model; + +import jakarta.persistence.*; +import java.util.List; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +@Table(name = "municipality") +public class Municipality extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "municipality_id", nullable = false) + private long id; + + // Code should match the koodiArvo of an entry in koodisto + @Column(name = "code", nullable = false, unique = true) + private String code; + + @Column(name = "name_fi", nullable = false) + private String nameFI; + + @Column(name = "name_sv", nullable = false) + private String nameSV; + + @ManyToMany(fetch = FetchType.LAZY, mappedBy = "municipalities") + private List examiners; +} 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/EnrollmentAppointmentStatus.java b/backend/vkt/src/main/java/fi/oph/vkt/model/type/EnrollmentAppointmentStatus.java new file mode 100644 index 000000000..ed480f00e --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/model/type/EnrollmentAppointmentStatus.java @@ -0,0 +1,11 @@ +package fi.oph.vkt.model.type; + +public enum EnrollmentAppointmentStatus { + COMPLETED, + CANCELED, + EXPECTING_PAYMENT, + WAITING_AUTHENTICATION, + CANCELED_PAYMENT, + ENROLLMENT_CREATED, + CONTACT_CREATED, +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/model/type/EnrollmentGradeType.java b/backend/vkt/src/main/java/fi/oph/vkt/model/type/EnrollmentGradeType.java new file mode 100644 index 000000000..a98dade47 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/model/type/EnrollmentGradeType.java @@ -0,0 +1,7 @@ +package fi.oph.vkt.model.type; + +public enum EnrollmentGradeType { + GOOD, + SATISFACTORY, + FAILED, +} 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/model/type/ExamLevel.java b/backend/vkt/src/main/java/fi/oph/vkt/model/type/ExamLevel.java index 929d5ee90..d2011bb9a 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/model/type/ExamLevel.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/model/type/ExamLevel.java @@ -2,4 +2,5 @@ public enum ExamLevel { EXCELLENT, + GOOD_AND_SATISFACTORY, } 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..7ab446638 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/repository/EnrollmentAppointmentRepository.java @@ -0,0 +1,39 @@ +package fi.oph.vkt.repository; + +import fi.oph.vkt.model.EnrollmentAppointment; +import fi.oph.vkt.model.Examiner; +import fi.oph.vkt.model.Person; +import fi.oph.vkt.model.type.EnrollmentAppointmentStatus; +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 findByIdAndAuthHashAndDeletedAtIsNull(final long id, final String authHash); + Optional findByIdAndPaymentLinkHashAndDeletedAtIsNull( + final long id, + final String paymentLinkHash + ); + + @Query( + "SELECT e" + + " FROM EnrollmentAppointment e" + + " WHERE e.examiner = ?1" + + " AND e.status IN (fi.oph.vkt.model.type.EnrollmentAppointmentStatus.CONTACT_CREATED, fi.oph.vkt.model.type.EnrollmentAppointmentStatus.WAITING_AUTHENTICATION)" + + " AND e.deletedAt IS NULL" + + " ORDER BY e.createdAt DESC" + ) + List findExaminerContactRequests(final Examiner examiner); + + @Query( + "SELECT e" + + " FROM EnrollmentAppointment e" + + " WHERE e.person = ?1" + + " AND e.status IN (fi.oph.vkt.model.type.EnrollmentAppointmentStatus.COMPLETED)" + + " AND e.deletedAt IS NULL" + + " ORDER BY e.createdAt DESC" + ) + List findPersonEnrollmentHistory(final Person person); +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/repository/EnrollmentGradesRepository.java b/backend/vkt/src/main/java/fi/oph/vkt/repository/EnrollmentGradesRepository.java new file mode 100644 index 000000000..8265a35cd --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/repository/EnrollmentGradesRepository.java @@ -0,0 +1,11 @@ +package fi.oph.vkt.repository; + +import fi.oph.vkt.model.EnrollmentAppointment; +import fi.oph.vkt.model.EnrollmentGrade; +import java.util.Optional; +import org.springframework.stereotype.Repository; + +@Repository +public interface EnrollmentGradesRepository extends BaseRepository { + Optional findByEnrollmentAppointment(final EnrollmentAppointment enrollmentAppointment); +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/repository/ExamEventRepository.java b/backend/vkt/src/main/java/fi/oph/vkt/repository/ExamEventRepository.java index 4c2ef5653..0771cb8c3 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/repository/ExamEventRepository.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/repository/ExamEventRepository.java @@ -37,17 +37,19 @@ public interface ExamEventRepository extends BaseRepository { "SELECT e.id" + " FROM ExamEvent e" + " LEFT JOIN e.enrollments en ON en.status = 'QUEUED'" + + " WHERE e.level = ?1" + " GROUP BY e.id" + " HAVING COUNT(en) > 0" ) - Set listClertExamEventIdsWithQueue(); + Set listClerkExamEventIdsWithQueue(final ExamLevel level); @Query( "SELECT new fi.oph.vkt.repository.ClerkExamEventProjection(e.id, e.language, e.level, e.date," + " e.registrationCloses, e.registrationOpens, COUNT(en), e.maxParticipants, e.isHidden, COUNT(en.id) filter (where en.status = 'AWAITING_APPROVAL'))" + " FROM ExamEvent e" + " LEFT JOIN e.enrollments en ON en.status = 'COMPLETED' OR en.status = 'AWAITING_PAYMENT' OR en.status = 'AWAITING_APPROVAL' OR en.status = 'EXPECTING_PAYMENT_UNFINISHED_ENROLLMENT'" + + " WHERE e.level = ?1" + " GROUP BY e.id" ) - List listClerkExamEventProjections(); + List listClerkExamEventProjections(final ExamLevel level); } diff --git a/backend/vkt/src/main/java/fi/oph/vkt/repository/ExaminerExamEventRepository.java b/backend/vkt/src/main/java/fi/oph/vkt/repository/ExaminerExamEventRepository.java new file mode 100644 index 000000000..1fe12309b --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/repository/ExaminerExamEventRepository.java @@ -0,0 +1,14 @@ +package fi.oph.vkt.repository; + +import fi.oph.vkt.api.dto.clerk.ClerkExamEventListDTO; +import fi.oph.vkt.model.Examiner; +import fi.oph.vkt.model.ExaminerExamEvent; +import java.util.List; +import java.util.Optional; +import org.springframework.stereotype.Repository; + +@Repository +public interface ExaminerExamEventRepository extends BaseRepository { + Optional findById(long id); + List findAllByExaminer(Examiner examiner); +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/repository/ExaminerRepository.java b/backend/vkt/src/main/java/fi/oph/vkt/repository/ExaminerRepository.java new file mode 100644 index 000000000..4130b1ce2 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/repository/ExaminerRepository.java @@ -0,0 +1,18 @@ +package fi.oph.vkt.repository; + +import fi.oph.vkt.model.Examiner; +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +@Repository +public interface ExaminerRepository extends BaseRepository { + List getAllByDeletedAtIsNullAndIsPublicIsTrue(); + List getAllByDeletedAtIsNull(); + Examiner getByOid(String oid); + Optional findByOid(String oid); + + @Query("SELECT e.oid FROM Examiner e WHERE e.deletedAt IS NULL") + List listExistingOnrIds(); +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/repository/MunicipalityRepository.java b/backend/vkt/src/main/java/fi/oph/vkt/repository/MunicipalityRepository.java new file mode 100644 index 000000000..0a4a9b53c --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/repository/MunicipalityRepository.java @@ -0,0 +1,10 @@ +package fi.oph.vkt.repository; + +import fi.oph.vkt.model.Municipality; +import java.util.Optional; +import org.springframework.stereotype.Repository; + +@Repository +public interface MunicipalityRepository extends BaseRepository { + Optional findByCode(String code); +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/scheduled/OnrPersonalDataUpdater.java b/backend/vkt/src/main/java/fi/oph/vkt/scheduled/OnrPersonalDataUpdater.java new file mode 100644 index 000000000..55e95cf41 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/scheduled/OnrPersonalDataUpdater.java @@ -0,0 +1,34 @@ +package fi.oph.vkt.scheduled; + +import fi.oph.vkt.service.ExaminerDetailsService; +import fi.oph.vkt.util.SchedulingUtil; +import jakarta.annotation.Resource; +import lombok.RequiredArgsConstructor; +import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class OnrPersonalDataUpdater { + + private static final Logger LOG = LoggerFactory.getLogger(OnrPersonalDataUpdater.class); + private static final String INITIAL_DELAY = "PT0S"; + private static final String FIXED_DELAY = "PT5M"; + private static final String LOCK_AT_LEAST = "PT0S"; + private static final String LOCK_AT_MOST = "PT3M"; + + @Resource + private final ExaminerDetailsService examinerDetailsService; + + @Scheduled(initialDelayString = INITIAL_DELAY, fixedDelayString = FIXED_DELAY) + @SchedulerLock(name = "updatePersonalDataFromOnr", lockAtLeastFor = LOCK_AT_LEAST, lockAtMostFor = LOCK_AT_MOST) + public void updateOnrCache() { + SchedulingUtil.runWithScheduledUser(() -> { + LOG.debug("updatePersonalDataFromOnr"); + examinerDetailsService.updateStoredPersonalData(); + }); + } +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/AbstractEnrollmentEmailService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/AbstractEnrollmentEmailService.java new file mode 100644 index 000000000..75120b184 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/AbstractEnrollmentEmailService.java @@ -0,0 +1,117 @@ +package fi.oph.vkt.service; + +import static fi.oph.vkt.util.LocalisationUtil.localeFI; +import static fi.oph.vkt.util.LocalisationUtil.localeSV; + +import fi.oph.vkt.model.EmailType; +import fi.oph.vkt.model.Enrollment; +import fi.oph.vkt.model.EnrollmentAppointment; +import fi.oph.vkt.model.EnrollmentCommon; +import fi.oph.vkt.model.ExamEvent; +import fi.oph.vkt.model.ExamEventCommon; +import fi.oph.vkt.model.ExaminerExamEvent; +import fi.oph.vkt.model.type.ExamLanguage; +import fi.oph.vkt.service.email.EmailAttachmentData; +import fi.oph.vkt.service.email.EmailData; +import fi.oph.vkt.service.email.EmailService; +import fi.oph.vkt.util.LocalisationUtil; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class AbstractEnrollmentEmailService { + + protected void createEmail( + final EmailService emailService, + final String recipientName, + final String recipientAddress, + final String subject, + final String body, + final List attachments, + final EmailType emailType + ) { + final EmailData emailData = EmailData + .builder() + .recipientName(recipientName) + .recipientAddress(recipientAddress) + .subject(subject) + .body(body) + .attachments(attachments) + .build(); + + emailService.saveEmail(emailType, emailData); + } + + protected Map getEmailParams(final EnrollmentCommon enrollment, final ExamEventCommon examEvent) { + final Map params = new HashMap<>(Map.of()); + + if (examEvent.getLanguage() == ExamLanguage.FI) { + params.put("examLanguageFI", LocalisationUtil.translate(localeFI, "lang.finnish")); + params.put("examLanguageSV", LocalisationUtil.translate(localeSV, "lang.finnish")); + } else { + params.put("examLanguageFI", LocalisationUtil.translate(localeFI, "lang.swedish")); + params.put("examLanguageSV", LocalisationUtil.translate(localeSV, "lang.swedish")); + } + + params.put("skillsFI", getEmailParamSkills(enrollment, localeFI, params.get("examLanguageFI"))); + params.put("skillsSV", getEmailParamSkills(enrollment, localeSV, params.get("examLanguageSV"))); + + params.put("partialExamsFI", getEmailParamPartialExams(enrollment, localeFI)); + params.put("partialExamsSV", getEmailParamPartialExams(enrollment, localeSV)); + + params.put( + "examLevelFI", + LocalisationUtil.translate( + localeFI, + enrollment instanceof EnrollmentAppointment ? "examLevel.goodAndSatisfactory" : "examLevel.excellent" + ) + ); + params.put( + "examLevelSV", + LocalisationUtil.translate( + localeSV, + enrollment instanceof EnrollmentAppointment ? "examLevel.goodAndSatisfactory" : "examLevel.excellent" + ) + ); + + params.put("examDate", examEvent.getDate().format(DateTimeFormatter.ofPattern("dd.MM.yyyy"))); + + params.put("type", "enrollment"); + params.put("isFree", false); + + return params; + } + + private String getEmailParamSkills(final EnrollmentCommon enrollment, final Locale locale, final Object... args) { + return joinNonEmptyStrings( + Stream.of( + enrollment.isTextualSkill() ? LocalisationUtil.translate(locale, "skill.textual", args) : "", + enrollment.isOralSkill() ? LocalisationUtil.translate(locale, "skill.oral", args) : "", + enrollment.isUnderstandingSkill() ? LocalisationUtil.translate(locale, "skill.understanding", args) : "" + ) + ); + } + + private String getEmailParamPartialExams(final EnrollmentCommon enrollment, final Locale locale) { + return joinNonEmptyStrings( + Stream.of( + enrollment.isWritingPartialExam() ? LocalisationUtil.translate(locale, "partialExam.writing") : "", + enrollment.isReadingComprehensionPartialExam() + ? LocalisationUtil.translate(locale, "partialExam.readingComprehension") + : "", + enrollment.isSpeakingPartialExam() ? LocalisationUtil.translate(locale, "partialExam.speaking") : "", + enrollment.isSpeechComprehensionPartialExam() + ? LocalisationUtil.translate(locale, "partialExam.speechComprehension") + : "" + ) + ); + } + + private String joinNonEmptyStrings(final Stream stream) { + return stream.filter(s -> !s.isEmpty()).collect(Collectors.joining(", ")); + } +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/AbstractEnrollmentService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/AbstractEnrollmentService.java index 683a35699..d64c74024 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/service/AbstractEnrollmentService.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/AbstractEnrollmentService.java @@ -1,7 +1,12 @@ package fi.oph.vkt.service; import fi.oph.vkt.api.dto.EnrollmentDTOCommonFields; +import fi.oph.vkt.api.dto.EnrollmentDTOSkillFields; +import fi.oph.vkt.api.dto.PublicEnrollmentContactCreateDTO; +import fi.oph.vkt.api.dto.examiner.ExaminerEnrollmentAppointmentUpdateDTO; import fi.oph.vkt.model.Enrollment; +import fi.oph.vkt.model.EnrollmentAppointment; +import fi.oph.vkt.model.EnrollmentCommon; import fi.oph.vkt.model.ExamEvent; import fi.oph.vkt.model.Person; import fi.oph.vkt.repository.EnrollmentRepository; @@ -9,7 +14,7 @@ public abstract class AbstractEnrollmentService { - protected void copyDtoFieldsToEnrollment(final Enrollment enrollment, final EnrollmentDTOCommonFields dto) { + protected void copyDtoSkillFieldsToEnrollment(final EnrollmentCommon enrollment, final EnrollmentDTOSkillFields dto) { enrollment.setOralSkill(dto.oralSkill()); enrollment.setTextualSkill(dto.textualSkill()); enrollment.setUnderstandingSkill(dto.understandingSkill()); @@ -17,6 +22,36 @@ protected void copyDtoFieldsToEnrollment(final Enrollment enrollment, final Enro enrollment.setSpeechComprehensionPartialExam(dto.speechComprehensionPartialExam()); enrollment.setWritingPartialExam(dto.writingPartialExam()); enrollment.setReadingComprehensionPartialExam(dto.readingComprehensionPartialExam()); + } + + protected void copyDtoFieldsToEnrollment( + final EnrollmentAppointment enrollment, + final ExaminerEnrollmentAppointmentUpdateDTO dto + ) { + copyDtoSkillFieldsToEnrollment(enrollment, dto); + enrollment.setEmail(dto.email()); + enrollment.setPhoneNumber(dto.phoneNumber()); + enrollment.setStreet(dto.street()); + enrollment.setPostalCode(dto.postalCode()); + enrollment.setTown(dto.town()); + enrollment.setCountry(dto.country()); + } + + protected void copyDtoFieldsToEnrollment( + final EnrollmentAppointment enrollment, + final PublicEnrollmentContactCreateDTO dto + ) { + enrollment.setPartialExamSelection(dto.partialExamSelection()); + enrollment.setHasPreviousEnrollment(dto.hasPreviousEnrollment()); + enrollment.setMessage(dto.message()); + enrollment.setPhoneNumber(dto.phoneNumber()); + enrollment.setEmail(dto.email()); + enrollment.setFirstName(dto.firstName()); + enrollment.setLastName(dto.lastName()); + } + + protected void copyDtoFieldsToEnrollment(final Enrollment enrollment, final EnrollmentDTOCommonFields dto) { + copyDtoSkillFieldsToEnrollment(enrollment, dto); enrollment.setPreviousEnrollment(dto.previousEnrollment()); enrollment.setDigitalCertificateConsent(dto.digitalCertificateConsent()); enrollment.setEmail(dto.email()); diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/ClerkEnrollmentService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/ClerkEnrollmentService.java index 68dd940b4..ddfc7efe4 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/service/ClerkEnrollmentService.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/ClerkEnrollmentService.java @@ -90,7 +90,9 @@ public ClerkEnrollmentDTO update(final ClerkEnrollmentUpdateDTO dto) { final ClerkEnrollmentAuditDTO newAuditDto = ClerkEnrollmentUtil.createClerkEnrollmentAuditDTO(enrollment); auditService.logUpdate(VktOperation.UPDATE_ENROLLMENT, enrollment.getId(), oldAuditDto, newAuditDto); - FreeEnrollmentDetails freeEnrollmentDetails = enrollmentRepository.countEnrollmentsByPerson(enrollment.getPerson()); + final FreeEnrollmentDetails freeEnrollmentDetails = enrollmentRepository.countEnrollmentsByPerson( + enrollment.getPerson() + ); return ClerkEnrollmentUtil.createClerkEnrollmentDTO( enrollmentRepository.getReferenceById(enrollment.getId()), freeEnrollmentDetails @@ -109,7 +111,9 @@ public ClerkEnrollmentDTO changeStatus(final ClerkEnrollmentStatusChangeDTO dto) final ClerkEnrollmentAuditDTO newAuditDto = ClerkEnrollmentUtil.createClerkEnrollmentAuditDTO(enrollment); auditService.logUpdate(VktOperation.UPDATE_ENROLLMENT_STATUS, enrollment.getId(), oldAuditDto, newAuditDto); - FreeEnrollmentDetails freeEnrollmentDetails = enrollmentRepository.countEnrollmentsByPerson(enrollment.getPerson()); + final FreeEnrollmentDetails freeEnrollmentDetails = enrollmentRepository.countEnrollmentsByPerson( + enrollment.getPerson() + ); return ClerkEnrollmentUtil.createClerkEnrollmentDTO( enrollmentRepository.getReferenceById(enrollment.getId()), freeEnrollmentDetails @@ -136,7 +140,9 @@ public ClerkEnrollmentDTO move(final ClerkEnrollmentMoveDTO dto) { final ClerkEnrollmentAuditDTO newAuditDto = ClerkEnrollmentUtil.createClerkEnrollmentAuditDTO(enrollment); auditService.logUpdate(VktOperation.MOVE_ENROLLMENT, enrollment.getId(), oldAuditDto, newAuditDto); - FreeEnrollmentDetails freeEnrollmentDetails = enrollmentRepository.countEnrollmentsByPerson(enrollment.getPerson()); + final FreeEnrollmentDetails freeEnrollmentDetails = enrollmentRepository.countEnrollmentsByPerson( + enrollment.getPerson() + ); return ClerkEnrollmentUtil.createClerkEnrollmentDTO( enrollmentRepository.getReferenceById(enrollment.getId()), freeEnrollmentDetails diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/ClerkExamEventService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/ClerkExamEventService.java index 8d6f0b5e8..67b57b0a0 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/service/ClerkExamEventService.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/ClerkExamEventService.java @@ -12,6 +12,7 @@ import fi.oph.vkt.audit.dto.ClerkExamEventAuditDTO; import fi.oph.vkt.model.Enrollment; import fi.oph.vkt.model.ExamEvent; +import fi.oph.vkt.model.type.ExamLevel; import fi.oph.vkt.repository.ClerkExamEventProjection; import fi.oph.vkt.repository.EnrollmentRepository; import fi.oph.vkt.repository.ExamEventRepository; @@ -43,9 +44,11 @@ public class ClerkExamEventService { private final AuditService auditService; @Transactional(readOnly = true) - public List list() { - final List examEventProjections = examEventRepository.listClerkExamEventProjections(); - final Set examEventIdsHavingQueue = examEventRepository.listClertExamEventIdsWithQueue(); + public List list(final ExamLevel level) { + final List examEventProjections = examEventRepository.listClerkExamEventProjections( + level + ); + final Set examEventIdsHavingQueue = examEventRepository.listClerkExamEventIdsWithQueue(level); final List examEventListDTOs = examEventProjections .stream() @@ -125,7 +128,7 @@ public ClerkExamEventDTO createExamEvent(final ClerkExamEventCreateDTO dto) { try { examEventRepository.saveAndFlush(examEvent); } catch (final DataIntegrityViolationException ex) { - if (DataIntegrityViolationExceptionUtil.isExamEventLanguageLevelDateUniquenessException(ex)) { + if (DataIntegrityViolationExceptionUtil.isExamEventLanguageLevelDateExaminerUniquenessException(ex)) { throw new APIException(APIExceptionType.EXAM_EVENT_DUPLICATE); } throw ex; @@ -148,7 +151,7 @@ public ClerkExamEventDTO updateExamEvent(final ClerkExamEventUpdateDTO dto) { try { examEventRepository.flush(); } catch (final DataIntegrityViolationException ex) { - if (DataIntegrityViolationExceptionUtil.isExamEventLanguageLevelDateUniquenessException(ex)) { + if (DataIntegrityViolationExceptionUtil.isExamEventLanguageLevelDateExaminerUniquenessException(ex)) { throw new APIException(APIExceptionType.EXAM_EVENT_DUPLICATE); } throw ex; diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/ClerkExaminerService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/ClerkExaminerService.java new file mode 100644 index 000000000..d94c44657 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/ClerkExaminerService.java @@ -0,0 +1,33 @@ +package fi.oph.vkt.service; + +import fi.oph.vkt.api.dto.examiner.ExaminerDetailsDTO; +import fi.oph.vkt.audit.AuditService; +import fi.oph.vkt.repository.ExaminerRepository; +import fi.oph.vkt.util.ExaminerUtil; +import java.util.List; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class ClerkExaminerService { + + private final ExaminerRepository examinerRepository; + private final AuditService auditService; + private final Environment environment; + + @Transactional(readOnly = true) + public List listExaminers() { + final String baseUrlAPI = environment.getRequiredProperty("app.base-url.api"); + + // TODO Audit log entry + return examinerRepository + .getAllByDeletedAtIsNull() + .stream() + .map(e -> ExaminerUtil.toExaminerDetailsDTO(e, List.of(), baseUrlAPI)) + .collect(Collectors.toList()); + } +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/ContactEmailService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/ContactEmailService.java new file mode 100644 index 000000000..5817cbc18 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/ContactEmailService.java @@ -0,0 +1,98 @@ +package fi.oph.vkt.service; + +import static fi.oph.vkt.util.LocalisationUtil.localeFI; +import static fi.oph.vkt.util.LocalisationUtil.localeSV; + +import fi.oph.vkt.model.EmailType; +import fi.oph.vkt.model.EnrollmentAppointment; +import fi.oph.vkt.model.Examiner; +import fi.oph.vkt.service.email.EmailService; +import fi.oph.vkt.util.LocalisationUtil; +import fi.oph.vkt.util.TemplateRenderer; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class ContactEmailService extends AbstractEnrollmentEmailService { + + private final EmailService emailService; + private final TemplateRenderer templateRenderer; + private final Environment environment; + + @Transactional + public void sendReceiptNotificationForContactRequest(final EnrollmentAppointment enrollment) + throws IOException, InterruptedException { + final Map templateParams = new HashMap<>(Map.of()); + final Examiner examiner = enrollment.getExaminer(); + final String examinerName = examiner.getNickname() + " " + examiner.getLastName(); + templateParams.put("examinerName", examinerName); + templateParams.put("message", enrollment.getMessage()); + final String recipientName = enrollment.getFirstName() + " " + enrollment.getLastName(); + final String recipientAddress = enrollment.getEmail(); + templateParams.put("name", recipientName); + templateParams.put("email", recipientAddress); + + // TODO Translate to Swedish + final String subject = String.format( + "%s", + LocalisationUtil.translate(localeFI, "subject.contact-request.receipt-notification") + ); + final String body = templateRenderer.renderContactRequestReceiptNotification(templateParams); + createEmail( + emailService, + recipientName, + recipientAddress, + subject, + body, + List.of(), + EmailType.ENROLLMENT_CONTACT_REQUEST + ); + } + + @Transactional + public void sendExaminerNotificationOfContactRequest(final EnrollmentAppointment enrollment) + throws IOException, InterruptedException { + final Map templateParams = new HashMap<>(Map.of()); + final Examiner examiner = enrollment.getExaminer(); + final String clerkBaseUrl = environment.getRequiredProperty("app.base-url.clerk"); + final String contactRequestURL = String.format( + clerkBaseUrl + "/tv/%s/yhteydenottopyynto/%s", + examiner.getOid(), + enrollment.getId() + ); + + final String requesterName = enrollment.getFirstName() + " " + enrollment.getLastName(); + final String requesterEmail = enrollment.getEmail(); + templateParams.put("requesterName", requesterName); + templateParams.put("requesterEmail", requesterEmail); + templateParams.put("message", enrollment.getMessage()); + templateParams.put("contactRequestURL", contactRequestURL); + + final String recipientName = examiner.getFirstName() + " " + examiner.getLastName(); + final String recipientAddress = examiner.getEmail(); + // TODO Translate to Swedish + final String subject = String.format( + "%s | %s", + LocalisationUtil.translate(localeFI, "subject.contact-request.notice-for-examiner"), + LocalisationUtil.translate(localeSV, "subject.contact-request.notice-for-examiner") + ); + final String body = templateRenderer.renderContactRequestNoticeForExaminer(templateParams); + + createEmail( + emailService, + recipientName, + recipientAddress, + subject, + body, + List.of(), + EmailType.ENROLLMENT_CONTACT_REQUEST + ); + } +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/ExaminerDetailsService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/ExaminerDetailsService.java new file mode 100644 index 000000000..5455705f6 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/ExaminerDetailsService.java @@ -0,0 +1,126 @@ +package fi.oph.vkt.service; + +import fi.oph.vkt.api.dto.examiner.ExaminerDetailsDTO; +import fi.oph.vkt.api.dto.examiner.ExaminerDetailsInitDTO; +import fi.oph.vkt.api.dto.examiner.ExaminerDetailsUpsertDTO; +import fi.oph.vkt.audit.AuditService; +import fi.oph.vkt.audit.VktOperation; +import fi.oph.vkt.model.EnrollmentAppointment; +import fi.oph.vkt.model.Examiner; +import fi.oph.vkt.model.type.EnrollmentAppointmentStatus; +import fi.oph.vkt.repository.EnrollmentAppointmentRepository; +import fi.oph.vkt.repository.ExaminerRepository; +import fi.oph.vkt.service.onr.OnrService; +import fi.oph.vkt.service.onr.PersonalData; +import fi.oph.vkt.util.ExaminerUtil; +import fi.oph.vkt.util.exception.APIException; +import fi.oph.vkt.util.exception.APIExceptionType; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class ExaminerDetailsService { + + private final ExaminerRepository examinerRepository; + private final EnrollmentAppointmentRepository enrollmentAppointmentRepository; + private final MunicipalityService municipalityService; + private final OnrService onrService; + private final AuditService auditService; + private final Environment environment; + + private PersonalData getOnrPersonalData(final String oid) { + Map oidToData = onrService.getOnrPersonalData(List.of(oid)); + return oidToData.get(oid); + } + + @Transactional(readOnly = true) + public ExaminerDetailsInitDTO getInitialExaminerPersonalData(final String oid) { + if (examinerRepository.findByOid(oid).isPresent()) { + throw new APIException(APIExceptionType.EXAMINER_ALREADY_INITIALIZED); + } + final PersonalData personalData = this.getOnrPersonalData(oid); + if (personalData == null) { + throw new APIException(APIExceptionType.EXAMINER_ONR_NOT_FOUND); + } + + auditService.logById(VktOperation.GET_EXAMINER_INITIAL_DETAILS, oid); + + return ExaminerDetailsInitDTO + .builder() + .oid(oid) + .lastName(personalData.getLastName()) + .firstName(personalData.getFirstName()) + .build(); + } + + @Transactional + public ExaminerDetailsDTO upsertExaminer(final String oid, ExaminerDetailsUpsertDTO examinerDetailsUpsertDTO) { + // TODO Audit log entry + final Optional existing = examinerRepository.findByOid(oid); + final Examiner examiner = existing.orElse(new Examiner()); + + examiner.setOid(oid); + + final PersonalData personalData = this.getOnrPersonalData(oid); + if (personalData == null) { + throw new APIException(APIExceptionType.EXAMINER_ONR_NOT_FOUND); + } + + examiner.setLastName(personalData.getLastName()); + examiner.setFirstName(personalData.getFirstName()); + examiner.setNickname(personalData.getNickname()); + examiner.setEmail(examinerDetailsUpsertDTO.email()); + examiner.setPhoneNumber(examinerDetailsUpsertDTO.phoneNumber()); + examiner.setMunicipalities( + examinerDetailsUpsertDTO + .municipalities() + .stream() + .map(municipality -> municipalityService.getOrCreateByCode(municipality.code())) + .collect(Collectors.toList()) + ); + examiner.setExamLanguageFinnish(examinerDetailsUpsertDTO.examLanguageFinnish()); + examiner.setExamLanguageSwedish(examinerDetailsUpsertDTO.examLanguageSwedish()); + examiner.setPublic(examinerDetailsUpsertDTO.isPublic()); + examinerRepository.saveAndFlush(examiner); + final String baseUrlAPI = environment.getRequiredProperty("app.base-url.api"); + + return ExaminerUtil.toExaminerDetailsDTO(examiner, List.of(), baseUrlAPI); + } + + @Transactional(readOnly = true) + public ExaminerDetailsDTO getExaminer(final String oid) { + final Examiner examiner = examinerRepository.getByOid(oid); + if (examiner == null) { + throw new APIException(APIExceptionType.EXAMINER_NOT_FOUND); + } + + auditService.logById(VktOperation.LIST_EXAMINER_DETAILS, oid); + + final String baseUrlAPI = environment.getRequiredProperty("app.base-url.api"); + final List enrollmentAppointments = enrollmentAppointmentRepository.findExaminerContactRequests( + examiner + ); + + return ExaminerUtil.toExaminerDetailsDTO(examiner, enrollmentAppointments, baseUrlAPI); + } + + @Transactional + public void updateStoredPersonalData() { + final List onrIds = examinerRepository.listExistingOnrIds(); + final Map oidToPersonalData = onrService.getOnrPersonalData(onrIds); + oidToPersonalData.forEach((k, v) -> { + final Examiner examiner = examinerRepository.getByOid(k); + examiner.setLastName(v.getLastName()); + examiner.setFirstName(v.getFirstName()); + examiner.setNickname(v.getNickname()); + examinerRepository.saveAndFlush(examiner); + }); + } +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/ExaminerEnrollmentEmailService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/ExaminerEnrollmentEmailService.java new file mode 100644 index 000000000..f4f6bf1e2 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/ExaminerEnrollmentEmailService.java @@ -0,0 +1,64 @@ +package fi.oph.vkt.service; + +import static fi.oph.vkt.util.LocalisationUtil.localeFI; +import static fi.oph.vkt.util.LocalisationUtil.localeSV; + +import fi.oph.vkt.model.*; +import fi.oph.vkt.service.email.EmailAttachmentData; +import fi.oph.vkt.service.email.EmailService; +import fi.oph.vkt.service.receipt.ReceiptRenderer; +import fi.oph.vkt.util.ClerkEnrollmentUtil; +import fi.oph.vkt.util.LocalisationUtil; +import fi.oph.vkt.util.TemplateRenderer; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class ExaminerEnrollmentEmailService extends AbstractEnrollmentEmailService { + + private final EmailService emailService; + private final Environment environment; + private final TemplateRenderer templateRenderer; + + @Transactional + public void sendEnrollmentAppointmentAuthLink(final EnrollmentAppointment enrollment) + throws IOException, InterruptedException { + final String baseUrlAPI = environment.getRequiredProperty("app.base-url.api"); + final Map templateParams = getEmailParams(enrollment, enrollment.getExaminerExamEvent()); + + final String authUrl = ClerkEnrollmentUtil.getAuthUrl(baseUrlAPI, enrollment.getId(), enrollment.getAuthHash()); + templateParams.put("enrollmentAuthLink", authUrl); + + final Examiner examiner = enrollment.getExaminer(); + final String examinerName = examiner.getNickname() + " " + examiner.getLastName(); + templateParams.put("examinerName", examinerName); + + final ExaminerExamEvent examEvent = enrollment.getExaminerExamEvent(); + templateParams.put("examLocation", examEvent.getLocation()); + + final String recipientName = enrollment.getFirstName() + " " + enrollment.getLastName(); + final String recipientAddress = enrollment.getEmail(); + final String subject = String.format( + "%s | %s", + LocalisationUtil.translate(localeFI, "subject.enrollment-appointment-authentication"), + LocalisationUtil.translate(localeSV, "subject.enrollment-appointment-authentication") + ); + final String body = templateRenderer.renderEnrollmentAppointmentAuthLink(templateParams); + + createEmail( + emailService, + recipientName, + recipientAddress, + subject, + body, + List.of(), + EmailType.ENROLLMENT_APPOINTMENT_AUTH_LINK + ); + } +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/ExaminerEnrollmentService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/ExaminerEnrollmentService.java new file mode 100644 index 000000000..ba8be32b9 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/ExaminerEnrollmentService.java @@ -0,0 +1,291 @@ +package fi.oph.vkt.service; + +import fi.oph.vkt.api.dto.clerk.ClerkEnrollmentContactRequestDTO; +import fi.oph.vkt.api.dto.examiner.ExaminerEnrollmentAppointmentDTO; +import fi.oph.vkt.api.dto.examiner.ExaminerEnrollmentAppointmentHistoryDTO; +import fi.oph.vkt.api.dto.examiner.ExaminerEnrollmentAppointmentUpdateDTO; +import fi.oph.vkt.api.dto.examiner.ExaminerEnrollmentGradesDTO; +import fi.oph.vkt.audit.AuditService; +import fi.oph.vkt.audit.VktOperation; +import fi.oph.vkt.model.EnrollmentAppointment; +import fi.oph.vkt.model.EnrollmentGrade; +import fi.oph.vkt.model.ExaminerExamEvent; +import fi.oph.vkt.model.Person; +import fi.oph.vkt.model.type.EnrollmentAppointmentStatus; +import fi.oph.vkt.repository.EnrollmentAppointmentRepository; +import fi.oph.vkt.repository.EnrollmentGradesRepository; +import fi.oph.vkt.repository.ExaminerExamEventRepository; +import fi.oph.vkt.util.ClerkEnrollmentUtil; +import fi.oph.vkt.util.ExaminerUtil; +import fi.oph.vkt.util.UUIDSource; +import fi.oph.vkt.util.exception.APIException; +import fi.oph.vkt.util.exception.APIExceptionType; +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class ExaminerEnrollmentService extends AbstractEnrollmentService { + + private final EnrollmentAppointmentRepository enrollmentAppointmentRepository; + private final EnrollmentGradesRepository enrollmentGradesRepository; + private final ExaminerExamEventRepository examinerExamEventRepository; + private final Environment environment; + private final UUIDSource uuidSource; + private final ExaminerEnrollmentEmailService examinerEnrollmentEmailService; + private final AuditService auditService; + + private static void checkExaminerOid(EnrollmentAppointment enrollmentAppointment, String oid) { + if (!enrollmentAppointment.getExaminer().getOid().equals(oid)) { + throw new APIException(APIExceptionType.EXAMINER_ENROLLMENT_OID_MISMATCH); + } + } + + @Transactional + public ExaminerEnrollmentAppointmentDTO updateAppointment( + final String oid, + final Long id, + final ExaminerEnrollmentAppointmentUpdateDTO dto + ) { + // TODO Audit log entry + if (!Objects.equals(id, dto.id())) { + throw new APIException(APIExceptionType.EXAMINER_APPOINTMENT_ID_MISMATCH); + } + final EnrollmentAppointment enrollmentAppointment = enrollmentAppointmentRepository.getReferenceById(dto.id()); + checkExaminerOid(enrollmentAppointment, oid); + final String baseUrlAPI = environment.getRequiredProperty("app.base-url.api"); + + enrollmentAppointment.assertVersion(dto.version()); + + if (dto.examEvent() != null) { + final ExaminerExamEvent examinerExamEvent = examinerExamEventRepository.getReferenceById(dto.examEvent()); + enrollmentAppointment.setExaminerExamEvent(examinerExamEvent); + } + + copyDtoFieldsToEnrollment(enrollmentAppointment, dto); + enrollmentAppointmentRepository.flush(); + + return ClerkEnrollmentUtil.createClerkEnrollmentAppointmentDTO(enrollmentAppointment, baseUrlAPI); + } + + @Transactional(readOnly = true) + public ClerkEnrollmentContactRequestDTO getEnrollmentContactRequest( + final String oid, + final long enrollmentContactId + ) { + final EnrollmentAppointment enrollmentAppointment = enrollmentAppointmentRepository.getReferenceById( + enrollmentContactId + ); + checkExaminerOid(enrollmentAppointment, oid); + + auditService.logById(VktOperation.VIEW_EXAMINER_CONTACT_REQUEST, enrollmentContactId); + + return ClerkEnrollmentUtil.createClerkEnrollmentContactDTO(enrollmentAppointment); + } + + @Transactional + public ExaminerEnrollmentAppointmentDTO convertToAppointment(final String oid, final long enrollmentContactId) { + final EnrollmentAppointment enrollmentAppointment = enrollmentAppointmentRepository.getReferenceById( + enrollmentContactId + ); + checkExaminerOid(enrollmentAppointment, oid); + + final String baseUrlAPI = environment.getRequiredProperty("app.base-url.api"); + + enrollmentAppointment.setStatus(EnrollmentAppointmentStatus.ENROLLMENT_CREATED); + + if (enrollmentAppointment.getAuthHash() == null) { + enrollmentAppointment.setAuthHash(uuidSource.getRandomNonce()); + } + + if (enrollmentAppointment.getPaymentLinkHash() == null) { + enrollmentAppointment.setPaymentLinkHash(uuidSource.getRandomNonce()); + } + + enrollmentAppointmentRepository.flush(); + + auditService.logById(VktOperation.CONVERT_EXAMINER_CONTACT_REQUEST_TO_APPOINTMENT, enrollmentContactId); + + return ClerkEnrollmentUtil.createClerkEnrollmentAppointmentDTO(enrollmentAppointment, baseUrlAPI); + } + + @Transactional(readOnly = true) + public ExaminerEnrollmentGradesDTO getAppointmentGrades(final String oid, final long enrollmentAppointmentId) { + final EnrollmentAppointment enrollmentAppointment = enrollmentAppointmentRepository.getReferenceById( + enrollmentAppointmentId + ); + checkExaminerOid(enrollmentAppointment, oid); + final Optional enrollmentGradeOptional = enrollmentGradesRepository.findByEnrollmentAppointment( + enrollmentAppointment + ); + + auditService.logById(VktOperation.VIEW_EXAMINER_ENROLLMENT_GRADES, enrollmentAppointmentId); + + return enrollmentGradeOptional.map(ExaminerUtil::createGradesDTO).orElse(null); + } + + @Transactional(readOnly = true) + public ExaminerEnrollmentAppointmentDTO getEnrollmentAppointment( + final String oid, + final long enrollmentAppointmentId + ) { + final EnrollmentAppointment enrollmentAppointment = enrollmentAppointmentRepository.getReferenceById( + enrollmentAppointmentId + ); + + checkExaminerOid(enrollmentAppointment, oid); + + auditService.logById(VktOperation.VIEW_EXAMINER_ENROLLMENT, enrollmentAppointmentId); + + final String baseUrlAPI = environment.getRequiredProperty("app.base-url.api"); + + return ClerkEnrollmentUtil.createClerkEnrollmentAppointmentDTO(enrollmentAppointment, baseUrlAPI); + } + + @Transactional + public ExaminerEnrollmentGradesDTO upsertAppointmentGrades( + final String oid, + final long enrollmentAppointmentId, + final ExaminerEnrollmentGradesDTO dto + ) { + final EnrollmentAppointment enrollmentAppointment = enrollmentAppointmentRepository.getReferenceById( + enrollmentAppointmentId + ); + + checkExaminerOid(enrollmentAppointment, oid); + + final Optional enrollmentGradeOptional = enrollmentGradesRepository.findByEnrollmentAppointment( + enrollmentAppointment + ); + final EnrollmentGrade enrollmentGrade = enrollmentGradeOptional.orElseGet(EnrollmentGrade::new); + + enrollmentGrade.assertVersion(dto.version()); + if (dto.speakingPartialExam() != null) { + enrollmentGrade.setSpeakingPartialExamGrade(dto.speakingPartialExam().grade()); + enrollmentGrade.setSpeakingPartialExamComment(dto.speakingPartialExam().comment()); + } else { + enrollmentGrade.setSpeakingPartialExamGrade(null); + enrollmentGrade.setSpeakingPartialExamComment(null); + } + + if (dto.writingPartialExam() != null) { + enrollmentGrade.setWritingPartialExamGrade(dto.writingPartialExam().grade()); + enrollmentGrade.setWritingPartialExamComment(dto.writingPartialExam().comment()); + } else { + enrollmentGrade.setWritingPartialExamGrade(null); + enrollmentGrade.setWritingPartialExamComment(null); + } + + if (dto.speechComprehensionPartialExam() != null) { + enrollmentGrade.setSpeechComprehensionPartialExamGrade(dto.speechComprehensionPartialExam().grade()); + enrollmentGrade.setSpeechComprehensionPartialExamComment(dto.speechComprehensionPartialExam().comment()); + } else { + enrollmentGrade.setSpeechComprehensionPartialExamGrade(null); + enrollmentGrade.setSpeechComprehensionPartialExamComment(null); + } + + if (dto.readingComprehensionPartialExam() != null) { + enrollmentGrade.setReadingComprehensionPartialExamGrade(dto.readingComprehensionPartialExam().grade()); + enrollmentGrade.setReadingComprehensionPartialExamComment(dto.readingComprehensionPartialExam().comment()); + } else { + enrollmentGrade.setReadingComprehensionPartialExamGrade(null); + enrollmentGrade.setReadingComprehensionPartialExamComment(null); + } + + enrollmentGradesRepository.saveAndFlush(enrollmentGrade); + + enrollmentAppointment.setGrade(enrollmentGrade); + enrollmentAppointmentRepository.saveAndFlush(enrollmentAppointment); + + return ExaminerUtil.createGradesDTO(enrollmentGrade); + } + + @Transactional + public ExaminerEnrollmentAppointmentDTO sendEnrollmentAppointmentLink( + final String oid, + final long enrollmentAppointmentId + ) throws IOException, InterruptedException { + final EnrollmentAppointment enrollmentAppointment = enrollmentAppointmentRepository.getReferenceById( + enrollmentAppointmentId + ); + final String baseUrlAPI = environment.getRequiredProperty("app.base-url.api"); + + checkExaminerOid(enrollmentAppointment, oid); + + enrollmentAppointment.setExpiresAt(LocalDateTime.now().plusDays(3)); + enrollmentAppointment.setSentAt(LocalDateTime.now()); + enrollmentAppointment.setStatus(EnrollmentAppointmentStatus.WAITING_AUTHENTICATION); + + examinerEnrollmentEmailService.sendEnrollmentAppointmentAuthLink(enrollmentAppointment); + + enrollmentAppointmentRepository.flush(); + + return ClerkEnrollmentUtil.createClerkEnrollmentAppointmentDTO(enrollmentAppointment, baseUrlAPI); + } + + @Transactional + public void deleteEnrollmentContactRequest(final String oid, final long enrollmentContactId) { + final EnrollmentAppointment enrollmentAppointment = enrollmentAppointmentRepository.getReferenceById( + enrollmentContactId + ); + + checkExaminerOid(enrollmentAppointment, oid); + + auditService.logById(VktOperation.DELETE_EXAMINER_CONTACT_REQUEST, enrollmentContactId); + + enrollmentAppointment.setDeletedAt(LocalDateTime.now()); + + enrollmentAppointmentRepository.flush(); + } + + @Transactional + public void cancelEnrollmentAppointment(final String oid, final long enrollmentAppointmentId) { + final EnrollmentAppointment enrollmentAppointment = enrollmentAppointmentRepository.getReferenceById( + enrollmentAppointmentId + ); + + checkExaminerOid(enrollmentAppointment, oid); + + enrollmentAppointment.setStatus(EnrollmentAppointmentStatus.CANCELED); + + auditService.logById(VktOperation.CANCEL_ENROLLMENT_APPOINTMENT, enrollmentAppointmentId); + + enrollmentAppointmentRepository.flush(); + } + + @Transactional(readOnly = true) + public List getEnrollmentAppointmentHistory( + final String oid, + final long enrollmentAppointmentId + ) { + final EnrollmentAppointment enrollmentAppointment = enrollmentAppointmentRepository.getReferenceById( + enrollmentAppointmentId + ); + + checkExaminerOid(enrollmentAppointment, oid); + + final Person person = enrollmentAppointment.getPerson(); + if (person == null) { + return List.of(); + } + + auditService.logById(VktOperation.VIEW_EXAMINER_ENROLLMENT_HISTORY, enrollmentAppointmentId); + + final List enrollmentAppointments = enrollmentAppointmentRepository.findPersonEnrollmentHistory( + person + ); + + return enrollmentAppointments + .stream() + .filter(e -> e.getId() != enrollmentAppointmentId) + .map(ClerkEnrollmentUtil::createClerkEnrollmentAppointmentHistoryDTO) + .toList(); + } +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/ExaminerExamEventService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/ExaminerExamEventService.java new file mode 100644 index 000000000..255bdc6f5 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/ExaminerExamEventService.java @@ -0,0 +1,155 @@ +package fi.oph.vkt.service; + +import fi.oph.vkt.api.dto.MunicipalityDTO; +import fi.oph.vkt.api.dto.examiner.ExaminerExamEventDTO; +import fi.oph.vkt.api.dto.examiner.ExaminerExamEventUpsertDTO; +import fi.oph.vkt.audit.AuditService; +import fi.oph.vkt.audit.VktOperation; +import fi.oph.vkt.model.EnrollmentAppointment; +import fi.oph.vkt.model.Examiner; +import fi.oph.vkt.model.ExaminerExamEvent; +import fi.oph.vkt.model.Municipality; +import fi.oph.vkt.repository.ExaminerExamEventRepository; +import fi.oph.vkt.repository.ExaminerRepository; +import fi.oph.vkt.util.ExaminerUtil; +import fi.oph.vkt.util.exception.APIException; +import fi.oph.vkt.util.exception.APIExceptionType; +import fi.oph.vkt.view.ExamEventXlsxData; +import fi.oph.vkt.view.ExamEventXlsxDataRowUtil; +import fi.oph.vkt.view.ExamEventXlsxView; +import fi.oph.vkt.view.ExaminerExamEventXlsxData; +import fi.oph.vkt.view.ExaminerExamEventXlsxView; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.servlet.view.document.AbstractXlsxView; + +@Service +@RequiredArgsConstructor +public class ExaminerExamEventService { + + private final ExaminerExamEventRepository examinerExamEventRepository; + private final Environment environment; + + private final AuditService auditService; + private final ExaminerRepository examinerRepository; + + @Transactional(readOnly = true) + public ExaminerExamEventDTO getExamEvent(final String oid, final long examEventId) { + final ExaminerExamEventDTO examEventDTO = getExamEventWithoutAudit(oid, examEventId); + + auditService.logById(VktOperation.GET_EXAM_EVENT, examEventId); + return examEventDTO; + } + + private ExaminerExamEvent getExamEventForExaminer(final String oid, final long examEventId) { + ExaminerExamEvent examinerExamEvent = examinerExamEventRepository + .findById(examEventId) + .orElseThrow(() -> new APIException(APIExceptionType.EXAMINER_EXAM_EVENT_NOT_FOUND)); + Examiner examiner = examinerExamEvent.getExaminer(); + if (!examiner.getOid().equals(oid)) { + throw new APIException(APIExceptionType.EXAMINER_EXAM_EVENT_EXAMINER_MISMATCH); + } + return examinerExamEvent; + } + + private ExaminerExamEventDTO getExamEventWithoutAudit(final String oid, final long examEventId) { + ExaminerExamEvent examEvent = getExamEventForExaminer(oid, examEventId); + Examiner examiner = examEvent.getExaminer(); + if (!examiner.getOid().equals(oid)) { + throw new APIException(APIExceptionType.EXAMINER_EXAM_EVENT_EXAMINER_MISMATCH); + } + final String baseUrlAPI = environment.getRequiredProperty("app.base-url.api"); + + return ExaminerUtil.toExaminerExamEventDTO(examEvent, baseUrlAPI); + } + + private Municipality getExaminerMunicipalityOrThrow(Examiner examiner, MunicipalityDTO municipalityDTO) { + return examiner + .getMunicipalities() + .stream() + .filter(m -> m.getCode().equals(municipalityDTO.code())) + .findFirst() + .orElseThrow(() -> new APIException(APIExceptionType.EXAMINER_MUNICIPALITY_MISMATCH)); + } + + private void updateExamEventDetails(Examiner examiner, ExaminerExamEvent examEvent, ExaminerExamEventUpsertDTO dto) { + examEvent.setDate(dto.date()); + examEvent.setLanguage(dto.language()); + examEvent.setMunicipality(getExaminerMunicipalityOrThrow(examiner, dto.municipality())); + examEvent.setHidden(dto.isHidden()); + examEvent.setExamTime(dto.examTime()); + examEvent.setLocation(dto.location()); + examEvent.setOtherInformation(dto.otherInformation()); + examEvent.setMaxParticipants(dto.maxParticipants()); + examEvent.setRegistrationCloses(dto.registrationCloses()); + } + + @Transactional + public ExaminerExamEventDTO createExamEvent(final String oid, final ExaminerExamEventUpsertDTO dto) { + Examiner examiner = examinerRepository + .findByOid(oid) + .orElseThrow(() -> new APIException(APIExceptionType.EXAMINER_NOT_FOUND)); + ExaminerExamEvent examEvent = new ExaminerExamEvent(); + examEvent.setExaminer(examiner); + updateExamEventDetails(examiner, examEvent, dto); + + ExaminerExamEvent examinerExamEvent = examinerExamEventRepository.saveAndFlush(examEvent); + final String baseUrlAPI = environment.getRequiredProperty("app.base-url.api"); + return ExaminerUtil.toExaminerExamEventDTO(examinerExamEvent, baseUrlAPI); + } + + @Transactional + public ExaminerExamEventDTO updateExamEvent( + final String oid, + final Long examEventId, + final ExaminerExamEventUpsertDTO dto + ) { + ExaminerExamEvent examEvent = getExamEventForExaminer(oid, examEventId); + updateExamEventDetails(examEvent.getExaminer(), examEvent, dto); + ExaminerExamEvent examinerExamEvent = examinerExamEventRepository.saveAndFlush(examEvent); + final String baseUrlAPI = environment.getRequiredProperty("app.base-url.api"); + return ExaminerUtil.toExaminerExamEventDTO(examinerExamEvent, baseUrlAPI); + } + + @Transactional(readOnly = true) + public List list(final String oid) { + final Examiner examiner = examinerRepository + .findByOid(oid) + .orElseThrow(() -> new APIException(APIExceptionType.EXAMINER_NOT_FOUND)); + final List examinerExamEvents = examinerExamEventRepository.findAllByExaminer(examiner); + + return examinerExamEvents.stream().map(ExaminerUtil::toExaminerExamEventWithoutEnrollmentsDTO).toList(); + } + + @Transactional(readOnly = true) + public AbstractXlsxView getExamEventExcel(final String oid, final long examEventId) { + final ExaminerExamEvent examEvent = examinerExamEventRepository.getReferenceById(examEventId); + + if (!examEvent.getExaminer().getOid().equals(oid)) { + throw new APIException(APIExceptionType.EXAMINER_EXAM_EVENT_EXAMINER_MISMATCH); + } + + final List enrollments = examEvent + .getEnrollments() + .stream() + .sorted(excelEnrollmentComparator()) + .toList(); + + final ExaminerExamEventXlsxData excelData = ExamEventXlsxDataRowUtil.createExcelData(examEvent, enrollments); + final AbstractXlsxView excel = new ExaminerExamEventXlsxView(excelData); + + auditService.logById(VktOperation.GET_EXAM_EVENT_EXCEL, examEventId); + return excel; + } + + private static Comparator excelEnrollmentComparator() { + final Comparator byStatus = Comparator.comparing(EnrollmentAppointment::getStatus); + final Comparator byCreatedAt = Comparator.comparing(EnrollmentAppointment::getCreatedAt); + return byStatus.thenComparing(byCreatedAt); + } +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/MunicipalityService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/MunicipalityService.java new file mode 100644 index 000000000..e27877f0e --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/MunicipalityService.java @@ -0,0 +1,69 @@ +package fi.oph.vkt.service; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import fi.oph.vkt.model.Municipality; +import fi.oph.vkt.repository.MunicipalityRepository; +import jakarta.annotation.PostConstruct; +import jakarta.transaction.Transactional; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class MunicipalityService { + + private final MunicipalityRepository municipalityRepository; + private Map codeToFi; + private Map codeToSv; + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final String KOODISTO_MUNICIPALITIES_JSON = "koodisto/koodisto_kunnat.json"; + + @PostConstruct + public void init() { + codeToFi = new HashMap<>(); + codeToSv = new HashMap<>(); + + try (final InputStream is = new ClassPathResource(KOODISTO_MUNICIPALITIES_JSON).getInputStream()) { + final List koodisto = deserializeJson(is); + koodisto.forEach(koodistoEntry -> { + codeToFi.put(koodistoEntry.koodiArvo(), koodistoEntry.fi()); + codeToSv.put(koodistoEntry.koodiArvo(), koodistoEntry.sv()); + }); + } catch (final IOException e) { + throw new RuntimeException(e); + } + } + + @Transactional + public Municipality getOrCreateByCode(final String code) { + Optional existingMunicipality = municipalityRepository.findByCode(code); + if (existingMunicipality.isPresent()) { + return existingMunicipality.get(); + } else { + // TODO Validate code? + Municipality municipality = new Municipality(); + municipality.setCode(code); + municipality.setNameFI(codeToFi.get(code)); + municipality.setNameSV(codeToSv.get(code)); + municipalityRepository.saveAndFlush(municipality); + return municipality; + } + } + + private List deserializeJson(final InputStream is) throws IOException { + return OBJECT_MAPPER.readValue(is, new TypeReference<>() {}); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + private record KoodistoEntry(@NonNull String koodiArvo, @NonNull String fi, @NonNull String sv) {} +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/PaymentService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/PaymentService.java index 1e36f3c6e..af9100c49 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/service/PaymentService.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/PaymentService.java @@ -2,18 +2,22 @@ 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.Payment; import fi.oph.vkt.model.Person; import fi.oph.vkt.model.type.AppLocale; +import fi.oph.vkt.model.type.EnrollmentAppointmentStatus; import fi.oph.vkt.model.type.EnrollmentSkill; import fi.oph.vkt.model.type.EnrollmentStatus; +import fi.oph.vkt.model.type.ExamLevel; import fi.oph.vkt.model.type.PaymentStatus; import fi.oph.vkt.payment.PaymentProvider; import fi.oph.vkt.payment.paytrail.Customer; import fi.oph.vkt.payment.paytrail.Item; import fi.oph.vkt.payment.paytrail.PaytrailConfig; import fi.oph.vkt.payment.paytrail.PaytrailResponseDTO; +import fi.oph.vkt.repository.EnrollmentAppointmentRepository; import fi.oph.vkt.repository.EnrollmentRepository; import fi.oph.vkt.repository.PaymentRepository; import fi.oph.vkt.util.EnrollmentUtil; @@ -24,6 +28,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,32 +45,79 @@ public class PaymentService { private final PaymentProvider paymentProvider; private final PaymentRepository paymentRepository; private final EnrollmentRepository enrollmentRepository; + private final EnrollmentAppointmentRepository enrollmentAppointmentRepository; private final Environment environment; private final PublicEnrollmentEmailService publicEnrollmentEmailService; - private Item getItem(final EnrollmentSkill enrollmentSkill, final int unitPrice) { + private Item getItem(final EnrollmentSkill enrollmentSkill, final int unitPrice, final ExamLevel examLevel) { return Item .builder() .units(1) .unitPrice(unitPrice) .vatPercentage(PaytrailConfig.VAT) - .productCode(enrollmentSkill.toString()) + .productCode(examLevel.toString() + "-" + enrollmentSkill.toString()) .build(); } + private List getItems(final EnrollmentAppointment enrollmentAppointment) { + final List itemList = new ArrayList<>(); + + if (enrollmentAppointment.isTextualSkill()) { + itemList.add( + getItem( + EnrollmentSkill.TEXTUAL, + EnrollmentUtil.getTextualSkillFee(enrollmentAppointment), + ExamLevel.GOOD_AND_SATISFACTORY + ) + ); + } + if (enrollmentAppointment.isOralSkill()) { + itemList.add( + getItem( + EnrollmentSkill.ORAL, + EnrollmentUtil.getOralSkillFee(enrollmentAppointment), + ExamLevel.GOOD_AND_SATISFACTORY + ) + ); + } + if (enrollmentAppointment.isUnderstandingSkill()) { + itemList.add( + getItem( + EnrollmentSkill.UNDERSTANDING, + EnrollmentUtil.getUnderstandingSkillFee(enrollmentAppointment), + ExamLevel.GOOD_AND_SATISFACTORY + ) + ); + } + + return itemList; + } + private List getItems(final Enrollment enrollment, final FreeEnrollmentDetails freeEnrollmentDetails) { final List itemList = new ArrayList<>(); if (enrollment.isTextualSkill()) { itemList.add( - getItem(EnrollmentSkill.TEXTUAL, EnrollmentUtil.getTextualSkillFee(enrollment, freeEnrollmentDetails)) + getItem( + EnrollmentSkill.TEXTUAL, + EnrollmentUtil.getTextualSkillFee(enrollment, freeEnrollmentDetails), + ExamLevel.EXCELLENT + ) ); } if (enrollment.isOralSkill()) { - itemList.add(getItem(EnrollmentSkill.ORAL, EnrollmentUtil.getOralSkillFee(enrollment, freeEnrollmentDetails))); + itemList.add( + getItem( + EnrollmentSkill.ORAL, + EnrollmentUtil.getOralSkillFee(enrollment, freeEnrollmentDetails), + ExamLevel.EXCELLENT + ) + ); } if (enrollment.isUnderstandingSkill()) { - itemList.add(getItem(EnrollmentSkill.UNDERSTANDING, EnrollmentUtil.getUnderstandingSkillFee(enrollment))); + itemList.add( + getItem(EnrollmentSkill.UNDERSTANDING, EnrollmentUtil.getUnderstandingSkillFee(enrollment), ExamLevel.EXCELLENT) + ); } return itemList; @@ -78,6 +130,19 @@ private EnrollmentStatus getPaymentSuccessEnrollmentNextStatus(final Enrollment return enrollment.enrollmentNeedsApproval() ? EnrollmentStatus.AWAITING_APPROVAL : EnrollmentStatus.COMPLETED; } + private void setEnrollmentStatus( + final EnrollmentAppointment enrollmentAppointment, + final PaymentStatus paymentStatus + ) { + switch (paymentStatus) { + case NEW, PENDING, DELAYED -> {} + case OK -> enrollmentAppointment.setStatus(EnrollmentAppointmentStatus.COMPLETED); + case FAIL -> { + enrollmentAppointment.setStatus(EnrollmentAppointmentStatus.CANCELED_PAYMENT); + } + } + } + private void setEnrollmentStatus(final Enrollment enrollment, final PaymentStatus paymentStatus) { switch (paymentStatus) { case NEW -> { @@ -131,23 +196,39 @@ public Payment finalizePayment(final Long paymentId, final Map p throw new APIException(APIExceptionType.PAYMENT_REFERENCE_MISMATCH); } - final Enrollment enrollment = payment.getEnrollment(); - FreeEnrollmentDetails freeEnrollmentDetails = enrollmentRepository.countEnrollmentsByPerson(enrollment.getPerson()); + if (payment.getEnrollment() != null) { + final Enrollment enrollment = payment.getEnrollment(); + setEnrollmentStatus(enrollment, newStatus); + final FreeEnrollmentDetails freeEnrollmentDetails = enrollmentRepository.countEnrollmentsByPerson( + enrollment.getPerson() + ); - setEnrollmentStatus(enrollment, newStatus); + setEnrollmentStatus(enrollment, newStatus); - payment.setPaymentStatus(newStatus); - paymentRepository.saveAndFlush(payment); + payment.setPaymentStatus(newStatus); + paymentRepository.saveAndFlush(payment); + + if (newStatus == PaymentStatus.OK) { + if (enrollment.getFreeEnrollment() != null) { + publicEnrollmentEmailService.sendPartiallyFreeEnrollmentConfirmationEmail( + enrollment, + enrollment.getPerson(), + freeEnrollmentDetails + ); + } else { + publicEnrollmentEmailService.sendEnrollmentConfirmationEmail(enrollment); + } + } + } else { + final EnrollmentAppointment enrollmentAppointment = payment.getEnrollmentAppointment(); + setEnrollmentStatus(enrollmentAppointment, newStatus); - if (newStatus == PaymentStatus.OK) { - if (enrollment.getFreeEnrollment() != null) { - publicEnrollmentEmailService.sendPartiallyFreeEnrollmentConfirmationEmail( - enrollment, - enrollment.getPerson(), - freeEnrollmentDetails - ); - } else { - publicEnrollmentEmailService.sendEnrollmentConfirmationEmail(enrollment); + payment.setPaymentStatus(newStatus); + paymentRepository.saveAndFlush(payment); + + // FIXME + if (newStatus == PaymentStatus.OK) { + publicEnrollmentEmailService.sendEnrollmentAppointmentConfirmationEmail(enrollmentAppointment); } } @@ -169,9 +250,67 @@ private String getFinalizePaymentRedirectUrl(final Long paymentId, final String final Payment payment = paymentRepository .findById(paymentId) .orElseThrow(() -> new NotFoundException("Payment not found")); - final ExamEvent examEvent = payment.getEnrollment().getExamEvent(); - return String.format("%s/ilmoittaudu/%d/maksu/%s", baseUrl, examEvent.getId(), state); + return payment.getEnrollment() != null + ? String.format( + "%s/erinomainen-taito/ilmoittaudu/%d/maksu/%s", + baseUrl, + payment.getEnrollment().getExamEvent().getId(), + state + ) + : String.format( + "%s/hyva-ja-tyydyttava-taito/ilmoittaudu/%d/maksu/%s", + baseUrl, + payment.getEnrollmentAppointment().getId(), + state + ); + } + + @Transactional + public String createPaymentForEnrollmentAppointment( + final Long enrollmentId, + final Person person, + final AppLocale appLocale + ) { + final EnrollmentAppointment enrollmentAppointment = enrollmentAppointmentRepository + .findById(enrollmentId) + .orElseThrow(() -> new NotFoundException("Enrollment not found")); + + if (enrollmentAppointment.getPerson() == null || enrollmentAppointment.getPerson().getId() != person.getId()) { + throw new APIException(APIExceptionType.PAYMENT_PERSON_SESSION_MISMATCH); + } + + final List itemList = getItems(enrollmentAppointment); + final Customer customer = Customer + .builder() + .email(getCustomerField(enrollmentAppointment.getEmail(), Customer.EMAIL_MAX_LENGTH)) + .phone(getCustomerField(enrollmentAppointment.getPhoneNumber(), Customer.PHONE_MAX_LENGTH)) + .firstName(getCustomerField(person.getFirstName(), Customer.FIRST_NAME_MAX_LENGTH)) + .lastName(getCustomerField(person.getLastName(), Customer.LAST_NAME_MAX_LENGTH)) + .build(); + + final int amount = EnrollmentUtil.getTotalFee(enrollmentAppointment); + + final Payment payment = new Payment(); + payment.setEnrollmentAppointment(enrollmentAppointment); + payment.setAmount(amount); + paymentRepository.saveAndFlush(payment); + + final PaytrailResponseDTO response = paymentProvider.createPayment( + itemList, + payment.getId(), + customer, + amount, + appLocale + ); + + payment.setTransactionId(response.getTransactionId()); + payment.setReference(response.getReference()); + payment.setPaymentUrl(response.getHref()); + payment.setPaymentStatus(PaymentStatus.NEW); + paymentRepository.saveAndFlush(payment); + + return payment.getPaymentUrl(); } @Transactional 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..efd5e8d03 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentAppointmentService.java @@ -0,0 +1,49 @@ +package fi.oph.vkt.service; + +import fi.oph.vkt.model.EnrollmentAppointment; +import fi.oph.vkt.model.Person; +import fi.oph.vkt.repository.EnrollmentAppointmentRepository; +import fi.oph.vkt.util.exception.APIException; +import fi.oph.vkt.util.exception.APIExceptionType; +import java.time.LocalDateTime; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class PublicEnrollmentAppointmentService extends AbstractEnrollmentService { + + private final EnrollmentAppointmentRepository enrollmentAppointmentRepository; + + public EnrollmentAppointment getEnrollmentAppointmentByHash( + final long enrollmentAppointmentId, + final String authHash + ) { + final EnrollmentAppointment enrollmentAppointment = enrollmentAppointmentRepository + .findByIdAndAuthHashAndDeletedAtIsNull(enrollmentAppointmentId, authHash) + .orElseThrow(); + + if (enrollmentAppointment.getExpiresAt().isBefore(LocalDateTime.now())) { + throw new APIException(APIExceptionType.AUTH_HASH_EXPIRED); + } + + return enrollmentAppointment; + } + + public void savePersonInfo(final long targetId, final Long appointmentId, final Person person) { + if (targetId != appointmentId) { + throw new APIException(APIExceptionType.SESSION_APPOINTMENT_ID_MISMATCH); + } + + final EnrollmentAppointment enrollmentAppointment = enrollmentAppointmentRepository + .findById(targetId) + .orElseThrow(); + + if (enrollmentAppointment.getPerson() != null && enrollmentAppointment.getPerson().getId() != person.getId()) { + throw new APIException(APIExceptionType.SESSION_APPOINTMENT_PERSON_MISMATCH); + } + + enrollmentAppointment.setPerson(person); + enrollmentAppointmentRepository.saveAndFlush(enrollmentAppointment); + } +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentEmailService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentEmailService.java index 806fdbb18..00a382785 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentEmailService.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentEmailService.java @@ -4,14 +4,9 @@ import static fi.oph.vkt.util.LocalisationUtil.localeSV; import fi.oph.vkt.api.dto.FreeEnrollmentDetails; -import fi.oph.vkt.model.EmailType; -import fi.oph.vkt.model.Enrollment; -import fi.oph.vkt.model.ExamEvent; -import fi.oph.vkt.model.Person; -import fi.oph.vkt.model.type.ExamLanguage; +import fi.oph.vkt.model.*; import fi.oph.vkt.model.type.FreeEnrollmentSource; import fi.oph.vkt.service.email.EmailAttachmentData; -import fi.oph.vkt.service.email.EmailData; import fi.oph.vkt.service.email.EmailService; import fi.oph.vkt.service.receipt.ReceiptData; import fi.oph.vkt.service.receipt.ReceiptRenderer; @@ -19,13 +14,10 @@ import fi.oph.vkt.util.LocalisationUtil; import fi.oph.vkt.util.TemplateRenderer; import java.io.IOException; -import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.Stream; import lombok.RequiredArgsConstructor; import org.springframework.core.env.Environment; import org.springframework.stereotype.Service; @@ -33,7 +25,7 @@ @Service @RequiredArgsConstructor -public class PublicEnrollmentEmailService { +public class PublicEnrollmentEmailService extends AbstractEnrollmentEmailService { private final EmailService emailService; private final Environment environment; @@ -43,7 +35,7 @@ public class PublicEnrollmentEmailService { @Transactional public void sendEnrollmentConfirmationEmail(final Enrollment enrollment) throws IOException, InterruptedException { final Person person = enrollment.getPerson(); - final Map templateParams = getEmailParams(enrollment); + final Map templateParams = getEmailParams(enrollment, enrollment.getExamEvent()); templateParams.put("type", "enrollment"); final String recipientName = person.getFirstName() + " " + person.getLastName(); @@ -62,12 +54,20 @@ public void sendEnrollmentConfirmationEmail(final Enrollment enrollment) throws ? List.of(createReceiptAttachment(enrollment, localeFI), createReceiptAttachment(enrollment, localeSV)) : List.of(); // for local development - createEmail(recipientName, recipientAddress, subject, body, attachments, EmailType.ENROLLMENT_CONFIRMATION); + createEmail( + emailService, + recipientName, + recipientAddress, + subject, + body, + attachments, + EmailType.ENROLLMENT_CONFIRMATION + ); } @Transactional public void sendEnrollmentToQueueConfirmationEmail(final Enrollment enrollment, final Person person) { - final Map templateParams = getEmailParams(enrollment); + final Map templateParams = getEmailParams(enrollment, enrollment.getExamEvent()); templateParams.put("type", "queue"); final String recipientName = person.getFirstName() + " " + person.getLastName(); @@ -80,71 +80,20 @@ public void sendEnrollmentToQueueConfirmationEmail(final Enrollment enrollment, final String body = templateRenderer.renderEnrollmentConfirmationEmailBody(templateParams); - createEmail(recipientName, recipientAddress, subject, body, List.of(), EmailType.ENROLLMENT_TO_QUEUE_CONFIRMATION); - } - - private Map getEmailParams(final Enrollment enrollment) { - final ExamEvent examEvent = enrollment.getExamEvent(); - - final Map params = new HashMap<>(Map.of()); - - if (examEvent.getLanguage() == ExamLanguage.FI) { - params.put("examLanguageFI", LocalisationUtil.translate(LocalisationUtil.localeFI, "lang.finnish")); - params.put("examLanguageSV", LocalisationUtil.translate(LocalisationUtil.localeSV, "lang.finnish")); - } else { - params.put("examLanguageFI", LocalisationUtil.translate(LocalisationUtil.localeFI, "lang.swedish")); - params.put("examLanguageSV", LocalisationUtil.translate(LocalisationUtil.localeSV, "lang.swedish")); - } - - params.put("examLevelFI", LocalisationUtil.translate(localeFI, "examLevel.excellent")); - params.put("examLevelSV", LocalisationUtil.translate(localeSV, "examLevel.excellent")); - - params.put("examDate", examEvent.getDate().format(DateTimeFormatter.ofPattern("dd.MM.yyyy"))); - - params.put("skillsFI", getEmailParamSkills(enrollment, localeFI, params.get("examLanguageFI"))); - params.put("skillsSV", getEmailParamSkills(enrollment, localeSV, params.get("examLanguageSV"))); - - params.put("partialExamsFI", getEmailParamPartialExams(enrollment, localeFI)); - params.put("partialExamsSV", getEmailParamPartialExams(enrollment, localeSV)); - - params.put("type", "enrollment"); - params.put("isFree", false); - - return params; - } - - private String getEmailParamSkills(final Enrollment enrollment, final Locale locale, final Object... args) { - return joinNonEmptyStrings( - Stream.of( - enrollment.isTextualSkill() ? LocalisationUtil.translate(locale, "skill.textual", args) : "", - enrollment.isOralSkill() ? LocalisationUtil.translate(locale, "skill.oral", args) : "", - enrollment.isUnderstandingSkill() ? LocalisationUtil.translate(locale, "skill.understanding", args) : "" - ) - ); - } - - private String getEmailParamPartialExams(final Enrollment enrollment, final Locale locale) { - return joinNonEmptyStrings( - Stream.of( - enrollment.isWritingPartialExam() ? LocalisationUtil.translate(locale, "partialExam.writing") : "", - enrollment.isReadingComprehensionPartialExam() - ? LocalisationUtil.translate(locale, "partialExam.readingComprehension") - : "", - enrollment.isSpeakingPartialExam() ? LocalisationUtil.translate(locale, "partialExam.speaking") : "", - enrollment.isSpeechComprehensionPartialExam() - ? LocalisationUtil.translate(locale, "partialExam.speechComprehension") - : "" - ) + createEmail( + emailService, + recipientName, + recipientAddress, + subject, + body, + List.of(), + EmailType.ENROLLMENT_TO_QUEUE_CONFIRMATION ); } - private String joinNonEmptyStrings(final Stream stream) { - return stream.filter(s -> !s.isEmpty()).collect(Collectors.joining(", ")); - } - - private EmailAttachmentData createReceiptAttachment(final Enrollment enrollment, final Locale locale) + private EmailAttachmentData createReceiptAttachment(final EnrollmentCommon enrollment, final Locale locale) throws IOException, InterruptedException { - final ReceiptData receiptData = receiptRenderer.getReceiptData(enrollment.getId(), locale); + final ReceiptData receiptData = receiptRenderer.getReceiptData(enrollment, locale); final byte[] receiptBytes = receiptRenderer.getReceiptPdfBytes(receiptData, locale); final String attachmentNamePrefix = LocalisationUtil.translate(locale, "payment.receipt"); @@ -157,26 +106,6 @@ private EmailAttachmentData createReceiptAttachment(final Enrollment enrollment, .build(); } - private void createEmail( - final String recipientName, - final String recipientAddress, - final String subject, - final String body, - final List attachments, - final EmailType emailType - ) { - final EmailData emailData = EmailData - .builder() - .recipientName(recipientName) - .recipientAddress(recipientAddress) - .subject(subject) - .body(body) - .attachments(attachments) - .build(); - - emailService.saveEmail(emailType, emailData); - } - @Transactional public void sendFreeEnrollmentConfirmationEmail( final Enrollment enrollment, @@ -184,7 +113,7 @@ public void sendFreeEnrollmentConfirmationEmail( final FreeEnrollmentDetails freeEnrollmentDetails ) { final Map templateParams = withFreeEmailParams( - getEmailParams(enrollment), + getEmailParams(enrollment, enrollment.getExamEvent()), freeEnrollmentDetails, enrollment.getFreeEnrollment().getSource(), "enrollment" @@ -199,7 +128,15 @@ public void sendFreeEnrollmentConfirmationEmail( ); final String body = templateRenderer.renderEnrollmentConfirmationEmailBody(templateParams); - createEmail(recipientName, recipientAddress, subject, body, List.of(), EmailType.ENROLLMENT_CONFIRMATION); + createEmail( + emailService, + recipientName, + recipientAddress, + subject, + body, + List.of(), + EmailType.ENROLLMENT_CONFIRMATION + ); } @Transactional @@ -209,7 +146,7 @@ public void sendFreeEnrollmentToQueueConfirmationEmail( final FreeEnrollmentDetails freeEnrollmentDetails ) { final Map templateParams = withFreeEmailParams( - getEmailParams(enrollment), + getEmailParams(enrollment, enrollment.getExamEvent()), freeEnrollmentDetails, enrollment.getFreeEnrollment().getSource(), "queue" @@ -224,7 +161,15 @@ public void sendFreeEnrollmentToQueueConfirmationEmail( ); final String body = templateRenderer.renderEnrollmentConfirmationEmailBody(templateParams); - createEmail(recipientName, recipientAddress, subject, body, List.of(), EmailType.ENROLLMENT_TO_QUEUE_CONFIRMATION); + createEmail( + emailService, + recipientName, + recipientAddress, + subject, + body, + List.of(), + EmailType.ENROLLMENT_TO_QUEUE_CONFIRMATION + ); } @Transactional @@ -234,7 +179,7 @@ public void sendPartiallyFreeEnrollmentConfirmationEmail( final FreeEnrollmentDetails freeEnrollmentDetails ) throws IOException, InterruptedException { final Map templateParams = withFreeEmailParams( - getEmailParams(enrollment), + getEmailParams(enrollment, enrollment.getExamEvent()), freeEnrollmentDetails, enrollment.getFreeEnrollment().getSource(), "enrollment" @@ -257,7 +202,15 @@ public void sendPartiallyFreeEnrollmentConfirmationEmail( ? List.of(createReceiptAttachment(enrollment, localeFI), createReceiptAttachment(enrollment, localeSV)) : List.of(); // for local development - createEmail(recipientName, recipientAddress, subject, body, attachments, EmailType.ENROLLMENT_CONFIRMATION); + createEmail( + emailService, + recipientName, + recipientAddress, + subject, + body, + attachments, + EmailType.ENROLLMENT_CONFIRMATION + ); } @Transactional @@ -267,7 +220,7 @@ public void sendPartiallyFreeEnrollmentToQueueConfirmationEmail( final FreeEnrollmentDetails freeEnrollmentDetails ) { final Map templateParams = withFreeEmailParams( - getEmailParams(enrollment), + getEmailParams(enrollment, enrollment.getExamEvent()), freeEnrollmentDetails, enrollment.getFreeEnrollment().getSource(), "queue" @@ -283,7 +236,15 @@ public void sendPartiallyFreeEnrollmentToQueueConfirmationEmail( ); final String body = templateRenderer.renderEnrollmentConfirmationEmailBody(templateParams); - createEmail(recipientName, recipientAddress, subject, body, List.of(), EmailType.ENROLLMENT_TO_QUEUE_CONFIRMATION); + createEmail( + emailService, + recipientName, + recipientAddress, + subject, + body, + List.of(), + EmailType.ENROLLMENT_TO_QUEUE_CONFIRMATION + ); } public Map withFreeEmailParams( @@ -319,4 +280,47 @@ public Map withFreeEmailParams( return freeParams; } + + @Transactional + public void sendEnrollmentAppointmentConfirmationEmail(final EnrollmentAppointment enrollmentAppointment) + throws IOException, InterruptedException { + final Map templateParams = getEmailParams( + enrollmentAppointment, + enrollmentAppointment.getExaminerExamEvent() + ); + final ExaminerExamEvent examEvent = enrollmentAppointment.getExaminerExamEvent(); + templateParams.put("examTime", examEvent.getExamTime()); + templateParams.put("examLocation", examEvent.getLocation()); + templateParams.put("otherInformation", examEvent.getOtherInformation()); + final Person person = enrollmentAppointment.getPerson(); + + final String recipientName = person.getFirstName() + " " + person.getLastName(); + final String recipientAddress = enrollmentAppointment.getEmail(); + final String subject = String.format( + "%s | %s", + LocalisationUtil.translate(localeFI, "subject.enrollment-appointment-confirmation"), + LocalisationUtil.translate(localeSV, "subject.enrollment-appointment-confirmation") + ); + final String body = templateRenderer.renderEnrollmentAppointmentConfirmationEmailBody(templateParams); + + final List attachments = environment.getRequiredProperty( + "app.email.sending-enabled", + Boolean.class + ) + ? List.of( + createReceiptAttachment(enrollmentAppointment, localeFI), + createReceiptAttachment(enrollmentAppointment, localeSV) + ) + : List.of(); // for local development + + createEmail( + emailService, + recipientName, + recipientAddress, + subject, + body, + attachments, + EmailType.ENROLLMENT_APPOINTMENT_CONFIRMATION + ); + } } diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentService.java index dde4150c7..2790bd5d2 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentService.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicEnrollmentService.java @@ -3,26 +3,37 @@ 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.PublicAppointmentExamDateDTO; 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.PublicExaminerNameDTO; 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.Examiner; +import fi.oph.vkt.model.ExaminerExamEvent; 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.EnrollmentAppointmentStatus; 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.ExaminerRepository; import fi.oph.vkt.repository.FreeEnrollmentRepository; import fi.oph.vkt.repository.ReservationRepository; import fi.oph.vkt.repository.UploadedFileAttachmentRepository; @@ -34,6 +45,7 @@ import fi.oph.vkt.util.exception.APIException; import fi.oph.vkt.util.exception.APIExceptionType; import fi.oph.vkt.util.exception.NotFoundException; +import java.io.IOException; import java.time.LocalDateTime; import java.util.List; import java.util.Map; @@ -49,6 +61,7 @@ public class PublicEnrollmentService extends AbstractEnrollmentService { private final EnrollmentRepository enrollmentRepository; + private final EnrollmentAppointmentRepository enrollmentAppointmentRepository; private final ExamEventRepository examEventRepository; private final PublicEnrollmentEmailService publicEnrollmentEmailService; private final PublicReservationService publicReservationService; @@ -58,6 +71,8 @@ public class PublicEnrollmentService extends AbstractEnrollmentService { private final FeatureFlagService featureFlagService; private final UploadedFileAttachmentRepository uploadedFileAttachmentRepository; private final KoskiService koskiService; + private final ExaminerRepository examinerRepository; + private final ContactEmailService contactEmailService; @Transactional public PublicEnrollmentInitialisationDTO initialiseEnrollment(final long examEventId, final Person person) { @@ -470,6 +485,13 @@ private void clearAddress(final Enrollment enrollment) { enrollment.setCountry(null); } + private void clearAddress(final EnrollmentAppointment enrollmentAppointment) { + enrollmentAppointment.setStreet(null); + enrollmentAppointment.setPostalCode(null); + enrollmentAppointment.setTown(null); + enrollmentAppointment.setCountry(null); + } + @Transactional public PublicEnrollmentDTO createEnrollmentToQueue( final PublicEnrollmentCreateDTO dto, @@ -593,4 +615,110 @@ public Map getPresignedPostRequest( return s3Service.getPresignedPostRequest(key, extension); } + + private PublicEnrollmentAppointmentDTO createEnrollmentAppointmentDTO( + final EnrollmentAppointment enrollmentAppointment + ) { + final ExaminerExamEvent examEvent = enrollmentAppointment.getExaminerExamEvent(); + final Examiner examiner = enrollmentAppointment.getExaminer(); + final Person person = enrollmentAppointment.getPerson(); + final PublicPersonDTO personDTO = person == null ? null : PersonUtil.createPublicPersonDTO(person); + final PublicExaminerNameDTO examinerNameDTO = PublicExaminerNameDTO + .builder() + .name(examiner.getNickname() + " " + examiner.getLastName()) + .build(); + final PublicAppointmentExamDateDTO examDateDTO = PublicAppointmentExamDateDTO + .builder() + .date(examEvent.getDate()) + .location(examEvent.getLocation()) + .examiner(examinerNameDTO) + .language(examEvent.getLanguage()) + .build(); + + return PublicEnrollmentAppointmentDTO + .builder() + .id(enrollmentAppointment.getId()) + .oralSkill(enrollmentAppointment.isOralSkill()) + .textualSkill(enrollmentAppointment.isTextualSkill()) + .understandingSkill(enrollmentAppointment.isUnderstandingSkill()) + .speakingPartialExam(enrollmentAppointment.isSpeakingPartialExam()) + .speechComprehensionPartialExam(enrollmentAppointment.isSpeechComprehensionPartialExam()) + .writingPartialExam(enrollmentAppointment.isWritingPartialExam()) + .readingComprehensionPartialExam(enrollmentAppointment.isReadingComprehensionPartialExam()) + .digitalCertificateConsent(enrollmentAppointment.isDigitalCertificateConsent()) + .email(enrollmentAppointment.getEmail()) + .phoneNumber(enrollmentAppointment.getPhoneNumber()) + .street(enrollmentAppointment.getStreet() == null ? "" : enrollmentAppointment.getStreet()) + .postalCode(enrollmentAppointment.getPostalCode() == null ? "" : enrollmentAppointment.getPostalCode()) + .town(enrollmentAppointment.getTown() == null ? "" : enrollmentAppointment.getTown()) + .country(enrollmentAppointment.getCountry() == null ? "" : enrollmentAppointment.getCountry()) + .status(enrollmentAppointment.getStatus()) + .person(personDTO) + .examEvent(examDateDTO) + .build(); + } + + @Transactional(readOnly = true) + public PublicEnrollmentAppointmentDTO getEnrollmentAppointment(final long enrollmentAppointmentId) { + final EnrollmentAppointment enrollmentAppointment = enrollmentAppointmentRepository.getReferenceById( + enrollmentAppointmentId + ); + + return createEnrollmentAppointmentDTO(enrollmentAppointment); + } + + @Transactional + public PublicEnrollmentAppointmentDTO saveEnrollmentAppointment( + final PublicEnrollmentAppointmentUpdateDTO dto, + final Person person + ) { + final EnrollmentAppointment enrollmentAppointment = enrollmentAppointmentRepository.getReferenceById(dto.id()); + + if (person.getId() != enrollmentAppointment.getPerson().getId()) { + throw new APIException(APIExceptionType.RESERVATION_PERSON_SESSION_MISMATCH); + } + + enrollmentAppointment.setPerson(person); + enrollmentAppointment.setStreet(dto.street()); + enrollmentAppointment.setPostalCode(dto.postalCode()); + enrollmentAppointment.setTown(dto.town()); + enrollmentAppointment.setCountry(dto.country()); + enrollmentAppointment.setPhoneNumber(dto.phoneNumber()); + + if (dto.digitalCertificateConsent()) { + clearAddress(enrollmentAppointment); + } + + enrollmentAppointmentRepository.saveAndFlush(enrollmentAppointment); + + return createEnrollmentAppointmentDTO(enrollmentAppointment); + } + + @Transactional + public void createEnrollmentContact(final PublicEnrollmentContactCreateDTO dto, final long examinerId) + throws IOException, InterruptedException { + final EnrollmentAppointment enrollmentAppointment = new EnrollmentAppointment(); + final Examiner examiner = examinerRepository.getReferenceById(examinerId); + + enrollmentAppointment.setStatus(EnrollmentAppointmentStatus.CONTACT_CREATED); + enrollmentAppointment.setExaminer(examiner); + copyDtoFieldsToEnrollment(enrollmentAppointment, dto); + + // Save contact request first to ensure we have a persisted ID for the enrollment appointment. + // This is needed to create a correct link to the contact request in the examiner's UI. + enrollmentAppointmentRepository.saveAndFlush(enrollmentAppointment); + + // Send emails to contact requester and the examiner. + contactEmailService.sendReceiptNotificationForContactRequest(enrollmentAppointment); + contactEmailService.sendExaminerNotificationOfContactRequest(enrollmentAppointment); + } + + public EnrollmentAppointment getEnrollmentAppointmentByIdAndPaymentLink( + final long enrollmentAppointmentId, + final String paymentLinkHash + ) { + return enrollmentAppointmentRepository + .findByIdAndPaymentLinkHashAndDeletedAtIsNull(enrollmentAppointmentId, paymentLinkHash) + .orElseThrow(); + } } diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/PublicExaminerService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicExaminerService.java new file mode 100644 index 000000000..791ea094f --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/PublicExaminerService.java @@ -0,0 +1,87 @@ +package fi.oph.vkt.service; + +import fi.oph.vkt.api.dto.PublicExaminerDTO; +import fi.oph.vkt.api.dto.PublicExaminerExamDateDTO; +import fi.oph.vkt.api.dto.PublicMunicipalityDTO; +import fi.oph.vkt.model.ExamEvent; +import fi.oph.vkt.model.Examiner; +import fi.oph.vkt.model.ExaminerExamEvent; +import fi.oph.vkt.model.Municipality; +import fi.oph.vkt.model.type.EnrollmentAppointmentStatus; +import fi.oph.vkt.model.type.ExamLanguage; +import fi.oph.vkt.repository.ExaminerRepository; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class PublicExaminerService { + + private final ExaminerRepository examinerRepository; + + private static PublicMunicipalityDTO toPublicMunicipalityDTO(final Municipality municipality) { + return PublicMunicipalityDTO.builder().fi(municipality.getNameFI()).sv(municipality.getNameSV()).build(); + } + + private static PublicExaminerExamDateDTO toPublicExaminerExamDateDTO(final ExaminerExamEvent examEvent) { + final boolean isFull = + examEvent.getMaxParticipants() != null && + examEvent.getMaxParticipants() <= + examEvent.getEnrollments().stream().filter(e -> e.getStatus() == EnrollmentAppointmentStatus.COMPLETED).count(); + + return PublicExaminerExamDateDTO.builder().examDate(examEvent.getDate()).isFull(isFull).build(); + } + + private static PublicExaminerDTO toPublicExaminerDTO(Examiner examiner) { + final List languages = new ArrayList<>(); + if (examiner.isExamLanguageFinnish()) { + languages.add(ExamLanguage.FI); + } + if (examiner.isExamLanguageSwedish()) { + languages.add(ExamLanguage.SV); + } + return PublicExaminerDTO + .builder() + .id(examiner.getId()) + .lastName(examiner.getLastName()) + .firstName(examiner.getNickname()) + .languages(languages) + .municipalities( + examiner + .getMunicipalities() + .stream() + .map(PublicExaminerService::toPublicMunicipalityDTO) + .collect(Collectors.toList()) + ) + .examDates( + examiner + .getExamEvents() + .stream() + .filter(e -> !e.isHidden() && !e.getDate().isBefore(LocalDate.now())) + .map(PublicExaminerService::toPublicExaminerExamDateDTO) + .collect(Collectors.toList()) + ) + .build(); + } + + @Transactional(readOnly = true) + public List listExaminers() { + return examinerRepository + .getAllByDeletedAtIsNullAndIsPublicIsTrue() + .stream() + .map(PublicExaminerService::toPublicExaminerDTO) + .collect(Collectors.toList()); + } + + @Transactional(readOnly = true) + public PublicExaminerDTO getExaminer(final long examinerId) { + final Examiner examiner = examinerRepository.getReferenceById(examinerId); + + return toPublicExaminerDTO(examiner); + } +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/onr/OnrOperationApi.java b/backend/vkt/src/main/java/fi/oph/vkt/service/onr/OnrOperationApi.java new file mode 100644 index 000000000..d49d197fd --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/onr/OnrOperationApi.java @@ -0,0 +1,8 @@ +package fi.oph.vkt.service.onr; + +import java.util.List; +import java.util.Map; + +public interface OnrOperationApi { + Map fetchPersonalDatas(List onrIds) throws Exception; +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/onr/OnrOperationApiImpl.java b/backend/vkt/src/main/java/fi/oph/vkt/service/onr/OnrOperationApiImpl.java new file mode 100644 index 000000000..e0a80fd34 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/onr/OnrOperationApiImpl.java @@ -0,0 +1,84 @@ +package fi.oph.vkt.service.onr; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import fi.oph.vkt.config.Constants; +import fi.vm.sade.javautils.nio.cas.CasClient; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import net.minidev.json.JSONArray; +import org.asynchttpclient.Request; +import org.asynchttpclient.RequestBuilder; +import org.asynchttpclient.Response; +import org.asynchttpclient.util.HttpConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +public class OnrOperationApiImpl implements OnrOperationApi { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final Logger LOG = LoggerFactory.getLogger(OnrOperationApiImpl.class); + + private final CasClient onrClient; + + private final String onrServiceUrl; + + public OnrOperationApiImpl(final CasClient onrClient, final String onrServiceUrl) { + this.onrClient = onrClient; + this.onrServiceUrl = onrServiceUrl; + + OBJECT_MAPPER.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true); + } + + @Override + public Map fetchPersonalDatas(final List onrIds) throws Exception { + // /henkilo/masterHenkilosByOidList might be usable as an endpoint for fetching master person data for persons + // which have been marked passive + final Request request = defaultRequestBuilder() + .setUrl(onrServiceUrl + "/henkilo/henkilotByHenkiloOidList") + .setMethod(HttpConstants.Methods.POST) + .setBody(JSONArray.toJSONString(onrIds)) + .build(); + + final Response response = onrClient.executeBlocking(request); + + if (response.getStatusCode() == HttpStatus.OK.value()) { + final List personalDataDTOS = OBJECT_MAPPER.readValue( + response.getResponseBody(), + new TypeReference<>() {} + ); + + final Map personalDatas = new HashMap<>(); + personalDataDTOS.forEach(dto -> personalDatas.put(dto.getOnrId(), createPersonalData(dto))); + return personalDatas; + } else { + throw new RuntimeException( + "ONR service called with POST /henkilo/henkilotByHenkiloOidList returned unexpected status code: " + + response.getStatusCode() + ); + } + } + + private PersonalData createPersonalData(final PersonalDataDTO personalDataDTO) { + return PersonalData + .builder() + .onrId(personalDataDTO.getOnrId()) + .lastName(personalDataDTO.getLastName()) + .firstName(personalDataDTO.getFirstName()) + .nickname(personalDataDTO.getNickname()) + .build(); + } + + private RequestBuilder defaultRequestBuilder() { + return new RequestBuilder() + .addHeader("Accept", MediaType.APPLICATION_JSON_VALUE) + .addHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE) + .addHeader("Caller-Id", Constants.CALLER_ID) + .setRequestTimeout((int) TimeUnit.MINUTES.toMillis(2)); + } +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/onr/OnrService.java b/backend/vkt/src/main/java/fi/oph/vkt/service/onr/OnrService.java new file mode 100644 index 000000000..38c47a0ba --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/onr/OnrService.java @@ -0,0 +1,28 @@ +package fi.oph.vkt.service.onr; + +import jakarta.annotation.Resource; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class OnrService { + + private static final Logger LOG = LoggerFactory.getLogger(OnrService.class); + + @Resource + private final OnrOperationApi api; + + public Map getOnrPersonalData(final List onrIds) { + try { + return api.fetchPersonalDatas(onrIds); + } catch (final Exception e) { + LOG.error("Fetching personal data from ONR failed", e); + return Map.of(); + } + } +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/onr/PersonalData.java b/backend/vkt/src/main/java/fi/oph/vkt/service/onr/PersonalData.java new file mode 100644 index 000000000..d72613ae9 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/onr/PersonalData.java @@ -0,0 +1,24 @@ +package fi.oph.vkt.service.onr; + +import lombok.Builder; +import lombok.Getter; +import lombok.NonNull; +import lombok.Setter; + +@Builder +@Getter +@Setter +public class PersonalData { + + // Always returned from ONR + private String onrId; + + @NonNull + private String lastName; + + @NonNull + private String firstName; + + @NonNull + private String nickname; +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/onr/PersonalDataDTO.java b/backend/vkt/src/main/java/fi/oph/vkt/service/onr/PersonalDataDTO.java new file mode 100644 index 000000000..1853774c5 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/onr/PersonalDataDTO.java @@ -0,0 +1,24 @@ +package fi.oph.vkt.service.onr; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@JsonIgnoreProperties(ignoreUnknown = true) +public class PersonalDataDTO { + + @JsonProperty("oidHenkilo") + private String onrId; + + @JsonProperty("sukunimi") + private String lastName; + + @JsonProperty("etunimet") + private String firstName; + + @JsonProperty("kutsumanimi") + private String nickname; +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/onr/mock/MockOnrOperationApiImpl.java b/backend/vkt/src/main/java/fi/oph/vkt/service/onr/mock/MockOnrOperationApiImpl.java new file mode 100644 index 000000000..830b6e81e --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/onr/mock/MockOnrOperationApiImpl.java @@ -0,0 +1,29 @@ +package fi.oph.vkt.service.onr.mock; + +import fi.oph.vkt.service.onr.OnrOperationApi; +import fi.oph.vkt.service.onr.PersonalData; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class MockOnrOperationApiImpl implements OnrOperationApi { + + // Cache personal data in a cache to try and ensure we return the same data per oid + // at least during the lifetime of the current JVM process + final Map personalDataCache = new HashMap<>(); + final PersonalDataFactory personalDataFactory = new PersonalDataFactory(); + + @Override + public Map fetchPersonalDatas(final List onrIds) { + HashMap datas = new HashMap<>(); + for (String onrId : onrIds) { + if (!personalDataCache.containsKey(onrId)) { + personalDataCache.put(onrId, personalDataFactory.create(onrId)); + } + datas.put(onrId, personalDataCache.get(onrId)); + } + return datas; + } +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/onr/mock/PersonalDataFactory.java b/backend/vkt/src/main/java/fi/oph/vkt/service/onr/mock/PersonalDataFactory.java new file mode 100644 index 000000000..5beef2fc0 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/onr/mock/PersonalDataFactory.java @@ -0,0 +1,162 @@ +package fi.oph.vkt.service.onr.mock; + +import fi.oph.vkt.service.onr.PersonalData; +import fi.oph.vkt.util.CyclicIterable; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; + +public class PersonalDataFactory { + + private final Random rng = new Random(); + private final AtomicInteger counter = new AtomicInteger(rng.nextInt()); + + public PersonalData create(final String onrId) { + final int counterValue = counter.incrementAndGet(); + + final String lastName = lastNames.next(); + final boolean isMale = counterValue % 2 == 0; + final String nickname = isMale ? maleNicknames.next() : femaleNicknames.next(); + final String secondName = isMale ? maleSecondNames.next() : femaleSecondNames.next(); + + return PersonalData + .builder() + .onrId(onrId) + .lastName(lastName) + .firstName(nickname + " " + secondName) + .nickname(nickname) + .build(); + } + + private static Iterator cyclicIterator(final String... values) { + return new CyclicIterable<>(Arrays.asList(values)).iterator(); + } + + private final Iterator maleNicknames = cyclicIterator( + "Antti", + "Eero", + "Ilkka", + "Jari", + "Juha", + "Matti", + "Pekka", + "Timo", + "Iiro", + "Jukka", + "Hugo", + "Jaakko", + "Lasse", + "Kyösti", + "Markku", + "Kristian", + "Mikael", + "Nooa", + "Otto", + "Olli", + "Aapo" + ); + + private final Iterator maleSecondNames = cyclicIterator( + "Kalle", + "Kari", + "Marko", + "Mikko", + "Tapani", + "Ville", + "Jesse", + "Joose", + "Sakari", + "Tero", + "Samu", + "Roope", + "Panu", + "Matias", + "Seppo", + "Rauno", + "Aapeli" + ); + + private final Iterator femaleNicknames = cyclicIterator( + "Anneli", + "Ella", + "Hanna", + "Iiris", + "Liisa", + "Maria", + "Ninni", + "Viivi", + "Sointu", + "Ulla", + "Varpu", + "Raili", + "Neea", + "Noora", + "Mirka", + "Oona", + "Jonna", + "Jaana", + "Katja", + "Jenni", + "Reija" + ); + + private final Iterator femaleSecondNames = cyclicIterator( + "Anna", + "Iida", + "Kerttu", + "Kristiina", + "Marjatta", + "Ronja", + "Sara", + "Helena", + "Aino", + "Erika", + "Emmi", + "Aada", + "Eveliina", + "Nanna", + "Olga", + "Inkeri", + "Petra" + ); + + private final Iterator lastNames = cyclicIterator( + "Aaltonen", + "Alanen", + "Eskola", + "Hakala", + "Heikkinen", + "Heinonen", + "Hiltunen", + "Hirvonen", + "Hämäläinen", + "Kallio", + "Karjalainen", + "Kinnunen", + "Korhonen", + "Koskinen", + "Laakso", + "Lahtinen", + "Laine", + "Lehtonen", + "Leinonen", + "Leppänen", + "Manninen", + "Mattila", + "Mäkinen", + "Nieminen", + "Noronen", + "Ojala", + "Paavola", + "Pitkänen", + "Räsänen", + "Saarinen", + "Salo", + "Salonen", + "Toivonen", + "Tuominen", + "Turunen", + "Valtonen", + "Virtanen", + "Väisänen" + ); +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/receipt/ReceiptData.java b/backend/vkt/src/main/java/fi/oph/vkt/service/receipt/ReceiptData.java index dc9d4161b..21a4108a7 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/service/receipt/ReceiptData.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/receipt/ReceiptData.java @@ -12,5 +12,6 @@ public record ReceiptData( @NonNull String exam, @NonNull String participant, @NonNull String totalAmount, - @NonNull List items + @NonNull List items, + String examiner ) {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/service/receipt/ReceiptRenderer.java b/backend/vkt/src/main/java/fi/oph/vkt/service/receipt/ReceiptRenderer.java index 2cbc195f1..bca1551a4 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/service/receipt/ReceiptRenderer.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/service/receipt/ReceiptRenderer.java @@ -5,10 +5,7 @@ import com.github.jhonnymertz.wkhtmltopdf.wrapper.Pdf; import com.github.jhonnymertz.wkhtmltopdf.wrapper.configurations.WrapperConfig; import fi.oph.vkt.api.dto.FreeEnrollmentDetails; -import fi.oph.vkt.model.Enrollment; -import fi.oph.vkt.model.ExamEvent; -import fi.oph.vkt.model.Payment; -import fi.oph.vkt.model.Person; +import fi.oph.vkt.model.*; import fi.oph.vkt.model.type.ExamLanguage; import fi.oph.vkt.model.type.ExamLevel; import fi.oph.vkt.repository.EnrollmentRepository; @@ -40,12 +37,19 @@ public class ReceiptRenderer { private final TemplateRenderer templateRenderer; @Transactional(readOnly = true) - public ReceiptData getReceiptData(final long enrollmentId, final Locale locale) { - final Enrollment enrollment = enrollmentRepository.getReferenceById(enrollmentId); - final ExamEvent examEvent = enrollment.getExamEvent(); + public ReceiptData getReceiptData(final EnrollmentCommon enrollment, final Locale locale) { + final boolean isEnrollment = enrollment instanceof Enrollment; + final ExamEventCommon examEvent = isEnrollment + ? ((Enrollment) enrollment).getExamEvent() + : ((EnrollmentAppointment) enrollment).getExaminerExamEvent(); final Person person = enrollment.getPerson(); - final Payment payment = enrollment.getPayments().get(0); - final FreeEnrollmentDetails freeEnrollmentDetails = enrollmentRepository.countEnrollmentsByPerson(person); + final List payments = isEnrollment + ? ((Enrollment) enrollment).getPayments() + : ((EnrollmentAppointment) enrollment).getPayments(); + final Payment payment = payments.get(0); + final FreeEnrollmentDetails freeEnrollmentDetails = isEnrollment + ? enrollmentRepository.countEnrollmentsByPerson(person) + : null; final ExamLanguage examLanguage = examEvent.getLanguage(); @@ -64,6 +68,12 @@ public ReceiptData getReceiptData(final long enrollmentId, final Locale locale) final List items = getReceiptItems(enrollment, examLanguage, locale, freeEnrollmentDetails); + String examinerName = null; + if (!isEnrollment) { + final Examiner examiner = ((EnrollmentAppointment) enrollment).getExaminer(); + examinerName = examiner.getNickname() + " " + examiner.getLastName(); + } + return ReceiptData .builder() .date(date) @@ -73,6 +83,7 @@ public ReceiptData getReceiptData(final long enrollmentId, final Locale locale) .participant(participant) .totalAmount(totalAmount) .items(items) + .examiner(examinerName) .build(); } @@ -82,21 +93,21 @@ private static String getLang(final ExamLanguage examLanguage, final Locale loca return StringUtils.capitalize(LocalisationUtil.translate(locale, key)); } - private static String getLevelDescription(final ExamEvent examEvent, final Locale locale) { - final String key = examEvent.getLevel() == ExamLevel.EXCELLENT ? "examLevel.excellent" : "-"; + private static String getLevelDescription(final ExamEventCommon examEvent, final Locale locale) { + final String key = examEvent instanceof ExamEvent ? "examLevel.excellent" : "examLevel.goodAndSatisfactory"; return LocalisationUtil.translate(locale, key); } private static List getReceiptItems( - final Enrollment enrollment, + final EnrollmentCommon enrollment, final ExamLanguage examLanguage, final Locale locale, final FreeEnrollmentDetails freeEnrollmentDetails ) { final String examLanguageKey = examLanguage == ExamLanguage.FI ? "lang.finnish" : "lang.swedish"; final String examLanguageName = LocalisationUtil.translate(locale, examLanguageKey); - + final boolean isEnrollment = enrollment instanceof Enrollment; return Stream .of( Optional.ofNullable( @@ -104,7 +115,14 @@ private static List getReceiptItems( ? ReceiptItem .builder() .name(StringUtils.capitalize(LocalisationUtil.translate(locale, "skill.textual", examLanguageName))) - .amount(String.format("%s €", EnrollmentUtil.getTextualSkillFee(enrollment, freeEnrollmentDetails) / 100)) + .amount( + String.format( + "%s €", + isEnrollment + ? EnrollmentUtil.getTextualSkillFee((Enrollment) enrollment, freeEnrollmentDetails) / 100 + : EnrollmentUtil.getTextualSkillFee((EnrollmentAppointment) enrollment) / 100 + ) + ) .build() : null ), @@ -113,7 +131,14 @@ private static List getReceiptItems( ? ReceiptItem .builder() .name(StringUtils.capitalize(LocalisationUtil.translate(locale, "skill.oral", examLanguageName))) - .amount(String.format("%s €", EnrollmentUtil.getOralSkillFee(enrollment, freeEnrollmentDetails) / 100)) + .amount( + String.format( + "%s €", + isEnrollment + ? EnrollmentUtil.getOralSkillFee((Enrollment) enrollment, freeEnrollmentDetails) / 100 + : EnrollmentUtil.getOralSkillFee((EnrollmentAppointment) enrollment) / 100 + ) + ) .build() : null ), @@ -122,7 +147,14 @@ private static List getReceiptItems( ? ReceiptItem .builder() .name(StringUtils.capitalize(LocalisationUtil.translate(locale, "skill.understanding", examLanguageName))) - .amount(String.format("%s €", EnrollmentUtil.getUnderstandingSkillFee(enrollment) / 100)) + .amount( + String.format( + "%s €", + isEnrollment + ? EnrollmentUtil.getUnderstandingSkillFee((Enrollment) enrollment) / 100 + : EnrollmentUtil.getUnderstandingSkillFee((EnrollmentAppointment) enrollment) / 100 + ) + ) .build() : null ) diff --git a/backend/vkt/src/main/java/fi/oph/vkt/util/AuthorizationUtil.java b/backend/vkt/src/main/java/fi/oph/vkt/util/AuthorizationUtil.java new file mode 100644 index 000000000..c0483f0ce --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/util/AuthorizationUtil.java @@ -0,0 +1,13 @@ +package fi.oph.vkt.util; + +import org.springframework.security.core.Authentication; + +public class AuthorizationUtil { + + public static boolean hasRole(final Authentication authentication, final String role) { + return authentication + .getAuthorities() + .stream() + .anyMatch((grantedAuthority -> grantedAuthority.getAuthority().equals("ROLE_" + role))); + } +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/util/ClerkEnrollmentUtil.java b/backend/vkt/src/main/java/fi/oph/vkt/util/ClerkEnrollmentUtil.java index b31260064..fe4c22f11 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/util/ClerkEnrollmentUtil.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/util/ClerkEnrollmentUtil.java @@ -4,12 +4,20 @@ import fi.oph.vkt.api.dto.FreeEnrollmentDetails; import fi.oph.vkt.api.dto.FreeEnrollmentDetailsDTO; import fi.oph.vkt.api.dto.KoskiEducationsDTO; +import fi.oph.vkt.api.dto.clerk.ClerkEnrollmentContactRequestDTO; import fi.oph.vkt.api.dto.clerk.ClerkEnrollmentDTO; import fi.oph.vkt.api.dto.clerk.ClerkFreeEnrollmentBasisDTO; import fi.oph.vkt.api.dto.clerk.ClerkPaymentDTO; import fi.oph.vkt.api.dto.clerk.ClerkPersonDTO; +import fi.oph.vkt.api.dto.examiner.ExaminerAuthLinkDTO; +import fi.oph.vkt.api.dto.examiner.ExaminerEnrollmentAppointmentDTO; +import fi.oph.vkt.api.dto.examiner.ExaminerEnrollmentAppointmentHistoryDTO; +import fi.oph.vkt.api.dto.examiner.ExaminerExamEventDTO; import fi.oph.vkt.audit.dto.ClerkEnrollmentAuditDTO; import fi.oph.vkt.model.Enrollment; +import fi.oph.vkt.model.EnrollmentAppointment; +import fi.oph.vkt.model.EnrollmentGrade; +import fi.oph.vkt.model.Examiner; import fi.oph.vkt.model.FreeEnrollment; import fi.oph.vkt.model.KoskiEducations; import fi.oph.vkt.model.Person; @@ -144,4 +152,114 @@ public static KoskiEducationsDTO createKoskiEducationsDTO(final KoskiEducations .other(koskiEducations.getOther()) .build(); } + + public static String getAuthUrl(final String baseUrlAPI, final long id, final String hash) { + return String.format("%s/enrollment/appointment/%d/redirect/%s", baseUrlAPI, id, hash); + } + + public static ExaminerEnrollmentAppointmentDTO createClerkEnrollmentAppointmentDTO( + final EnrollmentAppointment enrollmentAppointment, + final String baseUrlAPI + ) { + final List paymentDTOs = enrollmentAppointment + .getPayments() + .stream() + .map(ClerkPaymentUtil::createClerkPaymentDTO) + .sorted(Comparator.comparing(ClerkPaymentDTO::createdAt).reversed()) + .toList(); + + final ExaminerAuthLinkDTO examinerAuthLinkDTO = enrollmentAppointment.getAuthHash() != null + ? ExaminerAuthLinkDTO + .builder() + .url(getAuthUrl(baseUrlAPI, enrollmentAppointment.getId(), enrollmentAppointment.getAuthHash())) + .expiresAt(enrollmentAppointment.getExpiresAt()) + .sentAt(enrollmentAppointment.getSentAt()) + .build() + : null; + + final String paymentLinkUrl = String.format( + "%s/enrollment/appointment/%d/redirectPayment/%s", + baseUrlAPI, + enrollmentAppointment.getId(), + enrollmentAppointment.getPaymentLinkHash() + ); + + final ExaminerExamEventDTO examinerExamEventDTO = enrollmentAppointment.getExaminerExamEvent() != null + ? ExaminerUtil.toExaminerExamEventWithoutEnrollmentsDTO(enrollmentAppointment.getExaminerExamEvent()) + : null; + + return ExaminerEnrollmentAppointmentDTO + .builder() + .id(enrollmentAppointment.getId()) + .version(enrollmentAppointment.getVersion()) + .enrollmentTime(enrollmentAppointment.getCreatedAt()) + .oralSkill(enrollmentAppointment.isOralSkill()) + .textualSkill(enrollmentAppointment.isTextualSkill()) + .understandingSkill(enrollmentAppointment.isUnderstandingSkill()) + .speakingPartialExam(enrollmentAppointment.isSpeakingPartialExam()) + .speechComprehensionPartialExam(enrollmentAppointment.isSpeechComprehensionPartialExam()) + .writingPartialExam(enrollmentAppointment.isWritingPartialExam()) + .readingComprehensionPartialExam(enrollmentAppointment.isReadingComprehensionPartialExam()) + .street(enrollmentAppointment.getStreet()) + .postalCode(enrollmentAppointment.getPostalCode()) + .town(enrollmentAppointment.getTown()) + .country(enrollmentAppointment.getCountry()) + .status(enrollmentAppointment.getStatus()) + .email(enrollmentAppointment.getEmail()) + .phoneNumber(enrollmentAppointment.getPhoneNumber()) + .firstName(enrollmentAppointment.getFirstName()) + .lastName(enrollmentAppointment.getLastName()) + .authLink(examinerAuthLinkDTO) + .paymentLinkUrl(paymentLinkUrl) + .examEvent(examinerExamEventDTO) + .payments(paymentDTOs) + .hasPreviousEnrollment(enrollmentAppointment.isHasPreviousEnrollment()) + .previousEnrollment(enrollmentAppointment.getPreviousEnrollment()) + .build(); + } + + public static ClerkEnrollmentContactRequestDTO createClerkEnrollmentContactDTO( + final EnrollmentAppointment enrollmentAppointment + ) { + return ClerkEnrollmentContactRequestDTO + .builder() + .id(enrollmentAppointment.getId()) + .version(enrollmentAppointment.getVersion()) + .enrollmentTime(enrollmentAppointment.getCreatedAt()) + .isFullExam(enrollmentAppointment.getPartialExamSelection() == null) + .partialExamSelection(enrollmentAppointment.getPartialExamSelection()) + .status(enrollmentAppointment.getStatus()) + .phoneNumber(enrollmentAppointment.getPhoneNumber()) + .email(enrollmentAppointment.getEmail()) + .firstName(enrollmentAppointment.getFirstName()) + .lastName(enrollmentAppointment.getLastName()) + .hasPreviousEnrollment(enrollmentAppointment.isHasPreviousEnrollment()) + .message(enrollmentAppointment.getMessage()) + .build(); + } + + public static ExaminerEnrollmentAppointmentHistoryDTO createClerkEnrollmentAppointmentHistoryDTO( + final EnrollmentAppointment enrollmentAppointment + ) { + final Examiner examiner = enrollmentAppointment.getExaminer(); + final ExaminerExamEventDTO examinerExamEventDTO = enrollmentAppointment.getExaminerExamEvent() != null + ? ExaminerUtil.toExaminerExamEventWithoutEnrollmentsDTO(enrollmentAppointment.getExaminerExamEvent()) + : null; + final EnrollmentGrade grade = enrollmentAppointment.getGrade(); + + return ExaminerEnrollmentAppointmentHistoryDTO + .builder() + .enrollmentTime(enrollmentAppointment.getCreatedAt()) + .oralSkill(enrollmentAppointment.isOralSkill()) + .textualSkill(enrollmentAppointment.isTextualSkill()) + .understandingSkill(enrollmentAppointment.isUnderstandingSkill()) + .speakingPartialExam(enrollmentAppointment.isSpeakingPartialExam()) + .speechComprehensionPartialExam(enrollmentAppointment.isSpeechComprehensionPartialExam()) + .writingPartialExam(enrollmentAppointment.isWritingPartialExam()) + .readingComprehensionPartialExam(enrollmentAppointment.isReadingComprehensionPartialExam()) + .examEvent(examinerExamEventDTO) + .examinerName(examiner.getNickname() + " " + examiner.getLastName()) + .grades(grade != null ? ExaminerUtil.createGradesDTO(grade) : null) + .build(); + } } diff --git a/backend/vkt/src/main/java/fi/oph/vkt/util/CyclicIterable.java b/backend/vkt/src/main/java/fi/oph/vkt/util/CyclicIterable.java new file mode 100644 index 000000000..0bf473751 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/util/CyclicIterable.java @@ -0,0 +1,37 @@ +package fi.oph.vkt.util; + +import java.util.Iterator; +import java.util.List; + +public class CyclicIterable implements Iterable { + + private final List coll; + + private int index = 0; + + public CyclicIterable(final List coll) { + this.coll = coll; + } + + public Iterator iterator() { + return new Iterator<>() { + @Override + public boolean hasNext() { + return true; + } + + @Override + public T next() { + if (index >= coll.size()) { + index = 0; + } + return coll.get(index++); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/util/EnrollmentUtil.java b/backend/vkt/src/main/java/fi/oph/vkt/util/EnrollmentUtil.java index 779705bfa..a30711812 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/util/EnrollmentUtil.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/util/EnrollmentUtil.java @@ -2,6 +2,7 @@ import fi.oph.vkt.api.dto.FreeEnrollmentDetails; import fi.oph.vkt.model.Enrollment; +import fi.oph.vkt.model.EnrollmentAppointment; import fi.oph.vkt.model.Person; import fi.oph.vkt.model.type.ExamLevel; import java.util.regex.Matcher; @@ -10,8 +11,17 @@ public class EnrollmentUtil { private static final int SKILL_FEE = 25700; + private static final int SKILL_APPOINTMENT_FEE = 12900; public static final Integer FREE_ENROLLMENT_LIMIT = 3; + public static int getTotalFee(final EnrollmentAppointment enrollmentAppointment) { + return ( + getTextualSkillFee(enrollmentAppointment) + + getOralSkillFee(enrollmentAppointment) + + getUnderstandingSkillFee(enrollmentAppointment) + ); + } + public static int getTotalFee(final Enrollment enrollment, final FreeEnrollmentDetails freeEnrollmentDetails) { return ( getTextualSkillFee(enrollment, freeEnrollmentDetails) + @@ -20,6 +30,10 @@ public static int getTotalFee(final Enrollment enrollment, final FreeEnrollmentD ); } + public static int getTextualSkillFee(final EnrollmentAppointment enrollmentAppointment) { + return enrollmentAppointment.isTextualSkill() ? SKILL_APPOINTMENT_FEE : 0; + } + public static int getTextualSkillFee(final Enrollment enrollment, final FreeEnrollmentDetails freeEnrollmentDetails) { if (!enrollment.isTextualSkill()) { return 0; @@ -30,6 +44,10 @@ public static int getTextualSkillFee(final Enrollment enrollment, final FreeEnro : SKILL_FEE; } + public static int getOralSkillFee(final EnrollmentAppointment enrollmentAppointment) { + return enrollmentAppointment.isOralSkill() ? SKILL_APPOINTMENT_FEE : 0; + } + public static int getOralSkillFee(final Enrollment enrollment, final FreeEnrollmentDetails freeEnrollmentDetails) { if (!enrollment.isOralSkill()) { return 0; @@ -52,6 +70,14 @@ public static boolean validateAttachmentId(final String attachmentId, final Pers return parsedExamEventId.equals(examEventId) && matcher.group(2).equals(person.getUuid().toString()); } + public static int getUnderstandingSkillFee(final EnrollmentAppointment enrollmentAppointment) { + if (enrollmentAppointment.isTextualSkill() && enrollmentAppointment.isOralSkill()) { + return 0; + } + + return SKILL_APPOINTMENT_FEE; + } + public static int getUnderstandingSkillFee(final Enrollment enrollment) { if (enrollment.isTextualSkill() && enrollment.isOralSkill()) { return 0; diff --git a/backend/vkt/src/main/java/fi/oph/vkt/util/ExaminerUtil.java b/backend/vkt/src/main/java/fi/oph/vkt/util/ExaminerUtil.java new file mode 100644 index 000000000..91342bf6f --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/util/ExaminerUtil.java @@ -0,0 +1,130 @@ +package fi.oph.vkt.util; + +import fi.oph.vkt.api.dto.EnrollmentGradeDTO; +import fi.oph.vkt.api.dto.MunicipalityDTO; +import fi.oph.vkt.api.dto.examiner.ExaminerContactRequestDTO; +import fi.oph.vkt.api.dto.examiner.ExaminerDetailsDTO; +import fi.oph.vkt.api.dto.examiner.ExaminerEnrollmentGradesDTO; +import fi.oph.vkt.api.dto.examiner.ExaminerExamEventDTO; +import fi.oph.vkt.model.EnrollmentAppointment; +import fi.oph.vkt.model.EnrollmentGrade; +import fi.oph.vkt.model.Examiner; +import fi.oph.vkt.model.ExaminerExamEvent; +import fi.oph.vkt.model.Municipality; +import fi.oph.vkt.model.type.EnrollmentGradeType; +import java.util.List; + +public class ExaminerUtil { + + public static MunicipalityDTO toMunicipalityDTO(final Municipality municipality) { + return MunicipalityDTO.builder().code(municipality.getCode()).build(); + } + + public static ExaminerContactRequestDTO toContactRequestDTO(final EnrollmentAppointment enrollmentAppointment) { + return ExaminerContactRequestDTO + .builder() + .id(enrollmentAppointment.getId()) + .firstName(enrollmentAppointment.getFirstName()) + .lastName(enrollmentAppointment.getLastName()) + .build(); + } + + public static ExaminerExamEventDTO toExaminerExamEventWithoutEnrollmentsDTO( + final ExaminerExamEvent examinerExamEvent + ) { + return ExaminerExamEventDTO + .builder() + .id(examinerExamEvent.getId()) + .version(examinerExamEvent.getVersion()) + .date(examinerExamEvent.getDate()) + .language(examinerExamEvent.getLanguage()) + .isHidden(examinerExamEvent.isHidden()) + .municipality(toMunicipalityDTO(examinerExamEvent.getMunicipality())) + .location(examinerExamEvent.getLocation()) + .examTime(examinerExamEvent.getExamTime()) + .otherInformation(examinerExamEvent.getOtherInformation()) + .registrationCloses(examinerExamEvent.getRegistrationCloses()) + .maxParticipants(examinerExamEvent.getMaxParticipants()) + .enrollments(List.of()) + .build(); + } + + public static ExaminerExamEventDTO toExaminerExamEventDTO( + final ExaminerExamEvent examinerExamEvent, + final String baseUrlAPI + ) { + return ExaminerExamEventDTO + .builder() + .id(examinerExamEvent.getId()) + .version(examinerExamEvent.getVersion()) + .date(examinerExamEvent.getDate()) + .language(examinerExamEvent.getLanguage()) + .isHidden(examinerExamEvent.isHidden()) + .municipality(toMunicipalityDTO(examinerExamEvent.getMunicipality())) + .location(examinerExamEvent.getLocation()) + .examTime(examinerExamEvent.getExamTime()) + .otherInformation(examinerExamEvent.getOtherInformation()) + .registrationCloses(examinerExamEvent.getRegistrationCloses()) + .maxParticipants(examinerExamEvent.getMaxParticipants()) + .enrollments( + examinerExamEvent + .getEnrollments() + .stream() + .map(e -> ClerkEnrollmentUtil.createClerkEnrollmentAppointmentDTO(e, baseUrlAPI)) + .toList() + ) + .build(); + } + + public static ExaminerDetailsDTO toExaminerDetailsDTO( + final Examiner examiner, + final List enrollmentAppointments, + final String baseUrlAPI + ) { + return ExaminerDetailsDTO + .builder() + .id(examiner.getId()) + .version(examiner.getVersion()) + .oid(examiner.getOid()) + .lastName(examiner.getLastName()) + .firstName(examiner.getFirstName()) + .email(examiner.getEmail()) + .phoneNumber(examiner.getPhoneNumber()) + .municipalities(examiner.getMunicipalities().stream().map(ExaminerUtil::toMunicipalityDTO).toList()) + .isPublic(examiner.isPublic()) + .examLanguageFinnish(examiner.isExamLanguageFinnish()) + .examLanguageSwedish(examiner.isExamLanguageSwedish()) + .examEvents(examiner.getExamEvents().stream().map(e -> toExaminerExamEventDTO(e, baseUrlAPI)).toList()) + .contactRequests(enrollmentAppointments.stream().map(ExaminerUtil::toContactRequestDTO).toList()) + .build(); + } + + private static EnrollmentGradeDTO createGradeDTO(final EnrollmentGradeType grade, final String comment) { + return grade == null ? null : EnrollmentGradeDTO.builder().grade(grade).comment(comment).build(); + } + + public static ExaminerEnrollmentGradesDTO createGradesDTO(final EnrollmentGrade enrollmentGrade) { + return ExaminerEnrollmentGradesDTO + .builder() + .version(enrollmentGrade.getVersion()) + .writingPartialExam( + createGradeDTO(enrollmentGrade.getWritingPartialExamGrade(), enrollmentGrade.getWritingPartialExamComment()) + ) + .readingComprehensionPartialExam( + createGradeDTO( + enrollmentGrade.getReadingComprehensionPartialExamGrade(), + enrollmentGrade.getReadingComprehensionPartialExamComment() + ) + ) + .speakingPartialExam( + createGradeDTO(enrollmentGrade.getSpeakingPartialExamGrade(), enrollmentGrade.getSpeakingPartialExamComment()) + ) + .speechComprehensionPartialExam( + createGradeDTO( + enrollmentGrade.getSpeechComprehensionPartialExamGrade(), + enrollmentGrade.getSpeechComprehensionPartialExamComment() + ) + ) + .build(); + } +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/util/SessionUtil.java b/backend/vkt/src/main/java/fi/oph/vkt/util/SessionUtil.java index 1b2790b8f..2c3e394a9 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/util/SessionUtil.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/util/SessionUtil.java @@ -7,6 +7,7 @@ public class SessionUtil { private static final String PERSON_ID_SESSION_KEY = "person_id"; + private static final String APPOINTMENT_ID_SESSION_KEY = "appointment_id"; public static boolean hasPersonId(final HttpSession session) { return session.getAttribute(PERSON_ID_SESSION_KEY) != null; @@ -25,4 +26,19 @@ public static Long getPersonId(final HttpSession session) { public static void setPersonId(final HttpSession session, final Long personId) { session.setAttribute(PERSON_ID_SESSION_KEY, personId); } + + public static void setAppointmentId(final HttpSession session, final Long appointmentId) { + session.setAttribute(APPOINTMENT_ID_SESSION_KEY, appointmentId); + final Long id = (Long) session.getAttribute(APPOINTMENT_ID_SESSION_KEY); + } + + public static Long getAppointmentId(final HttpSession session) { + final Long appointmentId = (Long) session.getAttribute(APPOINTMENT_ID_SESSION_KEY); + + if (appointmentId == null) { + throw new APIException(APIExceptionType.SESSION_MISSING_APPOINTMENT_ID); + } + + return appointmentId; + } } diff --git a/backend/vkt/src/main/java/fi/oph/vkt/util/TemplateRenderer.java b/backend/vkt/src/main/java/fi/oph/vkt/util/TemplateRenderer.java index 853dbb34d..037d82896 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/util/TemplateRenderer.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/util/TemplateRenderer.java @@ -1,6 +1,5 @@ package fi.oph.vkt.util; -import fi.oph.vkt.model.type.FreeEnrollmentSource; import java.util.Locale; import java.util.Map; import java.util.Optional; @@ -19,6 +18,22 @@ public String renderEnrollmentConfirmationEmailBody(final Map pa return renderTemplate("enrollment-confirmation", params, Optional.empty()); } + public String renderEnrollmentAppointmentAuthLink(final Map params) { + return renderTemplate("enrollment-appointment-auth-link", params, Optional.empty()); + } + + public String renderEnrollmentAppointmentConfirmationEmailBody(final Map params) { + return renderTemplate("enrollment-appointment-confirmation", params, Optional.empty()); + } + + public String renderContactRequestReceiptNotification(final Map params) { + return renderTemplate("contact-request-receipt-notification.html", params, Optional.empty()); + } + + public String renderContactRequestNoticeForExaminer(final Map params) { + return renderTemplate("examiner-contact-request.html", params, Optional.empty()); + } + public String renderReceipt(final Locale locale, final Map params) { return renderTemplate("receipt", params, Optional.of(locale)); } 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..f51c1ae19 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 @@ -12,11 +12,11 @@ public class UIRouteUtil { private final Environment environment; public String getEnrollmentContactDetailsUrl(final long examEventId) { - return String.format("%s/ilmoittaudu/%s/tiedot", getPublicBaseUrl(), examEventId); + return String.format("%s/erinomainen-taito/ilmoittaudu/%s/tiedot", getPublicBaseUrl(), examEventId); } public String getEnrollmentPreviewUrl(final long examEventId) { - return String.format("%s/ilmoittaudu/%s/esikatsele", getPublicBaseUrl(), examEventId); + return String.format("%s/erinomainen-taito/ilmoittaudu/%s/esikatsele", getPublicBaseUrl(), examEventId); } public String getPublicFrontPageUrlWithGenericError() { @@ -30,4 +30,20 @@ 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/hyva-ja-tyydyttava-taito/ilmoittaudu/%s/tunnistaudu", + getPublicBaseUrl(), + enrollmentAppointmentId + ); + } + + public String getEnrollmentAppointmentContactDetailsUrl(final long enrollmentAppointmentId) { + return String.format( + "%s/hyva-ja-tyydyttava-taito/ilmoittaudu/%s/tiedot", + getPublicBaseUrl(), + enrollmentAppointmentId + ); + } } diff --git a/backend/vkt/src/main/java/fi/oph/vkt/util/exception/APIExceptionType.java b/backend/vkt/src/main/java/fi/oph/vkt/util/exception/APIExceptionType.java index eda9849a2..1081a0821 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/util/exception/APIExceptionType.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/util/exception/APIExceptionType.java @@ -29,12 +29,25 @@ public enum APIExceptionType { FREE_ENROLLMENT_NO_PROOF_PROVIDED, SESSION_MISSING_PERSON_ID, SESSION_MISSING_QUEUE_EXAM_ID, + SESSION_MISSING_APPOINTMENT_ID, KOSKI_DATA_MISMATCH, USER_ATTACHMENTS_MISSING, TOO_MANY_ATTACHMENTS, INVALID_ATTACHMENT, ATTACHMENT_PERSON_MISMATCH, - TICKET_VALIDATION_ERROR; + TICKET_VALIDATION_ERROR, + SESSION_APPOINTMENT_PERSON_MISMATCH, + SESSION_APPOINTMENT_ID_MISMATCH, + APPOINTMENT_ID_MISMATCH, + EXAMINER_ALREADY_INITIALIZED, + EXAMINER_ONR_NOT_FOUND, + EXAMINER_NOT_FOUND, + EXAMINER_EXAM_EVENT_NOT_FOUND, + EXAMINER_MUNICIPALITY_MISMATCH, + EXAMINER_EXAM_EVENT_EXAMINER_MISMATCH, + EXAMINER_ENROLLMENT_OID_MISMATCH, + EXAMINER_APPOINTMENT_ID_MISMATCH, + AUTH_HASH_EXPIRED; public String getCode() { final StringBuilder codeBuilder = new StringBuilder(); diff --git a/backend/vkt/src/main/java/fi/oph/vkt/util/exception/DataIntegrityViolationExceptionUtil.java b/backend/vkt/src/main/java/fi/oph/vkt/util/exception/DataIntegrityViolationExceptionUtil.java index 87ff2ae22..71e10deb1 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/util/exception/DataIntegrityViolationExceptionUtil.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/util/exception/DataIntegrityViolationExceptionUtil.java @@ -4,8 +4,13 @@ public class DataIntegrityViolationExceptionUtil { - public static boolean isExamEventLanguageLevelDateUniquenessException(final DataIntegrityViolationException ex) { - return matchesConstraint(ex, "uk_exam_event_language_level_date"); + public static boolean isExamEventLanguageLevelDateExaminerUniquenessException( + final DataIntegrityViolationException ex + ) { + return ( + matchesConstraint(ex, "uk_exam_event_language_level_date_examiner") || + matchesConstraint(ex, "uk_exam_event_language_level_date") + ); } private static boolean matchesConstraint(final DataIntegrityViolationException ex, final String constraint) { diff --git a/backend/vkt/src/main/java/fi/oph/vkt/view/ExamEventCommonXlsxDataRow.java b/backend/vkt/src/main/java/fi/oph/vkt/view/ExamEventCommonXlsxDataRow.java new file mode 100644 index 000000000..be046aa61 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/view/ExamEventCommonXlsxDataRow.java @@ -0,0 +1,3 @@ +package fi.oph.vkt.view; + +public interface ExamEventCommonXlsxDataRow {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/view/ExamEventCommonXlsxView.java b/backend/vkt/src/main/java/fi/oph/vkt/view/ExamEventCommonXlsxView.java new file mode 100644 index 000000000..c4d223768 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/view/ExamEventCommonXlsxView.java @@ -0,0 +1,45 @@ +package fi.oph.vkt.view; + +import jakarta.servlet.http.HttpServletResponse; +import java.util.List; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.xssf.usermodel.XSSFFont; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.web.servlet.view.document.AbstractXlsxView; + +public abstract class ExamEventCommonXlsxView extends AbstractXlsxView { + + protected static void setFilenameHeader(final HttpServletResponse response, final String filename) { + response.addHeader("Content-Disposition", String.format("attachment; filename=\"%s\"", filename)); + } + + protected static void setNullableValue(final Cell cell, final String string) { + if (string != null) { + cell.setCellValue(string); + } else { + cell.setBlank(); + } + } + + protected static void createExcelHeader(final XSSFWorkbook workbook, final Sheet sheet, final List headers) { + final Row header = sheet.createRow(0); + final CellStyle headerStyle = workbook.createCellStyle(); + final XSSFFont font = workbook.createFont(); + font.setBold(true); + headerStyle.setFont(font); + for (int i = 0; i < headers.size(); i++) { + final Cell cell = header.createCell(i); + cell.setCellStyle(headerStyle); + cell.setCellValue(headers.get(i)); + } + } + + protected static void autoresizeExcelColumns(final Sheet sheet, final List headers) { + for (int i = 0; i < headers.size(); i++) { + sheet.autoSizeColumn(i); + } + } +} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/view/ExamEventXlsxDataRow.java b/backend/vkt/src/main/java/fi/oph/vkt/view/ExamEventXlsxDataRow.java index c6585d872..8998d830d 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/view/ExamEventXlsxDataRow.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/view/ExamEventXlsxDataRow.java @@ -34,4 +34,5 @@ public record ExamEventXlsxDataRow( String postalCode, String town, String country -) {} +) + implements ExamEventCommonXlsxDataRow {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/view/ExamEventXlsxDataRowUtil.java b/backend/vkt/src/main/java/fi/oph/vkt/view/ExamEventXlsxDataRowUtil.java index 9b4b4330f..14c740b99 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/view/ExamEventXlsxDataRowUtil.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/view/ExamEventXlsxDataRowUtil.java @@ -1,10 +1,13 @@ package fi.oph.vkt.view; import fi.oph.vkt.model.Enrollment; +import fi.oph.vkt.model.EnrollmentAppointment; import fi.oph.vkt.model.ExamEvent; +import fi.oph.vkt.model.ExaminerExamEvent; import fi.oph.vkt.model.FreeEnrollment; import fi.oph.vkt.model.KoskiEducations; import fi.oph.vkt.model.Person; +import fi.oph.vkt.model.type.EnrollmentAppointmentStatus; import fi.oph.vkt.model.type.EnrollmentStatus; import fi.oph.vkt.model.type.FreeEnrollmentSource; import fi.oph.vkt.model.type.FreeEnrollmentType; @@ -16,17 +19,76 @@ public class ExamEventXlsxDataRowUtil { private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd"); private static final DateTimeFormatter DATETIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + private static ExamEventXlsxData createExamEventExcel( + final ExamEvent examEvent, + final List excelDataRows + ) { + return ExamEventXlsxData + .builder() + .date(DATE_FORMAT.format(examEvent.getDate())) + .language(examEvent.getLanguage().name()) + .rows(excelDataRows) + .build(); + } + + private static ExaminerExamEventXlsxData createExamEventExcel( + final ExaminerExamEvent examEvent, + final List excelDataRows + ) { + return ExaminerExamEventXlsxData + .builder() + .date(DATE_FORMAT.format(examEvent.getDate())) + .language(examEvent.getLanguage().name()) + .rows(excelDataRows) + .build(); + } + public static ExamEventXlsxData createExcelData(final ExamEvent examEvent, final List enrollments) { final List excelDataRows = enrollments .stream() .map(enrollment -> createDataRow(enrollment, enrollment.getPerson())) .toList(); - return ExamEventXlsxData + return createExamEventExcel(examEvent, excelDataRows); + } + + public static ExaminerExamEventXlsxData createExcelData( + final ExaminerExamEvent examEvent, + final List enrollments + ) { + final List excelDataRows = enrollments + .stream() + .map(enrollment -> createDataRow(enrollment, enrollment.getPerson())) + .toList(); + + return createExamEventExcel(examEvent, excelDataRows); + } + + private static ExaminerExamEventXlsxDataRow createDataRow( + final EnrollmentAppointment enrollment, + final Person person + ) { + return ExaminerExamEventXlsxDataRow .builder() - .date(DATE_FORMAT.format(examEvent.getDate())) - .language(examEvent.getLanguage().name()) - .rows(excelDataRows) + .enrollmentTime(DATETIME_FORMAT.format(enrollment.getCreatedAt())) + .lastName(person.getLastName()) + .firstName(person.getFirstName()) + .previousEnrollment(boolToInt(enrollment.isHasPreviousEnrollment())) + .status(statusToText(enrollment.getStatus())) + .textualSkill(boolToInt(enrollment.isTextualSkill())) + .oralSkill(boolToInt(enrollment.isOralSkill())) + .understandingSkill(boolToInt(enrollment.isUnderstandingSkill())) + .writing(boolToInt(enrollment.isWritingPartialExam())) + .readingComprehension(boolToInt(enrollment.isReadingComprehensionPartialExam())) + .speaking(boolToInt(enrollment.isSpeakingPartialExam())) + .speechComprehension(boolToInt(enrollment.isSpeechComprehensionPartialExam())) + .email(enrollment.getEmail()) + .phoneNumber(enrollment.getPhoneNumber()) + .digitalCertificateConsent(boolToInt(enrollment.isDigitalCertificateConsent())) + .street(enrollment.getStreet()) + .postalCode(enrollment.getPostalCode()) + .town(enrollment.getTown()) + .country(enrollment.getCountry()) .build(); } @@ -106,6 +168,18 @@ private static ExamEventXlsxDataRow createDataRow(final Enrollment enrollment, f return builder.build(); } + private static String statusToText(final EnrollmentAppointmentStatus status) { + return switch (status) { + case COMPLETED -> "Maksettu"; + case CANCELED -> "Peruttu"; + case EXPECTING_PAYMENT -> "Odottaa maksua"; + case WAITING_AUTHENTICATION -> "Odottaa tunnistautumista"; + case CANCELED_PAYMENT -> "Maksu peruutettu"; + case ENROLLMENT_CREATED -> "Ilmoittautuminen luotu (tunnistautumislinkkiä ei vielä lähetetty)"; + case CONTACT_CREATED -> "Yhteydenotto luotu"; + }; + } + private static String statusToText(final EnrollmentStatus status) { return switch (status) { case COMPLETED -> "Maksettu"; diff --git a/backend/vkt/src/main/java/fi/oph/vkt/view/ExamEventXlsxView.java b/backend/vkt/src/main/java/fi/oph/vkt/view/ExamEventXlsxView.java index 66b2e82d0..8b9465cd0 100644 --- a/backend/vkt/src/main/java/fi/oph/vkt/view/ExamEventXlsxView.java +++ b/backend/vkt/src/main/java/fi/oph/vkt/view/ExamEventXlsxView.java @@ -6,16 +6,12 @@ import java.util.List; import java.util.Map; import lombok.NonNull; -import org.apache.poi.ss.usermodel.Cell; -import org.apache.poi.ss.usermodel.CellStyle; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; -import org.apache.poi.xssf.usermodel.XSSFFont; import org.apache.poi.xssf.usermodel.XSSFWorkbook; -import org.springframework.web.servlet.view.document.AbstractXlsxView; -public class ExamEventXlsxView extends AbstractXlsxView { +public class ExamEventXlsxView extends ExamEventCommonXlsxView { private final ExamEventXlsxData data; @@ -30,14 +26,13 @@ protected void buildExcelDocument( final @NonNull HttpServletRequest request, final @NonNull HttpServletResponse response ) { - setFilenameHeader(response, String.format("VKT_tilaisuus_%s_%s.xlsx", data.date(), data.language())); + setFilenameHeader( + response, + String.format("VKT_erinomainen_taito_tilaisuus_%s_%s.xlsx", data.date(), data.language()) + ); writeExcel(workbook); } - private static void setFilenameHeader(final HttpServletResponse response, final String filename) { - response.addHeader("Content-Disposition", String.format("attachment; filename=\"%s\"", filename)); - } - private void writeExcel(final Workbook workbook) { final List headers = List.of( "Päivä", @@ -125,31 +120,4 @@ private void writeExcel(final Workbook workbook) { autoresizeExcelColumns(sheet, headers); } - - private static void setNullableValue(final Cell cell, final String string) { - if (string != null) { - cell.setCellValue(string); - } else { - cell.setBlank(); - } - } - - private static void createExcelHeader(final XSSFWorkbook workbook, final Sheet sheet, final List headers) { - final Row header = sheet.createRow(0); - final CellStyle headerStyle = workbook.createCellStyle(); - final XSSFFont font = workbook.createFont(); - font.setBold(true); - headerStyle.setFont(font); - for (int i = 0; i < headers.size(); i++) { - final Cell cell = header.createCell(i); - cell.setCellStyle(headerStyle); - cell.setCellValue(headers.get(i)); - } - } - - private static void autoresizeExcelColumns(final Sheet sheet, final List headers) { - for (int i = 0; i < headers.size(); i++) { - sheet.autoSizeColumn(i); - } - } } diff --git a/backend/vkt/src/main/java/fi/oph/vkt/view/ExaminerExamEventXlsxData.java b/backend/vkt/src/main/java/fi/oph/vkt/view/ExaminerExamEventXlsxData.java new file mode 100644 index 000000000..3af1d7dae --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/view/ExaminerExamEventXlsxData.java @@ -0,0 +1,12 @@ +package fi.oph.vkt.view; + +import java.util.List; +import lombok.Builder; +import lombok.NonNull; + +@Builder +public record ExaminerExamEventXlsxData( + @NonNull String date, + @NonNull String language, + @NonNull List rows +) {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/view/ExaminerExamEventXlsxDataRow.java b/backend/vkt/src/main/java/fi/oph/vkt/view/ExaminerExamEventXlsxDataRow.java new file mode 100644 index 000000000..4941c73b0 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/view/ExaminerExamEventXlsxDataRow.java @@ -0,0 +1,29 @@ +package fi.oph.vkt.view; + +import fi.oph.vkt.model.type.FreeEnrollmentSource; +import lombok.Builder; +import lombok.NonNull; + +@Builder +public record ExaminerExamEventXlsxDataRow( + @NonNull String enrollmentTime, + @NonNull String lastName, + @NonNull String firstName, + @NonNull Integer previousEnrollment, + @NonNull String status, + @NonNull Integer textualSkill, + @NonNull Integer oralSkill, + @NonNull Integer understandingSkill, + @NonNull Integer writing, + @NonNull Integer readingComprehension, + @NonNull Integer speaking, + @NonNull Integer speechComprehension, + @NonNull String email, + @NonNull String phoneNumber, + @NonNull Integer digitalCertificateConsent, + String street, + String postalCode, + String town, + String country +) + implements ExamEventCommonXlsxDataRow {} diff --git a/backend/vkt/src/main/java/fi/oph/vkt/view/ExaminerExamEventXlsxView.java b/backend/vkt/src/main/java/fi/oph/vkt/view/ExaminerExamEventXlsxView.java new file mode 100644 index 000000000..c16847266 --- /dev/null +++ b/backend/vkt/src/main/java/fi/oph/vkt/view/ExaminerExamEventXlsxView.java @@ -0,0 +1,94 @@ +package fi.oph.vkt.view; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.List; +import java.util.Map; +import lombok.NonNull; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; + +public class ExaminerExamEventXlsxView extends ExamEventCommonXlsxView { + + private final ExaminerExamEventXlsxData data; + + public ExaminerExamEventXlsxView(final ExaminerExamEventXlsxData data) { + this.data = data; + } + + @Override + protected void buildExcelDocument( + final @NonNull Map model, + final @NonNull Workbook workbook, + final @NonNull HttpServletRequest request, + final @NonNull HttpServletResponse response + ) { + setFilenameHeader( + response, + String.format("VKT_hyva_ja_tyydyttava_taito_tilaisuus_%s_%s.xlsx", data.date(), data.language()) + ); + writeExcel(workbook); + } + + private void writeExcel(final Workbook workbook) { + final List headers = List.of( + "Päivä", + "Kieli", + "Ilmoittautumisaika", + "Sukunimi", + "Etunimi", + "Aiempi tutkintopäivä", + "Tila", + "KT", // Kirjallinen taito + "ST", // Suullinen taito + "YT", // Ymmärtämisen taito + "KI", // Kirjoittaminen + "TY", // Tekstin ymmärtäminen + "PU", // Puhuminen + "PY", // Puheen ymmärtäminen, + "Sähköposti", + "Puhelin", + "Sähk. Tod.", + "Katu", + "Postinumero", + "Kaupunki", + "Maa" + ); + final Sheet sheet = workbook.createSheet("Tilaisuuden tiedot"); + + createExcelHeader((XSSFWorkbook) workbook, sheet, headers); + + for (int i = 0; i < data.rows().size(); i++) { + final Row row = sheet.createRow(i + 1); + final ExaminerExamEventXlsxDataRow dataRow = data.rows().get(i); + + int ci = 0; + row.createCell(ci).setCellValue(data.date()); + row.createCell(++ci).setCellValue(data.language()); + row.createCell(++ci).setCellValue(dataRow.enrollmentTime()); + row.createCell(++ci).setCellValue(dataRow.lastName()); + row.createCell(++ci).setCellValue(dataRow.firstName()); + row.createCell(++ci).setCellValue(dataRow.previousEnrollment()); + row.createCell(++ci).setCellValue(dataRow.status()); + row.createCell(++ci).setCellValue(dataRow.textualSkill()); + row.createCell(++ci).setCellValue(dataRow.oralSkill()); + row.createCell(++ci).setCellValue(dataRow.understandingSkill()); + row.createCell(++ci).setCellValue(dataRow.writing()); + row.createCell(++ci).setCellValue(dataRow.readingComprehension()); + row.createCell(++ci).setCellValue(dataRow.speaking()); + row.createCell(++ci).setCellValue(dataRow.speechComprehension()); + + row.createCell(++ci).setCellValue(dataRow.email()); + row.createCell(++ci).setCellValue(dataRow.phoneNumber()); + row.createCell(++ci).setCellValue(dataRow.digitalCertificateConsent()); + row.createCell(++ci).setCellValue(dataRow.street()); + row.createCell(++ci).setCellValue(dataRow.postalCode()); + row.createCell(++ci).setCellValue(dataRow.town()); + row.createCell(++ci).setCellValue(dataRow.country()); + } + + autoresizeExcelColumns(sheet, headers); + } +} diff --git a/backend/vkt/src/main/resources/application.yaml b/backend/vkt/src/main/resources/application.yaml index 20c4b3b40..9486ebbda 100644 --- a/backend/vkt/src/main/resources/application.yaml +++ b/backend/vkt/src/main/resources/application.yaml @@ -73,6 +73,7 @@ cas: app: base-url: public: ${public-base-url:http://localhost:4002}/vkt + clerk: ${clerk-base-url:http://localhost:4002/vkt} api: ${public-base-url:http://localhost:${server.port}}/vkt/api/v1 cas-oppija: login-url: ${cas-oppija.login-url:https://testiopintopolku.fi/cas-oppija/login} @@ -87,8 +88,8 @@ app: service-url: ${email.service-url:null} koski: url: ${koski.url:null} - user: ${koski.user:null} - password: ${koski.password:null} + user: ${cas.username:null} + password: ${cas.password:null} payment: paytrail: url: https://services.paytrail.com @@ -96,5 +97,11 @@ app: account: ${payment.paytrail.account:null} featureFlags: freeEnrollmentAllowed: ${feature-flags.free-enrollment-allowed:false} + goodAndSatisfactoryLevel: ${feature-flags.good-and-satisfactory-level:false} aws: s3-bucket: ${aws.s3-bucket:test} + onr: + service-url: ${onr.service-url:https://virkailija.untuvaopintopolku.fi/oppijanumerorekisteri-service} + cas: + username: ${cas.username:vkt} + password: ${cas.password:password} 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..3d48b7683 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,331 @@ ALTER TABLE exam_event ADD COLUMN registration_opens TIMESTAMP WITH TIME ZONE; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + UPDATE enrollment_appointment SET has_previous_enrollment = (previous_enrollment IS NOT NULL); + + + diff --git a/backend/vkt/src/main/resources/email-templates/contact-request-receipt-notification.html b/backend/vkt/src/main/resources/email-templates/contact-request-receipt-notification.html new file mode 100644 index 000000000..6af6ba9c0 --- /dev/null +++ b/backend/vkt/src/main/resources/email-templates/contact-request-receipt-notification.html @@ -0,0 +1,41 @@ + + + +

+ Hei, +

+
+

+ Olet lähettänyt viestin seuraavalle Valtionhallinnon kielitutkintojen hyvän ja tyydyttävän taidon tutkintojen tutkintosuoritusten vastaanottajalle: +
+ +

+

+ Lähettämäsi viesti ja yhteystietosi: +

+

+ Viesti: +
+ +

+

+ Yhteystietosi: +
+ Nimi: +
+ Sähköpostiosoite: +

+

+ Mitä seuraavaksi? +
+ Tutkintosuoritusten vastaanottaja ottaa sinuun yhteyttä sähköpostitse. +

+

+ Älä vastaa tähän viestiin - viesti on lähetetty automaattisesti. +

+

+ Ystävällisin terveisin
+ Opetushallitus +

+ + diff --git a/backend/vkt/src/main/resources/email-templates/enrollment-appointment-auth-link.html b/backend/vkt/src/main/resources/email-templates/enrollment-appointment-auth-link.html new file mode 100644 index 000000000..dcb50b727 --- /dev/null +++ b/backend/vkt/src/main/resources/email-templates/enrollment-appointment-auth-link.html @@ -0,0 +1,39 @@ + + + +

+ Olet ilmoittautumassa seuraavaan Valtionhallinnon kielitutkintojen hyvän ja tyydyttävän taidon tutkintoon: +

+ +

+ Tutkinnon kieli:
+ Tutkinnon taso:
+ Tutkintosuorituksen vastaanottaja:
+ Tutkintopäivä:
+ Tutkintopaikka: +

+ +

+ Vahvista ilmoittautumisesi tunnistautumalla vahvasti ja maksamalla tutkintomaksu: +

    +
  • Tunnistaudu Suomi.fi-palvelun kautta.
  • +
  • Sinut ohjataan automaattisesti ilmoittautumislomakkeelle.
  • +
  • Tarkista ilmoittautumisesi tiedot lomakkeella.
  • +
  • Siirry maksamaan tutkintomaksu. Ilmoittautumisesi vahvistuu, kun maksat tutkintomaksun.
  • +
+

+ +

+ Siirry tunnistautumiseen alla olevan linkin kautta:
+ Tunnistaudu ja maksa ilmoittautuminen +

+ +

+ Älä vastaa tähän viestiin - viesti on lähetetty automaattisesti. +

+

+ Ystävällisin terveisin
+ Opetushallitus +

+ + diff --git a/backend/vkt/src/main/resources/email-templates/enrollment-appointment-confirmation.html b/backend/vkt/src/main/resources/email-templates/enrollment-appointment-confirmation.html new file mode 100644 index 000000000..693f09745 --- /dev/null +++ b/backend/vkt/src/main/resources/email-templates/enrollment-appointment-confirmation.html @@ -0,0 +1,59 @@ + + + +

+ Hei, +

+
+

Olet ilmoittautunut Valtionhallinnon kielitutkintoon. Ohessa tiedot ilmoittautumisestasi. Liitteenä myös maksukuitti suomeksi ja ruotsiksi.

+ +

+ Ilmoittautumisen tiedot:
+ Tutkinnon kieli:
+ Tutkinnon taso:
+ Tutkintopäivä: + + Tutkinnon alkamisaika:
+
+ + Tutkintopaikka:
+
+

+

+ Valitsemasi taidot:
+ Valitsemasi osakokeet: +

+ + +

+ Tutkintosuorituksen vastaanottajan antamat tarkemmat saapumisohjeet: +

+
+
+ + Ota tutkintoon mukaan virallinen voimassa oleva henkilöllisyystodistus. +
+ Huomaa, että esimerkiksi ajokortti tai oleskelulupakortti eivät ole virallisia henkilöllisyystodistuksia. +
+ Hyväksyttäviä henkilöllisyystodistuksia ovat: +
+
    +
  • passi
  • +
  • henkilökortti
  • +
  • muukalaispassi
  • +
  • pakolaisen matkustusasiakirja
  • +
  • Puolustusvoimien kuvallinen henkilökortti.
  • +
+
+

+ Jos et pääse osallistumaan tutkintoon, johon olet ilmoittautunut, ilmoita asiasta mahdollisimman pian tutkintosuorituksen vastaanottajalle. +

+

+ Älä vastaa tähän viestiin - viesti on lähetetty automaattisesti. +

+

+ Ystävällisin terveisin
+ Opetushallitus +

+ + diff --git a/backend/vkt/src/main/resources/email-templates/examiner-contact-request.html b/backend/vkt/src/main/resources/email-templates/examiner-contact-request.html new file mode 100644 index 000000000..099e9935b --- /dev/null +++ b/backend/vkt/src/main/resources/email-templates/examiner-contact-request.html @@ -0,0 +1,36 @@ + + + +

+ Hei, +

+ +

+ Sinuun on otettu yhteyttä Valtionhallinnon kielitutkinnon hyvän ja tyydyttävän taidon tutkinnon suorittamista varten. +
+ Ohessa sinulle jätetty viesti ja tiedot sen lähettäjästä. +
+ Siirry linkin kautta VKT-virkailijan käyttöliittymään lukemaan asiakkaan viesti: + +

+

+ Viesti: +
+ +

+

+ Lähettäjän tiedot: +
+ Nimi: +
+ Sähköpostiosoite: +

+

+ Älä vastaa tähän viestiin - viesti on lähetetty automaattisesti. +

+

+ Ystävällisin terveisin
+ Opetushallitus +

+ + diff --git a/backend/vkt/src/main/resources/email-templates/receipt.html b/backend/vkt/src/main/resources/email-templates/receipt.html index d3e235420..a2be64050 100644 --- a/backend/vkt/src/main/resources/email-templates/receipt.html +++ b/backend/vkt/src/main/resources/email-templates/receipt.html @@ -64,6 +64,9 @@

: [[${paymentReference}]]

: [[${exam}]]

: [[${participant}]]

+ +

: [[${examiner}]]

+
diff --git a/backend/vkt/src/main/resources/koodisto/koodisto_kunnat.json b/backend/vkt/src/main/resources/koodisto/koodisto_kunnat.json new file mode 100644 index 000000000..c4d95dc9b --- /dev/null +++ b/backend/vkt/src/main/resources/koodisto/koodisto_kunnat.json @@ -0,0 +1 @@ +[{"koodiUri":"kunta_402","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_402","versio":2,"koodiArvo":"402","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Lapinlahti","sv":"Lapinlahti"},{"koodiUri":"kunta_153","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_153","versio":2,"koodiArvo":"153","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Imatra","sv":"Imatra"},{"koodiUri":"kunta_172","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_172","versio":2,"koodiArvo":"172","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Joutsa","sv":"Joutsa"},{"koodiUri":"kunta_436","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_436","versio":2,"koodiArvo":"436","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Lumijoki","sv":"Lumijoki"},{"koodiUri":"kunta_204","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_204","versio":2,"koodiArvo":"204","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kaavi","sv":"Kaavi"},{"koodiUri":"kunta_146","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_146","versio":2,"koodiArvo":"146","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Ilomantsi","sv":"Ilomants"},{"koodiUri":"kunta_245","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_245","versio":2,"koodiArvo":"245","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kerava","sv":"Kervo"},{"koodiUri":"kunta_271","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_271","versio":2,"koodiArvo":"271","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kokemäki","sv":"Kumo"},{"koodiUri":"kunta_312","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_312","versio":2,"koodiArvo":"312","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kyyjärvi","sv":"Kyyjärvi"},{"koodiUri":"kunta_211","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_211","versio":2,"koodiArvo":"211","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kangasala","sv":"Kangasala"},{"koodiUri":"kunta_434","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_434","versio":2,"koodiArvo":"434","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Loviisa","sv":"Lovisa"},{"koodiUri":"kunta_233","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_233","versio":2,"koodiArvo":"233","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kauhava","sv":"Kauhava"},{"koodiUri":"kunta_686","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_686","versio":2,"koodiArvo":"686","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Rautalampi","sv":"Rautalampi"},{"koodiUri":"kunta_577","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_577","versio":2,"koodiArvo":"577","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Paimio","sv":"Pemar"},{"koodiUri":"kunta_143","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_143","versio":2,"koodiArvo":"143","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Ikaalinen","sv":"Ikalis"},{"koodiUri":"kunta_480","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_480","versio":2,"koodiArvo":"480","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Marttila","sv":"Marttila"},{"koodiUri":"kunta_580","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_580","versio":2,"koodiArvo":"580","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Parikkala","sv":"Parikkala"},{"koodiUri":"kunta_508","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_508","versio":2,"koodiArvo":"508","voimassaAlkuPvm":"2009-01-01","paivitysPvm":"2018-02-08","fi":"Mänttä-Vilppula","sv":"Mänttä-Vilppula"},{"koodiUri":"kunta_700","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_700","versio":2,"koodiArvo":"700","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Ruokolahti","sv":"Ruokolahti"},{"koodiUri":"kunta_421","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_421","versio":2,"koodiArvo":"421","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Lestijärvi","sv":"Lestijärvi"},{"koodiUri":"kunta_309","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_309","versio":2,"koodiArvo":"309","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Outokumpu","sv":"Outokumpu"},{"koodiUri":"kunta_934","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_934","versio":2,"koodiArvo":"934","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Vimpeli","sv":"Vindala"},{"koodiUri":"kunta_416","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_416","versio":2,"koodiArvo":"416","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Lemi","sv":"Lemi"},{"koodiUri":"kunta_077","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_077","versio":2,"koodiArvo":"077","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Hankasalmi","sv":"Hankasalmi"},{"koodiUri":"kunta_739","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_739","versio":2,"koodiArvo":"739","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Savitaipale","sv":"Savitaipale"},{"koodiUri":"kunta_697","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_697","versio":2,"koodiArvo":"697","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Ristijärvi","sv":"Ristijärvi"},{"koodiUri":"kunta_440","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_440","versio":2,"koodiArvo":"440","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Luoto","sv":"Larsmo"},{"koodiUri":"kunta_483","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_483","versio":2,"koodiArvo":"483","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Merijärvi","sv":"Merijärvi"},{"koodiUri":"kunta_250","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_250","versio":2,"koodiArvo":"250","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kihniö","sv":"Kihniö"},{"koodiUri":"kunta_831","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_831","versio":2,"koodiArvo":"831","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Taipalsaari","sv":"Taipalsaari"},{"koodiUri":"kunta_109","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_109","versio":2,"koodiArvo":"109","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Hämeenlinna","sv":"Tavastehus"},{"koodiUri":"kunta_924","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_924","versio":2,"koodiArvo":"924","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Veteli","sv":"Vetil"},{"koodiUri":"kunta_475","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_475","versio":2,"koodiArvo":"475","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Maalahti","sv":"Malax"},{"koodiUri":"kunta_905","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_905","versio":2,"koodiArvo":"905","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Vaasa","sv":"Vasa"},{"koodiUri":"kunta_140","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_140","versio":2,"koodiArvo":"140","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Iisalmi","sv":"Idensalmi"},{"koodiUri":"kunta_734","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_734","versio":2,"koodiArvo":"734","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Salo","sv":"Salo"},{"koodiUri":"kunta_244","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_244","versio":2,"koodiArvo":"244","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kempele","sv":"Kempele"},{"koodiUri":"kunta_091","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_091","versio":2,"koodiArvo":"091","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Helsinki","sv":"Helsingfors"},{"koodiUri":"kunta_108","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_108","versio":2,"koodiArvo":"108","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Hämeenkyrö","sv":"Tavastkyro"},{"koodiUri":"kunta_849","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_849","versio":2,"koodiArvo":"849","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Toholampi","sv":"Toholampi"},{"koodiUri":"kunta_781","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_781","versio":2,"koodiArvo":"781","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Sysmä","sv":"Sysmä"},{"koodiUri":"kunta_019","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_019","versio":2,"koodiArvo":"019","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Aura","sv":"Aura"},{"koodiUri":"kunta_761","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_761","versio":2,"koodiArvo":"761","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Somero","sv":"Somero"},{"koodiUri":"kunta_604","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_604","versio":2,"koodiArvo":"604","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Pirkkala","sv":"Birkala"},{"koodiUri":"kunta_747","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_747","versio":2,"koodiArvo":"747","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Siikainen","sv":"Siikainen"},{"koodiUri":"kunta_441","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_441","versio":2,"koodiArvo":"441","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Luumäki","sv":"Luumäki"},{"koodiUri":"kunta_435","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_435","versio":2,"koodiArvo":"435","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Luhanka","sv":"Luhanka"},{"koodiUri":"kunta_624","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_624","versio":2,"koodiArvo":"624","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Pyhtää","sv":"Pyttis"},{"koodiUri":"kunta_893","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_893","versio":2,"koodiArvo":"893","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Uusikaarlepyy","sv":"Nykarleby"},{"koodiUri":"kunta_152","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_152","versio":2,"koodiArvo":"152","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2021-01-01","fi":"Isokyrö","sv":"Storkyro"},{"koodiUri":"kunta_491","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_491","versio":2,"koodiArvo":"491","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Mikkeli","sv":"St Michel"},{"koodiUri":"kunta_684","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_684","versio":2,"koodiArvo":"684","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Rauma","sv":"Raumo"},{"koodiUri":"kunta_181","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_181","versio":2,"koodiArvo":"181","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Jämijärvi","sv":"Jämijärvi"},{"koodiUri":"kunta_169","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_169","versio":2,"koodiArvo":"169","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Jokioinen","sv":"Jokioinen"},{"koodiUri":"kunta_407","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_407","versio":2,"koodiArvo":"407","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Lapinjärvi","sv":"Lappträsk"},{"koodiUri":"kunta_489","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_489","versio":2,"koodiArvo":"489","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Miehikkälä","sv":"Miehikkälä"},{"koodiUri":"kunta_588","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_588","versio":2,"koodiArvo":"588","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Pertunmaa","sv":"Pertunmaa"},{"koodiUri":"kunta_562","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_562","versio":2,"koodiArvo":"562","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Orivesi","sv":"Orivesi"},{"koodiUri":"kunta_758","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_758","versio":2,"koodiArvo":"758","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Sodankylä","sv":"Sodankylä"},{"koodiUri":"kunta_625","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_625","versio":2,"koodiArvo":"625","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Pyhäjoki","sv":"Pyhäjoki"},{"koodiUri":"kunta_601","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_601","versio":2,"koodiArvo":"601","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Pihtipudas","sv":"Pihtipudas"},{"koodiUri":"kunta_295","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_295","versio":2,"koodiArvo":"295","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kumlinge","sv":"Kumlinge"},{"koodiUri":"kunta_611","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_611","versio":2,"koodiArvo":"611","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Pornainen","sv":"Borgnäs"},{"koodiUri":"kunta_145","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_145","versio":2,"koodiArvo":"145","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Ilmajoki","sv":"Ilmajoki"},{"koodiUri":"kunta_009","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_009","versio":2,"koodiArvo":"009","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Alavieska","sv":"Alavieska"},{"koodiUri":"kunta_687","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_687","versio":2,"koodiArvo":"687","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Rautavaara","sv":"Rautavaara"},{"koodiUri":"kunta_276","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_276","versio":2,"koodiArvo":"276","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kontiolahti","sv":"Kontiolahti"},{"koodiUri":"kunta_935","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_935","versio":2,"koodiArvo":"935","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Virolahti","sv":"Virolahti"},{"koodiUri":"kunta_035","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_035","versio":2,"koodiArvo":"035","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Brändö","sv":"Brändö"},{"koodiUri":"kunta_179","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_179","versio":2,"koodiArvo":"179","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Jyväskylä","sv":"Jyväskylä"},{"koodiUri":"kunta_694","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_694","versio":2,"koodiArvo":"694","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Riihimäki","sv":"Riihimäki"},{"koodiUri":"kunta_918","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_918","versio":2,"koodiArvo":"918","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Vehmaa","sv":"Vehmaa"},{"koodiUri":"kunta_050","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_050","versio":2,"koodiArvo":"050","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Eura","sv":"Eura"},{"koodiUri":"kunta_318","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_318","versio":2,"koodiArvo":"318","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kökar","sv":"Kökar"},{"koodiUri":"kunta_149","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_149","versio":2,"koodiArvo":"149","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Inkoo","sv":"Ingå"},{"koodiUri":"kunta_256","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_256","versio":2,"koodiArvo":"256","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kinnula","sv":"Kinnula"},{"koodiUri":"kunta_495","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_495","versio":2,"koodiArvo":"495","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Multia","sv":"Multia"},{"koodiUri":"kunta_626","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_626","versio":2,"koodiArvo":"626","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Pyhäjärvi","sv":"Pyhäjärvi"},{"koodiUri":"kunta_976","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_976","versio":2,"koodiArvo":"976","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Ylitornio","sv":"Övertorneå"},{"koodiUri":"kunta_214","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_214","versio":2,"koodiArvo":"214","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kankaanpää","sv":"Kankaanpää"},{"koodiUri":"kunta_535","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_535","versio":2,"koodiArvo":"535","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Nivala","sv":"Nivala"},{"koodiUri":"kunta_170","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_170","versio":2,"koodiArvo":"170","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Jomala","sv":"Jomala"},{"koodiUri":"kunta_086","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_086","versio":2,"koodiArvo":"086","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Hausjärvi","sv":"Hausjärvi"},{"koodiUri":"kunta_417","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_417","versio":2,"koodiArvo":"417","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Lemland","sv":"Lemland"},{"koodiUri":"kunta_423","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_423","versio":2,"koodiArvo":"423","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Lieto","sv":"Lundo"},{"koodiUri":"kunta_400","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_400","versio":2,"koodiArvo":"400","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Laitila","sv":"Laitila"},{"koodiUri":"kunta_635","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_635","versio":2,"koodiArvo":"635","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Pälkäne","sv":"Pälkäne"},{"koodiUri":"kunta_230","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_230","versio":2,"koodiArvo":"230","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Karvia","sv":"Karvia"},{"koodiUri":"kunta_678","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_678","versio":2,"koodiArvo":"678","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Raahe","sv":"Brahestad"},{"koodiUri":"kunta_051","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_051","versio":2,"koodiArvo":"051","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Eurajoki","sv":"Euraåminne"},{"koodiUri":"kunta_202","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_202","versio":2,"koodiArvo":"202","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kaarina","sv":"St Karins"},{"koodiUri":"kunta_915","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_915","versio":2,"koodiArvo":"915","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Varkaus","sv":"Varkaus"},{"koodiUri":"kunta_232","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_232","versio":2,"koodiArvo":"232","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kauhajoki","sv":"Kauhajoki"},{"koodiUri":"kunta_178","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_178","versio":2,"koodiArvo":"178","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Juva","sv":"Juva"},{"koodiUri":"kunta_768","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_768","versio":2,"koodiArvo":"768","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Sulkava","sv":"Sulkava"},{"koodiUri":"kunta_584","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_584","versio":2,"koodiArvo":"584","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Perho","sv":"Perho"},{"koodiUri":"kunta_704","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_704","versio":2,"koodiArvo":"704","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Rusko","sv":"Rusko"},{"koodiUri":"kunta_280","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_280","versio":2,"koodiArvo":"280","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Korsnäs","sv":"Korsnäs"},{"koodiUri":"kunta_751","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_751","versio":2,"koodiArvo":"751","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Simo","sv":"Simo"},{"koodiUri":"kunta_106","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_106","versio":2,"koodiArvo":"106","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Hyvinkää","sv":"Hyvinge"},{"koodiUri":"kunta_065","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_065","versio":2,"koodiArvo":"065","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Geta","sv":"Geta"},{"koodiUri":"kunta_285","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_285","versio":2,"koodiArvo":"285","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kotka","sv":"Kotka"},{"koodiUri":"kunta_564","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_564","versio":2,"koodiArvo":"564","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Oulu","sv":"Uleåborg"},{"koodiUri":"kunta_531","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_531","versio":2,"koodiArvo":"531","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Nakkila","sv":"Nakkila"},{"koodiUri":"kunta_074","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_074","versio":2,"koodiArvo":"074","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Halsua","sv":"Halsua"},{"koodiUri":"kunta_076","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_076","versio":2,"koodiArvo":"076","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Hammarland","sv":"Hammarland"},{"koodiUri":"kunta_889","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_889","versio":2,"koodiArvo":"889","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Utajärvi","sv":"Utajärvi"},{"koodiUri":"kunta_837","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_837","versio":2,"koodiArvo":"837","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Tampere","sv":"Tammerfors"},{"koodiUri":"kunta_200","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_200","versio":2,"koodiArvo":"200","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2023-12-19","fi":"Ulkomaa","sv":"Utlandet"},{"koodiUri":"kunta_218","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_218","versio":2,"koodiArvo":"218","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Karijoki","sv":"Bötom"},{"koodiUri":"kunta_213","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_213","versio":2,"koodiArvo":"213","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kangasniemi","sv":"Kangasniemi"},{"koodiUri":"kunta_216","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_216","versio":2,"koodiArvo":"216","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kannonkoski","sv":"Kannonkoski"},{"koodiUri":"kunta_541","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_541","versio":2,"koodiArvo":"541","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Nurmes","sv":"Nurmes"},{"koodiUri":"kunta_834","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_834","versio":2,"koodiArvo":"834","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Tammela","sv":"Tammela"},{"koodiUri":"kunta_304","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_304","versio":2,"koodiArvo":"304","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kustavi","sv":"Gustavs"},{"koodiUri":"kunta_081","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_081","versio":2,"koodiArvo":"081","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Hartola","sv":"Hartola"},{"koodiUri":"kunta_578","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_578","versio":2,"koodiArvo":"578","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Paltamo","sv":"Paltamo"},{"koodiUri":"kunta_922","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_922","versio":2,"koodiArvo":"922","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Vesilahti","sv":"Vesilahti"},{"koodiUri":"kunta_484","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_484","versio":2,"koodiArvo":"484","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Merikarvia","sv":"Sastmola"},{"koodiUri":"kunta_749","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_749","versio":2,"koodiArvo":"749","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Siilinjärvi","sv":"Siilinjärvi"},{"koodiUri":"kunta_097","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_097","versio":2,"koodiArvo":"097","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Hirvensalmi","sv":"Hirvensalmi"},{"koodiUri":"kunta_505","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_505","versio":2,"koodiArvo":"505","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Mäntsälä","sv":"Mäntsälä"},{"koodiUri":"kunta_272","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_272","versio":2,"koodiArvo":"272","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kokkola","sv":"Karleby"},{"koodiUri":"kunta_171","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_171","versio":2,"koodiArvo":"171","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2021-01-01","fi":"Joroinen","sv":"Jorois"},{"koodiUri":"kunta_290","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_290","versio":2,"koodiArvo":"290","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kuhmo","sv":"Kuhmo"},{"koodiUri":"kunta_010","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_010","versio":2,"koodiArvo":"010","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Alavus","sv":"Alavo"},{"koodiUri":"kunta_420","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_420","versio":2,"koodiArvo":"420","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Leppävirta","sv":"Leppävirta"},{"koodiUri":"kunta_075","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_075","versio":2,"koodiArvo":"075","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Hamina","sv":"Fredrikshamn"},{"koodiUri":"kunta_177","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_177","versio":2,"koodiArvo":"177","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Juupajoki","sv":"Juupajoki"},{"koodiUri":"kunta_240","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_240","versio":2,"koodiArvo":"240","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kemi","sv":"Kemi"},{"koodiUri":"kunta_498","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_498","versio":2,"koodiArvo":"498","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Muonio","sv":"Muonio"},{"koodiUri":"kunta_069","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_069","versio":2,"koodiArvo":"069","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Haapajärvi","sv":"Haapajärvi"},{"koodiUri":"kunta_753","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_753","versio":2,"koodiArvo":"753","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Sipoo","sv":"Sibbo"},{"koodiUri":"kunta_698","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_698","versio":2,"koodiArvo":"698","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Rovaniemi","sv":"Rovaniemi"},{"koodiUri":"kunta_592","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_592","versio":2,"koodiArvo":"592","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Petäjävesi","sv":"Petäjävesi"},{"koodiUri":"kunta_061","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_061","versio":2,"koodiArvo":"061","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Forssa","sv":"Forssa"},{"koodiUri":"kunta_765","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_765","versio":2,"koodiArvo":"765","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2022-09-22","fi":"Sotkamo","sv":"Sotkamo"},{"koodiUri":"kunta_186","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_186","versio":2,"koodiArvo":"186","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Järvenpää","sv":"Träskända"},{"koodiUri":"kunta_844","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_844","versio":2,"koodiArvo":"844","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Tervo","sv":"Tervo"},{"koodiUri":"kunta_738","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_738","versio":2,"koodiArvo":"738","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Sauvo","sv":"Sagu"},{"koodiUri":"kunta_623","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_623","versio":2,"koodiArvo":"623","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Puumala","sv":"Puumala"},{"koodiUri":"kunta_851","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_851","versio":2,"koodiArvo":"851","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Tornio","sv":"Torneå"},{"koodiUri":"kunta_111","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_111","versio":2,"koodiArvo":"111","voimassaAlkuPvm":"1997-01-01","paivitysPvm":"2018-02-08","fi":"Heinola","sv":"Heinola"},{"koodiUri":"kunta_691","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_691","versio":2,"koodiArvo":"691","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Reisjärvi","sv":"Reisjärvi"},{"koodiUri":"kunta_231","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_231","versio":2,"koodiArvo":"231","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kaskinen","sv":"Kaskö"},{"koodiUri":"kunta_927","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_927","versio":2,"koodiArvo":"927","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Vihti","sv":"Vichtis"},{"koodiUri":"kunta_300","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_300","versio":2,"koodiArvo":"300","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kuortane","sv":"Kuortane"},{"koodiUri":"kunta_167","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_167","versio":2,"koodiArvo":"167","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Joensuu","sv":"Joensuu"},{"koodiUri":"kunta_989","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_989","versio":2,"koodiArvo":"989","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Ähtäri","sv":"Etseri"},{"koodiUri":"kunta_072","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_072","versio":2,"koodiArvo":"072","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Hailuoto","sv":"Karlö"},{"koodiUri":"kunta_260","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_260","versio":2,"koodiArvo":"260","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kitee","sv":"Kitee"},{"koodiUri":"kunta_833","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_833","versio":2,"koodiArvo":"833","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Taivassalo","sv":"Tövsala"},{"koodiUri":"kunta_771","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_771","versio":2,"koodiArvo":"771","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Sund","sv":"Sund"},{"koodiUri":"kunta_047","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_047","versio":2,"koodiArvo":"047","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Enontekiö","sv":"Enontekis"},{"koodiUri":"kunta_545","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_545","versio":2,"koodiArvo":"545","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Närpiö","sv":"Närpes"},{"koodiUri":"kunta_859","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_859","versio":2,"koodiArvo":"859","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Tyrnävä","sv":"Tyrnävä"},{"koodiUri":"kunta_291","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_291","versio":2,"koodiArvo":"291","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2021-01-01","fi":"Kuhmoinen","sv":"Kuhmoinen"},{"koodiUri":"kunta_500","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_500","versio":2,"koodiArvo":"500","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Muurame","sv":"Muurame"},{"koodiUri":"kunta_683","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_683","versio":2,"koodiArvo":"683","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Ranua","sv":"Ranua"},{"koodiUri":"kunta_857","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_857","versio":2,"koodiArvo":"857","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Tuusniemi","sv":"Tuusniemi"},{"koodiUri":"kunta_241","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_241","versio":2,"koodiArvo":"241","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Keminmaa","sv":"Keminmaa"},{"koodiUri":"kunta_403","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_403","versio":2,"koodiArvo":"403","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Lappajärvi","sv":"Lappajärvi"},{"koodiUri":"kunta_619","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_619","versio":2,"koodiArvo":"619","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Punkalaidun","sv":"Punkalaidun"},{"koodiUri":"kunta_062","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_062","versio":2,"koodiArvo":"062","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Föglö","sv":"Föglö"},{"koodiUri":"kunta_563","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_563","versio":2,"koodiArvo":"563","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Oulainen","sv":"Oulainen"},{"koodiUri":"kunta_832","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_832","versio":2,"koodiArvo":"832","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Taivalkoski","sv":"Taivalkoski"},{"koodiUri":"kunta_925","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_925","versio":2,"koodiArvo":"925","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Vieremä","sv":"Vieremä"},{"koodiUri":"kunta_142","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_142","versio":2,"koodiArvo":"142","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2021-01-01","fi":"Iitti","sv":"Iitti"},{"koodiUri":"kunta_090","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_090","versio":2,"koodiArvo":"090","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2021-01-01","fi":"Heinävesi","sv":"Heinävesi"},{"koodiUri":"kunta_607","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_607","versio":2,"koodiArvo":"607","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Polvijärvi","sv":"Polvijärvi"},{"koodiUri":"kunta_418","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_418","versio":2,"koodiArvo":"418","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Lempäälä","sv":"Lempäälä"},{"koodiUri":"kunta_105","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_105","versio":2,"koodiArvo":"105","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Hyrynsalmi","sv":"Hyrynsalmi"},{"koodiUri":"kunta_082","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_082","versio":2,"koodiArvo":"082","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Hattula","sv":"Hattula"},{"koodiUri":"kunta_049","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_049","versio":2,"koodiArvo":"049","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Espoo","sv":"Esbo"},{"koodiUri":"kunta_992","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_992","versio":2,"koodiArvo":"992","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Äänekoski","sv":"Äänekoski"},{"koodiUri":"kunta_322","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_322","versio":2,"koodiArvo":"322","voimassaAlkuPvm":"2009-01-01","paivitysPvm":"2018-02-08","fi":"Kemiönsaari","sv":"Kimitoön"},{"koodiUri":"kunta_405","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_405","versio":2,"koodiArvo":"405","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Lappeenranta","sv":"Villmanstrand"},{"koodiUri":"kunta_581","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_581","versio":2,"koodiArvo":"581","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Parkano","sv":"Parkano"},{"koodiUri":"kunta_931","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_931","versio":2,"koodiArvo":"931","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Viitasaari","sv":"Viitasaari"},{"koodiUri":"kunta_790","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_790","versio":2,"koodiArvo":"790","voimassaAlkuPvm":"2009-01-01","paivitysPvm":"2018-02-08","fi":"Sastamala","sv":"Sastamala"},{"koodiUri":"kunta_630","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_630","versio":2,"koodiArvo":"630","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Pyhäntä","sv":"Pyhäntä"},{"koodiUri":"kunta_425","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_425","versio":2,"koodiArvo":"425","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Liminka","sv":"Limingo"},{"koodiUri":"kunta_543","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_543","versio":2,"koodiArvo":"543","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Nurmijärvi","sv":"Nurmijärvi"},{"koodiUri":"kunta_778","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_778","versio":2,"koodiArvo":"778","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Suonenjoki","sv":"Suonenjoki"},{"koodiUri":"kunta_151","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_151","versio":2,"koodiArvo":"151","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Isojoki","sv":"Storå"},{"koodiUri":"kunta_908","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_908","versio":2,"koodiArvo":"908","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Valkeakoski","sv":"Valkeakoski"},{"koodiUri":"kunta_892","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_892","versio":2,"koodiArvo":"892","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Uurainen","sv":"Uurainen"},{"koodiUri":"kunta_239","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_239","versio":2,"koodiArvo":"239","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Keitele","sv":"Keitele"},{"koodiUri":"kunta_762","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_762","versio":2,"koodiArvo":"762","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Sonkajärvi","sv":"Sonkajärvi"},{"koodiUri":"kunta_710","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_710","versio":2,"koodiArvo":"710","voimassaAlkuPvm":"2009-01-01","paivitysPvm":"2018-02-08","fi":"Raasepori","sv":"Raseborg"},{"koodiUri":"kunta_636","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_636","versio":2,"koodiArvo":"636","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Pöytyä","sv":"Pöytyä"},{"koodiUri":"kunta_777","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_777","versio":2,"koodiArvo":"777","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Suomussalmi","sv":"Suomussalmi"},{"koodiUri":"kunta_977","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_977","versio":2,"koodiArvo":"977","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Ylivieska","sv":"Ylivieska"},{"koodiUri":"kunta_098","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_098","versio":2,"koodiArvo":"098","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2022-09-22","fi":"Hollola","sv":"Hollola"},{"koodiUri":"kunta_043","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_043","versio":2,"koodiArvo":"043","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Eckerö","sv":"Eckerö"},{"koodiUri":"kunta_887","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_887","versio":2,"koodiArvo":"887","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Urjala","sv":"Urjala"},{"koodiUri":"kunta_886","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_886","versio":2,"koodiArvo":"886","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Ulvila","sv":"Ulvsby"},{"koodiUri":"kunta_052","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_052","versio":2,"koodiArvo":"052","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Evijärvi","sv":"Evijärvi"},{"koodiUri":"kunta_494","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_494","versio":2,"koodiArvo":"494","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Muhos","sv":"Muhos"},{"koodiUri":"kunta_503","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_503","versio":2,"koodiArvo":"503","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Mynämäki","sv":"Mynämäki"},{"koodiUri":"kunta_791","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_791","versio":2,"koodiArvo":"791","voimassaAlkuPvm":"2009-01-01","paivitysPvm":"2018-02-08","fi":"Siikalatva","sv":"Siikalatva"},{"koodiUri":"kunta_236","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_236","versio":2,"koodiArvo":"236","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kaustinen","sv":"Kaustby"},{"koodiUri":"kunta_946","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_946","versio":2,"koodiArvo":"946","voimassaAlkuPvm":"2011-01-01","paivitysPvm":"2018-02-08","fi":"Vöyri","sv":"Vörå"},{"koodiUri":"kunta_226","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_226","versio":2,"koodiArvo":"226","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Karstula","sv":"Karstula"},{"koodiUri":"kunta_615","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_615","versio":2,"koodiArvo":"615","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Pudasjärvi","sv":"Pudasjärvi"},{"koodiUri":"kunta_060","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_060","versio":2,"koodiArvo":"060","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Finström","sv":"Finström"},{"koodiUri":"kunta_766","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_766","versio":2,"koodiArvo":"766","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Sottunga","sv":"Sottunga"},{"koodiUri":"kunta_504","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_504","versio":2,"koodiArvo":"504","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Myrskylä","sv":"Mörskom"},{"koodiUri":"kunta_224","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_224","versio":2,"koodiArvo":"224","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Karkkila","sv":"Högfors"},{"koodiUri":"kunta_608","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_608","versio":2,"koodiArvo":"608","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Pomarkku","sv":"Påmark"},{"koodiUri":"kunta_529","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_529","versio":2,"koodiArvo":"529","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Naantali","sv":"Nådendal"},{"koodiUri":"kunta_433","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_433","versio":2,"koodiArvo":"433","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Loppi","sv":"Loppi"},{"koodiUri":"kunta_845","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_845","versio":2,"koodiArvo":"845","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Tervola","sv":"Tervola"},{"koodiUri":"kunta_297","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_297","versio":2,"koodiArvo":"297","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kuopio","sv":"Kuopio"},{"koodiUri":"kunta_176","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_176","versio":2,"koodiArvo":"176","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Juuka","sv":"Juuka"},{"koodiUri":"kunta_748","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_748","versio":2,"koodiArvo":"748","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Siikajoki","sv":"Siikajoki"},{"koodiUri":"kunta_481","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_481","versio":2,"koodiArvo":"481","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Masku","sv":"Masku"},{"koodiUri":"kunta_746","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_746","versio":2,"koodiArvo":"746","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Sievi","sv":"Sievi"},{"koodiUri":"kunta_071","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_071","versio":2,"koodiArvo":"071","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Haapavesi","sv":"Haapavesi"},{"koodiUri":"kunta_079","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_079","versio":2,"koodiArvo":"079","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Harjavalta","sv":"Harjavalta"},{"koodiUri":"kunta_538","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_538","versio":2,"koodiArvo":"538","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Nousiainen","sv":"Nousis"},{"koodiUri":"kunta_890","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_890","versio":2,"koodiArvo":"890","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Utsjoki","sv":"Utsjoki"},{"koodiUri":"kunta_148","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_148","versio":2,"koodiArvo":"148","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Inari","sv":"Enare"},{"koodiUri":"kunta_921","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_921","versio":2,"koodiArvo":"921","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Vesanto","sv":"Vesanto"},{"koodiUri":"kunta_593","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_593","versio":2,"koodiArvo":"593","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Pieksämäki","sv":"Pieksämäki"},{"koodiUri":"kunta_595","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_595","versio":2,"koodiArvo":"595","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Pielavesi","sv":"Pielavesi"},{"koodiUri":"kunta_599","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_599","versio":2,"koodiArvo":"599","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Pedersöre","sv":"Pedersöre"},{"koodiUri":"kunta_499","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_499","versio":2,"koodiArvo":"499","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Mustasaari","sv":"Korsholm"},{"koodiUri":"kunta_702","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_702","versio":2,"koodiArvo":"702","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Ruovesi","sv":"Ruovesi"},{"koodiUri":"kunta_005","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_005","versio":2,"koodiArvo":"005","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Alajärvi","sv":"Alajärvi"},{"koodiUri":"kunta_740","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_740","versio":2,"koodiArvo":"740","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Savonlinna","sv":"Nyslott"},{"koodiUri":"kunta_263","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_263","versio":2,"koodiArvo":"263","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kiuruvesi","sv":"Kiuruvesi"},{"koodiUri":"kunta_785","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_785","versio":2,"koodiArvo":"785","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Vaala","sv":"Vaala"},{"koodiUri":"kunta_046","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_046","versio":2,"koodiArvo":"046","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Enonkoski","sv":"Enonkoski"},{"koodiUri":"kunta_854","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_854","versio":2,"koodiArvo":"854","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Pello","sv":"Pello"},{"koodiUri":"kunta_576","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_576","versio":2,"koodiArvo":"576","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Padasjoki","sv":"Padasjoki"},{"koodiUri":"kunta_426","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_426","versio":2,"koodiArvo":"426","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Liperi","sv":"Liperi"},{"koodiUri":"kunta_755","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_755","versio":2,"koodiArvo":"755","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Siuntio","sv":"Sjundeå"},{"koodiUri":"kunta_445","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_445","versio":2,"koodiArvo":"445","voimassaAlkuPvm":"2009-01-01","paivitysPvm":"2019-01-21","fi":"Parainen","sv":"Pargas"},{"koodiUri":"kunta_759","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_759","versio":2,"koodiArvo":"759","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Soini","sv":"Soini"},{"koodiUri":"kunta_092","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_092","versio":2,"koodiArvo":"092","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Vantaa","sv":"Vanda"},{"koodiUri":"kunta_284","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_284","versio":2,"koodiArvo":"284","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Koski Tl","sv":"Koski (Ål)"},{"koodiUri":"kunta_020","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_020","versio":2,"koodiArvo":"020","voimassaAlkuPvm":"2007-01-01","paivitysPvm":"2020-12-28","fi":"Akaa","sv":"Akaa"},{"koodiUri":"kunta_301","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_301","versio":2,"koodiArvo":"301","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kurikka","sv":"Kurikka"},{"koodiUri":"kunta_936","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_936","versio":2,"koodiArvo":"936","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Virrat","sv":"Virdois"},{"koodiUri":"kunta_286","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_286","versio":2,"koodiArvo":"286","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kouvola","sv":"Kouvola"},{"koodiUri":"kunta_742","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_742","versio":2,"koodiArvo":"742","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Savukoski","sv":"Savukoski"},{"koodiUri":"kunta_858","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_858","versio":2,"koodiArvo":"858","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Tuusula","sv":"Tusby"},{"koodiUri":"kunta_103","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_103","versio":2,"koodiArvo":"103","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Humppila","sv":"Humppila"},{"koodiUri":"kunta_249","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_249","versio":2,"koodiArvo":"249","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Keuruu","sv":"Keuruu"},{"koodiUri":"kunta_287","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_287","versio":2,"koodiArvo":"287","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kristiinankaupunki","sv":"Kristinestad"},{"koodiUri":"kunta_941","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_941","versio":2,"koodiArvo":"941","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Vårdö","sv":"Vårdö"},{"koodiUri":"kunta_305","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_305","versio":2,"koodiArvo":"305","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kuusamo","sv":"Kuusamo"},{"koodiUri":"kunta_980","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_980","versio":2,"koodiArvo":"980","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Ylöjärvi","sv":"Ylöjärvi"},{"koodiUri":"kunta_783","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_783","versio":2,"koodiArvo":"783","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Säkylä","sv":"Säkylä"},{"koodiUri":"kunta_273","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_273","versio":2,"koodiArvo":"273","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kolari","sv":"Kolari"},{"koodiUri":"kunta_614","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_614","versio":2,"koodiArvo":"614","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Posio","sv":"Posio"},{"koodiUri":"kunta_257","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_257","versio":2,"koodiArvo":"257","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kirkkonummi","sv":"Kyrkslätt"},{"koodiUri":"kunta_609","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_609","versio":2,"koodiArvo":"609","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Pori","sv":"Björneborg"},{"koodiUri":"kunta_265","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_265","versio":2,"koodiArvo":"265","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kivijärvi","sv":"Kivijärvi"},{"koodiUri":"kunta_422","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_422","versio":2,"koodiArvo":"422","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Lieksa","sv":"Lieksa"},{"koodiUri":"kunta_689","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_689","versio":2,"koodiArvo":"689","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Rautjärvi","sv":"Rautjärvi"},{"koodiUri":"kunta_275","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_275","versio":2,"koodiArvo":"275","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Konnevesi","sv":"Konnevesi"},{"koodiUri":"kunta_408","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_408","versio":2,"koodiArvo":"408","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Lapua","sv":"Lappo"},{"koodiUri":"kunta_732","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_732","versio":2,"koodiArvo":"732","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Salla","sv":"Salla"},{"koodiUri":"kunta_853","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_853","versio":2,"koodiArvo":"853","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Turku","sv":"Åbo"},{"koodiUri":"kunta_616","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_616","versio":2,"koodiArvo":"616","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Pukkila","sv":"Pukkila"},{"koodiUri":"kunta_399","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_399","versio":2,"koodiArvo":"399","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Laihia","sv":"Laihia"},{"koodiUri":"kunta_016","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_016","versio":2,"koodiArvo":"016","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Asikkala","sv":"Asikkala"},{"koodiUri":"kunta_478","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_478","versio":2,"koodiArvo":"478","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Maarianhamina","sv":"Mariehamn"},{"koodiUri":"kunta_018","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_018","versio":2,"koodiArvo":"018","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Askola","sv":"Askola"},{"koodiUri":"kunta_846","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_846","versio":2,"koodiArvo":"846","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Teuva","sv":"Östermark"},{"koodiUri":"kunta_631","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_631","versio":2,"koodiArvo":"631","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Pyhäranta","sv":"Pyhäranta"},{"koodiUri":"kunta_398","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_398","versio":2,"koodiArvo":"398","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Lahti","sv":"Lahtis"},{"koodiUri":"kunta_165","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_165","versio":2,"koodiArvo":"165","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Janakkala","sv":"Janakkala"},{"koodiUri":"kunta_598","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_598","versio":2,"koodiArvo":"598","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Pietarsaari","sv":"Jakobstad"},{"koodiUri":"kunta_235","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_235","versio":2,"koodiArvo":"235","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kauniainen","sv":"Grankulla"},{"koodiUri":"kunta_288","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_288","versio":2,"koodiArvo":"288","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kruunupyy","sv":"Kronoby"},{"koodiUri":"kunta_507","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_507","versio":2,"koodiArvo":"507","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Mäntyharju","sv":"Mäntyharju"},{"koodiUri":"kunta_681","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_681","versio":2,"koodiArvo":"681","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Rantasalmi","sv":"Rantasalmi"},{"koodiUri":"kunta_981","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_981","versio":2,"koodiArvo":"981","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Ypäjä","sv":"Ypäjä"},{"koodiUri":"kunta_208","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_208","versio":2,"koodiArvo":"208","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kalajoki","sv":"Kalajoki"},{"koodiUri":"kunta_410","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_410","versio":2,"koodiArvo":"410","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Laukaa","sv":"Laukaa"},{"koodiUri":"kunta_729","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_729","versio":2,"koodiArvo":"729","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Saarijärvi","sv":"Saarijärvi"},{"koodiUri":"kunta_078","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_078","versio":2,"koodiArvo":"078","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Hanko","sv":"Hangö"},{"koodiUri":"kunta_561","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_561","versio":2,"koodiArvo":"561","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Oripää","sv":"Oripää"},{"koodiUri":"kunta_182","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_182","versio":2,"koodiArvo":"182","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Jämsä","sv":"Jämsä"},{"koodiUri":"kunta_317","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_317","versio":2,"koodiArvo":"317","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kärsämäki","sv":"Kärsämäki"},{"koodiUri":"kunta_895","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_895","versio":2,"koodiArvo":"895","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Uusikaupunki","sv":"Nystad"},{"koodiUri":"kunta_850","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_850","versio":2,"koodiArvo":"850","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Toivakka","sv":"Toivakka"},{"koodiUri":"kunta_316","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_316","versio":2,"koodiArvo":"316","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kärkölä","sv":"Kärkölä"},{"koodiUri":"kunta_217","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_217","versio":2,"koodiArvo":"217","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kannus","sv":"Kannus"},{"koodiUri":"kunta_620","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_620","versio":2,"koodiArvo":"620","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Puolanka","sv":"Puolanka"},{"koodiUri":"kunta_320","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_320","versio":2,"koodiArvo":"320","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2022-09-22","fi":"Kemijärvi","sv":"Kemijärvi"},{"koodiUri":"kunta_430","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_430","versio":2,"koodiArvo":"430","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Loimaa","sv":"Loimaa"},{"koodiUri":"kunta_638","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_638","versio":2,"koodiArvo":"638","voimassaAlkuPvm":"1997-01-01","paivitysPvm":"2018-02-08","fi":"Porvoo","sv":"Borgå"},{"koodiUri":"kunta_560","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_560","versio":2,"koodiArvo":"560","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Orimattila","sv":"Orimattila"},{"koodiUri":"kunta_680","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_680","versio":2,"koodiArvo":"680","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Raisio","sv":"Reso"},{"koodiUri":"kunta_736","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_736","versio":2,"koodiArvo":"736","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Saltvik","sv":"Saltvik"},{"koodiUri":"kunta_261","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_261","versio":2,"koodiArvo":"261","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kittilä","sv":"Kittilä"},{"koodiUri":"kunta_205","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_205","versio":2,"koodiArvo":"205","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Kajaani","sv":"Kajana"},{"koodiUri":"kunta_707","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_707","versio":2,"koodiArvo":"707","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Rääkkylä","sv":"Rääkkylä"},{"koodiUri":"kunta_743","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_743","versio":2,"koodiArvo":"743","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Seinäjoki","sv":"Seinäjoki"},{"koodiUri":"kunta_102","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_102","versio":2,"koodiArvo":"102","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Huittinen","sv":"Huittinen"},{"koodiUri":"kunta_583","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_583","versio":2,"koodiArvo":"583","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Pelkosenniemi","sv":"Pelkosenniemi"},{"koodiUri":"kunta_848","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_848","versio":2,"koodiArvo":"848","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Tohmajärvi","sv":"Tohmajärvi"},{"koodiUri":"kunta_536","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_536","versio":2,"koodiArvo":"536","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Nokia","sv":"Nokia"},{"koodiUri":"kunta_444","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_444","versio":2,"koodiArvo":"444","voimassaAlkuPvm":"1997-01-01","paivitysPvm":"2018-02-08","fi":"Lohja","sv":"Lojo"},{"koodiUri":"kunta_438","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_438","versio":2,"koodiArvo":"438","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Lumparland","sv":"Lumparland"},{"koodiUri":"kunta_139","resourceUri":"https://virkailija.opintopolku.fi/koodisto-service/rest/codeelement/kunta_139","versio":2,"koodiArvo":"139","voimassaAlkuPvm":"1990-01-01","paivitysPvm":"2018-02-08","fi":"Ii","sv":"Ii"}] diff --git a/backend/vkt/src/main/resources/localisation_fi.properties b/backend/vkt/src/main/resources/localisation_fi.properties index 7e0a304c5..43090c313 100644 --- a/backend/vkt/src/main/resources/localisation_fi.properties +++ b/backend/vkt/src/main/resources/localisation_fi.properties @@ -4,7 +4,9 @@ common.participant = Osallistuja common.phone = Puhelin common.receipt = Kuitti common.total = Yhteensä +common.examiner = Tutkinnon vastaanottaja examLevel.excellent = erinomainen taito +examLevel.goodAndSatisfactory = hyvä ja tyydyttävä taito lang.finnish = suomi lang.swedish = ruotsi oph.name = Opetushallitus @@ -27,3 +29,7 @@ skill.mail.textual = Kirjallinen taito skill.mail.oral = Suullinen taito subject.enrollment-confirmation = Vahvistus ilmoittautumisesta Valtionhallinnon kielitutkintoon (VKT) subject.enrollment-to-queue-confirmation = Vahvistus ilmoittautumisesta jonotuspaikalle Valtionhallinnon kielitutkintoon (VKT) +subject.enrollment-appointment-confirmation = Vahvistus ilmoittautumisesta Valtionhallinnon kielitutkintoon (VKT) +subject.enrollment-appointment-authentication = Ilmoittautuminen Valtionhallinnon kielitutkintoon (VKT) +subject.contact-request.receipt-notification = Valtionhallinnon kielitutkinnot: Lähettämäsi viesti tutkintosuorituksen vastaanottajalle +subject.contact-request.notice-for-examiner = Yhteydenotto Valtionhallinnon kielitutkinnosta (VKT) diff --git a/backend/vkt/src/main/resources/localisation_sv.properties b/backend/vkt/src/main/resources/localisation_sv.properties index 8e94641e9..96f8dc73c 100644 --- a/backend/vkt/src/main/resources/localisation_sv.properties +++ b/backend/vkt/src/main/resources/localisation_sv.properties @@ -4,7 +4,9 @@ common.participant = Deltagare common.phone = Telefon common.receipt = Kvitto common.total = Totalt +common.examiner = Examinator examLevel.excellent = utmärkta språkkunskaper +examLevel.goodAndSatisfactory = goda och nöjaktiga kunskaper lang.finnish = finska lang.swedish = svenska oph.name = Utbildningsstyrelsen @@ -27,3 +29,7 @@ skill.mail.textual = Förmåga att använda språket i skrift skill.mail.oral = Förmåga att använda språket i tal subject.enrollment-confirmation = Bekräftelse av anmälan till Språkexamen för statsförvaltningen (VKT) subject.enrollment-to-queue-confirmation = Bekräftelse av plats i kön för Språkexamen för statsförvaltningen (VKT) +subject.enrollment-appointment-confirmation = Bekräftelse av anmälan till Språkexamen för statsförvaltningen (VKT) +subject.enrollment-appointment-authentication = Ilmoittautuminen Valtionhallinnon kielitutkintoon (VKT) +subject.contact-request.receipt-notification = Valtionhallinnon kielitutkinnot: Lähettämäsi viesti tutkintosuorituksen vastaanottajalle +subject.contact-request.notice-for-examiner = Yhteydenotto Valtionhallinnon kielitutkinnosta (VKT) diff --git a/backend/vkt/src/main/resources/oph-configuration/vkt.properties.template b/backend/vkt/src/main/resources/oph-configuration/vkt.properties.template index 1e845099c..81e1764b5 100644 --- a/backend/vkt/src/main/resources/oph-configuration/vkt.properties.template +++ b/backend/vkt/src/main/resources/oph-configuration/vkt.properties.template @@ -19,6 +19,7 @@ email.service-url=${virkailija.host.alb}/ryhmasahkoposti-service/email/firewall reservation.duration=PT30M public-base-url={{vkt_app_url}} +clerk-base-url={{vkt_virkailija_url}} cas-oppija.login-url={{opintopolku_baseurl}}/cas-oppija/login cas-oppija.logout-url={{opintopolku_baseurl}}/cas-oppija/logout @@ -29,9 +30,12 @@ payment.paytrail.secret={{vkt_paytrail_secret}} payment.paytrail.account={{vkt_paytrail_account}} koski.url={{vkt_koski_url}} -koski.user={{vkt_koski_user}} -koski.password={{vkt_koski_password}} +onr.service-url=${virkailija.cas.base-url}/oppijanumerorekisteri-service + +cas.username={{vkt_koski_user}} +cas.password={{vkt_koski_password}} feature-flags.free-enrollment-allowed={{vkt_feature_free_enrollment_allowed}} +feature-flags.good-and-satisfactory-level={{vkt_feature_good_and_satisfactory_level}} aws.s3-bucket={{vkt_aws_s3_bucket}} diff --git a/backend/vkt/src/test/java/fi/oph/vkt/service/ClerkEnrollmentServiceTest.java b/backend/vkt/src/test/java/fi/oph/vkt/service/ClerkEnrollmentServiceTest.java index 104ad6c3b..9aa42bbe0 100644 --- a/backend/vkt/src/test/java/fi/oph/vkt/service/ClerkEnrollmentServiceTest.java +++ b/backend/vkt/src/test/java/fi/oph/vkt/service/ClerkEnrollmentServiceTest.java @@ -25,10 +25,7 @@ import fi.oph.vkt.model.Person; import fi.oph.vkt.model.type.EnrollmentStatus; import fi.oph.vkt.model.type.ExamLanguage; -import fi.oph.vkt.repository.EnrollmentRepository; -import fi.oph.vkt.repository.ExamEventRepository; -import fi.oph.vkt.repository.FreeEnrollmentRepository; -import fi.oph.vkt.repository.PaymentRepository; +import fi.oph.vkt.repository.*; import fi.oph.vkt.service.koski.KoskiService; import fi.oph.vkt.util.ClerkEnrollmentUtil; import fi.oph.vkt.util.UUIDSource; diff --git a/backend/vkt/src/test/java/fi/oph/vkt/service/ClerkExamEventServiceTest.java b/backend/vkt/src/test/java/fi/oph/vkt/service/ClerkExamEventServiceTest.java index 8f93850f4..8db00582d 100644 --- a/backend/vkt/src/test/java/fi/oph/vkt/service/ClerkExamEventServiceTest.java +++ b/backend/vkt/src/test/java/fi/oph/vkt/service/ClerkExamEventServiceTest.java @@ -116,7 +116,7 @@ public void testListExamEvents() { createEnrollment(futureEvent, EnrollmentStatus.AWAITING_PAYMENT); createEnrollment(futureEvent, EnrollmentStatus.CANCELED); - final List examEventListDTOs = clerkExamEventService.list(); + final List examEventListDTOs = clerkExamEventService.list(ExamLevel.EXCELLENT); assertEquals(7, examEventListDTOs.size()); final List expectedExamEventsOrdered = List.of( diff --git a/backend/vkt/src/test/java/fi/oph/vkt/service/PaymentServiceTest.java b/backend/vkt/src/test/java/fi/oph/vkt/service/PaymentServiceTest.java index 95284767a..faa742915 100644 --- a/backend/vkt/src/test/java/fi/oph/vkt/service/PaymentServiceTest.java +++ b/backend/vkt/src/test/java/fi/oph/vkt/service/PaymentServiceTest.java @@ -23,11 +23,13 @@ import fi.oph.vkt.model.type.AppLocale; import fi.oph.vkt.model.type.EnrollmentSkill; import fi.oph.vkt.model.type.EnrollmentStatus; +import fi.oph.vkt.model.type.ExamLevel; import fi.oph.vkt.model.type.PaymentStatus; import fi.oph.vkt.payment.paytrail.Customer; import fi.oph.vkt.payment.paytrail.Item; import fi.oph.vkt.payment.paytrail.PaytrailPaymentProvider; import fi.oph.vkt.payment.paytrail.PaytrailResponseDTO; +import fi.oph.vkt.repository.EnrollmentAppointmentRepository; import fi.oph.vkt.repository.EnrollmentRepository; import fi.oph.vkt.repository.PaymentRepository; import fi.oph.vkt.util.exception.APIException; @@ -56,6 +58,9 @@ public class PaymentServiceTest { @Resource private EnrollmentRepository enrollmentRepository; + @Resource + private EnrollmentAppointmentRepository enrollmentAppointmentRepository; + @Resource private TestEntityManager entityManager; @@ -94,19 +99,32 @@ public void testCreatePaymentWithAllSkills() { paymentProvider, paymentRepository, enrollmentRepository, + enrollmentAppointmentRepository, environment, publicEnrollmentEmailService ); final String redirectUrl = paymentService.createPaymentForEnrollment(enrollment.getId(), person, AppLocale.FI); final List items = List.of( - Item.builder().units(1).unitPrice(25700).vatPercentage(0).productCode(EnrollmentSkill.TEXTUAL.toString()).build(), - Item.builder().units(1).unitPrice(25700).vatPercentage(0).productCode(EnrollmentSkill.ORAL.toString()).build(), + Item + .builder() + .units(1) + .unitPrice(25700) + .vatPercentage(0) + .productCode(ExamLevel.EXCELLENT + "-" + EnrollmentSkill.TEXTUAL) + .build(), + Item + .builder() + .units(1) + .unitPrice(25700) + .vatPercentage(0) + .productCode(ExamLevel.EXCELLENT + "-" + EnrollmentSkill.ORAL) + .build(), Item .builder() .units(1) .unitPrice(0) .vatPercentage(0) - .productCode(EnrollmentSkill.UNDERSTANDING.toString()) + .productCode(ExamLevel.EXCELLENT + "-" + EnrollmentSkill.UNDERSTANDING) .build() ); final Customer customer = Customer @@ -160,19 +178,26 @@ public void testCreatePaymentWithoutOralSkill() { paymentProvider, paymentRepository, enrollmentRepository, + enrollmentAppointmentRepository, environment, publicEnrollmentEmailService ); final String redirectUrl = paymentService.createPaymentForEnrollment(enrollment.getId(), person, AppLocale.FI); final List items = List.of( - Item.builder().units(1).unitPrice(25700).vatPercentage(0).productCode(EnrollmentSkill.TEXTUAL.toString()).build(), + Item + .builder() + .units(1) + .unitPrice(25700) + .vatPercentage(0) + .productCode(ExamLevel.EXCELLENT + "-" + EnrollmentSkill.TEXTUAL) + .build(), Item .builder() .units(1) .unitPrice(0) .vatPercentage(0) - .productCode(EnrollmentSkill.UNDERSTANDING.toString()) + .productCode(ExamLevel.EXCELLENT + "-" + EnrollmentSkill.UNDERSTANDING) .build() ); @@ -203,6 +228,7 @@ public void testCreatePaymentPassesProperCustomerDataToPaymentProvider() { paymentProvider, paymentRepository, enrollmentRepository, + enrollmentAppointmentRepository, environment, publicEnrollmentEmailService ); @@ -237,6 +263,7 @@ public void testCreatePaymentWrongPerson() { paymentProvider, paymentRepository, enrollmentRepository, + enrollmentAppointmentRepository, environment, publicEnrollmentEmailService ); @@ -259,6 +286,7 @@ public void testCreatePaymentEnrollmentNotFound() { paymentProvider, paymentRepository, enrollmentRepository, + enrollmentAppointmentRepository, environment, publicEnrollmentEmailService ); @@ -287,6 +315,7 @@ public void testFinalizePaymentOnSuccess() throws IOException, InterruptedExcept paymentProvider, paymentRepository, enrollmentRepository, + enrollmentAppointmentRepository, environment, publicEnrollmentEmailService ); @@ -315,6 +344,7 @@ public void testFinalizePaymentOnFailure() throws IOException, InterruptedExcept paymentProvider, paymentRepository, enrollmentRepository, + enrollmentAppointmentRepository, environment, publicEnrollmentEmailService ); @@ -340,6 +370,7 @@ public void testFinalizePaymentValidationFailed() { paymentProvider, paymentRepository, enrollmentRepository, + enrollmentAppointmentRepository, environment, publicEnrollmentEmailService ); @@ -368,6 +399,7 @@ public void testFinalizePaymentOnFailureAlreadyPaid() { paymentProvider, paymentRepository, enrollmentRepository, + enrollmentAppointmentRepository, environment, publicEnrollmentEmailService ); @@ -396,6 +428,7 @@ public void testFinalizePaymentOnSuccessAlreadyPaid() throws IOException, Interr paymentProvider, paymentRepository, enrollmentRepository, + enrollmentAppointmentRepository, environment, publicEnrollmentEmailService ); @@ -420,6 +453,7 @@ public void testFinalizePaymentAmountMustMatch() { paymentProvider, paymentRepository, enrollmentRepository, + enrollmentAppointmentRepository, environment, publicEnrollmentEmailService ); @@ -449,6 +483,7 @@ public void testFinalizePaymentReferenceIdMustMatch() { paymentProvider, paymentRepository, enrollmentRepository, + enrollmentAppointmentRepository, environment, publicEnrollmentEmailService ); @@ -471,6 +506,7 @@ public void testFinalizePaymentPaymentNotFound() { paymentProvider, paymentRepository, enrollmentRepository, + enrollmentAppointmentRepository, environment, publicEnrollmentEmailService ); diff --git a/backend/vkt/src/test/java/fi/oph/vkt/service/PublicEnrollmentEmailServiceTest.java b/backend/vkt/src/test/java/fi/oph/vkt/service/PublicEnrollmentEmailServiceTest.java index 9cbfa7ea1..2c9c83c98 100644 --- a/backend/vkt/src/test/java/fi/oph/vkt/service/PublicEnrollmentEmailServiceTest.java +++ b/backend/vkt/src/test/java/fi/oph/vkt/service/PublicEnrollmentEmailServiceTest.java @@ -67,7 +67,7 @@ public void setup() throws IOException, InterruptedException { final EmailService emailService = new EmailService(emailRepository, emailAttachmentRepository, emailSender); final ReceiptRenderer receiptRenderer = mock(ReceiptRenderer.class); - when(receiptRenderer.getReceiptData(anyLong(), any())) + when(receiptRenderer.getReceiptData(any(), any())) .thenReturn( ReceiptData .builder() diff --git a/backend/vkt/src/test/java/fi/oph/vkt/service/PublicEnrollmentServiceTest.java b/backend/vkt/src/test/java/fi/oph/vkt/service/PublicEnrollmentServiceTest.java index d007fe3cd..2d28be507 100644 --- a/backend/vkt/src/test/java/fi/oph/vkt/service/PublicEnrollmentServiceTest.java +++ b/backend/vkt/src/test/java/fi/oph/vkt/service/PublicEnrollmentServiceTest.java @@ -25,8 +25,10 @@ import fi.oph.vkt.model.Person; import fi.oph.vkt.model.Reservation; import fi.oph.vkt.model.type.EnrollmentStatus; +import fi.oph.vkt.repository.EnrollmentAppointmentRepository; import fi.oph.vkt.repository.EnrollmentRepository; import fi.oph.vkt.repository.ExamEventRepository; +import fi.oph.vkt.repository.ExaminerRepository; import fi.oph.vkt.repository.FreeEnrollmentRepository; import fi.oph.vkt.repository.ReservationRepository; import fi.oph.vkt.repository.UploadedFileAttachmentRepository; @@ -59,6 +61,9 @@ public class PublicEnrollmentServiceTest { @Resource private EnrollmentRepository enrollmentRepository; + @Resource + private EnrollmentAppointmentRepository enrollmentAppointmentRepository; + @Resource private ExamEventRepository examEventRepository; @@ -85,9 +90,19 @@ public class PublicEnrollmentServiceTest { @Resource private UploadedFileAttachmentRepository uploadedFileAttachmentRepository; + @Resource + private ExaminerRepository examinerRepository; + + @MockBean + private ContactEmailService contactEmailServiceMock; + @BeforeEach public void setup() throws IOException, InterruptedException { doNothing().when(publicEnrollmentEmailServiceMock).sendEnrollmentToQueueConfirmationEmail(any(), any()); + doNothing().when(publicEnrollmentEmailServiceMock).sendEnrollmentConfirmationEmail(any()); + doNothing().when(publicEnrollmentEmailServiceMock).sendEnrollmentAppointmentConfirmationEmail(any()); + doNothing().when(contactEmailServiceMock).sendReceiptNotificationForContactRequest(any()); + doNothing().when(contactEmailServiceMock).sendExaminerNotificationOfContactRequest(any()); final Environment environment = mock(Environment.class); when(environment.getRequiredProperty("app.reservation.duration")).thenReturn(ONE_MINUTE.toString()); @@ -102,6 +117,7 @@ public void setup() throws IOException, InterruptedException { publicEnrollmentService = new PublicEnrollmentService( enrollmentRepository, + enrollmentAppointmentRepository, examEventRepository, publicEnrollmentEmailServiceMock, publicReservationService, @@ -110,7 +126,9 @@ public void setup() throws IOException, InterruptedException { s3Service, featureFlagService, uploadedFileAttachmentRepository, - koskiService + koskiService, + examinerRepository, + contactEmailServiceMock ); } diff --git a/backend/vkt/src/test/java/fi/oph/vkt/service/receipt/ReceiptRendererTest.java b/backend/vkt/src/test/java/fi/oph/vkt/service/receipt/ReceiptRendererTest.java index 79f00bbab..66e82ce76 100644 --- a/backend/vkt/src/test/java/fi/oph/vkt/service/receipt/ReceiptRendererTest.java +++ b/backend/vkt/src/test/java/fi/oph/vkt/service/receipt/ReceiptRendererTest.java @@ -67,7 +67,7 @@ public void testGetReceiptDataInFinnish() { entityManager.persist(enrollment); entityManager.persist(payment); - final ReceiptData receiptData = receiptRenderer.getReceiptData(enrollment.getId(), LocalisationUtil.localeFI); + final ReceiptData receiptData = receiptRenderer.getReceiptData(enrollment, LocalisationUtil.localeFI); assertNotNull(receiptData); assertEquals("RF-123", receiptData.paymentReference()); assertEquals("Ruotsi, erinomainen taito, 07.10.2024", receiptData.exam()); @@ -105,7 +105,7 @@ public void testGetReceiptDataInSwedish() { entityManager.persist(enrollment); entityManager.persist(payment); - final ReceiptData receiptData = receiptRenderer.getReceiptData(enrollment.getId(), LocalisationUtil.localeSV); + final ReceiptData receiptData = receiptRenderer.getReceiptData(enrollment, LocalisationUtil.localeSV); assertNotNull(receiptData); assertEquals("Svenska, utmärkta språkkunskaper, 07.10.2024", receiptData.exam()); diff --git a/frontend/package.json b/frontend/package.json index 4dccbc0c0..d580a63e1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,6 +28,7 @@ "@emotion/react": "^11.13.0", "@emotion/styled": "^11.13.0", "@fontsource/roboto": "^5.1.0", + "@mui/base": "5.0.0-beta.58", "@mui/icons-material": "^5.16.7", "@mui/material": "^5.16.7", "@mui/system": "^5.16.7", @@ -109,8 +110,5 @@ "webpack": "^5.94.0", "webpack-cli": "^5.1.4", "webpack-dev-server": "^4.15.1" - }, - "@comments": { - "react-router-dom": "Do not update past 6.13.0 until useBlocker is fixed, see https://github.com/remix-run/react-router/issues/11155" } } diff --git a/frontend/packages/shared/CHANGELOG.MD b/frontend/packages/shared/CHANGELOG.MD index db4d7c429..8236d5937 100644 --- a/frontend/packages/shared/CHANGELOG.MD +++ b/frontend/packages/shared/CHANGELOG.MD @@ -9,6 +9,18 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Released] +## [1.11.6] - 2024-11-12 + +### Changed +- LabeledTextField accepts an optional className argument +- sortOptionsByLabels accepts an optional locale argument; if not supplied, 'fi-FI' is used by default +- Preliminary, minimal (validation) support for + +## [1.11.5] - 2024-09-27 + +### Added +- MobileNavigationMenuWithPortal component + ## [1.11.4] - 2024-10-07 ### Fixed diff --git a/frontend/packages/shared/package.json b/frontend/packages/shared/package.json index 048f83414..35bc27955 100644 --- a/frontend/packages/shared/package.json +++ b/frontend/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@opetushallitus/kieli-ja-kaantajatutkinnot.shared", - "version": "1.11.4", + "version": "1.11.6", "description": "Shared Frontend Package", "exports": { "./components": "./src/components/index.tsx", diff --git a/frontend/packages/shared/src/components/ComboBox/ComboBox.tsx b/frontend/packages/shared/src/components/ComboBox/ComboBox.tsx index 053a473d5..7ce31fb8e 100644 --- a/frontend/packages/shared/src/components/ComboBox/ComboBox.tsx +++ b/frontend/packages/shared/src/components/ComboBox/ComboBox.tsx @@ -1,6 +1,9 @@ +import CheckBoxIcon from '@mui/icons-material/CheckBox'; +import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank'; import { Autocomplete, AutocompleteProps, + Checkbox, createFilterOptions, FilterOptionsState, FormControl, @@ -40,12 +43,13 @@ type AutoCompleteComboBox = Omit< | 'onChange' >; -const compareOptionLabels = (a: ComboBoxOption, b: ComboBoxOption) => { - return a.label.localeCompare(b.label, undefined, { sensitivity: 'base' }); -}; +export const sortOptionsByLabels = ( + options: Array, + locale: string = 'fi-FI', +) => { + const collator = new Intl.Collator(locale, { sensitivity: 'base' }); -export const sortOptionsByLabels = (options: Array) => { - return options.sort(compareOptionLabels); + return options.sort((a, b) => collator.compare(a.label, b.label)); }; const isOptionEqualToValue = ( @@ -167,3 +171,66 @@ export const LabeledComboBox = ({ ); }; + +type AutoCompleteMultipleComboBox = AutocompleteProps< + ComboBoxOption, + true, + false, + false +>; + +export const LabeledMultipleCheckboxDropdown = ({ + id, + label, + helperText, + showError, + values, + variant, + value, + onChange, + ...rest +}: Omit & + Omit & { + id: string; + }) => { + const errorStyles = showError ? { color: 'error.main' } : {}; + const icon = ; + const checkedIcon = ; + return ( + + + option.value === value.value} + renderOption={(props, option, { selected }) => { + const { key, ...optionProps } = props; + return ( +
  • + + {option?.label} +
  • + ); + }} + renderInput={(params) => ( + + )} + onChange={onChange} + {...rest} + /> + {showError && {helperText}} +
    + ); +}; diff --git a/frontend/packages/shared/src/components/LabeledTextField/LabeledTextField.tsx b/frontend/packages/shared/src/components/LabeledTextField/LabeledTextField.tsx index 25332e7fa..05721bfcd 100644 --- a/frontend/packages/shared/src/components/LabeledTextField/LabeledTextField.tsx +++ b/frontend/packages/shared/src/components/LabeledTextField/LabeledTextField.tsx @@ -7,6 +7,7 @@ import { Text } from '../Text/Text'; export type LabeledTextFieldProps = { id: string; label: string; + className?: string; } & CustomTextFieldProps; export const LabeledTextField = ({ @@ -14,12 +15,13 @@ export const LabeledTextField = ({ label, placeholder, error, + className, ...rest }: LabeledTextFieldProps) => { const errorStyles = error ? { color: 'error.main' } : {}; return ( -
    +