2019-06-18 16:55:47 +01:00
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package satellitedb
import (
"context"
2019-06-19 21:49:04 +01:00
"database/sql"
2019-06-18 16:55:47 +01:00
"time"
"github.com/skyrings/skyring-common/tools/uuid"
"github.com/zeebo/errs"
2019-11-14 19:46:15 +00:00
"storj.io/storj/private/currency"
"storj.io/storj/private/dbutil/pgutil"
2019-06-18 16:55:47 +01:00
"storj.io/storj/satellite/console"
2019-07-16 00:30:00 +01:00
"storj.io/storj/satellite/rewards"
2019-06-18 16:55:47 +01:00
dbx "storj.io/storj/satellite/satellitedb/dbx"
)
2019-11-04 14:37:39 +00:00
// ensures that usercredits implements console.UserCredits.
var _ console . UserCredits = ( * usercredits ) ( nil )
2019-06-18 16:55:47 +01:00
type usercredits struct {
2019-12-14 02:29:54 +00:00
db * satelliteDB
2019-07-30 14:21:00 +01:00
tx * dbx . Tx
2019-06-18 16:55:47 +01:00
}
2019-06-19 21:49:04 +01:00
// 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 ,
2019-08-01 18:46:33 +01:00
( 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 )
2019-06-18 16:55:47 +01:00
if err != nil {
2019-06-19 21:49:04 +01:00
return nil , errs . Wrap ( err )
2019-06-18 16:55:47 +01:00
}
2019-07-30 14:21:00 +01:00
defer func ( ) { err = errs . Combine ( err , usageRows . Close ( ) ) } ( )
2019-06-18 16:55:47 +01:00
2019-06-19 21:49:04 +01:00
usage := console . UserCreditUsage { }
2019-06-18 16:55:47 +01:00
2019-06-19 21:49:04 +01:00
for usageRows . Next ( ) {
var (
2019-07-01 20:16:49 +01:00
usedCreditInCents sql . NullInt64
availableCreditInCents sql . NullInt64
referred sql . NullInt64
2019-06-19 21:49:04 +01:00
)
2019-07-01 20:16:49 +01:00
err = usageRows . Scan ( & usedCreditInCents , & availableCreditInCents , & referred )
2019-06-19 21:49:04 +01:00
if err != nil {
return nil , errs . Wrap ( err )
}
usage . Referred += referred . Int64
2019-07-01 20:16:49 +01:00
usage . UsedCredits = usage . UsedCredits . Add ( currency . Cents ( int ( usedCreditInCents . Int64 ) ) )
usage . AvailableCredits = usage . AvailableCredits . Add ( currency . Cents ( int ( availableCreditInCents . Int64 ) ) )
2019-06-18 16:55:47 +01:00
}
2019-06-19 21:49:04 +01:00
return & usage , nil
2019-06-18 16:55:47 +01:00
}
// Create insert a new record of user credit
2019-08-14 20:53:48 +01:00
func ( c * usercredits ) Create ( ctx context . Context , userCredit console . CreateCredit ) ( err error ) {
2019-07-16 00:30:00 +01:00
if userCredit . ExpiresAt . Before ( time . Now ( ) . UTC ( ) ) {
return errs . New ( "user credit is already expired" )
}
2019-08-01 18:46:33 +01:00
var referrerID [ ] byte
if userCredit . ReferredBy != nil {
referrerID = userCredit . ReferredBy [ : ]
}
2019-08-14 20:53:48 +01:00
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
}
2019-08-01 18:46:33 +01:00
2019-08-14 20:53:48 +01:00
var (
result sql . Result
statement string
)
2019-10-18 22:27:57 +01:00
statement = `
2019-08-01 18:46:33 +01:00
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
2019-08-14 20:53:48 +01:00
WHERE COALESCE ( ( SELECT COUNT ( offer_id ) FROM user_credits WHERE offer_id = ? AND referred_by IS NOT NULL ) < NULLIF ( ? , 0 ) , ? ) ;
2019-07-16 00:30:00 +01:00
`
2019-10-18 22:27:57 +01:00
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 )
2019-07-16 00:30:00 +01:00
2019-06-18 16:55:47 +01:00
if err != nil {
2019-08-14 20:53:48 +01:00
// check to see if there's a constraint error
2019-10-18 22:27:57 +01:00
if pgutil . IsConstraintError ( err ) {
2019-08-14 20:53:48 +01:00
_ , 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 )
}
2019-11-06 18:37:53 +00:00
return rewards . ErrReachedMaxCapacity . Wrap ( err )
2019-08-14 20:53:48 +01:00
}
2019-07-16 00:30:00 +01:00
return errs . Wrap ( err )
2019-06-18 16:55:47 +01:00
}
2019-07-16 00:30:00 +01:00
rows , err := result . RowsAffected ( )
if err != nil {
return errs . Wrap ( err )
}
if rows != 1 {
2019-11-06 18:37:53 +00:00
return rewards . ErrReachedMaxCapacity . New ( "failed to create new credit" )
2019-07-30 14:21:00 +01:00
}
return nil
}
2019-08-01 18:46:33 +01:00
// UpdateEarnedCredits updates user credits after user activated their account
2019-07-30 14:21:00 +01:00
func ( c * usercredits ) UpdateEarnedCredits ( ctx context . Context , userID uuid . UUID ) error {
2019-10-18 22:27:57 +01:00
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
`
2019-07-30 14:21:00 +01:00
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 )
2019-07-16 00:30:00 +01:00
}
return nil
2019-06-18 16:55:47 +01:00
}
// 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 ) {
2019-12-19 09:57:29 +00:00
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" )
}
2019-06-18 16:55:47 +01:00
2019-12-19 09:57:29 +00:00
values := make ( [ ] interface { } , len ( availableCredits ) * 2 )
rowIds := make ( [ ] interface { } , len ( availableCredits ) )
2019-06-18 16:55:47 +01:00
2019-12-19 09:57:29 +00:00
remainingCharge = creditsToCharge
for i , credit := range availableCredits {
if remainingCharge == 0 {
break
}
2019-06-18 16:55:47 +01:00
2019-12-19 09:57:29 +00:00
creditsForUpdateInCents := credit . CreditsEarnedInCents - credit . CreditsUsedInCents
2019-06-18 16:55:47 +01:00
2019-12-19 09:57:29 +00:00
if remainingCharge < creditsForUpdateInCents {
creditsForUpdateInCents = remainingCharge
}
2019-06-18 16:55:47 +01:00
2019-12-19 09:57:29 +00:00
values [ i % 2 ] = credit . Id
values [ ( i % 2 + 1 ) ] = creditsForUpdateInCents
rowIds [ i ] = credit . Id
2019-06-18 16:55:47 +01:00
2019-12-19 09:57:29 +00:00
remainingCharge -= creditsForUpdateInCents
}
2019-06-18 16:55:47 +01:00
2019-12-19 09:57:29 +00:00
values = append ( values , rowIds ... )
2019-06-18 16:55:47 +01:00
2019-12-19 09:57:29 +00:00
statement := generateQuery ( len ( availableCredits ) , true )
2019-06-18 16:55:47 +01:00
2019-12-19 09:57:29 +00:00
_ , err = tx . Tx . ExecContext ( ctx , c . db . Rebind ( ` UPDATE user_credits SET credits_used_in_cents = CASE ` + statement ) , values ... )
return err
} )
2019-06-18 16:55:47 +01:00
if err != nil {
2019-12-19 09:57:29 +00:00
return creditsToCharge , errs . Wrap ( err )
2019-06-18 16:55:47 +01:00
}
2019-12-19 09:57:29 +00:00
return remainingCharge , nil
2019-06-18 16:55:47 +01:00
}
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
}