ducky/subscriptions
2015-10-04
Parent:aab6ba5ae392
ducky/subscriptions/subscription.go
Make api subpackage golint-passing. Add comments to all the exported functions, methods, and variables in the api subpackage, to make golint happy. Also, make the individual endpoints in the api subpackage unexported, as there's no real use case for exporting them. The handlers depend on the placeholders in the endpoint, so we need them to be controlled in unison, which means it's probably a bad idea to declare the route outside of the API package. And the only reason to expose the Handler is so people can declare custom endpoints.
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 and not stored in DB (helper properties for the API)
70 StripeSource *string `json:"stripe_source,omitempty"`
71 Email *string `json:"email,omitempty"`
73 // User-controlled and stored in DB
74 Plan *string `json:"plan,omitempty"`
75 Canceling *bool `json:"cenceling,omitempty"`
77 // System-controlled
78 StripeSubscription *string `json:"stripe_subscription,omitempty"`
79 Status *string `json:"status,omitempty"`
80 TrialStart *time.Time `json:"trial_start,omitempty"`
81 TrialEnd *time.Time `json:"trial_end,omitempty"`
82 PeriodStart *time.Time `json:"period_start,omitempty"`
83 PeriodEnd *time.Time `json:"period_end,omitempty"`
84 CanceledAt *time.Time `json:"canceled_at,omitempty"`
85 FailedChargeAttempts *int `json:"failed_charge_attempts,omitempty"`
86 LastFailedCharge *time.Time `json:"last_failed_charge,omitempty"`
87 LastNotified *time.Time `json:"last_notified,omitempty"`
88 }
90 // IsEmpty returns true if the SubscriptionChange doesn't request
91 // a change to any property of the Subscription.
92 func (change SubscriptionChange) IsEmpty() bool {
93 if change.Plan != nil {
94 return false
95 }
96 if change.Canceling != nil {
97 return false
98 }
99 if change.StripeSubscription != nil {
100 return false
101 }
102 if change.Status != nil {
103 return false
104 }
105 if change.TrialStart != nil {
106 return false
107 }
108 if change.TrialEnd != nil {
109 return false
110 }
111 if change.PeriodStart != nil {
112 return false
113 }
114 if change.PeriodEnd != nil {
115 return false
116 }
117 if change.CanceledAt != nil {
118 return false
119 }
120 if change.LastNotified != nil {
121 return false
122 }
123 if change.LastFailedCharge != nil {
124 return false
125 }
126 if change.FailedChargeAttempts != nil {
127 return false
128 }
129 return true
130 }
132 // ApplyChange updates a Subscription based on the changes requested
133 // by a SubscriptionChange.
134 func (s *Subscription) ApplyChange(change SubscriptionChange) {
135 if change.StripeSubscription != nil {
136 s.StripeSubscription = *change.StripeSubscription
137 }
138 if change.Plan != nil {
139 s.Plan = *change.Plan
140 }
141 if change.Status != nil {
142 s.Status = *change.Status
143 }
144 if change.Canceling != nil {
145 s.Canceling = *change.Canceling
146 }
147 if change.TrialStart != nil {
148 s.TrialStart = *change.TrialStart
149 }
150 if change.TrialEnd != nil {
151 s.TrialEnd = *change.TrialEnd
152 }
153 if change.PeriodStart != nil {
154 s.PeriodStart = *change.PeriodStart
155 }
156 if change.PeriodEnd != nil {
157 s.PeriodEnd = *change.PeriodEnd
158 }
159 if change.CanceledAt != nil {
160 s.CanceledAt = *change.CanceledAt
161 }
162 if change.LastFailedCharge != nil {
163 s.LastFailedCharge = *change.LastFailedCharge
164 }
165 if change.LastNotified != nil {
166 s.LastNotified = *change.LastNotified
167 }
168 if change.FailedChargeAttempts != nil {
169 s.FailedChargeAttempts = *change.FailedChargeAttempts
170 }
171 }
173 // IsAcceptablePlan returns true if the user can select the specified
174 // plan, taking into account their admin status. If a plan exists, and
175 // is not designated as an admin-only plan, any user selecting it will
176 // return true. If a plan exists, but is designated as admin-only,
177 // IsAcceptablePlan will only return true if admin is true. If the plan
178 // doesn't exist, IsAcceptablePlan always returns false.
179 func IsAcceptablePlan(plan string, admin bool) bool {
180 for p, adminOnly := range planOptions {
181 if plan == p {
182 return admin || adminOnly == false
183 }
184 }
185 return false
186 }
188 // SubscriptionStats represents a set of statistics about our Subscription
189 // data that will be exposed to the Prometheus scraper.
190 type SubscriptionStats struct {
191 Number int64
192 Canceling int64
193 Failing int64
194 Plans map[string]int64
195 // BUG(paddy): Currently, Kubernetes doesn't offer any way to contact _all_ nodes in a service.
196 // Because of this, we can only report stats that will be identical across nodes, e.g. stats
197 // that come from the database. More info here: https://github.com/GoogleCloudPlatform/kubernetes/issues/6666
198 // In the future, we'll need per-node metrics. For now, we'll make do.
199 //
200 // Actually, as of https://github.com/GoogleCloudPlatform/kubernetes/pull/9073, we may be all set:
201 // "SRV Records are created for named ports that are part of normal or Headless Services. For each named port,
202 // the SRV record would have the form _my-port-name._my-port-protocol.my-svc.my-namespace.svc.cluster.local.
203 // For a regular service, this resolves to the port number and the CNAME: my-svc.my-namespace.svc.cluster.local.
204 // For a headless service, this resolves to multiple answers, one for each pod that is backing the service, and
205 // contains the port number and a CNAME of the pod with the format auto-generated-name.my-svc.my-namespace.svc.cluster.local
206 // SRV records always contain the 'svc' segment in them and are not supported for old-style CNAMEs where the 'svc' segment
207 // was omitted."
208 }
210 // SubscriptionStore is an interface describing datastore interactions for
211 // Subscriptions.
212 type SubscriptionStore interface {
213 Reset() error
214 CreateSubscription(sub Subscription) error
215 UpdateSubscription(id uuid.ID, change SubscriptionChange) error
216 DeleteSubscription(id uuid.ID) error
217 GetSubscriptions(ids []uuid.ID) (map[string]Subscription, error)
218 GetSubscriptionStats() (SubscriptionStats, error)
219 }