From 990a614897981c62df9368c9511de12e8fa67ec0 Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Tue, 13 Feb 2024 19:51:39 +0100 Subject: [PATCH] Change draft_payment return type to row of payment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- deploy/draft_payment.sql | 22 +++++++++++----------- pkg/booking/cart.go | 40 ++++++++++++++++------------------------ pkg/database/funcs.go | 10 ---------- pkg/database/zeronull.go | 25 +++++++++++++++++++++++++ pkg/uuid/uuid.go | 27 ++++++++++++++++++++++++++- pkg/uuid/uuid_test.go | 26 ++++++++++++++++++++------ test/draft_payment.sql | 2 +- 7 files changed, 99 insertions(+), 53 deletions(-) create mode 100644 pkg/database/zeronull.go diff --git a/deploy/draft_payment.sql b/deploy/draft_payment.sql index 594d9e0..887f10a 100644 --- a/deploy/draft_payment.sql +++ b/deploy/draft_payment.sql @@ -17,10 +17,10 @@ set search_path to camper, public; 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 - pid integer; + p payment; begin insert into payment ( slug @@ -86,14 +86,14 @@ begin , total = excluded.total , zone_preferences = excluded.zone_preferences , updated_at = current_timestamp - returning payment_id, payment.slug - into pid, payment_slug + returning * + into p ; if array_length(coalesce(options, array[]::option_units[]), 1) > 0 then delete from payment_option - where payment_id = pid + where payment_id = p.payment_id and campsite_type_option_id not in ( select campsite_type_option_id from unnest(options) as option(campsite_type_option_id, units) @@ -105,7 +105,7 @@ begin , units , subtotal ) - select pid + select p.payment_id , campsite_type_option_id , units , case when per_night then sum(cost * units)::integer else max(cost * units)::integer end @@ -125,26 +125,26 @@ begin with option as ( select sum(subtotal)::integer as subtotal from payment_option - where payment_id = pid + where payment_id = p.payment_id ) update payment set total = subtotal_nights + subtotal_adults + subtotal_teenagers + subtotal_children + subtotal_dogs + subtotal_tourist_tax + coalesce(option.subtotal, 0) from option - where payment_id = pid + where payment_id = p.payment_id ; else delete from payment_option - where payment_id = pid; + where payment_id = p.payment_id; update payment 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; - return payment_slug; + return p; end; $$ language plpgsql diff --git a/pkg/booking/cart.go b/pkg/booking/cart.go index 7fac263..e09411f 100644 --- a/pkg/booking/cart.go +++ b/pkg/booking/cart.go @@ -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, ` - select payment_id + select payment.slug + , payment_id , departure_date - arrival_date , to_price(subtotal_nights, 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_tourist_tax, 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 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 numNights int var nights string @@ -125,7 +117,7 @@ func newBookingCart(ctx context.Context, conn *database.Conn, f *bookingForm, ca var dogs string var touristTax 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) { return cart, nil } diff --git a/pkg/database/funcs.go b/pkg/database/funcs.go index 3859915..a157095 100644 --- a/pkg/database/funcs.go +++ b/pkg/database/funcs.go @@ -7,8 +7,6 @@ package database import ( "context" - "time" - "golang.org/x/text/language" ) @@ -350,14 +348,6 @@ func (tx *Tx) TranslateHome(ctx context.Context, companyID int, langTag language 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) { 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) } diff --git a/pkg/database/zeronull.go b/pkg/database/zeronull.go new file mode 100644 index 0000000..560a0e9 --- /dev/null +++ b/pkg/database/zeronull.go @@ -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) +} diff --git a/pkg/uuid/uuid.go b/pkg/uuid/uuid.go index 84dfd9e..969765d 100644 --- a/pkg/uuid/uuid.go +++ b/pkg/uuid/uuid.go @@ -5,7 +5,12 @@ 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 { if len(s) != 36 { @@ -27,3 +32,23 @@ func Valid(s string) bool { } 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 +} diff --git a/pkg/uuid/uuid_test.go b/pkg/uuid/uuid_test.go index d57174f..98249b6 100644 --- a/pkg/uuid/uuid_test.go +++ b/pkg/uuid/uuid_test.go @@ -13,15 +13,16 @@ import ( type test struct { in string isUuid bool + bytes [16]byte } var tests = []test{ - {"f47ac10b-58cc-0372-8567-0e02b2c3d479", true}, - {"2bc1be74-169d-4300-a239-49a1196a045d", true}, - {"12bc1be74-169d-4300-a239-49a1196a045d", false}, - {"2bc1be74-169d-4300-a239-49a1196a045", false}, - {"2bc1be74-1x9d-4300-a239-49a1196a045d", false}, - {"2bc1be74-169d-4300-a239-49a1196ag45d", false}, + {"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, [16]byte{0x2b, 0xc1, 0xbe, 0x74, 0x16, 0x9d, 0x43, 0x00, 0xa2, 0x39, 0x49, 0xa1, 0x19, 0x6a, 0x04, 0x5d}}, + {"12bc1be74-169d-4300-a239-49a1196a045d", false, [16]byte{}}, + {"2bc1be74-169d-4300-a239-49a1196a045", false, [16]byte{}}, + {"2bc1be74-1x9d-4300-a239-49a1196a045d", false, [16]byte{}}, + {"2bc1be74-169d-4300-a239-49a1196ag45d", false, [16]byte{}}, } 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) { for _, tt := range tests { testValid(t, 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) } } diff --git a/test/draft_payment.sql b/test/draft_payment.sql index 0baa028..49c65b2 100644 --- a/test/draft_payment.sql +++ b/test/draft_payment.sql @@ -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 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 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']);