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,
|
||||
"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)
|
||||
|
|
249
main.go
249
main.go
|
@ -8,12 +8,14 @@ import (
|
|||
"github.com/joho/godotenv"
|
||||
"github.com/stripe/stripe-go/v72"
|
||||
"github.com/stripe/stripe-go/v72/checkout/session"
|
||||
"github.com/stripe/stripe-go/v72/paymentintent"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"payment-poc/migration"
|
||||
"payment-poc/state"
|
||||
stripe2 "payment-poc/stripe"
|
||||
"payment-poc/viva"
|
||||
"payment-poc/wspay"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -24,15 +26,28 @@ import (
|
|||
var devMigrations embed.FS
|
||||
|
||||
var BackendUrl string
|
||||
var ShopId string
|
||||
var ShopSecret string
|
||||
var WsPayShopId string
|
||||
var WsPayShopSecret string
|
||||
|
||||
var VivaMerchantId string
|
||||
var VivaApiKey string
|
||||
var VivaSourceCode string
|
||||
var VivaClientId string
|
||||
var VivaClientSecret string
|
||||
|
||||
func init() {
|
||||
godotenv.Load()
|
||||
|
||||
BackendUrl = envMustExist("BACKEND_URL")
|
||||
ShopId = envMustExist("WSPAY_SHOP_ID")
|
||||
ShopSecret = envMustExist("WSPAY_SHOP_SECRET")
|
||||
WsPayShopId = envMustExist("WSPAY_SHOP_ID")
|
||||
WsPayShopSecret = envMustExist("WSPAY_SHOP_SECRET")
|
||||
|
||||
VivaMerchantId = envMustExist("VIVA_WALLET_MERCHANT_ID")
|
||||
VivaApiKey = envMustExist("VIVA_WALLET_API_KEY")
|
||||
VivaSourceCode = envMustExist("VIVA_WALLET_SOURCE_CODE")
|
||||
VivaClientId = envMustExist("VIVA_WALLET_CLIENT_ID")
|
||||
VivaClientSecret = envMustExist("VIVA_WALLET_CLIENT_SECRET")
|
||||
|
||||
stripe.Key = envMustExist("STRIPE_KEY")
|
||||
|
||||
log.SetPrefix("")
|
||||
|
@ -55,10 +70,15 @@ func main() {
|
|||
"formatCurrency": func(current int64) string {
|
||||
return fmt.Sprintf("%d,%02d", current/100, current%100)
|
||||
},
|
||||
"formatCurrency2": func(current int64) string {
|
||||
return fmt.Sprintf("%d.%02d", current/100, current%100)
|
||||
},
|
||||
"formatState": func(stt state.PaymentState) string {
|
||||
switch stt {
|
||||
case state.StateCanceled:
|
||||
return "Otkazano"
|
||||
case state.StateVoided:
|
||||
return "Otkazano sa strane administratora"
|
||||
case state.StateAccepted:
|
||||
return "Prihvačeno"
|
||||
case state.StateError:
|
||||
|
@ -93,13 +113,22 @@ func main() {
|
|||
stripeService := stripe2.Service{
|
||||
DB: client,
|
||||
}
|
||||
vivaService := viva.Service{
|
||||
DB: client,
|
||||
ClientId: VivaClientId,
|
||||
ClientSecret: VivaClientSecret,
|
||||
SourceCode: VivaSourceCode,
|
||||
MerchantId: VivaMerchantId,
|
||||
ApiKey: VivaApiKey,
|
||||
}
|
||||
|
||||
g.LoadHTMLGlob("./templates/*.gohtml")
|
||||
|
||||
g.GET("/", func(c *gin.Context) {
|
||||
wspayEntries, _ := wspayService.FetchAll()
|
||||
stripeEntries, _ := stripeService.FetchAll()
|
||||
c.HTML(200, "index.gohtml", gin.H{"WsPay": wspayEntries, "Stripe": stripeEntries})
|
||||
vivaEntries, _ := vivaService.FetchAll()
|
||||
c.HTML(200, "index.gohtml", gin.H{"WsPay": wspayEntries, "Stripe": stripeEntries, "Viva": vivaEntries})
|
||||
})
|
||||
|
||||
g.GET("/methods", func(c *gin.Context) {
|
||||
|
@ -113,6 +142,7 @@ func main() {
|
|||
|
||||
setupWsPayEndpoints(g.Group("wspay"), wspayService)
|
||||
setupStripeEndpoints(g.Group("stripe"), stripeService)
|
||||
setupVivaEndpoints(g.Group("viva"), vivaService)
|
||||
|
||||
log.Fatal(http.ListenAndServe(":5281", g))
|
||||
}
|
||||
|
@ -130,6 +160,147 @@ func parseDateTime(dateTime string) time.Time {
|
|||
return t
|
||||
}
|
||||
|
||||
func setupVivaEndpoints(g *gin.RouterGroup, vivaService viva.Service) {
|
||||
g.GET("", func(c *gin.Context) {
|
||||
amount, err := strconv.ParseFloat(c.Query("amount"), 64)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
entry, err := vivaService.CreateEntry(int64(amount * 100))
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
log.Printf("Created initial viva entry (ammount=%d)", amount)
|
||||
|
||||
entry, err = vivaService.CreatePaymentOrder(entry)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
vivaService.Update(entry)
|
||||
c.Redirect(http.StatusSeeOther, "https://demo.vivapayments.com/web/checkout?ref="+entry.OrderId)
|
||||
})
|
||||
|
||||
g.POST("complete/:id", func(c *gin.Context) {
|
||||
id := uuid.MustParse(c.Param("id"))
|
||||
amount, err := strconv.ParseFloat(c.PostForm("amount"), 64)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
entry, err := vivaService.FetchById(id)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
if int64(amount*100) > entry.TotalAmount || int64(amount*100) < 1 {
|
||||
c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
if entry.State == state.StateInitialized || entry.State == state.StateAccepted {
|
||||
entry, err = vivaService.CompleteTransaction(entry, int64(amount*100))
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
vivaService.Update(entry)
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusSeeOther, "/viva/info/"+id.String())
|
||||
})
|
||||
|
||||
g.POST("cancel/:id", func(c *gin.Context) {
|
||||
id := uuid.MustParse(c.Param("id"))
|
||||
entry, err := vivaService.FetchById(id)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
if entry.State == state.StateInitialized || entry.State == state.StateAccepted {
|
||||
entry, err = vivaService.CancelTransaction(entry)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
vivaService.Update(entry)
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusSeeOther, "/viva/info/"+id.String())
|
||||
})
|
||||
|
||||
g.GET("success", func(c *gin.Context) {
|
||||
transactionId := uuid.MustParse(c.Query("t"))
|
||||
orderId := viva.OrderId(c.Query("s"))
|
||||
lang := c.Query("lang")
|
||||
eventId := c.Query("eventId")
|
||||
eci := c.Query("eci")
|
||||
|
||||
log.Printf("Received success response for viva payment %s", orderId)
|
||||
entry, err := vivaService.FetchByOrderId(orderId)
|
||||
if err != nil {
|
||||
log.Printf("Couldn't find payment info for viva payment %s", orderId)
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
entry.State = state.StateAccepted
|
||||
entry.ECI = eci
|
||||
entry.Lang = lang
|
||||
entry.EventId = eventId
|
||||
entry.TransactionId = transactionId
|
||||
|
||||
if err := vivaService.Update(entry); err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Viva payment %s received correctly, returning redirect", entry.OrderId)
|
||||
c.Redirect(http.StatusTemporaryRedirect, "/")
|
||||
})
|
||||
g.GET("error", func(c *gin.Context) {
|
||||
transactionId := uuid.MustParse(c.Query("t"))
|
||||
orderId := viva.OrderId(c.Query("s"))
|
||||
lang := c.Query("lang")
|
||||
eventId := c.Query("eventId")
|
||||
eci := c.Query("eci")
|
||||
|
||||
log.Printf("Received error response for viva payment %s", orderId)
|
||||
entry, err := vivaService.FetchByOrderId(orderId)
|
||||
if err != nil {
|
||||
log.Printf("Couldn't find payment info for viva payment %s", orderId)
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
entry.State = state.StateAccepted
|
||||
entry.ECI = eci
|
||||
entry.Lang = lang
|
||||
entry.EventId = eventId
|
||||
entry.TransactionId = transactionId
|
||||
|
||||
if err := vivaService.Update(entry); err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Viva payment %s received correctly, returning redirect", entry.OrderId)
|
||||
c.Redirect(http.StatusTemporaryRedirect, "/")
|
||||
})
|
||||
g.GET("info/:id", func(c *gin.Context) {
|
||||
id := uuid.MustParse(c.Param("id"))
|
||||
entry, err := vivaService.FetchById(id)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
c.HTML(200, "viva_info.gohtml", gin.H{"Entry": entry})
|
||||
})
|
||||
}
|
||||
|
||||
func setupStripeEndpoints(g *gin.RouterGroup, stripeService stripe2.Service) {
|
||||
g.GET("", func(c *gin.Context) {
|
||||
amount, err := strconv.ParseFloat(c.Query("amount"), 64)
|
||||
|
@ -181,6 +352,66 @@ func setupStripeEndpoints(g *gin.RouterGroup, stripeService stripe2.Service) {
|
|||
c.Redirect(http.StatusSeeOther, result.URL)
|
||||
})
|
||||
|
||||
g.POST("complete/:id", func(c *gin.Context) {
|
||||
id := uuid.MustParse(c.Param("id"))
|
||||
amount, err := strconv.ParseFloat(c.PostForm("amount"), 64)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
entry, err := stripeService.FetchById(id)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
if int64(amount*100) > entry.TotalAmount || int64(amount*100) < 1 {
|
||||
c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
if entry.State == state.StateInitialized || entry.State == state.StateAccepted {
|
||||
params := &stripe.PaymentIntentCaptureParams{
|
||||
AmountToCapture: stripe.Int64(int64(amount * 100)),
|
||||
}
|
||||
pi, err := paymentintent.Capture(entry.PaymentIntentId, params)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
log.Printf("received state on completion: %v", pi.Status)
|
||||
if pi.Status == stripe.PaymentIntentStatusSucceeded || pi.Status == stripe.PaymentIntentStatusProcessing {
|
||||
entry.TotalAmount = pi.Amount
|
||||
entry.State = state.StateCompleted
|
||||
stripeService.Update(entry)
|
||||
}
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusSeeOther, "/stripe/info/"+id.String())
|
||||
})
|
||||
|
||||
g.POST("cancel/:id", func(c *gin.Context) {
|
||||
id := uuid.MustParse(c.Param("id"))
|
||||
entry, err := stripeService.FetchById(id)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
if entry.State == state.StateInitialized || entry.State == state.StateAccepted {
|
||||
params := &stripe.PaymentIntentCancelParams{}
|
||||
pi, err := paymentintent.Cancel(entry.PaymentIntentId, params)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
log.Printf("received state on completion: %v", pi.Status)
|
||||
if pi.Status == stripe.PaymentIntentStatusCanceled {
|
||||
entry.State = state.StateCanceled
|
||||
stripeService.Update(entry)
|
||||
}
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusSeeOther, "/stripe/info/"+id.String())
|
||||
})
|
||||
|
||||
g.GET("success", func(c *gin.Context) {
|
||||
id := uuid.MustParse(c.Query("token"))
|
||||
|
||||
|
@ -241,7 +472,7 @@ func setupWsPayEndpoints(g *gin.RouterGroup, wspayService wspay.Service) {
|
|||
return
|
||||
}
|
||||
|
||||
entry, err := wspayService.CreateEntry(ShopId, int64(amount*100))
|
||||
entry, err := wspayService.CreateEntry(WsPayShopId, int64(amount*100))
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
|
@ -250,14 +481,14 @@ func setupWsPayEndpoints(g *gin.RouterGroup, wspayService wspay.Service) {
|
|||
log.Printf("Created initial wspay form (ammount=%d)", amount)
|
||||
|
||||
form := wspay.WsPayForm{
|
||||
ShopID: ShopId,
|
||||
ShopID: WsPayShopId,
|
||||
ShoppingCartID: entry.ShoppingCartID,
|
||||
Version: "2.0",
|
||||
TotalAmount: entry.TotalAmount,
|
||||
ReturnURL: BackendUrl + "/wspay/success",
|
||||
ReturnErrorURL: BackendUrl + "/wspay/error",
|
||||
CancelURL: BackendUrl + "/wspay/cancel",
|
||||
Signature: wspay.CalculateFormSignature(ShopId, ShopSecret, entry.ShoppingCartID, entry.TotalAmount),
|
||||
Signature: wspay.CalculateFormSignature(WsPayShopId, WsPayShopSecret, entry.ShoppingCartID, entry.TotalAmount),
|
||||
}
|
||||
|
||||
c.HTML(200, "wspay.gohtml", gin.H{"Action": wspay.AuthorisationForm, "Form": form})
|
||||
|
@ -276,7 +507,7 @@ func setupWsPayEndpoints(g *gin.RouterGroup, wspayService wspay.Service) {
|
|||
return
|
||||
}
|
||||
|
||||
if err := wspay.CompareFormReturnSignature(response.Signature, ShopId, ShopSecret, response.ShoppingCartID, response.Success, response.ApprovalCode); err != nil {
|
||||
if err := wspay.CompareFormReturnSignature(response.Signature, WsPayShopId, WsPayShopSecret, response.ShoppingCartID, response.Success, response.ApprovalCode); err != nil {
|
||||
log.Printf("Invalid signature for transaction %s", response.ShoppingCartID)
|
||||
c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
|
|
|
@ -75,6 +75,26 @@
|
|||
{{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>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -25,5 +25,6 @@
|
|||
<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>
|
|
@ -24,5 +24,19 @@
|
|||
<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>
|
|
@ -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