internal/account/account.go

Summary

Maintainability
A
4 hrs
Test Coverage
package account

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

    "github.com/pkg/errors"
)

// Error is a client error.
type Error struct {
    Message string
    Status  int
}

// Error implementation.
func (e *Error) Error() string {
    return e.Message
}

// Card model.
type Card struct {
    ID       string `json:"id"`
    Brand    string `json:"brand"`
    LastFour string `json:"last_four"`
}

// CouponDuration is the coupon duration.
type CouponDuration string

// Durations.
const (
    Forever   CouponDuration = "forever"
    Once                     = "once"
    Repeating                = "repeating"
)

// Coupon model.
type Coupon struct {
    ID             string         `json:"id"`
    Amount         int            `json:"amount"`
    Percent        int            `json:"percent"`
    Duration       CouponDuration `json:"duration"`
    DurationPeriod int            `json:"duration_period"`
}

// Discount returns the final price from the given amount.
func (c *Coupon) Discount(n int) int {
    if c.Amount != 0 {
        return n - c.Amount
    }

    return n - int(float64(n)*(float64(c.Percent)/100))
}

// Description returns a humanized description of the savings.
func (c *Coupon) Description() (s string) {
    switch {
    case c.Amount != 0:
        n := fmt.Sprintf("%0.2f", float64(c.Amount)/100)
        s += fmt.Sprintf("$%s off", strings.Replace(n, ".00", "", 1))
    case c.Percent != 0:
        s += fmt.Sprintf("%d%% off", c.Percent)
    }

    switch c.Duration {
    case Repeating:
        s += fmt.Sprintf(" for %d months", c.DurationPeriod)
    default:
        s += fmt.Sprintf(" %s", c.Duration)
    }

    return s
}

// Discount model.
type Discount struct {
    Coupon Coupon `json:"coupon"`
}

// Plan model.
type Plan struct {
    ID         string    `json:"id"`
    Name       string    `json:"name"`
    Product    string    `json:"product"`
    Plan       string    `json:"plan"`
    Amount     int       `json:"amount"`
    Interval   string    `json:"interval"`
    Status     string    `json:"status"`
    Canceled   bool      `json:"canceled"`
    Discount   *Discount `json:"discount"`
    CreatedAt  time.Time `json:"created_at"`
    CanceledAt time.Time `json:"canceled_at"`
}

// User model.
type User struct {
    Email     string    `json:"email"`
    CreatedAt time.Time `json:"created_at"`
}

// Team model.
type Team struct {
    ID        string    `json:"id"`
    Name      string    `json:"name"`
    Owner     string    `json:"owner"`
    Type      string    `json:"type"`
    Card      *Card     `json:"card"`
    Members   []User    `json:"members"`
    Invites   []string  `json:"invites"`
    UpdatedAt time.Time `json:"updated_at"`
    CreatedAt time.Time `json:"created_at"`
}

// Client implementation.
type Client struct {
    url string
}

// New client.
func New(url string) *Client {
    return &Client{
        url: url,
    }
}

// GetCoupon by id.
func (c *Client) GetCoupon(id string) (coupon *Coupon, err error) {
    res, err := c.request("", "GET", "/billing/coupons/"+id, nil)
    if err != nil {
        return nil, err
    }
    defer res.Body.Close()

    coupon = new(Coupon)
    err = json.NewDecoder(res.Body).Decode(coupon)
    return
}

// AddCard adds or updates the default card via stripe token.
func (c *Client) AddCard(token, cardToken string) error {
    in := struct {
        Token string `json:"token"`
    }{
        Token: cardToken,
    }

    res, err := c.requestJSON(token, "POST", "/billing/cards", in)
    if err != nil {
        return err
    }
    defer res.Body.Close()

    return nil
}

// GetTeam returns the active team.
func (c *Client) GetTeam(token string) (*Team, error) {
    res, err := c.request(token, "GET", "/", nil)
    if err != nil {
        return nil, err
    }
    defer res.Body.Close()

    var t Team
    return &t, json.NewDecoder(res.Body).Decode(&t)
}

// GetCards returns the user's cards.
func (c *Client) GetCards(token string) (cards []Card, err error) {
    res, err := c.request(token, "GET", "/billing/cards", nil)
    if err != nil {
        return nil, err
    }
    defer res.Body.Close()

    err = json.NewDecoder(res.Body).Decode(&cards)
    return
}

// GetPlans returns the user's plan(s).
func (c *Client) GetPlans(token string) (plans []Plan, err error) {
    res, err := c.request(token, "GET", "/billing/plans", nil)
    if err != nil {
        return nil, err
    }
    defer res.Body.Close()

    err = json.NewDecoder(res.Body).Decode(&plans)
    return
}

// RemoveCard removes a user's card by id.
func (c *Client) RemoveCard(token, id string) error {
    res, err := c.request(token, "DELETE", "/billing/cards/"+id, nil)
    if err != nil {
        return err
    }
    defer res.Body.Close()

    return nil
}

// AddPlan subscribes to plan.
func (c *Client) AddPlan(token, product, interval, coupon string) error {
    in := struct {
        Product  string `json:"product"`
        Interval string `json:"interval"`
        Coupon   string `json:"coupon"`
    }{
        Product:  product,
        Interval: interval,
        Coupon:   coupon,
    }

    res, err := c.requestJSON(token, "PUT", "/billing/plans", in)
    if err != nil {
        return err
    }
    defer res.Body.Close()

    return nil
}

