camper/pkg/payment/public.go

262 lines
7.3 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"
)
const (
StatusDraft = "draft"
StatusPending = "pending"
StatusFailed = "failed"
StatusCompleted = "completed"
StatusRefunded = "refunded"
)
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)
case "notification":
handleNotification(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 request.Sign(r.Context(), conn, company)
}
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")
}
func handleNotification(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.MethodPost:
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
signed := redsys.SignedResponse{
MerchantParameters: r.Form.Get("Ds_MerchantParameters"),
Signature: r.Form.Get("Ds_Signature"),
SignatureVersion: r.Form.Get("Ds_SignatureVersion"),
}
response, err := signed.Decode(r.Context(), conn, company)
if err != nil {
panic(err)
}
if response == nil {
http.Error(w, "Invalid response", http.StatusBadRequest)
return
}
if response.OrderNumber != payment.OrderNumber() {
http.Error(w, "Response for a different payment", http.StatusBadRequest)
return
}
status, err := response.Process(r.Context(), conn, payment.Slug)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
switch status {
case StatusCompleted:
default:
}
w.WriteHeader(http.StatusNoContent)
default:
httplib.MethodNotAllowed(w, r, http.MethodPost)
}
default:
http.NotFound(w, r)
}
}