ducky/subscriptions

Paddy 2015-09-27 Parent:c4cfceb2f2fb Child:aab6ba5ae392

11:0ae1ff0ee306 Go to Latest

ducky/subscriptions/subscription.go

Add comments, move ChangingSystemProperties to the api package. Add comments to all our exported types and variables in subscription.go, both to make golint happy and because it's good to have comments. Move the subscriptions.ChangingSystemProperties helper to api.changingSystemProperties, because it returns API-specific strings and there's no real reason it has to be in the subscriptions package--everything it needs to work on is exported.

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@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 }