Compare commits

..

No commits in common. "7b2523e2b15fd138baf1394172d2fe426e18e1f8" and "b4b0396b3000d6322a621113c936c102939ab9a0" have entirely different histories.

27 changed files with 1518 additions and 1541 deletions

View File

@ -1,54 +0,0 @@
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_id"`
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
}

View File

@ -1,57 +0,0 @@
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") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)`,
&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 = &currentTime
_, 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
}

View File

@ -1,30 +1,65 @@
CREATE TABLE IF NOT EXISTS "payment_entry"
CREATE TABLE IF NOT EXISTS "wspay"
( (
"id" uuid NOT NULL, "id" uuid NOT NULL,
"created" timestamp NOT NULL, "shop_id" varchar(128) NOT NULL,
"modified" timestamp DEFAULT NULL, "shopping_card_id" varchar(128) NOT NULL,
"gateway" varchar(255) NOT NULL,
"state" varchar(255) NOT NULL,
"lang" varchar(16) DEFAULT NULL,
"error" varchar(255) DEFAULT NULL,
"amount" int DEFAULT NULL,
"total_amount" int NOT NULL, "total_amount" int NOT NULL,
"eci" varchar(255) DEFAULT NULL, "lang" varchar(128) DEFAULT '',
"payment_intent_id" varchar(255) DEFAULT NULL, "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 '',
"shopping_card_id" varchar(255) DEFAULT NULL, "payment_plan" varchar(128) DEFAULT '',
"stan" varchar(255) DEFAULT NULL, "credit_card_name" varchar(128) DEFAULT '',
"success" int DEFAULT NULL, "credit_card_number" varchar(128) DEFAULT '',
"approval_code" varchar(255) DEFAULT NULL, "payment_method" varchar(128) DEFAULT '',
"currency_code" int DEFAULT 0,
"order_id" varchar(255) DEFAULT NULL, "date_time" timestamp DEFAULT current_timestamp,
"transaction_id" uuid DEFAULT NULL,
"event_id" varchar(255) DEFAULT NULL, "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) PRIMARY KEY (id)
); );

View File

@ -0,0 +1 @@
package prod

995
main.go

File diff suppressed because it is too large Load Diff

View File

@ -1,44 +0,0 @@
package mock
import (
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"log"
"payment-poc/database"
"payment-poc/state"
)
type Service struct {
BackendUrl string
}
func (s Service) UpdatePayment(entry database.PaymentEntry) (updatedEntry database.PaymentEntry, err error) {
return entry, nil
}
func (s Service) CreatePaymentUrl(entry database.PaymentEntry) (updateEntry database.PaymentEntry, url string, err error) {
return entry, "/mock/gateway/" + entry.Id.String(), nil
}
func (s Service) CompleteTransaction(entry database.PaymentEntry, amount int64) (database.PaymentEntry, error) {
entry.Amount = &amount
entry.State = state.StateCompleted
return entry, nil
}
func (s Service) CancelTransaction(entry database.PaymentEntry) (database.PaymentEntry, error) {
entry.State = state.StateVoided
return entry, nil
}
func (s Service) HandleResponse(c *gin.Context, provider *database.PaymentEntryProvider, paymentState state.PaymentState) (string, error) {
id := uuid.MustParse(c.Query("id"))
entry, err := provider.FetchById(id)
if err != nil {
return "", err
}
entry.State = paymentState
_, err = provider.UpdateEntry(entry)
log.Printf("[%s:%s] received authorization response", entry.Id.String(), entry.State)
return "/entries/" + id.String(), err
}

View File

