Compare commits

...

5 Commits

Author SHA1 Message Date
Borna Rajković 7b2523e2b1 Organization + added logs 2023-07-31 10:01:37 +02:00
Borna Rajković fe6f3b6672 Added payment update action 2023-07-31 09:21:54 +02:00
Borna Rajković b6203d8a03 Added mock provider 2023-07-31 09:21:37 +02:00
Borna Rajković 1307c7bc6e Code cleanup 2023-07-30 12:14:05 +02:00
Borna Rajković dcc9754d43 WIP Code cleanup 2023-07-27 22:46:37 +02:00
27 changed files with 1522 additions and 1499 deletions

54
database/model.go Normal file
View File

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

57
database/provider.go Normal file
View File

@ -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") 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,65 +1,30 @@
CREATE TABLE IF NOT EXISTS "wspay"
CREATE TABLE IF NOT EXISTS "payment_entry"
(
"id" uuid NOT NULL,
"shop_id" varchar(128) NOT NULL,
"shopping_card_id" varchar(128) NOT NULL,
"created" timestamp NOT NULL,
"modified" timestamp DEFAULT 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,
"lang" varchar(128) DEFAULT '',
"eci" 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 '',
"payment_intent_id" varchar(255) DEFAULT NULL,
"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,
"shopping_card_id" varchar(255) DEFAULT NULL,
"stan" varchar(255) DEFAULT NULL,
"success" int DEFAULT NULL,
"approval_code" varchar(255) DEFAULT NULL,
"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 '',
"order_id" varchar(255) DEFAULT NULL,
"transaction_id" uuid DEFAULT NULL,
"total_amount" int NOT NULL,
"event_id" varchar(128) DEFAULT '',
"eci" varchar(128) DEFAULT '',
"payment_state" varchar(256) DEFAULT '',
"event_id" varchar(255) DEFAULT NULL,
PRIMARY KEY (id)
);

View File

@ -1 +0,0 @@
package prod

959
main.go

File diff suppressed because it is too large Load Diff

44
providers/mock/service.go Normal file
View File

@ -0,0 +1,44 @@
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
}

143
providers/stripe/service.go Normal file
View File

@ -0,0 +1,143 @@
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
}

271
providers/viva/service.go Normal file
View File

@ -0,0 +1,271 @@
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
}

55
providers/viva/viva.go Normal file
View File

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

386
providers/wspay/service.go Normal file
View File

@ -0,0 +1,386 @@
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

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

View File

@ -3,9 +3,14 @@ package state
type PaymentState string
const (
// initial state
StatePreinitialized PaymentState = "preinitialized"
// initial state
StateInitialized PaymentState = "initialized"
// state given to async payments (eg. GooglePay,ApplePay...)
StatePending PaymentState = "pending"
// state on response
StateAccepted PaymentState = "accepted"
StateError PaymentState = "error"
@ -16,3 +21,12 @@ const (
StateVoided PaymentState = "voided"
StateCanceled PaymentState = "canceled"
)
type PaymentGateway string
const (
GatewayWsPay PaymentGateway = "wspay"
GatewayStripe PaymentGateway = "stripe"
GatewayVivaWallet PaymentGateway = "viva-wallet"
GatewayMock PaymentGateway = "mock"
)

View File

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

View File

@ -1,46 +0,0 @@
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

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

79
templates/info.gohtml Normal file
View File

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

View File

@ -5,31 +5,23 @@
<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>Obrada odgovora</title>
<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 class="container">
<p>Obrada odgovora...</p>
<div id="error"></div>
<script>
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>
<body>
<!-- As a link -->
<nav class="navbar navbar-dark bg-dark">
<section class="container">
<a class="navbar-brand" href="/">Payment-poc</a>
</section>
</nav>
<section class="container">
<h2>Mock gateway {{.Entry.Id.String}}</h2>
<p>{{formatCurrency .Entry.TotalAmount}}</p>
<a href="/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>
</html>

View File

@ -1,42 +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>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

@ -1,44 +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>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"
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>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">
<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>
@ -15,9 +15,9 @@
</style>
</head>
<body class="container" style="margin-top: 32px">
<h2>Započni proces plačanja</h2>
<h2>Započni proces plaćanja</h2>
<form action="{{.Action}}" method="POST">
<form id="wspay-form" action="{{.Action}}" method="POST">
<input type="hidden" name="ShopID" value="{{.Form.ShopID}}">
<input type="hidden" name="ShoppingCartID" value="{{.Form.ShoppingCartID}}">
<input type="hidden" name="Version" value="{{.Form.Version}}">
@ -26,36 +26,10 @@
<input type="hidden" name="ReturnURL" value="{{.Form.ReturnURL}}">
<input type="hidden" name="CancelURL" value="{{.Form.CancelURL}}">
<input type="hidden" name="ReturnErrorURL" value="{{.Form.ReturnErrorURL}}">
<input type="submit" class="btn btn-primary" value="Koristi normalni redirect">
</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>
window.addEventListener(
"message",
(event) => {
console.log("received response")
window.location.href = "/";
},
false
);
document.querySelector("#wspay-form").submit();
</script>
</body>
</html>

View File

@ -1,62 +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>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>

View File

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

View File

@ -1,235 +0,0 @@
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
}

View File

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

View File

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

View File

@ -1,143 +0,0 @@
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")
}
}