Added payment update action
This commit is contained in:
		
							parent
							
								
									b6203d8a03
								
							
						
					
					
						commit
						fe6f3b6672
					
				
							
								
								
									
										153
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										153
									
								
								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() { | ||||
| @ -57,6 +59,7 @@ func main() { | ||||
| 
 | ||||
| 	g.SetFuncMap(template.FuncMap{ | ||||
| 		"formatCurrency":    formatCurrency, | ||||
| 		"formatCurrencyPtr": formatCurrencyPtr, | ||||
| 		"decimalCurrency":   decimalCurrency, | ||||
| 		"formatState":       formatState, | ||||
| 		"omitempty":         omitempty, | ||||
| @ -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": mapGateways(paymentGateways)}) | ||||
| 	}) | ||||
| 		c.HTML(200, "methods.gohtml", gin.H{"Amount": amount, "Gateways": gateways}) | ||||
| 	}) | ||||
| 	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) | ||||
| } | ||||
|  | ||||
| @ -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" | ||||
| ) | ||||
|  | ||||
| @ -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 | ||||
| } | ||||
|  | ||||
| @ -24,7 +24,15 @@ | ||||
|         } | ||||
|     </style> | ||||
| </head> | ||||
| <body class="container"> | ||||
| <body> | ||||
|     <!-- As a link --> | ||||
|     <nav class="navbar navbar-dark bg-dark"> | ||||
|         <section class="container"> | ||||
|             <a class="navbar-brand" href="/">Payment-poc</a> | ||||
|         </section> | ||||
|     </nav> | ||||
| 
 | ||||
|     <section  class="container"> | ||||
|     <h2>Novo plaćanje</h2> | ||||
| 
 | ||||
|     <form method="get" action="/methods"> | ||||
| @ -58,5 +66,6 @@ | ||||
|             </tbody> | ||||
|         </table> | ||||
|     </div> | ||||
|     </section> | ||||
| </body> | ||||
| </html> | ||||
| @ -15,16 +15,30 @@ | ||||
|         } | ||||
|     </style> | ||||
| </head> | ||||
| <body class="container"> | ||||
| <body> | ||||
|     <!-- As a link --> | ||||
|     <nav class="navbar navbar-dark bg-dark"> | ||||
|         <section class="container"> | ||||
|             <a class="navbar-brand" href="/">Payment-poc</a> | ||||
|         </section> | ||||
|     </nav> | ||||
| 
 | ||||
|     <section class="container"> | ||||
| 
 | ||||
|     <h2>Plaćanje {{.Entry.Id}}</h2> | ||||
| 
 | ||||
|     {{if not (eq .Entry.State "preinitialized")}} | ||||
|         <form method="post" action="/entries/{{.Entry.Id}}/refresh"> | ||||
|             <button class="btn btn-primary">Ažuriraj</button> | ||||
|         </form> | ||||
|     {{end}} | ||||
| 
 | ||||
|     <table class="table"> | ||||
|         <tr><th>Id: </th><td>{{.Entry.Id}}</td></tr> | ||||
|         <tr><th>Datum izrade: </th><td>{{.Entry.Created.Format "Jan 02, 2006 15:04:05 UTC"}}</td></tr> | ||||
|         <tr><th>Zadnja izmjena: </th><td>{{or (.Entry.Modified.Format "Jan 02, 2006 15:04:05 UTC") .Entry.Created.Format "Jan 02, 2006 15:04:05 UTC"}}</td></tr> | ||||
|         <tr><th>Gateway: </th><td>{{.Entry.Gateway}}</td></tr> | ||||
|         <tr><th>Naplaćena vrijednost: </th><td>{{or .Entry.Amount "-"}}</td></tr> | ||||
|         <tr><th>Naplaćena vrijednost: </th><td>{{formatCurrencyPtr .Entry.Amount}}</td></tr> | ||||
|         <tr><th>Ukupna vrijednost: </th><td>{{formatCurrency .Entry.TotalAmount}}</td></tr> | ||||
|         <tr><th>Jezik: </th><td>{{or .Entry.Lang "-"}}</td></tr> | ||||
|         <tr><th>Greške: </th><td>{{or .Entry.Error "-"}}</td></tr> | ||||
| @ -32,7 +46,6 @@ | ||||
| 
 | ||||