@ -1,143 +0,0 @@
package stripe
import (
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"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"
)
type Service struct {
ApiKey string
BackendUrl string
}
func (s *Service) UpdatePayment(entry database.PaymentEntry) (updatedEntry database.PaymentEntry, err error) {
pi, err := paymentintent.Get(*entry.PaymentIntentId, nil)
if err != nil {
return entry, err
}
newState := determineState(pi.Status)
if entry.State != newState && newState != "" {
log.Printf("[%s] updated state for %s -> %s", entry.Id.String(), entry.State, newState)
if pi.AmountReceived > 0 {
entry.Amount = &pi.AmountReceived
}
entry.State = newState
}
return entry, nil
}
func determineState(status stripe.PaymentIntentStatus) state.PaymentState {
switch status {
case stripe.PaymentIntentStatusCanceled:
return state.StateCanceled
case stripe.PaymentIntentStatusProcessing:
return state.StatePending
case stripe.PaymentIntentStatusRequiresAction:
return state.StatePending
case stripe.PaymentIntentStatusRequiresCapture:
return state.StateAccepted
case stripe.PaymentIntentStatusRequiresConfirmation:
return state.StatePending
case stripe.PaymentIntentStatusRequiresPaymentMethod:
return state.StateVoided
case stripe.PaymentIntentStatusSucceeded:
return state.StateCompleted
}
return ""
}
func (s *Service) CreatePaymentUrl(entry database.PaymentEntry) (database.PaymentEntry, string, error) {
entry, url, err := s.InitializePayment(entry)
if err != nil {
return entry, "", err
}
return entry, url, nil
}
func (s *Service) InitializePayment(entry database.PaymentEntry) (database.PaymentEntry, string, error) {
currency := string(stripe.CurrencyEUR)
productName := "Example product"
productDescription := "Simple example product"
params := &stripe.CheckoutSessionParams{
LineItems: []*stripe.CheckoutSessionLineItemParams{
{
PriceData: &stripe.CheckoutSessionLineItemPriceDataParams{
Currency: &currency,
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.State = state.StateInitialized
entry.PaymentIntentId = &result.PaymentIntent.ID
return entry, result.URL, nil
}
func (s *Service) CompleteTransaction(entry database.PaymentEntry, amount int64) (database.PaymentEntry, error) {
params := &stripe.PaymentIntentCaptureParams{
AmountToCapture: stripe.Int64(amount),
}
pi, err := paymentintent.Capture(*entry.PaymentIntentId, params)
if err != nil {
return database.PaymentEntry{}, err
}
log.Printf("received state on completion: %v", pi.Status)
newState := determineState(pi.Status)
entry.State = newState
if newState == state.StateCompleted || newState == state.StatePending {
entry.Amount = &pi.AmountReceived
}
return entry, nil
}
func (s *Service) CancelTransaction(entry database.PaymentEntry) (database.PaymentEntry, error) {
params := &stripe.PaymentIntentCancelParams{}
pi, err := paymentintent.Cancel(*entry.PaymentIntentId, params)
if err != nil {
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, provider *database.PaymentEntryProvider, paymentState state.PaymentState) (string, error) {
id := uuid.MustParse(c.Query("token"))
entry, err := provider.FetchById(id)
if err != nil {
return "", err
}
entry.State = paymentState
if _, err := provider.UpdateEntry(entry); err != nil {
return "", err
}
log.Printf("[%s:%s] received authorization response", entry.Id.String(), entry.State)
return "/entries/" + entry.Id.String(), nil
}

View File

@ -1,271 +0,0 @@
package viva
import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"io"
"log"
"net/http"
"net/url"
"payment-poc/database"
"payment-poc/state"
"strconv"
"time"
)
type Service struct {
ClientId string
ClientSecret string
SourceCode string
MerchantId string
ApiKey string
token string
expiration time.Time
}
func (s *Service) UpdatePayment(entry database.PaymentEntry) (updatedEntry database.PaymentEntry, err error) {
token, err := s.oAuthToken()
httpResponse, err := createRequest(
"GET",
"https://demo-api.vivapayments.com/checkout/v2/transactions/"+entry.TransactionId.String(),
map[string]string{"authorization": "Bearer " + token, "content-type": "application/json"},
[]byte{},
)
if err != nil {
return database.PaymentEntry{}, err
}
var response TransactionStatusResponse
err = readResponse(httpResponse, &response)
if err != nil {
return database.PaymentEntry{}, err
}
newState := determineStatus(response.StatusId)
if entry.State != newState && newState != "" {
log.Printf("[%s:%s] updated state %s -> %s", entry.Id.String(), entry.State, entry.State, newState)
entry.State = newState
}
return entry, nil
}
func determineStatus(id TransactionStatus) state.PaymentState {
switch id {
case PaymentPreauthorized:
return state.StateAccepted
case PaymentPending:
return state.StatePending
case PaymentSuccessful:
return state.StateCompleted
case PaymentUnsuccessful:
return state.StateError
case PaymentRefunded:
return state.StateVoided
case PaymentVoided:
return state.StateVoided
}
log.Printf("Unknonw transactionStatus: %s", string(id))
return ""
}
func (s *Service) CreatePaymentUrl(entry database.PaymentEntry) (database.PaymentEntry, string, error) {
entry, err := s.InitializePayment(entry)
if err != nil {
return entry, "", err
}
return entry, "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,
Description: "Example payment",
MerchantDescription: "Example payment",
PreAuth: true,
AllowRecurring: false,
Source: s.SourceCode,
}
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 {
return database.PaymentEntry{}, err
}
var response OrderResponse
err = readResponse(httpResponse, &response)
if err != nil {
return database.PaymentEntry{}, err
}
entry.State = state.StateInitialized
entry.OrderId = &response.OrderId
return entry, nil
}
func (s *Service) CompleteTransaction(entry database.PaymentEntry, amount int64) (database.PaymentEntry, error) {
completionRequest := TransactionCompleteRequest{
Amount: amount,
CustomerDescription: "Example transaction",
}
httpResponse, err := createRequest(
"POST",
"https://demo.vivapayments.com/api/transactions/"+entry.TransactionId.String(),
map[string]string{"authorization": "Bearer " + s.basicAuth(),
"content-type": "application/json",
},
toJson(completionRequest),
)
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(response.Amount * 100)
entry.Amount = &paidAmount
entry.State = state.StateCompleted
} else {
return database.PaymentEntry{}, errors.New("received invalid status = " + response.StatusId)
}
return entry, nil
}
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))
}
func (s *Service) HandleResponse(c *gin.Context, provider *database.PaymentEntryProvider, state state.PaymentState) (string, error) {
transactionId := uuid.MustParse(c.Query("t"))
orderId := database.OrderId(c.Query("s"))
lang := c.Query("lang")
eventId := c.Query("eventId")
eci := c.Query("eci")
log.Printf("[%s] received error response for viva payment", orderId)
entry, err := provider.FetchByOrderId(orderId)
if err != nil {
log.Printf("[%s] couldn't find payment info for viva payment", orderId)
return "", err
}
entry.State = state
entry.ECI = &eci
entry.Lang = &lang
entry.EventId = &eventId
entry.TransactionId = &transactionId
if _, err := provider.UpdateEntry(entry); err != nil {
return "", err
}
log.Printf("[%s:%s] received authorization response", entry.Id.String(), entry.State)
return "/entries/" + entry.Id.String(), nil
}

