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