ducky/subscriptions

Paddy 2015-06-14 Parent:56a2bef197cd Child:61c4ce5850da

1:f1a22fc2321d Browse Files

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.

postgres.go sql/postgres_init.sql subscription.go subscription_memstore.go subscription_postgres.go subscription_store_test.go

     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/postgres.go	Sun Jun 14 02:48:08 2015 -0400
     1.3 @@ -0,0 +1,27 @@
     1.4 +package subscriptions
     1.5 +
     1.6 +import (
     1.7 +	"database/sql"
     1.8 +)
     1.9 +
    1.10 +// NewPostgres returns a usable Postgres instance, if and only
    1.11 +// if error is nil. If error is not nil, the returned Postgres
    1.12 +// instance should not be used.
    1.13 +func NewPostgres(conn string) (Postgres, error) {
    1.14 +	db, err := sql.Open("postgres", conn)
    1.15 +	if err != nil {
    1.16 +		return Postgres{}, err
    1.17 +	}
    1.18 +	return Postgres{db}, nil
    1.19 +}
    1.20 +
    1.21 +// Postgres represents a thin wrapper around *sql.DB, so we can
    1.22 +// inherit its methods but also define our own (so we can fulfill
    1.23 +// our subscriptionStore interface on Postgres). Note that the
    1.24 +// value of a Postgres variable contains a _pointer_ to an sql.DB
    1.25 +// pool of connections, so it's not necessary to use the address of
    1.26 +// the Postgres variable itself (though there shouldn't be any harm
    1.27 +// in doing so).
    1.28 +type Postgres struct {
    1.29 +	*sql.DB
    1.30 +}
     2.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     2.2 +++ b/sql/postgres_init.sql	Sun Jun 14 02:48:08 2015 -0400
     2.3 @@ -0,0 +1,11 @@
     2.4 +CREATE TABLE IF NOT EXISTS subscriptions (
     2.5 +	user_id VARCHAR(36) PRIMARY KEY,
     2.6 +	stripe_customer VARCHAR(36) UNIQUE NOT NULL,
     2.7 +	amount INTEGER NOT NULL,
     2.8 +	period VARCHAR(16) NOT NULL,
     2.9 +	created TIMESTAMPTZ NOT NULL,
    2.10 +	begin_charging TIMESTAMPTZ NOT NULL,
    2.11 +	last_charged TIMESTAMPTZ NOT NULL,
    2.12 +	last_notified TIMESTAMPTZ NOT NULL,
    2.13 +	in_lockout BOOLEAN NOT NULL
    2.14 +);
     3.1 --- a/subscription.go	Thu Jun 11 23:15:01 2015 -0400
     3.2 +++ b/subscription.go	Sun Jun 14 02:48:08 2015 -0400
     3.3 @@ -1,6 +1,7 @@
     3.4  package subscriptions
     3.5  
     3.6  import (
     3.7 +	"database/sql/driver"
     3.8  	"errors"
     3.9  	"time"
    3.10  
    3.11 @@ -30,16 +31,39 @@
    3.12  	// ErrNoSubscriptionID is returned when one or more Subscription IDs
    3.13  	// are required, but none are provided.
    3.14  	ErrNoSubscriptionID = errors.New("no Subscription ID provided")
    3.15 +	// ErrPeriodInvalid is returned when attempting to use a period that
    3.16 +	// is not a valid period.
    3.17 +	ErrPeriodInvalid = errors.New("invalid period")
    3.18  )
    3.19  
    3.20  type period string
    3.21  
    3.22 +func (p period) Value() (driver.Value, error) {
    3.23 +	return string(p), nil
    3.24 +}
    3.25 +
    3.26 +func (p *period) Scan(src interface{}) error {
    3.27 +	if src == nil {
    3.28 +		*p = period("")
    3.29 +		return nil
    3.30 +	}
    3.31 +	switch src.(type) {
    3.32 +	case []byte:
    3.33 +		*p = period(string(src.([]byte)))
    3.34 +		return nil
    3.35 +	case string:
    3.36 +		*p = period(src.(string))
    3.37 +		return nil
    3.38 +	default:
    3.39 +		return ErrPeriodInvalid
    3.40 +	}
    3.41 +}
    3.42 +
    3.43  // Subscription represents the state of a user's payments. It holds
    3.44  // metadata about the last time a user was charged, how much a user
    3.45  // should be charged, how to charge a user and how much to charge
    3.46  // the user.
    3.47  type Subscription struct {
    3.48 -	ID             uuid.ID
    3.49  	UserID         uuid.ID
    3.50  	StripeCustomer string
    3.51  	Amount         int
    3.52 @@ -138,6 +162,27 @@
    3.53  	return s[i].LastCharged.Before(s[j].LastCharged)
    3.54  }
    3.55  
    3.56 +// SubscriptionStats represents a set of statistics about our Subscription
    3.57 +// data that will be exposed to the Prometheus scraper.
    3.58 +type SubscriptionStats struct {
    3.59 +	Number      int64
    3.60 +	TotalAmount int64
    3.61 +	MeanAmount  float64
    3.62 +	// BUG(paddy): Currently, Kubernetes doesn't offer any way to contact _all_ nodes in a service.
    3.63 +	// Because of this, we can only report stats that will be identical across nodes, e.g. stats
    3.64 +	// that come from the database. More info here: https://github.com/GoogleCloudPlatform/kubernetes/issues/6666
    3.65 +	// In the future, we'll need per-node metrics. For now, we'll make do.
    3.66 +	//
    3.67 +	// Actually, as of https://github.com/GoogleCloudPlatform/kubernetes/pull/9073, we may be all set:
    3.68 +	// "SRV Records are created for named ports that are part of normal or Headless Services. For each named port,
    3.69 +	// the SRV record would have the form _my-port-name._my-port-protocol.my-svc.my-namespace.svc.cluster.local.
    3.70 +	// For a regular service, this resolves to the port number and the CNAME: my-svc.my-namespace.svc.cluster.local.
    3.71 +	// For a headless service, this resolves to multiple answers, one for each pod that is backing the service, and
    3.72 +	// contains the port number and a CNAME of the pod with the format auto-generated-name.my-svc.my-namespace.svc.cluster.local
    3.73 +	// SRV records always contain the 'svc' segment in them and are not supported for old-style CNAMEs where the 'svc' segment
    3.74 +	// was omitted."
    3.75 +}
    3.76 +
    3.77  type subscriptionStore interface {
    3.78  	reset() error
    3.79  	createSubscription(sub Subscription) error
    3.80 @@ -145,5 +190,5 @@
    3.81  	deleteSubscription(id uuid.ID) error
    3.82  	listSubscriptionsLastChargedBefore(time.Time) ([]Subscription, error)
    3.83  	getSubscriptions(ids []uuid.ID) (map[string]Subscription, error)
    3.84 -	getSubscriptionByUser(id uuid.ID) (Subscription, error)
    3.85 +	getSubscriptionStats() (SubscriptionStats, error)
    3.86  }
     4.1 --- a/subscription_memstore.go	Thu Jun 11 23:15:01 2015 -0400
     4.2 +++ b/subscription_memstore.go	Sun Jun 14 02:48:08 2015 -0400
     4.3 @@ -20,13 +20,13 @@
     4.4  	m.subscriptionLock.Lock()
     4.5  	defer m.subscriptionLock.Unlock()
     4.6  
     4.7 -	if _, ok := m.subscriptions[sub.ID.String()]; ok {
     4.8 +	if _, ok := m.subscriptions[sub.UserID.String()]; ok {
     4.9  		return ErrSubscriptionAlreadyExists
    4.10  	}
    4.11  	if stripeCustomerInMemstore(sub.StripeCustomer, m) {
    4.12  		return ErrStripeCustomerAlreadyExists
    4.13  	}
    4.14 -	m.subscriptions[sub.ID.String()] = sub
    4.15 +	m.subscriptions[sub.UserID.String()] = sub
    4.16  	return nil
    4.17  }
    4.18  
    4.19 @@ -97,20 +97,24 @@
    4.20  		if !ok {
    4.21  			continue
    4.22  		}
    4.23 -		result[s.ID.String()] = s
    4.24 +		result[s.UserID.String()] = s
    4.25  	}
    4.26  	return result, nil
    4.27  }
    4.28  
    4.29 -func (m *Memstore) getSubscriptionByUser(id uuid.ID) (Subscription, error) {
    4.30 +func (m *Memstore) getSubscriptionStats() (SubscriptionStats, error) {
    4.31  	m.subscriptionLock.RLock()
    4.32  	defer m.subscriptionLock.RUnlock()
    4.33  
    4.34 +	stats := SubscriptionStats{}
    4.35 +
    4.36  	for _, subscription := range m.subscriptions {
    4.37 -		if subscription.UserID.Equal(id) {
    4.38 -			return subscription, nil
    4.39 -		}
    4.40 +		stats.Number++
    4.41 +		stats.TotalAmount += int64(subscription.Amount)
    4.42  	}
    4.43  
    4.44 -	return Subscription{}, ErrSubscriptionNotFound
    4.45 +	if stats.Number > 0 {
    4.46 +		stats.MeanAmount = float64(stats.TotalAmount) / float64(stats.Number)
    4.47 +	}
    4.48 +	return stats, nil
    4.49  }
     5.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     5.2 +++ b/subscription_postgres.go	Sun Jun 14 02:48:08 2015 -0400
     5.3 @@ -0,0 +1,220 @@
     5.4 +package subscriptions
     5.5 +
     5.6 +import (
     5.7 +	"database/sql"
     5.8 +	"time"
     5.9 +
    5.10 +	"code.secondbit.org/uuid.hg"
    5.11 +	"github.com/lib/pq"
    5.12 +	"github.com/secondbit/pan"
    5.13 +)
    5.14 +
    5.15 +// GetSQLTableName fulfills the pan.SQLTableNamer interface, allowing
    5.16 +// us to manipulate Subscriptions with pan.
    5.17 +func (s Subscription) GetSQLTableName() string {
    5.18 +	return "subscriptions"
    5.19 +}
    5.20 +
    5.21 +func (p Postgres) resetSQL() *pan.Query {
    5.22 +	var sub Subscription
    5.23 +	query := pan.New(pan.POSTGRES, "TRUNCATE "+pan.GetTableName(sub))
    5.24 +	return query.FlushExpressions(" ")
    5.25 +}
    5.26 +
    5.27 +func (p Postgres) reset() error {
    5.28 +	query := p.resetSQL()
    5.29 +	_, err := p.Exec(query.String(), query.Args...)
    5.30 +	if err != nil {
    5.31 +		return err
    5.32 +	}
    5.33 +	return nil
    5.34 +}
    5.35 +
    5.36 +func (p Postgres) createSubscriptionSQL(sub Subscription) *pan.Query {
    5.37 +	fields, values := pan.GetFields(sub)
    5.38 +	query := pan.New(pan.POSTGRES, "INSERT INTO "+pan.GetTableName(sub))
    5.39 +	query.Include("(" + pan.QueryList(fields) + ")")
    5.40 +	query.Include("VALUES")
    5.41 +	query.Include("("+pan.VariableList(len(values))+")", values...)
    5.42 +	return query.FlushExpressions(" ")
    5.43 +}
    5.44 +
    5.45 +func (p Postgres) createSubscription(sub Subscription) error {
    5.46 +	query := p.createSubscriptionSQL(sub)
    5.47 +	_, err := p.Exec(query.String(), query.Args...)
    5.48 +	if e, ok := err.(*pq.Error); ok && e.Constraint == "subscriptions_pkey" {
    5.49 +		err = ErrSubscriptionAlreadyExists
    5.50 +	} else if e, ok := err.(*pq.Error); ok && e.Constraint == "subscriptions_stripe_customer_key" {
    5.51 +		err = ErrStripeCustomerAlreadyExists
    5.52 +	}
    5.53 +	return err
    5.54 +}
    5.55 +
    5.56 +func (p Postgres) updateSubscriptionSQL(id uuid.ID, change SubscriptionChange) *pan.Query {
    5.57 +	var sub Subscription
    5.58 +	query := pan.New(pan.POSTGRES, "UPDATE "+pan.GetTableName(sub)+" SET")
    5.59 +	query.IncludeIfNotNil(pan.GetUnquotedColumn(sub, "StripeCustomer")+" = ?", change.StripeCustomer)
    5.60 +	query.IncludeIfNotNil(pan.GetUnquotedColumn(sub, "Amount")+" = ?", change.Amount)
    5.61 +	query.IncludeIfNotNil(pan.GetUnquotedColumn(sub, "Period")+" = ?", change.Period)
    5.62 +	query.IncludeIfNotNil(pan.GetUnquotedColumn(sub, "BeginCharging")+" = ?", change.BeginCharging)
    5.63 +	query.IncludeIfNotNil(pan.GetUnquotedColumn(sub, "LastCharged")+" = ?", change.LastCharged)
    5.64 +	query.IncludeIfNotNil(pan.GetUnquotedColumn(sub, "LastNotified")+" = ?", change.LastNotified)
    5.65 +	query.IncludeIfNotNil(pan.GetUnquotedColumn(sub, "InLockout")+" = ?", change.InLockout)
    5.66 +	query.FlushExpressions(", ")
    5.67 +	query.IncludeWhere()
    5.68 +	query.Include(pan.GetUnquotedColumn(sub, "UserID")+" = ?", id)
    5.69 +	return query.FlushExpressions(" ")
    5.70 +}
    5.71 +
    5.72 +func (p Postgres) updateSubscription(id uuid.ID, change SubscriptionChange) error {
    5.73 +	if change.IsEmpty() {
    5.74 +		return ErrSubscriptionChangeEmpty
    5.75 +	}
    5.76 +
    5.77 +	query := p.updateSubscriptionSQL(id, change)
    5.78 +	res, err := p.Exec(query.String(), query.Args...)
    5.79 +	if e, ok := err.(*pq.Error); ok && e.Constraint == "subscriptions_stripe_customer_key" {
    5.80 +		return ErrStripeCustomerAlreadyExists
    5.81 +	} else if err != nil {
    5.82 +		return err
    5.83 +	}
    5.84 +	rows, err := res.RowsAffected()
    5.85 +	if err != nil {
    5.86 +		return err
    5.87 +	}
    5.88 +	if rows < 1 {
    5.89 +		return ErrSubscriptionNotFound
    5.90 +	}
    5.91 +	return nil
    5.92 +}
    5.93 +
    5.94 +func (p Postgres) deleteSubscriptionSQL(id uuid.ID) *pan.Query {
    5.95 +	var sub Subscription
    5.96 +	query := pan.New(pan.POSTGRES, "DELETE FROM "+pan.GetTableName(sub))
    5.97 +	query.IncludeWhere()
    5.98 +	query.Include(pan.GetUnquotedColumn(sub, "UserID")+" = ?", id)
    5.99 +	return query.FlushExpressions(" ")
   5.100 +}
   5.101 +
   5.102 +func (p Postgres) deleteSubscription(id uuid.ID) error {
   5.103 +	query := p.deleteSubscriptionSQL(id)
   5.104 +	res, err := p.Exec(query.String(), query.Args...)
   5.105 +	if err != nil {
   5.106 +		return err
   5.107 +	}
   5.108 +	rows, err := res.RowsAffected()
   5.109 +	if err != nil {
   5.110 +		return err
   5.111 +	}
   5.112 +	if rows < 1 {
   5.113 +		return ErrSubscriptionNotFound
   5.114 +	}
   5.115 +	return nil
   5.116 +}
   5.117 +
   5.118 +func (p Postgres) listSubscriptionsLastChargedBeforeSQL(cutoff time.Time) *pan.Query {
   5.119 +	var sub Subscription
   5.120 +	fields, _ := pan.GetFields(sub)
   5.121 +	query := pan.New(pan.POSTGRES, "SELECT "+pan.QueryList(fields)+" FROM "+pan.GetTableName(sub))
   5.122 +	query.IncludeWhere()
   5.123 +	query.Include(pan.GetUnquotedColumn(sub, "LastCharged")+" < ?", cutoff)
   5.124 +	query.IncludeOrder(pan.GetUnquotedColumn(sub, "LastCharged") + " ASC")
   5.125 +	return query.FlushExpressions(" ")
   5.126 +}
   5.127 +
   5.128 +func (p Postgres) listSubscriptionsLastChargedBefore(cutoff time.Time) ([]Subscription, error) {
   5.129 +	var results []Subscription
   5.130 +	query := p.listSubscriptionsLastChargedBeforeSQL(cutoff)
   5.131 +	rows, err := p.Query(query.String(), query.Args...)
   5.132 +	if err != nil {
   5.133 +		return results, err
   5.134 +	}
   5.135 +	for rows.Next() {
   5.136 +		var sub Subscription
   5.137 +		err := pan.Unmarshal(rows, &sub)
   5.138 +		if err != nil {
   5.139 +			return results, err
   5.140 +		}
   5.141 +		results = append(results, sub)
   5.142 +	}
   5.143 +	if err := rows.Err(); err != nil {
   5.144 +		return results, err
   5.145 +	}
   5.146 +	return results, nil
   5.147 +}
   5.148 +
   5.149 +func (p Postgres) getSubscriptionsSQL(ids []uuid.ID) *pan.Query {
   5.150 +	var sub Subscription
   5.151 +	fields, _ := pan.GetFields(sub)
   5.152 +	intIDs := make([]interface{}, len(ids))
   5.153 +	for pos, id := range ids {
   5.154 +		intIDs[pos] = id
   5.155 +	}
   5.156 +	query := pan.New(pan.POSTGRES, "SELECT "+pan.QueryList(fields)+" FROM "+pan.GetTableName(sub))
   5.157 +	query.IncludeWhere()
   5.158 +	query.Include(pan.GetUnquotedColumn(sub, "UserID") + " IN")
   5.159 +	query.Include("("+pan.VariableList(len(intIDs))+")", intIDs...)
   5.160 +	return query.FlushExpressions(" ")
   5.161 +}
   5.162 +
   5.163 +func (p Postgres) getSubscriptions(ids []uuid.ID) (map[string]Subscription, error) {
   5.164 +	results := map[string]Subscription{}
   5.165 +	if len(ids) < 1 {
   5.166 +		return results, ErrNoSubscriptionID
   5.167 +	}
   5.168 +	query := p.getSubscriptionsSQL(ids)
   5.169 +	rows, err := p.Query(query.String(), query.Args...)
   5.170 +	if err != nil {
   5.171 +		return results, err
   5.172 +	}
   5.173 +	for rows.Next() {
   5.174 +		var sub Subscription
   5.175 +		err := pan.Unmarshal(rows, &sub)
   5.176 +		if err != nil {
   5.177 +			return results, err
   5.178 +		}
   5.179 +		results[sub.UserID.String()] = sub
   5.180 +	}
   5.181 +	if err := rows.Err(); err != nil {
   5.182 +		return results, err
   5.183 +	}
   5.184 +	return results, nil
   5.185 +}
   5.186 +
   5.187 +func (p Postgres) getSubscriptionStatsSQL() *pan.Query {
   5.188 +	var sub Subscription
   5.189 +	amountColumn := pan.GetUnquotedColumn(sub, "Amount")
   5.190 +	query := pan.New(pan.POSTGRES, "SELECT")
   5.191 +	query.Include("COUNT(*), SUM(" + amountColumn + "), AVG(" + amountColumn + ")")
   5.192 +	query.Include("FROM " + pan.GetTableName(sub))
   5.193 +	return query.FlushExpressions(" ")
   5.194 +}
   5.195 +
   5.196 +func (p Postgres) getSubscriptionStats() (SubscriptionStats, error) {
   5.197 +	query := p.getSubscriptionStatsSQL()
   5.198 +	rows, err := p.Query(query.String(), query.Args...)
   5.199 +	if err != nil {
   5.200 +		return SubscriptionStats{}, err
   5.201 +	}
   5.202 +	var stats SubscriptionStats
   5.203 +	for rows.Next() {
   5.204 +		var number, total sql.NullInt64
   5.205 +		var mean sql.NullFloat64
   5.206 +		if err := rows.Scan(number, total, mean); err != nil {
   5.207 +			return stats, err
   5.208 +		}
   5.209 +		if number.Valid {
   5.210 +			stats.Number = number.Int64
   5.211 +		}
   5.212 +		if total.Valid {
   5.213 +			stats.TotalAmount = total.Int64
   5.214 +		}
   5.215 +		if mean.Valid {
   5.216 +			stats.MeanAmount = mean.Float64
   5.217 +		}
   5.218 +	}
   5.219 +	if err := rows.Err(); err != nil {
   5.220 +		return stats, err
   5.221 +	}
   5.222 +	return stats, nil
   5.223 +}
     6.1 --- a/subscription_store_test.go	Thu Jun 11 23:15:01 2015 -0400
     6.2 +++ b/subscription_store_test.go	Sun Jun 14 02:48:08 2015 -0400
     6.3 @@ -1,6 +1,7 @@
     6.4  package subscriptions
     6.5  
     6.6  import (
     6.7 +	"os"
     6.8  	"strconv"
     6.9  	"testing"
    6.10  	"time"
    6.11 @@ -18,14 +19,21 @@
    6.12  	subscriptionChangeInLockout
    6.13  )
    6.14  
    6.15 +func init() {
    6.16 +	if os.Getenv("PG_TEST_DB") != "" {
    6.17 +		p, err := NewPostgres(os.Getenv("PG_TEST_DB"))
    6.18 +		if err != nil {
    6.19 +			panic(err)
    6.20 +		}
    6.21 +		testSubscriptionStores = append(testSubscriptionStores, p)
    6.22 +	}
    6.23 +}
    6.24 +
    6.25  var testSubscriptionStores = []subscriptionStore{
    6.26  	NewMemstore(),
    6.27  }
    6.28  
    6.29  func compareSubscriptions(sub1, sub2 Subscription) (bool, string, interface{}, interface{}) {
    6.30 -	if !sub1.ID.Equal(sub2.ID) {
    6.31 -		return false, "ID", sub1.ID, sub2.ID
    6.32 -	}
    6.33  	if !sub1.UserID.Equal(sub2.UserID) {
    6.34  		return false, "UserID", sub1.UserID, sub2.UserID
    6.35  	}
    6.36 @@ -59,7 +67,7 @@
    6.37  func subscriptionMapContains(subscriptionMap map[string]Subscription, subscriptions ...Subscription) (bool, []Subscription) {
    6.38  	var missing []Subscription
    6.39  	for _, sub := range subscriptions {
    6.40 -		if _, ok := subscriptionMap[sub.ID.String()]; !ok {
    6.41 +		if _, ok := subscriptionMap[sub.UserID.String()]; !ok {
    6.42  			missing = append(missing, sub)
    6.43  		}
    6.44  	}
    6.45 @@ -77,26 +85,25 @@
    6.46  		}
    6.47  		customerID := uuid.NewID()
    6.48  		sub := Subscription{
    6.49 -			ID:             uuid.NewID(),
    6.50  			UserID:         customerID,
    6.51  			StripeCustomer: "stripeCustomer1",
    6.52  			Amount:         200,
    6.53  			Period:         MonthlyPeriod,
    6.54 -			Created:        time.Now(),
    6.55 -			BeginCharging:  time.Now().Add(time.Hour),
    6.56 +			Created:        time.Now().Round(time.Millisecond),
    6.57 +			BeginCharging:  time.Now().Round(time.Millisecond).Add(time.Hour),
    6.58  		}
    6.59  		err = store.createSubscription(sub)
    6.60  		if err != nil {
    6.61  			t.Errorf("Error creating subscription in %T: %+v\n", store, err)
    6.62  		}
    6.63 -		retrieved, err := store.getSubscriptions([]uuid.ID{sub.ID})
    6.64 +		retrieved, err := store.getSubscriptions([]uuid.ID{sub.UserID})
    6.65  		if err != nil {
    6.66  			t.Errorf("Error retrieving subscription from %T: %+v\n", store, err)
    6.67  		}
    6.68 -		if _, returned := retrieved[sub.ID.String()]; !returned {
    6.69 -			t.Errorf("Error retrieving subscription from %T: %s wasn't in the results.", store, sub.ID)
    6.70 +		if _, returned := retrieved[sub.UserID.String()]; !returned {
    6.71 +			t.Errorf("Error retrieving subscription from %T: %s wasn't in the results.", store, sub.UserID)
    6.72  		}
    6.73 -		ok, field, expected, result := compareSubscriptions(sub, retrieved[sub.ID.String()])
    6.74 +		ok, field, expected, result := compareSubscriptions(sub, retrieved[sub.UserID.String()])
    6.75  		if !ok {
    6.76  			t.Errorf("Expected %s to be %v, got %v from %T\n", field, expected, result, store)
    6.77  		}
    6.78 @@ -104,10 +111,10 @@
    6.79  		if err != ErrSubscriptionAlreadyExists {
    6.80  			t.Errorf("Unexpected error creating subscription in %T (wanted %+v): %+v\n", store, ErrSubscriptionAlreadyExists, err)
    6.81  		}
    6.82 -		sub.ID = uuid.NewID()
    6.83 +		sub.UserID = uuid.NewID()
    6.84  		err = store.createSubscription(sub)
    6.85  		if err != ErrStripeCustomerAlreadyExists {
    6.86 -			t.Errorf("Unexpected error creating subscription in %T (wanted %+v): %+v\n", store, ErrStripeCustomerAlreadyExists, err)
    6.87 +			t.Errorf("Unexpected error creating subscription in %T (wanted %+v): %#+v\n", store, ErrStripeCustomerAlreadyExists, err)
    6.88  		}
    6.89  		sub.StripeCustomer = "stripeCustomer2"
    6.90  		err = store.createSubscription(sub)
    6.91 @@ -120,27 +127,25 @@
    6.92  func TestUpdateSubscription(t *testing.T) {
    6.93  	variations := 1 << 7
    6.94  	sub := Subscription{
    6.95 -		ID:             uuid.NewID(),
    6.96  		UserID:         uuid.NewID(),
    6.97  		StripeCustomer: "default",
    6.98  		Amount:         -1,
    6.99  		Period:         MonthlyPeriod,
   6.100 -		Created:        time.Now().Add(time.Hour * -24),
   6.101 -		BeginCharging:  time.Now().Add(time.Hour * -24),
   6.102 -		LastCharged:    time.Now().Add(time.Hour * -24),
   6.103 -		LastNotified:   time.Now().Add(time.Hour * -24),
   6.104 +		Created:        time.Now().Round(time.Millisecond).Add(time.Hour * -24),
   6.105 +		BeginCharging:  time.Now().Round(time.Millisecond).Add(time.Hour * -24),
   6.106 +		LastCharged:    time.Now().Round(time.Millisecond).Add(time.Hour * -24),
   6.107 +		LastNotified:   time.Now().Round(time.Millisecond).Add(time.Hour * -24),
   6.108  		InLockout:      true,
   6.109  	}
   6.110  	sub2 := Subscription{
   6.111 -		ID:             uuid.NewID(),
   6.112  		UserID:         uuid.NewID(),
   6.113  		StripeCustomer: "stripeCustomer2",
   6.114  		Amount:         -2,
   6.115  		Period:         MonthlyPeriod,
   6.116 -		Created:        time.Now(),
   6.117 -		BeginCharging:  time.Now(),
   6.118 -		LastCharged:    time.Now(),
   6.119 -		LastNotified:   time.Now(),
   6.120 +		Created:        time.Now().Round(time.Millisecond),
   6.121 +		BeginCharging:  time.Now().Round(time.Millisecond),
   6.122 +		LastCharged:    time.Now().Round(time.Millisecond),
   6.123 +		LastNotified:   time.Now().Round(time.Millisecond),
   6.124  		InLockout:      false,
   6.125  	}
   6.126  
   6.127 @@ -179,19 +184,19 @@
   6.128  		}
   6.129  
   6.130  		if i&subscriptionChangeBeginCharging != 0 {
   6.131 -			beginCharging = time.Now().Add(time.Hour * time.Duration(i))
   6.132 +			beginCharging = time.Now().Round(time.Millisecond).Add(time.Hour * time.Duration(i))
   6.133  			change.BeginCharging = &beginCharging
   6.134  			expectation.BeginCharging = beginCharging
   6.135  		}
   6.136  
   6.137  		if i&subscriptionChangeLastCharged != 0 {
   6.138 -			lastCharged = time.Now().Add(time.Hour * time.Duration(i))
   6.139 +			lastCharged = time.Now().Round(time.Millisecond).Add(time.Hour * time.Duration(i))
   6.140  			change.LastCharged = &lastCharged
   6.141  			expectation.LastCharged = lastCharged
   6.142  		}
   6.143  
   6.144  		if i&subscriptionChangeLastNotified != 0 {
   6.145 -			lastNotified = time.Now().Add(time.Hour * time.Duration(i))
   6.146 +			lastNotified = time.Now().Round(time.Millisecond).Add(time.Hour * time.Duration(i))
   6.147  			change.LastNotified = &lastNotified
   6.148  			expectation.LastNotified = lastNotified
   6.149  		}
   6.150 @@ -221,19 +226,19 @@
   6.151  			if err != nil {
   6.152  				t.Fatalf("Error saving subscription in %T: %s\n", store, err)
   6.153  			}
   6.154 -			err = store.updateSubscription(sub.ID, change)
   6.155 +			err = store.updateSubscription(sub.UserID, change)
   6.156  			if err != nil {
   6.157  				t.Errorf("Error updating subscription in %T: %s\n", store, err)
   6.158  			}
   6.159 -			retrieved, err := store.getSubscriptions([]uuid.ID{sub.ID})
   6.160 +			retrieved, err := store.getSubscriptions([]uuid.ID{sub.UserID})
   6.161  			if err != nil {
   6.162  				t.Errorf("Error getting subscription from %T: %s\n", store, err)
   6.163  			}
   6.164  			ok, missing := subscriptionMapContains(retrieved, sub)
   6.165  			if !ok {
   6.166 -				t.Errorf("Expected to retrieve %s from %T, but missing was %+v\n", sub.ID.String(), store, missing)
   6.167 +				t.Errorf("Expected to retrieve %s from %T, but missing was %+v\n", sub.UserID.String(), store, missing)
   6.168  			}
   6.169 -			match, field, expected, got = compareSubscriptions(expectation, retrieved[sub.ID.String()])
   6.170 +			match, field, expected, got = compareSubscriptions(expectation, retrieved[sub.UserID.String()])
   6.171  			if !match {
   6.172  				t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T\n", field, expected, got, store)
   6.173  			}
   6.174 @@ -253,7 +258,7 @@
   6.175  			t.Fatalf("Error saving subscription in %T: %+v\n", store, err)
   6.176  		}
   6.177  		change := SubscriptionChange{}
   6.178 -		err = store.updateSubscription(sub.ID, change)
   6.179 +		err = store.updateSubscription(sub.UserID, change)
   6.180  		if err != ErrSubscriptionChangeEmpty {
   6.181  			t.Errorf("Expected err to be %+v, but got %+v from %T\n", ErrSubscriptionChangeEmpty, err, store)
   6.182  		}
   6.183 @@ -263,7 +268,7 @@
   6.184  		if err != ErrSubscriptionNotFound {
   6.185  			t.Errorf("Expected err to be %+v, but got %+v from %T\n", ErrSubscriptionNotFound, err, store)
   6.186  		}
   6.187 -		err = store.updateSubscription(sub.ID, change)
   6.188 +		err = store.updateSubscription(sub.UserID, change)
   6.189  		if err != ErrStripeCustomerAlreadyExists {
   6.190  			t.Errorf("Expected err to be %+v, but got %+v from %T\n", ErrStripeCustomerAlreadyExists, err, store)
   6.191  		}
   6.192 @@ -277,12 +282,10 @@
   6.193  			t.Fatalf("Error resetting %T: %+v\n", store, err)
   6.194  		}
   6.195  		sub1 := Subscription{
   6.196 -			ID:             uuid.NewID(),
   6.197  			UserID:         uuid.NewID(),
   6.198  			StripeCustomer: "stripeCustomer1",
   6.199  		}
   6.200  		sub2 := Subscription{
   6.201 -			ID:             uuid.NewID(),
   6.202  			UserID:         uuid.NewID(),
   6.203  			StripeCustomer: "stripeCustomer2",
   6.204  		}
   6.205 @@ -294,27 +297,23 @@
   6.206  		if err != nil {
   6.207  			t.Fatalf("Error creating %+v in %T: %+v\n", sub1, store, err)
   6.208  		}
   6.209 -		err = store.deleteSubscription(sub1.ID)
   6.210 +		err = store.deleteSubscription(sub1.UserID)
   6.211  		if err != nil {
   6.212  			t.Fatalf("Error deleting %+v in %T: %+v\n", sub1, store, err)
   6.213  		}
   6.214 -		retrieved, err := store.getSubscriptions([]uuid.ID{sub1.ID, sub2.ID})
   6.215 +		retrieved, err := store.getSubscriptions([]uuid.ID{sub1.UserID, sub2.UserID})
   6.216  		if err != nil {
   6.217  			t.Errorf("Error retrieving subscriptions from %T: %+v\n", store, err)
   6.218  		}
   6.219  		ok, missing := subscriptionMapContains(retrieved, sub1)
   6.220  		if ok {
   6.221 -			t.Errorf("Expected not to retrieve %s from %T, but missing was %+v\n", sub1.ID.String(), store, missing)
   6.222 +			t.Errorf("Expected not to retrieve %s from %T, but missing was %+v\n", sub1.UserID.String(), store, missing)
   6.223  		}
   6.224  		ok, missing = subscriptionMapContains(retrieved, sub2)
   6.225  		if !ok {
   6.226 -			t.Errorf("Expected to retrieve %s from %T, but missing was %+v\n", sub2.ID.String(), store, missing)
   6.227 +			t.Errorf("Expected to retrieve %s from %T, but missing was %+v\n", sub2.UserID.String(), store, missing)
   6.228  		}
   6.229 -		_, err = store.getSubscriptionByUser(sub1.UserID)
   6.230 -		if err != ErrSubscriptionNotFound {
   6.231 -			t.Errorf("Expected err to be %+v, but got %+v from %T\n", ErrSubscriptionNotFound, err, store)
   6.232 -		}
   6.233 -		err = store.deleteSubscription(sub1.ID)
   6.234 +		err = store.deleteSubscription(sub1.UserID)
   6.235  		if err != ErrSubscriptionNotFound {
   6.236  			t.Errorf("Expected err to be %+v, but got %+v from %T\n", ErrSubscriptionNotFound, err, store)
   6.237  		}
   6.238 @@ -328,33 +327,30 @@
   6.239  			t.Fatalf("Error resetting %T: %+v\n", store, err)
   6.240  		}
   6.241  		sub1 := Subscription{
   6.242 -			ID:             uuid.NewID(),
   6.243  			UserID:         uuid.NewID(),
   6.244  			StripeCustomer: "stripeCustomer1",
   6.245  			Amount:         200,
   6.246  			Period:         MonthlyPeriod,
   6.247 -			Created:        time.Now().Add(time.Hour * -24 * 32),
   6.248 -			BeginCharging:  time.Now().Add(time.Hour * -24),
   6.249 -			LastCharged:    time.Now().Add(time.Hour * -24),
   6.250 +			Created:        time.Now().Round(time.Millisecond).Add(time.Hour * -24 * 32),
   6.251 +			BeginCharging:  time.Now().Round(time.Millisecond).Add(time.Hour * -24),
   6.252 +			LastCharged:    time.Now().Round(time.Millisecond).Add(time.Hour * -24),
   6.253  		}
   6.254  		sub2 := Subscription{
   6.255 -			ID:             uuid.NewID(),
   6.256  			UserID:         uuid.NewID(),
   6.257  			StripeCustomer: "stripeCustomer2",
   6.258  			Amount:         300,
   6.259  			Period:         MonthlyPeriod,
   6.260 -			Created:        time.Now().Add(time.Hour * -24 * 61),
   6.261 -			BeginCharging:  time.Now().Add(time.Hour * -24 * 31),
   6.262 -			LastCharged:    time.Now().Add(time.Hour * -24 * 31),
   6.263 +			Created:        time.Now().Round(time.Millisecond).Add(time.Hour * -24 * 61),
   6.264 +			BeginCharging:  time.Now().Round(time.Millisecond).Add(time.Hour * -24 * 31),
   6.265 +			LastCharged:    time.Now().Round(time.Millisecond).Add(time.Hour * -24 * 31),
   6.266  		}
   6.267  		sub3 := Subscription{
   6.268 -			ID:             uuid.NewID(),
   6.269  			UserID:         uuid.NewID(),
   6.270  			StripeCustomer: "stripeCustomer3",
   6.271  			Amount:         100,
   6.272  			Period:         MonthlyPeriod,
   6.273 -			Created:        time.Now().Add(time.Hour * -1),
   6.274 -			BeginCharging:  time.Now().Add(time.Hour * 31),
   6.275 +			Created:        time.Now().Round(time.Millisecond).Add(time.Hour * -1),
   6.276 +			BeginCharging:  time.Now().Round(time.Millisecond).Add(time.Hour * 31),
   6.277  			LastCharged:    time.Time{},
   6.278  		}
   6.279  		err = store.createSubscription(sub1)
   6.280 @@ -374,7 +370,7 @@
   6.281  		t.Logf("sub3: %+v\n", sub3)
   6.282  		// subscriptions last charged before right now
   6.283  		// should be sub1, sub2, and sub3
   6.284 -		results, err := store.listSubscriptionsLastChargedBefore(time.Now())
   6.285 +		results, err := store.listSubscriptionsLastChargedBefore(time.Now().Round(time.Millisecond))
   6.286  		if err != nil {
   6.287  			t.Errorf("Unexpected error listing subscriptions in %T: %+v\n", store, err)
   6.288  		}
   6.289 @@ -395,7 +391,7 @@
   6.290  		}
   6.291  		// subscriptions last charged before a week ago
   6.292  		// should be sub2, sub3
   6.293 -		results, err = store.listSubscriptionsLastChargedBefore(time.Now().Add(time.Hour * -24 * 7))
   6.294 +		results, err = store.listSubscriptionsLastChargedBefore(time.Now().Round(time.Millisecond).Add(time.Hour * -24 * 7))
   6.295  		if err != nil {
   6.296  			t.Errorf("Unexpected error listing subscriptions in %T: %+v\n", store, err)
   6.297  		}
   6.298 @@ -412,7 +408,7 @@
   6.299  		}
   6.300  		// subscriptions last charged before 32 days ago
   6.301  		// should be sub3
   6.302 -		results, err = store.listSubscriptionsLastChargedBefore(time.Now().Add(time.Hour * -24 * 32))
   6.303 +		results, err = store.listSubscriptionsLastChargedBefore(time.Now().Round(time.Millisecond).Add(time.Hour * -24 * 32))
   6.304  		if err != nil {
   6.305  			t.Errorf("Unexpected error listing subscriptions in %T: %+v\n", store, err)
   6.306  		}
   6.307 @@ -433,33 +429,30 @@
   6.308  			t.Fatalf("Error resetting %T: %+v\n", store, err)
   6.309  		}
   6.310  		sub1 := Subscription{
   6.311 -			ID:             uuid.NewID(),
   6.312  			UserID:         uuid.NewID(),
   6.313  			StripeCustomer: "stripeCustomer1",
   6.314  			Amount:         200,
   6.315  			Period:         MonthlyPeriod,
   6.316 -			Created:        time.Now(),
   6.317 -			BeginCharging:  time.Now().Add(time.Hour),
   6.318 +			Created:        time.Now().Round(time.Millisecond),
   6.319 +			BeginCharging:  time.Now().Round(time.Millisecond).Add(time.Hour),
   6.320  		}
   6.321  		sub2 := Subscription{
   6.322 -			ID:             uuid.NewID(),
   6.323  			UserID:         uuid.NewID(),
   6.324  			StripeCustomer: "stripeCustomer2",
   6.325  			Amount:         300,
   6.326  			Period:         MonthlyPeriod,
   6.327 -			Created:        time.Now().Add(time.Hour * -720),
   6.328 -			BeginCharging:  time.Now().Add(time.Hour*-720 + time.Hour*2),
   6.329 -			LastCharged:    time.Now(),
   6.330 +			Created:        time.Now().Round(time.Millisecond).Add(time.Hour * -720),
   6.331 +			BeginCharging:  time.Now().Round(time.Millisecond).Add(time.Hour*-720 + time.Hour*2),
   6.332 +			LastCharged:    time.Now().Round(time.Millisecond),
   6.333  		}
   6.334  		sub3 := Subscription{
   6.335 -			ID:             uuid.NewID(),
   6.336  			UserID:         uuid.NewID(),
   6.337  			StripeCustomer: "stripeCustomer3",
   6.338  			Amount:         100,
   6.339  			Period:         MonthlyPeriod,
   6.340 -			Created:        time.Now().Add(time.Hour * -1440),
   6.341 -			BeginCharging:  time.Now().Add(time.Hour * -1440),
   6.342 -			LastNotified:   time.Now().Add(time.Hour * -720),
   6.343 +			Created:        time.Now().Round(time.Millisecond).Add(time.Hour * -1440),
   6.344 +			BeginCharging:  time.Now().Round(time.Millisecond).Add(time.Hour * -1440),
   6.345 +			LastNotified:   time.Now().Round(time.Millisecond).Add(time.Hour * -720),
   6.346  			InLockout:      true,
   6.347  		}
   6.348  		err = store.createSubscription(sub1)
   6.349 @@ -478,63 +471,63 @@
   6.350  		if err != ErrNoSubscriptionID {
   6.351  			t.Errorf("Error retrieving no subscriptions from %T. Expected %+v, got %+v\n", store, ErrNoSubscriptionID, err)
   6.352  		}
   6.353 -		retrieved, err = store.getSubscriptions([]uuid.ID{sub1.ID})
   6.354 +		retrieved, err = store.getSubscriptions([]uuid.ID{sub1.UserID})
   6.355  		if err != nil {
   6.356 -			t.Errorf("Error retrieving %s from %T: %+v\n", sub1.ID, store, err)
   6.357 +			t.Errorf("Error retrieving %s from %T: %+v\n", sub1.UserID, store, err)
   6.358  		}
   6.359  		ok, missing := subscriptionMapContains(retrieved, sub1)
   6.360  		if !ok {
   6.361  			t.Logf("Results: %+v\n", retrieved)
   6.362  			t.Errorf("Expected %+v to be in the results, was not for %T.\n", missing, store)
   6.363  		}
   6.364 -		retrieved, err = store.getSubscriptions([]uuid.ID{sub1.ID, sub2.ID})
   6.365 +		retrieved, err = store.getSubscriptions([]uuid.ID{sub1.UserID, sub2.UserID})
   6.366  		if err != nil {
   6.367 -			t.Errorf("Error retrieving %s and %s from %T: %+v\n", sub1.ID, sub2.ID, store, err)
   6.368 +			t.Errorf("Error retrieving %s and %s from %T: %+v\n", sub1.UserID, sub2.UserID, store, err)
   6.369  		}
   6.370  		ok, missing = subscriptionMapContains(retrieved, sub1, sub2)
   6.371  		if !ok {
   6.372  			t.Logf("Results: %+v\n", retrieved)
   6.373  			t.Errorf("Expected %+v to be in the results, was not for %T.\n", missing, store)
   6.374  		}
   6.375 -		retrieved, err = store.getSubscriptions([]uuid.ID{sub1.ID, sub3.ID})
   6.376 +		retrieved, err = store.getSubscriptions([]uuid.ID{sub1.UserID, sub3.UserID})
   6.377  		if err != nil {
   6.378 -			t.Errorf("Error retrieving %s and %s from %T: %+v\n", sub1.ID, sub3.ID, store, err)
   6.379 +			t.Errorf("Error retrieving %s and %s from %T: %+v\n", sub1.UserID, sub3.UserID, store, err)
   6.380  		}
   6.381  		ok, missing = subscriptionMapContains(retrieved, sub1, sub3)
   6.382  		if !ok {
   6.383  			t.Logf("Results: %+v\n", retrieved)
   6.384  			t.Errorf("Expected %+v to be in the results, was not for %T.\n", missing, store)
   6.385  		}
   6.386 -		retrieved, err = store.getSubscriptions([]uuid.ID{sub1.ID, sub2.ID, sub3.ID})
   6.387 +		retrieved, err = store.getSubscriptions([]uuid.ID{sub1.UserID, sub2.UserID, sub3.UserID})
   6.388  		if err != nil {
   6.389 -			t.Errorf("Error retrieving %s, %s, and %s from %T: %+v\n", sub1.ID, sub2.ID, sub3.ID, store, err)
   6.390 +			t.Errorf("Error retrieving %s, %s, and %s from %T: %+v\n", sub1.UserID, sub2.UserID, sub3.UserID, store, err)
   6.391  		}
   6.392  		ok, missing = subscriptionMapContains(retrieved, sub1, sub2, sub3)
   6.393  		if !ok {
   6.394  			t.Logf("Results: %+v\n", retrieved)
   6.395  			t.Errorf("Expected %+v to be in the results, was not for %T.\n", missing, store)
   6.396  		}
   6.397 -		retrieved, err = store.getSubscriptions([]uuid.ID{sub2.ID})
   6.398 +		retrieved, err = store.getSubscriptions([]uuid.ID{sub2.UserID})
   6.399  		if err != nil {
   6.400 -			t.Errorf("Error retrieving %s from %T: %+v\n", sub2.ID, store, err)
   6.401 +			t.Errorf("Error retrieving %s from %T: %+v\n", sub2.UserID, store, err)
   6.402  		}
   6.403  		ok, missing = subscriptionMapContains(retrieved, sub2)
   6.404  		if !ok {
   6.405  			t.Logf("Results: %+v\n", retrieved)
   6.406  			t.Errorf("Expected %+v to be in the results, was not for %T.\n", missing, store)
   6.407  		}
   6.408 -		retrieved, err = store.getSubscriptions([]uuid.ID{sub2.ID, sub3.ID})
   6.409 +		retrieved, err = store.getSubscriptions([]uuid.ID{sub2.UserID, sub3.UserID})
   6.410  		if err != nil {
   6.411 -			t.Errorf("Error retrieving %s and %s from %T: %+v\n", sub2.ID, sub3.ID, store, err)
   6.412 +			t.Errorf("Error retrieving %s and %s from %T: %+v\n", sub2.UserID, sub3.UserID, store, err)
   6.413  		}
   6.414  		ok, missing = subscriptionMapContains(retrieved, sub2, sub3)
   6.415  		if !ok {
   6.416  			t.Logf("Results: %+v\n", retrieved)
   6.417  			t.Errorf("Expected %+v to be in the results, was not for %T.\n", missing, store)
   6.418  		}
   6.419 -		retrieved, err = store.getSubscriptions([]uuid.ID{sub3.ID})
   6.420 +		retrieved, err = store.getSubscriptions([]uuid.ID{sub3.UserID})
   6.421  		if err != nil {
   6.422 -			t.Errorf("Error retrieving %s from %T: %+v\n", sub3.ID, store, err)
   6.423 +			t.Errorf("Error retrieving %s from %T: %+v\n", sub3.UserID, store, err)
   6.424  		}
   6.425  		ok, missing = subscriptionMapContains(retrieved, sub3)
   6.426  		if !ok {
   6.427 @@ -548,7 +541,7 @@
   6.428  		if len(retrieved) != 0 {
   6.429  			t.Errorf("Expected no results, %T returned %+v\n", store, retrieved)
   6.430  		}
   6.431 -		retrieved, err = store.getSubscriptions([]uuid.ID{sub1.ID, sub2.ID, uuid.NewID(), sub3.ID})
   6.432 +		retrieved, err = store.getSubscriptions([]uuid.ID{sub1.UserID, sub2.UserID, uuid.NewID(), sub3.UserID})
   6.433  		if err != nil {
   6.434  			t.Errorf("Error retrieving non-existent ID from %T: %+v\n", store, err)
   6.435  		}
   6.436 @@ -562,67 +555,3 @@
   6.437  		}
   6.438  	}
   6.439  }
   6.440 -
   6.441 -func TestGetSubscriptionByUser(t *testing.T) {
   6.442 -	for _, store := range testSubscriptionStores {
   6.443 -		err := store.reset()
   6.444 -		if err != nil {
   6.445 -			t.Fatalf("Error resetting %T: %+v\n", store, err)
   6.446 -		}
   6.447 -		sub1 := Subscription{
   6.448 -			ID:             uuid.NewID(),
   6.449 -			UserID:         uuid.NewID(),
   6.450 -			StripeCustomer: "stripeCustomer1",
   6.451 -		}
   6.452 -		sub2 := Subscription{
   6.453 -			ID:             uuid.NewID(),
   6.454 -			UserID:         uuid.NewID(),
   6.455 -			StripeCustomer: "stripeCustomer2",
   6.456 -		}
   6.457 -		sub3 := Subscription{
   6.458 -			ID:             uuid.NewID(),
   6.459 -			UserID:         uuid.NewID(),
   6.460 -			StripeCustomer: "stripeCustomer3",
   6.461 -		}
   6.462 -		err = store.createSubscription(sub1)
   6.463 -		if err != nil {
   6.464 -			t.Fatalf("Error creating %+v in %T: %+v\n", sub1, store, err)
   6.465 -		}
   6.466 -		err = store.createSubscription(sub2)
   6.467 -		if err != nil {
   6.468 -			t.Fatalf("Error creating %+v in %T: %+v\n", sub1, store, err)
   6.469 -		}
   6.470 -		err = store.createSubscription(sub3)
   6.471 -		if err != nil {
   6.472 -			t.Fatalf("Error creating %+v in %T: %+v\n", sub1, store, err)
   6.473 -		}
   6.474 -		retrieved, err := store.getSubscriptionByUser(sub1.UserID)
   6.475 -		if err != nil {
   6.476 -			t.Errorf("Error retrieving subscription %+v from %T: %+v\n", sub1, store, err)
   6.477 -		}
   6.478 -		ok, field, expected, result := compareSubscriptions(sub1, retrieved)
   6.479 -		if !ok {
   6.480 -			t.Errorf("Expected %s to be %+v, but was %+v in %T\n", field, expected, result, store)
   6.481 -		}
   6.482 -		retrieved, err = store.getSubscriptionByUser(sub2.UserID)
   6.483 -		if err != nil {
   6.484 -			t.Errorf("Error retrieving subscription %+v from %T: %+v\n", sub2, store, err)
   6.485 -		}
   6.486 -		ok, field, expected, result = compareSubscriptions(sub2, retrieved)
   6.487 -		if !ok {
   6.488 -			t.Errorf("Expected %s to be %+v, but was %+v in %T\n", field, expected, result, store)
   6.489 -		}
   6.490 -		retrieved, err = store.getSubscriptionByUser(sub3.UserID)
   6.491 -		if err != nil {
   6.492 -			t.Errorf("Error retrieving subscription %+v from %T: %+v\n", sub3, store, err)
   6.493 -		}
   6.494 -		ok, field, expected, result = compareSubscriptions(sub3, retrieved)
   6.495 -		if !ok {
   6.496 -			t.Errorf("Expected %s to be %+v, but was %+v in %T\n", field, expected, result, store)
   6.497 -		}
   6.498 -		retrieved, err = store.getSubscriptionByUser(uuid.NewID())
   6.499 -		if err != ErrSubscriptionNotFound {
   6.500 -			t.Errorf("Expected err to be %+v, got %+v from %T\n", ErrSubscriptionNotFound, err, store)
   6.501 -		}
   6.502 -	}
   6.503 -}