ducky/subscriptions

Paddy 2015-10-04 Parent:fb2c0e498e37

17:7eef47ecc01c Go to Latest

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.

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 }