/* * SPDX-FileCopyrightText: 2023 jordi fita mas * SPDX-License-Identifier: AGPL-3.0-only */ package booking import ( "context" "crypto/rand" "encoding/hex" "fmt" "net/http" "dev.tandem.ws/tandem/camper/pkg/auth" "dev.tandem.ws/tandem/camper/pkg/database" "dev.tandem.ws/tandem/camper/pkg/form" httplib "dev.tandem.ws/tandem/camper/pkg/http" "dev.tandem.ws/tandem/camper/pkg/locale" "dev.tandem.ws/tandem/camper/pkg/redsys" "dev.tandem.ws/tandem/camper/pkg/template" ) type PublicHandler struct { } func NewPublicHandler() *PublicHandler { return &PublicHandler{} } func (h *PublicHandler) Handler(user *auth.User, company *auth.Company, conn *database.Conn) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var head string head, r.URL.Path = httplib.ShiftPath(r.URL.Path) switch head { case "": switch r.Method { case http.MethodGet: page := newPublicPage(r.Context(), company, conn, user.Locale) page.MustRender(w, r, user, company, conn) case http.MethodPost: makeReservation(w, r, user, company, conn) default: httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPost) } case "success": handleSuccessfulPayment(w, r, user, company, conn) case "failure": handleFailedPayment(w, r, user, company, conn) default: http.NotFound(w, r) } }) } func makeReservation(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { f := newBookingForm(r.Context(), company, conn, user.Locale) if err := f.Parse(r); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if ok, err := f.Valid(r.Context(), conn, user.Locale); err != nil { panic(err) } else if !ok { if !httplib.IsHTMxRequest(r) { w.WriteHeader(http.StatusUnprocessableEntity) } page := newPublicPageWithForm(f) page.MustRender(w, r, user, company, conn) return } schema := httplib.Protocol(r) authority := httplib.Host(r) request := &redsys.Request{ TransactionType: redsys.TransactionTypeCharge, Amount: "12.34", OrderNumber: randomOrderNumber(), Product: "Test Booking", SuccessURL: fmt.Sprintf("%s://%s/%s/booking/success", schema, authority, user.Locale.Language), FailureURL: fmt.Sprintf("%s://%s/%s/booking/failure", schema, authority, user.Locale.Language), NotificationURL: fmt.Sprintf("%s://%s/%s/booking/notification", schema, authority, user.Locale.Language), ConsumerLanguage: user.Language, } signed, err := redsys.SignRequest(r.Context(), conn, company, request) if err != nil { panic(err) } page := newPaymentPage(signed) page.MustRender(w, r, user, company, conn) } func randomOrderNumber() string { bytes := make([]byte, 6) if _, err := rand.Read(bytes); err != nil { panic(err) } return hex.EncodeToString(bytes) } type publicPage struct { *template.PublicPage Form *bookingForm } func newPublicPage(ctx context.Context, company *auth.Company, conn *database.Conn, l *locale.Locale) *publicPage { return newPublicPageWithForm(newBookingForm(ctx, company, conn, l)) } func newPublicPageWithForm(form *bookingForm) *publicPage { return &publicPage{ PublicPage: template.NewPublicPage(), Form: form, } } func (p *publicPage) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { p.Setup(r, user, company, conn) template.MustRenderPublic(w, r, user, company, "booking.gohtml", p) } type bookingForm struct { FullName *form.Input Address *form.Input PostalCode *form.Input City *form.Input Country *form.Select Email *form.Input Phone *form.Input Adults *form.Input Teenagers *form.Input Children *form.Input Dogs *form.Input CampsiteType *form.Select CampsiteTypeOptions map[string][]*campsiteTypeOption ArrivalDate *form.Input DepartureDate *form.Input AreaPreferences *form.Input ACSICard *form.Checkbox Agreement *form.Checkbox } type campsiteTypeOption struct { Label string Min int Max int Input *form.Input } func newBookingForm(ctx context.Context, company *auth.Company, conn *database.Conn, l *locale.Locale) *bookingForm { f := &bookingForm{ FullName: &form.Input{ Name: "full_name", }, Address: &form.Input{ Name: "address", }, PostalCode: &form.Input{ Name: "postal_code", }, City: &form.Input{ Name: "city", }, Country: &form.Select{ Name: "country", Options: form.MustGetOptions(ctx, conn, "select country.country_code, coalesce(i18n.name, country.name) as l10n_name from country left join country_i18n as i18n on country.country_code = i18n.country_code and i18n.lang_tag = $1 order by l10n_name", l.Language), }, Email: &form.Input{ Name: "email", }, Phone: &form.Input{ Name: "phone", }, Adults: &form.Input{ Name: "adults", Val: "2", }, Teenagers: &form.Input{ Name: "teenagers", Val: "0", }, Children: &form.Input{ Name: "children", Val: "0", }, Dogs: &form.Input{ Name: "dogs", Val: "0", }, CampsiteType: &form.Select{ Name: "campsite_type", Options: form.MustGetOptions(ctx, conn, "select type.slug, coalesce(i18n.name, type.name) as l10n_name from campsite_type as type left join campsite_type_i18n as i18n on type.campsite_type_id = i18n.campsite_type_id and i18n.lang_tag = $1 where company_id = $2 order by l10n_name", l.Language, company.ID), }, CampsiteTypeOptions: make(map[string][]*campsiteTypeOption), ArrivalDate: &form.Input{ Name: "arrival_date", }, DepartureDate: &form.Input{ Name: "departure_date", }, AreaPreferences: &form.Input{ Name: "area_preferences", }, ACSICard: &form.Checkbox{ Name: "acsi_card", }, Agreement: &form.Checkbox{ Name: "agreement", }, } rows, err := conn.Query(ctx, ` select 'campsite_type_option_' || option.campsite_type_option_id , slug , coalesce(option.name, i18n.name) as l10_name , lower(range)::text , lower(range) , upper(range) from campsite_type_option as option join campsite_type using (campsite_type_id) left join campsite_type_option_i18n as i18n on i18n.campsite_type_option_id = option.campsite_type_id and i18n.lang_tag = $1 where company_id = $2 `, l.Language, company.ID) if err != nil { panic(err) } for rows.Next() { var slug string option := &campsiteTypeOption{ Input: &form.Input{}, } if err := rows.Scan(&option.Input.Name, &slug, &option.Label, &option.Input.Val, &option.Min, &option.Max); err != nil { panic(err) } f.CampsiteTypeOptions[slug] = append(f.CampsiteTypeOptions[slug], option) } if rows.Err() != nil { panic(rows.Err()) } return f } func (f *bookingForm) Parse(r *http.Request) error { if err := r.ParseForm(); err != nil { return err } f.FullName.FillValue(r) f.Address.FillValue(r) f.PostalCode.FillValue(r) f.City.FillValue(r) f.Country.FillValue(r) f.Email.FillValue(r) f.Phone.FillValue(r) f.Adults.FillValue(r) f.Teenagers.FillValue(r) f.Children.FillValue(r) f.Dogs.FillValue(r) f.CampsiteType.FillValue(r) f.ArrivalDate.FillValue(r) f.DepartureDate.FillValue(r) f.AreaPreferences.FillValue(r) f.ACSICard.FillValue(r) f.Agreement.FillValue(r) for _, options := range f.CampsiteTypeOptions { for _, option := range options { option.Input.FillValue(r) } } return nil } func (f *bookingForm) Valid(ctx context.Context, conn *database.Conn, l *locale.Locale) (bool, error) { v := form.NewValidator(l) var country string if v.CheckSelectedOptions(f.Country, l.GettextNoop("Selected country is not valid.")) { country = f.Country.Selected[0] } if v.CheckRequired(f.FullName, l.GettextNoop("Full name can not be empty.")) { v.CheckMinLength(f.FullName, 1, l.GettextNoop("Full name must have at least one letter.")) } if f.PostalCode.Val != "" { if _, err := v.CheckValidPostalCode(ctx, conn, f.PostalCode, country, l.GettextNoop("This postal code is not valid.")); err != nil { return false, err } } if v.CheckRequired(f.Email, l.GettextNoop("Email can not be empty.")) { v.CheckValidEmail(f.Email, l.GettextNoop("This email is not valid. It should be like name@domain.com.")) } if v.CheckRequired(f.Phone, l.GettextNoop("Phone can not be empty.")) { if _, err := v.CheckValidPhone(ctx, conn, f.Phone, country, l.GettextNoop("This phone number is not valid.")); err != nil { return false, err } } if v.CheckRequired(f.Adults, l.GettextNoop("Number of adults can not be empty")) { if v.CheckValidInteger(f.Adults, l.GettextNoop("Number of adults must be an integer.")) { v.CheckMinInteger(f.Adults, 1, l.GettextNoop("Number of adults must be one or greater.")) } } if v.CheckRequired(f.Teenagers, l.GettextNoop("Number of teenagers can not be empty")) { if v.CheckValidInteger(f.Teenagers, l.GettextNoop("Number of teenagers must be an integer.")) { v.CheckMinInteger(f.Teenagers, 0, l.GettextNoop("Number of teenagers must be zero or greater.")) } } if v.CheckRequired(f.Children, l.GettextNoop("Number of children can not be empty")) { if v.CheckValidInteger(f.Children, l.GettextNoop("Number of children must be an integer.")) { v.CheckMinInteger(f.Children, 0, l.GettextNoop("Number of children must be zero or greater.")) } } if v.CheckRequired(f.Dogs, l.GettextNoop("Number of dogs can not be empty")) { if v.CheckValidInteger(f.Dogs, l.GettextNoop("Number of dogs must be an integer.")) { v.CheckMinInteger(f.Dogs, 0, l.GettextNoop("Number of dogs must be zero or greater.")) } } var validBefore bool v.CheckSelectedOptions(f.CampsiteType, l.GettextNoop("Selected campsite type is not valid.")) if v.CheckRequired(f.ArrivalDate, l.GettextNoop("Arrival date can not be empty")) { if v.CheckValidDate(f.ArrivalDate, l.GettextNoop("Arrival date must be a valid date.")) { validBefore = true } } if v.CheckRequired(f.DepartureDate, l.GettextNoop("Departure date can not be empty")) { if v.CheckValidDate(f.DepartureDate, l.GettextNoop("Departure date must be a valid date.")) && validBefore { v.CheckDateAfter(f.DepartureDate, f.ArrivalDate, l.GettextNoop("The departure date must be after the arrival date.")) } } v.Check(f.Agreement, f.Agreement.Checked, l.GettextNoop("It is mandatory to agree to the reservation conditions.")) for _, options := range f.CampsiteTypeOptions { for _, option := range options { if v.CheckRequired(option.Input, fmt.Sprintf(l.Gettext("%s can not be empty"), option.Label)) { if v.CheckValidInteger(option.Input, fmt.Sprintf(l.Gettext("%s must be an integer."), option.Label)) { if v.CheckMinInteger(option.Input, option.Min, fmt.Sprintf(l.Gettext("%s must be %d or greater."), option.Label, option.Min)) { v.CheckMaxInteger(option.Input, option.Max, fmt.Sprintf(l.Gettext("%s must be at most %d."), option.Label, option.Max)) } } } } } return v.AllOK, nil }