ducky/subscriptions
ducky/subscriptions/subscription.go
Add comments, move ChangingSystemProperties to the api package. Add comments to all our exported types and variables in subscription.go, both to make golint happy and because it's good to have comments. Move the subscriptions.ChangingSystemProperties helper to api.changingSystemProperties, because it returns API-specific strings and there's no real reason it has to be in the subscriptions package--everything it needs to work on is exported.
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 }