WIP Code cleanup
This commit is contained in:
parent
b4b0396b30
commit
dcc9754d43
|
@ -0,0 +1,54 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"payment-poc/state"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PaymentEntry struct {
|
||||||
|
Id uuid.UUID `db:"id"`
|
||||||
|
|
||||||
|
Created time.Time `db:"created"`
|
||||||
|
Modified *time.Time `db:"modified"`
|
||||||
|
|
||||||
|
Gateway state.PaymentGateway `db:"gateway"`
|
||||||
|
State state.PaymentState `db:"state"`
|
||||||
|
|
||||||
|
Lang *string `db:"lang"`
|
||||||
|
|
||||||
|
Error *string `db:"error"`
|
||||||
|
|
||||||
|
// paid amount
|
||||||
|
Amount *int64 `db:"amount"`
|
||||||
|
// preauthorized amount
|
||||||
|
TotalAmount int64 `db:"total_amount"`
|
||||||
|
|
||||||
|
// used for wspay and viva
|
||||||
|
ECI *string `db:"eci"`
|
||||||
|
|
||||||
|
// stripe field
|
||||||
|
PaymentIntentId *string `db:"payment_intent_id"`
|
||||||
|
|
||||||
|
// wspay field
|
||||||
|
ShoppingCardID *string `db:"shopping_card_it"`
|
||||||
|
STAN *string `db:"stan"`
|
||||||
|
Success *int `db:"success"`
|
||||||
|
ApprovalCode *string `db:"approval_code"`
|
||||||
|
|
||||||
|
// viva field
|
||||||
|
OrderId *OrderId `db:"order_id"`
|
||||||
|
TransactionId *uuid.UUID `db:"transaction_id"`
|
||||||
|
EventId *string `db:"event_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PaymentEntryProvider struct {
|
||||||
|
DB *sqlx.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PaymentEntryProvider) CreateEntry(entry PaymentEntry) (PaymentEntry, error) {
|
||||||
|
if entry.Id == uuid.Nil {
|
||||||
|
entry.Id = uuid.Must(uuid.NewRandom())
|
||||||
|
}
|
||||||
|
entry.Created = time.Now()
|
||||||
|
|
||||||
|
_, err := p.DB.Exec(`INSERT INTO "payment_entry" ("id", "created", "gateway", "state", "lang", "error", "amount", "total_amount", "eci", "payment_intent_id", "shopping_card_id", "stan", "success", "approval_code", "order_id", "transaction_id", "event_id")`,
|
||||||
|
&entry.Id, &entry.Created, &entry.Gateway, &entry.State, &entry.Lang, &entry.Error, &entry.Amount, &entry.TotalAmount, &entry.ECI, &entry.PaymentIntentId, &entry.ShoppingCardID, &entry.STAN, &entry.Success, &entry.ApprovalCode, &entry.OrderId, &entry.TransactionId, &entry.EventId,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return PaymentEntry{}, err
|
||||||
|
}
|
||||||
|
return p.FetchById(entry.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PaymentEntryProvider) UpdateEntry(entry PaymentEntry) (PaymentEntry, error) {
|
||||||
|
currentTime := time.Now()
|
||||||
|
entry.Modified = ¤tTime
|
||||||
|
|
||||||
|
_, err := p.DB.Exec(`UPDATE "payment_entry" SET "modified" = $2, "state" = $3, "lang" = $4, "error" = $5, "amount" = $6, "eci" = $7, "payment_intent_id" = $8, "shopping_card_id" = $9, "stan" = $10, "success" = $11, "approval_code" = $12, "order_id" = $13, "transaction_id" = $14, "event_id" = $15 WHERE "id" = $1`,
|
||||||
|
&entry.Id, &entry.Modified, &entry.State, &entry.Lang, &entry.Error, &entry.Amount, &entry.ECI, &entry.PaymentIntentId, &entry.ShoppingCardID, &entry.STAN, &entry.Success, &entry.ApprovalCode, &entry.OrderId, &entry.TransactionId, &entry.EventId,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return PaymentEntry{}, err
|
||||||
|
}
|
||||||
|
return p.FetchById(entry.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PaymentEntryProvider) FetchById(id uuid.UUID) (PaymentEntry, error) {
|
||||||
|
entry := PaymentEntry{}
|
||||||
|
err := p.DB.Get(&entry, `SELECT * FROM "payment_entry" WHERE "id" = $1`, id)
|
||||||
|
return entry, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PaymentEntryProvider) FetchAll() ([]PaymentEntry, error) {
|
||||||
|
var entries []PaymentEntry
|
||||||
|
err := p.DB.Select(&entries, `SELECT * FROM "payment_entry"`)
|
||||||
|
return entries, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PaymentEntryProvider) FetchByOrderId(orderId OrderId) (PaymentEntry, error) {
|
||||||
|
entry := PaymentEntry{}
|
||||||
|
err := p.DB.Get(&entry, `SELECT * FROM "payment_entry" WHERE "order_id" = $1`, orderId)
|
||||||
|
return entry, err
|
||||||
|
}
|
|
@ -1 +1,65 @@
|
||||||
package prod
|
|
||||||
|
CREATE TABLE IF NOT EXISTS "wspay"
|
||||||
|
(
|
||||||
|
"id" uuid NOT NULL,
|
||||||
|
"shop_id" varchar(128) NOT NULL,
|
||||||
|
"shopping_card_id" varchar(128) NOT NULL,
|
||||||
|
"total_amount" int NOT NULL,
|
||||||
|
|
||||||
|
"lang" varchar(128) DEFAULT '',
|
||||||
|
|
||||||
|
"customer_first_name" varchar(128) DEFAULT '',
|
||||||
|
"customer_last_name" varchar(128) DEFAULT '',
|
||||||
|
"customer_address" varchar(128) DEFAULT '',
|
||||||
|
"customer_city" varchar(128) DEFAULT '',
|
||||||
|
"customer_zip" varchar(128) DEFAULT '',
|
||||||
|
"customer_country" varchar(128) DEFAULT '',
|
||||||
|
"customer_phone" varchar(128) DEFAULT '',
|
||||||
|
|
||||||
|
"payment_plan" varchar(128) DEFAULT '',
|
||||||
|
"credit_card_name" varchar(128) DEFAULT '',
|
||||||
|
"credit_card_number" varchar(128) DEFAULT '',
|
||||||
|
"payment_method" varchar(128) DEFAULT '',
|
||||||
|
"currency_code" int DEFAULT 0,
|
||||||
|
|
||||||
|
"date_time" timestamp DEFAULT current_timestamp,
|
||||||
|
|
||||||
|
"eci" varchar(256) DEFAULT '',
|
||||||
|
"stan" varchar(256) DEFAULT '',
|
||||||
|
|
||||||
|
"success" int DEFAULT 0,
|
||||||
|
"approval_code" varchar(256) DEFAULT '',
|
||||||
|
"error_message" varchar(256) DEFAULT '',
|
||||||
|
"error_codes" varchar(256) DEFAULT '',
|
||||||
|
|
||||||
|
"payment_state" varchar(256) DEFAULT '',
|
||||||
|
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
CONSTRAINT unique_id UNIQUE ("shopping_card_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
|
|
@ -16,3 +16,11 @@ const (
|
||||||
StateVoided PaymentState = "voided"
|
StateVoided PaymentState = "voided"
|
||||||
StateCanceled PaymentState = "canceled"
|
StateCanceled PaymentState = "canceled"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type PaymentGateway string
|
||||||
|
|
||||||
|
const (
|
||||||
|
GatewayWsPay PaymentGateway = "wspay"
|
||||||
|
GatewayStripe PaymentGateway = "stripe"
|
||||||
|
GatewayVivaWallet PaymentGateway = "viva-wallet"
|
||||||
|
)
|
||||||
|
|
|
@ -1,46 +1,114 @@
|
||||||
package stripe
|
package stripe
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/stripe/stripe-go/v72"
|
||||||
|
"github.com/stripe/stripe-go/v72/checkout/session"
|
||||||
|
"github.com/stripe/stripe-go/v72/paymentintent"
|
||||||
|
"log"
|
||||||
|
"payment-poc/database"
|
||||||
"payment-poc/state"
|
"payment-poc/state"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
DB *sqlx.DB
|
Provider *database.PaymentEntryProvider
|
||||||
|
ApiKey string
|
||||||
|
BackendUrl string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) CreateEntry(totalAmount int64) (StripeDb, error) {
|
func (s *Service) CreatePaymentUrl(amount int64) (url string, err error) {
|
||||||
id := uuid.Must(uuid.NewRandom())
|
entry, err := s.Provider.CreateEntry(database.PaymentEntry{
|
||||||
entry := StripeDb{
|
Gateway: state.GatewayVivaWallet,
|
||||||
Id: id,
|
|
||||||
TotalAmount: totalAmount,
|
|
||||||
State: state.StateInitialized,
|
State: state.StateInitialized,
|
||||||
}
|
TotalAmount: amount,
|
||||||
_, err := s.DB.Exec(`INSERT INTO "stripe" ("id", "total_amount", "payment_state") VALUES ($1, $2, $3)`,
|
})
|
||||||
&entry.Id, &entry.TotalAmount, &entry.State,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return StripeDb{}, err
|
return "", err
|
||||||
}
|
}
|
||||||
return s.FetchById(id)
|
entry, url, err = s.InitializePayment(entry)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
entry, err = s.Provider.UpdateEntry(entry)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return url, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) FetchAll() ([]StripeDb, error) {
|
func (s *Service) InitializePayment(entry database.PaymentEntry) (database.PaymentEntry, string, error) {
|
||||||
var entries []StripeDb
|
|
||||||
err := s.DB.Select(&entries, `SELECT * FROM "stripe"`)
|
currency := string(stripe.CurrencyEUR)
|
||||||
return entries, err
|
productName := "Example product"
|
||||||
|
productDescription := "Simple example product"
|
||||||
|
|
||||||
|
params := &stripe.CheckoutSessionParams{
|
||||||
|
LineItems: []*stripe.CheckoutSessionLineItemParams{
|
||||||
|
{
|
||||||
|
PriceData: &stripe.CheckoutSessionLineItemPriceDataParams{
|
||||||
|
Currency: ¤cy,
|
||||||
|
ProductData: &stripe.CheckoutSessionLineItemPriceDataProductDataParams{
|
||||||
|
Name: &productName,
|
||||||
|
Description: &productDescription,
|
||||||
|
},
|
||||||
|
UnitAmount: &entry.TotalAmount,
|
||||||
|
},
|
||||||
|
Quantity: stripe.Int64(1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Mode: stripe.String(string(stripe.CheckoutSessionModePayment)),
|
||||||
|
PaymentIntentData: &stripe.CheckoutSessionPaymentIntentDataParams{
|
||||||
|
CaptureMethod: stripe.String("manual"),
|
||||||
|
},
|
||||||
|
SuccessURL: stripe.String(s.BackendUrl + "/stripe/success?token=" + entry.Id.String()),
|
||||||
|
CancelURL: stripe.String(s.BackendUrl + "/stripe/cancel?token=" + entry.Id.String()),
|
||||||
|
}
|
||||||
|
result, err := session.New(params)
|
||||||
|
if err != nil {
|
||||||
|
return database.PaymentEntry{}, "", err
|
||||||
|
}
|
||||||
|
entry.PaymentIntentId = &result.PaymentIntent.ID
|
||||||
|
|
||||||
|
return entry, result.URL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) FetchById(id uuid.UUID) (StripeDb, error) {
|
func (s *Service) CompleteTransaction(entry database.PaymentEntry, amount int64) (database.PaymentEntry, error) {
|
||||||
entry := StripeDb{}
|
params := &stripe.PaymentIntentCaptureParams{
|
||||||
err := s.DB.Get(&entry, `SELECT * FROM "stripe" WHERE "id" = $1`, id)
|
AmountToCapture: stripe.Int64(amount),
|
||||||
return entry, err
|
}
|
||||||
|
pi, err := paymentintent.Capture(*entry.PaymentIntentId, params)
|
||||||
|
if err != nil {
|
||||||
|
return database.PaymentEntry{}, err
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
return entry, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Update(entry StripeDb) error {
|
func (s *Service) CancelTransaction(entry database.PaymentEntry) (database.PaymentEntry, error) {
|
||||||
_, err := s.DB.Exec(`UPDATE "stripe" set "payment_intent_id" = $2, "payment_state" = $3 WHERE "id" = $1`,
|
params := &stripe.PaymentIntentCancelParams{}
|
||||||
&entry.Id, &entry.PaymentIntentId, &entry.State,
|
pi, err := paymentintent.Cancel(*entry.PaymentIntentId, params)
|
||||||
)
|
if err != nil {
|
||||||
return err
|
return database.PaymentEntry{}, err
|
||||||
|
}
|
||||||
|
log.Printf("received state on completion: %v", pi.Status)
|
||||||
|
if pi.Status == stripe.PaymentIntentStatusCanceled {
|
||||||
|
entry.State = state.StateCanceled
|
||||||
|
}
|
||||||
|
return entry, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) HandleResponse(c *gin.Context, paymentState state.PaymentState) (string, error) {
|
||||||
|
id := uuid.MustParse(c.Query("token"))
|
||||||
|
entry, err := s.Provider.FetchById(id)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
entry.State = paymentState
|
||||||
|
s.Provider.UpdateEntry(entry)
|
||||||
|
return "/entries/" + entry.Id.String(), nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
<form class="mb-3" method="post" action="/stripe/complete/{{.Entry.Id}}">
|
<form class="mb-3" method="post" action="/stripe/complete/{{.Entry.Id}}">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label" for="amount">Završi transakciju</label>
|
<label class="form-label" for="amount">Završi transakciju</label>
|
||||||
<input class="form-control" id="amount" required name="amount" type="number" value="{{formatCurrency2 .Entry.TotalAmount}}" step="0.01" min="0.01" max="{{formatCurrency2 .Entry.TotalAmount}}">
|
<input class="form-control" id="amount" required name="amount" type="number" value="{{decimalCurrency .Entry.TotalAmount}}" step="0.01" min="0.01" max="{{decimalCurrency .Entry.TotalAmount}}">
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" type="submit">Završi transakciju</button>
|
<button class="btn btn-primary" type="submit">Završi transakciju</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
<form class="mb-3" method="post" action="/viva/complete/{{.Entry.Id}}">
|
<form class="mb-3" method="post" action="/viva/complete/{{.Entry.Id}}">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label" for="amount">Završi transakciju</label>
|
<label class="form-label" for="amount">Završi transakciju</label>
|
||||||
<input class="form-control" id="amount" required name="amount" type="number" value="{{formatCurrency2 .Entry.TotalAmount}}" step="0.01" min="0.01" max="{{formatCurrency2 .Entry.TotalAmount}}">
|
<input class="form-control" id="amount" required name="amount" type="number" value="{{decimalCurrency .Entry.TotalAmount}}" step="0.01" min="0.01" max="{{decimalCurrency .Entry.TotalAmount}}">
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" type="submit">Završi transakciju</button>
|
<button class="btn btn-primary" type="submit">Završi transakciju</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -50,7 +50,7 @@
|
||||||
<form class="mb-3" method="post" action="/wspay/complete/{{.Entry.Id}}">
|
<form class="mb-3" method="post" action="/wspay/complete/{{.Entry.Id}}">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label" for="amount">Završi transakciju</label>
|
<label class="form-label" for="amount">Završi transakciju</label>
|
||||||
<input class="form-control" id="amount" required name="amount" type="number" value="{{formatCurrency2 .Entry.TotalAmount}}" step="0.01" min="0.01" max="{{formatCurrency2 .Entry.TotalAmount}}">
|
<input class="form-control" id="amount" required name="amount" type="number" value="{{decimalCurrency .Entry.TotalAmount}}" step="0.01" min="0.01" max="{{decimalCurrency .Entry.TotalAmount}}">
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" type="submit">Završi transakciju</button>
|
<button class="btn btn-primary" type="submit">Završi transakciju</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
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"`
|
|
||||||
}
|
|
348
viva/service.go
348
viva/service.go
|
@ -5,43 +5,58 @@ import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/jmoiron/sqlx"
|
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"payment-poc/database"
|
||||||
"payment-poc/state"
|
"payment-poc/state"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
DB *sqlx.DB
|
Provider *database.PaymentEntryProvider
|
||||||
Token string
|
|
||||||
Expiration time.Time
|
|
||||||
ClientId string
|
ClientId string
|
||||||
ClientSecret string
|
ClientSecret string
|
||||||
SourceCode string
|
SourceCode string
|
||||||
|
|
||||||
MerchantId string
|
MerchantId string
|
||||||
ApiKey string
|
ApiKey string
|
||||||
|
|
||||||
|
token string
|
||||||
|
expiration time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) OAuthToken() (string, error) {
|
func (s *Service) CreatePaymentUrl(amount int64) (url string, err error) {
|
||||||
if s.Token != "" && s.Expiration.After(time.Now()) {
|
entry, err := s.Provider.CreateEntry(database.PaymentEntry{
|
||||||
return s.Token, nil
|
Gateway: state.GatewayVivaWallet,
|
||||||
}
|
State: state.StateInitialized,
|
||||||
return s.fetchOAuthToken()
|
TotalAmount: amount,
|
||||||
}
|
})
|
||||||
|
|
||||||
func (s *Service) CreatePaymentOrder(entry VivaDb) (VivaDb, error) {
|
|
||||||
token, err := s.OAuthToken()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return VivaDb{}, err
|
return "", err
|
||||||
}
|
}
|
||||||
orderRequest := VivaOrderRequest{
|
entry, err = s.InitializePayment(entry)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
entry, err = s.Provider.UpdateEntry(entry)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return "https://demo.vivapayments.com/web/checkout?ref=" + string(*entry.OrderId), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) InitializePayment(entry database.PaymentEntry) (database.PaymentEntry, error) {
|
||||||
|
token, err := s.oAuthToken()
|
||||||
|
if err != nil {
|
||||||
|
return database.PaymentEntry{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
request := OrderRequest{
|
||||||
Amount: entry.TotalAmount,
|
Amount: entry.TotalAmount,
|
||||||
Description: "Example payment",
|
Description: "Example payment",
|
||||||
MerchantDescription: "Example payment",
|
MerchantDescription: "Example payment",
|
||||||
|
@ -50,186 +65,173 @@ func (s *Service) CreatePaymentOrder(entry VivaDb) (VivaDb, error) {
|
||||||
Source: s.SourceCode,
|
Source: s.SourceCode,
|
||||||
}
|
}
|
||||||
|
|
||||||
content, err := json.Marshal(&orderRequest)
|
httpResponse, err := createRequest(
|
||||||
|
"POST",
|
||||||
|
"https://demo-api.vivapayments.com/checkout/v2/orders",
|
||||||
|
map[string]string{"authorization": "Bearer " + token, "content-type": "application/json"},
|
||||||
|
toJson(request),
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return VivaDb{}, err
|
return database.PaymentEntry{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
request, err := http.NewRequest("POST", "https://demo-api.vivapayments.com/checkout/v2/orders", bytes.NewReader(content))
|
var response OrderResponse
|
||||||
request.Header.Add("authorization", "Bearer "+token)
|
err = readResponse(httpResponse, &response)
|
||||||
request.Header.Add("content-type", "application/json")
|
|
||||||
|
|
||||||
response, err := http.DefaultClient.Do(request)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return VivaDb{}, err
|
return database.PaymentEntry{}, err
|
||||||
}
|
}
|
||||||
if response.StatusCode == http.StatusOK {
|
entry.OrderId = &response.OrderId
|
||||||
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
|
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) {
|
func (s *Service) CompleteTransaction(entry database.PaymentEntry, amount int64) (database.PaymentEntry, error) {
|
||||||
id := uuid.Must(uuid.NewRandom())
|
completionRequest := TransactionCompleteRequest{
|
||||||
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,
|
Amount: amount,
|
||||||
CustomerDescription: "Example transaction",
|
CustomerDescription: "Example transaction",
|
||||||
}
|
}
|
||||||
content, err := json.Marshal(&completionRequest)
|
httpResponse, err := createRequest(
|
||||||
if err != nil {
|
"POST",
|
||||||
return VivaDb{}, err
|
"https://demo.vivapayments.com/api/transactions/"+entry.TransactionId.String(),
|
||||||
}
|
map[string]string{"authorization": "Bearer " + s.basicAuth(),
|
||||||
|
"content-type": "application/json",
|
||||||
request, err := http.NewRequest("POST", "https://demo.vivapayments.com/api/transactions/"+entry.TransactionId.String(), bytes.NewReader(content))
|
},
|
||||||
request.Header.Add("authorization", "Bearer "+s.BasicAuth())
|
toJson(completionRequest),
|
||||||
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" {
|
if err != nil {
|
||||||
entry.TotalAmount = int64(transactionResponse.Amount * 100)
|
return database.PaymentEntry{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var response TransactionResponse
|
||||||
|
err = readResponse(httpResponse, &response)
|
||||||
|
if err != nil {
|
||||||
|
return database.PaymentEntry{}, err
|
||||||
|
}
|
||||||
|
if response.StatusId == "F" {
|
||||||
|
paidAmount := int64(response.Amount * 100)
|
||||||
|
entry.Amount = &paidAmount
|
||||||
entry.State = state.StateCompleted
|
entry.State = state.StateCompleted
|
||||||
} else {
|
} else {
|
||||||
return VivaDb{}, errors.New("received invalid status = " + transactionResponse.StatusId)
|
return database.PaymentEntry{}, errors.New("received invalid status = " + response.StatusId)
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return VivaDb{}, errors.New("received wrong status, expected 200 received " + strconv.FormatInt(int64(response.StatusCode), 10))
|
|
||||||
}
|
}
|
||||||
return entry, nil
|
return entry, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) BasicAuth() string {
|
func (s *Service) CancelTransaction(entry database.PaymentEntry) (database.PaymentEntry, error) {
|
||||||
|
httpResponse, err := createRequest(
|
||||||
|
"DELETE",
|
||||||
|
"https://demo.vivapayments.com/api/transactions/"+entry.TransactionId.String(),
|
||||||
|
map[string]string{"authorization": "Bearer " + s.basicAuth(),
|
||||||
|
"content-type": "application/json",
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return database.PaymentEntry{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var response TransactionResponse
|
||||||
|
err = readResponse(httpResponse, &response)
|
||||||
|
if err != nil {
|
||||||
|
return database.PaymentEntry{}, err
|
||||||
|
}
|
||||||
|
if response.StatusId == "F" {
|
||||||
|
paidAmount := int64(0)
|
||||||
|
entry.Amount = &paidAmount
|
||||||
|
entry.State = state.StateVoided
|
||||||
|
} else {
|
||||||
|
return database.PaymentEntry{}, errors.New("received invalid status = " + response.StatusId)
|
||||||
|
}
|
||||||
|
return entry, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) oAuthToken() (string, error) {
|
||||||
|
if s.token != "" && s.expiration.After(time.Now()) {
|
||||||
|
return s.token, nil
|
||||||
|
}
|
||||||
|
return s.fetchOAuthToken()
|
||||||
|
}
|
||||||
|
|
||||||
|
func readResponse[T any](httpResponse *http.Response, response T) error {
|
||||||
|
if httpResponse.StatusCode == http.StatusOK {
|
||||||
|
content, err := io.ReadAll(httpResponse.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return json.Unmarshal(content, response)
|
||||||
|
} else {
|
||||||
|
return errors.New("received wrong status, expected 200 received " + strconv.FormatInt(int64(httpResponse.StatusCode), 10))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createRequest(method string, url string, headers map[string]string, content []byte) (*http.Response, error) {
|
||||||
|
request, err := http.NewRequest(method, url, bytes.NewReader(content))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for key, value := range headers {
|
||||||
|
request.Header.Add(key, value)
|
||||||
|
}
|
||||||
|
return http.DefaultClient.Do(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func toJson[T any](request T) []byte {
|
||||||
|
response, err := json.Marshal(request)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) fetchOAuthToken() (string, error) {
|
||||||
|
form := url.Values{
|
||||||
|
"grant_type": []string{"client_credentials"},
|
||||||
|
}
|
||||||
|
httpResponse, err := createRequest(
|
||||||
|
"POST",
|
||||||
|
"https://demo-accounts.vivapayments.com/connect/token",
|
||||||
|
map[string]string{"content-type": "application/x-www-form-urlencoded", "authorization": "Basic " + base64.StdEncoding.EncodeToString([]byte(s.ClientId+":"+s.ClientSecret))},
|
||||||
|
[]byte(form.Encode()),
|
||||||
|
)
|
||||||
|
var response OAuthResponse
|
||||||
|
|
||||||
|
err = readResponse(httpResponse, &response)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
s.token = response.AccessToken
|
||||||
|
s.expiration = time.Now().Add(time.Duration(response.ExpiresIn) * time.Second)
|
||||||
|
|
||||||
|
return s.token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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) CancelTransaction(entry VivaDb) (VivaDb, error) {
|
func (s *Service) HandleResponse(c *gin.Context, expectedState state.PaymentState) (string, error) {
|
||||||
request, err := http.NewRequest("DELETE", "https://demo.vivapayments.com/api/transactions/"+entry.TransactionId.String()+"?amount="+strconv.FormatInt(entry.TotalAmount, 10), bytes.NewReader([]byte{}))
|
transactionId := uuid.MustParse(c.Query("t"))
|
||||||
request.Header.Add("authorization", "Bearer "+s.BasicAuth())
|
orderId := database.OrderId(c.Query("s"))
|
||||||
|
lang := c.Query("lang")
|
||||||
|
eventId := c.Query("eventId")
|
||||||
|
eci := c.Query("eci")
|
||||||
|
|
||||||
response, err := http.DefaultClient.Do(request)
|
log.Printf("Received error response for viva payment %s", orderId)
|
||||||
|
entry, err := s.Provider.FetchByOrderId(orderId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return VivaDb{}, err
|
log.Printf("Couldn't find payment info for viva payment %s", orderId)
|
||||||
|
return "", err
|
||||||
}
|
}
|
||||||
if response.StatusCode == http.StatusOK {
|
|
||||||
transactionResponse := VivaTransactionResponse{}
|
entry.State = expectedState
|
||||||
content, err := io.ReadAll(response.Body)
|
entry.ECI = &eci
|
||||||
if err != nil {
|
entry.Lang = &lang
|
||||||
return VivaDb{}, err
|
entry.EventId = &eventId
|
||||||
|
entry.TransactionId = &transactionId
|
||||||
|
|
||||||
|
if _, err := s.Provider.UpdateEntry(entry); err != nil {
|
||||||
|
return "", err
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(content, &transactionResponse); err != nil {
|
|
||||||
return VivaDb{}, err
|
log.Printf("Viva payment %s received correctly, returning redirect", orderId)
|
||||||
} else {
|
return "/entries/" + entry.Id.String(), nil
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
23
viva/viva.go
23
viva/viva.go
|
@ -1,17 +1,8 @@
|
||||||
package viva
|
package viva
|
||||||
|
|
||||||
type OrderId string
|
import "payment-poc/database"
|
||||||
|
|
||||||
func (o OrderId) MarshalJSON() ([]byte, error) {
|
type OrderRequest struct {
|
||||||
return []byte(o), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *OrderId) UnmarshalJSON(value []byte) error {
|
|
||||||
*o = OrderId(value)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type VivaOrderRequest struct {
|
|
||||||
Amount int64 `json:"amount"`
|
Amount int64 `json:"amount"`
|
||||||
Description string `json:"customerTrns"`
|
Description string `json:"customerTrns"`
|
||||||
MerchantDescription string `json:"merchantTrns"`
|
MerchantDescription string `json:"merchantTrns"`
|
||||||
|
@ -20,21 +11,21 @@ type VivaOrderRequest struct {
|
||||||
Source string `json:"sourceCode"`
|
Source string `json:"sourceCode"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type VivaOrderResponse struct {
|
type OrderResponse struct {
|
||||||
OrderId OrderId `json:"orderCode"`
|
OrderId database.OrderId `json:"orderCode"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type VivaOAuthResponse struct {
|
type OAuthResponse struct {
|
||||||
AccessToken string `json:"access_token"`
|
AccessToken string `json:"access_token"`
|
||||||
ExpiresIn int `json:"expires_in"`
|
ExpiresIn int `json:"expires_in"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type VivaTransactionCompleteRequest struct {
|
type TransactionCompleteRequest struct {
|
||||||
Amount int64 `json:"amount"`
|
Amount int64 `json:"amount"`
|
||||||
CustomerDescription string `json:"customerTrns"`
|
CustomerDescription string `json:"customerTrns"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type VivaTransactionResponse struct {
|
type TransactionResponse struct {
|
||||||
Amount float64 `json:"Amount"`
|
Amount float64 `json:"Amount"`
|
||||||
StatusId string `json:"StatusId"`
|
StatusId string `json:"StatusId"`
|
||||||
ErrorCode int64 `json:"ErrorCode"`
|
ErrorCode int64 `json:"ErrorCode"`
|
||||||
|
|
225
wspay/service.go
225
wspay/service.go
|
@ -1,60 +1,186 @@
|
||||||
package wspay
|
package wspay
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"crypto/sha512"
|
"crypto/sha512"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/jmoiron/sqlx"
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"payment-poc/database"
|
||||||
"payment-poc/state"
|
"payment-poc/state"
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
DB *sqlx.DB
|
Provider *database.PaymentEntryProvider
|
||||||
|
ShopId string
|
||||||
|
ShopSecret string
|
||||||
|
BackendUrl string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) CreateEntry(shopId string, totalAmount int64) (WsPayDb, error) {
|
func (s *Service) CreatePaymentUrl(amount int64) (string, error) {
|
||||||
id := uuid.Must(uuid.NewRandom())
|
entry, err := s.Provider.CreateEntry(database.PaymentEntry{
|
||||||
entry := WsPayDb{
|
Gateway: state.GatewayVivaWallet,
|
||||||
Id: id,
|
|
||||||
ShopID: shopId,
|
|
||||||
ShoppingCartID: id.String(),
|
|
||||||
TotalAmount: totalAmount,
|
|
||||||
State: state.StateInitialized,
|
State: state.StateInitialized,
|
||||||
|
TotalAmount: amount,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
}
|
}
|
||||||
_, err := s.DB.Exec(`INSERT INTO "wspay" ("id", "shop_id", "shopping_card_id", "total_amount", "payment_state") VALUES ($1, $2, $3, $4, $5)`,
|
return "/wspay/initialize/" + entry.Id.String(), nil
|
||||||
&entry.Id, &entry.ShopID, &entry.ShoppingCartID, &entry.TotalAmount, &entry.State,
|
}
|
||||||
|
|
||||||
|
func (s *Service) CompleteTransaction(entry database.PaymentEntry, amount int64) (database.PaymentEntry, error) {
|
||||||
|
if entry.State == state.StateAccepted {
|
||||||
|
var request = CompletionRequest{
|
||||||
|
Version: "2.0",
|
||||||
|
WsPayOrderId: entry.Id.String(),
|
||||||
|
ShopId: s.ShopId,
|
||||||
|
ApprovalCode: *entry.ApprovalCode,
|
||||||
|
STAN: *entry.STAN,
|
||||||
|
Amount: amount,
|
||||||
|
Signature: CalculateCompletionSignature(s.ShopId, s.ShopSecret, entry.Id.String(), *entry.STAN, *entry.ApprovalCode, amount),
|
||||||
|
}
|
||||||
|
|
||||||
|
httpResponse, err := createRequest(
|
||||||
|
"POST",
|
||||||
|
"https://test.wspay.biz/api/services/completion",
|
||||||
|
map[string]string{"content-type": "application/json"},
|
||||||
|
toJson(request),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return WsPayDb{}, err
|
return database.PaymentEntry{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var response CompletionResponse
|
||||||
|
err = readResponse(httpResponse, &response)
|
||||||
|
if err != nil {
|
||||||
|
return database.PaymentEntry{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if CompareCompletionReturnSignature(response.Signature, s.ShopId, s.ShopSecret, entry.Id.String(), *entry.STAN, response.ActionSuccess, response.ApprovalCode) != nil {
|
||||||
|
entry.Amount = &amount
|
||||||
|
entry.State = state.StateCompleted
|
||||||
|
} else {
|
||||||
|
return database.PaymentEntry{}, errors.New("invalid signature")
|
||||||
|
}
|
||||||
|
return entry, nil
|
||||||
|
} else {
|
||||||
|
return database.PaymentEntry{}, errors.New("payment is in invalid state")
|
||||||
}
|
}
|
||||||
return s.FetchById(id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) FetchAll() ([]WsPayDb, error) {
|
func (s *Service) CancelTransaction(entry database.PaymentEntry) (database.PaymentEntry, error) {
|
||||||
var entries []WsPayDb
|
if entry.State == state.StateAccepted {
|
||||||
err := s.DB.Select(&entries, `SELECT * FROM "wspay"`)
|
var request = CompletionRequest{
|
||||||
return entries, err
|
Version: "2.0",
|
||||||
}
|
WsPayOrderId: entry.Id.String(),
|
||||||
|
ShopId: s.ShopId,
|
||||||
|
ApprovalCode: *entry.ApprovalCode,
|
||||||
|
STAN: *entry.STAN,
|
||||||
|
Amount: entry.TotalAmount,
|
||||||
|
Signature: CalculateCompletionSignature(s.ShopId, s.ShopSecret, entry.Id.String(), *entry.STAN, *entry.ApprovalCode, entry.TotalAmount),
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) FetchById(id uuid.UUID) (WsPayDb, error) {
|
httpResponse, err := createRequest(
|
||||||
entry := WsPayDb{}
|
"POST",
|
||||||
err := s.DB.Get(&entry, `SELECT * FROM "wspay" WHERE "id" = $1`, id)
|
"https://test.wspay.biz/api/services/void",
|
||||||
return entry, err
|
map[string]string{"content-type": "application/json"},
|
||||||
}
|
toJson(request),
|
||||||
|
|
||||||
func (s *Service) FetchByShoppingCartID(id string) (WsPayDb, error) {
|
|
||||||
entry := WsPayDb{}
|
|
||||||
err := s.DB.Get(&entry, `SELECT * FROM "wspay" WHERE "shopping_card_id" = $1`, id)
|
|
||||||
return entry, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) Update(entry WsPayDb) error {
|
|
||||||
_, err := s.DB.Exec(`UPDATE "wspay" set "lang" = $2, "customer_first_name" = $3, "customer_last_name" = $4, "customer_address" = $5, "customer_city" = $6, "customer_zip" = $7, "customer_country" = $8, "customer_phone" = $9, "payment_plan" = $10, "credit_card_name" = $11, "credit_card_number" = $12, "payment_method" = $13, "currency_code" = $14, "date_time" = $15, "eci" = $16, "stan" = $17, "success" = $18, "approval_code" = $19, "error_message" = $20, "error_codes" = $21, "payment_state" = $22 WHERE "id" = $1`,
|
|
||||||
&entry.Id, &entry.Lang, &entry.CustomerFirstName, &entry.CustomerLastName, &entry.CustomerAddress, &entry.CustomerCity, &entry.CustomerZIP, &entry.CustomerCountry, &entry.CustomerPhone, &entry.PaymentPlan, &entry.CreditCardName, &entry.CreditCardNumber, &entry.PaymentMethod, &entry.CurrencyCode, &entry.DateTime, &entry.ECI, &entry.STAN, &entry.Success, &entry.ApprovalCode, &entry.ErrorMessage, &entry.ErrorCodes, &entry.State,
|
|
||||||
)
|
)
|
||||||
return err
|
if err != nil {
|
||||||
|
return database.PaymentEntry{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var response CompletionResponse
|
||||||
|
err = readResponse(httpResponse, &response)
|
||||||
|
if err != nil {
|
||||||
|
return database.PaymentEntry{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if CompareCompletionReturnSignature(response.Signature, s.ShopId, s.ShopSecret, entry.Id.String(), *entry.STAN, response.ActionSuccess, response.ApprovalCode) != nil {
|
||||||
|
entry.State = state.StateCanceled
|
||||||
|
} else {
|
||||||
|
return database.PaymentEntry{}, errors.New("invalid signature")
|
||||||
|
}
|
||||||
|
return entry, nil
|
||||||
|
} else {
|
||||||
|
return database.PaymentEntry{}, errors.New("payment is in invalid state")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) InitializePayment(entry database.PaymentEntry) Form {
|
||||||
|
form := Form{
|
||||||
|
ShopID: s.ShopId,
|
||||||
|
ShoppingCartID: entry.Id.String(),
|
||||||
|
Version: "2.0",
|
||||||
|
TotalAmount: entry.TotalAmount,
|
||||||
|
ReturnURL: s.BackendUrl + "/wspay/success",
|
||||||
|
ReturnErrorURL: s.BackendUrl + "/wspay/error",
|
||||||
|
CancelURL: s.BackendUrl + "/wspay/cancel",
|
||||||
|
Signature: CalculateFormSignature(s.ShopId, s.ShopSecret, entry.Id.String(), entry.TotalAmount),
|
||||||
|
}
|
||||||
|
return form
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) HandleSuccessResponse(c *gin.Context) (string, error) {
|
||||||
|
response := FormReturn{}
|
||||||
|
if err := c.ShouldBind(&response); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
entry, err := s.Provider.FetchById(uuid.MustParse(response.ShoppingCartID))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := CompareFormReturnSignature(response.Signature, s.ShopId, s.ShopSecret, response.ShoppingCartID, response.Success, response.ApprovalCode); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.Lang = &response.Lang
|
||||||
|
entry.ECI = &response.ECI
|
||||||
|
entry.STAN = &response.STAN
|
||||||
|
entry.Success = &response.Success
|
||||||
|
entry.ApprovalCode = &response.ApprovalCode
|
||||||
|
entry.State = state.StateAccepted
|
||||||
|
|
||||||
|
if _, err := s.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) {
|
||||||
|
response := FormError{}
|
||||||
|
if err := c.ShouldBind(&response); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
entry, err := s.Provider.FetchById(uuid.MustParse(response.ShoppingCartID))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := CompareFormReturnSignature(response.Signature, s.ShopId, s.ShopSecret, response.ShoppingCartID, response.Success, response.ApprovalCode); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.Lang = &response.Lang
|
||||||
|
entry.ECI = &response.ECI
|
||||||
|
entry.Success = &response.Success
|
||||||
|
entry.ApprovalCode = &response.ApprovalCode
|
||||||
|
entry.Error = &response.ErrorMessage
|
||||||
|
entry.State = paymentState
|
||||||
|
|
||||||
|
if _, err := s.Provider.UpdateEntry(entry); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return "/entries/" + entry.Id.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CalculateFormSignature(shopId string, secret string, cartId string, amount int64) string {
|
func CalculateFormSignature(shopId string, secret string, cartId string, amount int64) string {
|
||||||
|
@ -141,3 +267,34 @@ func CompareCompletionReturnSignature(signature string, shopId string, secret st
|
||||||
return errors.New("signature mismatch")
|
return errors.New("signature mismatch")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func readResponse[T any](httpResponse *http.Response, response T) error {
|
||||||
|
if httpResponse.StatusCode == http.StatusOK {
|
||||||
|
content, err := io.ReadAll(httpResponse.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return json.Unmarshal(content, response)
|
||||||
|
} else {
|
||||||
|
return errors.New("received wrong status, expected 200 received " + strconv.FormatInt(int64(httpResponse.StatusCode), 10))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createRequest(method string, url string, headers map[string]string, content []byte) (*http.Response, error) {
|
||||||
|
request, err := http.NewRequest(method, url, bytes.NewReader(content))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for key, value := range headers {
|
||||||
|
request.Header.Add(key, value)
|
||||||
|
}
|
||||||
|
return http.DefaultClient.Do(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func toJson[T any](request T) []byte {
|
||||||
|
response, err := json.Marshal(request)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ package wspay
|
||||||
|
|
||||||
const AuthorisationForm = "https://formtest.wspay.biz/authorization.aspx"
|
const AuthorisationForm = "https://formtest.wspay.biz/authorization.aspx"
|
||||||
|
|
||||||
type WsPayForm struct {
|
type Form struct {
|
||||||
// required args
|
// required args
|
||||||
ShopID string
|
ShopID string
|
||||||
ShoppingCartID string
|
ShoppingCartID string
|
||||||
|
@ -31,7 +31,7 @@ type WsPayForm struct {
|
||||||
CurrencyCode int
|
CurrencyCode int
|
||||||
}
|
}
|
||||||
|
|
||||||
type WsPayFormReturn struct {
|
type FormReturn struct {
|
||||||
CustomerFirstName string `form:"CustomerFirstname"`
|
CustomerFirstName string `form:"CustomerFirstname"`
|
||||||
CustomerSurname string `form:"CustomerSurname"`
|
CustomerSurname string `form:"CustomerSurname"`
|
||||||
CustomerAddress string `form:"CustomerAddress"`
|
CustomerAddress string `form:"CustomerAddress"`
|
||||||
|
@ -61,7 +61,7 @@ type WsPayFormReturn struct {
|
||||||
Signature string `form:"Signature"`
|
Signature string `form:"Signature"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type WsPayFormError struct {
|
type FormError struct {
|
||||||
CustomerFirstName string
|
CustomerFirstName string
|
||||||
CustomerSurname string
|
CustomerSurname string
|
||||||
CustomerAddress string
|
CustomerAddress string
|
||||||
|
@ -88,7 +88,7 @@ type WsPayFormError struct {
|
||||||
Signature string
|
Signature string
|
||||||
}
|
}
|
||||||
|
|
||||||
type WsPayFormCancel struct {
|
type FormCancel struct {
|
||||||
ResponseCode int
|
ResponseCode int
|
||||||
ShoppingCartID string
|
ShoppingCartID string
|
||||||
ApprovalCode string
|
ApprovalCode string
|
||||||
|
@ -96,7 +96,7 @@ type WsPayFormCancel struct {
|
||||||
Signature string
|
Signature string
|
||||||
}
|
}
|
||||||
|
|
||||||
type WsPayCompletionRequest struct {
|
type CompletionRequest struct {
|
||||||
Version string
|
Version string
|
||||||
WsPayOrderId string
|
WsPayOrderId string
|
||||||
ShopId string
|
ShopId string
|
||||||
|
@ -106,7 +106,7 @@ type WsPayCompletionRequest struct {
|
||||||
Signature string
|
Signature string
|
||||||
}
|
}
|
||||||
|
|
||||||
type WsPayCompletionResponse struct {
|
type CompletionResponse struct {
|
||||||
WsPayOrderId string
|
WsPayOrderId string
|
||||||
ShopId string
|
ShopId string
|
||||||
ApprovalCode string
|
ApprovalCode string
|
||||||
|
@ -116,14 +116,14 @@ type WsPayCompletionResponse struct {
|
||||||
Signature string
|
Signature string
|
||||||
}
|
}
|
||||||
|
|
||||||
type WsPayStatusCheckRequest struct {
|
type StatusCheckRequest struct {
|
||||||
Version string
|
Version string
|
||||||
ShopId string
|
ShopId string
|
||||||
ShoppingCartId string
|
ShoppingCartId string
|
||||||
Signature string
|
Signature string
|
||||||
}
|
}
|
||||||
|
|
||||||
type WsPayStatusCheckResponse struct {
|
type StatusCheckResponse struct {
|
||||||
WsPayOrderId string
|
WsPayOrderId string
|
||||||
Signature string
|
Signature string
|
||||||
STAN string
|
STAN string
|
||||||
|
|
Loading…
Reference in New Issue