Reorganized code + migration to go1.22

This commit is contained in:
Borna Rajković 2024-04-01 20:29:24 +02:00
parent e3d77d55b9
commit 7369777098
25 changed files with 825 additions and 605 deletions

332
api/api.go Normal file
View File

@ -0,0 +1,332 @@
package api
import (
"errors"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"log/slog"
"net/http"
"payment-poc/domain/database"
"payment-poc/domain/providers"
"payment-poc/domain/providers/mock"
stripe2 "payment-poc/domain/providers/stripe"
"payment-poc/domain/providers/viva"
wspay2 "payment-poc/domain/providers/wspay"
"payment-poc/domain/state"
"strconv"
"time"
)
func NoMethod() gin.HandlerFunc {
return func(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{
"status": 404,
"created": time.Now(),
"message": "no handler for method '" + c.Request.Method + "'",
})
}
}
func NoRoute() gin.HandlerFunc {
return func(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{
"status": 404,
"created": time.Now(),
"message": "no handler for " + c.Request.Method + " '" + c.Request.URL.RequestURI() + "'",
})
}
}
func RefreshPayment(entryProvider *database.PaymentEntryProvider, paymentGateways map[state.PaymentGateway]providers.PaymentProvider) gin.HandlerFunc {
return 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 {
slog.Info("fetching payment info", "entry_id", entry.Id.String(), "state", entry.State)
entry, err = paymentGateway.UpdatePayment(entry)
if err == nil {
entryProvider.UpdateEntry(entry)
slog.Info("fetched payment info", "entry_id", entry.Id.String(), "state", entry.State)
}
c.Redirect(http.StatusSeeOther, "/entries/"+id.String())
} else {
if err != nil {
c.AbortWithError(http.StatusInternalServerError, errors.New("payment gateway not supported: "+string(entry.Gateway)))
return
}
}
}
}
func CancelPayment(entryProvider *database.PaymentEntryProvider, paymentGateways map[state.PaymentGateway]providers.PaymentProvider) gin.HandlerFunc {
return 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 {
slog.Info("canceling payment", "entry_id", id.String(), "state", entry.State)
entry, err = paymentGateway.CancelTransaction(entry)
if err == nil {
entryProvider.UpdateEntry(entry)
slog.Info("canceled payment", "entry_id", entry.Id.String(), "state", entry.State)
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
}
}
}
}
func CompletePayment(entryProvider *database.PaymentEntryProvider, paymentGateways map[state.PaymentGateway]providers.PaymentProvider) gin.HandlerFunc {
return 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
}
slog.Info("completing payment with amount", "entry_id", id.String(), "state", entry.State, "amount", float64(amount)/100.0)
entry, err = paymentGateway.CompleteTransaction(entry, amount)
if err == nil {
entryProvider.UpdateEntry(entry)
slog.Info("completed payment", "entry_id", id.String(), "state", entry.State)
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
}
}
}
}
func GetEntry(provider *database.PaymentEntryProvider) gin.HandlerFunc {
return func(c *gin.Context) {
id := uuid.MustParse(c.Param("id"))
entry, err := provider.FetchById(id)
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
c.HTML(200, "info.gohtml", gin.H{"Entry": entry})
}
}
func InitializePayment(entryProvider *database.PaymentEntryProvider, paymentGateways map[state.PaymentGateway]providers.PaymentProvider) gin.HandlerFunc {
return 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
}
entry, err := entryProvider.CreateEntry(database.PaymentEntry{
Gateway: gateway,
State: state.StatePreinitialized,
TotalAmount: amount,
})
slog.Info("creating payment", "entry_id", entry.Id.String(), "state", entry.State, "gateway", gateway, "amount", float64(amount)/100.0)
if entry, url, err := paymentGateway.CreatePaymentUrl(entry); err == nil {
slog.Info("created redirect url", "entry_id", entry.Id.String(), "state", entry.State)
entryProvider.UpdateEntry(entry)
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
}
}
}
func GetGateways(gateways map[state.PaymentGateway]providers.PaymentProvider) gin.HandlerFunc {
return 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, "Gateways": mapGateways(gateways)})
}
}
func GetIndex(provider *database.PaymentEntryProvider) gin.HandlerFunc {
return func(c *gin.Context) {
entries, _ := provider.FetchAll()
c.HTML(200, "index.gohtml", gin.H{"Entries": entries})
}
}
func VivaOnFailure(vivaService viva.Service, provider *database.PaymentEntryProvider) gin.HandlerFunc {
return func(c *gin.Context) {
url, err := vivaService.HandleResponse(c, provider, state.StateError)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.Redirect(http.StatusSeeOther, url)
}
}
func VivaOnSuccess(vivaService viva.Service, provider *database.PaymentEntryProvider) gin.HandlerFunc {
return func(c *gin.Context) {
url, err := vivaService.HandleResponse(c, provider, state.StateAccepted)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.Redirect(http.StatusSeeOther, url)
}
}
func StripeOnFailure(stripeService stripe2.Service, provider *database.PaymentEntryProvider) gin.HandlerFunc {
return func(c *gin.Context) {
url, err := stripeService.HandleResponse(c, provider, state.StateError)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.Redirect(http.StatusSeeOther, url)
}
}
func StripeOnSuccess(stripeService stripe2.Service, provider *database.PaymentEntryProvider) gin.HandlerFunc {
return func(c *gin.Context) {
url, err := stripeService.HandleResponse(c, provider, state.StateAccepted)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.Redirect(http.StatusSeeOther, url)
}
}
func WsPayOnFailure(wspayService wspay2.Service, provider *database.PaymentEntryProvider, finalState state.PaymentState) gin.HandlerFunc {
return func(c *gin.Context) {
url, err := wspayService.HandleErrorResponse(c, provider, finalState)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.Redirect(http.StatusSeeOther, url)
}
}
func WsPayOnSuccess(wspayService wspay2.Service, provider *database.PaymentEntryProvider) gin.HandlerFunc {
return func(c *gin.Context) {
url, err := wspayService.HandleSuccessResponse(c, provider)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.Redirect(http.StatusSeeOther, url)
}
}
func MockOnFailure(mockService mock.Service, provider *database.PaymentEntryProvider) gin.HandlerFunc {
return func(c *gin.Context) {
url, err := mockService.HandleResponse(c, provider, state.StateError)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.Redirect(http.StatusSeeOther, url)
}
}
func MockOnSuccess(mockService mock.Service, provider *database.PaymentEntryProvider) gin.HandlerFunc {
return func(c *gin.Context) {
url, err := mockService.HandleResponse(c, provider, state.StateAccepted)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.Redirect(http.StatusSeeOther, url)
}
}
func MockOpenGateway(provider *database.PaymentEntryProvider) gin.HandlerFunc {
return func(c *gin.Context) {
id := uuid.MustParse(c.Param("id"))
entry, err := provider.FetchById(id)
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
c.HTML(http.StatusOK, "mock_gateway.gohtml", gin.H{"Entry": entry})
}
}
func mapGateways(gateways map[state.PaymentGateway]providers.PaymentProvider) map[string]string {
providerMap := map[string]string{}
for key := range gateways {
providerMap[string(key)] = mapGatewayName(key)
}
return providerMap
}
func mapGatewayName(key state.PaymentGateway) string {
switch key {
case state.GatewayStripe:
return "Stripe"
case state.GatewayVivaWallet:
return "Viva wallet"
case state.GatewayWsPay:
return "WsPay"
case state.GatewayMock:
return "mock"
}
return ""
}
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
case string(state.GatewayMock):
return state.GatewayMock, nil
}
return "", errors.New("unknown gateway: " + gateway)
}
func fetchAmount(amount string) (int64, error) {
if amount, err := strconv.ParseFloat(amount, 64); err == nil {
return int64(amount * 100), nil
} else {
return 0, err
}
}

39
api/middleware.go Normal file
View File

@ -0,0 +1,39 @@
package api
import (
"fmt"
"github.com/gin-gonic/gin"
"os"
"strings"
)
func Auth() gin.HandlerFunc {
if !hasProfile("no-auth") {
return gin.BasicAuth(loadAuth())
}
return nil
}
func hasProfile(value string) bool {
profileOptions := strings.Split(os.Getenv("PROFILE"), ",")
for _, option := range profileOptions {
if option == value {
return true
}
}
return false
}
func loadAuth() map[string]string {
credentials := envMustExist("AUTH")
values := strings.Split(credentials, ":")
return map[string]string{values[0]: values[1]}
}
func envMustExist(env string) string {
if value, exists := os.LookupEnv(env); !exists {
panic(fmt.Sprintf("env variable '%s' not defined", env))
} else {
return value
}
}

84
api/routes.go Normal file
View File

