storj/docs/blueprints/archive/marketing-offer-program.md
Yingrong Zhao d29946d3d7
docs/blueprints: referral manager v1 (#3038)
* archive old referral offer doc

* init referral manager

* add Referral Manager CLI definition

* add draft implementation steps

* use existing registration_token table for storing invitation tokens

* fix numbering

* add open question about auth

* wip pseudocode

* add user interface design

* separate token generation step from getting users step

* change button to be a referral tab on the UI

* simplify some sentences. clarified CLI referral manager interaction

* add prevent two processes running at the same time

* add cli code

* clear users when CLI exites earlier or referral manager finishes sending tokens to satellites

* use transaction for saving tokens on satellite so we can roll back if error occurs

* add auth token and GetSatelliteURLs

* add schema for satellites table

* update to only have satelite to talk to referral manager

* add definition for eligible users and owner's satellite

* fix indentation

* add saving user info during redeem pseudocode

* wip

* address reviews

* add unredeemed token in users table

* address comments
2019-10-07 09:38:05 -04:00

13 KiB
Raw Permalink Blame History

Marketing Offer Program Design Document

Abstract

Build trust with new users by allowing them to try out the network. Increase viral growth of our platform through word-of-mouth marketing. Discover the best client referral offer type during our beta release by testing the performance of different client offering types.

Free Tier Credit: A free trial for all users who create an account on the Tardigrade network without using a Referral Offer link.

Referral Offer Credit: An incentive program for motivating existing clients to refer their peers to set up paid accounts on the Tardigrade network. Existing clients give their UUID Referral Link to their friends for their friends to create an account and receive an Invite Offer Credit that is greater than the default Free Tier Credit.

Background

  • When a user wants to issue a referral link: An existing client who has an account on one of the Tardigrade Satellites is able to retrieve their unique referral link from their dashboard. They can issue their referral link to their friends in the following ways:

    • Copy the static link associated to their account with the default invite message from their dashboard
    • Twitter Share
  • When a referral link is accepted: The invitee can accept their invite to redeem their Referral Offer by going through the Referral Link to create an account on the Satellite they were invited to/through. Upon creating their account they must enter their payment information into the Stripe integration and accept/acknowledge that: If they use up their referral credit before the expiration date they should expect a bill for any amount of activity they acquire beyond what was credited to them When their offer period expires they should expect a bill for any activity on the Tardigrade network going forward billed monthly at unit per hour

AWARDED CREDIT

  • When free credit is awarded: It is displayed as the USD value that is equivalent to the storage & bandwidth allotted amount for the credit The credit is automatically applied to the account and will have a max limit that can be reached within a set timeframe, as well as an expiration date for it to be used by. The user will be charged for activity that is beyond the Free Credit limit within the set timeframe The user will be charged for activity that is after the expiration date. Example statement to the user: “You will not be charged until all of your $XX credit is used or it expires. Your $XX credit will be applied immediately” A user's free credit can only be applied to projects that they create

    A client can see their awarded Free Credits in their dashboard with associated expiration date A client who has been awarded storage credit for their referrals is able to retain the awarded credits when the referral offer they received the credit under has been updated/changed

CATEGORIES OF FREE CREDIT & OFFER TYPES

Default Free Tier Offer:

  • This is the credit that all new users who join the network receive if they create an account without a Referral Link. Once a new user successfully creates their account they will have 14 days to use their Free Credit allowance.
    The Default Offer will remain constant, with no expiration date to the offer and does not have a redemption cap The Offer Program Admin is able to adjust the default offer One Default Offer per Tardigrade Satellite. When there is a lapse of the Referral Testing Offer/the Referral Testing Offer has expired or reached a cap, the Default Offer will be the default to the Referral Testing Offer that new users will redeem until a new Referral Testing Offer is generated.

Referral Testing Offer:

  • This is the credit awarded to the Invitee upon creating their account using a Referral Link. This credit can have a different expiration date than the awarded Referrer Credit and the Default Free Credit. An admin of the Referral Program is able to easily adjust the current offer through the Admin Satellite GUI

Referrer Award Credit:

  • This is the credit awarded to the Referrer once the Invitee has created a paid account and has successfully paid their second invoice. This credit can have a different expiration date than the awarded Invitee Credit and the Default Free Credit.

Offer Types Table

Design

Database

offer table

    id - int
    name -  text
    description - text  
    award_credits_in_cents - integer
    invitee_credits_in_cents - integer
    redeemable_cap - integer
    num_redeemed - integer
    created_at - timestamp
    expires_at - timestamp
    award_credit_duration_days - int
    invitee_credit_duration_days - int
    // ACTIVE=1, DEFAULT=2, DONE=0
    status - int
    // FreeCredit=0, Referral=1
    type - int
    PRIMARY KEY (id)

user_credit table

    id - int
    user_id - bytea
    offer_id - int
    credits_earned_in_cents - int
    credits_used_in_cents - int
    // AWARD=1, INVITEE=2, NO_TYPE=0
    credit_type - int
    expires_at - timestamp
    created_at - timestamp
    referred_by - bytea (nullable)
    FOREIGN KEY (offer_id)
    FOREIGN KEY (referred_by)
    FOREIGN KEY (user_id)
    PRIMARY KEY (id)

user table

    total_Referred - int

Marketing Service

satellite/marketing/service.go

func (m *marketing) GetCurrentOffer(ctx context.Context, offerType OfferType) (*Offer, error) {
  offer, err := m.db.Marketing().Offers().GetOfferByType(ctx, offerType)
  if err != nil {
    return nil, Error.Wrap(err)
  }

  return offer, nil
}

func (m *marketing) StopOffer(ctx context.Context, offerId Offer.ID) error {
  o := UpdateOffer{
    Status: Done,
    ExpiresAt: time.Now(),
  }
  err := m.db.Marketing().Offers().UpdateOfferByID(ctx, offerId, o)
  if err != nil {
    return Error.Wrap(err)
  }

  return nil
}

func (m *marketing) Create(ctx context.Context, offer Offer) error {
  if offer.Status == Default {
    offer.ExpiresAt = time.Now().AddDate(100, 0, 0)
  }

  err := m.db.Marketing().Offers().CreateOffer(ctx, offer)
  if err != nil {
    return Error.Wrap(err)
  }

  return nil
}

func (m *marketing) ListAllOffers(ctx context.Context) ([]Offers, error)

satellite/marketing/offers.go

type Offers interface {
  ListAllOffers(ctx context.Context) ([]Offer, error)
  GetCurrentOffer(ctx context.Context, offerId Offer.ID) (Offer, error)
  Create(ctx context.Context, offer *Offer)
}

type OfferStatus int
const (
  Done OfferStatus = 0
  Active OfferStatus = 1
  Default OfferStatus = 2
)

type OfferType int
const (
    FreeCredit = 0
    Referral = 1
)

type UpdateOffer struct {
  NumRedeemed int 
  Status OfferStatus
  ExpiresAt time.time
}

satellite/satellitedb/offers.go

func (o *offersDB) Create(ctx context.Context, offer *marketing.Offer) error {
  tx, err := o.db.Open(ctx)
  if err != nil {
    return marketing.OfferError.Wrap(err)
  }

  _, err := tx.Get_Offer_By_Status(ctx, dbx.Offer_Status(offer.Status))
  if err == sql.ErrNoRows {
    _, err := o.db.Create_Offer(ctx, offerDbx)
    if err != nil {
      return Error.Wrap(errs.Combine(err, tx.Rollback()))
    }

    return nil
  }

  if err != nil {
    return Error.Wrap(errs.Combine(err, tx.Rollback()))
  }

  updateOffer := marketing.UpdateOffer{
    Status: Done,
    ExpiresAt: time.Now(),
  }
  _, err := tx.Update_Offer_By_Status(ctx, dbx.Offer_Status(offer.Status), updateOffer)
  if err != nil {
    return Error.Wrap(err)
  }

  _, err := o.db.Create_Offer(ctx, offerDbx)
  if err != nil {
    return Error.Wrap(errs.Combine(err, tx.Rollback()))
  }

  return nil
}

satellite/console/credit.go

  • Create a user_credit interface to interact with the user_credit table
  • Credits will be stored in cents as its unit.
type Credit interface {
  AvailableCredits(ctx context.Context, userId uuid.UUID) (int, error)
  ListByCreditType(ctx context.Context, userId uuid.UUID, creditType Credit.Type) ([]Credit, error)
  Update(ctx context.Context, credit *Credit) (*Credit, error)
  Create(ctx context.Context, credit *Credit) (*Credit, error)
}

satellite/console/database.go

  • New method for retrieving user credit data from user_credit table
type DB interface {
	// Users is a getter for Users repository
  Users() Users
  // Credits is a getter for Credits repository
  Credits() Credits
	// Projects is a getter for Projects repository
	Projects() Projects
	// ProjectMembers is a getter for ProjectMembers repository
	ProjectMembers() ProjectMembers
	// APIKeys is a getter for APIKeys repository
	APIKeys() APIKeys
	// BucketUsage is a getter for accounting.BucketUsage repository
	BucketUsage() accounting.BucketUsage
	// RegistrationTokens is a getter for RegistrationTokens repository
	RegistrationTokens() RegistrationTokens
	// UsageRollups is a getter for UsageRollups repository
	UsageRollups() UsageRollups

	// BeginTransaction is a method for opening transaction
  BeginTx(ctx context.Context) (DBTx, error)
}

satellite/marketing/marketingweb/server.go

  • Open a new private port on the satellite for admin users to manage referral offer configuration and other marketing configuration for various programs on our satellites
  • For right now, we will rely on our VPN to restrict access to the admin GUI. Only people who are on our VPN will have access to this page.
// NewServer creates a new instance of marketingweb server
Func NewServer(logger *zap.Logger, config Config, service *marketing.Service, listener net.Listener) *Server {}

// Register marketingweb server onto satellite
peer.Offer.Endpoint = marketingweb.NewServer(logger, config, service, listener)

The referral link url will be a static url that contains userid as the unique identifier Exp: https://mars.tardigrade.io/register?uuid=<userid>

Segment.io service

pkg/analytics/analytics.go

  • For email service, we will design several event triggers in customer.io using their event triggered campaign. We will be using analytics-go package for back-end server and analytics.js for Satellite GUI from segment.io to send our trigger event to customer.io We will create a new package in storj/pkg for analytics that will check DNT first before sending data to customer.io
  • We will add a new configuration for storing segment tracking Id specifically for tardigrade branded satellite
client := analytics.New()

How to send an event from satellite to customer.io?

client.Enqueue(analytics.Track{
  UserId: "f4ca124298",
  Event:  "sign-ups",
  Properties: analytics.NewProperties().Set("referred", "true"),
})

How to send an event from Satellite GUI to customer.io?

analytics.track('Signed Up', {
  referred_by: brandonisawesome
});

List of trigger events:

  • Send_referral event - triggered from the Satellite GUI
  • Referral_redeemed event - triggered from the consoleweb server. Reason: activation success page is served as a static page, therefore we need to send this event from the back-end after user activated their account

How to send notification for a specific use based on their state?

By utilizing customer.ios attribute feature, we can update users state through each step in our referral program and deliver appropriate content to them.

How to update users state in customer.io?

analytics.identify("97980cfea0067", {
  name: "Peter Gibbons",
  email: "peter@mail.test",
  low_credits: true,
});

Then,we will be using Customer.ios segment triggered campaign to send out notifications.

How does email get sent out when admin starts a new offer? Due to the limitation of the customer.io API, we cant create a new campaign through an API call. An Admin needs to go to customer.io web interface to create a new campaign manually.

Front End

Satellite GUI

src/store/modules/userReferralStats.ts

  • A store for managing users referral stats from the backend

src/components/referral/.ts*

  • Based on mockups, we will create components to display users referral stats

src/components/account/AccountArea.vue

  • Add display for users referral link and copy to clipboard button using vue-clipboard2

github.com/storj/tardigrade-satellite-theme

  • Add .env file that stores segment tracking id for each satellite

Admin GUI

we will be using go template for the UI /web/admin/offer/home.html

Rationale

As if the current user_credit table design, we will have a new entry each time when a user earns a credit. The reason why we designed this way is due to the starting date for the expiration date of credits. Each credit will be expired at a different time based on the duration we set for a particular offer and the date the credit is awarded to a user.

We will check against the credit duration interval when inserting a new row into the user_credit table and update the expires_at for each entry accordingly.

Open issues (if applicable)