Reorganized code + migration to go1.22
This commit is contained in:
1 parent
e3d77d55b9
commit
7369777098
25 files changed
+825
-605
No files matched your search
+332
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user