Added viva wallet support + completion on stripe
This commit is contained in:
parent
cefc5314f2
commit
9440fa9778
|
@ -43,11 +43,22 @@ CREATE TABLE IF NOT EXISTS "stripe"
|
||||||
(
|
(
|
||||||
"id" uuid NOT NULL,
|
"id" uuid NOT NULL,
|
||||||
"total_amount" int NOT NULL,
|
"total_amount" int NOT NULL,
|
||||||
|
|
||||||
"lang" varchar(128) DEFAULT '',
|
"lang" varchar(128) DEFAULT '',
|
||||||
|
|
||||||
"payment_intent_id" varchar(256) 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 '',
|
"payment_state" varchar(256) DEFAULT '',
|
||||||
|
|
||||||
PRIMARY KEY (id)
|
PRIMARY KEY (id)
|
||||||
|
|
249
main.go
249
main.go
|
@ -8,12 +8,14 @@ import (
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
"github.com/stripe/stripe-go/v72"
|
"github.com/stripe/stripe-go/v72"
|
||||||
"github.com/stripe/stripe-go/v72/checkout/session"
|
"github.com/stripe/stripe-go/v72/checkout/session"
|
||||||
|
"github.com/stripe/stripe-go/v72/paymentintent"
|
||||||
"html/template"
|
"html/template"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"payment-poc/migration"
|
"payment-poc/migration"
|
||||||
"payment-poc/state"
|
"payment-poc/state"
|
||||||
stripe2 "payment-poc/stripe"
|
stripe2 "payment-poc/stripe"
|
||||||
|
"payment-poc/viva"
|
||||||
"payment-poc/wspay"
|
"payment-poc/wspay"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -24,15 +26,28 @@ import (
|
||||||
var devMigrations embed.FS
|
var devMigrations embed.FS
|
||||||
|
|
||||||
var BackendUrl string
|
var BackendUrl string
|
||||||
var ShopId string
|
var WsPayShopId string
|
||||||
var ShopSecret string
|
var WsPayShopSecret string
|
||||||
|
|
||||||
|
var VivaMerchantId string
|
||||||
|
var VivaApiKey string
|
||||||
|
var VivaSourceCode string
|
||||||
|
var VivaClientId string
|
||||||
|
var VivaClientSecret string
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
godotenv.Load()
|
godotenv.Load()
|
||||||
|
|
||||||
BackendUrl = envMustExist("BACKEND_URL")
|
BackendUrl = envMustExist("BACKEND_URL")
|
||||||
ShopId = envMustExist("WSPAY_SHOP_ID")
|
WsPayShopId = envMustExist("WSPAY_SHOP_ID")
|
||||||
ShopSecret = envMustExist("WSPAY_SHOP_SECRET")
|
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")
|
stripe.Key = envMustExist("STRIPE_KEY")
|
||||||
|
|
||||||
log.SetPrefix("")
|
log.SetPrefix("")
|
||||||
|
@ -55,10 +70,15 @@ func main() {
|
||||||
"formatCurrency": func(current int64) string {
|
"formatCurrency": func(current int64) string {
|
||||||
return fmt.Sprintf("%d,%02d", current/100, current%100)
|
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 {
|
"formatState": func(stt state.PaymentState) string {
|
||||||
switch stt {
|
switch stt {
|
||||||
case state.StateCanceled:
|
case state.StateCanceled:
|
||||||
return "Otkazano"
|
return "Otkazano"
|
||||||
|
case state.StateVoided:
|
||||||
|
return "Otkazano sa strane administratora"
|
||||||
case state.StateAccepted:
|
case state.StateAccepted:
|
||||||
return "Prihvačeno"
|
return "Prihvačeno"
|
||||||
case state.StateError:
|
case state.StateError:
|
||||||
|
@ -93,13 +113,22 @@ func main() {
|
||||||
stripeService := stripe2.Service{
|
stripeService := stripe2.Service{
|
||||||
DB: client,
|
DB: client,
|
||||||
}
|
}
|
||||||
|
vivaService := viva.Service{
|
||||||
|
DB: client,
|
||||||
|
ClientId: VivaClientId,
|
||||||
|
ClientSecret: VivaClientSecret,
|
||||||
|
SourceCode: VivaSourceCode,
|
||||||
|
MerchantId: VivaMerchantId,
|
||||||
|
ApiKey: VivaApiKey,
|
||||||
|
}
|
||||||
|
|
||||||
g.LoadHTMLGlob("./templates/*.gohtml")
|
g.LoadHTMLGlob("./templates/*.gohtml")
|
||||||
|
|
||||||
g.GET("/", func(c *gin.Context) {
|
g.GET("/", func(c *gin.Context) {
|
||||||
wspayEntries, _ := wspayService.FetchAll()
|
wspayEntries, _ := wspayService.FetchAll()
|
||||||
stripeEntries, _ := stripeService.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) {
|
g.GET("/methods", func(c *gin.Context) {
|
||||||
|
@ -113,6 +142,7 @@ func main() {
|
||||||
|
|
||||||
setupWsPayEndpoints(g.Group("wspay"), wspayService)
|
setupWsPayEndpoints(g.Group("wspay"), wspayService)
|
||||||
setupStripeEndpoints(g.Group("stripe"), stripeService)
|
setupStripeEndpoints(g.Group("stripe"), stripeService)
|
||||||
|
setupVivaEndpoints(g.Group("viva"), vivaService)
|
||||||
|
|
||||||
log.Fatal(http.ListenAndServe(":5281", g))
|
log.Fatal(http.ListenAndServe(":5281", g))
|
||||||
}
|
}
|
||||||
|
@ -130,6 +160,147 @@ func parseDateTime(dateTime string) time.Time {
|
||||||
return t
|
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) {
|
func setupStripeEndpoints(g *gin.RouterGroup, stripeService stripe2.Service) {
|
||||||
g.GET("", func(c *gin.Context) {
|
g.GET("", func(c *gin.Context) {
|
||||||
amount, err := strconv.ParseFloat(c.Query("amount"), 64)
|
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)
|
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) {
|
g.GET("success", func(c *gin.Context) {
|
||||||
id := uuid.MustParse(c.Query("token"))
|
id := uuid.MustParse(c.Query("token"))
|
||||||
|
|
||||||
|
@ -241,7 +472,7 @@ func setupWsPayEndpoints(g *gin.RouterGroup, wspayService wspay.Service) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
entry, err := wspayService.CreateEntry(ShopId, int64(amount*100))
|
entry, err := wspayService.CreateEntry(WsPayShopId, int64(amount*100))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.AbortWithError(http.StatusBadRequest, err)
|
c.AbortWithError(http.StatusBadRequest, err)
|
||||||
return
|
return
|
||||||
|
@ -250,14 +481,14 @@ func setupWsPayEndpoints(g *gin.RouterGroup, wspayService wspay.Service) {
|
||||||
log.Printf("Created initial wspay form (ammount=%d)", amount)
|
log.Printf("Created initial wspay form (ammount=%d)", amount)
|
||||||
|
|
||||||
form := wspay.WsPayForm{
|
form := wspay.WsPayForm{
|
||||||
ShopID: ShopId,
|
ShopID: WsPayShopId,
|
||||||
ShoppingCartID: entry.ShoppingCartID,
|
ShoppingCartID: entry.ShoppingCartID,
|
||||||
Version: "2.0",
|
Version: "2.0",
|
||||||
TotalAmount: entry.TotalAmount,
|
TotalAmount: entry.TotalAmount,
|
||||||
ReturnURL: BackendUrl + "/wspay/success",
|
ReturnURL: BackendUrl + "/wspay/success",
|
||||||
ReturnErrorURL: BackendUrl + "/wspay/error",
|
ReturnErrorURL: BackendUrl + "/wspay/error",
|
||||||
CancelURL: BackendUrl + "/wspay/cancel",
|
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})
|
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
|
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)
|
log.Printf("Invalid signature for transaction %s", response.ShoppingCartID)
|
||||||
c.AbortWithError(http.StatusBadRequest, err)
|
c.AbortWithError(http.StatusBadRequest, err)
|
||||||
return
|
return
|
||||||
|
|
|
@ -75,6 +75,26 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -25,5 +25,6 @@
|
||||||
<h2>Izaberi metodu plačanja</h2>
|
<h2>Izaberi metodu plačanja</h2>
|
||||||
<a class="btn btn-success" href="/wspay?amount={{.Amount}}">WsPay</a>
|
<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="/stripe?amount={{.Amount}}">Stripe</a>
|
||||||
|
<a class="btn btn-success" href="/viva?amount={{.Amount}}">Viva</a>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -24,5 +24,19 @@
|
||||||
<tr><th>Jezik: </th><td>{{omitempty .Entry.Lang}}</td></tr>
|
<tr><th>Jezik: </th><td>{{omitempty .Entry.Lang}}</td></tr>
|
||||||
<tr><th>Stanje: </th><td>{{formatState .Entry.State}}</td></tr>
|
<tr><th>Stanje: </th><td>{{formatState .Entry.State}}</td></tr>
|
||||||
</table>
|
</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>
|
</body>
|
||||||
</html>
|
</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>
|
|
@ -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"`
|
||||||
|
}
|
Loading…
Reference in New Issue