Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add infinity referrals logic. DevOps can specified if on specific lvl… #6

Merged
merged 3 commits into from
Jul 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading