Compare commits
3 Commits
3de5c8b0f1
...
b4b0396b30
Author | SHA1 | Date |
---|---|---|
Borna Rajković | b4b0396b30 | |
Borna Rajković | 9440fa9778 | |
Borna Rajković | cefc5314f2 |
|
@ -37,3 +37,29 @@ CREATE TABLE IF NOT EXISTS "wspay"
|
|||
PRIMARY KEY (id),
|
||||
CONSTRAINT unique_id UNIQUE ("shopping_card_id")
|
||||
);
|
||||
|
||||
|
||||
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)
|
||||
);
|
||||
|
|
|
@ -2,7 +2,7 @@ version: '3.1'
|
|||
|
||||
services:
|
||||
backend:
|
||||
image: registry.bbr-dev.info/payment-poc/backend:latest
|
||||
image: registry.s2internal.com/opgdirekt/payment-poc/backend:latest
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
- database
|
||||
|
|
1
go.mod
1
go.mod
|
@ -26,6 +26,7 @@ require (
|
|||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
github.com/stripe/stripe-go/v72 v72.122.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
|
|
9
go.sum
9
go.sum
|
@ -61,6 +61,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
|||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
|
@ -68,6 +69,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
|
|||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stripe/stripe-go/v72 v72.122.0 h1:eRXWqnEwGny6dneQ5BsxGzUCED5n180u8n665JHlut8=
|
||||
github.com/stripe/stripe-go/v72 v72.122.0/go.mod h1:QwqJQtduHubZht9mek5sds9CtQcKFdsykV9ZepRWwo0=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
|
@ -75,14 +78,19 @@ github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZ
|
|||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
|
@ -92,6 +100,7 @@ google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cn
|
|||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
577
main.go
577
main.go
|
@ -1,15 +1,25 @@
|
|||
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"
|
||||
|
@ -20,15 +30,30 @@ 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("SHOP_ID")
|
||||
ShopSecret = envMustExist("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("")
|
||||
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
|
||||
|
@ -50,22 +75,27 @@ func main() {
|
|||
"formatCurrency": func(current int64) string {
|
||||
return fmt.Sprintf("%d,%02d", current/100, current%100)
|
||||
},
|
||||
"formatState": func(state wspay.PaymentState) string {
|
||||
switch state {
|
||||
case wspay.StateCanceled:
|
||||
"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 wspay.StateAccepted:
|
||||
case state.StateVoided:
|
||||
return "Otkazano sa strane administratora"
|
||||
case state.StateAccepted:
|
||||
return "Prihvačeno"
|
||||
case wspay.StateError:
|
||||
case state.StateError:
|
||||
return "Greška"
|
||||
case wspay.StateInitialized:
|
||||
case state.StateInitialized:
|
||||
return "Inicijalna izrada"
|
||||
case wspay.StateCanceledInitialization:
|
||||
case state.StateCanceledInitialization:
|
||||
return "Otkazano tijekom izrade"
|
||||
case wspay.StateCompleted:
|
||||
case state.StateCompleted:
|
||||
return "Završeno"
|
||||
}
|
||||
return "nepoznato stanje '" + string(state) + "'"
|
||||
return "nepoznato stanje '" + string(stt) + "'"
|
||||
},
|
||||
"omitempty": func(value string) string {
|
||||
if value == "" {
|
||||
|
@ -85,22 +115,369 @@ func main() {
|
|||
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) {
|
||||
entries, err := wspayService.FetchAll()
|
||||
log.Printf("%v", err)
|
||||
c.HTML(200, "index.gohtml", gin.H{"Entries": entries})
|
||||
wspayEntries, _ := wspayService.FetchAll()
|
||||
stripeEntries, _ := stripeService.FetchAll()
|
||||
vivaEntries, _ := vivaService.FetchAll()
|
||||
c.HTML(200, "index.gohtml", gin.H{"WsPay": wspayEntries, "Stripe": stripeEntries, "Viva": vivaEntries})
|
||||
})
|
||||
g.GET("/initial", func(c *gin.Context) {
|
||||
|
||||
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 := wspayService.CreateEntry(ShopId, int64(amount*100))
|
||||
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
|
||||
|
@ -109,19 +486,137 @@ func main() {
|
|||
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 + "/initial/success",
|
||||
ReturnErrorURL: BackendUrl + "/initial/error",
|
||||
CancelURL: BackendUrl + "/initial/cancel",
|
||||
Signature: wspay.CalculateFormSignature(ShopId, ShopSecret, entry.ShoppingCartID, 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, "initial.gohtml", gin.H{"Action": wspay.AuthorisationForm, "Form": form})
|
||||
c.HTML(200, "wspay.gohtml", gin.H{"Action": wspay.AuthorisationForm, "Form": form})
|
||||
})
|
||||
g.GET("/initial/success", func(c *gin.Context) {
|
||||
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)
|
||||
|
@ -135,7 +630,7 @@ func main() {
|
|||
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
|
||||
|
@ -161,7 +656,7 @@ func main() {
|
|||
entry.ApprovalCode = response.ApprovalCode
|
||||
entry.ErrorMessage = response.ErrorMessage
|
||||
|
||||
entry.State = wspay.StateAccepted
|
||||
entry.State = state.StateAccepted
|
||||
|
||||
if err := wspayService.Update(entry); err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
|
@ -176,10 +671,7 @@ func main() {
|
|||
c.Redirect(http.StatusTemporaryRedirect, "/")
|
||||
}
|
||||
})
|
||||
g.GET("/iframe", func(c *gin.Context) {
|
||||
c.HTML(200, "iframe_handler.gohtml", gin.H{})
|
||||
})
|
||||
g.GET("/initial/error", func(c *gin.Context) {
|
||||
g.GET("error", func(c *gin.Context) {
|
||||
response := wspay.WsPayFormError{}
|
||||
if err := c.ShouldBind(&response); err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
|
@ -212,7 +704,7 @@ func main() {
|
|||
entry.ErrorMessage = response.ErrorMessage
|
||||
entry.ErrorCodes = response.ErrorCodes
|
||||
|
||||
entry.State = wspay.StateError
|
||||
entry.State = state.StateError
|
||||
|
||||
if err := wspayService.Update(entry); err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
|
@ -234,9 +726,9 @@ func main() {
|
|||
c.AbortWithError(http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
c.HTML(200, "info.gohtml", gin.H{"Entry": entry})
|
||||
c.HTML(200, "wspay_info.gohtml", gin.H{"Entry": entry})
|
||||
})
|
||||
g.GET("/initial/cancel", func(c *gin.Context) {
|
||||
g.GET("cancel", func(c *gin.Context) {
|
||||
response := wspay.WsPayFormCancel{}
|
||||
if err := c.ShouldBind(&response); err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
|
@ -249,7 +741,7 @@ func main() {
|
|||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
entry.State = wspay.StateCanceledInitialization
|
||||
entry.State = state.StateCanceledInitialization
|
||||
|
||||
if err := wspayService.Update(entry); err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
|
@ -264,19 +756,4 @@ func main() {
|
|||
c.Redirect(http.StatusTemporaryRedirect, "/")
|
||||
}
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
|
|
8
makefile
8
makefile
|
@ -17,10 +17,10 @@ docker-dev:
|
|||
|
||||
|
||||
docker-prod:
|
||||
docker image build -t registry.bbr-dev.info/payment-poc/backend:$(VERSION) .
|
||||
docker tag registry.bbr-dev.info/payment-poc/backend:$(VERSION) registry.bbr-dev.info/payment-poc/backend:latest
|
||||
docker image push registry.bbr-dev.info/payment-poc/backend:$(VERSION)
|
||||
docker image push registry.bbr-dev.info/payment-poc/backend:latest
|
||||
docker image build -t registry.s2internal.com/opgdirekt/payment-poc/backend:$(VERSION) .
|
||||
docker tag registry.s2internal.com/opgdirekt/payment-poc/backend:$(VERSION) registry.s2internal.com/opgdirekt/payment-poc/backend:latest
|
||||
docker image push registry.s2internal.com/opgdirekt/payment-poc/backend:$(VERSION)
|
||||
docker image push registry.s2internal.com/opgdirekt/payment-poc/backend:latest
|
||||
|
||||
release:
|
||||
git tag $(VERSION)
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
package state
|
||||
|
||||
type PaymentState string
|
||||
|
||||
const (
|
||||
// initial state
|
||||
StateInitialized PaymentState = "initialized"
|
||||
|
||||
// state on response
|
||||
StateAccepted PaymentState = "accepted"
|
||||
StateError PaymentState = "error"
|
||||
StateCanceledInitialization PaymentState = "canceled_initialization"
|
||||
|
||||
// state after confirmation
|
||||
StateCompleted PaymentState = "completed"
|
||||
StateVoided PaymentState = "voided"
|
||||
StateCanceled PaymentState = "canceled"
|
||||
)
|
|
@ -0,0 +1,16 @@
|
|||
package stripe
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"payment-poc/state"
|
||||
)
|
||||
|
||||
type StripeDb struct {
|
||||
Id uuid.UUID `db:"id"`
|
||||
TotalAmount int64 `db:"total_amount"`
|
||||
Lang string `db:"lang"`
|
||||
|
||||
PaymentIntentId string `db:"payment_intent_id"`
|
||||
|
||||
State state.PaymentState `db:"payment_state"`
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package stripe
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"payment-poc/state"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
DB *sqlx.DB
|
||||
}
|
||||
|
||||
func (s *Service) CreateEntry(totalAmount int64) (StripeDb, error) {
|
||||
id := uuid.Must(uuid.NewRandom())
|
||||
entry := StripeDb{
|
||||
Id: id,
|
||||
TotalAmount: totalAmount,
|
||||
State: state.StateInitialized,
|
||||
}
|
||||
_, err := s.DB.Exec(`INSERT INTO "stripe" ("id", "total_amount", "payment_state") VALUES ($1, $2, $3)`,
|
||||
&entry.Id, &entry.TotalAmount, &entry.State,
|
||||
)
|
||||
if err != nil {
|
||||
return StripeDb{}, err
|
||||
}
|
||||
return s.FetchById(id)
|
||||
}
|
||||
|
||||
func (s *Service) FetchAll() ([]StripeDb, error) {
|
||||
var entries []StripeDb
|
||||
err := s.DB.Select(&entries, `SELECT * FROM "stripe"`)
|
||||
return entries, err
|
||||
}
|
||||
|
||||
func (s *Service) FetchById(id uuid.UUID) (StripeDb, error) {
|
||||
entry := StripeDb{}
|
||||
err := s.DB.Get(&entry, `SELECT * FROM "stripe" WHERE "id" = $1`, id)
|
||||
return entry, err
|
||||
}
|
||||
|
||||
func (s *Service) Update(entry StripeDb) error {
|
||||
_, err := s.DB.Exec(`UPDATE "stripe" set "payment_intent_id" = $2, "payment_state" = $3 WHERE "id" = $1`,
|
||||
&entry.Id, &entry.PaymentIntentId, &entry.State,
|
||||
)
|
||||
return err
|
||||
}
|
|
@ -13,6 +13,9 @@
|
|||
tr > td:nth-child(2) {
|
||||
text-align: right;
|
||||
}
|
||||
tr > th:nth-child(2) {
|
||||
text-align: right;
|
||||
}
|
||||
td, th {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
@ -24,7 +27,7 @@
|
|||
<body class="container">
|
||||
<h2>Novo plačanje</h2>
|
||||
|
||||
<form method="get" action="/initial">
|
||||
<form method="get" action="/methods">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="amount">Vrijednost</label>
|
||||
<input class="form-control" id="amount" required name="amount" type="number" step="0.01" min="0">
|
||||
|
@ -33,7 +36,7 @@
|
|||
</form>
|
||||
|
||||
<div>
|
||||
<h2>Plačanja</h2>
|
||||
<h2>WsPay</h2>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -43,9 +46,49 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Entries}}
|
||||
{{range .WsPay}}
|
||||
<tr>
|
||||
<td><a class="link-primary" href="/info/{{.Id}}">{{.Id}}</a></td>
|
||||
<td><a class="link-primary" href="/wspay/info/{{.Id}}">{{.Id}}</a></td>
|
||||
<td>{{formatCurrency .TotalAmount}}</td>
|
||||
<td>{{formatState .State}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Stripe</h2>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Vrijednost</th>
|
||||
<th>Stanje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Stripe}}
|
||||
<tr>
|
||||
<td><a class="link-primary" href="/stripe/info/{{.Id}}">{{.Id}}</a></td>
|
||||
<td>{{formatCurrency .TotalAmount}}</td>
|
||||
<td>{{formatState .State}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Viva</h2>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Vrijednost</th>
|
||||
<th>Stanje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Viva}}
|
||||
<tr>
|
||||
<td><a class="link-primary" href="/viva/info/{{.Id}}">{{.Id}}</a></td>
|
||||
<td>{{formatCurrency .TotalAmount}}</td>
|
||||
<td>{{formatState .State}}</td>
|
||||
</tr>
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Index</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
|
||||
<style>
|
||||
th {text-align: left}
|
||||
tr > td:nth-child(2) {
|
||||
text-align: right;
|
||||
}
|
||||
td, th {
|
||||
padding: 0 8px;
|
||||
}
|
||||
h2 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="container">
|
||||
<h2>Izaberi metodu plačanja</h2>
|
||||
<a class="btn btn-success" href="/wspay?amount={{.Amount}}">WsPay</a>
|
||||
<a class="btn btn-success" href="/stripe?amount={{.Amount}}">Stripe</a>
|
||||
<a class="btn btn-success" href="/viva?amount={{.Amount}}">Viva</a>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,42 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Info</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
|
||||
<style>
|
||||
th {text-align: left}
|
||||
h2 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="container">
|
||||
<h2>Plačanje {{.Entry.Id}}</h2>
|
||||
|
||||
<table class="table">
|
||||
<tr><th>Id: </th><td>{{.Entry.Id}}</td></tr>
|
||||
<tr><th>Ukupna vrijednost: </th><td>{{formatCurrency .Entry.TotalAmount}}</td></tr>
|
||||
<tr><th>Jezik: </th><td>{{omitempty .Entry.Lang}}</td></tr>
|
||||
<tr><th>Stanje: </th><td>{{formatState .Entry.State}}</td></tr>
|
||||
</table>
|
||||
|
||||
|
||||
{{if eq .Entry.State "accepted"}}
|
||||
<form class="mb-3" method="post" action="/stripe/complete/{{.Entry.Id}}">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="amount">Završi transakciju</label>
|
||||
<input class="form-control" id="amount" required name="amount" type="number" value="{{formatCurrency2 .Entry.TotalAmount}}" step="0.01" min="0.01" max="{{formatCurrency2 .Entry.TotalAmount}}">
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">Završi transakciju</button>
|
||||
</form>
|
||||
<form method="post" action="/stripe/cancel/{{.Entry.Id}}">
|
||||
<button class="btn btn-primary" type="submit">Otkaži transakciju</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,44 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Info</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
|
||||
<style>
|
||||
th {text-align: left}
|
||||
h2 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="container">
|
||||
<h2>Plačanje {{.Entry.Id}}</h2>
|
||||
|
||||
<table class="table">
|
||||
<tr><th>Id: </th><td>{{.Entry.Id}}</td></tr>
|
||||
<tr><th>Order id: </th><td>{{.Entry.OrderId}}</td></tr>
|
||||
<tr><th>Transaction id: </th><td>{{.Entry.TransactionId.String}}</td></tr>
|
||||
<tr><th>Ukupna vrijednost: </th><td>{{formatCurrency .Entry.TotalAmount}}</td></tr>
|
||||
<tr><th>Jezik: </th><td>{{omitempty .Entry.Lang}}</td></tr>
|
||||
<tr><th>Događaj: </th><td>{{.Entry.EventId}}</td></tr>
|
||||
<tr><th>Stanje: </th><td>{{formatState .Entry.State}}</td></tr>
|
||||
</table>
|
||||
|
||||
{{if eq .Entry.State "accepted"}}
|
||||
<form class="mb-3" method="post" action="/viva/complete/{{.Entry.Id}}">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="amount">Završi transakciju</label>
|
||||
<input class="form-control" id="amount" required name="amount" type="number" value="{{formatCurrency2 .Entry.TotalAmount}}" step="0.01" min="0.01" max="{{formatCurrency2 .Entry.TotalAmount}}">
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">Završi transakciju</button>
|
||||
</form>
|
||||
<form method="post" action="/viva/cancel/{{.Entry.Id}}">
|
||||
<button class="btn btn-primary" type="submit">Otkaži transakciju</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</body>
|
||||
</html>
|
|
@ -45,5 +45,18 @@
|
|||
|
||||
<tr><th>Stanje: </th><td>{{formatState .Entry.State}}</td></tr>
|
||||
</table>
|
||||
|
||||
{{if eq .Entry.State "accepted"}}
|
||||
<form class="mb-3" method="post" action="/wspay/complete/{{.Entry.Id}}">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="amount">Završi transakciju</label>
|
||||
<input class="form-control" id="amount" required name="amount" type="number" value="{{formatCurrency2 .Entry.TotalAmount}}" step="0.01" min="0.01" max="{{formatCurrency2 .Entry.TotalAmount}}">
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">Završi transakciju</button>
|
||||
</form>
|
||||
<form method="post" action="/wspay/cancel/{{.Entry.Id}}">
|
||||
<button class="btn btn-primary" type="submit">Otkaži transakciju</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,26 @@
|
|||
package viva
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"payment-poc/state"
|
||||
"time"
|
||||
)
|
||||
|
||||
const VivaUrl = "https://demo-api.vivapayments.com"
|
||||
|
||||
type VivaDb struct {
|
||||
Id uuid.UUID `db:"id"`
|
||||
OrderId string `db:"order_id"`
|
||||
TransactionId uuid.UUID `db:"transaction_id"`
|
||||
TotalAmount int64 `db:"total_amount"`
|
||||
Lang string `db:"lang"`
|
||||
|
||||
EventId string `db:"event_id"`
|
||||
ECI string `db:"eci"`
|
||||
|
||||
DateTime time.Time `db:"date_time"`
|
||||
|
||||
// transaction response
|
||||
|
||||
State state.PaymentState `db:"payment_state"`
|
||||
}
|
|
@ -0,0 +1,235 @@
|
|||
package viva
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"payment-poc/state"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
DB *sqlx.DB
|
||||
Token string
|
||||
Expiration time.Time
|
||||
ClientId string
|
||||
ClientSecret string
|
||||
SourceCode string
|
||||
|
||||
MerchantId string
|
||||
ApiKey string
|
||||
}
|
||||
|
||||
func (s *Service) OAuthToken() (string, error) {
|
||||
if s.Token != "" && s.Expiration.After(time.Now()) {
|
||||
return s.Token, nil
|
||||
}
|
||||
return s.fetchOAuthToken()
|
||||
}
|
||||
|
||||
func (s *Service) CreatePaymentOrder(entry VivaDb) (VivaDb, error) {
|
||||
token, err := s.OAuthToken()
|
||||
if err != nil {
|
||||
return VivaDb{}, err
|
||||
}
|
||||
orderRequest := VivaOrderRequest{
|
||||
Amount: entry.TotalAmount,
|
||||
Description: "Example payment",
|
||||
MerchantDescription: "Example payment",
|
||||
PreAuth: true,
|
||||
AllowRecurring: false,
|
||||
Source: s.SourceCode,
|
||||
}
|
||||
|
||||
content, err := json.Marshal(&orderRequest)
|
||||
if err != nil {
|
||||
return VivaDb{}, err
|
||||
}
|
||||
|
||||
request, err := http.NewRequest("POST", "https://demo-api.vivapayments.com/checkout/v2/orders", bytes.NewReader(content))
|
||||
request.Header.Add("authorization", "Bearer "+token)
|
||||
request.Header.Add("content-type", "application/json")
|
||||
|
||||
response, err := http.DefaultClient.Do(request)
|
||||
if err != nil {
|
||||
return VivaDb{}, err
|
||||
}
|
||||
if response.StatusCode == http.StatusOK {
|
||||
orderResponse := VivaOrderResponse{}
|
||||
content, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return VivaDb{}, err
|
||||
}
|
||||
if err := json.Unmarshal(content, &orderResponse); err != nil {
|
||||
return VivaDb{}, err
|
||||
} else {
|
||||
entry.OrderId = string(orderResponse.OrderId)
|
||||
return entry, nil
|
||||
}
|
||||
} else {
|
||||
return VivaDb{}, errors.New("received wrong status, expected 200 received " + strconv.FormatInt(int64(response.StatusCode), 10))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) CreateEntry(totalAmount int64) (VivaDb, error) {
|
||||
id := uuid.Must(uuid.NewRandom())
|
||||
entry := VivaDb{
|
||||
Id: id,
|
||||
TotalAmount: totalAmount,
|
||||
State: state.StateInitialized,
|
||||
}
|
||||
_, err := s.DB.Exec(`INSERT INTO "viva" ("id", "total_amount", "payment_state") VALUES ($1, $2, $3)`,
|
||||
&entry.Id, &entry.TotalAmount, &entry.State,
|
||||
)
|
||||
if err != nil {
|
||||
return VivaDb{}, err
|
||||
}
|
||||
return s.FetchById(id)
|
||||
}
|
||||
|
||||
func (s *Service) FetchAll() ([]VivaDb, error) {
|
||||
var entries []VivaDb
|
||||
err := s.DB.Select(&entries, `SELECT * FROM "viva"`)
|
||||
return entries, err
|
||||
}
|
||||
|
||||
func (s *Service) FetchById(id uuid.UUID) (VivaDb, error) {
|
||||
entry := VivaDb{}
|
||||
err := s.DB.Get(&entry, `SELECT * FROM "viva" WHERE "id" = $1`, id)
|
||||
return entry, err
|
||||
}
|
||||
|
||||
func (s *Service) FetchByOrderId(id OrderId) (VivaDb, error) {
|
||||
entry := VivaDb{}
|
||||
err := s.DB.Get(&entry, `SELECT * FROM "viva" WHERE "order_id" = $1`, string(id))
|
||||
return entry, err
|
||||
}
|
||||
|
||||
func (s *Service) Update(entry VivaDb) error {
|
||||
_, err := s.DB.Exec(`UPDATE "viva" set "order_id" = $2, "transaction_id" = $3, "payment_state" = $4 WHERE "id" = $1`,
|
||||
&entry.Id, &entry.OrderId, &entry.TransactionId, &entry.State,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Service) fetchOAuthToken() (string, error) {
|
||||
|
||||
form := url.Values{
|
||||
"grant_type": []string{"client_credentials"},
|
||||
}
|
||||
request, err := http.NewRequest("POST", "https://demo-accounts.vivapayments.com/connect/token", strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
request.Header.Add("content-type", "application/x-www-form-urlencoded")
|
||||
request.SetBasicAuth(s.ClientId, s.ClientSecret)
|
||||
|
||||
response, err := http.DefaultClient.Do(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if response.StatusCode == http.StatusOK {
|
||||
oauthObject := VivaOAuthResponse{}
|
||||
content, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := json.Unmarshal(content, &oauthObject); err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
s.Token = oauthObject.AccessToken
|
||||
s.Expiration = time.Now().Add(time.Duration(oauthObject.ExpiresIn) * time.Second)
|
||||
}
|
||||
} else {
|
||||
return "", errors.New("received wrong status, expected 200 received " + strconv.FormatInt(int64(response.StatusCode), 10))
|
||||
}
|
||||
return s.Token, nil
|
||||
}
|
||||
|
||||
func (s *Service) CompleteTransaction(entry VivaDb, amount int64) (VivaDb, error) {
|
||||
completionRequest := VivaTransactionCompleteRequest{
|
||||
Amount: amount,
|
||||
CustomerDescription: "Example transaction",
|
||||
}
|
||||
content, err := json.Marshal(&completionRequest)
|
||||
if err != nil {
|
||||
return VivaDb{}, err
|
||||
}
|
||||
|
||||
request, err := http.NewRequest("POST", "https://demo.vivapayments.com/api/transactions/"+entry.TransactionId.String(), bytes.NewReader(content))
|
||||
request.Header.Add("authorization", "Bearer "+s.BasicAuth())
|
||||
request.Header.Add("content-type", "application/json")
|
||||
|
||||
response, err := http.DefaultClient.Do(request)
|
||||
if err != nil {
|
||||
return VivaDb{}, err
|
||||
}
|
||||
if response.StatusCode == http.StatusOK {
|
||||
transactionResponse := VivaTransactionResponse{}
|
||||
content, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return VivaDb{}, err
|
||||
}
|
||||
if err := json.Unmarshal(content, &transactionResponse); err != nil {
|
||||
return VivaDb{}, err
|
||||
} else {
|
||||
log.Printf("Received transaction response: success=%v, eventId=%d, status=%s, amount=%f, errorCode=%d, errorText=%s",
|
||||
transactionResponse.Success, transactionResponse.EventId, transactionResponse.StatusId, transactionResponse.Amount, transactionResponse.ErrorCode, transactionResponse.ErrorText,
|
||||
)
|
||||
if transactionResponse.StatusId == "F" {
|
||||
entry.TotalAmount = int64(transactionResponse.Amount * 100)
|
||||
entry.State = state.StateCompleted
|
||||
} else {
|
||||
return VivaDb{}, errors.New("received invalid status = " + transactionResponse.StatusId)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return VivaDb{}, errors.New("received wrong status, expected 200 received " + strconv.FormatInt(int64(response.StatusCode), 10))
|
||||
}
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
func (s *Service) BasicAuth() string {
|
||||
return base64.StdEncoding.EncodeToString([]byte(s.MerchantId + ":" + s.ApiKey))
|
||||
}
|
||||
|
||||
func (s *Service) CancelTransaction(entry VivaDb) (VivaDb, error) {
|
||||
request, err := http.NewRequest("DELETE", "https://demo.vivapayments.com/api/transactions/"+entry.TransactionId.String()+"?amount="+strconv.FormatInt(entry.TotalAmount, 10), bytes.NewReader([]byte{}))
|
||||
request.Header.Add("authorization", "Bearer "+s.BasicAuth())
|
||||
|
||||
response, err := http.DefaultClient.Do(request)
|
||||
if err != nil {
|
||||
return VivaDb{}, err
|
||||
}
|
||||
if response.StatusCode == http.StatusOK {
|
||||
transactionResponse := VivaTransactionResponse{}
|
||||
content, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return VivaDb{}, err
|
||||
}
|
||||
if err := json.Unmarshal(content, &transactionResponse); err != nil {
|
||||
return VivaDb{}, err
|
||||
} else {
|
||||
log.Printf("Received transaction response: success=%v, eventId=%d, status=%s, amount=%f, errorCode=%d, errorText=%s",
|
||||
transactionResponse.Success, transactionResponse.EventId, transactionResponse.StatusId, transactionResponse.Amount, transactionResponse.ErrorCode, transactionResponse.ErrorText,
|
||||
)
|
||||
if transactionResponse.StatusId == "F" {
|
||||
entry.State = state.StateVoided
|
||||
} else {
|
||||
return VivaDb{}, errors.New("received invalid status = " + transactionResponse.StatusId)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return VivaDb{}, errors.New("received wrong status, expected 200 received " + strconv.FormatInt(int64(response.StatusCode), 10))
|
||||
}
|
||||
return entry, nil
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package viva
|
||||
|
||||
type OrderId string
|
||||
|
||||
func (o OrderId) MarshalJSON() ([]byte, error) {
|
||||
return []byte(o), nil
|
||||
}
|
||||
|
||||
func (o *OrderId) UnmarshalJSON(value []byte) error {
|
||||
*o = OrderId(value)
|
||||
return nil
|
||||
}
|
||||
|
||||
type VivaOrderRequest struct {
|
||||
Amount int64 `json:"amount"`
|
||||
Description string `json:"customerTrns"`
|
||||
MerchantDescription string `json:"merchantTrns"`
|
||||
PreAuth bool `json:"preauth"`
|
||||
AllowRecurring bool `json:"allowRecurring"`
|
||||
Source string `json:"sourceCode"`
|
||||
}
|
||||
|
||||
type VivaOrderResponse struct {
|
||||
OrderId OrderId `json:"orderCode"`
|
||||
}
|
||||
|
||||
type VivaOAuthResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
}
|
||||
|
||||
type VivaTransactionCompleteRequest struct {
|
||||
Amount int64 `json:"amount"`
|
||||
CustomerDescription string `json:"customerTrns"`
|
||||
}
|
||||
|
||||
type VivaTransactionResponse struct {
|
||||
Amount float64 `json:"Amount"`
|
||||
StatusId string `json:"StatusId"`
|
||||
ErrorCode int64 `json:"ErrorCode"`
|
||||
ErrorText string `json:"ErrorText"`
|
||||
EventId int64 `json:"EventId"`
|
||||
Success bool `json:"Success"`
|
||||
}
|
|
@ -2,26 +2,10 @@ package wspay
|
|||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"payment-poc/state"
|
||||
"time"
|
||||
)
|
||||
|
||||
type PaymentState string
|
||||
|
||||
const (
|
||||
// initial state
|
||||
StateInitialized PaymentState = "initialized"
|
||||
|
||||
// state on response
|
||||
StateAccepted PaymentState = "accepted"
|
||||
StateError PaymentState = "error"
|
||||
StateCanceledInitialization PaymentState = "canceled_initialization"
|
||||
|
||||
// state after confirmation
|
||||
StateCompleted PaymentState = "completed"
|
||||
StateVoided PaymentState = "voided"
|
||||
StateCanceled PaymentState = "canceled"
|
||||
)
|
||||
|
||||
type WsPayDb struct {
|
||||
Id uuid.UUID `db:"id"`
|
||||
ShopID string `db:"shop_id"`
|
||||
|
@ -53,5 +37,5 @@ type WsPayDb struct {
|
|||
ErrorMessage string `db:"error_message"`
|
||||
ErrorCodes string `db:"error_codes"`
|
||||
|
||||
State PaymentState `db:"payment_state"`
|
||||
State state.PaymentState `db:"payment_state"`
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"errors"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"payment-poc/state"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
|
@ -20,7 +21,7 @@ func (s *Service) CreateEntry(shopId string, totalAmount int64) (WsPayDb, error)
|
|||
ShopID: shopId,
|
||||
ShoppingCartID: id.String(),
|
||||
TotalAmount: totalAmount,
|
||||
State: StateInitialized,
|
||||
State: state.StateInitialized,
|
||||
}
|
||||
_, err := s.DB.Exec(`INSERT INTO "wspay" ("id", "shop_id", "shopping_card_id", "total_amount", "payment_state") VALUES ($1, $2, $3, $4, $5)`,
|
||||
&entry.Id, &entry.ShopID, &entry.ShoppingCartID, &entry.TotalAmount, &entry.State,
|
||||
|
@ -73,6 +74,27 @@ func CalculateFormSignature(shopId string, secret string, cartId string, amount
|
|||
return hex.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
||||
func CalculateCompletionSignature(shopId string, secret string, cartId string, stan string, approvalCode string, amount int64) string {
|
||||
/**
|
||||
Represents a signature created from string formatted from following values in a following order using
|
||||
SHA512 algorithm:
|
||||
ShopID
|
||||
WsPayOrderId
|
||||
SecretKey
|
||||
STAN
|
||||
SecretKey
|
||||
ApprovalCode
|
||||
SecretKey
|
||||
Amount
|
||||
SecretKey
|
||||
WsPayOrderId
|
||||
*/
|
||||
signature := shopId + cartId + secret + stan + secret + approvalCode + secret + strconv.FormatInt(amount, 10) + secret + cartId
|
||||
hash := sha512.New()
|
||||
hash.Write([]byte(signature))
|
||||
return hex.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
||||
func CompareFormReturnSignature(signature string, shopId string, secret string, cartId string, success int, approvalCode string) error {
|
||||
/**
|
||||
Represents a signature created from string formatted from following values in a following order using
|
||||
|
@ -96,3 +118,26 @@ func CompareFormReturnSignature(signature string, shopId string, secret string,
|
|||
return errors.New("signature mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func CompareCompletionReturnSignature(signature string, shopId string, secret string, stan string, actionSuccess string, approvalCode string, cartId string) error {
|
||||
/**
|
||||
Represents a signature created from string formatted from following values in a following order using
|
||||
SHA512 algorithm:
|
||||
ShopID
|
||||
SecretKey
|
||||
STAN
|
||||
ActionSuccess
|
||||
SecretKey
|
||||
ApprovalCode
|
||||
WsPayOrderId
|
||||
Merchant should validate this signature to make sure that the request is originating from WSPayForm.
|
||||
*/
|
||||
calculatedSignature := shopId + secret + stan + actionSuccess + secret + approvalCode + cartId
|
||||
hash := sha512.New()
|
||||
hash.Write([]byte(calculatedSignature))
|
||||
if hex.EncodeToString(hash.Sum(nil)) == signature {
|
||||
return nil
|
||||
} else {
|
||||
return errors.New("signature mismatch")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -102,7 +102,7 @@ type WsPayCompletionRequest struct {
|
|||
ShopId string
|
||||
ApprovalCode string
|
||||
STAN string
|
||||
Amount string
|
||||
Amount int64
|
||||
Signature string
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue