2024-02-12 17:06:17 +00:00
package payment
import (
"context"
"fmt"
2024-02-13 04:20:35 +00:00
ht "html/template"
2024-02-12 17:06:17 +00:00
"net/http"
2024-02-13 04:20:35 +00:00
tt "text/template"
2024-02-12 17:06:17 +00:00
"time"
"dev.tandem.ws/tandem/camper/pkg/auth"
"dev.tandem.ws/tandem/camper/pkg/database"
httplib "dev.tandem.ws/tandem/camper/pkg/http"
2024-02-13 04:20:35 +00:00
"dev.tandem.ws/tandem/camper/pkg/locale"
"dev.tandem.ws/tandem/camper/pkg/mail"
2024-02-12 17:06:17 +00:00
"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 (
StatusCompleted = "completed"
)
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 )
}
2024-02-13 04:53:11 +00:00
template . MustRenderPublicFiles ( w , r , user , company , p , "payment/request.gohtml" , "payment/details.gohtml" )
2024-02-12 17:06:17 +00:00
}
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 :
2024-02-13 04:20:35 +00:00
_ = sendEmail ( r . Context ( ) , conn , payment , company , user . Locale ) /* shrug */
2024-02-13 01:38:38 +00:00
default :
}
w . WriteHeader ( http . StatusNoContent )
default :
httplib . MethodNotAllowed ( w , r , http . MethodPost )
}
default :
http . NotFound ( w , r )
}
}
2024-02-13 04:20:35 +00:00
type CompletedEmail struct {
CurrentLocale string
CustomerFullName string
PaymentReference string
AccommodationName string
ArrivalDate string
DepartureDate string
Total string
CompanyAddress * address
}
type address struct {
TradeName string
Address string
PostalCode string
Province string
City string
Country string
}
func sendEmail ( ctx context . Context , conn * database . Conn , payment * Payment , company * auth . Company , locale * locale . Locale ) error {
email := & CompletedEmail {
CurrentLocale : locale . Language . String ( ) ,
PaymentReference : payment . OrderNumber ( ) ,
Total : template . FormatPrice ( payment . Total , locale . Language , locale . CurrencyPattern , company . DecimalDigits , company . CurrencySymbol ) ,
CompanyAddress : & address { } ,
}
var fromAddress string
var toAddress string
if err := conn . QueryRow ( ctx , `
select company . email : : text
, customer . email : : text
, customer . full_name
, coalesce ( i18n . name , campsite_type . name )
, to_char ( arrival_date , ' DD / MM / YYYY ' )
, to_char ( departure_date , ' DD / MM / YYYY ' )
, company . trade_name
, company . address
, company . postal_code
, company . province
, company . city
, coalesce ( country_i18n . name , country . name ) as country_name
from payment
join payment_customer as customer using ( payment_id )
join campsite_type using ( campsite_type_id )
left join campsite_type_i18n as i18n on i18n . campsite_type_id = campsite_type . campsite_type_id and i18n . lang_tag = $ 2
join company on company . company_id = payment . company_id
join country on country . country_code = company . country_code
left join country_i18n on country . country_code = country_i18n . country_code and country_i18n . lang_tag = $ 2
where payment_id = $ 1
` , payment . ID , locale . Language ) . Scan ( & fromAddress ,
& toAddress ,
& email . CustomerFullName ,
& email . AccommodationName ,
& email . ArrivalDate ,
& email . DepartureDate ,
& email . CompanyAddress . TradeName ,
& email . CompanyAddress . Address ,
& email . CompanyAddress . PostalCode ,
& email . CompanyAddress . Province ,
& email . CompanyAddress . City ,
& email . CompanyAddress . Country ,
) ; err != nil {
return err
}
m := mail . NewMsg ( )
if err := m . From ( fromAddress ) ; err != nil {
return err
}
if err := m . ReplyTo ( fromAddress ) ; err != nil {
return err
}
if err := m . To ( toAddress ) ; err != nil {
return err
}
m . Subject ( locale . Pgettext ( "Booking payment successfully received" , "subject" ) )
baseTemplate := "body.go"
baseFilename := "web/templates/mail/payment/" + baseTemplate
body , err := tt . New ( baseTemplate + "txt" ) . Funcs ( tt . FuncMap {
"gettext" : locale . Get ,
"pgettext" : locale . GetC ,
} ) . ParseFiles ( baseFilename + "txt" )
if err != nil {
return err
}
if err := m . SetBodyTextTemplate ( body , email ) ; err != nil {
return err
}
alternative , err := ht . New ( baseTemplate + "html" ) . Funcs ( tt . FuncMap {
"gettext" : locale . Get ,
"pgettext" : locale . GetC ,
"raw" : func ( s string ) ht . HTML { return ht . HTML ( s ) } ,
} ) . ParseFiles ( baseFilename + "html" )
if err != nil {
return err
}
if err := m . AddAlternativeHTMLTemplate ( alternative , email ) ; err != nil {
return err
}
return m . WriteToSendmail ( )
}