WIP Code cleanup

This commit is contained in:
Borna Rajković 2023-07-27 22:46:37 +02:00
parent b4b0396b30
commit dcc9754d43
14 changed files with 880 additions and 902 deletions

54
database/model.go Normal file
View File

@ -0,0 +1,54 @@
package database
import (
"github.com/google/uuid"
"payment-poc/state"
"time"
)
type PaymentEntry struct {
Id uuid.UUID `db:"id"`
Created time.Time `db:"created"`
Modified *time.Time `db:"modified"`
Gateway state.PaymentGateway `db:"gateway"`
State state.PaymentState `db:"state"`
Lang *string `db:"lang"`
Error *string `db:"error"`
// paid amount
Amount *int64 `db:"amount"`
// preauthorized amount
TotalAmount int64 `db:"total_amount"`
// used for wspay and viva
ECI *string `db:"eci"`
// stripe field
PaymentIntentId *string `db:"payment_intent_id"`
// wspay field
ShoppingCardID *string `db:"shopping_card_it"`
STAN *string `db:"stan"`
Success *int `db:"success"`
ApprovalCode *string `db:"approval_code"`
// viva field
OrderId *OrderId `db:"order_id"`
TransactionId *uuid.UUID `db:"transaction_id"`
EventId *string `db:"event_id"`
}
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
}

57
database/provider.go Normal file
View File

@ -0,0 +1,57 @@
package database
import (
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"time"
)
type PaymentEntryProvider struct {
DB *sqlx.DB
}
func (p *PaymentEntryProvider) CreateEntry(entry PaymentEntry) (PaymentEntry, error) {
if entry.Id == uuid.Nil {
entry.Id = uuid.Must(uuid.NewRandom())
}
entry.Created = time.Now()
_, err := p.DB.Exec(`INSERT INTO "payment_entry" ("id", "created", "gateway", "state", "lang", "error", "amount", "total_amount", "eci", "payment_intent_id", "shopping_card_id", "stan", "success", "approval_code", "order_id", "transaction_id", "event_id")`,
&entry.Id, &entry.Created, &entry.Gateway, &entry.State, &entry.Lang, &entry.Error, &entry.Amount, &entry.TotalAmount, &entry.ECI, &entry.PaymentIntentId, &entry.ShoppingCardID, &entry.STAN, &entry.Success, &entry.ApprovalCode, &entry.OrderId, &entry.TransactionId, &entry.EventId,
)
if err != nil {
return PaymentEntry{}, err
}
return p.FetchById(entry.Id)
}
func (p *PaymentEntryProvider) UpdateEntry(entry PaymentEntry) (PaymentEntry, error) {
currentTime := time.Now()
entry.Modified = &currentTime
_, err := p.DB.Exec(`UPDATE "payment_entry" SET "modified" = $2, "state" = $3, "lang" = $4, "error" = $5, "amount" = $6, "eci" = $7, "payment_intent_id" = $8, "shopping_card_id" = $9, "stan" = $10, "success" = $11, "approval_code" = $12, "order_id" = $13, "transaction_id" = $14, "event_id" = $15 WHERE "id" = $1`,
&entry.Id, &entry.Modified, &entry.State, &entry.Lang, &entry.Error, &entry.Amount, &entry.ECI, &entry.PaymentIntentId, &entry.ShoppingCardID, &entry.STAN, &entry.Success, &entry.ApprovalCode, &entry.OrderId, &entry.TransactionId, &entry.EventId,
)
if err != nil {
return PaymentEntry{}, err
}
return p.FetchById(entry.Id)
}
func (p *PaymentEntryProvider) FetchById(id uuid.UUID) (PaymentEntry, error) {
entry := PaymentEntry{}
err := p.DB.Get(&entry, `SELECT * FROM "payment_entry" WHERE "id" = $1`, id)
return entry, err
}
func (p *PaymentEntryProvider) FetchAll() ([]PaymentEntry, error) {
var entries []PaymentEntry
err := p.DB.Select(&entries, `SELECT * FROM "payment_entry"`)
return entries, err
}
func (p *PaymentEntryProvider) FetchByOrderId(orderId OrderId) (PaymentEntry, error) {
entry := PaymentEntry{}
err := p.DB.Get(&entry, `SELECT * FROM "payment_entry" WHERE "order_id" = $1`, orderId)
return entry, err
}

View File

@ -1 +1,65 @@
package prod
CREATE TABLE IF NOT EXISTS "wspay"
(
"id" uuid NOT NULL,
"shop_id" varchar(128) NOT NULL,
"shopping_card_id" varchar(128) NOT NULL,
"total_amount" int NOT NULL,
"lang" varchar(128) DEFAULT '',
"customer_first_name" varchar(128) DEFAULT '',
"customer_last_name" varchar(128) DEFAULT '',
"customer_address" varchar(128) DEFAULT '',
"customer_city" varchar(128) DEFAULT '',
"customer_zip" varchar(128) DEFAULT '',
"customer_country" varchar(128) DEFAULT '',
"customer_phone" varchar(128) DEFAULT '',
"payment_plan" varchar(128) DEFAULT '',
"credit_card_name" varchar(128) DEFAULT '',
"credit_card_number" varchar(128) DEFAULT '',
"payment_method" varchar(128) DEFAULT '',
"currency_code" int DEFAULT 0,
"date_time" timestamp DEFAULT current_timestamp,
"eci" varchar(256) DEFAULT '',
"stan" varchar(256) DEFAULT '',
"success" int DEFAULT 0,
"approval_code" varchar(256) DEFAULT '',
"error_message" varchar(256) DEFAULT '',
"error_codes" varchar(256) DEFAULT '',
"payment_state" varchar(256) DEFAULT '',
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)
);

