ducky/subscriptions

Paddy 2015-10-04 Parent:aab6ba5ae392

17:7eef47ecc01c Go to Latest

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.

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