ducky/subscriptions

Paddy 2015-06-11 Child:f1a22fc2321d

0:56a2bef197cd Go to Latest

ducky/subscriptions/subscription.go

First implementation of datastore interface. Define Subscriptions, and the SubscriptionChange type that can modify Subscriptions. Define the subscriptionStore, which describes how Subscriptions are to be created, retrieved, updated, and deleted in/from the storage backend they're stored in. Create a Memstore implementation of the subscriptionStore, which stores all our data in-memory, for use in testing. Write tests to cover the subscriptionStore interface, testing the entire Memstore. We'll plug future storage backends into these tests, to make sure they all behave the same, and to exercise each storage backend without requiring a suite of tests for each. At this point, we have 100% test coverage and no complaints from golint or go vet. I expect it's all downhill from here.

History
1 package subscriptions
3 import (
4 "errors"
5 "time"
7 "code.secondbit.org/uuid.hg"
8 )
10 const (
11 // MonthlyPeriod represents a period of once a month.
12 MonthlyPeriod period = "monthly"
13 )
15 var (
16 // ErrSubscriptionAlreadyExists is returned when a Subscription
17 // with an identical ID already exists in the subscriptionStore.
18 ErrSubscriptionAlreadyExists = errors.New("Subscription already exists")
19 // ErrSubscriptionNotFound is returned when a single Subscription
20 // is acted upon or requested, but cannot be found.
21 ErrSubscriptionNotFound = errors.New("Subscription not found")
22 // ErrStripeCustomerAlreadyExists is returned when a Subscription
23 // is created or updates its StripeCustomer property, but that
24 // StripeCustomer is already associated with another Subscription.
25 ErrStripeCustomerAlreadyExists = errors.New("Stripe customer already assigned to another Subscription")
26 // ErrSubscriptionChangeEmpty is returned when a SubscriptionChange
27 // is empty but is passed to subscriptionStore.UpdateSubscription
28 // anyways.
29 ErrSubscriptionChangeEmpty = errors.New("SubscriptionChange is empty")
30 // ErrNoSubscriptionID is returned when one or more Subscription IDs
31 // are required, but none are provided.
32 ErrNoSubscriptionID = errors.New("no Subscription ID provided")
33 )
35 type period string
37 // Subscription represents the state of a user's payments. It holds
38 // metadata about the last time a user was charged, how much a user
39 // should be charged, how to charge a user and how much to charge
40 // the user.
41 type Subscription struct {
42 ID uuid.ID
43 UserID uuid.ID
44 StripeCustomer string
45 Amount int
46 Period period
47 Created time.Time
48 BeginCharging time.Time
49 LastCharged time.Time
50 LastNotified time.Time
51 InLockout bool
52 }
54 // SubscriptionChange represents desired changes to a Subscription
55 // object. A nil value means that property should remain unchanged.
56 type SubscriptionChange struct {
57 StripeCustomer *string
58 Amount *int
59 Period *period
60 BeginCharging *time.Time
61 LastCharged *time.Time
62 LastNotified *time.Time
63 InLockout *bool
64 }
66 // IsEmpty returns true if the SubscriptionChange doesn't request
67 // a change to any property of the Subscription.
68 func (change SubscriptionChange) IsEmpty() bool {
69 if change.StripeCustomer != nil {
70 return false
71 }
72 if change.Amount != nil {
73 return false
74 }
75 if change.Period != nil {
76 return false
77 }
78 if change.BeginCharging != nil {
79 return false
80 }
81 if change.LastCharged != nil {
82 return false
83 }
84 if change.LastNotified != nil {
85 return false
86 }
87 if change.InLockout != nil {
88 return false
89 }
90 return true
91 }
93 // ApplyChange updates a Subscription based on the changes requested
94 // by a SubscriptionChange.
95 func (s *Subscription) ApplyChange(change SubscriptionChange) {
96 if change.StripeCustomer != nil {
97 s.StripeCustomer = *change.StripeCustomer
98 }
99 if change.Amount != nil {
100 s.Amount = *change.Amount
101 }
102 if change.Period != nil {
103 s.Period = *change.Period
104 }
105 if change.BeginCharging != nil {
106 s.BeginCharging = *change.BeginCharging
107 }
108 if change.LastCharged != nil {
109 s.LastCharged = *change.LastCharged
110 }
111 if change.LastNotified != nil {
112 s.LastNotified = *change.LastNotified
113 }
114 if change.InLockout != nil {
115 s.InLockout = *change.InLockout
116 }
117 }
119 // ByLastChargeDate allows us to sort a []Subscription by the LastCharged
120 // property, with the lowest LastCharged date first.
121 type ByLastChargeDate []Subscription
123 // Len returns the length the SubscriptionsByLastChargeDate. It fulfills
124 // the sort.Interface interface.
125 func (s ByLastChargeDate) Len() int {
126 return len(s)
127 }
129 // Swap puts the item in position i in position j, and the item in position
130 // j in position i. It fulfills the sort.Interface interface.
131 func (s ByLastChargeDate) Swap(i, j int) {
132 s[i], s[j] = s[j], s[i]
133 }
135 // Less returns true if the item in position i should be sorted before the
136 // item in position j.
137 func (s ByLastChargeDate) Less(i, j int) bool {
138 return s[i].LastCharged.Before(s[j].LastCharged)
139 }
141 type subscriptionStore interface {
142 reset() error
143 createSubscription(sub Subscription) error
144 updateSubscription(id uuid.ID, change SubscriptionChange) error
145 deleteSubscription(id uuid.ID) error
146 listSubscriptionsLastChargedBefore(time.Time) ([]Subscription, error)
147 getSubscriptions(ids []uuid.ID) (map[string]Subscription, error)
148 getSubscriptionByUser(id uuid.ID) (Subscription, error)
149 }