ducky/subscriptions
ducky/subscriptions/subscription.go
Export all subscriptionStore methods. We're not going to wrap all our subscriptionStore interactions in a Context type, so we need to expose all the functions so other packages can call them. Also, it's now SubscriptionStore. Also creates a SubscriptionRequest type that is used to create a new Subscription instance, along with a Validate method on it, to detect errors when creating a SubscriptionRequest. Update our CreateSubscription function to be New, and have it take a SubscriptionRequest, a Stripe instance, and a SubscriptionStore as arguments. It'll create the Subscription in the SubscriptionStore, create a customer in Stripe, and create the subscription in Stripe, then associate the Stripe subscription with the created Subscription. Also, fix our StripeSubscriptionChange function to correctly equate a missing/zero Unix timestamp from Stripe with a missing/zero time.Time.
1.1 --- a/subscription.go Tue Jun 16 23:09:59 2015 -0400 1.2 +++ b/subscription.go Mon Jun 22 18:34:07 2015 -0400 1.3 @@ -4,6 +4,7 @@ 1.4 "errors" 1.5 "time" 1.6 1.7 + "code.secondbit.org/api.hg" 1.8 "code.secondbit.org/uuid.hg" 1.9 ) 1.10 1.11 @@ -25,6 +26,10 @@ 1.12 // ErrNoSubscriptionID is returned when one or more Subscription IDs 1.13 // are required, but none are provided. 1.14 ErrNoSubscriptionID = errors.New("no Subscription ID provided") 1.15 + 1.16 + planOptions = []string{"basic_monthly", "basic_yearly", "supporter_monthly", "supporter_yearly"} 1.17 + 1.18 + Version string 1.19 ) 1.20 1.21 // Subscription represents the state of a user's payments. It holds 1.22 @@ -32,20 +37,20 @@ 1.23 // should be charged, how to charge a user and how much to charge 1.24 // the user. 1.25 type Subscription struct { 1.26 - UserID uuid.ID 1.27 - StripeSubscription string 1.28 - Plan string 1.29 - Status string 1.30 - Canceling bool 1.31 - Created time.Time 1.32 - TrialStart time.Time 1.33 - TrialEnd time.Time 1.34 - PeriodStart time.Time 1.35 - PeriodEnd time.Time 1.36 - CanceledAt time.Time 1.37 - FailedChargeAttempts int 1.38 - LastFailedCharge time.Time 1.39 - LastNotified time.Time 1.40 + UserID uuid.ID `json:"user_id"` 1.41 + StripeSubscription string `json:"stripe_subscription"` 1.42 + Plan string `json:"plan"` 1.43 + Status string `json:"status"` 1.44 + Canceling bool `json:"canceling"` 1.45 + Created time.Time `json:"created"` 1.46 + TrialStart time.Time `json:"trial_start,omitempty"` 1.47 + TrialEnd time.Time `json:"trial_end,omitempty"` 1.48 + PeriodStart time.Time `json:"period_start,omitempty"` 1.49 + PeriodEnd time.Time `json:"period_end,omitempty"` 1.50 + CanceledAt time.Time `json:"canceled_at,omitempty"` 1.51 + FailedChargeAttempts int `json:"failed_charge_attempts"` 1.52 + LastFailedCharge time.Time `json:"last_failed_charge,omitempty"` 1.53 + LastNotified time.Time `json:"last_notified,omitempty"` 1.54 } 1.55 1.56 // SubscriptionChange represents desired changes to a Subscription 1.57 @@ -148,6 +153,53 @@ 1.58 } 1.59 } 1.60 1.61 +type SubscriptionRequest struct { 1.62 + UserID uuid.ID `json:"user_id"` 1.63 + Email string `json:"email"` 1.64 + StripeToken string `json:"stripe_token"` 1.65 + Plan string `json:"plan"` 1.66 +} 1.67 + 1.68 +func (req SubscriptionRequest) Validate(user uuid.ID, admin bool) []api.RequestError { 1.69 + var errors []api.RequestError 1.70 + if req.UserID == nil { 1.71 + errors = append(errors, api.RequestError{Slug: api.RequestErrMissing, Field: "/user_id"}) 1.72 + } else if !req.UserID.Equal(user) && !admin { 1.73 + errors = append(errors, api.RequestError{Slug: api.RequestErrAccessDenied, Field: "/user_id"}) 1.74 + } 1.75 + if req.Email == "" { 1.76 + errors = append(errors, api.RequestError{Slug: api.RequestErrMissing, Field: "/email"}) 1.77 + } 1.78 + if req.StripeToken == "" { 1.79 + errors = append(errors, api.RequestError{Slug: api.RequestErrMissing, Field: "/stripe_token"}) 1.80 + } 1.81 + if req.Plan == "" { 1.82 + errors = append(errors, api.RequestError{Slug: api.RequestErrMissing, Field: "/plan"}) 1.83 + } else { 1.84 + var acceptablePlan bool 1.85 + for _, p := range planOptions { 1.86 + if req.Plan == p { 1.87 + acceptablePlan = true 1.88 + break 1.89 + } 1.90 + } 1.91 + if !acceptablePlan { 1.92 + errors = append(errors, api.RequestError{Slug: api.RequestErrInvalidValue, Field: "/plan"}) 1.93 + } 1.94 + } 1.95 + return errors 1.96 +} 1.97 + 1.98 +func SubscriptionFromRequest(req SubscriptionRequest) Subscription { 1.99 + return Subscription{ 1.100 + UserID: req.UserID, 1.101 + StripeSubscription: req.StripeToken, 1.102 + Plan: req.Plan, 1.103 + Created: time.Now(), 1.104 + TrialStart: time.Now(), 1.105 + } 1.106 +} 1.107 + 1.108 // SubscriptionStats represents a set of statistics about our Subscription 1.109 // data that will be exposed to the Prometheus scraper. 1.110 type SubscriptionStats struct { 1.111 @@ -170,11 +222,11 @@ 1.112 // was omitted." 1.113 } 1.114 1.115 -type subscriptionStore interface { 1.116 - reset() error 1.117 - createSubscription(sub Subscription) error 1.118 - updateSubscription(id uuid.ID, change SubscriptionChange) error 1.119 - deleteSubscription(id uuid.ID) error 1.120 - getSubscriptions(ids []uuid.ID) (map[string]Subscription, error) 1.121 - getSubscriptionStats() (SubscriptionStats, error) 1.122 +type SubscriptionStore interface { 1.123 + Reset() error 1.124 + CreateSubscription(sub Subscription) error 1.125 + UpdateSubscription(id uuid.ID, change SubscriptionChange) error 1.126 + DeleteSubscription(id uuid.ID) error 1.127 + GetSubscriptions(ids []uuid.ID) (map[string]Subscription, error) 1.128 + GetSubscriptionStats() (SubscriptionStats, error) 1.129 }