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