|         {{if eq .Entry.Gateway "wspay"}} | ||||
|             <tr><th>WsPay</th><td></td></tr> | ||||
|             <tr><th>Shopping cart ID: </th><td>{{or .Entry.ShoppingCartID "-"}}</td></tr> | ||||
|             <tr><th>Success: </th><td>{{or .Entry.Success "-"}}</td></tr> | ||||
|         {{end}} | ||||
| 
 | ||||
| @ -61,5 +74,6 @@ | ||||
|             <button class="btn btn-primary" type="submit">Otkaži plaćanje</button> | ||||
|         </form> | ||||
|     {{end}} | ||||
|     </section> | ||||
| </body> | ||||
| </html> | ||||
| @ -21,10 +21,19 @@ | ||||
|         } | ||||
|     </style> | ||||
| </head> | ||||
| <body class="container"> | ||||
| <body> | ||||
|     <!-- As a link --> | ||||
|     <nav class="navbar navbar-dark bg-dark"> | ||||
|         <section class="container"> | ||||
|             <a class="navbar-brand" href="/">Payment-poc</a> | ||||
|         </section> | ||||
|     </nav> | ||||
| 
 | ||||
|     <section  class="container"> | ||||
|     <h2>Izaberi metodu plaćanja</h2> | ||||
|     {{range .Gateways}} | ||||
|         <a class="btn btn-success" href="/{{.}}?amount={{$.Amount}}">{{.}}</a> | ||||
|     {{ range $key, $value := .Gateways }} | ||||
|         <a class="btn btn-success" href="/methods/{{$key}}?amount={{$.Amount}}">{{$value}}</a> | ||||
|     {{end}} | ||||
|     </section> | ||||
| </body> | ||||
| </html> | ||||
							
								
								
									
										27
									
								
								templates/mock_gateway.gohtml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								templates/mock_gateway.gohtml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,27 @@ | ||||
| <!doctype html> | ||||
| <html lang="en"> | ||||
| <head> | ||||
|     <meta charset="UTF-8"> | ||||
|     <meta name="viewport" | ||||
|           content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> | ||||
|     <meta http-equiv="X-UA-Compatible" content="ie=edge"> | ||||
|     <title>Index</title> | ||||
|     <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous"> | ||||
|     <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script> | ||||
| </head> | ||||
| <body> | ||||
|     <!-- As a link --> | ||||
|     <nav class="navbar navbar-dark bg-dark"> | ||||
|         <section class="container"> | ||||
|             <a class="navbar-brand" href="/">Payment-poc</a> | ||||
|         </section> | ||||
|     </nav> | ||||
| 
 | ||||
|     <section class="container"> | ||||
|     <h2>Mock gateway {{.Entry.Id.String}}</h2> | ||||
|     <p>{{formatCurrency .Entry.TotalAmount}}</p> | ||||
|     <a href="/mock/success?id={{.Entry.Id.String}}" class="btn btn-success">Potvrdi plaćanje</a> | ||||
|     <a href="/mock/error?id={{.Entry.Id.String}}" class="btn btn-danger">Otkaži plaćanje</a> | ||||
|     </section> | ||||
| </body> | ||||
| </html> | ||||
| @ -5,7 +5,7 @@ | ||||
|     <meta name="viewport" | ||||
|           content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> | ||||
|     <meta http-equiv="X-UA-Compatible" content="ie=edge"> | ||||
|     <title>Izradi plančanje</title> | ||||
|     <title>Izradi planćanje</title> | ||||
|     <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous"> | ||||
|     <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script> | ||||
|     <style> | ||||
| @ -15,7 +15,7 @@ | ||||
|     </style> | ||||
| </head> | ||||
| <body class="container" style="margin-top: 32px"> | ||||
|     <h2>Započni proces plačanja</h2> | ||||
|     <h2>Započni proces plaćanja</h2> | ||||
| 
 | ||||
