378 lines
11 KiB
Go
378 lines
11 KiB
Go
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) {
|
|
payments, err := collectPaymentEntries(r.Context(), company, conn, user.Locale)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
page := &paymentIndex{
|
|
Payments: payments,
|
|
}
|
|
page.MustRender(w, r, user, company)
|
|
}
|
|
|
|
type paymentEntry struct {
|
|
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) ([]*paymentEntry, error) {
|
|
rows, err := conn.Query(ctx, `
|
|
select '/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 = $2
|
|
where company_id = $1
|
|
order by created_at desc
|
|
`, company.ID, locale.Language)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var payments []*paymentEntry
|
|
for rows.Next() {
|
|
entry := &paymentEntry{}
|
|
if err = rows.Scan(
|
|
&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
|
|
}
|
|
|
|
func (page *paymentIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
|
|
template.MustRenderAdmin(w, r, user, company, "payment/index.gohtml", page)
|
|
}
|
|
|
|
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)
|
|
}
|