diff --git a/deploy/available_payment_status.sql b/deploy/available_payment_status.sql index 118a2bd..fa8585b 100644 --- a/deploy/available_payment_status.sql +++ b/deploy/available_payment_status.sql @@ -7,27 +7,35 @@ begin; insert into camper.payment_status (payment_status, name) values ('draft', 'Draft') , ('pending', 'Pending') + , ('preauth', 'Authorization held') , ('failed', 'Failed') , ('completed', 'Completed') , ('refunded', 'Refunded') + , ('voided', 'Voided') ; insert into camper.payment_status_i18n (payment_status, lang_tag, name) values ('draft', 'ca', 'Esborrany') , ('pending', 'ca', 'Pendent') + , ('preauth', 'ca', 'Autorització retinguda') , ('failed', 'ca', 'No realitzat') , ('completed', 'ca', 'Completat') , ('refunded', 'ca', 'Reemborsat') + , ('voided', 'ca', 'Anul·lat') , ('draft', 'es', 'Borrador') , ('pending', 'es', 'Pendiente') + , ('preauth', 'es', 'Autorización retenida') , ('failed', 'es', 'Fallido') , ('completed', 'es', 'Completado') , ('refunded', 'es', 'Reembolsado') + , ('voided', 'es', 'Anulado') , ('draft', 'fr', 'Brouillon') , ('pending', 'fr', 'En attente') + , ('preauth', 'fr', 'Autorisation détenue') , ('failed', 'fr', 'Échouée') , ('completed', 'fr', 'Terminée') , ('refunded', 'fr', 'Remboursée') + , ('voided', 'fr', 'Annulé') ; commit; diff --git a/deploy/payment_redsys_response.sql b/deploy/payment_redsys_response.sql index a66eef6..9cec7c7 100644 --- a/deploy/payment_redsys_response.sql +++ b/deploy/payment_redsys_response.sql @@ -9,7 +9,8 @@ begin; set search_path to camper, public; create table payment_redsys_response ( - payment_id integer primary key references payment, + payment_redsys_response_id integer generated by default as identity primary key, + payment_id integer not null references payment, response_code integer not null, date_time timestamp without time zone not null, secure_payment boolean not null, diff --git a/deploy/process_payment_response.sql b/deploy/process_payment_response.sql index 9d1c47c..897b846 100644 --- a/deploy/process_payment_response.sql +++ b/deploy/process_payment_response.sql @@ -17,22 +17,40 @@ declare pid integer; next_status text; begin - if response.transaction_type <> 0 then + if response.transaction_type not in (0, 1, 2, 3, 9) then raise invalid_parameter_value using message = response.transaction_type || ' is not a processable transaction type'; end if; - update payment - set payment_status = case when response.response_code < 100 then 'completed' else 'failed' end - , updated_at = current_timestamp - where slug = payment_slug - and payment_status in ('pending', 'failed') - returning payment_id, payment_status - into pid, next_status; - + select payment_id into pid from payment where slug = payment_slug; if pid is null then return ''; end if; + update payment + set payment_status = case response.transaction_type + when 0 then -- charge + case when response.response_code < 100 then 'completed' else 'failed' end + when 1 then -- authorization hold + case when response.response_code < 100 then 'preauth' else 'failed' end + when 2 then -- confirm authorization hold + case when response.response_code = 900 then 'completed' else payment_status end + when 3 then -- refund + case when response.response_code = 900 then 'refunded' else payment_status end + when 9 then -- void authorization hold + case when response.response_code = 400 then 'voided' else payment_status end + else + payment_status + end + , updated_at = current_timestamp + where payment_id = pid + and ( + (response.transaction_type in (0, 1) and payment_status in ('pending', 'failed')) + or (response.transaction_type in (2, 9) and payment_status in ('preauth', 'failed')) + or (response.transaction_type = 3 and payment_status = 'completed') + ) + returning payment_status + into next_status; + insert into payment_redsys_response ( payment_id , response_code @@ -61,21 +79,9 @@ begin , response.error_code from currency where currency.currency_code = response.currency_code - on conflict (payment_id) do update - set response_code = excluded.response_code - , date_time = excluded.date_time - , secure_payment = excluded.secure_payment - , transaction_type = excluded.transaction_type - , amount = excluded.amount - , currency_code = excluded.currency_code - , order_number = excluded.order_number - , authorization_code = excluded.authorization_code - , merchant_code = excluded.merchant_code - , terminal_number = excluded.terminal_number - , error_code = excluded.error_code ; - return next_status; + return coalesce(next_status, ''); end; $$ language plpgsql diff --git a/test/payment_redsys_response.sql b/test/payment_redsys_response.sql index 1bd4d3c..14c5c16 100644 --- a/test/payment_redsys_response.sql +++ b/test/payment_redsys_response.sql @@ -5,7 +5,7 @@ reset client_min_messages; begin; -select plan(59); +select plan(63); set search_path to camper, public; @@ -16,8 +16,13 @@ select table_privs_are('payment_redsys_response', 'employee', array['SELECT', 'I select table_privs_are('payment_redsys_response', 'admin', array['SELECT', 'INSERT', 'UPDATE', 'DELETE']); select table_privs_are('payment_redsys_response', 'authenticator', array[]::text[]); +select has_column('payment_redsys_response', 'payment_redsys_response_id'); +select col_is_pk('payment_redsys_response', 'payment_redsys_response_id'); +select col_type_is('payment_redsys_response', 'payment_redsys_response_id', 'integer'); +select col_not_null('payment_redsys_response', 'payment_redsys_response_id'); +select col_hasnt_default('payment_redsys_response', 'payment_redsys_response_id'); + select has_column('payment_redsys_response', 'payment_id'); -select col_is_pk('payment_redsys_response', 'payment_id'); select col_is_fk('payment_redsys_response', 'payment_id'); select fk_ok('payment_redsys_response', 'payment_id', 'payment', 'payment_id'); select col_type_is('payment_redsys_response', 'payment_id', 'integer'); diff --git a/test/process_payment_response.sql b/test/process_payment_response.sql index 87f7f4a..8fc74f3 100644 --- a/test/process_payment_response.sql +++ b/test/process_payment_response.sql @@ -5,7 +5,7 @@ reset client_min_messages; begin; -select plan(19); +select plan(24); set search_path to camper, public; @@ -52,6 +52,9 @@ values (22, '4ef35e2f-ef98-42d6-a724-913bd761ca8c', 2, 12, '2024-08-28', '2024-0 , (30, '31910d73-d343-44b7-8a29-f7e075b64933', 2, 12, '2024-08-29', '2024-09-03', 71000, 1, 0, 2, 0, 3, 0, 0, 0, 1750, 72750, 'EUR', '', 'refunded', '2024-01-05 05:05:05', '2024-01-05 05:05:05') , (32, 'c9488490-ac09-4402-90cd-f6f0546f04c0', 2, 12, '2024-08-29', '2024-09-03', 71000, 1, 0, 2, 0, 3, 0, 0, 0, 1750, 72750, 'EUR', '', 'pending', '2024-01-05 05:05:05', '2024-01-05 05:05:05') , (34, '5819823e-c0ac-4baa-a3ae-515fbb70e909', 2, 12, '2024-08-29', '2024-09-03', 71000, 1, 0, 2, 0, 3, 0, 0, 0, 1750, 72750, 'EUR', '', 'pending', '2024-01-05 05:05:05', '2024-01-06 06:06:06') + , (36, 'f2871c2d-e11a-41e8-b264-0a8605c77dc1', 2, 12, '2024-08-29', '2024-09-03', 71000, 1, 0, 2, 0, 3, 0, 0, 0, 1750, 72750, 'EUR', '', 'pending', '2024-01-05 05:05:05', '2024-01-06 06:06:06') + , (38, '01505d14-6f4d-48a2-9a98-3a2099ab7eef', 2, 12, '2024-08-29', '2024-09-03', 71000, 1, 0, 2, 0, 3, 0, 0, 0, 1750, 72750, 'EUR', '', 'preauth', '2024-01-05 05:05:05', '2024-01-06 06:06:06') + , (40, '7cae7d1c-d626-41e0-b1c5-48359e515579', 2, 12, '2024-08-29', '2024-09-03', 71000, 1, 0, 2, 0, 3, 0, 0, 0, 1750, 72750, 'EUR', '', 'preauth', '2024-01-05 05:05:05', '2024-01-06 06:06:06') ; insert into payment_redsys_response (payment_id, response_code, date_time, secure_payment, transaction_type, amount, currency_code, order_number, authorization_code, merchant_code, terminal_number, error_code) @@ -100,10 +103,41 @@ select is( 'Should NOT change a payment twice' ); +select is( + process_payment_response('f2871c2d-e11a-41e8-b264-0a8605c77dc1', row('3322450', 5, 0, '2024-02-07 12:23:34', true, 1, '12.40', 'EUR', '00000032c948', '130', '')::redsys_response), + 'preauth', + 'Should preauth a pending payment if response code < 100' +); + +select is( + process_payment_response('f2871c2d-e11a-41e8-b264-0a8605c77dc1', row('3322450', 5, 900, '2024-02-07 12:23:34', true, 2, '12.40', 'EUR', '00000032c948', '131', '')::redsys_response), + 'completed', + 'Should confirm a preauth payment if response code = 900' +); + +select is( + process_payment_response('01505d14-6f4d-48a2-9a98-3a2099ab7eef', row('3322450', 5, 101, '2024-02-07 12:23:34', true, 2, '12.40', 'EUR', '00000032c948', '132', '')::redsys_response), + 'preauth', + 'Should leave a preauth payment as is if response code <> 900' +); + +select is( + process_payment_response('7cae7d1c-d626-41e0-b1c5-48359e515579', row('3322450', 5, 400, '2024-02-07 12:23:34', true, 9, '12.40', 'EUR', '00000032c948', '133', '')::redsys_response), + 'voided', + 'Should void a preauth payment if response code = 400' +); + +select is( + process_payment_response('01505d14-6f4d-48a2-9a98-3a2099ab7eef', row('3322450', 5, 900, '2024-02-07 12:23:34', true, 9, '12.40', 'EUR', '00000032c948', '134', '')::redsys_response), + 'preauth', + 'Should leave a preauth payment as is if response code <> 400' +); + + select throws_ok( - $$ select process_payment_response('5819823e-c0ac-4baa-a3ae-515fbb70e909', row('3322445', 2, 0, '2024-02-02 12:23:34', false, 3, '12.41', 'USD', '000000345819', '130', '')::redsys_response) $$, - '22023', '3 is not a processable transaction type', - 'Only transaction type = 0 are allowed for now' + $$ select process_payment_response('5819823e-c0ac-4baa-a3ae-515fbb70e909', row('3322445', 2, 0, '2024-02-02 12:23:34', false, 13, '12.41', 'USD', '000000345819', '135', '')::redsys_response) $$, + '22023', '13 is not a processable transaction type', + 'Only transaction types 0, 1, 2, 3, and 9 are allowed.' ); select bag_eq( @@ -115,15 +149,28 @@ select bag_eq( , (30, 'refunded', '2024-01-05 05:05:05') , (32, 'completed', current_timestamp) , (34, 'pending', '2024-01-06 06:06:06') + , (36, 'completed', current_timestamp) + , (38, 'preauth', current_timestamp) + , (40, 'voided', current_timestamp) $$, 'Should have updated payments' ); select bag_eq( $$ select payment_id, merchant_code, terminal_number, response_code, date_time::text, secure_payment, transaction_type, amount, currency_code, order_number, authorization_code, error_code from payment_redsys_response $$, - $$ values (24, '3322446', 2, 100, '2024-02-03 12:23:34', false, 0, 1236, 'USD', '000000246d1b', '125', 'ERR') - , (28, '3322448', 4, 99, '2024-02-05 12:23:34', true, 0, 1238, 'EUR', '00000028b770', '127', '') - , (32, '3322450', 6, 0, '2024-02-07 12:23:34', true, 0, 1240, 'EUR', '00000032c948', '129', 'NOPE') + $$ values (22, '3322445', 2, 0, '2024-02-02 12:23:34', true, 0, 1235, 'EUR', '000000224ef3', '124', '') + , (24, '3322446', 2, 100, '2024-02-03 12:23:34', false, 0, 1236, 'USD', '000000246d1b', '125', 'ERR') + , (26, '3322447', 3, 0, '2024-02-04 12:23:34', true, 0, 1237, 'EUR', '000000268d3a', '126', '') + , (28, '1234567', 5, 0, '2023-01-01 01:01:01', false, 1, 1000, 'EUR', 'huh?', '123', '123') + , (28, '3322448', 4, 99, '2024-02-05 12:23:34', true, 0, 1238, 'EUR', '00000028b770', '127', '') + , (30, '3322449', 5, 0, '2024-02-06 12:23:34', false, 0, 1239, 'EUR', '000000303190', '128', '') + , (32, '3322450', 6, 0, '2024-02-07 12:23:34', true, 0, 1240, 'EUR', '00000032c948', '129', 'NOPE') + , (32, '3322450', 6, 0, '2024-02-07 12:23:34', true, 0, 1240, 'EUR', '00000032c948', '129', '') + , (36, '3322450', 5, 0, '2024-02-07 12:23:34', true, 1, 1240, 'EUR', '00000032c948', '130', '') + , (36, '3322450', 5, 900, '2024-02-07 12:23:34', true, 2, 1240, 'EUR', '00000032c948', '131', '') + , (38, '3322450', 5, 101, '2024-02-07 12:23:34', true, 2, 1240, 'EUR', '00000032c948', '132', '') + , (38, '3322450', 5, 900, '2024-02-07 12:23:34', true, 9, 1240, 'EUR', '00000032c948', '134', '') + , (40, '3322450', 5, 400, '2024-02-07 12:23:34', true, 9, 1240, 'EUR', '00000032c948', '133', '') $$, 'Should have added responses' ); diff --git a/verify/available_payment_status.sql b/verify/available_payment_status.sql index 083cd9e..bc81b60 100644 --- a/verify/available_payment_status.sql +++ b/verify/available_payment_status.sql @@ -6,25 +6,34 @@ set search_path to camper; select 1 / count(*) from payment_status where payment_status = 'draft' and name = 'Draft'; select 1 / count(*) from payment_status where payment_status = 'pending' and name = 'Pending'; +select 1 / count(*) from payment_status where payment_status = 'preauth' and name = 'Authorization held'; select 1 / count(*) from payment_status where payment_status = 'failed' and name = 'Failed'; select 1 / count(*) from payment_status where payment_status = 'completed' and name = 'Completed'; select 1 / count(*) from payment_status where payment_status = 'refunded' and name = 'Refunded'; +select 1 / count(*) from payment_status where payment_status = 'voided' and name = 'Voided'; select 1 / count(*) from payment_status_i18n where payment_status = 'draft' and lang_tag = 'ca' and name = 'Esborrany'; select 1 / count(*) from payment_status_i18n where payment_status = 'pending' and lang_tag = 'ca' and name = 'Pendent'; +select 1 / count(*) from payment_status_i18n where payment_status = 'preauth' and lang_tag = 'ca' and name = 'Autorització retinguda'; select 1 / count(*) from payment_status_i18n where payment_status = 'failed' and lang_tag = 'ca' and name = 'No realitzat'; select 1 / count(*) from payment_status_i18n where payment_status = 'completed' and lang_tag = 'ca' and name = 'Completat'; select 1 / count(*) from payment_status_i18n where payment_status = 'refunded' and lang_tag = 'ca' and name = 'Reemborsat'; +select 1 / count(*) from payment_status_i18n where payment_status = 'voided' and lang_tag = 'ca' and name = 'Anul·lat'; select 1 / count(*) from payment_status_i18n where payment_status = 'draft' and lang_tag = 'es' and name = 'Borrador'; select 1 / count(*) from payment_status_i18n where payment_status = 'pending' and lang_tag = 'es' and name = 'Pendiente'; +select 1 / count(*) from payment_status_i18n where payment_status = 'preauth' and lang_tag = 'es' and name = 'Autorización retenida'; select 1 / count(*) from payment_status_i18n where payment_status = 'failed' and lang_tag = 'es' and name = 'Fallido'; select 1 / count(*) from payment_status_i18n where payment_status = 'completed' and lang_tag = 'es' and name = 'Completado'; select 1 / count(*) from payment_status_i18n where payment_status = 'refunded' and lang_tag = 'es' and name = 'Reembolsado'; +select 1 / count(*) from payment_status_i18n where payment_status = 'voided' and lang_tag = 'es' and name = 'Anulado'; select 1 / count(*) from payment_status_i18n where payment_status = 'draft' and lang_tag = 'fr' and name = 'Brouillon'; +select 1 / count(*) from payment_status_i18n where payment_status = 'pending' and lang_tag = 'fr' and name = 'En attente'; +select 1 / count(*) from payment_status_i18n where payment_status = 'preauth' and lang_tag = 'fr' and name = 'Autorisation détenue'; select 1 / count(*) from payment_status_i18n where payment_status = 'failed' and lang_tag = 'fr' and name = 'Échouée'; select 1 / count(*) from payment_status_i18n where payment_status = 'completed' and lang_tag = 'fr' and name = 'Terminée'; select 1 / count(*) from payment_status_i18n where payment_status = 'refunded' and lang_tag = 'fr' and name = 'Remboursée'; +select 1 / count(*) from payment_status_i18n where payment_status = 'voided' and lang_tag = 'fr' and name = 'Annulé'; rollback; diff --git a/verify/payment_redsys_response.sql b/verify/payment_redsys_response.sql index 399e32f..ccacf37 100644 --- a/verify/payment_redsys_response.sql +++ b/verify/payment_redsys_response.sql @@ -2,7 +2,8 @@ begin; -select payment_id +select payment_redsys_response_id + , payment_id , response_code , date_time , secure_payment diff --git a/web/static/camper.css b/web/static/camper.css index 23d0b00..2956baf 100644 --- a/web/static/camper.css +++ b/web/static/camper.css @@ -729,7 +729,7 @@ label[x-show] > span, label[x-show] > br { /**/ .booking-created .booking-status, -.payment-draft .payment-status { +.payment-pending .payment-status { background-color: var(--camper--color--light-blue); } @@ -739,17 +739,19 @@ label[x-show] > span, label[x-show] > br { } .booking-confirmed .booking-status, -.payment-pending .payment-status { +.payment-preauth .payment-status { background-color: var(--camper--color--hay); } .booking-checked-in .booking-status, -.payment-complete .payment-status { +.payment-completed .payment-status { background-color: var(--camper--color--light-green); } .booking-invoiced .booking-status, -.payment-refunded .payment-status { +.payment-refunded .payment-status, +.payment-draft .payment-status, +.payment-voided .payment-status { background-color: var(--camper--color--light-gray); }