Reorganized code + migration to go1.22

This commit is contained in:
brajkovic committed 2024-04-01 20:29:24 +02:00
1 parent e3d77d55b9
commit 7369777098
25 files changed
+825 -605

No files matched your search

+416
View File
@@ -0,0 +1,416 @@
package wspay
import (
"bytes"
"crypto/sha512"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"io"
"log/slog"
"net/http"
"payment-poc/domain/database"
"payment-poc/domain/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.WsPayOrderId) == nil {
newValue := int64(response.Amount * 100)
entry.Amount = &newValue
newState := determineState(response)
if entry.State != newState && newState != "" {
slog.Info("Updated state", "entry_id", entry.Id.String(), "state", entry.State, "new_state", newState)
entry.State = newState
}
entry.WsPayOrderId = response.WsPayOrderId
} 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) {
entry, url, err := s.InitializePayment(entry)
if err != nil {
return entry, "", err
}
return entry, url, 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.WsPayOrderId,
ShopID: s.ShopId,
ApprovalCode: *entry.ApprovalCode,
STAN: *entry.STAN,
Amount: strconv.FormatInt(amount, 10),
Signature: CalculateCompletionSignature(s.ShopId, s.ShopSecret, entry.WsPayOrderId, *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.STAN, response.ActionSuccess, response.ApprovalCode, entry.WsPayOrderId) == 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.WsPayOrderId,
ShopID: s.ShopId,
ApprovalCode: *entry.ApprovalCode,
STAN: *entry.STAN,
Amount: strconv.FormatInt(entry.TotalAmount, 10),
Signature: CalculateCompletionSignature(s.ShopId, s.ShopSecret, entry.WsPayOrderId, *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.STAN, response.ActionSuccess, response.ApprovalCode, entry.WsPayOrderId) == nil {
entry.State = state.StateVoided
} 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) (database.PaymentEntry, string, error) {
formattedAmount := fmt.Sprintf("%d,%02d", entry.TotalAmount/100, entry.TotalAmount%100)
var request = CreateTransaction{
ShopID: s.ShopId,
ShoppingCardID: entry.Id.String(),
Version: "2.0",
TotalAmount: formattedAmount,
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),
}
httpResponse, err := createRequest(
"POST",
"https://formtest.wspay.biz/api/create-transaction",
map[string]string{"content-type": "application/json"},
toJson(request),
)
if err != nil {
return database.PaymentEntry{}, "", err
}
var response TransactionResponse
err = readResponse(httpResponse, &response)
if err != nil {
return database.PaymentEntry{}, "", err
}
if response.TransactionId == nil {
return database.PaymentEntry{}, "", errors.New("received bad response")
}
entry.State = state.StateInitialized
return entry, *response.PaymentFormUrl, nil
}
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
entry.WsPayOrderId = response.WsPayOrderId
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, wsPayOrderId 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 + wsPayOrderId + secret + stan + secret + approvalCode + secret + strconv.FormatInt(amount, 10) + secret + wsPayOrderId
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, wsPayOrderId 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 + wsPayOrderId
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, wsPayOrderId 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 + wsPayOrderId
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
}
+145
View File
@@ -0,0 +1,145 @@
package wspay
type CreateTransaction struct {
ShopID string `json:"ShopID"`
ShoppingCardID string `json:"ShoppingCartID"`
Version string `json:"Version"`
TotalAmount string `json:"TotalAmount"`
ReturnUrl string `json:"ReturnURL"`
ReturnErrorUrl string `json:"ReturnErrorURL"`
CancelUrl string `json:"CancelURL"`
Signature string `json:"Signature"`
}
type TransactionResponse struct {
TransactionId *string `json:"TransactionId"`
PaymentFormUrl *string `json:"PaymentFormUrl"`
}
type FormReturn struct {
CustomerFirstName string `form:"CustomerFirstname"`
CustomerSurname string `form:"CustomerSurname"`
CustomerAddress string `form:"CustomerAddress"`
CustomerCity string `form:"CustomerCity"`
CustomerZIP string `form:"CustomerZIP"`
CustomerCountry string `form:"CustomerCountry"`
CustomerPhone string `form:"CustomerPhone"`
CustomerEmail string `form:"CustomerEmail"`
ShoppingCartID string `form:"ShoppingCartID"`
Lang string `form:"Lang"`
DateTime string `form:"DateTime"` //yyyymmddHHMMss
Amount string `form:"Amount"` // eg. 123,43
ECI string `form:"ECI"`
STAN string `form:"STAN"`
Partner string `form:"Partner"`
WsPayOrderId string `form:"WsPayOrderId"`
PaymentType string `form:"PaymentType"`
CreditCardNumber string `form:"CreditCardNumber"` // masked number
PaymentPlan string `form:"PaymentPlan"`
ShopPostedPaymentPlan string `form:"ShopPostedPaymentPlan"`
ShopPostedLang string `form:"ShopPostedLang"`
ShopPostedCreditCardName string `form:"ShopPostedCreditCardName"`
Success int `form:"Success"`
ApprovalCode string `form:"ApprovalCode"`
ErrorMessage string `form:"ErrorMessage"`
ShopPostedPaymentMethod string `form:"ShopPostedPaymentMethod"`
Signature string `form:"Signature"`
}
type FormError struct {
CustomerFirstName string
CustomerSurname string
CustomerAddress string
CustomerCity string
CustomerZIP string
CustomerCountry string
CustomerPhone string
CustomerEmail string
ShoppingCartID string
Lang string
DateTime string //yyyymmddHHMMss
Amount string // eg. 123,43
ECI string
PaymentType string
PaymentPlan string
ShopPostedPaymentPlan string
ShopPostedLang string
ShopPostedCreditCardName string
Success int
ApprovalCode string
ErrorMessage string
ShopPostedPaymentMethod string
ErrorCodes string
Signature string
}
type FormCancel struct {
ResponseCode int
ShoppingCartID string
ApprovalCode string
Success int
Signature string
}
type CompletionRequest struct {
Version string
WsPayOrderId string
ShopID string
ApprovalCode string
STAN string
Amount string
Signature string
}
type CompletionResponse struct {
WsPayOrderId string
ShopID string
ApprovalCode string
STAN string
ErrorMessage string
ActionSuccess string
Signature string
}
type StatusCheckRequest struct {
Version string
ShopID string
ShoppingCartId string
Signature string
}
type StatusCheckResponse struct {
WsPayOrderId string
Signature string
STAN string
ApprovalCode string
ShopID string
ShoppingCartID string
Amount float64
CurrencyCode int
ActionSuccess string
Success string // deprecated
Authorized string
Completed string
Voided string
Refunded string
PaymentPlan string
Partner string
OnSite string
CreditCardName string
CreditCardNumber string
ECI string
CustomerFirstName string
CustomerLastName string
CustomerCity string
CustomerZIP string
CustomerCountry string
CustomerPhone string
CustomerEmail string
TransactionDateTime string // yyyymmddHHMMss
IsLessThan30DaysFromTransaction bool
CanBeCompleted bool
CanBeVoided bool
CanBeRefunded bool
ExpirationDate string
}