841
main.go
View File

@ -1,21 +1,18 @@
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"
"os"
"payment-poc/database"
"payment-poc/migration"
"payment-poc/state"
stripe2 "payment-poc/stripe"
@ -29,32 +26,15 @@ import (
//go:embed db/dev/*.sql
var devMigrations embed.FS
var BackendUrl string
var WsPayShopId string
var WsPayShopSecret string
var VivaMerchantId string
var VivaApiKey string
var VivaSourceCode string
var VivaClientId string
var VivaClientSecret string
type PaymentProvider interface {
CreatePaymentUrl(amount int64) (string, error)
CompleteTransaction(entry database.PaymentEntry, amount int64) (database.PaymentEntry, error)
CancelTransaction(entry database.PaymentEntry) (database.PaymentEntry, error)
}
func init() {
godotenv.Load()
BackendUrl = envMustExist("BACKEND_URL")
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)
}
@ -69,40 +49,16 @@ func main() {
}
g := gin.Default()
g.Use(gin.BasicAuth(getAccounts()))
if !hasProfile("no-auth") {
g.Use(gin.BasicAuth(getAccounts()))
}
g.SetFuncMap(template.FuncMap{
"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:
return "Greška"
case state.StateInitialized:
return "Inicijalna izrada"
case state.StateCanceledInitialization:
return "Otkazano tijekom izrade"
case state.StateCompleted:
return "Završeno"
}
return "nepoznato stanje '" + string(stt) + "'"
},
"omitempty": func(value string) string {
if value == "" {
return "-"
}
return value
},
"formatCurrency": formatCurrency,
"decimalCurrency": decimalCurrency,
"formatState": formatState,
"omitempty": omitempty,
})
g.NoRoute(func(c *gin.Context) {
@ -112,46 +68,151 @@ func main() {
c.JSON(http.StatusMethodNotAllowed, gin.H{"message": "no action on given method", "created": time.Now()})
})
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,
}
backendUrl := envMustExist("BACKEND_URL")
paymentGateways := map[state.PaymentGateway]PaymentProvider{}
entryProvider := database.PaymentEntryProvider{DB: client}
g.LoadHTMLGlob("./templates/*.gohtml")
g.GET("/", func(c *gin.Context) {
wspayEntries, _ := wspayService.FetchAll()
stripeEntries, _ := stripeService.FetchAll()
vivaEntries, _ := vivaService.FetchAll()
c.HTML(200, "index.gohtml", gin.H{"WsPay": wspayEntries, "Stripe": stripeEntries, "Viva": vivaEntries})
})
if hasProfile(string(state.GatewayWsPay)) {
wspayService := wspay.Service{
Provider: &entryProvider,
ShopId: envMustExist("WSPAY_SHOP_ID"),
ShopSecret: envMustExist("WSPAY_SHOP_SECRET"),
BackendUrl: backendUrl,
}
setupWsPayEndpoints(g.Group("wspay"), &wspayService)
paymentGateways[state.GatewayWsPay] = &wspayService
}
if hasProfile(string(state.GatewayStripe)) {
stripeService := stripe2.Service{
Provider: &entryProvider,
ApiKey: envMustExist("STRIPE_KEY"),
BackendUrl: backendUrl,
}
setupStripeEndpoints(g.Group("stripe"), &stripeService)
paymentGateways[state.GatewayStripe] = &stripeService
stripe.Key = envMustExist("STRIPE_KEY")
}
if hasProfile(string(state.GatewayVivaWallet)) {
vivaService := viva.Service{
Provider: &entryProvider,
ClientId: envMustExist("VIVA_WALLET_CLIENT_ID"),
ClientSecret: envMustExist("VIVA_WALLET_CLIENT_SECRET"),
SourceCode: envMustExist("VIVA_WALLET_SOURCE_CODE"),
MerchantId: envMustExist("VIVA_WALLET_MERCHANT_ID"),
ApiKey: envMustExist("VIVA_WALLET_API_KEY"),
}
setupVivaEndpoints(g.Group("viva"), &vivaService)
paymentGateways[state.GatewayVivaWallet] = &vivaService
}
g.GET("/", func(c *gin.Context) {
entries, _ := entryProvider.FetchAll()
c.HTML(200, "index.gohtml", gin.H{"Entries": entries})
})
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})
var gateways []state.PaymentGateway
for key := range paymentGateways {
gateways = append(gateways, key)
}
c.HTML(200, "methods.gohtml", gin.H{"Amount": amount, "Gateways": gateways})
})
g.GET("/:gateway", func(c *gin.Context) {
gateway, err := fetchGateway(c.Param("gateway"))
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
if paymentGateway, contains := paymentGateways[gateway]; contains {
amount, err := fetchAmount(c.Query("amount"))
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
if url, err := paymentGateway.CreatePaymentUrl(amount); err == nil {
c.Redirect(http.StatusSeeOther, url)
} else {
c.AbortWithError(http.StatusBadRequest, err)
return
}
} else {
c.AbortWithError(http.StatusBadRequest, errors.New("unsupported payment gateway: "+string(gateway)))
return
}
})
g.POST("/entries/:id/complete", func(c *gin.Context) {
id := uuid.MustParse(c.Param("id"))
entry, err := entryProvider.FetchById(id)
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
if paymentGateway, ok := paymentGateways[entry.Gateway]; ok {
amount, err := fetchAmount(c.PostForm("amount"))
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
entry, err = paymentGateway.CompleteTransaction(entry, amount)
if err != nil {
entryProvider.UpdateEntry(entry)
c.Redirect(http.StatusSeeOther, "/entries/"+id.String())
} else {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
} else {
if err != nil {
c.AbortWithError(http.StatusInternalServerError, errors.New("payment gateway not supported: "+string(entry.Gateway)))
return
}
}
})
g.POST("/entries/:id/cancel", func(c *gin.Context) {
id := uuid.MustParse(c.Param("id"))
entry, err := entryProvider.FetchById(id)
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
if paymentGateway, ok := paymentGateways[entry.Gateway]; ok {
entry, err = paymentGateway.CancelTransaction(entry)
if err != nil {
entryProvider.UpdateEntry(entry)
c.Redirect(http.StatusSeeOther, "/entries/"+id.String())
} else {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
} else {
if err != nil {
c.AbortWithError(http.StatusInternalServerError, errors.New("payment gateway not supported: "+string(entry.Gateway)))
return
}
}
})
setupWsPayEndpoints(g.Group("wspay"), wspayService)
setupStripeEndpoints(g.Group("stripe"), stripeService)
setupVivaEndpoints(g.Group("viva"), vivaService)
log.Fatal(http.ListenAndServe(":5281", g))
}
func fetchGateway(gateway string) (state.PaymentGateway, error) {
switch gateway {
case string(state.GatewayWsPay):
return state.GatewayWsPay, nil
case string(state.GatewayStripe):
return state.GatewayStripe, nil
case string(state.GatewayVivaWallet):
return state.GatewayVivaWallet, nil
}
return "", errors.New("unknown gateway: " + gateway)
}
func getAccounts() gin.Accounts {
auth := strings.Split(envMustExist("AUTH"), ":")
return gin.Accounts{auth[0]: auth[1]}
@ -165,595 +226,137 @@ 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())
})
func fetchAmount(amount string) (int64, error) {
if amount, err := strconv.ParseFloat(amount, 64); err == nil {
return int64(amount * 100), nil
} else {
return 0, err
}
}
func setupVivaEndpoints(g *gin.RouterGroup, vivaService *viva.Service) {
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)
url, err := vivaService.HandleResponse(c, state.StateAccepted)
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, "/")
c.Redirect(http.StatusSeeOther, url)
})
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)
url, err := vivaService.HandleResponse(c, state.StateError)
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})
c.Redirect(http.StatusSeeOther, url)
})
}
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: &currency,
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())
})
func setupStripeEndpoints(g *gin.RouterGroup, stripeService *stripe2.Service) {
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)
url, err := stripeService.HandleResponse(c, state.StateAccepted)
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, "/")
c.Redirect(http.StatusSeeOther, url)
})
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)
url, err := stripeService.HandleResponse(c, state.StateError)
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})
c.Redirect(http.StatusSeeOther, url)
})
}
func setupWsPayEndpoints(g *gin.RouterGroup, wspayService wspay.Service) {
g.GET("", func(c *gin.Context) {
amount, err := strconv.ParseFloat(c.Query("amount"), 64)
func setupWsPayEndpoints(g *gin.RouterGroup, wspayService *wspay.Service) {
g.GET("/initialize/:id", func(c *gin.Context) {
entry, err := wspayService.Provider.FetchById(uuid.MustParse(c.Param("id")))
if err != nil {
c.AbortWithError(http.StatusNotFound, err)
return
}
if entry.State != state.StateInitialized {
c.AbortWithError(http.StatusBadRequest, err)
return
}
entry, err := wspayService.CreateEntry(WsPayShopId, int64(amount*100))
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
log.Printf("Created initial wspay form (ammount=%d)", amount)
form := wspay.WsPayForm{
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(WsPayShopId, WsPayShopSecret, entry.ShoppingCartID, entry.TotalAmount),
}
form := wspayService.InitializePayment(entry)
c.HTML(200, "wspay.gohtml", gin.H{"Action": wspay.AuthorisationForm, "Form": form})
})
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)
return
}
log.Printf("Received success response for transaction %s", response.ShoppingCartID)
entry, err := wspayService.FetchByShoppingCartID(response.ShoppingCartID)
url, err := wspayService.HandleSuccessResponse(c)
if err != nil {
log.Printf("Couldn't find payment info for transaction %s", response.ShoppingCartID)
c.AbortWithError(http.StatusInternalServerError, err)
return
}
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
}
entry.Lang = response.Lang
entry.CustomerFirstName = response.CustomerFirstName
entry.CustomerLastName = response.CustomerSurname
entry.CustomerAddress = response.CustomerAddress
entry.CustomerCity = response.CustomerCity
entry.CustomerZIP = response.CustomerZIP
entry.CustomerCountry = response.CustomerCountry
entry.CustomerPhone = response.CustomerPhone
entry.PaymentPlan = response.PaymentPlan
entry.CreditCardNumber = response.CreditCardNumber
entry.DateTime = parseDateTime(response.DateTime)
entry.ECI = response.ECI
entry.STAN = response.STAN
entry.Success = response.Success
entry.ApprovalCode = response.ApprovalCode
entry.ErrorMessage = response.ErrorMessage
entry.State = state.StateAccepted
if err := wspayService.Update(entry); err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
if c.Query("iframe") != "" {
log.Printf("Transaction %s received correctly, returning iframe response", response.ShoppingCartID)
c.HTML(200, "iframe_handler.gohtml", gin.H{})
} else {
log.Printf("Transaction %s received correctly, returning redirect", response.ShoppingCartID)
c.Redirect(http.StatusTemporaryRedirect, "/")
}
c.Redirect(http.StatusSeeOther, url)
})
g.GET("error", func(c *gin.Context) {
response := wspay.WsPayFormError{}
if err := c.ShouldBind(&response); err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
log.Printf("Received error response for transaction %s", response.ShoppingCartID)
entry, err := wspayService.FetchByShoppingCartID(response.ShoppingCartID)
url, err := wspayService.HandleErrorResponse(c, state.StateError)
if err != nil {
log.Printf("Couldn't find payment info for transaction %s", response.ShoppingCartID)
c.AbortWithError(http.StatusInternalServerError, err)
return
}
entry.Lang = response.Lang
entry.CustomerFirstName = response.CustomerFirstName
entry.CustomerLastName = response.CustomerSurname
entry.CustomerAddress = response.CustomerAddress
entry.CustomerCity = response.CustomerCity
entry.CustomerZIP = response.CustomerZIP
entry.CustomerCountry = response.CustomerCountry
entry.CustomerPhone = response.CustomerPhone
entry.PaymentPlan = response.PaymentPlan
entry.DateTime = parseDateTime(response.DateTime)
entry.ECI = response.ECI
entry.Success = response.Success
entry.ApprovalCode = response.ApprovalCode
entry.ErrorMessage = response.ErrorMessage
entry.ErrorCodes = response.ErrorCodes
entry.State = state.StateError
if err := wspayService.Update(entry); err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
if c.Query("iframe") != "" {
log.Printf("Transaction %s received correctly, returning iframe response", response.ShoppingCartID)
c.HTML(200, "iframe_handler.gohtml", gin.H{})
} else {
log.Printf("Transaction %s received correctly, returning redirect", response.ShoppingCartID)
c.Redirect(http.StatusTemporaryRedirect, "/")
}
})
g.GET("info/: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
}
c.HTML(200, "wspay_info.gohtml", gin.H{"Entry": entry})
c.Redirect(http.StatusSeeOther, url)
})
g.GET("cancel", func(c *gin.Context) {
response := wspay.WsPayFormCancel{}
if err := c.ShouldBind(&response); err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
log.Printf("Received error response for transaction %s", response.ShoppingCartID)
entry, err := wspayService.FetchByShoppingCartID(response.ShoppingCartID)
url, err := wspayService.HandleErrorResponse(c, state.StateCanceled)
if err != nil {
log.Printf("Couldn't find payment info for transaction %s", response.ShoppingCartID)
c.AbortWithError(http.StatusInternalServerError, err)
return
}
entry.State = state.StateCanceledInitialization
if err := wspayService.Update(entry); err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
if c.Query("iframe") != "" {
log.Printf("Transaction %s received correctly, returning iframe response", response.ShoppingCartID)
c.HTML(200, "iframe_handler.gohtml", gin.H{})
} else {
log.Printf("Transaction %s received correctly, returning redirect", response.ShoppingCartID)
c.Redirect(http.StatusTemporaryRedirect, "/")
}
c.Redirect(http.StatusSeeOther, url)
})
}
func hasProfile(profile string) bool {
profiles := strings.Split(os.Getenv("PROFILE"), ",")
for _, p := range profiles {
if profile == strings.TrimSpace(p) {
return true
}
}
return false
}
func formatState(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:
return "Greška"
case state.StateInitialized:
return "Inicijalna izrada"
case state.StateCanceledInitialization:
return "Otkazano tijekom izrade"
case state.StateCompleted:
return "Završeno"
}
return "nepoznato stanje '" + string(stt) + "'"
}
func formatCurrency(current int64) string {
return fmt.Sprintf("%d,%02d", current/100, current%100)
}
func decimalCurrency(current int64) string {
return fmt.Sprintf("%d,%02d", current/100, current%100)
}
func omitempty(value string) string {
if value == "" {
return "-"
}
return value
}