View File

@ -1,55 +0,0 @@
package viva
import "payment-poc/database"
type OrderRequest 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 OrderResponse struct {
OrderId database.OrderId `json:"orderCode"`
}
type OAuthResponse struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
}
type TransactionCompleteRequest struct {
Amount int64 `json:"amount"`
CustomerDescription string `json:"customerTrns"`
}
type TransactionResponse 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"`
}
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"`
}

View File

@ -1,386 +0,0 @@
package wspay
import (
"bytes"
"crypto/sha512"
"encoding/hex"
"encoding/json"
"errors"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"io"
"log"
"net/http"
"payment-poc/database"
"payment-poc/state"
"strconv"
)
type Service struct {
ShopId string
ShopSecret string
BackendUrl string
}
func (s *Service) UpdatePayment(entry database.PaymentEntry) (updatedEntry database.PaymentEntry, err error) {
var request = StatusCheckRequest{
Version: "2.0",
ShopId: s.ShopId,
ShoppingCartId: entry.Id.String(),
Signature: CalculateStatusCheckSignature(s.ShopId, s.ShopSecret, entry.Id.String()),
}
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) {
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 {
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")
}
}
func (s *Service) CancelTransaction(entry database.PaymentEntry) (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: entry.TotalAmount,
Signature: CalculateCompletionSignature(s.ShopId, s.ShopSecret, entry.Id.String(), *entry.STAN, *entry.ApprovalCode, entry.TotalAmount),
}
httpResponse, err := createRequest(
"POST",
"https://test.wspay.biz/api/services/void",
map[string]string{"content-type": "application/json"},
toJson(request),
)
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, provider *database.PaymentEntryProvider) (string, error) {
response := FormReturn{}
if err := c.ShouldBind(&response); err != nil {
return "", err
}
entry, err := 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 := provider.UpdateEntry(entry); err != nil {
return "", err
}
return "/entries/" + entry.Id.String(), nil
}
func (s *Service) HandleErrorResponse(c *gin.Context, provider *database.PaymentEntryProvider, paymentState state.PaymentState) (string, error) {
response := FormError{}
if err := c.ShouldBind(&response); err != nil {
return "", err
}
entry, err := 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 := provider.UpdateEntry(entry); err != nil {
return "", err
}
return "/entries/" + entry.Id.String(), nil
}
func CalculateFormSignature(shopId string, secret string, cartId string, amount int64) string {
/**
Represents a signature created from string formatted from following values in a following order using
SHA512 algorithm:
ShopID
SecretKey
ShoppingCartID
SecretKey
TotalAmount
SecretKey
*/
signature := shopId + secret + cartId + secret + strconv.FormatInt(amount, 10) + secret
hash := sha512.New()
hash.Write([]byte(signature))
return hex.EncodeToString(hash.Sum(nil))
}
func CalculateCompletionSignature(shopId string, secret string, cartId string, stan string, approvalCode string, amount int64) string {
/**
Represents a signature created from string formatted from following values in a following order using
SHA512 algorithm:
ShopID
WsPayOrderId
SecretKey
STAN
SecretKey
ApprovalCode
SecretKey
Amount
SecretKey
WsPayOrderId
*/
signature := shopId + cartId + secret + stan + secret + approvalCode + secret + strconv.FormatInt(amount, 10) + secret + cartId
hash := sha512.New()
hash.Write([]byte(signature))
return hex.EncodeToString(hash.Sum(nil))
}
func CompareFormReturnSignature(signature string, shopId string, secret string, cartId string, success int, approvalCode string) error {
/**
Represents a signature created from string formatted from following values in a following order using
SHA512 algorithm:
ShopID
SecretKey
ShoppingCartID
SecretKey
Success
SecretKey
ApprovalCode
SecretKey
Merchant should validate this signature to make sure that the request is originating from WSPayForm.
*/
calculatedSignature := shopId + secret + cartId + secret + strconv.FormatInt(int64(success), 10) + secret + approvalCode + secret
hash := sha512.New()
hash.Write([]byte(calculatedSignature))
if hex.EncodeToString(hash.Sum(nil)) == signature {
return nil
} else {
return errors.New("signature mismatch")
}
}
func CompareCompletionReturnSignature(signature string, shopId string, secret string, stan 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
STAN
ActionSuccess
SecretKey
ApprovalCode
WsPayOrderId
Merchant should validate this signature to make sure that the request is originating from WSPayForm.
*/
calculatedSignature := shopId + secret + stan + actionSuccess + secret + 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 CompareStatusCheckReturnSignature(signature string, shopId string, secret string, actionSuccess string, approvalCode string, cartId string) error {
/**
Represents a signature created from string formatted from following values in a following order using
SHA512 algorithm:
ShopID
SecretKey
ActionSuccess
ApprovalCode
SecretKey
ShopID
ApprovalCode
WsPayOrderId
*/
calculatedSignature := shopId + secret + actionSuccess + approvalCode + secret + shopId + approvalCode + cartId
hash := sha512.New()
hash.Write([]byte(calculatedSignature))
if hex.EncodeToString(hash.Sum(nil)) == signature {
return nil
} else {
return errors.New("signature mismatch")
}
}
func CalculateStatusCheckSignature(shopId string, secret string, cartId string) string {
/**
Represents a signature created from string formatted from following values in a following order using
SHA512 algorithm:
ShopID
SecretKey
ShoppingCartID
SecretKey
ShopID
ShoppingCartID
*/
signature := shopId + secret + cartId + secret + shopId + cartId
hash := sha512.New()
hash.Write([]byte(signature))
return hex.EncodeToString(hash.Sum(nil))
}
func readResponse[T any](httpResponse *http.Response, response T) error {
if httpResponse.StatusCode == http.StatusOK {
content, err := io.ReadAll(httpResponse.Body)
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
}

View File

@ -3,14 +3,9 @@ package state
type PaymentState string type PaymentState string
const ( const (
// initial state
StatePreinitialized PaymentState = "preinitialized"
// 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"
@ -21,12 +16,3 @@ 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"
GatewayMock PaymentGateway = "mock"
)

16
stripe/model.go Normal file
View File

@ -0,0 +1,16 @@
package stripe
import (
"github.com/google/uuid"
"payment-poc/state"
)
type StripeDb struct {
Id uuid.UUID `db:"id"`
TotalAmount int64 `db:"total_amount"`
Lang string `db:"lang"`
PaymentIntentId string `db:"payment_intent_id"`
State state.PaymentState `db:"payment_state"`
}

46
stripe/service.go Normal file
View File

@ -0,0 +1,46 @@
package stripe
import (
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"payment-poc/state"
)
type Service struct {
DB *sqlx.DB
}
func (s *Service) CreateEntry(totalAmount int64) (StripeDb, error) {
id := uuid.Must(uuid.NewRandom())
entry := StripeDb{
Id: id,
TotalAmount: totalAmount,
State: state.StateInitialized,
}
_, 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 {
return StripeDb{}, err
}
return s.FetchById(id)
}
func (s *Service) FetchAll() ([]StripeDb, error) {
var entries []StripeDb
err := s.DB.Select(&entries, `SELECT * FROM "stripe"`)
return entries, err
}
func (s *Service) FetchById(id uuid.UUID) (StripeDb, error) {
entry := StripeDb{}
err := s.DB.Get(&entry, `SELECT * FROM "stripe" WHERE "id" = $1`, id)
return entry, err
}
func (s *Service) Update(entry StripeDb) error {
_, err := s.DB.Exec(`UPDATE "stripe" set "payment_intent_id" = $2, "payment_state" = $3 WHERE "id" = $1`,
&entry.Id, &entry.PaymentIntentId, &entry.State,
)
return err
}

View File

@ -5,23 +5,31 @@
<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>Index</title> <title>Obrada odgovora</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>
th {text-align: left}
h2 {
margin-top: 16px;
}
</style>
</head> </head>
<body> <body class="container">
<!-- As a link -->
<nav class="navbar navbar-dark bg-dark"> <p>Obrada odgovora...</p>
<section class="container"> <div id="error"></div>
<a class="navbar-brand" href="/">Payment-poc</a>
</section> <script>
</nav> window.onload = () => {
setTimeout(() => {
window?.top?.postMessage(JSON.stringify({"success": true}), {targetOrigin: "*"});
if(window?.top) {
document.querySelector("#error").innerHTML = `<p>Izgleda da je došlo do greške, jer stranica nije otvorena u iframe-u</p><a href="/">Stisnite ovdje da se vratite na naslovnicu</a>`;
}
}, 2000);
}
</script>
<section class="container">
<h2>Mock gateway {{.Entry.Id.String}}</h2>
<p>{{formatCurrency .Entry.TotalAmount}}</p>
<a href="/providers/mock/success?id={{.Entry.Id.String}}" class="btn btn-success">Potvrdi plaćanje</a>
<a href="/providers/mock/error?id={{.Entry.Id.String}}" class="btn btn-danger">Otkaži plaćanje</a>
</section>
</body> </body>
</html> </html>

View File

@ -10,10 +10,10 @@
<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>
th {text-align: left} th {text-align: left}
tr > td:nth-child(3) { tr > td:nth-child(2) {
text-align: right; text-align: right;
} }
tr > th:nth-child(3) { tr > th:nth-child(2) {
text-align: right; text-align: right;
} }
td, th { td, th {
@ -24,41 +24,71 @@
} }
</style> </style>
</head> </head>
<body> <body class="container">
<!-- As a link --> <h2>Novo plačanje</h2>
<nav class="navbar navbar-dark bg-dark">
<section class="container">
<a class="navbar-brand" href="/">Payment-poc</a>
</section>
</nav>
<section class="container">
<h2>Novo plaćanje</h2>
<form method="get" action="/methods"> <form method="get" action="/methods">
<div class="mb-3"> <div class="mb-3">
<label class="form-label" for="amount">Vrijednost</label> <label class="form-label" for="amount">Vrijednost</label>
<input class="form-control" id="amount" required name="amount" type="number" step="0.01" min="0"> <input class="form-control" id="amount" required name="amount" type="number" step="0.01" min="0">
</div> </div>
<button class="btn btn-primary" type="submit">Izradi novo plaćanje</button> <button class="btn btn-primary" type="submit">Izradi novo plačanje</button>
</form> </form>
<div> <div>
<h2>Entries</h2> <h2>WsPay</h2>
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<th>Id</th> <th>Id</th>
<th>Gateway</th>
<th>Vrijednost</th> <th>Vrijednost</th>
<th>Stanje</th> <th>Stanje</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{{range .Entries}} {{range .WsPay}}
<tr> <tr>
<td><a class="link-primary" href="/entries/{{.Id}}">{{.Id}}</a></td> <td><a class="link-primary" href="/wspay/info/{{.Id}}">{{.Id}}</a></td>
<td>{{.Gateway}}</td> <td>{{formatCurrency .TotalAmount}}</td>
<td>{{formatState .State}}</td>
</tr>
{{end}}
</tbody>
</table>
<h2>Stripe</h2>
<table class="table">
<thead>
<tr>
<th>Id</th>
<th>Vrijednost</th>
<th>Stanje</th>
</tr>
</thead>
<tbody>
{{range .Stripe}}
<tr>
<td><a class="link-primary" href="/stripe/info/{{.Id}}">{{.Id}}</a></td>
<td>{{formatCurrency .TotalAmount}}</td>
<td>{{formatState .State}}</td>
</tr>
{{end}}
</tbody>
</table>
<h2>Viva</h2>
<table class="table">
<thead>
<tr>
<th>Id</th>
<th>Vrijednost</th>
<th>Stanje</th>
</tr>
</thead>
<tbody>
{{range .Viva}}
<tr>
<td><a class="link-primary" href="/viva/info/{{.Id}}">{{.Id}}</a></td>
<td>{{formatCurrency .TotalAmount}}</td> <td>{{formatCurrency .TotalAmount}}</td>
<td>{{formatState .State}}</td> <td>{{formatState .State}}</td>
</tr> </tr>
@ -66,6 +96,5 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</section>
</body> </body>
</html> </html>

View File

@ -1,79 +0,0 @@
<!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>
<style>
th {text-align: left}
h2 {
margin-top: 16px;
}
</style>
</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>Plaćanje {{.Entry.Id}}</h2>
{{if not (eq .Entry.State "preinitialized")}}
<form method="post" action="/entries/{{.Entry.Id}}/refresh">
<button class="btn btn-primary">Ažuriraj</button>
</form>
{{end}}
<table class="table">
<tr><th>Id: </th><td>{{.Entry.Id}}</td></tr>
<tr><th>Datum izrade: </th><td>{{.Entry.Created.Format "Jan 02, 2006 15:04:05 UTC"}}</td></tr>
<tr><th>Zadnja izmjena: </th><td>{{or (.Entry.Modified.Format "Jan 02, 2006 15:04:05 UTC") .Entry.Created.Format "Jan 02, 2006 15:04:05 UTC"}}</td></tr>
<tr><th>Gateway: </th><td>{{.Entry.Gateway}}</td></tr>
<tr><th>Naplaćena vrijednost: </th><td>{{formatCurrencyPtr .Entry.Amount}}</td></tr>
<tr><th>Ukupna vrijednost: </th><td>{{formatCurrency .Entry.TotalAmount}}</td></tr>
<tr><th>Jezik: </th><td>{{or .Entry.Lang "-"}}</td></tr>
<tr><th>Greške: </th><td>{{or .Entry.Error "-"}}</td></tr>
<tr><th>Stanje: </th><td>{{formatState .Entry.State}}</td></tr>
{{if eq .Entry.Gateway "wspay"}}
<tr><th>WsPay</th><td></td></tr>
<tr><th>Success: </th><td>{{or .Entry.Success "-"}}</td></tr>
{{end}}
{{if eq .Entry.Gateway "stripe"}}
<tr><th>Stripe</th><td></td></tr>
<tr><th>Payment intent ID: </th><td>{{or .Entry.PaymentIntentId "-"}}</td></tr>
{{end}}
{{if eq .Entry.Gateway "viva-wallet"}}
<tr><th>Viva wallet</th><td></td></tr>
<tr><th>Order ID: </th><td>{{or .Entry.OrderId "-"}}</td></tr>
<tr><th>Transaction ID: </th><td>{{or .Entry.TransactionId "-"}}</td></tr>
<tr><th>Event ID: </th><td>{{or .Entry.EventId "-"}}</td></tr>
{{end}}
</table>
{{if eq .Entry.State "accepted"}}
<form class="mb-3" method="post" action="/entries/{{.Entry.Id}}/complete">
<div class="mb-3">
<label class="form-label" for="amount">Završi plaćanje</label>
<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>
<button class="btn btn-primary" type="submit">Završi plaćanje</button>
</form>
<form method="post" action="/entries/{{.Entry.Id}}/cancel">
<button class="btn btn-primary" type="submit">Otkaži plaćanje</button>
</form>
{{end}}
</section>
</body>
</html>

View File

@ -21,19 +21,10 @@
} }
</style> </style>
</head> </head>
<body> <body class="container">
<!-- As a link --> <h2>Izaberi metodu plačanja</h2>
<nav class="navbar navbar-dark bg-dark"> <a class="btn btn-success" href="/wspay?amount={{.Amount}}">WsPay</a>
<section class="container"> <a class="btn btn-success" href="/stripe?amount={{.Amount}}">Stripe</a>
<a class="navbar-brand" href="/">Payment-poc</a> <a class="btn btn-success" href="/viva?amount={{.Amount}}">Viva</a>
</section>
</nav>
<section class="container">
<h2>Izaberi metodu plaćanja</h2>
{{ range $key, $value := .Gateways }}
<a class="btn btn-success" href="/methods/{{$key}}?amount={{$.Amount}}">{{$value}}</a>
{{end}}
</section>
</body> </body>
</html> </html>

View File

@ -0,0 +1,42 @@
<!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>Info</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
<style>
th {text-align: left}
h2 {
margin-top: 16px;
}
</style>
</head>
<body class="container">
<h2>Plačanje {{.Entry.Id}}</h2>
<table class="table">
<tr><th>Id: </th><td>{{.Entry.Id}}</td></tr>
<tr><th>Ukupna vrijednost: </th><td>{{formatCurrency .Entry.TotalAmount}}</td></tr>
<tr><th>Jezik: </th><td>{{omitempty .Entry.Lang}}</td></tr>
<tr><th>Stanje: </th><td>{{formatState .Entry.State}}</td></tr>
</table>
{{if eq .Entry.State "accepted"}}
<form class="mb-3" method="post" action="/stripe/complete/{{.Entry.Id}}">
<div class="mb-3">
<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}}">
</div>
<button class="btn btn-primary" type="submit">Završi transakciju</button>
</form>
<form method="post" action="/stripe/cancel/{{.Entry.Id}}">
<button class="btn btn-primary" type="submit">Otkaži transakciju</button>
</form>
{{end}}
</body>
</html>

