ducky/subscriptions
ducky/subscriptions/subscription.go
Fix go vet errors. We had a few logging statements that used placeholders but didn't provide any variables to fill them. We now specify the appropriate variables.
| 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@6 | 69 // User-controlled |
| paddy@6 | 70 StripeSource *string `json:"stripe_source,omitempty"` |
| paddy@6 | 71 Email *string `json:"email,omitempty"` |
| paddy@6 | 72 Plan *string `json:"plan,omitempty"` |
| paddy@6 | 73 Canceling *bool `json:"cenceling,omitempty"` |
| paddy@6 | 74 |
| paddy@6 | 75 // System-controlled |
| paddy@6 | 76 StripeSubscription *string `json:"stripe_subscription,omitempty"` |
| paddy@6 | 77 Status *string `json:"status,omitempty"` |
| paddy@6 | 78 TrialStart *time.Time `json:"trial_start,omitempty"` |
| paddy@6 | 79 TrialEnd *time.Time `json:"trial_end,omitempty"` |
| paddy@6 | 80 PeriodStart *time.Time `json:"period_start,omitempty"` |
| paddy@6 | 81 PeriodEnd *time.Time `json:"period_end,omitempty"` |
| paddy@6 | 82 CanceledAt *time.Time `json:"canceled_at,omitempty"` |
| paddy@6 | 83 FailedChargeAttempts *int `json:"failed_charge_attempts,omitempty"` |
| paddy@6 | 84 LastFailedCharge *time.Time `json:"last_failed_charge,omitempty"` |
| paddy@6 | 85 LastNotified *time.Time `json:"last_notified,omitempty"` |
| paddy@0 | 86 } |
| paddy@0 | 87 |
| paddy@0 | 88 // IsEmpty returns true if the SubscriptionChange doesn't request |
| paddy@0 | 89 // a change to any property of the Subscription. |
| paddy@0 | 90 func (change SubscriptionChange) IsEmpty() bool { |
| paddy@6 | 91 if change.StripeSource != nil { |
| paddy@6 | 92 return false |
| paddy@6 | 93 } |
| paddy@6 | 94 if change.Email != nil { |
| paddy@0 | 95 return false |
| paddy@0 | 96 } |
| paddy@2 | 97 if change.Plan != nil { |
| paddy@0 | 98 return false |
| paddy@0 | 99 } |
| paddy@6 | 100 if change.Canceling != nil { |
| paddy@0 | 101 return false |
| paddy@0 | 102 } |
| paddy@6 | 103 if change.StripeSubscription != nil { |
| paddy@6 | 104 return false |
| paddy@6 | 105 } |
| paddy@6 | 106 if change.Status != nil { |
| paddy@0 | 107 return false |
| paddy@0 | 108 } |
| paddy@2 | 109 if change.TrialStart != nil { |
| paddy@2 | 110 return false |
| paddy@2 | 111 } |
| paddy@2 | 112 if change.TrialEnd != nil { |
| paddy@2 | 113 return false |
| paddy@2 | 114 } |
| paddy@2 | 115 if change.PeriodStart != nil { |
| paddy@2 | 116 return false |
| paddy@2 | 117 } |
| paddy@2 | 118 if change.PeriodEnd != nil { |
| paddy@2 | 119 return false |
| paddy@2 | 120 } |
| paddy@2 | 121 if change.CanceledAt != nil { |
| paddy@0 | 122 return false |
| paddy@0 | 123 } |
| paddy@0 | 124 if change.LastNotified != nil { |
| paddy@0 | 125 return false |
| paddy@0 | 126 } |
| paddy@2 | 127 if change.LastFailedCharge != nil { |
| paddy@2 | 128 return false |
| paddy@2 | 129 } |
| paddy@2 | 130 if change.FailedChargeAttempts != nil { |
| paddy@0 | 131 return false |
| paddy@0 | 132 } |
| paddy@0 | 133 return true |
| paddy@0 | 134 } |
| paddy@0 | 135 |
| paddy@0 | 136 // ApplyChange updates a Subscription based on the changes requested |
| paddy@0 | 137 // by a SubscriptionChange. |
| paddy@0 | 138 func (s *Subscription) ApplyChange(change SubscriptionChange) { |
| paddy@2 | 139 if change.StripeSubscription != nil { |
| paddy@2 | 140 s.StripeSubscription = *change.StripeSubscription |
| paddy@0 | 141 } |
| paddy@2 | 142 if change.Plan != nil { |
| paddy@2 | 143 s.Plan = *change.Plan |
| paddy@0 | 144 } |
| paddy@2 | 145 if change.Status != nil { |
| paddy@2 | 146 s.Status = *change.Status |
| paddy@0 | 147 } |
| paddy@2 | 148 if change.Canceling != nil { |
| paddy@2 | 149 s.Canceling = *change.Canceling |
| paddy@0 | 150 } |
| paddy@2 | 151 if change.TrialStart != nil { |
| paddy@2 | 152 s.TrialStart = *change.TrialStart |
| paddy@2 | 153 } |
| paddy@2 | 154 if change.TrialEnd != nil { |
| paddy@2 | 155 s.TrialEnd = *change.TrialEnd |
| paddy@2 | 156 } |
| paddy@2 | 157 if change.PeriodStart != nil { |
| paddy@2 | 158 s.PeriodStart = *change.PeriodStart |
| paddy@2 | 159 } |
| paddy@2 | 160 if change.PeriodEnd != nil { |
| paddy@2 | 161 s.PeriodEnd = *change.PeriodEnd |
| paddy@2 | 162 } |
| paddy@2 | 163 if change.CanceledAt != nil { |
| paddy@2 | 164 s.CanceledAt = *change.CanceledAt |
| paddy@2 | 165 } |
| paddy@2 | 166 if change.LastFailedCharge != nil { |
| paddy@2 | 167 s.LastFailedCharge = *change.LastFailedCharge |
| paddy@0 | 168 } |
| paddy@0 | 169 if change.LastNotified != nil { |
| paddy@0 | 170 s.LastNotified = *change.LastNotified |
| paddy@0 | 171 } |
| paddy@2 | 172 if change.FailedChargeAttempts != nil { |
| paddy@2 | 173 s.FailedChargeAttempts = *change.FailedChargeAttempts |
| paddy@0 | 174 } |
| paddy@0 | 175 } |
| paddy@0 | 176 |
| paddy@11 | 177 // IsAcceptablePlan returns true if the user can select the specified |
| paddy@11 | 178 // plan, taking into account their admin status. If a plan exists, and |
| paddy@11 | 179 // is not designated as an admin-only plan, any user selecting it will |
| paddy@11 | 180 // return true. If a plan exists, but is designated as admin-only, |
| paddy@11 | 181 // IsAcceptablePlan will only return true if admin is true. If the plan |
| paddy@11 | 182 // doesn't exist, IsAcceptablePlan always returns false. |
| paddy@6 | 183 func IsAcceptablePlan(plan string, admin bool) bool { |
| paddy@6 | 184 for p, adminOnly := range planOptions { |
| paddy@6 | 185 if plan == p { |
| paddy@6 | 186 return admin || adminOnly == false |
| paddy@3 | 187 } |
| paddy@3 | 188 } |
| paddy@6 | 189 return false |
| paddy@3 | 190 } |
| paddy@3 | 191 |
| paddy@1 | 192 // SubscriptionStats represents a set of statistics about our Subscription |
| paddy@1 | 193 // data that will be exposed to the Prometheus scraper. |
| paddy@1 | 194 type SubscriptionStats struct { |
| paddy@2 | 195 Number int64 |
| paddy@2 | 196 Canceling int64 |
| paddy@2 | 197 Failing int64 |
| paddy@2 | 198 Plans map[string]int64 |
| paddy@1 | 199 // BUG(paddy): Currently, Kubernetes doesn't offer any way to contact _all_ nodes in a service. |
| paddy@1 | 200 // Because of this, we can only report stats that will be identical across nodes, e.g. stats |
| paddy@1 | 201 // that come from the database. More info here: https://github.com/GoogleCloudPlatform/kubernetes/issues/6666 |
| paddy@1 | 202 // In the future, we'll need per-node metrics. For now, we'll make do. |
| paddy@1 | 203 // |
| paddy@1 | 204 // Actually, as of https://github.com/GoogleCloudPlatform/kubernetes/pull/9073, we may be all set: |
| paddy@1 | 205 // "SRV Records are created for named ports that are part of normal or Headless Services. For each named port, |
| paddy@1 | 206 // the SRV record would have the form _my-port-name._my-port-protocol.my-svc.my-namespace.svc.cluster.local. |
| paddy@1 | 207 // For a regular service, this resolves to the port number and the CNAME: my-svc.my-namespace.svc.cluster.local. |
| paddy@1 | 208 // For a headless service, this resolves to multiple answers, one for each pod that is backing the service, and |
| paddy@1 | 209 // 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 | 210 // SRV records always contain the 'svc' segment in them and are not supported for old-style CNAMEs where the 'svc' segment |
| paddy@1 | 211 // was omitted." |
| paddy@1 | 212 } |
| paddy@1 | 213 |
| paddy@11 | 214 // SubscriptionStore is an interface describing datastore interactions for |
| paddy@11 | 215 // Subscriptions. |
| paddy@3 | 216 type SubscriptionStore interface { |
| paddy@3 | 217 Reset() error |
| paddy@3 | 218 CreateSubscription(sub Subscription) error |
| paddy@3 | 219 UpdateSubscription(id uuid.ID, change SubscriptionChange) error |
| paddy@3 | 220 DeleteSubscription(id uuid.ID) error |
| paddy@3 | 221 GetSubscriptions(ids []uuid.ID) (map[string]Subscription, error) |
| paddy@3 | 222 GetSubscriptionStats() (SubscriptionStats, error) |
| paddy@0 | 223 } |