Skip to content

Commit

Permalink
Merge pull request #16 from rarimo/feat/user-levels
Browse files Browse the repository at this point in the history
Feat/user levels
  • Loading branch information
Zaptoss authored May 24, 2024
2 parents bfe21e1 + 78b0b11 commit f780dc2
Show file tree
Hide file tree
Showing 18 changed files with 240 additions and 102 deletions.
78 changes: 15 additions & 63 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,89 +18,41 @@ event_types:
frequency: one-time
action_url: https://...
logo: https://...
- name: get_poh
title: Get PoH credential
reward: 50
description: Prove that you are human
short_description: Short description
frequency: one-time
expires_at: 2020-01-01T00:00:00Z
- name: free_weekly
title: Free weekly points
reward: 100
frequency: weekly
description: Get free points every week by visiting the platform and claiming your reward
short_description: Short description
- name: daily_login
title: Daily login
reward: 5
frequency: daily
description: Login every day
short_description: Short description
disabled: true
- name: be_referred
title: Referral welcome bonus
reward: 5
frequency: one-time
description: Be referred by a friend and get a welcome bonus
short_description: Short description
no_auto_open: true
- name: referral_common
title: Refer new users
reward: 25
frequency: one-time
description: Refer friends and get a reward for each friend who verifies the passport
short_description: Short description
- name: referral_specific
title: Refer user {:did}
reward: 25
frequency: unlimited
description: The user {:did} has verified the passport. Claim the reward!
short_description: Short description
no_auto_open: true
- name: planned
title: Planned event
reward: 25
frequency: unlimited
description: Event that start at specified time
short_description: Short description
starts_at: 2020-01-01T00:00:00Z
- name: generate_proof_age
title: Generate proof age
reward: 25
frequency: one-time
description: Event that become fulfilled when user create proof that prove age
short_description: Short description
- name: generate_proof_nationality
title: Generate proof nationality
reward: 50
frequency: one-time
description: Event that become fulfilled when user create proof that prove nationality
short_description: Short description
- name: verify_proof_age
title: Verify proof age
reward: 25
frequency: one-time
description: Event that become fulfilled when user verify someone else's proof age
short_description: Short description
- name: verify_proof_nationality
title: Verify proof nationality
reward: 50
frequency: one-time
description: Event that become fulfilled when user verify someone else's proof nationality
short_description: Short description
- name: verified_proof_age
title: Have proof age verified
reward: 25
frequency: one-time
description: Event that become fulfilled when another user verify you proof age (user that verify must have verified passport)
short_description: Short description
- name: verified_proof_nationality
title: Have proof nationality verified
reward: 50
frequency: one-time
description: Event that become fulfilled when another user verify you proof nationality (user that verify must have verified passport)
short_description: Short description

levels:
levels:
- lvl: 1
threshold: 0
referrals: 5
withdrawal_allowed: false
- lvl: 2
threshold: 20
referrals: 5
withdrawal_allowed: true
- lvl: 3
threshold: 40
referrals: 5
withdrawal_allowed: true

auth:
addr: http://rarime-auth
Expand Down
15 changes: 6 additions & 9 deletions docs/spec/components/schemas/Balance.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,16 @@ allOf:
type: object
required:
- amount
- is_verified
- is_disabled
- created_at
- updated_at
- level
properties:
amount:
type: integer
format: int64
description: Amount of points
example: 580
is_verified:
type: boolean
description: Whether the user has scanned passport
example: true
is_disabled:
type: boolean
description: |
Expand Down Expand Up @@ -56,7 +52,8 @@ allOf:
example: ["73k3bdYaFWM", "9csIL7dW65m"]
items:
type: string
is_withdrawal_allowed:
type: boolean
description: Whether the user can withdraw tokens. Returned only for the single user.
example: true
level:
type: integer
format: int
description: The level indicates user permissions and features
example: 2
5 changes: 5 additions & 0 deletions docs/spec/components/schemas/Withdraw.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ allOf:
required:
- amount
- address
- proof
properties:
amount:
type: integer
Expand All @@ -20,3 +21,7 @@ allOf:
type: string
description: Rarimo address to withdraw to. Can be any valid address.
example: rarimo15hcd6tv7pe8hk2re7hu0zg0aphqdm2dtjrs0ds
proof:
type: object
format: json.RawMessage
description: JSON encoded ZK passport verification proof.
1 change: 1 addition & 0 deletions internal/assets/migrations/001_initial.sql
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ CREATE TABLE IF NOT EXISTS balances
created_at integer NOT NULL default EXTRACT('EPOCH' FROM NOW()),
updated_at integer NOT NULL default EXTRACT('EPOCH' FROM NOW()),
referred_by text UNIQUE,
level INT NOT NULL
);