View File

@ -0,0 +1,44 @@
<!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>Info</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
<style>
th {text-align: left}
h2 {
margin-top: 16px;
}
</style>
</head>
<body class="container">
<h2>Plačanje {{.Entry.Id}}</h2>
<table class="table">
<tr><th>Id: </th><td>{{.Entry.Id}}</td></tr>
<tr><th>Order id: </th><td>{{.Entry.OrderId}}</td></tr>
<tr><th>Transaction id: </th><td>{{.Entry.TransactionId.String}}</td></tr>
<tr><th>Ukupna vrijednost: </th><td>{{formatCurrency .Entry.TotalAmount}}</td></tr>
<tr><th>Jezik: </th><td>{{omitempty .Entry.Lang}}</td></tr>
<tr><th>Događaj: </th><td>{{.Entry.EventId}}</td></tr>
<tr><th>Stanje: </th><td>{{formatState .Entry.State}}</td></tr>
</table>
{{if eq .Entry.State "accepted"}}
<form class="mb-3" method="post" action="/viva/complete/{{.Entry.Id}}">
<div class="mb-3">
<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}}">
</div>
<button class="btn btn-primary" type="submit">Završi transakciju</button>
</form>
<form method="post" action="/viva/cancel/{{.Entry.Id}}">
<button class="btn btn-primary" type="submit">Otkaži transakciju</button>
</form>
{{end}}
</body>
</html>

