Implement PostgreSQL support, drop subscription IDs.
Create a Postgres object that wraps database/sql, so we can attach methods to it
and fulfill interfaces.
Create a postgres_init.sql script that will create the subscriptions table in a
PostgreSQL database.
Make our period type fulfill the driver.Valuer and driver.Scanner types, so it
can be stored in and retrieved from SQL databases.
Create a SubscriptionStats type, and add a method to our subscriptionStore
interface that will allow us to retrieve current stats about the Subscriptions
it is storing.
Deprecated the ID property of our Subscription type, and use the
Subscription.UserID property instead as our primary key. Subscriptions should be
unique per user and we generally will want to access Subscriptions in the
context of the User they belong to, so the UserID is a better primary key. This
also means we removed the getSubscriptionByUserID method (and implementations)
from our subscriptionStore, as getSubscriptions now fills that role.
Implement our getSubscriptionStats method in the memstore.
Implement the subscriptionStore interface on our new Postgres type.
Run the subscription store tests on our Postgres type, as well, if the
PG_TEST_DB environment variable is set.
Round all our timestamps in our tests to the nearest millisecond, as Postgres
silently truncates all timestamps to the nearest millisecond, and it was causing
false test failures.
Remove the tests for our getSubscriptionStoreByUser method, as that was removed.
8 "code.secondbit.org/uuid.hg"
12 // MonthlyPeriod represents a period of once a month.
13 MonthlyPeriod period = "monthly"
17 // ErrSubscriptionAlreadyExists is returned when a Subscription
18 // with an identical ID already exists in the subscriptionStore.
19 ErrSubscriptionAlreadyExists = errors.New("Subscription already exists")
20 // ErrSubscriptionNotFound is returned when a single Subscription
21 // is acted upon or requested, but cannot be found.
22 ErrSubscriptionNotFound = errors.New("Subscription not found")
23 // ErrStripeCustomerAlreadyExists is returned when a Subscription
24 // is created or updates its StripeCustomer property, but that
25 // StripeCustomer is already associated with another Subscription.
26 ErrStripeCustomerAlreadyExists = errors.New("Stripe customer already assigned to another Subscription")
27 // ErrSubscriptionChangeEmpty is returned when a SubscriptionChange
28 // is empty but is passed to subscriptionStore.UpdateSubscription
30 ErrSubscriptionChangeEmpty = errors.New("SubscriptionChange is empty")
31 // ErrNoSubscriptionID is returned when one or more Subscription IDs
32 // are required, but none are provided.
33 ErrNoSubscriptionID = errors.New("no Subscription ID provided")
34 // ErrPeriodInvalid is returned when attempting to use a period that
35 // is not a valid period.
36 ErrPeriodInvalid = errors.New("invalid period")
41 func (p period) Value() (driver.Value, error) {
45 func (p *period) Scan(src interface{}) error {
52 *p = period(string(src.([]byte)))
55 *p = period(src.(string))
58 return ErrPeriodInvalid
62 // Subscription represents the state of a user's payments. It holds
63 // metadata about the last time a user was charged, how much a user
64 // should be charged, how to charge a user and how much to charge
66 type Subscription struct {
72 BeginCharging time.Time
74 LastNotified time.Time
78 // SubscriptionChange represents desired changes to a Subscription
79 // object. A nil value means that property should remain unchanged.
80 type SubscriptionChange struct {
81 StripeCustomer *string
84 BeginCharging *time.Time
85 LastCharged *time.Time
86 LastNotified *time.Time
90 // IsEmpty returns true if the SubscriptionChange doesn't request
91 // a change to any property of the Subscription.
92 func (change SubscriptionChange) IsEmpty() bool {
93 if change.StripeCustomer != nil {
96 if change.Amount != nil {
99 if change.Period != nil {
102 if change.BeginCharging != nil {
105 if change.LastCharged != nil {
108 if change.LastNotified != nil {
111 if change.InLockout != nil {
117 // ApplyChange updates a Subscription based on the changes requested
118 // by a SubscriptionChange.
119 func (s *Subscription) ApplyChange(change SubscriptionChange) {
120 if change.StripeCustomer != nil {
121 s.StripeCustomer = *change.StripeCustomer
123 if change.Amount != nil {
124 s.Amount = *change.Amount
126 if change.Period != nil {
127 s.Period = *change.Period
129 if change.BeginCharging != nil {
130 s.BeginCharging = *change.BeginCharging
132 if change.LastCharged != nil {
133 s.LastCharged = *change.LastCharged
135 if change.LastNotified != nil {
136 s.LastNotified = *change.LastNotified
138 if change.InLockout != nil {
139 s.InLockout = *change.InLockout
143 // ByLastChargeDate allows us to sort a []Subscription by the LastCharged
144 // property, with the lowest LastCharged date first.
145 type ByLastChargeDate []Subscription
147 // Len returns the length the SubscriptionsByLastChargeDate. It fulfills
148 // the sort.Interface interface.
149 func (s ByLastChargeDate) Len() int {
153 // Swap puts the item in position i in position j, and the item in position
154 // j in position i. It fulfills the sort.Interface interface.
155 func (s ByLastChargeDate) Swap(i, j int) {
156 s[i], s[j] = s[j], s[i]
159 // Less returns true if the item in position i should be sorted before the
160 // item in position j.
161 func (s ByLastChargeDate) Less(i, j int) bool {
162 return s[i].LastCharged.Before(s[j].LastCharged)
165 // SubscriptionStats represents a set of statistics about our Subscription
166 // data that will be exposed to the Prometheus scraper.
167 type SubscriptionStats struct {
171 // BUG(paddy): Currently, Kubernetes doesn't offer any way to contact _all_ nodes in a service.
172 // Because of this, we can only report stats that will be identical across nodes, e.g. stats
173 // that come from the database. More info here: https://github.com/GoogleCloudPlatform/kubernetes/issues/6666
174 // In the future, we'll need per-node metrics. For now, we'll make do.
176 // Actually, as of https://github.com/GoogleCloudPlatform/kubernetes/pull/9073, we may be all set:
177 // "SRV Records are created for named ports that are part of normal or Headless Services. For each named port,
178 // the SRV record would have the form _my-port-name._my-port-protocol.my-svc.my-namespace.svc.cluster.local.
179 // For a regular service, this resolves to the port number and the CNAME: my-svc.my-namespace.svc.cluster.local.
180 // For a headless service, this resolves to multiple answers, one for each pod that is backing the service, and
181 // contains the port number and a CNAME of the pod with the format auto-generated-name.my-svc.my-namespace.svc.cluster.local
182 // SRV records always contain the 'svc' segment in them and are not supported for old-style CNAMEs where the 'svc' segment
186 type subscriptionStore interface {
188 createSubscription(sub Subscription) error
189 updateSubscription(id uuid.ID, change SubscriptionChange) error
190 deleteSubscription(id uuid.ID) error
191 listSubscriptionsLastChargedBefore(time.Time) ([]Subscription, error)
192 getSubscriptions(ids []uuid.ID) (map[string]Subscription, error)
193 getSubscriptionStats() (SubscriptionStats, error)