ducky/subscriptions
2015-10-04
Parent:fb2c0e498e37
ducky/subscriptions/subscription_postgres.go
Document our client to make golint happy. Take care of all the documentation warnings in the client subpackage, which means golint now returns successfully.
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 }