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 package main
import ( import (
"bytes"
"embed" "embed"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
"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/paymentintent"
"html/template" "html/template"
"io"
"log" "log"
"net/http" "net/http"
"os"
"payment-poc/database"
"payment-poc/migration" "payment-poc/migration"
"payment-poc/state" "payment-poc/state"
stripe2 "payment-poc/stripe" stripe2 "payment-poc/stripe"
@ -29,32 +26,15 @@ import (
//go:embed db/dev/*.sql //go:embed db/dev/*.sql
var devMigrations embed.FS var devMigrations embed.FS
var BackendUrl string type PaymentProvider interface {
var WsPayShopId string CreatePaymentUrl(amount int64) (string, error)
var WsPayShopSecret string CompleteTransaction(entry database.PaymentEntry, amount int64) (database.PaymentEntry, error)
CancelTransaction(entry database.PaymentEntry) (database.PaymentEntry, error)
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")
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.SetPrefix("")
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
} }
@ -69,40 +49,16 @@ func main() {
} }
g := gin.Default() g := gin.Default()
g.Use(gin.BasicAuth(getAccounts()))
if !hasProfile("no-auth") {
g.Use(gin.BasicAuth(getAccounts()))
}
g.SetFuncMap(template.FuncMap{ g.SetFuncMap(template.FuncMap{
"formatCurrency": func(current int64) string { "formatCurrency": formatCurrency,
return fmt.Sprintf("%d,%02d", current/100, current%100) "decimalCurrency": decimalCurrency,
}, "formatState": formatState,
"formatCurrency2": func(current int64) string { "omitempty": omitempty,
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
},
}) })
g.NoRoute(func(c *gin.Context) { 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()}) c.JSON(http.StatusMethodNotAllowed, gin.H{"message": "no action on given method", "created": time.Now()})
}) })
wspayService := wspay.Service{ backendUrl := envMustExist("BACKEND_URL")
DB: client,
} paymentGateways := map[state.PaymentGateway]PaymentProvider{}
stripeService := stripe2.Service{ entryProvider := database.PaymentEntryProvider{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) { if hasProfile(string(state.GatewayWsPay)) {
wspayEntries, _ := wspayService.FetchAll() wspayService := wspay.Service{
stripeEntries, _ := stripeService.FetchAll() Provider: &entryProvider,
vivaEntries, _ := vivaService.FetchAll() ShopId: envMustExist("WSPAY_SHOP_ID"),
c.HTML(200, "index.gohtml", gin.H{"WsPay": wspayEntries, "Stripe": stripeEntries, "Viva": vivaEntries}) 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) { g.GET("/methods", func(c *gin.Context) {
amount, err := strconv.ParseFloat(c.Query("amount"), 64) amount, err := strconv.ParseFloat(c.Query("amount"), 64)
if err != nil { if err != nil {
amount = 10.00 amount = 10.00
} }
var gateways []state.PaymentGateway
c.HTML(200, "methods.gohtml", gin.H{"Amount": amount}) 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)) 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 { func getAccounts() gin.Accounts {
auth := strings.Split(envMustExist("AUTH"), ":") auth := strings.Split(envMustExist("AUTH"), ":")
return gin.Accounts{auth[0]: auth[1]} return gin.Accounts{auth[0]: auth[1]}
@ -165,595 +226,137 @@ func parseDateTime(dateTime string) time.Time {
return t return t
} }
func setupVivaEndpoints(g *gin.RouterGroup, vivaService viva.Service) { func fetchAmount(amount string) (int64, error) {
g.GET("", func(c *gin.Context) { if amount, err := strconv.ParseFloat(amount, 64); err == nil {
amount, err := strconv.ParseFloat(c.Query("amount"), 64) return int64(amount * 100), nil
if err != nil { } else {
c.AbortWithError(http.StatusBadRequest, err) return 0, 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 setupVivaEndpoints(g *gin.RouterGroup, vivaService *viva.Service) {
g.GET("success", func(c *gin.Context) { g.GET("success", func(c *gin.Context) {
transactionId := uuid.MustParse(c.Query("t")) url, err := vivaService.HandleResponse(c, state.StateAccepted)
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 { if err != nil {
log.Printf("Couldn't find payment info for viva payment %s", orderId)
c.AbortWithError(http.StatusInternalServerError, err) c.AbortWithError(http.StatusInternalServerError, err)
return return
} }
c.Redirect(http.StatusSeeOther, url)
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) { g.GET("error", func(c *gin.Context) {
transactionId := uuid.MustParse(c.Query("t")) url, err := vivaService.HandleResponse(c, state.StateError)
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 { if err != nil {
log.Printf("Couldn't find payment info for viva payment %s", orderId)
c.AbortWithError(http.StatusInternalServerError, err) c.AbortWithError(http.StatusInternalServerError, err)
return return
} }
c.Redirect(http.StatusSeeOther, url)
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) {
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())
})
g.GET("success", func(c *gin.Context) { g.GET("success", func(c *gin.Context) {
id := uuid.MustParse(c.Query("token")) url, err := stripeService.HandleResponse(c, state.StateAccepted)
log.Printf("Received success response for stripe payment %s", id)
entry, err := stripeService.FetchById(id)
if err != nil { if err != nil {
log.Printf("Couldn't find payment info for stripe payment %s", id)
c.AbortWithError(http.StatusInternalServerError, err) c.AbortWithError(http.StatusInternalServerError, err)
return return
} }
c.Redirect(http.StatusSeeOther, url)
entry.State = state.StateAccepted
if err := stripeService.Update(entry); err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
log.Printf("Stripe payment %s received correctly, returning redirect", id)
c.Redirect(http.StatusTemporaryRedirect, "/")
}) })
g.GET("error", func(c *gin.Context) { g.GET("error", func(c *gin.Context) {
id := uuid.MustParse(c.Query("token")) url, err := stripeService.HandleResponse(c, state.StateError)
log.Printf("Received error response for stripe payment %s", id)
entry, err := stripeService.FetchById(id)
if err != nil { if err != nil {
log.Printf("Couldn't find payment info for stripe payment %s", id)
c.AbortWithError(http.StatusInternalServerError, err) c.AbortWithError(http.StatusInternalServerError, err)
return return
} }
c.Redirect(http.StatusSeeOther, url)
entry.State = state.StateError
if err := stripeService.Update(entry); err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
log.Printf("Stripe payment %s received correctly, returning redirect", id)
c.Redirect(http.StatusTemporaryRedirect, "/")
})
g.GET("info/:id", func(c *gin.Context) {
id := uuid.MustParse(c.Param("id"))
entry, err := stripeService.FetchById(id)
if err != nil {
c.AbortWithError(http.StatusNotFound, err)
return
}
c.HTML(200, "stripe_info.gohtml", gin.H{"Entry": entry})
}) })
} }
func setupWsPayEndpoints(g *gin.RouterGroup, wspayService wspay.Service) { func setupWsPayEndpoints(g *gin.RouterGroup, wspayService *wspay.Service) {
g.GET("", func(c *gin.Context) { g.GET("/initialize/:id", func(c *gin.Context) {
amount, err := strconv.ParseFloat(c.Query("amount"), 64) entry, err := wspayService.Provider.FetchById(uuid.MustParse(c.Param("id")))
if err != nil { if err != nil {
c.AbortWithError(http.StatusNotFound, err)
return
}
if entry.State != state.StateInitialized {
c.AbortWithError(http.StatusBadRequest, err) c.AbortWithError(http.StatusBadRequest, err)
return return
} }
entry, err := wspayService.CreateEntry(WsPayShopId, int64(amount*100)) form := wspayService.InitializePayment(entry)
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),
}
c.HTML(200, "wspay.gohtml", gin.H{"Action": wspay.AuthorisationForm, "Form": form}) 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) { g.GET("success", func(c *gin.Context) {
response := wspay.WsPayFormReturn{} url, err := wspayService.HandleSuccessResponse(c)
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)
if err != nil { if err != nil {
log.Printf("Couldn't find payment info for transaction %s", response.ShoppingCartID)
c.AbortWithError(http.StatusInternalServerError, err) c.AbortWithError(http.StatusInternalServerError, err)
return return
} }
c.Redirect(http.StatusSeeOther, url)
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, "/")
}
}) })
g.GET("error", func(c *gin.Context) { g.GET("error", func(c *gin.Context) {
response := wspay.WsPayFormError{} url, err := wspayService.HandleErrorResponse(c, state.StateError)
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)
if err != nil { if err != nil {
log.Printf("Couldn't find payment info for transaction %s", response.ShoppingCartID)
c.AbortWithError(http.StatusInternalServerError, err) c.AbortWithError(http.StatusInternalServerError, err)
return return
} }
c.Redirect(http.StatusSeeOther, url)
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})
}) })
g.GET("cancel", func(c *gin.Context) { g.GET("cancel", func(c *gin.Context) {
response := wspay.WsPayFormCancel{} url, err := wspayService.HandleErrorResponse(c, state.StateCanceled)
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)
if err != nil { if err != nil {
log.Printf("Couldn't find payment info for transaction %s", response.ShoppingCartID)
c.AbortWithError(http.StatusInternalServerError, err) c.AbortWithError(http.StatusInternalServerError, err)
return return
} }
entry.State = state.StateCanceledInitialization c.Redirect(http.StatusSeeOther, url)
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, "/")
}
}) })
} }
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" StateVoided PaymentState = "voided"
StateCanceled PaymentState = "canceled" 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 package stripe
import ( import (
"github.com/gin-gonic/gin"
"github.com/google/uuid" "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" "payment-poc/state"
) )
type Service struct { type Service struct {
DB *sqlx.DB Provider *database.PaymentEntryProvider
ApiKey string
BackendUrl string
} }
func (s *Service) CreateEntry(totalAmount int64) (StripeDb, error) { func (s *Service) CreatePaymentUrl(amount int64) (url string, err error) {
id := uuid.Must(uuid.NewRandom()) entry, err := s.Provider.CreateEntry(database.PaymentEntry{
entry := StripeDb{ Gateway: state.GatewayVivaWallet,
Id: id,
TotalAmount: totalAmount,
State: state.StateInitialized, State: state.StateInitialized,
} TotalAmount: amount,
_, err := s.DB.Exec(`INSERT INTO "stripe" ("id", "total_amount", "payment_state") VALUES ($1, $2, $3)`, })
&entry.Id, &entry.TotalAmount, &entry.State,
)
if err != nil { 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) { func (s *Service) InitializePayment(entry database.PaymentEntry) (database.PaymentEntry, string, error) {
var entries []StripeDb
err := s.DB.Select(&entries, `SELECT * FROM "stripe"`) currency := string(stripe.CurrencyEUR)
return entries, err 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) { func (s *Service) CompleteTransaction(entry database.PaymentEntry, amount int64) (database.PaymentEntry, error) {
entry := StripeDb{} params := &stripe.PaymentIntentCaptureParams{
err := s.DB.Get(&entry, `SELECT * FROM "stripe" WHERE "id" = $1`, id) AmountToCapture: stripe.Int64(amount),
return entry, err }
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 { func (s *Service) CancelTransaction(entry database.PaymentEntry) (database.PaymentEntry, error) {
_, err := s.DB.Exec(`UPDATE "stripe" set "payment_intent_id" = $2, "payment_state" = $3 WHERE "id" = $1`, params := &stripe.PaymentIntentCancelParams{}
&entry.Id, &entry.PaymentIntentId, &entry.State, pi, err := paymentintent.Cancel(*entry.PaymentIntentId, params)
) if err != nil {
return err 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}}"> <form class="mb-3" method="post" action="/stripe/complete/{{.Entry.Id}}">
<div class="mb-3"> <div class="mb-3">
<label class="form-label" for="amount">Završi transakciju</label> <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> </div>
<button class="btn btn-primary" type="submit">Završi transakciju</button> <button class="btn btn-primary" type="submit">Završi transakciju</button>
</form> </form>

View File

@ -32,7 +32,7 @@
<form class="mb-3" method="post" action="/viva/complete/{{.Entry.Id}}"> <form class="mb-3" method="post" action="/viva/complete/{{.Entry.Id}}">
<div class="mb-3"> <div class="mb-3">
<label class="form-label" for="amount">Završi transakciju</label> <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> </div>
<button class="btn btn-primary" type="submit">Završi transakciju</button> <button class="btn btn-primary" type="submit">Završi transakciju</button>
</form> </form>

View File

@ -50,7 +50,7 @@
<form class="mb-3" method="post" action="/wspay/complete/{{.Entry.Id}}"> <form class="mb-3" method="post" action="/wspay/complete/{{.Entry.Id}}">
<div class="mb-3"> <div class="mb-3">
<label class="form-label" for="amount">Završi transakciju</label> <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> </div>
<button class="btn btn-primary" type="submit">Završi transakciju</button> <button class="btn btn-primary" type="submit">Završi transakciju</button>
</form> </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/base64"
"encoding/json" "encoding/json"
"errors" "errors"
"github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jmoiron/sqlx"
"io" "io"
"log" "log"
"net/http" "net/http"
"net/url" "net/url"
"payment-poc/database"
"payment-poc/state" "payment-poc/state"
"strconv" "strconv"
"strings"
"time" "time"
) )
type Service struct { type Service struct {
DB *sqlx.DB Provider *database.PaymentEntryProvider
Token string
Expiration time.Time
ClientId string ClientId string
ClientSecret string ClientSecret string
SourceCode string SourceCode string
MerchantId string MerchantId string
ApiKey string ApiKey string
token string
expiration time.Time
} }
func (s *Service) OAuthToken() (string, error) { func (s *Service) CreatePaymentUrl(amount int64) (url string, err error) {
if s.Token != "" && s.Expiration.After(time.Now()) { entry, err := s.Provider.CreateEntry(database.PaymentEntry{
return s.Token, nil Gateway: state.GatewayVivaWallet,
} State: state.StateInitialized,
return s.fetchOAuthToken() TotalAmount: amount,
} })
func (s *Service) CreatePaymentOrder(entry VivaDb) (VivaDb, error) {
token, err := s.OAuthToken()
if err != nil { 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, Amount: entry.TotalAmount,
Description: "Example payment", Description: "Example payment",
MerchantDescription: "Example payment", MerchantDescription: "Example payment",
@ -50,186 +65,173 @@ func (s *Service) CreatePaymentOrder(entry VivaDb) (VivaDb, error) {
Source: s.SourceCode, Source: s.SourceCode,
} }
content, err := json.Marshal(&orderRequest) httpResponse, err := createRequest(
if err != nil { "POST",
return VivaDb{}, err "https://demo-api.vivapayments.com/checkout/v2/orders",
} map[string]string{"authorization": "Bearer " + token, "content-type": "application/json"},
toJson(request),
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 { if err != nil {
return VivaDb{}, err return database.PaymentEntry{}, err
} }
return s.FetchById(id)
}
func (s *Service) FetchAll() ([]VivaDb, error) { var response OrderResponse
var entries []VivaDb err = readResponse(httpResponse, &response)
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 { if err != nil {
return "", err return database.PaymentEntry{}, err
} }
request.Header.Add("content-type", "application/x-www-form-urlencoded") entry.OrderId = &response.OrderId
request.SetBasicAuth(s.ClientId, s.ClientSecret) return entry, nil
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) { func (s *Service) CompleteTransaction(entry database.PaymentEntry, amount int64) (database.PaymentEntry, error) {
completionRequest := VivaTransactionCompleteRequest{ completionRequest := TransactionCompleteRequest{
Amount: amount, Amount: amount,
CustomerDescription: "Example transaction", 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 { 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)) var response TransactionResponse
request.Header.Add("authorization", "Bearer "+s.BasicAuth()) err = readResponse(httpResponse, &response)
request.Header.Add("content-type", "application/json")
response, err := http.DefaultClient.Do(request)
if err != nil { if err != nil {
return VivaDb{}, err return database.PaymentEntry{}, err
} }
if response.StatusCode == http.StatusOK { if response.StatusId == "F" {
transactionResponse := VivaTransactionResponse{} paidAmount := int64(response.Amount * 100)
content, err := io.ReadAll(response.Body) entry.Amount = &paidAmount
if err != nil { entry.State = state.StateCompleted
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 { } 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 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)) return base64.StdEncoding.EncodeToString([]byte(s.MerchantId + ":" + s.ApiKey))
} }
func (s *Service) CancelTransaction(entry VivaDb) (VivaDb, error) { func (s *Service) HandleResponse(c *gin.Context, expectedState state.PaymentState) (string, error) {
request, err := http.NewRequest("DELETE", "https://demo.vivapayments.com/api/transactions/"+entry.TransactionId.String()+"?amount="+strconv.FormatInt(entry.TotalAmount, 10), bytes.NewReader([]byte{})) transactionId := uuid.MustParse(c.Query("t"))
request.Header.Add("authorization", "Bearer "+s.BasicAuth()) 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 { 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{} entry.State = expectedState
content, err := io.ReadAll(response.Body) entry.ECI = &eci
if err != nil { entry.Lang = &lang
return VivaDb{}, err entry.EventId = &eventId
} entry.TransactionId = &transactionId
if err := json.Unmarshal(content, &transactionResponse); err != nil {
return VivaDb{}, err if _, err := s.Provider.UpdateEntry(entry); err != nil {
} else { return "", err
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
log.Printf("Viva payment %s received correctly, returning redirect", orderId)
return "/entries/" + entry.Id.String(), nil
} }

View File

@ -1,17 +1,8 @@
package viva package viva
type OrderId string import "payment-poc/database"
func (o OrderId) MarshalJSON() ([]byte, error) { type OrderRequest struct {
return []byte(o), nil
}
func (o *OrderId) UnmarshalJSON(value []byte) error {
*o = OrderId(value)
return nil
}
type VivaOrderRequest struct {
Amount int64 `json:"amount"` Amount int64 `json:"amount"`
Description string `json:"customerTrns"` Description string `json:"customerTrns"`
MerchantDescription string `json:"merchantTrns"` MerchantDescription string `json:"merchantTrns"`
@ -20,21 +11,21 @@ type VivaOrderRequest struct {
Source string `json:"sourceCode"` Source string `json:"sourceCode"`
} }
type VivaOrderResponse struct { type OrderResponse struct {
OrderId OrderId `json:"orderCode"` OrderId database.OrderId `json:"orderCode"`
} }
type VivaOAuthResponse struct { type OAuthResponse struct {
AccessToken string `json:"access_token"` AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"` ExpiresIn int `json:"expires_in"`
} }
type VivaTransactionCompleteRequest struct { type TransactionCompleteRequest struct {
Amount int64 `json:"amount"` Amount int64 `json:"amount"`
CustomerDescription string `json:"customerTrns"` CustomerDescription string `json:"customerTrns"`
} }
type VivaTransactionResponse struct { type TransactionResponse struct {
Amount float64 `json:"Amount"` Amount float64 `json:"Amount"`
StatusId string `json:"StatusId"` StatusId string `json:"StatusId"`
ErrorCode int64 `json:"ErrorCode"` ErrorCode int64 `json:"ErrorCode"`

View File

@ -1,60 +1,186 @@
package wspay package wspay
import ( import (
"bytes"
"crypto/sha512" "crypto/sha512"
"encoding/hex" "encoding/hex"
"encoding/json"
"errors" "errors"
"github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jmoiron/sqlx" "io"
"net/http"
"payment-poc/database"
"payment-poc/state" "payment-poc/state"
"strconv" "strconv"
) )
type Service struct { 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) { func (s *Service) CreatePaymentUrl(amount int64) (string, error) {
id := uuid.Must(uuid.NewRandom()) entry, err := s.Provider.CreateEntry(database.PaymentEntry{
entry := WsPayDb{ Gateway: state.GatewayVivaWallet,
Id: id, State: state.StateInitialized,
ShopID: shopId, TotalAmount: amount,
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,
)
if err != nil { 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) { func (s *Service) CompleteTransaction(entry database.PaymentEntry, amount int64) (database.PaymentEntry, error) {
var entries []WsPayDb if entry.State == state.StateAccepted {
err := s.DB.Select(&entries, `SELECT * FROM "wspay"`) var request = CompletionRequest{
return entries, err 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) { func (s *Service) CancelTransaction(entry database.PaymentEntry) (database.PaymentEntry, error) {
entry := WsPayDb{} if entry.State == state.StateAccepted {
err := s.DB.Get(&entry, `SELECT * FROM "wspay" WHERE "id" = $1`, id) var request = CompletionRequest{
return entry, err 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) { func (s *Service) InitializePayment(entry database.PaymentEntry) Form {
entry := WsPayDb{} form := Form{
err := s.DB.Get(&entry, `SELECT * FROM "wspay" WHERE "shopping_card_id" = $1`, id) ShopID: s.ShopId,
return entry, err 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 { func (s *Service) HandleSuccessResponse(c *gin.Context) (string, 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`, response := FormReturn{}
&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, if err := c.ShouldBind(&response); err != nil {
) return "", err
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 { 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") 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" const AuthorisationForm = "https://formtest.wspay.biz/authorization.aspx"
type WsPayForm struct { type Form struct {
// required args // required args
ShopID string ShopID string
ShoppingCartID string ShoppingCartID string
@ -31,7 +31,7 @@ type WsPayForm struct {
CurrencyCode int CurrencyCode int
} }
type WsPayFormReturn struct { type FormReturn struct {
CustomerFirstName string `form:"CustomerFirstname"` CustomerFirstName string `form:"CustomerFirstname"`
CustomerSurname string `form:"CustomerSurname"` CustomerSurname string `form:"CustomerSurname"`
CustomerAddress string `form:"CustomerAddress"` CustomerAddress string `form:"CustomerAddress"`
@ -61,7 +61,7 @@ type WsPayFormReturn struct {
Signature string `form:"Signature"` Signature string `form:"Signature"`
} }
type WsPayFormError struct { type FormError struct {
CustomerFirstName string CustomerFirstName string
CustomerSurname string CustomerSurname string
CustomerAddress string CustomerAddress string
@ -88,7 +88,7 @@ type WsPayFormError struct {
Signature string Signature string
} }
type WsPayFormCancel struct { type FormCancel struct {
ResponseCode int ResponseCode int
ShoppingCartID string ShoppingCartID string
ApprovalCode string ApprovalCode string
@ -96,7 +96,7 @@ type WsPayFormCancel struct {
Signature string Signature string
} }
type WsPayCompletionRequest struct { type CompletionRequest struct {
Version string Version string
WsPayOrderId string WsPayOrderId string
ShopId string ShopId string
@ -106,7 +106,7 @@ type WsPayCompletionRequest struct {
Signature string Signature string
} }
type WsPayCompletionResponse struct { type CompletionResponse struct {
WsPayOrderId string WsPayOrderId string
ShopId string ShopId string
ApprovalCode string ApprovalCode string
@ -116,14 +116,14 @@ type WsPayCompletionResponse struct {
Signature string Signature string
} }
type WsPayStatusCheckRequest struct { type StatusCheckRequest struct {
Version string Version string
ShopId string ShopId string
ShoppingCartId string ShoppingCartId string
Signature string Signature string
} }
type WsPayStatusCheckResponse struct { type StatusCheckResponse struct {
WsPayOrderId string WsPayOrderId string
Signature string Signature string
STAN string STAN string