262 lines
7.3 KiB
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)
|
|
}
|
|
}
|