diff --git a/main.go b/main.go index 6c6da23..17fe5e8 100644 --- a/main.go +++ b/main.go @@ -14,11 +14,11 @@ import ( "os" "payment-poc/database" "payment-poc/migration" + "payment-poc/mock" "payment-poc/state" stripe2 "payment-poc/stripe" "payment-poc/viva" "payment-poc/wspay" - "sort" "strconv" "strings" "time" @@ -28,9 +28,11 @@ import ( var devMigrations embed.FS type PaymentProvider interface { - CreatePaymentUrl(amount int64) (string, error) + 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() { @@ -56,10 +58,11 @@ func main() { } g.SetFuncMap(template.FuncMap{ - "formatCurrency": formatCurrency, - "decimalCurrency": decimalCurrency, - "formatState": formatState, - "omitempty": omitempty, + "formatCurrency": formatCurrency, + "formatCurrencyPtr": formatCurrencyPtr, + "decimalCurrency": decimalCurrency, + "formatState": formatState, + "omitempty": omitempty, }) g.NoRoute(func(c *gin.Context) { @@ -72,40 +75,45 @@ func main() { backendUrl := envMustExist("BACKEND_URL") paymentGateways := map[state.PaymentGateway]PaymentProvider{} - entryProvider := database.PaymentEntryProvider{DB: client} + 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 + } + 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) + wsPayHandlers(g.Group("wspay"), entryProvider, &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) + stripeHandlers(g.Group("stripe"), entryProvider, &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) + vivaHandlers(g.Group("viva"), entryProvider, &vivaService) paymentGateways[state.GatewayVivaWallet] = &vivaService } @@ -118,16 +126,9 @@ func main() { if err != nil { amount = 10.00 } - var gateways []state.PaymentGateway - for key := range paymentGateways { - gateways = append(gateways, key) - } - sort.Slice(gateways, func(i, j int) bool { - return string(gateways[i]) < string(gateways[j]) - }) - c.HTML(200, "methods.gohtml", gin.H{"Amount": amount, "Gateways": gateways}) + c.HTML(200, "methods.gohtml", gin.H{"Amount": amount, "Gateways": mapGateways(paymentGateways)}) }) - g.GET("/:gateway", func(c *gin.Context) { + g.GET("/methods/:gateway", func(c *gin.Context) { gateway, err := fetchGateway(c.Param("gateway")) if err != nil { c.AbortWithError(http.StatusBadRequest, err) @@ -139,7 +140,13 @@ func main() { c.AbortWithError(http.StatusBadRequest, err) return } - if url, err := paymentGateway.CreatePaymentUrl(amount); err == nil { + entry, err := entryProvider.CreateEntry(database.PaymentEntry{ + Gateway: gateway, + State: state.StatePreinitialized, + TotalAmount: amount, + }) + if entry, url, err := paymentGateway.CreatePaymentUrl(entry); err == nil { + entryProvider.UpdateEntry(entry) c.Redirect(http.StatusSeeOther, url) } else { c.AbortWithError(http.StatusBadRequest, err) @@ -210,10 +217,81 @@ func main() { } } }) + 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 { + entry, err = paymentGateway.UpdatePayment(entry) + if err == nil { + entryProvider.UpdateEntry(entry) + } + 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): @@ -222,6 +300,8 @@ func fetchGateway(gateway string) (state.PaymentGateway, error) { 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) } @@ -239,9 +319,9 @@ func fetchAmount(amount string) (int64, error) { } } -func setupVivaEndpoints(g *gin.RouterGroup, vivaService *viva.Service) { +func vivaHandlers(g *gin.RouterGroup, provider *database.PaymentEntryProvider, vivaService *viva.Service) { g.GET("success", func(c *gin.Context) { - url, err := vivaService.HandleResponse(c, state.StateAccepted) + url, err := vivaService.HandleResponse(c, provider, state.StateAccepted) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return @@ -249,7 +329,7 @@ func setupVivaEndpoints(g *gin.RouterGroup, vivaService *viva.Service) { c.Redirect(http.StatusSeeOther, url) }) g.GET("error", func(c *gin.Context) { - url, err := vivaService.HandleResponse(c, state.StateError) + url, err := vivaService.HandleResponse(c, provider, state.StateError) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return @@ -258,10 +338,10 @@ func setupVivaEndpoints(g *gin.RouterGroup, vivaService *viva.Service) { }) } -func setupStripeEndpoints(g *gin.RouterGroup, stripeService *stripe2.Service) { +func stripeHandlers(g *gin.RouterGroup, provider *database.PaymentEntryProvider, stripeService *stripe2.Service) { g.GET("success", func(c *gin.Context) { - url, err := stripeService.HandleResponse(c, state.StateAccepted) + url, err := stripeService.HandleResponse(c, provider, state.StateAccepted) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return @@ -269,7 +349,7 @@ func setupStripeEndpoints(g *gin.RouterGroup, stripeService *stripe2.Service) { c.Redirect(http.StatusSeeOther, url) }) g.GET("error", func(c *gin.Context) { - url, err := stripeService.HandleResponse(c, state.StateError) + url, err := stripeService.HandleResponse(c, provider, state.StateError) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return @@ -278,25 +358,24 @@ func setupStripeEndpoints(g *gin.RouterGroup, stripeService *stripe2.Service) { }) } -func setupWsPayEndpoints(g *gin.RouterGroup, wspayService *wspay.Service) { +func wsPayHandlers(g *gin.RouterGroup, provider *database.PaymentEntryProvider, wspayService *wspay.Service) { g.GET("/initialize/:id", func(c *gin.Context) { - entry, err := wspayService.Provider.FetchById(uuid.MustParse(c.Param("id"))) + entry, err := provider.FetchById(uuid.MustParse(c.Param("id"))) if err != nil { c.AbortWithError(http.StatusNotFound, err) return } - if entry.State != state.StateInitialized { + if entry.State != state.StatePreinitialized { 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) + url, err := wspayService.HandleSuccessResponse(c, provider) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return @@ -304,7 +383,7 @@ func setupWsPayEndpoints(g *gin.RouterGroup, wspayService *wspay.Service) { c.Redirect(http.StatusSeeOther, url) }) g.GET("error", func(c *gin.Context) { - url, err := wspayService.HandleErrorResponse(c, state.StateError) + url, err := wspayService.HandleErrorResponse(c, provider, state.StateError) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return @@ -312,7 +391,7 @@ func setupWsPayEndpoints(g *gin.RouterGroup, wspayService *wspay.Service) { c.Redirect(http.StatusSeeOther, url) }) g.GET("cancel", func(c *gin.Context) { - url, err := wspayService.HandleErrorResponse(c, state.StateCanceled) + url, err := wspayService.HandleErrorResponse(c, provider, state.StateCanceled) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return @@ -336,7 +415,7 @@ func formatState(stt state.PaymentState) string { case state.StateCanceled: return "Otkazana" case state.StateVoided: - return "Otkazana sa strane administratora" + return "Poništena" case state.StateAccepted: return "Predautorizirana" case state.StateError: @@ -357,6 +436,14 @@ 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) } diff --git a/state/model.go b/state/model.go index b8c24b5..002a4fa 100644 --- a/state/model.go +++ b/state/model.go @@ -8,6 +8,9 @@ const ( // initial state StateInitialized PaymentState = "initialized" + // state given to async payments (eg. GooglePay,ApplePay...) + StatePending PaymentState = "pending" + // state on response StateAccepted PaymentState = "accepted" StateError PaymentState = "error" @@ -25,4 +28,5 @@ const ( GatewayWsPay PaymentGateway = "wspay" GatewayStripe PaymentGateway = "stripe" GatewayVivaWallet PaymentGateway = "viva-wallet" + GatewayMock PaymentGateway = "mock" ) diff --git a/stripe/service.go b/stripe/service.go index 9d81214..259ae47 100644 --- a/stripe/service.go +++ b/stripe/service.go @@ -12,29 +12,53 @@ import ( ) type Service struct { - Provider *database.PaymentEntryProvider ApiKey string BackendUrl string } -func (s *Service) CreatePaymentUrl(amount int64) (url string, err error) { - entry, err := s.Provider.CreateEntry(database.PaymentEntry{ - Gateway: state.GatewayStripe, - State: state.StateInitialized, - TotalAmount: amount, - }) +func (s *Service) UpdatePayment(entry database.PaymentEntry) (updatedEntry database.PaymentEntry, err error) { + pi, err := paymentintent.Get(*entry.PaymentIntentId, nil) if err != nil { - return "", err + return entry, err } - entry, url, err = s.InitializePayment(entry) + newState := determineState(pi.Status) + + if entry.State != newState && newState != "" { + log.Printf("Updated state for %s: %s -> %s", entry.Id.String(), entry.State, newState) + if pi.AmountReceived > 0 { + entry.Amount = &pi.AmountReceived + } + entry.State = newState + } + return entry, nil +} + +func determineState(status stripe.PaymentIntentStatus) state.PaymentState { + switch status { + case stripe.PaymentIntentStatusCanceled: + return state.StateCanceled + case stripe.PaymentIntentStatusProcessing: + return state.StatePending + case stripe.PaymentIntentStatusRequiresAction: + return state.StatePending + case stripe.PaymentIntentStatusRequiresCapture: + return state.StateAccepted + case stripe.PaymentIntentStatusRequiresConfirmation: + return state.StatePending + case stripe.PaymentIntentStatusRequiresPaymentMethod: + return state.StateVoided + case stripe.PaymentIntentStatusSucceeded: + return state.StateCompleted + } + return "" +} + +func (s *Service) CreatePaymentUrl(entry database.PaymentEntry) (database.PaymentEntry, string, error) { + entry, url, err := s.InitializePayment(entry) if err != nil { - return "", err + return entry, "", err } - entry, err = s.Provider.UpdateEntry(entry) - if err != nil { - return "", err - } - return url, nil + return entry, url, nil } func (s *Service) InitializePayment(entry database.PaymentEntry) (database.PaymentEntry, string, error) { @@ -68,6 +92,7 @@ func (s *Service) InitializePayment(entry database.PaymentEntry) (database.Payme if err != nil { return database.PaymentEntry{}, "", err } + entry.State = state.StateInitialized entry.PaymentIntentId = &result.PaymentIntent.ID return entry, result.URL, nil @@ -82,9 +107,10 @@ func (s *Service) CompleteTransaction(entry database.PaymentEntry, amount int64) return database.PaymentEntry{}, err } log.Printf("received state on completion: %v", pi.Status) - if pi.Status == stripe.PaymentIntentStatusSucceeded || pi.Status == stripe.PaymentIntentStatusProcessing { + newState := determineState(pi.Status) + entry.State = newState + if newState == state.StateCompleted || newState == state.StatePending { entry.Amount = &pi.AmountReceived - entry.State = state.StateCompleted } return entry, nil } @@ -102,13 +128,13 @@ func (s *Service) CancelTransaction(entry database.PaymentEntry) (database.Payme return entry, nil } -func (s *Service) HandleResponse(c *gin.Context, paymentState state.PaymentState) (string, error) { +func (s *Service) HandleResponse(c *gin.Context, provider *database.PaymentEntryProvider, paymentState state.PaymentState) (string, error) { id := uuid.MustParse(c.Query("token")) - entry, err := s.Provider.FetchById(id) + entry, err := provider.FetchById(id) if err != nil { return "", err } entry.State = paymentState - s.Provider.UpdateEntry(entry) + provider.UpdateEntry(entry) return "/entries/" + entry.Id.String(), nil } diff --git a/templates/index.gohtml b/templates/index.gohtml index 4b9df32..acdf4a5 100644 --- a/templates/index.gohtml +++ b/templates/index.gohtml @@ -24,7 +24,15 @@ } -
+ + + + +Id: | {{.Entry.Id}} |
---|---|
Datum izrade: | {{.Entry.Created.Format "Jan 02, 2006 15:04:05 UTC"}} |
Zadnja izmjena: | {{or (.Entry.Modified.Format "Jan 02, 2006 15:04:05 UTC") .Entry.Created.Format "Jan 02, 2006 15:04:05 UTC"}} |
Gateway: | {{.Entry.Gateway}} |
Naplaćena vrijednost: | {{or .Entry.Amount "-"}} |
Naplaćena vrijednost: | {{formatCurrencyPtr .Entry.Amount}} |
Ukupna vrijednost: | {{formatCurrency .Entry.TotalAmount}} |
Jezik: | {{or .Entry.Lang "-"}} |
Greške: | {{or .Entry.Error "-"}} |
WsPay | |
Shopping cart ID: | {{or .Entry.ShoppingCartID "-"}} |
Success: | {{or .Entry.Success "-"}} |