406 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			406 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package main
 | |
| 
 | |
| import (
 | |
| 	"embed"
 | |
| 	"fmt"
 | |
| 	"github.com/gin-gonic/gin"
 | |
| 	"github.com/google/uuid"
 | |
| 	"github.com/joho/godotenv"
 | |
| 	"github.com/stripe/stripe-go/v72"
 | |
| 	"github.com/stripe/stripe-go/v72/checkout/session"
 | |
| 	"html/template"
 | |
| 	"log"
 | |
| 	"net/http"
 | |
| 	"payment-poc/migration"
 | |
| 	"payment-poc/state"
 | |
| 	stripe2 "payment-poc/stripe"
 | |
| 	"payment-poc/wspay"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 	"time"
 | |
| )
 | |
| 
 | |
| //go:embed db/dev/*.sql
 | |
| var devMigrations embed.FS
 | |
| 
 | |
| var BackendUrl string
 | |
| var ShopId string
 | |
| var ShopSecret string
 | |
| 
 | |
| func init() {
 | |
| 	godotenv.Load()
 | |
| 
 | |
| 	BackendUrl = envMustExist("BACKEND_URL")
 | |
| 	ShopId = envMustExist("WSPAY_SHOP_ID")
 | |
| 	ShopSecret = envMustExist("WSPAY_SHOP_SECRET")
 | |
| 	stripe.Key = envMustExist("STRIPE_KEY")
 | |
| 
 | |
| 	log.SetPrefix("")
 | |
| 	log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
 | |
| }
 | |
| 
 | |
