205 lines
5.8 KiB
Go
205 lines
5.8 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/redsys"
|
||
|
"dev.tandem.ws/tandem/camper/pkg/template"
|
||
|
"dev.tandem.ws/tandem/camper/pkg/uuid"
|
||
|
)
|
||
|
|
||
|
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 paymentSlug string
|
||
|
paymentSlug, r.URL.Path = httplib.ShiftPath(r.URL.Path)
|
||
|
|
||
|
if !uuid.Valid(paymentSlug) {
|
||
|
http.NotFound(w, r)
|
||
|
return
|
||
|
}
|
||
|
payment, err := fetchPayment(r.Context(), conn, paymentSlug)
|
||
|
if err != nil {
|
||
|
if database.ErrorIsNotFound(err) {
|
||
|
http.NotFound(w, r)
|
||
|
return
|
||
|
}
|
||
|
panic(err)
|
||
|
}
|
||
|
|
||
|
var head string
|
||
|
head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
|
||
|
switch head {
|
||
|
case "":
|
||
|
switch r.Method {
|
||
|
case http.MethodGet:
|
||
|
page := newPaymentPage(payment)
|
||
|
page.MustRender(w, r, user, company, conn)
|
||
|
default:
|
||
|
httplib.MethodNotAllowed(w, r, http.MethodGet)
|
||
|
}
|
||
|
case "success":
|
||
|
handleSuccessfulPayment(w, r, user, company, conn, payment)
|
||
|
case "failure":
|
||
|
handleFailedPayment(w, r, user, company, conn, payment)
|
||
|
default:
|
||
|
http.NotFound(w, r)
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
|
||
|
func fetchPayment(ctx context.Context, conn *database.Conn, paymentSlug string) (*Payment, error) {
|
||
|
row := conn.QueryRow(ctx, `
|
||
|
select payment_id
|
||
|
, payment.slug::text
|
||
|
, payment.created_at
|
||
|
, to_price(total, decimal_digits)
|
||
|
from payment
|
||
|
join company using (company_id)
|
||
|
join currency using (currency_code)
|
||
|
where payment.slug = $1
|
||
|
and payment_status <> 'draft'
|
||
|
`, paymentSlug)
|
||
|
payment := &Payment{}
|
||
|
if err := row.Scan(&payment.ID, &payment.Slug, &payment.CreateTime, &payment.Total); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return payment, nil
|
||
|
}
|
||
|
|
||
|
type Payment struct {
|
||
|
ID int
|
||
|
Slug string
|
||
|
Total string
|
||
|
CreateTime time.Time
|
||
|
}
|
||
|
|
||
|
func (payment *Payment) createRequest(r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) (*redsys.SignedRequest, error) {
|
||
|
schema := httplib.Protocol(r)
|
||
|
authority := httplib.Host(r)
|
||
|
baseURL := fmt.Sprintf("%s://%s/%s/payments/%s", schema, authority, user.Locale.Language, payment.Slug)
|
||
|
request := &redsys.Request{
|
||
|
TransactionType: redsys.TransactionTypeCharge,
|
||
|
Amount: payment.Total,
|
||
|
OrderNumber: payment.OrderNumber(),
|
||
|
Product: user.Locale.Pgettext("Campsite Booking", "order product name"),
|
||
|
SuccessURL: fmt.Sprintf("%s/success", baseURL),
|
||
|
FailureURL: fmt.Sprintf("%s/failure", baseURL),
|
||
|
NotificationURL: fmt.Sprintf("%s/notification", baseURL),
|
||
|
ConsumerLanguage: user.Locale.Language,
|
||
|
}
|
||
|
return redsys.SignRequest(r.Context(), conn, company, request)
|
||
|
}
|
||
|
|
||
|
func (payment *Payment) OrderNumber() string {
|
||
|
return fmt.Sprintf("%08d%s", payment.ID, payment.Slug[:4])
|
||
|
}
|
||
|
|
||
|
type paymentPage struct {
|
||
|
*template.PublicPage
|
||
|
Environment string
|
||
|
Payment *Payment
|
||
|
Request *redsys.SignedRequest
|
||
|
}
|
||
|
|
||
|
func newPaymentPage(payment *Payment) *paymentPage {
|
||
|
return &paymentPage{
|
||
|
PublicPage: template.NewPublicPage(),
|
||
|
Payment: payment,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (p *paymentPage) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
|
||
|
p.Setup(r, user, company, conn)
|
||
|
request, err := p.Payment.createRequest(r, user, company, conn)
|
||
|
if err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
p.Request = request
|
||
|
if err := conn.QueryRow(r.Context(), "select environment from redsys where company_id = $1", company.ID).Scan(&p.Environment); err != nil && !database.ErrorIsNotFound(err) {
|
||
|
panic(err)
|
||
|
}
|
||
|
template.MustRenderPublic(w, r, user, company, "payment/request.gohtml", p)
|
||
|
}
|
||
|
func handleSuccessfulPayment(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, payment *Payment) {
|
||
|
var head string
|
||
|
head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
|
||
|
|
||
|
switch head {
|
||
|
case "":
|
||
|
switch r.Method {
|
||
|
case http.MethodGet:
|
||
|
page := newSuccessfulPaymentPage(payment)
|
||
|
page.MustRender(w, r, user, company, conn)
|
||
|
default:
|
||
|
httplib.MethodNotAllowed(w, r, http.MethodGet)
|
||
|
}
|
||
|
default:
|
||
|
http.NotFound(w, r)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
type successfulPaymentPage struct {
|
||
|
*template.PublicPage
|
||
|
Payment *Payment
|
||
|
}
|
||
|
|
||
|
func newSuccessfulPaymentPage(payment *Payment) *successfulPaymentPage {
|
||
|
return &successfulPaymentPage{
|
||
|
PublicPage: template.NewPublicPage(),
|
||
|
Payment: payment,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (p *successfulPaymentPage) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
|
||
|
p.Setup(r, user, company, conn)
|
||
|
template.MustRenderPublicFiles(w, r, user, company, p, "payment/success.gohtml", "payment/details.gohtml")
|
||
|
}
|
||
|
|
||
|
func handleFailedPayment(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, payment *Payment) {
|
||
|
var head string
|
||
|
head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
|
||
|
|
||
|
switch head {
|
||
|
case "":
|
||
|
switch r.Method {
|
||
|
case http.MethodGet:
|
||
|
page := newFailedPaymentPage(payment)
|
||
|
page.MustRender(w, r, user, company, conn)
|
||
|
default:
|
||
|
httplib.MethodNotAllowed(w, r, http.MethodGet)
|
||
|
}
|
||
|
default:
|
||
|
http.NotFound(w, r)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
type failedPaymentPage struct {
|
||
|
*template.PublicPage
|
||
|
Payment *Payment
|
||
|
}
|
||
|
|
||
|
func newFailedPaymentPage(payment *Payment) *failedPaymentPage {
|
||
|
return &failedPaymentPage{
|
||
|
PublicPage: template.NewPublicPage(),
|
||
|
Payment: payment,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (p *failedPaymentPage) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
|
||
|
p.Setup(r, user, company, conn)
|
||
|
template.MustRenderPublicFiles(w, r, user, company, p, "payment/failure.gohtml", "payment/details.gohtml")
|
||
|
}
|