Skip to content

Commit

Permalink
Backward compability endpoints. Conversation resolving. Fix bug with …
Browse files Browse the repository at this point in the history
…claim be_referred event for user which doesn't scan passport. Now events add for user in create_balance endpoint against active_balance
  • Loading branch information
Zaptoss committed Jul 16, 2024
1 parent e662a0e commit 5dcc8c8
Show file tree
Hide file tree
Showing 10 changed files with 330 additions and 68 deletions.
35 changes: 35 additions & 0 deletions docs/spec/paths/integrations@geo-points-svc@v2@[email protected]
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
post:
tags:
- Points balance
summary: Create points balance V2
description: |
Create an empty balance inactive for authorized user who makes the request.
If balance already exists, but it is disabled (it was not referred by another user,
but has fulfilled some event), you should use PATCH balances/ endpoint as well.
operationId: createPointsBalance
requestBody:
content:
application/vnd.api+json:
schema:
type: object
required:
- data
properties:
data:
$ref: '#/components/schemas/ActivateBalanceKey'
responses:
204:
description: Created
400:
$ref: '#/components/responses/invalidParameter'
401:
$ref: '#/components/responses/invalidAuth'
409:
description: Balance already exists for provided nullifier
content:
application/vnd.api+json:
schema:
$ref: '#/components/schemas/Errors'
500:
$ref: '#/components/responses/internalError'
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
patch:
tags:
- Points balance
summary: Activate points balance
description: |
Activate points balance for authorized user who makes the request. Rank is included
in response.
This operation might be time-consuming, because `open` events should be added for
the new account synchronously (to display them right after the request).
operationId: activatePointsBalance
requestBody:
content:
application/vnd.api+json:
schema:
type: object
required:
- data
properties:
data:
$ref: '#/components/schemas/ActivateBalance'
responses:
200:
description: Created
content:
application/vnd.api+json:
schema:
type: object
required:
- data
properties:
data:
$ref: '#/components/schemas/Balance'
400:
$ref: '#/components/responses/invalidParameter'
401:
$ref: '#/components/responses/invalidAuth'
404:
$ref: '#/components/responses/notFound'
409:
description: Balance already activated
content:
application/vnd.api+json:
schema:
$ref: '#/components/schemas/Errors'
500:
$ref: '#/components/responses/internalError'
74 changes: 15 additions & 59 deletions internal/service/handlers/activate_balance.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,19 @@ func ActivateBalance(w http.ResponseWriter, r *http.Request) {
return fmt.Errorf("update balance amount and level: %w", err)
}

