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 }