Added viva wallet support + completion on stripe

This commit is contained in:
Borna Rajković 2023-07-27 10:11:40 +02:00
parent cefc5314f2
commit 9440fa9778
9 changed files with 638 additions and 12 deletions

View File

@ -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
View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

26
viva/model.go Normal file
View File

@ -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"`
}

235
viva/service.go Normal file
View File

@ -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
}

44
viva/viva.go Normal file
View File

@ -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"`
}