ducky/subscriptions

Paddy 2015-09-30 Parent:0ae1ff0ee306 Child:aab6ba5ae392

14:fb2c0e498e37 Go to Latest

ducky/subscriptions/subscription.go

Update with comments for all exported functions. We now have golint-approved comments for all the exported functions in the subscriptions package. Next challenge: all the sub-packages!

History
1 package subscriptions
3 import (
4 "errors"
5 "time"
7 "code.secondbit.org/uuid.hg"
8 )
10 var (
11 // ErrSubscriptionAlreadyExists is returned when a Subscription
12 // with an identical ID already exists in the subscriptionStore.
13 ErrSubscriptionAlreadyExists = errors.New("Subscription already exists")
14 // ErrSubscriptionNotFound is returned when a single Subscription
15 // is acted upon or requested, but cannot be found.
16 ErrSubscriptionNotFound = errors.New("Subscription not found")
17 // ErrStripeSubscriptionAlreadyExists is returned when a Subscription
18 // is created or updates its StripeSubscription property, but that
19 // StripeSubscription is already associated with another Subscription.
20 ErrStripeSubscriptionAlreadyExists = errors.New("Stripe subscription already assigned to another Subscription")
21 // ErrSubscriptionChangeEmpty is returned when a SubscriptionChange
22 // is empty but is passed to subscriptionStore.UpdateSubscription
23 // anyways.
24 ErrSubscriptionChangeEmpty = errors.New("SubscriptionChange is empty")
25 // ErrNoSubscriptionID is returned when one or more Subscription IDs
26 // are required, but none are provided.
27 ErrNoSubscriptionID = errors.New("no Subscription ID provided")
29 planOptions = map[string]bool{
30 "basic_monthly": false,
31 "basic_yearly": false,
32 "supporter_monthly": false,
33 "supporter_yearly": false,
34 "free": true,
35 PendingPlan: true,
36 }
38 // Version tracks the build ID of the binary, set using
39 // ldflags.
40 Version string
41 )
43 // Subscription represents the state of a user's payments. It holds
44 // metadata about the last time a user was charged, how much a user
45 // should be charged, how to charge a user and how much to charge
46 // the user.
47 type Subscription struct {
48 UserID uuid.ID `json:"user_id"`
49 StripeSubscription string `json:"stripe_subscription"`
50 Plan string `json:"plan"`
51 Status string `json:"status"`
52 Canceling bool `json:"canceling"`
53 Created time.Time `json:"created"`
54 TrialStart time.Time `json:"trial_start,omitempty"`
55 TrialEnd time.Time `json:"trial_end,omitempty"`
56 PeriodStart time.Time `json:"period_start,omitempty"`
57 PeriodEnd time.Time `json:"period_end,omitempty"`
58 CanceledAt time.Time `json:"canceled_at,omitempty"`
59 FailedChargeAttempts int `json:"failed_charge_attempts"`
60 LastFailedCharge time.Time `json:"last_failed_charge,omitempty"`
61 LastNotified time.Time `json:"last_notified,omitempty"`
62 }
64 // SubscriptionChange represents desired changes to a Subscription
65 // object. A nil value means that property should remain unchanged.
66 type SubscriptionChange struct {
67 UserID uuid.ID `json:"user_id"`
69 // User-controlled
70 StripeSource *string `json:"stripe_source,omitempty"`
71 Email *string `json:"email,omitempty"`
72 Plan *string `json:"plan,omitempty"`
73 Canceling *bool `json:"cenceling,omitempty"`
75 // System-controlled
76 StripeSubscription *string `json:"stripe_subscription,omitempty"`
77 Status *string `json:"status,omitempty"`
78 TrialStart *time.Time `json:"trial_start,omitempty"`
79 TrialEnd *time.Time `json:"trial_end,omitempty"`
80 PeriodStart *time.Time `json:"period_start,omitempty"`
81 PeriodEnd *time.Time `json:"period_end,omitempty"`
82 CanceledAt *time.Time `json:"canceled_at,omitempty"`
83 FailedChargeAttempts *int `json:"failed_charge_attempts,omitempty"`
84 LastFailedCharge *time.Time `json:"last_failed_charge,omitempty"`
85 LastNotified *time.Time `json:"last_notified,omitempty"`
86 }
88 // IsEmpty returns true if the SubscriptionChange doesn't request
89 // a change to any property of the Subscription.
90 func (change SubscriptionChange) IsEmpty() bool {
91 if change.StripeSource != nil {
92 return false
93 }
94 if change.Email != nil {
95 return false
96 }
97 if change.Plan != nil {
98 return false
99 }
100 if change.Canceling != nil {
101 return false
102 }
103 if change.StripeSubscription != nil {
104 return false
105 }
106 if change.Status != nil {
107 return false
108 }
109 if change.TrialStart != nil {
110 return false
111 }
112 if change.TrialEnd != nil {
113 return false
114 }
115 if change.PeriodStart != nil {
116 return false
117 }
118 if change.PeriodEnd != nil {
119 return false
120 }
121 if change.CanceledAt != nil {
122 return false
123 }
124 if change.LastNotified != nil {
125 return false
126 }
127 if change.LastFailedCharge != nil {
128 return false
129 }
130 if change.FailedChargeAttempts != nil {
131 return false
132 }
133 return true
134 }
136 // ApplyChange updates a Subscription based on the changes requested
137 // by a SubscriptionChange.
138 func (s *Subscription) ApplyChange(change SubscriptionChange) {
139 if change.StripeSubscription != nil {
140 s.StripeSubscription = *change.StripeSubscription
141 }
142 if change.Plan != nil {
143 s.Plan = *change.Plan
144 }
145 if change.Status != nil {
146 s.Status = *change.Status
147 }
148 if change.Canceling != nil {
149 s.Canceling = *change.Canceling
150 }
151 if change.TrialStart != nil {
152 s.TrialStart = *change.TrialStart
153 }
154 if change.TrialEnd != nil {
155 s.TrialEnd = *change.TrialEnd
156 }
157 if change.PeriodStart != nil {
158 s.PeriodStart = *change.PeriodStart
159 }
160 if change.PeriodEnd != nil {
161 s.PeriodEnd = *change.PeriodEnd
162 }
163 if change.CanceledAt != nil {
164 s.CanceledAt = *change.CanceledAt
165 }
166 if change.LastFailedCharge != nil {
167 s.LastFailedCharge = *change.LastFailedCharge
168 }
169 if change.LastNotified != nil {
170 s.LastNotified = *change.LastNotified
171 }
172 if change.FailedChargeAttempts != nil {
173 s.FailedChargeAttempts = *change.FailedChargeAttempts
174 }
175 }
177 // IsAcceptablePlan returns true if the user can select the specified
178 // plan, taking into account their admin status. If a plan exists, and
179 // is not designated as an admin-only plan, any user selecting it will
180 // return true. If a plan exists, but is designated as admin-only,
181 // IsAcceptablePlan will only return true if admin is true. If the plan
182 // doesn't exist, IsAcceptablePlan always returns false.
183 func IsAcceptablePlan(plan string, admin bool) bool {
184 for p, adminOnly := range planOptions {
185 if plan == p {
186 return admin || adminOnly == false
187 }
188 }
189 return false
190 }
192 // SubscriptionStats represents a set of statistics about our Subscription
193 // data that will be exposed to the Prometheus scraper.
194 type SubscriptionStats struct {
195 Number int64
196 Canceling int64
197 Failing int64
198 Plans map[string]int64
199 // BUG(paddy): Currently, Kubernetes doesn't offer any way to contact _all_ nodes in a service.
200 // Because of this, we can only report stats that will be identical across nodes, e.g. stats
201 // that come from the database. More info here: https://github.com/GoogleCloudPlatform/kubernetes/issues/6666
202 // In the future, we'll need per-node metrics. For now, we'll make do.
203 //
204 // Actually, as of https://github.com/GoogleCloudPlatform/kubernetes/pull/9073, we may be all set:
205 // "SRV Records are created for named ports that are part of normal or Headless Services. For each named port,
206 // the SRV record would have the form _my-port-name._my-port-protocol.my-svc.my-namespace.svc.cluster.local.
207 // For a regular service, this resolves to the port number and the CNAME: my-svc.my-namespace.svc.cluster.local.
208 // For a headless service, this resolves to multiple answers, one for each pod that is backing the service, and
209 // contains the port number and a CNAME of the pod with the format auto-generated-name.my-svc.my-namespace.svc.cluster.local
210 // SRV records always contain the 'svc' segment in them and are not supported for old-style CNAMEs where the 'svc' segment
211 // was omitted."
212 }
214 // SubscriptionStore is an interface describing datastore interactions for
215 // Subscriptions.
216 type SubscriptionStore interface {
217 Reset() error
218 CreateSubscription(sub Subscription) error
219 UpdateSubscription(id uuid.ID, change SubscriptionChange) error
220 DeleteSubscription(id uuid.ID) error
221 GetSubscriptions(ids []uuid.ID) (map[string]Subscription, error)
222 GetSubscriptionStats() (SubscriptionStats, error)
223 }