Files
card-to-iban/backend/internal/zarinhub/client.go

164 lines
3.1 KiB
Go

package zarinhub
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"card-to-iban/internal/service"
)
const maxResponseBodySize = 1 << 20
type Client struct {
httpClient *http.Client
endpointURL string
accessToken string
}
var _ service.CardToIBANClient = (*Client)(nil)
func NewClient(
httpClient *http.Client,
endpointURL string,
accessToken string,
) *Client {
return &Client{
httpClient: httpClient,
endpointURL: endpointURL,
accessToken: accessToken,
}
}
func (c *Client) Convert(
ctx context.Context,
cardNumber string,
) (service.ConversionResult, error) {
payload, err := json.Marshal(convertRequest{
Card: cardNumber,
})
if err != nil {
return service.ConversionResult{},
fmt.Errorf("marshal zarinhub request: %w", err)
}
req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
c.endpointURL,
bytes.NewReader(payload),
)
if err != nil {
return service.ConversionResult{},
fmt.Errorf("create zarinhub request: %w", err)
}
req.Header.Set(
"Authorization",
"Bearer "+c.accessToken,
)
req.Header.Set(
"Content-Type",
"application/json",
)
resp, err := c.httpClient.Do(req)
if err != nil {
switch {
case errors.Is(err, context.DeadlineExceeded):
return service.ConversionResult{},
fmt.Errorf("%w: %v", ErrTimeout, err)
case errors.Is(err, context.Canceled):
return service.ConversionResult{},
fmt.Errorf("zarinhub request canceled: %w", err)
}
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
return service.ConversionResult{},
fmt.Errorf("%w: %v", ErrTimeout, err)
}
return service.ConversionResult{},
fmt.Errorf("%w: %v", ErrUnavailable, err)
}
defer resp.Body.Close()
result := service.ConversionResult{
UpstreamHTTPStatus: resp.StatusCode,
}
body, err := io.ReadAll(
io.LimitReader(resp.Body, maxResponseBodySize+1),
)
if err != nil {
return result,
fmt.Errorf(
"%w: failed to read response body: %v",
ErrInvalidResponse,
err,
)
}
if len(body) > maxResponseBodySize {
return result,
fmt.Errorf(
"%w: response body is too large",
ErrInvalidResponse,
)
}
result.RawResponse = body
switch {
case resp.StatusCode == http.StatusUnauthorized ||
resp.StatusCode == http.StatusForbidden:
return result, ErrAuthentication
case resp.StatusCode == http.StatusRequestTimeout:
return result, ErrTimeout
case resp.StatusCode == http.StatusTooManyRequests:
return result, ErrUnavailable
case resp.StatusCode >= http.StatusInternalServerError:
return result, ErrUnavailable
case resp.StatusCode < http.StatusOK ||
resp.StatusCode >= http.StatusMultipleChoices:
return result, ErrRejected
}
var decoded convertResponse
if err := json.Unmarshal(body, &decoded); err != nil {
return result,
fmt.Errorf("%w: %v", ErrInvalidResponse, err)
}
if !decoded.Meta.IsSuccess {
return result, ErrRejected
}
if decoded.Data.IBAN == "" {
return result,
fmt.Errorf(
"%w: successful response did not contain iban",
ErrInvalidResponse,
)
}
result.IBAN = decoded.Data.IBAN
return result, nil
}