diff --git a/db/dev/v1_0.sql b/db/dev/v1_0.sql index 7e2b1e3..a6a8494 100644 --- a/db/dev/v1_0.sql +++ b/db/dev/v1_0.sql @@ -43,11 +43,22 @@ CREATE TABLE IF NOT EXISTS "stripe" ( "id" uuid NOT NULL, "total_amount" int NOT NULL, - "lang" varchar(128) DEFAULT '', - "payment_intent_id" varchar(256) DEFAULT '', - + "payment_state" varchar(256) DEFAULT '', + + PRIMARY KEY (id) +); + + +CREATE TABLE IF NOT EXISTS "viva" +( + "id" uuid NOT NULL, + "order_id" varchar(24) DEFAULT '', + "transaction_id" uuid DEFAULT NULL, + "total_amount" int NOT NULL, + "event_id" varchar(128) DEFAULT '', + "eci" varchar(128) DEFAULT '', "payment_state" varchar(256) DEFAULT '', PRIMARY KEY (id) diff --git a/main.go b/main.go index d4ac2cf..92262e2 100644 --- a/main.go +++ b/main.go @@ -8,12 +8,14 @@ import ( "github.com/joho/godotenv" "github.com/stripe/stripe-go/v72" "github.com/stripe/stripe-go/v72/checkout/session" + "github.com/stripe/stripe-go/v72/paymentintent" "html/template" "log" "net/http" "payment-poc/migration" "payment-poc/state" stripe2 "payment-poc/stripe" + "payment-poc/viva" "payment-poc/wspay" "strconv" "strings" @@ -24,15 +26,28 @@ import ( var devMigrations embed.FS var BackendUrl string -var ShopId string -var ShopSecret string +var WsPayShopId string +var WsPayShopSecret string + +var VivaMerchantId string +var VivaApiKey string +var VivaSourceCode string +var VivaClientId string +var VivaClientSecret string func init() { godotenv.Load() BackendUrl = envMustExist("BACKEND_URL") - ShopId = envMustExist("WSPAY_SHOP_ID") - ShopSecret = envMustExist("WSPAY_SHOP_SECRET") + WsPayShopId = envMustExist("WSPAY_SHOP_ID") + WsPayShopSecret = envMustExist("WSPAY_SHOP_SECRET") + + VivaMerchantId = envMustExist("VIVA_WALLET_MERCHANT_ID") + VivaApiKey = envMustExist("VIVA_WALLET_API_KEY") + VivaSourceCode = envMustExist("VIVA_WALLET_SOURCE_CODE") + VivaClientId = envMustExist("VIVA_WALLET_CLIENT_ID") + VivaClientSecret = envMustExist("VIVA_WALLET_CLIENT_SECRET") + stripe.Key = envMustExist("STRIPE_KEY") log.SetPrefix("") @@ -55,10 +70,15 @@ func main() { "formatCurrency": func(current int64) string { return fmt.Sprintf("%d,%02d", current/100, current%100) }, + "formatCurrency2": func(current int64) string { + return fmt.Sprintf("%d.%02d", current/100, current%100) + }, "formatState": func(stt state.PaymentState) string { switch stt { case state.StateCanceled: return "Otkazano" + case state.StateVoided: + return "Otkazano sa strane administratora" case state.StateAccepted: return "Prihvačeno" case state.StateError: @@ -93,13 +113,22 @@ func main() { stripeService := stripe2.Service{ DB: client, } + vivaService := viva.Service{ + DB: client, + ClientId: VivaClientId, + ClientSecret: VivaClientSecret, + SourceCode: VivaSourceCode, + MerchantId: VivaMerchantId, + ApiKey: VivaApiKey, + } g.LoadHTMLGlob("./templates/*.gohtml") g.GET("/", func(c *gin.Context) { wspayEntries, _ := wspayService.FetchAll() stripeEntries, _ := stripeService.FetchAll() - c.HTML(200, "index.gohtml", gin.H{"WsPay": wspayEntries, "Stripe": stripeEntries}) + vivaEntries, _ := vivaService.FetchAll() + c.HTML(200, "index.gohtml", gin.H{"WsPay": wspayEntries, "Stripe": stripeEntries, "Viva": vivaEntries}) }) g.GET("/methods", func(c *gin.Context) { @@ -113,6 +142,7 @@ func main() { setupWsPayEndpoints(g.Group("wspay"), wspayService) setupStripeEndpoints(g.Group("stripe"), stripeService) + setupVivaEndpoints(g.Group("viva"), vivaService) log.Fatal(http.ListenAndServe(":5281", g)) } @@ -130,6 +160,147 @@ func parseDateTime(dateTime string) time.Time { return t } +func setupVivaEndpoints(g *gin.RouterGroup, vivaService viva.Service) { + g.GET("", func(c *gin.Context) { + amount, err := strconv.ParseFloat(c.Query("amount"), 64) + if err != nil { + c.AbortWithError(http.StatusBadRequest, err) + return + } + + entry, err := vivaService.CreateEntry(int64(amount * 100)) + if err != nil { + c.AbortWithError(http.StatusBadRequest, err) + return + } + log.Printf("Created initial viva entry (ammount=%d)", amount) + + entry, err = vivaService.CreatePaymentOrder(entry) + if err != nil { + c.AbortWithError(http.StatusBadRequest, err) + return + } + + vivaService.Update(entry) + c.Redirect(http.StatusSeeOther, "https://demo.vivapayments.com/web/checkout?ref="+entry.OrderId) + }) + + g.POST("complete/:id", func(c *gin.Context) { + id := uuid.MustParse(c.Param("id")) + amount, err := strconv.ParseFloat(c.PostForm("amount"), 64) + if err != nil { + c.AbortWithError(http.StatusBadRequest, err) + return + } + entry, err := vivaService.FetchById(id) + if err != nil { + c.AbortWithError(http.StatusNotFound, err) + return + } + if int64(amount*100) > entry.TotalAmount || int64(amount*100) < 1 { + c.AbortWithError(http.StatusBadRequest, err) + return + } + if entry.State == state.StateInitialized || entry.State == state.StateAccepted { + entry, err = vivaService.CompleteTransaction(entry, int64(amount*100)) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + vivaService.Update(entry) + } + + c.Redirect(http.StatusSeeOther, "/viva/info/"+id.String()) + }) + + g.POST("cancel/:id", func(c *gin.Context) { + id := uuid.MustParse(c.Param("id")) + entry, err := vivaService.FetchById(id) + if err != nil { + c.AbortWithError(http.StatusNotFound, err) + return + } + if entry.State == state.StateInitialized || entry.State == state.StateAccepted { + entry, err = vivaService.CancelTransaction(entry) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + vivaService.Update(entry) + } + + c.Redirect(http.StatusSeeOther, "/viva/info/"+id.String()) + }) + + g.GET("success", func(c *gin.Context) { + transactionId := uuid.MustParse(c.Query("t")) + orderId := viva.OrderId(c.Query("s")) + lang := c.Query("lang") + eventId := c.Query("eventId") + eci := c.Query("eci") + + log.Printf("Received success response for viva payment %s", orderId) + entry, err := vivaService.FetchByOrderId(orderId) + if err != nil { + log.Printf("Couldn't find payment info for viva payment %s", orderId) + c.AbortWithError(http.StatusInternalServerError, err) + return + } + + entry.State = state.StateAccepted + entry.ECI = eci + entry.Lang = lang + entry.EventId = eventId + entry.TransactionId = transactionId + + if err := vivaService.Update(entry); err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + + log.Printf("Viva payment %s received correctly, returning redirect", entry.OrderId) + c.Redirect(http.StatusTemporaryRedirect, "/") + }) + g.GET("error", func(c *gin.Context) { + transactionId := uuid.MustParse(c.Query("t")) + orderId := viva.OrderId(c.Query("s")) + lang := c.Query("lang") + eventId := c.Query("eventId") + eci := c.Query("eci") + + log.Printf("Received error response for viva payment %s", orderId) + entry, err := vivaService.FetchByOrderId(orderId) + if err != nil { + log.Printf("Couldn't find payment info for viva payment %s", orderId) + c.AbortWithError(http.StatusInternalServerError, err) + return + } + + entry.State = state.StateAccepted + entry.ECI = eci + entry.Lang = lang + entry.EventId = eventId + entry.TransactionId = transactionId + + if err := vivaService.Update(entry); err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + + log.Printf("Viva payment %s received correctly, returning redirect", entry.OrderId) + c.Redirect(http.StatusTemporaryRedirect, "/") + }) + g.GET("info/:id", func(c *gin.Context) { + id := uuid.MustParse(c.Param("id")) + entry, err := vivaService.FetchById(id) + if err != nil { + c.AbortWithError(http.StatusNotFound, err) + return + } + c.HTML(200, "viva_info.gohtml", gin.H{"Entry": entry}) + }) +} + func setupStripeEndpoints(g *gin.RouterGroup, stripeService stripe2.Service) { g.GET("", func(c *gin.Context) { amount, err := strconv.ParseFloat(c.Query("amount"), 64) @@ -181,6 +352,66 @@ func setupStripeEndpoints(g *gin.RouterGroup, stripeService stripe2.Service) { c.Redirect(http.StatusSeeOther, result.URL) }) + g.POST("complete/:id", func(c *gin.Context) { + id := uuid.MustParse(c.Param("id")) + amount, err := strconv.ParseFloat(c.PostForm("amount"), 64) + if err != nil { + c.AbortWithError(http.StatusBadRequest, err) + return + } + entry, err := stripeService.FetchById(id) + if err != nil { + c.AbortWithError(http.StatusNotFound, err) + return + } + if int64(amount*100) > entry.TotalAmount || int64(amount*100) < 1 { + c.AbortWithError(http.StatusBadRequest, err) + return + } + if entry.State == state.StateInitialized || entry.State == state.StateAccepted { + params := &stripe.PaymentIntentCaptureParams{ + AmountToCapture: stripe.Int64(int64(amount * 100)), + } + pi, err := paymentintent.Capture(entry.PaymentIntentId, params) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + log.Printf("received state on completion: %v", pi.Status) + if pi.Status == stripe.PaymentIntentStatusSucceeded || pi.Status == stripe.PaymentIntentStatusProcessing { + entry.TotalAmount = pi.Amount + entry.State = state.StateCompleted + stripeService.Update(entry) + } + } + + c.Redirect(http.StatusSeeOther, "/stripe/info/"+id.String()) + }) + + g.POST("cancel/:id", func(c *gin.Context) { + id := uuid.MustParse(c.Param("id")) + entry, err := stripeService.FetchById(id) + if err != nil { + c.AbortWithError(http.StatusNotFound, err) + return + } + if entry.State == state.StateInitialized || entry.State == state.StateAccepted { + params := &stripe.PaymentIntentCancelParams{} + pi, err := paymentintent.Cancel(entry.PaymentIntentId, params) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + log.Printf("received state on completion: %v", pi.Status) + if pi.Status == stripe.PaymentIntentStatusCanceled { + entry.State = state.StateCanceled + stripeService.Update(entry) + } + } + + c.Redirect(http.StatusSeeOther, "/stripe/info/"+id.String()) + }) + g.GET("success", func(c *gin.Context) { id := uuid.MustParse(c.Query("token")) @@ -241,7 +472,7 @@ func setupWsPayEndpoints(g *gin.RouterGroup, wspayService wspay.Service) { return } - entry, err := wspayService.CreateEntry(ShopId, int64(amount*100)) + entry, err := wspayService.CreateEntry(WsPayShopId, int64(amount*100)) if err != nil { c.AbortWithError(http.StatusBadRequest, err) return @@ -250,14 +481,14 @@ func setupWsPayEndpoints(g *gin.RouterGroup, wspayService wspay.Service) { log.Printf("Created initial wspay form (ammount=%d)", amount) form := wspay.WsPayForm{ - ShopID: ShopId, + ShopID: WsPayShopId, ShoppingCartID: entry.ShoppingCartID, Version: "2.0", TotalAmount: entry.TotalAmount, ReturnURL: BackendUrl + "/wspay/success", ReturnErrorURL: BackendUrl + "/wspay/error", CancelURL: BackendUrl + "/wspay/cancel", - Signature: wspay.CalculateFormSignature(ShopId, ShopSecret, entry.ShoppingCartID, entry.TotalAmount), + Signature: wspay.CalculateFormSignature(WsPayShopId, WsPayShopSecret, entry.ShoppingCartID, entry.TotalAmount), } c.HTML(200, "wspay.gohtml", gin.H{"Action": wspay.AuthorisationForm, "Form": form}) @@ -276,7 +507,7 @@ func setupWsPayEndpoints(g *gin.RouterGroup, wspayService wspay.Service) { return } - if err := wspay.CompareFormReturnSignature(response.Signature, ShopId, ShopSecret, response.ShoppingCartID, response.Success, response.ApprovalCode); err != nil { + if err := wspay.CompareFormReturnSignature(response.Signature, WsPayShopId, WsPayShopSecret, response.ShoppingCartID, response.Success, response.ApprovalCode); err != nil { log.Printf("Invalid signature for transaction %s", response.ShoppingCartID) c.AbortWithError(http.StatusBadRequest, err) return diff --git a/templates/index.gohtml b/templates/index.gohtml index 8795714..35794d3 100644 --- a/templates/index.gohtml +++ b/templates/index.gohtml @@ -75,6 +75,26 @@ {{end}} + +

Viva

+ + + + + + + + + + {{range .Viva}} + + + + + + {{end}} + +
IdVrijednostStanje
{{.Id}}{{formatCurrency .TotalAmount}}{{formatState .State}}
\ No newline at end of file diff --git a/templates/methods.gohtml b/templates/methods.gohtml index 59e83d8..9982918 100644 --- a/templates/methods.gohtml +++ b/templates/methods.gohtml @@ -25,5 +25,6 @@

Izaberi metodu plačanja

WsPay Stripe + Viva \ No newline at end of file diff --git a/templates/stripe_info.gohtml b/templates/stripe_info.gohtml index 4af15b8..6132644 100644 --- a/templates/stripe_info.gohtml +++ b/templates/stripe_info.gohtml @@ -24,5 +24,19 @@ Jezik: {{omitempty .Entry.Lang}} Stanje: {{formatState .Entry.State}} + + + {{if eq .Entry.State "accepted"}} +
+
+ + +
+ +
+
+ +
+ {{end}} \ No newline at end of file diff --git a/templates/viva_info.gohtml b/templates/viva_info.gohtml new file mode 100644 index 0000000..22c66fa --- /dev/null +++ b/templates/viva_info.gohtml @@ -0,0 +1,44 @@ + + + + + + + Info + + + + + +

Plačanje {{.Entry.Id}}

+ + + + + + + + + +
Id: {{.Entry.Id}}
Order id: {{.Entry.OrderId}}
Transaction id: {{.Entry.TransactionId.String}}
Ukupna vrijednost: {{formatCurrency .Entry.TotalAmount}}
Jezik: {{omitempty .Entry.Lang}}
Događaj: {{.Entry.EventId}}
Stanje: {{formatState .Entry.State}}
+ + {{if eq .Entry.State "accepted"}} +
+
+ + +
+ +
+
+ +
+ {{end}} + + \ No newline at end of file diff --git a/viva/model.go b/viva/model.go new file mode 100644 index 0000000..f2d6e35 --- /dev/null +++ b/viva/model.go @@ -0,0 +1,26 @@ +package viva + +import ( + "github.com/google/uuid" + "payment-poc/state" + "time" +) + +const VivaUrl = "https://demo-api.vivapayments.com" + +type VivaDb struct { + Id uuid.UUID `db:"id"` + OrderId string `db:"order_id"` + TransactionId uuid.UUID `db:"transaction_id"` + TotalAmount int64 `db:"total_amount"` + Lang string `db:"lang"` + + EventId string `db:"event_id"` + ECI string `db:"eci"` + + DateTime time.Time `db:"date_time"` + + // transaction response + + State state.PaymentState `db:"payment_state"` +} diff --git a/viva/service.go b/viva/service.go new file mode 100644 index 0000000..a6aa129 --- /dev/null +++ b/viva/service.go @@ -0,0 +1,235 @@ +package viva + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "errors" + "github.com/google/uuid" + "github.com/jmoiron/sqlx" + "io" + "log" + "net/http" + "net/url" + "payment-poc/state" + "strconv" + "strings" + "time" +) + +type Service struct { + DB *sqlx.DB + Token string + Expiration time.Time + ClientId string + ClientSecret string + SourceCode string + + MerchantId string + ApiKey string +} + +func (s *Service) OAuthToken() (string, error) { + if s.Token != "" && s.Expiration.After(time.Now()) { + return s.Token, nil + } + return s.fetchOAuthToken() +} + +func (s *Service) CreatePaymentOrder(entry VivaDb) (VivaDb, error) { + token, err := s.OAuthToken() + if err != nil { + return VivaDb{}, err + } + orderRequest := VivaOrderRequest{ + Amount: entry.TotalAmount, + Description: "Example payment", + MerchantDescription: "Example payment", + PreAuth: true, + AllowRecurring: false, + Source: s.SourceCode, + } + + content, err := json.Marshal(&orderRequest) + if err != nil { + return VivaDb{}, err + } + + request, err := http.NewRequest("POST", "https://demo-api.vivapayments.com/checkout/v2/orders", bytes.NewReader(content)) + request.Header.Add("authorization", "Bearer "+token) + request.Header.Add("content-type", "application/json") + + response, err := http.DefaultClient.Do(request) + if err != nil { + return VivaDb{}, err + } + if response.StatusCode == http.StatusOK { + orderResponse := VivaOrderResponse{} + content, err := io.ReadAll(response.Body) + if err != nil { + return VivaDb{}, err + } + if err := json.Unmarshal(content, &orderResponse); err != nil { + return VivaDb{}, err + } else { + entry.OrderId = string(orderResponse.OrderId) + return entry, nil + } + } else { + return VivaDb{}, errors.New("received wrong status, expected 200 received " + strconv.FormatInt(int64(response.StatusCode), 10)) + } +} + +func (s *Service) CreateEntry(totalAmount int64) (VivaDb, error) { + id := uuid.Must(uuid.NewRandom()) + entry := VivaDb{ + Id: id, + TotalAmount: totalAmount, + State: state.StateInitialized, + } + _, err := s.DB.Exec(`INSERT INTO "viva" ("id", "total_amount", "payment_state") VALUES ($1, $2, $3)`, + &entry.Id, &entry.TotalAmount, &entry.State, + ) + if err != nil { + return VivaDb{}, err + } + return s.FetchById(id) +} + +func (s *Service) FetchAll() ([]VivaDb, error) { + var entries []VivaDb + err := s.DB.Select(&entries, `SELECT * FROM "viva"`) + return entries, err +} + +func (s *Service) FetchById(id uuid.UUID) (VivaDb, error) { + entry := VivaDb{} + err := s.DB.Get(&entry, `SELECT * FROM "viva" WHERE "id" = $1`, id) + return entry, err +} + +func (s *Service) FetchByOrderId(id OrderId) (VivaDb, error) { + entry := VivaDb{} + err := s.DB.Get(&entry, `SELECT * FROM "viva" WHERE "order_id" = $1`, string(id)) + return entry, err +} + +func (s *Service) Update(entry VivaDb) error { + _, err := s.DB.Exec(`UPDATE "viva" set "order_id" = $2, "transaction_id" = $3, "payment_state" = $4 WHERE "id" = $1`, + &entry.Id, &entry.OrderId, &entry.TransactionId, &entry.State, + ) + return err +} + +func (s *Service) fetchOAuthToken() (string, error) { + + form := url.Values{ + "grant_type": []string{"client_credentials"}, + } + request, err := http.NewRequest("POST", "https://demo-accounts.vivapayments.com/connect/token", strings.NewReader(form.Encode())) + if err != nil { + return "", err + } + request.Header.Add("content-type", "application/x-www-form-urlencoded") + request.SetBasicAuth(s.ClientId, s.ClientSecret) + + response, err := http.DefaultClient.Do(request) + if err != nil { + return "", err + } + if response.StatusCode == http.StatusOK { + oauthObject := VivaOAuthResponse{} + content, err := io.ReadAll(response.Body) + if err != nil { + return "", err + } + if err := json.Unmarshal(content, &oauthObject); err != nil { + return "", err + } else { + s.Token = oauthObject.AccessToken + s.Expiration = time.Now().Add(time.Duration(oauthObject.ExpiresIn) * time.Second) + } + } else { + return "", errors.New("received wrong status, expected 200 received " + strconv.FormatInt(int64(response.StatusCode), 10)) + } + return s.Token, nil +} + +func (s *Service) CompleteTransaction(entry VivaDb, amount int64) (VivaDb, error) { + completionRequest := VivaTransactionCompleteRequest{ + Amount: amount, + CustomerDescription: "Example transaction", + } + content, err := json.Marshal(&completionRequest) + if err != nil { + return VivaDb{}, err + } + + request, err := http.NewRequest("POST", "https://demo.vivapayments.com/api/transactions/"+entry.TransactionId.String(), bytes.NewReader(content)) + request.Header.Add("authorization", "Bearer "+s.BasicAuth()) + request.Header.Add("content-type", "application/json") + + response, err := http.DefaultClient.Do(request) + if err != nil { + return VivaDb{}, err + } + if response.StatusCode == http.StatusOK { + transactionResponse := VivaTransactionResponse{} + content, err := io.ReadAll(response.Body) + if err != nil { + return VivaDb{}, err + } + if err := json.Unmarshal(content, &transactionResponse); err != nil { + return VivaDb{}, err + } else { + log.Printf("Received transaction response: success=%v, eventId=%d, status=%s, amount=%f, errorCode=%d, errorText=%s", + transactionResponse.Success, transactionResponse.EventId, transactionResponse.StatusId, transactionResponse.Amount, transactionResponse.ErrorCode, transactionResponse.ErrorText, + ) + if transactionResponse.StatusId == "F" { + entry.TotalAmount = int64(transactionResponse.Amount * 100) + entry.State = state.StateCompleted + } else { + return VivaDb{}, errors.New("received invalid status = " + transactionResponse.StatusId) + } + } + } else { + return VivaDb{}, errors.New("received wrong status, expected 200 received " + strconv.FormatInt(int64(response.StatusCode), 10)) + } + return entry, nil +} + +func (s *Service) BasicAuth() string { + return base64.StdEncoding.EncodeToString([]byte(s.MerchantId + ":" + s.ApiKey)) +} + +func (s *Service) CancelTransaction(entry VivaDb) (VivaDb, error) { + request, err := http.NewRequest("DELETE", "https://demo.vivapayments.com/api/transactions/"+entry.TransactionId.String()+"?amount="+strconv.FormatInt(entry.TotalAmount, 10), bytes.NewReader([]byte{})) + request.Header.Add("authorization", "Bearer "+s.BasicAuth()) + + response, err := http.DefaultClient.Do(request) + if err != nil { + return VivaDb{}, err + } + if response.StatusCode == http.StatusOK { + transactionResponse := VivaTransactionResponse{} + content, err := io.ReadAll(response.Body) + if err != nil { + return VivaDb{}, err + } + if err := json.Unmarshal(content, &transactionResponse); err != nil { + return VivaDb{}, err + } else { + log.Printf("Received transaction response: success=%v, eventId=%d, status=%s, amount=%f, errorCode=%d, errorText=%s", + transactionResponse.Success, transactionResponse.EventId, transactionResponse.StatusId, transactionResponse.Amount, transactionResponse.ErrorCode, transactionResponse.ErrorText, + ) + if transactionResponse.StatusId == "F" { + entry.State = state.StateVoided + } else { + return VivaDb{}, errors.New("received invalid status = " + transactionResponse.StatusId) + } + } + } else { + return VivaDb{}, errors.New("received wrong status, expected 200 received " + strconv.FormatInt(int64(response.StatusCode), 10)) + } + return entry, nil +} diff --git a/viva/viva.go b/viva/viva.go new file mode 100644 index 0000000..64336a4 --- /dev/null +++ b/viva/viva.go @@ -0,0 +1,44 @@ +package viva + +type OrderId string + +func (o OrderId) MarshalJSON() ([]byte, error) { + return []byte(o), nil +} + +func (o *OrderId) UnmarshalJSON(value []byte) error { + *o = OrderId(value) + return nil +} + +type VivaOrderRequest struct { + Amount int64 `json:"amount"` + Description string `json:"customerTrns"` + MerchantDescription string `json:"merchantTrns"` + PreAuth bool `json:"preauth"` + AllowRecurring bool `json:"allowRecurring"` + Source string `json:"sourceCode"` +} + +type VivaOrderResponse struct { + OrderId OrderId `json:"orderCode"` +} + +type VivaOAuthResponse struct { + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` +} + +type VivaTransactionCompleteRequest struct { + Amount int64 `json:"amount"` + CustomerDescription string `json:"customerTrns"` +} + +type VivaTransactionResponse struct { + Amount float64 `json:"Amount"` + StatusId string `json:"StatusId"` + ErrorCode int64 `json:"ErrorCode"` + ErrorText string `json:"ErrorText"` + EventId int64 `json:"EventId"` + Success bool `json:"Success"` +}