@ -0,0 +1,84 @@
package api
import (
"github.com/gin-gonic/gin"
"github.com/jmoiron/sqlx"
"log/slog"
"payment-poc/domain/database"
"payment-poc/domain/providers"
"payment-poc/domain/providers/mock"
stripe2 "payment-poc/domain/providers/stripe"
"payment-poc/domain/providers/viva"
wspay2 "payment-poc/domain/providers/wspay"
"payment-poc/domain/state"
)
func RegisterRoutes(server *gin.Engine, db *sqlx.DB) {
backendUrl := envMustExist("BACKEND_URL")
paymentGateways := map[state.PaymentGateway]providers.PaymentProvider{}
entryProvider := &database.PaymentEntryProvider{DB: db}
server.GET("/", GetIndex(entryProvider))
server.GET("/methods", GetGateways(paymentGateways))
server.GET("/methods/:gateway", InitializePayment(entryProvider, paymentGateways))
server.GET("/entries/:id", GetEntry(entryProvider))
server.POST("/entries/:id/complete", CompletePayment(entryProvider, paymentGateways))
server.POST("/entries/:id/cancel", CancelPayment(entryProvider, paymentGateways))
server.POST("/entries/:id/refresh", RefreshPayment(entryProvider, paymentGateways))
if hasProfile(string(state.GatewayMock)) {
mockService := mock.Service{
BackendUrl: backendUrl,
}
paymentGateways[state.GatewayMock] = &mockService
mockGroup := server.Group("mock")
mockGroup.GET("/gateway/:id", MockOpenGateway(entryProvider))
mockGroup.GET("success", MockOnSuccess(mockService, entryProvider))
mockGroup.GET("error", MockOnFailure(mockService, entryProvider))
slog.Info("Registered provider", slog.Any("provider", state.GatewayMock))
}
if hasProfile(string(state.GatewayWsPay)) {
wspayService := wspay2.Service{
ShopId: envMustExist("WSPAY_SHOP_ID"),
ShopSecret: envMustExist("WSPAY_SHOP_SECRET"),
BackendUrl: backendUrl,
}
paymentGateways[state.GatewayWsPay] = &wspayService
wspayGroup := server.Group("wspay")
wspayGroup.GET("success", WsPayOnSuccess(wspayService, entryProvider))
wspayGroup.GET("error", WsPayOnFailure(wspayService, entryProvider, state.StateError))
wspayGroup.GET("cancel", WsPayOnFailure(wspayService, entryProvider, state.StateCanceled))
slog.Info("Registered provider", slog.Any("provider", state.GatewayWsPay))
}
if hasProfile(string(state.GatewayStripe)) {
stripeService := stripe2.Service{
ApiKey: envMustExist("STRIPE_KEY"),
BackendUrl: backendUrl,
}
paymentGateways[state.GatewayStripe] = &stripeService
stripeGroup := server.Group("stripe")
stripeGroup.GET("success", StripeOnSuccess(stripeService, entryProvider))
stripeGroup.GET("error", StripeOnFailure(stripeService, entryProvider))
slog.Info("Registered provider", slog.Any("provider", state.GatewayStripe))
}
if hasProfile(string(state.GatewayVivaWallet)) {
vivaService := viva.Service{
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"),
}
paymentGateways[state.GatewayVivaWallet] = &vivaService
vivaGroup := server.Group("viva")
vivaGroup.GET("success", VivaOnSuccess(vivaService, entryProvider))
vivaGroup.GET("error", VivaOnFailure(vivaService, entryProvider))
slog.Info("Registered provider", slog.Any("provider", state.GatewayVivaWallet))
}
}

25
api/server.go Normal file
View File

@ -0,0 +1,25 @@
package api
import (
"github.com/gin-gonic/gin"
"github.com/jmoiron/sqlx"
)
func SetupServer(db *sqlx.DB) *gin.Engine {
server := createServer()
LoadTemplates(server)
RegisterRoutes(server, db)
return server
}
func createServer() *gin.Engine {
server := gin.New()
server.NoRoute(NoRoute())
server.NoMethod(NoMethod())
server.Use(gin.Recovery())
auth := Auth()
if auth != nil {
server.Use(Auth())
}
return server
}

64
api/templates.go Normal file
View File

@ -0,0 +1,64 @@
package api
import (
"fmt"
"github.com/gin-gonic/gin"
"html/template"
"payment-poc/domain/state"
)
func LoadTemplates(server *gin.Engine) {
server.SetFuncMap(template.FuncMap{
"formatCurrency": formatCurrency,
"formatCurrencyPtr": formatCurrencyPtr,
"decimalCurrency": decimalCurrency,
"formatState": formatState,
"omitempty": omitempty,
})
server.LoadHTMLGlob("./templates/*.gohtml")
}
func formatState(stt state.PaymentState) string {
switch stt {
case state.StateCanceled:
return "Otkazana"
case state.StateVoided:
return "Poništena"
case state.StateAccepted:
return "Predautorizirana"
case state.StateError:
return "Greška"
case state.StatePreinitialized:
return "Predinicijalizirana"
case state.StateInitialized:
return "Inicijalizirana"
case state.StateCanceledInitialization:
return "Otkazana tijekom izrade"
case state.StateCompleted:
return "Autorizirana"
}
return "nepoznato stanje '" + string(stt) + "'"
}
func formatCurrency(current int64) string {
return fmt.Sprintf("%d,%02d", current/100, current%100)
}
func formatCurrencyPtr(current *int64) string {
if current != nil {
return fmt.Sprintf("%d,%02d", (*current)/100, (*current)%100)
} else {
return "-"
}
}
func decimalCurrency(current int64) string {
return fmt.Sprintf("%d,%02d", current/100, current%100)
}
func omitempty(value string) string {
if value == "" {
return "-"
}
return value
}

14
db.go
View File

