Skip to content

Commit

Permalink
Merge pull request #6 from rarimo/feat/infinity-referrals
Browse files Browse the repository at this point in the history
Add infinity referrals logic. DevOps can specified if on specific lvl…
  • Loading branch information
Zaptoss authored Jul 8, 2024
2 parents 948a60d + 27113db commit 48abd9a
Show file tree
Hide file tree
Showing 16 changed files with 186 additions and 50 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ docker-compose.yaml
docs/node_modules
docs/web_deploy
vendor/
configs
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,23 @@ Body:
```json
{
"nullifier": "0x0000000000000000000000000000000000000000000000000000000000000000",
"count": 2
"count": 2,
"infinity": true
}
```
Response:
```json
{
"referral": "kPRQYQUcWzW",
"usage_left": 2
"usage_left": 2,
"infinity": true
}
```

Parameters:
- `nullifier` - nullifier to create or edit referrals for
- `count` - number of referral usage
- `infinity` - specify if referrals code have unlimited usage count

### Local build

Expand Down
3 changes: 3 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ levels:
- lvl: 1
threshold: 0
referrals: 1
- lvl: 2
threshold: 10
infinity: true

auth:
addr: http://rarime-auth
Expand Down
9 changes: 5 additions & 4 deletions docs/spec/components/schemas/Balance.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,11 @@ allOf:
format: int
description: Rank of the user in the full leaderboard. Returned only for the single user.
example: 294
referral_code:
type: string
description: User referral code. Returned only for the single user.
example: "6xM70VgX4eh"
referral_codes:
type: array
description: Referral codes. Returned only for the single user.
items:
$ref: '#/components/schemas/ReferralCode'
level:
type: integer
format: int
Expand Down
28 changes: 28 additions & 0 deletions docs/spec/components/schemas/ReferralCode.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
type: object
required:
- id
- status
properties:
id:
type: string
description: Referral code itself, unique identifier
example: "bDSCcQB8Hhk"
status:
type: string
description: |
Status of the code, belonging to this user (referrer):
1. infinity: the code have unlimited usage count and user can get points for each user who scanned passport
2. active: the code is not used yet by another user (referee)
3. awaiting: the code is used by referee who has scanned passport, but the referrer hasn't yet
4. rewarded: the code is used, both referee and referrer have scanned passports
5. consumed: the code is used by referee who has not scanned passport yet
The list is sorted by priority. E.g. if the referee has scanned passport,
but referrer not, the status would be `consumed`. If both not scann passport yet
status would be `awaiting`.
enum:
- infinity
- active
- awaiting
- rewarded
- consumed
3 changes: 2 additions & 1 deletion internal/assets/migrations/001_initial.sql
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ CREATE TABLE IF NOT EXISTS referrals
(
id text PRIMARY KEY,
nullifier TEXT NOT NULL REFERENCES balances (nullifier),
usage_left INTEGER NOT NULL DEFAULT 1
usage_left INTEGER NOT NULL DEFAULT 1,
infinity BOOLEAN NOT NULL DEFAULT FALSE
);

CREATE INDEX IF NOT EXISTS referrals_nullifier_index ON referrals (nullifier);
Expand Down
10 changes: 7 additions & 3 deletions internal/config/levels.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import (
)

type Level struct {
Level int `fig:"lvl,required"`
Threshold int `fig:"threshold,required"`
Referrals int `fig:"referrals,required"`
Level int `fig:"lvl,required"`
Threshold int `fig:"threshold,required"`
Referrals int `fig:"referrals"`
Infinity bool `fig:"infinity"`
}

type Levels map[int]Level
Expand Down Expand Up @@ -63,6 +64,9 @@ func (l Levels) LvlUp(currentLevel int, totalAmount int64) (refCoundToAdd int, n
}

newLevel = slices.Max(lvls)
if l[newLevel].Infinity {
refCoundToAdd = -1
}
return
}

Expand Down
49 changes: 45 additions & 4 deletions internal/data/pg/referrals.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ func (q *referrals) Insert(referrals ...data.Referral) error {
return nil
}

stmt := squirrel.Insert(referralsTable).Columns("id", "nullifier", "usage_left")
stmt := squirrel.Insert(referralsTable).Columns("id", "nullifier", "usage_left", "infinity")
for _, ref := range referrals {
stmt = stmt.Values(ref.ID, ref.Nullifier, ref.UsageLeft)
stmt = stmt.Values(ref.ID, ref.Nullifier, ref.UsageLeft, ref.Infinity)
}

