ducky/subscriptions

Paddy 2015-07-13 Parent:c4cfceb2f2fb Child:0ae1ff0ee306

7:9e138933e4ce Go to Latest

ducky/subscriptions/subscription.go

Create a client for working with subscriptions. We mostly copied our code.secondbit.org/auth.hg/client package to create a simple client library for communicating with our Subscriptions API. Right now, the client only has support for creating a subscription. It remains untested, but it builds.

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 string
39 )
41 // Subscription represents the state of a user's payments. It holds
42 // metadata about the last time a user was charged, how much a user
43 // should be charged, how to charge a user and how much to charge
44 // the user.
45 type Subscription struct {
46 UserID uuid.ID `json:"user_id"`
47 StripeSubscription string `json:"stripe_subscription"`
48 Plan string `json:"plan"`
49 Status string `json:"status"`
50 Canceling bool `json:"canceling"`
51 Created time.Time `json:"created"`
52 TrialStart time.Time `json:"trial_start,omitempty"`
53 TrialEnd time.Time `json:"trial_end,omitempty"`
54 PeriodStart time.Time `json:"period_start,omitempty"`
55 PeriodEnd time.Time `json:"period_end,omitempty"`
56 CanceledAt time.Time `json:"canceled_at,omitempty"`
57 FailedChargeAttempts int `json:"failed_charge_attempts"`
58 LastFailedCharge time.Time `json:"last_failed_charge,omitempty"`
59 LastNotified time.Time `json:"last_notified,omitempty"`
60 }
62 // SubscriptionChange represents desired changes to a Subscription
63 // object. A nil value means that property should remain unchanged.
64 type SubscriptionChange struct {
65 UserID uuid.ID `json:"user_id"`
67 // User-controlled
68 StripeSource *string `json:"stripe_source,omitempty"`
69 Email *string `json:"email,omitempty"`
70 Plan *string `json:"plan,omitempty"`
71 Canceling *bool `json:"cenceling,omitempty"`
73 // System-controlled
74 StripeSubscription *string `json:"stripe_subscription,omitempty"`
75 Status *string `json:"status,omitempty"`
76 TrialStart *time.Time `json:"trial_start,omitempty"`
77 TrialEnd *time.Time `json:"trial_end,omitempty"`
78 PeriodStart *time.Time `json:"period_start,omitempty"`
79 PeriodEnd *time.Time `json:"period_end,omitempty"`
80 CanceledAt *time.Time `json:"canceled_at,omitempty"`
81 FailedChargeAttempts *int `json:"failed_charge_attempts,omitempty"`
82 LastFailedCharge *time.Time `json:"last_failed_charge,omitempty"`
83 LastNotified *time.Time `json:"last_notified,omitempty"`
84 }
86 // IsEmpty returns true if the SubscriptionChange doesn't request
87 // a change to any property of the Subscription.
88 func (change SubscriptionChange) IsEmpty() bool {
89 if change.StripeSource != nil {
90 return false
91 }
92 if change.Email != nil {
93 return false
94 }
95 if change.Plan != nil {
96 return false
97 }
98 if change.Canceling != nil {
99 return false
100 }
101 if change.StripeSubscription != nil {
102 return false
103 }
104 if change.Status != nil {
105 return false
106 }
107 if change.TrialStart != nil {
108 return false
109 }
110 if change.TrialEnd != nil {
111 return false
112 }
113 if change.PeriodStart != nil {
114 return false
115 }
116 if change.PeriodEnd != nil {
117 return false
118 }
119 if change.CanceledAt != nil {
120 return false
121 }
122 if change.LastNotified != nil {
123 return false
124 }
125 if change.LastFailedCharge != nil {
126 return false
127 }
128 if change.FailedChargeAttempts != nil {
129 return false
130 }
131 return true
132 }
134 // ApplyChange updates a Subscription based on the changes requested
135 // by a SubscriptionChange.
136 func (s *Subscription) ApplyChange(change SubscriptionChange) {
137 if change.StripeSubscription != nil {
138 s.StripeSubscription = *change.StripeSubscription
139 }
140 if change.Plan != nil {
141 s.Plan = *change.Plan
142 }
143 if change.Status != nil {
144 s.Status = *change.Status
145 }
146 if change.Canceling != nil {
147 s.Canceling = *change.Canceling
148 }
149 if change.TrialStart != nil {
150 s.TrialStart = *change.TrialStart
151 }
152 if change.TrialEnd != nil {
153 s.TrialEnd = *change.TrialEnd
154 }
155 if change.PeriodStart != nil {
156 s.PeriodStart = *change.PeriodStart
157 }
158 if change.PeriodEnd != nil {
159 s.PeriodEnd = *change.PeriodEnd
160 }
161 if change.CanceledAt != nil {
162 s.CanceledAt = *change.CanceledAt
163 }
164 if change.LastFailedCharge != nil {
165 s.LastFailedCharge = *change.LastFailedCharge
166 }
167 if change.LastNotified != nil {
168 s.LastNotified = *change.LastNotified
169 }
170 if change.FailedChargeAttempts != nil {
171 s.FailedChargeAttempts = *change.FailedChargeAttempts
172 }
173 }
175 func ChangingSystemProperties(change SubscriptionChange) []string {
176 var changes []string
177 if change.StripeSubscription != nil {
178 changes = append(changes, "/stripe_subscription")
179 }
180 if change.Status != nil {
181 changes = append(changes, "/status")
182 }
183 if change.TrialStart != nil {
184 changes = append(changes, "/trial_start")
185 }
186 if change.TrialEnd != nil {
187 changes = append(changes, "/trial_end")
188 }
189 if change.PeriodStart != nil {
190 changes = append(changes, "/period_start")
191 }
192 if change.PeriodEnd != nil {
193 changes = append(changes, "/period_end")
194 }
195 if change.CanceledAt != nil {
196 changes = append(changes, "/canceled_at")
197 }
198 if change.FailedChargeAttempts != nil {
199 changes = append(changes, "/failed_charge_attempts")
200 }
201 if change.LastFailedCharge != nil {
202 changes = append(changes, "/last_failed_charge")
203 }
204 if change.LastNotified != nil {
205 changes = append(changes, "/last_notified")
206 }
207 return changes
208 }
210 func IsAcceptablePlan(plan string, admin bool) bool {
211 for p, adminOnly := range planOptions {
212 if plan == p {
213 return admin || adminOnly == false
214 }
215 }
216 return false
217 }
219 // SubscriptionStats represents a set of statistics about our Subscription
220 // data that will be exposed to the Prometheus scraper.
221 type SubscriptionStats struct {
222 Number int64
223 Canceling int64
224 Failing int64
225 Plans map[string]int64
226 // BUG(paddy): Currently, Kubernetes doesn't offer any way to contact _all_ nodes in a service.
227 // Because of this, we can only report stats that will be identical across nodes, e.g. stats
228 // that come from the database. More info here: https://github.com/GoogleCloudPlatform/kubernetes/issues/6666
229 // In the future, we'll need per-node metrics. For now, we'll make do.
230 //
231 // Actually, as of https://github.com/GoogleCloudPlatform/kubernetes/pull/9073, we may be all set:
232 // "SRV Records are created for named ports that are part of normal or Headless Services. For each named port,
233 // the SRV record would have the form _my-port-name._my-port-protocol.my-svc.my-namespace.svc.cluster.local.
234 // For a regular service, this resolves to the port number and the CNAME: my-svc.my-namespace.svc.cluster.local.
235 // For a headless service, this resolves to multiple answers, one for each pod that is backing the service, and
236 // contains the port number and a CNAME of the pod with the format auto-generated-name.my-svc.my-namespace.svc.cluster.local
237 // SRV records always contain the 'svc' segment in them and are not supported for old-style CNAMEs where the 'svc' segment
238 // was omitted."
239 }
241 type SubscriptionStore interface {
242 Reset() error
243 CreateSubscription(sub Subscription) error
244 UpdateSubscription(id uuid.ID, change SubscriptionChange) error
245 DeleteSubscription(id uuid.ID) error
246 GetSubscriptions(ids []uuid.ID) (map[string]Subscription, error)
247 GetSubscriptionStats() (SubscriptionStats, error)
248 }