diff --git a/resources/db/migration/V20241209__add_invalidated_at_to_invoices.sql b/resources/db/migration/V20241209__add_invalidated_at_to_invoices.sql new file mode 100644 index 0000000..84d2238 --- /dev/null +++ b/resources/db/migration/V20241209__add_invalidated_at_to_invoices.sql @@ -0,0 +1,29 @@ +ALTER TABLE invoices ADD COLUMN invalidated_at TIMESTAMP; + +CREATE OR REPLACE VIEW all_invoices +AS +SELECT + i.id, + i.order_id, + i.first_name, + i.last_name, + i.email, + i.amount, + i.origin, + i.reference, + i.due_date, + i.created_at, + s.secret, + CASE + WHEN p.paid_at IS NOT NULL THEN 'paid' + WHEN i.invalidated_at IS NOT NULL THEN 'invalidated' + WHEN i.due_date < CURRENT_DATE THEN 'overdue' + ELSE 'active' + END AS status, + p.paid_at, + i.metadata, + i.vat, + i.invalidated_at +FROM invoices i + LEFT OUTER JOIN latest_secrets s on (i.id = s.id) + LEFT OUTER JOIN latest_payments p on (i.id = p.id); diff --git a/src/clj/maksut/maksut/db/maksut_queries.clj b/src/clj/maksut/maksut/db/maksut_queries.clj index f2f7884..649cdb1 100644 --- a/src/clj/maksut/maksut/db/maksut_queries.clj +++ b/src/clj/maksut/maksut/db/maksut_queries.clj @@ -21,6 +21,10 @@ (declare select-payment) (declare insert-payment!) (declare insert-secret-for-invoice!) +(declare invalidate-laskut-by-reference!) + +(defn invalidate-laskut-by-reference [db refs] + (invalidate-laskut-by-reference! db {:refs refs})) (defn- insert-new-secret [db invoice-id order-id] ;prefix secrets with order-id to force them unique even if random would generate two identical @@ -50,9 +54,10 @@ (let [status (:status old-ai) same-origin (= (:origin old-ai) (:origin new))] (cond - (= status "overdue") (maksut-error :invoice-invalidstate-overdue (str "Ei voi muuttaa, eräpäivä mennyt: " new)) - (= status "paid") (maksut-error :invoice-invalidstate-paid (str "Ei voi muuttaa, lasku on jo maksettu: " new)) - (not same-origin) (maksut-error :invoice-createerror-originclash (str "Sama lasku eri lähteestä on jo olemassa: " new))) + (= status "overdue") (maksut-error :invoice-invalidstate-overdue (str "Ei voi muuttaa, eräpäivä mennyt: " new)) + (= status "paid") (maksut-error :invoice-invalidstate-paid (str "Ei voi muuttaa, lasku on jo maksettu: " new)) + (= status "invalidated") (maksut-error :invoice-invalidstate-invalidated (str "Ei voi muuttaa, mitätöity: " new)) + (not same-origin) (maksut-error :invoice-createerror-originclash (str "Sama lasku eri lähteestä on jo olemassa: " new))) true)) (defn get-lasku [db order-id] diff --git a/src/clj/maksut/maksut/db/maksut_queries.sql b/src/clj/maksut/maksut/db/maksut_queries.sql index cb65a7c..7238666 100644 --- a/src/clj/maksut/maksut/db/maksut_queries.sql +++ b/src/clj/maksut/maksut/db/maksut_queries.sql @@ -36,6 +36,11 @@ SET --~ (when (some? (:metadata params)) ", metadata = :metadata") WHERE order_id = :order-id AND CURRENT_DATE <= due_date; +-- :name invalidate-laskut-by-reference! :! :n +UPDATE invoices +SET invalidated_at = now() +WHERE reference IN (:v*:refs) AND CURRENT_DATE <= due_date; + -- :name get-lasku-locked :? :1 SELECT * FROM invoices diff --git a/src/clj/maksut/maksut/maksut_service.clj b/src/clj/maksut/maksut/maksut_service.clj index b6370e6..a27ec24 100644 --- a/src/clj/maksut/maksut/maksut_service.clj +++ b/src/clj/maksut/maksut/maksut_service.clj @@ -166,4 +166,13 @@ (get-lasku-contact [_ _ secret] (if-let [laskut (seq (maksut-queries/get-laskut-by-secret db secret))] {:contact (contact-email (first laskut))} - (maksut-error :invoice-notfound-secret (str "Linkki on väärä tai vanhentunut: " secret) {:status-code 404})))) + (maksut-error :invoice-notfound-secret (str "Linkki on väärä tai vanhentunut: " secret) {:status-code 404}))) + + ; NB: only marks the payments invalid on our side so that it can't be accidentally paid anymore. + ; The actual Paytrail payment is still open and updated internally. + (invalidate-laskut [_ _ input] + (log/info (str "Invalidating invoices with references:" keys)) + (let [{:keys [keys]} input + _ (maksut-queries/invalidate-laskut-by-reference db keys) + statuses (maksut-queries/check-laskut-statuses-by-reference db keys)] + (map LaskuStatus->json statuses)))) diff --git a/src/clj/maksut/maksut/maksut_service_protocol.clj b/src/clj/maksut/maksut/maksut_service_protocol.clj index c5b2df5..3878245 100644 --- a/src/clj/maksut/maksut/maksut_service_protocol.clj +++ b/src/clj/maksut/maksut/maksut_service_protocol.clj @@ -7,4 +7,5 @@ (check-status [this session input]) (get-lasku [this session order-id]) (get-lasku-contact [this session secret]) - (get-laskut-by-secret [this session secret])) + (get-laskut-by-secret [this session secret]) + (invalidate-laskut [this session input])) diff --git a/src/clj/maksut/payment/payment_service.clj b/src/clj/maksut/payment/payment_service.clj index aa808ab..818d8c4 100644 --- a/src/clj/maksut/payment/payment_service.clj +++ b/src/clj/maksut/payment/payment_service.clj @@ -86,7 +86,7 @@ "items" [{"description" (case origin "tutu" (create-description language-code order-number) "astu" (str (get-translation (keyword language-code) :astukuitti/oph) " " form-name) - "kkhakemusmaksu" (create-kk-payment-description language-code form-name)) + "kkhakemusmaksu" (create-kk-payment-description language-code haku-name)) "units" 1 "unitPrice" amount-in-euro-cents "vatPercentage" (or vat vat-zero) @@ -158,6 +158,7 @@ (cond (not (some? lasku)) (maksut-error :invoice-notfound (str "Laskua ei löydy: " secret)) (= (:status lasku) "overdue") (maksut-error :invoice-invalidstate-overdue (str "Lasku on erääntynyt: " secret)) + (= (:status lasku) "invalidated") (maksut-error :invoice-invalidstate-invalidated (str "Lasku on mitätöity: " secret)) (= (:status lasku) "paid") (maksut-error :invoice-invalidstate-paid (str "Lasku on jo maksettu: " secret))) (when (not= (:status lasku) "active") diff --git a/src/clj/maksut/routes.clj b/src/clj/maksut/routes.clj index 3461474..42ede19 100644 --- a/src/clj/maksut/routes.clj +++ b/src/clj/maksut/routes.clj @@ -177,6 +177,18 @@ (let [x (maksut-protocol/check-status maksut-service session input)] (response/ok x)))}}]] + ["/lasku-invalidate" + ["" + {:post {:middleware auth + :tags ["Lasku"] + :summary "Mitätöi yhden tai useamman laskun viitenumeron perusteella" + :responses {200 {:body schema/LaskuStatusList}} + :parameters {:body schema/LaskuRefList} + :handler (fn [{session :session {input :body} :parameters}] + (log/info "Invalidate invoices for" (count input) "keys") + (let [resp (maksut-protocol/invalidate-laskut maksut-service session input)] + (response/ok resp)))}}]] + ["/lasku/:application-key" ["" {:get {:middleware auth diff --git a/src/cljc/maksut/api_schemas.cljc b/src/cljc/maksut/api_schemas.cljc index 96bbe59..7331301 100644 --- a/src/cljc/maksut/api_schemas.cljc +++ b/src/cljc/maksut/api_schemas.cljc @@ -73,7 +73,8 @@ (s/enum :active :paid - :overdue)) + :overdue + :invalidated)) (s/defschema LaskuRefList {:keys [s/Str]}) diff --git a/src/cljc/maksut/translations.cljc b/src/cljc/maksut/translations.cljc index edb1985..30bba08 100644 --- a/src/cljc/maksut/translations.cljc +++ b/src/cljc/maksut/translations.cljc @@ -79,6 +79,9 @@ :KkHakemusmaksuPanel.eraantynyt {:fi "Hakemusmaksun määräaika on erääntynyt, etkä voi enää maksaa hakemusmaksua. Hakemustasi ei käsitellä, etkä voi tulla valituksi koulutukseen. Mikäli hakuaikaa on vielä jäljellä, voit täyttää uuden hakemuksen." :en "The due date for your application fee payment has expired. You can no longer pay the application fee. Your application will not be reviewed, and you cannot be offered admission. If the application period is still ongoing, you can fill in and send a new application." :sv "Tidsfristen för ansökningsavgiften har gått ut och du kan inte längre betala avgiften. Din ansökan behandlas inte och du kan inte bli antagen till utbildningen. Om ansökningstiden ännu pågår kan du fylla i en ny ansökan."} + :KkHakemusmaksuPanel.mitatoity {:fi "Olet jo maksanut hakemusmaksun tälle aloituskaudelle." + :en "EN Olet jo maksanut hakemusmaksun tälle aloituskaudelle." + :sv "SV Olet jo maksanut hakemusmaksun tälle aloituskaudelle."} :KkHakemusmaksuPanel.aloituskausi {:fi "Alkamiskausi" :en "Start term" :sv "Starttermin"} @@ -118,6 +121,9 @@ :Maksu.overdue {:fi "Erääntynyt" :en "Expired" :sv "Förfallen"} + :Maksu.invalidated {:fi "Mitätöity" + :en "EN: mitätöity" + :sv "SV: mitätöity"} :Maksu.summa {:fi "Määrä" :en "Amount" :sv "Summa"} diff --git a/src/maksut-ui/app/components/KkHakemusmaksuPanel.tsx b/src/maksut-ui/app/components/KkHakemusmaksuPanel.tsx index f2e86b8..01a65a7 100644 --- a/src/maksut-ui/app/components/KkHakemusmaksuPanel.tsx +++ b/src/maksut-ui/app/components/KkHakemusmaksuPanel.tsx @@ -53,6 +53,12 @@ const KkHakemusmaksuPanel = ({ lasku }: { lasku: Lasku }) => { {t('eraantynyt')} ); + } else if (lasku.status === 'invalidated') { + return ( + <> + {t('mitatoity')} + + ); } else { return ( <> diff --git a/src/maksut-ui/app/components/Maksu.tsx b/src/maksut-ui/app/components/Maksu.tsx index 7a4b3f3..3fa2d6c 100644 --- a/src/maksut-ui/app/components/Maksu.tsx +++ b/src/maksut-ui/app/components/Maksu.tsx @@ -27,6 +27,11 @@ const StatusRow = ({ status }: { status: PaymentStatus }) => { dot: '#61a33b', text: '#237a00', }, + invalidated: { + backgroundColor: '#e2fae4', + dot: '#61a33b', + text: '#237a00', + }, }; return ( diff --git a/src/maksut-ui/app/lib/types.ts b/src/maksut-ui/app/lib/types.ts index c5ec2d3..35b87e0 100644 --- a/src/maksut-ui/app/lib/types.ts +++ b/src/maksut-ui/app/lib/types.ts @@ -1,4 +1,4 @@ -export type PaymentStatus = 'active' | 'paid' | 'overdue'; +export type PaymentStatus = 'active' | 'paid' | 'overdue' | 'invalidated'; export type PaymentState = | 'kasittelymaksamatta' diff --git a/test/clj/maksut/payment/payment_service_spec.clj b/test/clj/maksut/payment/payment_service_spec.clj index e5f821c..34768d1 100644 --- a/test/clj/maksut/payment/payment_service_spec.clj +++ b/test/clj/maksut/payment/payment_service_spec.clj @@ -376,6 +376,45 @@ (is (= (:code data) :invoice-invalidstate-overdue))))) )) +(deftest pay-invalidated-invoice + (let [service (:payment-service @test-system) + maksut-service (:maksut-service @test-system) + db (:db @test-system) + + due-date (time/from-now (time/days +7)) + db-data (db-invoice-hakemusmaksu due-date) + secret "foobar" + invoice-insert (test-fixtures/add-invoice! db + (merge db-data {:invalidated_at (to-sql-date "2024-12-09 10:23:54")})) + invoice-id (-> invoice-insert first :id)] + + (jdbc/insert! db :secrets {:fk_invoice invoice-id + :secret secret}) + + (testing "Try to pay invoice that has been invalidated" + (let [exc (catch-thrown-info (payment-protocol/payment service maksut-test-fixtures/fake-session + {:order-id (:order_id db-data) + :locale "fi" + :secret secret})) + data (:data exc)] + (is (= (:type data) :maksut.error)) + (is (= (:code data) :invoice-invalidstate-invalidated)) + )) + + (testing "Try to edit invalidated invoice" + (let [lasku {:reference (:reference db-data) + :first-name (:first_name db-data) + :last-name (:last_name db-data) + :email (:email db-data) + :amount "222.00" + :due-days 7 + :origin (:origin db-data)}] + (let [exc (catch-thrown-info (maksut-protocol/create maksut-service maksut-test-fixtures/fake-session lasku)) + data (:data exc)] + (is (= (:type data) :maksut.error)) + (is (= (:code data) :invoice-invalidstate-invalidated))))) + )) + (deftest pay-at-due-date (let [service (:payment-service @test-system) db (:db @test-system)