ducky/subscriptions
2015-10-04
Parent:aab6ba5ae392
ducky/subscriptions/subscription.go
Document our client to make golint happy. Take care of all the documentation warnings in the client subpackage, which means golint now returns successfully.
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 }