202 lines
6.0 KiB
Go
202 lines
6.0 KiB
Go
|
package booking
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"net/http"
|
||
|
"strconv"
|
||
|
"time"
|
||
|
|
||
|
"github.com/jackc/pgx/v4"
|
||
|
|
||
|
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||
|
"dev.tandem.ws/tandem/camper/pkg/database"
|
||
|
httplib "dev.tandem.ws/tandem/camper/pkg/http"
|
||
|
"dev.tandem.ws/tandem/camper/pkg/locale"
|
||
|
"dev.tandem.ws/tandem/camper/pkg/template"
|
||
|
)
|
||
|
|
||
|
func handleBookingCart(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
|
||
|
var head string
|
||
|
head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
|
||
|
|
||
|
switch head {
|
||
|
case "":
|
||
|
switch r.Method {
|
||
|
case http.MethodGet:
|
||
|
f := newBookingForm(r.Context(), company, conn, user.Locale)
|
||
|
if err := f.Parse(r); err != nil {
|
||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||
|
return
|
||
|
}
|
||
|
if cart, err := computeCart(r.Context(), conn, f); err != nil {
|
||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||
|
return
|
||
|
} else {
|
||
|
cart.MustRender(w, r, user, company)
|
||
|
}
|
||
|
default:
|
||
|
httplib.MethodNotAllowed(w, r, http.MethodGet)
|
||
|
}
|
||
|
default:
|
||
|
http.NotFound(w, r)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
type bookingCart struct {
|
||
|
Lines []*cartLine
|
||
|
Total string
|
||
|
}
|
||
|
|
||
|
type cartLine struct {
|
||
|
Concept string
|
||
|
Units int
|
||
|
Subtotal string
|
||
|
}
|
||
|
|
||
|
func (cart *bookingCart) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
|
||
|
template.MustRenderPublicNoLayout(w, r, user, company, "booking/cart.gohtml", cart)
|
||
|
}
|
||
|
|
||
|
func computeCart(ctx context.Context, conn *database.Conn, f *bookingForm) (*bookingCart, error) {
|
||
|
cart := &bookingCart{
|
||
|
Total: "0.0",
|
||
|
}
|
||
|
campsiteType := f.CampsiteType.String()
|
||
|
if campsiteType == "" {
|
||
|
return cart, nil
|
||
|
}
|
||
|
arrivalDate, err := time.Parse(database.ISODateFormat, f.ArrivalDate.Val)
|
||
|
if err != nil {
|
||
|
return cart, nil
|
||
|
}
|
||
|
departureDate, err := time.Parse(database.ISODateFormat, f.DepartureDate.Val)
|
||
|
if err != nil {
|
||
|
return cart, nil
|
||
|
}
|
||
|
numAdults, err := strconv.Atoi(f.NumberAdults.Val)
|
||
|
if err != nil {
|
||
|
return cart, nil
|
||
|
}
|
||
|
numTeenagers, err := strconv.Atoi(f.NumberTeenagers.Val)
|
||
|
if err != nil {
|
||
|
return cart, nil
|
||
|
}
|
||
|
numChildren, err := strconv.Atoi(f.NumberChildren.Val)
|
||
|
if err != nil {
|
||
|
return cart, nil
|
||
|
}
|
||
|
|
||
|
optionMap := make(map[int]*campsiteTypeOption)
|
||
|
typeOptions := f.CampsiteTypeOptions[campsiteType]
|
||
|
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_per_night)::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(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_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($7::integer[], $8::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(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
|
||
|
, tourist_tax
|
||
|
, decimal_digits
|
||
|
`, pgx.QueryResultFormats{pgx.BinaryFormatCode}, arrivalDate, departureDate.AddDate(0, 0, -1), campsiteType, numAdults, numTeenagers, numChildren, optionIDs, optionUnits)
|
||
|
|
||
|
var numNights int
|
||
|
var nights string
|
||
|
var adults string
|
||
|
var teenagers string
|
||
|
var children string
|
||
|
var touristTax string
|
||
|
var total string
|
||
|
var optionPrices database.RecordArray
|
||
|
if err = row.Scan(&numNights, &nights, &adults, &teenagers, &children, &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"))
|
||
|
|
||
|
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
|
||
|
}
|
||
|
|
||
|
return cart, nil
|
||
|
}
|