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-29 15:59:30 +00:00
"log"
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 (
2024-03-24 21:06:59 +00:00
StatusCompleted = "completed"
StatusPreAuthenticated = "preauth"
2024-02-13 01:38:38 +00:00
)
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
2024-02-14 03:54:42 +00:00
, payment . reference
2024-02-12 17:06:17 +00:00
, payment . created_at
, to_price ( total , decimal_digits )
2024-02-13 22:45:25 +00:00
, to_price ( payment . down_payment , decimal_digits )
2024-02-12 17:06:17 +00:00
from payment
join currency using ( currency_code )
where payment . slug = $ 1
and payment_status < > ' draft '
` , paymentSlug )
payment := & Payment { }
2024-02-14 03:54:42 +00:00
if err := row . Scan ( & payment . ID , & payment . Slug , & payment . Reference , & payment . CreateTime , & payment . Total , & payment . DownPayment ) ; err != nil {
2024-02-12 17:06:17 +00:00
return nil , err
}
return payment , nil
}
type Payment struct {
2024-02-13 22:45:25 +00:00
ID int
Slug string
2024-02-14 03:54:42 +00:00
Reference string
2024-02-13 22:45:25 +00:00
Total string
DownPayment string
CreateTime time . Time
2024-02-12 17:06:17 +00:00
}
func ( payment * Payment ) createRequest ( r * http . Request , user * auth . User , company * auth . Company , conn * database . Conn ) ( * redsys . SignedRequest , error ) {
2024-03-24 21:06:59 +00:00
baseURL := publicBaseURL ( r , user , payment . Slug )
2024-02-13 01:38:38 +00:00
request := redsys . Request {
2024-03-24 21:06:59 +00:00
TransactionType : redsys . TransactionTypePreauth ,
2024-02-13 22:45:25 +00:00
Amount : payment . DownPayment ,
2024-02-14 03:54:42 +00:00
OrderNumber : payment . Reference ,
2024-02-12 17:06:17 +00:00
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
}
2024-03-24 21:06:59 +00:00
func publicBaseURL ( r * http . Request , user * auth . User , slug string ) string {
schema := httplib . Protocol ( r )
authority := httplib . Host ( r )
return fmt . Sprintf ( "%s://%s/%s/payments/%s" , schema , authority , user . Locale . Language , slug )
}
2024-02-12 17:06:17 +00:00
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
}
2024-02-14 03:54:42 +00:00
if response . OrderNumber != payment . Reference {
2024-02-13 01:38:38 +00:00
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 {
2024-03-24 21:06:59 +00:00
case StatusPreAuthenticated :
2024-02-29 15:59:30 +00:00
if err := sendEmail ( r , conn , payment , company , user . Locale ) ; err != nil {
log . Println ( "Could not send email:" , err )
}
2024-04-29 18:59:22 +00:00
// TODO: use email to send marshalled payment
if _ , err := conn . Exec ( r . Context ( ) , "select unmarshal_booking(marshal_payment($1))" , payment . ID ) ; err != nil {
log . Println ( "Could not marshal payment:" , err )
}
2024-02-13 01:38:38 +00:00
default :
}
2024-02-28 12:42:12 +00:00
w . WriteHeader ( http . StatusOK )
2024-02-13 01:38:38 +00:00
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
2024-02-13 22:45:25 +00:00
DownPayment string
2024-02-13 04:20:35 +00:00
CompanyAddress * address
}
2024-02-29 15:59:30 +00:00
type NotificationEmail struct {
CompanyName string
BaseURL string
Details * paymentDetails
}
2024-02-13 04:20:35 +00:00
type address struct {
TradeName string
Address string
PostalCode string
Province string
City string
Country string
}
2024-02-29 15:59:30 +00:00
func sendEmail ( r * http . Request , conn * database . Conn , payment * Payment , company * auth . Company , locale * locale . Locale ) error {
2024-02-13 04:20:35 +00:00
email := & CompletedEmail {
CurrentLocale : locale . Language . String ( ) ,
2024-02-14 03:54:42 +00:00
PaymentReference : payment . Reference ,
2024-02-13 04:20:35 +00:00
Total : template . FormatPrice ( payment . Total , locale . Language , locale . CurrencyPattern , company . DecimalDigits , company . CurrencySymbol ) ,
2024-02-13 22:45:25 +00:00
DownPayment : template . FormatPrice ( payment . DownPayment , locale . Language , locale . CurrencyPattern , company . DecimalDigits , company . CurrencySymbol ) ,
2024-02-13 04:20:35 +00:00
CompanyAddress : & address { } ,
}
var fromAddress string
var toAddress string
2024-02-29 15:59:30 +00:00
if err := conn . QueryRow ( r . Context ( ) , `
2024-02-13 04:20:35 +00:00
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
}
2024-02-29 15:59:30 +00:00
details , notificationErr := fetchPaymentDetails ( r . Context ( ) , conn , payment . Slug , company . DefaultLocale ( ) )
if notificationErr == nil {
schema := httplib . Protocol ( r )
authority := httplib . Host ( r )
notification := & NotificationEmail {
CompanyName : email . CompanyAddress . TradeName ,
BaseURL : fmt . Sprintf ( "%s://%s/admin/payments/%s" , schema , authority , payment . Slug ) ,
Details : details ,
}
notificationErr = sendEmailTemplate ( fromAddress , fromAddress , notification , company , "details.go" , false , company . DefaultLocale ( ) )
}
if err := sendEmailTemplate ( fromAddress , toAddress , email , company , "body.go" , true , locale ) ; err != nil {
return err
}
return notificationErr
}
func sendEmailTemplate ( fromAddress string , toAddress string , data interface { } , company * auth . Company , baseTemplate string , addAlternativeHTML bool , locale * locale . Locale ) error {
2024-02-13 04:20:35 +00:00
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" ) )
baseFilename := "web/templates/mail/payment/" + baseTemplate
body , err := tt . New ( baseTemplate + "txt" ) . Funcs ( tt . FuncMap {
"gettext" : locale . Get ,
"pgettext" : locale . GetC ,
2024-02-29 15:59:30 +00:00
"formatPrice" : func ( price string ) string {
return template . FormatPrice ( price , locale . Language , locale . CurrencyPattern , company . DecimalDigits , company . CurrencySymbol )
} ,
2024-02-13 04:20:35 +00:00
} ) . ParseFiles ( baseFilename + "txt" )
if err != nil {
return err
}
2024-02-29 15:59:30 +00:00
if err := m . SetBodyTextTemplate ( body , data ) ; err != nil {
2024-02-13 04:20:35 +00:00
return err
}
2024-02-29 15:59:30 +00:00
if addAlternativeHTML {
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 , data ) ; err != nil {
return err
}
2024-02-13 04:20:35 +00:00
}
return m . WriteToSendmail ( )
}