View File

@ -16,3 +16,11 @@ const (
StateVoided PaymentState = "voided"
StateCanceled PaymentState = "canceled"
)
type PaymentGateway string
const (
GatewayWsPay PaymentGateway = "wspay"
GatewayStripe PaymentGateway = "stripe"
GatewayVivaWallet PaymentGateway = "viva-wallet"
)

View File

@ -1,46 +1,114 @@
package stripe
import (
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"github.com/stripe/stripe-go/v72"
"github.com/stripe/stripe-go/v72/checkout/session"
"github.com/stripe/stripe-go/v72/paymentintent"
"log"
"payment-poc/database"
"payment-poc/state"
)
type Service struct {
DB *sqlx.DB
Provider *database.PaymentEntryProvider
ApiKey string
BackendUrl string
}
func (s *Service) CreateEntry(totalAmount int64) (StripeDb, error) {
id := uuid.Must(uuid.NewRandom())
entry := StripeDb{
Id: id,
TotalAmount: totalAmount,
func (s *Service) CreatePaymentUrl(amount int64) (url string, err error) {
entry, err := s.Provider.CreateEntry(database.PaymentEntry{
Gateway: state.GatewayVivaWallet,
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,
)
TotalAmount: amount,
})
if err != nil {
return StripeDb{}, err
return "", err
}
return s.FetchById(id)
entry, url, err = s.InitializePayment(entry)
if err != nil {
return "", err
}
entry, err = s.Provider.UpdateEntry(entry)
if err != nil {
return "", err
}
return url, nil
}
func (s *Service) FetchAll() ([]StripeDb, error) {
var entries []StripeDb
err := s.DB.Select(&entries, `SELECT * FROM "stripe"`)
return entries, err
func (s *Service) InitializePayment(entry database.PaymentEntry) (database.PaymentEntry, string, error) {
currency := string(stripe.CurrencyEUR)
productName := "Example product"
productDescription := "Simple example product"
params := &stripe.CheckoutSessionParams{
LineItems: []*stripe.CheckoutSessionLineItemParams{
{
PriceData: &stripe.CheckoutSessionLineItemPriceDataParams{
Currency: &currency,
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(s.BackendUrl + "/stripe/success?token=" + entry.Id.String()),
CancelURL: stripe.String(s.BackendUrl + "/stripe/cancel?token=" + entry.Id.String()),
}
result, err := session.New(params)
if err != nil {
return database.PaymentEntry{}, "", err
}
entry.PaymentIntentId = &result.PaymentIntent.ID
return entry, result.URL, nil
}
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) CompleteTransaction(entry database.PaymentEntry, amount int64) (database.PaymentEntry, error) {
params := &stripe.PaymentIntentCaptureParams{
AmountToCapture: stripe.Int64(amount),
}
pi, err := paymentintent.Capture(*entry.PaymentIntentId, params)
if err != nil {
return database.PaymentEntry{}, err
}
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
}
return entry, nil
}
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
func (s *Service) CancelTransaction(entry database.PaymentEntry) (database.PaymentEntry, error) {
params := &stripe.PaymentIntentCancelParams{}
pi, err := paymentintent.Cancel(*entry.PaymentIntentId, params)
if err != nil {
return database.PaymentEntry{}, err
}
log.Printf("received state on completion: %v", pi.Status)
if pi.Status == stripe.PaymentIntentStatusCanceled {
entry.State = state.StateCanceled
}
return entry, nil
}
func (s *Service) HandleResponse(c *gin.Context, paymentState state.PaymentState) (string, error) {
id := uuid.MustParse(c.Query("token"))
entry, err := s.Provider.FetchById(id)
if err != nil {
return "", err
}
entry.State = paymentState
s.Provider.UpdateEntry(entry)
return "/entries/" + entry.Id.String(), nil
}

View File

@ -30,7 +30,7 @@
<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}}">
<input class="form-control" id="amount" required name="amount" type="number" value="{{decimalCurrency .Entry.TotalAmount}}" step="0.01" min="0.01" max="{{decimalCurrency .Entry.TotalAmount}}">
</div>
<button class="btn btn-primary" type="submit">Završi transakciju</button>
</form>

View File

@ -32,7 +32,7 @@
<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}}">
<input class="form-control" id="amount" required name="amount" type="number" value="{{decimalCurrency .Entry.TotalAmount}}" step="0.01" min="0.01" max="{{decimalCurrency .Entry.TotalAmount}}">
</div>
<button class="btn btn-primary" type="submit">Završi transakciju</button>
</form>

