From ff6750fbeabe1dfb0f7c51ddbafc8660e486a8f2 Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Tue, 13 Feb 2024 02:38:38 +0100 Subject: [PATCH] Handle payment notifications from Redsys I have to basically do the reverse of signing the request to verify that the notification comes from them. Lots of code just for that. I return the changed status from the PL/pgSQL function because i will need to email customers when a payment is completed, and i need to know when. --- deploy/decode_base64url.sql | 22 +++++ deploy/payment_redsys_response.sql | 30 +++++++ deploy/process_payment_response.sql | 89 ++++++++++++++++++ deploy/redsys_decode_response.sql | 50 +++++++++++ deploy/redsys_response.sql | 23 +++++ pkg/database/types.go | 29 +++++- pkg/payment/public.go | 61 ++++++++++++- pkg/redsys/client.go | 106 ++++++++++++++++++---- revert/decode_base64url.sql | 7 ++ revert/payment_redsys_response.sql | 7 ++ revert/process_payment_response.sql | 7 ++ revert/redsys_decode_response.sql | 7 ++ revert/redsys_response.sql | 7 ++ sqitch.plan | 5 ++ test/decode_base64url.sql | 42 +++++++++ test/payment_redsys_response.sql | 89 ++++++++++++++++++ test/process_payment_response.sql | 135 ++++++++++++++++++++++++++++ test/redsys_decode_response.sql | 56 ++++++++++++ test/redsys_response.sql | 29 ++++++ verify/decode_base64url.sql | 7 ++ verify/payment_redsys_response.sql | 20 +++++ verify/process_payment_response.sql | 7 ++ verify/redsys_decode_response.sql | 7 ++ verify/redsys_response.sql | 7 ++ 24 files changed, 827 insertions(+), 22 deletions(-) create mode 100644 deploy/decode_base64url.sql create mode 100644 deploy/payment_redsys_response.sql create mode 100644 deploy/process_payment_response.sql create mode 100644 deploy/redsys_decode_response.sql create mode 100644 deploy/redsys_response.sql create mode 100644 revert/decode_base64url.sql create mode 100644 revert/payment_redsys_response.sql create mode 100644 revert/process_payment_response.sql create mode 100644 revert/redsys_decode_response.sql create mode 100644 revert/redsys_response.sql create mode 100644 test/decode_base64url.sql create mode 100644 test/payment_redsys_response.sql create mode 100644 test/process_payment_response.sql create mode 100644 test/redsys_decode_response.sql create mode 100644 test/redsys_response.sql create mode 100644 verify/decode_base64url.sql create mode 100644 verify/payment_redsys_response.sql create mode 100644 verify/process_payment_response.sql create mode 100644 verify/redsys_decode_response.sql create mode 100644 verify/redsys_response.sql diff --git a/deploy/decode_base64url.sql b/deploy/decode_base64url.sql new file mode 100644 index 0000000..1fe6b86 --- /dev/null +++ b/deploy/decode_base64url.sql @@ -0,0 +1,22 @@ +-- Deploy camper:decode_base64url to pg +-- requires: roles +-- requires: schema_camper + +begin; + +set search_path to camper, public; + +create or replace function decode_base64url(encoded text) returns bytea as +$$ + select decode(translate(encoded, '-_', '+/'), 'base64'); +$$ + language sql + immutable +; + +revoke execute on function decode_base64url(text) from public; +grant execute on function decode_base64url(text) to guest; +grant execute on function decode_base64url(text) to employee; +grant execute on function decode_base64url(text) to admin; + +commit; diff --git a/deploy/payment_redsys_response.sql b/deploy/payment_redsys_response.sql new file mode 100644 index 0000000..a66eef6 --- /dev/null +++ b/deploy/payment_redsys_response.sql @@ -0,0 +1,30 @@ +-- Deploy camper:payment_redsys_response to pg +-- requires: roles +-- requires: schema_camper +-- requires: payment +-- requires: currency_code + +begin; + +set search_path to camper, public; + +create table payment_redsys_response ( + payment_id integer primary key references payment, + response_code integer not null, + date_time timestamp without time zone not null, + secure_payment boolean not null, + transaction_type integer not null, + amount integer not null, + currency_code currency_code not null references currency, + order_number text not null, + authorization_code text not null, + merchant_code text not null, + terminal_number integer not null, + error_code text not null +); + +grant select, insert, update on table payment_redsys_response to guest; +grant select, insert, update on table payment_redsys_response to employee; +grant select, insert, update, delete on table payment_redsys_response to admin; + +commit; diff --git a/deploy/process_payment_response.sql b/deploy/process_payment_response.sql new file mode 100644 index 0000000..9d1c47c --- /dev/null +++ b/deploy/process_payment_response.sql @@ -0,0 +1,89 @@ +-- Deploy camper:process_payment_response to pg +-- requires: roles +-- requires: schema_camper +-- requires: redsys_response +-- requires: payment +-- requires: payment_redsys_response +-- requires: parse_price +-- requires: currency + +begin; + +set search_path to camper, public; + +create or replace function process_payment_response(payment_slug uuid, response redsys_response) returns text as +$$ +declare + pid integer; + next_status text; +begin + if response.transaction_type <> 0 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; + + if pid is null then + return ''; + end if; + + 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 + ) + select pid + , response.response_code + , response.date_time + , response.secure_payment + , response.transaction_type + , parse_price(response.amount, decimal_digits) + , response.currency_code + , response.order_number + , response.authorization_code + , response.merchant_code + , response.terminal_number + , 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; +end; +$$ + language plpgsql +; + +revoke execute on function process_payment_response(uuid, redsys_response) from public; +grant execute on function process_payment_response(uuid, redsys_response) to guest; +grant execute on function process_payment_response(uuid, redsys_response) to employee; +grant execute on function process_payment_response(uuid, redsys_response) to admin; + +commit; diff --git a/deploy/redsys_decode_response.sql b/deploy/redsys_decode_response.sql new file mode 100644 index 0000000..65cf6ad --- /dev/null +++ b/deploy/redsys_decode_response.sql @@ -0,0 +1,50 @@ +-- Deploy camper:redsys_decode_response to pg +-- requires: roles +-- requires: schema_camper +-- requires: extension_pgcrypto +-- requires: decode_base64url +-- requires: redsys_encrypt +-- requires: redsys_response +-- requires: company +-- requires: currency +-- requires: to_price + +begin; + +set search_path to camper, public; + +create or replace function redsys_decode_response(company_id integer, encoded_data text, encoded_signature text, signature_version text) returns redsys_response as +$$ +select row ( + obj ->> 'Ds_MerchantCode' + , to_number((obj ->> 'Ds_Terminal'), '999')::integer + , to_number((obj ->> 'Ds_Response'), '9999')::integer + , to_timestamp((obj ->> 'Ds_Date') || ' ' || (obj ->> 'Ds_Hour'), 'DD/MM/YYYY HH24:MI') + , obj ->> 'Ds_SecurePayment' = '1' + , (obj ->> 'Ds_TransactionType')::integer + , to_price((obj ->> 'Ds_Amount')::integer, decimal_digits) + , currency_code + , obj ->> 'Ds_Order' + , coalesce(obj ->> 'Ds_AuthorisationCode', '') + , coalesce(obj ->> 'Ds_ErrorCode', '') + )::redsys_response + from ( + select convert_to(encoded_data, 'UTF-8') as raw + , convert_from(decode_base64url(encoded_data), 'UTF-8')::jsonb as obj + , decode_base64url(encoded_signature) as signature + , signature_version + ) as response + join currency on currency.redsys_code = (obj ->> 'Ds_Currency')::integer + where signature = hmac(raw, redsys_encrypt(company_id, convert_to(obj ->> 'Ds_Order', 'UTF-8')), 'sha256') + and signature_version = 'HMAC_SHA256_V1'; +$$ + language sql + stable +; + +revoke execute on function redsys_decode_response(integer, text, text, text) from public; +grant execute on function redsys_decode_response(integer, text, text, text) to guest; +grant execute on function redsys_decode_response(integer, text, text, text) to employee; +grant execute on function redsys_decode_response(integer, text, text, text) to admin; + +commit; diff --git a/deploy/redsys_response.sql b/deploy/redsys_response.sql new file mode 100644 index 0000000..fe17048 --- /dev/null +++ b/deploy/redsys_response.sql @@ -0,0 +1,23 @@ +-- Deploy camper:redsys_response to pg +-- requires: schema_camper +-- requires: currency_code + +begin; + +set search_path to camper, public; + +create type redsys_response as ( + merchant_code text +, terminal_number integer +, response_code integer +, date_time timestamp +, secure_payment boolean +, transaction_type integer +, amount text +, currency_code currency_code +, order_number text +, authorization_code text +, error_code text +); + +commit; diff --git a/pkg/database/types.go b/pkg/database/types.go index fa71745..23a9274 100644 --- a/pkg/database/types.go +++ b/pkg/database/types.go @@ -13,9 +13,10 @@ import ( ) const ( - RedsysRequestTypeName = "redsys_request" - RedsysSignedRequestTypeName = "redsys_signed_request" OptionUnitsTypeName = "option_units" + RedsysRequestTypeName = "redsys_request" + RedsysResponseTypeName = "redsys_response" + RedsysSignedRequestTypeName = "redsys_signed_request" ) var ( @@ -69,6 +70,30 @@ func registerConnectionTypes(ctx context.Context, conn *pgx.Conn) error { return err } + redsysResponseType, err := pgtype.NewCompositeType( + RedsysResponseTypeName, + []pgtype.CompositeTypeField{ + {"merchant_code", pgtype.TextOID}, + {"terminal_number", pgtype.Int4OID}, + {"response_code", pgtype.Int4OID}, + {"date_time", pgtype.TimestampOID}, + {"secure_payment", pgtype.BoolOID}, + {"transaction_type", pgtype.Int4OID}, + {"amount", pgtype.TextOID}, + {"currency_code", pgtype.TextOID}, + {"order_number", pgtype.TextOID}, + {"authorization_code", pgtype.TextOID}, + {"error_code", pgtype.TextOID}, + }, + conn.ConnInfo(), + ) + if err != nil { + return err + } + if _, err = registerType(ctx, conn, redsysResponseType, redsysResponseType.TypeName()); err != nil { + return err + } + redsysSignedRequestType, err := pgtype.NewCompositeType( RedsysSignedRequestTypeName, []pgtype.CompositeTypeField{ diff --git a/pkg/payment/public.go b/pkg/payment/public.go index ae38931..71b2114 100644 --- a/pkg/payment/public.go +++ b/pkg/payment/public.go @@ -14,6 +14,14 @@ import ( "dev.tandem.ws/tandem/camper/pkg/uuid" ) +const ( + StatusDraft = "draft" + StatusPending = "pending" + StatusFailed = "failed" + StatusCompleted = "completed" + StatusRefunded = "refunded" +) + type PublicHandler struct { } @@ -54,6 +62,8 @@ func (h *PublicHandler) Handler(user *auth.User, company *auth.Company, conn *da handleSuccessfulPayment(w, r, user, company, conn, payment) case "failure": handleFailedPayment(w, r, user, company, conn, payment) + case "notification": + handleNotification(w, r, user, company, conn, payment) default: http.NotFound(w, r) } @@ -90,7 +100,7 @@ func (payment *Payment) createRequest(r *http.Request, user *auth.User, company schema := httplib.Protocol(r) authority := httplib.Host(r) baseURL := fmt.Sprintf("%s://%s/%s/payments/%s", schema, authority, user.Locale.Language, payment.Slug) - request := &redsys.Request{ + request := redsys.Request{ TransactionType: redsys.TransactionTypeCharge, Amount: payment.Total, OrderNumber: payment.OrderNumber(), @@ -100,7 +110,7 @@ func (payment *Payment) createRequest(r *http.Request, user *auth.User, company NotificationURL: fmt.Sprintf("%s/notification", baseURL), ConsumerLanguage: user.Locale.Language, } - return redsys.SignRequest(r.Context(), conn, company, request) + return request.Sign(r.Context(), conn, company) } func (payment *Payment) OrderNumber() string { @@ -202,3 +212,50 @@ func (p *failedPaymentPage) MustRender(w http.ResponseWriter, r *http.Request, u p.Setup(r, user, company, conn) template.MustRenderPublicFiles(w, r, user, company, p, "payment/failure.gohtml", "payment/details.gohtml") } + +func handleNotification(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, payment *Payment) { + var head string + head, r.URL.Path = httplib.ShiftPath(r.URL.Path) + + switch head { + case "": + switch r.Method { + case http.MethodPost: + if err := r.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + signed := redsys.SignedResponse{ + MerchantParameters: r.Form.Get("Ds_MerchantParameters"), + Signature: r.Form.Get("Ds_Signature"), + SignatureVersion: r.Form.Get("Ds_SignatureVersion"), + } + response, err := signed.Decode(r.Context(), conn, company) + if err != nil { + panic(err) + } + if response == nil { + http.Error(w, "Invalid response", http.StatusBadRequest) + return + } + if response.OrderNumber != payment.OrderNumber() { + http.Error(w, "Response for a different payment", http.StatusBadRequest) + return + } + status, err := response.Process(r.Context(), conn, payment.Slug) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + switch status { + case StatusCompleted: + default: + } + w.WriteHeader(http.StatusNoContent) + default: + httplib.MethodNotAllowed(w, r, http.MethodPost) + } + default: + http.NotFound(w, r) + } +} diff --git a/pkg/redsys/client.go b/pkg/redsys/client.go index ac86fd4..d981ce0 100644 --- a/pkg/redsys/client.go +++ b/pkg/redsys/client.go @@ -8,15 +8,16 @@ package redsys import ( "context" "fmt" - "github.com/jackc/pgtype" "golang.org/x/text/language" + "time" "dev.tandem.ws/tandem/camper/pkg/auth" "dev.tandem.ws/tandem/camper/pkg/database" ) type TransactionType int +type ResponseCode int const ( TransactionTypeCharge TransactionType = 0 @@ -26,6 +27,16 @@ const ( TransactionTypePreauthVoid TransactionType = 9 TransactionTypeSplitAuth TransactionType = 7 TransactionTypeSplitConfirm TransactionType = 8 + + ResponsePaymentAuthorized ResponseCode = 99 + ResponseRefundAuthorized ResponseCode = 900 + ResponseCancelAuthorized ResponseCode = 400 + ResponseCardExpired ResponseCode = 101 + ResponseCardException ResponseCode = 102 + ResponseTooManyPINAttempts ResponseCode = 106 + ResponseCardNotEffective ResponseCode = 125 + ResponseIncorrectSecureCode ResponseCode = 129 + ResponseDenied ResponseCode = 172 ) type Request struct { @@ -40,13 +51,7 @@ type Request struct { ConsumerLanguage language.Tag } -type SignedRequest struct { - MerchantParameters string - Signature string - SignatureVersion string -} - -func SignRequest(ctx context.Context, conn *database.Conn, company *auth.Company, req *Request) (*SignedRequest, error) { +func (req Request) Sign(ctx context.Context, conn *database.Conn, company *auth.Company) (*SignedRequest, error) { row := conn.QueryRow(ctx, "select redsys_sign_request($1, $2)", company.ID, req) signed := &SignedRequest{} if err := row.Scan(&signed); err != nil { @@ -55,22 +60,22 @@ func SignRequest(ctx context.Context, conn *database.Conn, company *auth.Company return signed, nil } -func (src Request) EncodeText(ci *pgtype.ConnInfo, dst []byte) ([]byte, error) { +func (req Request) EncodeText(ci *pgtype.ConnInfo, dst []byte) ([]byte, error) { typeName := database.RedsysRequestTypeName dt, ok := ci.DataTypeForName(typeName) if !ok { return nil, fmt.Errorf("unable to find oid for type name %v", typeName) } values := []interface{}{ - src.TransactionType, - src.Amount, - src.OrderNumber, - src.Product, - src.CardHolder, - src.SuccessURL, - src.FailureURL, - src.NotificationURL, - src.ConsumerLanguage, + req.TransactionType, + req.Amount, + req.OrderNumber, + req.Product, + req.CardHolder, + req.SuccessURL, + req.FailureURL, + req.NotificationURL, + req.ConsumerLanguage, } ct := pgtype.NewValue(dt.Value).(*pgtype.CompositeType) if err := ct.Set(values); err != nil { @@ -79,6 +84,12 @@ func (src Request) EncodeText(ci *pgtype.ConnInfo, dst []byte) ([]byte, error) { return ct.EncodeText(ci, dst) } +type SignedRequest struct { + MerchantParameters string + Signature string + SignatureVersion string +} + func (dst *SignedRequest) DecodeText(ci *pgtype.ConnInfo, src []byte) error { typeName := database.RedsysSignedRequestTypeName dt, ok := ci.DataTypeForName(typeName) @@ -91,3 +102,62 @@ func (dst *SignedRequest) DecodeText(ci *pgtype.ConnInfo, src []byte) error { } return ct.AssignTo(dst) } + +type SignedResponse struct { + MerchantParameters string + Signature string + SignatureVersion string +} + +func (signed SignedResponse) Decode(ctx context.Context, conn *database.Conn, company *auth.Company) (*Response, error) { + row := conn.QueryRow(ctx, "select redsys_decode_response($1, $2, $3, $4)", company.ID, signed.MerchantParameters, signed.Signature, signed.SignatureVersion) + response := &Response{} + if err := row.Scan(&response); err != nil { + return nil, err + } + return response, nil +} + +type Response struct { + MerchantCode string + TerminalNumber int + ResponseCode ResponseCode + DateTime time.Time + SecurePayment bool + TransactionType TransactionType + Amount string + CurrencyCode string + OrderNumber string + AuthorizationCode string + ErrorCode string +} + +func (response *Response) Process(ctx context.Context, conn *database.Conn, paymentSlug string) (string, error) { + return conn.GetText(ctx, "select process_payment_response($1, $2)", paymentSlug, response) +} + +func (response *Response) EncodeBinary(ci *pgtype.ConnInfo, dst []byte) ([]byte, error) { + typeName := database.RedsysResponseTypeName + dt, ok := ci.DataTypeForName(typeName) + if !ok { + return nil, fmt.Errorf("unable to find oid for type name %v", typeName) + } + values := []interface{}{ + response.MerchantCode, + response.TerminalNumber, + response.ResponseCode, + response.DateTime, + response.SecurePayment, + response.TransactionType, + response.Amount, + response.CurrencyCode, + response.OrderNumber, + response.AuthorizationCode, + response.ErrorCode, + } + ct := pgtype.NewValue(dt.Value).(*pgtype.CompositeType) + if err := ct.Set(values); err != nil { + return nil, err + } + return ct.EncodeBinary(ci, dst) +} diff --git a/revert/decode_base64url.sql b/revert/decode_base64url.sql new file mode 100644 index 0000000..480127a --- /dev/null +++ b/revert/decode_base64url.sql @@ -0,0 +1,7 @@ +-- Revert camper:decode_base64url from pg + +begin; + +drop function if exists camper.decode_base64url(text); + +commit; diff --git a/revert/payment_redsys_response.sql b/revert/payment_redsys_response.sql new file mode 100644 index 0000000..022dbb6 --- /dev/null +++ b/revert/payment_redsys_response.sql @@ -0,0 +1,7 @@ +-- Revert camper:payment_redsys_response from pg + +begin; + +drop table if exists camper.payment_redsys_response; + +commit; diff --git a/revert/process_payment_response.sql b/revert/process_payment_response.sql new file mode 100644 index 0000000..63ee60a --- /dev/null +++ b/revert/process_payment_response.sql @@ -0,0 +1,7 @@ +-- Revert camper:process_payment_response from pg + +begin; + +drop function if exists camper.process_payment_response(uuid, camper.redsys_response); + +commit; diff --git a/revert/redsys_decode_response.sql b/revert/redsys_decode_response.sql new file mode 100644 index 0000000..48c61f3 --- /dev/null +++ b/revert/redsys_decode_response.sql @@ -0,0 +1,7 @@ +-- Revert camper:redsys_decode_response from pg + +begin; + +drop function if exists camper.redsys_decode_response(integer, text, text, text); + +commit; diff --git a/revert/redsys_response.sql b/revert/redsys_response.sql new file mode 100644 index 0000000..3191d3c --- /dev/null +++ b/revert/redsys_response.sql @@ -0,0 +1,7 @@ +-- Revert camper:redsys_response from pg + +begin; + +drop type if exists camper.redsys_response; + +commit; diff --git a/sqitch.plan b/sqitch.plan index ffd20f1..e8f77a5 100644 --- a/sqitch.plan +++ b/sqitch.plan @@ -248,3 +248,8 @@ payment_customer [roles schema_camper payment country country_code extension_pg_ payment_option [roles schema_camper payment campsite_type_option] 2024-02-12T00:58:07Z jordi fita mas # Add relation of payment for campsite type options draft_payment [roles schema_camper season_calendar season campsite_type campsite_type_pet_cost campsite_type_cost campsite_type_option_cost campsite_type_option payment payment_option] 2024-02-12T01:31:52Z jordi fita mas # Add function to create a payment draft ready_payment [roles schema_camper payment payment_customer country_code email extension_pg_libphonenumber] 2024-02-12T12:57:24Z jordi fita mas # Add function to ready a draft payment +redsys_response [schema_camper currency_code] 2024-02-12T19:49:29Z jordi fita mas # Add type for Redsys responses +decode_base64url [roles schema_camper] 2024-02-12T20:03:17Z jordi fita mas # Add function to decode the so-called base64url to bytea +redsys_decode_response [roles schema_camper extension_pgcrypto decode_base64url redsys_encrypt redsys_response company currency to_price] 2024-02-12T20:52:09Z jordi fita mas # Add function to decode a Redsys signed response +payment_redsys_response [roles schema_camper payment currency_code] 2024-02-12T21:32:23Z jordi fita mas # Add relation for Redsys responses to payments +process_payment_response [roles schema_camper redsys_response payment payment_redsys_response parse_price currency] 2024-02-12T22:04:48Z jordi fita mas # Add function to process Redsys response of a payment diff --git a/test/decode_base64url.sql b/test/decode_base64url.sql new file mode 100644 index 0000000..ef47669 --- /dev/null +++ b/test/decode_base64url.sql @@ -0,0 +1,42 @@ +-- Test decode_base64url +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(13); + +set search_path to camper, public; + +select has_function('camper', 'decode_base64url', array['text']); +select function_lang_is('camper', 'decode_base64url', array['text'], 'sql'); +select function_returns('camper', 'decode_base64url', array['text'], 'bytea'); +select isnt_definer('camper', 'decode_base64url', array['text']); +select volatility_is('camper', 'decode_base64url', array['text'], 'immutable'); +select function_privs_are('camper', 'decode_base64url', array ['text'], 'guest', array['EXECUTE']); +select function_privs_are('camper', 'decode_base64url', array ['text'], 'employee', array['EXECUTE']); +select function_privs_are('camper', 'decode_base64url', array ['text'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'decode_base64url', array ['text'], 'authenticator', array[]::text[]); + +select is( + decode_base64url('sq7HjrUOBfKmC576ILgskD5srU870gJ7'), + decode('sq7HjrUOBfKmC576ILgskD5srU870gJ7', 'base64') +); +select is( + decode_base64url('K-gWWTl-gKlkxEGDsuc0UunIQwoFLM0t'), + decode('K+gWWTl+gKlkxEGDsuc0UunIQwoFLM0t', 'base64') +); +select is( + decode_base64url('cQGdqnb4V_nSKe8zTzyP_VaDGd2rAmzt'), + decode('cQGdqnb4V/nSKe8zTzyP/VaDGd2rAmzt', 'base64') +); +select is( + decode_base64url('x59e-kbXSpgsmy31V5l2BtTuKv8_pVKg'), + decode('x59e+kbXSpgsmy31V5l2BtTuKv8/pVKg', 'base64') +); + +select * +from finish(); + +rollback; diff --git a/test/payment_redsys_response.sql b/test/payment_redsys_response.sql new file mode 100644 index 0000000..1bd4d3c --- /dev/null +++ b/test/payment_redsys_response.sql @@ -0,0 +1,89 @@ +-- Test payment_redsys_response +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(59); + +set search_path to camper, public; + +select has_table('payment_redsys_response'); +select has_pk('payment_redsys_response'); +select table_privs_are('payment_redsys_response', 'guest', array['SELECT', 'INSERT', 'UPDATE']); +select table_privs_are('payment_redsys_response', 'employee', array['SELECT', 'INSERT', 'UPDATE']); +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_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'); +select col_not_null('payment_redsys_response', 'payment_id'); +select col_hasnt_default('payment_redsys_response', 'payment_id'); + +select has_column('payment_redsys_response', 'response_code'); +select col_type_is('payment_redsys_response', 'response_code', 'integer'); +select col_not_null('payment_redsys_response', 'response_code'); +select col_hasnt_default('payment_redsys_response', 'response_code'); + +select has_column('payment_redsys_response', 'date_time'); +select col_type_is('payment_redsys_response', 'date_time', 'timestamp without time zone'); +select col_not_null('payment_redsys_response', 'date_time'); +select col_hasnt_default('payment_redsys_response', 'date_time'); + +select has_column('payment_redsys_response', 'secure_payment'); +select col_type_is('payment_redsys_response', 'secure_payment', 'boolean'); +select col_not_null('payment_redsys_response', 'secure_payment'); +select col_hasnt_default('payment_redsys_response', 'secure_payment'); + +select has_column('payment_redsys_response', 'transaction_type'); +select col_type_is('payment_redsys_response', 'transaction_type', 'integer'); +select col_not_null('payment_redsys_response', 'transaction_type'); +select col_hasnt_default('payment_redsys_response', 'transaction_type'); + +select has_column('payment_redsys_response', 'amount'); +select col_type_is('payment_redsys_response', 'amount', 'integer'); +select col_not_null('payment_redsys_response', 'amount'); +select col_hasnt_default('payment_redsys_response', 'amount'); + +select has_column('payment_redsys_response', 'currency_code'); +select col_is_fk('payment_redsys_response', 'currency_code'); +select fk_ok('payment_redsys_response', 'currency_code', 'currency', 'currency_code'); +select col_type_is('payment_redsys_response', 'currency_code', 'currency_code'); +select col_not_null('payment_redsys_response', 'currency_code'); +select col_hasnt_default('payment_redsys_response', 'currency_code'); + +select has_column('payment_redsys_response', 'order_number'); +select col_type_is('payment_redsys_response', 'order_number', 'text'); +select col_not_null('payment_redsys_response', 'order_number'); +select col_hasnt_default('payment_redsys_response', 'order_number'); + +select has_column('payment_redsys_response', 'authorization_code'); +select col_type_is('payment_redsys_response', 'authorization_code', 'text'); +select col_not_null('payment_redsys_response', 'authorization_code'); +select col_hasnt_default('payment_redsys_response', 'authorization_code'); + +select has_column('payment_redsys_response', 'merchant_code'); +select col_type_is('payment_redsys_response', 'merchant_code', 'text'); +select col_not_null('payment_redsys_response', 'merchant_code'); +select col_hasnt_default('payment_redsys_response', 'merchant_code'); + +select has_column('payment_redsys_response', 'terminal_number'); +select col_type_is('payment_redsys_response', 'terminal_number', 'integer'); +select col_not_null('payment_redsys_response', 'terminal_number'); +select col_hasnt_default('payment_redsys_response', 'terminal_number'); + +select has_column('payment_redsys_response', 'error_code'); +select col_type_is('payment_redsys_response', 'error_code', 'text'); +select col_not_null('payment_redsys_response', 'error_code'); +select col_hasnt_default('payment_redsys_response', 'error_code'); + + +select * +from finish(); + +rollback; + diff --git a/test/process_payment_response.sql b/test/process_payment_response.sql new file mode 100644 index 0000000..4000a64 --- /dev/null +++ b/test/process_payment_response.sql @@ -0,0 +1,135 @@ +-- Test process_payment_response +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(19); + +set search_path to camper, public; + +select has_function('camper', 'process_payment_response', array['uuid', 'redsys_response']); +select function_lang_is('camper', 'process_payment_response', array['uuid', 'redsys_response'], 'plpgsql'); +select function_returns('camper', 'process_payment_response', array['uuid', 'redsys_response'], 'text'); +select isnt_definer('camper', 'process_payment_response', array['uuid', 'redsys_response']); +select volatility_is('camper', 'process_payment_response', array['uuid', 'redsys_response'], 'volatile'); +select function_privs_are('camper', 'process_payment_response', array ['uuid', 'redsys_response'], 'guest', array['EXECUTE']); +select function_privs_are('camper', 'process_payment_response', array ['uuid', 'redsys_response'], 'employee', array['EXECUTE']); +select function_privs_are('camper', 'process_payment_response', array ['uuid', 'redsys_response'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'process_payment_response', array ['uuid', 'redsys_response'], 'authenticator', array[]::text[]); + +set client_min_messages to warning; +truncate payment_redsys_response cascade; +truncate payment cascade; +truncate campsite_type cascade; +truncate media cascade; +truncate media_content cascade; +truncate company cascade; +reset client_min_messages; + +insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, rtc_number, tourist_tax, country_code, currency_code, default_lang_tag) +values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 350, 'ES', 'EUR', 'ca') +; + +insert into media_content (media_type, bytes) +values ('image/x-xpixmap', 'static char *s[]={"1 1 1 1","a c #ffffff","a"};') +; + +insert into media (media_id, company_id, original_filename, content_hash) +values (10, 2, 'cover2.xpm', sha256('static char *s[]={"1 1 1 1","a c #ffffff","a"};')) +; + +insert into campsite_type (campsite_type_id, slug, company_id, name, media_id, max_campers, bookable_nights, overflow_allowed) +values (12, 'c1b6f4fc-32c1-4cd5-b796-0c5059152a52', 2, 'Plots', 10, 6, '[1, 7]', true) +; + +insert into payment (payment_id, slug, company_id, campsite_type_id, arrival_date, departure_date, subtotal_nights, number_adults, subtotal_adults, number_teenagers, subtotal_teenagers, number_children, subtotal_children, number_dogs, subtotal_dogs, subtotal_tourist_tax, total, zone_preferences, payment_status, created_at, updated_at) +values (22, '4ef35e2f-ef98-42d6-a724-913bd761ca8c', 2, 12, '2024-08-28', '2024-09-04', 3200, 2, 10420, 4, 20840, 6, 25080, 3, 2450, 4900, 79160, 'pref I before E', 'draft', '2024-01-01 01:01:01', '2024-01-01 01:01:01') + , (24, '6d1b8e4c-c3c6-4fe4-92c1-2cbf94526693', 2, 12, '2024-08-29', '2024-09-03', 71000, 1, 0, 2, 0, 3, 0, 0, 0, 1750, 72750, '', 'pending', '2024-01-02 02:02:02', '2024-01-02 02:02:02') + , (26, '8d38a482-8a25-4d85-9929-e5f425fcac04', 2, 12, '2024-08-29', '2024-09-03', 71000, 1, 0, 2, 0, 3, 0, 0, 0, 1750, 72750, '', 'completed', '2024-01-03 03:03:03', '2024-01-03 03:03:03') + , (28, 'b770f8b7-f148-4ab4-a786-aa070af598e5', 2, 12, '2024-08-29', '2024-09-03', 71000, 1, 0, 2, 0, 3, 0, 0, 0, 1750, 72750, '', 'failed', '2024-01-04 04:04:04', '2024-01-04 04:04:04') + , (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, '', '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, '', '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, '', 'pending', '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) +values (28, 0, '2023-01-01 01:01:01', false, 1, 1000, 'EUR', 'huh?', '123', '1234567', 5, '123') +; + +select is( + process_payment_response('4ef35e2f-ef98-42d6-a724-913bd761ca8c', row('3322445', 2, 0, '2024-02-02 12:23:34', true, 0, '12.35', 'EUR', '000000224ef3', '124', '')::redsys_response), + '', + 'Should not change a draft payment' +); + +select is( + process_payment_response('6d1b8e4c-c3c6-4fe4-92c1-2cbf94526693', row('3322446', 2, 100, '2024-02-03 12:23:34', false, 0, '12.36', 'USD', '000000246d1b', '125', 'ERR')::redsys_response), + 'failed', + 'Should fail a pending payment if response code > 99' +); + +select is( + process_payment_response('8d38a482-8a25-4d85-9929-e5f425fcac04', row('3322447', 3, 0, '2024-02-04 12:23:34', true, 0, '12.37', 'EUR', '000000268d3a', '126', '')::redsys_response), + '', + 'Should not change a completed payment' +); + +select is( + process_payment_response('b770f8b7-f148-4ab4-a786-aa070af598e5', row('3322448', 4, 99, '2024-02-05 12:23:34', true, 0, '12.38', 'EUR', '00000028b770', '127', '')::redsys_response), + 'completed', + 'Should change a failed payment if response code < 100' +); + +select is( + process_payment_response('31910d73-d343-44b7-8a29-f7e075b64933', row('3322449', 5, 0, '2024-02-06 12:23:34', false, 0, '12.39', 'EUR', '000000303190', '128', '')::redsys_response), + '', + 'Should not change a refunded payment' +); + +select is( + process_payment_response('c9488490-ac09-4402-90cd-f6f0546f04c0', row('3322450', 6, 0, '2024-02-07 12:23:34', true, 0, '12.40', 'EUR', '00000032c948', '129', 'NOPE')::redsys_response), + 'completed', + 'Should change a pending payment if response code < 100' +); + +select is( + process_payment_response('c9488490-ac09-4402-90cd-f6f0546f04c0', row('3322450', 6, 0, '2024-02-07 12:23:34', true, 0, '12.40', 'EUR', '00000032c948', '129', '')::redsys_response), + '', + 'Should NOT change a payment twice' +); + +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 bag_eq( + $$ select payment_id, payment_status, updated_at from payment $$, + $$ values (22, 'draft', '2024-01-01 01:01:01') + , (24, 'failed', current_timestamp) + , (26, 'completed', '2024-01-03 03:03:03') + , (28, 'completed', current_timestamp) + , (30, 'refunded', '2024-01-05 05:05:05') + , (32, 'completed', current_timestamp) + , (34, 'pending', '2024-01-06 06:06:06') + $$, + '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') + $$, + 'Should have added responses' +); + + +select * +from finish(); + +rollback; diff --git a/test/redsys_decode_response.sql b/test/redsys_decode_response.sql new file mode 100644 index 0000000..e40a96f --- /dev/null +++ b/test/redsys_decode_response.sql @@ -0,0 +1,56 @@ +-- Test redsys_decode_response +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(12); + +set search_path to camper, public; + +select has_function('camper', 'redsys_decode_response', array['integer', 'text', 'text', 'text']); +select function_lang_is('camper', 'redsys_decode_response', array['integer', 'text', 'text', 'text'], 'sql'); +select function_returns('camper', 'redsys_decode_response', array['integer', 'text', 'text', 'text'], 'redsys_response'); +select isnt_definer('camper', 'redsys_decode_response', array['integer', 'text', 'text', 'text']); +select volatility_is('camper', 'redsys_decode_response', array['integer', 'text', 'text', 'text'], 'stable'); +select function_privs_are('camper', 'redsys_decode_response', array ['integer', 'text', 'text', 'text'], 'guest', array['EXECUTE']); +select function_privs_are('camper', 'redsys_decode_response', array ['integer', 'text', 'text', 'text'], 'employee', array['EXECUTE']); +select function_privs_are('camper', 'redsys_decode_response', array ['integer', 'text', 'text', 'text'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'redsys_decode_response', array ['integer', 'text', 'text', 'text'], 'authenticator', array[]::text[]); + + +set client_min_messages to warning; +truncate redsys cascade; +truncate company cascade; +reset client_min_messages; + +insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, rtc_number, tourist_tax, country_code, currency_code, default_lang_tag) +values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 60, 'ES', 'EUR', 'ca') + , (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', '', 60, 'FR', 'USD', 'ca') +; + +insert into redsys (company_id, merchant_code, terminal_number, environment, integration, encrypt_key) +values (2, '222222222', 2, 'live', 'insite', decode('x8RS+UkDkAN3i5hGU92U2c5BhJlffg8w', 'base64')) + , (4, '999008881', 4, 'test', 'insite', decode('sq7HjrUOBfKmC576ILgskD5srU870gJ7', 'base64')) +; + +select is( + redsys_decode_response(4, 'eyJEc19NZXJjaGFudENvZGUiOiIzNjE3MTY5NjIiLCJEc19UZXJtaW5hbCI6IjAwMSIsIkRzX09yZGVyIjoiNjk5ZmQ2YWRjYTE4IiwiRHNfQW1vdW50IjoiMTAwMCIsIkRzX0N1cnJlbmN5IjoiOTc4IiwiRHNfRGF0ZSI6IjEyXC8wMlwvMjAyNCIsIkRzX0hvdXIiOiIxODowMCIsIkRzX1NlY3VyZVBheW1lbnQiOiIxIiwiRHNfQ2FyZF9Db3VudHJ5IjoiNzI0IiwiRHNfUmVzcG9uc2UiOiIwMDAwIiwiRHNfTWVyY2hhbnREYXRhIjoiIiwiRHNfVHJhbnNhY3Rpb25UeXBlIjoiMCIsIkRzX0NvbnN1bWVyTGFuZ3VhZ2UiOiIyIiwiRHNfQXV0aG9yaXNhdGlvbkNvZGUiOiIyOTM3MzYiLCJEc19DYXJkX0JyYW5kIjoiMSIsIkRzX1Byb2Nlc3NlZFBheU1ldGhvZCI6IjgwIn0=', 'uOR3HLlSMH3zrHINAoDv1-MK9tSj5Vo4Og9oAh6Uj08=', 'HMAC_SHA256_V1'), + row('361716962', 1, 0, '2024-02-12 18:00:00.000000 +00:00'::timestamp, true, 0, '10.00', 'EUR', '699fd6adca18', '293736', '')::redsys_response +); + +select is( + redsys_decode_response(4, 'eyJEc19NZXJjaGFudF9NZXJjaGFudENvZGUiIDogIjk5OTAwODg4MSIsICJEc19NZXJjaGFudF9UZXJtaW5hbCIgOiAiNCIsICJEc19NZXJjaGFudF9NZXJjaGFudE5hbWUiIDogIkNvbXBhbnkgNCIsICJEc19NZXJjaGFudF9UcmFuc2FjdGlvblR5cGUiIDogIjEiLCAiRHNfTWVyY2hhbnRfQW1vdW50IiA6ICIyMTQ0MDAiLCAiRHNfTWVyY2hhbnRfQ3VycmVuY3kiIDogIjg0MCIsICJEc19NZXJjaGFudF9PcmRlciIgOiAiMDAwMEFCQ0RFIiwgIkRzX01lcmNoYW50X1Byb2R1Y3RkZXNjcmlwdGlvbiIgOiAiQm9va2luZyBBQkNERSIsICJEc19NZXJjaGFudF9UaXR1bGFyIiA6ICJDdXN0b21lciBGdWxsIE5hbWUiLCAiRHNfTWVyY2hhbnRfVXJsT0siIDogImh0dHA6Ly9leGFtcGxlLmNhdC9zdWNjZXNzIiwgIkRzX01lcmNoYW50X1VybEtPIiA6ICJodHRwOi8vZXhhbXBsZS5jYXQvZmFpbHVyZSIsICJEc19NZXJjaGFudF9NZXJjaGFudFVSTCIgOiAiaHR0cDovL2V4YW1wbGUuY2F0L25vdGlmeSIsICJEc19NZXJjaGFudF9Db25zdW1lckxhbmd1YWdlIiA6ICIyIn0=', 'uOR3HLlSMH3zrHINAoDv1-MK9tSj5Vo4Og9oAh6Uj08=', 'HMAC_SHA256_V1'), + null::redsys_response +); + +select is( + redsys_decode_response(2, 'eyJEc19NZXJjaGFudENvZGUiOiIzNjE3MTY5NjIiLCJEc19UZXJtaW5hbCI6IjAwMSIsIkRzX09yZGVyIjoiNjk5ZmQ2YWRjYTE4IiwiRHNfQW1vdW50IjoiMTAwMCIsIkRzX0N1cnJlbmN5IjoiOTc4IiwiRHNfRGF0ZSI6IjEyXC8wMlwvMjAyNCIsIkRzX0hvdXIiOiIxODowMCIsIkRzX1NlY3VyZVBheW1lbnQiOiIxIiwiRHNfQ2FyZF9Db3VudHJ5IjoiNzI0IiwiRHNfUmVzcG9uc2UiOiIwMDAwIiwiRHNfTWVyY2hhbnREYXRhIjoiIiwiRHNfVHJhbnNhY3Rpb25UeXBlIjoiMCIsIkRzX0NvbnN1bWVyTGFuZ3VhZ2UiOiIyIiwiRHNfQXV0aG9yaXNhdGlvbkNvZGUiOiIyOTM3MzYiLCJEc19DYXJkX0JyYW5kIjoiMSIsIkRzX1Byb2Nlc3NlZFBheU1ldGhvZCI6IjgwIn0=', 'uOR3HLlSMH3zrHINAoDv1-MK9tSj5Vo4Og9oAh6Uj08=', 'HMAC_SHA256_V1'), + null::redsys_response +); + +select * +from finish(); + +rollback; diff --git a/test/redsys_response.sql b/test/redsys_response.sql new file mode 100644 index 0000000..70bd7c5 --- /dev/null +++ b/test/redsys_response.sql @@ -0,0 +1,29 @@ +-- Test redsys_response +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(13); + +set search_path to camper, public; + +select has_composite('camper', 'redsys_response', 'Composite type camper.redsys_response should exist'); +select columns_are('camper', 'redsys_response', array['merchant_code', 'terminal_number', 'response_code', 'date_time', 'secure_payment', 'transaction_type', 'amount', 'currency_code', 'order_number', 'authorization_code', 'error_code']); +select col_type_is('camper'::name, 'redsys_response'::name, 'merchant_code'::name, 'text'); +select col_type_is('camper'::name, 'redsys_response'::name, 'terminal_number'::name, 'integer'); +select col_type_is('camper'::name, 'redsys_response'::name, 'response_code'::name, 'integer'); +select col_type_is('camper'::name, 'redsys_response'::name, 'date_time'::name, 'timestamp without time zone'); +select col_type_is('camper'::name, 'redsys_response'::name, 'secure_payment'::name, 'boolean'); +select col_type_is('camper'::name, 'redsys_response'::name, 'transaction_type'::name, 'integer'); +select col_type_is('camper'::name, 'redsys_response'::name, 'amount'::name, 'text'); +select col_type_is('camper'::name, 'redsys_response'::name, 'currency_code'::name, 'currency_code'); +select col_type_is('camper'::name, 'redsys_response'::name, 'order_number'::name, 'text'); +select col_type_is('camper'::name, 'redsys_response'::name, 'authorization_code'::name, 'text'); +select col_type_is('camper'::name, 'redsys_response'::name, 'error_code'::name, 'text'); + +select * +from finish(); + +rollback; diff --git a/verify/decode_base64url.sql b/verify/decode_base64url.sql new file mode 100644 index 0000000..189e861 --- /dev/null +++ b/verify/decode_base64url.sql @@ -0,0 +1,7 @@ +-- Verify camper:decode_base64url on pg + +begin; + +select has_function_privilege('camper.decode_base64url(text)', 'execute'); + +rollback; diff --git a/verify/payment_redsys_response.sql b/verify/payment_redsys_response.sql new file mode 100644 index 0000000..399e32f --- /dev/null +++ b/verify/payment_redsys_response.sql @@ -0,0 +1,20 @@ +-- Verify camper:payment_redsys_response on pg + +begin; + +select payment_id + , response_code + , date_time + , secure_payment + , transaction_type + , amount + , currency_code + , order_number + , authorization_code + , merchant_code + , terminal_number + , error_code +from camper.payment_redsys_response +where false; + +rollback; diff --git a/verify/process_payment_response.sql b/verify/process_payment_response.sql new file mode 100644 index 0000000..21aeb34 --- /dev/null +++ b/verify/process_payment_response.sql @@ -0,0 +1,7 @@ +-- Verify camper:process_payment_response on pg + +begin; + +select has_function_privilege('camper.process_payment_response(uuid, camper.redsys_response)', 'execute'); + +rollback; diff --git a/verify/redsys_decode_response.sql b/verify/redsys_decode_response.sql new file mode 100644 index 0000000..602a952 --- /dev/null +++ b/verify/redsys_decode_response.sql @@ -0,0 +1,7 @@ +-- Verify camper:redsys_decode_response on pg + +begin; + +select has_function_privilege('camper.redsys_decode_response(integer, text, text, text)', 'execute'); + +rollback; diff --git a/verify/redsys_response.sql b/verify/redsys_response.sql new file mode 100644 index 0000000..0b13fce --- /dev/null +++ b/verify/redsys_response.sql @@ -0,0 +1,7 @@ +-- Verify camper:redsys_response on pg + +begin; + +select pg_catalog.has_type_privilege('camper.redsys_response', 'usage'); + +rollback;