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.
| paddy@0 | 1 package subscriptions |
| paddy@0 | 2 |
| paddy@0 | 3 import ( |
| paddy@0 | 4 "errors" |
| paddy@0 | 5 "time" |
| paddy@0 | 6 |
| paddy@0 | 7 "code.secondbit.org/uuid.hg" |
| paddy@0 | 8 ) |
| paddy@0 | 9 |
| paddy@0 | 10 var ( |
| paddy@0 | 11 // ErrSubscriptionAlreadyExists is returned when a Subscription |
| paddy@0 | 12 // with an identical ID already exists in the subscriptionStore. |
| paddy@0 | 13 ErrSubscriptionAlreadyExists = errors.New("Subscription already exists") |
| paddy@0 | 14 // ErrSubscriptionNotFound is returned when a single Subscription |
| paddy@0 | 15 // is acted upon or requested, but cannot be found. |
| paddy@0 | 16 ErrSubscriptionNotFound = errors.New("Subscription not found") |
| paddy@2 | 17 // ErrStripeSubscriptionAlreadyExists is returned when a Subscription |
| paddy@2 | 18 // is created or updates its StripeSubscription property, but that |
| paddy@2 | 19 // StripeSubscription is already associated with another Subscription. |
| paddy@2 | 20 ErrStripeSubscriptionAlreadyExists = errors.New("Stripe subscription already assigned to another Subscription") |
| paddy@0 | 21 // ErrSubscriptionChangeEmpty is returned when a SubscriptionChange |
| paddy@0 | 22 // is empty but is passed to subscriptionStore.UpdateSubscription |
| paddy@0 | 23 // anyways. |
| paddy@0 | 24 ErrSubscriptionChangeEmpty = errors.New("SubscriptionChange is empty") |
| paddy@0 | 25 // ErrNoSubscriptionID is returned when one or more Subscription IDs |
| paddy@0 | 26 // are required, but none are provided. |
| paddy@0 | 27 ErrNoSubscriptionID = errors.New("no Subscription ID provided") |
| paddy@3 | 28 |
| paddy@6 | 29 planOptions = map[string]bool{ |
| paddy@6 | 30 "basic_monthly": false, |
| paddy@6 | 31 "basic_yearly": false, |
| paddy@6 | 32 "supporter_monthly": false, |
| paddy@6 | 33 "supporter_yearly": false, |
| paddy@6 | 34 "free": true, |
| paddy@6 | 35 PendingPlan: true, |
| paddy@6 | 36 } |
| paddy@3 | 37 |
| paddy@11 | 38 // Version tracks the build ID of the binary, set using |
| paddy@11 | 39 // ldflags. |
| paddy@3 | 40 Version string |
| paddy@0 | 41 ) |
| paddy@0 | 42 |
| paddy@0 | 43 // Subscription represents the state of a user's payments. It holds |
| paddy@0 | 44 // metadata about the last time a user was charged, how much a user |
| paddy@0 | 45 // should be charged, how to charge a user and how much to charge |
| paddy@0 | 46 // the user. |
| paddy@0 | 47 type Subscription struct { |
| paddy@3 | 48 UserID uuid.ID `json:"user_id"` |
| paddy@3 | 49 StripeSubscription string `json:"stripe_subscription"` |
| paddy@3 | 50 Plan string `json:"plan"` |
| paddy@3 | 51 Status string `json:"status"` |
| paddy@3 | 52 Canceling bool `json:"canceling"` |
| paddy@3 | 53 Created time.Time `json:"created"` |
| paddy@3 | 54 TrialStart time.Time `json:"trial_start,omitempty"` |
| paddy@3 | 55 TrialEnd time.Time `json:"trial_end,omitempty"` |
| paddy@3 | 56 PeriodStart time.Time `json:"period_start,omitempty"` |
| paddy@3 | 57 PeriodEnd time.Time `json:"period_end,omitempty"` |
| paddy@3 | 58 CanceledAt time.Time `json:"canceled_at,omitempty"` |
| paddy@3 | 59 FailedChargeAttempts int `json:"failed_charge_attempts"` |
| paddy@3 | 60 LastFailedCharge time.Time `json:"last_failed_charge,omitempty"` |
| paddy@3 | 61 LastNotified time.Time `json:"last_notified,omitempty"` |
| paddy@0 | 62 } |
| paddy@0 | 63 |
| paddy@0 | 64 // SubscriptionChange represents desired changes to a Subscription |
| paddy@0 | 65 // object. A nil value means that property should remain unchanged. |
| paddy@0 | 66 type SubscriptionChange struct { |
| paddy@6 | 67 UserID uuid.ID `json:"user_id"` |
| paddy@6 | 68 |
| paddy@15 | 69 // User-controlled and not stored in DB (helper properties for the API) |
| paddy@6 | 70 StripeSource *string `json:"stripe_source,omitempty"` |
| paddy@6 | 71 Email *string `json:"email,omitempty"` |
| paddy@15 | 72 |
| paddy@15 | 73 // User-controlled and stored in DB |
| paddy@15 | 74 Plan *string `json:"plan,omitempty"` |
| paddy@15 | 75 Canceling *bool `json:"cenceling,omitempty"` |
| paddy@6 | 76 |
| paddy@6 | 77 // System-controlled |
| paddy@6 | 78 StripeSubscription *string `json:"stripe_subscription,omitempty"` |
| paddy@6 | 79 Status *string `json:"status,omitempty"` |
| paddy@6 | 80 TrialStart *time.Time `json:"trial_start,omitempty"` |
| paddy@6 | 81 TrialEnd *time.Time `json:"trial_end,omitempty"` |
| paddy@6 | 82 PeriodStart *time.Time `json:"period_start,omitempty"` |
| paddy@6 | 83 PeriodEnd *time.Time `json:"period_end,omitempty"` |
| paddy@6 | 84 CanceledAt *time.Time `json:"canceled_at,omitempty"` |
| paddy@6 | 85 FailedChargeAttempts *int `json:"failed_charge_attempts,omitempty"` |
| paddy@6 | 86 LastFailedCharge *time.Time `json:"last_failed_charge,omitempty"` |
| paddy@6 | 87 LastNotified *time.Time `json:"last_notified,omitempty"` |
| paddy@0 | 88 } |
| paddy@0 | 89 |
| paddy@0 | 90 // IsEmpty returns true if the SubscriptionChange doesn't request |
| paddy@0 | 91 // a change to any property of the Subscription. |
| paddy@0 | 92 func (change SubscriptionChange) IsEmpty() bool { |
| paddy@2 | 93 if change.Plan != nil { |
| paddy@0 | 94 return false |
| paddy@0 | 95 } |
| paddy@6 | 96 if change.Canceling != nil { |
| paddy@0 | 97 return false |
| paddy@0 | 98 } |
| paddy@6 | 99 if change.StripeSubscription != nil { |
| paddy@6 | 100 return false |
| paddy@6 | 101 } |
| paddy@6 | 102 if change.Status != nil { |
| paddy@0 | 103 return false |
| paddy@0 | 104 } |
| paddy@2 | 105 if change.TrialStart != nil { |
| paddy@2 | 106 return false |
| paddy@2 | 107 } |
| paddy@2 | 108 if change.TrialEnd != nil { |
| paddy@2 | 109 return false |
| paddy@2 | 110 } |
| paddy@2 | 111 if change.PeriodStart != nil { |
| paddy@2 | 112 return false |
| paddy@2 | 113 } |
| paddy@2 | 114 if change.PeriodEnd != nil { |
| paddy@2 | 115 return false |
| paddy@2 | 116 } |
| paddy@2 | 117 if change.CanceledAt != nil { |
| paddy@0 | 118 return false |
| paddy@0 | 119 } |
| paddy@0 | 120 if change.LastNotified != nil { |
| paddy@0 | 121 return false |
| paddy@0 | 122 } |
| paddy@2 | 123 if change.LastFailedCharge != nil { |
| paddy@2 | 124 return false |
| paddy@2 | 125 } |
| paddy@2 | 126 if change.FailedChargeAttempts != nil { |
| paddy@0 | 127 return false |
| paddy@0 | 128 } |
| paddy@0 | 129 return true |
| paddy@0 | 130 } |
| paddy@0 | 131 |
| paddy@0 | 132 // ApplyChange updates a Subscription based on the changes requested |
| paddy@0 | 133 // by a SubscriptionChange. |
| paddy@0 | 134 func (s *Subscription) ApplyChange(change SubscriptionChange) { |
| paddy@2 | 135 if change.StripeSubscription != nil { |
| paddy@2 | 136 s.StripeSubscription = *change.StripeSubscription |
| paddy@0 | 137 } |
| paddy@2 | 138 if change.Plan != nil { |
| paddy@2 | 139 s.Plan = *change.Plan |
| paddy@0 | 140 } |
| paddy@2 | 141 if change.Status != nil { |
| paddy@2 | 142 s.Status = *change.Status |
| paddy@0 | 143 } |
| paddy@2 | 144 if change.Canceling != nil { |
| paddy@2 | 145 s.Canceling = *change.Canceling |
| paddy@0 | 146 } |
| paddy@2 | 147 if change.TrialStart != nil { |
| paddy@2 | 148 s.TrialStart = *change.TrialStart |
| paddy@2 | 149 } |
| paddy@2 | 150 if change.TrialEnd != nil { |
| paddy@2 | 151 s.TrialEnd = *change.TrialEnd |
| paddy@2 | 152 } |
| paddy@2 | 153 if change.PeriodStart != nil { |
| paddy@2 | 154 s.PeriodStart = *change.PeriodStart |
| paddy@2 | 155 } |
| paddy@2 | 156 if change.PeriodEnd != nil { |
| paddy@2 | 157 s.PeriodEnd = *change.PeriodEnd |
| paddy@2 | 158 } |
| paddy@2 | 159 if change.CanceledAt != nil { |
| paddy@2 | 160 s.CanceledAt = *change.CanceledAt |
| paddy@2 | 161 } |
| paddy@2 | 162 if change.LastFailedCharge != nil { |
| paddy@2 | 163 s.LastFailedCharge = *change.LastFailedCharge |
| paddy@0 | 164 } |
| paddy@0 | 165 if change.LastNotified != nil { |
| paddy@0 | 166 s.LastNotified = *change.LastNotified |
| paddy@0 | 167 } |
| paddy@2 | 168 if change.FailedChargeAttempts != nil { |
| paddy@2 | 169 s.FailedChargeAttempts = *change.FailedChargeAttempts |
| paddy@0 | 170 } |
| paddy@0 | 171 } |
| paddy@0 | 172 |
| paddy@11 | 173 // IsAcceptablePlan returns true if the user can select the specified |
| paddy@11 | 174 // plan, taking into account their admin status. If a plan exists, and |
| paddy@11 | 175 // is not designated as an admin-only plan, any user selecting it will |
| paddy@11 | 176 // return true. If a plan exists, but is designated as admin-only, |
| paddy@11 | 177 // IsAcceptablePlan will only return true if admin is true. If the plan |
| paddy@11 | 178 // doesn't exist, IsAcceptablePlan always returns false. |
| paddy@6 | 179 func IsAcceptablePlan(plan string, admin bool) bool { |
| paddy@6 | 180 for p, adminOnly := range planOptions { |
| paddy@6 | 181 if plan == p { |
| paddy@6 | 182 return admin || adminOnly == false |
| paddy@3 | 183 } |
| paddy@3 | 184 } |
| paddy@6 | 185 return false |
| paddy@3 | 186 } |
| paddy@3 | 187 |
| paddy@1 | 188 // SubscriptionStats represents a set of statistics about our Subscription |
| paddy@1 | 189 // data that will be exposed to the Prometheus scraper. |
| paddy@1 | 190 type SubscriptionStats struct { |
| paddy@2 | 191 Number int64 |
| paddy@2 | 192 Canceling int64 |
| paddy@2 | 193 Failing int64 |
| paddy@2 | 194 Plans map[string]int64 |
| paddy@1 | 195 // BUG(paddy): Currently, Kubernetes doesn't offer any way to contact _all_ nodes in a service. |
| paddy@1 | 196 // Because of this, we can only report stats that will be identical across nodes, e.g. stats |
| paddy@1 | 197 // that come from the database. More info here: https://github.com/GoogleCloudPlatform/kubernetes/issues/6666 |
| paddy@1 | 198 // In the future, we'll need per-node metrics. For now, we'll make do. |
| paddy@1 | 199 // |
| paddy@1 | 200 // Actually, as of https://github.com/GoogleCloudPlatform/kubernetes/pull/9073, we may be all set: |
| paddy@1 | 201 // "SRV Records are created for named ports that are part of normal or Headless Services. For each named port, |
| paddy@1 | 202 // the SRV record would have the form _my-port-name._my-port-protocol.my-svc.my-namespace.svc.cluster.local. |
| paddy@1 | 203 // For a regular service, this resolves to the port number and the CNAME: my-svc.my-namespace.svc.cluster.local. |
| paddy@1 | 204 // For a headless service, this resolves to multiple answers, one for each pod that is backing the service, and |
| paddy@1 | 205 // contains the port number and a CNAME of the pod with the format auto-generated-name.my-svc.my-namespace.svc.cluster.local |
| paddy@1 | 206 // SRV records always contain the 'svc' segment in them and are not supported for old-style CNAMEs where the 'svc' segment |
| paddy@1 | 207 // was omitted." |
| paddy@1 | 208 } |
| paddy@1 | 209 |
| paddy@11 | 210 // SubscriptionStore is an interface describing datastore interactions for |
| paddy@11 | 211 // Subscriptions. |
| paddy@3 | 212 type SubscriptionStore interface { |
| paddy@3 | 213 Reset() error |
| paddy@3 | 214 CreateSubscription(sub Subscription) error |
| paddy@3 | 215 UpdateSubscription(id uuid.ID, change SubscriptionChange) error |
| paddy@3 | 216 DeleteSubscription(id uuid.ID) error |
| paddy@3 | 217 GetSubscriptions(ids []uuid.ID) (map[string]Subscription, error) |
| paddy@3 | 218 GetSubscriptionStats() (SubscriptionStats, error) |
| paddy@0 | 219 } |