diff --git a/internal/data/evtypes/filters.go b/internal/data/evtypes/filters.go index c96d89f..b984cdb 100644 --- a/internal/data/evtypes/filters.go +++ b/internal/data/evtypes/filters.go @@ -38,6 +38,12 @@ func FilterByFrequency(f models.Frequency) func(models.EventType) bool { } } +func FilterByAutoClaim(autoClaim bool) func(models.EventType) bool { + return func(ev models.EventType) bool { + return ev.AutoClaim != autoClaim + } +} + func FilterByNames(names ...string) func(models.EventType) bool { return func(ev models.EventType) bool { if len(names) == 0 { diff --git a/internal/service/handlers/create_balance.go b/internal/service/handlers/create_balance.go index 1699d8d..a18ae1f 100644 --- a/internal/service/handlers/create_balance.go +++ b/internal/service/handlers/create_balance.go @@ -45,13 +45,20 @@ func CreateBalance(w http.ResponseWriter, r *http.Request) { ape.RenderErr(w, problems.InternalError()) return } - if referral == nil { ape.RenderErr(w, problems.NotFound()) return } - events := prepareEventsWithRef(nullifier, req.Data.Attributes.ReferredBy, r) + 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()) @@ -84,15 +91,11 @@ func CreateBalance(w http.ResponseWriter, r *http.Request) { ape.Render(w, newBalanceResponse(*balance, &referrals[0])) } -func prepareEventsWithRef(nullifier, refBy string, r *http.Request) []data.Event { +func prepareEventsWithRef(nullifier, refBy string, isGenesisRef bool, r *http.Request) []data.Event { events := EventTypes(r).PrepareEvents(nullifier, evtypes.FilterNotOpenable) - if refBy == "" { - return events - } - refType := EventTypes(r).Get(models.TypeBeReferred, evtypes.FilterInactive) - if refType == nil { - Log(r).Debug("`Be referred` event is inactive, skipping it") + + if refBy == "" || isGenesisRef || refType == nil { return events } diff --git a/internal/service/handlers/edit_referrals.go b/internal/service/handlers/edit_referrals.go index ef63d62..458cfc9 100644 --- a/internal/service/handlers/edit_referrals.go +++ b/internal/service/handlers/edit_referrals.go @@ -35,7 +35,7 @@ func EditReferrals(w http.ResponseWriter, r *http.Request) { var code string err = EventsQ(r).Transaction(func() error { - events := prepareEventsWithRef(req.Nullifier, "", r) + events := prepareEventsWithRef(req.Nullifier, "", true, r) if err = createBalanceWithEvents(req.Nullifier, nil, events, r); err != nil { return fmt.Errorf("failed to create balance with events: %w", err) } diff --git a/internal/service/handlers/verify_passport.go b/internal/service/handlers/verify_passport.go index 00efac9..e79e0ab 100644 --- a/internal/service/handlers/verify_passport.go +++ b/internal/service/handlers/verify_passport.go @@ -233,6 +233,11 @@ func doPassportScanUpdates(r *http.Request, balance data.Balance, anonymousID st 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) + } + // Adds a friend event for the referrer. If the event // is inactive, then nothing happens. If active, the // fulfilled event is added and, if possible, the event claimed @@ -356,6 +361,42 @@ func claimReferralSpecificEvents(r *http.Request, evTypeRef *models.EventType, n return nil } +func claimBeReferredEvent(r *http.Request, balance data.Balance) error { + evTypeBeRef := EventTypes(r).Get(models.TypeBeReferred, evtypes.FilterInactive) + if evTypeBeRef == nil || !evTypeBeRef.AutoClaim { + return nil + } + + event, err := EventsQ(r).FilterByNullifier(balance.Nullifier). + FilterByType(models.TypeBeReferred). + FilterByStatus(data.EventFulfilled). + Get() + if err != nil { + return fmt.Errorf("get fulfilled be referred event: %w", err) + } + if event == nil { + Log(r).Debug("User is not eligible for be_referred event") + return nil + } + + _, err = EventsQ(r).FilterByID(event.ID).Update(data.EventClaimed, nil, &evTypeBeRef.Reward) + if err != nil { + return fmt.Errorf("update event status: %w", err) + } + + err = DoClaimEventUpdates( + Levels(r), + ReferralsQ(r), + BalancesQ(r), + balance, + evTypeBeRef.Reward) + if err != nil { + return fmt.Errorf("do claim event updates for be_referred: %w", err) + } + + return nil +} + func addEventForReferrer(r *http.Request, evTypeRef *models.EventType, balance data.Balance) error { if evTypeRef == nil { return nil diff --git a/internal/service/workers/nooneisforgotten/main.go b/internal/service/workers/nooneisforgotten/main.go index 2d55fa8..64871a4 100644 --- a/internal/service/workers/nooneisforgotten/main.go +++ b/internal/service/workers/nooneisforgotten/main.go @@ -28,7 +28,7 @@ func Run(cfg config.Config, sig chan struct{}) { } if err := pg.NewEvents(db).Transaction(func() error { - return claimReferralSpecificEvents(db, cfg.EventTypes(), cfg.Levels()) + return autoClaimEvents(db, cfg.EventTypes(), cfg.Levels()) }); err != nil { panic(fmt.Errorf("failed to claim referral specific events: %w", err)) } @@ -131,23 +131,24 @@ func updateReferralUserEvents(db *pgdb.DB, types *evtypes.Types) error { return nil } -// claimReferralSpecificEvents claim fulfilled events for invited -// friends which have passport scanned, if it possible -func claimReferralSpecificEvents(db *pgdb.DB, types *evtypes.Types, levels config.Levels) error { - evType := types.Get(models.TypeReferralSpecific, evtypes.FilterInactive) - if evType == nil || !evType.AutoClaim { +// autoClaimEvents claim fulfilled events which have auto-claim enabled. This is +// useful if some events were inactive, then became active and must be claimed +// automatically. +func autoClaimEvents(db *pgdb.DB, types *evtypes.Types, levels config.Levels) error { + claimTypes := types.Names(evtypes.FilterByAutoClaim(true)) + if len(claimTypes) == 0 { return nil } events, err := pg.NewEvents(db). - FilterByType(models.TypeReferralSpecific). + FilterByType(claimTypes...). FilterByStatus(data.EventFulfilled). Select() if err != nil { - return fmt.Errorf("failed to select passport scan events: %w", err) + return fmt.Errorf("failed to select fulfilled events: %w", err) } - // we need to have maps which link nullifiers to events slice + // nullifiers var is used only for selection, so we don't care about duplicates nullifiers := make([]string, 0, len(events)) for _, event := range events { nullifiers = append(nullifiers, event.Nullifier) @@ -164,24 +165,38 @@ func claimReferralSpecificEvents(db *pgdb.DB, types *evtypes.Types, levels confi return errors.New("critical: events present, but no balances with nullifier") } - toClaim := make([]string, 0, len(events)) - // select events to claim only for verified balances + // select events to claim only for verified balances, group by type name + claimByTypes := make(map[string][]data.Event, len(claimTypes)) for _, event := range events { for _, balance := range balances { if event.Nullifier != balance.Nullifier || !balance.IsVerified { continue } - toClaim = append(toClaim, event.ID) + claimByTypes[event.Type] = append(claimByTypes[event.Type], event) break } } - if len(toClaim) == 0 { + if len(claimByTypes) == 0 { return nil } - _, err = pg.NewEvents(db).FilterByID(toClaim...).Update(data.EventClaimed, nil, &evType.Reward) - if err != nil { - return fmt.Errorf("update event status: %w", err) + rewardByNullifier := make(map[string]int64, len(balances)) + for _, evType := range types.List(evtypes.FilterByAutoClaim(true)) { + byType := claimByTypes[evType.Name] + if len(byType) == 0 { + continue + } + + ids := make([]string, 0, len(byType)) + for _, ev := range byType { + ids = append(ids, ev.ID) + rewardByNullifier[ev.Nullifier] += evType.Reward + } + + _, err = pg.NewEvents(db).FilterByID(ids...).Update(data.EventClaimed, nil, &evType.Reward) + if err != nil { + return fmt.Errorf("update event status: %w", err) + } } for _, balance := range balances { @@ -190,7 +205,7 @@ func claimReferralSpecificEvents(db *pgdb.DB, types *evtypes.Types, levels confi pg.NewReferrals(db), pg.NewBalances(db), balance, - evType.Reward) + rewardByNullifier[balance.Nullifier]) if err != nil { return fmt.Errorf("failed to do claim event updates for referral specific event: %w", err) }