diff --git a/db/dev/v1_0.sql b/db/dev/v1_0.sql index 7e2b1e3..a6a8494 100644 --- a/db/dev/v1_0.sql +++ b/db/dev/v1_0.sql @@ -43,11 +43,22 @@ CREATE TABLE IF NOT EXISTS "stripe" ( "id" uuid NOT NULL, "total_amount" int NOT NULL, - "lang" varchar(128) DEFAULT '', - "payment_intent_id" varchar(256) DEFAULT '', - + "payment_state" varchar(256) DEFAULT '', + + PRIMARY KEY (id) +); + + +CREATE TABLE IF NOT EXISTS "viva" +( + "id" uuid NOT NULL, + "order_id" varchar(24) DEFAULT '', + "transaction_id" uuid DEFAULT NULL, + "total_amount" int NOT NULL, + "event_id" varchar(128) DEFAULT '', + "eci" varchar(128) DEFAULT '', "payment_state" varchar(256) DEFAULT '', PRIMARY KEY (id) diff --git a/main.go b/main.go index d4ac2cf..92262e2 100644 --- a/main.go +++ b/main.go @@ -8,12 +8,14 @@ import ( "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" "log" "net/http" "payment-poc/migration" "payment-poc/state" stripe2 "payment-poc/stripe" + "payment-poc/viva" "payment-poc/wspay" "strconv" "strings" @@ -24,15 +26,28 @@ import ( var devMigrations embed.FS var BackendUrl string -var ShopId string -var ShopSecret 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") - ShopId = envMustExist("WSPAY_SHOP_ID") - ShopSecret = envMustExist("WSPAY_SHOP_SECRET") + 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("") @@ -55,10 +70,15 @@ func main() { "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: @@ -93,13 +113,22 @@ func main() { 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() - c.HTML(200, "index.gohtml", gin.H{"WsPay": wspayEntries, "Stripe": stripeEntries}) + vivaEntries, _ := vivaService.FetchAll() + c.HTML(200, "index.gohtml", gin.H{"WsPay": wspayEntries, "Stripe": stripeEntries, "Viva": vivaEntries}) }) g.GET("/methods", func(c *gin.Context) { @@ -113,6 +142,7 @@ func main() { setupWsPayEndpoints(g.Group("wspay"), wspayService) setupStripeEndpoints(g.Group("stripe"), stripeService) + setupVivaEndpoints(g.Group("viva"), vivaService) log.Fatal(http.ListenAndServe(":5281", g)) } @@ -130,6 +160,147 @@ func parseDateTime(dateTime string) time.Time { 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) @@ -181,6 +352,66 @@ func setupStripeEndpoints(g *gin.RouterGroup, stripeService stripe2.Service) { 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")) @@ -241,7 +472,7 @@ func setupWsPayEndpoints(g *gin.RouterGroup, wspayService wspay.Service) { return } - entry, err := wspayService.CreateEntry(ShopId, int64(amount*100)) + entry, err := wspayService.CreateEntry(WsPayShopId, int64(amount*100)) if err != nil { c.AbortWithError(http.StatusBadRequest, err) return @@ -250,14 +481,14 @@ func setupWsPayEndpoints(g *gin.RouterGroup, wspayService wspay.Service) { log.Printf("Created initial wspay form (ammount=%d)", amount) form := wspay.WsPayForm{ - ShopID: ShopId, + 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(ShopId, ShopSecret, entry.ShoppingCartID, entry.TotalAmount), + Signature: wspay.CalculateFormSignature(WsPayShopId, WsPayShopSecret, entry.ShoppingCartID, entry.TotalAmount), } c.HTML(200, "wspay.gohtml", gin.H{"Action": wspay.AuthorisationForm, "Form": form}) @@ -276,7 +507,7 @@ func setupWsPayEndpoints(g *gin.RouterGroup, wspayService wspay.Service) { return } - if err := wspay.CompareFormReturnSignature(response.Signature, ShopId, ShopSecret, response.ShoppingCartID, response.Success, response.ApprovalCode); err != nil { + 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 diff --git a/templates/index.gohtml b/templates/index.gohtml index 8795714..35794d3 100644 --- a/templates/index.gohtml +++ b/templates/index.gohtml @@ -75,6 +75,26 @@ {{end}} + +
Id | +Vrijednost | +Stanje | +
---|---|---|
{{.Id}} | +{{formatCurrency .TotalAmount}} | +{{formatState .State}} | +