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 }