From 14feaf9e011141b2f23a4642e8b5fcc60132eb00 Mon Sep 17 00:00:00 2001 From: Zaptoss Date: Thu, 23 May 2024 20:45:34 +0300 Subject: [PATCH] Add levels to balances. Level specify which possibilities user have --- config.yaml | 15 +++++++ docs/spec/components/schemas/Balance.yaml | 15 +++---- internal/assets/migrations/001_initial.sql | 1 + internal/config/levels.go | 38 ++++++++++++++++ internal/config/main.go | 2 + internal/data/balances.go | 2 + internal/data/pg/balances.go | 11 +++++ internal/service/handlers/claim_event.go | 52 +++++++++++++++++----- internal/service/handlers/ctx.go | 11 +++++ internal/service/handlers/get_balance.go | 1 + internal/service/router.go | 1 + resources/model_balance_attributes.go | 6 +-- 12 files changed, 130 insertions(+), 25 deletions(-) create mode 100644 internal/config/levels.go diff --git a/config.yaml b/config.yaml index b2f1da2..2451a85 100644 --- a/config.yaml +++ b/config.yaml @@ -102,6 +102,21 @@ event_types: 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 diff --git a/docs/spec/components/schemas/Balance.yaml b/docs/spec/components/schemas/Balance.yaml index a703b45..26ceefb 100644 --- a/docs/spec/components/schemas/Balance.yaml +++ b/docs/spec/components/schemas/Balance.yaml @@ -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: | @@ -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 how many possibilities the user has + example: 2 diff --git a/internal/assets/migrations/001_initial.sql b/internal/assets/migrations/001_initial.sql index 49ad7b6..28ed895 100644 --- a/internal/assets/migrations/001_initial.sql +++ b/internal/assets/migrations/001_initial.sql @@ -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 DEFAULT 1 ); CREATE INDEX IF NOT EXISTS balances_page_index ON balances (amount, updated_at) WHERE referred_by IS NOT NULL; diff --git a/internal/config/levels.go b/internal/config/levels.go new file mode 100644 index 0000000..4d2b04e --- /dev/null +++ b/internal/config/levels.go @@ -0,0 +1,38 @@ +package config + +import ( + "gitlab.com/distributed_lab/figure/v3" + "gitlab.com/distributed_lab/kit/kv" + "gitlab.com/distributed_lab/logan/v3/errors" +) + +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) Leveler() Levels { + return c.leveler.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(errors.Wrap(err, "failed to figure out levels config")) + } + + res := make(Levels) + for _, v := range cfg.Lvls { + res[v.Level] = v + } + + return res + }).(Levels) +} diff --git a/internal/config/main.go b/internal/config/main.go index 85b3678..3790553 100644 --- a/internal/config/main.go +++ b/internal/config/main.go @@ -20,6 +20,7 @@ type Config interface { evtypes.EventTypeser sbtcheck.SbtChecker + Leveler() Levels Verifier() *zk.Verifier PointPrice() PointsPrice } @@ -33,6 +34,7 @@ type config struct { evtypes.EventTypeser sbtcheck.SbtChecker + leveler comfig.Once verifier comfig.Once pointPrice comfig.Once getter kv.Getter diff --git a/internal/data/balances.go b/internal/data/balances.go index a6babef..8c619aa 100644 --- a/internal/data/balances.go +++ b/internal/data/balances.go @@ -13,6 +13,7 @@ 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 { @@ -20,6 +21,7 @@ type BalancesQ interface { Insert(Balance) error UpdateAmountBy(points int64) error SetReferredBy(referralCode string) error + SetLevel(level int) error Page(*pgdb.OffsetPageParams) BalancesQ Select() ([]Balance, error) diff --git a/internal/data/pg/balances.go b/internal/data/pg/balances.go index d8c699f..600f71e 100644 --- a/internal/data/pg/balances.go +++ b/internal/data/pg/balances.go @@ -65,6 +65,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 diff --git a/internal/service/handlers/claim_event.go b/internal/service/handlers/claim_event.go index c11dac0..86b6001 100644 --- a/internal/service/handlers/claim_event.go +++ b/internal/service/handlers/claim_event.go @@ -3,14 +3,15 @@ package handlers import ( "fmt" "net/http" + "slices" "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" "gitlab.com/distributed_lab/ape/problems" + "gitlab.com/distributed_lab/logan/v3/errors" ) func ClaimEvent(w http.ResponseWriter, r *http.Request) { @@ -48,15 +49,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 { @@ -70,7 +62,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) @@ -90,8 +82,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 := levelsToUp(r, balance.Level, reward+balance.Amount) + if level != 0 { + count, err := ReferralsQ(r).FilterByNullifier(event.Nullifier).Count() + if err != nil { + return errors.Wrap(err, "failed to get referral count") + } + + refToAdd := prepareReferralsToAdd(event.Nullifier, uint64(refsCount), count) + if err = ReferralsQ(r).Insert(refToAdd...); err != nil { + return errors.Wrap(err, "failed to insert referrals") + } + + if err = BalancesQ(r).FilterByNullifier(event.Nullifier).SetLevel(level); err != nil { + return errors.Wrap(err, "failed to update level") + } + } + updated, err := EventsQ(r).FilterByID(event.ID).Update(data.EventClaimed, nil, &reward) if err != nil { return fmt.Errorf("update event status: %w", err) @@ -108,6 +117,25 @@ func claimEventWithPoints(event data.Event, reward int64, r *http.Request) (clai return } +// Take summary points after claim and calculate new lvls +func levelsToUp(r *http.Request, level int, points int64) (referralsToAdd int, newLevel int) { + var lvls []int + for k, v := range Levels(r) { + if k <= level { + continue + } + if int64(v.Threshold) > points { + break + } + + referralsToAdd += v.Referrals + lvls = append(lvls, k) + } + + newLevel = slices.Max(lvls) + return +} + func newClaimEventResponse( event data.Event, meta resources.EventStaticMeta, diff --git a/internal/service/handlers/ctx.go b/internal/service/handlers/ctx.go index 83f2e09..d292c67 100644 --- a/internal/service/handlers/ctx.go +++ b/internal/service/handlers/ctx.go @@ -26,6 +26,7 @@ const ( broadcasterCtxKey pointPriceCtxKey verifierCtxKey + levelsCtxKey ) func CtxLog(entry *logan.Entry) func(context.Context) context.Context { @@ -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) +} diff --git a/internal/service/handlers/get_balance.go b/internal/service/handlers/get_balance.go index e866df3..683a7b5 100644 --- a/internal/service/handlers/get_balance.go +++ b/internal/service/handlers/get_balance.go @@ -66,6 +66,7 @@ func newBalanceModel(balance data.Balance) resources.Balance { CreatedAt: balance.CreatedAt, UpdatedAt: balance.UpdatedAt, Rank: balance.Rank, + Level: balance.Level, }, } } diff --git a/internal/service/router.go b/internal/service/router.go index ae90838..376a4d4 100644 --- a/internal/service/router.go +++ b/internal/service/router.go @@ -20,6 +20,7 @@ func Run(ctx context.Context, cfg config.Config) { handlers.CtxEventTypes(cfg.EventTypes()), handlers.CtxBroadcaster(cfg.Broadcaster()), handlers.CtxPointPrice(cfg.PointPrice()), + handlers.CtxLevels(cfg.Leveler()), ), handlers.DBCloneMiddleware(cfg.DB()), ) diff --git a/resources/model_balance_attributes.go b/resources/model_balance_attributes.go index 7df05b4..6ad6b76 100644 --- a/resources/model_balance_attributes.go +++ b/resources/model_balance_attributes.go @@ -15,10 +15,8 @@ type BalanceAttributes struct { CreatedAt int32 `json:"created_at"` // Whether the user was not referred by anybody, but the balance with some events was reserved. It happens when the user fulfills some event before the balance creation. IsDisabled bool `json:"is_disabled"` - // Whether the user has scanned passport - IsVerified bool `json:"is_verified"` - // Whether the user can withdraw tokens. Returned only for the single user. - IsWithdrawalAllowed *bool `json:"is_withdrawal_allowed,omitempty"` + // The level indicates how many possibilities the user has + Level int `json:"level"` // Rank of the user in the full leaderboard. Returned only for the single user. Rank *int `json:"rank,omitempty"` // Unix timestamp of the last points accruing