ducky/subscriptions

Paddy 2015-09-27 Parent:0ae1ff0ee306 Child:aab6ba5ae392

13:1ff031bebf9e Go to Latest

ducky/subscriptions/subscription.go

Add golint comments. Comment on some more of our exported types, functions, and variables, both to make golint happy and because uncommented code never ever ends well.

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 }