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 }