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 @@ } - + + + + +

Novo plaćanje

@@ -58,5 +66,6 @@ +
\ No newline at end of file diff --git a/templates/info.gohtml b/templates/info.gohtml index 30f7c54..b256938 100644 --- a/templates/info.gohtml +++ b/templates/info.gohtml @@ -15,16 +15,30 @@ } - + + + + +

Plaćanje {{.Entry.Id}}

+ {{if not (eq .Entry.State "preinitialized")}} + + + + {{end}} + - + @@ -32,7 +46,6 @@ {{if eq .Entry.Gateway "wspay"}} - {{end}} @@ -61,5 +74,6 @@ {{end}} + \ No newline at end of file diff --git a/templates/methods.gohtml b/templates/methods.gohtml index 82b38a0..4f8cbe6 100644 --- a/templates/methods.gohtml +++ b/templates/methods.gohtml @@ -21,10 +21,19 @@ } - + + + + +

Izaberi metodu plaćanja

- {{range .Gateways}} - {{.}} + {{ range $key, $value := .Gateways }} + {{$value}} {{end}} +
\ No newline at end of file diff --git a/templates/mock_gateway.gohtml b/templates/mock_gateway.gohtml new file mode 100644 index 0000000..6b9b436 --- /dev/null +++ b/templates/mock_gateway.gohtml @@ -0,0 +1,27 @@ + + + + + + + Index + + + + + + + +
+

Mock gateway {{.Entry.Id.String}}

+

{{formatCurrency .Entry.TotalAmount}}

+ Potvrdi plaćanje + Otkaži plaćanje +
+ + \ No newline at end of file diff --git a/templates/wspay.gohtml b/templates/wspay.gohtml index 5f88d07..1eb536c 100644 --- a/templates/wspay.gohtml +++ b/templates/wspay.gohtml @@ -5,7 +5,7 @@ - Izradi plančanje + Izradi planćanje -

Započni proces plačanja

+

Započni proces plaćanja

diff --git a/viva/service.go b/viva/service.go index 98c4fbb..fc3b710 100644 --- a/viva/service.go +++ b/viva/service.go @@ -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 } diff --git a/viva/viva.go b/viva/viva.go index 18e2f26..0d161ac 100644 --- a/viva/viva.go +++ b/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"` +} diff --git a/wspay/service.go b/wspay/service.go index 544a479..5552f32 100644 --- a/wspay/service.go +++ b/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) diff --git a/wspay/wspay.go b/wspay/wspay.go index 56d3963..94bad71 100644 --- a/wspay/wspay.go +++ b/wspay/wspay.go @@ -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 @@ -151,7 +151,7 @@ type StatusCheckResponse struct { CustomerCountry string CustomerPhone string CustomerEmail string - TransactionDateTime string //yyyymmddHHMMss + TransactionDateTime string // yyyymmddHHMMss IsLessThan30DaysFromTransaction bool CanBeCompleted bool CanBeVoided bool
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 "-"}}