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
@ -23,8 +25,8 @@ type UserCredit struct {
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
} }
@ -32,6 +34,6 @@ type UserCredit struct {
// 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...)
@ -171,8 +172,8 @@ func convertDBCredit(userCreditDBX *dbx.UserCredit) (*console.UserCredit, error)
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()
@ -36,21 +35,21 @@ func TestUsercredits(t *testing.T) {
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),
}, },
} }
@ -76,15 +75,15 @@ func TestUsercredits(t *testing.T) {
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,
@ -96,15 +95,15 @@ func TestUsercredits(t *testing.T) {
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,
@ -116,15 +115,15 @@ func TestUsercredits(t *testing.T) {
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>