ducky/subscriptions

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

9:8eb19bcbf17d Go to Latest

ducky/subscriptions/subscription.go

Return errors from responses in client. When the client makes a request, non-200 responses _are not_ considered errors. So we need to check the response.Errors property, and if it has errors, _then_ we consider the request to have an error. To make this happen, we created an httpErrors type that fulfills the error interface and just wraps the response Errors property. Then callers can type-cast it and interrogate it.

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