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
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 }