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
@ -23,8 +25,8 @@ type UserCredit struct {
UserID uuid.UUID
OfferID int
ReferredBy uuid.UUID
CreditsEarnedInCents int
CreditsUsedInCents int
CreditsEarned currency.USD
CreditsUsed currency.USD
ExpiresAt time.Time
CreatedAt time.Time
}
@ -32,6 +34,6 @@ type UserCredit struct {
// 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
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...)
@ -171,8 +172,8 @@ func convertDBCredit(userCreditDBX *dbx.UserCredit) (*console.UserCredit, error)
UserID: userID,
OfferID: userCreditDBX.OfferId,
ReferredBy: referredByID,
CreditsEarnedInCents: userCreditDBX.CreditsEarnedInCents,
CreditsUsedInCents: userCreditDBX.CreditsUsedInCents,
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()
@ -36,21 +35,21 @@ func TestUsercredits(t *testing.T) {
UserID: randomID,
OfferID: offer.ID,
ReferredBy: referrer.ID,
CreditsEarnedInCents: 100,
CreditsEarned: currency.Cents(100),
ExpiresAt: time.Now().UTC().AddDate(0, 1, 0),
},
{
UserID: user.ID,
OfferID: 10,
ReferredBy: referrer.ID,
CreditsEarnedInCents: 100,
CreditsEarned: currency.Cents(100),
ExpiresAt: time.Now().UTC().AddDate(0, 1, 0),
},
{
UserID: user.ID,
OfferID: offer.ID,
ReferredBy: randomID,
CreditsEarnedInCents: 100,
CreditsEarned: currency.Cents(100),
ExpiresAt: time.Now().UTC().AddDate(0, 1, 0),
},
}
@ -76,15 +75,15 @@ func TestUsercredits(t *testing.T) {
UserID: user.ID,
OfferID: offer.ID,
ReferredBy: referrer.ID,
CreditsEarnedInCents: 100,
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,
@ -96,15 +95,15 @@ func TestUsercredits(t *testing.T) {
UserID: user.ID,
OfferID: offer.ID,
ReferredBy: referrer.ID,
CreditsEarnedInCents: 100,
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,
@ -116,15 +115,15 @@ func TestUsercredits(t *testing.T) {
UserID: user.ID,
OfferID: offer.ID,
ReferredBy: referrer.ID,
CreditsEarnedInCents: 100,
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>