camper/pkg/booking/cart.go

188 lines
5.7 KiB
Go
Raw Normal View History

package booking
import (
"context"
"strconv"
"time"
"github.com/jackc/pgx/v4"
"dev.tandem.ws/tandem/camper/pkg/database"
"dev.tandem.ws/tandem/camper/pkg/locale"
)
type bookingCart struct {
Lines []*cartLine
Total string
Enabled bool
}
type cartLine struct {
Concept string
Units int
Subtotal string
}
func newBookingCart(ctx context.Context, conn *database.Conn, f *bookingForm, campsiteType string) (*bookingCart, error) {
cart := &bookingCart{
Total: "0.0",
}
if f.Dates == nil {
return cart, nil
}
arrivalDate, err := time.Parse(database.ISODateFormat, f.Dates.ArrivalDate.Val)
if err != nil {
return cart, nil
}
departureDate, err := time.Parse(database.ISODateFormat, f.Dates.DepartureDate.Val)
if err != nil {
return cart, nil
}
if f.Guests == nil {
return cart, nil
}
numAdults, err := strconv.Atoi(f.Guests.NumberAdults.Val)
if err != nil {
return cart, nil
}
numTeenagers, err := strconv.Atoi(f.Guests.NumberTeenagers.Val)
if err != nil {
return cart, nil
}
numChildren, err := strconv.Atoi(f.Guests.NumberChildren.Val)
if err != nil {
return cart, nil
}
numDogs := 0
if f.Guests.NumberDogs != nil {
numDogs, err = strconv.Atoi(f.Guests.NumberDogs.Val)
if err != nil {
return cart, nil
}
}
optionMap := make(map[int]*campsiteTypeOption)
var typeOptions []*campsiteTypeOption
if f.Options != nil {
typeOptions = f.Options.Options
}
optionIDs := make([]int, 0, len(typeOptions))
optionUnits := make([]int, 0, len(typeOptions))
for _, option := range typeOptions {
units, _ := strconv.Atoi(option.Input.Val)
if units < 1 {
continue
}
optionMap[option.ID] = option
optionIDs = append(optionIDs, option.ID)
optionUnits = append(optionUnits, units)
}
row := conn.QueryRow(ctx, `
with per_person as (
select count(*) as num_nights
, sum(cost.cost_per_night * ceiling(($4::numeric + $5::numeric + $6::numeric) / max_campers::numeric)::integer)::integer as nights
, sum(cost_per_adult * $4)::integer as adults
, sum(cost_per_teenager * $5)::integer as teenagers
, sum(cost_per_child * $6)::integer as children
, sum(case when $7 > 0 then coalesce(pet.cost_per_night, 0) else 0 end)::integer as dogs
, sum(tourist_tax * $4)::integer as tourist_tax
, max(decimal_digits) as decimal_digits
from generate_series($1, $2, interval '1 day') as date(day)
left join season_calendar on season_range @> date.day::date
left join season using (season_id)
left join campsite_type using (company_id)
left join campsite_type_pet_cost as pet using (campsite_type_id)
left join campsite_type_cost as cost using (campsite_type_id, season_id)
left join company using (company_id)
left join currency using (currency_code)
where campsite_type.slug = $3
), per_option as (
select campsite_type_option_id
, sum(cost_per_night * units)::integer as option_cost
from generate_series($1, $2, interval '1 day') as date(day)
join season_calendar on season_range @> date.day::date
join campsite_type_option_cost using (season_id)
join unnest($8::integer[], $9::integer[]) as option_units(campsite_type_option_id, units) using (campsite_type_option_id)
group by campsite_type_option_id
union all select -1, 0
)
select num_nights
, coalesce(to_price(nights, decimal_digits), '')
, coalesce(to_price(adults, decimal_digits), '')
, coalesce(to_price(teenagers, decimal_digits), '')
, coalesce(to_price(children, decimal_digits), '')
, coalesce(to_price(dogs, decimal_digits), '')
, coalesce(to_price(tourist_tax, decimal_digits), '')
, coalesce(to_price(nights + adults + teenagers + children + tourist_tax + sum(option_cost)::integer, decimal_digits), '')
, array_agg((campsite_type_option_id, to_price(option_cost, decimal_digits)))
from per_person, per_option
group by num_nights
, nights
, adults
, teenagers
, children
, dogs
, tourist_tax
, decimal_digits
`, pgx.QueryResultFormats{pgx.BinaryFormatCode}, arrivalDate, departureDate.AddDate(0, 0, -1), campsiteType, numAdults, numTeenagers, numChildren, numDogs, optionIDs, optionUnits)
var numNights int
var nights string
var adults string
var teenagers string
var children string
var dogs string
var touristTax string
var total string
var optionPrices database.RecordArray
if err = row.Scan(&numNights, &nights, &adults, &teenagers, &children, &dogs, &touristTax, &total, &optionPrices); err != nil {
if database.ErrorIsNotFound(err) {
return cart, nil
}
return nil, err
}
maybeAddLine := func(units int, subtotal string, concept string) {
if units > 0 && subtotal != "" {
cart.Lines = append(cart.Lines, &cartLine{
Concept: concept,
Units: units,
Subtotal: subtotal,
})
}
}
maybeAddLine(numNights, nights, locale.PgettextNoop("Night", "cart"))
maybeAddLine(numAdults, adults, locale.PgettextNoop("Adult", "cart"))
maybeAddLine(numTeenagers, teenagers, locale.PgettextNoop("Teenager", "cart"))
maybeAddLine(numChildren, children, locale.PgettextNoop("Child", "cart"))
maybeAddLine(numDogs, dogs, locale.PgettextNoop("Dog", "cart"))
for _, el := range optionPrices.Elements {
optionID := el.Fields[0].Get()
if optionID == nil {
continue
}
subtotal := el.Fields[1].Get()
if subtotal == nil {
continue
}
option := optionMap[int(optionID.(int32))]
if option == nil {
continue
}
units, _ := strconv.Atoi(option.Input.Val)
maybeAddLine(units, subtotal.(string), option.Label)
}
maybeAddLine(numAdults, touristTax, locale.PgettextNoop("Tourist tax", "cart"))
if total != "" {
cart.Total = total
cart.Enabled = f.Guests.Error == nil
}
return cart, nil
}