View File

@ -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,9 +15,9 @@
</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 action="{{.Action}}" method="POST">
<input type="hidden" name="ShopID" value="{{.Form.ShopID}}"> <input type="hidden" name="ShopID" value="{{.Form.ShopID}}">
<input type="hidden" name="ShoppingCartID" value="{{.Form.ShoppingCartID}}"> <input type="hidden" name="ShoppingCartID" value="{{.Form.ShoppingCartID}}">
<input type="hidden" name="Version" value="{{.Form.Version}}"> <input type="hidden" name="Version" value="{{.Form.Version}}">
@ -26,10 +26,36 @@
<input type="hidden" name="ReturnURL" value="{{.Form.ReturnURL}}"> <input type="hidden" name="ReturnURL" value="{{.Form.ReturnURL}}">
<input type="hidden" name="CancelURL" value="{{.Form.CancelURL}}"> <input type="hidden" name="CancelURL" value="{{.Form.CancelURL}}">
<input type="hidden" name="ReturnErrorURL" value="{{.Form.ReturnErrorURL}}"> <input type="hidden" name="ReturnErrorURL" value="{{.Form.ReturnErrorURL}}">
<input type="submit" class="btn btn-primary" value="Koristi normalni redirect">
</form> </form>
<h2>Započni normalni proces u iframe-u</h2>
<form target="payment-frame" name="pay" action="{{.Action}}" method="POST">
<input type="hidden" name="Iframe" value="True">
<input type="hidden" name="IframeResponseTarget" value="SELF">
<input type="hidden" name="ShopID" value="{{.Form.ShopID}}">
<input type="hidden" name="ShoppingCartID" value="{{.Form.ShoppingCartID}}">
<input type="hidden" name="Version" value="{{.Form.Version}}">
<input type="hidden" name="TotalAmount" value="{{formatCurrency .Form.TotalAmount}}">
<input type="hidden" name="Signature" value="{{.Form.Signature}}">
<input type="hidden" name="ReturnURL" value="{{.Form.ReturnURL}}?iframe=true">
<input type="hidden" name="CancelURL" value="{{.Form.CancelURL}}?iframe=true">
<input type="hidden" name="ReturnErrorURL" value="{{.Form.ReturnErrorURL}}?iframe=true">
<input type="submit" class="btn btn-primary" value="Koristi navigaciju u iframe-u">
</form>
<iframe id="payment-frame" name="payment-frame" style="width: 100%; min-height: 600px"></iframe>
<script> <script>
document.querySelector("#wspay-form").submit(); window.addEventListener(
"message",
(event) => {
console.log("received response")
window.location.href = "/";
},
false
);
</script> </script>
</body> </body>
</html> </html>

