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:
parent
74e12a0bb4
commit
e8605d312e
37
internal/currency/usd.go
Normal file
37
internal/currency/usd.go
Normal 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)
|
||||||
|
}
|
38
internal/currency/usd_test.go
Normal file
38
internal/currency/usd_test.go
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
|
58
satellite/marketingweb/form.go
Normal file
58
satellite/marketingweb/form.go
Normal 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)
|
||||||
|
}
|
@ -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)
|
||||||
|
@ -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"},
|
||||||
},
|
},
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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 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">∞</div>
|
<div class="col">∞</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 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 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>
|
||||||
|
Loading…
Reference in New Issue
Block a user