diff --git a/internal/data/balances.go b/internal/data/balances.go index 974a98c..4390aa3 100644 --- a/internal/data/balances.go +++ b/internal/data/balances.go @@ -36,7 +36,7 @@ type BalancesQ interface { GetWithRank(nullifier string) (*Balance, error) SelectWithRank() ([]Balance, error) - // Filters not applied. Return balances which already have scanned passport, but there no fulfilled or claimed events for this + // Filters not applied. Return balances which already have scanned passport, but there no claimed events for this WithoutPassportEvent() ([]WithoutPassportEventBalance, error) WithoutReferralEvent() ([]ReferredReferrer, error) @@ -46,7 +46,8 @@ type BalancesQ interface { type WithoutPassportEventBalance struct { Balance - EventID *string `db:"event_id"` + EventID string `db:"event_id"` + EventStatus EventStatus `db:"event_status"` } type ReferredReferrer struct { diff --git a/internal/data/pg/balances.go b/internal/data/pg/balances.go index 06494e0..74ba933 100644 --- a/internal/data/pg/balances.go +++ b/internal/data/pg/balances.go @@ -145,10 +145,10 @@ func (q *balances) GetWithRank(nullifier string) (*data.Balance, error) { func (q *balances) WithoutPassportEvent() ([]data.WithoutPassportEventBalance, error) { var res []data.WithoutPassportEventBalance stmt := fmt.Sprintf(` - SELECT b.*, e.id AS event_id - FROM %s AS b LEFT JOIN %s AS e + SELECT b.*, e.id AS event_id, e.status AS event_status + FROM %s AS b INNER JOIN %s AS e ON b.nullifier = e.nullifier AND e.type='passport_scan' - WHERE (e.status NOT IN ('fulfilled', 'claimed') OR e.nullifier IS NULL) + WHERE e.status NOT IN ('claimed') AND b.referred_by IS NOT NULL AND b.country IS NOT NULL `, balancesTable, eventsTable) diff --git a/internal/service/handlers/claim_event.go b/internal/service/handlers/claim_event.go index 5ae0f88..90b0849 100644 --- a/internal/service/handlers/claim_event.go +++ b/internal/service/handlers/claim_event.go @@ -5,7 +5,9 @@ import ( "net/http" "github.com/rarimo/decentralized-auth-svc/pkg/auth" + "github.com/rarimo/rarime-points-svc/internal/config" "github.com/rarimo/rarime-points-svc/internal/data" + "github.com/rarimo/rarime-points-svc/internal/data/evtypes" "github.com/rarimo/rarime-points-svc/internal/data/pg" "github.com/rarimo/rarime-points-svc/internal/service/requests" "github.com/rarimo/rarime-points-svc/resources" @@ -83,7 +85,7 @@ func ClaimEvent(w http.ResponseWriter, r *http.Request) { } err = EventsQ(r).Transaction(func() error { - event, err = claimEventWithPoints(r, *event, evType.Reward, balance) + event, err = claimEvent(r, event, balance) if err != nil { return err } @@ -107,49 +109,82 @@ func ClaimEvent(w http.ResponseWriter, r *http.Request) { ape.Render(w, newClaimEventResponse(*event, evType.Resource(), *balance)) } -// claimEventWithPoints requires event to exist -func claimEventWithPoints(r *http.Request, event data.Event, reward int64, balance *data.Balance) (claimed *data.Event, err error) { - // Upgrade level logic when threshold is reached - refsCount, level := Levels(r).LvlUp(balance.Level, reward+balance.Amount) - if level != balance.Level { - count, err := ReferralsQ(r).FilterByNullifier(balance.Nullifier).Count() +// claimEvent requires event to exist +// call in transaction to prevent unexpected changes +func claimEvent(r *http.Request, event *data.Event, balance *data.Balance) (claimed *data.Event, err error) { + if event == nil { + return nil, nil + } + + evType := EventTypes(r).Get(event.Type, evtypes.FilterInactive) + if evType == nil { + return event, nil + } + + claimed, err = EventsQ(r).FilterByID(event.ID).Update(data.EventClaimed, nil, &evType.Reward) + if err != nil { + return nil, fmt.Errorf("update event status: %w", err) + } + + err = DoClaimEventUpdates( + Levels(r), + ReferralsQ(r), + BalancesQ(r), + CountriesQ(r), + *balance, + evType.Reward) + if err != nil { + return nil, fmt.Errorf("failed to do claim event updates: %w", err) + } + + return claimed, nil +} + +// do updates which link to claim event: +// update reserved amount in country; +// lvlup and update referrals count; +// accruing points; +// +// Balance must be active and with verified passport +func DoClaimEventUpdates( + levels config.Levels, + referralsQ data.ReferralsQ, + balancesQ data.BalancesQ, + countriesQ data.CountriesQ, + balance data.Balance, + reward int64) (err error) { + + refsCount, level := levels.LvlUp(balance.Level, reward+balance.Amount) + if refsCount > 0 { + count, err := referralsQ.New().FilterByNullifier(balance.Nullifier).Count() if err != nil { - return nil, fmt.Errorf("failed to get referral count: %w", err) + return fmt.Errorf("failed to get referral count: %w", err) } refToAdd := prepareReferralsToAdd(balance.Nullifier, uint64(refsCount), count) - if err = ReferralsQ(r).Insert(refToAdd...); err != nil { - return nil, fmt.Errorf("failed to insert referrals: %w", err) + if err = referralsQ.New().Insert(refToAdd...); err != nil { + return fmt.Errorf("failed to insert referrals: %w", err) } - err = BalancesQ(r).FilterByNullifier(balance.Nullifier).Update(map[string]any{ - data.ColLevel: level, - }) - if err != nil { - return nil, fmt.Errorf("failed to update level: %w", err) - } - } - - claimed, err = EventsQ(r).FilterByID(event.ID).Update(data.EventClaimed, nil, &reward) - if err != nil { - return nil, fmt.Errorf("update event status: %w", err) + return nil } - err = BalancesQ(r).FilterByNullifier(balance.Nullifier).Update(map[string]any{ + err = balancesQ.FilterByNullifier(balance.Nullifier).Update(map[string]any{ data.ColAmount: pg.AddToValue(data.ColAmount, reward), + data.ColLevel: level, }) if err != nil { - return nil, fmt.Errorf("update balance amount: %w", err) + return fmt.Errorf("update balance amount and level: %w", err) } - err = CountriesQ(r).FilterByCodes(*balance.Country).Update(map[string]any{ + err = countriesQ.FilterByCodes(*balance.Country).Update(map[string]any{ data.ColReserved: pg.AddToValue(data.ColReserved, reward), }) if err != nil { - return nil, fmt.Errorf("increase country reserve: %w", err) + return fmt.Errorf("increase country reserve: %w", err) } - return claimed, nil + return nil } func newClaimEventResponse( diff --git a/internal/service/handlers/verify_passport.go b/internal/service/handlers/verify_passport.go index 34adbc5..c779ac1 100644 --- a/internal/service/handlers/verify_passport.go +++ b/internal/service/handlers/verify_passport.go @@ -2,6 +2,7 @@ package handlers import ( "fmt" + "math" "math/big" "net/http" @@ -134,18 +135,29 @@ func doPassportScanUpdates(r *http.Request, balance data.Balance, proof zkptypes // because for claim event must be country code balance.Country = &country.Code - if err = fulfillPassportScanEvent(r, balance, *country); err != nil { + if err = fulfillOrClaimPassportScanEvent(r, balance, *country); err != nil { return fmt.Errorf("fulfill passport scan event: %w", err) } - if err = addEventForReferrer(r, balance); err != nil { - return fmt.Errorf("add event for referrer: %w", err) + evTypeRef := EventTypes(r).Get(evtypes.TypeReferralSpecific) + if evTypeRef == nil { + Log(r).Debug("Referral specific event type is inactive") + return nil } - if err = claimReferralSpecificEvents(r, balance, true); err != nil { + if err = claimReferralSpecificEvents(r, evTypeRef, balance.Nullifier); err != nil { return fmt.Errorf("failed to claim referral specific events: %w", err) } + if evtypes.FilterInactive(*evTypeRef) { + Log(r).Debug("Referral specific event type is inactive: event not added") + return nil + } + + if err = addEventForReferrer(r, evTypeRef, balance); err != nil { + return fmt.Errorf("add event for referrer: %w", err) + } + return nil } @@ -173,7 +185,7 @@ func updateBalanceCountry(r *http.Request, balance data.Balance, proof zkptypes. return country, nil } -func fulfillPassportScanEvent(r *http.Request, balance data.Balance, country data.Country) error { +func fulfillOrClaimPassportScanEvent(r *http.Request, balance data.Balance, country data.Country) error { evTypePassport := EventTypes(r).Get(evtypes.TypePassportScan, evtypes.FilterInactive) if evTypePassport == nil { Log(r).Debug("Passport scan event type is inactive") @@ -191,41 +203,61 @@ func fulfillPassportScanEvent(r *http.Request, balance data.Balance, country dat return errors.New("inconsistent state: balance has no country, event type is active, but no open event was found") } - event, err = EventsQ(r). - FilterByID(event.ID). - Update(data.EventFulfilled, nil, nil) + if !evTypePassport.AutoClaim || !country.ReserveAllowed || country.Reserved >= country.ReserveLimit { + _, err = EventsQ(r). + FilterByID(event.ID). + Update(data.EventFulfilled, nil, nil) + if err != nil { + return fmt.Errorf("failed to update event: %w", err) + } + + return nil + } + + _, err = EventsQ(r).FilterByID(event.ID).Update(data.EventClaimed, nil, &evTypePassport.Reward) if err != nil { - return fmt.Errorf("failed to update event: %w", err) + return fmt.Errorf("update event status: %w", err) } - if !evTypePassport.AutoClaim || !country.ReserveAllowed || country.Reserved >= country.ReserveLimit { - return nil + err = DoClaimEventUpdates( + Levels(r), + ReferralsQ(r), + BalancesQ(r), + CountriesQ(r), + balance, + evTypePassport.Reward) + if err != nil { + return fmt.Errorf("failed to do claim event updates for passport scan: %w", err) } - _, err = claimEventWithPoints(r, *event, evTypePassport.Reward, &balance) - return err + return nil } -// if autoClaim true - events will be claimed if autoclaim enabled for event -// if false - claim events without check if autoclaim enabled -func claimReferralSpecificEvents(r *http.Request, balance data.Balance, autoClaim bool) error { - evTypeRef := EventTypes(r).Get(evtypes.TypeReferralSpecific, evtypes.FilterInactive) +func claimReferralSpecificEvents(r *http.Request, evTypeRef *evtypes.EventConfig, nullifier string) error { if evTypeRef == nil { Log(r).Debug("Referral specific event type is inactive") return nil } - - if autoClaim && !evTypeRef.AutoClaim { + if !evTypeRef.AutoClaim { + Log(r).Debugf("auto claim for referral specific disabled") return nil } + // balance can't be nil because of previous logic + balance, err := BalancesQ(r).FilterByNullifier(nullifier).FilterDisabled().Get() + if err != nil { + return fmt.Errorf("failed to get balance: %w", err) + } + + // country can't be nill because of previous logic country, err := CountriesQ(r).FilterByCodes(*balance.Country).Get() if err != nil { return fmt.Errorf("failed to get referrer country: %w", err) } - if country == nil { - return fmt.Errorf("failed to get referrer country: must be present in database") + if !country.ReserveAllowed || country.Reserved >= country.ReserveLimit { + Log(r).Debug("Country disallowed for reserve or limit was reached after passport scan") + return nil } events, err := EventsQ(r).FilterByNullifier(balance.Nullifier). @@ -235,27 +267,40 @@ func claimReferralSpecificEvents(r *http.Request, balance data.Balance, autoClai return fmt.Errorf("get fulfilled referral specific events: %w", err) } - if len(events) == 0 { + countToClaim := int64(len(events)) + if countToClaim == 0 { return nil } + if country.Reserved+countToClaim*evTypeRef.Reward >= country.ReserveLimit+evTypeRef.Reward { + countToClaim = int64(math.Ceil(float64(country.ReserveLimit-country.Reserved) / float64(evTypeRef.Reward))) + } - if !country.ReserveAllowed || country.Reserved >= country.ReserveLimit { - return nil + eventsToClaimed := make([]string, countToClaim) + for i := 0; i < int(countToClaim); i++ { + eventsToClaimed[i] = events[i].ID } - for _, event := range events { - if _, err = claimEventWithPoints(r, event, evTypeRef.Reward, &balance); err != nil { - return fmt.Errorf("failed to claim referral specific event: %w", err) - } + _, err = EventsQ(r).FilterByID(eventsToClaimed...).Update(data.EventClaimed, nil, &evTypeRef.Reward) + if err != nil { + return fmt.Errorf("update event status: %w", err) } - return nil + err = DoClaimEventUpdates( + Levels(r), + ReferralsQ(r), + BalancesQ(r), + CountriesQ(r), + *balance, + countToClaim*evTypeRef.Reward) + if err != nil { + return fmt.Errorf("failed to do claim event updates for referral specific events: %w", err) + } + + return nil } -func addEventForReferrer(r *http.Request, balance data.Balance) error { - evTypeRef := EventTypes(r).Get(evtypes.TypeReferralSpecific, evtypes.FilterInactive) +func addEventForReferrer(r *http.Request, evTypeRef *evtypes.EventConfig, balance data.Balance) error { if evTypeRef == nil { - Log(r).Debug("Referral event type is inactive, not fulfilling event for referrer") return nil } @@ -264,20 +309,21 @@ func addEventForReferrer(r *http.Request, balance data.Balance) error { if err != nil { return fmt.Errorf("get referral by ID: %w", err) } - - event, err := EventsQ(r).InsertOne(data.Event{ - Nullifier: referral.Nullifier, - Type: evTypeRef.Name, - Status: data.EventFulfilled, - Meta: data.Jsonb(fmt.Sprintf(`{"nullifier": "%s"}`, balance.Nullifier)), - }) - - if err != nil { - return fmt.Errorf("failed to insert event for referrer: %w", err) + if referral == nil { + return fmt.Errorf("critical: referred_by not null, but row in referrals absent") } if !evTypeRef.AutoClaim { - Log(r).Debugf("auto claim for referral specific disabled") + err = EventsQ(r).Insert(data.Event{ + Nullifier: referral.Nullifier, + Type: evTypeRef.Name, + Status: data.EventFulfilled, + Meta: data.Jsonb(fmt.Sprintf(`{"nullifier": "%s"}`, balance.Nullifier)), + }) + if err != nil { + return fmt.Errorf("failed to insert fulfilled event for referrer: %w", err) + } + return nil } @@ -285,14 +331,12 @@ func addEventForReferrer(r *http.Request, balance data.Balance) error { if err != nil { return fmt.Errorf("failed to get referrer balance: %w", err) } - if referrerBalance == nil { - return fmt.Errorf("referrer balance not exists: %s", referral.Nullifier) + return fmt.Errorf("critical: referrer balance not exist [%s], while referral code exist", referral.Nullifier) } - // genesis address have codes, but haven't referred_by if !referrerBalance.ReferredBy.Valid || referrerBalance.Country == nil { - Log(r).Debug("Referrer have invalid referred_by or not scan passport") + Log(r).Debug("Referrer is genesis balance or not scanned passport") return nil } @@ -300,19 +344,46 @@ func addEventForReferrer(r *http.Request, balance data.Balance) error { if err != nil { return fmt.Errorf("failed to get referrer country: %w", err) } - if country == nil { - return fmt.Errorf("failed to get referrer country: must be present in database") + return fmt.Errorf("critical: country must be present in database") } if !country.ReserveAllowed || country.Reserved >= country.ReserveLimit { Log(r).Debug("Referrer country have ReserveAllowed false or limit reached") + + err = EventsQ(r).Insert(data.Event{ + Nullifier: referral.Nullifier, + Type: evTypeRef.Name, + Status: data.EventFulfilled, + Meta: data.Jsonb(fmt.Sprintf(`{"nullifier": "%s"}`, balance.Nullifier)), + }) + if err != nil { + return fmt.Errorf("failed to insert fulfilled event for referrer: %w", err) + } + return nil } - _, err = claimEventWithPoints(r, *event, evTypeRef.Reward, referrerBalance) + err = EventsQ(r).Insert(data.Event{ + Nullifier: referral.Nullifier, + Type: evTypeRef.Name, + Status: data.EventClaimed, + PointsAmount: &evTypeRef.Reward, + Meta: data.Jsonb(fmt.Sprintf(`{"nullifier": "%s"}`, balance.Nullifier)), + }) + if err != nil { + return fmt.Errorf("failed to insert claimed event for referrer: %w", err) + } + + err = DoClaimEventUpdates( + Levels(r), + ReferralsQ(r), + BalancesQ(r), + CountriesQ(r), + *referrerBalance, + evTypeRef.Reward) if err != nil { - return fmt.Errorf("failed to claim referral_specific event for referrer: %w", err) + return fmt.Errorf("failed to do claim event updates for referrer referral specific events: %w", err) } return nil diff --git a/internal/service/workers/nooneisforgotten/main.go b/internal/service/workers/nooneisforgotten/main.go index b85eaff..92c858a 100644 --- a/internal/service/workers/nooneisforgotten/main.go +++ b/internal/service/workers/nooneisforgotten/main.go @@ -2,149 +2,170 @@ package nooneisforgotten import ( "fmt" + "math" + "sort" "github.com/rarimo/rarime-points-svc/internal/config" "github.com/rarimo/rarime-points-svc/internal/data" "github.com/rarimo/rarime-points-svc/internal/data/evtypes" "github.com/rarimo/rarime-points-svc/internal/data/pg" - "github.com/rarimo/rarime-points-svc/internal/service/referralid" + "github.com/rarimo/rarime-points-svc/internal/service/handlers" "gitlab.com/distributed_lab/kit/pgdb" ) func Run(cfg config.Config, sig chan struct{}) { db := cfg.DB().Clone() + if err := pg.NewEvents(db).Transaction(func() error { + return updatePassportScanEvents(db, cfg.EventTypes(), cfg.Levels()) + }); err != nil { + panic(fmt.Errorf("failed to update passport scan events: %w", err)) + } - evType := cfg.EventTypes().Get(evtypes.TypePassportScan, evtypes.FilterInactive) - if evType != nil { - if err := updatePassportScanEvents(db); err != nil { - panic(err) - } - - if evType.AutoClaim { - err := claimPassportScanEvents(cfg) - if err != nil { - panic(err) - } - } + if err := pg.NewEvents(db).Transaction(func() error { + return updateReferralUserEvents(db, cfg.EventTypes()) + }); err != nil { + panic(fmt.Errorf("failed to update referral user events")) } - evType = cfg.EventTypes().Get(evtypes.TypeReferralSpecific, evtypes.FilterInactive) - if evType != nil { - if err := updateReferralUserEvents(db); err != nil { - panic(err) - } + if err := pg.NewEvents(db).Transaction(func() error { + return claimReferralSpecificEvents(db, cfg.EventTypes(), cfg.Levels()) + }); err != nil { + panic(fmt.Errorf("failed to claim referral specific events")) } + sig <- struct{}{} } -func updatePassportScanEvents(db *pgdb.DB) error { +func updatePassportScanEvents(db *pgdb.DB, types evtypes.Types, levels config.Levels) error { + evType := types.Get(evtypes.TypePassportScan) + if evType == nil { + return nil + } + + if !evType.AutoClaim && evtypes.FilterInactive(*evType) { + return nil + } + balances, err := pg.NewBalances(db).WithoutPassportEvent() if err != nil { return fmt.Errorf("failed to select balances without points for passport scan: %w", err) } - toUpdate := make([]string, 0, len(balances)) + toFulfill := make([]string, 0, len(balances)) + countriesBalancesMap := make(map[string][]data.WithoutPassportEventBalance, len(balances)) + countriesList := make([]string, 0, len(balances)) for _, balance := range balances { - if balance.EventID != nil { - toUpdate = append(toUpdate, *balance.EventID) - continue + if balance.EventStatus == data.EventOpen { + toFulfill = append(toFulfill, balance.EventID) } - } - if len(toUpdate) != 0 { - _, err = pg.NewEvents(db). - FilterByID(toUpdate...). - Update(data.EventFulfilled, nil, nil) - if err != nil { - return fmt.Errorf("failed to update passport scan events: %w", err) + // country must exist because of db query logic + if _, ok := countriesBalancesMap[*balance.Country]; !ok { + countriesList = append(countriesList, *balance.Country) + countriesBalancesMap[*balance.Country] = make([]data.WithoutPassportEventBalance, 0, len(balances)) } + countriesBalancesMap[*balance.Country] = append(countriesBalancesMap[*balance.Country], balance) } - return nil -} + if !evType.AutoClaim { + if len(toFulfill) != 0 { + _, err = pg.NewEvents(db). + FilterByID(toFulfill...). + Update(data.EventFulfilled, nil, nil) + if err != nil { + return fmt.Errorf("failed to update passport scan events: %w", err) + } + } -func claimPassportScanEvents(cfg config.Config) error { - evType := cfg.EventTypes().Get(evtypes.TypePassportScan, evtypes.FilterInactive) - if evType == nil { return nil } - db := cfg.DB().Clone() - events, err := pg.NewEvents(db).FilterByType(evtypes.TypePassportScan).FilterByStatus(data.EventFulfilled).Select() + countries, err := pg.NewCountries(db).FilterByCodes(countriesList...).Select() if err != nil { - return fmt.Errorf("failed to select passport scan events: %w", err) + return fmt.Errorf("failed to select countries: %w", err) } - if len(events) == 0 { - return nil - } + // we need sort, because firstly claim already fulfilled event + // and then open events + for _, country := range countries { + if !country.ReserveAllowed || country.Reserved >= country.ReserveLimit { + continue + } - eventsMap := make(map[string]data.Event, len(events)) - nullifiers := make([]string, len(events)) - for i, event := range events { - nullifiers[i] = event.Nullifier - eventsMap[event.Nullifier] = event - } + sort.SliceStable(countriesBalancesMap[country.Code], func(i, j int) bool { + if countriesBalancesMap[country.Code][i].EventStatus == countriesBalancesMap[country.Code][j].EventStatus { + return false + } + if countriesBalancesMap[country.Code][i].EventStatus == data.EventOpen { + return true + } + return false + }) - balances, err := pg.NewBalances(db).FilterByNullifier(nullifiers...).FilterDisabled().Select() - if err != nil { - return fmt.Errorf("failed to select balances for claim passport scan event: %w", err) - } + countToClaim := int(math.Min( + float64(len(countriesBalancesMap[country.Code])), + math.Ceil(float64(country.ReserveLimit-country.Reserved)/float64(evType.Reward)))) - if len(balances) == 0 { - return nil - } + for i := 0; i < countToClaim; i++ { + // if event is inactive we claim only fulfilled events + if countriesBalancesMap[country.Code][i].EventStatus == data.EventOpen && evtypes.FilterInactive(*evType) { + break + } - countryCodesMap := make(map[string]int64, len(balances)) - for _, balance := range balances { - // normally should never happen - if balance.Country == nil { - return fmt.Errorf("balance have fulfilled passport scan event, but have no country") - } - if _, ok := countryCodesMap[*balance.Country]; !ok { - countryCodesMap[*balance.Country] = 0 - } - } + eventID := countriesBalancesMap[country.Code][i].EventID + _, err = pg.NewEvents(db).FilterByID(eventID).Update(data.EventClaimed, nil, &evType.Reward) + if err != nil { + return fmt.Errorf("update event status: %w", err) + } - countryCodesSlice := make([]string, 0, len(countryCodesMap)) - for k := range countryCodesMap { - countryCodesSlice = append(countryCodesSlice, k) - } + err = handlers.DoClaimEventUpdates( + levels, + pg.NewReferrals(db), + pg.NewBalances(db), + pg.NewCountries(db), + countriesBalancesMap[country.Code][i].Balance, + evType.Reward) + if err != nil { + return fmt.Errorf("failed to do claim event updates for passport scan: %w", err) + } - countries, err := pg.NewCountries(db).FilterByCodes(countryCodesSlice...).Select() - if err != nil { - return fmt.Errorf("failed to select countries for claim passport scan events: %w", err) + countriesBalancesMap[country.Code][i].EventStatus = data.EventClaimed + } } - for _, country := range countries { - if !country.ReserveAllowed || country.Reserved >= country.ReserveLimit { - delete(countryCodesMap, country.Code) - continue - } - countryCodesMap[country.Code] = country.ReserveLimit - country.Reserved + if evtypes.FilterInactive(*evType) { + return nil } - for _, balance := range balances { - // country should exists because of previous validation - if _, ok := countryCodesMap[*balance.Country]; !ok { - continue + toFulfill = make([]string, 0, len(balances)) + for _, balances := range countriesBalancesMap { + for _, balance := range balances { + if balance.EventStatus == data.EventOpen { + toFulfill = append(toFulfill, balance.EventID) + } } + } - countryCodesMap[*balance.Country] -= evType.Reward - if countryCodesMap[*balance.Country] <= 0 { - delete(countryCodesMap, *balance.Country) - } + if len(toFulfill) == 0 { + return nil + } - _, err = claimEventWithPoints(cfg, eventsMap[balance.Nullifier], evType.Reward, &balance) - if err != nil { - return fmt.Errorf("failed to claim passport scan event: %w", err) - } + _, err = pg.NewEvents(db). + FilterByID(toFulfill...). + Update(data.EventFulfilled, nil, nil) + if err != nil { + return fmt.Errorf("failed to update passport scan events: %w", err) } return nil } -func updateReferralUserEvents(db *pgdb.DB) error { +func updateReferralUserEvents(db *pgdb.DB, types evtypes.Types) error { + evTypeRef := types.Get(evtypes.TypeReferralSpecific, evtypes.FilterInactive) + if evTypeRef == nil { + return nil + } + refPairs, err := pg.NewBalances(db).WithoutReferralEvent() if err != nil { return fmt.Errorf("failed to select balances without points for referred users: %w", err) @@ -160,23 +181,26 @@ func updateReferralUserEvents(db *pgdb.DB) error { }) } - if len(toInsert) != 0 { - err = pg.NewEvents(db).Insert(toInsert...) - if err != nil { - return fmt.Errorf("failed to insert referred user events: %w", err) - } + if len(toInsert) == 0 { + return nil + } + + if err = pg.NewEvents(db).Insert(toInsert...); err != nil { + return fmt.Errorf("failed to insert referred user events: %w", err) } return nil } -func claimReferralSpecificEvents(cfg config.Config) error { - evType := cfg.EventTypes().Get(evtypes.TypeReferralSpecific, evtypes.FilterInactive) +func claimReferralSpecificEvents(db *pgdb.DB, types evtypes.Types, levels config.Levels) error { + evType := types.Get(evtypes.TypeReferralSpecific) if evType == nil { return nil } + if !evType.AutoClaim { + return nil + } - db := cfg.DB().Clone() events, err := pg.NewEvents(db).FilterByType(evtypes.TypeReferralSpecific).FilterByStatus(data.EventFulfilled).Select() if err != nil { return fmt.Errorf("failed to select passport scan events: %w", err) @@ -186,135 +210,80 @@ func claimReferralSpecificEvents(cfg config.Config) error { return nil } - eventsMap := make(map[string][]data.Event, len(events)) - nullifiers := make([]string, len(events)) - for i, event := range events { - nullifiers[i] = event.Nullifier - eventsMap[event.Nullifier] = append(eventsMap[event.Nullifier], event) + nullifiersEventsMap := make(map[string][]data.Event, len(events)) + nullifiers := make([]string, 0, len(events)) + for _, event := range events { + if _, ok := nullifiersEventsMap[event.Nullifier]; !ok { + nullifiersEventsMap[event.Nullifier] = make([]data.Event, 0, len(events)) + nullifiers = append(nullifiers, event.Nullifier) + } + nullifiersEventsMap[event.Nullifier] = append(nullifiersEventsMap[event.Nullifier], event) } balances, err := pg.NewBalances(db).FilterByNullifier(nullifiers...).FilterDisabled().Select() if err != nil { return fmt.Errorf("failed to select balances for claim passport scan event: %w", err) } - if len(balances) == 0 { - return nil + return fmt.Errorf("critical: events present, but no balances with nullifier") } - countryCodesMap := make(map[string]int64, len(balances)) + countriesBalancesMap := make(map[string][]data.Balance, len(balances)) for _, balance := range balances { - // normally should never happen - if balance.Country == nil { - return fmt.Errorf("balance have fulfilled passport scan event, but have no country") - } - if _, ok := countryCodesMap[*balance.Country]; !ok { - countryCodesMap[*balance.Country] = 0 + // country can't be nil because of db query logic + if _, ok := countriesBalancesMap[*balance.Country]; !ok { + countriesBalancesMap[*balance.Country] = make([]data.Balance, 0, len(balances)) } + + countriesBalancesMap[*balance.Country] = append(countriesBalancesMap[*balance.Country], balance) } - countryCodesSlice := make([]string, 0, len(countryCodesMap)) - for k := range countryCodesMap { - countryCodesSlice = append(countryCodesSlice, k) + countryCodes := make([]string, 0, len(countriesBalancesMap)) + for k := range countriesBalancesMap { + countryCodes = append(countryCodes, k) } - countries, err := pg.NewCountries(db).FilterByCodes(countryCodesSlice...).Select() + countries, err := pg.NewCountries(db).FilterByCodes(countryCodes...).Select() if err != nil { return fmt.Errorf("failed to select countries for claim passport scan events: %w", err) } + toClaim := make([]string, 0, len(events)) for _, country := range countries { if !country.ReserveAllowed || country.Reserved >= country.ReserveLimit { - delete(countryCodesMap, country.Code) - continue - } - countryCodesMap[country.Code] = country.ReserveLimit - country.Reserved - } - - for _, balance := range balances { - // country should exists because of previous validation - if _, ok := countryCodesMap[*balance.Country]; !ok { continue } - countryCodesMap[*balance.Country] -= evType.Reward - if countryCodesMap[*balance.Country] <= 0 { - delete(countryCodesMap, *balance.Country) - } - - for _, event := range eventsMap[balance.Nullifier] { - _, err = claimEventWithPoints(cfg, event, evType.Reward, &balance) - if err != nil { - return fmt.Errorf("failed to claim passport scan event: %w", err) + limit := country.ReserveLimit - country.Reserved + for _, balance := range countriesBalancesMap[country.Code] { + if limit <= 0 { + break } - } - } - - return nil -} - -// claimEventWithPoints requires event to exist -func claimEventWithPoints(cfg config.Config, event data.Event, reward int64, balance *data.Balance) (claimed *data.Event, err error) { - err = pg.NewEvents(cfg.DB().Clone()).Transaction(func() error { - db := cfg.DB().Clone() - // Upgrade level logic when threshold is reached - refsCount, level := cfg.Levels().LvlUp(balance.Level, reward+balance.Amount) - if level != balance.Level { - count, err := pg.NewReferrals(db).FilterByNullifier(balance.Nullifier).Count() - if err != nil { - return fmt.Errorf("failed to get referral count: %w", err) - } - - refToAdd := prepareReferralsToAdd(balance.Nullifier, uint64(refsCount), count) - if err = pg.NewReferrals(db).Insert(refToAdd...); err != nil { - return fmt.Errorf("failed to insert referrals: %w", err) + toAccrue := int64(0) + for _, event := range nullifiersEventsMap[balance.Nullifier] { + limit -= evType.Reward + toClaim = append(toClaim, event.ID) + toAccrue += evType.Reward } - err = pg.NewBalances(db).FilterByNullifier(balance.Nullifier).Update(map[string]any{ - data.ColLevel: level, - }) + err = handlers.DoClaimEventUpdates( + levels, + pg.NewReferrals(db), + pg.NewBalances(db), + pg.NewCountries(db), + balance, + evType.Reward) if err != nil { - return fmt.Errorf("failed to update level: %w", err) + return fmt.Errorf("failed to do claim event updates for referral specific event: %w", err) } } + } - claimed, err = pg.NewEvents(db).FilterByID(event.ID).Update(data.EventClaimed, nil, &reward) - if err != nil { - return fmt.Errorf("update event status: %w", err) - } - - err = pg.NewBalances(db).FilterByNullifier(balance.Nullifier).Update(map[string]any{ - data.ColAmount: pg.AddToValue(data.ColAmount, reward), - }) - if err != nil { - return fmt.Errorf("update balance amount: %w", err) - } - - err = pg.NewCountries(db).FilterByCodes(*balance.Country).Update(map[string]any{ - data.ColReserved: pg.AddToValue(data.ColReserved, reward), - }) - if err != nil { - return fmt.Errorf("increase country reserve: %w", err) - } - - return nil - }) - - return claimed, nil -} - -func prepareReferralsToAdd(nullifier string, count, index uint64) []data.Referral { - refCodes := referralid.NewMany(nullifier, count, index) - refs := make([]data.Referral, len(refCodes)) - - for i, code := range refCodes { - refs[i] = data.Referral{ - ID: code, - Nullifier: nullifier, - UsageLeft: 1, - } + _, err = pg.NewEvents(db).FilterByID(toClaim...).Update(data.EventClaimed, nil, &evType.Reward) + if err != nil { + return fmt.Errorf("update event status: %w", err) } - return refs + return nil }