-
Notifications
You must be signed in to change notification settings - Fork 393
/
usercredits.go
225 lines (184 loc) · 6.65 KB
/
usercredits.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package satellitedb
import (
"context"
"database/sql"
"time"
"github.com/zeebo/errs"
"storj.io/common/uuid"
"storj.io/storj/private/currency"
"storj.io/storj/private/dbutil/pgutil"
"storj.io/storj/satellite/console"
"storj.io/storj/satellite/rewards"
"storj.io/storj/satellite/satellitedb/dbx"
)
// ensures that usercredits implements console.UserCredits.
var _ console.UserCredits = (*usercredits)(nil)
type usercredits struct {
db *satelliteDB
tx *dbx.Tx
}
// GetCreditUsage returns the total amount of referral a user has made based on user id, total available credits, and total used credits based on user id.
func (c *usercredits) GetCreditUsage(ctx context.Context, userID uuid.UUID, expirationEndDate time.Time) (*console.UserCreditUsage, error) {
usageRows, err := c.db.DB.QueryContext(ctx, c.db.Rebind(`SELECT a.used_credit, b.available_credit, c.referred
FROM (SELECT SUM(credits_used_in_cents) AS used_credit FROM user_credits WHERE user_id = ?) AS a,
(SELECT SUM(credits_earned_in_cents - credits_used_in_cents) AS available_credit FROM user_credits WHERE expires_at > ? AND user_id = ?) AS b,
(SELECT count(id) AS referred FROM user_credits WHERE user_credits.user_id = ? AND user_credits.type = ?) AS c;`), userID[:], expirationEndDate, userID[:], userID[:], console.Referrer)
if err != nil {
return nil, errs.Wrap(err)
}
defer func() { err = errs.Combine(err, usageRows.Close()) }()
usage := console.UserCreditUsage{}
for usageRows.Next() {
var (
usedCreditInCents sql.NullInt64
availableCreditInCents sql.NullInt64
referred sql.NullInt64
)
err = usageRows.Scan(&usedCreditInCents, &availableCreditInCents, &referred)
if err != nil {
return nil, errs.Wrap(err)
}
usage.Referred += referred.Int64
usage.UsedCredits = usage.UsedCredits.Add(currency.Cents(int(usedCreditInCents.Int64)))
usage.AvailableCredits = usage.AvailableCredits.Add(currency.Cents(int(availableCreditInCents.Int64)))
}
return &usage, usageRows.Err()
}
// Create insert a new record of user credit.
func (c *usercredits) Create(ctx context.Context, userCredit console.CreateCredit) (err error) {
if userCredit.ExpiresAt.Before(time.Now().UTC()) {
return errs.New("user credit is already expired")
}
var referrerID []byte
if userCredit.ReferredBy != nil {
referrerID = userCredit.ReferredBy[:]
}
var shouldCreate bool
switch userCredit.OfferInfo.Type {
case rewards.Partner:
shouldCreate = false
default:
shouldCreate = userCredit.OfferInfo.Status.IsDefault()
}
var dbExec interface {
ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
}
if c.tx != nil {
dbExec = c.tx.Tx
} else {
dbExec = c.db.DB
}
var (
result sql.Result
statement string
)
statement = `
INSERT INTO user_credits (user_id, offer_id, credits_earned_in_cents, credits_used_in_cents, expires_at, referred_by, type, created_at)
SELECT * FROM (VALUES (?::bytea, ?::int, ?::int, 0, ?::timestamp, NULLIF(?::bytea, ?::bytea), ?::text, now())) AS v
WHERE COALESCE((SELECT COUNT(offer_id) FROM user_credits WHERE offer_id = ? AND referred_by IS NOT NULL ) < NULLIF(?, 0), ?);
`
result, err = dbExec.ExecContext(ctx, c.db.Rebind(statement),
userCredit.UserID[:],
userCredit.OfferID,
userCredit.CreditsEarned.Cents(),
userCredit.ExpiresAt, referrerID, new([]byte),
userCredit.Type,
userCredit.OfferID,
userCredit.OfferInfo.RedeemableCap, shouldCreate)
if err != nil {
// check to see if there's a constraint error
if pgutil.IsConstraintError(err) {
_, err := dbExec.ExecContext(ctx, c.db.Rebind(`UPDATE offers SET status = ? AND expires_at = ? WHERE id = ?`), rewards.Done, time.Now().UTC(), userCredit.OfferID)
if err != nil {
return errs.Wrap(err)
}
return rewards.ErrReachedMaxCapacity.Wrap(err)
}
return errs.Wrap(err)
}
rows, err := result.RowsAffected()
if err != nil {
return errs.Wrap(err)
}
if rows != 1 {
return rewards.ErrReachedMaxCapacity.New("failed to create new credit")
}
return nil
}
// UpdateEarnedCredits updates user credits after user activated their account.
func (c *usercredits) UpdateEarnedCredits(ctx context.Context, userID uuid.UUID) error {
statement := `
UPDATE user_credits SET credits_earned_in_cents = offers.invitee_credit_in_cents
FROM offers
WHERE user_id = ? AND credits_earned_in_cents = 0 AND offer_id = offers.id
`
result, err := c.db.DB.ExecContext(ctx, c.db.Rebind(statement), userID[:])
if err != nil {
return err
}
affected, err := result.RowsAffected()
if err != nil {
return err
}
if affected != 1 {
return console.NoCreditForUpdateErr.New("row affected: %d", affected)
}
return nil
}
// UpdateAvailableCredits updates user's available credits based on their spending and the time of their spending.
func (c *usercredits) UpdateAvailableCredits(ctx context.Context, creditsToCharge int, id uuid.UUID, expirationEndDate time.Time) (remainingCharge int, err error) {
err = c.db.WithTx(ctx, func(ctx context.Context, tx *dbx.Tx) (err error) {
availableCredits, err := tx.All_UserCredit_By_UserId_And_ExpiresAt_Greater_And_CreditsUsedInCents_Less_CreditsEarnedInCents_OrderBy_Asc_ExpiresAt(ctx,
dbx.UserCredit_UserId(id[:]),
dbx.UserCredit_ExpiresAt(expirationEndDate),
)
if err != nil {
return err
}
if len(availableCredits) == 0 {
return errs.New("No available credits")
}
values := make([]interface{}, len(availableCredits)*2)
rowIds := make([]interface{}, len(availableCredits))
remainingCharge = creditsToCharge
for i, credit := range availableCredits {
if remainingCharge == 0 {
break
}
creditsForUpdateInCents := credit.CreditsEarnedInCents - credit.CreditsUsedInCents
if remainingCharge < creditsForUpdateInCents {
creditsForUpdateInCents = remainingCharge
}
values[i%2] = credit.Id
values[(i%2 + 1)] = creditsForUpdateInCents
rowIds[i] = credit.Id
remainingCharge -= creditsForUpdateInCents
}
values = append(values, rowIds...)
statement := generateQuery(len(availableCredits), true)
_, err = tx.Tx.ExecContext(ctx, c.db.Rebind(`UPDATE user_credits SET credits_used_in_cents = CASE `+statement), values...)
return err
})
if err != nil {
return creditsToCharge, errs.Wrap(err)
}
return remainingCharge, nil
}
func generateQuery(totalRows int, toInt bool) (query string) {
whereClause := `WHERE id IN (`
condition := `WHEN id=? THEN ? `
if toInt {
condition = `WHEN id=? THEN ?::int `
}
for i := 0; i < totalRows; i++ {
query += condition
if i == totalRows-1 {
query += ` END ` + whereClause + ` ?);`
break
}
whereClause += `?, `
}
return query
}