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 , case when per_night then sum(cost * units)::integer else max(cost * units)::integer end 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 campsite_type_option using (campsite_type_option_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 , per_night 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 }