github.com/apex/up@v1.7.1/internal/account/account.go (about)

     1  package account
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"net/http"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/pkg/errors"
    15  )
    16  
    17  // Error is a client error.
    18  type Error struct {
    19  	Message string
    20  	Status  int
    21  }
    22  
    23  // Error implementation.
    24  func (e *Error) Error() string {
    25  	return e.Message
    26  }
    27  
    28  // Card model.
    29  type Card struct {
    30  	ID       string `json:"id"`
    31  	Brand    string `json:"brand"`
    32  	LastFour string `json:"last_four"`
    33  }
    34  
    35  // CouponDuration is the coupon duration.
    36  type CouponDuration string
    37  
    38  // Durations.
    39  const (
    40  	Forever   CouponDuration = "forever"
    41  	Once                     = "once"
    42  	Repeating                = "repeating"
    43  )
    44  
    45  // Coupon model.
    46  type Coupon struct {
    47  	ID             string         `json:"id"`
    48  	Amount         int            `json:"amount"`
    49  	Percent        int            `json:"percent"`
    50  	Duration       CouponDuration `json:"duration"`
    51  	DurationPeriod int            `json:"duration_period"`
    52  }
    53  
    54  // Discount returns the final price from the given amount.
    55  func (c *Coupon) Discount(n int) int {
    56  	if c.Amount != 0 {
    57  		return n - c.Amount
    58  	}
    59  
    60  	return n - int(float64(n)*(float64(c.Percent)/100))
    61  }
    62  
    63  // Description returns a humanized description of the savings.
    64  func (c *Coupon) Description() (s string) {
    65  	switch {
    66  	case c.Amount != 0:
    67  		n := fmt.Sprintf("%0.2f", float64(c.Amount)/100)
    68  		s += fmt.Sprintf("$%s off", strings.Replace(n, ".00", "", 1))
    69  	case c.Percent != 0:
    70  		s += fmt.Sprintf("%d%% off", c.Percent)
    71  	}
    72  
    73  	switch c.Duration {
    74  	case Repeating:
    75  		s += fmt.Sprintf(" for %d months", c.DurationPeriod)
    76  	default:
    77  		s += fmt.Sprintf(" %s", c.Duration)
    78  	}
    79  
    80  	return s
    81  }
    82  
    83  // Discount model.
    84  type Discount struct {
    85  	Coupon Coupon `json:"coupon"`
    86  }
    87  
    88  // Plan model.
    89  type Plan struct {
    90  	ID         string    `json:"id"`
    91  	Name       string    `json:"name"`
    92  	Product    string    `json:"product"`
    93  	Plan       string    `json:"plan"`
    94  	Amount     int       `json:"amount"`
    95  	Interval   string    `json:"interval"`
    96  	Status     string    `json:"status"`
    97  	Canceled   bool      `json:"canceled"`
    98  	Discount   *Discount `json:"discount"`
    99  	CreatedAt  time.Time `json:"created_at"`
   100  	CanceledAt time.Time `json:"canceled_at"`
   101  }
   102  
   103  // User model.
   104  type User struct {
   105  	Email     string    `json:"email"`
   106  	CreatedAt time.Time `json:"created_at"`
   107  }
   108  
   109  // Team model.
   110  type Team struct {
   111  	ID        string    `json:"id"`
   112  	Name      string    `json:"name"`
   113  	Owner     string    `json:"owner"`
   114  	Type      string    `json:"type"`
   115  	Card      *Card     `json:"card"`
   116  	Members   []User    `json:"members"`
   117  	Invites   []string  `json:"invites"`
   118  	UpdatedAt time.Time `json:"updated_at"`
   119  	CreatedAt time.Time `json:"created_at"`
   120  }
   121  
   122  // Client implementation.
   123  type Client struct {
   124  	url string
   125  }
   126  
   127  // New client.
   128  func New(url string) *Client {
   129  	return &Client{
   130  		url: url,
   131  	}
   132  }
   133  
   134  // GetCoupon by id.
   135  func (c *Client) GetCoupon(id string) (coupon *Coupon, err error) {
   136  	res, err := c.request("", "GET", "/billing/coupons/"+id, nil)
   137  	if err != nil {
   138  		return nil, err
   139  	}
   140  	defer res.Body.Close()
   141  
   142  	coupon = new(Coupon)
   143  	err = json.NewDecoder(res.Body).Decode(coupon)
   144  	return
   145  }
   146  
   147  // AddCard adds or updates the default card via stripe token.
   148  func (c *Client) AddCard(token, cardToken string) error {
   149  	in := struct {
   150  		Token string `json:"token"`
   151  	}{
   152  		Token: cardToken,
   153  	}
   154  
   155  	res, err := c.requestJSON(token, "POST", "/billing/cards", in)
   156  	if err != nil {
   157  		return err
   158  	}
   159  	defer res.Body.Close()
   160  
   161  	return nil
   162  }
   163  
   164  // GetTeam returns the active team.
   165  func (c *Client) GetTeam(token string) (*Team, error) {
   166  	res, err := c.request(token, "GET", "/", nil)
   167  	if err != nil {
   168  		return nil, err
   169  	}
   170  	defer res.Body.Close()
   171  
   172  	var t Team
   173  	return &t, json.NewDecoder(res.Body).Decode(&t)
   174  }
   175  
   176  // GetCards returns the user's cards.
   177  func (c *Client) GetCards(token string) (cards []Card, err error) {
   178  	res, err := c.request(token, "GET", "/billing/cards", nil)
   179  	if err != nil {
   180  		return nil, err
   181  	}
   182  	defer res.Body.Close()
   183  
   184  	err = json.NewDecoder(res.Body).Decode(&cards)
   185  	return
   186  }
   187  
   188  // GetPlans returns the user's plan(s).
   189  func (c *Client) GetPlans(token string) (plans []Plan, err error) {
   190  	res, err := c.request(token, "GET", "/billing/plans", nil)
   191  	if err != nil {
   192  		return nil, err
   193  	}
   194  	defer res.Body.Close()
   195  
   196  	err = json.NewDecoder(res.Body).Decode(&plans)
   197  	return
   198  }
   199  
   200  // RemoveCard removes a user's card by id.
   201  func (c *Client) RemoveCard(token, id string) error {
   202  	res, err := c.request(token, "DELETE", "/billing/cards/"+id, nil)
   203  	if err != nil {
   204  		return err
   205  	}
   206  	defer res.Body.Close()
   207  
   208  	return nil
   209  }
   210  
   211  // AddPlan subscribes to plan.
   212  func (c *Client) AddPlan(token, product, interval, coupon string) error {
   213  	in := struct {
   214  		Product  string `json:"product"`
   215  		Interval string `json:"interval"`
   216  		Coupon   string `json:"coupon"`
   217  	}{
   218  		Product:  product,
   219  		Interval: interval,
   220  		Coupon:   coupon,
   221  	}
   222  
   223  	res, err := c.requestJSON(token, "PUT", "/billing/plans", in)
   224  	if err != nil {
   225  		return err
   226  	}
   227  	defer res.Body.Close()
   228  
   229  	return nil
   230  }
   231  
   232  // AddTeam adds a new team.
   233  func (c *Client) AddTeam(token, id, name string) error {
   234  	in := struct {
   235  		ID   string `json:"id"`
   236  		Name string `json:"name"`
   237  	}{
   238  		ID:   id,
   239  		Name: name,
   240  	}
   241  
   242  	res, err := c.requestJSON(token, "POST", "/", in)
   243  	if err != nil {
   244  		return err
   245  	}
   246  	defer res.Body.Close()
   247  
   248  	return nil
   249  }
   250  
   251  // AddInvite adds a team invitation.
   252  func (c *Client) AddInvite(token, email string) error {
   253  	in := struct {
   254  		Email string `json:"email"`
   255  	}{
   256  		Email: email,
   257  	}
   258  
   259  	res, err := c.requestJSON(token, "POST", "/invites", in)
   260  	if err != nil {
   261  		return err
   262  	}
   263  	defer res.Body.Close()
   264  
   265  	return nil
   266  }
   267  
   268  // RemoveMember removes a team member or invitation if present.
   269  func (c *Client) RemoveMember(token, email string) error {
   270  	in := struct {
   271  		Email string `json:"email"`
   272  	}{
   273  		Email: email,
   274  	}
   275  
   276  	res, err := c.requestJSON(token, "DELETE", "/member", in)
   277  	if err != nil {
   278  		return err
   279  	}
   280  	defer res.Body.Close()
   281  
   282  	return nil
   283  }
   284  
   285  // RemovePlan unsubscribes from a plan.
   286  func (c *Client) RemovePlan(token, product string) error {
   287  	path := fmt.Sprintf("/billing/plans/%s", product)
   288  	res, err := c.request(token, "DELETE", path, nil)
   289  	if err != nil {
   290  		return err
   291  	}
   292  	defer res.Body.Close()
   293  
   294  	return nil
   295  }
   296  
   297  // AddFeedback sends customer feedback.
   298  func (c *Client) AddFeedback(token, message string) error {
   299  	in := struct {
   300  		Message string `json:"message"`
   301  	}{
   302  		Message: message,
   303  	}
   304  
   305  	res, err := c.requestJSON(token, "POST", "/feedback", in)
   306  	if err != nil {
   307  		return err
   308  	}
   309  	defer res.Body.Close()
   310  
   311  	return nil
   312  }
   313  
   314  // Login signs in the user.
   315  func (c *Client) Login(email, team string) (code string, err error) {
   316  	in := struct {
   317  		Email string `json:"email"`
   318  		Team  string `json:"team"`
   319  	}{
   320  		Email: email,
   321  		Team:  team,
   322  	}
   323  
   324  	res, err := c.requestJSON("", "POST", "/login", in)
   325  	if err != nil {
   326  		return "", err
   327  	}
   328  	defer res.Body.Close()
   329  
   330  	var out struct {
   331  		Code string `json:"code"`
   332  	}
   333  
   334  	err = json.NewDecoder(res.Body).Decode(&out)
   335  	code = out.Code
   336  	return
   337  }
   338  
   339  // LoginWithToken signs in with the given email by
   340  // sending a verification email and returning
   341  // a code which can be exchanged for an access key.
   342  //
   343  // When an auth token is provided the user is already
   344  // authenticated, so this can be used to switch to
   345  // another team, if the user is a member or owner.
   346  //
   347  // The team id is optional, and may only be used when
   348  // the user's email has been invited to the team.
   349  func (c *Client) LoginWithToken(token, email, team string) (code string, err error) {
   350  	in := struct {
   351  		Email string `json:"email"`
   352  		Team  string `json:"team"`
   353  	}{
   354  		Email: email,
   355  		Team:  team,
   356  	}
   357  
   358  	res, err := c.requestJSON(token, "POST", "/login", in)
   359  	if err != nil {
   360  		return "", err
   361  	}
   362  	defer res.Body.Close()
   363  
   364  	var out struct {
   365  		Code string `json:"code"`
   366  	}
   367  
   368  	err = json.NewDecoder(res.Body).Decode(&out)
   369  	code = out.Code
   370  	return
   371  }
   372  
   373  // GetAccessToken with the given email, team, and code.
   374  func (c *Client) GetAccessToken(email, team, code string) (key string, err error) {
   375  	in := struct {
   376  		Email string `json:"email"`
   377  		Team  string `json:"team"`
   378  		Code  string `json:"code"`
   379  	}{
   380  		Email: email,
   381  		Team:  team,
   382  		Code:  code,
   383  	}
   384  
   385  	res, err := c.requestJSON("", "POST", "/access_token", in)
   386  	if err != nil {
   387  		return "", err
   388  	}
   389  	defer res.Body.Close()
   390  
   391  	b, err := ioutil.ReadAll(res.Body)
   392  	if err != nil {
   393  		return "", err
   394  	}
   395  
   396  	return string(b), nil
   397  }
   398  
   399  // PollAccessToken polls for an access token.
   400  func (c *Client) PollAccessToken(ctx context.Context, email, team, code string) (key string, err error) {
   401  	keyC := make(chan string, 1)
   402  	errC := make(chan error, 1)
   403  
   404  	go func() {
   405  		for {
   406  			key, err = c.GetAccessToken(email, team, code)
   407  
   408  			if err, ok := err.(*Error); ok && err.Status == http.StatusUnauthorized {
   409  				time.Sleep(5 * time.Second)
   410  				continue
   411  			}
   412  
   413  			if err != nil {
   414  				errC <- err
   415  				return
   416  			}
   417  
   418  			keyC <- key
   419  		}
   420  	}()
   421  
   422  	select {
   423  	case <-ctx.Done():
   424  		return "", ctx.Err()
   425  	case e := <-errC:
   426  		return "", e
   427  	case k := <-keyC:
   428  		return k, nil
   429  	}
   430  }
   431  
   432  // requestJSON helper.
   433  func (c *Client) requestJSON(token, method, path string, v interface{}) (*http.Response, error) {
   434  	b, err := json.Marshal(v)
   435  	if err != nil {
   436  		return nil, errors.Wrap(err, "marshaling")
   437  	}
   438  
   439  	return c.request(token, method, path, bytes.NewReader(b))
   440  }
   441  
   442  // request helper.
   443  func (c *Client) request(token, method, path string, body io.Reader) (*http.Response, error) {
   444  	req, err := http.NewRequest(method, c.url+path, body)
   445  	if err != nil {
   446  		return nil, errors.Wrap(err, "creating request")
   447  	}
   448  
   449  	if body != nil {
   450  		req.Header.Set("Content-Type", "application/json")
   451  	}
   452  
   453  	if token != "" {
   454  		req.Header.Set("Authorization", "Bearer "+token)
   455  	}
   456  
   457  	req.Header.Set("Accept", "application/json")
   458  
   459  	res, err := http.DefaultClient.Do(req)
   460  	if err != nil {
   461  		return nil, errors.Wrap(err, "requesting")
   462  	}
   463  
   464  	if res.StatusCode >= 400 {
   465  		b, _ := ioutil.ReadAll(res.Body)
   466  		res.Body.Close()
   467  		return nil, &Error{
   468  			Message: strings.TrimSpace(string(b)),
   469  			Status:  res.StatusCode,
   470  		}
   471  	}
   472  
   473  	return res, nil
   474  }