Reorganized code + migration to go1.22
This commit is contained in:
parent
e3d77d55b9
commit
7369777098
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
14
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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
);
|
|
@ -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"`
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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"`
|
|
@ -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 {
|
|
@ -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
|
4
go.mod
4
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
|
||||
|
|
446
main.go
446
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
|
||||
}
|
||||
|
|
28
makefile
28
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 ./...
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
21
render.go
21
render.go
|
@ -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
|
||||
}
|
||||
}
|
|
@ -20,8 +20,8 @@
|
|||
<section class="container">
|
||||
<h2>Mock gateway {{.Entry.Id.String}}</h2>
|
||||
<p>{{formatCurrency .Entry.TotalAmount}}</p>
|
||||
<a href="/providers/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/success?id={{.Entry.Id.String}}" class="btn btn-success">Potvrdi plaćanje</a>
|
||||
<a href="/mock/error?id={{.Entry.Id.String}}" class="btn btn-danger">Otkaži plaćanje</a>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue