Reorganized code + migration to go1.22

This commit is contained in:
brajkovic committed 2024-04-01 20:29:24 +02:00
1 parent e3d77d55b9
commit 7369777098
25 files changed
+825 -605

No files matched your search

+332
View File
@@ -0,0 +1,332 @@
package api
import (
"errors"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"log/slog"
"net/http"
"payment-poc/domain/database"
"payment-poc/domain/providers"
"payment-poc/domain/providers/mock"
stripe2 "payment-poc/domain/providers/stripe"
"payment-poc/domain/providers/viva"
wspay2 "payment-poc/domain/providers/wspay"
"payment-poc/domain/state"
"strconv"
"time"
)
func NoMethod() gin.HandlerFunc {
return func(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{
"status": 404,
"created": time.Now(),
"message": "no handler for method '" + c.Request.Method + "'",
})
}
}
func NoRoute() gin.HandlerFunc {
return func(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{
"status": 404,
"created": time.Now(),
"message": "no handler for " + c.Request.Method + " '" + c.Request.URL.RequestURI() + "'",
})
}
}
func RefreshPayment(entryProvider *database.PaymentEntryProvider, paymentGateways map[state.PaymentGateway]providers.PaymentProvider) gin.HandlerFunc {
return func(c *gin.Context) {
id := uuid.MustParse(c.Param("id"))
entry, err := entryProvider.FetchById(id)
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
if paymentGateway, ok := paymentGateways[entry.Gateway]; ok {
slog.Info("fetching payment info", "entry_id", entry.Id.String(), "state", entry.State)
entry, err = paymentGateway.UpdatePayment(entry)
if err == nil {
entryProvider.UpdateEntry(entry)
slog.Info("fetched payment info", "entry_id", entry.Id.String(), "state", entry.State)
}
c.Redirect(http.StatusSeeOther, "/entries/"+id.String())
} else {
if err != nil {
c.AbortWithError(http.StatusInternalServerError, errors.New("payment gateway not supported: "+string(entry.Gateway)))
return
}
}
}
}
func CancelPayment(entryProvider *database.PaymentEntryProvider, paymentGateways map[state.PaymentGateway]providers.PaymentProvider) gin.HandlerFunc {
return func(c *gin.Context) {
id := uuid.MustParse(c.Param("id"))
entry, err := entryProvider.FetchById(id)
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
if paymentGateway, ok := paymentGateways[entry.Gateway]; ok {
slog.Info("canceling payment", "entry_id", id.String(), "state", entry.State)
entry, err = paymentGateway.CancelTransaction(entry)
if err == nil {
entryProvider.UpdateEntry(entry)
slog.Info("canceled payment", "entry_id", entry.Id.String(), "state", entry.State)
c.Redirect(http.StatusSeeOther, "/entries/"+id.String())
} else {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
} else {
if err != nil {
c.AbortWithError(http.StatusInternalServerError, errors.New("payment gateway not supported: "+string(entry.Gateway)))
return
}
}
}
}
func CompletePayment(entryProvider *database.PaymentEntryProvider, paymentGateways map[state.PaymentGateway]providers.PaymentProvider) gin.HandlerFunc {
return func(c *gin.Context) {
id := uuid.MustParse(c.Param("id"))
entry, err := entryProvider.FetchById(id)
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
if paymentGateway, ok := paymentGateways[entry.Gateway]; ok {
amount, err := fetchAmount(c.PostForm("amount"))
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
slog.Info("completing payment with amount", "entry_id", id.String(), "state", entry.State, "amount", float64(amount)/100.0)
entry, err = paymentGateway.CompleteTransaction(entry, amount)
if err == nil {
entryProvider.UpdateEntry(entry)
slog.Info("completed payment", "entry_id", id.String(), "state", entry.State)
c.Redirect(http.StatusSeeOther, "/entries/"+id.String())
} else {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
} else {
if err != nil {
c.AbortWithError(http.StatusInternalServerError, errors.New("payment gateway not supported: "+string(entry.Gateway)))
return
}
}
}
}
func GetEntry(provider *database.PaymentEntryProvider) gin.HandlerFunc {
return func(c *gin.Context) {
id := uuid.MustParse(c.Param("id"))
entry, err := provider.FetchById(id)
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
c.HTML(200, "info.gohtml", gin.H{"Entry": entry})
}
}
func InitializePayment(entryProvider *database.PaymentEntryProvider, paymentGateways map[state.PaymentGateway]providers.PaymentProvider) gin.HandlerFunc {
return func(c *gin.Context) {
gateway, err := fetchGateway(c.Param("gateway"))
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
if paymentGateway, contains := paymentGateways[gateway]; contains {
amount, err := fetchAmount(c.Query("amount"))
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
entry, err := entryProvider.CreateEntry(database.PaymentEntry{
Gateway: gateway,
State: state.StatePreinitialized,
TotalAmount: amount,
})
slog.Info("creating payment", "entry_id", entry.Id.String(), "state", entry.State, "gateway", gateway, "amount", float64(amount)/100.0)
if entry, url, err := paymentGateway.CreatePaymentUrl(entry); err == nil {
slog.Info("created redirect url", "entry_id", entry.Id.String(), "state", entry.State)
entryProvider.UpdateEntry(entry)
c.Redirect(http.StatusSeeOther, url)
} else {
c.AbortWithError(http.StatusBadRequest, err)
return
}
} else {
c.AbortWithError(http.StatusBadRequest, errors.New("unsupported payment gateway: "+string(gateway)))
return
}
}
}
func GetGateways(gateways map[state.PaymentGateway]providers.PaymentProvider) gin.HandlerFunc {
return func(c *gin.Context) {
amount, err := strconv.ParseFloat(c.Query("amount"), 64)
if err != nil {
amount = 10.00
}
c.HTML(200, "methods.gohtml", gin.H{"Amount": amount, "Gateways": mapGateways(gateways)})
}
}
func GetIndex(provider *database.PaymentEntryProvider) gin.HandlerFunc {
return func(c *gin.Context) {
entries, _ := provider.FetchAll()
c.HTML(200, "index.gohtml", gin.H{"Entries": entries})
}
}
func VivaOnFailure(vivaService viva.Service, provider *database.PaymentEntryProvider) gin.HandlerFunc {
return func(c *gin.Context) {
url, err := vivaService.HandleResponse(c, provider, state.StateError)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.Redirect(http.StatusSeeOther, url)
}
}
func VivaOnSuccess(vivaService viva.Service, provider *database.PaymentEntryProvider) gin.HandlerFunc {
return func(c *gin.Context) {
url, err := vivaService.HandleResponse(c, provider, state.StateAccepted)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.Redirect(http.StatusSeeOther, url)
}
}
func StripeOnFailure(stripeService stripe2.Service, provider *database.PaymentEntryProvider) gin.HandlerFunc {
return func(c *gin.Context) {
url, err := stripeService.HandleResponse(c, provider, state.StateError)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.Redirect(http.StatusSeeOther, url)
}
}
func StripeOnSuccess(stripeService stripe2.Service, provider *database.PaymentEntryProvider) gin.HandlerFunc {
return func(c *gin.Context) {
url, err := stripeService.HandleResponse(c, provider, state.StateAccepted)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.Redirect(http.StatusSeeOther, url)
}
}
func WsPayOnFailure(wspayService wspay2.Service, provider *database.PaymentEntryProvider, finalState state.PaymentState) gin.HandlerFunc {
return func(c *gin.Context) {
url, err := wspayService.HandleErrorResponse(c, provider, finalState)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.Redirect(http.StatusSeeOther, url)
}
}
func WsPayOnSuccess(wspayService wspay2.Service, provider *database.PaymentEntryProvider) gin.HandlerFunc {
return func(c *gin.Context) {
url, err := wspayService.HandleSuccessResponse(c, provider)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.Redirect(http.StatusSeeOther, url)
}
}
func MockOnFailure(mockService mock.Service, provider *database.PaymentEntryProvider) gin.HandlerFunc {
return func(c *gin.Context) {
url, err := mockService.HandleResponse(c, provider, state.StateError)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.Redirect(http.StatusSeeOther, url)
}
}
func MockOnSuccess(mockService mock.Service, provider *database.PaymentEntryProvider) gin.HandlerFunc {
return func(c *gin.Context) {
url, err := mockService.HandleResponse(c, provider, state.StateAccepted)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.Redirect(http.StatusSeeOther, url)
}
}
func MockOpenGateway(provider *database.PaymentEntryProvider) gin.HandlerFunc {
return func(c *gin.Context) {
id := uuid.MustParse(c.Param("id"))
entry, err := provider.FetchById(id)
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
c.HTML(http.StatusOK, "mock_gateway.gohtml", gin.H{"Entry": entry})
}
}
func mapGateways(gateways map[state.PaymentGateway]providers.PaymentProvider) map[string]string {
providerMap := map[string]string{}
for key := range gateways {
providerMap[string(key)] = mapGatewayName(key)
}
return providerMap
}
func mapGatewayName(key state.PaymentGateway) string {
switch key {
case state.GatewayStripe:
return "Stripe"
case state.GatewayVivaWallet:
return "Viva wallet"
case state.GatewayWsPay:
return "WsPay"
case state.GatewayMock:
return "mock"
}
return ""
}
func fetchGateway(gateway string) (state.PaymentGateway, error) {
switch gateway {
case string(state.GatewayWsPay):
return state.GatewayWsPay, nil
case string(state.GatewayStripe):
return state.GatewayStripe, nil
case string(state.GatewayVivaWallet):
return state.GatewayVivaWallet, nil
case string(state.GatewayMock):
return state.GatewayMock, nil
}
return "", errors.New("unknown gateway: " + gateway)
}
func fetchAmount(amount string) (int64, error) {
if amount, err := strconv.ParseFloat(amount, 64); err == nil {
return int64(amount * 100), nil
} else {
return 0, err
}
}
+39
View File
@@ -0,0 +1,39 @@
package api
import (
"fmt"
"github.com/gin-gonic/gin"
"os"
"strings"
)
func Auth() gin.HandlerFunc {
if !hasProfile("no-auth") {
return gin.BasicAuth(loadAuth())
}
return nil
}
func hasProfile(value string) bool {
profileOptions := strings.Split(os.Getenv("PROFILE"), ",")
for _, option := range profileOptions {
if option == value {
return true
}
}
return false
}
func loadAuth() map[string]string {
credentials := envMustExist("AUTH")
values := strings.Split(credentials, ":")
return map[string]string{values[0]: values[1]}
}
func envMustExist(env string) string {
if value, exists := os.LookupEnv(env); !exists {
panic(fmt.Sprintf("env variable '%s' not defined", env))
} else {
return value
}
}
+84
View File
@@ -0,0 +1,84 @@
package api
import (
"github.com/gin-gonic/gin"
"github.com/jmoiron/sqlx"
"log/slog"
"payment-poc/domain/database"
"payment-poc/domain/providers"
"payment-poc/domain/providers/mock"
stripe2 "payment-poc/domain/providers/stripe"
"payment-poc/domain/providers/viva"
wspay2 "payment-poc/domain/providers/wspay"
"payment-poc/domain/state"
)
func RegisterRoutes(server *gin.Engine, db *sqlx.DB) {
backendUrl := envMustExist("BACKEND_URL")
paymentGateways := map[state.PaymentGateway]providers.PaymentProvider{}
entryProvider := &database.PaymentEntryProvider{DB: db}
server.GET("/", GetIndex(entryProvider))
server.GET("/methods", GetGateways(paymentGateways))
server.GET("/methods/:gateway", InitializePayment(entryProvider, paymentGateways))
server.GET("/entries/:id", GetEntry(entryProvider))
server.POST("/entries/:id/complete", CompletePayment(entryProvider, paymentGateways))
server.POST("/entries/:id/cancel", CancelPayment(entryProvider, paymentGateways))
server.POST("/entries/:id/refresh", RefreshPayment(entryProvider, paymentGateways))
if hasProfile(string(state.GatewayMock)) {
mockService := mock.Service{
BackendUrl: backendUrl,
}
paymentGateways[state.GatewayMock] = &mockService
mockGroup := server.Group("mock")
mockGroup.GET("/gateway/:id", MockOpenGateway(entryProvider))
mockGroup.GET("success", MockOnSuccess(mockService, entryProvider))
mockGroup.GET("error", MockOnFailure(mockService, entryProvider))
slog.Info("Registered provider", slog.Any("provider", state.GatewayMock))
}
if hasProfile(string(state.GatewayWsPay)) {
wspayService := wspay2.Service{
ShopId: envMustExist("WSPAY_SHOP_ID"),
ShopSecret: envMustExist("WSPAY_SHOP_SECRET"),
BackendUrl: backendUrl,
}
paymentGateways[state.GatewayWsPay] = &wspayService
wspayGroup := server.Group("wspay")
wspayGroup.GET("success", WsPayOnSuccess(wspayService, entryProvider))
wspayGroup.GET("error", WsPayOnFailure(wspayService, entryProvider, state.StateError))
wspayGroup.GET("cancel", WsPayOnFailure(wspayService, entryProvider, state.StateCanceled))
slog.Info("Registered provider", slog.Any("provider", state.GatewayWsPay))
}
if hasProfile(string(state.GatewayStripe)) {
stripeService := stripe2.Service{
ApiKey: envMustExist("STRIPE_KEY"),
BackendUrl: backendUrl,
}
paymentGateways[state.GatewayStripe] = &stripeService
stripeGroup := server.Group("stripe")
stripeGroup.GET("success", StripeOnSuccess(stripeService, entryProvider))
stripeGroup.GET("error", StripeOnFailure(stripeService, entryProvider))
slog.Info("Registered provider", slog.Any("provider", state.GatewayStripe))
}
if hasProfile(string(state.GatewayVivaWallet)) {
vivaService := viva.Service{
ClientId: envMustExist("VIVA_WALLET_CLIENT_ID"),
ClientSecret: envMustExist("VIVA_WALLET_CLIENT_SECRET"),
SourceCode: envMustExist("VIVA_WALLET_SOURCE_CODE"),
MerchantId: envMustExist("VIVA_WALLET_MERCHANT_ID"),
ApiKey: envMustExist("VIVA_WALLET_API_KEY"),
}
paymentGateways[state.GatewayVivaWallet] = &vivaService
vivaGroup := server.Group("viva")
vivaGroup.GET("success", VivaOnSuccess(vivaService, entryProvider))
vivaGroup.GET("error", VivaOnFailure(vivaService, entryProvider))
slog.Info("Registered provider", slog.Any("provider", state.GatewayVivaWallet))
}
}
+25
View File
@@ -0,0 +1,25 @@
package api
import (
"github.com/gin-gonic/gin"
"github.com/jmoiron/sqlx"
)
func SetupServer(db *sqlx.DB) *gin.Engine {
server := createServer()
LoadTemplates(server)
RegisterRoutes(server, db)
return server
}
func createServer() *gin.Engine {
server := gin.New()
server.NoRoute(NoRoute())
server.NoMethod(NoMethod())
server.Use(gin.Recovery())
auth := Auth()
if auth != nil {
server.Use(Auth())
}
return server
}
+64
View File
@@ -0,0 +1,64 @@
package api
import (
"fmt"
"github.com/gin-gonic/gin"
"html/template"
"payment-poc/domain/state"
)
func LoadTemplates(server *gin.Engine) {
server.SetFuncMap(template.FuncMap{
"formatCurrency": formatCurrency,
"formatCurrencyPtr": formatCurrencyPtr,
"decimalCurrency": decimalCurrency,
"formatState": formatState,
"omitempty": omitempty,
})
server.LoadHTMLGlob("./templates/*.gohtml")
}
func formatState(stt state.PaymentState) string {
switch stt {
case state.StateCanceled:
return "Otkazana"
case state.StateVoided:
return "Poništena"
case state.StateAccepted:
return "Predautorizirana"
case state.StateError:
return "Greška"
case state.StatePreinitialized:
return "Predinicijalizirana"
case state.StateInitialized:
return "Inicijalizirana"
case state.StateCanceledInitialization:
return "Otkazana tijekom izrade"
case state.StateCompleted:
return "Autorizirana"
}
return "nepoznato stanje '" + string(stt) + "'"
}
func formatCurrency(current int64) string {
return fmt.Sprintf("%d,%02d", current/100, current%100)
}
func formatCurrencyPtr(current *int64) string {
if current != nil {
return fmt.Sprintf("%d,%02d", (*current)/100, (*current)%100)
} else {
return "-"
}
}
func decimalCurrency(current int64) string {
return fmt.Sprintf("%d,%02d", current/100, current%100)
}
func omitempty(value string) string {
if value == "" {
return "-"
}
return value
}