storj/satellite/satellitedb/usercredits.go
Yingrong Zhao 954ca3c6ee
add db implementation for user_credits table (#2169)
* add dbx queries

* add migration file

* start service

* Add TotalReferredCountByUserId and availableCreditsByUserID

* implement UserCredits interface and UserCredit struct type

* add UserCredits into consoledb

* add setupData helper function

* add test for update

* update lock file

* fix lint error

* add invalidUserCredits tests

* rename method

* adds comments

* add checks for erros in setupData

* change update method to only execute one query per request

* rename vairable

* should return a signal from Update method if the charge is not fully complete

* changes for readability

* prevent sql injection

* rename

* improve readability
2019-06-18 11:55:47 -04:00

187 lines
5.3 KiB
Go

// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package satellitedb
import (
"context"
"time"
"github.com/lib/pq"
"github.com/mattn/go-sqlite3"
"github.com/skyrings/skyring-common/tools/uuid"
"github.com/zeebo/errs"
"storj.io/storj/satellite/console"
dbx "storj.io/storj/satellite/satellitedb/dbx"
)
type usercredits struct {
db *dbx.DB
}
// TotalReferredCount returns the total amount of referral a user has made based on user id
func (c *usercredits) TotalReferredCount(ctx context.Context, id uuid.UUID) (int64, error) {
totalReferred, err := c.db.Count_UserCredit_By_ReferredBy(ctx, dbx.UserCredit_ReferredBy(id[:]))
if err != nil {
return totalReferred, errs.Wrap(err)
}
return totalReferred, nil
}
// GetAvailableCredits returns all records of user credit that are not expired or used
func (c *usercredits) GetAvailableCredits(ctx context.Context, referrerID uuid.UUID, expirationEndDate time.Time) ([]console.UserCredit, error) {
availableCredits, err := c.db.All_UserCredit_By_UserId_And_ExpiresAt_Greater_And_CreditsUsedInCents_Less_CreditsEarnedInCents_OrderBy_Asc_ExpiresAt(ctx,
dbx.UserCredit_UserId(referrerID[:]),
dbx.UserCredit_ExpiresAt(expirationEndDate),
)
if err != nil {
return nil, errs.Wrap(err)
}
return userCreditsFromDBX(availableCredits)
}
// Create insert a new record of user credit
func (c *usercredits) Create(ctx context.Context, userCredit console.UserCredit) (*console.UserCredit, error) {
credit, err := c.db.Create_UserCredit(ctx,
dbx.UserCredit_UserId(userCredit.UserID[:]),
dbx.UserCredit_OfferId(userCredit.OfferID),
dbx.UserCredit_CreditsEarnedInCents(userCredit.CreditsEarnedInCents),
dbx.UserCredit_ExpiresAt(userCredit.ExpiresAt),
dbx.UserCredit_Create_Fields{
ReferredBy: dbx.UserCredit_ReferredBy(userCredit.ReferredBy[:]),
},
)
if err != nil {
return nil, errs.Wrap(err)
}
return convertDBCredit(credit)
}
// 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) {
tx, err := c.db.Open(ctx)
if err != nil {
return creditsToCharge, errs.Wrap(err)
}
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 creditsToCharge, errs.Wrap(errs.Combine(err, tx.Rollback()))
}
if len(availableCredits) == 0 {
return creditsToCharge, errs.Combine(errs.New("No available credits"), tx.Commit())
}
values := make([]interface{}, len(availableCredits)*2)
rowIds := make([]interface{}, len(availableCredits))
remainingCharge = creditsToCharge
for i, credit := range availableCredits {
if remainingCharge == 0 {
break
}
creditsForUpdate := credit.CreditsEarnedInCents - credit.CreditsUsedInCents
if remainingCharge < creditsForUpdate {
creditsForUpdate = remainingCharge
}
values[i%2] = credit.Id
values[(i%2 + 1)] = creditsForUpdate
rowIds[i] = credit.Id
remainingCharge -= creditsForUpdate
}
values = append(values, rowIds...)
var statement string
switch t := c.db.Driver().(type) {
case *sqlite3.SQLiteDriver:
statement = generateQuery(len(availableCredits), false)
case *pq.Driver:
statement = generateQuery(len(availableCredits), true)
default:
return creditsToCharge, errs.New("Unsupported database %t", t)
}
_, err = tx.Tx.ExecContext(ctx, c.db.Rebind(`UPDATE user_credits SET
credits_used_in_cents = CASE `+statement), values...)
if err != nil {
return creditsToCharge, errs.Wrap(errs.Combine(err, tx.Rollback()))
}
return remainingCharge, errs.Wrap(tx.Commit())
}
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
}
func userCreditsFromDBX(userCreditsDBX []*dbx.UserCredit) ([]console.UserCredit, error) {
var userCredits []console.UserCredit
errList := new(errs.Group)
for _, credit := range userCreditsDBX {
uc, err := convertDBCredit(credit)
if err != nil {
errList.Add(err)
continue
}
userCredits = append(userCredits, *uc)
}
return userCredits, errList.Err()
}
func convertDBCredit(userCreditDBX *dbx.UserCredit) (*console.UserCredit, error) {
if userCreditDBX == nil {
return nil, errs.New("userCreditDBX parameter is nil")
}
userID, err := bytesToUUID(userCreditDBX.UserId)
if err != nil {
return nil, err
}
referredByID, err := bytesToUUID(userCreditDBX.ReferredBy)
if err != nil {
return nil, err
}
return &console.UserCredit{
ID: userCreditDBX.Id,
UserID: userID,
OfferID: userCreditDBX.OfferId,
ReferredBy: referredByID,
CreditsEarnedInCents: userCreditDBX.CreditsEarnedInCents,
CreditsUsedInCents: userCreditDBX.CreditsUsedInCents,
ExpiresAt: userCreditDBX.ExpiresAt,
CreatedAt: userCreditDBX.CreatedAt,
}, nil
}