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) {
|
func connectToDb() (*sqlx.DB, error) {
|
||||||
host := envMustExist("PSQL_HOST")
|
host := envMustExist("PSQL_HOST")
|
||||||
port := envMustExist("PSQL_PORT")
|
port := envMustExist("PSQL_PORT")
|
||||||
user := envMustExist("PSQL_USER")
|
user := envMustExist("PSQL_USER")
|
||||||
password := envMustExist("PSQL_PASSWORD")
|
password := envMustExist("PSQL_PASSWORD")
|
||||||
dbname := envMustExist("PSQL_DB")
|
dbname := envMustExist("PSQL_DB")
|
||||||
|
sslMode := envOrDefault("PSQL_SSLMODE", "disable")
|
||||||
|
schema := envOrDefault("PSQL_SCHEMA", "public")
|
||||||
|
|
||||||
psqlInfo := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
psqlInfo := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s search_path=%s",
|
||||||
host, port, user, password, dbname)
|
host, port, user, password, dbname, sslMode, schema)
|
||||||
|
|
||||||
db, err := sqlx.Open("postgres", psqlInfo)
|
db, err := sqlx.Open("postgres", psqlInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -17,6 +17,7 @@ CREATE TABLE IF NOT EXISTS "payment_entry"
|
||||||
|
|
||||||
"payment_intent_id" varchar(255) DEFAULT NULL,
|
"payment_intent_id" varchar(255) DEFAULT NULL,
|
||||||
|
|
||||||
|
"ws_pay_order_id" varchar(255) DEFAULT '',
|
||||||
"shopping_card_id" varchar(255) DEFAULT NULL,
|
"shopping_card_id" varchar(255) DEFAULT NULL,
|
||||||
"stan" varchar(255) DEFAULT NULL,
|
"stan" varchar(255) DEFAULT NULL,
|
||||||
"success" int DEFAULT NULL,
|
"success" int DEFAULT NULL,
|
||||||
|
|
|
@ -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 (
|
import (
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"payment-poc/state"
|
"payment-poc/domain/state"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@ type PaymentEntry struct {
|
||||||
PaymentIntentId *string `db:"payment_intent_id"`
|
PaymentIntentId *string `db:"payment_intent_id"`
|
||||||
|
|
||||||
// wspay field
|
// wspay field
|
||||||
|
WsPayOrderId string `db:"ws_pay_order_id"`
|
||||||
ShoppingCardID *string `db:"shopping_card_id"`
|
ShoppingCardID *string `db:"shopping_card_id"`
|
||||||
STAN *string `db:"stan"`
|
STAN *string `db:"stan"`
|
||||||
Success *int `db:"success"`
|
Success *int `db:"success"`
|
|
@ -29,8 +29,8 @@ func (p *PaymentEntryProvider) UpdateEntry(entry PaymentEntry) (PaymentEntry, er
|
||||||
currentTime := time.Now()
|
currentTime := time.Now()
|
||||||
entry.Modified = ¤tTime
|
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`,
|
_, err := p.DB.Exec(`UPDATE "payment_entry" SET "modified" = $2, "state" = $3, "lang" = $4, "error" = $5, "amount" = $6, "eci" = $7, "payment_intent_id" = $8, "shopping_card_id" = $9, "stan" = $10, "success" = $11, "approval_code" = $12, "order_id" = $13, "transaction_id" = $14, "event_id" = $15, "ws_pay_order_id" = $16 WHERE "id" = $1`,
|
||||||
&entry.Id, &entry.Modified, &entry.State, &entry.Lang, &entry.Error, &entry.Amount, &entry.ECI, &entry.PaymentIntentId, &entry.ShoppingCardID, &entry.STAN, &entry.Success, &entry.ApprovalCode, &entry.OrderId, &entry.TransactionId, &entry.EventId,
|
&entry.Id, &entry.Modified, &entry.State, &entry.Lang, &entry.Error, &entry.Amount, &entry.ECI, &entry.PaymentIntentId, &entry.ShoppingCardID, &entry.STAN, &entry.Success, &entry.ApprovalCode, &entry.OrderId, &entry.TransactionId, &entry.EventId, &entry.WsPayOrderId,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return PaymentEntry{}, err
|
return PaymentEntry{}, err
|
|
@ -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"
|
||||||
"github.com/stripe/stripe-go/v72/checkout/session"
|
"github.com/stripe/stripe-go/v72/checkout/session"
|
||||||
"github.com/stripe/stripe-go/v72/paymentintent"
|
"github.com/stripe/stripe-go/v72/paymentintent"
|
||||||
"log"
|
"log/slog"
|
||||||
"payment-poc/database"
|
database2 "payment-poc/domain/database"
|
||||||
"payment-poc/state"
|
"payment-poc/domain/state"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
|
@ -16,15 +16,19 @@ type Service struct {
|
||||||
BackendUrl string
|
BackendUrl string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) UpdatePayment(entry database.PaymentEntry) (updatedEntry database.PaymentEntry, err error) {
|
func (s *Service) UpdatePayment(entry database2.PaymentEntry) (updatedEntry database2.PaymentEntry, err error) {
|
||||||
pi, err := paymentintent.Get(*entry.PaymentIntentId, nil)
|
client := paymentintent.Client{
|
||||||
|
B: stripe.GetBackend(stripe.APIBackend),
|
||||||
|
Key: s.ApiKey,
|
||||||
|
}
|
||||||
|
pi, err := client.Get(*entry.PaymentIntentId, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return entry, err
|
return entry, err
|
||||||
}
|
}
|
||||||
newState := determineState(pi.Status)
|
newState := determineState(pi.Status)
|
||||||
|
|
||||||
if entry.State != newState && newState != "" {
|
if entry.State != newState && newState != "" {
|
||||||
log.Printf("[%s] updated state for %s -> %s", entry.Id.String(), entry.State, newState)
|
slog.Info("updated state", "entry_id", entry.Id.String(), "state", entry.State, "new_state", newState)
|
||||||
if pi.AmountReceived > 0 {
|
if pi.AmountReceived > 0 {
|
||||||
entry.Amount = &pi.AmountReceived
|
entry.Amount = &pi.AmountReceived
|
||||||
}
|
}
|
||||||
|
@ -53,7 +57,7 @@ func determineState(status stripe.PaymentIntentStatus) state.PaymentState {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) CreatePaymentUrl(entry database.PaymentEntry) (database.PaymentEntry, string, error) {
|
func (s *Service) CreatePaymentUrl(entry database2.PaymentEntry) (database2.PaymentEntry, string, error) {
|
||||||
entry, url, err := s.InitializePayment(entry)
|
entry, url, err := s.InitializePayment(entry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return entry, "", err
|
return entry, "", err
|
||||||
|
@ -61,7 +65,7 @@ func (s *Service) CreatePaymentUrl(entry database.PaymentEntry) (database.Paymen
|
||||||
return entry, url, nil
|
return entry, url, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) InitializePayment(entry database.PaymentEntry) (database.PaymentEntry, string, error) {
|
func (s *Service) InitializePayment(entry database2.PaymentEntry) (database2.PaymentEntry, string, error) {
|
||||||
|
|
||||||
currency := string(stripe.CurrencyEUR)
|
currency := string(stripe.CurrencyEUR)
|
||||||
productName := "Example product"
|
productName := "Example product"
|
||||||
|
@ -88,9 +92,13 @@ func (s *Service) InitializePayment(entry database.PaymentEntry) (database.Payme
|
||||||
SuccessURL: stripe.String(s.BackendUrl + "/stripe/success?token=" + entry.Id.String()),
|
SuccessURL: stripe.String(s.BackendUrl + "/stripe/success?token=" + entry.Id.String()),
|
||||||
CancelURL: stripe.String(s.BackendUrl + "/stripe/cancel?token=" + entry.Id.String()),
|
CancelURL: stripe.String(s.BackendUrl + "/stripe/cancel?token=" + entry.Id.String()),
|
||||||
}
|
}
|
||||||
result, err := session.New(params)
|
client := session.Client{
|
||||||
|
B: stripe.GetBackend(stripe.APIBackend),
|
||||||
|
Key: s.ApiKey,
|
||||||
|
}
|
||||||
|
result, err := client.New(params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return database.PaymentEntry{}, "", err
|
return database2.PaymentEntry{}, "", err
|
||||||
}
|
}
|
||||||
entry.State = state.StateInitialized
|
entry.State = state.StateInitialized
|
||||||
entry.PaymentIntentId = &result.PaymentIntent.ID
|
entry.PaymentIntentId = &result.PaymentIntent.ID
|
||||||
|
@ -98,15 +106,19 @@ func (s *Service) InitializePayment(entry database.PaymentEntry) (database.Payme
|
||||||
return entry, result.URL, nil
|
return entry, result.URL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) CompleteTransaction(entry database.PaymentEntry, amount int64) (database.PaymentEntry, error) {
|
func (s *Service) CompleteTransaction(entry database2.PaymentEntry, amount int64) (database2.PaymentEntry, error) {
|
||||||
params := &stripe.PaymentIntentCaptureParams{
|
params := &stripe.PaymentIntentCaptureParams{
|
||||||
AmountToCapture: stripe.Int64(amount),
|
AmountToCapture: stripe.Int64(amount),
|
||||||
}
|
}
|
||||||
pi, err := paymentintent.Capture(*entry.PaymentIntentId, params)
|
client := paymentintent.Client{
|
||||||
if err != nil {
|
B: stripe.GetBackend(stripe.APIBackend),
|
||||||
return database.PaymentEntry{}, err
|
Key: s.ApiKey,
|
||||||
}
|
}
|
||||||
log.Printf("received state on completion: %v", pi.Status)
|
pi, err := client.Capture(*entry.PaymentIntentId, params)
|
||||||
|
if err != nil {
|
||||||
|
return database2.PaymentEntry{}, err
|
||||||
|
}
|
||||||
|
slog.Info("received state on completion", "entry_id", entry.Id.String(), "state", entry.State, "new_state", pi.Status)
|
||||||
newState := determineState(pi.Status)
|
newState := determineState(pi.Status)
|
||||||
entry.State = newState
|
entry.State = newState
|
||||||
if newState == state.StateCompleted || newState == state.StatePending {
|
if newState == state.StateCompleted || newState == state.StatePending {
|
||||||
|
@ -115,20 +127,24 @@ func (s *Service) CompleteTransaction(entry database.PaymentEntry, amount int64)
|
||||||
return entry, nil
|
return entry, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) CancelTransaction(entry database.PaymentEntry) (database.PaymentEntry, error) {
|
func (s *Service) CancelTransaction(entry database2.PaymentEntry) (database2.PaymentEntry, error) {
|
||||||
params := &stripe.PaymentIntentCancelParams{}
|
params := &stripe.PaymentIntentCancelParams{}
|
||||||
pi, err := paymentintent.Cancel(*entry.PaymentIntentId, params)
|
client := paymentintent.Client{
|
||||||
if err != nil {
|
B: stripe.GetBackend(stripe.APIBackend),
|
||||||
return database.PaymentEntry{}, err
|
Key: s.ApiKey,
|
||||||
}
|
}
|
||||||
log.Printf("received state on completion: %v", pi.Status)
|
pi, err := client.Cancel(*entry.PaymentIntentId, params)
|
||||||
|
if err != nil {
|
||||||
|
return database2.PaymentEntry{}, err
|
||||||
|
}
|
||||||
|
slog.Info("received state on completion", "entry_id", entry.Id.String(), "state", entry.State, "new_state", pi.Status)
|
||||||
if pi.Status == stripe.PaymentIntentStatusCanceled {
|
if pi.Status == stripe.PaymentIntentStatusCanceled {
|
||||||
entry.State = state.StateCanceled
|
entry.State = state.StateCanceled
|
||||||
}
|
}
|
||||||
return entry, nil
|
return entry, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) HandleResponse(c *gin.Context, provider *database.PaymentEntryProvider, paymentState state.PaymentState) (string, error) {
|
func (s *Service) HandleResponse(c *gin.Context, provider *database2.PaymentEntryProvider, paymentState state.PaymentState) (string, error) {
|
||||||
id := uuid.MustParse(c.Query("token"))
|
id := uuid.MustParse(c.Query("token"))
|
||||||
entry, err := provider.FetchById(id)
|
entry, err := provider.FetchById(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -138,6 +154,6 @@ func (s *Service) HandleResponse(c *gin.Context, provider *database.PaymentEntry
|
||||||
if _, err := provider.UpdateEntry(entry); err != nil {
|
if _, err := provider.UpdateEntry(entry); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
log.Printf("[%s:%s] received authorization response", entry.Id.String(), entry.State)
|
slog.Info("received authorization response", "entry_id", entry.Id.String(), "state", entry.State)
|
||||||
return "/entries/" + entry.Id.String(), nil
|
return "/entries/" + entry.Id.String(), nil
|
||||||
}
|
}
|
|
@ -8,11 +8,11 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"payment-poc/database"
|
database2 "payment-poc/domain/database"
|
||||||
"payment-poc/state"
|
"payment-poc/domain/state"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
@ -29,7 +29,7 @@ type Service struct {
|
||||||
expiration time.Time
|
expiration time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) UpdatePayment(entry database.PaymentEntry) (updatedEntry database.PaymentEntry, err error) {
|
func (s *Service) UpdatePayment(entry database2.PaymentEntry) (updatedEntry database2.PaymentEntry, err error) {
|
||||||
token, err := s.oAuthToken()
|
token, err := s.oAuthToken()
|
||||||
httpResponse, err := createRequest(
|
httpResponse, err := createRequest(
|
||||||
"GET",
|
"GET",
|
||||||
|
@ -38,19 +38,23 @@ func (s *Service) UpdatePayment(entry database.PaymentEntry) (updatedEntry datab
|
||||||
[]byte{},
|
[]byte{},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return database.PaymentEntry{}, err
|
return database2.PaymentEntry{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var response TransactionStatusResponse
|
var response TransactionStatusResponse
|
||||||
err = readResponse(httpResponse, &response)
|
err = readResponse(httpResponse, &response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return database.PaymentEntry{}, err
|
return database2.PaymentEntry{}, err
|
||||||
}
|
}
|
||||||
newState := determineStatus(response.StatusId)
|
newState := determineStatus(response.StatusId)
|
||||||
|
|
||||||
if entry.State != newState && newState != "" {
|
if entry.State != newState && newState != "" {
|
||||||
log.Printf("[%s:%s] updated state %s -> %s", entry.Id.String(), entry.State, entry.State, newState)
|
slog.Info("updated state", "entry_id", entry.Id.String(), "state", entry.State, "new_state", newState)
|
||||||
entry.State = newState
|
entry.State = newState
|
||||||
|
if entry.State == state.StateCompleted {
|
||||||
|
amount := int64(response.Amount * 100)
|
||||||
|
entry.Amount = &amount
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return entry, nil
|
return entry, nil
|
||||||
}
|
}
|
||||||
|
@ -70,11 +74,11 @@ func determineStatus(id TransactionStatus) state.PaymentState {
|
||||||
case PaymentVoided:
|
case PaymentVoided:
|
||||||
return state.StateVoided
|
return state.StateVoided
|
||||||
}
|
}
|
||||||
log.Printf("Unknonw transactionStatus: %s", string(id))
|
slog.Info("unknown transaction status", "status", string(id))
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) CreatePaymentUrl(entry database.PaymentEntry) (database.PaymentEntry, string, error) {
|
func (s *Service) CreatePaymentUrl(entry database2.PaymentEntry) (database2.PaymentEntry, string, error) {
|
||||||
entry, err := s.InitializePayment(entry)
|
entry, err := s.InitializePayment(entry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return entry, "", err
|
return entry, "", err
|
||||||
|
@ -82,10 +86,10 @@ func (s *Service) CreatePaymentUrl(entry database.PaymentEntry) (database.Paymen
|
||||||
return entry, "https://demo.vivapayments.com/web/checkout?ref=" + string(*entry.OrderId), nil
|
return entry, "https://demo.vivapayments.com/web/checkout?ref=" + string(*entry.OrderId), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) InitializePayment(entry database.PaymentEntry) (database.PaymentEntry, error) {
|
func (s *Service) InitializePayment(entry database2.PaymentEntry) (database2.PaymentEntry, error) {
|
||||||
token, err := s.oAuthToken()
|
token, err := s.oAuthToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return database.PaymentEntry{}, err
|
return database2.PaymentEntry{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
request := OrderRequest{
|
request := OrderRequest{
|
||||||
|
@ -104,20 +108,20 @@ func (s *Service) InitializePayment(entry database.PaymentEntry) (database.Payme
|
||||||
toJson(request),
|
toJson(request),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return database.PaymentEntry{}, err
|
return database2.PaymentEntry{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var response OrderResponse
|
var response OrderResponse
|
||||||
err = readResponse(httpResponse, &response)
|
err = readResponse(httpResponse, &response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return database.PaymentEntry{}, err
|
return database2.PaymentEntry{}, err
|
||||||
}
|
}
|
||||||
entry.State = state.StateInitialized
|
entry.State = state.StateInitialized
|
||||||
entry.OrderId = &response.OrderId
|
entry.OrderId = &response.OrderId
|
||||||
return entry, nil
|
return entry, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) CompleteTransaction(entry database.PaymentEntry, amount int64) (database.PaymentEntry, error) {
|
func (s *Service) CompleteTransaction(entry database2.PaymentEntry, amount int64) (database2.PaymentEntry, error) {
|
||||||
completionRequest := TransactionCompleteRequest{
|
completionRequest := TransactionCompleteRequest{
|
||||||
Amount: amount,
|
Amount: amount,
|
||||||
CustomerDescription: "Example transaction",
|
CustomerDescription: "Example transaction",
|
||||||
|
@ -125,54 +129,55 @@ func (s *Service) CompleteTransaction(entry database.PaymentEntry, amount int64)
|
||||||
httpResponse, err := createRequest(
|
httpResponse, err := createRequest(
|
||||||
"POST",
|
"POST",
|
||||||
"https://demo.vivapayments.com/api/transactions/"+entry.TransactionId.String(),
|
"https://demo.vivapayments.com/api/transactions/"+entry.TransactionId.String(),
|
||||||
map[string]string{"authorization": "Bearer " + s.basicAuth(),
|
map[string]string{"authorization": "Basic " + s.basicAuth(),
|
||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
},
|
},
|
||||||
toJson(completionRequest),
|
toJson(completionRequest),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return database.PaymentEntry{}, err
|
return database2.PaymentEntry{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var response TransactionResponse
|
var response TransactionResponse
|
||||||
err = readResponse(httpResponse, &response)
|
err = readResponse(httpResponse, &response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return database.PaymentEntry{}, err
|
return database2.PaymentEntry{}, err
|
||||||
}
|
}
|
||||||
if response.StatusId == "F" {
|
if response.StatusId == "F" {
|
||||||
paidAmount := int64(response.Amount * 100)
|
paidAmount := response.Amount * 100
|
||||||
entry.Amount = &paidAmount
|
entry.Amount = &paidAmount
|
||||||
entry.State = state.StateCompleted
|
entry.State = state.StateCompleted
|
||||||
} else {
|
} else {
|
||||||
return database.PaymentEntry{}, errors.New("received invalid status = " + response.StatusId)
|
return database2.PaymentEntry{}, errors.New("received invalid status = " + response.StatusId)
|
||||||
}
|
}
|
||||||
return entry, nil
|
return entry, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) CancelTransaction(entry database.PaymentEntry) (database.PaymentEntry, error) {
|
func (s *Service) CancelTransaction(entry database2.PaymentEntry) (database2.PaymentEntry, error) {
|
||||||
|
amount := strconv.FormatInt(entry.TotalAmount, 10)
|
||||||
httpResponse, err := createRequest(
|
httpResponse, err := createRequest(
|
||||||
"DELETE",
|
"DELETE",
|
||||||
"https://demo.vivapayments.com/api/transactions/"+entry.TransactionId.String(),
|
"https://demo.vivapayments.com/api/transactions/"+entry.TransactionId.String()+"?amount="+amount,
|
||||||
map[string]string{"authorization": "Bearer " + s.basicAuth(),
|
map[string]string{"authorization": "Basic " + s.basicAuth(),
|
||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
},
|
},
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return database.PaymentEntry{}, err
|
return database2.PaymentEntry{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var response TransactionResponse
|
var response TransactionResponse
|
||||||
err = readResponse(httpResponse, &response)
|
err = readResponse(httpResponse, &response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return database.PaymentEntry{}, err
|
return database2.PaymentEntry{}, err
|
||||||
}
|
}
|
||||||
if response.StatusId == "F" {
|
if response.StatusId == "F" {
|
||||||
paidAmount := int64(0)
|
paidAmount := int64(0)
|
||||||
entry.Amount = &paidAmount
|
entry.Amount = &paidAmount
|
||||||
entry.State = state.StateVoided
|
entry.State = state.StateVoided
|
||||||
} else {
|
} else {
|
||||||
return database.PaymentEntry{}, errors.New("received invalid status = " + response.StatusId)
|
return database2.PaymentEntry{}, errors.New("received invalid status = " + response.StatusId)
|
||||||
}
|
}
|
||||||
return entry, nil
|
return entry, nil
|
||||||
}
|
}
|
||||||
|
@ -241,17 +246,17 @@ func (s *Service) basicAuth() string {
|
||||||
return base64.StdEncoding.EncodeToString([]byte(s.MerchantId + ":" + s.ApiKey))
|
return base64.StdEncoding.EncodeToString([]byte(s.MerchantId + ":" + s.ApiKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) HandleResponse(c *gin.Context, provider *database.PaymentEntryProvider, state state.PaymentState) (string, error) {
|
func (s *Service) HandleResponse(c *gin.Context, provider *database2.PaymentEntryProvider, state state.PaymentState) (string, error) {
|
||||||
transactionId := uuid.MustParse(c.Query("t"))
|
transactionId := uuid.MustParse(c.Query("t"))
|
||||||
orderId := database.OrderId(c.Query("s"))
|
orderId := database2.OrderId(c.Query("s"))
|
||||||
lang := c.Query("lang")
|
lang := c.Query("lang")
|
||||||
eventId := c.Query("eventId")
|
eventId := c.Query("eventId")
|
||||||
eci := c.Query("eci")
|
eci := c.Query("eci")
|
||||||
|
|
||||||
log.Printf("[%s] received error response for viva payment", orderId)
|
slog.Info("received error response from viva payment", "order_id", orderId)
|
||||||
entry, err := provider.FetchByOrderId(orderId)
|
entry, err := provider.FetchByOrderId(orderId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[%s] couldn't find payment info for viva payment", orderId)
|
slog.Error("couldn't find payment info for viva payment", "order_id", orderId)
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -265,7 +270,7 @@ func (s *Service) HandleResponse(c *gin.Context, provider *database.PaymentEntry
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[%s:%s] received authorization response", entry.Id.String(), entry.State)
|
slog.Info("received authorization response", "entry_id", entry.Id.String(), "state", entry.State)
|
||||||
|
|
||||||
return "/entries/" + entry.Id.String(), nil
|
return "/entries/" + entry.Id.String(), nil
|
||||||
}
|
}
|
|
@ -1,6 +1,8 @@
|
||||||
package viva
|
package viva
|
||||||
|
|
||||||
import "payment-poc/database"
|
import (
|
||||||
|
"payment-poc/domain/database"
|
||||||
|
)
|
||||||
|
|
||||||
type OrderRequest struct {
|
type OrderRequest struct {
|
||||||
Amount int64 `json:"amount"`
|
Amount int64 `json:"amount"`
|
||||||
|
@ -26,7 +28,7 @@ type TransactionCompleteRequest struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type TransactionResponse struct {
|
type TransactionResponse struct {
|
||||||
Amount float64 `json:"Amount"`
|
Amount int64 `json:"Amount"`
|
||||||
StatusId string `json:"StatusId"`
|
StatusId string `json:"StatusId"`
|
||||||
ErrorCode int64 `json:"ErrorCode"`
|
ErrorCode int64 `json:"ErrorCode"`
|
||||||
ErrorText string `json:"ErrorText"`
|
ErrorText string `json:"ErrorText"`
|
||||||
|
@ -37,9 +39,9 @@ type TransactionResponse struct {
|
||||||
type TransactionStatus string
|
type TransactionStatus string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
PaymentSuccessful TransactionStatus = "F"
|
PaymentSuccessful TransactionStatus = "C"
|
||||||
PaymentPending TransactionStatus = "A"
|
PaymentPending TransactionStatus = "A"
|
||||||
PaymentPreauthorized TransactionStatus = "C"
|
PaymentPreauthorized TransactionStatus = "F"
|
||||||
PaymentUnsuccessful TransactionStatus = "E"
|
PaymentUnsuccessful TransactionStatus = "E"
|
||||||
PaymentRefunded TransactionStatus = "R"
|
PaymentRefunded TransactionStatus = "R"
|
||||||
PaymentVoided TransactionStatus = "X"
|
PaymentVoided TransactionStatus = "X"
|
||||||
|
@ -47,7 +49,7 @@ const (
|
||||||
|
|
||||||
type TransactionStatusResponse struct {
|
type TransactionStatusResponse struct {
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Amount int `json:"amount"`
|
Amount float64 `json:"amount"`
|
||||||
OrderCode database.OrderId `json:"orderCode"`
|
OrderCode database.OrderId `json:"orderCode"`
|
||||||
StatusId TransactionStatus `json:"statusId"`
|
StatusId TransactionStatus `json:"statusId"`
|
||||||
FullName string `json:"fullName"`
|
FullName string `json:"fullName"`
|
|
@ -10,10 +10,10 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"payment-poc/database"
|
"payment-poc/domain/database"
|
||||||
"payment-poc/state"
|
"payment-poc/domain/state"
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ type Service struct {
|
||||||
func (s *Service) UpdatePayment(entry database.PaymentEntry) (updatedEntry database.PaymentEntry, err error) {
|
func (s *Service) UpdatePayment(entry database.PaymentEntry) (updatedEntry database.PaymentEntry, err error) {
|
||||||
var request = StatusCheckRequest{
|
var request = StatusCheckRequest{
|
||||||
Version: "2.0",
|
Version: "2.0",
|
||||||
ShopId: s.ShopId,
|
ShopID: s.ShopId,
|
||||||
ShoppingCartId: entry.Id.String(),
|
ShoppingCartId: entry.Id.String(),
|
||||||
Signature: CalculateStatusCheckSignature(s.ShopId, s.ShopSecret, entry.Id.String()),
|
Signature: CalculateStatusCheckSignature(s.ShopId, s.ShopSecret, entry.Id.String()),
|
||||||
}
|
}
|
||||||
|
@ -47,16 +47,16 @@ func (s *Service) UpdatePayment(entry database.PaymentEntry) (updatedEntry datab
|
||||||
return database.PaymentEntry{}, err
|
return database.PaymentEntry{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if CompareStatusCheckReturnSignature(response.Signature, s.ShopId, s.ShopSecret, response.ActionSuccess, response.ApprovalCode, entry.Id.String()) != nil {
|
if CompareStatusCheckReturnSignature(response.Signature, s.ShopId, s.ShopSecret, response.ActionSuccess, response.ApprovalCode, entry.WsPayOrderId) == nil {
|
||||||
entry.Amount = &response.Amount
|
newValue := int64(response.Amount * 100)
|
||||||
|
entry.Amount = &newValue
|
||||||
newState := determineState(response)
|
newState := determineState(response)
|
||||||
|
|
||||||
if entry.State != newState && newState != "" {
|
if entry.State != newState && newState != "" {
|
||||||
log.Printf("Updated state for %s: %s -> %s", entry.Id.String(), entry.State, newState)
|
slog.Info("Updated state", "entry_id", entry.Id.String(), "state", entry.State, "new_state", newState)
|
||||||
entry.State = newState
|
entry.State = newState
|
||||||
}
|
}
|
||||||
|
entry.WsPayOrderId = response.WsPayOrderId
|
||||||
entry.State = state.StateCompleted
|
|
||||||
} else {
|
} else {
|
||||||
return database.PaymentEntry{}, errors.New("invalid signature")
|
return database.PaymentEntry{}, errors.New("invalid signature")
|
||||||
}
|
}
|
||||||
|
@ -89,12 +89,12 @@ func (s *Service) CompleteTransaction(entry database.PaymentEntry, amount int64)
|
||||||
if entry.State == state.StateAccepted {
|
if entry.State == state.StateAccepted {
|
||||||
var request = CompletionRequest{
|
var request = CompletionRequest{
|
||||||
Version: "2.0",
|
Version: "2.0",
|
||||||
WsPayOrderId: entry.Id.String(),
|
WsPayOrderId: entry.WsPayOrderId,
|
||||||
ShopId: s.ShopId,
|
ShopID: s.ShopId,
|
||||||
ApprovalCode: *entry.ApprovalCode,
|
ApprovalCode: *entry.ApprovalCode,
|
||||||
STAN: *entry.STAN,
|
STAN: *entry.STAN,
|
||||||
Amount: amount,
|
Amount: strconv.FormatInt(amount, 10),
|
||||||
Signature: CalculateCompletionSignature(s.ShopId, s.ShopSecret, entry.Id.String(), *entry.STAN, *entry.ApprovalCode, amount),
|
Signature: CalculateCompletionSignature(s.ShopId, s.ShopSecret, entry.WsPayOrderId, *entry.STAN, *entry.ApprovalCode, amount),
|
||||||
}
|
}
|
||||||
|
|
||||||
httpResponse, err := createRequest(
|
httpResponse, err := createRequest(
|
||||||
|
@ -113,7 +113,7 @@ func (s *Service) CompleteTransaction(entry database.PaymentEntry, amount int64)
|
||||||
return database.PaymentEntry{}, err
|
return database.PaymentEntry{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if CompareCompletionReturnSignature(response.Signature, s.ShopId, s.ShopSecret, entry.Id.String(), *entry.STAN, response.ActionSuccess, response.ApprovalCode) != nil {
|
if CompareCompletionReturnSignature(response.Signature, s.ShopId, s.ShopSecret, *entry.STAN, response.ActionSuccess, response.ApprovalCode, entry.WsPayOrderId) == nil {
|
||||||
entry.Amount = &amount
|
entry.Amount = &amount
|
||||||
entry.State = state.StateCompleted
|
entry.State = state.StateCompleted
|
||||||
} else {
|
} else {
|
||||||
|
@ -129,12 +129,12 @@ func (s *Service) CancelTransaction(entry database.PaymentEntry) (database.Payme
|
||||||
if entry.State == state.StateAccepted {
|
if entry.State == state.StateAccepted {
|
||||||
var request = CompletionRequest{
|
var request = CompletionRequest{
|
||||||
Version: "2.0",
|
Version: "2.0",
|
||||||
WsPayOrderId: entry.Id.String(),
|
WsPayOrderId: entry.WsPayOrderId,
|
||||||
ShopId: s.ShopId,
|
ShopID: s.ShopId,
|
||||||
ApprovalCode: *entry.ApprovalCode,
|
ApprovalCode: *entry.ApprovalCode,
|
||||||
STAN: *entry.STAN,
|
STAN: *entry.STAN,
|
||||||
Amount: entry.TotalAmount,
|
Amount: strconv.FormatInt(entry.TotalAmount, 10),
|
||||||
Signature: CalculateCompletionSignature(s.ShopId, s.ShopSecret, entry.Id.String(), *entry.STAN, *entry.ApprovalCode, entry.TotalAmount),
|
Signature: CalculateCompletionSignature(s.ShopId, s.ShopSecret, entry.WsPayOrderId, *entry.STAN, *entry.ApprovalCode, entry.TotalAmount),
|
||||||
}
|
}
|
||||||
|
|
||||||
httpResponse, err := createRequest(
|
httpResponse, err := createRequest(
|
||||||
|
@ -153,8 +153,8 @@ func (s *Service) CancelTransaction(entry database.PaymentEntry) (database.Payme
|
||||||
return database.PaymentEntry{}, err
|
return database.PaymentEntry{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if CompareCompletionReturnSignature(response.Signature, s.ShopId, s.ShopSecret, entry.Id.String(), *entry.STAN, response.ActionSuccess, response.ApprovalCode) != nil {
|
if CompareCompletionReturnSignature(response.Signature, s.ShopId, s.ShopSecret, *entry.STAN, response.ActionSuccess, response.ApprovalCode, entry.WsPayOrderId) == nil {
|
||||||
entry.State = state.StateCanceled
|
entry.State = state.StateVoided
|
||||||
} else {
|
} else {
|
||||||
return database.PaymentEntry{}, errors.New("invalid signature")
|
return database.PaymentEntry{}, errors.New("invalid signature")
|
||||||
}
|
}
|
||||||
|
@ -222,6 +222,7 @@ func (s *Service) HandleSuccessResponse(c *gin.Context, provider *database.Payme
|
||||||
entry.Success = &response.Success
|
entry.Success = &response.Success
|
||||||
entry.ApprovalCode = &response.ApprovalCode
|
entry.ApprovalCode = &response.ApprovalCode
|
||||||
entry.State = state.StateAccepted
|
entry.State = state.StateAccepted
|
||||||
|
entry.WsPayOrderId = response.WsPayOrderId
|
||||||
|
|
||||||
if _, err := provider.UpdateEntry(entry); err != nil {
|
if _, err := provider.UpdateEntry(entry); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
@ -275,7 +276,7 @@ func CalculateFormSignature(shopId string, secret string, cartId string, amount
|
||||||
return hex.EncodeToString(hash.Sum(nil))
|
return hex.EncodeToString(hash.Sum(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
func CalculateCompletionSignature(shopId string, secret string, cartId string, stan string, approvalCode string, amount int64) string {
|
func CalculateCompletionSignature(shopId string, secret string, wsPayOrderId string, stan string, approvalCode string, amount int64) string {
|
||||||
/**
|
/**
|
||||||
Represents a signature created from string formatted from following values in a following order using
|
Represents a signature created from string formatted from following values in a following order using
|
||||||
SHA512 algorithm:
|
SHA512 algorithm:
|
||||||
|
@ -290,7 +291,7 @@ func CalculateCompletionSignature(shopId string, secret string, cartId string, s
|
||||||
SecretKey
|
SecretKey
|
||||||
WsPayOrderId
|
WsPayOrderId
|
||||||
*/
|
*/
|
||||||
signature := shopId + cartId + secret + stan + secret + approvalCode + secret + strconv.FormatInt(amount, 10) + secret + cartId
|
signature := shopId + wsPayOrderId + secret + stan + secret + approvalCode + secret + strconv.FormatInt(amount, 10) + secret + wsPayOrderId
|
||||||
hash := sha512.New()
|
hash := sha512.New()
|
||||||
hash.Write([]byte(signature))
|
hash.Write([]byte(signature))
|
||||||
return hex.EncodeToString(hash.Sum(nil))
|
return hex.EncodeToString(hash.Sum(nil))
|
||||||
|
@ -320,7 +321,7 @@ func CompareFormReturnSignature(signature string, shopId string, secret string,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func CompareCompletionReturnSignature(signature string, shopId string, secret string, stan string, actionSuccess string, approvalCode string, cartId string) error {
|
func CompareCompletionReturnSignature(signature string, shopId string, secret string, stan string, actionSuccess string, approvalCode string, wsPayOrderId string) error {
|
||||||
/**
|
/**
|
||||||
Represents a signature created from string formatted from following values in a following order using
|
Represents a signature created from string formatted from following values in a following order using
|
||||||
SHA512 algorithm:
|
SHA512 algorithm:
|
||||||
|
@ -333,7 +334,7 @@ func CompareCompletionReturnSignature(signature string, shopId string, secret st
|
||||||
WsPayOrderId
|
WsPayOrderId
|
||||||
Merchant should validate this signature to make sure that the request is originating from WSPayForm.
|
Merchant should validate this signature to make sure that the request is originating from WSPayForm.
|
||||||
*/
|
*/
|
||||||
calculatedSignature := shopId + secret + stan + actionSuccess + secret + approvalCode + cartId
|
calculatedSignature := shopId + secret + stan + actionSuccess + secret + approvalCode + wsPayOrderId
|
||||||
hash := sha512.New()
|
hash := sha512.New()
|
||||||
hash.Write([]byte(calculatedSignature))
|
hash.Write([]byte(calculatedSignature))
|
||||||
if hex.EncodeToString(hash.Sum(nil)) == signature {
|
if hex.EncodeToString(hash.Sum(nil)) == signature {
|
||||||
|
@ -343,7 +344,7 @@ func CompareCompletionReturnSignature(signature string, shopId string, secret st
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func CompareStatusCheckReturnSignature(signature string, shopId string, secret string, actionSuccess string, approvalCode string, cartId string) error {
|
func CompareStatusCheckReturnSignature(signature string, shopId string, secret string, actionSuccess string, approvalCode string, wsPayOrderId string) error {
|
||||||
/**
|
/**
|
||||||
Represents a signature created from string formatted from following values in a following order using
|
Represents a signature created from string formatted from following values in a following order using
|
||||||
SHA512 algorithm:
|
SHA512 algorithm:
|
||||||
|
@ -356,7 +357,7 @@ func CompareStatusCheckReturnSignature(signature string, shopId string, secret s
|
||||||
ApprovalCode
|
ApprovalCode
|
||||||
WsPayOrderId
|
WsPayOrderId
|
||||||
*/
|
*/
|
||||||
calculatedSignature := shopId + secret + actionSuccess + approvalCode + secret + shopId + approvalCode + cartId
|
calculatedSignature := shopId + secret + actionSuccess + approvalCode + secret + shopId + approvalCode + wsPayOrderId
|
||||||
hash := sha512.New()
|
hash := sha512.New()
|
||||||
hash.Write([]byte(calculatedSignature))
|
hash.Write([]byte(calculatedSignature))
|
||||||
if hex.EncodeToString(hash.Sum(nil)) == signature {
|
if hex.EncodeToString(hash.Sum(nil)) == signature {
|
|
@ -84,16 +84,16 @@ type FormCancel struct {
|
||||||
type CompletionRequest struct {
|
type CompletionRequest struct {
|
||||||
Version string
|
Version string
|
||||||
WsPayOrderId string
|
WsPayOrderId string
|
||||||
ShopId string
|
ShopID string
|
||||||
ApprovalCode string
|
ApprovalCode string
|
||||||
STAN string
|
STAN string
|
||||||
Amount int64
|
Amount string
|
||||||
Signature string
|
Signature string
|
||||||
}
|
}
|
||||||
|
|
||||||
type CompletionResponse struct {
|
type CompletionResponse struct {
|
||||||
WsPayOrderId string
|
WsPayOrderId string
|
||||||
ShopId string
|
ShopID string
|
||||||
ApprovalCode string
|
ApprovalCode string
|
||||||
STAN string
|
STAN string
|
||||||
ErrorMessage string
|
ErrorMessage string
|
||||||
|
@ -103,7 +103,7 @@ type CompletionResponse struct {
|
||||||
|
|
||||||
type StatusCheckRequest struct {
|
type StatusCheckRequest struct {
|
||||||
Version string
|
Version string
|
||||||
ShopId string
|
ShopID string
|
||||||
ShoppingCartId string
|
ShoppingCartId string
|
||||||
Signature string
|
Signature string
|
||||||
}
|
}
|
||||||
|
@ -115,8 +115,8 @@ type StatusCheckResponse struct {
|
||||||
ApprovalCode string
|
ApprovalCode string
|
||||||
ShopID string
|
ShopID string
|
||||||
ShoppingCartID string
|
ShoppingCartID string
|
||||||
Amount int64
|
Amount float64
|
||||||
CurrencyCode string
|
CurrencyCode int
|
||||||
ActionSuccess string
|
ActionSuccess string
|
||||||
Success string // deprecated
|
Success string // deprecated
|
||||||
Authorized string
|
Authorized string
|
||||||
|
@ -125,7 +125,7 @@ type StatusCheckResponse struct {
|
||||||
Refunded string
|
Refunded string
|
||||||
PaymentPlan string
|
PaymentPlan string
|
||||||
Partner string
|
Partner string
|
||||||
OnSite int
|
OnSite string
|
||||||
CreditCardName string
|
CreditCardName string
|
||||||
CreditCardNumber string
|
CreditCardNumber string
|
||||||
ECI string
|
ECI string
|
4
go.mod
4
go.mod
|
@ -1,6 +1,6 @@
|
||||||
module payment-poc
|
module payment-poc
|
||||||
|
|
||||||
go 1.19
|
go 1.22
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gin-gonic/gin v1.9.1
|
github.com/gin-gonic/gin v1.9.1
|
||||||
|
@ -8,6 +8,7 @@ require (
|
||||||
github.com/jmoiron/sqlx v1.3.5
|
github.com/jmoiron/sqlx v1.3.5
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/lib/pq v1.10.9
|
github.com/lib/pq v1.10.9
|
||||||
|
github.com/stripe/stripe-go/v72 v72.122.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
@ -26,7 +27,6 @@ require (
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||||
github.com/stripe/stripe-go/v72 v72.122.0 // indirect
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||||
golang.org/x/arch v0.3.0 // indirect
|
golang.org/x/arch v0.3.0 // indirect
|
||||||
|
|
446
main.go
446
main.go
|
@ -2,399 +2,51 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
"github.com/stripe/stripe-go/v72"
|
"log/slog"
|
||||||
"html/template"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"payment-poc/database"
|
"payment-poc/api"
|
||||||
"payment-poc/migration"
|
"payment-poc/migration"
|
||||||
"payment-poc/providers/mock"
|
"runtime/debug"
|
||||||
stripe2 "payment-poc/providers/stripe"
|
|
||||||
"payment-poc/providers/viva"
|
|
||||||
wspay2 "payment-poc/providers/wspay"
|
|
||||||
"payment-poc/state"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed db/dev/*.sql
|
//go:embed db/dev/*.sql
|
||||||
var devMigrations embed.FS
|
var devMigrations embed.FS
|
||||||
|
|
||||||
type PaymentProvider interface {
|
|
||||||
CreatePaymentUrl(entry database.PaymentEntry) (updatedEntry database.PaymentEntry, url string, err error)
|
|
||||||
CompleteTransaction(entry database.PaymentEntry, amount int64) (database.PaymentEntry, error)
|
|
||||||
CancelTransaction(entry database.PaymentEntry) (database.PaymentEntry, error)
|
|
||||||
|
|
||||||
UpdatePayment(entry database.PaymentEntry) (updatedEntry database.PaymentEntry, err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
godotenv.Load()
|
godotenv.Load()
|
||||||
|
if !hasProfile("dev") {
|
||||||
log.SetPrefix("")
|
gin.SetMode(gin.ReleaseMode)
|
||||||
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
|
}
|
||||||
|
if value := os.Getenv("LOG_FORMAT"); value == "json" {
|
||||||
|
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{AddSource: true})))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
commit, buildTime := buildInfo()
|
||||||
|
slog.Info("build info", slog.String("commit", commit), slog.String("time", buildTime))
|
||||||
|
|
||||||
client, err := connectToDb()
|
client, err := connectToDb()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("couldn't connect to db: %v", err)
|
slog.Error("couldn't connect to db", slog.String("err", err.Error()))
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
if err := migration.InitializeMigrations(client, devMigrations); err != nil {
|
if err := migration.InitializeMigrations(client, devMigrations); err != nil {
|
||||||
log.Fatalf("couldn't execute migrations: %v", err)
|
slog.Error("couldn't finish migration", slog.String("err", err.Error()))
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
g := gin.Default()
|
server := api.SetupServer(client)
|
||||||
|
|
||||||
if !hasProfile("no-auth") {
|
port := ":" + getOrDefault("SERVER_PORT", "5281")
|
||||||
g.Use(gin.BasicAuth(getAccounts()))
|
slog.Info("app is ready", slog.String("port", port))
|
||||||
|
if err := http.ListenAndServe(port, server); err != nil {
|
||||||
|
slog.Error("Couldn't start server!\n", slog.Any("err", err.Error()))
|
||||||
}
|
}
|
||||||
|
|
||||||
g.SetFuncMap(template.FuncMap{
|
|
||||||
"formatCurrency": formatCurrency,
|
|
||||||
"formatCurrencyPtr": formatCurrencyPtr,
|
|
||||||
"decimalCurrency": decimalCurrency,
|
|
||||||
"formatState": formatState,
|
|
||||||
"omitempty": omitempty,
|
|
||||||
})
|
|
||||||
|
|
||||||
g.NoRoute(func(c *gin.Context) {
|
|
||||||
c.JSON(http.StatusNotFound, gin.H{"message": "no action on given url", "created": time.Now()})
|
|
||||||
})
|
|
||||||
g.NoMethod(func(c *gin.Context) {
|
|
||||||
c.JSON(http.StatusMethodNotAllowed, gin.H{"message": "no action on given method", "created": time.Now()})
|
|
||||||
})
|
|
||||||
|
|
||||||
backendUrl := envMustExist("BACKEND_URL")
|
|
||||||
|
|
||||||
paymentGateways := map[state.PaymentGateway]PaymentProvider{}
|
|
||||||
entryProvider := &database.PaymentEntryProvider{DB: client}
|
|
||||||
|
|
||||||
g.LoadHTMLGlob("./templates/*.gohtml")
|
|
||||||
|
|
||||||
if hasProfile(string(state.GatewayMock)) {
|
|
||||||
mockService := mock.Service{
|
|
||||||
BackendUrl: backendUrl,
|
|
||||||
}
|
|
||||||
mockHandlers(g.Group("mock"), entryProvider, &mockService)
|
|
||||||
paymentGateways[state.GatewayMock] = &mockService
|
|
||||||
log.Printf("Registered provider: %s", state.GatewayMock)
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasProfile(string(state.GatewayWsPay)) {
|
|
||||||
wspayService := wspay2.Service{
|
|
||||||
ShopId: envMustExist("WSPAY_SHOP_ID"),
|
|
||||||
ShopSecret: envMustExist("WSPAY_SHOP_SECRET"),
|
|
||||||
BackendUrl: backendUrl,
|
|
||||||
}
|
|
||||||
wsPayHandlers(g.Group("wspay"), entryProvider, &wspayService)
|
|
||||||
paymentGateways[state.GatewayWsPay] = &wspayService
|
|
||||||
log.Printf("Registered provider: %s", state.GatewayWsPay)
|
|
||||||
}
|
|
||||||
if hasProfile(string(state.GatewayStripe)) {
|
|
||||||
stripeService := stripe2.Service{
|
|
||||||
ApiKey: envMustExist("STRIPE_KEY"),
|
|
||||||
BackendUrl: backendUrl,
|
|
||||||
}
|
|
||||||
stripeHandlers(g.Group("stripe"), entryProvider, &stripeService)
|
|
||||||
paymentGateways[state.GatewayStripe] = &stripeService
|
|
||||||
stripe.Key = envMustExist("STRIPE_KEY")
|
|
||||||
log.Printf("Registered provider: %s", state.GatewayStripe)
|
|
||||||
}
|
|
||||||
if hasProfile(string(state.GatewayVivaWallet)) {
|
|
||||||
vivaService := viva.Service{
|
|
||||||
ClientId: envMustExist("VIVA_WALLET_CLIENT_ID"),
|
|
||||||
ClientSecret: envMustExist("VIVA_WALLET_CLIENT_SECRET"),
|
|
||||||
SourceCode: envMustExist("VIVA_WALLET_SOURCE_CODE"),
|
|
||||||
MerchantId: envMustExist("VIVA_WALLET_MERCHANT_ID"),
|
|
||||||
ApiKey: envMustExist("VIVA_WALLET_API_KEY"),
|
|
||||||
}
|
|
||||||
vivaHandlers(g.Group("viva"), entryProvider, &vivaService)
|
|
||||||
paymentGateways[state.GatewayVivaWallet] = &vivaService
|
|
||||||
log.Printf("Registered provider: %s", state.GatewayVivaWallet)
|
|
||||||
}
|
|
||||||
|
|
||||||
g.GET("/", func(c *gin.Context) {
|
|
||||||
entries, _ := entryProvider.FetchAll()
|
|
||||||
c.HTML(200, "index.gohtml", gin.H{"Entries": entries})
|
|
||||||
})
|
|
||||||
g.GET("/methods", func(c *gin.Context) {
|
|
||||||
amount, err := strconv.ParseFloat(c.Query("amount"), 64)
|
|
||||||
if err != nil {
|
|
||||||
amount = 10.00
|
|
||||||
}
|
|
||||||
c.HTML(200, "methods.gohtml", gin.H{"Amount": amount, "Gateways": mapGateways(paymentGateways)})
|
|
||||||
})
|
|
||||||
g.GET("/methods/:gateway", func(c *gin.Context) {
|
|
||||||
gateway, err := fetchGateway(c.Param("gateway"))
|
|
||||||
if err != nil {
|
|
||||||
c.AbortWithError(http.StatusBadRequest, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if paymentGateway, contains := paymentGateways[gateway]; contains {
|
|
||||||
amount, err := fetchAmount(c.Query("amount"))
|
|
||||||
if err != nil {
|
|
||||||
c.AbortWithError(http.StatusBadRequest, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
entry, err := entryProvider.CreateEntry(database.PaymentEntry{
|
|
||||||
Gateway: gateway,
|
|
||||||
State: state.StatePreinitialized,
|
|
||||||
TotalAmount: amount,
|
|
||||||
})
|
|
||||||
log.Printf("[%s:%s] creating payment with gateway '%s' for '%f'", entry.Id.String(), entry.State, gateway, float64(amount)/100.0)
|
|
||||||
if entry, url, err := paymentGateway.CreatePaymentUrl(entry); err == nil {
|
|
||||||
log.Printf("[%s:%s] created redirect url", entry.Id, entry.State)
|
|
||||||
entryProvider.UpdateEntry(entry)
|
|
||||||
c.Redirect(http.StatusSeeOther, url)
|
|
||||||
} else {
|
|
||||||
c.AbortWithError(http.StatusBadRequest, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
c.AbortWithError(http.StatusBadRequest, errors.New("unsupported payment gateway: "+string(gateway)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
g.GET("/entries/:id", func(c *gin.Context) {
|
|
||||||
id := uuid.MustParse(c.Param("id"))
|
|
||||||
entry, err := entryProvider.FetchById(id)
|
|
||||||
if err != nil {
|
|
||||||
c.AbortWithError(http.StatusBadRequest, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.HTML(200, "info.gohtml", gin.H{"Entry": entry})
|
|
||||||
})
|
|
||||||
g.POST("/entries/:id/complete", func(c *gin.Context) {
|
|
||||||
id := uuid.MustParse(c.Param("id"))
|
|
||||||
entry, err := entryProvider.FetchById(id)
|
|
||||||
if err != nil {
|
|
||||||
c.AbortWithError(http.StatusBadRequest, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if paymentGateway, ok := paymentGateways[entry.Gateway]; ok {
|
|
||||||
amount, err := fetchAmount(c.PostForm("amount"))
|
|
||||||
if err != nil {
|
|
||||||
c.AbortWithError(http.StatusBadRequest, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Printf("[%s:%s] completing payment with amount %f", id.String(), entry.State, float64(amount)/100.0)
|
|
||||||
entry, err = paymentGateway.CompleteTransaction(entry, amount)
|
|
||||||
if err == nil {
|
|
||||||
entryProvider.UpdateEntry(entry)
|
|
||||||
log.Printf("[%s:%s] completed payment", id.String(), entry.State)
|
|
||||||
c.Redirect(http.StatusSeeOther, "/entries/"+id.String())
|
|
||||||
} else {
|
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err != nil {
|
|
||||||
c.AbortWithError(http.StatusInternalServerError, errors.New("payment gateway not supported: "+string(entry.Gateway)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
g.POST("/entries/:id/cancel", func(c *gin.Context) {
|
|
||||||
id := uuid.MustParse(c.Param("id"))
|
|
||||||
entry, err := entryProvider.FetchById(id)
|
|
||||||
if err != nil {
|
|
||||||
c.AbortWithError(http.StatusBadRequest, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if paymentGateway, ok := paymentGateways[entry.Gateway]; ok {
|
|
||||||
log.Printf("[%s:%s] canceling payment", id.String(), entry.State)
|
|
||||||
entry, err = paymentGateway.CancelTransaction(entry)
|
|
||||||
if err == nil {
|
|
||||||
entryProvider.UpdateEntry(entry)
|
|
||||||
log.Printf("[%s:%s] canceled payment", id.String(), entry.State)
|
|
||||||
c.Redirect(http.StatusSeeOther, "/entries/"+id.String())
|
|
||||||
} else {
|
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err != nil {
|
|
||||||
c.AbortWithError(http.StatusInternalServerError, errors.New("payment gateway not supported: "+string(entry.Gateway)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
g.POST("/entries/:id/refresh", func(c *gin.Context) {
|
|
||||||
id := uuid.MustParse(c.Param("id"))
|
|
||||||
entry, err := entryProvider.FetchById(id)
|
|
||||||
if err != nil {
|
|
||||||
c.AbortWithError(http.StatusBadRequest, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if paymentGateway, ok := paymentGateways[entry.Gateway]; ok {
|
|
||||||
log.Printf("[%s:%s] fetching payment info", entry.Id.String(), entry.State)
|
|
||||||
entry, err = paymentGateway.UpdatePayment(entry)
|
|
||||||
if err == nil {
|
|
||||||
entryProvider.UpdateEntry(entry)
|
|
||||||
log.Printf("[%s:%s] fetched payment info", entry.Id.String(), entry.State)
|
|
||||||
}
|
|
||||||
c.Redirect(http.StatusSeeOther, "/entries/"+id.String())
|
|
||||||
} else {
|
|
||||||
if err != nil {
|
|
||||||
c.AbortWithError(http.StatusInternalServerError, errors.New("payment gateway not supported: "+string(entry.Gateway)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
log.Fatal(http.ListenAndServe(":5281", g))
|
|
||||||
}
|
|
||||||
|
|
||||||
func mockHandlers(g *gin.RouterGroup, provider *database.PaymentEntryProvider, mockService *mock.Service) {
|
|
||||||
g.GET("/gateway/:id", func(c *gin.Context) {
|
|
||||||
id := uuid.MustParse(c.Param("id"))
|
|
||||||
entry, err := provider.FetchById(id)
|
|
||||||
if err != nil {
|
|
||||||
c.AbortWithError(http.StatusBadRequest, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.HTML(http.StatusOK, "mock_gateway.gohtml", gin.H{"Entry": entry})
|
|
||||||
})
|
|
||||||
g.GET("success", func(c *gin.Context) {
|
|
||||||
url, err := mockService.HandleResponse(c, provider, state.StateAccepted)
|
|
||||||
if err != nil {
|
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.Redirect(http.StatusSeeOther, url)
|
|
||||||
})
|
|
||||||
g.GET("error", func(c *gin.Context) {
|
|
||||||
url, err := mockService.HandleResponse(c, provider, state.StateError)
|
|
||||||
if err != nil {
|
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.Redirect(http.StatusSeeOther, url)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func mapGateways(gateways map[state.PaymentGateway]PaymentProvider) map[string]string {
|
|
||||||
providerMap := map[string]string{}
|
|
||||||
|
|
||||||
for key := range gateways {
|
|
||||||
providerMap[string(key)] = mapGatewayName(key)
|
|
||||||
}
|
|
||||||
return providerMap
|
|
||||||
}
|
|
||||||
|
|
||||||
func mapGatewayName(key state.PaymentGateway) string {
|
|
||||||
switch key {
|
|
||||||
case state.GatewayStripe:
|
|
||||||
return "Stripe"
|
|
||||||
case state.GatewayVivaWallet:
|
|
||||||
return "Viva wallet"
|
|
||||||
case state.GatewayWsPay:
|
|
||||||
return "WsPay"
|
|
||||||
case state.GatewayMock:
|
|
||||||
return "mock"
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchGateway(gateway string) (state.PaymentGateway, error) {
|
|
||||||
switch gateway {
|
|
||||||
case string(state.GatewayWsPay):
|
|
||||||
return state.GatewayWsPay, nil
|
|
||||||
case string(state.GatewayStripe):
|
|
||||||
return state.GatewayStripe, nil
|
|
||||||
case string(state.GatewayVivaWallet):
|
|
||||||
return state.GatewayVivaWallet, nil
|
|
||||||
case string(state.GatewayMock):
|
|
||||||
return state.GatewayMock, nil
|
|
||||||
}
|
|
||||||
return "", errors.New("unknown gateway: " + gateway)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getAccounts() gin.Accounts {
|
|
||||||
auth := strings.Split(envMustExist("AUTH"), ":")
|
|
||||||
return gin.Accounts{auth[0]: auth[1]}
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchAmount(amount string) (int64, error) {
|
|
||||||
if amount, err := strconv.ParseFloat(amount, 64); err == nil {
|
|
||||||
return int64(amount * 100), nil
|
|
||||||
} else {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func vivaHandlers(g *gin.RouterGroup, provider *database.PaymentEntryProvider, vivaService *viva.Service) {
|
|
||||||
g.GET("success", func(c *gin.Context) {
|
|
||||||
url, err := vivaService.HandleResponse(c, provider, state.StateAccepted)
|
|
||||||
if err != nil {
|
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.Redirect(http.StatusSeeOther, url)
|
|
||||||
})
|
|
||||||
g.GET("error", func(c *gin.Context) {
|
|
||||||
url, err := vivaService.HandleResponse(c, provider, state.StateError)
|
|
||||||
if err != nil {
|
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.Redirect(http.StatusSeeOther, url)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func stripeHandlers(g *gin.RouterGroup, provider *database.PaymentEntryProvider, stripeService *stripe2.Service) {
|
|
||||||
|
|
||||||
g.GET("success", func(c *gin.Context) {
|
|
||||||
url, err := stripeService.HandleResponse(c, provider, state.StateAccepted)
|
|
||||||
if err != nil {
|
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.Redirect(http.StatusSeeOther, url)
|
|
||||||
})
|
|
||||||
g.GET("error", func(c *gin.Context) {
|
|
||||||
url, err := stripeService.HandleResponse(c, provider, state.StateError)
|
|
||||||
if err != nil {
|
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.Redirect(http.StatusSeeOther, url)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func wsPayHandlers(g *gin.RouterGroup, provider *database.PaymentEntryProvider, wspayService *wspay2.Service) {
|
|
||||||
g.GET("success", func(c *gin.Context) {
|
|
||||||
url, err := wspayService.HandleSuccessResponse(c, provider)
|
|
||||||
if err != nil {
|
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.Redirect(http.StatusSeeOther, url)
|
|
||||||
})
|
|
||||||
g.GET("error", func(c *gin.Context) {
|
|
||||||
url, err := wspayService.HandleErrorResponse(c, provider, state.StateError)
|
|
||||||
if err != nil {
|
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.Redirect(http.StatusSeeOther, url)
|
|
||||||
})
|
|
||||||
g.GET("cancel", func(c *gin.Context) {
|
|
||||||
url, err := wspayService.HandleErrorResponse(c, provider, state.StateCanceled)
|
|
||||||
if err != nil {
|
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.Redirect(http.StatusSeeOther, url)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func hasProfile(profile string) bool {
|
func hasProfile(profile string) bool {
|
||||||
|
@ -407,47 +59,25 @@ func hasProfile(profile string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatState(stt state.PaymentState) string {
|
func buildInfo() (string, string) {
|
||||||
switch stt {
|
revision := ""
|
||||||
case state.StateCanceled:
|
buildTime := ""
|
||||||
return "Otkazana"
|
|
||||||
case state.StateVoided:
|
if info, ok := debug.ReadBuildInfo(); ok {
|
||||||
return "Poništena"
|
for _, setting := range info.Settings {
|
||||||
case state.StateAccepted:
|
if setting.Key == "vcs.revision" {
|
||||||
return "Predautorizirana"
|
revision = setting.Value
|
||||||
case state.StateError:
|
} else if setting.Key == "vcs.time" {
|
||||||
return "Greška"
|
buildTime = setting.Value
|
||||||
case state.StatePreinitialized:
|
|
||||||
return "Predinicijalizirana"
|
|
||||||
case state.StateInitialized:
|
|
||||||
return "Inicijalizirana"
|
|
||||||
case state.StateCanceledInitialization:
|
|
||||||
return "Otkazana tijekom izrade"
|
|
||||||
case state.StateCompleted:
|
|
||||||
return "Autorizirana"
|
|
||||||
}
|
}
|
||||||
return "nepoznato stanje '" + string(stt) + "'"
|
}
|
||||||
|
}
|
||||||
|
return revision, buildTime
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatCurrency(current int64) string {
|
func getOrDefault(env string, defaultValue string) string {
|
||||||
return fmt.Sprintf("%d,%02d", current/100, current%100)
|
if value, present := os.LookupEnv(env); present {
|
||||||
}
|
|
||||||
|
|
||||||
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
|
return value
|
||||||
}
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
28
makefile
28
makefile
|
@ -1,30 +1,18 @@
|
||||||
# scripts for building app
|
# scripts for building app
|
||||||
# requires go 1.19+ and git installed
|
# requires go 1.22+ and git installed
|
||||||
|
|
||||||
VERSION := 0.1.0
|
VERSION := $(shell git describe --tags --always)
|
||||||
|
|
||||||
serve:
|
|
||||||
go run ./...
|
|
||||||
|
|
||||||
setup:
|
|
||||||
go get
|
|
||||||
|
|
||||||
docker-dev:
|
docker-dev:
|
||||||
docker image build -t registry.s2internal.com/opgdirekt/payment-poc/backend:$(VERSION)-dev .
|
docker image build -t registry.bbr-dev.info/payment-poc/backend/dev:latest .
|
||||||
docker tag registry.s2internal.com/opgdirekt/payment-poc/backend:$(VERSION)-dev registry.s2internal.com/opgdirekt/payment-poc/backend:latest-dev
|
docker image push registry.bbr-dev.info/payment-poc/backend/dev:latest
|
||||||
docker image push registry.s2internal.com/opgdirekt/payment-poc/backend:$(VERSION)-dev
|
|
||||||
docker image push registry.s2internal.com/opgdirekt/payment-poc/backend:latest-dev
|
|
||||||
|
|
||||||
|
|
||||||
docker-prod:
|
docker-prod:
|
||||||
docker image build -t registry.s2internal.com/opgdirekt/payment-poc/backend:$(VERSION) .
|
docker image build -t registry.bbr-dev.info/payment-poc/backend:$(VERSION) .
|
||||||
docker tag registry.s2internal.com/opgdirekt/payment-poc/backend:$(VERSION) registry.s2internal.com/opgdirekt/payment-poc/backend:latest
|
docker tag registry.bbr-dev.info/payment-poc/backend:$(VERSION) registry.bbr-dev.info/payment-poc/backend:latest
|
||||||
docker image push registry.s2internal.com/opgdirekt/payment-poc/backend:$(VERSION)
|
docker image push registry.bbr-dev.info/payment-poc/backend:$(VERSION)
|
||||||
docker image push registry.s2internal.com/opgdirekt/payment-poc/backend:latest
|
docker image push registry.bbr-dev.info/payment-poc/backend:latest
|
||||||
|
|
||||||
release:
|
|
||||||
git tag $(VERSION)
|
|
||||||
git push origin $(VERSION)
|
|
||||||
|
|
||||||
test:
|
test:
|
||||||
go test ./...
|
go test ./...
|
||||||
|
|
|
@ -4,10 +4,11 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log"
|
"log/slog"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -92,7 +93,8 @@ func validateMigrations(db *sqlx.DB, migrations map[string]Migration, migrationF
|
||||||
}
|
}
|
||||||
|
|
||||||
func executeMigration(db *sqlx.DB, name string, script string) error {
|
func executeMigration(db *sqlx.DB, name string, script string) error {
|
||||||
log.Printf("[INFO] script='%s' | migrations - executing", name)
|
logger := slog.Default().With(slog.String("script", name))
|
||||||
|
logger.Info("migrations - executing")
|
||||||
tx := db.MustBeginTx(context.Background(), nil)
|
tx := db.MustBeginTx(context.Background(), nil)
|
||||||
var err error = nil
|
var err error = nil
|
||||||
if _, e := tx.Exec(script); e != nil {
|
if _, e := tx.Exec(script); e != nil {
|
||||||
|
@ -102,10 +104,10 @@ func executeMigration(db *sqlx.DB, name string, script string) error {
|
||||||
err = e
|
err = e
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[ERROR] script='%s' | migrations - failed executing", name)
|
logger.Error("migrations - failed executing", slog.String("err", err.Error()))
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
} else {
|
} else {
|
||||||
log.Printf("[INFO] script='%s' | migrations - succesfully executed", name)
|
logger.Info("migrations - successfully executed")
|
||||||
tx.Commit()
|
tx.Commit()
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
|
@ -119,9 +121,9 @@ func validateMigration(name string, migration Migration, script string) error {
|
||||||
calculatedHash := hash(script)
|
calculatedHash := hash(script)
|
||||||
|
|
||||||
if calculatedHash != migration.Hash {
|
if calculatedHash != migration.Hash {
|
||||||
err := fmt.Sprintf("migrations - mismatch in hash for %s (expected '%s', calculated '%s')", name, migration.Hash, calculatedHash)
|
err := errors.New(fmt.Sprintf("migrations - mismatch in hash for %s (expected '%s', calculated '%s')", name, migration.Hash, calculatedHash))
|
||||||
log.Printf("[ERROR] script='%s' err='%s' | migrations - failed executing", script, err)
|
slog.Error("migrations - failed validation", slog.String("script", name), slog.String("err", err.Error()))
|
||||||
return fmt.Errorf("migrations - mismatch in hashes for %s", name)
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -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">
|
<section class="container">
|
||||||
<h2>Mock gateway {{.Entry.Id.String}}</h2>
|
<h2>Mock gateway {{.Entry.Id.String}}</h2>
|
||||||
<p>{{formatCurrency .Entry.TotalAmount}}</p>
|
<p>{{formatCurrency .Entry.TotalAmount}}</p>
|
||||||
<a href="/providers/mock/success?id={{.Entry.Id.String}}" class="btn btn-success">Potvrdi plaćanje</a>
|
<a href="/mock/success?id={{.Entry.Id.String}}" class="btn btn-success">Potvrdi plaćanje</a>
|
||||||
<a href="/providers/mock/error?id={{.Entry.Id.String}}" class="btn btn-danger">Otkaži plaćanje</a>
|
<a href="/mock/error?id={{.Entry.Id.String}}" class="btn btn-danger">Otkaži plaćanje</a>
|
||||||
</section>
|
</section>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
Loading…
Reference in New Issue