CREATE INDEX IF NOT EXISTS balances_page_index ON balances (amount, updated_at) WHERE referred_by IS NOT NULL;
Expand Down
77 changes: 77 additions & 0 deletions internal/config/levels.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package config

import (
"fmt"
"slices"

"gitlab.com/distributed_lab/figure/v3"
"gitlab.com/distributed_lab/kit/kv"
)

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

type Levels map[int]Level

func (c *config) Levels() Levels {
return c.levels.Do(func() interface{} {
var cfg struct {
Lvls []Level `fig:"levels,required"`
}

err := figure.Out(&cfg).
From(kv.MustGetStringMap(c.getter, "levels")).
Please()
if err != nil {
panic(fmt.Errorf("failed to figure out levels config: %w", err))
}

if len(cfg.Lvls) == 0 {
panic(fmt.Errorf("no levels provided in config: %w", err))
}

res := make(Levels, len(cfg.Lvls))
for _, v := range cfg.Lvls {
res[v.Level] = v
}

return res
}).(Levels)
}

// Calculate new lvl. New lvl always greater then current level
func (l Levels) LvlUp(currentLevel int, totalAmount int64) (refCoundToAdd int, newLevel int) {
lvls := make([]int, 0, len(l))
for k, v := range l {
if k <= currentLevel {
continue
}
if int64(v.Threshold) > totalAmount {
break
}

refCoundToAdd += v.Referrals
lvls = append(lvls, k)
}

if len(lvls) == 0 {
return 0, currentLevel
}

newLevel = slices.Max(lvls)
return
}

