183 lines
4.5 KiB
Go
183 lines
4.5 KiB
Go
package paybridge
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"gitea.futuresens.co.uk/futuresens/hardlink/internal/paymentsvc"
|
|
"gitea.futuresens.co.uk/futuresens/hardlink/internal/types"
|
|
"github.com/gorilla/websocket"
|
|
)
|
|
|
|
type Client struct {
|
|
URL string
|
|
APIKey string
|
|
TimeoutSeconds int
|
|
}
|
|
|
|
func NewClient(url, apiKey string, timeoutSeconds int) *Client {
|
|
if timeoutSeconds <= 0 {
|
|
timeoutSeconds = 300
|
|
}
|
|
|
|
return &Client{
|
|
URL: url,
|
|
APIKey: apiKey,
|
|
TimeoutSeconds: timeoutSeconds,
|
|
}
|
|
}
|
|
|
|
func (c *Client) Sale(ctx context.Context, req paymentsvc.SaleRequest) (*paymentsvc.Result, error) {
|
|
if req.Currency == "" {
|
|
req.Currency = "GBP"
|
|
}
|
|
|
|
payBridgeReq := PaymentRequest{
|
|
RequestID: req.RequestID,
|
|
Amount: req.Amount,
|
|
Currency: req.Currency,
|
|
Operation: "SALE",
|
|
TimeoutSeconds: c.TimeoutSeconds,
|
|
}
|
|
|
|
return c.doPayment(ctx, payBridgeReq)
|
|
}
|
|
|
|
func (c *Client) doPayment(ctx context.Context, req PaymentRequest) (*paymentsvc.Result, error) {
|
|
connectURL, err := c.connectURL()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ws, _, err := websocket.DefaultDialer.DialContext(ctx, connectURL, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %v", ErrConnectionFailed, err)
|
|
}
|
|
defer ws.Close()
|
|
|
|
done := make(chan struct{})
|
|
defer close(done)
|
|
|
|
go func() {
|
|
select {
|
|
case <-ctx.Done():
|
|
_ = ws.Close()
|
|
case <-done:
|
|
}
|
|
}()
|
|
|
|
jwt, err := c.readAuthSuccess(ws)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := ws.WriteJSON(Envelope{
|
|
Type: types.MesTypePaymentRequest,
|
|
JWT: jwt,
|
|
Data: req,
|
|
Timestamp: time.Now().UnixMilli(),
|
|
}); err != nil {
|
|
return nil, fmt.Errorf("send PayBridge payment_request: %w", err)
|
|
}
|
|
|
|
for {
|
|
_, raw, err := ws.ReadMessage()
|
|
if err != nil {
|
|
if ctx.Err() != nil {
|
|
return nil, ctx.Err()
|
|
}
|
|
return nil, fmt.Errorf("read PayBridge message: %w", err)
|
|
}
|
|
|
|
var head struct {
|
|
Type string `json:"type"`
|
|
}
|
|
if err := json.Unmarshal(raw, &head); err != nil {
|
|
return nil, fmt.Errorf("decode PayBridge message header: %w", err)
|
|
}
|
|
|
|
switch strings.ToLower(head.Type) {
|
|
case types.MesTypePaymentResult:
|
|
var result PaymentResultEnvelope
|
|
if err := json.Unmarshal(raw, &result); err != nil {
|
|
return nil, fmt.Errorf("decode payment_result: %w", err)
|
|
}
|
|
return mapPaymentResult(result), nil
|
|
|
|
case types.MesTypePaymentError:
|
|
var paymentErr PaymentErrorEnvelope
|
|
if err := json.Unmarshal(raw, &paymentErr); err != nil {
|
|
return nil, fmt.Errorf("decode payment_error: %w", err)
|
|
}
|
|
return mapPaymentError(req, paymentErr), nil
|
|
|
|
case types.ResultError:
|
|
var genericErr struct {
|
|
Error struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
} `json:"error"`
|
|
}
|
|
if err := json.Unmarshal(raw, &genericErr); err != nil {
|
|
return nil, fmt.Errorf("%w: %s", ErrUnexpectedMessage, string(raw))
|
|
}
|
|
if genericErr.Error.Message != "" {
|
|
return nil, fmt.Errorf("%w: %s: %s", ErrUnexpectedMessage, genericErr.Error.Code, genericErr.Error.Message)
|
|
}
|
|
return nil, fmt.Errorf("%w: %s", ErrUnexpectedMessage, string(raw))
|
|
|
|
case types.MesTypePaymentStatusUpdate, types.MesTypePaymentAccepted, types.MesTypeAuthSuccess:
|
|
// Intermediate message. Keep waiting for payment_result or payment_error.
|
|
|
|
default:
|
|
// PayBridge may introduce additional intermediate message types.
|
|
// Ignore them and continue waiting for the final result.
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Client) connectURL() (string, error) {
|
|
u, err := url.Parse(c.URL)
|
|
if err != nil {
|
|
return "", fmt.Errorf("parse PayBridge WebSocket URL: %w", err)
|
|
}
|
|
|
|
q := u.Query()
|
|
q.Set("api_key", c.APIKey)
|
|
u.RawQuery = q.Encode()
|
|
|
|
return u.String(), nil
|
|
}
|
|
|
|
func (c *Client) readAuthSuccess(ws *websocket.Conn) (string, error) {
|
|
if err := ws.SetReadDeadline(time.Now().Add(10 * time.Second)); err != nil {
|
|
return "", fmt.Errorf("%w: set auth read deadline: %v", ErrAuthFailed, err)
|
|
}
|
|
defer ws.SetReadDeadline(time.Time{})
|
|
|
|
_, raw, err := ws.ReadMessage()
|
|
if err != nil {
|
|
return "", fmt.Errorf("%w: read auth_success: %v", ErrAuthFailed, err)
|
|
}
|
|
|
|
var auth struct {
|
|
Type string `json:"type"`
|
|
JWT string `json:"jwt"`
|
|
}
|
|
if err := json.Unmarshal(raw, &auth); err != nil {
|
|
return "", fmt.Errorf("%w: decode auth_success: %v", ErrAuthFailed, err)
|
|
}
|
|
if !strings.EqualFold(auth.Type, types.MesTypeAuthSuccess) {
|
|
return "", fmt.Errorf("%w: expected auth_success, got %s: %s", ErrAuthFailed, auth.Type, string(raw))
|
|
}
|
|
if auth.JWT == "" {
|
|
return "", fmt.Errorf("%w: auth_success did not contain jwt", ErrAuthFailed)
|
|
}
|
|
|
|
return auth.JWT, nil
|
|
}
|