jordi fita mas 23be6ff26c Add ask_zone_preferences and overflow_allowed to campsite_type
The “overflow” is for when people want to book plots for more guests
than is permitted, which the system would need to add a new plot to the
“shopping cart”, as it were; not implemented yet.

The ask zone preferences is to whether show the corresponding input on
the booking form, that it was done implicitly when the campsite type had
options, because up until now it was only for plots, but it is no longer
the case, thus i need to know when to show it; now it is explicit.
2024-01-29 03:38:11 +01:00

346 lines
10 KiB
Go

/*
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
* 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)
_ = r.ParseForm()
page.Form.CampsiteType.FillValue(r)
page.Form.ArrivalDate.FillValue(r)
page.Form.DepartureDate.FillValue(r)
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.Locale.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
CampsiteType *form.Select
CampsiteTypeOptions map[string][]*campsiteTypeOption
ArrivalDate *form.Input
DepartureDate *form.Input
ZonePreferences map[string]*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 {
var typeSelectOptions []*form.Option
zonePreferences := make(map[string]*form.Input)
rows, err := conn.Query(ctx, `
select type.slug
, coalesce(i18n.name, type.name) as l10n_name
, ask_zone_preferences
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
and active
order by position, l10n_name
`, l.Language, company.ID)
if err != nil {
panic(err)
}
defer rows.Close()
for rows.Next() {
option := &form.Option{}
var ask bool
if err = rows.Scan(&option.Value, &option.Label, &ask); err != nil {
panic(err)
}
typeSelectOptions = append(typeSelectOptions, option)
if ask {
zonePreferences[option.Value] = &form.Input{
Name: "zone_preferences_" + option.Value,
}
}
}
if rows.Err() != nil {
panic(rows.Err())
}
return &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",
},
CampsiteType: &form.Select{
Name: "campsite_type",
Options: typeSelectOptions,
},
CampsiteTypeOptions: mustGetCampsiteTypeOptions(ctx, conn, company, l),
ArrivalDate: &form.Input{
Name: "arrival_date",
},
DepartureDate: &form.Input{
Name: "departure_date",
},
ZonePreferences: zonePreferences,
ACSICard: &form.Checkbox{
Name: "acsi_card",
},
Agreement: &form.Checkbox{
Name: "agreement",
},
}
}
func mustGetCampsiteTypeOptions(ctx context.Context, conn *database.Conn, company *auth.Company, l *locale.Locale) map[string][]*campsiteTypeOption {
rows, err := conn.Query(ctx, `
select 'campsite_type_option_' || option.campsite_type_option_id
, slug
, coalesce(i18n.name, option.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_option_id and i18n.lang_tag = $1
where company_id = $2
and campsite_type.active
order by option.position, l10_name
`, l.Language, company.ID)
if err != nil {
panic(err)
}
defer rows.Close()
options := make(map[string][]*campsiteTypeOption)
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)
}
options[slug] = append(options[slug], option)
}
if rows.Err() != nil {
panic(rows.Err())
}
return options
}
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.CampsiteType.FillValue(r)
f.ArrivalDate.FillValue(r)
f.DepartureDate.FillValue(r)
f.ACSICard.FillValue(r)
f.Agreement.FillValue(r)
for _, preferences := range f.ZonePreferences {
preferences.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 country != "" && 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
}
}
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
}