// slices.Min will not panic because of previous logic
func (l Levels) MinLvl() int {
lvls := make([]int, 0, len(l))
for k := range l {
lvls = append(lvls, k)
}

return slices.Min(lvls)
}
2 changes: 2 additions & 0 deletions internal/config/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type Config interface {
evtypes.EventTypeser
sbtcheck.SbtChecker

Levels() Levels
Verifier() *zk.Verifier
PointPrice() PointsPrice
}
Expand All @@ -33,6 +34,7 @@ type config struct {
evtypes.EventTypeser
sbtcheck.SbtChecker

levels comfig.Once
verifier comfig.Once
pointPrice comfig.Once
getter kv.Getter
Expand Down
2 changes: 2 additions & 0 deletions internal/data/balances.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ type Balance struct {
UpdatedAt int32 `db:"updated_at"`
ReferredBy sql.NullString `db:"referred_by"`
Rank *int `db:"rank"`
Level int `db:"level"`
}

type BalancesQ interface {
New() BalancesQ
Insert(Balance) error
UpdateAmountBy(points int64) error
SetReferredBy(referralCode string) error
SetLevel(level int) error

Page(*pgdb.OffsetPageParams) BalancesQ
Select() ([]Balance, error)
Expand Down
12 changes: 12 additions & 0 deletions internal/data/pg/balances.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func (q *balances) Insert(bal data.Balance) error {
"nullifier": bal.Nullifier,
"amount": bal.Amount,
"referred_by": bal.ReferredBy,
"level": bal.Level,
})

if err := q.db.Exec(stmt); err != nil {
Expand Down Expand Up @@ -65,6 +66,17 @@ func (q *balances) SetReferredBy(referralCode string) error {
return nil
}

func (q *balances) SetLevel(level int) error {
stmt := q.updater.
Set("level", level)

if err := q.db.Exec(stmt); err != nil {
return fmt.Errorf("set level: %w", err)
}

return nil
}

func (q *balances) Page(page *pgdb.OffsetPageParams) data.BalancesQ {
q.selector = page.ApplyTo(q.selector, "amount", "updated_at")
return q
Expand Down
31 changes: 19 additions & 12 deletions internal/service/handlers/claim_event.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (

"github.com/rarimo/decentralized-auth-svc/pkg/auth"
"github.com/rarimo/rarime-points-svc/internal/data"
"github.com/rarimo/rarime-points-svc/internal/data/evtypes"
"github.com/rarimo/rarime-points-svc/internal/service/requests"
"github.com/rarimo/rarime-points-svc/resources"
"gitlab.com/distributed_lab/ape"
Expand Down Expand Up @@ -48,15 +47,6 @@ func ClaimEvent(w http.ResponseWriter, r *http.Request) {
ape.RenderErr(w, problems.Forbidden())
return
}
if event.Type == evtypes.TypePassportScan {
if event.PointsAmount == nil {
Log(r).WithError(err).Errorf("PointsAmount can't be nil for event %s",
event.Type)
ape.RenderErr(w, problems.InternalError())
return
}
evType.Reward = *event.PointsAmount
}

balance, err := BalancesQ(r).FilterByNullifier(event.Nullifier).FilterDisabled().Get()
if err != nil {
Expand All @@ -70,7 +60,7 @@ func ClaimEvent(w http.ResponseWriter, r *http.Request) {
return
}

event, err = claimEventWithPoints(*event, evType.Reward, r)
event, err = claimEventWithPoints(r, *event, evType.Reward, balance)
if err != nil {
Log(r).WithError(err).Errorf("Failed to claim event %s and accrue %d points to the balance %s",
event.ID, evType.Reward, event.Nullifier)
Expand All @@ -90,8 +80,25 @@ func ClaimEvent(w http.ResponseWriter, r *http.Request) {
}

// requires: event exist
func claimEventWithPoints(event data.Event, reward int64, r *http.Request) (claimed *data.Event, err error) {
func claimEventWithPoints(r *http.Request, event data.Event, reward int64, balance *data.Balance) (claimed *data.Event, err error) {
err = EventsQ(r).Transaction(func() error {
refsCount, level := Levels(r).LvlUp(balance.Level, reward+balance.Amount)
if level != balance.Level {
count, err := ReferralsQ(r).FilterByNullifier(event.Nullifier).Count()
if err != nil {
return fmt.Errorf("failed to get referral count: %w", err)
}

refToAdd := prepareReferralsToAdd(event.Nullifier, uint64(refsCount), count)
if err = ReferralsQ(r).Insert(refToAdd...); err != nil {
return fmt.Errorf("failed to insert referrals: %w", err)
}

if err = BalancesQ(r).FilterByNullifier(event.Nullifier).SetLevel(level); err != nil {
return fmt.Errorf("failed to update level: %w", err)
}
}

updated, err := EventsQ(r).FilterByID(event.ID).Update(data.EventClaimed, nil, &reward)
if err != nil {
return fmt.Errorf("update event status: %w", err)
Expand Down
2 changes: 2 additions & 0 deletions internal/service/handlers/create_balance.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ func createBalanceWithEvents(nullifier, refBy string, events []data.Event, r *ht
err := BalancesQ(r).Insert(data.Balance{
Nullifier: nullifier,
ReferredBy: sql.NullString{String: refBy, Valid: refBy != ""},
Level: Levels(r).MinLvl(),
})

if err != nil {
Expand All @@ -121,6 +122,7 @@ func createBalanceWithEventsAndReferrals(nullifier, refBy string, events []data.
err := BalancesQ(r).Insert(data.Balance{
Nullifier: nullifier,
ReferredBy: sql.NullString{String: refBy, Valid: refBy != ""},
Level: Levels(r).MinLvl(),
})

if err != nil {
Expand Down
11 changes: 11 additions & 0 deletions internal/service/handlers/ctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const (
broadcasterCtxKey
pointPriceCtxKey
verifierCtxKey
levelsCtxKey
)

func CtxLog(entry *logan.Entry) func(context.Context) context.Context {
Expand Down Expand Up @@ -127,3 +128,13 @@ func CtxVerifier(verifier *zk.Verifier) func(context.Context) context.Context {
func Verifier(r *http.Request) *zk.Verifier {
return r.Context().Value(verifierCtxKey).(*zk.Verifier)
}

func CtxLevels(levels config.Levels) func(context.Context) context.Context {
return func(ctx context.Context) context.Context {
return context.WithValue(ctx, levelsCtxKey, levels)
}
}

func Levels(r *http.Request) config.Levels {
return r.Context().Value(levelsCtxKey).(config.Levels)
}
Loading

0 comments on commit f780dc2

Please sign in to comment.