events := prepareEventsWithRef(nullifier, req.Data.Attributes.ReferredBy, refBalance.ReferredBy == nil, r)
Log(r).Debugf("%d events will be added for nullifier=%s", len(events), nullifier)
if err = EventsQ(r).Insert(events...); err != nil {
return fmt.Errorf("add open events: %w", err)
if refBalance.ReferredBy != nil {
err = EventsQ(r).Insert(data.Event{
Nullifier: nullifier,
Type: models.TypeBeReferred,
Status: data.EventFulfilled,
})
if err != nil {
return fmt.Errorf("failed to insert `be_referred` event: %w", err)
}
}

if err = ReferralsQ(r).Consume(referralCode); err != nil {
return fmt.Errorf("failed to consume referral")
return fmt.Errorf("failed to consume referral: %w", err)
}

evTypeRef := EventTypes(r).Get(models.TypeReferralSpecific, evtypes.FilterInactive)
Expand All @@ -96,20 +101,11 @@ func ActivateBalance(w http.ResponseWriter, r *http.Request) {
return nil
}

// Claim events for invited friends who scanned the passport.
// This is possible when the user registered in the referral
// program and invited friends, the friends scanned the passport,
// but since the user hadn't a supported passport, the event
// could not be claimed. And now that user has scanned the passport,
// it is necessary to claim events for user's friends if auto-claim
// is enabled
if err = claimReferralSpecificEvents(r, evTypeRef, balance.Nullifier); err != nil {
return fmt.Errorf("failed to claim referral specific events: %w", err)
}

// Be referred event is a welcome bonus when you created balance with non-genesis referral code
if err = claimBeReferredEvent(r, *balance); err != nil {
return fmt.Errorf("failed to claim be referred event: %w", err)
if balance.IsVerified {
// Be referred event is a welcome bonus when you created balance with non-genesis referral code
if err = claimBeReferredEvent(r, *balance); err != nil {
return fmt.Errorf("failed to claim be referred event: %w", err)
}
}

// Adds a friend event for the referrer. If the event
Expand Down Expand Up @@ -149,43 +145,3 @@ func ActivateBalance(w http.ResponseWriter, r *http.Request) {

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

func prepareEventsWithRef(nullifier, refBy string, isGenesisRef bool, r *http.Request) []data.Event {
events := EventTypes(r).PrepareEvents(nullifier, evtypes.FilterNotOpenable)
refType := EventTypes(r).Get(models.TypeBeReferred, evtypes.FilterInactive)

if refBy == "" || isGenesisRef || refType == nil {
return events
}

Log(r).WithFields(map[string]any{"nullifier": nullifier, "referred_by": refBy}).
Debug("`Be referred` event will be added for referee user")

return append(events, data.Event{
Nullifier: nullifier,
Type: models.TypeBeReferred,
Status: data.EventFulfilled,
})
}

// createBalanceWithEvents should be called in transaction to avoid database corruption
func createBalanceWithEvents(nullifier string, refBy *string, events []data.Event, r *http.Request) error {
balance := data.Balance{
Nullifier: nullifier,
ReferredBy: refBy,
Level: 0,
}

err := BalancesQ(r).Insert(balance)
if err != nil {
return fmt.Errorf("add balance: %w", err)
}

Log(r).Debugf("%d events will be added for nullifier=%s", len(events), nullifier)
if err = EventsQ(r).Insert(events...); err != nil {
return fmt.Errorf("add open events: %w", err)
}

// not consuming referral code
return nil
}
133 changes: 130 additions & 3 deletions internal/service/handlers/create_balance.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package handlers

import (
"fmt"
"net/http"

"github.com/rarimo/geo-auth-svc/pkg/auth"
"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/evtypes/models"
"github.com/rarimo/geo-points-svc/internal/service/requests"
"gitlab.com/distributed_lab/ape"
"gitlab.com/distributed_lab/ape/problems"
Expand All @@ -18,6 +21,7 @@ func CreateBalance(w http.ResponseWriter, r *http.Request) {
}

nullifier := req.Data.ID

if !auth.Authenticates(UserClaims(r), auth.UserGrant(nullifier)) {
ape.RenderErr(w, problems.Unauthorized())
return
Expand All @@ -35,11 +39,134 @@ func CreateBalance(w http.ResponseWriter, r *http.Request) {
return
}

if err = BalancesQ(r).Insert(data.Balance{Nullifier: nullifier}); err != nil {
Log(r).WithError(err).Error("Failed to insert balance")
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())
return
}
if referral == nil {
ape.RenderErr(w, problems.NotFound())
return
}

refBalance, err := BalancesQ(r).FilterByNullifier(referral.Nullifier).Get()
if err != nil || refBalance == nil { // must exist due to FK constraint
Log(r).WithError(err).Error("Failed to get referrer balance by nullifier")
ape.RenderErr(w, problems.InternalError())
return
}
isGenesisRef := refBalance.ReferredBy == nil

events := prepareEventsWithRef(nullifier, req.Data.Attributes.ReferredBy, isGenesisRef, r)
if err = createBalanceWithEventsAndReferrals(nullifier, &req.Data.Attributes.ReferredBy, events, r); err != nil {
Log(r).WithError(err).Error("Failed to create balance with events")
ape.RenderErr(w, problems.InternalError())
return
}

w.WriteHeader(http.StatusNoContent)
// We can't return inserted balance in a single query, because we can't calculate
// rank in transaction: RANK() is a window function allowed on a set of rows,
// while with RETURNING we operate a single one.
// Balance will exist cause of previous logic.
balance, err = BalancesQ(r).GetWithRank(nullifier)
if err != nil {
Log(r).WithError(err).Error("Failed to get created balance by nullifier")
ape.RenderErr(w, problems.InternalError())
return
}

referrals, err := ReferralsQ(r).
FilterByNullifier(nullifier).
WithStatus().
Select()
if err != nil {
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, 0))
}

func prepareEventsWithRef(nullifier, refBy string, isGenesisRef bool, r *http.Request) []data.Event {
events := EventTypes(r).PrepareEvents(nullifier, evtypes.FilterNotOpenable)
refType := EventTypes(r).Get(models.TypeBeReferred, evtypes.FilterInactive)

if refBy == "" || isGenesisRef || refType == nil {
return events
}

Log(r).WithFields(map[string]any{"nullifier": nullifier, "referred_by": refBy}).
Debug("`Be referred` event will be added for referee user")

return append(events, data.Event{
Nullifier: nullifier,
Type: models.TypeBeReferred,
Status: data.EventFulfilled,
})
}

// createBalanceWithEvents should be called in transaction to avoid database corruption
func createBalanceWithEvents(nullifier string, refBy *string, events []data.Event, r *http.Request) error {
balance := data.Balance{
Nullifier: nullifier,
ReferredBy: refBy,
Level: 0,
}

err := BalancesQ(r).Insert(balance)
if err != nil {
return fmt.Errorf("add balance: %w", err)
}

Log(r).Debugf("%d events will be added for nullifier=%s", len(events), nullifier)
if err = EventsQ(r).Insert(events...); err != nil {
return fmt.Errorf("add open events: %w", err)
}

// not consuming referral code
return nil
}

func createBalanceWithEventsAndReferrals(nullifier string, refBy *string, events []data.Event, r *http.Request) error {
return EventsQ(r).Transaction(func() error {
balance := data.Balance{
Nullifier: nullifier,
ReferredBy: refBy,
Level: 0,
}

err := BalancesQ(r).Insert(balance)
if err != nil {
return fmt.Errorf("add balance: %w", err)
}

Log(r).Debugf("%d events will be added for nullifier=%s", len(events), nullifier)
if err = EventsQ(r).Insert(events...); err != nil {
return fmt.Errorf("add open events: %w", err)
}

level, err := doLvlUpAndReferralsUpdate(Levels(r), ReferralsQ(r), balance, 0)
if err != nil {
return fmt.Errorf("failed to do lvlup and referrals update: %w", err)
}

err = BalancesQ(r).FilterByNullifier(balance.Nullifier).Update(map[string]any{
data.ColLevel: level,
})
if err != nil {
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
})
}
58 changes: 58 additions & 0 deletions internal/service/handlers/create_balance_v2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package handlers

import (
"fmt"
"net/http"

"github.com/rarimo/geo-auth-svc/pkg/auth"
"github.com/rarimo/geo-points-svc/internal/data"
"github.com/rarimo/geo-points-svc/internal/service/requests"
"gitlab.com/distributed_lab/ape"
"gitlab.com/distributed_lab/ape/problems"
)

func CreateBalanceV2(w http.ResponseWriter, r *http.Request) {
req, err := requests.NewCreateBalanceV2(r)
if err != nil {
ape.RenderErr(w, problems.BadRequest(err)...)
return
}

nullifier := req.Data.ID
if !auth.Authenticates(UserClaims(r), auth.UserGrant(nullifier)) {
ape.RenderErr(w, problems.Unauthorized())
return
}

balance, err := BalancesQ(r).FilterByNullifier(nullifier).Get()
if err != nil {
Log(r).WithError(err).Error("Failed to get balance by nullifier")
ape.RenderErr(w, problems.InternalError())
return
}

if balance != nil {
ape.RenderErr(w, problems.Conflict())
return
}

err = EventsQ(r).Transaction(func() error {
events := prepareEventsWithRef(nullifier, "", false, r)
Log(r).Debugf("%d events will be added for nullifier=%s", len(events), nullifier)
if err = EventsQ(r).Insert(events...); err != nil {
return fmt.Errorf("add open events: %w", err)
}

if err = BalancesQ(r).Insert(data.Balance{Nullifier: nullifier}); err != nil {
return fmt.Errorf("failed to insert balance: %w", err)
}
return nil
})
if err != nil {
Log(r).WithError(err).Error("Failed to create balance")
ape.RenderErr(w, problems.InternalError())
return
}

w.WriteHeader(http.StatusNoContent)
}
Loading

0 comments on commit 5dcc8c8

Please sign in to comment.