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
}