// AddTeam adds a new team.
func (c *Client) AddTeam(token, id, name string) error {
    in := struct {
        ID   string `json:"id"`
        Name string `json:"name"`
    }{
        ID:   id,
        Name: name,
    }

    res, err := c.requestJSON(token, "POST", "/", in)
    if err != nil {
        return err
    }
    defer res.Body.Close()

    return nil
}

// AddInvite adds a team invitation.
func (c *Client) AddInvite(token, email string) error {
    in := struct {
        Email string `json:"email"`
    }{
        Email: email,
    }

    res, err := c.requestJSON(token, "POST", "/invites", in)
    if err != nil {
        return err
    }
    defer res.Body.Close()

    return nil
}

// RemoveMember removes a team member or invitation if present.
func (c *Client) RemoveMember(token, email string) error {
    in := struct {
        Email string `json:"email"`
    }{
        Email: email,
    }

    res, err := c.requestJSON(token, "DELETE", "/member", in)
    if err != nil {
        return err
    }
    defer res.Body.Close()

    return nil
}

// RemovePlan unsubscribes from a plan.
func (c *Client) RemovePlan(token, product string) error {
    path := fmt.Sprintf("/billing/plans/%s", product)
    res, err := c.request(token, "DELETE", path, nil)
    if err != nil {
        return err
    }
    defer res.Body.Close()

    return nil
}

// AddFeedback sends customer feedback.
func (c *Client) AddFeedback(token, message string) error {
    in := struct {
        Message string `json:"message"`
    }{
        Message: message,
    }

    res, err := c.requestJSON(token, "POST", "/feedback", in)
    if err != nil {
        return err
    }
    defer res.Body.Close()

    return nil
}

// Login signs in the user.
func (c *Client) Login(email, team string) (code string, err error) {
    in := struct {
        Email string `json:"email"`
        Team  string `json:"team"`
    }{
        Email: email,
        Team:  team,
    }

    res, err := c.requestJSON("", "POST", "/login", in)
    if err != nil {
        return "", err
    }
    defer res.Body.Close()

    var out struct {
        Code string `json:"code"`
    }

    err = json.NewDecoder(res.Body).Decode(&out)
    code = out.Code
    return
}

// LoginWithToken signs in with the given email by
// sending a verification email and returning
// a code which can be exchanged for an access key.
//
// When an auth token is provided the user is already
// authenticated, so this can be used to switch to
// another team, if the user is a member or owner.
//
// The team id is optional, and may only be used when
// the user's email has been invited to the team.
func (c *Client) LoginWithToken(token, email, team string) (code string, err error) {
    in := struct {
        Email string `json:"email"`
        Team  string `json:"team"`
    }{
        Email: email,
        Team:  team,
    }

    res, err := c.requestJSON(token, "POST", "/login", in)
    if err != nil {
        return "", err
    }
    defer res.Body.Close()

    var out struct {
        Code string `json:"code"`
    }

    err = json.NewDecoder(res.Body).Decode(&out)
    code = out.Code
    return
}

// GetAccessToken with the given email, team, and code.
func (c *Client) GetAccessToken(email, team, code string) (key string, err error) {
    in := struct {
        Email string `json:"email"`
        Team  string `json:"team"`
        Code  string `json:"code"`
    }{
        Email: email,
        Team:  team,
        Code:  code,
    }

    res, err := c.requestJSON("", "POST", "/access_token", in)
    if err != nil {
        return "", err
    }
    defer res.Body.Close()

    b, err := ioutil.ReadAll(res.Body)
    if err != nil {
        return "", err
    }

    return string(b), nil
}

// PollAccessToken polls for an access token.
func (c *Client) PollAccessToken(ctx context.Context, email, team, code string) (key string, err error) {
    keyC := make(chan string, 1)
    errC := make(chan error, 1)

    go func() {
        for {
            key, err = c.GetAccessToken(email, team, code)

            if err, ok := err.(*Error); ok && err.Status == http.StatusUnauthorized {
                time.Sleep(5 * time.Second)
                continue
            }

            if err != nil {
                errC <- err
                return
            }

            keyC <- key
        }
    }()

    select {
    case <-ctx.Done():
        return "", ctx.Err()
    case e := <-errC:
        return "", e
    case k := <-keyC:
        return k, nil
    }
}

// requestJSON helper.
func (c *Client) requestJSON(token, method, path string, v interface{}) (*http.Response, error) {
    b, err := json.Marshal(v)
    if err != nil {
        return nil, errors.Wrap(err, "marshaling")
    }

    return c.request(token, method, path, bytes.NewReader(b))
}

// request helper.
func (c *Client) request(token, method, path string, body io.Reader) (*http.Response, error) {
    req, err := http.NewRequest(method, c.url+path, body)
    if err != nil {
        return nil, errors.Wrap(err, "creating request")
    }

    if body != nil {
        req.Header.Set("Content-Type", "application/json")
    }

    if token != "" {
        req.Header.Set("Authorization", "Bearer "+token)
    }

    req.Header.Set("Accept", "application/json")

    res, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, errors.Wrap(err, "requesting")
    }

    if res.StatusCode >= 400 {
        b, _ := ioutil.ReadAll(res.Body)
        res.Body.Close()
        return nil, &Error{
            Message: strings.TrimSpace(string(b)),
            Status:  res.StatusCode,
        }
    }

    return res, nil
}