View File

@ -50,7 +50,7 @@
<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}}">
<input class="form-control" id="amount" required name="amount" type="number" value="{{decimalCurrency .Entry.TotalAmount}}" step="0.01" min="0.01" max="{{decimalCurrency .Entry.TotalAmount}}">
</div>
<button class="btn btn-primary" type="submit">Završi transakciju</button>
</form>

View File

@ -1,26 +0,0 @@
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"`
}

View File

@ -5,43 +5,58 @@ import (
"encoding/base64"
"encoding/json"
"errors"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"io"
"log"
"net/http"
"net/url"
"payment-poc/database"
"payment-poc/state"
"strconv"
"strings"
"time"
)
type Service struct {
DB *sqlx.DB
Token string
Expiration time.Time
Provider *database.PaymentEntryProvider
ClientId string
ClientSecret string
SourceCode string
MerchantId string
ApiKey string
token string
expiration time.Time
}
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()
func (s *Service) CreatePaymentUrl(amount int64) (url string, err error) {
entry, err := s.Provider.CreateEntry(database.PaymentEntry{
Gateway: state.GatewayVivaWallet,
State: state.StateInitialized,
TotalAmount: amount,
})
if err != nil {
return VivaDb{}, err
return "", err
}
orderRequest := VivaOrderRequest{
entry, err = s.InitializePayment(entry)
if err != nil {
return "", err
}
entry, err = s.Provider.UpdateEntry(entry)
if err != nil {
return "", err
}
return "https://demo.vivapayments.com/web/checkout?ref=" + string(*entry.OrderId), nil
}
func (s *Service) InitializePayment(entry database.PaymentEntry) (database.PaymentEntry, error) {
token, err := s.oAuthToken()
if err != nil {
return database.PaymentEntry{}, err
}
request := OrderRequest{
Amount: entry.TotalAmount,
Description: "Example payment",
MerchantDescription: "Example payment",
@ -50,186 +65,173 @@ func (s *Service) CreatePaymentOrder(entry VivaDb) (VivaDb, error) {
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,
httpResponse, err := createRequest(
"POST",
"https://demo-api.vivapayments.com/checkout/v2/orders",
map[string]string{"authorization": "Bearer " + token, "content-type": "application/json"},
toJson(request),
)
if err != nil {
return VivaDb{}, err
return database.PaymentEntry{}, 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()))
var response OrderResponse
err = readResponse(httpResponse, &response)
if err != nil {
return "", err
return database.PaymentEntry{}, 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
entry.OrderId = &response.OrderId
return entry, nil
}
func (s *Service) CompleteTransaction(entry VivaDb, amount int64) (VivaDb, error) {
completionRequest := VivaTransactionCompleteRequest{
func (s *Service) CompleteTransaction(entry database.PaymentEntry, amount int64) (database.PaymentEntry, error) {
completionRequest := TransactionCompleteRequest{
Amount: amount,
CustomerDescription: "Example transaction",
}
content, err := json.Marshal(&completionRequest)
httpResponse, err := createRequest(
"POST",
"https://demo.vivapayments.com/api/transactions/"+entry.TransactionId.String(),
map[string]string{"authorization": "Bearer " + s.basicAuth(),
"content-type": "application/json",
},
toJson(completionRequest),
)
if err != nil {
return VivaDb{}, err
return database.PaymentEntry{}, 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)
var response TransactionResponse
err = readResponse(httpResponse, &response)
if err != nil {
return VivaDb{}, err
return database.PaymentEntry{}, 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)
}
}
if response.StatusId == "F" {
paidAmount := int64(response.Amount * 100)
entry.Amount = &paidAmount
entry.State = state.StateCompleted
} else {
return VivaDb{}, errors.New("received wrong status, expected 200 received " + strconv.FormatInt(int64(response.StatusCode), 10))
return database.PaymentEntry{}, errors.New("received invalid status = " + response.StatusId)
}
return entry, nil
}
func (s *Service) BasicAuth() string {
func (s *Service) CancelTransaction(entry database.PaymentEntry) (database.PaymentEntry, error) {
httpResponse, err := createRequest(
"DELETE",
"https://demo.vivapayments.com/api/transactions/"+entry.TransactionId.String(),
map[string]string{"authorization": "Bearer " + s.basicAuth(),
"content-type": "application/json",
},
nil,
)
if err != nil {
return database.PaymentEntry{}, err
}
var response TransactionResponse
err = readResponse(httpResponse, &response)
if err != nil {
return database.PaymentEntry{}, err
}
if response.StatusId == "F" {
paidAmount := int64(0)
entry.Amount = &paidAmount
entry.State = state.StateVoided
} else {
return database.PaymentEntry{}, errors.New("received invalid status = " + response.StatusId)
}
return entry, nil
}
func (s *Service) oAuthToken() (string, error) {
if s.token != "" && s.expiration.After(time.Now()) {
return s.token, nil
}
return s.fetchOAuthToken()
}
func readResponse[T any](httpResponse *http.Response, response T) error {
if httpResponse.StatusCode == http.StatusOK {
content, err := io.ReadAll(httpResponse.Body)
if err != nil {
return err
}
return json.Unmarshal(content, response)
} else {
return errors.New("received wrong status, expected 200 received " + strconv.FormatInt(int64(httpResponse.StatusCode), 10))
}
}
func createRequest(method string, url string, headers map[string]string, content []byte) (*http.Response, error) {
request, err := http.NewRequest(method, url, bytes.NewReader(content))
if err != nil {
return nil, err
}
for key, value := range headers {
request.Header.Add(key, value)
}
return http.DefaultClient.Do(request)
}
func toJson[T any](request T) []byte {
response, err := json.Marshal(request)
if err != nil {
panic(err)
}
return response
}
func (s *Service) fetchOAuthToken() (string, error) {
form := url.Values{
"grant_type": []string{"client_credentials"},
}
httpResponse, err := createRequest(
"POST",
"https://demo-accounts.vivapayments.com/connect/token",
map[string]string{"content-type": "application/x-www-form-urlencoded", "authorization": "Basic " + base64.StdEncoding.EncodeToString([]byte(s.ClientId+":"+s.ClientSecret))},
[]byte(form.Encode()),
)
var response OAuthResponse
err = readResponse(httpResponse, &response)
if err != nil {
return "", err
}
s.token = response.AccessToken
s.expiration = time.Now().Add(time.Duration(response.ExpiresIn) * time.Second)
return s.token, 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())
func (s *Service) HandleResponse(c *gin.Context, expectedState state.PaymentState) (string, error) {
transactionId := uuid.MustParse(c.Query("t"))
orderId := database.OrderId(c.Query("s"))
lang := c.Query("lang")
eventId := c.Query("eventId")
eci := c.Query("eci")
response, err := http.DefaultClient.Do(request)
log.Printf("Received error response for viva payment %s", orderId)
entry, err := s.Provider.FetchByOrderId(orderId)
if err != nil {
return VivaDb{}, err
log.Printf("Couldn't find payment info for viva payment %s", orderId)
return "", 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))
entry.State = expectedState
entry.ECI = &eci
entry.Lang = &lang
entry.EventId = &eventId
entry.TransactionId = &transactionId
if _, err := s.Provider.UpdateEntry(entry); err != nil {
return "", err
}
return entry, nil
log.Printf("Viva payment %s received correctly, returning redirect", orderId)
return "/entries/" + entry.Id.String(), nil
}

View File

@ -1,17 +1,8 @@
package viva
type OrderId string
import "payment-poc/database"
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 {
type OrderRequest struct {
Amount int64 `json:"amount"`
Description string `json:"customerTrns"`
MerchantDescription string `json:"merchantTrns"`
@ -20,21 +11,21 @@ type VivaOrderRequest struct {
Source string `json:"sourceCode"`
}
type VivaOrderResponse struct {
OrderId OrderId `json:"orderCode"`
type OrderResponse struct {
OrderId database.OrderId `json:"orderCode"`
}
type VivaOAuthResponse struct {
type OAuthResponse struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
}
type VivaTransactionCompleteRequest struct {
type TransactionCompleteRequest struct {
Amount int64 `json:"amount"`
CustomerDescription string `json:"customerTrns"`
}
type VivaTransactionResponse struct {
type TransactionResponse struct {
Amount float64 `json:"Amount"`
StatusId string `json:"StatusId"`
ErrorCode int64 `json:"ErrorCode"`

View File

@ -1,60 +1,186 @@
package wspay
import (
"bytes"
"crypto/sha512"
"encoding/hex"
"encoding/json"
"errors"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"io"
"net/http"
"payment-poc/database"
"payment-poc/state"
"strconv"
)
type Service struct {
DB *sqlx.DB
Provider *database.PaymentEntryProvider
ShopId string
ShopSecret string
BackendUrl string
}
func (s *Service) CreateEntry(shopId string, totalAmount int64) (WsPayDb, error) {
id := uuid.Must(uuid.NewRandom())
entry := WsPayDb{
Id: id,
ShopID: shopId,
ShoppingCartID: id.String(),
TotalAmount: totalAmount,
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,
)
func (s *Service) CreatePaymentUrl(amount int64) (string, error) {
entry, err := s.Provider.CreateEntry(database.PaymentEntry{
Gateway: state.GatewayVivaWallet,
State: state.StateInitialized,
TotalAmount: amount,
})
if err != nil {
return WsPayDb{}, err
return "", err
}
return s.FetchById(id)
return "/wspay/initialize/" + entry.Id.String(), nil
}
func (s *Service) FetchAll() ([]WsPayDb, error) {
var entries []WsPayDb
err := s.DB.Select(&entries, `SELECT * FROM "wspay"`)
return entries, err
func (s *Service) CompleteTransaction(entry database.PaymentEntry, amount int64) (database.PaymentEntry, error) {
if entry.State == state.StateAccepted {
var request = CompletionRequest{
Version: "2.0",
WsPayOrderId: entry.Id.String(),
ShopId: s.ShopId,
ApprovalCode: *entry.ApprovalCode,
STAN: *entry.STAN,
Amount: amount,
Signature: CalculateCompletionSignature(s.ShopId, s.ShopSecret, entry.Id.String(), *entry.STAN, *entry.ApprovalCode, amount),
}
httpResponse, err := createRequest(
"POST",
"https://test.wspay.biz/api/services/completion",
map[string]string{"content-type": "application/json"},
toJson(request),
)
if err != nil {
return database.PaymentEntry{}, err
}
var response CompletionResponse
err = readResponse(httpResponse, &response)
if err != nil {
return database.PaymentEntry{}, err
}
if CompareCompletionReturnSignature(response.Signature, s.ShopId, s.ShopSecret, entry.Id.String(), *entry.STAN, response.ActionSuccess, response.ApprovalCode) != nil {
entry.Amount = &amount
entry.State = state.StateCompleted
} else {
return database.PaymentEntry{}, errors.New("invalid signature")
}
return entry, nil
} else {
return database.PaymentEntry{}, errors.New("payment is in invalid state")
}
}
func (s *Service) FetchById(id uuid.UUID) (WsPayDb, error) {
entry := WsPayDb{}
err := s.DB.Get(&entry, `SELECT * FROM "wspay" WHERE "id" = $1`, id)
return entry, err
func (s *Service) CancelTransaction(entry database.PaymentEntry) (database.PaymentEntry, error) {
if entry.State == state.StateAccepted {
var request = CompletionRequest{
Version: "2.0",
WsPayOrderId: entry.Id.String(),
ShopId: s.ShopId,
ApprovalCode: *entry.ApprovalCode,
STAN: *entry.STAN,
Amount: entry.TotalAmount,
Signature: CalculateCompletionSignature(s.ShopId, s.ShopSecret, entry.Id.String(), *entry.STAN, *entry.ApprovalCode, entry.TotalAmount),
}
httpResponse, err := createRequest(
"POST",
"https://test.wspay.biz/api/services/void",
map[string]string{"content-type": "application/json"},
toJson(request),
)
if err != nil {
return database.PaymentEntry{}, err
}
var response CompletionResponse
err = readResponse(httpResponse, &response)
if err != nil {
return database.PaymentEntry{}, err
}
if CompareCompletionReturnSignature(response.Signature, s.ShopId, s.ShopSecret, entry.Id.String(), *entry.STAN, response.ActionSuccess, response.ApprovalCode) != nil {
entry.State = state.StateCanceled
} else {
return database.PaymentEntry{}, errors.New("invalid signature")
}
return entry, nil
} else {
return database.PaymentEntry{}, errors.New("payment is in invalid state")
}
}
func (s *Service) FetchByShoppingCartID(id string) (WsPayDb, error) {
entry := WsPayDb{}
err := s.DB.Get(&entry, `SELECT * FROM "wspay" WHERE "shopping_card_id" = $1`, id)
return entry, err
func (s *Service) InitializePayment(entry database.PaymentEntry) Form {
form := Form{
ShopID: s.ShopId,
ShoppingCartID: entry.Id.String(),
Version: "2.0",
TotalAmount: entry.TotalAmount,
ReturnURL: s.BackendUrl + "/wspay/success",
ReturnErrorURL: s.BackendUrl + "/wspay/error",
CancelURL: s.BackendUrl + "/wspay/cancel",
Signature: CalculateFormSignature(s.ShopId, s.ShopSecret, entry.Id.String(), entry.TotalAmount),
}
return form
}
func (s *Service) Update(entry WsPayDb) error {
_, err := s.DB.Exec(`UPDATE "wspay" set "lang" = $2, "customer_first_name" = $3, "customer_last_name" = $4, "customer_address" = $5, "customer_city" = $6, "customer_zip" = $7, "customer_country" = $8, "customer_phone" = $9, "payment_plan" = $10, "credit_card_name" = $11, "credit_card_number" = $12, "payment_method" = $13, "currency_code" = $14, "date_time" = $15, "eci" = $16, "stan" = $17, "success" = $18, "approval_code" = $19, "error_message" = $20, "error_codes" = $21, "payment_state" = $22 WHERE "id" = $1`,
&entry.Id, &entry.Lang, &entry.CustomerFirstName, &entry.CustomerLastName, &entry.CustomerAddress, &entry.CustomerCity, &entry.CustomerZIP, &entry.CustomerCountry, &entry.CustomerPhone, &entry.PaymentPlan, &entry.CreditCardName, &entry.CreditCardNumber, &entry.PaymentMethod, &entry.CurrencyCode, &entry.DateTime, &entry.ECI, &entry.STAN, &entry.Success, &entry.ApprovalCode, &entry.ErrorMessage, &entry.ErrorCodes, &entry.State,
)
return err
func (s *Service) HandleSuccessResponse(c *gin.Context) (string, error) {
response := FormReturn{}
if err := c.ShouldBind(&response); err != nil {
return "", err
}
entry, err := s.Provider.FetchById(uuid.MustParse(response.ShoppingCartID))
if err != nil {
return "", err
}
if err := CompareFormReturnSignature(response.Signature, s.ShopId, s.ShopSecret, response.ShoppingCartID, response.Success, response.ApprovalCode); err != nil {
return "", err
}
entry.Lang = &response.Lang
entry.ECI = &response.ECI
entry.STAN = &response.STAN
entry.Success = &response.Success
entry.ApprovalCode = &response.ApprovalCode
entry.State = state.StateAccepted
if _, err := s.Provider.UpdateEntry(entry); err != nil {
return "", err
}
return "/entries/" + entry.Id.String(), nil
}
func (s *Service) HandleErrorResponse(c *gin.Context, paymentState state.PaymentState) (string, error) {
response := FormError{}
if err := c.ShouldBind(&response); err != nil {
return "", err
}
entry, err := s.Provider.FetchById(uuid.MustParse(response.ShoppingCartID))
if err != nil {
return "", err
}
if err := CompareFormReturnSignature(response.Signature, s.ShopId, s.ShopSecret, response.ShoppingCartID, response.Success, response.ApprovalCode); err != nil {
return "", err
}
entry.Lang = &response.Lang
entry.ECI = &response.ECI
entry.Success = &response.Success
entry.ApprovalCode = &response.ApprovalCode
entry.Error = &response.ErrorMessage
entry.State = paymentState
if _, err := s.Provider.UpdateEntry(entry); err != nil {
return "", err
}
return "/entries/" + entry.Id.String(), nil
}
func CalculateFormSignature(shopId string, secret string, cartId string, amount int64) string {
@ -141,3 +267,34 @@ func CompareCompletionReturnSignature(signature string, shopId string, secret st
return errors.New("signature mismatch")
}
}
func readResponse[T any](httpResponse *http.Response, response T) error {
if httpResponse.StatusCode == http.StatusOK {
content, err := io.ReadAll(httpResponse.Body)
if err != nil {
return err
}
return json.Unmarshal(content, response)
} else {
return errors.New("received wrong status, expected 200 received " + strconv.FormatInt(int64(httpResponse.StatusCode), 10))
}
}
func createRequest(method string, url string, headers map[string]string, content []byte) (*http.Response, error) {
request, err := http.NewRequest(method, url, bytes.NewReader(content))
if err != nil {
return nil, err
}
for key, value := range headers {
request.Header.Add(key, value)
}
return http.DefaultClient.Do(request)
}
func toJson[T any](request T) []byte {
response, err := json.Marshal(request)
if err != nil {
panic(err)
}
return response
}

View File

@ -2,7 +2,7 @@ package wspay
const AuthorisationForm = "https://formtest.wspay.biz/authorization.aspx"
type WsPayForm struct {
type Form struct {
// required args
ShopID string
ShoppingCartID string
@ -31,7 +31,7 @@ type WsPayForm struct {
CurrencyCode int
}
type WsPayFormReturn struct {
type FormReturn struct {
CustomerFirstName string `form:"CustomerFirstname"`
CustomerSurname string `form:"CustomerSurname"`
CustomerAddress string `form:"CustomerAddress"`
@ -61,7 +61,7 @@ type WsPayFormReturn struct {
Signature string `form:"Signature"`
}
type WsPayFormError struct {
type FormError struct {
CustomerFirstName string
CustomerSurname string
CustomerAddress string
@ -88,7 +88,7 @@ type WsPayFormError struct {
Signature string
}
type WsPayFormCancel struct {
type FormCancel struct {
ResponseCode int
ShoppingCartID string
ApprovalCode string
@ -96,7 +96,7 @@ type WsPayFormCancel struct {
Signature string
}
type WsPayCompletionRequest struct {
type CompletionRequest struct {
Version string
WsPayOrderId string
ShopId string
@ -106,7 +106,7 @@ type WsPayCompletionRequest struct {
Signature string
}
type WsPayCompletionResponse struct {
type CompletionResponse struct {
WsPayOrderId string
ShopId string
ApprovalCode string
@ -116,14 +116,14 @@ type WsPayCompletionResponse struct {
Signature string
}
type WsPayStatusCheckRequest struct {
type StatusCheckRequest struct {
Version string
ShopId string
ShoppingCartId string
Signature string
}
type WsPayStatusCheckResponse struct {
type StatusCheckResponse struct {
WsPayOrderId string
Signature string
STAN string