not tracked not covered covered
package exchange

import (
        "context"
        "encoding/json"
        "fmt"
        "io/ioutil"
        "math"
        "net/http"
        "time"

        "github.com/PacktPublishing/Hands-On-Dependency-Injection-in-Go/ch12/acme/internal/logging"
)

const (
        // request URL for the exchange rate API
		urlFormat = "%s/api/historical?access_key=%s&date=2018-06-20¤cies=%s"

        // default price that is sent when an error occurs
        defaultPrice = 0.0
)

// NewConverter creates and initializes the converter
func NewConverter(cfg Config) *Converter {
        return &Converter{
                cfg: cfg,
        }
}

// Config is the config for Converter
type Config interface {
        Logger() logging.Logger
        ExchangeBaseURL() string
        ExchangeAPIKey() string
}

// Converter will convert the base price to the currency supplied
// Note: we are expecting sane inputs and therefore skipping input validation
type Converter struct {
        cfg Config
}

// Exchange will perform the conversion
func (c *Converter) Exchange(ctx context.Context, basePrice float64, currency string) (float64, error) {
        // load rate from the external API
        response, err := c.loadRateFromServer(ctx, currency)
        if err != nil {
                return defaultPrice, err
        }

        // extract rate from response
        rate, err := c.extractRate(response, currency)
        if err != nil {
                return defaultPrice, err
        }

        // apply rate and round to 2 decimal places
        return math.Floor((basePrice/rate)*100) / 100, nil
}

// load rate from the external API
func (c *Converter) loadRateFromServer(ctx context.Context, currency string) (*http.Response, error) {
        // build the request
        url := fmt.Sprintf(urlFormat,
                c.cfg.ExchangeBaseURL(),
                c.cfg.ExchangeAPIKey(),
                currency)

        // perform request
        req, err := http.NewRequest("GET", url, nil)
        if err != nil {
                c.logger().Warn("[exchange] failed to create request. err: %s", err)
                return nil, err
        }

        // set latency budget for the upstream call
        subCtx, cancel := context.WithTimeout(ctx, 1*time.Second)
        defer cancel()

        // replace the default context with our custom one
        req = req.WithContext(subCtx)

        // perform the HTTP request
        response, err := http.DefaultClient.Do(req)
        if err != nil {
                c.logger().Warn("[exchange] failed to load. err: %s", err)
                return nil, err
        }

        if response.StatusCode != http.StatusOK {
                err = fmt.Errorf("request failed with code %d", response.StatusCode)
                c.logger().Warn("[exchange] %s", err)
                return nil, err
        }

        return response, nil
}

func (c *Converter) extractRate(response *http.Response, currency string) (float64, error) {
        defer func() {
                _ = response.Body.Close()
        }()

        // extract data from response
        data, err := c.extractResponse(response)
        if err != nil {
                return defaultPrice, err
        }

        // pull rate from response data
        rate, found := data.Quotes["USD" + currency]
        if !found {
                err = fmt.Errorf("response did not include expected currency '%s'", currency)
                c.logger().Error("[exchange] %s", err)
                return defaultPrice, err
        }

        // happy path
        return rate, nil
}

func (c *Converter) extractResponse(response *http.Response) (*apiResponseFormat, error) {
        payload, err := ioutil.ReadAll(response.Body)
        if err != nil {
                c.logger().Error("[exchange] failed to ready response body. err: %s", err)
                return nil, err
        }

        data := &apiResponseFormat{}
        err = json.Unmarshal(payload, data)
        if err != nil {
                c.logger().Error("[exchange] error converting response. err: %s", err)
                return nil, err
        }

        // happy path
        return data, nil
}

func (c *Converter) logger() logging.Logger {
        return c.cfg.Logger()
}

// the response format from the exchange rate API
type apiResponseFormat struct {
        Quotes map[string]float64 `json:"quotes"`
}