satellite/rewards: use USD type (#2384)

* fix String converison

* add method

* rename to USD

* fix types

* fix parsing of forms

* fix tests

* fix header

* use larger type

* use int64

* rename func

* move currency to separate package

* convert types, renames

* fix usercredits

* remove unnecessary conversion

* fix comment and named params
This commit is contained in:
Egon Elbre 2019-07-01 22:16:49 +03:00 committed by Faris Huskovic
parent 74e12a0bb4
commit e8605d312e
15 changed files with 253 additions and 163 deletions

37
internal/currency/usd.go Normal file
View File

@ -0,0 +1,37 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information
package currency
import (
"fmt"
)
// USD describes USD currency.
type USD struct {
cents int
}
// Dollars converts dollars to USD amount.
func Dollars(dollars int) USD {
return USD{dollars * 100}
}
// Cents converts cents to USD amount.
func Cents(cents int) USD {
return USD{cents}
}
// Cents returns amount in cents.
func (usd USD) Cents() int { return usd.cents }
// Add adds two usd values and returns the result.
func (usd USD) Add(b USD) USD { return USD{usd.cents + b.cents} }
// String returns the value in dollars.
func (usd USD) String() string {
if usd.cents < 0 {
return fmt.Sprintf("-%d.%02d", -usd.cents/100, -usd.cents%100)
}
return fmt.Sprintf("%d.%02d", usd.cents/100, usd.cents%100)
}

View File

@ -0,0 +1,38 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information
package currency_test
import (
"testing"
"github.com/stretchr/testify/assert"
"storj.io/storj/internal/currency"
)
func TestCentDollarString(t *testing.T) {
type Test struct {
Amount currency.USD
Expected string
}
tests := []Test{
{currency.Cents(1), "0.01"},
{currency.Cents(100), "1.00"},
{currency.Cents(101), "1.01"},
{currency.Cents(110), "1.10"},
{currency.Cents(123456789), "1234567.89"},
{currency.Cents(-1), "-0.01"},
{currency.Cents(-100), "-1.00"},
{currency.Cents(-101), "-1.01"},
{currency.Cents(-110), "-1.10"},
{currency.Cents(-123456789), "-1234567.89"},
}
for _, test := range tests {
s := test.Amount.String()
assert.Equal(t, test.Expected, s, test.Amount.Cents())
}
}

View File

@ -8,6 +8,8 @@ import (
"time" "time"
"github.com/skyrings/skyring-common/tools/uuid" "github.com/skyrings/skyring-common/tools/uuid"
"storj.io/storj/internal/currency"
) )
// UserCredits holds information to interact with database // UserCredits holds information to interact with database
@ -19,19 +21,19 @@ type UserCredits interface {
// UserCredit holds information about an user's credit // UserCredit holds information about an user's credit
type UserCredit struct { type UserCredit struct {
ID int ID int
UserID uuid.UUID UserID uuid.UUID
OfferID int OfferID int
ReferredBy uuid.UUID ReferredBy uuid.UUID
CreditsEarnedInCents int CreditsEarned currency.USD
CreditsUsedInCents int CreditsUsed currency.USD
ExpiresAt time.Time ExpiresAt time.Time
CreatedAt time.Time CreatedAt time.Time
} }
// UserCreditUsage holds information about credit usage information // UserCreditUsage holds information about credit usage information
type UserCreditUsage struct { type UserCreditUsage struct {
Referred int64 Referred int64
AvailableCredits int64 AvailableCredits currency.USD
UsedCredits int64 UsedCredits currency.USD
} }

View File

@ -0,0 +1,58 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package marketingweb
import (
"net/http"
"reflect"
"strconv"
"time"
"github.com/gorilla/schema"
"storj.io/storj/internal/currency"
"storj.io/storj/satellite/rewards"
)
// parseOfferForm decodes POST form data into a new offer.
func parseOfferForm(w http.ResponseWriter, req *http.Request) (rewards.NewOffer, error) {
err := req.ParseForm()
if err != nil {
return rewards.NewOffer{}, err
}
var offer rewards.NewOffer
err = decoder.Decode(&offer, req.PostForm)
return offer, err
}
var (
decoder = schema.NewDecoder()
)
// init safely registers convertStringToTime for the decoder.
func init() {
decoder.RegisterConverter(time.Time{}, convertStringToTime)
decoder.RegisterConverter(currency.USD{}, convertStringToUSD)
}
// convertStringToUSD formats dollars strings as USD amount.
func convertStringToUSD(s string) reflect.Value {
value, err := strconv.Atoi(s)
if err != nil {
// invalid decoder value
return reflect.Value{}
}
return reflect.ValueOf(currency.Dollars(value))
}
// convertStringToTime formats form time input as time.Time.
func convertStringToTime(value string) reflect.Value {
v, err := time.Parse("2006-01-02", value)
if err != nil {
// invalid decoder value
return reflect.Value{}
}
return reflect.ValueOf(v)
}

View File

@ -9,11 +9,8 @@ import (
"net" "net"
"net/http" "net/http"
"path/filepath" "path/filepath"
"reflect"
"time"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/gorilla/schema"
"github.com/zeebo/errs" "github.com/zeebo/errs"
"go.uber.org/zap" "go.uber.org/zap"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
@ -21,11 +18,8 @@ import (
"storj.io/storj/satellite/rewards" "storj.io/storj/satellite/rewards"
) )
var ( // Error is satellite marketing error type
// Error is satellite marketing error type var Error = errs.Class("satellite marketing error")
Error = errs.Class("satellite marketing error")
decoder = schema.NewDecoder()
)
// Config contains configuration for marketingweb server // Config contains configuration for marketingweb server
type Config struct { type Config struct {
@ -55,11 +49,6 @@ type offerSet struct {
FreeCredits rewards.Offers FreeCredits rewards.Offers
} }
// init safely registers convertStringToTime for the decoder.
func init() {
decoder.RegisterConverter(time.Time{}, convertStringToTime)
}
// organizeOffers organizes offers by type. // organizeOffers organizes offers by type.
func organizeOffers(offers []rewards.Offer) offerSet { func organizeOffers(offers []rewards.Offer) offerSet {
var os offerSet var os offerSet
@ -158,9 +147,7 @@ func (s *Server) parseTemplates() (err error) {
filepath.Join(s.templateDir, "err.html"), filepath.Join(s.templateDir, "err.html"),
) )
s.templates.home, err = template.New("landingPage").Funcs(template.FuncMap{ s.templates.home, err = template.New("landingPage").ParseFiles(homeFiles...)
"ToDollars": rewards.ToDollars,
}).ParseFiles(homeFiles...)
if err != nil { if err != nil {
return Error.Wrap(err) return Error.Wrap(err)
} }
@ -183,33 +170,6 @@ func (s *Server) parseTemplates() (err error) {
return nil return nil
} }
// convertStringToTime formats form time input as time.Time.
func convertStringToTime(value string) reflect.Value {
v, err := time.Parse("2006-01-02", value)
if err != nil {
// invalid decoder value
return reflect.Value{}
}
return reflect.ValueOf(v)
}
// parseOfferForm decodes POST form data into a new offer.
func parseOfferForm(w http.ResponseWriter, req *http.Request) (o rewards.NewOffer, e error) {
err := req.ParseForm()
if err != nil {
return o, err
}
if err := decoder.Decode(&o, req.PostForm); err != nil {
return o, err
}
o.InviteeCreditInCents = rewards.ToCents(o.InviteeCreditInCents)
o.AwardCreditInCents = rewards.ToCents(o.AwardCreditInCents)
return o, nil
}
// CreateOffer handles requests to create new offers. // CreateOffer handles requests to create new offers.
func (s *Server) CreateOffer(w http.ResponseWriter, req *http.Request) { func (s *Server) CreateOffer(w http.ResponseWriter, req *http.Request) {
offer, err := parseOfferForm(w, req) offer, err := parseOfferForm(w, req)

View File

@ -32,9 +32,9 @@ func TestCreateOffer(t *testing.T) {
"Name": {"Referral Credit"}, "Name": {"Referral Credit"},
"Description": {"desc"}, "Description": {"desc"},
"ExpiresAt": {"2119-06-27"}, "ExpiresAt": {"2119-06-27"},
"InviteeCreditInCents": {"50"}, "InviteeCredit": {"50"},
"InviteeCreditDurationDays": {"50"}, "InviteeCreditDurationDays": {"50"},
"AwardCreditInCents": {"50"}, "AwardCredit": {"50"},
"AwardCreditDurationDays": {"50"}, "AwardCreditDurationDays": {"50"},
"RedeemableCap": {"150"}, "RedeemableCap": {"150"},
}, },
@ -44,7 +44,7 @@ func TestCreateOffer(t *testing.T) {
"Name": {"Free Credit Credit"}, "Name": {"Free Credit Credit"},
"Description": {"desc"}, "Description": {"desc"},
"ExpiresAt": {"2119-06-27"}, "ExpiresAt": {"2119-06-27"},
"InviteeCreditInCents": {"50"}, "InviteeCredit": {"50"},
"InviteeCreditDurationDays": {"50"}, "InviteeCreditDurationDays": {"50"},
"RedeemableCap": {"150"}, "RedeemableCap": {"150"},
}, },

View File

@ -5,21 +5,11 @@ package rewards
import ( import (
"context" "context"
"fmt"
"time" "time"
"storj.io/storj/internal/currency"
) )
// ToCents converts USD credit amounts to cents.
func ToCents(dollars int) int {
return dollars * 100
}
// ToDollars converts credit amounts in cents to USD.
func ToDollars(cents int) string {
formattedAmount := fmt.Sprintf("%d.%d0", (cents / 100), (cents % 100))
return formattedAmount
}
// DB holds information about offer // DB holds information about offer
type DB interface { type DB interface {
ListAll(ctx context.Context) ([]Offer, error) ListAll(ctx context.Context) ([]Offer, error)
@ -34,8 +24,8 @@ type NewOffer struct {
Name string Name string
Description string Description string
AwardCreditInCents int AwardCredit currency.USD
InviteeCreditInCents int InviteeCredit currency.USD
RedeemableCap int RedeemableCap int
@ -83,8 +73,8 @@ type Offer struct {
Name string Name string
Description string Description string
AwardCreditInCents int AwardCredit currency.USD
InviteeCreditInCents int InviteeCredit currency.USD
AwardCreditDurationDays int AwardCreditDurationDays int
InviteeCreditDurationDays int InviteeCreditDurationDays int

View File

@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"storj.io/storj/internal/currency"
"storj.io/storj/internal/testcontext" "storj.io/storj/internal/testcontext"
"storj.io/storj/internal/testplanet" "storj.io/storj/internal/testplanet"
"storj.io/storj/satellite/rewards" "storj.io/storj/satellite/rewards"
@ -23,8 +24,8 @@ func TestOffer_Database(t *testing.T) {
{ {
Name: "test", Name: "test",
Description: "test offer 1", Description: "test offer 1",
AwardCreditInCents: 100, AwardCredit: currency.Cents(100),
InviteeCreditInCents: 50, InviteeCredit: currency.Cents(50),
AwardCreditDurationDays: 60, AwardCreditDurationDays: 60,
InviteeCreditDurationDays: 30, InviteeCreditDurationDays: 30,
RedeemableCap: 50, RedeemableCap: 50,
@ -35,8 +36,8 @@ func TestOffer_Database(t *testing.T) {
{ {
Name: "test", Name: "test",
Description: "test offer 2", Description: "test offer 2",
AwardCreditInCents: 100, AwardCredit: currency.Cents(100),
InviteeCreditInCents: 50, InviteeCredit: currency.Cents(50),
AwardCreditDurationDays: 60, AwardCreditDurationDays: 60,
InviteeCreditDurationDays: 30, InviteeCreditDurationDays: 30,
RedeemableCap: 50, RedeemableCap: 50,
@ -86,8 +87,8 @@ func TestOffer_Database(t *testing.T) {
{ {
Name: "test", Name: "test",
Description: "test offer", Description: "test offer",
AwardCreditInCents: 100, AwardCredit: currency.Cents(100),
InviteeCreditInCents: 50, InviteeCredit: currency.Cents(50),
AwardCreditDurationDays: 60, AwardCreditDurationDays: 60,
InviteeCreditDurationDays: 30, InviteeCreditDurationDays: 30,
RedeemableCap: 50, RedeemableCap: 50,
@ -98,8 +99,8 @@ func TestOffer_Database(t *testing.T) {
{ {
Name: "test", Name: "test",
Description: "test offer", Description: "test offer",
AwardCreditInCents: 100, AwardCredit: currency.Cents(100),
InviteeCreditInCents: 50, InviteeCredit: currency.Cents(50),
AwardCreditDurationDays: 60, AwardCreditDurationDays: 60,
InviteeCreditDurationDays: 30, InviteeCreditDurationDays: 30,
RedeemableCap: 50, RedeemableCap: 50,

View File

@ -10,6 +10,7 @@ import (
"github.com/zeebo/errs" "github.com/zeebo/errs"
"storj.io/storj/internal/currency"
"storj.io/storj/satellite/rewards" "storj.io/storj/satellite/rewards"
dbx "storj.io/storj/satellite/satellitedb/dbx" dbx "storj.io/storj/satellite/satellitedb/dbx"
) )
@ -51,14 +52,17 @@ func (db *offersDB) GetCurrentByType(ctx context.Context, offerType rewards.Offe
rows := db.db.DB.QueryRowContext(ctx, db.db.Rebind(statement), rewards.Active, offerType, time.Now().UTC(), offerType, rewards.Default) rows := db.db.DB.QueryRowContext(ctx, db.db.Rebind(statement), rewards.Active, offerType, time.Now().UTC(), offerType, rewards.Default)
var awardCreditInCents, inviteeCreditInCents int
o := rewards.Offer{} o := rewards.Offer{}
err := rows.Scan(&o.ID, &o.Name, &o.Description, &o.AwardCreditInCents, &o.InviteeCreditInCents, &o.AwardCreditDurationDays, &o.InviteeCreditDurationDays, &o.RedeemableCap, &o.NumRedeemed, &o.ExpiresAt, &o.CreatedAt, &o.Status, &o.Type) err := rows.Scan(&o.ID, &o.Name, &o.Description, &awardCreditInCents, &inviteeCreditInCents, &o.AwardCreditDurationDays, &o.InviteeCreditDurationDays, &o.RedeemableCap, &o.NumRedeemed, &o.ExpiresAt, &o.CreatedAt, &o.Status, &o.Type)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return nil, offerErr.New("no current offer") return nil, offerErr.New("no current offer")
} }
if err != nil { if err != nil {
return nil, offerErr.Wrap(err) return nil, offerErr.Wrap(err)
} }
o.AwardCredit = currency.Cents(awardCreditInCents)
o.InviteeCredit = currency.Cents(inviteeCreditInCents)
return &o, nil return &o, nil
} }
@ -93,8 +97,8 @@ func (db *offersDB) Create(ctx context.Context, o *rewards.NewOffer) (*rewards.O
offerDbx, err := tx.Create_Offer(ctx, offerDbx, err := tx.Create_Offer(ctx,
dbx.Offer_Name(o.Name), dbx.Offer_Name(o.Name),
dbx.Offer_Description(o.Description), dbx.Offer_Description(o.Description),
dbx.Offer_AwardCreditInCents(o.AwardCreditInCents), dbx.Offer_AwardCreditInCents(o.AwardCredit.Cents()),
dbx.Offer_InviteeCreditInCents(o.InviteeCreditInCents), dbx.Offer_InviteeCreditInCents(o.InviteeCredit.Cents()),
dbx.Offer_AwardCreditDurationDays(o.AwardCreditDurationDays), dbx.Offer_AwardCreditDurationDays(o.AwardCreditDurationDays),
dbx.Offer_InviteeCreditDurationDays(o.InviteeCreditDurationDays), dbx.Offer_InviteeCreditDurationDays(o.InviteeCreditDurationDays),
dbx.Offer_RedeemableCap(o.RedeemableCap), dbx.Offer_RedeemableCap(o.RedeemableCap),
@ -175,8 +179,8 @@ func convertDBOffer(offerDbx *dbx.Offer) (*rewards.Offer, error) {
ID: offerDbx.Id, ID: offerDbx.Id,
Name: offerDbx.Name, Name: offerDbx.Name,
Description: offerDbx.Description, Description: offerDbx.Description,
AwardCreditInCents: offerDbx.AwardCreditInCents, AwardCredit: currency.Cents(offerDbx.AwardCreditInCents),
InviteeCreditInCents: offerDbx.InviteeCreditInCents, InviteeCredit: currency.Cents(offerDbx.InviteeCreditInCents),
RedeemableCap: offerDbx.RedeemableCap, RedeemableCap: offerDbx.RedeemableCap,
NumRedeemed: offerDbx.NumRedeemed, NumRedeemed: offerDbx.NumRedeemed,
ExpiresAt: offerDbx.ExpiresAt, ExpiresAt: offerDbx.ExpiresAt,

View File

@ -13,6 +13,7 @@ import (
"github.com/skyrings/skyring-common/tools/uuid" "github.com/skyrings/skyring-common/tools/uuid"
"github.com/zeebo/errs" "github.com/zeebo/errs"
"storj.io/storj/internal/currency"
"storj.io/storj/satellite/console" "storj.io/storj/satellite/console"
dbx "storj.io/storj/satellite/satellitedb/dbx" dbx "storj.io/storj/satellite/satellitedb/dbx"
) )
@ -36,18 +37,18 @@ func (c *usercredits) GetCreditUsage(ctx context.Context, userID uuid.UUID, expi
for usageRows.Next() { for usageRows.Next() {
var ( var (
usedCredit sql.NullInt64 usedCreditInCents sql.NullInt64
availableCredit sql.NullInt64 availableCreditInCents sql.NullInt64
referred sql.NullInt64 referred sql.NullInt64
) )
err = usageRows.Scan(&usedCredit, &availableCredit, &referred) err = usageRows.Scan(&usedCreditInCents, &availableCreditInCents, &referred)
if err != nil { if err != nil {
return nil, errs.Wrap(err) return nil, errs.Wrap(err)
} }
usage.UsedCredits += usedCredit.Int64
usage.Referred += referred.Int64 usage.Referred += referred.Int64
usage.AvailableCredits += availableCredit.Int64 usage.UsedCredits = usage.UsedCredits.Add(currency.Cents(int(usedCreditInCents.Int64)))
usage.AvailableCredits = usage.AvailableCredits.Add(currency.Cents(int(availableCreditInCents.Int64)))
} }
return &usage, nil return &usage, nil
@ -58,7 +59,7 @@ func (c *usercredits) Create(ctx context.Context, userCredit console.UserCredit)
credit, err := c.db.Create_UserCredit(ctx, credit, err := c.db.Create_UserCredit(ctx,
dbx.UserCredit_UserId(userCredit.UserID[:]), dbx.UserCredit_UserId(userCredit.UserID[:]),
dbx.UserCredit_OfferId(userCredit.OfferID), dbx.UserCredit_OfferId(userCredit.OfferID),
dbx.UserCredit_CreditsEarnedInCents(userCredit.CreditsEarnedInCents), dbx.UserCredit_CreditsEarnedInCents(userCredit.CreditsEarned.Cents()),
dbx.UserCredit_ExpiresAt(userCredit.ExpiresAt), dbx.UserCredit_ExpiresAt(userCredit.ExpiresAt),
dbx.UserCredit_Create_Fields{ dbx.UserCredit_Create_Fields{
ReferredBy: dbx.UserCredit_ReferredBy(userCredit.ReferredBy[:]), ReferredBy: dbx.UserCredit_ReferredBy(userCredit.ReferredBy[:]),
@ -98,17 +99,17 @@ func (c *usercredits) UpdateAvailableCredits(ctx context.Context, creditsToCharg
break break
} }
creditsForUpdate := credit.CreditsEarnedInCents - credit.CreditsUsedInCents creditsForUpdateInCents := credit.CreditsEarnedInCents - credit.CreditsUsedInCents
if remainingCharge < creditsForUpdate { if remainingCharge < creditsForUpdateInCents {
creditsForUpdate = remainingCharge creditsForUpdateInCents = remainingCharge
} }
values[i%2] = credit.Id values[i%2] = credit.Id
values[(i%2 + 1)] = creditsForUpdate values[(i%2 + 1)] = creditsForUpdateInCents
rowIds[i] = credit.Id rowIds[i] = credit.Id
remainingCharge -= creditsForUpdate remainingCharge -= creditsForUpdateInCents
} }
values = append(values, rowIds...) values = append(values, rowIds...)
@ -167,13 +168,13 @@ func convertDBCredit(userCreditDBX *dbx.UserCredit) (*console.UserCredit, error)
} }
return &console.UserCredit{ return &console.UserCredit{
ID: userCreditDBX.Id, ID: userCreditDBX.Id,
UserID: userID, UserID: userID,
OfferID: userCreditDBX.OfferId, OfferID: userCreditDBX.OfferId,
ReferredBy: referredByID, ReferredBy: referredByID,
CreditsEarnedInCents: userCreditDBX.CreditsEarnedInCents, CreditsEarned: currency.Cents(userCreditDBX.CreditsEarnedInCents),
CreditsUsedInCents: userCreditDBX.CreditsUsedInCents, CreditsUsed: currency.Cents(userCreditDBX.CreditsUsedInCents),
ExpiresAt: userCreditDBX.ExpiresAt, ExpiresAt: userCreditDBX.ExpiresAt,
CreatedAt: userCreditDBX.CreatedAt, CreatedAt: userCreditDBX.CreatedAt,
}, nil }, nil
} }

View File

@ -10,6 +10,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"storj.io/storj/internal/currency"
"storj.io/storj/internal/testcontext" "storj.io/storj/internal/testcontext"
"storj.io/storj/internal/testrand" "storj.io/storj/internal/testrand"
"storj.io/storj/satellite" "storj.io/storj/satellite"
@ -19,8 +20,6 @@ import (
) )
func TestUsercredits(t *testing.T) { func TestUsercredits(t *testing.T) {
t.Parallel()
satellitedbtest.Run(t, func(t *testing.T, db satellite.DB) { satellitedbtest.Run(t, func(t *testing.T, db satellite.DB) {
ctx := testcontext.New(t) ctx := testcontext.New(t)
defer ctx.Cleanup() defer ctx.Cleanup()
@ -33,25 +32,25 @@ func TestUsercredits(t *testing.T) {
// test foreign key constraint for inserting a new user credit entry with randomID // test foreign key constraint for inserting a new user credit entry with randomID
var invalidUserCredits = []console.UserCredit{ var invalidUserCredits = []console.UserCredit{
{ {
UserID: randomID, UserID: randomID,
OfferID: offer.ID, OfferID: offer.ID,
ReferredBy: referrer.ID, ReferredBy: referrer.ID,
CreditsEarnedInCents: 100, CreditsEarned: currency.Cents(100),
ExpiresAt: time.Now().UTC().AddDate(0, 1, 0), ExpiresAt: time.Now().UTC().AddDate(0, 1, 0),
}, },
{ {
UserID: user.ID, UserID: user.ID,
OfferID: 10, OfferID: 10,
ReferredBy: referrer.ID, ReferredBy: referrer.ID,
CreditsEarnedInCents: 100, CreditsEarned: currency.Cents(100),
ExpiresAt: time.Now().UTC().AddDate(0, 1, 0), ExpiresAt: time.Now().UTC().AddDate(0, 1, 0),
}, },
{ {
UserID: user.ID, UserID: user.ID,
OfferID: offer.ID, OfferID: offer.ID,
ReferredBy: randomID, ReferredBy: randomID,
CreditsEarnedInCents: 100, CreditsEarned: currency.Cents(100),
ExpiresAt: time.Now().UTC().AddDate(0, 1, 0), ExpiresAt: time.Now().UTC().AddDate(0, 1, 0),
}, },
} }
@ -73,18 +72,18 @@ func TestUsercredits(t *testing.T) {
}{ }{
{ {
userCredit: console.UserCredit{ userCredit: console.UserCredit{
UserID: user.ID, UserID: user.ID,
OfferID: offer.ID, OfferID: offer.ID,
ReferredBy: referrer.ID, ReferredBy: referrer.ID,
CreditsEarnedInCents: 100, CreditsEarned: currency.Cents(100),
ExpiresAt: time.Now().UTC().AddDate(0, 1, 0), ExpiresAt: time.Now().UTC().AddDate(0, 1, 0),
}, },
chargedCredits: 120, chargedCredits: 120,
expected: result{ expected: result{
remainingCharge: 20, remainingCharge: 20,
usage: console.UserCreditUsage{ usage: console.UserCreditUsage{
AvailableCredits: 0, AvailableCredits: currency.Cents(0),
UsedCredits: 100, UsedCredits: currency.Cents(100),
Referred: 0, Referred: 0,
}, },
hasErr: false, hasErr: false,
@ -93,18 +92,18 @@ func TestUsercredits(t *testing.T) {
{ {
// simulate a credit that's already expired // simulate a credit that's already expired
userCredit: console.UserCredit{ userCredit: console.UserCredit{
UserID: user.ID, UserID: user.ID,
OfferID: offer.ID, OfferID: offer.ID,
ReferredBy: referrer.ID, ReferredBy: referrer.ID,
CreditsEarnedInCents: 100, CreditsEarned: currency.Cents(100),
ExpiresAt: time.Now().UTC().AddDate(0, 0, -5), ExpiresAt: time.Now().UTC().AddDate(0, 0, -5),
}, },
chargedCredits: 60, chargedCredits: 60,
expected: result{ expected: result{
remainingCharge: 60, remainingCharge: 60,
usage: console.UserCreditUsage{ usage: console.UserCreditUsage{
AvailableCredits: 0, AvailableCredits: currency.Cents(0),
UsedCredits: 100, UsedCredits: currency.Cents(100),
Referred: 0, Referred: 0,
}, },
hasErr: true, hasErr: true,
@ -113,18 +112,18 @@ func TestUsercredits(t *testing.T) {
{ {
// simulate a credit that's not expired // simulate a credit that's not expired
userCredit: console.UserCredit{ userCredit: console.UserCredit{
UserID: user.ID, UserID: user.ID,
OfferID: offer.ID, OfferID: offer.ID,
ReferredBy: referrer.ID, ReferredBy: referrer.ID,
CreditsEarnedInCents: 100, CreditsEarned: currency.Cents(100),
ExpiresAt: time.Now().UTC().AddDate(0, 0, 5), ExpiresAt: time.Now().UTC().AddDate(0, 0, 5),
}, },
chargedCredits: 80, chargedCredits: 80,
expected: result{ expected: result{
remainingCharge: 0, remainingCharge: 0,
usage: console.UserCreditUsage{ usage: console.UserCreditUsage{
AvailableCredits: 20, AvailableCredits: currency.Cents(20),
UsedCredits: 180, UsedCredits: currency.Cents(180),
Referred: 0, Referred: 0,
}, },
hasErr: false, hasErr: false,
@ -193,8 +192,8 @@ func setupData(ctx context.Context, t *testing.T, db satellite.DB) (user *consol
offer, err = offersDB.Create(ctx, &rewards.NewOffer{ offer, err = offersDB.Create(ctx, &rewards.NewOffer{
Name: "test", Name: "test",
Description: "test offer 1", Description: "test offer 1",
AwardCreditInCents: 100, AwardCredit: currency.Cents(100),
InviteeCreditInCents: 50, InviteeCredit: currency.Cents(50),
AwardCreditDurationDays: 60, AwardCreditDurationDays: 60,
InviteeCreditDurationDays: 30, InviteeCreditDurationDays: 30,
RedeemableCap: 50, RedeemableCap: 50,

View File

@ -29,8 +29,8 @@ See LICENSE for copying information. -->
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-group col-md-4"> <div class="form-group col-md-4">
<label for="AwardCreditInCents">Award Credit</label> <label for="AwardCredit">Award Credit</label>
<input type="number" class="form-control" name="AwardCreditInCents" id="AwardCreditInCents" placeholder="$50" min="1" required> <input type="number" class="form-control" name="AwardCredit" id="AwardCredit" placeholder="$50" min="1" required>
</div> </div>
<div class="form-group col-md-4"> <div class="form-group col-md-4">
<label for="AwardCreditDurationDays">Award Credit Exp</label> <label for="AwardCreditDurationDays">Award Credit Exp</label>

View File

@ -18,7 +18,7 @@ See LICENSE for copying information. -->
{{$defaultOffer := .FreeCredits.GetDefaultFromSet}} {{$defaultOffer := .FreeCredits.GetDefaultFromSet}}
<div class="row data-row"> <div class="row data-row">
<div class="col ml-3">{{$defaultOffer.Name}}</div> <div class="col ml-3">{{$defaultOffer.Name}}</div>
<div class="col">${{ToDollars $defaultOffer.AwardCreditInCents}}</div> <div class="col">${{$defaultOffer.AwardCredit}}</div>
<div class="col">{{$defaultOffer.NumRedeemed}}</div> <div class="col">{{$defaultOffer.NumRedeemed}}</div>
<div class="col">{{$defaultOffer.RedeemableCap}}</div> <div class="col">{{$defaultOffer.RedeemableCap}}</div>
<div class="col">{{printf "%.10s" $defaultOffer.CreatedAt}}</div> <div class="col">{{printf "%.10s" $defaultOffer.CreatedAt}}</div>
@ -32,7 +32,7 @@ See LICENSE for copying information. -->
{{$currentOffer := .FreeCredits.GetCurrentFromSet}} {{$currentOffer := .FreeCredits.GetCurrentFromSet}}
<div class="row data-row"> <div class="row data-row">
<div class="col ml-3">{{$currentOffer.Name}}</div> <div class="col ml-3">{{$currentOffer.Name}}</div>
<div class="col">${{ToDollars $currentOffer.AwardCreditInCents}}</div> <div class="col">${{$currentOffer.AwardCredit}}</div>
<div class="col">{{$currentOffer.NumRedeemed}}</div> <div class="col">{{$currentOffer.NumRedeemed}}</div>
<div class="col">{{$currentOffer.RedeemableCap}}</div> <div class="col">{{$currentOffer.RedeemableCap}}</div>
<div class="col">{{printf "%.10s" $currentOffer.CreatedAt}}</div> <div class="col">{{printf "%.10s" $currentOffer.CreatedAt}}</div>
@ -53,7 +53,7 @@ See LICENSE for copying information. -->
{{if $offer.IsDone}} {{if $offer.IsDone}}
<div class="row data-row"> <div class="row data-row">
<div class="col ml-3">{{$offer.Name}}</div> <div class="col ml-3">{{$offer.Name}}</div>
<div class="col">${{ToDollars $offer.AwardCreditInCents}}</div> <div class="col">${{$offer.AwardCredit}}</div>
<div class="col">{{$offer.NumRedeemed}}</div> <div class="col">{{$offer.NumRedeemed}}</div>
<div class="col">{{$offer.RedeemableCap}}</div> <div class="col">{{$offer.RedeemableCap}}</div>
<div class="col">{{printf "%.10s" $offer.CreatedAt}}</div> <div class="col">{{printf "%.10s" $offer.CreatedAt}}</div>

View File

@ -29,16 +29,16 @@ See LICENSE for copying information. -->
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-group col-md-2"> <div class="form-group col-md-2">
<label for="InviteeCreditInCents">Give Credit</label> <label for="InviteeCredit">Give Credit</label>
<input type="number" class="form-control" name="InviteeCreditInCents" id="InviteeCreditInCents" placeholder="$50" required> <input type="number" class="form-control" name="InviteeCredit" id="InviteeCredit" placeholder="$50" required>
</div> </div>
<div class="form-group col-md-2"> <div class="form-group col-md-2">
<label for="InviteeCreditDurationDays">Give Credit Exp.</label> <label for="InviteeCreditDurationDays">Give Credit Exp.</label>
<input type="number" class="form-control" name="InviteeCreditDurationDays" id="InviteeCreditDurationDays" min="1" placeholder="14 days" required> <input type="number" class="form-control" name="InviteeCreditDurationDays" id="InviteeCreditDurationDays" min="1" placeholder="14 days" required>
</div> </div>
<div class="form-group col-md-2"> <div class="form-group col-md-2">
<label for="AwardCreditInCents">Award Credit</label> <label for="AwardCredit">Award Credit</label>
<input type="number" class="form-control" name="AwardCreditInCents" id="AwardCreditInCents" placeholder="$50" min="1" required> <input type="number" class="form-control" name="AwardCredit" id="AwardCredit" placeholder="$50" min="1" required>
</div> </div>
<div class="form-group col-md-3"> <div class="form-group col-md-3">
<label for="AwardCreditDurationDays">Award Credit Exp.</label> <label for="AwardCreditDurationDays">Award Credit Exp.</label>

View File

@ -14,13 +14,13 @@ See LICENSE for copying information. -->
<div class="col col-heading">Status</div> <div class="col col-heading">Status</div>
</div><hr> </div><hr>
<div class="row offer-heading "> <div class="row offer-heading ">
<p class="offer-type">default offer</p> <p class="offer-type">Default&nbsp;Offer</p>
</div> </div>
{{$defaultOffer := .ReferralOffers.GetDefaultFromSet}} {{$defaultOffer := .ReferralOffers.GetDefaultFromSet}}
<div class="row data-row"> <div class="row data-row">
<div class="col ml-3">{{$defaultOffer.Name}}</div> <div class="col ml-3">{{$defaultOffer.Name}}</div>
<div class="col">${{ToDollars $defaultOffer.InviteeCreditInCents}}</div> <div class="col">${{$defaultOffer.InviteeCredit}}</div>
<div class="col">${{ToDollars $defaultOffer.AwardCreditInCents}}</div> <div class="col">${{$defaultOffer.AwardCredit}}</div>
<div class="col">{{$defaultOffer.NumRedeemed}}</div> <div class="col">{{$defaultOffer.NumRedeemed}}</div>
<div class="col">&#8734;</div> <div class="col">&#8734;</div>
<div class="col">{{printf "%.10s" $defaultOffer.CreatedAt}}</div> <div class="col">{{printf "%.10s" $defaultOffer.CreatedAt}}</div>
@ -28,14 +28,14 @@ See LICENSE for copying information. -->
<div class="col"></div> <div class="col"></div>
</div><hr> </div><hr>
<div class="row offer-heading "> <div class="row offer-heading ">
<p class="offer-type">current offer</p> <p class="offer-type">Current&nbsp;Offer</p>
</div> </div>
{{if gt (len .ReferralOffers.Set) 0}} {{if gt (len .ReferralOffers.Set) 0}}
{{$currentOffer := .ReferralOffers.GetCurrentFromSet}} {{$currentOffer := .ReferralOffers.GetCurrentFromSet}}
<div class="row data-row"> <div class="row data-row">
<div class="col ml-3">{{$currentOffer.Name}}</div> <div class="col ml-3">{{$currentOffer.Name}}</div>
<div class="col">${{ToDollars $currentOffer.InviteeCreditInCents}}</div> <div class="col">${{$currentOffer.InviteeCredit}}</div>
<div class="col">${{ToDollars $currentOffer.AwardCreditInCents}}</div> <div class="col">${{$currentOffer.AwardCredit}}</div>
<div class="col">{{$currentOffer.NumRedeemed}}</div> <div class="col">{{$currentOffer.NumRedeemed}}</div>
<div class="col">{{$currentOffer.RedeemableCap}}</div> <div class="col">{{$currentOffer.RedeemableCap}}</div>
<div class="col">{{printf "%.10s" $currentOffer.CreatedAt}}</div> <div class="col">{{printf "%.10s" $currentOffer.CreatedAt}}</div>
@ -48,7 +48,7 @@ See LICENSE for copying information. -->
</div><hr> </div><hr>
{{end}} {{end}}
<div class="row offer-heading "> <div class="row offer-heading ">
<p class="offer-type">other offers</p> <p class="offer-type">Other&nbsp;Offers</p>
</div> </div>
{{if gt (len .ReferralOffers.Set) 0}} {{if gt (len .ReferralOffers.Set) 0}}
{{range .ReferralOffers.Set}} {{range .ReferralOffers.Set}}
@ -56,8 +56,8 @@ See LICENSE for copying information. -->
{{if $offer.IsDone}} {{if $offer.IsDone}}
<div class="row data-row"> <div class="row data-row">
<div class="col ml-3">{{$offer.Name}}</div> <div class="col ml-3">{{$offer.Name}}</div>
<div class="col">${{ToDollars $offer.InviteeCreditInCents}}</div> <div class="col">${{$offer.InviteeCredit}}</div>
<div class="col">${{ToDollars $offer.AwardCreditInCents}}</div> <div class="col">${{$offer.AwardCredit}}</div>
<div class="col">{{.NumRedeemed}}</div> <div class="col">{{.NumRedeemed}}</div>
<div class="col">{{.RedeemableCap}}</div> <div class="col">{{.RedeemableCap}}</div>
<div class="col">{{printf "%.10s" .CreatedAt}}</div> <div class="col">{{printf "%.10s" .CreatedAt}}</div>