camper/pkg/payment/admin.go

395 lines
11 KiB
Go
Raw Permalink Normal View History

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) {
2024-05-03 18:09:07 +00:00
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{
2024-05-03 18:09:07 +00:00
Payments: filters.buildCursor(payments),
Filters: filters,
}
page.MustRender(w, r, user, company)
}
type paymentEntry struct {
2024-05-03 18:09:07 +00:00
ID int
URL string
Reference string
DownPayment string
Total string
Status string
StatusLabel string
CreatedAt time.Time
}
2024-05-03 18:09:07 +00:00
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
2024-05-03 18:09:07 +00:00
and payment_status_i18n.lang_tag = $1
where (%s)
order by created_at desc
2024-05-03 18:09:07 +00:00
, 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(
2024-05-03 18:09:07 +00:00
&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
2024-05-03 18:09:07 +00:00
Filters *filterForm
}
func (page *paymentIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
2024-05-03 18:09:07 +00:00
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)
}