Update subscription_creator to use the new strategy.
When creating subscriptions through the client, detect when the returned error
is saying the account already has a subscription, or the subscription already
exists in stripe.
Add an UpdateSubscription function that will update a subscription through the
API.
Update our subscription_creator listener to listen for profile creation and
login verification messages. This mainly involved fixing the constants (the
system, model, and topic) that the listener for profile creation was listening
for. It also meant adding a new updateMessageHandler that listens for login
verification, tries to create a subscription that has the user ID and email of
the login that was verified, and if a subscription already exists, updates it
instead to use the email address that was just verified. This will ensure that
users get their receipts automatically emailed to them by Stripe.
7 "code.secondbit.org/uuid.hg"
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
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,
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
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"`
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"`
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"`
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"`
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 {
92 if change.Email != nil {
95 if change.Plan != nil {
98 if change.Canceling != nil {
101 if change.StripeSubscription != nil {
104 if change.Status != nil {
107 if change.TrialStart != nil {
110 if change.TrialEnd != nil {
113 if change.PeriodStart != nil {
116 if change.PeriodEnd != nil {
119 if change.CanceledAt != nil {
122 if change.LastNotified != nil {
125 if change.LastFailedCharge != nil {
128 if change.FailedChargeAttempts != nil {
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
140 if change.Plan != nil {
141 s.Plan = *change.Plan
143 if change.Status != nil {
144 s.Status = *change.Status
146 if change.Canceling != nil {
147 s.Canceling = *change.Canceling
149 if change.TrialStart != nil {
150 s.TrialStart = *change.TrialStart
152 if change.TrialEnd != nil {
153 s.TrialEnd = *change.TrialEnd
155 if change.PeriodStart != nil {
156 s.PeriodStart = *change.PeriodStart
158 if change.PeriodEnd != nil {
159 s.PeriodEnd = *change.PeriodEnd
161 if change.CanceledAt != nil {
162 s.CanceledAt = *change.CanceledAt
164 if change.LastFailedCharge != nil {
165 s.LastFailedCharge = *change.LastFailedCharge
167 if change.LastNotified != nil {
168 s.LastNotified = *change.LastNotified
170 if change.FailedChargeAttempts != nil {
171 s.FailedChargeAttempts = *change.FailedChargeAttempts
175 func ChangingSystemProperties(change SubscriptionChange) []string {
177 if change.StripeSubscription != nil {
178 changes = append(changes, "/stripe_subscription")
180 if change.Status != nil {
181 changes = append(changes, "/status")
183 if change.TrialStart != nil {
184 changes = append(changes, "/trial_start")
186 if change.TrialEnd != nil {
187 changes = append(changes, "/trial_end")
189 if change.PeriodStart != nil {
190 changes = append(changes, "/period_start")
192 if change.PeriodEnd != nil {
193 changes = append(changes, "/period_end")
195 if change.CanceledAt != nil {
196 changes = append(changes, "/canceled_at")
198 if change.FailedChargeAttempts != nil {
199 changes = append(changes, "/failed_charge_attempts")
201 if change.LastFailedCharge != nil {
202 changes = append(changes, "/last_failed_charge")
204 if change.LastNotified != nil {
205 changes = append(changes, "/last_notified")
210 func IsAcceptablePlan(plan string, admin bool) bool {
211 for p, adminOnly := range planOptions {
213 return admin || adminOnly == false
219 // SubscriptionStats represents a set of statistics about our Subscription
220 // data that will be exposed to the Prometheus scraper.
221 type SubscriptionStats struct {
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.
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
241 type SubscriptionStore interface {
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)