|     <form id="wspay-form" action="{{.Action}}" method="POST"> | ||||
|         <input type="hidden" name="ShopID" value="{{.Form.ShopID}}"> | ||||
|  | ||||
| @ -18,7 +18,6 @@ import ( | ||||
| ) | ||||
| 
 | ||||
| type Service struct { | ||||
| 	Provider     *database.PaymentEntryProvider | ||||
| 	ClientId     string | ||||
| 	ClientSecret string | ||||
| 	SourceCode   string | ||||
| @ -30,24 +29,57 @@ type Service struct { | ||||
| 	expiration time.Time | ||||
| } | ||||
| 
 | ||||
| func (s *Service) CreatePaymentUrl(amount int64) (url string, err error) { | ||||
| 	entry, err := s.Provider.CreateEntry(database.PaymentEntry{ | ||||
| 		Gateway:     state.GatewayVivaWallet, | ||||
| 		State:       state.StateInitialized, | ||||
| 		TotalAmount: amount, | ||||
| 	}) | ||||
| func (s *Service) UpdatePayment(entry database.PaymentEntry) (updatedEntry database.PaymentEntry, err error) { | ||||
| 	token, err := s.oAuthToken() | ||||
| 	httpResponse, err := createRequest( | ||||
| 		"GET", | ||||
| 		"https://demo-api.vivapayments.com/checkout/v2/transactions/"+entry.TransactionId.String(), | ||||
| 		map[string]string{"authorization": "Bearer " + token, "content-type": "application/json"}, | ||||
| 		[]byte{}, | ||||
| 	) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 		return database.PaymentEntry{}, err | ||||
| 	} | ||||
| 	entry, err = s.InitializePayment(entry) | ||||
| 
 | ||||
| 	var response TransactionStatusResponse | ||||
| 	err = readResponse(httpResponse, &response) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 		return database.PaymentEntry{}, err | ||||
| 	} | ||||
| 	entry, err = s.Provider.UpdateEntry(entry) | ||||
| 	newState := determineStatus(response.StatusId) | ||||
| 
 | ||||
| 	if entry.State != newState && newState != "" { | ||||
| 		log.Printf("Updated state for %s: %s -> %s", entry.Id.String(), entry.State, newState) | ||||
| 		entry.State = newState | ||||
| 	} | ||||
| 	return entry, nil | ||||
| } | ||||
| 
 | ||||
| func determineStatus(id TransactionStatus) state.PaymentState { | ||||
| 	switch id { | ||||
| 	case PaymentPreauthorized: | ||||
| 		return state.StateAccepted | ||||
| 	case PaymentPending: | ||||
| 		return state.StatePending | ||||
| 	case PaymentSuccessful: | ||||
| 		return state.StateCompleted | ||||
| 	case PaymentUnsuccessful: | ||||
| 		return state.StateError | ||||
| 	case PaymentRefunded: | ||||
| 		return state.StateVoided | ||||
| 	case PaymentVoided: | ||||
| 		return state.StateVoided | ||||
| 	} | ||||
| 	log.Printf("Unknonw transactionStatus: %s", string(id)) | ||||
| 	return "" | ||||
| } | ||||
| 
 | ||||
| func (s *Service) CreatePaymentUrl(entry database.PaymentEntry) (database.PaymentEntry, string, error) { | ||||
| 	entry, err := s.InitializePayment(entry) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 		return entry, "", err | ||||
| 	} | ||||
| 	return "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) { | ||||
| @ -80,6 +112,7 @@ func (s *Service) InitializePayment(entry database.PaymentEntry) (database.Payme | ||||
| 	if err != nil { | ||||
| 		return database.PaymentEntry{}, err | ||||
| 	} | ||||
| 	entry.State = state.StateInitialized | ||||
| 	entry.OrderId = &response.OrderId | ||||
| 	return entry, nil | ||||
| } | ||||
| @ -208,7 +241,7 @@ func (s *Service) basicAuth() string { | ||||
| 	return base64.StdEncoding.EncodeToString([]byte(s.MerchantId + ":" + s.ApiKey)) | ||||
| } | ||||
| 
 | ||||