if err := q.db.Exec(stmt); err != nil {
Expand All @@ -51,10 +51,13 @@ func (q *referrals) Insert(referrals ...data.Referral) error {
return nil
}

func (q *referrals) Update(usageLeft int) (*data.Referral, error) {
func (q *referrals) Update(usageLeft int, infinity bool) (*data.Referral, error) {
var res data.Referral

if err := q.db.Get(&res, q.updater.Set("usage_left", usageLeft).Suffix("RETURNING *")); err != nil {
if err := q.db.Get(&res, q.updater.SetMap(map[string]interface{}{
"usage_left": usageLeft,
"infinity": infinity,
}).Suffix("RETURNING *")); err != nil {
return nil, fmt.Errorf("update referral: %w", err)
}

Expand Down Expand Up @@ -96,10 +99,48 @@ func (q *referrals) Count() (uint64, error) {
return res.Count, nil
}

func (q *referrals) WithStatus() data.ReferralsQ {
var (
joinReferrer = fmt.Sprintf("JOIN %s rr ON %s.nullifier = rr.nullifier", balancesTable, referralsTable)
joinReferee = fmt.Sprintf("LEFT JOIN %s re ON %s.id = re.referred_by", balancesTable, referralsTable)

status = fmt.Sprintf(`CASE
WHEN infinity = TRUE THEN '%s'
WHEN usage_left > 0 THEN '%s'
WHEN rr.is_verified = FALSE AND re.is_verified = TRUE THEN '%s'
WHEN rr.is_verified = TRUE AND re.is_verified = TRUE THEN '%s'
ELSE '%s'
END AS status`,
data.StatusInfinity, data.StatusActive, data.StatusAwaiting,
data.StatusRewarded, data.StatusConsumed,
)
)

q.selector = q.selector.Column(status).
JoinClause(joinReferrer).
JoinClause(joinReferee)

return q
}

func (q *referrals) Consume(id string) error {
stmt := q.consumer.Where(squirrel.Eq{"id": id})

if err := q.db.Exec(stmt); err != nil {
return fmt.Errorf("consume referral [%v]: %w", id, err)
}

return nil
}

func (q *referrals) FilterByNullifier(nullifier string) data.ReferralsQ {
return q.applyCondition(squirrel.Eq{fmt.Sprintf("%s.nullifier", referralsTable): nullifier})
}

func (q *referrals) FilterInactive() data.ReferralsQ {
return q.applyCondition(squirrel.Or{squirrel.Gt{fmt.Sprintf("%s.usage_left", referralsTable): 0}, squirrel.Eq{fmt.Sprintf("%s.infinity", referralsTable): true}})
}

func (q *referrals) applyCondition(cond squirrel.Sqlizer) data.ReferralsQ {
q.selector = q.selector.Where(cond)
q.consumer = q.consumer.Where(cond)
Expand Down
15 changes: 14 additions & 1 deletion internal/data/referrals.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
package data

const (
StatusInfinity = "infinity"
StatusActive = "active"
StatusAwaiting = "awaiting"
StatusRewarded = "rewarded"
StatusConsumed = "consumed"
)

type Referral struct {
ID string `db:"id"`
Nullifier string `db:"nullifier"`
UsageLeft int32 `db:"usage_left"`
Infinity bool `db:"infinity"`
Status string `db:"status"`
}

type ReferralsQ interface {
Expand All @@ -13,8 +23,11 @@ type ReferralsQ interface {
Select() ([]Referral, error)
Get(id string) (*Referral, error)
Count() (uint64, error)
Consume(id string) error

Update(usageLeft int) (*Referral, error)
WithStatus() ReferralsQ
Update(usageLeft int, infinity bool) (*Referral, error)

FilterByNullifier(string) ReferralsQ
FilterInactive() ReferralsQ
}
29 changes: 20 additions & 9 deletions internal/service/handlers/claim_event.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/rarimo/geo-points-svc/internal/data"
"github.com/rarimo/geo-points-svc/internal/data/evtypes"
"github.com/rarimo/geo-points-svc/internal/data/pg"
"github.com/rarimo/geo-points-svc/internal/service/referralid"
"github.com/rarimo/geo-points-svc/internal/service/requests"
"github.com/rarimo/geo-points-svc/resources"
"gitlab.com/distributed_lab/ape"
Expand Down Expand Up @@ -141,16 +142,26 @@ func DoClaimEventUpdates(

func doLvlUpAndReferralsUpdate(levels config.Levels, referralsQ data.ReferralsQ, balance data.Balance, reward int64) (level int, err error) {
refsCount, level := levels.LvlUp(balance.Level, reward+balance.Amount)
if refsCount > 0 {
count, err := referralsQ.New().FilterByNullifier(balance.Nullifier).Count()
if err != nil {
return 0, fmt.Errorf("failed to get referral count: %w", err)
}
// we need +2 because refsCount can be -1
referrals := make([]data.Referral, 0, refsCount+2)

refToAdd := prepareReferralsToAdd(balance.Nullifier, uint64(refsCount), count)
if err = referralsQ.New().Insert(refToAdd...); err != nil {
return 0, fmt.Errorf("failed to insert referrals: %w", err)
}
// count used to calculate ref code
count, err := referralsQ.New().FilterByNullifier(balance.Nullifier).Count()
if err != nil {
return 0, fmt.Errorf("failed to get referral count: %w", err)
}
switch {
case refsCount > 0:
referrals = append(referrals, prepareReferralsToAdd(balance.Nullifier, uint64(refsCount), count)...)
case refsCount == -1:
referrals = append(referrals, data.Referral{
ID: referralid.New(balance.Nullifier, count),
Nullifier: balance.Nullifier,
Infinity: true,
})
}
if err = referralsQ.New().Insert(referrals...); err != nil {
return 0, fmt.Errorf("failed to insert referrals: %w", err)
}

return level, nil
Expand Down
24 changes: 15 additions & 9 deletions internal/service/handlers/create_balance.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func CreateBalance(w http.ResponseWriter, r *http.Request) {
return
}

referral, err := ReferralsQ(r).Get(req.Data.Attributes.ReferredBy) // infinite referrals allowed
referral, err := ReferralsQ(r).FilterInactive().Get(req.Data.Attributes.ReferredBy)
if err != nil {
Log(r).WithError(err).Error("Failed to get referral by ID")
ape.RenderErr(w, problems.InternalError())
Expand Down Expand Up @@ -76,19 +76,17 @@ func CreateBalance(w http.ResponseWriter, r *http.Request) {
return
}

referrals, err := ReferralsQ(r).FilterByNullifier(nullifier).Select()
referrals, err := ReferralsQ(r).
FilterByNullifier(nullifier).
WithStatus().
Select()
if err != nil {
Log(r).WithError(err).Error("Failed to get referral code by nullifier")
ape.RenderErr(w, problems.InternalError())
return
}
if len(referrals) != 1 {
Log(r).WithError(err).Error("There must be only 1 referral code")
Log(r).WithError(err).Error("Failed to get referrals by nullifier with rewarding field")
ape.RenderErr(w, problems.InternalError())
return
}

ape.Render(w, newBalanceResponse(*balance, &referrals[0]))
ape.Render(w, newBalanceResponse(*balance, referrals))
}

func prepareEventsWithRef(nullifier, refBy string, isGenesisRef bool, r *http.Request) []data.Event {
Expand Down Expand Up @@ -161,6 +159,14 @@ func createBalanceWithEventsAndReferrals(nullifier string, refBy *string, events
return fmt.Errorf("update balance amount and level: %w", err)
}

if refBy == nil {
return nil
}

if err = ReferralsQ(r).Consume(*refBy); err != nil {
return fmt.Errorf("failed to consume referral")
}

return nil
})
}
8 changes: 6 additions & 2 deletions internal/service/handlers/edit_referrals.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ func EditReferrals(w http.ResponseWriter, r *http.Request) {
ID: code,
Nullifier: req.Nullifier,
UsageLeft: int32(req.Count),
Infinity: req.Infinity,
})
if err != nil {
return fmt.Errorf("failed to insert referral for nullifier [%s]: %w", req.Nullifier, err)
Expand All @@ -61,7 +62,8 @@ func EditReferrals(w http.ResponseWriter, r *http.Request) {
ape.Render(w, struct {
Ref string `json:"referral"`
UsageLeft uint64 `json:"usage_left"`
}{code, req.Count})
Infinity bool `json:"infinity"`
}{code, req.Count, req.Infinity})
return
}

Expand All @@ -82,7 +84,7 @@ func EditReferrals(w http.ResponseWriter, r *http.Request) {
return
}

referral, err := ReferralsQ(r).FilterByNullifier(req.Nullifier).Update(int(req.Count))
referral, err := ReferralsQ(r).FilterByNullifier(req.Nullifier).Update(int(req.Count), req.Infinity)
if err != nil {
Log(r).WithError(err).Errorf("failed to update referral usage count for nullifier [%s]", req.Nullifier)
ape.RenderErr(w, problems.InternalError())
Expand All @@ -97,9 +99,11 @@ func EditReferrals(w http.ResponseWriter, r *http.Request) {
ape.Render(w, struct {
Ref string `json:"referral"`
UsageLeft uint64 `json:"usage_left"`
Infinity bool `json:"infinity"`
}{
referral.ID,
uint64(referral.UsageLeft),
req.Infinity,
})

}
Expand Down
Loading

0 comments on commit 48abd9a

Please sign in to comment.