| func main() {
 | |
| 	client, err := connectToDb()
 | |
| 	if err != nil {
 | |
| 		log.Fatalf("couldn't connect to db: %v", err)
 | |
| 	}
 | |
| 	if err := migration.InitializeMigrations(client, devMigrations); err != nil {
 | |
| 		log.Fatalf("couldn't execute migrations: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	g := gin.Default()
 | |
| 	g.Use(gin.BasicAuth(getAccounts()))
 | |
| 
 | |
| 	g.SetFuncMap(template.FuncMap{
 | |
| 		"formatCurrency": func(current int64) string {
 | |
| 			return fmt.Sprintf("%d,%02d", current/100, current%100)
 | |
| 		},
 | |
| 		"formatState": func(stt state.PaymentState) string {
 | |
| 			switch stt {
 | |
| 			case state.StateCanceled:
 | |
| 				return "Otkazano"
 | |
| 			case state.StateAccepted:
 | |
| 				return "Prihvačeno"
 | |
| 			case state.StateError:
 | |
| 				return "Greška"
 | |
| 			case state.StateInitialized:
 | |
| 				return "Inicijalna izrada"
 | |
| 			case state.StateCanceledInitialization:
 | |
| 				return "Otkazano tijekom izrade"
 | |
| 			case state.StateCompleted:
 | |
| 				return "Završeno"
 | |
| 			}
 | |
| 			return "nepoznato stanje '" + string(stt) + "'"
 | |
| 		},
 | |
| 		"omitempty": func(value string) string {
 | |
| 			if value == "" {
 | |
| 				return "-"
 | |
| 			}
 | |
| 			return value
 | |
| 		},
 | |
| 	})
 | |
| 
 | |
| 	g.NoRoute(func(c *gin.Context) {
 | |
| 		c.JSON(http.StatusNotFound, gin.H{"message": "no action on given url", "created": time.Now()})
 | |
| 	})
 | |
| 	g.NoMethod(func(c *gin.Context) {
 | |
| 		c.JSON(http.StatusMethodNotAllowed, gin.H{"message": "no action on given method", "created": time.Now()})
 | |
| 	})
 | |
| 
 | |
| 	wspayService := wspay.Service{
 | |
| 		DB: client,
 | |
| 	}
 | |
| 	stripeService := stripe2.Service{
 | |
| 		DB: client,
 | |
| 	}
 | |
| 
 | |
| 	g.LoadHTMLGlob("./templates/*.gohtml")
 | |
| 
 | |
| 	g.GET("/", func(c *gin.Context) {
 | |
| 		wspayEntries, _ := wspayService.FetchAll()
 | |
| 		stripeEntries, _ := stripeService.FetchAll()
 | |
| 		c.HTML(200, "index.gohtml", gin.H{"WsPay": wspayEntries, "Stripe": stripeEntries})
 | |
| 	})
 | |
| 
 | |
| 	g.GET("/methods", func(c *gin.Context) {
 | |
| 		amount, err := strconv.ParseFloat(c.Query("amount"), 64)
 | |
| 		if err != nil {
 | |
| 			amount = 10.00
 | |
| 		}
 | |
| 
 | |
| 		c.HTML(200, "methods.gohtml", gin.H{"Amount": amount})
 | |
| 	})
 | |
| 
 | |
| 	setupWsPayEndpoints(g.Group("wspay"), wspayService)
 | |
| 	setupStripeEndpoints(g.Group("stripe"), stripeService)
 | |
| 
 | |
| 	log.Fatal(http.ListenAndServe(":5281", g))
 | |
| }
 | |
| 
 | |
| func getAccounts() gin.Accounts {
 | |
| 	auth := strings.Split(envMustExist("AUTH"), ":")
 | |
| 	return gin.Accounts{auth[0]: auth[1]}
 | |
| }
 | |
| 
 | |
| func parseDateTime(dateTime string) time.Time {
 | |
| 	t, err := time.Parse("20060102150405", dateTime)
 | |
| 	if err != nil {
 | |
| 		log.Printf("couldn't parse response time %s: %v", dateTime, err)
 | |
| 	}
 | |
| 	return t
 | |
| }
 | |
| 
 | |
| func setupStripeEndpoints(g *gin.RouterGroup, stripeService stripe2.Service) {
 | |
| 	g.GET("", func(c *gin.Context) {
 | |
| 		amount, err := strconv.ParseFloat(c.Query("amount"), 64)
 | |
| 		if err != nil {
 | |
| 			c.AbortWithError(http.StatusBadRequest, err)
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		entry, err := stripeService.CreateEntry(int64(amount * 100))
 | |
| 		if err != nil {
 | |
| 			c.AbortWithError(http.StatusBadRequest, err)
 | |
| 			return
 | |
| 		}
 | |
| 		log.Printf("Created initial stripe entry (ammount=%d)", amount)
 | |
| 
 | |
| 		currency := string(stripe.CurrencyEUR)
 | |
| 		productName := "Example product"
 | |
| 		productDescription := "Simple example product"
 | |
| 
 | |
| 		params := &stripe.CheckoutSessionParams{
 | |
| 			LineItems: []*stripe.CheckoutSessionLineItemParams{
 | |
| 				{
 | |
| 					PriceData: &stripe.CheckoutSessionLineItemPriceDataParams{
 | |
| 						Currency: ¤cy,
 | |
| 						ProductData: &stripe.CheckoutSessionLineItemPriceDataProductDataParams{
 | |
| 							Name:        &productName,
 | |
| 							Description: &productDescription,
 | |
| 						},
 | |
| 						UnitAmount: &entry.TotalAmount,
 | |
| 					},
 | |
| 					Quantity: stripe.Int64(1),
 | |
| 				},
 | |
| 			},
 | |
| 			Mode: stripe.String(string(stripe.CheckoutSessionModePayment)),
 | |
| 			PaymentIntentData: &stripe.CheckoutSessionPaymentIntentDataParams{
 | |
| 				CaptureMethod: stripe.String("manual"),
 | |
| 			},
 | |
| 			SuccessURL: stripe.String(BackendUrl + "/stripe/success?token=" + entry.Id.String()),
 | |
| 			CancelURL:  stripe.String(BackendUrl + "/stripe/cancel?token=" + entry.Id.String()),
 | |
| 		}
 | |
| 		result, err := session.New(params)
 | |
| 		if err != nil {
 | |
| 			c.AbortWithError(http.StatusBadRequest, err)
 | |
| 			return
 | |
| 		}
 | |
| 		entry.PaymentIntentId = result.PaymentIntent.ID
 | |
| 		stripeService.Update(entry)
 | |
| 
 | |
| 		c.Redirect(http.StatusSeeOther, result.URL)
 | |
| 	})
 | |
| 
 | |
| 	g.GET("success", func(c *gin.Context) {
 | |
| 		id := uuid.MustParse(c.Query("token"))
 | |
| 
 | |
| 		log.Printf("Received success response for stripe payment %s", id)
 | |
| 		entry, err := stripeService.FetchById(id)
 | |
| 		if err != nil {
 | |
| 			log.Printf("Couldn't find payment info for stripe payment %s", id)
 | |
| 			c.AbortWithError(http.StatusInternalServerError, err)
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		entry.State = state.StateAccepted
 | |
| 
 | |
| 		if err := stripeService.Update(entry); err != nil {
 | |
| 			c.AbortWithError(http.StatusInternalServerError, err)
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		log.Printf("Stripe payment %s received correctly, returning redirect", id)
 | |
| 		c.Redirect(http.StatusTemporaryRedirect, "/")
 | |
| 	})
 | |
| 	g.GET("error", func(c *gin.Context) {
 | |
| 		id := uuid.MustParse(c.Query("token"))
 | |
| 
 | |
| 		log.Printf("Received error response for stripe payment %s", id)
 | |
| 		entry, err := stripeService.FetchById(id)
 | |
| 		if err != nil {
 | |
| 			log.Printf("Couldn't find payment info for stripe payment %s", id)
 | |
| 			c.AbortWithError(http.StatusInternalServerError, err)
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		entry.State = state.StateError
 | |
| 
 | |
| 		if err := stripeService.Update(entry); err != nil {
 | |
| 			c.AbortWithError(http.StatusInternalServerError, err)
 | |
| 			return
 | |
| 		}
 | |
| 		log.Printf("Stripe payment %s received correctly, returning redirect", id)
 | |
| 		c.Redirect(http.StatusTemporaryRedirect, "/")
 | |
| 	})
 | |
| 	g.GET("info/:id", func(c *gin.Context) {
 | |
| 		id := uuid.MustParse(c.Param("id"))
 | |
| 		entry, err := stripeService.FetchById(id)
 | |
| 		if err != nil {
 | |
| 			c.AbortWithError(http.StatusNotFound, err)
 | |
| 			return
 | |
| 		}
 | |
| 		c.HTML(200, "stripe_info.gohtml", gin.H{"Entry": entry})
 | |
| 	})
 | |
| }
 | |
| 
 | |
| func setupWsPayEndpoints(g *gin.RouterGroup, wspayService wspay.Service) {
 | |
| 	g.GET("", func(c *gin.Context) {
 | |
| 		amount, err := strconv.ParseFloat(c.Query("amount"), 64)
 | |
| 		if err != nil {
 | |
| 			c.AbortWithError(http.StatusBadRequest, err)
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		entry, err := wspayService.CreateEntry(ShopId, int64(amount*100))
 | |
| 		if err != nil {
 | |
| 			c.AbortWithError(http.StatusBadRequest, err)
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		log.Printf("Created initial wspay form (ammount=%d)", amount)
 | |
| 
 | |
| 		form := wspay.WsPayForm{
 | |
| 			ShopID:         ShopId,
 | |
| 			ShoppingCartID: entry.ShoppingCartID,
 | |
| 			Version:        "2.0",
 | |
| 			TotalAmount:    entry.TotalAmount,
 | |
| 			ReturnURL:      BackendUrl + "/wspay/success",
 | |
| 			ReturnErrorURL: BackendUrl + "/wspay/error",
 | |
| 			CancelURL:      BackendUrl + "/wspay/cancel",
 | |
| 			Signature:      wspay.CalculateFormSignature(ShopId, ShopSecret, entry.ShoppingCartID, entry.TotalAmount),
 | |
| 		}
 | |
| 
 | |
| 		c.HTML(200, "wspay.gohtml", gin.H{"Action": wspay.AuthorisationForm, "Form": form})
 | |
| 	})
 | |
| 	g.GET("success", func(c *gin.Context) {
 | |
| 		response := wspay.WsPayFormReturn{}
 | |
| 		if err := c.ShouldBind(&response); err != nil {
 | |
| 			c.AbortWithError(http.StatusInternalServerError, err)
 | |
| 			return
 | |
| 		}
 | |
| 		log.Printf("Received success response for transaction %s", response.ShoppingCartID)
 | |
| 		entry, err := wspayService.FetchByShoppingCartID(response.ShoppingCartID)
 | |
| 		if err != nil {
 | |
| 			log.Printf("Couldn't find payment info for transaction %s", response.ShoppingCartID)
 | |
| 			c.AbortWithError(http.StatusInternalServerError, err)
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		if err := wspay.CompareFormReturnSignature(response.Signature, ShopId, ShopSecret, response.ShoppingCartID, response.Success, response.ApprovalCode); err != nil {
 | |
| 			log.Printf("Invalid signature for transaction %s", response.ShoppingCartID)
 | |
| 			c.AbortWithError(http.StatusBadRequest, err)
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		entry.Lang = response.Lang
 | |
| 		entry.CustomerFirstName = response.CustomerFirstName
 | |
| 		entry.CustomerLastName = response.CustomerSurname
 | |
| 		entry.CustomerAddress = response.CustomerAddress
 | |
| 		entry.CustomerCity = response.CustomerCity
 | |
| 		entry.CustomerZIP = response.CustomerZIP
 | |
| 		entry.CustomerCountry = response.CustomerCountry
 | |
| 		entry.CustomerPhone = response.CustomerPhone
 | |
| 
 | |
| 		entry.PaymentPlan = response.PaymentPlan
 | |
| 		entry.CreditCardNumber = response.CreditCardNumber
 | |
| 		entry.DateTime = parseDateTime(response.DateTime)
 | |
| 
 | |
| 		entry.ECI = response.ECI
 | |
| 		entry.STAN = response.STAN
 | |
| 
 | |
| 		entry.Success = response.Success
 | |
| 		entry.ApprovalCode = response.ApprovalCode
 | |
| 		entry.ErrorMessage = response.ErrorMessage
 | |
| 
 | |
| 		entry.State = state.StateAccepted
 | |
| 
 | |
| 		if err := wspayService.Update(entry); err != nil {
 | |
| 			c.AbortWithError(http.StatusInternalServerError, err)
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		if c.Query("iframe") != "" {
 | |
| 			log.Printf("Transaction %s received correctly, returning iframe response", response.ShoppingCartID)
 | |
| 			c.HTML(200, "iframe_handler.gohtml", gin.H{})
 | |
| 		} else {
 | |
| 			log.Printf("Transaction %s received correctly, returning redirect", response.ShoppingCartID)
 | |
| 			c.Redirect(http.StatusTemporaryRedirect, "/")
 | |
| 		}
 | |
| 	})
 | |
| 	g.GET("error", func(c *gin.Context) {
 | |
| 		response := wspay.WsPayFormError{}
 | |
| 		if err := c.ShouldBind(&response); err != nil {
 | |
| 			c.AbortWithError(http.StatusInternalServerError, err)
 | |
| 			return
 | |
| 		}
 | |
| 		log.Printf("Received error response for transaction %s", response.ShoppingCartID)
 | |
| 		entry, err := wspayService.FetchByShoppingCartID(response.ShoppingCartID)
 | |
| 		if err != nil {
 | |
| 			log.Printf("Couldn't find payment info for transaction %s", response.ShoppingCartID)
 | |
| 			c.AbortWithError(http.StatusInternalServerError, err)
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		entry.Lang = response.Lang
 | |
| 		entry.CustomerFirstName = response.CustomerFirstName
 | |
| 		entry.CustomerLastName = response.CustomerSurname
 | |
| 		entry.CustomerAddress = response.CustomerAddress
 | |
| 		entry.CustomerCity = response.CustomerCity
 | |
| 		entry.CustomerZIP = response.CustomerZIP
 | |
| 		entry.CustomerCountry = response.CustomerCountry
 | |
| 		entry.CustomerPhone = response.CustomerPhone
 | |
| 
 | |
| 		entry.PaymentPlan = response.PaymentPlan
 | |
| 		entry.DateTime = parseDateTime(response.DateTime)
 | |
| 
 | |
| 		entry.ECI = response.ECI
 | |
| 
 | |
| 		entry.Success = response.Success
 | |
| 		entry.ApprovalCode = response.ApprovalCode
 | |
| 		entry.ErrorMessage = response.ErrorMessage
 | |
| 		entry.ErrorCodes = response.ErrorCodes
 | |
| 
 | |
| 		entry.State = state.StateError
 | |
| 
 | |
| 		if err := wspayService.Update(entry); err != nil {
 | |
| 			c.AbortWithError(http.StatusInternalServerError, err)
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		if c.Query("iframe") != "" {
 | |
| 			log.Printf("Transaction %s received correctly, returning iframe response", response.ShoppingCartID)
 | |
| 			c.HTML(200, "iframe_handler.gohtml", gin.H{})
 | |
| 		} else {
 | |
| 			log.Printf("Transaction %s received correctly, returning redirect", response.ShoppingCartID)
 | |
| 			c.Redirect(http.StatusTemporaryRedirect, "/")
 | |
| 		}
 | |
| 	})
 | |
| 	g.GET("info/:id", func(c *gin.Context) {
 | |
| 		id := uuid.MustParse(c.Param("id"))
 | |
| 		entry, err := wspayService.FetchById(id)
 | |
| 		if err != nil {
 | |
| 			c.AbortWithError(http.StatusNotFound, err)
 | |
| 			return
 | |
| 		}
 | |
| 		c.HTML(200, "wspay_info.gohtml", gin.H{"Entry": entry})
 | |
| 	})
 | |
| 	g.GET("cancel", func(c *gin.Context) {
 | |
| 		response := wspay.WsPayFormCancel{}
 | |
| 		if err := c.ShouldBind(&response); err != nil {
 | |
| 			c.AbortWithError(http.StatusInternalServerError, err)
 | |
| 			return
 | |
| 		}
 | |
| 		log.Printf("Received error response for transaction %s", response.ShoppingCartID)
 | |
| 		entry, err := wspayService.FetchByShoppingCartID(response.ShoppingCartID)
 | |
| 		if err != nil {
 | |
| 			log.Printf("Couldn't find payment info for transaction %s", response.ShoppingCartID)
 | |
| 			c.AbortWithError(http.StatusInternalServerError, err)
 | |
| 			return
 | |
| 		}
 | |
| 		entry.State = state.StateCanceledInitialization
 | |
| 
 | |
| 		if err := wspayService.Update(entry); err != nil {
 | |
| 			c.AbortWithError(http.StatusInternalServerError, err)
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		if c.Query("iframe") != "" {
 | |
| 			log.Printf("Transaction %s received correctly, returning iframe response", response.ShoppingCartID)
 | |
| 			c.HTML(200, "iframe_handler.gohtml", gin.H{})
 | |
| 		} else {
 | |
| 			log.Printf("Transaction %s received correctly, returning redirect", response.ShoppingCartID)
 | |
| 			c.Redirect(http.StatusTemporaryRedirect, "/")
 | |
| 		}
 | |
| 	})
 | |
| }
 |