package payment import ( "context" "fmt" "net/http" "time" "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/redsys" "dev.tandem.ws/tandem/camper/pkg/template" "dev.tandem.ws/tandem/camper/pkg/uuid" ) 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: servePaymentIndex(w, r, user, company, conn) default: httplib.MethodNotAllowed(w, r, http.MethodGet) } case "settings": head, r.URL.Path = httplib.ShiftPath(r.URL.Path) switch head { case "": switch r.Method { case http.MethodGet: f := newSettingsForm(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: if !uuid.Valid(head) { http.NotFound(w, r) return } payment, err := fetchPaymentDetails(r.Context(), conn, head, user.Locale) if err != nil { if database.ErrorIsNotFound(err) { http.NotFound(w, r) return } panic(err) } h.paymentHandler(user, company, conn, payment).ServeHTTP(w, r) } }) } func (h *AdminHandler) paymentHandler(user *auth.User, company *auth.Company, conn *database.Conn, payment *paymentDetails) 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: if payment.Status == StatusPreAuthenticated { var err error if err = conn.QueryRow(r.Context(), "select environment from redsys where company_id = $1", company.ID).Scan(&payment.Environment); err != nil && !database.ErrorIsNotFound(err) { panic(err) } payment.AcceptPreauthRequest, err = payment.createRequest(r, user, company, conn, redsys.TransactionTypePreauthConfirm) if err != nil { panic(err) } payment.VoidPreauthRequest, err = payment.createRequest(r, user, company, conn, redsys.TransactionTypePreauthVoid) if err != nil { panic(err) } } payment.MustRender(w, r, user, company) default: httplib.MethodNotAllowed(w, r, http.MethodGet) } default: http.NotFound(w, r) } }) } func servePaymentIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { filters := newFilterForm(r.Context(), conn, company, user.Locale) if err := filters.Parse(r); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } payments, err := collectPaymentEntries(r.Context(), company, conn, user.Locale, filters) if err != nil { panic(err) } page := &paymentIndex{ Payments: filters.buildCursor(payments), Filters: filters, } page.MustRender(w, r, user, company) } type paymentEntry struct { ID int URL string Reference string DownPayment string Total string Status string StatusLabel string CreatedAt time.Time } func collectPaymentEntries(ctx context.Context, company *auth.Company, conn *database.Conn, locale *locale.Locale, filters *filterForm) ([]*paymentEntry, error) { where, args := filters.BuildQuery([]interface{}{locale.Language}) rows, err := conn.Query(ctx, fmt.Sprintf(` select payment_id , '/admin/payments/' || payment.slug , payment.reference , to_price(payment.down_payment, decimal_digits) , to_price(total, decimal_digits) , payment.payment_status , coalesce(payment_status_i18n.name, payment_status.name) , created_at from payment join currency using (currency_code) join payment_status using (payment_status) left join payment_status_i18n on payment_status_i18n.payment_status = payment.payment_status and payment_status_i18n.lang_tag = $1 where (%s) order by created_at desc , payment_id limit %d `, where, filters.PerPage()+1), args...) if err != nil { return nil, err } defer rows.Close() var payments []*paymentEntry for rows.Next() { entry := &paymentEntry{} if err = rows.Scan( &entry.ID, &entry.URL, &entry.Reference, &entry.DownPayment, &entry.Total, &entry.Status, &entry.StatusLabel, &entry.CreatedAt, ); err != nil { return nil, err } payments = append(payments, entry) } return payments, nil } type paymentIndex struct { Payments []*paymentEntry Filters *filterForm } func (page *paymentIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { if httplib.IsHTMxRequest(r) && page.Filters.Paginated() { template.MustRenderAdminNoLayout(w, r, user, company, "payment/results.gohtml", page) } else { template.MustRenderAdminFiles(w, r, user, company, page, "payment/index.gohtml", "payment/results.gohtml") } } type paymentDetails struct { ID int Slug string Reference string CampsiteType string ArrivalDate time.Time DepartureDate time.Time NumNights int NumAdults int NumTeenagers int NumChildren int NumDogs int SubtotalTouristTax string Total string DownPaymentPercent int DownPayment string ZonePreferences string ACSICard bool Status string StatusLabel string CreatedAt time.Time UpdatedAt time.Time Options []*paymentOption Customer *paymentCustomer Environment string AcceptPreauthRequest *redsys.SignedRequest VoidPreauthRequest *redsys.SignedRequest } type paymentOption struct { Label string Units int } type paymentCustomer struct { FullName string Address string PostalCode string City string Country string Email string Phone string Language string } func fetchPaymentDetails(ctx context.Context, conn *database.Conn, slug string, locale *locale.Locale) (*paymentDetails, error) { row := conn.QueryRow(ctx, ` select payment_id , payment.slug , payment.reference , coalesce(campsite_type_i18n.name, campsite_type.name) , arrival_date , departure_date , departure_date - arrival_date , number_adults , number_teenagers , number_children , number_dogs , to_price(subtotal_tourist_tax, decimal_digits) , to_price(total, decimal_digits) , (down_payment_percent * 100)::integer , to_price(payment.down_payment, decimal_digits) , zone_preferences , acsi_card , payment.payment_status , coalesce(payment_status_i18n.name, payment_status.name) , created_at , updated_at from payment join currency using (currency_code) join campsite_type using (campsite_type_id) join payment_status using (payment_status) left join payment_status_i18n on payment_status_i18n.payment_status = payment.payment_status and payment_status_i18n.lang_tag = $2 left join campsite_type_i18n on campsite_type_i18n.campsite_type_id = campsite_type.campsite_type_id and campsite_type_i18n.lang_tag = $2 where payment.slug = $1 `, slug, locale.Language) details := &paymentDetails{} if err := row.Scan( &details.ID, &details.Slug, &details.Reference, &details.CampsiteType, &details.ArrivalDate, &details.DepartureDate, &details.NumNights, &details.NumAdults, &details.NumTeenagers, &details.NumChildren, &details.NumDogs, &details.SubtotalTouristTax, &details.Total, &details.DownPaymentPercent, &details.DownPayment, &details.ZonePreferences, &details.ACSICard, &details.Status, &details.StatusLabel, &details.CreatedAt, &details.UpdatedAt, ); err != nil { return nil, err } var err error details.Options, err = fetchPaymentOptions(ctx, conn, details.ID, locale) if err != nil && !database.ErrorIsNotFound(err) { return nil, err } details.Customer, err = fetchPaymentCustomer(ctx, conn, details.ID, locale) if err != nil && !database.ErrorIsNotFound(err) { return nil, err } return details, nil } func fetchPaymentOptions(ctx context.Context, conn *database.Conn, paymentID int, locale *locale.Locale) ([]*paymentOption, error) { rows, err := conn.Query(ctx, ` select coalesce(option_i18n.name, option.name) , units from payment_option join campsite_type_option as option using (campsite_type_option_id) left join campsite_type_option_i18n as option_i18n on option_i18n.campsite_type_option_id = option.campsite_type_option_id and option_i18n.lang_tag = $2 where payment_id = $1 `, paymentID, locale.Language) if err != nil { return nil, err } defer rows.Close() var options []*paymentOption for rows.Next() { option := &paymentOption{} if err = rows.Scan(&option.Label, &option.Units); err != nil { return nil, err } options = append(options, option) } return options, nil } func fetchPaymentCustomer(ctx context.Context, conn *database.Conn, paymentID int, locale *locale.Locale) (*paymentCustomer, error) { row := conn.QueryRow(ctx, ` select full_name , address , postal_code , city , coalesce(country_i18n.name, country.name) , email , phone::text , language.endonym from payment_customer join country using (country_code) left join country_i18n on country.country_code = country_i18n.country_code and country_i18n.lang_tag = $2 join language on payment_customer.lang_tag = language.lang_tag where payment_id = $1 `, paymentID, locale.Language) customer := &paymentCustomer{} if err := row.Scan( &customer.FullName, &customer.Address, &customer.PostalCode, &customer.City, &customer.Country, &customer.Email, &customer.Phone, &customer.Language, ); err != nil { return nil, err } return customer, nil } func (f *paymentDetails) createRequest(r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, transactionType redsys.TransactionType) (*redsys.SignedRequest, error) { schema := httplib.Protocol(r) authority := httplib.Host(r) paymentURL := fmt.Sprintf("%s://%s/admin/payments/%s", schema, authority, f.Slug) request := redsys.Request{ TransactionType: transactionType, Amount: f.DownPayment, OrderNumber: f.Reference, SuccessURL: paymentURL, FailureURL: paymentURL, NotificationURL: fmt.Sprintf("%s/notification", publicBaseURL(r, user, f.Slug)), ConsumerLanguage: user.Locale.Language, } return request.Sign(r.Context(), conn, company) } func (f *paymentDetails) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { template.MustRenderAdmin(w, r, user, company, "payment/details.gohtml", f) }