ducky/subscriptions

Paddy 2015-10-04 Parent:fb2c0e498e37

16:b063bc0a6e84 Go to Latest

ducky/subscriptions/subscription_postgres.go

Make api subpackage golint-passing. Add comments to all the exported functions, methods, and variables in the api subpackage, to make golint happy. Also, make the individual endpoints in the api subpackage unexported, as there's no real use case for exporting them. The handlers depend on the placeholders in the endpoint, so we need them to be controlled in unison, which means it's probably a bad idea to declare the route outside of the API package. And the only reason to expose the Handler is so people can declare custom endpoints.

History
1 package subscriptions
3 import (
4 "log"
6 "code.secondbit.org/uuid.hg"
8 "github.com/lib/pq"
9 "github.com/secondbit/pan"
10 )
12 // GetSQLTableName fulfills the pan.SQLTableNamer interface, allowing
13 // us to manipulate Subscriptions with pan.
14 func (s Subscription) GetSQLTableName() string {
15 return "subscriptions"
16 }
18 func (p Postgres) resetSQL() *pan.Query {
19 var subscription Subscription
20 query := pan.New(pan.POSTGRES, "TRUNCATE "+pan.GetTableName(subscription))
21 return query.FlushExpressions(" ")
22 }
24 // Reset returns the database to its initialised state. It is meant to
25 // be used during testing to reset the databases to a known state between
26 // tests. It should never be used in production.
27 func (p Postgres) Reset() error {
28 query := p.resetSQL()
29 _, err := p.Exec(query.String(), query.Args...)
30 if err != nil {
31 return err
32 }
33 return nil
34 }
36 func (p Postgres) createSubscriptionSQL(subscription Subscription) *pan.Query {
37 fields, values := pan.GetFields(subscription)
38 query := pan.New(pan.POSTGRES, "INSERT INTO "+pan.GetTableName(subscription))
39 query.Include("(" + pan.QueryList(fields) + ")")
40 query.Include("VALUES")
41 query.Include("("+pan.VariableList(len(values))+")", values...)
42 return query.FlushExpressions(" ")
43 }
45 // CreateSubscription stores the passed Subscription in the database. If
46 // a Subscription sharing the same UserID already exists, an
47 // ErrSubscriptionAlreadyExists error will be returned. If a Subscription
48 // sharing the same StripeSubscription already exists, an
49 // ErrStripeSubscriptionAlreadyExists error will be returned.
50 func (p Postgres) CreateSubscription(sub Subscription) error {
51 query := p.createSubscriptionSQL(sub)
52 _, err := p.Exec(query.String(), query.Args...)
53 if e, ok := err.(*pq.Error); ok && e.Constraint == "subscriptions_pkey" {
54 err = ErrSubscriptionAlreadyExists
55 } else if e, ok := err.(*pq.Error); ok && e.Constraint == "subscriptions_stripe_subscription_key" {
56 err = ErrStripeSubscriptionAlreadyExists
57 }
58 return err
59 }
61 func (p Postgres) updateSubscriptionSQL(id uuid.ID, change SubscriptionChange) *pan.Query {
62 var subscription Subscription
63 query := pan.New(pan.POSTGRES, "UPDATE "+pan.GetTableName(subscription)+" SET")
64 query.IncludeIfNotNil(pan.GetUnquotedColumn(subscription, "StripeSubscription")+" = ?", change.StripeSubscription)
65 query.IncludeIfNotNil(pan.GetUnquotedColumn(subscription, "Plan")+" = ?", change.Plan)
66 query.IncludeIfNotNil(pan.GetUnquotedColumn(subscription, "Status")+" = ?", change.Status)
67 query.IncludeIfNotNil(pan.GetUnquotedColumn(subscription, "Canceling")+" = ?", change.Canceling)
68 query.IncludeIfNotNil(pan.GetUnquotedColumn(subscription, "TrialStart")+" = ?", change.TrialStart)
69 query.IncludeIfNotNil(pan.GetUnquotedColumn(subscription, "TrialEnd")+" = ?", change.TrialEnd)
70 query.IncludeIfNotNil(pan.GetUnquotedColumn(subscription, "PeriodStart")+" = ?", change.PeriodStart)
71 query.IncludeIfNotNil(pan.GetUnquotedColumn(subscription, "PeriodEnd")+" = ?", change.PeriodEnd)
72 query.IncludeIfNotNil(pan.GetUnquotedColumn(subscription, "CanceledAt")+" = ?", change.CanceledAt)
73 query.IncludeIfNotNil(pan.GetUnquotedColumn(subscription, "FailedChargeAttempts")+" = ?", change.FailedChargeAttempts)
74 query.IncludeIfNotNil(pan.GetUnquotedColumn(subscription, "LastFailedCharge")+" = ?", change.LastFailedCharge)
75 query.IncludeIfNotNil(pan.GetUnquotedColumn(subscription, "LastNotified")+" = ?", change.LastNotified)
76 query.FlushExpressions(", ")
77 query.IncludeWhere()
78 query.Include(pan.GetUnquotedColumn(subscription, "UserID")+" = ?", id)
79 return query.FlushExpressions(" ")
80 }
82 // UpdateSubscription applies the SubscriptionChange passed to the Subscription
83 // stored in the database associated with the passed ID. If change is empty,
84 // an ErrSubscriptionChangeEmpty error is returned. If no Subscription is found
85 // in the database with the passed ID, an ErrSubscriptionNotFound error is returned.
86 // If change is updating the StripeSubscription and a Subscription in the database
87 // already has that value set for StripeSubscription, an
88 // ErrStripeSubscriptionAlreadyExists error is returned.
89 func (p Postgres) UpdateSubscription(id uuid.ID, change SubscriptionChange) error {
90 if change.IsEmpty() {
91 return ErrSubscriptionChangeEmpty
92 }
94 query := p.updateSubscriptionSQL(id, change)
95 res, err := p.Exec(query.String(), query.Args...)
96 if e, ok := err.(*pq.Error); ok && e.Constraint == "subscriptions_stripe_subscription_key" {
97 return ErrStripeSubscriptionAlreadyExists
98 } else if err != nil {
99 return err
100 }
101 rows, err := res.RowsAffected()
102 if err != nil {
103 return err
104 }
105 if rows < 1 {
106 return ErrSubscriptionNotFound
107 }
108 return nil
109 }
111 func (p Postgres) deleteSubscriptionSQL(id uuid.ID) *pan.Query {
112 var subscription Subscription
113 query := pan.New(pan.POSTGRES, "DELETE FROM "+pan.GetTableName(subscription))
114 query.IncludeWhere()
115 query.Include(pan.GetUnquotedColumn(subscription, "UserID")+" = ?", id)
116 return query.FlushExpressions(" ")
117 }
119 // DeleteSubscription removes the Subscription stored in the database associated
120 // with the passed ID from the database. If no Subscription is found
121 // in the database with the passed ID, an ErrSubscriptionNotFound error is returned.
122 func (p Postgres) DeleteSubscription(id uuid.ID) error {
123 query := p.deleteSubscriptionSQL(id)
124 res, err := p.Exec(query.String(), query.Args...)
125 if err != nil {
126 return err
127 }
128 rows, err := res.RowsAffected()
129 if err != nil {
130 return err
131 }
132 if rows < 1 {
133 return ErrSubscriptionNotFound
134 }
135 return nil
136 }
138 func (p Postgres) getSubscriptionsSQL(ids []uuid.ID) *pan.Query {
139 var subscription Subscription
140 fields, _ := pan.GetFields(subscription)
141 intIDs := make([]interface{}, len(ids))
142 for pos, id := range ids {
143 intIDs[pos] = id
144 }
145 query := pan.New(pan.POSTGRES, "SELECT "+pan.QueryList(fields)+" FROM "+pan.GetTableName(subscription))
146 query.IncludeWhere()
147 query.Include(pan.GetUnquotedColumn(subscription, "UserID") + " IN")
148 query.Include("("+pan.VariableList(len(intIDs))+")", intIDs...)
149 return query.FlushExpressions(" ")
150 }
152 // GetSubscriptions retrieves the Subscriptions stored in the database associated
153 // with the passed IDs. If no IDs are passed, an ErrNoSubscriptionID error is
154 // returned. No matter how many of the IDs are found (including none), a map is
155 // returned, with the key being a String()ed version of the ID for the Subscription in
156 // the value. If no error is returned, the map will represent all of the Subscriptions
157 // matching the passed IDs that exist in the database, even if it's empty.
158 func (p Postgres) GetSubscriptions(ids []uuid.ID) (map[string]Subscription, error) {
159 results := map[string]Subscription{}
160 if len(ids) < 1 {
161 return results, ErrNoSubscriptionID
162 }
163 query := p.getSubscriptionsSQL(ids)
164 rows, err := p.Query(query.String(), query.Args...)
165 if err != nil {
166 return results, err
167 }
168 for rows.Next() {
169 var subscription Subscription
170 err := pan.Unmarshal(rows, &subscription)
171 if err != nil {
172 return results, err
173 }
174 results[subscription.UserID.String()] = subscription
175 }
176 if err := rows.Err(); err != nil {
177 return results, err
178 }
179 return results, nil
180 }
182 func (p Postgres) getSubscriptionStatsCountSQL() *pan.Query {
183 var subscription Subscription
184 query := pan.New(pan.POSTGRES, "SELECT COUNT(*) FROM")
185 query.Include(pan.GetTableName(subscription))
186 return query.FlushExpressions(" ")
187 }
189 func (p Postgres) getSubscriptionStatsCancelingSQL() *pan.Query {
190 var subscription Subscription
191 query := pan.New(pan.POSTGRES, "SELECT COUNT(*) FROM")
192 query.Include(pan.GetTableName(subscription))
193 query.IncludeWhere()
194 query.Include(pan.GetUnquotedColumn(subscription, "Canceling")+" = ?", true)
195 return query.FlushExpressions(" ")
196 }
198 func (p Postgres) getSubscriptionStatsFailingSQL() *pan.Query {
199 var subscription Subscription
200 query := pan.New(pan.POSTGRES, "SELECT COUNT(*) FROM")
201 query.Include(pan.GetTableName(subscription))
202 query.IncludeWhere()
203 statuses := []interface{}{"past_due", "unpaid"}
204 query.Include(pan.GetUnquotedColumn(subscription, "Status")+" IN ("+pan.VariableList(len(statuses))+")", statuses...)
205 return query.FlushExpressions(" ")
206 }
208 func (p Postgres) getSubscriptionStatsPlansSQL() *pan.Query {
209 var subscription Subscription
210 fields := []interface{}{pan.GetUnquotedColumn(subscription, "Plan"), "COUNT(*)"}
211 query := pan.New(pan.POSTGRES, "SELECT "+pan.QueryList(fields)+" FROM")
212 query.Include(pan.GetTableName(subscription))
213 query.Include("GROUP BY " + pan.GetUnquotedColumn(subscription, "Plan"))
214 return query.FlushExpressions(" ")
215 }
217 // GetSubscriptionStats returns statistics about the subscription data stored in the
218 // database as a SubscriptionStats variable. The number of Subscriptions, the
219 // breakdown of how many Subscriptions belong to each plan, the number of
220 // Subscriptions that are canceling, and the number of Subscriptions whose payment
221 // information is failing are all tracked.
222 func (p Postgres) GetSubscriptionStats() (SubscriptionStats, error) {
223 stats := SubscriptionStats{
224 Plans: map[string]int64{},
225 }
226 query := p.getSubscriptionStatsCountSQL()
227 err := p.QueryRow(query.String(), query.Args...).Scan(&stats.Number)
228 if err != nil {
229 log.Printf("Error querying for total subscriptions: %+v\n", err)
230 return stats, err
231 }
232 query = p.getSubscriptionStatsCancelingSQL()
233 err = p.QueryRow(query.String(), query.Args...).Scan(&stats.Canceling)
234 if err != nil {
235 log.Printf("Error querying for canceling subscriptions: %+v\n", err)
236 return stats, err
237 }
238 query = p.getSubscriptionStatsFailingSQL()
239 err = p.QueryRow(query.String(), query.Args...).Scan(&stats.Failing)
240 if err != nil {
241 log.Printf("Error querying for failing subscriptions: %+v\n", err)
242 return stats, err
243 }
244 query = p.getSubscriptionStatsPlansSQL()
245 rows, err := p.Query(query.String(), query.Args...)
246 if err != nil {
247 log.Printf("Error querying for plans: %+v\n", err)
248 return stats, err
249 }
250 for rows.Next() {
251 var plan string
252 var count int64
253 err := rows.Scan(&plan, &count)
254 if err != nil {
255 log.Printf("Error scanning database row for plans: %+v\n", err)
256 continue
257 }
258 stats.Plans[plan] = count
259 }
260 if err := rows.Err(); err != nil {
261 log.Printf("Error querying for plans: %+v\n", err)
262 return stats, err
263 }
264 return stats, nil
265 }