package main import ( "embed" "errors" "fmt" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/joho/godotenv" "github.com/stripe/stripe-go/v72" "html/template" "log" "net/http" "os" "payment-poc/database" "payment-poc/migration" "payment-poc/state" stripe2 "payment-poc/stripe" "payment-poc/viva" "payment-poc/wspay" "strconv" "strings" "time" ) //go:embed db/dev/*.sql var devMigrations embed.FS type PaymentProvider interface { CreatePaymentUrl(amount int64) (string, error) CompleteTransaction(entry database.PaymentEntry, amount int64) (database.PaymentEntry, error) CancelTransaction(entry database.PaymentEntry) (database.PaymentEntry, error) } func init() { godotenv.Load() log.SetPrefix("") log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) } func main() { client, err := connectToDb() if err != nil { log.Fatalf("couldn't connect to db: %v", err) } if err := migration.InitializeMigrations(client, devMigrations); err != nil { log.Fatalf("couldn't execute migrations: %v", err) } g := gin.Default() if !hasProfile("no-auth") { g.Use(gin.BasicAuth(getAccounts())) } g.SetFuncMap(template.FuncMap{ "formatCurrency": formatCurrency, "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.GatewayWsPay)) { wspayService := wspay.Service{ Provider: &entryProvider, ShopId: envMustExist("WSPAY_SHOP_ID"), ShopSecret: envMustExist("WSPAY_SHOP_SECRET"), BackendUrl: backendUrl, } setupWsPayEndpoints(g.Group("wspay"), &wspayService) paymentGateways[state.GatewayWsPay] = &wspayService } if hasProfile(string(state.GatewayStripe)) { stripeService := stripe2.Service{ Provider: &entryProvider, ApiKey: envMustExist("STRIPE_KEY"), BackendUrl: backendUrl, } setupStripeEndpoints(g.Group("stripe"), &stripeService) paymentGateways[state.GatewayStripe] = &stripeService stripe.Key = envMustExist("STRIPE_KEY") } if hasProfile(string(state.GatewayVivaWallet)) { vivaService := viva.Service{ Provider: &entryProvider, 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"), } setupVivaEndpoints(g.Group("viva"), &vivaService) paymentGateways[state.GatewayVivaWallet] = &vivaService } 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 } var gateways []state.PaymentGateway for key := range paymentGateways { gateways = append(gateways, key) } c.HTML(200, "methods.gohtml", gin.H{"Amount": amount, "Gateways": gateways}) }) g.GET("/: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 } if url, err := paymentGateway.CreatePaymentUrl(amount); err == nil { 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.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 } entry, err = paymentGateway.CompleteTransaction(entry, amount) if err != nil { entryProvider.UpdateEntry(entry) 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 { entry, err = paymentGateway.CancelTransaction(entry) if err != nil { entryProvider.UpdateEntry(entry) 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 } } }) log.Fatal(http.ListenAndServe(":5281", g)) } 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 } return "", errors.New("unknown gateway: " + gateway) } func getAccounts() gin.Accounts { auth := strings.Split(envMustExist("AUTH"), ":") return gin.Accounts{auth[0]: auth[1]} } func parseDateTime(dateTime string) time.Time { t, err := time.Parse("20060102150405", dateTime) if err != nil { log.Printf("couldn't parse response time %s: %v", dateTime, err) } return t } 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 setupVivaEndpoints(g *gin.RouterGroup, vivaService *viva.Service) { g.GET("success", func(c *gin.Context) { url, err := vivaService.HandleResponse(c, 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, state.StateError) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } c.Redirect(http.StatusSeeOther, url) }) } func setupStripeEndpoints(g *gin.RouterGroup, stripeService *stripe2.Service) { g.GET("success", func(c *gin.Context) { url, err := stripeService.HandleResponse(c, 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, state.StateError) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } c.Redirect(http.StatusSeeOther, url) }) } func setupWsPayEndpoints(g *gin.RouterGroup, wspayService *wspay.Service) { g.GET("/initialize/:id", func(c *gin.Context) { entry, err := wspayService.Provider.FetchById(uuid.MustParse(c.Param("id"))) if err != nil { c.AbortWithError(http.StatusNotFound, err) return } if entry.State != state.StateInitialized { c.AbortWithError(http.StatusBadRequest, err) return } form := wspayService.InitializePayment(entry) c.HTML(200, "wspay.gohtml", gin.H{"Action": wspay.AuthorisationForm, "Form": form}) }) g.GET("success", func(c *gin.Context) { url, err := wspayService.HandleSuccessResponse(c) 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, 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, state.StateCanceled) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } c.Redirect(http.StatusSeeOther, url) }) } func hasProfile(profile string) bool { profiles := strings.Split(os.Getenv("PROFILE"), ",") for _, p := range profiles { if profile == strings.TrimSpace(p) { return true } } return false } func formatState(stt state.PaymentState) string { switch stt { case state.StateCanceled: return "Otkazano" case state.StateVoided: return "Otkazano sa strane administratora" case state.StateAccepted: return "Prihvačeno" case state.StateError: return "Greška" case state.StateInitialized: return "Inicijalna izrada" case state.StateCanceledInitialization: return "Otkazano tijekom izrade" case state.StateCompleted: return "Završeno" } return "nepoznato stanje '" + string(stt) + "'" } func formatCurrency(current int64) string { return fmt.Sprintf("%d,%02d", current/100, current%100) } func decimalCurrency(current int64) string { return fmt.Sprintf("%d,%02d", current/100, current%100) } func omitempty(value string) string { if value == "" { return "-" } return value }