satellite/rewards: ensure that partner information is asked from a service (#3275)

This commit is contained in:
Egon Elbre 2019-11-05 14:58:09 +02:00 committed by GitHub
parent bee1acef4e
commit 9c59efd33d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 474 additions and 239 deletions

View File

@ -46,6 +46,7 @@ import (
"storj.io/storj/satellite/payments/paymentsconfig"
"storj.io/storj/satellite/payments/stripecoinpayments"
"storj.io/storj/satellite/repair/irreparable"
"storj.io/storj/satellite/rewards"
"storj.io/storj/satellite/vouchers"
)
@ -119,6 +120,8 @@ type API struct {
}
Marketing struct {
PartnersService *rewards.PartnersService
Listener net.Listener
Endpoint *marketingweb.Server
}
@ -384,6 +387,36 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB, pointerDB metai
}
}
{ // setup marketing portal
log.Debug("Satellite API Process setting up marketing server")
peer.Marketing.PartnersService = rewards.NewPartnersService(
peer.Log.Named("partners"),
rewards.DefaultPartnersDB,
[]string{
"https://us-central-1.tardigrade.io/",
"https://asia-east-1.tardigrade.io/",
"https://europe-west-1.tardigrade.io/",
},
)
peer.Marketing.Listener, err = net.Listen("tcp", config.Marketing.Address)
if err != nil {
return nil, errs.Combine(err, peer.Close())
}
peer.Marketing.Endpoint, err = marketingweb.NewServer(
peer.Log.Named("marketing:endpoint"),
config.Marketing,
peer.DB.Rewards(),
peer.Marketing.PartnersService,
peer.Marketing.Listener,
)
if err != nil {
return nil, errs.Combine(err, peer.Close())
}
}
{ // setup console
log.Debug("Satellite API Process setting up console")
consoleConfig := config.Console
@ -400,6 +433,7 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB, pointerDB metai
&consoleauth.Hmac{Secret: []byte(consoleConfig.AuthTokenSecret)},
peer.DB.Console(),
peer.DB.Rewards(),
peer.Marketing.PartnersService,
peer.Payments.Accounts,
consoleConfig.PasswordCost,
)
@ -415,24 +449,6 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB, pointerDB metai
)
}
{ // setup marketing portal
log.Debug("Satellite API Process setting up marketing server")
marketingConfig := config.Marketing
peer.Marketing.Listener, err = net.Listen("tcp", marketingConfig.Address)
if err != nil {
return nil, errs.Combine(err, peer.Close())
}
peer.Marketing.Endpoint, err = marketingweb.NewServer(
peer.Log.Named("marketing:endpoint"),
marketingConfig,
peer.DB.Rewards(),
peer.Marketing.Listener,
)
if err != nil {
return nil, errs.Combine(err, peer.Close())
}
}
{ // setup node stats endpoint
log.Debug("Satellite API Process setting up node stats endpoint")
peer.NodeStats.Endpoint = nodestats.NewEndpoint(

View File

@ -50,6 +50,16 @@ func TestGrapqhlMutation(t *testing.T) {
log := zaptest.NewLogger(t)
partnersService := rewards.NewPartnersService(
log.Named("partners"),
rewards.DefaultPartnersDB,
[]string{
"https://us-central-1.tardigrade.io/",
"https://asia-east-1.tardigrade.io/",
"https://europe-west-1.tardigrade.io/",
},
)
paymentsConfig := stripecoinpayments.Config{}
payments := stripecoinpayments.NewService(log, paymentsConfig, db.Customers(), db.CoinpaymentsTransactions())
@ -58,6 +68,7 @@ func TestGrapqhlMutation(t *testing.T) {
&consoleauth.Hmac{Secret: []byte("my-suppa-secret-key")},
db.Console(),
db.Rewards(),
partnersService,
payments.Accounts(),
console.TestPasswordCost,
)

View File

@ -21,6 +21,7 @@ import (
"storj.io/storj/satellite/console/consoleweb/consoleql"
"storj.io/storj/satellite/mailservice"
"storj.io/storj/satellite/payments/stripecoinpayments"
"storj.io/storj/satellite/rewards"
"storj.io/storj/satellite/satellitedb/satellitedbtest"
)
@ -31,6 +32,16 @@ func TestGraphqlQuery(t *testing.T) {
log := zaptest.NewLogger(t)
partnersService := rewards.NewPartnersService(
log.Named("partners"),
rewards.DefaultPartnersDB,
[]string{
"https://us-central-1.tardigrade.io/",
"https://asia-east-1.tardigrade.io/",
"https://europe-west-1.tardigrade.io/",
},
)
paymentsConfig := stripecoinpayments.Config{}
payments := stripecoinpayments.NewService(log, paymentsConfig, db.Customers(), db.CoinpaymentsTransactions())
@ -39,6 +50,7 @@ func TestGraphqlQuery(t *testing.T) {
&consoleauth.Hmac{Secret: []byte("my-suppa-secret-key")},
db.Console(),
db.Rewards(),
partnersService,
payments.Accounts(),
console.TestPasswordCost,
)

View File

@ -69,6 +69,7 @@ type Service struct {
log *zap.Logger
store DB
rewards rewards.DB
partners *rewards.PartnersService
accounts payments.Accounts
passwordCost int
@ -80,7 +81,7 @@ type PaymentsService struct {
}
// NewService returns new instance of Service
func NewService(log *zap.Logger, signer Signer, store DB, rewards rewards.DB, accounts payments.Accounts, passwordCost int) (*Service, error) {
func NewService(log *zap.Logger, signer Signer, store DB, rewards rewards.DB, partners *rewards.PartnersService, accounts payments.Accounts, passwordCost int) (*Service, error) {
if signer == nil {
return nil, errs.New("signer can't be nil")
}
@ -99,6 +100,7 @@ func NewService(log *zap.Logger, signer Signer, store DB, rewards rewards.DB, ac
Signer: signer,
store: store,
rewards: rewards,
partners: partners,
accounts: accounts,
passwordCost: passwordCost,
}, nil
@ -232,7 +234,8 @@ func (s *Service) CreateUser(ctx context.Context, user CreateUser, tokenSecret R
s.log.Error("internal error", zap.Error(err))
return nil, ErrConsoleInternal.Wrap(err)
}
currentReward, err := offers.GetActiveOffer(offerType, user.PartnerID)
currentReward, err := s.partners.GetActiveOffer(ctx, offers, offerType, user.PartnerID)
if err != nil && !rewards.NoCurrentOfferErr.Has(err) {
s.log.Error("internal error", zap.Error(err))
return nil, ErrConsoleInternal.Wrap(err)
@ -633,7 +636,8 @@ func (s *Service) GetCurrentRewardByType(ctx context.Context, offerType rewards.
s.log.Error("internal error", zap.Error(err))
return nil, ErrConsoleInternal.Wrap(err)
}
return offers.GetActiveOffer(offerType, "")
return s.partners.GetActiveOffer(ctx, offers, offerType, "")
}
// GetUserCreditUsage is a method for querying users' credit information up until now

View File

@ -1,20 +1,24 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information
package rewards
package marketingweb
import "sort"
import (
"context"
"storj.io/storj/satellite/rewards"
)
// OrganizedOffers contains a list of offers organized by status.
type OrganizedOffers struct {
Active Offer
Default Offer
Done Offers
Active rewards.Offer
Default rewards.Offer
Done rewards.Offers
}
// OpenSourcePartner contains all data for an Open Source Partner.
type OpenSourcePartner struct {
PartnerInfo
rewards.PartnerInfo
PartnerOffers OrganizedOffers
}
@ -28,22 +32,17 @@ type OfferSet struct {
PartnerTables PartnerSet
}
type referralInfo struct {
UserID string
PartnerID string
}
// OrganizeOffersByStatus organizes offers by OfferStatus.
func (offers Offers) OrganizeOffersByStatus() OrganizedOffers {
func (server *Server) OrganizeOffersByStatus(offers rewards.Offers) OrganizedOffers {
var oo OrganizedOffers
for _, offer := range offers {
switch offer.Status {
case Active:
case rewards.Active:
oo.Active = offer
case Default:
case rewards.Default:
oo.Default = offer
case Done:
case rewards.Done:
oo.Done = append(oo.Done, offer)
}
}
@ -51,56 +50,48 @@ func (offers Offers) OrganizeOffersByStatus() OrganizedOffers {
}
// OrganizeOffersByType organizes offers by OfferType.
func (offers Offers) OrganizeOffersByType() OfferSet {
func (server *Server) OrganizeOffersByType(offers rewards.Offers) OfferSet {
var (
fc, ro, p Offers
fc, ro, p rewards.Offers
offerSet OfferSet
)
for _, offer := range offers {
switch offer.Type {
case FreeCredit:
case rewards.FreeCredit:
fc = append(fc, offer)
case Referral:
case rewards.Referral:
ro = append(ro, offer)
case Partner:
case rewards.Partner:
p = append(p, offer)
default:
continue
}
}
offerSet.FreeCredits = fc.OrganizeOffersByStatus()
offerSet.ReferralOffers = ro.OrganizeOffersByStatus()
offerSet.PartnerTables = organizePartnerData(p)
offerSet.FreeCredits = server.OrganizeOffersByStatus(fc)
offerSet.ReferralOffers = server.OrganizeOffersByStatus(ro)
offerSet.PartnerTables = server.organizePartnerData(p)
return offerSet
}
// createPartnerSet generates a PartnerSet from the config file.
func createPartnerSet() PartnerSet {
partners := LoadPartnerInfos()
func (server *Server) createPartnerSet() PartnerSet {
all, _ := server.partners.All(context.TODO()) // TODO: don't ignore error
var ps PartnerSet
keys := make([]string, len(partners))
i := 0
for k := range partners {
keys[i] = k
i++
}
sort.Strings(keys)
for _, key := range keys {
for _, partner := range all {
ps = append(ps, OpenSourcePartner{
PartnerInfo: partners[key],
PartnerInfo: partner,
})
}
return ps
}
// matchOffersToPartnerSet assigns offers to the partner they belong to.
func matchOffersToPartnerSet(offers Offers, partnerSet PartnerSet) PartnerSet {
func (server *Server) matchOffersToPartnerSet(offers rewards.Offers, partnerSet PartnerSet) PartnerSet {
for i := range partnerSet {
var partnerOffersByName Offers
var partnerOffersByName rewards.Offers
for _, o := range offers {
if o.Name == partnerSet[i].PartnerInfo.Name {
@ -108,7 +99,7 @@ func matchOffersToPartnerSet(offers Offers, partnerSet PartnerSet) PartnerSet {
}
}
partnerSet[i].PartnerOffers = partnerOffersByName.OrganizeOffersByStatus()
partnerSet[i].PartnerOffers = server.OrganizeOffersByStatus(partnerOffersByName)
}
return partnerSet
@ -117,16 +108,7 @@ func matchOffersToPartnerSet(offers Offers, partnerSet PartnerSet) PartnerSet {
// organizePartnerData returns a list of Open Source Partners
// whose offers have been organized by status, type, and
// assigned to the correct partner.
func organizePartnerData(offers Offers) PartnerSet {
partnerData := matchOffersToPartnerSet(offers, createPartnerSet())
func (server *Server) organizePartnerData(offers rewards.Offers) PartnerSet {
partnerData := server.matchOffersToPartnerSet(offers, server.createPartnerSet())
return partnerData
}
// getTardigradeDomains returns domain names for tardigrade satellites
func getTardigradeDomains() []string {
return []string{
"https://us-central-1.tardigrade.io/",
"https://asia-east-1.tardigrade.io/",
"https://europe-west-1.tardigrade.io/",
}
}

View File

@ -33,11 +33,15 @@ type Config struct {
//
// architecture: Endpoint
type Server struct {
log *zap.Logger
config Config
listener net.Listener
server http.Server
db rewards.DB
log *zap.Logger
config Config
listener net.Listener
server http.Server
rewards rewards.DB
partners *rewards.PartnersService
templateDir string
templates struct {
home *template.Template
@ -58,12 +62,14 @@ func (s *Server) commonPages() []string {
}
// NewServer creates new instance of offersweb server
func NewServer(logger *zap.Logger, config Config, db rewards.DB, listener net.Listener) (*Server, error) {
func NewServer(logger *zap.Logger, config Config, rewards rewards.DB, partners *rewards.PartnersService, listener net.Listener) (*Server, error) {
s := &Server{
log: logger,
config: config,
listener: listener,
db: db,
rewards: rewards,
partners: partners,
}
logger.Sugar().Debugf("Starting Marketing Admin UI on %s...", s.listener.Addr().String())
@ -93,14 +99,14 @@ func (s *Server) GetOffers(w http.ResponseWriter, req *http.Request) {
return
}
offers, err := s.db.ListAll(req.Context())
offers, err := s.rewards.ListAll(req.Context())
if err != nil {
s.log.Error("failed to retrieve all offers", zap.Error(err))
s.serveInternalError(w, req, err)
return
}
if err := s.templates.home.ExecuteTemplate(w, "base", offers.OrganizeOffersByType()); err != nil {
if err := s.templates.home.ExecuteTemplate(w, "base", s.OrganizeOffersByType(offers)); err != nil {
s.log.Error("failed to execute template", zap.Error(err))
}
}
@ -135,7 +141,7 @@ func (s *Server) parseTemplates() (err error) {
s.templates.home, err = template.New("home-page").Funcs(template.FuncMap{
"BaseURL": s.GetBaseURL,
"ReferralLink": rewards.GeneratePartnerLink,
"ReferralLink": s.generatePartnerLink,
}).ParseFiles(homeFiles...)
if err != nil {
return Error.Wrap(err)
@ -165,6 +171,10 @@ func (s *Server) parseTemplates() (err error) {
return nil
}
func (s *Server) generatePartnerLink(offerName string) ([]string, error) {
return s.partners.GeneratePartnerLink(context.TODO(), offerName)
}
// CreateOffer handles requests to create new offers.
func (s *Server) CreateOffer(w http.ResponseWriter, req *http.Request) {
offer, err := parseOfferForm(w, req)
@ -190,7 +200,7 @@ func (s *Server) CreateOffer(w http.ResponseWriter, req *http.Request) {
return
}
if _, err := s.db.Create(req.Context(), &offer); err != nil {
if _, err := s.rewards.Create(req.Context(), &offer); err != nil {
s.log.Error("failed to insert new offer", zap.Error(err))
s.serveBadRequest(w, req, err)
return
@ -208,7 +218,7 @@ func (s *Server) StopOffer(w http.ResponseWriter, req *http.Request) {
return
}
if err := s.db.Finish(req.Context(), offerID); err != nil {
if err := s.rewards.Finish(req.Context(), offerID); err != nil {
s.log.Error("failed to stop offer", zap.Error(err))
s.serveInternalError(w, req, err)
return

View File

@ -0,0 +1,44 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package rewards
import (
"encoding/json"
"os"
"strings"
"github.com/zeebo/errs"
)
// PartnerList defines a json struct for defining partners.
type PartnerList struct {
Partners []PartnerInfo
}
// PartnerInfo contains information about a partner.
type PartnerInfo struct {
Name string
ID string
}
// UserAgent returns partners cano user agent.
func (p *PartnerInfo) UserAgent() string { return p.Name }
// CanonicalUserAgentProduct returns canonicalizes the user agent product, which is suitable for lookups.
func CanonicalUserAgentProduct(product string) string { return strings.ToLower(product) }
// PartnersListFromJSONFile loads a json definition of partners.
func PartnersListFromJSONFile(path string) (*PartnerList, error) {
file, err := os.Open(path)
if err != nil {
return nil, Error.Wrap(err)
}
defer func() {
err = errs.Combine(err, Error.Wrap(file.Close()))
}()
var list PartnerList
err = json.NewDecoder(file).Decode(&list)
return &list, Error.Wrap(err)
}

View File

@ -3,192 +3,115 @@
package rewards
import (
"encoding/base64"
"encoding/json"
"path"
// DefaultPartnersDB is current default settings.
var DefaultPartnersDB = func() PartnersDB {
list := DefaultPartners()
db, err := NewPartnersStaticDB(&list)
if err != nil {
panic(err)
}
return db
}()
"github.com/zeebo/errs"
)
var (
// NoMatchPartnerIDErr is the error class used when an offer has reached its redemption capacity
NoMatchPartnerIDErr = errs.Class("partner not exist")
)
// PartnerInfo contains the name and ID of an Open Source Partner
type PartnerInfo struct {
ID, Name string
}
// Partners contains a list of partners.
type Partners map[string]PartnerInfo
// LoadPartnerInfos returns our current Open Source Partners.
func LoadPartnerInfos() Partners {
return Partners{
"120bf202-8252-437e-ac12-0e364bee852e": PartnerInfo{
// DefaultPartners lists Storj default open-source partners.
func DefaultPartners() PartnerList {
return PartnerList{
Partners: []PartnerInfo{{
Name: "Blocknify",
ID: "120bf202-8252-437e-ac12-0e364bee852e",
},
"53688ea5-8695-4060-a2c6-b56969217909": PartnerInfo{
}, {
Name: "Breaker",
ID: "53688ea5-8695-4060-a2c6-b56969217909",
},
"2fb801c6-a6d7-4d82-a838-32fef98cc398": PartnerInfo{
}, {
Name: "Confluent",
ID: "2fb801c6-a6d7-4d82-a838-32fef98cc398",
},
"e28c8847-b323-4a7d-8111-25a0578a58bb": PartnerInfo{
}, {
Name: "Consensys",
ID: "e28c8847-b323-4a7d-8111-25a0578a58bb",
},
"0af89ac1-0189-42c6-a47c-e169780b3818": PartnerInfo{
}, {
Name: "Couchbase",
ID: "0af89ac1-0189-42c6-a47c-e169780b3818",
},
"881b92f6-77aa-42ee-961a-b80009d45dd8": PartnerInfo{
}, {
Name: "Digital Ocean",
ID: "881b92f6-77aa-42ee-961a-b80009d45dd8",
},
"cadac3fb-6a3f-4d17-9748-cc66d0617d55": PartnerInfo{
}, {
Name: "Deloitte",
ID: "cadac3fb-6a3f-4d17-9748-cc66d0617d55",
},
"53fb82d7-73ff-4a1a-ab0c-6968cffc850e": PartnerInfo{
}, {
Name: "DVLabs",
ID: "53fb82d7-73ff-4a1a-ab0c-6968cffc850e",
},
"86c33256-cded-434c-aaac-405343974394": PartnerInfo{
}, {
Name: "Fluree",
ID: "86c33256-cded-434c-aaac-405343974394",
},
"3e1b911a-c778-47ea-878c-9f3f264f8bc1": PartnerInfo{
}, {
Name: "Flexential",
ID: "3e1b911a-c778-47ea-878c-9f3f264f8bc1",
},
"706011f3-400e-45eb-a796-90cce2a7d67e": PartnerInfo{
}, {
Name: "Heroku",
ID: "706011f3-400e-45eb-a796-90cce2a7d67e",
},
"1519bdee-ed18-45fe-86c6-4c7fa9668a14": PartnerInfo{
}, {
Name: "Infura",
ID: "1519bdee-ed18-45fe-86c6-4c7fa9668a14",
},
"e56c6a65-d5bf-457a-a414-e55c36624f73": PartnerInfo{
}, {
Name: "GroundX",
ID: "e56c6a65-d5bf-457a-a414-e55c36624f73",
},
"8ee019ef-2aae-4867-9c18-41c65ea318c4": PartnerInfo{
}, {
Name: "MariaDB",
ID: "8ee019ef-2aae-4867-9c18-41c65ea318c4",
},
"bbd340b2-0ae4-4254-af90-eaba6c273abb": PartnerInfo{
}, {
Name: "MongoDB",
ID: "bbd340b2-0ae4-4254-af90-eaba6c273abb",
},
"3405a882-0cb2-4f91-a6e0-21be193b80e5": PartnerInfo{
}, {
Name: "Netki",
ID: "3405a882-0cb2-4f91-a6e0-21be193b80e5",
},
"a1ba07a4-e095-4a43-914c-1d56c9ff5afd": PartnerInfo{
}, {
Name: "FileZilla",
ID: "a1ba07a4-e095-4a43-914c-1d56c9ff5afd",
},
"e50a17b3-4d82-4da7-8719-09312a83685d": PartnerInfo{
}, {
Name: "InfluxDB",
ID: "e50a17b3-4d82-4da7-8719-09312a83685d",
},
"c10228c2-af70-4e4d-be49-e8bfbe9ca8ef": PartnerInfo{
}, {
Name: "Mysterium Network",
ID: "c10228c2-af70-4e4d-be49-e8bfbe9ca8ef",
},
"OSPP005": PartnerInfo{
}, {
Name: "Kafka",
ID: "OSPP005",
},
"5bffe844-5da7-4aa9-bf37-7d695cf819f2": PartnerInfo{
}, {
Name: "Minio",
ID: "5bffe844-5da7-4aa9-bf37-7d695cf819f2",
},
"42f588fb-f39d-4886-81af-b614ca16ce37": PartnerInfo{
}, {
Name: "Nextcloud",
ID: "42f588fb-f39d-4886-81af-b614ca16ce37",
},
"3b53a9b3-2005-476c-9ffd-894ed832abe4": PartnerInfo{
}, {
Name: "Node Haven",
ID: "3b53a9b3-2005-476c-9ffd-894ed832abe4",
},
"dc01ed96-2990-4819-9cb3-45d4846b9ad1": PartnerInfo{
}, {
Name: "Plesk",
ID: "dc01ed96-2990-4819-9cb3-45d4846b9ad1",
},
"b02b9f0d-fac7-439c-8ba2-0c4634d5826f": PartnerInfo{
}, {
Name: "Pydio",
ID: "b02b9f0d-fac7-439c-8ba2-0c4634d5826f",
},
"57855387-5a58-4a2b-97d2-15b1d76eea3c": PartnerInfo{
}, {
Name: "Raiden Network",
ID: "57855387-5a58-4a2b-97d2-15b1d76eea3c",
},
"4400d796-3777-4964-8536-22a4ae439ed3": PartnerInfo{
}, {
Name: "Satoshi Soup",
ID: "4400d796-3777-4964-8536-22a4ae439ed3",
},
"6e40f882-ef77-4a5d-b5ad-18525d3df023": PartnerInfo{
}, {
Name: "Sirin Labs",
ID: "6e40f882-ef77-4a5d-b5ad-18525d3df023",
},
"b6114126-c06d-49f9-8d23-3e0dd2e350ab": PartnerInfo{
}, {
Name: "Status Messenger",
ID: "b6114126-c06d-49f9-8d23-3e0dd2e350ab",
},
"aeedbe32-1519-4320-b2f4-33725c65af54": PartnerInfo{
}, {
Name: "Temporal",
ID: "aeedbe32-1519-4320-b2f4-33725c65af54",
},
"7bf23e53-6393-4bd0-8bf9-53ecf0de742f": PartnerInfo{
}, {
Name: "Terminal.co",
ID: "7bf23e53-6393-4bd0-8bf9-53ecf0de742f",
},
"8cd605fa-ad00-45b6-823e-550eddc611d6": PartnerInfo{
}, {
Name: "Zenko",
ID: "8cd605fa-ad00-45b6-823e-550eddc611d6",
},
}},
}
}
// GeneratePartnerLink returns base64 encoded partner referral link
func GeneratePartnerLink(offerName string) ([]string, error) {
pID, err := GetPartnerID(offerName)
if err != nil {
return nil, errs.Wrap(err)
}
referralInfo := &referralInfo{UserID: "", PartnerID: pID}
refJSON, err := json.Marshal(referralInfo)
if err != nil {
return nil, errs.Wrap(err)
}
domains := getTardigradeDomains()
referralLinks := make([]string, len(domains))
encoded := base64.StdEncoding.EncodeToString(refJSON)
for i, url := range domains {
referralLinks[i] = path.Join(url, "ref", encoded)
}
return referralLinks, nil
}
// GetPartnerID returns partner ID based on partner name
func GetPartnerID(partnerName string) (partnerID string, err error) {
partners := LoadPartnerInfos()
for i := range partners {
if partners[i].Name == partnerName {
return partners[i].ID, nil
}
}
return "", errs.New("partner id not found")
}

View File

@ -0,0 +1,50 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package rewards_test
import (
"testing"
"github.com/stretchr/testify/require"
"storj.io/storj/internal/testcontext"
"storj.io/storj/satellite/rewards"
)
func TestStaticDB(t *testing.T) {
ctx := testcontext.New(t)
defer ctx.Cleanup()
world := rewards.PartnerInfo{
Name: "World",
ID: "WORLD0",
}
hello := rewards.PartnerInfo{
Name: "Hello",
ID: "11111111-1111-1111-1111-111111111111",
}
db, err := rewards.NewPartnersStaticDB(&rewards.PartnerList{
Partners: []rewards.PartnerInfo{world, hello},
})
require.NotNil(t, db)
require.NoError(t, err)
byID, err := db.ByID(ctx, "WORLD0")
require.NoError(t, err)
require.Equal(t, world, byID)
byName, err := db.ByName(ctx, "World")
require.NoError(t, err)
require.Equal(t, world, byName)
byUserAgent, err := db.ByUserAgent(ctx, "wOrLd")
require.NoError(t, err)
require.Equal(t, world, byUserAgent)
all, err := db.All(ctx)
require.NoError(t, err)
require.EqualValues(t, []rewards.PartnerInfo{hello, world}, all)
}

View File

@ -0,0 +1,121 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package rewards
import (
"context"
"encoding/base64"
"encoding/json"
"path"
"github.com/zeebo/errs"
"go.uber.org/zap"
)
var (
// Error is the default error class for partners package.
Error = errs.Class("partners error class")
// ErrNotExist is returned when a particular partner does not exist.
ErrNotExist = errs.Class("partner does not exist")
)
// PartnersDB allows access to partners database.
//
// architecture: Database
type PartnersDB interface {
// All returns all partners.
All(ctx context.Context) ([]PartnerInfo, error)
// ByName returns partner definitions for a given name.
ByName(ctx context.Context, name string) (PartnerInfo, error)
// ByID returns partner definition corresponding to an id.
ByID(ctx context.Context, id string) (PartnerInfo, error)
// ByUserAgent returns partner definition corresponding to an user agent string.
ByUserAgent(ctx context.Context, agent string) (PartnerInfo, error)
}
// PartnersService allows manipulating and accessing partner information.
//
// architecture: Service
type PartnersService struct {
log *zap.Logger
db PartnersDB
domains []string
}
// NewPartnersService returns a service for handling partner information.
func NewPartnersService(log *zap.Logger, db PartnersDB, domains []string) *PartnersService {
return &PartnersService{
log: log,
db: db,
domains: domains,
}
}
// GeneratePartnerLink returns base64 encoded partner referral link.
func (service *PartnersService) GeneratePartnerLink(ctx context.Context, offerName string) ([]string, error) {
partner, err := service.db.ByName(ctx, offerName)
if err != nil {
return nil, Error.Wrap(err)
}
type info struct {
UserID string
PartnerID string
}
referralInfo := &info{UserID: "", PartnerID: partner.ID}
refJSON, err := json.Marshal(referralInfo)
if err != nil {
return nil, errs.Wrap(err)
}
// TODO: why is this using base64?
encoded := base64.StdEncoding.EncodeToString(refJSON)
var links []string
for _, domain := range service.domains {
links = append(links, path.Join(domain, "ref", encoded))
}
return links, nil
}
// GetActiveOffer returns an offer that is active based on its type
func (service *PartnersService) GetActiveOffer(ctx context.Context, offers Offers, offerType OfferType, partnerID string) (offer *Offer, err error) {
if len(offers) < 1 {
return nil, NoCurrentOfferErr.New("no active offers")
}
switch offerType {
case Partner:
if partnerID == "" {
return nil, errs.New("partner ID is empty")
}
partnerInfo, err := service.db.ByID(ctx, partnerID)
if err != nil {
return nil, NoMatchPartnerIDErr.Wrap(err)
}
for i := range offers {
if offers[i].Name == partnerInfo.Name {
offer = &offers[i]
}
}
default:
if len(offers) > 1 {
return nil, errs.New("multiple active offers found")
}
offer = &offers[0]
}
return offer, nil
}
// PartnerByName looks up partner by name.
func (service *PartnersService) PartnerByName(ctx context.Context, name string) (PartnerInfo, error) {
return service.db.ByName(ctx, name)
}
// All returns all partners.
func (service *PartnersService) All(ctx context.Context) ([]PartnerInfo, error) {
return service.db.All(ctx)
}

View File

@ -0,0 +1,91 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package rewards
import (
"context"
"sort"
"github.com/zeebo/errs"
)
// PartnersStaticDB implements partner lookup based on a static definition.
type PartnersStaticDB struct {
list *PartnerList
byName map[string]PartnerInfo
byID map[string]PartnerInfo
byUserAgent map[string]PartnerInfo
}
var _ PartnersDB = (*PartnersStaticDB)(nil)
// NewPartnersStaticDB creates a new PartnersStaticDB.
func NewPartnersStaticDB(list *PartnerList) (*PartnersStaticDB, error) {
db := &PartnersStaticDB{
list: list,
byName: map[string]PartnerInfo{},
byID: map[string]PartnerInfo{},
byUserAgent: map[string]PartnerInfo{},
}
sort.Slice(list.Partners, func(i, k int) bool {
return list.Partners[i].Name < list.Partners[k].Name
})
var errg errs.Group
for _, p := range list.Partners {
if _, exists := db.byName[p.Name]; exists {
errg.Add(Error.New("name %q already exists", p.Name))
} else {
db.byName[p.Name] = p
}
if _, exists := db.byID[p.ID]; exists {
errg.Add(Error.New("id %q already exists", p.ID))
} else {
db.byID[p.ID] = p
}
useragent := CanonicalUserAgentProduct(p.UserAgent())
if _, exists := db.byUserAgent[useragent]; exists {
errg.Add(Error.New("user agent %q already exists", useragent))
} else {
db.byUserAgent[useragent] = p
}
}
return db, errg.Err()
}
// All returns all partners.
func (db *PartnersStaticDB) All(ctx context.Context) ([]PartnerInfo, error) {
return append([]PartnerInfo{}, db.list.Partners...), nil
}
// ByName returns partner definitions for a given name.
func (db *PartnersStaticDB) ByName(ctx context.Context, name string) (PartnerInfo, error) {
partner, ok := db.byName[name]
if !ok {
return PartnerInfo{}, ErrNotExist.New("%q", name)
}
return partner, nil
}
// ByID returns partner definition corresponding to an id.
func (db *PartnersStaticDB) ByID(ctx context.Context, id string) (PartnerInfo, error) {
partner, ok := db.byID[id]
if !ok {
return PartnerInfo{}, ErrNotExist.New("%q", id)
}
return partner, nil
}
// ByUserAgent returns partner definition corresponding to an user agent product string.
func (db *PartnersStaticDB) ByUserAgent(ctx context.Context, agent string) (PartnerInfo, error) {
partner, ok := db.byUserAgent[CanonicalUserAgentProduct(agent)]
if !ok {
return PartnerInfo{}, ErrNotExist.New("%q", agent)
}
return partner, nil
}

View File

@ -17,6 +17,8 @@ var (
MaxRedemptionErr = errs.Class("offer redemption has reached its capacity")
// NoCurrentOfferErr is the error class used when no current offer is set
NoCurrentOfferErr = errs.Class("no current offer")
// NoMatchPartnerIDErr is the error class used when an offer has reached its redemption capacity
NoMatchPartnerIDErr = errs.Class("partner not exist")
)
// DB holds information about offer
@ -83,13 +85,10 @@ const (
type OfferStatus int
const (
// Done is the status of an offer that is no longer in use.
Done = OfferStatus(iota)
// Default is the status of an offer when there is no active offer.
Default
// Active is the status of an offer that is currently in use.
Active
)
@ -120,35 +119,6 @@ func (o Offer) IsEmpty() bool {
return o.Name == ""
}
// GetActiveOffer returns an offer that is active based on its type
func (offers Offers) GetActiveOffer(offerType OfferType, partnerID string) (offer *Offer, err error) {
if len(offers) < 1 {
return nil, NoCurrentOfferErr.New("no active offers")
}
switch offerType {
case Partner:
if partnerID == "" {
return nil, errs.New("partner ID is empty")
}
partnerInfo, ok := LoadPartnerInfos()[partnerID]
if !ok {
return nil, NoMatchPartnerIDErr.New("no partnerInfo found")
}
for i := range offers {
if offers[i].Name == partnerInfo.Name {
offer = &offers[i]
}
}
default:
if len(offers) > 1 {
return nil, errs.New("multiple active offers found")
}
offer = &offers[0]
}
return offer, nil
}
// IsDefault checks if a offer's status is default
func (status OfferStatus) IsDefault() bool {
return status == Default

View File

@ -71,10 +71,11 @@ func TestOffer_Database(t *testing.T) {
require.NoError(t, err)
var pID string
if new.Type == rewards.Partner {
pID, err = rewards.GetPartnerID(new.Name)
partner, err := planet.Satellites[0].API.Marketing.PartnersService.PartnerByName(ctx, new.Name)
require.NoError(t, err)
pID = partner.ID
}
c, err := offers.GetActiveOffer(new.Type, pID)
c, err := planet.Satellites[0].API.Marketing.PartnersService.GetActiveOffer(ctx, offers, new.Type, pID)
require.NoError(t, err)
require.Equal(t, new, c)