package main import ( "bytes" "embed" "encoding/json" "errors" "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" "github.com/stripe/stripe-go/v72/paymentintent" "html/template" "io" "log" "net/http" "payment-poc/migration" "payment-poc/state" stripe2 "payment-poc/stripe" "payment-poc/viva" "payment-poc/wspay" "strconv" "strings" "time" ) //go:embed db/dev/*.sql var devMigrations embed.FS var BackendUrl string var WsPayShopId string var WsPayShopSecret string var VivaMerchantId string var VivaApiKey string var VivaSourceCode string var VivaClientId string var VivaClientSecret string func init() { godotenv.Load() BackendUrl = envMustExist("BACKEND_URL") WsPayShopId = envMustExist("WSPAY_SHOP_ID") WsPayShopSecret = envMustExist("WSPAY_SHOP_SECRET") VivaMerchantId = envMustExist("VIVA_WALLET_MERCHANT_ID") VivaApiKey = envMustExist("VIVA_WALLET_API_KEY") VivaSourceCode = envMustExist("VIVA_WALLET_SOURCE_CODE") VivaClientId = envMustExist("VIVA_WALLET_CLIENT_ID") VivaClientSecret = envMustExist("VIVA_WALLET_CLIENT_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) }, "formatCurrency2": 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.StateVoided: return "Otkazano sa strane administratora" 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, } vivaService := viva.Service{ DB: client, ClientId: VivaClientId, ClientSecret: VivaClientSecret, SourceCode: VivaSourceCode, MerchantId: VivaMerchantId, ApiKey: VivaApiKey, } g.LoadHTMLGlob("./templates/*.gohtml") g.GET("/", func(c *gin.Context) { wspayEntries, _ := wspayService.FetchAll() stripeEntries, _ := stripeService.FetchAll() vivaEntries, _ := vivaService.FetchAll() c.HTML(200, "index.gohtml", gin.H{"WsPay": wspayEntries, "Stripe": stripeEntries, "Viva": vivaEntries}) }) 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) setupVivaEndpoints(g.Group("viva"), vivaService) 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 setupVivaEndpoints(g *gin.RouterGroup, vivaService viva.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 := vivaService.CreateEntry(int64(amount * 100)) if err != nil { c.AbortWithError(http.StatusBadRequest, err) return } log.Printf("Created initial viva entry (ammount=%d)", amount) entry, err = vivaService.CreatePaymentOrder(entry) if err != nil { c.AbortWithError(http.StatusBadRequest, err) return } vivaService.Update(entry) c.Redirect(http.StatusSeeOther, "https://demo.vivapayments.com/web/checkout?ref="+entry.OrderId) }) g.POST("complete/:id", func(c *gin.Context) { id := uuid.MustParse(c.Param("id")) amount, err := strconv.ParseFloat(c.PostForm("amount"), 64) if err != nil { c.AbortWithError(http.StatusBadRequest, err) return } entry, err := vivaService.FetchById(id) if err != nil { c.AbortWithError(http.StatusNotFound, err) return } if int64(amount*100) > entry.TotalAmount || int64(amount*100) < 1 { c.AbortWithError(http.StatusBadRequest, err) return } if entry.State == state.StateInitialized || entry.State == state.StateAccepted { entry, err = vivaService.CompleteTransaction(entry, int64(amount*100)) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } vivaService.Update(entry) } c.Redirect(http.StatusSeeOther, "/viva/info/"+id.String()) }) g.POST("cancel/:id", func(c *gin.Context) { id := uuid.MustParse(c.Param("id")) entry, err := vivaService.FetchById(id) if err != nil { c.AbortWithError(http.StatusNotFound, err) return } if entry.State == state.StateInitialized || entry.State == state.StateAccepted { entry, err = vivaService.CancelTransaction(entry) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } vivaService.Update(entry) } c.Redirect(http.StatusSeeOther, "/viva/info/"+id.String()) }) g.GET("success", func(c *gin.Context) { transactionId := uuid.MustParse(c.Query("t")) orderId := viva.OrderId(c.Query("s")) lang := c.Query("lang") eventId := c.Query("eventId") eci := c.Query("eci") log.Printf("Received success response for viva payment %s", orderId) entry, err := vivaService.FetchByOrderId(orderId) if err != nil { log.Printf("Couldn't find payment info for viva payment %s", orderId) c.AbortWithError(http.StatusInternalServerError, err) return } entry.State = state.StateAccepted entry.ECI = eci entry.Lang = lang entry.EventId = eventId entry.TransactionId = transactionId if err := vivaService.Update(entry); err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } log.Printf("Viva payment %s received correctly, returning redirect", entry.OrderId) c.Redirect(http.StatusTemporaryRedirect, "/") }) g.GET("error", func(c *gin.Context) { transactionId := uuid.MustParse(c.Query("t")) orderId := viva.OrderId(c.Query("s")) lang := c.Query("lang") eventId := c.Query("eventId") eci := c.Query("eci") log.Printf("Received error response for viva payment %s", orderId) entry, err := vivaService.FetchByOrderId(orderId) if err != nil { log.Printf("Couldn't find payment info for viva payment %s", orderId) c.AbortWithError(http.StatusInternalServerError, err) return } entry.State = state.StateAccepted entry.ECI = eci entry.Lang = lang entry.EventId = eventId entry.TransactionId = transactionId if err := vivaService.Update(entry); err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } log.Printf("Viva payment %s received correctly, returning redirect", entry.OrderId) c.Redirect(http.StatusTemporaryRedirect, "/") }) g.GET("info/:id", func(c *gin.Context) { id := uuid.MustParse(c.Param("id")) entry, err := vivaService.FetchById(id) if err != nil { c.AbortWithError(http.StatusNotFound, err) return } c.HTML(200, "viva_info.gohtml", gin.H{"Entry": entry}) }) } 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.POST("complete/:id", func(c *gin.Context) { id := uuid.MustParse(c.Param("id")) amount, err := strconv.ParseFloat(c.PostForm("amount"), 64) if err != nil { c.AbortWithError(http.StatusBadRequest, err) return } entry, err := stripeService.FetchById(id) if err != nil { c.AbortWithError(http.StatusNotFound, err) return } if int64(amount*100) > entry.TotalAmount || int64(amount*100) < 1 { c.AbortWithError(http.StatusBadRequest, err) return } if entry.State == state.StateInitialized || entry.State == state.StateAccepted { params := &stripe.PaymentIntentCaptureParams{ AmountToCapture: stripe.Int64(int64(amount * 100)), } pi, err := paymentintent.Capture(entry.PaymentIntentId, params) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } log.Printf("received state on completion: %v", pi.Status) if pi.Status == stripe.PaymentIntentStatusSucceeded || pi.Status == stripe.PaymentIntentStatusProcessing { entry.TotalAmount = pi.Amount entry.State = state.StateCompleted stripeService.Update(entry) } } c.Redirect(http.StatusSeeOther, "/stripe/info/"+id.String()) }) g.POST("cancel/: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 } if entry.State == state.StateInitialized || entry.State == state.StateAccepted { params := &stripe.PaymentIntentCancelParams{} pi, err := paymentintent.Cancel(entry.PaymentIntentId, params) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } log.Printf("received state on completion: %v", pi.Status) if pi.Status == stripe.PaymentIntentStatusCanceled { entry.State = state.StateCanceled stripeService.Update(entry) } } c.Redirect(http.StatusSeeOther, "/stripe/info/"+id.String()) }) 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(WsPayShopId, 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: WsPayShopId, ShoppingCartID: entry.ShoppingCartID, Version: "2.0", TotalAmount: entry.TotalAmount, ReturnURL: BackendUrl + "/wspay/success", ReturnErrorURL: BackendUrl + "/wspay/error", CancelURL: BackendUrl + "/wspay/cancel", Signature: wspay.CalculateFormSignature(WsPayShopId, WsPayShopSecret, entry.ShoppingCartID, entry.TotalAmount), } c.HTML(200, "wspay.gohtml", gin.H{"Action": wspay.AuthorisationForm, "Form": form}) }) g.POST("complete/:id", func(c *gin.Context) { id := uuid.MustParse(c.Param("id")) amount, err := strconv.ParseFloat(c.PostForm("amount"), 64) if err != nil { c.AbortWithError(http.StatusBadRequest, err) return } entry, err := wspayService.FetchById(id) if err != nil { c.AbortWithError(http.StatusNotFound, err) return } if int64(amount*100) > entry.TotalAmount || int64(amount*100) < 1 { c.AbortWithError(http.StatusBadRequest, err) return } if entry.State == state.StateAccepted { var request = wspay.WsPayCompletionRequest{ Version: "2.0", WsPayOrderId: entry.ShoppingCartID, ShopId: entry.ShopID, ApprovalCode: entry.ApprovalCode, STAN: entry.STAN, Amount: int64(amount * 100), Signature: wspay.CalculateCompletionSignature(WsPayShopId, WsPayShopSecret, entry.ShoppingCartID, entry.STAN, entry.ApprovalCode, int64(amount*100)), } content, _ := json.Marshal(&request) response, err := http.Post("https://test.wspay.biz/api/services/completion", "application/json", bytes.NewBuffer(content)) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } if response.StatusCode == http.StatusOK { transactionResponse := wspay.WsPayCompletionResponse{} content, err := io.ReadAll(response.Body) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } if err := json.Unmarshal(content, &transactionResponse); err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } else { log.Printf("Received transaction response: success=%s, errorMessage=%s, approvalCode=%s", transactionResponse.ActionSuccess, transactionResponse.ErrorMessage, transactionResponse.ApprovalCode, ) if wspay.CompareCompletionReturnSignature(transactionResponse.Signature, WsPayShopId, WsPayShopSecret, entry.ShoppingCartID, entry.STAN, transactionResponse.ActionSuccess, transactionResponse.ApprovalCode) != nil { entry.TotalAmount = int64(amount * 100) entry.State = state.StateCompleted wspayService.Update(entry) } else { c.AbortWithError(http.StatusInternalServerError, errors.New("received invalid signature")) return } } } else { c.AbortWithError(http.StatusInternalServerError, errors.New("received wrong status, expected 200 received "+strconv.FormatInt(int64(response.StatusCode), 10))) return } } c.Redirect(http.StatusSeeOther, "/wspay/info/"+id.String()) }) g.POST("cancel/: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 } if entry.State == state.StateAccepted { var request = wspay.WsPayCompletionRequest{ Version: "2.0", WsPayOrderId: entry.ShoppingCartID, ShopId: entry.ShopID, ApprovalCode: entry.ApprovalCode, STAN: entry.STAN, Amount: entry.TotalAmount, Signature: wspay.CalculateCompletionSignature(WsPayShopId, WsPayShopSecret, entry.ShoppingCartID, entry.STAN, entry.ApprovalCode, entry.TotalAmount), } content, _ := json.Marshal(&request) response, err := http.Post("https://test.wspay.biz/api/services/void", "application/json", bytes.NewBuffer(content)) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } if response.StatusCode == http.StatusOK { transactionResponse := wspay.WsPayCompletionResponse{} content, err := io.ReadAll(response.Body) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } if err := json.Unmarshal(content, &transactionResponse); err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } else { log.Printf("Received transaction response: success=%s, errorMessage=%s, approvalCode=%s", transactionResponse.ActionSuccess, transactionResponse.ErrorMessage, transactionResponse.ApprovalCode, ) if wspay.CompareCompletionReturnSignature(transactionResponse.Signature, WsPayShopId, WsPayShopSecret, entry.ShoppingCartID, entry.STAN, transactionResponse.ActionSuccess, transactionResponse.ApprovalCode) != nil { entry.State = state.StateCanceled wspayService.Update(entry) } else { c.AbortWithError(http.StatusInternalServerError, errors.New("received invalid signature")) return } } } else { c.AbortWithError(http.StatusInternalServerError, errors.New("received wrong status, expected 200 received "+strconv.FormatInt(int64(response.StatusCode), 10))) return } } c.Redirect(http.StatusSeeOther, "/wspay/info/"+id.String()) }) 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, WsPayShopId, WsPayShopSecret, 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, "/") } }) }