Skip to content

Commit

Permalink
Merge pull request #25 from rarimo/feat/edit-referrals
Browse files Browse the repository at this point in the history
Change edit_referrals endpoint logic. Now genesis balance have only o…
  • Loading branch information
Zaptoss authored Jun 14, 2024
2 parents 125b499 + 96f23a1 commit 5ad6d35
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 97 deletions.
37 changes: 6 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ The path for internal endpoints is `/integrations/rarime-points-svc/v1/private/*

### Add referrals

Private endpoint to set number of available referral codes or create a new
_System user_ with referrals. _System user_ is unable to claim events or
Private endpoint to set usage count for genesis referral code or create a new
_System user_ with genesis referral code. _System user_ is unable to claim events or
withdraw, it has `is_disabled` attribute set to `true`, so the client app should
not allow it interactions with the system, although it is technically possible
to do other actions.
Expand All @@ -44,44 +44,19 @@ Body:
{
"nullifier": "0x0000000000000000000000000000000000000000000000000000000000000000",
"count": 2,
"genesis": true
}
```
Response variants:
Response:
```json
{
"added_ref": "kPRQYQUcWzW",
"referral": "kPRQYQUcWzW",
"usage_left": 2
}
```
or
```json
{
"added_referrals":[
"kPRQYQUcWzW",
"kPRQYQUcaaa",
"kPRQYQUcbbb",
"kPRQYQUcccc"
]
}
```

Parameters:
- `nullifier` - nullifier to create or edit referrals for
- `count` - number of referrals to set/number of referral usage for genesis
- `genesis` - specify add many referrals with one usage or one referral with many usage


Behavior:
a) User does not exist -> create a _System user_ with the specified number of
referrals (if count == 0, do not create)
b) User exists, `N` > `count` -> add `N - count` active referrals
c) User exists, `N` < `count` -> consume `count - N` active referrals (not delete!)
d) User exists, `N` = `count` -> do nothing
f) Flag `genesis = true` that mean that will be created only one referral with
`usage_count = count`. Referrals which have `usage_count <= 0` is inactive

Where `N` is the current number of active referrals for the user, `count` is
query parameter value.
- `count` - number of referral usage

### Local build

Expand Down
17 changes: 15 additions & 2 deletions internal/data/pg/referrals.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,16 @@ type referrals struct {
db *pgdb.DB
selector squirrel.SelectBuilder
updater squirrel.UpdateBuilder
consumer squirrel.UpdateBuilder
counter squirrel.SelectBuilder
}

func NewReferrals(db *pgdb.DB) data.ReferralsQ {
return &referrals{
db: db,
selector: squirrel.Select("*").From(referralsTable),
updater: squirrel.Update(referralsTable).Set("usage_left", squirrel.Expr("usage_left - 1")),
updater: squirrel.Update(referralsTable),
consumer: squirrel.Update(referralsTable).Set("usage_left", squirrel.Expr("usage_left - 1")),
counter: squirrel.Select("COUNT(*) as count").From(referralsTable),
}
}
Expand All @@ -49,6 +51,16 @@ func (q *referrals) Insert(referrals ...data.Referral) error {
return nil
}

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

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

return &res, nil
}

