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"
"github.com/skyrings/skyring-common/tools/uuid"
"storj.io/storj/internal/currency"
)
// UserCredits holds information to interact with database
@ -19,19 +21,19 @@ type UserCredits interface {
// UserCredit holds information about an user's credit
type UserCredit struct {
ID int
UserID uuid.UUID
OfferID int
ReferredBy uuid.UUID
CreditsEarnedInCents int
CreditsUsedInCents int
ExpiresAt time.Time
CreatedAt time.Time
ID int
UserID uuid.UUID
OfferID int
ReferredBy uuid.UUID
CreditsEarned currency.USD
CreditsUsed currency.USD
ExpiresAt time.Time
CreatedAt time.Time
}
// UserCreditUsage holds information about credit usage information
type UserCreditUsage struct {
Referred int64
AvailableCredits int64
UsedCredits int64
AvailableCredits currency.USD
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/http"
"path/filepath"
"reflect"
"time"
"github.com/gorilla/mux"
"github.com/gorilla/schema"
"github.com/zeebo/errs"
"go.uber.org/zap"
"golang.org/x/sync/errgroup"
@ -21,11 +18,8 @@ import (
"storj.io/storj/satellite/rewards"
)
var (
// Error is satellite marketing error type
Error = errs.Class("satellite marketing error")
decoder = schema.NewDecoder()
)
// Error is satellite marketing error type
var Error = errs.Class("satellite marketing error")
// Config contains configuration for marketingweb server
type Config struct {
@ -55,11 +49,6 @@ type offerSet struct {
FreeCredits rewards.Offers
}
// init safely registers convertStringToTime for the decoder.
func init() {
decoder.RegisterConverter(time.Time{}, convertStringToTime)
}
// organizeOffers organizes offers by type.
func organizeOffers(offers []rewards.Offer) offerSet {
var os offerSet
@ -158,9 +147,7 @@ func (s *Server) parseTemplates() (err error) {
filepath.Join(s.templateDir, "err.html"),
)
s.templates.home, err = template.New("landingPage").Funcs(template.FuncMap{
"ToDollars": rewards.ToDollars,
}).ParseFiles(homeFiles...)
s.templates.home, err = template.New("landingPage").ParseFiles(homeFiles...)
if err != nil {
return Error.Wrap(err)
}
@ -183,33 +170,6 @@ func (s *Server) parseTemplates() (err error) {
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.
func (s *Server) CreateOffer(w http.ResponseWriter, req *http.Request) {
offer, err := parseOfferForm(w, req)

View File

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

View File

@ -5,21 +5,11 @@ package rewards
import (
"context"
"fmt"
"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
type DB interface {
ListAll(ctx context.Context) ([]Offer, error)
@ -34,8 +24,8 @@ type NewOffer struct {
Name string
Description string
AwardCreditInCents int
InviteeCreditInCents int
AwardCredit currency.USD
InviteeCredit currency.USD
RedeemableCap int
@ -83,8 +73,8 @@ type Offer struct {
Name string
Description string
AwardCreditInCents int
InviteeCreditInCents int
AwardCredit currency.USD
InviteeCredit currency.USD
AwardCreditDurationDays int
InviteeCreditDurationDays int

View File

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

View File

@ -10,6 +10,7 @@ import (
"github.com/zeebo/errs"
"storj.io/storj/internal/currency"
"storj.io/storj/satellite/rewards"
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)
var awardCreditInCents, inviteeCreditInCents int
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 {
return nil, offerErr.New("no current offer")
}
if err != nil {
return nil, offerErr.Wrap(err)
}
o.AwardCredit = currency.Cents(awardCreditInCents)
o.InviteeCredit = currency.Cents(inviteeCreditInCents)
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,
dbx.Offer_Name(o.Name),
dbx.Offer_Description(o.Description),
dbx.Offer_AwardCreditInCents(o.AwardCreditInCents),
dbx.Offer_InviteeCreditInCents(o.InviteeCreditInCents),
dbx.Offer_AwardCreditInCents(o.AwardCredit.Cents()),
dbx.Offer_InviteeCreditInCents(o.InviteeCredit.Cents()),
dbx.Offer_AwardCreditDurationDays(o.AwardCreditDurationDays),
dbx.Offer_InviteeCreditDurationDays(o.InviteeCreditDurationDays),
dbx.Offer_RedeemableCap(o.RedeemableCap),
@ -175,8 +179,8 @@ func convertDBOffer(offerDbx *dbx.Offer) (*rewards.Offer, error) {
ID: offerDbx.Id,
Name: offerDbx.Name,
Description: offerDbx.Description,
AwardCreditInCents: offerDbx.AwardCreditInCents,
InviteeCreditInCents: offerDbx.InviteeCreditInCents,
AwardCredit: currency.Cents(offerDbx.AwardCreditInCents),
InviteeCredit: currency.Cents(offerDbx.InviteeCreditInCents),
RedeemableCap: offerDbx.RedeemableCap,
NumRedeemed: offerDbx.NumRedeemed,
ExpiresAt: offerDbx.ExpiresAt,

View File

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

View File

@ -10,6 +10,7 @@ import (
"github.com/stretchr/testify/require"
"storj.io/storj/internal/currency"
"storj.io/storj/internal/testcontext"
"storj.io/storj/internal/testrand"
"storj.io/storj/satellite"
@ -19,8 +20,6 @@ import (
)
func TestUsercredits(t *testing.T) {
t.Parallel()
satellitedbtest.Run(t, func(t *testing.T, db satellite.DB) {
ctx := testcontext.New(t)
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
var invalidUserCredits = []console.UserCredit{
{
UserID: randomID,
OfferID: offer.ID,
ReferredBy: referrer.ID,
CreditsEarnedInCents: 100,
ExpiresAt: time.Now().UTC().AddDate(0, 1, 0),
UserID: randomID,
OfferID: offer.ID,
ReferredBy: referrer.ID,
CreditsEarned: currency.Cents(100),
ExpiresAt: time.Now().UTC().AddDate(0, 1, 0),
},
{
UserID: user.ID,
OfferID: 10,
ReferredBy: referrer.ID,
CreditsEarnedInCents: 100,
ExpiresAt: time.Now().UTC().AddDate(0, 1, 0),
UserID: user.ID,
OfferID: 10,
ReferredBy: referrer.ID,
CreditsEarned: currency.Cents(100),
ExpiresAt: time.Now().UTC().AddDate(0, 1, 0),
},
{
UserID: user.ID,
OfferID: offer.ID,
ReferredBy: randomID,
CreditsEarnedInCents: 100,
ExpiresAt: time.Now().UTC().AddDate(0, 1, 0),
UserID: user.ID,
OfferID: offer.ID,
ReferredBy: randomID,
CreditsEarned: currency.Cents(100),
ExpiresAt: time.Now().UTC().AddDate(0, 1, 0),
},
}
@ -73,18 +72,18 @@ func TestUsercredits(t *testing.T) {
}{
{
userCredit: console.UserCredit{
UserID: user.ID,
OfferID: offer.ID,
ReferredBy: referrer.ID,
CreditsEarnedInCents: 100,
ExpiresAt: time.Now().UTC().AddDate(0, 1, 0),
UserID: user.ID,
OfferID: offer.ID,
ReferredBy: referrer.ID,
CreditsEarned: currency.Cents(100),
ExpiresAt: time.Now().UTC().AddDate(0, 1, 0),
},
chargedCredits: 120,
expected: result{
remainingCharge: 20,
usage: console.UserCreditUsage{
AvailableCredits: 0,
UsedCredits: 100,
AvailableCredits: currency.Cents(0),
UsedCredits: currency.Cents(100),
Referred: 0,
},
hasErr: false,
@ -93,18 +92,18 @@ func TestUsercredits(t *testing.T) {
{
// simulate a credit that's already expired
userCredit: console.UserCredit{
UserID: user.ID,
OfferID: offer.ID,
ReferredBy: referrer.ID,
CreditsEarnedInCents: 100,
ExpiresAt: time.Now().UTC().AddDate(0, 0, -5),
UserID: user.ID,
OfferID: offer.ID,
ReferredBy: referrer.ID,
CreditsEarned: currency.Cents(100),
ExpiresAt: time.Now().UTC().AddDate(0, 0, -5),
},
chargedCredits: 60,
expected: result{
remainingCharge: 60,
usage: console.UserCreditUsage{
AvailableCredits: 0,
UsedCredits: 100,
AvailableCredits: currency.Cents(0),
UsedCredits: currency.Cents(100),
Referred: 0,
},
hasErr: true,
@ -113,18 +112,18 @@ func TestUsercredits(t *testing.T) {
{
// simulate a credit that's not expired
userCredit: console.UserCredit{
UserID: user.ID,
OfferID: offer.ID,
ReferredBy: referrer.ID,
CreditsEarnedInCents: 100,
ExpiresAt: time.Now().UTC().AddDate(0, 0, 5),
UserID: user.ID,
OfferID: offer.ID,
ReferredBy: referrer.ID,
CreditsEarned: currency.Cents(100),
ExpiresAt: time.Now().UTC().AddDate(0, 0, 5),
},
chargedCredits: 80,
expected: result{
remainingCharge: 0,
usage: console.UserCreditUsage{
AvailableCredits: 20,
UsedCredits: 180,
AvailableCredits: currency.Cents(20),
UsedCredits: currency.Cents(180),
Referred: 0,
},
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{
Name: "test",
Description: "test offer 1",
AwardCreditInCents: 100,
InviteeCreditInCents: 50,
AwardCredit: currency.Cents(100),
InviteeCredit: currency.Cents(50),
AwardCreditDurationDays: 60,
InviteeCreditDurationDays: 30,
RedeemableCap: 50,

View File

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

View File

@ -18,7 +18,7 @@ See LICENSE for copying information. -->
{{$defaultOffer := .FreeCredits.GetDefaultFromSet}}
<div class="row data-row">
<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.RedeemableCap}}</div>
<div class="col">{{printf "%.10s" $defaultOffer.CreatedAt}}</div>
@ -32,7 +32,7 @@ See LICENSE for copying information. -->
{{$currentOffer := .FreeCredits.GetCurrentFromSet}}
<div class="row data-row">
<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.RedeemableCap}}</div>
<div class="col">{{printf "%.10s" $currentOffer.CreatedAt}}</div>
@ -53,7 +53,7 @@ See LICENSE for copying information. -->
{{if $offer.IsDone}}
<div class="row data-row">
<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.RedeemableCap}}</div>
<div class="col">{{printf "%.10s" $offer.CreatedAt}}</div>

View File

@ -29,16 +29,16 @@ See LICENSE for copying information. -->
</div>
<div class="form-row">
<div class="form-group col-md-2">
<label for="InviteeCreditInCents">Give Credit</label>
<input type="number" class="form-control" name="InviteeCreditInCents" id="InviteeCreditInCents" placeholder="$50" required>
<label for="InviteeCredit">Give Credit</label>
<input type="number" class="form-control" name="InviteeCredit" id="InviteeCredit" placeholder="$50" required>
</div>
<div class="form-group col-md-2">
<label for="InviteeCreditDurationDays">Give Credit Exp.</label>
<input type="number" class="form-control" name="InviteeCreditDurationDays" id="InviteeCreditDurationDays" min="1" placeholder="14 days" required>
</div>
<div class="form-group col-md-2">
<label for="AwardCreditInCents">Award Credit</label>
<input type="number" class="form-control" name="AwardCreditInCents" id="AwardCreditInCents" placeholder="$50" min="1" required>
<label for="AwardCredit">Award Credit</label>
<input type="number" class="form-control" name="AwardCredit" id="AwardCredit" placeholder="$50" min="1" required>
</div>
<div class="form-group col-md-3">
<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><hr>
<div class="row offer-heading ">
<p class="offer-type">default offer</p>
<p class="offer-type">Default&nbsp;Offer</p>
</div>
{{$defaultOffer := .ReferralOffers.GetDefaultFromSet}}
<div class="row data-row">
<div class="col ml-3">{{$defaultOffer.Name}}</div>
<div class="col">${{ToDollars $defaultOffer.InviteeCreditInCents}}</div>
<div class="col">${{ToDollars $defaultOffer.AwardCreditInCents}}</div>
<div class="col">${{$defaultOffer.InviteeCredit}}</div>
<div class="col">${{$defaultOffer.AwardCredit}}</div>
<div class="col">{{$defaultOffer.NumRedeemed}}</div>
<div class="col">&#8734;</div>
<div class="col">{{printf "%.10s" $defaultOffer.CreatedAt}}</div>
@ -28,14 +28,14 @@ See LICENSE for copying information. -->
<div class="col"></div>
</div><hr>
<div class="row offer-heading ">
<p class="offer-type">current offer</p>
<p class="offer-type">Current&nbsp;Offer</p>
</div>
{{if gt (len .ReferralOffers.Set) 0}}
{{$currentOffer := .ReferralOffers.GetCurrentFromSet}}
<div class="row data-row">
<div class="col ml-3">{{$currentOffer.Name}}</div>
<div class="col">${{ToDollars $currentOffer.InviteeCreditInCents}}</div>
<div class="col">${{ToDollars $currentOffer.AwardCreditInCents}}</div>
<div class="col">${{$currentOffer.InviteeCredit}}</div>
<div class="col">${{$currentOffer.AwardCredit}}</div>
<div class="col">{{$currentOffer.NumRedeemed}}</div>
<div class="col">{{$currentOffer.RedeemableCap}}</div>
<div class="col">{{printf "%.10s" $currentOffer.CreatedAt}}</div>
@ -48,7 +48,7 @@ See LICENSE for copying information. -->
</div><hr>
{{end}}
<div class="row offer-heading ">
<p class="offer-type">other offers</p>
<p class="offer-type">Other&nbsp;Offers</p>
</div>
{{if gt (len .ReferralOffers.Set) 0}}
{{range .ReferralOffers.Set}}
@ -56,8 +56,8 @@ See LICENSE for copying information. -->
{{if $offer.IsDone}}
<div class="row data-row">
<div class="col ml-3">{{$offer.Name}}</div>
<div class="col">${{ToDollars $offer.InviteeCreditInCents}}</div>
<div class="col">${{ToDollars $offer.AwardCreditInCents}}</div>
<div class="col">${{$offer.InviteeCredit}}</div>
<div class="col">${{$offer.AwardCredit}}</div>
<div class="col">{{.NumRedeemed}}</div>
<div class="col">{{.RedeemableCap}}</div>
<div class="col">{{printf "%.10s" .CreatedAt}}</div>