View File

@ -0,0 +1,62 @@
<!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>Info</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
<style>
th {text-align: left}
h2 {
margin-top: 16px;
}
</style>
</head>
<body class="container">
<h2>Plačanje {{.Entry.Id}}</h2>
<table class="table">
<tr><th>CartId: </th><td>{{.Entry.ShoppingCartID}}</td></tr>
<tr><th>Ukupna vrijednost: </th><td>{{formatCurrency .Entry.TotalAmount}}</td></tr>
<tr><th>Jezik: </th><td>{{omitempty .Entry.Lang}}</td></tr>
<tr><th>Ime: </th><td>{{omitempty .Entry.CustomerFirstName}}</td></tr>
<tr><th>Prezime: </th><td>{{omitempty .Entry.CustomerLastName}}</td></tr>
<tr><th>Adresa: </th><td>{{omitempty .Entry.CustomerAddress}}</td></tr>
<tr><th>Grad: </th><td>{{omitempty .Entry.CustomerCity}}</td></tr>
<tr><th>ZIP: </th><td>{{omitempty .Entry.CustomerZIP}}</td></tr>
<tr><th>Zemlja: </th><td>{{omitempty .Entry.CustomerCountry}}</td></tr>
<tr><th>Broj telefona: </th><td>{{omitempty .Entry.CustomerPhone}}</td></tr>
<tr><th>Plan plačanja: </th><td>{{omitempty .Entry.PaymentPlan}}</td></tr>
<tr><th>Ime kartice: </th><td>{{omitempty .Entry.CreditCardName}}</td></tr>
<tr><th>Broj kartice: </th><td>{{omitempty .Entry.CreditCardNumber}}</td></tr>
<tr><th>Metoda plačanja: </th><td>{{omitempty .Entry.PaymentMethod}}</td></tr>
<tr><th>Oznaka valute: </th><td>{{.Entry.CurrencyCode}}</td></tr>
<tr><th>Datum i vrijeme: </th><td>{{.Entry.DateTime.Format "Jan 02, 2006 15:04:05 UTC"}}</td></tr>
<tr><th>Uspjeh: </th> <td>{{.Entry.Success}}</td></tr>
<tr><th>Kod: </th> <td>{{omitempty .Entry.ApprovalCode}}</td></tr>
<tr><th>Poruka greške: </th> <td>{{omitempty .Entry.ErrorMessage}}</td></tr>
<tr><th>Kodovi greške: </th> <td>{{omitempty .Entry.ErrorCodes}}</td></tr>
<tr><th>Stanje: </th><td>{{formatState .Entry.State}}</td></tr>
</table>
{{if eq .Entry.State "accepted"}}
<form class="mb-3" method="post" action="/wspay/complete/{{.Entry.Id}}">
<div class="mb-3">
<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}}">
</div>
<button class="btn btn-primary" type="submit">Završi transakciju</button>
</form>
<form method="post" action="/wspay/cancel/{{.Entry.Id}}">
<button class="btn btn-primary" type="submit">Otkaži transakciju</button>
</form>
{{end}}
</body>
</html>

