jordi fita mas d945f55096 Add “part” of the bookings’ management
“Part”, because it is not possible to add or actually manage any
booking yet, but it has the export feature that we need to validate the
project.
2024-01-18 21:05:30 +01:00

275 lines
7.4 KiB
Go

/*
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
* SPDX-License-Identifier: AGPL-3.0-only
*/
package booking
import (
"context"
"golang.org/x/text/language"
"net/http"
"strings"
"time"
"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/template"
)
type AdminHandler struct {
}
func NewAdminHandler() *AdminHandler {
return &AdminHandler{}
}
func (h *AdminHandler) 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:
serveBookingIndex(w, r, user, company, conn)
default:
httplib.MethodNotAllowed(w, r, http.MethodGet)
}
case "payment":
head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
switch head {
case "":
switch r.Method {
case http.MethodGet:
f := newPaymentForm(user.Locale)
if err := f.FillFromDatabase(r.Context(), company, conn); err != nil {
if !database.ErrorIsNotFound(err) {
panic(err)
}
}
f.MustRender(w, r, user, company)
case http.MethodPut:
updatePaymentSettings(w, r, user, company, conn)
default:
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut)
}
default:
http.NotFound(w, r)
}
default:
http.NotFound(w, r)
}
})
}
func serveBookingIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
bookings, err := collectBookingEntries(r.Context(), conn, user.Locale.Language)
if err != nil {
panic(err)
}
page := bookingIndex(bookings)
page.MustRender(w, r, user, company)
}
func collectBookingEntries(ctx context.Context, conn *database.Conn, lang language.Tag) ([]*bookingEntry, error) {
rows, err := conn.Query(ctx, `
select left(slug::text, 10)
, '/admin/booking/' || slug
, arrival_date
, departure_date
, holder_name
, booking.booking_status
, coalesce(i18n.name, status.name)
from booking
join booking_status as status using (booking_status)
left join booking_status_i18n as i18n on status.booking_status = i18n.booking_status and i18n.lang_tag = $1
order by arrival_date desc
`, lang)
if err != nil {
return nil, err
}
defer rows.Close()
var entries []*bookingEntry
for rows.Next() {
entry := &bookingEntry{}
if err = rows.Scan(&entry.Reference, &entry.URL, &entry.ArrivalDate, &entry.DepartureDate, &entry.HolderName, &entry.Status, &entry.StatusLabel); err != nil {
return nil, err
}
entries = append(entries, entry)
}
return entries, nil
}
type bookingEntry struct {
Reference string
URL string
ArrivalDate time.Time
DepartureDate time.Time
HolderName string
Status string
StatusLabel string
}
type bookingIndex []*bookingEntry
func (page bookingIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
switch r.URL.Query().Get("format") {
case "ods":
columns := []string{
"Reference",
"Arrival Date",
"Departure Date",
"Holder Name",
"Status",
}
ods, err := writeTableOds(page, columns, user.Locale, func(sb *strings.Builder, entry *bookingEntry) error {
if err := writeCellString(sb, entry.Reference); err != nil {
return err
}
writeCellDate(sb, entry.ArrivalDate)
writeCellDate(sb, entry.DepartureDate)
if err := writeCellString(sb, entry.HolderName); err != nil {
return err
}
if err := writeCellString(sb, entry.StatusLabel); err != nil {
return err
}
return nil
})
if err != nil {
panic(err)
}
mustWriteOdsResponse(w, ods, user.Locale.Pgettext("bookings.ods", "filename"))
default:
template.MustRenderAdmin(w, r, user, company, "booking/index.gohtml", page)
}
}
type paymentForm struct {
MerchantCode *form.Input
TerminalNumber *form.Input
Environment *form.Select
Integration *form.Select
EncryptKey *form.Input
}
func newPaymentForm(l *locale.Locale) *paymentForm {
return &paymentForm{
MerchantCode: &form.Input{
Name: "merchant_code",
},
TerminalNumber: &form.Input{
Name: "terminal_number",
},
Environment: &form.Select{
Name: "environment",
Options: []*form.Option{
{
Value: "test",
Label: l.Pgettext("Test", "redsys environment"),
},
{
Value: "live",
Label: l.Pgettext("Live", "redsys environment"),
},
},
},
Integration: &form.Select{
Name: "integration",
Options: []*form.Option{
{
Value: "insite",
Label: l.Pgettext("InSite", "redsys integration"),
},
{
Value: "redirect",
Label: l.Pgettext("Redirect", "redsys integration"),
},
},
},
EncryptKey: &form.Input{
Name: "encrypt_key",
},
}
}
func (f *paymentForm) FillFromDatabase(ctx context.Context, company *auth.Company, conn *database.Conn) error {
return conn.QueryRow(ctx, `
select merchant_code
, terminal_number::text
, array[environment::text]
, array[integration::text]
from redsys
where company_id = $1`, company.ID).Scan(
&f.MerchantCode.Val,
&f.TerminalNumber.Val,
&f.Environment.Selected,
&f.Integration.Selected,
)
}
func (f *paymentForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
template.MustRenderAdmin(w, r, user, company, "booking/payment.gohtml", f)
}
func (f *paymentForm) Parse(r *http.Request) error {
if err := r.ParseForm(); err != nil {
return err
}
f.MerchantCode.FillValue(r)
f.TerminalNumber.FillValue(r)
f.Environment.FillValue(r)
f.Integration.FillValue(r)
f.EncryptKey.FillValue(r)
return nil
}
func (f *paymentForm) Valid(l *locale.Locale) bool {
v := form.NewValidator(l)
if v.CheckRequired(f.MerchantCode, l.GettextNoop("Merchant code can not be empty.")) {
if v.CheckExactLength(f.MerchantCode, 9, l.GettextNoop("Merchant code must be exactly nine digits long.")) {
v.CheckValidInteger(f.MerchantCode, l.GettextNoop("Merchant code must be a number."))
}
}
if v.CheckRequired(f.TerminalNumber, l.GettextNoop("Terminal number can not be empty.")) {
message := l.GettextNoop("Terminal number must be a number between 1 and 999.")
if v.CheckValidInteger(f.TerminalNumber, message) {
if v.CheckMinInteger(f.TerminalNumber, 1, message) {
v.CheckMaxInteger(f.TerminalNumber, 999, message)
}
}
}
v.CheckSelectedOptions(f.Environment, l.GettextNoop("Selected environment is not valid."))
v.CheckSelectedOptions(f.Integration, l.GettextNoop("Selected integration is not valid."))
if f.EncryptKey.Val != "" {
v.CheckValidBase64(f.EncryptKey, l.GettextNoop("The merchant key is not valid."))
}
return v.AllOK
}
func updatePaymentSettings(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
f := newPaymentForm(user.Locale)
if ok, err := form.Handle(f, w, r, user); err != nil {
return
} else if !ok {
f.MustRender(w, r, user, company)
return
}
if err := conn.SetupRedsys(r.Context(), company.ID, f.MerchantCode.Val, f.TerminalNumber.Int(), f.Environment.Selected[0], f.Integration.Selected[0], f.EncryptKey.Val); err != nil {
panic(err)
}
httplib.Redirect(w, r, "/admin/booking/payment", http.StatusSeeOther)
}