Added payment update action
This commit is contained in:
parent
b6203d8a03
commit
fe6f3b6672
161
main.go
161
main.go
|
@ -14,11 +14,11 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"payment-poc/database"
|
"payment-poc/database"
|
||||||
"payment-poc/migration"
|
"payment-poc/migration"
|
||||||
|
"payment-poc/mock"
|
||||||
"payment-poc/state"
|
"payment-poc/state"
|
||||||
stripe2 "payment-poc/stripe"
|
stripe2 "payment-poc/stripe"
|
||||||
"payment-poc/viva"
|
"payment-poc/viva"
|
||||||
"payment-poc/wspay"
|
"payment-poc/wspay"
|
||||||
"sort"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -28,9 +28,11 @@ import (
|
||||||
var devMigrations embed.FS
|
var devMigrations embed.FS
|
||||||
|
|
||||||
type PaymentProvider interface {
|
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)
|
CompleteTransaction(entry database.PaymentEntry, amount int64) (database.PaymentEntry, error)
|
||||||
CancelTransaction(entry database.PaymentEntry) (database.PaymentEntry, error)
|
CancelTransaction(entry database.PaymentEntry) (database.PaymentEntry, error)
|
||||||
|
|
||||||
|
UpdatePayment(entry database.PaymentEntry) (updatedEntry database.PaymentEntry, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -56,10 +58,11 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
g.SetFuncMap(template.FuncMap{
|
g.SetFuncMap(template.FuncMap{
|
||||||
"formatCurrency": formatCurrency,
|
"formatCurrency": formatCurrency,
|
||||||
"decimalCurrency": decimalCurrency,
|
"formatCurrencyPtr": formatCurrencyPtr,
|
||||||
"formatState": formatState,
|
"decimalCurrency": decimalCurrency,
|
||||||
"omitempty": omitempty,
|
"formatState": formatState,
|
||||||
|
"omitempty": omitempty,
|
||||||
})
|
})
|
||||||
|
|
||||||
g.NoRoute(func(c *gin.Context) {
|
g.NoRoute(func(c *gin.Context) {
|
||||||
|
@ -72,40 +75,45 @@ func main() {
|
||||||
backendUrl := envMustExist("BACKEND_URL")
|
backendUrl := envMustExist("BACKEND_URL")
|
||||||
|
|
||||||
paymentGateways := map[state.PaymentGateway]PaymentProvider{}
|
paymentGateways := map[state.PaymentGateway]PaymentProvider{}
|
||||||
entryProvider := database.PaymentEntryProvider{DB: client}
|
entryProvider := &database.PaymentEntryProvider{DB: client}
|
||||||
|
|
||||||
g.LoadHTMLGlob("./templates/*.gohtml")
|
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)) {
|
if hasProfile(string(state.GatewayWsPay)) {
|
||||||
wspayService := wspay.Service{
|
wspayService := wspay.Service{
|
||||||
Provider: &entryProvider,
|
|
||||||
ShopId: envMustExist("WSPAY_SHOP_ID"),
|
ShopId: envMustExist("WSPAY_SHOP_ID"),
|
||||||
ShopSecret: envMustExist("WSPAY_SHOP_SECRET"),
|
ShopSecret: envMustExist("WSPAY_SHOP_SECRET"),
|
||||||
BackendUrl: backendUrl,
|
BackendUrl: backendUrl,
|
||||||
}
|
}
|
||||||
setupWsPayEndpoints(g.Group("wspay"), &wspayService)
|
wsPayHandlers(g.Group("wspay"), entryProvider, &wspayService)
|
||||||
paymentGateways[state.GatewayWsPay] = &wspayService
|
paymentGateways[state.GatewayWsPay] = &wspayService
|
||||||
}
|
}
|
||||||
if hasProfile(string(state.GatewayStripe)) {
|
if hasProfile(string(state.GatewayStripe)) {
|
||||||
stripeService := stripe2.Service{
|
stripeService := stripe2.Service{
|
||||||
Provider: &entryProvider,
|
|
||||||
ApiKey: envMustExist("STRIPE_KEY"),
|
ApiKey: envMustExist("STRIPE_KEY"),
|
||||||
BackendUrl: backendUrl,
|
BackendUrl: backendUrl,
|
||||||
}
|
}
|
||||||
setupStripeEndpoints(g.Group("stripe"), &stripeService)
|
stripeHandlers(g.Group("stripe"), entryProvider, &stripeService)
|
||||||
paymentGateways[state.GatewayStripe] = &stripeService
|
paymentGateways[state.GatewayStripe] = &stripeService
|
||||||
stripe.Key = envMustExist("STRIPE_KEY")
|
stripe.Key = envMustExist("STRIPE_KEY")
|
||||||
}
|
}
|
||||||
if hasProfile(string(state.GatewayVivaWallet)) {
|
if hasProfile(string(state.GatewayVivaWallet)) {
|
||||||
vivaService := viva.Service{
|
vivaService := viva.Service{
|
||||||
Provider: &entryProvider,
|
|
||||||
ClientId: envMustExist("VIVA_WALLET_CLIENT_ID"),
|
ClientId: envMustExist("VIVA_WALLET_CLIENT_ID"),
|
||||||
ClientSecret: envMustExist("VIVA_WALLET_CLIENT_SECRET"),
|
ClientSecret: envMustExist("VIVA_WALLET_CLIENT_SECRET"),
|
||||||
SourceCode: envMustExist("VIVA_WALLET_SOURCE_CODE"),
|
SourceCode: envMustExist("VIVA_WALLET_SOURCE_CODE"),
|
||||||
MerchantId: envMustExist("VIVA_WALLET_MERCHANT_ID"),
|
MerchantId: envMustExist("VIVA_WALLET_MERCHANT_ID"),
|
||||||
ApiKey: envMustExist("VIVA_WALLET_API_KEY"),
|
ApiKey: envMustExist("VIVA_WALLET_API_KEY"),
|
||||||
}
|
}
|
||||||
setupVivaEndpoints(g.Group("viva"), &vivaService)
|
vivaHandlers(g.Group("viva"), entryProvider, &vivaService)
|
||||||
paymentGateways[state.GatewayVivaWallet] = &vivaService
|
paymentGateways[state.GatewayVivaWallet] = &vivaService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,16 +126,9 @@ func main() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
amount = 10.00
|
amount = 10.00
|
||||||
}
|
}
|
||||||
var gateways []state.PaymentGateway
|
c.HTML(200, "methods.gohtml", gin.H{"Amount": amount, "Gateways": mapGateways(paymentGateways)})
|
||||||
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})
|
|
||||||
})
|
})
|
||||||
g.GET("/:gateway", func(c *gin.Context) {
|
g.GET("/methods/:gateway", func(c *gin.Context) {
|
||||||
gateway, err := fetchGateway(c.Param("gateway"))
|
gateway, err := fetchGateway(c.Param("gateway"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.AbortWithError(http.StatusBadRequest, err)
|
c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
@ -139,7 +140,13 @@ func main() {
|
||||||
c.AbortWithError(http.StatusBadRequest, err)
|
c.AbortWithError(http.StatusBadRequest, err)
|
||||||
return
|
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)
|
c.Redirect(http.StatusSeeOther, url)
|
||||||
} else {
|
} else {
|
||||||
c.AbortWithError(http.StatusBadRequest, err)
|
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))
|
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) {
|
func fetchGateway(gateway string) (state.PaymentGateway, error) {
|
||||||
switch gateway {
|
switch gateway {
|
||||||
case string(state.GatewayWsPay):
|
case string(state.GatewayWsPay):
|
||||||
|
@ -222,6 +300,8 @@ func fetchGateway(gateway string) (state.PaymentGateway, error) {
|
||||||
return state.GatewayStripe, nil
|
return state.GatewayStripe, nil
|
||||||
case string(state.GatewayVivaWallet):
|
case string(state.GatewayVivaWallet):
|
||||||
return state.GatewayVivaWallet, nil
|
return state.GatewayVivaWallet, nil
|
||||||
|
case string(state.GatewayMock):
|
||||||
|
return state.GatewayMock, nil
|
||||||
}
|
}
|
||||||
return "", errors.New("unknown gateway: " + gateway)
|
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) {
|
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 {
|
if err != nil {
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
|
@ -249,7 +329,7 @@ func setupVivaEndpoints(g *gin.RouterGroup, vivaService *viva.Service) {
|
||||||
c.Redirect(http.StatusSeeOther, url)
|
c.Redirect(http.StatusSeeOther, url)
|
||||||
})
|
})
|
||||||
g.GET("error", func(c *gin.Context) {
|
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 {
|
if err != nil {
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
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) {
|
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 {
|
if err != nil {
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
|
@ -269,7 +349,7 @@ func setupStripeEndpoints(g *gin.RouterGroup, stripeService *stripe2.Service) {
|
||||||
c.Redirect(http.StatusSeeOther, url)
|
c.Redirect(http.StatusSeeOther, url)
|
||||||
})
|
})
|
||||||
g.GET("error", func(c *gin.Context) {
|
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 {
|
if err != nil {
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
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) {
|
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 {
|
if err != nil {
|
||||||
c.AbortWithError(http.StatusNotFound, err)
|
c.AbortWithError(http.StatusNotFound, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if entry.State != state.StateInitialized {
|
if entry.State != state.StatePreinitialized {
|
||||||
c.AbortWithError(http.StatusBadRequest, err)
|
c.AbortWithError(http.StatusBadRequest, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
form := wspayService.InitializePayment(entry)
|
form := wspayService.InitializePayment(entry)
|
||||||
|
|
||||||
c.HTML(200, "wspay.gohtml", gin.H{"Action": wspay.AuthorisationForm, "Form": form})
|
c.HTML(200, "wspay.gohtml", gin.H{"Action": wspay.AuthorisationForm, "Form": form})
|
||||||
})
|
})
|
||||||
|
|
||||||
g.GET("success", func(c *gin.Context) {
|
g.GET("success", func(c *gin.Context) {
|
||||||
url, err := wspayService.HandleSuccessResponse(c)
|
url, err := wspayService.HandleSuccessResponse(c, provider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
|
@ -304,7 +383,7 @@ func setupWsPayEndpoints(g *gin.RouterGroup, wspayService *wspay.Service) {
|
||||||
c.Redirect(http.StatusSeeOther, url)
|
c.Redirect(http.StatusSeeOther, url)
|
||||||
})
|
})
|
||||||
g.GET("error", func(c *gin.Context) {
|
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 {
|
if err != nil {
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
|
@ -312,7 +391,7 @@ func setupWsPayEndpoints(g *gin.RouterGroup, wspayService *wspay.Service) {
|
||||||
c.Redirect(http.StatusSeeOther, url)
|
c.Redirect(http.StatusSeeOther, url)
|
||||||
})
|
})
|
||||||
g.GET("cancel", func(c *gin.Context) {
|
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 {
|
if err != nil {
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
|
@ -336,7 +415,7 @@ func formatState(stt state.PaymentState) string {
|
||||||
case state.StateCanceled:
|
case state.StateCanceled:
|
||||||
return "Otkazana"
|
return "Otkazana"
|
||||||
case state.StateVoided:
|
case state.StateVoided:
|
||||||
return "Otkazana sa strane administratora"
|
return "Poništena"
|
||||||
case state.StateAccepted:
|
case state.StateAccepted:
|
||||||
return "Predautorizirana"
|
return "Predautorizirana"
|
||||||
case state.StateError:
|
case state.StateError:
|
||||||
|
@ -357,6 +436,14 @@ func formatCurrency(current int64) string {
|
||||||
return fmt.Sprintf("%d,%02d", current/100, current%100)
|
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 {
|
func decimalCurrency(current int64) string {
|
||||||
return fmt.Sprintf("%d,%02d", current/100, current%100)
|
return fmt.Sprintf("%d,%02d", current/100, current%100)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,9 @@ const (
|
||||||
// initial state
|
// initial state
|
||||||
StateInitialized PaymentState = "initialized"
|
StateInitialized PaymentState = "initialized"
|
||||||
|
|
||||||
|
// state given to async payments (eg. GooglePay,ApplePay...)
|
||||||
|
StatePending PaymentState = "pending"
|
||||||
|
|
||||||
// state on response
|
// state on response
|
||||||
StateAccepted PaymentState = "accepted"
|
StateAccepted PaymentState = "accepted"
|
||||||
StateError PaymentState = "error"
|
StateError PaymentState = "error"
|
||||||
|
@ -25,4 +28,5 @@ const (
|
||||||
GatewayWsPay PaymentGateway = "wspay"
|
GatewayWsPay PaymentGateway = "wspay"
|
||||||
GatewayStripe PaymentGateway = "stripe"
|
GatewayStripe PaymentGateway = "stripe"
|
||||||
GatewayVivaWallet PaymentGateway = "viva-wallet"
|
GatewayVivaWallet PaymentGateway = "viva-wallet"
|
||||||
|
GatewayMock PaymentGateway = "mock"
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,29 +12,53 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
Provider *database.PaymentEntryProvider
|
|
||||||
ApiKey string
|
ApiKey string
|
||||||
BackendUrl string
|
BackendUrl string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) CreatePaymentUrl(amount int64) (url string, err error) {
|
func (s *Service) UpdatePayment(entry database.PaymentEntry) (updatedEntry database.PaymentEntry, err error) {
|
||||||
entry, err := s.Provider.CreateEntry(database.PaymentEntry{
|
pi, err := paymentintent.Get(*entry.PaymentIntentId, nil)
|
||||||
Gateway: state.GatewayStripe,
|
|
||||||
State: state.StateInitialized,
|
|
||||||
TotalAmount: amount,
|
|
||||||
})
|
|
||||||
if err != 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 {
|
if err != nil {
|
||||||
return "", err
|
return entry, "", err
|
||||||
}
|
}
|
||||||
entry, err = s.Provider.UpdateEntry(entry)
|
return entry, url, nil
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return url, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) InitializePayment(entry database.PaymentEntry) (database.PaymentEntry, string, error) {
|
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 {
|
if err != nil {
|
||||||
return database.PaymentEntry{}, "", err
|
return database.PaymentEntry{}, "", err
|
||||||
}
|
}
|
||||||
|
entry.State = state.StateInitialized
|
||||||
entry.PaymentIntentId = &result.PaymentIntent.ID
|
entry.PaymentIntentId = &result.PaymentIntent.ID
|
||||||
|
|
||||||
return entry, result.URL, nil
|
return entry, result.URL, nil
|
||||||
|
@ -82,9 +107,10 @@ func (s *Service) CompleteTransaction(entry database.PaymentEntry, amount int64)
|
||||||
return database.PaymentEntry{}, err
|
return database.PaymentEntry{}, err
|
||||||
}
|
}
|
||||||
log.Printf("received state on completion: %v", pi.Status)
|
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.Amount = &pi.AmountReceived
|
||||||
entry.State = state.StateCompleted
|
|
||||||
}
|
}
|
||||||
return entry, nil
|
return entry, nil
|
||||||
}
|
}
|
||||||
|
@ -102,13 +128,13 @@ func (s *Service) CancelTransaction(entry database.PaymentEntry) (database.Payme
|
||||||
return entry, nil
|
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"))
|
id := uuid.MustParse(c.Query("token"))
|
||||||
entry, err := s.Provider.FetchById(id)
|
entry, err := provider.FetchById(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
entry.State = paymentState
|
entry.State = paymentState
|
||||||
s.Provider.UpdateEntry(entry)
|
provider.UpdateEntry(entry)
|
||||||
return "/entries/" + entry.Id.String(), nil
|
return "/entries/" + entry.Id.String(), nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,15 @@
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</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>
|
<h2>Novo plaćanje</h2>
|
||||||
|
|
||||||
<form method="get" action="/methods">
|
<form method="get" action="/methods">
|
||||||
|
@ -58,5 +66,6 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -15,16 +15,30 @@
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</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>
|
<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">
|
<table class="table">
|
||||||
<tr><th>Id: </th><td>{{.Entry.Id}}</td></tr>
|
<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>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>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>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>Ukupna vrijednost: </th><td>{{formatCurrency .Entry.TotalAmount}}</td></tr>
|
||||||
<tr><th>Jezik: </th><td>{{or .Entry.Lang "-"}}</td></tr>
|
<tr><th>Jezik: </th><td>{{or .Entry.Lang "-"}}</td></tr>
|
||||||
<tr><th>Greške: </th><td>{{or .Entry.Error "-"}}</td></tr>
|
<tr><th>Greške: </th><td>{{or .Entry.Error "-"}}</td></tr>
|
||||||
|
@ -32,7 +46,6 @@
|
||||||
|
|
||||||
{{if eq .Entry.Gateway "wspay"}}
|
{{if eq .Entry.Gateway "wspay"}}
|
||||||
<tr><th>WsPay</th><td></td></tr>
|
<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>
|
<tr><th>Success: </th><td>{{or .Entry.Success "-"}}</td></tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
@ -61,5 +74,6 @@
|
||||||
<button class="btn btn-primary" type="submit">Otkaži plaćanje</button>
|
<button class="btn btn-primary" type="submit">Otkaži plaćanje</button>
|
||||||
</form>
|
</form>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
</section>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -21,10 +21,19 @@
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</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>
|
<h2>Izaberi metodu plaćanja</h2>
|
||||||
{{range .Gateways}}
|
{{ range $key, $value := .Gateways }}
|
||||||
<a class="btn btn-success" href="/{{.}}?amount={{$.Amount}}">{{.}}</a>
|
<a class="btn btn-success" href="/methods/{{$key}}?amount={{$.Amount}}">{{$value}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
</section>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -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"
|
<meta name="viewport"
|
||||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
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">
|
<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">
|
<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>
|
<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>
|
<style>
|
||||||
|
@ -15,7 +15,7 @@
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="container" style="margin-top: 32px">
|
<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">
|
<form id="wspay-form" action="{{.Action}}" method="POST">
|
||||||
<input type="hidden" name="ShopID" value="{{.Form.ShopID}}">
|
<input type="hidden" name="ShopID" value="{{.Form.ShopID}}">
|
||||||
|
|
|
@ -18,7 +18,6 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
Provider *database.PaymentEntryProvider
|
|
||||||
ClientId string
|
ClientId string
|
||||||
ClientSecret string
|
ClientSecret string
|
||||||
SourceCode string
|
SourceCode string
|
||||||
|
@ -30,24 +29,57 @@ type Service struct {
|
||||||
expiration time.Time
|
expiration time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) CreatePaymentUrl(amount int64) (url string, err error) {
|
func (s *Service) UpdatePayment(entry database.PaymentEntry) (updatedEntry database.PaymentEntry, err error) {
|
||||||
entry, err := s.Provider.CreateEntry(database.PaymentEntry{
|
token, err := s.oAuthToken()
|
||||||
Gateway: state.GatewayVivaWallet,
|
httpResponse, err := createRequest(
|
||||||
State: state.StateInitialized,
|
"GET",
|
||||||
TotalAmount: amount,
|
"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 {
|
if err != nil {
|
||||||
return "", err
|
return database.PaymentEntry{}, err
|
||||||
}
|
}
|
||||||
entry, err = s.InitializePayment(entry)
|
|
||||||
|
var response TransactionStatusResponse
|
||||||
|
err = readResponse(httpResponse, &response)
|
||||||
if err != nil {
|
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 {
|
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) {
|
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 {
|
if err != nil {
|
||||||
return database.PaymentEntry{}, err
|
return database.PaymentEntry{}, err
|
||||||
}
|
}
|
||||||
|
entry.State = state.StateInitialized
|
||||||
entry.OrderId = &response.OrderId
|
entry.OrderId = &response.OrderId
|
||||||
return entry, nil
|
return entry, nil
|
||||||
}
|
}
|
||||||
|
@ -208,7 +241,7 @@ func (s *Service) basicAuth() string {
|
||||||
return base64.StdEncoding.EncodeToString([]byte(s.MerchantId + ":" + s.ApiKey))
|
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"))
|
transactionId := uuid.MustParse(c.Query("t"))
|
||||||
orderId := database.OrderId(c.Query("s"))
|
orderId := database.OrderId(c.Query("s"))
|
||||||
lang := c.Query("lang")
|
lang := c.Query("lang")
|
||||||
|
@ -216,19 +249,19 @@ func (s *Service) HandleResponse(c *gin.Context, expectedState state.PaymentStat
|
||||||
eci := c.Query("eci")
|
eci := c.Query("eci")
|
||||||
|
|
||||||
log.Printf("Received error response for viva payment %s", orderId)
|
log.Printf("Received error response for viva payment %s", orderId)
|
||||||
entry, err := s.Provider.FetchByOrderId(orderId)
|
entry, err := provider.FetchByOrderId(orderId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Couldn't find payment info for viva payment %s", orderId)
|
log.Printf("Couldn't find payment info for viva payment %s", orderId)
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
entry.State = expectedState
|
entry.State = state
|
||||||
entry.ECI = &eci
|
entry.ECI = &eci
|
||||||
entry.Lang = &lang
|
entry.Lang = &lang
|
||||||
entry.EventId = &eventId
|
entry.EventId = &eventId
|
||||||
entry.TransactionId = &transactionId
|
entry.TransactionId = &transactionId
|
||||||
|
|
||||||
if _, err := s.Provider.UpdateEntry(entry); err != nil {
|
if _, err := provider.UpdateEntry(entry); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
20
viva/viva.go
20
viva/viva.go
|
@ -33,3 +33,23 @@ type TransactionResponse struct {
|
||||||
EventId int64 `json:"EventId"`
|
EventId int64 `json:"EventId"`
|
||||||
Success bool `json:"Success"`
|
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/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"payment-poc/database"
|
"payment-poc/database"
|
||||||
"payment-poc/state"
|
"payment-poc/state"
|
||||||
|
@ -16,22 +17,67 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
Provider *database.PaymentEntryProvider
|
|
||||||
ShopId string
|
ShopId string
|
||||||
ShopSecret string
|
ShopSecret string
|
||||||
BackendUrl string
|
BackendUrl string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) CreatePaymentUrl(amount int64) (string, error) {
|
func (s *Service) UpdatePayment(entry database.PaymentEntry) (updatedEntry database.PaymentEntry, err error) {
|
||||||
entry, err := s.Provider.CreateEntry(database.PaymentEntry{
|
var request = StatusCheckRequest{
|
||||||
Gateway: state.GatewayWsPay,
|
Version: "2.0",
|
||||||
State: state.StateInitialized,
|
ShopId: s.ShopId,
|
||||||
TotalAmount: amount,
|
ShoppingCartId: entry.Id.String(),
|
||||||
})
|
Signature: CalculateStatusCheckSignature(s.ShopId, s.ShopSecret, entry.Id.String()),
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
}
|
||||||
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) {
|
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
|
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{}
|
response := FormReturn{}
|
||||||
if err := c.ShouldBind(&response); err != nil {
|
if err := c.ShouldBind(&response); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
entry, err := s.Provider.FetchById(uuid.MustParse(response.ShoppingCartID))
|
entry, err := provider.FetchById(uuid.MustParse(response.ShoppingCartID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -148,19 +194,19 @@ func (s *Service) HandleSuccessResponse(c *gin.Context) (string, error) {
|
||||||
entry.ApprovalCode = &response.ApprovalCode
|
entry.ApprovalCode = &response.ApprovalCode
|
||||||
entry.State = state.StateAccepted
|
entry.State = state.StateAccepted
|
||||||
|
|
||||||
if _, err := s.Provider.UpdateEntry(entry); err != nil {
|
if _, err := provider.UpdateEntry(entry); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return "/entries/" + entry.Id.String(), nil
|
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{}
|
response := FormError{}
|
||||||
if err := c.ShouldBind(&response); err != nil {
|
if err := c.ShouldBind(&response); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
entry, err := s.Provider.FetchById(uuid.MustParse(response.ShoppingCartID))
|
entry, err := provider.FetchById(uuid.MustParse(response.ShoppingCartID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -176,7 +222,7 @@ func (s *Service) HandleErrorResponse(c *gin.Context, paymentState state.Payment
|
||||||
entry.Error = &response.ErrorMessage
|
entry.Error = &response.ErrorMessage
|
||||||
entry.State = paymentState
|
entry.State = paymentState
|
||||||
|
|
||||||
if _, err := s.Provider.UpdateEntry(entry); err != nil {
|
if _, err := provider.UpdateEntry(entry); err != nil {
|
||||||
return "", err
|
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 {
|
func readResponse[T any](httpResponse *http.Response, response T) error {
|
||||||
if httpResponse.StatusCode == http.StatusOK {
|
if httpResponse.StatusCode == http.StatusOK {
|
||||||
content, err := io.ReadAll(httpResponse.Body)
|
content, err := io.ReadAll(httpResponse.Body)
|
||||||
|
|
|
@ -130,14 +130,14 @@ type StatusCheckResponse struct {
|
||||||
ApprovalCode string
|
ApprovalCode string
|
||||||
ShopID string
|
ShopID string
|
||||||
ShoppingCartID string
|
ShoppingCartID string
|
||||||
Amount string
|
Amount int64
|
||||||
CurrencyCode string
|
CurrencyCode string
|
||||||
ActionSuccess string
|
ActionSuccess string
|
||||||
Success string // deprecated
|
Success string // deprecated
|
||||||
Authorized int
|
Authorized string
|
||||||
Completed int
|
Completed string
|
||||||
Voided int
|
Voided string
|
||||||
Refunded int
|
Refunded string
|
||||||
PaymentPlan string
|
PaymentPlan string
|
||||||
Partner string
|
Partner string
|
||||||
OnSite int
|
OnSite int
|
||||||
|
@ -151,7 +151,7 @@ type StatusCheckResponse struct {
|
||||||
CustomerCountry string
|
CustomerCountry string
|
||||||
CustomerPhone string
|
CustomerPhone string
|
||||||
CustomerEmail string
|
CustomerEmail string
|
||||||
TransactionDateTime string //yyyymmddHHMMss
|
TransactionDateTime string // yyyymmddHHMMss
|
||||||
IsLessThan30DaysFromTransaction bool
|
IsLessThan30DaysFromTransaction bool
|
||||||
CanBeCompleted bool
|
CanBeCompleted bool
|
||||||
CanBeVoided bool
|
CanBeVoided bool
|
||||||
|
|
Loading…
Reference in New Issue