func (q *referrals) Consume(ids ...string) ([]string, error) {
if len(ids) == 0 {
return nil, nil
Expand All @@ -58,7 +70,7 @@ func (q *referrals) Consume(ids ...string) ([]string, error) {
IDs []string `db:"id"`
}

stmt := q.updater.Where(squirrel.Eq{"id": ids}).Suffix("Returning id")
stmt := q.consumer.Where(squirrel.Eq{"id": ids}).Suffix("Returning id")

if err := q.db.Exec(stmt); err != nil {
return nil, fmt.Errorf("consume referrals [%v]: %w", ids, err)
Expand Down Expand Up @@ -128,6 +140,7 @@ func (q *referrals) FilterConsumed() data.ReferralsQ {

func (q *referrals) applyCondition(cond squirrel.Sqlizer) data.ReferralsQ {
q.selector = q.selector.Where(cond)
q.consumer = q.consumer.Where(cond)
q.updater = q.updater.Where(cond)
q.counter = q.counter.Where(cond)
return q
Expand Down
2 changes: 2 additions & 0 deletions internal/data/referrals.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ type ReferralsQ interface {
Get(id string) (*Referral, error)
Count() (uint64, error)

Update(usageLeft int) (*Referral, error)

FilterByNullifier(string) ReferralsQ
FilterConsumed() ReferralsQ
}
100 changes: 40 additions & 60 deletions internal/service/handlers/edit_referrals.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"net/http"

validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/rarimo/rarime-points-svc/internal/data"
"github.com/rarimo/rarime-points-svc/internal/service/referralid"
"github.com/rarimo/rarime-points-svc/internal/service/requests"
Expand All @@ -26,7 +27,7 @@ func EditReferrals(w http.ResponseWriter, r *http.Request) {
}

if balance == nil {
if *req.Count == 0 {
if req.Count == 0 {
Log(r).Debugf("Balance %s not found, skipping creation for count=0", req.Nullifier)
w.WriteHeader(http.StatusNoContent)
return
Expand All @@ -37,46 +38,63 @@ func EditReferrals(w http.ResponseWriter, r *http.Request) {
ape.RenderErr(w, problems.InternalError())
return
}
}

if req.Genesis {
count, err := ReferralsQ(r).FilterByNullifier(req.Nullifier).Count()
if err != nil {
Log(r).WithError(err).Error("Failed to get referrals count")
ape.RenderErr(w, problems.InternalError())
return
}

referral := referralid.New(req.Nullifier, count)

code := referralid.New(req.Nullifier, 0)
err = ReferralsQ(r).Insert(data.Referral{
ID: referral,
ID: code,
Nullifier: req.Nullifier,
UsageLeft: int32(*req.Count),
UsageLeft: int32(req.Count),
})
if err != nil {
Log(r).WithError(err).Error("Failed to insert genesis referral")
Log(r).WithError(err).Errorf("failed to insert referral for nullifier [%s]", req.Nullifier)
ape.RenderErr(w, problems.InternalError())
return
}

ape.Render(w, struct {
Ref string `json:"added_ref"`
UsageLeft int `json:"usage_left"`
}{referral, int(*req.Count)})
Ref string `json:"referral"`
UsageLeft uint64 `json:"usage_left"`
}{code, req.Count})
return
}

if balance.ReferredBy.Valid {
ape.RenderErr(w, problems.BadRequest(validation.Errors{"balance": fmt.Errorf("genesis balances must be inactive")})...)
return
}

added, err := adjustReferralsCount(req, r)
referrals, err := ReferralsQ(r).FilterByNullifier(req.Nullifier).Select()
if err != nil {
Log(r).WithError(err).Error("Failed to adjust referrals count")
Log(r).WithError(err).Errorf("failed to select referrals for nullifier [%s]", req.Nullifier)
ape.RenderErr(w, problems.InternalError())
return
}

if len(referrals) != 1 {
ape.RenderErr(w, problems.BadRequest(validation.Errors{"balance": fmt.Errorf("genesis balances must have only one referral")})...)
return
}

referral, err := ReferralsQ(r).FilterByNullifier(req.Nullifier).Update(int(req.Count))
if err != nil {
Log(r).WithError(err).Errorf("failed to update referral usage count for nullifier [%s]", req.Nullifier)
ape.RenderErr(w, problems.InternalError())
return
}
if referral == nil {
Log(r).Errorf("critical: referral absent for user [%s]", req.Nullifier)
ape.RenderErr(w, problems.InternalError())
return
}

ape.Render(w, struct {
Refs []string `json:"added_referrals"`
}{added})
Ref string `json:"referral"`
UsageLeft uint64 `json:"usage_left"`
}{
referral.ID,
uint64(referral.UsageLeft),
})

}

func prepareReferralsToAdd(nullifier string, count, index uint64) []data.Referral {
Expand All @@ -93,41 +111,3 @@ func prepareReferralsToAdd(nullifier string, count, index uint64) []data.Referra

return refs
}

func adjustReferralsCount(req requests.EditReferralsRequest, r *http.Request) (refsAdded []string, err error) {
active, err := ReferralsQ(r).FilterByNullifier(req.Nullifier).FilterConsumed().Count()
if err != nil {
return nil, fmt.Errorf("count active referrals: %w", err)
}

if *req.Count == active {
Log(r).Infof("No referrals to add or consume for nullifier %s", req.Nullifier)
return
}

if *req.Count < active {
toConsume := active - *req.Count
if err = ReferralsQ(r).ConsumeFirst(req.Nullifier, toConsume); err != nil {
return nil, fmt.Errorf("consume referrals: %w", err)
}
Log(r).Infof("Consumed %d referrals for nullifier %s", toConsume, req.Nullifier)
return
}

index, err := ReferralsQ(r).FilterByNullifier(req.Nullifier).Count()
if err != nil {
return nil, fmt.Errorf("count all referrals: %w", err)
}

toAdd := *req.Count - active
// balance must exist, according to preceding logic in EditReferrals
err = ReferralsQ(r).Insert(prepareReferralsToAdd(req.Nullifier, toAdd, index)...)
if err != nil {
return nil, fmt.Errorf("insert referrals: %w", err)
}
Log(r).Infof("Inserted %d referrals for nullifier %s", toAdd, req.Nullifier)

// while this is deterministic, the codes will be the same
refsAdded = referralid.NewMany(req.Nullifier, toAdd, index)
return
}
7 changes: 3 additions & 4 deletions internal/service/requests/edit_referrals.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ import (
)

type EditReferralsRequest struct {
Nullifier string `json:"nullifier"`
Count *uint64 `json:"count"`
Genesis bool `json:"genesis"`
Nullifier string `json:"nullifier"`
Count uint64 `json:"count"`
}

func NewEditReferrals(r *http.Request) (req EditReferralsRequest, err error) {
Expand All @@ -24,6 +23,6 @@ func NewEditReferrals(r *http.Request) (req EditReferralsRequest, err error) {

return req, validation.Errors{
"nullifier": validation.Validate(req.Nullifier, validation.Required, validation.Match(nullifierRegexp)),
"count": validation.Validate(req.Count, validation.NotNil),
"count": validation.Validate(req.Count, validation.Required),
}.Filter()
}

0 comments on commit 5ad6d35

Please sign in to comment.