26
viva/model.go Normal file
View File

@ -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"`
}

235
viva/service.go Normal file
View File

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

44
viva/viva.go Normal file
View File

@ -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"`
}

41
wspay/model.go Normal file
View File

@ -0,0 +1,41 @@
package wspay
import (
"github.com/google/uuid"
"payment-poc/state"
"time"
)
type WsPayDb struct {
Id uuid.UUID `db:"id"`
ShopID string `db:"shop_id"`
ShoppingCartID string `db:"shopping_card_id"`
TotalAmount int64 `db:"total_amount"`
Lang string `db:"lang"`
CustomerFirstName string `db:"customer_first_name"`
CustomerLastName string `db:"customer_last_name"`
CustomerAddress string `db:"customer_address"`
CustomerCity string `db:"customer_city"`
CustomerZIP string `db:"customer_zip"`
CustomerCountry string `db:"customer_country"`
CustomerPhone string `db:"customer_phone"`
PaymentPlan string `db:"payment_plan"`
CreditCardName string `db:"credit_card_name"`
CreditCardNumber string `db:"credit_card_number"`
PaymentMethod string `db:"payment_method"`
CurrencyCode int `db:"currency_code"`
DateTime time.Time `db:"date_time"`
ECI string `db:"eci"`
STAN string `db:"stan"`
Success int `db:"success"`
ApprovalCode string `db:"approval_code"`
ErrorMessage string `db:"error_message"`
ErrorCodes string `db:"error_codes"`
State state.PaymentState `db:"payment_state"`
}

143
wspay/service.go Normal file
View File

