2024-02-12 17:06:17 +00:00
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"
)
2024-02-13 01:38:38 +00:00
const (
StatusDraft = "draft"
StatusPending = "pending"
StatusFailed = "failed"
StatusCompleted = "completed"
StatusRefunded = "refunded"
)
2024-02-12 17:06:17 +00:00
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 )
2024-02-13 01:38:38 +00:00
case "notification" :
handleNotification ( w , r , user , company , conn , payment )
2024-02-12 17:06:17 +00:00
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 )
2024-02-13 01:38:38 +00:00
request := redsys . Request {
2024-02-12 17:06:17 +00:00
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 ,
}
2024-02-13 01:38:38 +00:00
return request . Sign ( r . Context ( ) , conn , company )
2024-02-12 17:06:17 +00:00
}
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" )
}
2024-02-13 01:38:38 +00:00
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 )
}
}