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.
This commit is contained in:
parent
15dde3f491
commit
ff6750fbea
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -13,9 +13,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
RedsysRequestTypeName = "redsys_request"
|
|
||||||
RedsysSignedRequestTypeName = "redsys_signed_request"
|
|
||||||
OptionUnitsTypeName = "option_units"
|
OptionUnitsTypeName = "option_units"
|
||||||
|
RedsysRequestTypeName = "redsys_request"
|
||||||
|
RedsysResponseTypeName = "redsys_response"
|
||||||
|
RedsysSignedRequestTypeName = "redsys_signed_request"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -69,6 +70,30 @@ func registerConnectionTypes(ctx context.Context, conn *pgx.Conn) error {
|
||||||
return err
|
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(
|
redsysSignedRequestType, err := pgtype.NewCompositeType(
|
||||||
RedsysSignedRequestTypeName,
|
RedsysSignedRequestTypeName,
|
||||||
[]pgtype.CompositeTypeField{
|
[]pgtype.CompositeTypeField{
|
||||||
|
|
|
@ -14,6 +14,14 @@ import (
|
||||||
"dev.tandem.ws/tandem/camper/pkg/uuid"
|
"dev.tandem.ws/tandem/camper/pkg/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
StatusDraft = "draft"
|
||||||
|
StatusPending = "pending"
|
||||||
|
StatusFailed = "failed"
|
||||||
|
StatusCompleted = "completed"
|
||||||
|
StatusRefunded = "refunded"
|
||||||
|
)
|
||||||
|
|
||||||
type PublicHandler struct {
|
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)
|
handleSuccessfulPayment(w, r, user, company, conn, payment)
|
||||||
case "failure":
|
case "failure":
|
||||||
handleFailedPayment(w, r, user, company, conn, payment)
|
handleFailedPayment(w, r, user, company, conn, payment)
|
||||||
|
case "notification":
|
||||||
|
handleNotification(w, r, user, company, conn, payment)
|
||||||
default:
|
default:
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
}
|
}
|
||||||
|
@ -90,7 +100,7 @@ func (payment *Payment) createRequest(r *http.Request, user *auth.User, company
|
||||||
schema := httplib.Protocol(r)
|
schema := httplib.Protocol(r)
|
||||||
authority := httplib.Host(r)
|
authority := httplib.Host(r)
|
||||||
baseURL := fmt.Sprintf("%s://%s/%s/payments/%s", schema, authority, user.Locale.Language, payment.Slug)
|
baseURL := fmt.Sprintf("%s://%s/%s/payments/%s", schema, authority, user.Locale.Language, payment.Slug)
|
||||||
request := &redsys.Request{
|
request := redsys.Request{
|
||||||
TransactionType: redsys.TransactionTypeCharge,
|
TransactionType: redsys.TransactionTypeCharge,
|
||||||
Amount: payment.Total,
|
Amount: payment.Total,
|
||||||
OrderNumber: payment.OrderNumber(),
|
OrderNumber: payment.OrderNumber(),
|
||||||
|
@ -100,7 +110,7 @@ func (payment *Payment) createRequest(r *http.Request, user *auth.User, company
|
||||||
NotificationURL: fmt.Sprintf("%s/notification", baseURL),
|
NotificationURL: fmt.Sprintf("%s/notification", baseURL),
|
||||||
ConsumerLanguage: user.Locale.Language,
|
ConsumerLanguage: user.Locale.Language,
|
||||||
}
|
}
|
||||||
return redsys.SignRequest(r.Context(), conn, company, request)
|
return request.Sign(r.Context(), conn, company)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (payment *Payment) OrderNumber() string {
|
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)
|
p.Setup(r, user, company, conn)
|
||||||
template.MustRenderPublicFiles(w, r, user, company, p, "payment/failure.gohtml", "payment/details.gohtml")
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -8,15 +8,16 @@ package redsys
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/jackc/pgtype"
|
"github.com/jackc/pgtype"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
"time"
|
||||||
|
|
||||||
"dev.tandem.ws/tandem/camper/pkg/auth"
|
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||||
"dev.tandem.ws/tandem/camper/pkg/database"
|
"dev.tandem.ws/tandem/camper/pkg/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TransactionType int
|
type TransactionType int
|
||||||
|
type ResponseCode int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TransactionTypeCharge TransactionType = 0
|
TransactionTypeCharge TransactionType = 0
|
||||||
|
@ -26,6 +27,16 @@ const (
|
||||||
TransactionTypePreauthVoid TransactionType = 9
|
TransactionTypePreauthVoid TransactionType = 9
|
||||||
TransactionTypeSplitAuth TransactionType = 7
|
TransactionTypeSplitAuth TransactionType = 7
|
||||||
TransactionTypeSplitConfirm TransactionType = 8
|
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 {
|
type Request struct {
|
||||||
|
@ -40,13 +51,7 @@ type Request struct {
|
||||||
ConsumerLanguage language.Tag
|
ConsumerLanguage language.Tag
|
||||||
}
|
}
|
||||||
|
|
||||||
type SignedRequest struct {
|
func (req Request) Sign(ctx context.Context, conn *database.Conn, company *auth.Company) (*SignedRequest, error) {
|
||||||
MerchantParameters string
|
|
||||||
Signature string
|
|
||||||
SignatureVersion string
|
|
||||||
}
|
|
||||||
|
|
||||||
func SignRequest(ctx context.Context, conn *database.Conn, company *auth.Company, req *Request) (*SignedRequest, error) {
|
|
||||||
row := conn.QueryRow(ctx, "select redsys_sign_request($1, $2)", company.ID, req)
|
row := conn.QueryRow(ctx, "select redsys_sign_request($1, $2)", company.ID, req)
|
||||||
signed := &SignedRequest{}
|
signed := &SignedRequest{}
|
||||||
if err := row.Scan(&signed); err != nil {
|
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
|
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
|
typeName := database.RedsysRequestTypeName
|
||||||
dt, ok := ci.DataTypeForName(typeName)
|
dt, ok := ci.DataTypeForName(typeName)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("unable to find oid for type name %v", typeName)
|
return nil, fmt.Errorf("unable to find oid for type name %v", typeName)
|
||||||
}
|
}
|
||||||
values := []interface{}{
|
values := []interface{}{
|
||||||
src.TransactionType,
|
req.TransactionType,
|
||||||
src.Amount,
|
req.Amount,
|
||||||
src.OrderNumber,
|
req.OrderNumber,
|
||||||
src.Product,
|
req.Product,
|
||||||
src.CardHolder,
|
req.CardHolder,
|
||||||
src.SuccessURL,
|
req.SuccessURL,
|
||||||
src.FailureURL,
|
req.FailureURL,
|
||||||
src.NotificationURL,
|
req.NotificationURL,
|
||||||
src.ConsumerLanguage,
|
req.ConsumerLanguage,
|
||||||
}
|
}
|
||||||
ct := pgtype.NewValue(dt.Value).(*pgtype.CompositeType)
|
ct := pgtype.NewValue(dt.Value).(*pgtype.CompositeType)
|
||||||
if err := ct.Set(values); err != nil {
|
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)
|
return ct.EncodeText(ci, dst)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SignedRequest struct {
|
||||||
|
MerchantParameters string
|
||||||
|
Signature string
|
||||||
|
SignatureVersion string
|
||||||
|
}
|
||||||
|
|
||||||
func (dst *SignedRequest) DecodeText(ci *pgtype.ConnInfo, src []byte) error {
|
func (dst *SignedRequest) DecodeText(ci *pgtype.ConnInfo, src []byte) error {
|
||||||
typeName := database.RedsysSignedRequestTypeName
|
typeName := database.RedsysSignedRequestTypeName
|
||||||
dt, ok := ci.DataTypeForName(typeName)
|
dt, ok := ci.DataTypeForName(typeName)
|
||||||
|
@ -91,3 +102,62 @@ func (dst *SignedRequest) DecodeText(ci *pgtype.ConnInfo, src []byte) error {
|
||||||
}
|
}
|
||||||
return ct.AssignTo(dst)
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
-- Revert camper:decode_base64url from pg
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
drop function if exists camper.decode_base64url(text);
|
||||||
|
|
||||||
|
commit;
|
|
@ -0,0 +1,7 @@
|
||||||
|
-- Revert camper:payment_redsys_response from pg
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
drop table if exists camper.payment_redsys_response;
|
||||||
|
|
||||||
|
commit;
|
|
@ -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;
|
|
@ -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;
|
|
@ -0,0 +1,7 @@
|
||||||
|
-- Revert camper:redsys_response from pg
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
drop type if exists camper.redsys_response;
|
||||||
|
|
||||||
|
commit;
|
|
@ -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 <jordi@tandem.blog> # Add relation of payment for campsite type options
|
payment_option [roles schema_camper payment campsite_type_option] 2024-02-12T00:58:07Z jordi fita mas <jordi@tandem.blog> # 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 <jordi@tandem.blog> # Add function to create a payment draft
|
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 <jordi@tandem.blog> # 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 <jordi@tandem.blog> # Add function to ready a draft payment
|
ready_payment [roles schema_camper payment payment_customer country_code email extension_pg_libphonenumber] 2024-02-12T12:57:24Z jordi fita mas <jordi@tandem.blog> # Add function to ready a draft payment
|
||||||
|
redsys_response [schema_camper currency_code] 2024-02-12T19:49:29Z jordi fita mas <jordi@tandem.blog> # Add type for Redsys responses
|
||||||
|
decode_base64url [roles schema_camper] 2024-02-12T20:03:17Z jordi fita mas <jordi@tandem.blog> # 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 <jordi@tandem.blog> # 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 <jordi@tandem.blog> # 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 <jordi@tandem.blog> # Add function to process Redsys response of a payment
|
||||||
|
|
|
@ -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;
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -0,0 +1,7 @@
|
||||||
|
-- Verify camper:decode_base64url on pg
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
select has_function_privilege('camper.decode_base64url(text)', 'execute');
|
||||||
|
|
||||||
|
rollback;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -0,0 +1,7 @@
|
||||||
|
-- Verify camper:redsys_response on pg
|
||||||
|
|
||||||
|
begin;
|
||||||
|
|
||||||
|
select pg_catalog.has_type_privilege('camper.redsys_response', 'usage');
|
||||||
|
|
||||||
|
rollback;
|
Loading…
Reference in New Issue