@ -15,15 +15,25 @@ func envMustExist(env string) string {
} }
} }
func envOrDefault(env string, defaultValue string) string {
if value, exists := os.LookupEnv(env); exists {
return value
} else {
return defaultValue
}
}
func connectToDb() (*sqlx.DB, error) { func connectToDb() (*sqlx.DB, error) {
host := envMustExist("PSQL_HOST") host := envMustExist("PSQL_HOST")
port := envMustExist("PSQL_PORT") port := envMustExist("PSQL_PORT")
user := envMustExist("PSQL_USER") user := envMustExist("PSQL_USER")
password := envMustExist("PSQL_PASSWORD") password := envMustExist("PSQL_PASSWORD")
dbname := envMustExist("PSQL_DB") dbname := envMustExist("PSQL_DB")
sslMode := envOrDefault("PSQL_SSLMODE", "disable")
schema := envOrDefault("PSQL_SCHEMA", "public")
psqlInfo := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", psqlInfo := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s search_path=%s",
host, port, user, password, dbname) host, port, user, password, dbname, sslMode, schema)
db, err := sqlx.Open("postgres", psqlInfo) db, err := sqlx.Open("postgres", psqlInfo)
if err != nil { if err != nil {

View File

@ -17,6 +17,7 @@ CREATE TABLE IF NOT EXISTS "payment_entry"
"payment_intent_id" varchar(255) DEFAULT NULL, "payment_intent_id" varchar(255) DEFAULT NULL,
"ws_pay_order_id" varchar(255) DEFAULT '',
"shopping_card_id" varchar(255) DEFAULT NULL, "shopping_card_id" varchar(255) DEFAULT NULL,
"stan" varchar(255) DEFAULT NULL, "stan" varchar(255) DEFAULT NULL,
"success" int DEFAULT NULL, "success" int DEFAULT NULL,

View File

@ -0,0 +1,31 @@
CREATE TABLE IF NOT EXISTS "payment_entry"
(
"id" uuid NOT NULL,
"created" timestamp NOT NULL,
"modified" timestamp DEFAULT NULL,
"gateway" varchar(255) NOT NULL,
"state" varchar(255) NOT NULL,
"lang" varchar(16) DEFAULT NULL,
"error" varchar(255) DEFAULT NULL,
"amount" int DEFAULT NULL,
"total_amount" int NOT NULL,
"eci" varchar(255) DEFAULT NULL,
"payment_intent_id" varchar(255) DEFAULT NULL,
"ws_pay_order_id" varchar(255) DEFAULT '',
"shopping_card_id" varchar(255) DEFAULT NULL,
"stan" varchar(255) DEFAULT NULL,
"success" int DEFAULT NULL,
"approval_code" varchar(255) DEFAULT NULL,
"order_id" varchar(255) DEFAULT NULL,
"transaction_id" uuid DEFAULT NULL,
"event_id" varchar(255) DEFAULT NULL,
PRIMARY KEY (id)
);

View File

@ -2,7 +2,7 @@ package database
import ( import (
"github.com/google/uuid" "github.com/google/uuid"
"payment-poc/state" "payment-poc/domain/state"
"time" "time"
) )
@ -31,6 +31,7 @@ type PaymentEntry struct {
PaymentIntentId *string `db:"payment_intent_id"` PaymentIntentId *string `db:"payment_intent_id"`
// wspay field // wspay field
WsPayOrderId string `db:"ws_pay_order_id"`
ShoppingCardID *string `db:"shopping_card_id"` ShoppingCardID *string `db:"shopping_card_id"`
STAN *string `db:"stan"` STAN *string `db:"stan"`
Success *int `db:"success"` Success *int `db:"success"`

View File

@ -29,8 +29,8 @@ func (p *PaymentEntryProvider) UpdateEntry(entry PaymentEntry) (PaymentEntry, er
currentTime := time.Now() currentTime := time.Now()
entry.Modified = &currentTime 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`, _, 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, "ws_pay_order_id" = $16 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, &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, &entry.WsPayOrderId,
) )
if err != nil { if err != nil {
return PaymentEntry{}, err return PaymentEntry{}, err

View File

@ -0,0 +1,44 @@
package mock
import (
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"log/slog"
database2 "payment-poc/domain/database"
"payment-poc/domain/state"
)
type Service struct {
BackendUrl string
}
func (s Service) UpdatePayment(entry database2.PaymentEntry) (updatedEntry database2.PaymentEntry, err error) {
return entry, nil
}
func (s Service) CreatePaymentUrl(entry database2.PaymentEntry) (updateEntry database2.PaymentEntry, url string, err error) {
return entry, "/mock/gateway/" + entry.Id.String(), nil
}
func (s Service) CompleteTransaction(entry database2.PaymentEntry, amount int64) (database2.PaymentEntry, error) {
entry.Amount = &amount
entry.State = state.StateCompleted
return entry, nil
}
func (s Service) CancelTransaction(entry database2.PaymentEntry) (database2.PaymentEntry, error) {
entry.State = state.StateVoided
return entry, nil
}
func (s Service) HandleResponse(c *gin.Context, provider *database2.PaymentEntryProvider, paymentState state.PaymentState) (string, error) {
id := uuid.MustParse(c.Query("id"))
entry, err := provider.FetchById(id)
if err != nil {
return "", err
}
entry.State = paymentState
_, err = provider.UpdateEntry(entry)
slog.Info("received authorization response", "entry_id", entry.Id.String(), "state", entry.State)
return "/entries/" + id.String(), err
}

View File

@ -0,0 +1,10 @@
package providers
import "payment-poc/domain/database"
type PaymentProvider interface {
CreatePaymentUrl(entry database.PaymentEntry) (updatedEntry database.PaymentEntry, url string, err error)
CompleteTransaction(entry database.PaymentEntry, amount int64) (database.PaymentEntry, error)
CancelTransaction(entry database.PaymentEntry) (database.PaymentEntry, error)
UpdatePayment(entry database.PaymentEntry) (updatedEntry database.PaymentEntry, err error)
}

View File

@ -6,9 +6,9 @@ import (
"github.com/stripe/stripe-go/v72" "github.com/stripe/stripe-go/v72"
"github.com/stripe/stripe-go/v72/checkout/session" "github.com/stripe/stripe-go/v72/checkout/session"
"github.com/stripe/stripe-go/v72/paymentintent" "github.com/stripe/stripe-go/v72/paymentintent"
"log" "log/slog"
"payment-poc/database" database2 "payment-poc/domain/database"
"payment-poc/state" "payment-poc/domain/state"
) )
type Service struct { type Service struct {
@ -16,15 +16,19 @@ type Service struct {
BackendUrl string BackendUrl string
} }
func (s *Service) UpdatePayment(entry database.PaymentEntry) (updatedEntry database.PaymentEntry, err error) { func (s *Service) UpdatePayment(entry database2.PaymentEntry) (updatedEntry database2.PaymentEntry, err error) {
pi, err := paymentintent.Get(*entry.PaymentIntentId, nil) client := paymentintent.Client{
B: stripe.GetBackend(stripe.APIBackend),
Key: s.ApiKey,
}
pi, err := client.Get(*entry.PaymentIntentId, nil)
if err != nil { if err != nil {
return entry, err return entry, err
} }
newState := determineState(pi.Status) newState := determineState(pi.Status)
if entry.State != newState && newState != "" { if entry.State != newState && newState != "" {
log.Printf("[%s] updated state for %s -> %s", entry.Id.String(), entry.State, newState) slog.Info("updated state", "entry_id", entry.Id.String(), "state", entry.State, "new_state", newState)
if pi.AmountReceived > 0 { if pi.AmountReceived > 0 {
entry.Amount = &pi.AmountReceived entry.Amount = &pi.AmountReceived
} }
@ -53,7 +57,7 @@ func determineState(status stripe.PaymentIntentStatus) state.PaymentState {
return "" return ""
} }
func (s *Service) CreatePaymentUrl(entry database.PaymentEntry) (database.PaymentEntry, string, error) { func (s *Service) CreatePaymentUrl(entry database2.PaymentEntry) (database2.PaymentEntry, string, error) {
entry, url, err := s.InitializePayment(entry) entry, url, err := s.InitializePayment(entry)
if err != nil { if err != nil {
return entry, "", err return entry, "", err
@ -61,7 +65,7 @@ func (s *Service) CreatePaymentUrl(entry database.PaymentEntry) (database.Paymen
return entry, url, nil return entry, url, nil
} }
func (s *Service) InitializePayment(entry database.PaymentEntry) (database.PaymentEntry, string, error) { func (s *Service) InitializePayment(entry database2.PaymentEntry) (database2.PaymentEntry, string, error) {
currency := string(stripe.CurrencyEUR) currency := string(stripe.CurrencyEUR)
productName := "Example product" productName := "Example product"
@ -88,9 +92,13 @@ func (s *Service) InitializePayment(entry database.PaymentEntry) (database.Payme
SuccessURL: stripe.String(s.BackendUrl + "/stripe/success?token=" + entry.Id.String()), SuccessURL: stripe.String(s.BackendUrl + "/stripe/success?token=" + entry.Id.String()),
CancelURL: stripe.String(s.BackendUrl + "/stripe/cancel?token=" + entry.Id.String()), CancelURL: stripe.String(s.BackendUrl + "/stripe/cancel?token=" + entry.Id.String()),
} }
result, err := session.New(params) client := session.Client{
B: stripe.GetBackend(stripe.APIBackend),
Key: s.ApiKey,
}
result, err := client.New(params)
if err != nil { if err != nil {
return database.PaymentEntry{}, "", err return database2.PaymentEntry{}, "", err
} }
entry.State = state.StateInitialized entry.State = state.StateInitialized
entry.PaymentIntentId = &result.PaymentIntent.ID entry.PaymentIntentId = &result.PaymentIntent.ID
@ -98,15 +106,19 @@ func (s *Service) InitializePayment(entry database.PaymentEntry) (database.Payme
return entry, result.URL, nil return entry, result.URL, nil
} }
func (s *Service) CompleteTransaction(entry database.PaymentEntry, amount int64) (database.PaymentEntry, error) { func (s *Service) CompleteTransaction(entry database2.PaymentEntry, amount int64) (database2.PaymentEntry, error) {
params := &stripe.PaymentIntentCaptureParams{ params := &stripe.PaymentIntentCaptureParams{
AmountToCapture: stripe.Int64(amount), AmountToCapture: stripe.Int64(amount),
} }
pi, err := paymentintent.Capture(*entry.PaymentIntentId, params) client := paymentintent.Client{
if err != nil { B: stripe.GetBackend(stripe.APIBackend),
return database.PaymentEntry{}, err Key: s.ApiKey,
} }
log.Printf("received state on completion: %v", pi.Status) pi, err := client.Capture(*entry.PaymentIntentId, params)
if err != nil {
return database2.PaymentEntry{}, err
}
slog.Info("received state on completion", "entry_id", entry.Id.String(), "state", entry.State, "new_state", pi.Status)
newState := determineState(pi.Status) newState := determineState(pi.Status)
entry.State = newState entry.State = newState
if newState == state.StateCompleted || newState == state.StatePending { if newState == state.StateCompleted || newState == state.StatePending {
@ -115,20 +127,24 @@ func (s *Service) CompleteTransaction(entry database.PaymentEntry, amount int64)
return entry, nil return entry, nil
} }
func (s *Service) CancelTransaction(entry database.PaymentEntry) (database.PaymentEntry, error) { func (s *Service) CancelTransaction(entry database2.PaymentEntry) (database2.PaymentEntry, error) {
params := &stripe.PaymentIntentCancelParams{} params := &stripe.PaymentIntentCancelParams{}
pi, err := paymentintent.Cancel(*entry.PaymentIntentId, params) client := paymentintent.Client{
if err != nil { B: stripe.GetBackend(stripe.APIBackend),
return database.PaymentEntry{}, err Key: s.ApiKey,
} }
log.Printf("received state on completion: %v", pi.Status) pi, err := client.Cancel(*entry.PaymentIntentId, params)
if err != nil {
return database2.PaymentEntry{}, err
}
slog.Info("received state on completion", "entry_id", entry.Id.String(), "state", entry.State, "new_state", pi.Status)
if pi.Status == stripe.PaymentIntentStatusCanceled { if pi.Status == stripe.PaymentIntentStatusCanceled {
entry.State = state.StateCanceled entry.State = state.StateCanceled
} }
return entry, nil return entry, nil
} }
func (s *Service) HandleResponse(c *gin.Context, provider *database.PaymentEntryProvider, paymentState state.PaymentState) (string, error) { func (s *Service) HandleResponse(c *gin.Context, provider *database2.PaymentEntryProvider, paymentState state.PaymentState) (string, error) {
id := uuid.MustParse(c.Query("token")) id := uuid.MustParse(c.Query("token"))
entry, err := provider.FetchById(id) entry, err := provider.FetchById(id)
if err != nil { if err != nil {
@ -138,6 +154,6 @@ func (s *Service) HandleResponse(c *gin.Context, provider *database.PaymentEntry
if _, err := provider.UpdateEntry(entry); err != nil { if _, err := provider.UpdateEntry(entry); err != nil {
return "", err return "", err
} }
log.Printf("[%s:%s] received authorization response", entry.Id.String(), entry.State) slog.Info("received authorization response", "entry_id", entry.Id.String(), "state", entry.State)
return "/entries/" + entry.Id.String(), nil return "/entries/" + entry.Id.String(), nil
} }

View File

@ -8,11 +8,11 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
"io" "io"
"log" "log/slog"
"net/http" "net/http"
"net/url" "net/url"
"payment-poc/database" database2 "payment-poc/domain/database"
"payment-poc/state" "payment-poc/domain/state"
"strconv" "strconv"
"time" "time"
) )
@ -29,7 +29,7 @@ type Service struct {
expiration time.Time expiration time.Time
} }
func (s *Service) UpdatePayment(entry database.PaymentEntry) (updatedEntry database.PaymentEntry, err error) { func (s *Service) UpdatePayment(entry database2.PaymentEntry) (updatedEntry database2.PaymentEntry, err error) {
token, err := s.oAuthToken() token, err := s.oAuthToken()
httpResponse, err := createRequest( httpResponse, err := createRequest(
"GET", "GET",
@ -38,19 +38,23 @@ func (s *Service) UpdatePayment(entry database.PaymentEntry) (updatedEntry datab
[]byte{}, []byte{},
) )
if err != nil { if err != nil {
return database.PaymentEntry{}, err return database2.PaymentEntry{}, err
} }
var response TransactionStatusResponse var response TransactionStatusResponse
err = readResponse(httpResponse, &response) err = readResponse(httpResponse, &response)
if err != nil { if err != nil {
return database.PaymentEntry{}, err return database2.PaymentEntry{}, err
} }
newState := determineStatus(response.StatusId) newState := determineStatus(response.StatusId)
if entry.State != newState && newState != "" { if entry.State != newState && newState != "" {
log.Printf("[%s:%s] updated state %s -> %s", entry.Id.String(), entry.State, entry.State, newState) slog.Info("updated state", "entry_id", entry.Id.String(), "state", entry.State, "new_state", newState)
entry.State = newState entry.State = newState
if entry.State == state.StateCompleted {
amount := int64(response.Amount * 100)
entry.Amount = &amount
}
} }
return entry, nil return entry, nil
} }
@ -70,11 +74,11 @@ func determineStatus(id TransactionStatus) state.PaymentState {
case PaymentVoided: case PaymentVoided:
return state.StateVoided return state.StateVoided
} }
log.Printf("Unknonw transactionStatus: %s", string(id)) slog.Info("unknown transaction status", "status", string(id))
return "" return ""
} }
func (s *Service) CreatePaymentUrl(entry database.PaymentEntry) (database.PaymentEntry, string, error) { func (s *Service) CreatePaymentUrl(entry database2.PaymentEntry) (database2.PaymentEntry, string, error) {
entry, err := s.InitializePayment(entry) entry, err := s.InitializePayment(entry)
if err != nil { if err != nil {
return entry, "", err return entry, "", err
@ -82,10 +86,10 @@ func (s *Service) CreatePaymentUrl(entry database.PaymentEntry) (database.Paymen
return entry, "https://demo.vivapayments.com/web/checkout?ref=" + string(*entry.OrderId), nil return entry, "https://demo.vivapayments.com/web/checkout?ref=" + string(*entry.OrderId), nil
} }
func (s *Service) InitializePayment(entry database.PaymentEntry) (database.PaymentEntry, error) { func (s *Service) InitializePayment(entry database2.PaymentEntry) (database2.PaymentEntry, error) {
token, err := s.oAuthToken() token, err := s.oAuthToken()
if err != nil { if err != nil {
return database.PaymentEntry{}, err return database2.PaymentEntry{}, err
} }
request := OrderRequest{ request := OrderRequest{
@ -104,20 +108,20 @@ func (s *Service) InitializePayment(entry database.PaymentEntry) (database.Payme
toJson(request), toJson(request),
) )
if err != nil { if err != nil {
return database.PaymentEntry{}, err return database2.PaymentEntry{}, err
} }
var response OrderResponse var response OrderResponse
err = readResponse(httpResponse, &response) err = readResponse(httpResponse, &response)
if err != nil { if err != nil {
return database.PaymentEntry{}, err return database2.PaymentEntry{}, err
} }
entry.State = state.StateInitialized entry.State = state.StateInitialized
entry.OrderId = &response.OrderId entry.OrderId = &response.OrderId
return entry, nil return entry, nil
} }
func (s *Service) CompleteTransaction(entry database.PaymentEntry, amount int64) (database.PaymentEntry, error) { func (s *Service) CompleteTransaction(entry database2.PaymentEntry, amount int64) (database2.PaymentEntry, error) {
completionRequest := TransactionCompleteRequest{ completionRequest := TransactionCompleteRequest{
Amount: amount, Amount: amount,
CustomerDescription: "Example transaction", CustomerDescription: "Example transaction",
@ -125,54 +129,55 @@ func (s *Service) CompleteTransaction(entry database.PaymentEntry, amount int64)
httpResponse, err := createRequest( httpResponse, err := createRequest(
"POST", "POST",
"https://demo.vivapayments.com/api/transactions/"+entry.TransactionId.String(), "https://demo.vivapayments.com/api/transactions/"+entry.TransactionId.String(),
map[string]string{"authorization": "Bearer " + s.basicAuth(), map[string]string{"authorization": "Basic " + s.basicAuth(),
"content-type": "application/json", "content-type": "application/json",
}, },
toJson(completionRequest), toJson(completionRequest),
) )
if err != nil { if err != nil {
return database.PaymentEntry{}, err return database2.PaymentEntry{}, err
} }
var response TransactionResponse var response TransactionResponse
err = readResponse(httpResponse, &response) err = readResponse(httpResponse, &response)
if err != nil { if err != nil {
return database.PaymentEntry{}, err return database2.PaymentEntry{}, err
} }
if response.StatusId == "F" { if response.StatusId == "F" {
paidAmount := int64(response.Amount * 100) paidAmount := response.Amount * 100
entry.Amount = &paidAmount entry.Amount = &paidAmount
entry.State = state.StateCompleted entry.State = state.StateCompleted
} else { } else {
return database.PaymentEntry{}, errors.New("received invalid status = " + response.StatusId) return database2.PaymentEntry{}, errors.New("received invalid status = " + response.StatusId)
} }
return entry, nil return entry, nil
} }
func (s *Service) CancelTransaction(entry database.PaymentEntry) (database.PaymentEntry, error) { func (s *Service) CancelTransaction(entry database2.PaymentEntry) (database2.PaymentEntry, error) {
amount := strconv.FormatInt(entry.TotalAmount, 10)
httpResponse, err := createRequest( httpResponse, err := createRequest(
"DELETE", "DELETE",
"https://demo.vivapayments.com/api/transactions/"+entry.TransactionId.String(), "https://demo.vivapayments.com/api/transactions/"+entry.TransactionId.String()+"?amount="+amount,
map[string]string{"authorization": "Bearer " + s.basicAuth(), map[string]string{"authorization": "Basic " + s.basicAuth(),
"content-type": "application/json", "content-type": "application/json",
}, },
nil, nil,
) )
if err != nil { if err != nil {
return database.PaymentEntry{}, err return database2.PaymentEntry{}, err
} }
var response TransactionResponse var response TransactionResponse
err = readResponse(httpResponse, &response) err = readResponse(httpResponse, &response)
if err != nil { if err != nil {
return database.PaymentEntry{}, err return database2.PaymentEntry{}, err
} }
if response.StatusId == "F" { if response.StatusId == "F" {
paidAmount := int64(0) paidAmount := int64(0)
entry.Amount = &paidAmount entry.Amount = &paidAmount
entry.State = state.StateVoided entry.State = state.StateVoided
} else { } else {
return database.PaymentEntry{}, errors.New("received invalid status = " + response.StatusId) return database2.PaymentEntry{}, errors.New("received invalid status = " + response.StatusId)
} }
return entry, nil return entry, nil
} }
@ -241,17 +246,17 @@ 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) HandleResponse(c *gin.Context, provider *database.PaymentEntryProvider, state state.PaymentState) (string, error) { func (s *Service) HandleResponse(c *gin.Context, provider *database2.PaymentEntryProvider, state state.PaymentState) (string, error) {
transactionId := uuid.MustParse(c.Query("t")) transactionId := uuid.MustParse(c.Query("t"))
orderId := database.OrderId(c.Query("s")) orderId := database2.OrderId(c.Query("s"))
lang := c.Query("lang") lang := c.Query("lang")
eventId := c.Query("eventId") eventId := c.Query("eventId")
eci := c.Query("eci") eci := c.Query("eci")
log.Printf("[%s] received error response for viva payment", orderId) slog.Info("received error response from viva payment", "order_id", orderId)
entry, err := provider.FetchByOrderId(orderId) entry, err := provider.FetchByOrderId(orderId)
if err != nil { if err != nil {
log.Printf("[%s] couldn't find payment info for viva payment", orderId) slog.Error("couldn't find payment info for viva payment", "order_id", orderId)
return "", err return "", err
} }
@ -265,7 +270,7 @@ func (s *Service) HandleResponse(c *gin.Context, provider *database.PaymentEntry
return "", err return "", err
} }
log.Printf("[%s:%s] received authorization response", entry.Id.String(), entry.State) slog.Info("received authorization response", "entry_id", entry.Id.String(), "state", entry.State)
return "/entries/" + entry.Id.String(), nil return "/entries/" + entry.Id.String(), nil
} }

View File

@ -1,6 +1,8 @@
package viva package viva
import "payment-poc/database" import (
"payment-poc/domain/database"
)
type OrderRequest struct { type OrderRequest struct {
Amount int64 `json:"amount"` Amount int64 `json:"amount"`
@ -26,20 +28,20 @@ type TransactionCompleteRequest struct {
} }
type TransactionResponse struct { type TransactionResponse struct {
Amount float64 `json:"Amount"` Amount int64 `json:"Amount"`
StatusId string `json:"StatusId"` StatusId string `json:"StatusId"`
ErrorCode int64 `json:"ErrorCode"` ErrorCode int64 `json:"ErrorCode"`
ErrorText string `json:"ErrorText"` ErrorText string `json:"ErrorText"`
EventId int64 `json:"EventId"` EventId int64 `json:"EventId"`
Success bool `json:"Success"` Success bool `json:"Success"`
} }
type TransactionStatus string type TransactionStatus string
const ( const (
PaymentSuccessful TransactionStatus = "F" PaymentSuccessful TransactionStatus = "C"
PaymentPending TransactionStatus = "A" PaymentPending TransactionStatus = "A"
PaymentPreauthorized TransactionStatus = "C" PaymentPreauthorized TransactionStatus = "F"
PaymentUnsuccessful TransactionStatus = "E" PaymentUnsuccessful TransactionStatus = "E"
PaymentRefunded TransactionStatus = "R" PaymentRefunded TransactionStatus = "R"
PaymentVoided TransactionStatus = "X" PaymentVoided TransactionStatus = "X"
@ -47,7 +49,7 @@ const (
type TransactionStatusResponse struct { type TransactionStatusResponse struct {
Email string `json:"email"` Email string `json:"email"`
Amount int `json:"amount"` Amount float64 `json:"amount"`
OrderCode database.OrderId `json:"orderCode"` OrderCode database.OrderId `json:"orderCode"`
StatusId TransactionStatus `json:"statusId"` StatusId TransactionStatus `json:"statusId"`
FullName string `json:"fullName"` FullName string `json:"fullName"`

View File

@ -10,10 +10,10 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
"io" "io"
"log" "log/slog"
"net/http" "net/http"
"payment-poc/database" "payment-poc/domain/database"
"payment-poc/state" "payment-poc/domain/state"
"strconv" "strconv"
) )
@ -26,7 +26,7 @@ type Service struct {
func (s *Service) UpdatePayment(entry database.PaymentEntry) (updatedEntry database.PaymentEntry, err error) { func (s *Service) UpdatePayment(entry database.PaymentEntry) (updatedEntry database.PaymentEntry, err error) {
var request = StatusCheckRequest{ var request = StatusCheckRequest{
Version: "2.0", Version: "2.0",
ShopId: s.ShopId, ShopID: s.ShopId,
ShoppingCartId: entry.Id.String(), ShoppingCartId: entry.Id.String(),
Signature: CalculateStatusCheckSignature(s.ShopId, s.ShopSecret, entry.Id.String()), Signature: CalculateStatusCheckSignature(s.ShopId, s.ShopSecret, entry.Id.String()),
} }
@ -47,16 +47,16 @@ func (s *Service) UpdatePayment(entry database.PaymentEntry) (updatedEntry datab
return database.PaymentEntry{}, err return database.PaymentEntry{}, err
} }
if CompareStatusCheckReturnSignature(response.Signature, s.ShopId, s.ShopSecret, response.ActionSuccess, response.ApprovalCode, entry.Id.String()) != nil { if CompareStatusCheckReturnSignature(response.Signature, s.ShopId, s.ShopSecret, response.ActionSuccess, response.ApprovalCode, entry.WsPayOrderId) == nil {
entry.Amount = &response.Amount newValue := int64(response.Amount * 100)
entry.Amount = &newValue
newState := determineState(response) newState := determineState(response)
if entry.State != newState && newState != "" { if entry.State != newState && newState != "" {
log.Printf("Updated state for %s: %s -> %s", entry.Id.String(), entry.State, newState) slog.Info("Updated state", "entry_id", entry.Id.String(), "state", entry.State, "new_state", newState)
entry.State = newState entry.State = newState
} }
entry.WsPayOrderId = response.WsPayOrderId
entry.State = state.StateCompleted
} else { } else {
return database.PaymentEntry{}, errors.New("invalid signature") return database.PaymentEntry{}, errors.New("invalid signature")
} }
@ -89,12 +89,12 @@ func (s *Service) CompleteTransaction(entry database.PaymentEntry, amount int64)
if entry.State == state.StateAccepted { if entry.State == state.StateAccepted {
var request = CompletionRequest{ var request = CompletionRequest{
Version: "2.0", Version: "2.0",
WsPayOrderId: entry.Id.String(), WsPayOrderId: entry.WsPayOrderId,
ShopId: s.ShopId, ShopID: s.ShopId,
ApprovalCode: *entry.ApprovalCode, ApprovalCode: *entry.ApprovalCode,
STAN: *entry.STAN, STAN: *entry.STAN,
Amount: amount, Amount: strconv.FormatInt(amount, 10),
Signature: CalculateCompletionSignature(s.ShopId, s.ShopSecret, entry.Id.String(), *entry.STAN, *entry.ApprovalCode, amount), Signature: CalculateCompletionSignature(s.ShopId, s.ShopSecret, entry.WsPayOrderId, *entry.STAN, *entry.ApprovalCode, amount),
} }
httpResponse, err := createRequest( httpResponse, err := createRequest(
@ -113,7 +113,7 @@ func (s *Service) CompleteTransaction(entry database.PaymentEntry, amount int64)
return database.PaymentEntry{}, err return database.PaymentEntry{}, err
} }
if CompareCompletionReturnSignature(response.Signature, s.ShopId, s.ShopSecret, entry.Id.String(), *entry.STAN, response.ActionSuccess, response.ApprovalCode) != nil { if CompareCompletionReturnSignature(response.Signature, s.ShopId, s.ShopSecret, *entry.STAN, response.ActionSuccess, response.ApprovalCode, entry.WsPayOrderId) == nil {
entry.Amount = &amount entry.Amount = &amount
entry.State = state.StateCompleted entry.State = state.StateCompleted
} else { } else {
@ -129,12 +129,12 @@ func (s *Service) CancelTransaction(entry database.PaymentEntry) (database.Payme
if entry.State == state.StateAccepted { if entry.State == state.StateAccepted {
var request = CompletionRequest{ var request = CompletionRequest{
Version: "2.0", Version: "2.0",
WsPayOrderId: entry.Id.String(), WsPayOrderId: entry.WsPayOrderId,
ShopId: s.ShopId, ShopID: s.ShopId,
ApprovalCode: *entry.ApprovalCode, ApprovalCode: *entry.ApprovalCode,
STAN: *entry.STAN, STAN: *entry.STAN,
Amount: entry.TotalAmount, Amount: strconv.FormatInt(entry.TotalAmount, 10),
Signature: CalculateCompletionSignature(s.ShopId, s.ShopSecret, entry.Id.String(), *entry.STAN, *entry.ApprovalCode, entry.TotalAmount), Signature: CalculateCompletionSignature(s.ShopId, s.ShopSecret, entry.WsPayOrderId, *entry.STAN, *entry.ApprovalCode, entry.TotalAmount),
} }
httpResponse, err := createRequest( httpResponse, err := createRequest(
@ -153,8 +153,8 @@ func (s *Service) CancelTransaction(entry database.PaymentEntry) (database.Payme
return database.PaymentEntry{}, err return database.PaymentEntry{}, err
} }
if CompareCompletionReturnSignature(response.Signature, s.ShopId, s.ShopSecret, entry.Id.String(), *entry.STAN, response.ActionSuccess, response.ApprovalCode) != nil { if CompareCompletionReturnSignature(response.Signature, s.ShopId, s.ShopSecret, *entry.STAN, response.ActionSuccess, response.ApprovalCode, entry.WsPayOrderId) == nil {
entry.State = state.StateCanceled entry.State = state.StateVoided
} else { } else {
return database.PaymentEntry{}, errors.New("invalid signature") return database.PaymentEntry{}, errors.New("invalid signature")
} }
@ -222,6 +222,7 @@ func (s *Service) HandleSuccessResponse(c *gin.Context, provider *database.Payme
entry.Success = &response.Success entry.Success = &response.Success
entry.ApprovalCode = &response.ApprovalCode entry.ApprovalCode = &response.ApprovalCode
entry.State = state.StateAccepted entry.State = state.StateAccepted
entry.WsPayOrderId = response.WsPayOrderId
if _, err := provider.UpdateEntry(entry); err != nil { if _, err := provider.UpdateEntry(entry); err != nil {
return "", err return "", err
@ -275,7 +276,7 @@ func CalculateFormSignature(shopId string, secret string, cartId string, amount
return hex.EncodeToString(hash.Sum(nil)) return hex.EncodeToString(hash.Sum(nil))
} }
func CalculateCompletionSignature(shopId string, secret string, cartId string, stan string, approvalCode string, amount int64) string { func CalculateCompletionSignature(shopId string, secret string, wsPayOrderId string, stan string, approvalCode string, amount int64) string {
/** /**
Represents a signature created from string formatted from following values in a following order using Represents a signature created from string formatted from following values in a following order using
SHA512 algorithm: SHA512 algorithm:
@ -290,7 +291,7 @@ func CalculateCompletionSignature(shopId string, secret string, cartId string, s
SecretKey SecretKey
WsPayOrderId WsPayOrderId
*/ */
signature := shopId + cartId + secret + stan + secret + approvalCode + secret + strconv.FormatInt(amount, 10) + secret + cartId signature := shopId + wsPayOrderId + secret + stan + secret + approvalCode + secret + strconv.FormatInt(amount, 10) + secret + wsPayOrderId
hash := sha512.New() hash := sha512.New()
hash.Write([]byte(signature)) hash.Write([]byte(signature))
return hex.EncodeToString(hash.Sum(nil)) return hex.EncodeToString(hash.Sum(nil))
@ -320,7 +321,7 @@ func CompareFormReturnSignature(signature string, shopId string, secret string,
} }
} }
func CompareCompletionReturnSignature(signature string, shopId string, secret string, stan string, actionSuccess string, approvalCode string, cartId string) error { func CompareCompletionReturnSignature(signature string, shopId string, secret string, stan string, actionSuccess string, approvalCode string, wsPayOrderId string) error {
/** /**
Represents a signature created from string formatted from following values in a following order using Represents a signature created from string formatted from following values in a following order using
SHA512 algorithm: SHA512 algorithm:
@ -333,7 +334,7 @@ func CompareCompletionReturnSignature(signature string, shopId string, secret st
WsPayOrderId WsPayOrderId
Merchant should validate this signature to make sure that the request is originating from WSPayForm. Merchant should validate this signature to make sure that the request is originating from WSPayForm.
*/ */
calculatedSignature := shopId + secret + stan + actionSuccess + secret + approvalCode + cartId calculatedSignature := shopId + secret + stan + actionSuccess + secret + approvalCode + wsPayOrderId
hash := sha512.New() hash := sha512.New()
hash.Write([]byte(calculatedSignature)) hash.Write([]byte(calculatedSignature))
if hex.EncodeToString(hash.Sum(nil)) == signature { if hex.EncodeToString(hash.Sum(nil)) == signature {
@ -343,7 +344,7 @@ func CompareCompletionReturnSignature(signature string, shopId string, secret st
} }
} }
func CompareStatusCheckReturnSignature(signature string, shopId string, secret string, actionSuccess string, approvalCode string, cartId string) error { func CompareStatusCheckReturnSignature(signature string, shopId string, secret string, actionSuccess string, approvalCode string, wsPayOrderId string) error {
/** /**
Represents a signature created from string formatted from following values in a following order using Represents a signature created from string formatted from following values in a following order using
SHA512 algorithm: SHA512 algorithm:
@ -356,7 +357,7 @@ func CompareStatusCheckReturnSignature(signature string, shopId string, secret s
ApprovalCode ApprovalCode
WsPayOrderId WsPayOrderId
*/ */
calculatedSignature := shopId + secret + actionSuccess + approvalCode + secret + shopId + approvalCode + cartId calculatedSignature := shopId + secret + actionSuccess + approvalCode + secret + shopId + approvalCode + wsPayOrderId
hash := sha512.New() hash := sha512.New()
hash.Write([]byte(calculatedSignature)) hash.Write([]byte(calculatedSignature))
if hex.EncodeToString(hash.Sum(nil)) == signature { if hex.EncodeToString(hash.Sum(nil)) == signature {

View File

@ -84,16 +84,16 @@ type FormCancel struct {
type CompletionRequest struct { type CompletionRequest struct {
Version string Version string
WsPayOrderId string WsPayOrderId string
ShopId string ShopID string
ApprovalCode string ApprovalCode string
STAN string STAN string
Amount int64 Amount string
Signature string Signature string
} }
type CompletionResponse struct { type CompletionResponse struct {
WsPayOrderId string WsPayOrderId string
ShopId string ShopID string
ApprovalCode string ApprovalCode string
STAN string STAN string
ErrorMessage string ErrorMessage string
@ -103,7 +103,7 @@ type CompletionResponse struct {
type StatusCheckRequest struct { type StatusCheckRequest struct {
Version string Version string
ShopId string ShopID string
ShoppingCartId string ShoppingCartId string
Signature string Signature string
} }
@ -115,8 +115,8 @@ type StatusCheckResponse struct {
ApprovalCode string ApprovalCode string
ShopID string ShopID string
ShoppingCartID string ShoppingCartID string
Amount int64 Amount float64
CurrencyCode string CurrencyCode int
ActionSuccess string ActionSuccess string
Success string // deprecated Success string // deprecated
Authorized string Authorized string
@ -125,7 +125,7 @@ type StatusCheckResponse struct {
Refunded string Refunded string
PaymentPlan string PaymentPlan string
Partner string Partner string
OnSite int OnSite string
CreditCardName string CreditCardName string
CreditCardNumber string CreditCardNumber string
ECI string ECI string

4
go.mod
View File

@ -1,6 +1,6 @@
module payment-poc module payment-poc
go 1.19 go 1.22
require ( require (
github.com/gin-gonic/gin v1.9.1 github.com/gin-gonic/gin v1.9.1
@ -8,6 +8,7 @@ require (
github.com/jmoiron/sqlx v1.3.5 github.com/jmoiron/sqlx v1.3.5
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/lib/pq v1.10.9 github.com/lib/pq v1.10.9
github.com/stripe/stripe-go/v72 v72.122.0
) )
require ( require (
@ -26,7 +27,6 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/stripe/stripe-go/v72 v72.122.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect github.com/ugorji/go/codec v1.2.11 // indirect
golang.org/x/arch v0.3.0 // indirect golang.org/x/arch v0.3.0 // indirect

446
main.go
View File

@ -2,399 +2,51 @@ package main
import ( import (
"embed" "embed"
"errors"
"fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"github.com/stripe/stripe-go/v72" "log/slog"
"html/template"
"log"
"net/http" "net/http"
"os" "os"
"payment-poc/database" "payment-poc/api"
"payment-poc/migration" "payment-poc/migration"
"payment-poc/providers/mock" "runtime/debug"
stripe2 "payment-poc/providers/stripe"
"payment-poc/providers/viva"
wspay2 "payment-poc/providers/wspay"
"payment-poc/state"
"strconv"
"strings" "strings"
"time"
) )
//go:embed db/dev/*.sql //go:embed db/dev/*.sql
var devMigrations embed.FS var devMigrations embed.FS
type PaymentProvider interface {
CreatePaymentUrl(entry database.PaymentEntry) (updatedEntry database.PaymentEntry, url string, err error)
CompleteTransaction(entry database.PaymentEntry, amount int64) (database.PaymentEntry, error)
CancelTransaction(entry database.PaymentEntry) (database.PaymentEntry, error)
UpdatePayment(entry database.PaymentEntry) (updatedEntry database.PaymentEntry, err error)
}
func init() { func init() {
godotenv.Load() godotenv.Load()
if !hasProfile("dev") {
log.SetPrefix("") gin.SetMode(gin.ReleaseMode)
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) }
if value := os.Getenv("LOG_FORMAT"); value == "json" {
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{AddSource: true})))
}
} }
func main() { func main() {
commit, buildTime := buildInfo()
slog.Info("build info", slog.String("commit", commit), slog.String("time", buildTime))
client, err := connectToDb() client, err := connectToDb()
if err != nil { if err != nil {
log.Fatalf("couldn't connect to db: %v", err) slog.Error("couldn't connect to db", slog.String("err", err.Error()))
os.Exit(1)
} }
if err := migration.InitializeMigrations(client, devMigrations); err != nil { if err := migration.InitializeMigrations(client, devMigrations); err != nil {
log.Fatalf("couldn't execute migrations: %v", err) slog.Error("couldn't finish migration", slog.String("err", err.Error()))
os.Exit(1)
} }
g := gin.Default() server := api.SetupServer(client)
if !hasProfile("no-auth") { port := ":" + getOrDefault("SERVER_PORT", "5281")
g.Use(gin.BasicAuth(getAccounts())) slog.Info("app is ready", slog.String("port", port))
if err := http.ListenAndServe(port, server); err != nil {
slog.Error("Couldn't start server!\n", slog.Any("err", err.Error()))
} }
g.SetFuncMap(template.FuncMap{
"formatCurrency": formatCurrency,
"formatCurrencyPtr": formatCurrencyPtr,
"decimalCurrency": decimalCurrency,
"formatState": formatState,
"omitempty": omitempty,
})
g.NoRoute(func(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{"message": "no action on given url", "created": time.Now()})
})
g.NoMethod(func(c *gin.Context) {
c.JSON(http.StatusMethodNotAllowed, gin.H{"message": "no action on given method", "created": time.Now()})
})
backendUrl := envMustExist("BACKEND_URL")
paymentGateways := map[state.PaymentGateway]PaymentProvider{}
entryProvider := &database.PaymentEntryProvider{DB: client}
g.LoadHTMLGlob("./templates/*.gohtml")
if hasProfile(string(state.GatewayMock)) {
mockService := mock.Service{
BackendUrl: backendUrl,
}
mockHandlers(g.Group("mock"), entryProvider, &mockService)
paymentGateways[state.GatewayMock] = &mockService
log.Printf("Registered provider: %s", state.GatewayMock)
}
if hasProfile(string(state.GatewayWsPay)) {
wspayService := wspay2.Service{
ShopId: envMustExist("WSPAY_SHOP_ID"),
ShopSecret: envMustExist("WSPAY_SHOP_SECRET"),
BackendUrl: backendUrl,
}
wsPayHandlers(g.Group("wspay"), entryProvider, &wspayService)
paymentGateways[state.GatewayWsPay] = &wspayService
log.Printf("Registered provider: %s", state.GatewayWsPay)
}
if hasProfile(string(state.GatewayStripe)) {
stripeService := stripe2.Service{
ApiKey: envMustExist("STRIPE_KEY"),
BackendUrl: backendUrl,
}
stripeHandlers(g.Group("stripe"), entryProvider, &stripeService)
paymentGateways[state.GatewayStripe] = &stripeService
stripe.Key = envMustExist("STRIPE_KEY")
log.Printf("Registered provider: %s", state.GatewayStripe)
}
if hasProfile(string(state.GatewayVivaWallet)) {
vivaService := viva.Service{
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"),
}
vivaHandlers(g.Group("viva"), entryProvider, &vivaService)
paymentGateways[state.GatewayVivaWallet] = &vivaService
log.Printf("Registered provider: %s", state.GatewayVivaWallet)
}
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, "Gateways": mapGateways(paymentGateways)})
})
g.GET("/methods/: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
}
entry, err := entryProvider.CreateEntry(database.PaymentEntry{
Gateway: gateway,
State: state.StatePreinitialized,
TotalAmount: amount,
})
log.Printf("[%s:%s] creating payment with gateway '%s' for '%f'", entry.Id.String(), entry.State, gateway, float64(amount)/100.0)
if entry, url, err := paymentGateway.CreatePaymentUrl(entry); err == nil {
log.Printf("[%s:%s] created redirect url", entry.Id, entry.State)
entryProvider.UpdateEntry(entry)
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.GET("/entries/:id", func(c *gin.Context) {
id := uuid.MustParse(c.Param("id"))
entry, err := entryProvider.FetchById(id)
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
c.HTML(200, "info.gohtml", gin.H{"Entry": entry})
})
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
}
log.Printf("[%s:%s] completing payment with amount %f", id.String(), entry.State, float64(amount)/100.0)
entry, err = paymentGateway.CompleteTransaction(entry, amount)
if err == nil {
entryProvider.UpdateEntry(entry)
log.Printf("[%s:%s] completed payment", id.String(), entry.State)
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 {
log.Printf("[%s:%s] canceling payment", id.String(), entry.State)
entry, err = paymentGateway.CancelTransaction(entry)
if err == nil {
entryProvider.UpdateEntry(entry)
log.Printf("[%s:%s] canceled payment", id.String(), entry.State)
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/refresh", 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 {
log.Printf("[%s:%s] fetching payment info", entry.Id.String(), entry.State)
entry, err = paymentGateway.UpdatePayment(entry)
if err == nil {
entryProvider.UpdateEntry(entry)
log.Printf("[%s:%s] fetched payment info", entry.Id.String(), entry.State)
}
c.Redirect(http.StatusSeeOther, "/entries/"+id.String())
} else {
if err != nil {
c.AbortWithError(http.StatusInternalServerError, errors.New("payment gateway not supported: "+string(entry.Gateway)))
return
}
}
})
log.Fatal(http.ListenAndServe(":5281", g))
}
func mockHandlers(g *gin.RouterGroup, provider *database.PaymentEntryProvider, mockService *mock.Service) {
g.GET("/gateway/:id", func(c *gin.Context) {
id := uuid.MustParse(c.Param("id"))
entry, err := provider.FetchById(id)
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
c.HTML(http.StatusOK, "mock_gateway.gohtml", gin.H{"Entry": entry})
})
g.GET("success", func(c *gin.Context) {
url, err := mockService.HandleResponse(c, provider, state.StateAccepted)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.Redirect(http.StatusSeeOther, url)
})
g.GET("error", func(c *gin.Context) {
url, err := mockService.HandleResponse(c, provider, state.StateError)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.Redirect(http.StatusSeeOther, url)
})
}
func mapGateways(gateways map[state.PaymentGateway]PaymentProvider) map[string]string {
providerMap := map[string]string{}
for key := range gateways {
providerMap[string(key)] = mapGatewayName(key)
}
return providerMap
}
func mapGatewayName(key state.PaymentGateway) string {
switch key {
case state.GatewayStripe:
return "Stripe"
case state.GatewayVivaWallet:
return "Viva wallet"
case state.GatewayWsPay:
return "WsPay"
case state.GatewayMock:
return "mock"
}
return ""
}
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
case string(state.GatewayMock):
return state.GatewayMock, nil
}
return "", errors.New("unknown gateway: " + gateway)
}
func getAccounts() gin.Accounts {
auth := strings.Split(envMustExist("AUTH"), ":")
return gin.Accounts{auth[0]: auth[1]}
}
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 vivaHandlers(g *gin.RouterGroup, provider *database.PaymentEntryProvider, vivaService *viva.Service) {
g.GET("success", func(c *gin.Context) {
url, err := vivaService.HandleResponse(c, provider, state.StateAccepted)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.Redirect(http.StatusSeeOther, url)
})
g.GET("error", func(c *gin.Context) {
url, err := vivaService.HandleResponse(c, provider, state.StateError)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.Redirect(http.StatusSeeOther, url)
})
}
func stripeHandlers(g *gin.RouterGroup, provider *database.PaymentEntryProvider, stripeService *stripe2.Service) {
g.GET("success", func(c *gin.Context) {
url, err := stripeService.HandleResponse(c, provider, state.StateAccepted)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.Redirect(http.StatusSeeOther, url)
})
g.GET("error", func(c *gin.Context) {
url, err := stripeService.HandleResponse(c, provider, state.StateError)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.Redirect(http.StatusSeeOther, url)
})
}
func wsPayHandlers(g *gin.RouterGroup, provider *database.PaymentEntryProvider, wspayService *wspay2.Service) {
g.GET("success", func(c *gin.Context) {
url, err := wspayService.HandleSuccessResponse(c, provider)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.Redirect(http.StatusSeeOther, url)
})
g.GET("error", func(c *gin.Context) {
url, err := wspayService.HandleErrorResponse(c, provider, state.StateError)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.Redirect(http.StatusSeeOther, url)
})
g.GET("cancel", func(c *gin.Context) {
url, err := wspayService.HandleErrorResponse(c, provider, state.StateCanceled)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.Redirect(http.StatusSeeOther, url)
})
} }
func hasProfile(profile string) bool { func hasProfile(profile string) bool {
@ -407,47 +59,25 @@ func hasProfile(profile string) bool {
return false return false
} }
func formatState(stt state.PaymentState) string { func buildInfo() (string, string) {
switch stt { revision := ""
case state.StateCanceled: buildTime := ""
return "Otkazana"
case state.StateVoided: if info, ok := debug.ReadBuildInfo(); ok {
return "Poništena" for _, setting := range info.Settings {
case state.StateAccepted: if setting.Key == "vcs.revision" {
return "Predautorizirana" revision = setting.Value
case state.StateError: } else if setting.Key == "vcs.time" {
return "Greška" buildTime = setting.Value
case state.StatePreinitialized: }
return "Predinicijalizirana" }
case state.StateInitialized:
return "Inicijalizirana"
case state.StateCanceledInitialization:
return "Otkazana tijekom izrade"
case state.StateCompleted:
return "Autorizirana"
} }
return "nepoznato stanje '" + string(stt) + "'" return revision, buildTime
} }
func formatCurrency(current int64) string { func getOrDefault(env string, defaultValue string) string {
return fmt.Sprintf("%d,%02d", current/100, current%100) if value, present := os.LookupEnv(env); present {
} return value
func formatCurrencyPtr(current *int64) string {
if current != nil {
return fmt.Sprintf("%d,%02d", (*current)/100, (*current)%100)
} else {
return "-"
} }
} return defaultValue
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

@ -1,30 +1,18 @@
# scripts for building app # scripts for building app
# requires go 1.19+ and git installed # requires go 1.22+ and git installed
VERSION := 0.1.0 VERSION := $(shell git describe --tags --always)
serve:
go run ./...
setup:
go get
docker-dev: docker-dev:
docker image build -t registry.s2internal.com/opgdirekt/payment-poc/backend:$(VERSION)-dev . docker image build -t registry.bbr-dev.info/payment-poc/backend/dev:latest .
docker tag registry.s2internal.com/opgdirekt/payment-poc/backend:$(VERSION)-dev registry.s2internal.com/opgdirekt/payment-poc/backend:latest-dev docker image push registry.bbr-dev.info/payment-poc/backend/dev:latest
docker image push registry.s2internal.com/opgdirekt/payment-poc/backend:$(VERSION)-dev
docker image push registry.s2internal.com/opgdirekt/payment-poc/backend:latest-dev
docker-prod: docker-prod:
docker image build -t registry.s2internal.com/opgdirekt/payment-poc/backend:$(VERSION) . docker image build -t registry.bbr-dev.info/payment-poc/backend:$(VERSION) .
docker tag registry.s2internal.com/opgdirekt/payment-poc/backend:$(VERSION) registry.s2internal.com/opgdirekt/payment-poc/backend:latest docker tag registry.bbr-dev.info/payment-poc/backend:$(VERSION) registry.bbr-dev.info/payment-poc/backend:latest
docker image push registry.s2internal.com/opgdirekt/payment-poc/backend:$(VERSION) docker image push registry.bbr-dev.info/payment-poc/backend:$(VERSION)
docker image push registry.s2internal.com/opgdirekt/payment-poc/backend:latest docker image push registry.bbr-dev.info/payment-poc/backend:latest
release:
git tag $(VERSION)
git push origin $(VERSION)
test: test:
go test ./... go test ./...

View File

@ -4,10 +4,11 @@ import (
"context" "context"
"crypto/sha256" "crypto/sha256"
"encoding/base64" "encoding/base64"
"errors"
"fmt" "fmt"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"io/fs" "io/fs"
"log" "log/slog"
"sort" "sort"
"strings" "strings"
"time" "time"
@ -92,7 +93,8 @@ func validateMigrations(db *sqlx.DB, migrations map[string]Migration, migrationF
} }
func executeMigration(db *sqlx.DB, name string, script string) error { func executeMigration(db *sqlx.DB, name string, script string) error {
log.Printf("[INFO] script='%s' | migrations - executing", name) logger := slog.Default().With(slog.String("script", name))
logger.Info("migrations - executing")
tx := db.MustBeginTx(context.Background(), nil) tx := db.MustBeginTx(context.Background(), nil)
var err error = nil var err error = nil
if _, e := tx.Exec(script); e != nil { if _, e := tx.Exec(script); e != nil {
@ -102,10 +104,10 @@ func executeMigration(db *sqlx.DB, name string, script string) error {
err = e err = e
} }
if err != nil { if err != nil {
log.Printf("[ERROR] script='%s' | migrations - failed executing", name) logger.Error("migrations - failed executing", slog.String("err", err.Error()))
tx.Rollback() tx.Rollback()
} else { } else {
log.Printf("[INFO] script='%s' | migrations - succesfully executed", name) logger.Info("migrations - successfully executed")
tx.Commit() tx.Commit()
} }
return err return err
@ -119,9 +121,9 @@ func validateMigration(name string, migration Migration, script string) error {
calculatedHash := hash(script) calculatedHash := hash(script)
if calculatedHash != migration.Hash { if calculatedHash != migration.Hash {
err := fmt.Sprintf("migrations - mismatch in hash for %s (expected '%s', calculated '%s')", name, migration.Hash, calculatedHash) err := errors.New(fmt.Sprintf("migrations - mismatch in hash for %s (expected '%s', calculated '%s')", name, migration.Hash, calculatedHash))
log.Printf("[ERROR] script='%s' err='%s' | migrations - failed executing", script, err) slog.Error("migrations - failed validation", slog.String("script", name), slog.String("err", err.Error()))
return fmt.Errorf("migrations - mismatch in hashes for %s", name) return err
} }
return nil return nil
} }

View File

@ -1,44 +0,0 @@
package mock
import (
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"log"
"payment-poc/database"
"payment-poc/state"
)
type Service struct {
BackendUrl string
}
func (s Service) UpdatePayment(entry database.PaymentEntry) (updatedEntry database.PaymentEntry, err error) {
return entry, nil
}
func (s Service) CreatePaymentUrl(entry database.PaymentEntry) (updateEntry database.PaymentEntry, url string, err error) {
return entry, "/mock/gateway/" + entry.Id.String(), nil
}
func (s Service) CompleteTransaction(entry database.PaymentEntry, amount int64) (database.PaymentEntry, error) {
entry.Amount = &amount
entry.State = state.StateCompleted
return entry, nil
}
func (s Service) CancelTransaction(entry database.PaymentEntry) (database.PaymentEntry, error) {
entry.State = state.StateVoided
return entry, nil
}
func (s Service) HandleResponse(c *gin.Context, provider *database.PaymentEntryProvider, paymentState state.PaymentState) (string, error) {
id := uuid.MustParse(c.Query("id"))
entry, err := provider.FetchById(id)
if err != nil {
return "", err
}
entry.State = paymentState
_, err = provider.UpdateEntry(entry)
log.Printf("[%s:%s] received authorization response", entry.Id.String(), entry.State)
return "/entries/" + id.String(), err
}

View File

@ -1,21 +0,0 @@
package main
import (
"encoding/json"
"log"
"net/http"
)
func render[T any](w http.ResponseWriter, r *http.Request, status int, response T) error {
if body, err := json.MarshalIndent(response, "", " "); err == nil {
w.Header().Add("content-type", "application/json")
w.WriteHeader(status)
_, err = w.Write(body)
return err
} else {
w.WriteHeader(http.StatusInternalServerError)
log.Printf("couldn't parse response")
_, err = w.Write([]byte{})
return err
}
}

View File

@ -20,8 +20,8 @@
<section class="container"> <section class="container">
<h2>Mock gateway {{.Entry.Id.String}}</h2> <h2>Mock gateway {{.Entry.Id.String}}</h2>
<p>{{formatCurrency .Entry.TotalAmount}}</p> <p>{{formatCurrency .Entry.TotalAmount}}</p>
<a href="/providers/mock/success?id={{.Entry.Id.String}}" class="btn btn-success">Potvrdi plaćanje</a> <a href="/mock/success?id={{.Entry.Id.String}}" class="btn btn-success">Potvrdi plaćanje</a>
<a href="/providers/mock/error?id={{.Entry.Id.String}}" class="btn btn-danger">Otkaži plaćanje</a> <a href="/mock/error?id={{.Entry.Id.String}}" class="btn btn-danger">Otkaži plaćanje</a>
</section> </section>
</body> </body>
</html> </html>