package zarinhub import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "net" "net/http" "card-to-iban/internal/service" ) 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(resp.Body) if err != nil { return result, fmt.Errorf( "%w: failed to read response body: %v", ErrInvalidResponse, err, ) } 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 }