diff --git a/api/api.go b/api/api.go new file mode 100644 index 0000000..481e057 --- /dev/null +++ b/api/api.go @@ -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 + } +} diff --git a/api/middleware.go b/api/middleware.go new file mode 100644 index 0000000..0b6899b --- /dev/null +++ b/api/middleware.go @@ -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 + } +} diff --git a/api/routes.go b/api/routes.go new file mode 100644 index 0000000..75ef988 --- /dev/null +++ b/api/routes.go @@ -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)) + } +} diff --git a/api/server.go b/api/server.go new file mode 100644 index 0000000..e7359d9 --- /dev/null +++ b/api/server.go @@ -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 +} diff --git a/api/templates.go b/api/templates.go new file mode 100644 index 0000000..567ab26 --- /dev/null +++ b/api/templates.go @@ -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 +} diff --git a/db.go b/db.go index 85fa139..d1802ef 100644 --- a/db.go +++ b/db.go @@ -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) { host := envMustExist("PSQL_HOST") port := envMustExist("PSQL_PORT") user := envMustExist("PSQL_USER") password := envMustExist("PSQL_PASSWORD") 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", - host, port, user, password, dbname) + psqlInfo := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s search_path=%s", + host, port, user, password, dbname, sslMode, schema) db, err := sqlx.Open("postgres", psqlInfo) if err != nil { diff --git a/db/dev/v1_0.sql b/db/dev/v1_0.sql index 0d3ecce..c08a2bb 100644 --- a/db/dev/v1_0.sql +++ b/db/dev/v1_0.sql @@ -17,6 +17,7 @@ CREATE TABLE IF NOT EXISTS "payment_entry" "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, diff --git a/db/prod/v1_0.sql b/db/prod/v1_0.sql index e69de29..c08a2bb 100644 --- a/db/prod/v1_0.sql +++ b/db/prod/v1_0.sql @@ -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) +); diff --git a/database/model.go b/domain/database/model.go similarity index 93% rename from database/model.go rename to domain/database/model.go index f53425a..06bed02 100644 --- a/database/model.go +++ b/domain/database/model.go @@ -2,7 +2,7 @@ package database import ( "github.com/google/uuid" - "payment-poc/state" + "payment-poc/domain/state" "time" ) @@ -31,6 +31,7 @@ type PaymentEntry struct { PaymentIntentId *string `db:"payment_intent_id"` // wspay field + WsPayOrderId string `db:"ws_pay_order_id"` ShoppingCardID *string `db:"shopping_card_id"` STAN *string `db:"stan"` Success *int `db:"success"` diff --git a/database/provider.go b/domain/database/provider.go similarity index 93% rename from database/provider.go rename to domain/database/provider.go index ab0099c..b002497 100644 --- a/database/provider.go +++ b/domain/database/provider.go @@ -29,8 +29,8 @@ func (p *PaymentEntryProvider) UpdateEntry(entry PaymentEntry) (PaymentEntry, er currentTime := time.Now() entry.Modified = ¤tTime - _, 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, + _, 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.WsPayOrderId, ) if err != nil { return PaymentEntry{}, err diff --git a/domain/providers/mock/service.go b/domain/providers/mock/service.go new file mode 100644 index 0000000..b73788e --- /dev/null +++ b/domain/providers/mock/service.go @@ -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 +} diff --git a/domain/providers/provider.go b/domain/providers/provider.go new file mode 100644 index 0000000..5c9db24 --- /dev/null +++ b/domain/providers/provider.go @@ -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) +} diff --git a/providers/stripe/service.go b/domain/providers/stripe/service.go similarity index 63% rename from providers/stripe/service.go rename to domain/providers/stripe/service.go index 04e68e7..9c7c748 100644 --- a/providers/stripe/service.go +++ b/domain/providers/stripe/service.go @@ -6,9 +6,9 @@ import ( "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" + "log/slog" + database2 "payment-poc/domain/database" + "payment-poc/domain/state" ) type Service struct { @@ -16,15 +16,19 @@ type Service struct { BackendUrl string } -func (s *Service) UpdatePayment(entry database.PaymentEntry) (updatedEntry database.PaymentEntry, err error) { - pi, err := paymentintent.Get(*entry.PaymentIntentId, nil) +func (s *Service) UpdatePayment(entry database2.PaymentEntry) (updatedEntry database2.PaymentEntry, err error) { + client := paymentintent.Client{ + B: stripe.GetBackend(stripe.APIBackend), + Key: s.ApiKey, + } + pi, err := client.Get(*entry.PaymentIntentId, nil) if err != nil { return entry, err } newState := determineState(pi.Status) 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 { entry.Amount = &pi.AmountReceived } @@ -53,7 +57,7 @@ func determineState(status stripe.PaymentIntentStatus) state.PaymentState { 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) if err != nil { return entry, "", err @@ -61,7 +65,7 @@ func (s *Service) CreatePaymentUrl(entry database.PaymentEntry) (database.Paymen 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) 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()), 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 { - return database.PaymentEntry{}, "", err + return database2.PaymentEntry{}, "", err } entry.State = state.StateInitialized entry.PaymentIntentId = &result.PaymentIntent.ID @@ -98,15 +106,19 @@ func (s *Service) InitializePayment(entry database.PaymentEntry) (database.Payme 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{ AmountToCapture: stripe.Int64(amount), } - pi, err := paymentintent.Capture(*entry.PaymentIntentId, params) - if err != nil { - return database.PaymentEntry{}, err + client := paymentintent.Client{ + B: stripe.GetBackend(stripe.APIBackend), + 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) entry.State = newState if newState == state.StateCompleted || newState == state.StatePending { @@ -115,20 +127,24 @@ func (s *Service) CompleteTransaction(entry database.PaymentEntry, amount int64) 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{} - pi, err := paymentintent.Cancel(*entry.PaymentIntentId, params) - if err != nil { - return database.PaymentEntry{}, err + client := paymentintent.Client{ + B: stripe.GetBackend(stripe.APIBackend), + 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 { entry.State = state.StateCanceled } 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")) entry, err := provider.FetchById(id) if err != nil { @@ -138,6 +154,6 @@ func (s *Service) HandleResponse(c *gin.Context, provider *database.PaymentEntry if _, err := provider.UpdateEntry(entry); err != nil { 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 } diff --git a/providers/viva/service.go b/domain/providers/viva/service.go similarity index 73% rename from providers/viva/service.go rename to domain/providers/viva/service.go index baaa5b4..022a33c 100644 --- a/providers/viva/service.go +++ b/domain/providers/viva/service.go @@ -8,11 +8,11 @@ import ( "github.com/gin-gonic/gin" "github.com/google/uuid" "io" - "log" + "log/slog" "net/http" "net/url" - "payment-poc/database" - "payment-poc/state" + database2 "payment-poc/domain/database" + "payment-poc/domain/state" "strconv" "time" ) @@ -29,7 +29,7 @@ type Service struct { 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() httpResponse, err := createRequest( "GET", @@ -38,19 +38,23 @@ func (s *Service) UpdatePayment(entry database.PaymentEntry) (updatedEntry datab []byte{}, ) if err != nil { - return database.PaymentEntry{}, err + return database2.PaymentEntry{}, err } var response TransactionStatusResponse err = readResponse(httpResponse, &response) if err != nil { - return database.PaymentEntry{}, err + return database2.PaymentEntry{}, err } newState := determineStatus(response.StatusId) 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 + if entry.State == state.StateCompleted { + amount := int64(response.Amount * 100) + entry.Amount = &amount + } } return entry, nil } @@ -70,11 +74,11 @@ func determineStatus(id TransactionStatus) state.PaymentState { case PaymentVoided: return state.StateVoided } - log.Printf("Unknonw transactionStatus: %s", string(id)) + slog.Info("unknown transaction status", "status", string(id)) 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) if err != nil { 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 } -func (s *Service) InitializePayment(entry database.PaymentEntry) (database.PaymentEntry, error) { +func (s *Service) InitializePayment(entry database2.PaymentEntry) (database2.PaymentEntry, error) { token, err := s.oAuthToken() if err != nil { - return database.PaymentEntry{}, err + return database2.PaymentEntry{}, err } request := OrderRequest{ @@ -104,20 +108,20 @@ func (s *Service) InitializePayment(entry database.PaymentEntry) (database.Payme toJson(request), ) if err != nil { - return database.PaymentEntry{}, err + return database2.PaymentEntry{}, err } var response OrderResponse err = readResponse(httpResponse, &response) if err != nil { - return database.PaymentEntry{}, err + return database2.PaymentEntry{}, err } entry.State = state.StateInitialized entry.OrderId = &response.OrderId 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{ Amount: amount, CustomerDescription: "Example transaction", @@ -125,54 +129,55 @@ func (s *Service) CompleteTransaction(entry database.PaymentEntry, amount int64) httpResponse, err := createRequest( "POST", "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", }, toJson(completionRequest), ) if err != nil { - return database.PaymentEntry{}, err + return database2.PaymentEntry{}, err } var response TransactionResponse err = readResponse(httpResponse, &response) if err != nil { - return database.PaymentEntry{}, err + return database2.PaymentEntry{}, err } if response.StatusId == "F" { - paidAmount := int64(response.Amount * 100) + paidAmount := response.Amount * 100 entry.Amount = &paidAmount entry.State = state.StateCompleted } else { - return database.PaymentEntry{}, errors.New("received invalid status = " + response.StatusId) + return database2.PaymentEntry{}, errors.New("received invalid status = " + response.StatusId) } 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( "DELETE", - "https://demo.vivapayments.com/api/transactions/"+entry.TransactionId.String(), - map[string]string{"authorization": "Bearer " + s.basicAuth(), + "https://demo.vivapayments.com/api/transactions/"+entry.TransactionId.String()+"?amount="+amount, + map[string]string{"authorization": "Basic " + s.basicAuth(), "content-type": "application/json", }, nil, ) if err != nil { - return database.PaymentEntry{}, err + return database2.PaymentEntry{}, err } var response TransactionResponse err = readResponse(httpResponse, &response) if err != nil { - return database.PaymentEntry{}, err + return database2.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 database2.PaymentEntry{}, errors.New("received invalid status = " + response.StatusId) } return entry, nil } @@ -241,17 +246,17 @@ func (s *Service) basicAuth() string { 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")) - orderId := database.OrderId(c.Query("s")) + orderId := database2.OrderId(c.Query("s")) lang := c.Query("lang") eventId := c.Query("eventId") 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) 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 } @@ -265,7 +270,7 @@ func (s *Service) HandleResponse(c *gin.Context, provider *database.PaymentEntry 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 } diff --git a/providers/viva/viva.go b/domain/providers/viva/viva.go similarity index 75% rename from providers/viva/viva.go rename to domain/providers/viva/viva.go index 0d161ac..3756a6a 100644 --- a/providers/viva/viva.go +++ b/domain/providers/viva/viva.go @@ -1,6 +1,8 @@ package viva -import "payment-poc/database" +import ( + "payment-poc/domain/database" +) type OrderRequest struct { Amount int64 `json:"amount"` @@ -26,20 +28,20 @@ type TransactionCompleteRequest struct { } type TransactionResponse struct { - Amount float64 `json:"Amount"` - StatusId string `json:"StatusId"` - ErrorCode int64 `json:"ErrorCode"` - ErrorText string `json:"ErrorText"` - EventId int64 `json:"EventId"` - Success bool `json:"Success"` + Amount int64 `json:"Amount"` + StatusId string `json:"StatusId"` + ErrorCode int64 `json:"ErrorCode"` + ErrorText string `json:"ErrorText"` + EventId int64 `json:"EventId"` + Success bool `json:"Success"` } type TransactionStatus string const ( - PaymentSuccessful TransactionStatus = "F" + PaymentSuccessful TransactionStatus = "C" PaymentPending TransactionStatus = "A" - PaymentPreauthorized TransactionStatus = "C" + PaymentPreauthorized TransactionStatus = "F" PaymentUnsuccessful TransactionStatus = "E" PaymentRefunded TransactionStatus = "R" PaymentVoided TransactionStatus = "X" @@ -47,7 +49,7 @@ const ( type TransactionStatusResponse struct { Email string `json:"email"` - Amount int `json:"amount"` + Amount float64 `json:"amount"` OrderCode database.OrderId `json:"orderCode"` StatusId TransactionStatus `json:"statusId"` FullName string `json:"fullName"` diff --git a/providers/wspay/service.go b/domain/providers/wspay/service.go similarity index 88% rename from providers/wspay/service.go rename to domain/providers/wspay/service.go index cbbaa79..550664a 100644 --- a/providers/wspay/service.go +++ b/domain/providers/wspay/service.go @@ -10,10 +10,10 @@ import ( "github.com/gin-gonic/gin" "github.com/google/uuid" "io" - "log" + "log/slog" "net/http" - "payment-poc/database" - "payment-poc/state" + "payment-poc/domain/database" + "payment-poc/domain/state" "strconv" ) @@ -26,7 +26,7 @@ type Service struct { func (s *Service) UpdatePayment(entry database.PaymentEntry) (updatedEntry database.PaymentEntry, err error) { var request = StatusCheckRequest{ Version: "2.0", - ShopId: s.ShopId, + ShopID: s.ShopId, ShoppingCartId: 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 } - if CompareStatusCheckReturnSignature(response.Signature, s.ShopId, s.ShopSecret, response.ActionSuccess, response.ApprovalCode, entry.Id.String()) != nil { - entry.Amount = &response.Amount + if CompareStatusCheckReturnSignature(response.Signature, s.ShopId, s.ShopSecret, response.ActionSuccess, response.ApprovalCode, entry.WsPayOrderId) == nil { + newValue := int64(response.Amount * 100) + entry.Amount = &newValue newState := determineState(response) 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 = state.StateCompleted + entry.WsPayOrderId = response.WsPayOrderId } else { 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 { var request = CompletionRequest{ Version: "2.0", - WsPayOrderId: entry.Id.String(), - ShopId: s.ShopId, + WsPayOrderId: entry.WsPayOrderId, + 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), + Amount: strconv.FormatInt(amount, 10), + Signature: CalculateCompletionSignature(s.ShopId, s.ShopSecret, entry.WsPayOrderId, *entry.STAN, *entry.ApprovalCode, amount), } httpResponse, err := createRequest( @@ -113,7 +113,7 @@ func (s *Service) CompleteTransaction(entry database.PaymentEntry, amount int64) 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.State = state.StateCompleted } else { @@ -129,12 +129,12 @@ func (s *Service) CancelTransaction(entry database.PaymentEntry) (database.Payme if entry.State == state.StateAccepted { var request = CompletionRequest{ Version: "2.0", - WsPayOrderId: entry.Id.String(), - ShopId: s.ShopId, + WsPayOrderId: entry.WsPayOrderId, + 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), + Amount: strconv.FormatInt(entry.TotalAmount, 10), + Signature: CalculateCompletionSignature(s.ShopId, s.ShopSecret, entry.WsPayOrderId, *entry.STAN, *entry.ApprovalCode, entry.TotalAmount), } httpResponse, err := createRequest( @@ -153,8 +153,8 @@ func (s *Service) CancelTransaction(entry database.PaymentEntry) (database.Payme 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 + if CompareCompletionReturnSignature(response.Signature, s.ShopId, s.ShopSecret, *entry.STAN, response.ActionSuccess, response.ApprovalCode, entry.WsPayOrderId) == nil { + entry.State = state.StateVoided } else { 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.ApprovalCode = &response.ApprovalCode entry.State = state.StateAccepted + entry.WsPayOrderId = response.WsPayOrderId if _, err := provider.UpdateEntry(entry); err != nil { return "", err @@ -275,7 +276,7 @@ func CalculateFormSignature(shopId string, secret string, cartId string, amount 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 SHA512 algorithm: @@ -290,7 +291,7 @@ func CalculateCompletionSignature(shopId string, secret string, cartId string, s SecretKey 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.Write([]byte(signature)) 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 SHA512 algorithm: @@ -333,7 +334,7 @@ func CompareCompletionReturnSignature(signature string, shopId string, secret st WsPayOrderId 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.Write([]byte(calculatedSignature)) 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 SHA512 algorithm: @@ -356,7 +357,7 @@ func CompareStatusCheckReturnSignature(signature string, shopId string, secret s ApprovalCode WsPayOrderId */ - calculatedSignature := shopId + secret + actionSuccess + approvalCode + secret + shopId + approvalCode + cartId + calculatedSignature := shopId + secret + actionSuccess + approvalCode + secret + shopId + approvalCode + wsPayOrderId hash := sha512.New() hash.Write([]byte(calculatedSignature)) if hex.EncodeToString(hash.Sum(nil)) == signature { diff --git a/providers/wspay/wspay.go b/domain/providers/wspay/wspay.go similarity index 95% rename from providers/wspay/wspay.go rename to domain/providers/wspay/wspay.go index db4932c..6ea1d94 100644 --- a/providers/wspay/wspay.go +++ b/domain/providers/wspay/wspay.go @@ -84,16 +84,16 @@ type FormCancel struct { type CompletionRequest struct { Version string WsPayOrderId string - ShopId string + ShopID string ApprovalCode string STAN string - Amount int64 + Amount string Signature string } type CompletionResponse struct { WsPayOrderId string - ShopId string + ShopID string ApprovalCode string STAN string ErrorMessage string @@ -103,7 +103,7 @@ type CompletionResponse struct { type StatusCheckRequest struct { Version string - ShopId string + ShopID string ShoppingCartId string Signature string } @@ -115,8 +115,8 @@ type StatusCheckResponse struct { ApprovalCode string ShopID string ShoppingCartID string - Amount int64 - CurrencyCode string + Amount float64 + CurrencyCode int ActionSuccess string Success string // deprecated Authorized string @@ -125,7 +125,7 @@ type StatusCheckResponse struct { Refunded string PaymentPlan string Partner string - OnSite int + OnSite string CreditCardName string CreditCardNumber string ECI string diff --git a/state/model.go b/domain/state/model.go similarity index 100% rename from state/model.go rename to domain/state/model.go diff --git a/go.mod b/go.mod index 686ac35..7b0e388 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module payment-poc -go 1.19 +go 1.22 require ( github.com/gin-gonic/gin v1.9.1 @@ -8,6 +8,7 @@ require ( github.com/jmoiron/sqlx v1.3.5 github.com/joho/godotenv v1.5.1 github.com/lib/pq v1.10.9 + github.com/stripe/stripe-go/v72 v72.122.0 ) require ( @@ -26,7 +27,6 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // 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/ugorji/go/codec v1.2.11 // indirect golang.org/x/arch v0.3.0 // indirect diff --git a/main.go b/main.go index 85eeae2..dba3a4a 100644 --- a/main.go +++ b/main.go @@ -2,399 +2,51 @@ package main import ( "embed" - "errors" - "fmt" "github.com/gin-gonic/gin" - "github.com/google/uuid" "github.com/joho/godotenv" - "github.com/stripe/stripe-go/v72" - "html/template" - "log" + "log/slog" "net/http" "os" - "payment-poc/database" + "payment-poc/api" "payment-poc/migration" - "payment-poc/providers/mock" - stripe2 "payment-poc/providers/stripe" - "payment-poc/providers/viva" - wspay2 "payment-poc/providers/wspay" - "payment-poc/state" - "strconv" + "runtime/debug" "strings" - "time" ) //go:embed db/dev/*.sql 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() { godotenv.Load() - - log.SetPrefix("") - log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) + if !hasProfile("dev") { + gin.SetMode(gin.ReleaseMode) + } + if value := os.Getenv("LOG_FORMAT"); value == "json" { + slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{AddSource: true}))) + } } func main() { + commit, buildTime := buildInfo() + slog.Info("build info", slog.String("commit", commit), slog.String("time", buildTime)) + client, err := connectToDb() 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 { - 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") { - g.Use(gin.BasicAuth(getAccounts())) + port := ":" + getOrDefault("SERVER_PORT", "5281") + 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 { @@ -407,47 +59,25 @@ func hasProfile(profile string) bool { return false } -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" +func buildInfo() (string, string) { + revision := "" + buildTime := "" + + if info, ok := debug.ReadBuildInfo(); ok { + for _, setting := range info.Settings { + if setting.Key == "vcs.revision" { + revision = setting.Value + } else if setting.Key == "vcs.time" { + buildTime = setting.Value + } + } } - return "nepoznato stanje '" + string(stt) + "'" + return revision, buildTime } -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 getOrDefault(env string, defaultValue string) string { + if value, present := os.LookupEnv(env); present { + return value } -} - -func decimalCurrency(current int64) string { - return fmt.Sprintf("%d,%02d", current/100, current%100) -} - -func omitempty(value string) string { - if value == "" { - return "-" - } - return value + return defaultValue } diff --git a/makefile b/makefile index 108bbed..2697ac8 100644 --- a/makefile +++ b/makefile @@ -1,30 +1,18 @@ # scripts for building app -# requires go 1.19+ and git installed +# requires go 1.22+ and git installed -VERSION := 0.1.0 - -serve: - go run ./... - -setup: - go get +VERSION := $(shell git describe --tags --always) docker-dev: - docker image build -t registry.s2internal.com/opgdirekt/payment-poc/backend:$(VERSION)-dev . - docker tag registry.s2internal.com/opgdirekt/payment-poc/backend:$(VERSION)-dev registry.s2internal.com/opgdirekt/payment-poc/backend:latest-dev - 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 image build -t registry.bbr-dev.info/payment-poc/backend/dev:latest . + docker image push registry.bbr-dev.info/payment-poc/backend/dev:latest + docker-prod: - docker image build -t registry.s2internal.com/opgdirekt/payment-poc/backend:$(VERSION) . - docker tag registry.s2internal.com/opgdirekt/payment-poc/backend:$(VERSION) registry.s2internal.com/opgdirekt/payment-poc/backend:latest - docker image push registry.s2internal.com/opgdirekt/payment-poc/backend:$(VERSION) - docker image push registry.s2internal.com/opgdirekt/payment-poc/backend:latest - -release: - git tag $(VERSION) - git push origin $(VERSION) + docker image build -t registry.bbr-dev.info/payment-poc/backend:$(VERSION) . + docker tag registry.bbr-dev.info/payment-poc/backend:$(VERSION) registry.bbr-dev.info/payment-poc/backend:latest + docker image push registry.bbr-dev.info/payment-poc/backend:$(VERSION) + docker image push registry.bbr-dev.info/payment-poc/backend:latest test: go test ./... diff --git a/migration/migration.go b/migration/migration.go index 64f48de..57696c9 100644 --- a/migration/migration.go +++ b/migration/migration.go @@ -4,10 +4,11 @@ import ( "context" "crypto/sha256" "encoding/base64" + "errors" "fmt" "github.com/jmoiron/sqlx" "io/fs" - "log" + "log/slog" "sort" "strings" "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 { - 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) var err error = nil if _, e := tx.Exec(script); e != nil { @@ -102,10 +104,10 @@ func executeMigration(db *sqlx.DB, name string, script string) error { err = e } if err != nil { - log.Printf("[ERROR] script='%s' | migrations - failed executing", name) + logger.Error("migrations - failed executing", slog.String("err", err.Error())) tx.Rollback() } else { - log.Printf("[INFO] script='%s' | migrations - succesfully executed", name) + logger.Info("migrations - successfully executed") tx.Commit() } return err @@ -119,9 +121,9 @@ func validateMigration(name string, migration Migration, script string) error { calculatedHash := hash(script) if calculatedHash != migration.Hash { - err := 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) - return fmt.Errorf("migrations - mismatch in hashes for %s", name) + err := errors.New(fmt.Sprintf("migrations - mismatch in hash for %s (expected '%s', calculated '%s')", name, migration.Hash, calculatedHash)) + slog.Error("migrations - failed validation", slog.String("script", name), slog.String("err", err.Error())) + return err } return nil } diff --git a/providers/mock/service.go b/providers/mock/service.go deleted file mode 100644 index b68ca31..0000000 --- a/providers/mock/service.go +++ /dev/null @@ -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 -} diff --git a/render.go b/render.go deleted file mode 100644 index 2f8a72e..0000000 --- a/render.go +++ /dev/null @@ -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 - } -} diff --git a/templates/mock_gateway.gohtml b/templates/mock_gateway.gohtml index 5308e31..6b9b436 100644 --- a/templates/mock_gateway.gohtml +++ b/templates/mock_gateway.gohtml @@ -20,8 +20,8 @@

Mock gateway {{.Entry.Id.String}}

{{formatCurrency .Entry.TotalAmount}}

- Potvrdi plaćanje - Otkaži plaćanje + Potvrdi plaćanje + Otkaži plaćanje
\ No newline at end of file