@ -0,0 +1,143 @@
package wspay
import (
"crypto/sha512"
"encoding/hex"
"errors"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"payment-poc/state"
"strconv"
)
type Service struct {
DB *sqlx.DB
}
func (s *Service) CreateEntry(shopId string, totalAmount int64) (WsPayDb, error) {
id := uuid.Must(uuid.NewRandom())
entry := WsPayDb{
Id: id,
ShopID: shopId,
ShoppingCartID: id.String(),
TotalAmount: totalAmount,
State: state.StateInitialized,
}
_, err := s.DB.Exec(`INSERT INTO "wspay" ("id", "shop_id", "shopping_card_id", "total_amount", "payment_state") VALUES ($1, $2, $3, $4, $5)`,
&entry.Id, &entry.ShopID, &entry.ShoppingCartID, &entry.TotalAmount, &entry.State,
)
if err != nil {
return WsPayDb{}, err
}
return s.FetchById(id)
}
func (s *Service) FetchAll() ([]WsPayDb, error) {
var entries []WsPayDb
err := s.DB.Select(&entries, `SELECT * FROM "wspay"`)
return entries, err
}
func (s *Service) FetchById(id uuid.UUID) (WsPayDb, error) {
entry := WsPayDb{}
err := s.DB.Get(&entry, `SELECT * FROM "wspay" WHERE "id" = $1`, id)
return entry, err
}
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
}
func CalculateFormSignature(shopId string, secret string, cartId string, amount int64) string {
/**
Represents a signature created from string formatted from following values in a following order using
SHA512 algorithm:
ShopID
SecretKey
ShoppingCartID
SecretKey
TotalAmount
SecretKey
*/
signature := shopId + secret + cartId + secret + strconv.FormatInt(amount, 10) + secret
hash := sha512.New()
hash.Write([]byte(signature))
return hex.EncodeToString(hash.Sum(nil))
}
func CalculateCompletionSignature(shopId string, secret string, cartId string, stan string, approvalCode string, amount int64) string {
/**
Represents a signature created from string formatted from following values in a following order using
SHA512 algorithm:
ShopID
WsPayOrderId
SecretKey
STAN
SecretKey
ApprovalCode
SecretKey
Amount
SecretKey
WsPayOrderId
*/
signature := shopId + cartId + secret + stan + secret + approvalCode + secret + strconv.FormatInt(amount, 10) + secret + cartId
hash := sha512.New()
hash.Write([]byte(signature))
return hex.EncodeToString(hash.Sum(nil))
}
func CompareFormReturnSignature(signature string, shopId string, secret string, cartId string, success int, approvalCode string) error {
/**
Represents a signature created from string formatted from following values in a following order using
SHA512 algorithm:
ShopID
SecretKey
ShoppingCartID
SecretKey
Success
SecretKey
ApprovalCode
SecretKey
Merchant should validate this signature to make sure that the request is originating from WSPayForm.
*/
calculatedSignature := shopId + secret + cartId + secret + strconv.FormatInt(int64(success), 10) + secret + approvalCode + secret
hash := sha512.New()
hash.Write([]byte(calculatedSignature))
if hex.EncodeToString(hash.Sum(nil)) == signature {
return nil
} else {
return errors.New("signature mismatch")
}
}
func CompareCompletionReturnSignature(signature string, shopId string, secret string, stan 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
STAN
ActionSuccess
SecretKey
ApprovalCode
WsPayOrderId
Merchant should validate this signature to make sure that the request is originating from WSPayForm.
*/
calculatedSignature := shopId + secret + stan + actionSuccess + secret + approvalCode + cartId
hash := sha512.New()
hash.Write([]byte(calculatedSignature))
if hex.EncodeToString(hash.Sum(nil)) == signature {
return nil
} else {
return errors.New("signature mismatch")
}
}

View File

@ -2,7 +2,7 @@ package wspay
const AuthorisationForm = "https://formtest.wspay.biz/authorization.aspx" const AuthorisationForm = "https://formtest.wspay.biz/authorization.aspx"
type Form struct { type WsPayForm struct {
// required args // required args
ShopID string ShopID string
ShoppingCartID string ShoppingCartID string
@ -31,7 +31,7 @@ type Form struct {
CurrencyCode int CurrencyCode int
} }
type FormReturn struct { type WsPayFormReturn 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 FormReturn struct {
Signature string `form:"Signature"` Signature string `form:"Signature"`
} }
type FormError struct { type WsPayFormError struct {
CustomerFirstName string CustomerFirstName string
CustomerSurname string CustomerSurname string
CustomerAddress string CustomerAddress string
@ -88,7 +88,7 @@ type FormError struct {
Signature string Signature string
} }
type FormCancel struct { type WsPayFormCancel struct {
ResponseCode int ResponseCode int
ShoppingCartID string ShoppingCartID string
ApprovalCode string ApprovalCode string
@ -96,7 +96,7 @@ type FormCancel struct {
Signature string Signature string
} }
type CompletionRequest struct { type WsPayCompletionRequest struct {
Version string Version string
WsPayOrderId string WsPayOrderId string
ShopId string ShopId string
@ -106,7 +106,7 @@ type CompletionRequest struct {
Signature string Signature string
} }
type CompletionResponse struct { type WsPayCompletionResponse struct {
WsPayOrderId string WsPayOrderId string
ShopId string ShopId string
ApprovalCode string ApprovalCode string
@ -116,28 +116,28 @@ type CompletionResponse struct {
Signature string Signature string
} }
type StatusCheckRequest struct { type WsPayStatusCheckRequest struct {
Version string Version string
ShopId string ShopId string
ShoppingCartId string ShoppingCartId string
Signature string Signature string
} }
type StatusCheckResponse struct { type WsPayStatusCheckResponse struct {
WsPayOrderId string WsPayOrderId string
Signature string Signature string
STAN string STAN string
ApprovalCode string ApprovalCode string
ShopID string ShopID string
ShoppingCartID string ShoppingCartID string
Amount int64 Amount string
CurrencyCode string CurrencyCode string
ActionSuccess string ActionSuccess string
Success string // deprecated Success string // deprecated
Authorized string Authorized int
Completed string Completed int
Voided string Voided int
Refunded string Refunded int
PaymentPlan string PaymentPlan string
Partner string Partner string
OnSite int OnSite int