Change draft_payment return type to row of payment

This way i can use the function in the from clause of the query that
i already had to do to get the totals formatted with to_price.  In this
case, i believe it is better to leave out Go’s function because it would
force me to perform two queries.

Instead of binding a nullable string pointer with the payment’s slug,
i wanted to use pgtype’s zeronull.Text type, but it can not work in this
case because it encodes the value as a text, while the parameters is
uuid.  I can not use zero.UUID, because it is a [16]byte array, while i
have it in a string.

Thus, had to create my own ZeroNullUUID type that use a string as a base
but encodes it as a UUID.
This commit is contained in:
jordi fita mas 2024-02-13 19:51:39 +01:00
parent 77a3f78176
commit 990a614897
7 changed files with 99 additions and 53 deletions

View File

@ -17,10 +17,10 @@ set search_path to camper, public;
create type option_units as (option_id integer, units integer); create type option_units as (option_id integer, units integer);
create or replace function draft_payment(payment_slug uuid, arrival_date date, departure_date date, campsite_type_slug uuid, num_adults integer, num_teenagers integer, num_children integer, num_dogs integer, zone_preferences text, options option_units[]) returns uuid as create or replace function draft_payment(payment_slug uuid, arrival_date date, departure_date date, campsite_type_slug uuid, num_adults integer, num_teenagers integer, num_children integer, num_dogs integer, zone_preferences text, options option_units[]) returns payment as
$$ $$
declare declare
pid integer; p payment;
begin begin
insert into payment ( insert into payment (
slug slug
@ -86,14 +86,14 @@ begin
, total = excluded.total , total = excluded.total
, zone_preferences = excluded.zone_preferences , zone_preferences = excluded.zone_preferences
, updated_at = current_timestamp , updated_at = current_timestamp
returning payment_id, payment.slug returning *
into pid, payment_slug into p
; ;
if array_length(coalesce(options, array[]::option_units[]), 1) > 0 then if array_length(coalesce(options, array[]::option_units[]), 1) > 0 then
delete delete
from payment_option from payment_option
where payment_id = pid where payment_id = p.payment_id
and campsite_type_option_id not in ( and campsite_type_option_id not in (
select campsite_type_option_id select campsite_type_option_id
from unnest(options) as option(campsite_type_option_id, units) from unnest(options) as option(campsite_type_option_id, units)
@ -105,7 +105,7 @@ begin
, units , units
, subtotal , subtotal
) )
select pid select p.payment_id
, campsite_type_option_id , campsite_type_option_id
, units , units
, case when per_night then sum(cost * units)::integer else max(cost * units)::integer end , case when per_night then sum(cost * units)::integer else max(cost * units)::integer end
@ -125,26 +125,26 @@ begin
with option as ( with option as (
select sum(subtotal)::integer as subtotal select sum(subtotal)::integer as subtotal
from payment_option from payment_option
where payment_id = pid where payment_id = p.payment_id
) )
update payment update payment
set total = subtotal_nights + subtotal_adults + subtotal_teenagers + subtotal_children + subtotal_dogs + subtotal_tourist_tax + coalesce(option.subtotal, 0) set total = subtotal_nights + subtotal_adults + subtotal_teenagers + subtotal_children + subtotal_dogs + subtotal_tourist_tax + coalesce(option.subtotal, 0)
from option from option
where payment_id = pid where payment_id = p.payment_id
; ;
else else
delete delete
from payment_option from payment_option
where payment_id = pid; where payment_id = p.payment_id;
update payment update payment
set total = subtotal_nights + subtotal_adults + subtotal_teenagers + subtotal_children + subtotal_dogs + subtotal_tourist_tax set total = subtotal_nights + subtotal_adults + subtotal_teenagers + subtotal_children + subtotal_dogs + subtotal_tourist_tax
where payment_id = pid where payment_id = p.payment_id
; ;
end if; end if;
return payment_slug; return p;
end; end;
$$ $$
language plpgsql language plpgsql

View File

@ -82,26 +82,9 @@ func newBookingCart(ctx context.Context, conn *database.Conn, f *bookingForm, ca
}) })
} }
paymentSlug, err := conn.DraftPayment(
ctx,
f.PaymentSlug.Val,
arrivalDate,
departureDate,
campsiteType,
numAdults,
numTeenagers,
numChildren,
numDogs,
zonePreferences,
optionUnits,
)
if err != nil {
return nil, err
}
f.PaymentSlug.Val = paymentSlug
row := conn.QueryRow(ctx, ` row := conn.QueryRow(ctx, `
select payment_id select payment.slug
, payment_id
, departure_date - arrival_date , departure_date - arrival_date
, to_price(subtotal_nights, decimal_digits) , to_price(subtotal_nights, decimal_digits)
, to_price(subtotal_adults, decimal_digits) , to_price(subtotal_adults, decimal_digits)
@ -110,12 +93,21 @@ func newBookingCart(ctx context.Context, conn *database.Conn, f *bookingForm, ca
, to_price(subtotal_dogs, decimal_digits) , to_price(subtotal_dogs, decimal_digits)
, to_price(subtotal_tourist_tax, decimal_digits) , to_price(subtotal_tourist_tax, decimal_digits)
, to_price(total, decimal_digits) , to_price(total, decimal_digits)
from payment from draft_payment($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) as payment
join company using (company_id) join company using (company_id)
join currency using (currency_code) join currency using (currency_code)
where payment.slug = $1 `,
`, paymentSlug) database.ZeroNullUUID(f.PaymentSlug.Val),
arrivalDate,
departureDate,
campsiteType,
numAdults,
numTeenagers,
numChildren,
numDogs,
zonePreferences,
database.OptionUnitsArray(optionUnits),
)
var paymentID int var paymentID int
var numNights int var numNights int
var nights string var nights string
@ -125,7 +117,7 @@ func newBookingCart(ctx context.Context, conn *database.Conn, f *bookingForm, ca
var dogs string var dogs string
var touristTax string var touristTax string
var total string var total string
if err = row.Scan(&paymentID, &numNights, &nights, &adults, &teenagers, &children, &dogs, &touristTax, &total); err != nil { if err = row.Scan(&f.PaymentSlug.Val, &paymentID, &numNights, &nights, &adults, &teenagers, &children, &dogs, &touristTax, &total); err != nil {
if database.ErrorIsNotFound(err) { if database.ErrorIsNotFound(err) {
return cart, nil return cart, nil
} }

View File

@ -7,8 +7,6 @@ package database
import ( import (
"context" "context"
"time"
"golang.org/x/text/language" "golang.org/x/text/language"
) )
@ -350,14 +348,6 @@ func (tx *Tx) TranslateHome(ctx context.Context, companyID int, langTag language
return err return err
} }
func (c *Conn) DraftPayment(ctx context.Context, paymentSlug string, arrivalDate time.Time, departureDate time.Time, campsiteTypeSlug string, numAdults int, numTeenagers int, numChildren int, numDogs int, zonePreferences string, options OptionUnitsArray) (string, error) {
var paymentSlugParam *string
if paymentSlug != "" {
paymentSlugParam = &paymentSlug
}
return c.GetText(ctx, "select draft_payment($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", paymentSlugParam, arrivalDate, departureDate, campsiteTypeSlug, numAdults, numTeenagers, numChildren, numDogs, zonePreferences, options)
}
func (c *Conn) ReadyPayment(ctx context.Context, paymentSlug string, customerName string, customerAddress string, customerPostCode string, customerCity string, customerCountryCode string, customerEmail string, customerPhone string, customerLangTag language.Tag, acsiCard bool) (int, error) { func (c *Conn) ReadyPayment(ctx context.Context, paymentSlug string, customerName string, customerAddress string, customerPostCode string, customerCity string, customerCountryCode string, customerEmail string, customerPhone string, customerLangTag language.Tag, acsiCard bool) (int, error) {
return c.GetInt(ctx, "select ready_payment($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", paymentSlug, customerName, customerAddress, customerPostCode, customerCity, customerCountryCode, customerEmail, customerPhone, customerLangTag, acsiCard) return c.GetInt(ctx, "select ready_payment($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", paymentSlug, customerName, customerAddress, customerPostCode, customerCity, customerCountryCode, customerEmail, customerPhone, customerLangTag, acsiCard)
} }

25
pkg/database/zeronull.go Normal file
View File

@ -0,0 +1,25 @@
package database
import (
"github.com/jackc/pgtype"
"dev.tandem.ws/tandem/camper/pkg/uuid"
)
type ZeroNullUUID string
func (src ZeroNullUUID) EncodeBinary(ci *pgtype.ConnInfo, buf []byte) ([]byte, error) {
if src == "" {
return nil, nil
}
bytes, err := uuid.Parse(string(src))
if err != nil {
return nil, err
}
nullable := pgtype.UUID{
Bytes: bytes,
Status: pgtype.Present,
}
return nullable.EncodeBinary(ci, buf)
}

View File

@ -5,7 +5,12 @@
package uuid package uuid
import "dev.tandem.ws/tandem/camper/pkg/hex" import (
gohex "encoding/hex"
"fmt"
"dev.tandem.ws/tandem/camper/pkg/hex"
)
func Valid(s string) bool { func Valid(s string) bool {
if len(s) != 36 { if len(s) != 36 {
@ -27,3 +32,23 @@ func Valid(s string) bool {
} }
return true return true
} }
func Parse(src string) (dst [16]byte, err error) {
switch len(src) {
case 36:
src = src[0:8] + src[9:13] + src[14:18] + src[19:23] + src[24:]
case 32:
// dashes already stripped, assume valid
default:
// assume invalid.
return dst, fmt.Errorf("cannot parse UUID %v", src)
}
buf, err := gohex.DecodeString(src)
if err != nil {
return dst, err
}
copy(dst[:], buf)
return dst, err
}

View File

@ -13,15 +13,16 @@ import (
type test struct { type test struct {
in string in string
isUuid bool isUuid bool
bytes [16]byte
} }
var tests = []test{ var tests = []test{
{"f47ac10b-58cc-0372-8567-0e02b2c3d479", true}, {"f47ac10b-58cc-0372-8567-0e02b2c3d479", true, [16]byte{0xf4, 0x7a, 0xc1, 0x0b, 0x58, 0xcc, 0x03, 0x72, 0x85, 0x67, 0x0e, 0x02, 0xb2, 0xc3, 0xd4, 0x79}},
{"2bc1be74-169d-4300-a239-49a1196a045d", true}, {"2bc1be74-169d-4300-a239-49a1196a045d", true, [16]byte{0x2b, 0xc1, 0xbe, 0x74, 0x16, 0x9d, 0x43, 0x00, 0xa2, 0x39, 0x49, 0xa1, 0x19, 0x6a, 0x04, 0x5d}},
{"12bc1be74-169d-4300-a239-49a1196a045d", false}, {"12bc1be74-169d-4300-a239-49a1196a045d", false, [16]byte{}},
{"2bc1be74-169d-4300-a239-49a1196a045", false}, {"2bc1be74-169d-4300-a239-49a1196a045", false, [16]byte{}},
{"2bc1be74-1x9d-4300-a239-49a1196a045d", false}, {"2bc1be74-1x9d-4300-a239-49a1196a045d", false, [16]byte{}},
{"2bc1be74-169d-4300-a239-49a1196ag45d", false}, {"2bc1be74-169d-4300-a239-49a1196ag45d", false, [16]byte{}},
} }
func testValid(t *testing.T, in string, isUuid bool) { func testValid(t *testing.T, in string, isUuid bool) {
@ -30,9 +31,22 @@ func testValid(t *testing.T, in string, isUuid bool) {
} }
} }
func testParse(t *testing.T, in string, expected [16]byte, isUuid bool) {
actual, err := Parse(in)
if isUuid && err != nil {
t.Errorf("Parse(%s) unexpected error %v", in, err)
} else if !isUuid && err == nil {
t.Errorf("Parse(%s) expected to fail but got %v", in, actual)
} else if actual != expected {
t.Errorf("Parse(%s) got %v expected %v", in, actual, expected)
}
}
func TestUUID(t *testing.T) { func TestUUID(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
testValid(t, tt.in, tt.isUuid) testValid(t, tt.in, tt.isUuid)
testValid(t, strings.ToUpper(tt.in), tt.isUuid) testValid(t, strings.ToUpper(tt.in), tt.isUuid)
testParse(t, tt.in, tt.bytes, tt.isUuid)
testParse(t, strings.ToUpper(tt.in), tt.bytes, tt.isUuid)
} }
} }

View File

@ -11,7 +11,7 @@ set search_path to camper, public;
select has_function('camper', 'draft_payment', array['uuid', 'date', 'date', 'uuid', 'integer', 'integer', 'integer', 'integer', 'text', 'option_units[]']); select has_function('camper', 'draft_payment', array['uuid', 'date', 'date', 'uuid', 'integer', 'integer', 'integer', 'integer', 'text', 'option_units[]']);
select function_lang_is('camper', 'draft_payment', array['uuid', 'date', 'date', 'uuid', 'integer', 'integer', 'integer', 'integer', 'text', 'option_units[]'], 'plpgsql'); select function_lang_is('camper', 'draft_payment', array['uuid', 'date', 'date', 'uuid', 'integer', 'integer', 'integer', 'integer', 'text', 'option_units[]'], 'plpgsql');
select function_returns('camper', 'draft_payment', array['uuid', 'date', 'date', 'uuid', 'integer', 'integer', 'integer', 'integer', 'text', 'option_units[]'], 'uuid'); select function_returns('camper', 'draft_payment', array['uuid', 'date', 'date', 'uuid', 'integer', 'integer', 'integer', 'integer', 'text', 'option_units[]'], 'payment');
select isnt_definer('camper', 'draft_payment', array['uuid', 'date', 'date', 'uuid', 'integer', 'integer', 'integer', 'integer', 'text', 'option_units[]']); select isnt_definer('camper', 'draft_payment', array['uuid', 'date', 'date', 'uuid', 'integer', 'integer', 'integer', 'integer', 'text', 'option_units[]']);
select volatility_is('camper', 'draft_payment', array['uuid', 'date', 'date', 'uuid', 'integer', 'integer', 'integer', 'integer', 'text', 'option_units[]'], 'volatile'); select volatility_is('camper', 'draft_payment', array['uuid', 'date', 'date', 'uuid', 'integer', 'integer', 'integer', 'integer', 'text', 'option_units[]'], 'volatile');
select function_privs_are('camper', 'draft_payment', array ['uuid', 'date', 'date', 'uuid', 'integer', 'integer', 'integer', 'integer', 'text', 'option_units[]'], 'guest', array['EXECUTE']); select function_privs_are('camper', 'draft_payment', array ['uuid', 'date', 'date', 'uuid', 'integer', 'integer', 'integer', 'integer', 'text', 'option_units[]'], 'guest', array['EXECUTE']);