2023-07-10 08:10:13 +00:00
|
|
|
package wspay
|
|
|
|
|
|
|
|
import (
|
2023-07-27 20:46:37 +00:00
|
|
|
"bytes"
|
2023-07-10 08:10:13 +00:00
|
|
|
"crypto/sha512"
|
|
|
|
"encoding/hex"
|
2023-07-27 20:46:37 +00:00
|
|
|
"encoding/json"
|
2023-07-10 08:10:13 +00:00
|
|
|
"errors"
|
2023-08-04 06:13:11 +00:00
|
|
|
"fmt"
|
2023-07-27 20:46:37 +00:00
|
|
|
"github.com/gin-gonic/gin"
|
2023-07-10 08:10:13 +00:00
|
|
|
"github.com/google/uuid"
|
2023-07-27 20:46:37 +00:00
|
|
|
"io"
|
2023-07-31 07:21:54 +00:00
|
|
|
"log"
|
2023-07-27 20:46:37 +00:00
|
|
|
"net/http"
|
|
|
|
"payment-poc/database"
|
2023-07-26 07:51:29 +00:00
|
|
|
"payment-poc/state"
|
2023-07-10 08:10:13 +00:00
|
|
|
"strconv"
|
|
|
|
)
|
|
|
|
|
|
|
|
type Service struct {
|
2023-07-27 20:46:37 +00:00
|
|
|
ShopId string
|
|
|
|
ShopSecret string
|
|
|
|
BackendUrl string
|
2023-07-10 08:10:13 +00:00
|
|
|
}
|
|
|
|
|
2023-07-31 07:21:54 +00:00
|
|
|
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),
|
|
|
|
)
|
2023-07-10 08:10:13 +00:00
|
|
|
if err != nil {
|
2023-07-31 07:21:54 +00:00
|
|
|
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
|
2023-07-10 08:10:13 +00:00
|
|
|
}
|
2023-07-31 07:21:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Service) CreatePaymentUrl(entry database.PaymentEntry) (database.PaymentEntry, string, error) {
|
2023-08-04 06:13:11 +00:00
|
|
|
entry, url, err := s.InitializePayment(entry)
|
|
|
|
if err != nil {
|
|
|
|
return entry, "", err
|
|
|
|
}
|
|
|
|
return entry, url, nil
|
2023-07-10 08:10:13 +00:00
|
|
|
}
|
|
|
|
|
2023-07-27 20:46:37 +00:00
|
|
|
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")
|
|
|
|
}
|
2023-07-10 08:10:13 +00:00
|
|
|
}
|
|
|
|
|
2023-08-04 06:13:11 +00:00
|
|
|
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{
|
2023-07-27 20:46:37 +00:00
|
|
|
ShopID: s.ShopId,
|
2023-08-04 06:13:11 +00:00
|
|
|
ShoppingCardID: entry.Id.String(),
|
2023-07-27 20:46:37 +00:00
|
|
|
Version: "2.0",
|
2023-08-04 06:13:11 +00:00
|
|
|
TotalAmount: formattedAmount,
|
|
|
|
ReturnUrl: s.BackendUrl + "/wspay/success",
|
|
|
|
ReturnErrorUrl: s.BackendUrl + "/wspay/error",
|
|
|
|
CancelUrl: s.BackendUrl + "/wspay/cancel",
|
2023-07-27 20:46:37 +00:00
|
|
|
Signature: CalculateFormSignature(s.ShopId, s.ShopSecret, entry.Id.String(), entry.TotalAmount),
|
|
|
|
}
|
2023-08-04 06:13:11 +00:00
|
|
|
|
|
|
|
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
|
2023-07-10 08:10:13 +00:00
|
|
|
}
|
|
|
|
|
2023-07-31 07:21:54 +00:00
|
|
|
func (s *Service) HandleSuccessResponse(c *gin.Context, provider *database.PaymentEntryProvider) (string, error) {
|
2023-07-27 20:46:37 +00:00
|
|
|
response := FormReturn{}
|
|
|
|
if err := c.ShouldBind(&response); err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
2023-07-31 07:21:54 +00:00
|
|
|
entry, err := provider.FetchById(uuid.MustParse(response.ShoppingCartID))
|
2023-07-27 20:46:37 +00:00
|
|
|
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
|
|
|
|
|
2023-07-31 07:21:54 +00:00
|
|
|
if _, err := provider.UpdateEntry(entry); err != nil {
|
2023-07-27 20:46:37 +00:00
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
return "/entries/" + entry.Id.String(), nil
|
2023-07-10 08:10:13 +00:00
|
|
|
}
|
|
|
|
|
2023-07-31 07:21:54 +00:00
|
|
|
func (s *Service) HandleErrorResponse(c *gin.Context, provider *database.PaymentEntryProvider, paymentState state.PaymentState) (string, error) {
|
2023-07-27 20:46:37 +00:00
|
|
|
response := FormError{}
|
|
|
|
if err := c.ShouldBind(&response); err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
2023-07-31 07:21:54 +00:00
|
|
|
entry, err := provider.FetchById(uuid.MustParse(response.ShoppingCartID))
|
2023-07-27 20:46:37 +00:00
|
|
|
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
|
|
|
|
|
2023-07-31 07:21:54 +00:00
|
|
|
if _, err := provider.UpdateEntry(entry); err != nil {
|
2023-07-27 20:46:37 +00:00
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
return "/entries/" + entry.Id.String(), nil
|
2023-07-10 08:10:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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))
|
|
|
|
}
|
|
|
|
|
2023-07-27 10:09:38 +00:00
|
|
|
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))
|
|
|
|
}
|
|
|
|
|
2023-07-10 08:10:13 +00:00
|
|
|
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")
|
|
|
|
}
|
|
|
|
}
|
2023-07-27 10:09:38 +00:00
|
|
|
|
|
|
|
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")
|
|
|
|
}
|
|
|
|
}
|
2023-07-27 20:46:37 +00:00
|
|
|
|
2023-07-31 07:21:54 +00:00
|
|
|
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))
|
|
|
|
}
|
|
|
|
|
2023-07-27 20:46:37 +00:00
|
|
|
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
|
|
|
|
}
|