| func (s *Service) HandleResponse(c *gin.Context, expectedState state.PaymentState) (string, error) { | ||||
| func (s *Service) HandleResponse(c *gin.Context, provider *database.PaymentEntryProvider, state state.PaymentState) (string, error) { | ||||
| 	transactionId := uuid.MustParse(c.Query("t")) | ||||
| 	orderId := database.OrderId(c.Query("s")) | ||||
| 	lang := c.Query("lang") | ||||
| @ -216,19 +249,19 @@ func (s *Service) HandleResponse(c *gin.Context, expectedState state.PaymentStat | ||||
| 	eci := c.Query("eci") | ||||
| 
 | ||||
| 	log.Printf("Received error response for viva payment %s", orderId) | ||||
| 	entry, err := s.Provider.FetchByOrderId(orderId) | ||||
| 	entry, err := provider.FetchByOrderId(orderId) | ||||
| 	if err != nil { | ||||
| 		log.Printf("Couldn't find payment info for viva payment %s", orderId) | ||||
| 		return "", err | ||||
| 	} | ||||
| 
 | ||||
| 	entry.State = expectedState | ||||
| 	entry.State = state | ||||
| 	entry.ECI = &eci | ||||
| 	entry.Lang = &lang | ||||
| 	entry.EventId = &eventId | ||||
| 	entry.TransactionId = &transactionId | ||||
| 
 | ||||
| 	if _, err := s.Provider.UpdateEntry(entry); err != nil { | ||||
| 	if _, err := provider.UpdateEntry(entry); err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										20
									
								
								viva/viva.go
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								viva/viva.go
									
									
									
									
									
								
							| @ -33,3 +33,23 @@ type TransactionResponse struct { | ||||
| 	EventId   int64   `json:"EventId"` | ||||
| 	Success   bool    `json:"Success"` | ||||
| } | ||||
| 
 | ||||
| type TransactionStatus string | ||||
| 
 | ||||
| const ( | ||||
| 	PaymentSuccessful    TransactionStatus = "F" | ||||
| 	PaymentPending       TransactionStatus = "A" | ||||
| 	PaymentPreauthorized TransactionStatus = "C" | ||||
| 	PaymentUnsuccessful  TransactionStatus = "E" | ||||
| 	PaymentRefunded      TransactionStatus = "R" | ||||
| 	PaymentVoided        TransactionStatus = "X" | ||||
| ) | ||||
| 
 | ||||
| type TransactionStatusResponse struct { | ||||
| 	Email      string            `json:"email"` | ||||
| 	Amount     int               `json:"amount"` | ||||
| 	OrderCode  database.OrderId  `json:"orderCode"` | ||||
| 	StatusId   TransactionStatus `json:"statusId"` | ||||
| 	FullName   string            `json:"fullName"` | ||||
| 	CardNumber string            `json:"cardNumber"` | ||||
| } | ||||
|  | ||||
							
								
								
									
										118
									
								
								wspay/service.go
									
									
									
									
									
								
							
							
						
						
									
										118
									
								
								wspay/service.go
									
									
									
									
									
								
							| @ -9,6 +9,7 @@ import ( | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/google/uuid" | ||||
| 	"io" | ||||
| 	"log" | ||||
| 	"net/http" | ||||
| 	"payment-poc/database" | ||||
| 	"payment-poc/state" | ||||
| @ -16,22 +17,67 @@ import ( | ||||
| ) | ||||
| 
 | ||||
| type Service struct { | ||||
| 	Provider   *database.PaymentEntryProvider | ||||
| 	ShopId     string | ||||
| 	ShopSecret string | ||||
| 	BackendUrl string | ||||
| } | ||||
| 
 | ||||
| func (s *Service) CreatePaymentUrl(amount int64) (string, error) { | ||||
| 	entry, err := s.Provider.CreateEntry(database.PaymentEntry{ | ||||
| 		Gateway:     state.GatewayWsPay, | ||||
| 		State:       state.StateInitialized, | ||||
| 		TotalAmount: amount, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| func (s *Service) UpdatePayment(entry database.PaymentEntry) (updatedEntry database.PaymentEntry, err error) { | ||||
| 	var request = StatusCheckRequest{ | ||||
| 		Version:        "2.0", | ||||
| 		ShopId:         s.ShopId, | ||||
| 		ShoppingCartId: entry.Id.String(), | ||||
| 		Signature:      CalculateStatusCheckSignature(s.ShopId, s.ShopSecret, entry.Id.String()), | ||||
| 	} | ||||
| 	return "/wspay/initialize/" + entry.Id.String(), nil | ||||
| 
 | ||||
| 	httpResponse, err := createRequest( | ||||
| 		"POST", | ||||
| 		"https://test.wspay.biz/api/services/statusCheck", | ||||
| 		map[string]string{"content-type": "application/json"}, | ||||
| 		toJson(request), | ||||
| 	) | ||||
| 	if err != nil { | ||||
| 		return database.PaymentEntry{}, err | ||||
| 	} | ||||
| 
 | ||||
| 	var response StatusCheckResponse | ||||
| 	err = readResponse(httpResponse, &response) | ||||
| 	if err != nil { | ||||
| 		return database.PaymentEntry{}, err | ||||
| 	} | ||||
| 
 | ||||
| 	if CompareStatusCheckReturnSignature(response.Signature, s.ShopId, s.ShopSecret, response.ActionSuccess, response.ApprovalCode, entry.Id.String()) != nil { | ||||
| 		entry.Amount = &response.Amount | ||||
| 		newState := determineState(response) | ||||
| 
 | ||||
| 		if entry.State != newState && newState != "" { | ||||
| 			log.Printf("Updated state for %s: %s -> %s", entry.Id.String(), entry.State, newState) | ||||
| 			entry.State = newState | ||||
| 		} | ||||
| 
 | ||||
| 		entry.State = state.StateCompleted | ||||
| 	} else { | ||||
| 		return database.PaymentEntry{}, errors.New("invalid signature") | ||||
| 	} | ||||
| 	return entry, nil | ||||
| } | ||||
| 
 | ||||
| func determineState(response StatusCheckResponse) state.PaymentState { | ||||
| 	if response.Completed == "1" { | ||||
| 		return state.StateCompleted | ||||
| 	} else if response.Voided == "1" { | ||||
| 		return state.StateVoided | ||||
| 	} else if response.Refunded == "1" { | ||||
| 		return state.StateVoided | ||||
| 	} else if response.Authorized == "1" { | ||||
| 		return state.StateAccepted | ||||
| 	} else { | ||||
| 		return state.StateInitialized | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (s *Service) CreatePaymentUrl(entry database.PaymentEntry) (database.PaymentEntry, string, error) { | ||||
| 	return entry, "/wspay/initialize/" + entry.Id.String(), nil | ||||
| } | ||||
| 
 | ||||
| func (s *Service) CompleteTransaction(entry database.PaymentEntry, amount int64) (database.PaymentEntry, error) { | ||||
| @ -127,12 +173,12 @@ func (s *Service) InitializePayment(entry database.PaymentEntry) Form { | ||||
| 	return form | ||||
| } | ||||
| 
 | ||||
| func (s *Service) HandleSuccessResponse(c *gin.Context) (string, error) { | ||||
| func (s *Service) HandleSuccessResponse(c *gin.Context, provider *database.PaymentEntryProvider) (string, error) { | ||||
| 	response := FormReturn{} | ||||
| 	if err := c.ShouldBind(&response); err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	entry, err := s.Provider.FetchById(uuid.MustParse(response.ShoppingCartID)) | ||||
| 	entry, err := provider.FetchById(uuid.MustParse(response.ShoppingCartID)) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| @ -148,19 +194,19 @@ func (s *Service) HandleSuccessResponse(c *gin.Context) (string, error) { | ||||
| 	entry.ApprovalCode = &response.ApprovalCode | ||||
| 	entry.State = state.StateAccepted | ||||
| 
 | ||||
| 	if _, err := s.Provider.UpdateEntry(entry); err != nil { | ||||
| 	if _, err := provider.UpdateEntry(entry); err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 
 | ||||
| 	return "/entries/" + entry.Id.String(), nil | ||||
| } | ||||
| 
 | ||||
| func (s *Service) HandleErrorResponse(c *gin.Context, paymentState state.PaymentState) (string, error) { | ||||
| func (s *Service) HandleErrorResponse(c *gin.Context, provider *database.PaymentEntryProvider, paymentState state.PaymentState) (string, error) { | ||||
| 	response := FormError{} | ||||
| 	if err := c.ShouldBind(&response); err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	entry, err := s.Provider.FetchById(uuid.MustParse(response.ShoppingCartID)) | ||||
| 	entry, err := provider.FetchById(uuid.MustParse(response.ShoppingCartID)) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| @ -176,7 +222,7 @@ func (s *Service) HandleErrorResponse(c *gin.Context, paymentState state.Payment | ||||
| 	entry.Error = &response.ErrorMessage | ||||
| 	entry.State = paymentState | ||||
| 
 | ||||
| 	if _, err := s.Provider.UpdateEntry(entry); err != nil { | ||||
| 	if _, err := provider.UpdateEntry(entry); err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 
 | ||||
| @ -268,6 +314,46 @@ func CompareCompletionReturnSignature(signature string, shopId string, secret st | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func CompareStatusCheckReturnSignature(signature string, shopId string, secret string, actionSuccess string, approvalCode string, cartId string) error { | ||||
| 	/** | ||||
| 	Represents a signature created from string formatted from following values in a following order using | ||||
| 	SHA512 algorithm: | ||||
| 	ShopID | ||||
| 	SecretKey | ||||
| 	ActionSuccess | ||||
| 	ApprovalCode | ||||
| 	SecretKey | ||||
| 	ShopID | ||||
| 	ApprovalCode | ||||
| 	WsPayOrderId | ||||
| 	*/ | ||||
| 	calculatedSignature := shopId + secret + actionSuccess + approvalCode + secret + shopId + approvalCode + cartId | ||||
| 	hash := sha512.New() | ||||
| 	hash.Write([]byte(calculatedSignature)) | ||||
| 	if hex.EncodeToString(hash.Sum(nil)) == signature { | ||||
| 		return nil | ||||
| 	} else { | ||||
| 		return errors.New("signature mismatch") | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func CalculateStatusCheckSignature(shopId string, secret string, cartId string) string { | ||||
| 	/** | ||||
| 	Represents a signature created from string formatted from following values in a following order using | ||||
| 	SHA512 algorithm: | ||||
| 	ShopID | ||||
| 	SecretKey | ||||
| 	ShoppingCartID | ||||
| 	SecretKey | ||||
| 	ShopID | ||||
| 	ShoppingCartID | ||||
| 	*/ | ||||
| 	signature := shopId + secret + cartId + secret + shopId + cartId | ||||
| 	hash := sha512.New() | ||||
| 	hash.Write([]byte(signature)) | ||||
| 	return hex.EncodeToString(hash.Sum(nil)) | ||||
| } | ||||
| 
 | ||||
| func readResponse[T any](httpResponse *http.Response, response T) error { | ||||
| 	if httpResponse.StatusCode == http.StatusOK { | ||||
| 		content, err := io.ReadAll(httpResponse.Body) | ||||
|  | ||||
| @ -130,14 +130,14 @@ type StatusCheckResponse struct { | ||||
| 	ApprovalCode                    string | ||||
| 	ShopID                          string | ||||
| 	ShoppingCartID                  string | ||||
| 	Amount                          string | ||||
| 	Amount                          int64 | ||||
| 	CurrencyCode                    string | ||||
| 	ActionSuccess                   string | ||||
| 	Success                         string // deprecated | ||||
| 	Authorized                      int | ||||
| 	Completed                       int | ||||
| 	Voided                          int | ||||
| 	Refunded                        int | ||||
| 	Authorized                      string | ||||
| 	Completed                       string | ||||
| 	Voided                          string | ||||
| 	Refunded                        string | ||||
| 	PaymentPlan                     string | ||||
| 	Partner                         string | ||||
| 	OnSite                          int | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user