From cc21089ce0dc8dce9d9631b76af0e8fd015c542b Mon Sep 17 00:00:00 2001 From: Roman Perebynos Date: Thu, 7 Mar 2024 17:28:12 +0200 Subject: [PATCH 01/42] Add new parameter for event in config 'StartAt'. Add opener in reopener; opener open events which start at specified time --- config.yaml | 6 ++ internal/data/evtypes/config.go | 6 ++ internal/data/evtypes/filters.go | 6 +- internal/data/evtypes/main.go | 1 + internal/service/workers/reopener/init.go | 76 +++++++++++++++++++++++ internal/service/workers/reopener/main.go | 5 ++ 6 files changed, 99 insertions(+), 1 deletion(-) diff --git a/config.yaml b/config.yaml index 4089c35..606c9a6 100644 --- a/config.yaml +++ b/config.yaml @@ -52,6 +52,12 @@ event_types: frequency: unlimited description: The user {:did} has verified the passport. Claim the reward! no_auto_open: true + - name: planned + title: Planned event + reward: 25 + frequency: unlimited + description: Event that start at specidied time + starts_at: 2020-01-01T00:00:00Z auth: addr: http://rarime-auth diff --git a/internal/data/evtypes/config.go b/internal/data/evtypes/config.go index fb52ee7..c93bca6 100644 --- a/internal/data/evtypes/config.go +++ b/internal/data/evtypes/config.go @@ -40,6 +40,12 @@ func (c *config) EventTypes() Types { if !checkFreqValue(t.Frequency) { panic(fmt.Errorf("invalid frequency: %s", t.Frequency)) } + + if t.ExpiresAt != nil && t.StartsAt != nil && !t.StartsAt.Before(*t.ExpiresAt) { + panic(fmt.Errorf("invalid starts_at; starts_at must be before expires_at: %s > %s", + t.StartsAt, t.ExpiresAt)) + } + m[t.Name] = t } diff --git a/internal/data/evtypes/filters.go b/internal/data/evtypes/filters.go index 615bbb0..6c9c823 100644 --- a/internal/data/evtypes/filters.go +++ b/internal/data/evtypes/filters.go @@ -10,8 +10,12 @@ func FilterExpired(ev EventConfig) bool { return ev.ExpiresAt != nil && ev.ExpiresAt.Before(time.Now().UTC()) } +func FilterNotStarted(ev EventConfig) bool { + return ev.StartsAt != nil && ev.StartsAt.After(time.Now().UTC()) +} + func FilterInactive(ev EventConfig) bool { - return ev.Disabled || FilterExpired(ev) + return ev.Disabled || FilterExpired(ev) || FilterNotStarted(ev) } func FilterNotOpenable(ev EventConfig) bool { diff --git a/internal/data/evtypes/main.go b/internal/data/evtypes/main.go index 846fd93..e383d1c 100644 --- a/internal/data/evtypes/main.go +++ b/internal/data/evtypes/main.go @@ -40,6 +40,7 @@ type EventConfig struct { Title string `fig:"title,required"` Frequency Frequency `fig:"frequency,required"` ExpiresAt *time.Time `fig:"expires_at"` + StartsAt *time.Time `fig:"starts_at"` NoAutoOpen bool `fig:"no_auto_open"` Disabled bool `fig:"disabled"` } diff --git a/internal/service/workers/reopener/init.go b/internal/service/workers/reopener/init.go index 3e0722e..791db39 100644 --- a/internal/service/workers/reopener/init.go +++ b/internal/service/workers/reopener/init.go @@ -1,13 +1,16 @@ package reopener import ( + "context" "fmt" "time" + "github.com/go-co-op/gocron/v2" "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/workers/cron" "gitlab.com/distributed_lab/logan/v3" ) @@ -115,3 +118,76 @@ func (c *initCollector) selectAbsent() ([]data.ReopenableEvent, error) { log.Infof("%d new (DID, type) pairs to open: %v", len(res), res) return res, nil } + +func initOpener(ctx context.Context, cfg config.Config) error { + log := cfg.Log().WithField("who", "opener[initializer]") + + notStartedEv := cfg.EventTypes().List(func(ev evtypes.EventConfig) bool { + return ev.Disabled || !evtypes.FilterNotStarted(ev) || ev.StartsAt == nil + }) + + if len(notStartedEv) != 0 { + log.Info("No events to open at Start time: all types already opened or there no types with StartAt") + return nil + } + + balancesQ := pg.NewBalances(cfg.DB().Clone()) + eventsQ := pg.NewEvents(cfg.DB().Clone()) + + for _, ev := range notStartedEv { + _, err := cron.NewJob( + gocron.OneTimeJob(gocron.OneTimeJobStartDateTime(*ev.StartsAt)), + gocron.NewTask(func() { + log := cfg.Log().WithField("who", fmt.Sprintf("opener[%s]", ev.Name)) + + var balances []data.Balance + var err error + + for i := 0; i < 4; i++ { + if balances, err = balancesQ.New().FilterDisabled().Select(); err == nil { + break + } + + log.Errorf("Failed to get balances: %s [retry %d]", err, i) + time.Sleep(time.Second * 5) + } + + if err != nil { + log.Errorf("Failed to get balances: %s", err) + return + } + + events := make([]data.Event, len(balances)) + status := data.EventOpen + if ev.Name == evtypes.TypeFreeWeekly { + status = data.EventFulfilled + } + + for i, balance := range balances { + events[i] = data.Event{UserDID: balance.DID, Type: ev.Name, Status: status} + } + + for i := 0; i < 4; i++ { + if err = eventsQ.New().Insert(events...); err == nil { + break + } + + log.Errorf("Failed to insert events: %s [retry %d]", err, i) + time.Sleep(time.Second * 5) + } + + if err != nil { + log.Errorf("Failed to insert events: %s", err) + return + } + }, ctx), + gocron.WithName(fmt.Sprintf("opener[%s]", ev.Name)), + ) + + if err != nil { + return fmt.Errorf("opener: failed to initialize job: %w", err) + } + } + + return nil +} diff --git a/internal/service/workers/reopener/main.go b/internal/service/workers/reopener/main.go index ee7b530..fdcdb97 100644 --- a/internal/service/workers/reopener/main.go +++ b/internal/service/workers/reopener/main.go @@ -20,6 +20,11 @@ func Run(ctx context.Context, cfg config.Config) { } cron.Init(cfg.Log()) + + if err := initOpener(ctx, cfg); err != nil { + panic(fmt.Errorf("reopener: failed to initialize opener: %w", err)) + } + atDayStart := gocron.NewAtTimes(gocron.NewAtTime(0, 0, 0)) daily := newWorker(cfg, evtypes.Daily) From 5f150871e40f013bdc7eb84f5abcf04fcf649320 Mon Sep 17 00:00:00 2001 From: Roman Perebynos Date: Thu, 7 Mar 2024 18:42:11 +0200 Subject: [PATCH 02/42] Change Render to RenderErr in AuthMiddleware --- internal/service/handlers/middleware.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/service/handlers/middleware.go b/internal/service/handlers/middleware.go index 56f8365..edc5ea4 100644 --- a/internal/service/handlers/middleware.go +++ b/internal/service/handlers/middleware.go @@ -18,12 +18,12 @@ func AuthMiddleware(auth *auth.Client, log *logan.Entry) func(http.Handler) http claims, err := auth.ValidateJWT(r) if err != nil { log.WithError(err).Info("Got invalid auth or validation error") - ape.Render(w, problems.Unauthorized()) + ape.RenderErr(w, problems.Unauthorized()) return } if len(claims) == 0 { - ape.Render(w, problems.Unauthorized()) + ape.RenderErr(w, problems.Unauthorized()) return } From 366f9c502d87854c2f48ba082d972e447b7f5ded Mon Sep 17 00:00:00 2001 From: Roman Perebynos Date: Fri, 8 Mar 2024 16:53:41 +0200 Subject: [PATCH 03/42] rename initOpener -> runStartingWachers; add function startingWatcher which open events at start time --- internal/data/evtypes/config.go | 2 +- internal/service/workers/reopener/init.go | 86 ++++++++++------------- internal/service/workers/reopener/main.go | 2 +- 3 files changed, 39 insertions(+), 51 deletions(-) diff --git a/internal/data/evtypes/config.go b/internal/data/evtypes/config.go index c93bca6..971b14f 100644 --- a/internal/data/evtypes/config.go +++ b/internal/data/evtypes/config.go @@ -42,7 +42,7 @@ func (c *config) EventTypes() Types { } if t.ExpiresAt != nil && t.StartsAt != nil && !t.StartsAt.Before(*t.ExpiresAt) { - panic(fmt.Errorf("invalid starts_at; starts_at must be before expires_at: %s > %s", + panic(fmt.Errorf("starts_at must be before expires_at: %s > %s", t.StartsAt, t.ExpiresAt)) } diff --git a/internal/service/workers/reopener/init.go b/internal/service/workers/reopener/init.go index 791db39..8e56df6 100644 --- a/internal/service/workers/reopener/init.go +++ b/internal/service/workers/reopener/init.go @@ -12,6 +12,7 @@ import ( "github.com/rarimo/rarime-points-svc/internal/data/pg" "github.com/rarimo/rarime-points-svc/internal/service/workers/cron" "gitlab.com/distributed_lab/logan/v3" + "gitlab.com/distributed_lab/running" ) func initialRun(cfg config.Config) error { @@ -119,11 +120,11 @@ func (c *initCollector) selectAbsent() ([]data.ReopenableEvent, error) { return res, nil } -func initOpener(ctx context.Context, cfg config.Config) error { +func runStartingWatchers(ctx context.Context, cfg config.Config) error { log := cfg.Log().WithField("who", "opener[initializer]") notStartedEv := cfg.EventTypes().List(func(ev evtypes.EventConfig) bool { - return ev.Disabled || !evtypes.FilterNotStarted(ev) || ev.StartsAt == nil + return ev.Disabled || !evtypes.FilterNotStarted(ev) }) if len(notStartedEv) != 0 { @@ -131,56 +132,10 @@ func initOpener(ctx context.Context, cfg config.Config) error { return nil } - balancesQ := pg.NewBalances(cfg.DB().Clone()) - eventsQ := pg.NewEvents(cfg.DB().Clone()) - for _, ev := range notStartedEv { _, err := cron.NewJob( gocron.OneTimeJob(gocron.OneTimeJobStartDateTime(*ev.StartsAt)), - gocron.NewTask(func() { - log := cfg.Log().WithField("who", fmt.Sprintf("opener[%s]", ev.Name)) - - var balances []data.Balance - var err error - - for i := 0; i < 4; i++ { - if balances, err = balancesQ.New().FilterDisabled().Select(); err == nil { - break - } - - log.Errorf("Failed to get balances: %s [retry %d]", err, i) - time.Sleep(time.Second * 5) - } - - if err != nil { - log.Errorf("Failed to get balances: %s", err) - return - } - - events := make([]data.Event, len(balances)) - status := data.EventOpen - if ev.Name == evtypes.TypeFreeWeekly { - status = data.EventFulfilled - } - - for i, balance := range balances { - events[i] = data.Event{UserDID: balance.DID, Type: ev.Name, Status: status} - } - - for i := 0; i < 4; i++ { - if err = eventsQ.New().Insert(events...); err == nil { - break - } - - log.Errorf("Failed to insert events: %s [retry %d]", err, i) - time.Sleep(time.Second * 5) - } - - if err != nil { - log.Errorf("Failed to insert events: %s", err) - return - } - }, ctx), + gocron.NewTask(startingWatcher(cfg, ev.Name), ctx), gocron.WithName(fmt.Sprintf("opener[%s]", ev.Name)), ) @@ -191,3 +146,36 @@ func initOpener(ctx context.Context, cfg config.Config) error { return nil } + +func startingWatcher(cfg config.Config, name string) func(context.Context) { + return func(ctx context.Context) { + log := cfg.Log().WithField("who", fmt.Sprintf("opener[%s]", name)) + + var balances []data.Balance + var err error + + running.UntilSuccess(ctx, log, fmt.Sprintf("opener[%s]", name), func(context.Context) (bool, error) { + if balances, err = pg.NewBalances(cfg.DB().Clone()).FilterDisabled().Select(); err == nil { + return false, err + } + return true, nil + }, retryPeriod, retryPeriod) + + events := make([]data.Event, len(balances)) + status := data.EventOpen + if name == evtypes.TypeFreeWeekly { + status = data.EventFulfilled + } + + for i, balance := range balances { + events[i] = data.Event{UserDID: balance.DID, Type: name, Status: status} + } + + running.UntilSuccess(ctx, log, fmt.Sprintf("opener[%s]", name), func(context.Context) (bool, error) { + if err = pg.NewEvents(cfg.DB().Clone()).Insert(events...); err == nil { + return false, err + } + return true, nil + }, retryPeriod, retryPeriod) + } +} diff --git a/internal/service/workers/reopener/main.go b/internal/service/workers/reopener/main.go index fdcdb97..37da614 100644 --- a/internal/service/workers/reopener/main.go +++ b/internal/service/workers/reopener/main.go @@ -21,7 +21,7 @@ func Run(ctx context.Context, cfg config.Config) { cron.Init(cfg.Log()) - if err := initOpener(ctx, cfg); err != nil { + if err := runStartingWatchers(ctx, cfg); err != nil { panic(fmt.Errorf("reopener: failed to initialize opener: %w", err)) } From 73e29e9c05a3f6a947e0d614dd4e7fb21cec6bfd Mon Sep 17 00:00:00 2001 From: Roman Perebynos Date: Fri, 8 Mar 2024 18:30:16 +0200 Subject: [PATCH 04/42] Add /events/{id} endpoint. Add fields 'ShortDescription' and 'ActionalURL' for event types in config --- config.yaml | 9 +++++ .../components/schemas/EventStaticMeta.yaml | 14 +++++++ ...ions@rarime-points-svc@v1@events@{id}.yaml | 25 ++++++++++++ internal/data/evtypes/main.go | 36 ++++++++++-------- internal/service/handlers/get_event.go | 38 +++++++++++++++++++ internal/service/requests/get_event.go | 14 +++++++ internal/service/router.go | 1 + resources/model_balance_attributes.go | 2 +- resources/model_event_static_meta.go | 16 ++++++-- 9 files changed, 135 insertions(+), 20 deletions(-) create mode 100644 internal/service/handlers/get_event.go create mode 100644 internal/service/requests/get_event.go diff --git a/config.yaml b/config.yaml index 606c9a6..cdfbc4d 100644 --- a/config.yaml +++ b/config.yaml @@ -17,11 +17,14 @@ event_types: title: Points for passport scan reward: 200 description: Get points for scan passport and share data + short_description: Short description frequency: one-time + actional_url: https://... - name: get_poh title: Get PoH credential reward: 50 description: Prove that you are human + short_description: Short description frequency: one-time expires_at: 2020-01-01T00:00:00Z - name: free_weekly @@ -29,34 +32,40 @@ event_types: reward: 100 frequency: weekly description: Get free points every week by visiting the platform and claiming your reward + short_description: Short description - name: daily_login title: Daily login reward: 5 frequency: daily description: Login every day + short_description: Short description disabled: true - name: be_referred title: Referral welcome bonus reward: 5 frequency: one-time description: Be referred by a friend and get a welcome bonus + short_description: Short description no_auto_open: true - name: referral_common title: Refer new users reward: 25 frequency: one-time description: Refer friends and get a reward for each friend who verifies the passport + short_description: Short description - name: referral_specific title: Refer user {:did} reward: 25 frequency: unlimited description: The user {:did} has verified the passport. Claim the reward! + short_description: Short description no_auto_open: true - name: planned title: Planned event reward: 25 frequency: unlimited description: Event that start at specidied time + short_description: Short description starts_at: 2020-01-01T00:00:00Z auth: diff --git a/docs/spec/components/schemas/EventStaticMeta.yaml b/docs/spec/components/schemas/EventStaticMeta.yaml index f699917..e3d9d00 100644 --- a/docs/spec/components/schemas/EventStaticMeta.yaml +++ b/docs/spec/components/schemas/EventStaticMeta.yaml @@ -7,6 +7,7 @@ required: - reward - title - description + - short_description - frequency - no_auto_open properties: @@ -25,14 +26,27 @@ properties: description: type: string example: Lorem ipsum dolor sit amet + short_description: + type: string + example: Short description frequency: type: string description: | Event frequency, which means how often you can fulfill certain task and claim the reward. enum: [one-time, daily, weekly, unlimited] + starts_at: + type: string + format: time.Time + description: General event starting date (UTC RFC3339) + example: 2020-01-01T00:00:00Z expires_at: type: string format: time.Time description: General event expiration date (UTC RFC3339) example: 2020-01-01T00:00:00Z + actional_url: + type: string + format: url.URL + description: Actional URL + example: https:// diff --git a/docs/spec/paths/integrations@rarime-points-svc@v1@events@{id}.yaml b/docs/spec/paths/integrations@rarime-points-svc@v1@events@{id}.yaml index a65de9f..8933483 100644 --- a/docs/spec/paths/integrations@rarime-points-svc@v1@events@{id}.yaml +++ b/docs/spec/paths/integrations@rarime-points-svc@v1@events@{id}.yaml @@ -1,3 +1,28 @@ +get: + tags: + - Events + summary: Get event + description: Returns event for a single user. + operationId: getEvent + responses: + 200: + description: Success + content: + application/vnd.api+json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/Event' + 400: + $ref: '#/components/responses/invalidParameter' + 401: + $ref: '#/components/responses/invalidAuth' + 500: + $ref: '#/components/responses/internalError' + patch: tags: - Events diff --git a/internal/data/evtypes/main.go b/internal/data/evtypes/main.go index e383d1c..5d436e2 100644 --- a/internal/data/evtypes/main.go +++ b/internal/data/evtypes/main.go @@ -1,6 +1,7 @@ package evtypes import ( + "net/url" "time" "github.com/rarimo/rarime-points-svc/internal/data" @@ -34,25 +35,30 @@ const ( ) type EventConfig struct { - Name string `fig:"name,required"` - Description string `fig:"description,required"` - Reward int64 `fig:"reward,required"` - Title string `fig:"title,required"` - Frequency Frequency `fig:"frequency,required"` - ExpiresAt *time.Time `fig:"expires_at"` - StartsAt *time.Time `fig:"starts_at"` - NoAutoOpen bool `fig:"no_auto_open"` - Disabled bool `fig:"disabled"` + Name string `fig:"name,required"` + Description string `fig:"description,required"` + ShortDescription string `fig:"short_description,required"` + Reward int64 `fig:"reward,required"` + Title string `fig:"title,required"` + Frequency Frequency `fig:"frequency,required"` + StartsAt *time.Time `fig:"starts_at"` + ExpiresAt *time.Time `fig:"expires_at"` + NoAutoOpen bool `fig:"no_auto_open"` + Disabled bool `fig:"disabled"` + ActionalUrl *url.URL `fig:"actional_url"` } func (e EventConfig) Resource() resources.EventStaticMeta { return resources.EventStaticMeta{ - Name: e.Name, - Description: e.Description, - Reward: e.Reward, - Title: e.Title, - Frequency: e.Frequency.String(), - ExpiresAt: e.ExpiresAt, + Name: e.Name, + Description: e.Description, + ShortDescription: e.ShortDescription, + Reward: e.Reward, + Title: e.Title, + Frequency: e.Frequency.String(), + StartsAt: e.StartsAt, + ExpiresAt: e.ExpiresAt, + ActionalUrl: e.ActionalUrl, } } diff --git a/internal/service/handlers/get_event.go b/internal/service/handlers/get_event.go new file mode 100644 index 0000000..860faef --- /dev/null +++ b/internal/service/handlers/get_event.go @@ -0,0 +1,38 @@ +package handlers + +import ( + "net/http" + + "github.com/rarimo/auth-svc/pkg/auth" + "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" +) + +func GetEvent(w http.ResponseWriter, r *http.Request) { + req, err := requests.NewGetEvent(r) + if err != nil { + ape.RenderErr(w, problems.BadRequest(err)...) + return + } + + event, err := EventsQ(r).FilterByID(req).Get() + if err != nil { + Log(r).WithError(err).Error("Failed to get event by ID") + ape.RenderErr(w, problems.InternalError()) + return + } + + if event == nil { + ape.RenderErr(w, problems.NotFound()) + return + } + + if !auth.Authenticates(UserClaims(r), auth.UserGrant(event.UserDID)) { + ape.RenderErr(w, problems.Unauthorized()) + return + } + + ape.Render(w, resources.EventResponse{Data: newEventModel(*event, EventTypes(r).Get(event.Type).Resource())}) +} diff --git a/internal/service/requests/get_event.go b/internal/service/requests/get_event.go new file mode 100644 index 0000000..dda1840 --- /dev/null +++ b/internal/service/requests/get_event.go @@ -0,0 +1,14 @@ +package requests + +import ( + "net/http" + + "github.com/go-chi/chi" + validation "github.com/go-ozzo/ozzo-validation/v4" +) + +func NewGetEvent(r *http.Request) (id string, err error) { + id = chi.URLParam(r, "id") + return id, validation.Errors{"id": validation.Validate(id, validation.Required)}. + Filter() +} diff --git a/internal/service/router.go b/internal/service/router.go index a1c44ee..22ee095 100644 --- a/internal/service/router.go +++ b/internal/service/router.go @@ -36,6 +36,7 @@ func Run(ctx context.Context, cfg config.Config) { r.Route("/events", func(r chi.Router) { r.Use(handlers.AuthMiddleware(cfg.Auth(), cfg.Log())) r.Get("/", handlers.ListEvents) + r.Get("/{id}", handlers.GetEvent) r.Patch("/{id}", handlers.ClaimEvent) }) r.Get("/balances", handlers.Leaderboard) diff --git a/resources/model_balance_attributes.go b/resources/model_balance_attributes.go index 24478b1..0dcced3 100644 --- a/resources/model_balance_attributes.go +++ b/resources/model_balance_attributes.go @@ -15,7 +15,7 @@ type BalanceAttributes struct { IsVerified bool `json:"is_verified"` // Rank of the user in the full leaderboard. Returned only for the single user. Rank *int `json:"rank,omitempty"` - // Referral codes used to build a referral link and send it to friends + // Referral codes used to build a referral link and send it to friends. Required if a balance is created ReferralCodes *[]string `json:"referral_codes,omitempty"` // Unix timestamp of the last points accruing UpdatedAt int32 `json:"updated_at"` diff --git a/resources/model_event_static_meta.go b/resources/model_event_static_meta.go index 16639e5..eebb7af 100644 --- a/resources/model_event_static_meta.go +++ b/resources/model_event_static_meta.go @@ -4,11 +4,16 @@ package resources -import "time" +import ( + "net/url" + "time" +) // Primary event metadata in plain JSON. This is a template to be filled by `dynamic` when it's present. type EventStaticMeta struct { - Description string `json:"description"` + // Actional URL + ActionalUrl *url.URL `json:"actional_url,omitempty"` + Description string `json:"description"` // General event expiration date (UTC RFC3339) ExpiresAt *time.Time `json:"expires_at,omitempty"` // Event frequency, which means how often you can fulfill certain task and claim the reward. @@ -16,6 +21,9 @@ type EventStaticMeta struct { // Unique event code name Name string `json:"name"` // Reward amount in points - Reward int64 `json:"reward"` - Title string `json:"title"` + Reward int64 `json:"reward"` + ShortDescription string `json:"short_description"` + // General event starting date (UTC RFC3339) + StartsAt *time.Time `json:"starts_at,omitempty"` + Title string `json:"title"` } From 3fb07974e92f7413814709ac1edf66a42ee86e90 Mon Sep 17 00:00:00 2001 From: violog <51th.apprent1ce.f0rce@gmail.com> Date: Mon, 11 Mar 2024 10:28:11 +0200 Subject: [PATCH 05/42] Typos fixes and tiny improvements --- config.yaml | 4 ++-- docs/spec/components/schemas/EventStaticMeta.yaml | 5 ++--- .../integrations@rarime-points-svc@v1@events@{id}.yaml | 4 +++- internal/data/evtypes/main.go | 4 ++-- internal/service/handlers/get_event.go | 4 ++-- internal/service/workers/expirywatch/main.go | 1 + internal/service/workers/expirywatch/watcher.go | 2 +- internal/service/workers/reopener/init.go | 8 ++++---- internal/service/workers/reopener/main.go | 1 + internal/service/workers/reopener/worker.go | 2 +- resources/db.go | 4 ++-- resources/included.go | 4 ++-- resources/model_details.go | 8 ++++---- resources/model_event_static_meta.go | 3 +-- 14 files changed, 28 insertions(+), 26 deletions(-) diff --git a/config.yaml b/config.yaml index cdfbc4d..dde1a0d 100644 --- a/config.yaml +++ b/config.yaml @@ -19,7 +19,7 @@ event_types: description: Get points for scan passport and share data short_description: Short description frequency: one-time - actional_url: https://... + action_url: https://... - name: get_poh title: Get PoH credential reward: 50 @@ -64,7 +64,7 @@ event_types: title: Planned event reward: 25 frequency: unlimited - description: Event that start at specidied time + description: Event that start at specified time short_description: Short description starts_at: 2020-01-01T00:00:00Z diff --git a/docs/spec/components/schemas/EventStaticMeta.yaml b/docs/spec/components/schemas/EventStaticMeta.yaml index e3d9d00..b2b7089 100644 --- a/docs/spec/components/schemas/EventStaticMeta.yaml +++ b/docs/spec/components/schemas/EventStaticMeta.yaml @@ -45,8 +45,7 @@ properties: format: time.Time description: General event expiration date (UTC RFC3339) example: 2020-01-01T00:00:00Z - actional_url: + action_url: type: string format: url.URL - description: Actional URL - example: https:// + example: https://robotornot.rarimo.com diff --git a/docs/spec/paths/integrations@rarime-points-svc@v1@events@{id}.yaml b/docs/spec/paths/integrations@rarime-points-svc@v1@events@{id}.yaml index 8933483..0756514 100644 --- a/docs/spec/paths/integrations@rarime-points-svc@v1@events@{id}.yaml +++ b/docs/spec/paths/integrations@rarime-points-svc@v1@events@{id}.yaml @@ -2,7 +2,7 @@ get: tags: - Events summary: Get event - description: Returns event for a single user. + description: Returns a single event by ID. operationId: getEvent responses: 200: @@ -20,6 +20,8 @@ get: $ref: '#/components/responses/invalidParameter' 401: $ref: '#/components/responses/invalidAuth' + 404: + $ref: '#/components/responses/notFound' 500: $ref: '#/components/responses/internalError' diff --git a/internal/data/evtypes/main.go b/internal/data/evtypes/main.go index 5d436e2..7ded0a7 100644 --- a/internal/data/evtypes/main.go +++ b/internal/data/evtypes/main.go @@ -45,7 +45,7 @@ type EventConfig struct { ExpiresAt *time.Time `fig:"expires_at"` NoAutoOpen bool `fig:"no_auto_open"` Disabled bool `fig:"disabled"` - ActionalUrl *url.URL `fig:"actional_url"` + ActionURL *url.URL `fig:"action_url"` } func (e EventConfig) Resource() resources.EventStaticMeta { @@ -58,7 +58,7 @@ func (e EventConfig) Resource() resources.EventStaticMeta { Frequency: e.Frequency.String(), StartsAt: e.StartsAt, ExpiresAt: e.ExpiresAt, - ActionalUrl: e.ActionalUrl, + ActionUrl: e.ActionURL, } } diff --git a/internal/service/handlers/get_event.go b/internal/service/handlers/get_event.go index 860faef..cefed0a 100644 --- a/internal/service/handlers/get_event.go +++ b/internal/service/handlers/get_event.go @@ -11,13 +11,13 @@ import ( ) func GetEvent(w http.ResponseWriter, r *http.Request) { - req, err := requests.NewGetEvent(r) + id, err := requests.NewGetEvent(r) if err != nil { ape.RenderErr(w, problems.BadRequest(err)...) return } - event, err := EventsQ(r).FilterByID(req).Get() + event, err := EventsQ(r).FilterByID(id).Get() if err != nil { Log(r).WithError(err).Error("Failed to get event by ID") ape.RenderErr(w, problems.InternalError()) diff --git a/internal/service/workers/expirywatch/main.go b/internal/service/workers/expirywatch/main.go index f1a6229..5f2616c 100644 --- a/internal/service/workers/expirywatch/main.go +++ b/internal/service/workers/expirywatch/main.go @@ -12,6 +12,7 @@ import ( ) const retryPeriod = 1 * time.Minute +const maxRetries = 12 func Run(ctx context.Context, cfg config.Config) { w := newWatcher(cfg) diff --git a/internal/service/workers/expirywatch/watcher.go b/internal/service/workers/expirywatch/watcher.go index 8c64d06..23d99ea 100644 --- a/internal/service/workers/expirywatch/watcher.go +++ b/internal/service/workers/expirywatch/watcher.go @@ -58,5 +58,5 @@ func (w *watcher) job(ctx context.Context, evType string) { return false, fmt.Errorf("clean open events: %w", err) } return true, nil - }, retryPeriod, retryPeriod, 12) + }, retryPeriod, retryPeriod, maxRetries) } diff --git a/internal/service/workers/reopener/init.go b/internal/service/workers/reopener/init.go index 8e56df6..765a4c7 100644 --- a/internal/service/workers/reopener/init.go +++ b/internal/service/workers/reopener/init.go @@ -154,12 +154,12 @@ func startingWatcher(cfg config.Config, name string) func(context.Context) { var balances []data.Balance var err error - running.UntilSuccess(ctx, log, fmt.Sprintf("opener[%s]", name), func(context.Context) (bool, error) { + running.WithThreshold(ctx, log, fmt.Sprintf("opener[%s]", name), func(context.Context) (bool, error) { if balances, err = pg.NewBalances(cfg.DB().Clone()).FilterDisabled().Select(); err == nil { return false, err } return true, nil - }, retryPeriod, retryPeriod) + }, retryPeriod, retryPeriod, maxRetries) events := make([]data.Event, len(balances)) status := data.EventOpen @@ -171,11 +171,11 @@ func startingWatcher(cfg config.Config, name string) func(context.Context) { events[i] = data.Event{UserDID: balance.DID, Type: name, Status: status} } - running.UntilSuccess(ctx, log, fmt.Sprintf("opener[%s]", name), func(context.Context) (bool, error) { + running.WithThreshold(ctx, log, fmt.Sprintf("opener[%s]", name), func(context.Context) (bool, error) { if err = pg.NewEvents(cfg.DB().Clone()).Insert(events...); err == nil { return false, err } return true, nil - }, retryPeriod, retryPeriod) + }, retryPeriod, retryPeriod, maxRetries) } } diff --git a/internal/service/workers/reopener/main.go b/internal/service/workers/reopener/main.go index 37da614..54ddce5 100644 --- a/internal/service/workers/reopener/main.go +++ b/internal/service/workers/reopener/main.go @@ -13,6 +13,7 @@ import ( ) const retryPeriod = 5 * time.Minute +const maxRetries = 12 func Run(ctx context.Context, cfg config.Config) { if err := initialRun(cfg); err != nil { diff --git a/internal/service/workers/reopener/worker.go b/internal/service/workers/reopener/worker.go index e0b4285..716a24d 100644 --- a/internal/service/workers/reopener/worker.go +++ b/internal/service/workers/reopener/worker.go @@ -45,7 +45,7 @@ func (w *worker) job(ctx context.Context) { return false, fmt.Errorf("reopen events: %w", err) } return true, nil - }, retryPeriod, retryPeriod, 12) + }, retryPeriod, retryPeriod, maxRetries) } func (w *worker) reopenEvents(types []string) error { diff --git a/resources/db.go b/resources/db.go index 0c07f2f..5381330 100644 --- a/resources/db.go +++ b/resources/db.go @@ -12,7 +12,7 @@ import ( "gitlab.com/distributed_lab/logan/v3/errors" ) -//driverValue - converts interface into db supported type +// driverValue - converts interface into db supported type func driverValue(data interface{}) (driver.Value, error) { data, err := json.Marshal(data) if err != nil { @@ -22,7 +22,7 @@ func driverValue(data interface{}) (driver.Value, error) { return data, nil } -//driveScan - converts jsonb into type struct +// driveScan - converts jsonb into type struct func driveScan(src, dest interface{}) error { data, err := convertJSONB(src) if err != nil { diff --git a/resources/included.go b/resources/included.go index 889272d..68d70a2 100644 --- a/resources/included.go +++ b/resources/included.go @@ -46,7 +46,7 @@ func (c *Included) add(include Resource) { c.includes[include.GetKey()] = json.RawMessage(data) } -//MarshalJSON - marshals include collection as array of json objects +// MarshalJSON - marshals include collection as array of json objects func (c Included) MarshalJSON() ([]byte, error) { uniqueEntries := make([]json.RawMessage, 0, len(c.includes)) for _, value := range c.includes { @@ -56,7 +56,7 @@ func (c Included) MarshalJSON() ([]byte, error) { return json.Marshal(uniqueEntries) } -//UmarshalJSON - unmarshal array of json objects into include collection +// UmarshalJSON - unmarshal array of json objects into include collection func (c *Included) UnmarshalJSON(data []byte) error { var keys []Key err := json.Unmarshal(data, &keys) diff --git a/resources/model_details.go b/resources/model_details.go index 303c93d..36e0d24 100644 --- a/resources/model_details.go +++ b/resources/model_details.go @@ -13,7 +13,7 @@ import ( type Details json.RawMessage -//UnmarshalJSON - casts data to Details +// UnmarshalJSON - casts data to Details func (d *Details) UnmarshalJSON(data []byte) error { if d == nil { return errors.New("regources.Details: UnmarshalJSON on nil pointer") @@ -22,7 +22,7 @@ func (d *Details) UnmarshalJSON(data []byte) error { return nil } -//MarshalJSON - casts Details to []byte +// MarshalJSON - casts Details to []byte func (d Details) MarshalJSON() ([]byte, error) { if d == nil { return []byte("null"), nil @@ -34,7 +34,7 @@ func (d Details) String() string { return string(d) } -//Value - implements db driver method for auto marshal +// Value - implements db driver method for auto marshal func (r Details) Value() (driver.Value, error) { result, err := json.Marshal(r) if err != nil { @@ -44,7 +44,7 @@ func (r Details) Value() (driver.Value, error) { return result, nil } -//Scan - implements db driver method for auto unmarshal +// Scan - implements db driver method for auto unmarshal func (r *Details) Scan(src interface{}) error { var data []byte switch rawData := src.(type) { diff --git a/resources/model_event_static_meta.go b/resources/model_event_static_meta.go index eebb7af..4bd02d7 100644 --- a/resources/model_event_static_meta.go +++ b/resources/model_event_static_meta.go @@ -11,8 +11,7 @@ import ( // Primary event metadata in plain JSON. This is a template to be filled by `dynamic` when it's present. type EventStaticMeta struct { - // Actional URL - ActionalUrl *url.URL `json:"actional_url,omitempty"` + ActionUrl *url.URL `json:"action_url,omitempty"` Description string `json:"description"` // General event expiration date (UTC RFC3339) ExpiresAt *time.Time `json:"expires_at,omitempty"` From b7cd17dd3ccaed0486a2a36d8266da928bab355f Mon Sep 17 00:00:00 2001 From: violog <51th.apprent1ce.f0rce@gmail.com> Date: Mon, 11 Mar 2024 10:38:29 +0200 Subject: [PATCH 06/42] Filter inactive types on getting event too --- internal/service/handlers/get_event.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/internal/service/handlers/get_event.go b/internal/service/handlers/get_event.go index cefed0a..a23e272 100644 --- a/internal/service/handlers/get_event.go +++ b/internal/service/handlers/get_event.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/rarimo/auth-svc/pkg/auth" + "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" @@ -29,10 +30,17 @@ func GetEvent(w http.ResponseWriter, r *http.Request) { return } + evType := EventTypes(r).Get(event.Type, evtypes.FilterInactive) + if evType == nil { + Log(r).Debugf("Event type is not active at this moment: %s", event.Type) + ape.RenderErr(w, problems.NotFound()) + return + } + if !auth.Authenticates(UserClaims(r), auth.UserGrant(event.UserDID)) { ape.RenderErr(w, problems.Unauthorized()) return } - ape.Render(w, resources.EventResponse{Data: newEventModel(*event, EventTypes(r).Get(event.Type).Resource())}) + ape.Render(w, resources.EventResponse{Data: newEventModel(*event, evType.Resource())}) } From fc551533182a87246ad644e233ac96ab4fb3fc0a Mon Sep 17 00:00:00 2001 From: Roman Perebynos Date: Mon, 11 Mar 2024 11:56:09 +0200 Subject: [PATCH 07/42] Add check if passport_hash already in use --- internal/data/balances.go | 1 + internal/data/pg/balances.go | 4 ++++ internal/service/handlers/verify_passport.go | 17 +++++++++++++++-- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/internal/data/balances.go b/internal/data/balances.go index 14ff88c..c1b91fc 100644 --- a/internal/data/balances.go +++ b/internal/data/balances.go @@ -32,5 +32,6 @@ type BalancesQ interface { GetWithRank(did string) (*Balance, error) FilterByDID(string) BalancesQ + FilterByPassportHash(string) BalancesQ FilterDisabled() BalancesQ } diff --git a/internal/data/pg/balances.go b/internal/data/pg/balances.go index 7ed3a55..9aac0e7 100644 --- a/internal/data/pg/balances.go +++ b/internal/data/pg/balances.go @@ -133,6 +133,10 @@ func (q *balances) FilterByDID(did string) data.BalancesQ { return q.applyCondition(squirrel.Eq{"did": did}) } +func (q *balances) FilterByPassportHash(passportHash string) data.BalancesQ { + return q.applyCondition(squirrel.Eq{"passport_hash": passportHash}) +} + func (q *balances) FilterDisabled() data.BalancesQ { return q.applyCondition(squirrel.NotEq{"referred_by": nil}) } diff --git a/internal/service/handlers/verify_passport.go b/internal/service/handlers/verify_passport.go index 590c2c2..a95a506 100644 --- a/internal/service/handlers/verify_passport.go +++ b/internal/service/handlers/verify_passport.go @@ -26,7 +26,20 @@ func VerifyPassport(w http.ResponseWriter, r *http.Request) { "expiry": req.Expiry.String(), }) - balance, err := BalancesQ(r).FilterByDID(req.UserDID).Get() + balance, err := BalancesQ(r).FilterByPassportHash(req.Hash).Get() + if err != nil { + log.WithError(err).Error("Failed to get balance by DID") + ape.RenderErr(w, problems.InternalError()) + return + } + + if balance != nil { + log.Error("passport_hash already in use") + ape.RenderErr(w, problems.Conflict()) + return + } + + balance, err = BalancesQ(r).FilterByDID(req.UserDID).Get() if err != nil { log.WithError(err).Error("Failed to get balance by DID") ape.RenderErr(w, problems.InternalError()) @@ -34,7 +47,7 @@ func VerifyPassport(w http.ResponseWriter, r *http.Request) { } var reward int64 - logMsgScan := "PassportScan event type is disabled or expired, not accruing points" + logMsgScan := "PassportScan event type is disabled or expired, not accuring points" evType := EventTypes(r).Get(evtypes.TypePassportScan, evtypes.FilterInactive) if evType != nil { var success bool From ddb25100bf8c011ab55e8966520d7aaf7a4b181f Mon Sep 17 00:00:00 2001 From: Zaptoss Date: Mon, 11 Mar 2024 12:16:21 +0200 Subject: [PATCH 08/42] If user try to set own passport and passport_hash already set, return NoContent --- internal/service/handlers/verify_passport.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/internal/service/handlers/verify_passport.go b/internal/service/handlers/verify_passport.go index a95a506..faab88c 100644 --- a/internal/service/handlers/verify_passport.go +++ b/internal/service/handlers/verify_passport.go @@ -34,8 +34,13 @@ func VerifyPassport(w http.ResponseWriter, r *http.Request) { } if balance != nil { - log.Error("passport_hash already in use") - ape.RenderErr(w, problems.Conflict()) + if balance.DID != req.UserDID { + log.Error("passport_hash already in use") + ape.RenderErr(w, problems.Conflict()) + return + } + + w.WriteHeader(http.StatusNoContent) return } @@ -47,7 +52,7 @@ func VerifyPassport(w http.ResponseWriter, r *http.Request) { } var reward int64 - logMsgScan := "PassportScan event type is disabled or expired, not accuring points" + logMsgScan := "PassportScan event type is disabled or expired, not accruing points" evType := EventTypes(r).Get(evtypes.TypePassportScan, evtypes.FilterInactive) if evType != nil { var success bool From d5564329cccdf70b43d5a28239ea9a34def04ac2 Mon Sep 17 00:00:00 2001 From: violog <51th.apprent1ce.f0rce@gmail.com> Date: Mon, 11 Mar 2024 13:39:28 +0200 Subject: [PATCH 09/42] Drop unnecessary utility function from passport handler --- internal/service/handlers/verify_passport.go | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/internal/service/handlers/verify_passport.go b/internal/service/handlers/verify_passport.go index faab88c..8fb1114 100644 --- a/internal/service/handlers/verify_passport.go +++ b/internal/service/handlers/verify_passport.go @@ -142,10 +142,7 @@ func VerifyPassport(w http.ResponseWriter, r *http.Request) { return nil } - refDID, err := getReferrerDID(*balance, r) - if err != nil { - return fmt.Errorf("get referrer DID by referred_by: %w", err) - } + refDID := balance.ReferredBy.String if refDID == "" { return nil } @@ -171,13 +168,3 @@ func VerifyPassport(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) } - -// TODO: implement new referrals flow -func getReferrerDID(balance data.Balance, r *http.Request) (string, error) { - if !balance.ReferredBy.Valid { - return "", nil - } - - refBy := balance.ReferredBy.String - return refBy, nil -} From 3d478ca8cab868e8cbf095d565173cc0201ad2cd Mon Sep 17 00:00:00 2001 From: Zaptoss Date: Mon, 11 Mar 2024 16:55:05 +0200 Subject: [PATCH 10/42] Change passport expiration logic --- internal/service/handlers/verify_passport.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/internal/service/handlers/verify_passport.go b/internal/service/handlers/verify_passport.go index 8fb1114..6c2fc3f 100644 --- a/internal/service/handlers/verify_passport.go +++ b/internal/service/handlers/verify_passport.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "net/http" + "time" "github.com/rarimo/rarime-points-svc/internal/data" "github.com/rarimo/rarime-points-svc/internal/data/evtypes" @@ -26,6 +27,8 @@ func VerifyPassport(w http.ResponseWriter, r *http.Request) { "expiry": req.Expiry.String(), }) + expireDate := time.Now().UTC().AddDate(0, 1, 0) + balance, err := BalancesQ(r).FilterByPassportHash(req.Hash).Get() if err != nil { log.WithError(err).Error("Failed to get balance by DID") @@ -40,6 +43,13 @@ func VerifyPassport(w http.ResponseWriter, r *http.Request) { return } + err = BalancesQ(r).FilterByDID(req.UserDID).SetPassport(balance.PassportHash.String, expireDate) + if err != nil { + log.WithError(err).Error("Failed to set expiration date") + ape.RenderErr(w, problems.InternalError()) + return + } + w.WriteHeader(http.StatusNoContent) return } @@ -79,7 +89,7 @@ func VerifyPassport(w http.ResponseWriter, r *http.Request) { balance = &data.Balance{ DID: req.UserDID, PassportHash: sql.NullString{String: req.Hash, Valid: true}, - PassportExpires: sql.NullTime{Time: req.Expiry, Valid: true}, + PassportExpires: sql.NullTime{Time: expireDate, Valid: true}, } if err = BalancesQ(r).Insert(*balance); err != nil { @@ -106,7 +116,7 @@ func VerifyPassport(w http.ResponseWriter, r *http.Request) { err = EventsQ(r).Transaction(func() error { // If you make this endpoint public, you should check the passport hash for // uniqueness and provide a better validation. Think about other changes too. - err = BalancesQ(r).FilterByDID(req.UserDID).SetPassport(req.Hash, req.Expiry) + err = BalancesQ(r).FilterByDID(req.UserDID).SetPassport(req.Hash, expireDate) if err != nil { return fmt.Errorf("set passport for balance by DID: %w", err) } From 6353f4b58dfb84b916ec3fa8e61b2d9d4280bd97 Mon Sep 17 00:00:00 2001 From: Zaptoss Date: Mon, 11 Mar 2024 17:27:41 +0200 Subject: [PATCH 11/42] Remove unnecessary request in DB in verify_passport --- internal/service/handlers/verify_passport.go | 131 +++++++++---------- 1 file changed, 62 insertions(+), 69 deletions(-) diff --git a/internal/service/handlers/verify_passport.go b/internal/service/handlers/verify_passport.go index 6c2fc3f..7b0ae5d 100644 --- a/internal/service/handlers/verify_passport.go +++ b/internal/service/handlers/verify_passport.go @@ -29,32 +29,7 @@ func VerifyPassport(w http.ResponseWriter, r *http.Request) { expireDate := time.Now().UTC().AddDate(0, 1, 0) - balance, err := BalancesQ(r).FilterByPassportHash(req.Hash).Get() - if err != nil { - log.WithError(err).Error("Failed to get balance by DID") - ape.RenderErr(w, problems.InternalError()) - return - } - - if balance != nil { - if balance.DID != req.UserDID { - log.Error("passport_hash already in use") - ape.RenderErr(w, problems.Conflict()) - return - } - - err = BalancesQ(r).FilterByDID(req.UserDID).SetPassport(balance.PassportHash.String, expireDate) - if err != nil { - log.WithError(err).Error("Failed to set expiration date") - ape.RenderErr(w, problems.InternalError()) - return - } - - w.WriteHeader(http.StatusNoContent) - return - } - - balance, err = BalancesQ(r).FilterByDID(req.UserDID).Get() + balance, err := BalancesQ(r).FilterByDID(req.UserDID).Get() if err != nil { log.WithError(err).Error("Failed to get balance by DID") ape.RenderErr(w, problems.InternalError()) @@ -113,65 +88,83 @@ func VerifyPassport(w http.ResponseWriter, r *http.Request) { return } - err = EventsQ(r).Transaction(func() error { - // If you make this endpoint public, you should check the passport hash for - // uniqueness and provide a better validation. Think about other changes too. - err = BalancesQ(r).FilterByDID(req.UserDID).SetPassport(req.Hash, expireDate) - if err != nil { - return fmt.Errorf("set passport for balance by DID: %w", err) - } - - if evType != nil { - passportScanEvent, err := EventsQ(r). - FilterByUserDID(req.UserDID). - FilterByType(evtypes.TypePassportScan). - FilterByStatus(data.EventOpen). - Get() + if !balance.PassportHash.Valid { + err = EventsQ(r).Transaction(func() error { + // If you make this endpoint public, you should check the passport hash for + // uniqueness and provide a better validation. Think about other changes too. + err = BalancesQ(r).FilterByDID(req.UserDID).SetPassport(req.Hash, expireDate) if err != nil { - return fmt.Errorf("get passport_scan event by DID: %w", err) + return fmt.Errorf("set passport for balance by DID: %w", err) } - logMsgOpenE := "PassportScan event not open" - if passportScanEvent != nil { - _, err = EventsQ(r). + + if evType != nil { + passportScanEvent, err := EventsQ(r). FilterByUserDID(req.UserDID). FilterByType(evtypes.TypePassportScan). - Update(data.EventFulfilled, json.RawMessage(passportScanEvent.Meta), &reward) + FilterByStatus(data.EventOpen). + Get() if err != nil { - return fmt.Errorf("update reward for passport_scan event by DID: %w", err) + return fmt.Errorf("get passport_scan event by DID: %w", err) } - logMsgOpenE = "PassportScan event open" - logMsgScan = "PassportScan event reward update successful" + logMsgOpenE := "PassportScan event not open" + if passportScanEvent != nil { + _, err = EventsQ(r). + FilterByUserDID(req.UserDID). + FilterByType(evtypes.TypePassportScan). + Update(data.EventFulfilled, json.RawMessage(passportScanEvent.Meta), &reward) + if err != nil { + return fmt.Errorf("update reward for passport_scan event by DID: %w", err) + } + logMsgOpenE = "PassportScan event open" + logMsgScan = "PassportScan event reward update successful" + } + log.Debug(logMsgOpenE) } - log.Debug(logMsgOpenE) - } - log.Debug(logMsgScan) + log.Debug(logMsgScan) - evType = EventTypes(r).Get(evtypes.TypeReferralSpecific, evtypes.FilterInactive) - if evType == nil { - log.Debug("Referral event type is disabled or expired, not accruing points to referrer") - return nil - } + evType = EventTypes(r).Get(evtypes.TypeReferralSpecific, evtypes.FilterInactive) + if evType == nil { + log.Debug("Referral event type is disabled or expired, not accruing points to referrer") + return nil + } - refDID := balance.ReferredBy.String - if refDID == "" { - return nil - } + refDID := balance.ReferredBy.String + if refDID == "" { + return nil + } + + err = EventsQ(r).Insert(data.Event{ + UserDID: refDID, + Type: evType.Name, + Status: data.EventFulfilled, + Meta: data.Jsonb(fmt.Sprintf(`{"did": "%s"}`, req.UserDID)), + }) + if err != nil { + return fmt.Errorf("add event for referrer: %w", err) + } - err = EventsQ(r).Insert(data.Event{ - UserDID: refDID, - Type: evType.Name, - Status: data.EventFulfilled, - Meta: data.Jsonb(fmt.Sprintf(`{"did": "%s"}`, req.UserDID)), + return nil }) + if err != nil { - return fmt.Errorf("add event for referrer: %w", err) + log.WithError(err).Error("Failed to set passport and add event for referrer") + ape.RenderErr(w, problems.InternalError()) + return } - return nil - }) + w.WriteHeader(http.StatusNoContent) + return + } + + if balance.PassportHash.String != req.Hash { + log.Error("passport_hash already in use") + ape.RenderErr(w, problems.Conflict()) + return + } + err = BalancesQ(r).FilterByDID(req.UserDID).SetPassport(balance.PassportHash.String, expireDate) if err != nil { - log.WithError(err).Error("Failed to set passport and add event for referrer") + log.WithError(err).Error("Failed to set expiration date") ape.RenderErr(w, problems.InternalError()) return } From 2c6a4c6f54aa3ca67211e9992af718f71e726889 Mon Sep 17 00:00:00 2001 From: Zaptoss Date: Tue, 12 Mar 2024 16:00:29 +0200 Subject: [PATCH 12/42] verify_passport handler refactoring --- internal/data/evtypes/main.go | 7 +- internal/service/handlers/verify_passport.go | 219 +++++++++++-------- internal/service/requests/verify_passport.go | 16 -- pkg/connector/models.go | 8 +- 4 files changed, 130 insertions(+), 120 deletions(-) diff --git a/internal/data/evtypes/main.go b/internal/data/evtypes/main.go index 7ded0a7..7d27d26 100644 --- a/internal/data/evtypes/main.go +++ b/internal/data/evtypes/main.go @@ -133,13 +133,14 @@ func (t Types) ensureInitialized() { } } -func (t Types) CalculatePassportScanReward(sharedFields ...string) (reward int64, success bool) { +func (t Types) CalculatePassportScanReward(sharedFields ...string) (*int64, bool) { + var reward int64 for _, field := range sharedFields { val, ok := t.passportRewards[field] if !ok { - return 0, false + return nil, false } reward += int64(val) } - return reward, true + return &reward, true } diff --git a/internal/service/handlers/verify_passport.go b/internal/service/handlers/verify_passport.go index 7b0ae5d..e87bb04 100644 --- a/internal/service/handlers/verify_passport.go +++ b/internal/service/handlers/verify_passport.go @@ -10,6 +10,7 @@ import ( "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/pkg/connector" "gitlab.com/distributed_lab/ape" "gitlab.com/distributed_lab/ape/problems" ) @@ -21,29 +22,41 @@ func VerifyPassport(w http.ResponseWriter, r *http.Request) { ape.RenderErr(w, problems.BadRequest(err)...) return } + log := Log(r).WithFields(map[string]any{ "user_did": req.UserDID, "hash": req.Hash, - "expiry": req.Expiry.String(), }) - expireDate := time.Now().UTC().AddDate(0, 1, 0) - - balance, err := BalancesQ(r).FilterByDID(req.UserDID).Get() + balance, err := BalancesQ(r).FilterByPassportHash(req.Hash).Get() if err != nil { - log.WithError(err).Error("Failed to get balance by DID") + log.WithError(err).Error("Failed to get balance by Hash") ape.RenderErr(w, problems.InternalError()) return } - var reward int64 - logMsgScan := "PassportScan event type is disabled or expired, not accruing points" + if balance != nil && balance.DID != req.UserDID { + log.Error("passport_hash already in use") + ape.RenderErr(w, problems.Conflict()) + return + } + + if balance == nil { + balance, err = BalancesQ(r).FilterByDID(req.UserDID).Get() + if err != nil { + log.WithError(err).Error("Failed to get balance by DID") + ape.RenderErr(w, problems.InternalError()) + return + } + } + + var reward *int64 evType := EventTypes(r).Get(evtypes.TypePassportScan, evtypes.FilterInactive) if evType != nil { var success bool reward, success = EventTypes(r).CalculatePassportScanReward(req.SharedData...) if !success { - log.WithError(err).Error("Failed to calculate PassportScanReward, incorrect fields") + log.Error("Failed to calculate PassportScanReward, incorrect fields") ape.RenderErr(w, problems.NotFound()) return } @@ -51,32 +64,8 @@ func VerifyPassport(w http.ResponseWriter, r *http.Request) { if balance == nil { log.Debug("Balance not found, creating new one") - events := EventTypes(r).PrepareEvents(req.UserDID, evtypes.FilterNotOpenable) - - for i := 0; i < len(events); i++ { - if events[i].Type == evtypes.TypePassportScan { - events[i].PointsAmount = &reward - events[i].Status = data.EventFulfilled - } - } - - err = EventsQ(r).Transaction(func() error { - balance = &data.Balance{ - DID: req.UserDID, - PassportHash: sql.NullString{String: req.Hash, Valid: true}, - PassportExpires: sql.NullTime{Time: expireDate, Valid: true}, - } - - if err = BalancesQ(r).Insert(*balance); err != nil { - return fmt.Errorf("add balance: %w", err) - } - log.Debugf("%d events will be added for user_did=%s", len(events), req.UserDID) - if err = EventsQ(r).Insert(events...); err != nil { - return fmt.Errorf("add open events: %w", err) - } - return nil - }) + err = createBalanceWithPassportTx(r, req, reward) if err != nil { log.WithError(err).Error("Failed to create balance with events") @@ -89,62 +78,7 @@ func VerifyPassport(w http.ResponseWriter, r *http.Request) { } if !balance.PassportHash.Valid { - err = EventsQ(r).Transaction(func() error { - // If you make this endpoint public, you should check the passport hash for - // uniqueness and provide a better validation. Think about other changes too. - err = BalancesQ(r).FilterByDID(req.UserDID).SetPassport(req.Hash, expireDate) - if err != nil { - return fmt.Errorf("set passport for balance by DID: %w", err) - } - - if evType != nil { - passportScanEvent, err := EventsQ(r). - FilterByUserDID(req.UserDID). - FilterByType(evtypes.TypePassportScan). - FilterByStatus(data.EventOpen). - Get() - if err != nil { - return fmt.Errorf("get passport_scan event by DID: %w", err) - } - logMsgOpenE := "PassportScan event not open" - if passportScanEvent != nil { - _, err = EventsQ(r). - FilterByUserDID(req.UserDID). - FilterByType(evtypes.TypePassportScan). - Update(data.EventFulfilled, json.RawMessage(passportScanEvent.Meta), &reward) - if err != nil { - return fmt.Errorf("update reward for passport_scan event by DID: %w", err) - } - logMsgOpenE = "PassportScan event open" - logMsgScan = "PassportScan event reward update successful" - } - log.Debug(logMsgOpenE) - } - log.Debug(logMsgScan) - - evType = EventTypes(r).Get(evtypes.TypeReferralSpecific, evtypes.FilterInactive) - if evType == nil { - log.Debug("Referral event type is disabled or expired, not accruing points to referrer") - return nil - } - - refDID := balance.ReferredBy.String - if refDID == "" { - return nil - } - - err = EventsQ(r).Insert(data.Event{ - UserDID: refDID, - Type: evType.Name, - Status: data.EventFulfilled, - Meta: data.Jsonb(fmt.Sprintf(`{"did": "%s"}`, req.UserDID)), - }) - if err != nil { - return fmt.Errorf("add event for referrer: %w", err) - } - - return nil - }) + err = setBalancePassportTx(r, req, reward, balance.ReferredBy.String) if err != nil { log.WithError(err).Error("Failed to set passport and add event for referrer") @@ -156,13 +90,7 @@ func VerifyPassport(w http.ResponseWriter, r *http.Request) { return } - if balance.PassportHash.String != req.Hash { - log.Error("passport_hash already in use") - ape.RenderErr(w, problems.Conflict()) - return - } - - err = BalancesQ(r).FilterByDID(req.UserDID).SetPassport(balance.PassportHash.String, expireDate) + err = BalancesQ(r).FilterByDID(req.UserDID).SetPassport(balance.PassportHash.String, time.Now().UTC().AddDate(0, 1, 0)) if err != nil { log.WithError(err).Error("Failed to set expiration date") ape.RenderErr(w, problems.InternalError()) @@ -171,3 +99,102 @@ func VerifyPassport(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) } + +func createBalanceWithPassportTx(r *http.Request, req connector.VerifyPassportRequest, reward *int64) error { + events := EventTypes(r).PrepareEvents(req.UserDID, evtypes.FilterNotOpenable) + + log := Log(r).WithFields(map[string]any{ + "user_did": req.UserDID, + "hash": req.Hash, + }) + + for i := 0; i < len(events); i++ { + if events[i].Type == evtypes.TypePassportScan { + events[i].PointsAmount = reward + events[i].Status = data.EventFulfilled + } + } + + return EventsQ(r).Transaction(func() (err error) { + balance := &data.Balance{ + DID: req.UserDID, + PassportHash: sql.NullString{String: req.Hash, Valid: true}, + PassportExpires: sql.NullTime{Time: time.Now().UTC().AddDate(0, 1, 0), Valid: true}, + } + + if err = BalancesQ(r).Insert(*balance); err != nil { + return fmt.Errorf("add balance: %w", err) + } + + log.Debugf("%d events will be added for user_did=%s", len(events), req.UserDID) + if err = EventsQ(r).Insert(events...); err != nil { + return fmt.Errorf("add open events: %w", err) + } + return nil + }) +} + +func setBalancePassportTx(r *http.Request, req connector.VerifyPassportRequest, reward *int64, refDID string) error { + log := Log(r).WithFields(map[string]any{ + "user_did": req.UserDID, + "hash": req.Hash, + }) + + logMsgScan := "PassportScan event type is disabled or expired, not accruing points" + return EventsQ(r).Transaction(func() error { + // If you make this endpoint public, you should check the passport hash for + // uniqueness and provide a better validation. Think about other changes too. + err := BalancesQ(r).FilterByDID(req.UserDID).SetPassport(req.Hash, time.Now().UTC().AddDate(0, 1, 0)) + if err != nil { + return fmt.Errorf("set passport for balance by DID: %w", err) + } + + if reward != nil { + passportScanEvent, err := EventsQ(r). + FilterByUserDID(req.UserDID). + FilterByType(evtypes.TypePassportScan). + FilterByStatus(data.EventOpen). + Get() + if err != nil { + return fmt.Errorf("get passport_scan event by DID: %w", err) + } + + logMsgOpenE := "PassportScan event not open" + if passportScanEvent != nil { + _, err = EventsQ(r). + FilterByUserDID(req.UserDID). + FilterByType(evtypes.TypePassportScan). + Update(data.EventFulfilled, json.RawMessage(passportScanEvent.Meta), reward) + if err != nil { + return fmt.Errorf("update reward for passport_scan event by DID: %w", err) + } + logMsgOpenE = "PassportScan event open" + logMsgScan = "PassportScan event reward update successful" + } + log.Debug(logMsgOpenE) + } + log.Debug(logMsgScan) + + evType := EventTypes(r).Get(evtypes.TypeReferralSpecific, evtypes.FilterInactive) + if evType == nil { + log.Debug("Referral event type is disabled or expired, not accruing points to referrer") + return nil + } + + if refDID == "" { + return nil + } + + err = EventsQ(r).Insert(data.Event{ + UserDID: refDID, + Type: evType.Name, + Status: data.EventFulfilled, + Meta: data.Jsonb(fmt.Sprintf(`{"did": "%s"}`, req.UserDID)), + }) + if err != nil { + return fmt.Errorf("add event for referrer: %w", err) + } + + return nil + }) +} diff --git a/internal/service/requests/verify_passport.go b/internal/service/requests/verify_passport.go index da18598..ef395ce 100644 --- a/internal/service/requests/verify_passport.go +++ b/internal/service/requests/verify_passport.go @@ -2,9 +2,7 @@ package requests import ( "encoding/json" - "errors" "net/http" - "time" validation "github.com/go-ozzo/ozzo-validation/v4" "github.com/rarimo/rarime-points-svc/pkg/connector" @@ -19,20 +17,6 @@ func NewVerifyPassport(r *http.Request) (req connector.VerifyPassportRequest, er return req, validation.Errors{ "user_did": validation.Validate(req.UserDID, validation.Required), "hash": validation.Validate(req.Hash, validation.Required), - "expiry": validation.Validate(req.Expiry, validation.Required, validation.By(isNotExpiredRule)), "shared_data": validation.Validate(req.SharedData, validation.Required, validation.Length(2, 0)), }.Filter() } - -func isNotExpiredRule(value interface{}) error { - v, ok := value.(time.Time) - if !ok { - panic("value is not a time.Time") // invalid function usage - } - - if v.Before(time.Now().UTC()) { - return errors.New("expiry is in the past") - } - - return nil -} diff --git a/pkg/connector/models.go b/pkg/connector/models.go index 1eca146..89f110d 100644 --- a/pkg/connector/models.go +++ b/pkg/connector/models.go @@ -3,7 +3,6 @@ package connector import ( "net/http" "strconv" - "time" "github.com/google/jsonapi" ) @@ -15,10 +14,9 @@ type FulfillEventRequest struct { } type VerifyPassportRequest struct { - UserDID string `json:"user_did"` - Hash string `json:"hash"` - Expiry time.Time `json:"expiry"` - SharedData []string `json:"shared_data"` + UserDID string `json:"user_did"` + Hash string `json:"hash"` + SharedData []string `json:"shared_data"` } // ErrorCode represents an error with a code indicating the unhappy flow that occurred From 622c18d93e342054fa4254f98d432d64f0190816 Mon Sep 17 00:00:00 2001 From: Zaptoss Date: Tue, 12 Mar 2024 16:48:00 +0200 Subject: [PATCH 13/42] Add field is_withdrawal_allowed --- docs/spec/components/schemas/Balance.yaml | 4 ++++ internal/assets/migrations/001_initial.sql | 1 + internal/data/balances.go | 18 ++++++++++-------- internal/data/pg/balances.go | 11 +++++++++++ internal/service/handlers/get_balance.go | 2 ++ internal/service/handlers/verify_passport.go | 11 ++++++++--- internal/service/handlers/withdraw.go | 2 ++ pkg/connector/models.go | 1 + resources/db.go | 4 ++-- resources/included.go | 4 ++-- resources/model_balance_attributes.go | 2 ++ resources/model_details.go | 8 ++++---- 12 files changed, 49 insertions(+), 19 deletions(-) diff --git a/docs/spec/components/schemas/Balance.yaml b/docs/spec/components/schemas/Balance.yaml index f08f9ef..3a86542 100644 --- a/docs/spec/components/schemas/Balance.yaml +++ b/docs/spec/components/schemas/Balance.yaml @@ -48,3 +48,7 @@ allOf: example: ["zgsScguZ", "jerUsmac"] items: type: string + is_withdrawal_allowed: + type: boolean + description: User haven't ability to withdraw + example: true diff --git a/internal/assets/migrations/001_initial.sql b/internal/assets/migrations/001_initial.sql index 72ce780..348514d 100644 --- a/internal/assets/migrations/001_initial.sql +++ b/internal/assets/migrations/001_initial.sql @@ -12,6 +12,7 @@ CREATE TABLE IF NOT EXISTS balances referred_by text UNIQUE, passport_hash text UNIQUE, passport_expires timestamp without time zone + is_withdrawal_allowed boolean NOT NULL default true ); CREATE INDEX IF NOT EXISTS balances_page_index ON balances (amount, updated_at) WHERE referred_by IS NOT NULL; diff --git a/internal/data/balances.go b/internal/data/balances.go index c1b91fc..c1d7d1a 100644 --- a/internal/data/balances.go +++ b/internal/data/balances.go @@ -8,14 +8,15 @@ import ( ) type Balance struct { - DID string `db:"did"` - Amount int64 `db:"amount"` - CreatedAt int32 `db:"created_at"` - UpdatedAt int32 `db:"updated_at"` - ReferredBy sql.NullString `db:"referred_by"` - PassportHash sql.NullString `db:"passport_hash"` - PassportExpires sql.NullTime `db:"passport_expires"` - Rank *int `db:"rank"` + DID string `db:"did"` + Amount int64 `db:"amount"` + CreatedAt int32 `db:"created_at"` + UpdatedAt int32 `db:"updated_at"` + ReferredBy sql.NullString `db:"referred_by"` + PassportHash sql.NullString `db:"passport_hash"` + PassportExpires sql.NullTime `db:"passport_expires"` + Rank *int `db:"rank"` + IsWithdrawalAllowed bool `db:"is_withdrawal_allowed"` } type BalancesQ interface { @@ -24,6 +25,7 @@ type BalancesQ interface { UpdateAmountBy(points int64) error SetPassport(hash string, exp time.Time) error SetReferredBy(referralCode string) error + SetIsWithdrawalAllowed(bool) error Page(*pgdb.OffsetPageParams) BalancesQ Select() ([]Balance, error) diff --git a/internal/data/pg/balances.go b/internal/data/pg/balances.go index 9aac0e7..4962dd1 100644 --- a/internal/data/pg/balances.go +++ b/internal/data/pg/balances.go @@ -81,6 +81,17 @@ func (q *balances) SetReferredBy(referralCode string) error { return nil } +func (q *balances) SetIsWithdrawalAllowed(state bool) error { + stmt := q.updater. + Set("is_withdrawal_allowed", state) + + if err := q.db.Exec(stmt); err != nil { + return fmt.Errorf("set is_withdrawal_allowed: %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/get_balance.go b/internal/service/handlers/get_balance.go index 294def5..7fff408 100644 --- a/internal/service/handlers/get_balance.go +++ b/internal/service/handlers/get_balance.go @@ -78,6 +78,8 @@ func newBalanceResponse(balance data.Balance, referrals []data.Referral) resourc return balanceResponse } + balanceResponse.Data.Attributes.IsWithdrawalAllowed = &balance.IsWithdrawalAllowed + referralCodes := make([]string, len(referrals)) balanceResponse.Data.Attributes.ReferralCodes = &referralCodes for i, referral := range referrals { diff --git a/internal/service/handlers/verify_passport.go b/internal/service/handlers/verify_passport.go index e87bb04..969cdbf 100644 --- a/internal/service/handlers/verify_passport.go +++ b/internal/service/handlers/verify_passport.go @@ -117,9 +117,10 @@ func createBalanceWithPassportTx(r *http.Request, req connector.VerifyPassportRe return EventsQ(r).Transaction(func() (err error) { balance := &data.Balance{ - DID: req.UserDID, - PassportHash: sql.NullString{String: req.Hash, Valid: true}, - PassportExpires: sql.NullTime{Time: time.Now().UTC().AddDate(0, 1, 0), Valid: true}, + DID: req.UserDID, + PassportHash: sql.NullString{String: req.Hash, Valid: true}, + PassportExpires: sql.NullTime{Time: time.Now().UTC().AddDate(0, 1, 0), Valid: true}, + IsWithdrawalAllowed: !req.IsUSA, } if err = BalancesQ(r).Insert(*balance); err != nil { @@ -149,6 +150,10 @@ func setBalancePassportTx(r *http.Request, req connector.VerifyPassportRequest, return fmt.Errorf("set passport for balance by DID: %w", err) } + if req.IsUSA && BalancesQ(r).FilterByDID(req.UserDID).SetIsWithdrawalAllowed(!req.IsUSA) != nil { + return fmt.Errorf("set is_withdrawal_allowed for balance by DID: %w", err) + } + if reward != nil { passportScanEvent, err := EventsQ(r). FilterByUserDID(req.UserDID). diff --git a/internal/service/handlers/withdraw.go b/internal/service/handlers/withdraw.go index e702fd6..3759436 100644 --- a/internal/service/handlers/withdraw.go +++ b/internal/service/handlers/withdraw.go @@ -121,6 +121,8 @@ func isEligibleToWithdraw(balance *data.Balance, amount int64) error { return mapValidationErr("is_verified", "user must have verified passport to withdraw") case balance.PassportExpires.Time.Before(time.Now().UTC()): return mapValidationErr("is_verified", "user passport is expired") + case !balance.IsWithdrawalAllowed: + return mapValidationErr("is_withdrawal_allowed", "withdrawal ability was disabled for this user") case balance.Amount < amount: return mapValidationErr("data/attributes/amount", "insufficient balance: %d", balance.Amount) } diff --git a/pkg/connector/models.go b/pkg/connector/models.go index 89f110d..c689d2a 100644 --- a/pkg/connector/models.go +++ b/pkg/connector/models.go @@ -17,6 +17,7 @@ type VerifyPassportRequest struct { UserDID string `json:"user_did"` Hash string `json:"hash"` SharedData []string `json:"shared_data"` + IsUSA bool `json:"is_usa"` } // ErrorCode represents an error with a code indicating the unhappy flow that occurred diff --git a/resources/db.go b/resources/db.go index 5381330..0c07f2f 100644 --- a/resources/db.go +++ b/resources/db.go @@ -12,7 +12,7 @@ import ( "gitlab.com/distributed_lab/logan/v3/errors" ) -// driverValue - converts interface into db supported type +//driverValue - converts interface into db supported type func driverValue(data interface{}) (driver.Value, error) { data, err := json.Marshal(data) if err != nil { @@ -22,7 +22,7 @@ func driverValue(data interface{}) (driver.Value, error) { return data, nil } -// driveScan - converts jsonb into type struct +//driveScan - converts jsonb into type struct func driveScan(src, dest interface{}) error { data, err := convertJSONB(src) if err != nil { diff --git a/resources/included.go b/resources/included.go index 68d70a2..889272d 100644 --- a/resources/included.go +++ b/resources/included.go @@ -46,7 +46,7 @@ func (c *Included) add(include Resource) { c.includes[include.GetKey()] = json.RawMessage(data) } -// MarshalJSON - marshals include collection as array of json objects +//MarshalJSON - marshals include collection as array of json objects func (c Included) MarshalJSON() ([]byte, error) { uniqueEntries := make([]json.RawMessage, 0, len(c.includes)) for _, value := range c.includes { @@ -56,7 +56,7 @@ func (c Included) MarshalJSON() ([]byte, error) { return json.Marshal(uniqueEntries) } -// UmarshalJSON - unmarshal array of json objects into include collection +//UmarshalJSON - unmarshal array of json objects into include collection func (c *Included) UnmarshalJSON(data []byte) error { var keys []Key err := json.Unmarshal(data, &keys) diff --git a/resources/model_balance_attributes.go b/resources/model_balance_attributes.go index 0dcced3..b7b8e15 100644 --- a/resources/model_balance_attributes.go +++ b/resources/model_balance_attributes.go @@ -13,6 +13,8 @@ type BalanceAttributes struct { IsDisabled bool `json:"is_disabled"` // Whether the user has scanned passport IsVerified bool `json:"is_verified"` + // User haven't ability to withdraw + IsWithdrawalAllowed *bool `json:"is_withdrawal_allowed,omitempty"` // Rank of the user in the full leaderboard. Returned only for the single user. Rank *int `json:"rank,omitempty"` // Referral codes used to build a referral link and send it to friends. Required if a balance is created diff --git a/resources/model_details.go b/resources/model_details.go index 36e0d24..303c93d 100644 --- a/resources/model_details.go +++ b/resources/model_details.go @@ -13,7 +13,7 @@ import ( type Details json.RawMessage -// UnmarshalJSON - casts data to Details +//UnmarshalJSON - casts data to Details func (d *Details) UnmarshalJSON(data []byte) error { if d == nil { return errors.New("regources.Details: UnmarshalJSON on nil pointer") @@ -22,7 +22,7 @@ func (d *Details) UnmarshalJSON(data []byte) error { return nil } -// MarshalJSON - casts Details to []byte +//MarshalJSON - casts Details to []byte func (d Details) MarshalJSON() ([]byte, error) { if d == nil { return []byte("null"), nil @@ -34,7 +34,7 @@ func (d Details) String() string { return string(d) } -// Value - implements db driver method for auto marshal +//Value - implements db driver method for auto marshal func (r Details) Value() (driver.Value, error) { result, err := json.Marshal(r) if err != nil { @@ -44,7 +44,7 @@ func (r Details) Value() (driver.Value, error) { return result, nil } -// Scan - implements db driver method for auto unmarshal +//Scan - implements db driver method for auto unmarshal func (r *Details) Scan(src interface{}) error { var data []byte switch rawData := src.(type) { From 50c2b077e46ea97fdc68ab5fca1c11c70845497f Mon Sep 17 00:00:00 2001 From: Zaptoss Date: Tue, 12 Mar 2024 17:48:14 +0200 Subject: [PATCH 14/42] Create endpoint for activate balance. Now CreateBalance endpoint only create new balance, if user haven't balance. --- ...@rarime-points-svc@v1@public@balances.yaml | 2 +- ...e-points-svc@v1@public@balances@{did}.yaml | 41 +++++++ internal/service/handlers/activate_balance.go | 106 ++++++++++++++++++ internal/service/handlers/create_balance.go | 61 ++-------- internal/service/requests/activate_balance.go | 26 +++++ internal/service/router.go | 1 + 6 files changed, 187 insertions(+), 50 deletions(-) create mode 100644 internal/service/handlers/activate_balance.go create mode 100644 internal/service/requests/activate_balance.go diff --git a/docs/spec/paths/integrations@rarime-points-svc@v1@public@balances.yaml b/docs/spec/paths/integrations@rarime-points-svc@v1@public@balances.yaml index cdc8462..a618e20 100644 --- a/docs/spec/paths/integrations@rarime-points-svc@v1@public@balances.yaml +++ b/docs/spec/paths/integrations@rarime-points-svc@v1@public@balances.yaml @@ -10,7 +10,7 @@ post: the new account synchronously (to display them right after 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 this endpoint as well. + but has fulfilled some event), you should use PATCH balances/{did} endpoint as well. operationId: createPointsBalance requestBody: content: diff --git a/docs/spec/paths/integrations@rarime-points-svc@v1@public@balances@{did}.yaml b/docs/spec/paths/integrations@rarime-points-svc@v1@public@balances@{did}.yaml index dcabba5..4d4608b 100644 --- a/docs/spec/paths/integrations@rarime-points-svc@v1@public@balances@{did}.yaml +++ b/docs/spec/paths/integrations@rarime-points-svc@v1@public@balances@{did}.yaml @@ -40,3 +40,44 @@ get: $ref: '#/components/responses/invalidAuth' 500: $ref: '#/components/responses/internalError' + +patch: + tags: + - Points balance + summary: Activate points balance + description: | + Activate inactive balance. Balance is inactive when referred_by field is null. + did in query must match with id in request body + operationId: activatePointsBalance + parameters: + - $ref: '#/components/parameters/pathDID' + requestBody: + content: + application/vnd.api+json: + schema: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/CreateBalance' + + + responses: + 200: + description: Success + 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' + 500: + $ref: '#/components/responses/internalError' diff --git a/internal/service/handlers/activate_balance.go b/internal/service/handlers/activate_balance.go new file mode 100644 index 0000000..cf9d39d --- /dev/null +++ b/internal/service/handlers/activate_balance.go @@ -0,0 +1,106 @@ +package handlers + +import ( + "fmt" + "net/http" + + "github.com/rarimo/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" + "gitlab.com/distributed_lab/ape" + "gitlab.com/distributed_lab/ape/problems" +) + +func ActivateBalance(w http.ResponseWriter, r *http.Request) { + req, err := requests.NewActivateBalance(r) + if err != nil { + ape.RenderErr(w, problems.BadRequest(err)...) + return + } + + did := req.Data.ID + + if !auth.Authenticates(UserClaims(r), auth.UserGrant(did)) { + ape.RenderErr(w, problems.Unauthorized()) + return + } + + balance, err := BalancesQ(r).FilterByDID(did).Get() + if err != nil { + Log(r).WithError(err).Error("Failed to get balance by DID") + ape.RenderErr(w, problems.InternalError()) + return + } + + // Balance should be inactive + if balance == nil || balance.ReferredBy.Valid { + ape.RenderErr(w, problems.Conflict()) + return + } + + referral, err := ReferralsQ(r).FilterByIsConsumed(false).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 + } + + referrals := prepareReferralsToAdd(did, 5, 0) + + err = EventsQ(r).Transaction(func() error { + Log(r).Debugf("%s referral code will be set for user_did=%s", req.Data.Attributes.ReferredBy, did) + if err = BalancesQ(r).FilterByDID(did).SetReferredBy(req.Data.Attributes.ReferredBy); err != nil { + return fmt.Errorf("set referred_by: %w", err) + } + + Log(r).Debugf("%d referrals will be added for user_did=%s", len(referrals), did) + if err = ReferralsQ(r).Insert(referrals...); err != nil { + return fmt.Errorf("add referrals: %w", err) + } + + Log(r).Debugf("%s referral will be consumed", req.Data.Attributes.ReferredBy) + if _, err = ReferralsQ(r).Consume(req.Data.Attributes.ReferredBy); err != nil { + return fmt.Errorf("consume referral: %w", err) + } + + if balance.PassportHash.Valid { + evType := EventTypes(r).Get(evtypes.TypeReferralSpecific, evtypes.FilterInactive) + if evType == nil { + Log(r).Debug("Referral event type is disabled or expired, not accruing points to referrer") + return nil + } + + err = EventsQ(r).Insert(data.Event{ + UserDID: referral.UserDID, + Type: evType.Name, + Status: data.EventFulfilled, + Meta: data.Jsonb(fmt.Sprintf(`{"did": "%s"}`, did)), + }) + if err != nil { + return fmt.Errorf("add event for referrer: %w", err) + } + } + return nil + }) + + if err != nil { + Log(r).WithError(err).Error("Failed to activate balance") + ape.RenderErr(w, problems.InternalError()) + return + } + + balance, err = BalancesQ(r).GetWithRank(did) + if err != nil { + Log(r).WithError(err).Error("Failed to get balance by DID with rank") + ape.RenderErr(w, problems.InternalError()) + return + } + + ape.Render(w, newBalanceResponse(*balance, referrals)) +} diff --git a/internal/service/handlers/create_balance.go b/internal/service/handlers/create_balance.go index a56704b..d1a4ce4 100644 --- a/internal/service/handlers/create_balance.go +++ b/internal/service/handlers/create_balance.go @@ -34,76 +34,39 @@ func CreateBalance(w http.ResponseWriter, r *http.Request) { return } - // Balance should not exist or be inactive - if balance != nil && balance.ReferredBy.Valid { + if balance != nil { ape.RenderErr(w, problems.Conflict()) return } referral, err := ReferralsQ(r).FilterByIsConsumed(false).Get(req.Data.Attributes.ReferredBy) - if referral == nil { - ape.RenderErr(w, problems.NotFound()) - return - } - if err != nil { Log(r).WithError(err).Error("Failed to get referral by ID") ape.RenderErr(w, problems.InternalError()) return } - referrals := prepareReferralsToAdd(did, 5, 0) - - if balance == nil { - events := prepareEventsWithRef(did, req.Data.Attributes.ReferredBy, r) - if err = createBalanceWithEventsAndReferrals(did, req.Data.Attributes.ReferredBy, events, referrals, r); err != nil { - Log(r).WithError(err).Error("Failed to create balance with events") - ape.RenderErr(w, problems.InternalError()) - return - } - - // 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(did) - if err != nil { - Log(r).WithError(err).Error("Failed to get created balance by DID") - ape.RenderErr(w, problems.InternalError()) - return - } - - ape.Render(w, newBalanceResponse(*balance, referrals)) + if referral == nil { + ape.RenderErr(w, problems.NotFound()) return } - err = EventsQ(r).Transaction(func() error { - Log(r).Debugf("%s referral code will be added for user_did=%s", req.Data.Attributes.ReferredBy, did) - if err = BalancesQ(r).FilterByDID(did).SetReferredBy(req.Data.Attributes.ReferredBy); err != nil { - return fmt.Errorf("set referred_by: %w", err) - } - - Log(r).Debugf("%d referrals will be added for user_did=%s", len(referrals), did) - if err = ReferralsQ(r).Insert(referrals...); err != nil { - return fmt.Errorf("add referrals: %w", err) - } - - Log(r).Debugf("%s referral will be consumed", req.Data.Attributes.ReferredBy) - if _, err = ReferralsQ(r).Consume(req.Data.Attributes.ReferredBy); err != nil { - return fmt.Errorf("consume referral: %w", err) - } - return nil - }) + referrals := prepareReferralsToAdd(did, 5, 0) - if err != nil { - Log(r).WithError(err).Error("Failed to activate balance") + events := prepareEventsWithRef(did, req.Data.Attributes.ReferredBy, r) + if err = createBalanceWithEventsAndReferrals(did, req.Data.Attributes.ReferredBy, events, referrals, r); err != nil { + Log(r).WithError(err).Error("Failed to create balance with events") ape.RenderErr(w, problems.InternalError()) return } + // 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(did) if err != nil { - Log(r).WithError(err).Error("Failed to get balance by DID with rank") + Log(r).WithError(err).Error("Failed to get created balance by DID") ape.RenderErr(w, problems.InternalError()) return } diff --git a/internal/service/requests/activate_balance.go b/internal/service/requests/activate_balance.go new file mode 100644 index 0000000..485e5c5 --- /dev/null +++ b/internal/service/requests/activate_balance.go @@ -0,0 +1,26 @@ +package requests + +import ( + "encoding/json" + "net/http" + + "github.com/go-chi/chi" + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/rarimo/rarime-points-svc/resources" +) + +func NewActivateBalance(r *http.Request) (req resources.CreateBalanceRequest, err error) { + did := chi.URLParam(r, "did") + if err = json.NewDecoder(r.Body).Decode(&req); err != nil { + err = newDecodeError("body", err) + return + } + + errs := validation.Errors{ + "data/id": validation.Validate(req.Data.ID, validation.Required, validation.In(did)), + "data/type": validation.Validate(req.Data.Type, validation.Required, validation.In(resources.CREATE_BALANCE)), + "data/attributes/referred_by": validation.Validate(req.Data.Attributes.ReferredBy, validation.Required), + } + + return req, errs.Filter() +} diff --git a/internal/service/router.go b/internal/service/router.go index ff48082..548d0b1 100644 --- a/internal/service/router.go +++ b/internal/service/router.go @@ -30,6 +30,7 @@ func Run(ctx context.Context, cfg config.Config) { r.Post("/", handlers.CreateBalance) r.Route("/{did}", func(r chi.Router) { r.Get("/", handlers.GetBalance) + r.Patch("/", handlers.ActivateBalance) r.Get("/withdrawals", handlers.ListWithdrawals) r.Post("/withdrawals", handlers.Withdraw) }) From 7d7fc31c4401fa3d3bca00512736923465138f50 Mon Sep 17 00:00:00 2001 From: Zaptoss Date: Tue, 12 Mar 2024 17:56:23 +0200 Subject: [PATCH 15/42] Add resource type update_balance --- docs/spec/components/schemas/CreateBalanceKey.yaml | 2 +- internal/service/requests/activate_balance.go | 2 +- resources/model_resource_type.go | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/spec/components/schemas/CreateBalanceKey.yaml b/docs/spec/components/schemas/CreateBalanceKey.yaml index ca44323..9707e16 100644 --- a/docs/spec/components/schemas/CreateBalanceKey.yaml +++ b/docs/spec/components/schemas/CreateBalanceKey.yaml @@ -9,4 +9,4 @@ properties: example: "did:iden3:readonly:tUDjWxnVJNi7t3FudukqrUcNwF5KVGoWgim5pp2jV" type: type: string - enum: [ create_balance ] + enum: [ create_balance, update_balance ] diff --git a/internal/service/requests/activate_balance.go b/internal/service/requests/activate_balance.go index 485e5c5..8344fc6 100644 --- a/internal/service/requests/activate_balance.go +++ b/internal/service/requests/activate_balance.go @@ -18,7 +18,7 @@ func NewActivateBalance(r *http.Request) (req resources.CreateBalanceRequest, er errs := validation.Errors{ "data/id": validation.Validate(req.Data.ID, validation.Required, validation.In(did)), - "data/type": validation.Validate(req.Data.Type, validation.Required, validation.In(resources.CREATE_BALANCE)), + "data/type": validation.Validate(req.Data.Type, validation.Required, validation.In(resources.UPDATE_BALANCE)), "data/attributes/referred_by": validation.Validate(req.Data.Attributes.ReferredBy, validation.Required), } diff --git a/resources/model_resource_type.go b/resources/model_resource_type.go index a236d96..dbb7e9f 100644 --- a/resources/model_resource_type.go +++ b/resources/model_resource_type.go @@ -11,6 +11,7 @@ const ( BALANCE ResourceType = "balance" CLAIM_EVENT ResourceType = "claim_event" CREATE_BALANCE ResourceType = "create_balance" + UPDATE_BALANCE ResourceType = "update_balance" EVENT ResourceType = "event" POINT_PRICE ResourceType = "point_price" WITHDRAW ResourceType = "withdraw" From bdb29631d34e20830f54d72258cf41bfbced3b71 Mon Sep 17 00:00:00 2001 From: violog <51th.apprent1ce.f0rce@gmail.com> Date: Wed, 13 Mar 2024 11:56:11 +0200 Subject: [PATCH 16/42] Fix syntax error in migration --- internal/assets/migrations/001_initial.sql | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/assets/migrations/001_initial.sql b/internal/assets/migrations/001_initial.sql index 348514d..6c300be 100644 --- a/internal/assets/migrations/001_initial.sql +++ b/internal/assets/migrations/001_initial.sql @@ -5,13 +5,13 @@ AS $$ BEGIN NEW.updated_at = EXTRACT('EPOCH' FROM NOW()); RETURN NEW; END; $$; CREATE TABLE IF NOT EXISTS balances ( - did text PRIMARY KEY, - amount bigint NOT NULL default 0, - created_at integer NOT NULL default EXTRACT('EPOCH' FROM NOW()), - updated_at integer NOT NULL default EXTRACT('EPOCH' FROM NOW()), - referred_by text UNIQUE, - passport_hash text UNIQUE, - passport_expires timestamp without time zone + did text PRIMARY KEY, + amount bigint NOT NULL default 0, + created_at integer NOT NULL default EXTRACT('EPOCH' FROM NOW()), + updated_at integer NOT NULL default EXTRACT('EPOCH' FROM NOW()), + referred_by text UNIQUE, + passport_hash text UNIQUE, + passport_expires timestamp without time zone, is_withdrawal_allowed boolean NOT NULL default true ); From 952c0b2bf92ed4b1bd2914d279a3342d5ad46bf0 Mon Sep 17 00:00:00 2001 From: Zaptoss Date: Wed, 13 Mar 2024 17:45:39 +0200 Subject: [PATCH 17/42] Add func fulFillPassportScanEvent. Code refactoring --- docs/spec/components/schemas/Balance.yaml | 2 +- ...e-points-svc@v1@public@balances@{did}.yaml | 9 +- internal/assets/migrations/001_initial.sql | 2 +- internal/data/balances.go | 2 + internal/data/pg/balances.go | 16 +++ internal/service/handlers/activate_balance.go | 10 +- internal/service/handlers/verify_passport.go | 111 +++++++++++------- 7 files changed, 107 insertions(+), 45 deletions(-) diff --git a/docs/spec/components/schemas/Balance.yaml b/docs/spec/components/schemas/Balance.yaml index 3a86542..b35ec12 100644 --- a/docs/spec/components/schemas/Balance.yaml +++ b/docs/spec/components/schemas/Balance.yaml @@ -50,5 +50,5 @@ allOf: type: string is_withdrawal_allowed: type: boolean - description: User haven't ability to withdraw + description: Whether the user can withdraw tokens. Returned only for the single user. example: true diff --git a/docs/spec/paths/integrations@rarime-points-svc@v1@public@balances@{did}.yaml b/docs/spec/paths/integrations@rarime-points-svc@v1@public@balances@{did}.yaml index 4d4608b..4cf57e5 100644 --- a/docs/spec/paths/integrations@rarime-points-svc@v1@public@balances@{did}.yaml +++ b/docs/spec/paths/integrations@rarime-points-svc@v1@public@balances@{did}.yaml @@ -62,7 +62,6 @@ patch: data: $ref: '#/components/schemas/CreateBalance' - responses: 200: description: Success @@ -79,5 +78,13 @@ patch: $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' diff --git a/internal/assets/migrations/001_initial.sql b/internal/assets/migrations/001_initial.sql index 6c300be..445e810 100644 --- a/internal/assets/migrations/001_initial.sql +++ b/internal/assets/migrations/001_initial.sql @@ -12,7 +12,7 @@ CREATE TABLE IF NOT EXISTS balances referred_by text UNIQUE, passport_hash text UNIQUE, passport_expires timestamp without time zone, - is_withdrawal_allowed boolean NOT NULL default true + is_withdrawal_allowed boolean NOT NULL default false ); CREATE INDEX IF NOT EXISTS balances_page_index ON balances (amount, updated_at) WHERE referred_by IS NOT NULL; diff --git a/internal/data/balances.go b/internal/data/balances.go index c1d7d1a..5f01dcc 100644 --- a/internal/data/balances.go +++ b/internal/data/balances.go @@ -27,6 +27,8 @@ type BalancesQ interface { SetReferredBy(referralCode string) error SetIsWithdrawalAllowed(bool) error + Update(Balance) (*Balance, error) + Page(*pgdb.OffsetPageParams) BalancesQ Select() ([]Balance, error) Get() (*Balance, error) diff --git a/internal/data/pg/balances.go b/internal/data/pg/balances.go index 4962dd1..39671c5 100644 --- a/internal/data/pg/balances.go +++ b/internal/data/pg/balances.go @@ -92,6 +92,22 @@ func (q *balances) SetIsWithdrawalAllowed(state bool) error { return nil } +func (q *balances) Update(bal data.Balance) (*data.Balance, error) { + stmt := q.updater. + Set("is_withdrawal_allowed", bal.IsWithdrawalAllowed). + Set("referred_by", bal.ReferredBy). + Set("passport_hash", bal.PassportHash). + Set("passport_expires", bal.PassportExpires). + Suffix("RETURNING *") + + var res data.Balance + if err := q.db.Get(&res, stmt); err != nil { + return nil, fmt.Errorf("insert balance %+v: %w", bal, err) + } + + return &res, 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/activate_balance.go b/internal/service/handlers/activate_balance.go index cf9d39d..e8dedec 100644 --- a/internal/service/handlers/activate_balance.go +++ b/internal/service/handlers/activate_balance.go @@ -33,8 +33,15 @@ func ActivateBalance(w http.ResponseWriter, r *http.Request) { return } + if balance == nil { + Log(r).Debug("Balance not exist") + ape.RenderErr(w, problems.NotFound()) + return + } + // Balance should be inactive - if balance == nil || balance.ReferredBy.Valid { + if balance.ReferredBy.Valid { + Log(r).Debug("Balance already activated") ape.RenderErr(w, problems.Conflict()) return } @@ -47,6 +54,7 @@ func ActivateBalance(w http.ResponseWriter, r *http.Request) { } if referral == nil { + Log(r).Debug("Referral code already consumed or not exists") ape.RenderErr(w, problems.NotFound()) return } diff --git a/internal/service/handlers/verify_passport.go b/internal/service/handlers/verify_passport.go index 969cdbf..f55ec5a 100644 --- a/internal/service/handlers/verify_passport.go +++ b/internal/service/handlers/verify_passport.go @@ -24,8 +24,9 @@ func VerifyPassport(w http.ResponseWriter, r *http.Request) { } log := Log(r).WithFields(map[string]any{ - "user_did": req.UserDID, - "hash": req.Hash, + "user_did": req.UserDID, + "hash": req.Hash, + "shared_data": req.SharedData, }) balance, err := BalancesQ(r).FilterByPassportHash(req.Hash).Get() @@ -78,7 +79,7 @@ func VerifyPassport(w http.ResponseWriter, r *http.Request) { } if !balance.PassportHash.Valid { - err = setBalancePassportTx(r, req, reward, balance.ReferredBy.String) + err = setBalancePassportTx(r, req, reward, balance.ReferredBy) if err != nil { log.WithError(err).Error("Failed to set passport and add event for referrer") @@ -104,8 +105,9 @@ func createBalanceWithPassportTx(r *http.Request, req connector.VerifyPassportRe events := EventTypes(r).PrepareEvents(req.UserDID, evtypes.FilterNotOpenable) log := Log(r).WithFields(map[string]any{ - "user_did": req.UserDID, - "hash": req.Hash, + "user_did": req.UserDID, + "hash": req.Hash, + "shared_data": req.SharedData, }) for i := 0; i < len(events); i++ { @@ -135,63 +137,56 @@ func createBalanceWithPassportTx(r *http.Request, req connector.VerifyPassportRe }) } -func setBalancePassportTx(r *http.Request, req connector.VerifyPassportRequest, reward *int64, refDID string) error { +func setBalancePassportTx(r *http.Request, req connector.VerifyPassportRequest, reward *int64, refBy sql.NullString) error { log := Log(r).WithFields(map[string]any{ - "user_did": req.UserDID, - "hash": req.Hash, + "user_did": req.UserDID, + "hash": req.Hash, + "shared_data": req.SharedData, }) - - logMsgScan := "PassportScan event type is disabled or expired, not accruing points" return EventsQ(r).Transaction(func() error { - // If you make this endpoint public, you should check the passport hash for - // uniqueness and provide a better validation. Think about other changes too. - err := BalancesQ(r).FilterByDID(req.UserDID).SetPassport(req.Hash, time.Now().UTC().AddDate(0, 1, 0)) + _, err := BalancesQ(r).FilterByDID(req.UserDID). + Update(data.Balance{ + ReferredBy: refBy, + PassportHash: sql.NullString{String: req.Hash, Valid: true}, + PassportExpires: sql.NullTime{Time: time.Now().UTC().AddDate(0, 1, 0), Valid: true}, + IsWithdrawalAllowed: !req.IsUSA}) + if err != nil { return fmt.Errorf("set passport for balance by DID: %w", err) } - if req.IsUSA && BalancesQ(r).FilterByDID(req.UserDID).SetIsWithdrawalAllowed(!req.IsUSA) != nil { - return fmt.Errorf("set is_withdrawal_allowed for balance by DID: %w", err) - } - + logMsgScan := "PassportScan event type is disabled or expired, not accruing points" if reward != nil { - passportScanEvent, err := EventsQ(r). - FilterByUserDID(req.UserDID). - FilterByType(evtypes.TypePassportScan). - FilterByStatus(data.EventOpen). - Get() - if err != nil { - return fmt.Errorf("get passport_scan event by DID: %w", err) + logMsgScan = "PassportScan event type available" + if err = fulFillPassportScanEvent(r, req, reward); err != nil { + return fmt.Errorf("fulfill passport scan event for user: %w", err) } - - logMsgOpenE := "PassportScan event not open" - if passportScanEvent != nil { - _, err = EventsQ(r). - FilterByUserDID(req.UserDID). - FilterByType(evtypes.TypePassportScan). - Update(data.EventFulfilled, json.RawMessage(passportScanEvent.Meta), reward) - if err != nil { - return fmt.Errorf("update reward for passport_scan event by DID: %w", err) - } - logMsgOpenE = "PassportScan event open" - logMsgScan = "PassportScan event reward update successful" - } - log.Debug(logMsgOpenE) } log.Debug(logMsgScan) + if !refBy.Valid { + log.Debug("User balance incative") + return nil + } + evType := EventTypes(r).Get(evtypes.TypeReferralSpecific, evtypes.FilterInactive) if evType == nil { log.Debug("Referral event type is disabled or expired, not accruing points to referrer") return nil } - if refDID == "" { - return nil + ref, err := ReferralsQ(r).Get(refBy.String) + if err != nil { + return fmt.Errorf("get referral: %w", err) + } + + // normally should never happen + if ref == nil { + return fmt.Errorf("referral code not found") } err = EventsQ(r).Insert(data.Event{ - UserDID: refDID, + UserDID: ref.UserDID, Type: evType.Name, Status: data.EventFulfilled, Meta: data.Jsonb(fmt.Sprintf(`{"did": "%s"}`, req.UserDID)), @@ -203,3 +198,37 @@ func setBalancePassportTx(r *http.Request, req connector.VerifyPassportRequest, return nil }) } + +func fulFillPassportScanEvent(r *http.Request, req connector.VerifyPassportRequest, reward *int64) error { + log := Log(r).WithFields(map[string]any{ + "user_did": req.UserDID, + "hash": req.Hash, + "shared_data": req.SharedData, + }) + + passportScanEvent, err := EventsQ(r). + FilterByUserDID(req.UserDID). + FilterByType(evtypes.TypePassportScan). + FilterByStatus(data.EventOpen). + Get() + + if err != nil { + return fmt.Errorf("get passport_scan event by DID: %w", err) + } + + if passportScanEvent != nil { + log.Debug("PassportScan event open") + + _, err = EventsQ(r). + FilterByUserDID(req.UserDID). + FilterByType(evtypes.TypePassportScan). + Update(data.EventFulfilled, json.RawMessage(passportScanEvent.Meta), reward) + if err != nil { + return fmt.Errorf("update reward for passport_scan event by DID: %w", err) + } + log.Debug("PassportScan event reward update successful") + return nil + } + log.Debug("PassportScan event not open") + return nil +} From c27948f4f38494bda3116e1a5c3a1e4b274c3475 Mon Sep 17 00:00:00 2001 From: Zaptoss Date: Wed, 13 Mar 2024 18:30:57 +0200 Subject: [PATCH 18/42] Change SetPassport function. Remove Update func for balances --- internal/data/balances.go | 5 +-- internal/data/pg/balances.go | 34 +++----------------- internal/service/handlers/verify_passport.go | 16 +++------ 3 files changed, 10 insertions(+), 45 deletions(-) diff --git a/internal/data/balances.go b/internal/data/balances.go index 5f01dcc..97f4284 100644 --- a/internal/data/balances.go +++ b/internal/data/balances.go @@ -23,11 +23,8 @@ type BalancesQ interface { New() BalancesQ Insert(Balance) error UpdateAmountBy(points int64) error - SetPassport(hash string, exp time.Time) error + SetPassport(hash string, exp time.Time, isWithdrawalAllowed bool) error SetReferredBy(referralCode string) error - SetIsWithdrawalAllowed(bool) error - - Update(Balance) (*Balance, error) Page(*pgdb.OffsetPageParams) BalancesQ Select() ([]Balance, error) diff --git a/internal/data/pg/balances.go b/internal/data/pg/balances.go index 39671c5..8e13abc 100644 --- a/internal/data/pg/balances.go +++ b/internal/data/pg/balances.go @@ -58,13 +58,14 @@ func (q *balances) UpdateAmountBy(points int64) error { return nil } -func (q *balances) SetPassport(hash string, exp time.Time) error { +func (q *balances) SetPassport(hash string, exp time.Time, isWithdrawalAllowed bool) error { stmt := q.updater. Set("passport_hash", hash). - Set("passport_expires", exp) + Set("passport_expires", exp). + Set("is_withdrawal_allowed", isWithdrawalAllowed) if err := q.db.Exec(stmt); err != nil { - return fmt.Errorf("set passport hash and expires: %w", err) + return fmt.Errorf("set passport hash and expires, and isWithdrawalAllowed: %w", err) } return nil @@ -81,33 +82,6 @@ func (q *balances) SetReferredBy(referralCode string) error { return nil } -func (q *balances) SetIsWithdrawalAllowed(state bool) error { - stmt := q.updater. - Set("is_withdrawal_allowed", state) - - if err := q.db.Exec(stmt); err != nil { - return fmt.Errorf("set is_withdrawal_allowed: %w", err) - } - - return nil -} - -func (q *balances) Update(bal data.Balance) (*data.Balance, error) { - stmt := q.updater. - Set("is_withdrawal_allowed", bal.IsWithdrawalAllowed). - Set("referred_by", bal.ReferredBy). - Set("passport_hash", bal.PassportHash). - Set("passport_expires", bal.PassportExpires). - Suffix("RETURNING *") - - var res data.Balance - if err := q.db.Get(&res, stmt); err != nil { - return nil, fmt.Errorf("insert balance %+v: %w", bal, err) - } - - return &res, 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/verify_passport.go b/internal/service/handlers/verify_passport.go index f55ec5a..d8ec207 100644 --- a/internal/service/handlers/verify_passport.go +++ b/internal/service/handlers/verify_passport.go @@ -91,9 +91,9 @@ func VerifyPassport(w http.ResponseWriter, r *http.Request) { return } - err = BalancesQ(r).FilterByDID(req.UserDID).SetPassport(balance.PassportHash.String, time.Now().UTC().AddDate(0, 1, 0)) + err = BalancesQ(r).FilterByDID(req.UserDID).SetPassport(balance.PassportHash.String, time.Now().UTC().AddDate(0, 1, 0), !req.IsUSA) if err != nil { - log.WithError(err).Error("Failed to set expiration date") + log.WithError(err).Error("Failed to update passport") ape.RenderErr(w, problems.InternalError()) return } @@ -144,13 +144,7 @@ func setBalancePassportTx(r *http.Request, req connector.VerifyPassportRequest, "shared_data": req.SharedData, }) return EventsQ(r).Transaction(func() error { - _, err := BalancesQ(r).FilterByDID(req.UserDID). - Update(data.Balance{ - ReferredBy: refBy, - PassportHash: sql.NullString{String: req.Hash, Valid: true}, - PassportExpires: sql.NullTime{Time: time.Now().UTC().AddDate(0, 1, 0), Valid: true}, - IsWithdrawalAllowed: !req.IsUSA}) - + err := BalancesQ(r).FilterByDID(req.UserDID).SetPassport(req.Hash, time.Now().UTC().AddDate(0, 1, 0), !req.IsUSA) if err != nil { return fmt.Errorf("set passport for balance by DID: %w", err) } @@ -158,7 +152,7 @@ func setBalancePassportTx(r *http.Request, req connector.VerifyPassportRequest, logMsgScan := "PassportScan event type is disabled or expired, not accruing points" if reward != nil { logMsgScan = "PassportScan event type available" - if err = fulFillPassportScanEvent(r, req, reward); err != nil { + if err = fulfillPassportScanEvent(r, req, reward); err != nil { return fmt.Errorf("fulfill passport scan event for user: %w", err) } } @@ -199,7 +193,7 @@ func setBalancePassportTx(r *http.Request, req connector.VerifyPassportRequest, }) } -func fulFillPassportScanEvent(r *http.Request, req connector.VerifyPassportRequest, reward *int64) error { +func fulfillPassportScanEvent(r *http.Request, req connector.VerifyPassportRequest, reward *int64) error { log := Log(r).WithFields(map[string]any{ "user_did": req.UserDID, "hash": req.Hash, From 19278eef3d3d7f42ee83610a340b71e59fbdfa20 Mon Sep 17 00:00:00 2001 From: Zaptoss Date: Wed, 13 Mar 2024 18:56:44 +0200 Subject: [PATCH 19/42] Add handle sql.ErrNoRows in Get - referrals --- internal/data/pg/referrals.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/data/pg/referrals.go b/internal/data/pg/referrals.go index 1959a09..00ef14c 100644 --- a/internal/data/pg/referrals.go +++ b/internal/data/pg/referrals.go @@ -1,6 +1,8 @@ package pg import ( + "database/sql" + "errors" "fmt" "github.com/Masterminds/squirrel" @@ -95,6 +97,9 @@ func (q *referrals) Get(id string) (*data.Referral, error) { var res data.Referral if err := q.db.Get(&res, q.selector.Where(squirrel.Eq{"id": id})); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } return nil, fmt.Errorf("get referral by ID: %w", err) } From 4eaf414c7c620a41bc06ae2642eba4539a78a743 Mon Sep 17 00:00:00 2001 From: Zaptoss Date: Thu, 14 Mar 2024 13:58:48 +0200 Subject: [PATCH 20/42] Change cursor pagination to offset pagination in list events. Order by fulfilled -> open -> claimed --- .../integrations@rarime-points-svc@v1@public@events.yaml | 2 +- internal/data/events.go | 2 +- internal/data/pg/events.go | 5 +++-- internal/service/handlers/list_events.go | 9 ++------- internal/service/requests/list_events.go | 4 ++-- 5 files changed, 9 insertions(+), 13 deletions(-) diff --git a/docs/spec/paths/integrations@rarime-points-svc@v1@public@events.yaml b/docs/spec/paths/integrations@rarime-points-svc@v1@public@events.yaml index 0f756fe..c1668e4 100644 --- a/docs/spec/paths/integrations@rarime-points-svc@v1@public@events.yaml +++ b/docs/spec/paths/integrations@rarime-points-svc@v1@public@events.yaml @@ -35,8 +35,8 @@ get: schema: type: boolean example: true - - $ref: '#/components/parameters/pageCursor' - $ref: '#/components/parameters/pageLimit' + - $ref: '#/components/parameters/pageNumber' - $ref: '#/components/parameters/pageOrder' responses: 200: diff --git a/internal/data/events.go b/internal/data/events.go index 9663b66..c1e51a2 100644 --- a/internal/data/events.go +++ b/internal/data/events.go @@ -44,7 +44,7 @@ type EventsQ interface { Delete() (rowsAffected int64, err error) Transaction(func() error) error - Page(*pgdb.CursorPageParams) EventsQ + Page(*pgdb.OffsetPageParams) EventsQ Select() ([]Event, error) Get() (*Event, error) // Count returns the total number of events that match filters. Page is not diff --git a/internal/data/pg/events.go b/internal/data/pg/events.go index 146ea9a..24182eb 100644 --- a/internal/data/pg/events.go +++ b/internal/data/pg/events.go @@ -99,8 +99,9 @@ func (q *events) Transaction(f func() error) error { return q.db.Transaction(f) } -func (q *events) Page(page *pgdb.CursorPageParams) data.EventsQ { - q.selector = page.ApplyTo(q.selector, "updated_at") +func (q *events) Page(page *pgdb.OffsetPageParams) data.EventsQ { + ord := "case when status = 'fulfilled' then 1 when status = 'open' then 2 when status = 'claimed' then 3 end" + q.selector = page.ApplyTo(q.selector.OrderBy(ord), "updated_at") return q } diff --git a/internal/service/handlers/list_events.go b/internal/service/handlers/list_events.go index 904486f..fca793b 100644 --- a/internal/service/handlers/list_events.go +++ b/internal/service/handlers/list_events.go @@ -35,7 +35,7 @@ func ListEvents(w http.ResponseWriter, r *http.Request) { FilterByStatus(req.FilterStatus...). FilterByType(req.FilterType...). FilterInactiveNotClaimed(inactiveTypes...). - Page(&req.CursorPageParams). + Page(&req.OffsetPageParams). Select() if err != nil { Log(r).WithError(err).Errorf("Failed to get filtered paginated event list: did=%s status=%v type=%v", @@ -67,13 +67,8 @@ func ListEvents(w http.ResponseWriter, r *http.Request) { return } - var last int32 - if len(events) > 0 { - last = events[len(events)-1].UpdatedAt - } - resp := newEventsResponse(events, meta) - resp.Links = req.CursorParams.GetLinks(r, last) + resp.Links = req.OffsetParams.GetLinks(r) if req.Count { _ = resp.PutMeta(struct { EventsCount int `json:"events_count"` diff --git a/internal/service/requests/list_events.go b/internal/service/requests/list_events.go index 08fda0d..0e030fa 100644 --- a/internal/service/requests/list_events.go +++ b/internal/service/requests/list_events.go @@ -10,7 +10,7 @@ import ( ) type ListEvents struct { - page.CursorParams + page.OffsetParams FilterDID *string `filter:"did"` FilterStatus []data.EventStatus `filter:"status"` FilterType []string `filter:"meta.static.name"` @@ -22,7 +22,7 @@ func NewListEvents(r *http.Request) (req ListEvents, err error) { err = newDecodeError("query", err) return } - if err = req.CursorParams.Validate(); err != nil { + if err = req.OffsetParams.Validate(); err != nil { return } From 3e6d9be3a4c1431f9ae2bf32ad55b82710d2f4c1 Mon Sep 17 00:00:00 2001 From: Zaptoss Date: Thu, 14 Mar 2024 16:23:51 +0200 Subject: [PATCH 21/42] Add logo field of type url.URL in event type config. Add disabled field in config for Points connector --- config.yaml | 1 + .../components/schemas/EventStaticMeta.yaml | 4 ++++ internal/data/evtypes/main.go | 2 ++ internal/data/pg/events.go | 2 +- pkg/connector/config.go | 17 ++++++++++++++++- pkg/connector/main.go | 11 ++++++++++- resources/model_balance_attributes.go | 2 +- resources/model_event_static_meta.go | 3 ++- 8 files changed, 37 insertions(+), 5 deletions(-) diff --git a/config.yaml b/config.yaml index dde1a0d..fb141cd 100644 --- a/config.yaml +++ b/config.yaml @@ -20,6 +20,7 @@ event_types: short_description: Short description frequency: one-time action_url: https://... + logo: https://... - name: get_poh title: Get PoH credential reward: 50 diff --git a/docs/spec/components/schemas/EventStaticMeta.yaml b/docs/spec/components/schemas/EventStaticMeta.yaml index b2b7089..ef598cb 100644 --- a/docs/spec/components/schemas/EventStaticMeta.yaml +++ b/docs/spec/components/schemas/EventStaticMeta.yaml @@ -49,3 +49,7 @@ properties: type: string format: url.URL example: https://robotornot.rarimo.com + logo: + type: string + format: url.URL + example: https://logo.com/some_logo.svg diff --git a/internal/data/evtypes/main.go b/internal/data/evtypes/main.go index 7d27d26..1493af0 100644 --- a/internal/data/evtypes/main.go +++ b/internal/data/evtypes/main.go @@ -46,6 +46,7 @@ type EventConfig struct { NoAutoOpen bool `fig:"no_auto_open"` Disabled bool `fig:"disabled"` ActionURL *url.URL `fig:"action_url"` + Logo *url.URL `fig:"logo"` } func (e EventConfig) Resource() resources.EventStaticMeta { @@ -59,6 +60,7 @@ func (e EventConfig) Resource() resources.EventStaticMeta { StartsAt: e.StartsAt, ExpiresAt: e.ExpiresAt, ActionUrl: e.ActionURL, + Logo: e.Logo, } } diff --git a/internal/data/pg/events.go b/internal/data/pg/events.go index 24182eb..171a11c 100644 --- a/internal/data/pg/events.go +++ b/internal/data/pg/events.go @@ -100,7 +100,7 @@ func (q *events) Transaction(f func() error) error { } func (q *events) Page(page *pgdb.OffsetPageParams) data.EventsQ { - ord := "case when status = 'fulfilled' then 1 when status = 'open' then 2 when status = 'claimed' then 3 end" + ord := fmt.Sprintf("case when status = '%s' then 1 when status = '%s' then 2 when status = '%s' then 3 end", data.EventFulfilled, data.EventOpen, data.EventClaimed) q.selector = page.ApplyTo(q.selector.OrderBy(ord), "updated_at") return q } diff --git a/pkg/connector/config.go b/pkg/connector/config.go index 9052ef1..9f04601 100644 --- a/pkg/connector/config.go +++ b/pkg/connector/config.go @@ -28,12 +28,27 @@ func NewPointer(getter kv.Getter) Pointer { func (p *points) Points() *Client { return p.once.Do(func() any { + var disabledConfig struct { + Disabled bool `fig:"disabled"` + } + + err := figure.Out(&disabledConfig). + From(kv.MustGetStringMap(p.getter, "points")). + Please() + if err != nil { + panic(fmt.Errorf("failed to figure out disabled for points: %s", err)) + } + + if disabledConfig.Disabled { + return &Client{disabled: true} + } + var cfg struct { Addr *url.URL `fig:"addr,required"` RequestTimeout time.Duration `fig:"request_timeout"` } - err := figure.Out(&cfg). + err = figure.Out(&cfg). From(kv.MustGetStringMap(p.getter, "points")). Please() if err != nil { diff --git a/pkg/connector/main.go b/pkg/connector/main.go index 6ac1161..682140a 100644 --- a/pkg/connector/main.go +++ b/pkg/connector/main.go @@ -16,7 +16,8 @@ import ( const privatePrefix = "/integrations/rarime-points-svc/v1/private" type Client struct { - conn *conn.Connector + disabled bool + conn *conn.Connector } func NewClient(cli iface.Client) *Client { @@ -24,6 +25,10 @@ func NewClient(cli iface.Client) *Client { } func (c *Client) FulfillEvent(ctx context.Context, req FulfillEventRequest) *Error { + if c.disabled { + return nil + } + u, _ := url.Parse(privatePrefix + "/events") err := c.conn.PatchJSON(u, req, ctx, nil) @@ -46,6 +51,10 @@ func (c *Client) FulfillEvent(ctx context.Context, req FulfillEventRequest) *Err } func (c *Client) VerifyPassport(ctx context.Context, req VerifyPassportRequest) error { + if c.disabled { + return nil + } + u, _ := url.Parse(privatePrefix + "/balances") return c.conn.PatchJSON(u, req, ctx, nil) } diff --git a/resources/model_balance_attributes.go b/resources/model_balance_attributes.go index b7b8e15..3719ee5 100644 --- a/resources/model_balance_attributes.go +++ b/resources/model_balance_attributes.go @@ -13,7 +13,7 @@ type BalanceAttributes struct { IsDisabled bool `json:"is_disabled"` // Whether the user has scanned passport IsVerified bool `json:"is_verified"` - // User haven't ability to withdraw + // Whether the user can withdraw tokens. Returned only for the single user. IsWithdrawalAllowed *bool `json:"is_withdrawal_allowed,omitempty"` // Rank of the user in the full leaderboard. Returned only for the single user. Rank *int `json:"rank,omitempty"` diff --git a/resources/model_event_static_meta.go b/resources/model_event_static_meta.go index 4bd02d7..1096896 100644 --- a/resources/model_event_static_meta.go +++ b/resources/model_event_static_meta.go @@ -16,7 +16,8 @@ type EventStaticMeta struct { // General event expiration date (UTC RFC3339) ExpiresAt *time.Time `json:"expires_at,omitempty"` // Event frequency, which means how often you can fulfill certain task and claim the reward. - Frequency string `json:"frequency"` + Frequency string `json:"frequency"` + Logo *url.URL `json:"logo,omitempty"` // Unique event code name Name string `json:"name"` // Reward amount in points From 370163bf455559702d71c5b9d4d6e7db08d707bf Mon Sep 17 00:00:00 2001 From: Zaptoss Date: Fri, 15 Mar 2024 09:25:06 +0200 Subject: [PATCH 22/42] Add logging if points connector disabled --- pkg/connector/config.go | 3 ++- pkg/connector/main.go | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/pkg/connector/config.go b/pkg/connector/config.go index 9f04601..32bfcd2 100644 --- a/pkg/connector/config.go +++ b/pkg/connector/config.go @@ -9,6 +9,7 @@ import ( "gitlab.com/distributed_lab/figure/v3" "gitlab.com/distributed_lab/kit/comfig" "gitlab.com/distributed_lab/kit/kv" + "gitlab.com/distributed_lab/logan/v3" ) const defaultTimeout = 10 * time.Second @@ -40,7 +41,7 @@ func (p *points) Points() *Client { } if disabledConfig.Disabled { - return &Client{disabled: true} + return &Client{disabled: true, log: logan.New()} } var cfg struct { diff --git a/pkg/connector/main.go b/pkg/connector/main.go index 682140a..29aa93f 100644 --- a/pkg/connector/main.go +++ b/pkg/connector/main.go @@ -11,21 +11,24 @@ import ( conn "gitlab.com/distributed_lab/json-api-connector" "gitlab.com/distributed_lab/json-api-connector/cerrors" iface "gitlab.com/distributed_lab/json-api-connector/client" + "gitlab.com/distributed_lab/logan/v3" ) const privatePrefix = "/integrations/rarime-points-svc/v1/private" type Client struct { disabled bool + log *logan.Entry conn *conn.Connector } func NewClient(cli iface.Client) *Client { - return &Client{conn: conn.NewConnector(cli)} + return &Client{conn: conn.NewConnector(cli), log: logan.New()} } func (c *Client) FulfillEvent(ctx context.Context, req FulfillEventRequest) *Error { if c.disabled { + c.log.Info("Points connector disabled") return nil } @@ -52,6 +55,7 @@ func (c *Client) FulfillEvent(ctx context.Context, req FulfillEventRequest) *Err func (c *Client) VerifyPassport(ctx context.Context, req VerifyPassportRequest) error { if c.disabled { + c.log.Info("Points connector disabled") return nil } From 93af134522e60a5723088662c1aada4392a02fbe Mon Sep 17 00:00:00 2001 From: Zaptoss Date: Fri, 15 Mar 2024 11:42:28 +0200 Subject: [PATCH 23/42] Add event types for generate proof --- config.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/config.yaml b/config.yaml index fb141cd..a9475c5 100644 --- a/config.yaml +++ b/config.yaml @@ -68,6 +68,18 @@ event_types: description: Event that start at specified time short_description: Short description starts_at: 2020-01-01T00:00:00Z + - name: generate_proof_age + title: Generate proof age + reward: 25 + frequency: one-time + description: Event that become fulfilled when user create proof that prove age + short_description: Short description + - name: generate_proof_nationality + title: Generate proof nationality + reward: 50 + frequency: one-time + description: Event that become fulfilled when user create proof that prove nationality + short_description: Short description auth: addr: http://rarime-auth From 996998effe99a253b0c83ea883ac3cf28984a882 Mon Sep 17 00:00:00 2001 From: Zaptoss Date: Mon, 18 Mar 2024 15:31:41 +0200 Subject: [PATCH 24/42] Add private endpoint /proofs for fulfillevents for proof verification. Add function for fullfill proof verify event in api --- .../handlers/fulfill_verify_proof_event.go | 103 ++++++++++++++++++ .../requests/fulfill_verify_proof_event.go | 22 ++++ internal/service/router.go | 1 + pkg/connector/main.go | 27 +++++ pkg/connector/models.go | 6 + 5 files changed, 159 insertions(+) create mode 100644 internal/service/handlers/fulfill_verify_proof_event.go create mode 100644 internal/service/requests/fulfill_verify_proof_event.go diff --git a/internal/service/handlers/fulfill_verify_proof_event.go b/internal/service/handlers/fulfill_verify_proof_event.go new file mode 100644 index 0000000..247a799 --- /dev/null +++ b/internal/service/handlers/fulfill_verify_proof_event.go @@ -0,0 +1,103 @@ +package handlers + +import ( + "fmt" + "net/http" + "time" + + "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" + api "github.com/rarimo/rarime-points-svc/pkg/connector" + "gitlab.com/distributed_lab/ape" + "gitlab.com/distributed_lab/ape/problems" +) + +func FulfillVerifyProofEvent(w http.ResponseWriter, r *http.Request) { + req, err := requests.NewFulfillVerifyProofEvent(r) + if err != nil { + Log(r).WithError(err).Debug("Bad request") + ape.RenderErr(w, problems.BadRequest(err)...) + return + } + + log := Log(r).WithFields(map[string]any{ + "user_did": req.UserDID, + "proof_type": req.ProofType, + "verifier_did": req.VerifierDID, + }) + + balance, err := BalancesQ(r).FilterByDID(req.UserDID).Get() + if err != nil { + log.WithError(err).Error("Failed to get balance by DID") + ape.RenderErr(w, api.CodeInternalError.JSONAPIError()) + return + } + + if balance == nil { + log.Error("Balance not exists") + ape.RenderErr(w, api.CodeInternalError.JSONAPIError()) + return + } + + verifierBalance, err := BalancesQ(r).FilterByDID(req.VerifierDID).Get() + if err != nil { + log.WithError(err).Error("Failed to get verifier balance by DID") + ape.RenderErr(w, api.CodeInternalError.JSONAPIError()) + return + } + + if verifierBalance == nil { + log.Error("Verifier balance not exists") + ape.RenderErr(w, api.CodeInternalError.JSONAPIError()) + return + } + + if err = verifyProofFulfill(r, req, req.VerifierDID, fmt.Sprintf("verify_proof_%s", req.ProofType)); err != nil { + log.WithError(err).Errorf("Failed to fulfill verify_proof_%s event for user", req.ProofType) + } + + // The verifier must have a verified passport for the owner of the proof to receive points + if verifierBalance.PassportHash.Valid && verifierBalance.PassportExpires.Time.Before(time.Now().UTC()) { + if err = verifyProofFulfill(r, req, req.UserDID, fmt.Sprintf("verified_proof_%s", req.ProofType)); err != nil { + log.WithError(err).Errorf("Failed to fulfill verified_proof_%s event for user", req.ProofType) + } + } + + w.WriteHeader(http.StatusNoContent) +} + +func verifyProofFulfill(r *http.Request, req api.FulfillVerifyProofEventRequest, did, eventName string) (err error) { + eventType := EventTypes(r).Get(eventName, evtypes.FilterInactive) + if eventType == nil { + Log(r).WithFields(map[string]any{ + "user_did": req.UserDID, + "proof_type": req.ProofType, + "verifier_did": req.VerifierDID, + }).Debugf("Event %s inactive", eventName) + return nil + } + + event, err := EventsQ(r).FilterByUserDID(did). + FilterByType(eventName). + FilterByStatus(data.EventOpen).Get() + if err != nil { + return fmt.Errorf("failed to get event %s by DID: %w", eventName, err) + } + + if event == nil { + Log(r).WithFields(map[string]any{ + "user_did": req.UserDID, + "proof_type": req.ProofType, + "verifier_did": req.VerifierDID, + }).Debugf("Event %s absent or already fulfilled for user", eventName) + return nil + } + + _, 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 +} diff --git a/internal/service/requests/fulfill_verify_proof_event.go b/internal/service/requests/fulfill_verify_proof_event.go new file mode 100644 index 0000000..7ff5976 --- /dev/null +++ b/internal/service/requests/fulfill_verify_proof_event.go @@ -0,0 +1,22 @@ +package requests + +import ( + "encoding/json" + "net/http" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/rarimo/rarime-points-svc/pkg/connector" +) + +func NewFulfillVerifyProofEvent(r *http.Request) (req connector.FulfillVerifyProofEventRequest, err error) { + if err = json.NewDecoder(r.Body).Decode(&req); err != nil { + err = newDecodeError("body", err) + return + } + + return req, validation.Errors{ + "user_did": validation.Validate(req.UserDID, validation.Required), + "proof_type": validation.Validate(req.ProofType, validation.Required), + "verifier_did": validation.Validate(req.VerifierDID, validation.Required), + }.Filter() +} diff --git a/internal/service/router.go b/internal/service/router.go index 548d0b1..887ac42 100644 --- a/internal/service/router.go +++ b/internal/service/router.go @@ -48,6 +48,7 @@ func Run(ctx context.Context, cfg config.Config) { r.Route("/private", func(r chi.Router) { r.Patch("/balances", handlers.VerifyPassport) r.Patch("/events", handlers.FulfillEvent) + r.Patch("/proofs", handlers.FulfillVerifyProofEvent) r.Post("/referrals", handlers.EditReferrals) }) }) diff --git a/pkg/connector/main.go b/pkg/connector/main.go index 29aa93f..de4c31d 100644 --- a/pkg/connector/main.go +++ b/pkg/connector/main.go @@ -53,6 +53,33 @@ func (c *Client) FulfillEvent(ctx context.Context, req FulfillEventRequest) *Err } } +func (c *Client) FulfillVerifyProofEvent(ctx context.Context, req FulfillVerifyProofEventRequest) *Error { + if c.disabled { + c.log.Info("Points connector disabled") + return nil + } + + u, _ := url.Parse(privatePrefix + "/proofs") + + err := c.conn.PatchJSON(u, req, ctx, nil) + if err == nil { + return nil + } + + baseErr := err + code, err := extractErrCode(err) + if err != nil { + return &Error{ + err: fmt.Errorf("failed to extract error code: %w; base error: %w", err, baseErr), + } + } + + return &Error{ + Code: code, + err: baseErr, + } +} + func (c *Client) VerifyPassport(ctx context.Context, req VerifyPassportRequest) error { if c.disabled { c.log.Info("Points connector disabled") diff --git a/pkg/connector/models.go b/pkg/connector/models.go index c689d2a..1d8bdf6 100644 --- a/pkg/connector/models.go +++ b/pkg/connector/models.go @@ -13,6 +13,12 @@ type FulfillEventRequest struct { ExternalID *string `json:"external_id,omitempty"` } +type FulfillVerifyProofEventRequest struct { + UserDID string `json:"user_did"` + ProofType string `json:"proof_type"` + VerifierDID string `json:"verifier_did"` +} + type VerifyPassportRequest struct { UserDID string `json:"user_did"` Hash string `json:"hash"` From b61dbcba6da6b06768f0cf7243c24179bc7e5df3 Mon Sep 17 00:00:00 2001 From: Zaptoss Date: Mon, 18 Mar 2024 15:37:33 +0200 Subject: [PATCH 25/42] Add event types verify_proof_* and verified_proof_* --- config.yaml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/config.yaml b/config.yaml index a9475c5..b357b7b 100644 --- a/config.yaml +++ b/config.yaml @@ -80,6 +80,30 @@ event_types: frequency: one-time description: Event that become fulfilled when user create proof that prove nationality short_description: Short description + - name: verify_proof_age + title: Verify proof age + reward: 25 + frequency: one-time + description: Event that become fulfilled when user verify someone else's proof age + short_description: Short description + - name: verify_proof_nationality + title: Verify proof nationality + reward: 50 + frequency: one-time + description: Event that become fulfilled when user verify someone else's proof nationality + short_description: Short description + - name: verified_proof_age + title: Have proof age verified + reward: 25 + frequency: one-time + description: Event that become fulfilled when another user verify you proof age (user that verify must have verified passport) + short_description: Short description + - name: verified_proof_nationality + title: Have proof nationality verified + reward: 50 + frequency: one-time + description: Event that become fulfilled when another user verify you proof nationality (user that verify must have verified passport) + short_description: Short description auth: addr: http://rarime-auth From 5c7100386a470523a2a111393ce8609a4e88f27b Mon Sep 17 00:00:00 2001 From: Zaptoss Date: Mon, 18 Mar 2024 18:12:56 +0200 Subject: [PATCH 26/42] Add logic for creating balance for verifier if balance absent --- .../handlers/fulfill_verify_proof_event.go | 67 ++++++++++++++----- 1 file changed, 52 insertions(+), 15 deletions(-) diff --git a/internal/service/handlers/fulfill_verify_proof_event.go b/internal/service/handlers/fulfill_verify_proof_event.go index 247a799..f073fae 100644 --- a/internal/service/handlers/fulfill_verify_proof_event.go +++ b/internal/service/handlers/fulfill_verify_proof_event.go @@ -27,41 +27,78 @@ func FulfillVerifyProofEvent(w http.ResponseWriter, r *http.Request) { "verifier_did": req.VerifierDID, }) - balance, err := BalancesQ(r).FilterByDID(req.UserDID).Get() + owner, err := BalancesQ(r).FilterByDID(req.UserDID).Get() if err != nil { log.WithError(err).Error("Failed to get balance by DID") ape.RenderErr(w, api.CodeInternalError.JSONAPIError()) return } - if balance == nil { - log.Error("Balance not exists") - ape.RenderErr(w, api.CodeInternalError.JSONAPIError()) + // Normally should never happen + if owner == nil { + log.Error("Proof owner balance not exists") + w.WriteHeader(http.StatusNoContent) return } - verifierBalance, err := BalancesQ(r).FilterByDID(req.VerifierDID).Get() + verifier, err := BalancesQ(r).FilterByDID(req.VerifierDID).Get() if err != nil { log.WithError(err).Error("Failed to get verifier balance by DID") ape.RenderErr(w, api.CodeInternalError.JSONAPIError()) return } - if verifierBalance == nil { - log.Error("Verifier balance not exists") - ape.RenderErr(w, api.CodeInternalError.JSONAPIError()) + // If the verifier does not have a balance, then create it + if verifier == nil { + events := EventTypes(r).PrepareEvents(req.VerifierDID, evtypes.FilterNotOpenable) + typeExists := false + for i, ev := range events { + if ev.Type == fmt.Sprintf("verify_proof_%s", req.ProofType) { + events[i].Status = data.EventFulfilled + typeExists = true + break + } + } + + if !typeExists { + log.Debug("Event type is not openable") + ape.RenderErr(w, api.CodeEventNotFound.JSONAPIError()) + return + } + + if err = createBalanceWithEvents(req.VerifierDID, "", events, r); err != nil { + log.WithError(err).Error("Failed to create balance with events") + ape.RenderErr(w, api.CodeInternalError.JSONAPIError()) + return + } + + w.WriteHeader(http.StatusNoContent) return } - if err = verifyProofFulfill(r, req, req.VerifierDID, fmt.Sprintf("verify_proof_%s", req.ProofType)); err != nil { - log.WithError(err).Errorf("Failed to fulfill verify_proof_%s event for user", req.ProofType) - } + err = EventsQ(r).Transaction(func() (err error) { + if err = verifyProofFulfill(r, req, req.VerifierDID, fmt.Sprintf("verify_proof_%s", req.ProofType)); err != nil { + log.WithError(err).Errorf("Failed to fulfill verify_proof_%s event for user", req.ProofType) + return + } - // The verifier must have a verified passport for the owner of the proof to receive points - if verifierBalance.PassportHash.Valid && verifierBalance.PassportExpires.Time.Before(time.Now().UTC()) { - if err = verifyProofFulfill(r, req, req.UserDID, fmt.Sprintf("verified_proof_%s", req.ProofType)); err != nil { - log.WithError(err).Errorf("Failed to fulfill verified_proof_%s event for user", req.ProofType) + // The verifier must have a verified passport for the owner of the proof to receive points + if verifier.PassportHash.Valid && verifier.PassportExpires.Time.Before(time.Now().UTC()) { + log.Debugf("Verifier have valid passport. Fulfill event for proof owner") + if err = verifyProofFulfill(r, req, req.UserDID, fmt.Sprintf("verified_proof_%s", req.ProofType)); err != nil { + log.WithError(err).Errorf("Failed to fulfill verified_proof_%s event for user", req.ProofType) + return + } + return } + + log.Debugf("Verifier haven't valid passport") + return + }) + if err != nil { + log.WithError(err).Error("Failed to fulfill verify proof events") + ape.RenderErr(w, api.CodeInternalError.JSONAPIError()) + return } w.WriteHeader(http.StatusNoContent) From 62dbcc47a389216b7b5dfc93730772938307e74b Mon Sep 17 00:00:00 2001 From: Zaptoss Date: Mon, 18 Mar 2024 18:30:47 +0200 Subject: [PATCH 27/42] reduce count of loggin --- internal/service/handlers/fulfill_verify_proof_event.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/internal/service/handlers/fulfill_verify_proof_event.go b/internal/service/handlers/fulfill_verify_proof_event.go index f073fae..908629c 100644 --- a/internal/service/handlers/fulfill_verify_proof_event.go +++ b/internal/service/handlers/fulfill_verify_proof_event.go @@ -78,18 +78,13 @@ func FulfillVerifyProofEvent(w http.ResponseWriter, r *http.Request) { err = EventsQ(r).Transaction(func() (err error) { if err = verifyProofFulfill(r, req, req.VerifierDID, fmt.Sprintf("verify_proof_%s", req.ProofType)); err != nil { - log.WithError(err).Errorf("Failed to fulfill verify_proof_%s event for user", req.ProofType) return } // The verifier must have a verified passport for the owner of the proof to receive points if verifier.PassportHash.Valid && verifier.PassportExpires.Time.Before(time.Now().UTC()) { log.Debugf("Verifier have valid passport. Fulfill event for proof owner") - if err = verifyProofFulfill(r, req, req.UserDID, fmt.Sprintf("verified_proof_%s", req.ProofType)); err != nil { - log.WithError(err).Errorf("Failed to fulfill verified_proof_%s event for user", req.ProofType) - return - } - return + return verifyProofFulfill(r, req, req.UserDID, fmt.Sprintf("verified_proof_%s", req.ProofType)) } log.Debugf("Verifier haven't valid passport") From 719069ac2e30c14c7295c2a8f8c48e8927369830 Mon Sep 17 00:00:00 2001 From: Zaptoss Date: Wed, 20 Mar 2024 16:08:08 +0200 Subject: [PATCH 28/42] Change logic verify proof endpoint. One request can contain many proofs --- .../handlers/fulfill_verify_proof_event.go | 39 +++++++++++++------ .../requests/fulfill_verify_proof_event.go | 2 +- pkg/connector/models.go | 6 +-- 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/internal/service/handlers/fulfill_verify_proof_event.go b/internal/service/handlers/fulfill_verify_proof_event.go index 908629c..b804966 100644 --- a/internal/service/handlers/fulfill_verify_proof_event.go +++ b/internal/service/handlers/fulfill_verify_proof_event.go @@ -23,7 +23,7 @@ func FulfillVerifyProofEvent(w http.ResponseWriter, r *http.Request) { log := Log(r).WithFields(map[string]any{ "user_did": req.UserDID, - "proof_type": req.ProofType, + "proof_types": req.ProofTypes, "verifier_did": req.VerifierDID, }) @@ -53,7 +53,7 @@ func FulfillVerifyProofEvent(w http.ResponseWriter, r *http.Request) { events := EventTypes(r).PrepareEvents(req.VerifierDID, evtypes.FilterNotOpenable) typeExists := false for i, ev := range events { - if ev.Type == fmt.Sprintf("verify_proof_%s", req.ProofType) { + if eventTypeIsOneOfProofs(ev.Type, req.ProofTypes) { events[i].Status = data.EventFulfilled typeExists = true break @@ -77,17 +77,24 @@ func FulfillVerifyProofEvent(w http.ResponseWriter, r *http.Request) { } err = EventsQ(r).Transaction(func() (err error) { - if err = verifyProofFulfill(r, req, req.VerifierDID, fmt.Sprintf("verify_proof_%s", req.ProofType)); err != nil { - return + passportValid := verifier.PassportHash.Valid && verifier.PassportExpires.Time.Before(time.Now().UTC()) + + if passportValid { + log.Debugf("Verifier have valid passport.") } + for _, proof := range req.ProofTypes { + if err = verifyProofFulfill(r, req, req.VerifierDID, fmt.Sprintf("verify_proof_%s", proof)); err != nil { + return + } - // The verifier must have a verified passport for the owner of the proof to receive points - if verifier.PassportHash.Valid && verifier.PassportExpires.Time.Before(time.Now().UTC()) { - log.Debugf("Verifier have valid passport. Fulfill event for proof owner") - return verifyProofFulfill(r, req, req.UserDID, fmt.Sprintf("verified_proof_%s", req.ProofType)) + // The verifier must have a verified passport for the owner of the proof to receive points + if passportValid { + if err = verifyProofFulfill(r, req, req.UserDID, fmt.Sprintf("verified_proof_%s", proof)); err != nil { + return + } + } } - log.Debugf("Verifier haven't valid passport") return }) if err != nil { @@ -104,7 +111,7 @@ func verifyProofFulfill(r *http.Request, req api.FulfillVerifyProofEventRequest, if eventType == nil { Log(r).WithFields(map[string]any{ "user_did": req.UserDID, - "proof_type": req.ProofType, + "event_name": eventName, "verifier_did": req.VerifierDID, }).Debugf("Event %s inactive", eventName) return nil @@ -120,7 +127,7 @@ func verifyProofFulfill(r *http.Request, req api.FulfillVerifyProofEventRequest, if event == nil { Log(r).WithFields(map[string]any{ "user_did": req.UserDID, - "proof_type": req.ProofType, + "event_name": eventName, "verifier_did": req.VerifierDID, }).Debugf("Event %s absent or already fulfilled for user", eventName) return nil @@ -133,3 +140,13 @@ func verifyProofFulfill(r *http.Request, req api.FulfillVerifyProofEventRequest, return nil } + +func eventTypeIsOneOfProofs(eventType string, proofs []string) bool { + for _, proof := range proofs { + if eventType == fmt.Sprintf("verify_proof_%s", proof) { + return true + } + } + + return false +} diff --git a/internal/service/requests/fulfill_verify_proof_event.go b/internal/service/requests/fulfill_verify_proof_event.go index 7ff5976..acc423b 100644 --- a/internal/service/requests/fulfill_verify_proof_event.go +++ b/internal/service/requests/fulfill_verify_proof_event.go @@ -16,7 +16,7 @@ func NewFulfillVerifyProofEvent(r *http.Request) (req connector.FulfillVerifyPro return req, validation.Errors{ "user_did": validation.Validate(req.UserDID, validation.Required), - "proof_type": validation.Validate(req.ProofType, validation.Required), + "proof_types": validation.Validate(req.ProofTypes, validation.Required), "verifier_did": validation.Validate(req.VerifierDID, validation.Required), }.Filter() } diff --git a/pkg/connector/models.go b/pkg/connector/models.go index 1d8bdf6..4147a8e 100644 --- a/pkg/connector/models.go +++ b/pkg/connector/models.go @@ -14,9 +14,9 @@ type FulfillEventRequest struct { } type FulfillVerifyProofEventRequest struct { - UserDID string `json:"user_did"` - ProofType string `json:"proof_type"` - VerifierDID string `json:"verifier_did"` + UserDID string `json:"user_did"` + ProofTypes []string `json:"proof_types"` + VerifierDID string `json:"verifier_did"` } type VerifyPassportRequest struct { From 0a7304e05566fbe0a68b690fbb8789be391ff558 Mon Sep 17 00:00:00 2001 From: violog <51th.apprent1ce.f0rce@gmail.com> Date: Wed, 20 Mar 2024 15:35:22 +0200 Subject: [PATCH 29/42] Add consumed referral codes in response --- docs/spec/components/schemas/Balance.yaml | 12 +++++++++-- internal/service/handlers/get_balance.go | 25 +++++++++++++++-------- resources/db.go | 4 ++-- resources/included.go | 4 ++-- resources/model_balance_attributes.go | 6 ++++-- resources/model_details.go | 8 ++++---- 6 files changed, 38 insertions(+), 21 deletions(-) diff --git a/docs/spec/components/schemas/Balance.yaml b/docs/spec/components/schemas/Balance.yaml index b35ec12..a703b45 100644 --- a/docs/spec/components/schemas/Balance.yaml +++ b/docs/spec/components/schemas/Balance.yaml @@ -42,12 +42,20 @@ allOf: format: int description: Rank of the user in the full leaderboard. Returned only for the single user. example: 294 - referral_codes: + active_referral_codes: type: array - description: Referral codes used to build a referral link and send it to friends. Required if a balance is created + description: | + Referral codes which can be used to build a referral link and send it + to friends. Returned only for the single user. example: ["zgsScguZ", "jerUsmac"] items: type: string + consumed_referral_codes: + type: array + description: Referral codes used by invited users. Returned only for the single user. + example: ["73k3bdYaFWM", "9csIL7dW65m"] + items: + type: string is_withdrawal_allowed: type: boolean description: Whether the user can withdraw tokens. Returned only for the single user. diff --git a/internal/service/handlers/get_balance.go b/internal/service/handlers/get_balance.go index 7fff408..261588e 100644 --- a/internal/service/handlers/get_balance.go +++ b/internal/service/handlers/get_balance.go @@ -44,7 +44,7 @@ func GetBalance(w http.ResponseWriter, r *http.Request) { var referrals []data.Referral if req.ReferralCodes { - referrals, err = ReferralsQ(r).FilterByUserDID(req.DID).FilterByIsConsumed(false).Select() + referrals, err = ReferralsQ(r).FilterByUserDID(req.DID).Select() if err != nil { Log(r).WithError(err).Error("Failed to get referrals by DID") ape.RenderErr(w, problems.InternalError()) @@ -73,17 +73,24 @@ func newBalanceModel(balance data.Balance) resources.Balance { } func newBalanceResponse(balance data.Balance, referrals []data.Referral) resources.BalanceResponse { - balanceResponse := resources.BalanceResponse{Data: newBalanceModel(balance)} + resp := resources.BalanceResponse{Data: newBalanceModel(balance)} + resp.Data.Attributes.IsWithdrawalAllowed = &balance.IsWithdrawalAllowed + if len(referrals) == 0 { - return balanceResponse + return resp } - balanceResponse.Data.Attributes.IsWithdrawalAllowed = &balance.IsWithdrawalAllowed + activeCodes, consumedCodes := make([]string, 0, len(referrals)), make([]string, 0, len(referrals)) + resp.Data.Attributes.ActiveReferralCodes = &activeCodes + resp.Data.Attributes.ConsumedReferralCodes = &consumedCodes - referralCodes := make([]string, len(referrals)) - balanceResponse.Data.Attributes.ReferralCodes = &referralCodes - for i, referral := range referrals { - referralCodes[i] = referral.ID + for _, ref := range referrals { + if ref.IsConsumed { + consumedCodes = append(consumedCodes, ref.ID) + continue + } + activeCodes = append(activeCodes, ref.ID) } - return balanceResponse + + return resp } diff --git a/resources/db.go b/resources/db.go index 0c07f2f..5381330 100644 --- a/resources/db.go +++ b/resources/db.go @@ -12,7 +12,7 @@ import ( "gitlab.com/distributed_lab/logan/v3/errors" ) -//driverValue - converts interface into db supported type +// driverValue - converts interface into db supported type func driverValue(data interface{}) (driver.Value, error) { data, err := json.Marshal(data) if err != nil { @@ -22,7 +22,7 @@ func driverValue(data interface{}) (driver.Value, error) { return data, nil } -//driveScan - converts jsonb into type struct +// driveScan - converts jsonb into type struct func driveScan(src, dest interface{}) error { data, err := convertJSONB(src) if err != nil { diff --git a/resources/included.go b/resources/included.go index 889272d..68d70a2 100644 --- a/resources/included.go +++ b/resources/included.go @@ -46,7 +46,7 @@ func (c *Included) add(include Resource) { c.includes[include.GetKey()] = json.RawMessage(data) } -//MarshalJSON - marshals include collection as array of json objects +// MarshalJSON - marshals include collection as array of json objects func (c Included) MarshalJSON() ([]byte, error) { uniqueEntries := make([]json.RawMessage, 0, len(c.includes)) for _, value := range c.includes { @@ -56,7 +56,7 @@ func (c Included) MarshalJSON() ([]byte, error) { return json.Marshal(uniqueEntries) } -//UmarshalJSON - unmarshal array of json objects into include collection +// UmarshalJSON - unmarshal array of json objects into include collection func (c *Included) UnmarshalJSON(data []byte) error { var keys []Key err := json.Unmarshal(data, &keys) diff --git a/resources/model_balance_attributes.go b/resources/model_balance_attributes.go index 3719ee5..7df05b4 100644 --- a/resources/model_balance_attributes.go +++ b/resources/model_balance_attributes.go @@ -5,8 +5,12 @@ package resources type BalanceAttributes struct { + // Referral codes which can be used to build a referral link and send it to friends. Returned only for the single user. + ActiveReferralCodes *[]string `json:"active_referral_codes,omitempty"` // Amount of points Amount int64 `json:"amount"` + // Referral codes used by invited users. Returned only for the single user. + ConsumedReferralCodes *[]string `json:"consumed_referral_codes,omitempty"` // Unix timestamp of balance creation 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. @@ -17,8 +21,6 @@ type BalanceAttributes struct { IsWithdrawalAllowed *bool `json:"is_withdrawal_allowed,omitempty"` // Rank of the user in the full leaderboard. Returned only for the single user. Rank *int `json:"rank,omitempty"` - // Referral codes used to build a referral link and send it to friends. Required if a balance is created - ReferralCodes *[]string `json:"referral_codes,omitempty"` // Unix timestamp of the last points accruing UpdatedAt int32 `json:"updated_at"` } diff --git a/resources/model_details.go b/resources/model_details.go index 303c93d..36e0d24 100644 --- a/resources/model_details.go +++ b/resources/model_details.go @@ -13,7 +13,7 @@ import ( type Details json.RawMessage -//UnmarshalJSON - casts data to Details +// UnmarshalJSON - casts data to Details func (d *Details) UnmarshalJSON(data []byte) error { if d == nil { return errors.New("regources.Details: UnmarshalJSON on nil pointer") @@ -22,7 +22,7 @@ func (d *Details) UnmarshalJSON(data []byte) error { return nil } -//MarshalJSON - casts Details to []byte +// MarshalJSON - casts Details to []byte func (d Details) MarshalJSON() ([]byte, error) { if d == nil { return []byte("null"), nil @@ -34,7 +34,7 @@ func (d Details) String() string { return string(d) } -//Value - implements db driver method for auto marshal +// Value - implements db driver method for auto marshal func (r Details) Value() (driver.Value, error) { result, err := json.Marshal(r) if err != nil { @@ -44,7 +44,7 @@ func (r Details) Value() (driver.Value, error) { return result, nil } -//Scan - implements db driver method for auto unmarshal +// Scan - implements db driver method for auto unmarshal func (r *Details) Scan(src interface{}) error { var data []byte switch rawData := src.(type) { From 98acc41156c0da483a6767b25359d208e8d7bd97 Mon Sep 17 00:00:00 2001 From: violog <51th.apprent1ce.f0rce@gmail.com> Date: Wed, 20 Mar 2024 15:51:26 +0200 Subject: [PATCH 30/42] Return added codes in response from edit referrals --- internal/service/handlers/edit_referrals.go | 24 ++++++++++++--------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/internal/service/handlers/edit_referrals.go b/internal/service/handlers/edit_referrals.go index bd22a7c..56c2dbd 100644 --- a/internal/service/handlers/edit_referrals.go +++ b/internal/service/handlers/edit_referrals.go @@ -48,14 +48,16 @@ func EditReferrals(w http.ResponseWriter, r *http.Request) { } } - if err = adjustReferralsCount(index, req, r); err != nil { + added, err := adjustReferralsCount(index, req, r) + if err != nil { Log(r).WithError(err).Error("Failed to adjust referrals count") ape.RenderErr(w, problems.InternalError()) return } - // TODO: return balance WITHOUT rank and with referrals included, or just referrals, also above - w.WriteHeader(http.StatusNoContent) + ape.Render(w, struct { + Refs []string `json:"added_referrals"` + }{added}) } func prepareReferralsToAdd(did string, count, index uint64) []data.Referral { @@ -72,26 +74,28 @@ func prepareReferralsToAdd(did string, count, index uint64) []data.Referral { return refs } -func adjustReferralsCount(index uint64, req requests.EditReferralsRequest, r *http.Request) error { +func adjustReferralsCount(index uint64, req requests.EditReferralsRequest, r *http.Request) (refsAdded []string, err error) { switch { case *req.Count < index: toConsume := index - *req.Count - if err := ReferralsQ(r).ConsumeFirst(req.DID, toConsume); err != nil { - return fmt.Errorf("consume referrals: %w", err) + if err = ReferralsQ(r).ConsumeFirst(req.DID, toConsume); err != nil { + return nil, fmt.Errorf("consume referrals: %w", err) } Log(r).Infof("Consumed %d referrals for DID %s", toConsume, req.DID) case *req.Count > index: toAdd := *req.Count - index - err := ReferralsQ(r).Insert(prepareReferralsToAdd(req.DID, toAdd, index)...) - if err != nil { - return fmt.Errorf("insert referrals: %w", err) + refsToAdd := prepareReferralsToAdd(req.DID, toAdd, index) + if err = ReferralsQ(r).Insert(refsToAdd...); err != nil { + return nil, fmt.Errorf("insert referrals: %w", err) } Log(r).Infof("Inserted %d referrals for DID %s", toAdd, req.DID) + // while this is deterministic, the codes will be the same + refsAdded = referralid.NewMany(req.DID, toAdd, index) default: Log(r).Infof("No referrals to add or consume for DID %s", req.DID) } - return nil + return } From 9ed234364b2c1f2e561912e40aadd87f1871e9c4 Mon Sep 17 00:00:00 2001 From: violog <51th.apprent1ce.f0rce@gmail.com> Date: Mon, 4 Mar 2024 18:57:03 +0200 Subject: [PATCH 31/42] Fix edit referrals endpoint, add docs for it in README.md --- README.md | 26 +++++++++++++++++++++ internal/service/handlers/edit_referrals.go | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a4ed8e2..dd0046f 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,32 @@ functionality to interact with these endpoints. 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 +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. + +Path: `/integrations/rarime-points-svc/v1/private/referrals` +Query parameters: +- `did` - user DID to create or edit referrals for +- `count` - number of referrals to set + +Example to set 200 referrals for user `did:example:123`: +```shell +curl -X POST "http://localhost/integrations/rarime-points-svc/v1/private/referrals?did=did:example:123&count=200" +``` + +Behavior: +a) User does not exist -> create a _System user_ with the specified number of +referrals +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 + +Where `N` is the current number of active referrals for the user, `count` is +query parameter value. + ### Local build We do use openapi:json standard for API. We use swagger for documenting our API. diff --git a/internal/service/handlers/edit_referrals.go b/internal/service/handlers/edit_referrals.go index 56c2dbd..102bb56 100644 --- a/internal/service/handlers/edit_referrals.go +++ b/internal/service/handlers/edit_referrals.go @@ -40,7 +40,7 @@ func EditReferrals(w http.ResponseWriter, r *http.Request) { var index uint64 if balance != nil { - index, err = ReferralsQ(r).FilterByUserDID(balance.DID).Count() + index, err = ReferralsQ(r).FilterByUserDID(balance.DID).FilterByIsConsumed(false).Count() if err != nil { Log(r).WithError(err).Error("Failed to get referral count for user DID") ape.RenderErr(w, problems.InternalError()) From 62868b0d46abed1a89b590904cd2d59989ce5ca0 Mon Sep 17 00:00:00 2001 From: violog <51th.apprent1ce.f0rce@gmail.com> Date: Mon, 4 Mar 2024 18:58:31 +0200 Subject: [PATCH 32/42] Fix text wraps in readme --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dd0046f..3b8042f 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,9 @@ The path for internal endpoints is `/integrations/rarime-points-svc/v1/private/* 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 -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. +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. Path: `/integrations/rarime-points-svc/v1/private/referrals` Query parameters: From 458e67f4ac557ef1087a972b81c53429be4c730f Mon Sep 17 00:00:00 2001 From: violog <51th.apprent1ce.f0rce@gmail.com> Date: Thu, 21 Mar 2024 14:23:11 +0200 Subject: [PATCH 33/42] Small fixes in referrals and proof event --- README.md | 2 +- internal/service/handlers/edit_referrals.go | 2 +- .../handlers/fulfill_verify_proof_event.go | 47 ++++++++++--------- 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 3b8042f..2948385 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ curl -X POST "http://localhost/integrations/rarime-points-svc/v1/private/referra Behavior: a) User does not exist -> create a _System user_ with the specified number of -referrals +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 diff --git a/internal/service/handlers/edit_referrals.go b/internal/service/handlers/edit_referrals.go index 102bb56..56c2dbd 100644 --- a/internal/service/handlers/edit_referrals.go +++ b/internal/service/handlers/edit_referrals.go @@ -40,7 +40,7 @@ func EditReferrals(w http.ResponseWriter, r *http.Request) { var index uint64 if balance != nil { - index, err = ReferralsQ(r).FilterByUserDID(balance.DID).FilterByIsConsumed(false).Count() + index, err = ReferralsQ(r).FilterByUserDID(balance.DID).Count() if err != nil { Log(r).WithError(err).Error("Failed to get referral count for user DID") ape.RenderErr(w, problems.InternalError()) diff --git a/internal/service/handlers/fulfill_verify_proof_event.go b/internal/service/handlers/fulfill_verify_proof_event.go index b804966..18ab58f 100644 --- a/internal/service/handlers/fulfill_verify_proof_event.go +++ b/internal/service/handlers/fulfill_verify_proof_event.go @@ -78,20 +78,21 @@ func FulfillVerifyProofEvent(w http.ResponseWriter, r *http.Request) { err = EventsQ(r).Transaction(func() (err error) { passportValid := verifier.PassportHash.Valid && verifier.PassportExpires.Time.Before(time.Now().UTC()) - if passportValid { log.Debugf("Verifier have valid passport.") } + for _, proof := range req.ProofTypes { if err = verifyProofFulfill(r, req, req.VerifierDID, fmt.Sprintf("verify_proof_%s", proof)); err != nil { return } - + if !passportValid { + continue + } // The verifier must have a verified passport for the owner of the proof to receive points - if passportValid { - if err = verifyProofFulfill(r, req, req.UserDID, fmt.Sprintf("verified_proof_%s", proof)); err != nil { - return - } + err = verifyProofFulfill(r, req, req.UserDID, fmt.Sprintf("verified_proof_%s", proof)) + if err != nil { + return } } @@ -106,36 +107,36 @@ func FulfillVerifyProofEvent(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) } -func verifyProofFulfill(r *http.Request, req api.FulfillVerifyProofEventRequest, did, eventName string) (err error) { - eventType := EventTypes(r).Get(eventName, evtypes.FilterInactive) +func verifyProofFulfill(r *http.Request, req api.FulfillVerifyProofEventRequest, did, evType string) (err error) { + log := Log(r).WithFields(map[string]any{ + "user_did": req.UserDID, + "event_name": evType, + "verifier_did": req.VerifierDID, + }) + + eventType := EventTypes(r).Get(evType, evtypes.FilterInactive) if eventType == nil { - Log(r).WithFields(map[string]any{ - "user_did": req.UserDID, - "event_name": eventName, - "verifier_did": req.VerifierDID, - }).Debugf("Event %s inactive", eventName) + log.Debugf("Event %s inactive", evType) return nil } - event, err := EventsQ(r).FilterByUserDID(did). - FilterByType(eventName). - FilterByStatus(data.EventOpen).Get() + event, err := EventsQ(r). + FilterByUserDID(did). + FilterByType(evType). + FilterByStatus(data.EventOpen). + Get() if err != nil { - return fmt.Errorf("failed to get event %s by DID: %w", eventName, err) + return fmt.Errorf("get event %s by DID: %w", evType, err) } if event == nil { - Log(r).WithFields(map[string]any{ - "user_did": req.UserDID, - "event_name": eventName, - "verifier_did": req.VerifierDID, - }).Debugf("Event %s absent or already fulfilled for user", eventName) + log.Debugf("Event %s absent or already fulfilled for user", evType) return nil } _, err = EventsQ(r).FilterByID(event.ID).Update(data.EventFulfilled, nil, nil) if err != nil { - return fmt.Errorf("failed to update event: %w", err) + return fmt.Errorf("update event %s by ID: %w", evType, err) } return nil From 08f07857f36fab311d749caf78c6a0cc143479ff Mon Sep 17 00:00:00 2001 From: violog <51th.apprent1ce.f0rce@gmail.com> Date: Fri, 22 Mar 2024 12:22:41 +0200 Subject: [PATCH 34/42] Fix count-index logic in edit referrals handler --- internal/data/pg/referrals.go | 2 +- internal/service/handlers/edit_referrals.go | 55 +++++++++++---------- internal/service/requests/edit_referrals.go | 2 +- 3 files changed, 31 insertions(+), 28 deletions(-) diff --git a/internal/data/pg/referrals.go b/internal/data/pg/referrals.go index 00ef14c..85ccb38 100644 --- a/internal/data/pg/referrals.go +++ b/internal/data/pg/referrals.go @@ -72,7 +72,7 @@ func (q *referrals) ConsumeFirst(did string, count uint64) error { UPDATE %s SET is_consumed = true WHERE id IN ( SELECT id FROM %s WHERE user_did = ? AND is_consumed = false - ORDER BY created_at ASC LIMIT %d + LIMIT %d ); `, referralsTable, referralsTable, count) diff --git a/internal/service/handlers/edit_referrals.go b/internal/service/handlers/edit_referrals.go index 56c2dbd..ba9b8e0 100644 --- a/internal/service/handlers/edit_referrals.go +++ b/internal/service/handlers/edit_referrals.go @@ -27,6 +27,7 @@ func EditReferrals(w http.ResponseWriter, r *http.Request) { if balance == nil { if *req.Count == 0 { + Log(r).Debugf("Balance %s not found, skipping creation for count=0", req.DID) w.WriteHeader(http.StatusNoContent) return } @@ -38,17 +39,7 @@ func EditReferrals(w http.ResponseWriter, r *http.Request) { } } - var index uint64 - if balance != nil { - index, err = ReferralsQ(r).FilterByUserDID(balance.DID).Count() - if err != nil { - Log(r).WithError(err).Error("Failed to get referral count for user DID") - ape.RenderErr(w, problems.InternalError()) - return - } - } - - added, err := adjustReferralsCount(index, req, r) + added, err := adjustReferralsCount(req, r) if err != nil { Log(r).WithError(err).Error("Failed to adjust referrals count") ape.RenderErr(w, problems.InternalError()) @@ -74,28 +65,40 @@ func prepareReferralsToAdd(did string, count, index uint64) []data.Referral { return refs } -func adjustReferralsCount(index uint64, req requests.EditReferralsRequest, r *http.Request) (refsAdded []string, err error) { - switch { - case *req.Count < index: - toConsume := index - *req.Count +func adjustReferralsCount(req requests.EditReferralsRequest, r *http.Request) (refsAdded []string, err error) { + active, err := ReferralsQ(r).FilterByUserDID(req.DID).FilterByIsConsumed(false).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 DID %s", req.DID) + return + } + + if *req.Count < active { + toConsume := active - *req.Count if err = ReferralsQ(r).ConsumeFirst(req.DID, toConsume); err != nil { return nil, fmt.Errorf("consume referrals: %w", err) } Log(r).Infof("Consumed %d referrals for DID %s", toConsume, req.DID) + return + } - case *req.Count > index: - toAdd := *req.Count - index - refsToAdd := prepareReferralsToAdd(req.DID, toAdd, index) - if err = ReferralsQ(r).Insert(refsToAdd...); err != nil { - return nil, fmt.Errorf("insert referrals: %w", err) - } - Log(r).Infof("Inserted %d referrals for DID %s", toAdd, req.DID) - // while this is deterministic, the codes will be the same - refsAdded = referralid.NewMany(req.DID, toAdd, index) + index, err := ReferralsQ(r).FilterByUserDID(req.DID).Count() + if err != nil { + return nil, fmt.Errorf("count all referrals: %w", err) + } - default: - Log(r).Infof("No referrals to add or consume for DID %s", req.DID) + toAdd := *req.Count - active + // balance must exist, according to preceding logic in EditReferrals + err = ReferralsQ(r).Insert(prepareReferralsToAdd(req.DID, toAdd, index)...) + if err != nil { + return nil, fmt.Errorf("insert referrals: %w", err) } + Log(r).Infof("Inserted %d referrals for DID %s", toAdd, req.DID) + // while this is deterministic, the codes will be the same + refsAdded = referralid.NewMany(req.DID, toAdd, index) return } diff --git a/internal/service/requests/edit_referrals.go b/internal/service/requests/edit_referrals.go index 17d02f4..f3c9015 100644 --- a/internal/service/requests/edit_referrals.go +++ b/internal/service/requests/edit_referrals.go @@ -20,6 +20,6 @@ func NewEditReferrals(r *http.Request) (req EditReferralsRequest, err error) { return req, validation.Errors{ "did": validation.Validate(req.DID, validation.Required), - "count": validation.Validate(req.Count, validation.Required), + "count": validation.Validate(req.Count, validation.NotNil), }.Filter() } From e4669dfe14b5d34566cf9f06220137df7f519c00 Mon Sep 17 00:00:00 2001 From: violog <51th.apprent1ce.f0rce@gmail.com> Date: Fri, 22 Mar 2024 15:25:02 +0200 Subject: [PATCH 35/42] Fix URL JSON marshalling by replacing it with string --- docs/spec/components/schemas/EventStaticMeta.yaml | 4 ++-- internal/data/evtypes/main.go | 12 ++++++++++-- resources/model_event_static_meta.go | 15 +++++++-------- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/docs/spec/components/schemas/EventStaticMeta.yaml b/docs/spec/components/schemas/EventStaticMeta.yaml index ef598cb..3652d1e 100644 --- a/docs/spec/components/schemas/EventStaticMeta.yaml +++ b/docs/spec/components/schemas/EventStaticMeta.yaml @@ -47,9 +47,9 @@ properties: example: 2020-01-01T00:00:00Z action_url: type: string - format: url.URL + description: Page where you can fulfill the event example: https://robotornot.rarimo.com logo: type: string - format: url.URL + description: Event logo example: https://logo.com/some_logo.svg diff --git a/internal/data/evtypes/main.go b/internal/data/evtypes/main.go index 1493af0..921c5b5 100644 --- a/internal/data/evtypes/main.go +++ b/internal/data/evtypes/main.go @@ -50,6 +50,14 @@ type EventConfig struct { } func (e EventConfig) Resource() resources.EventStaticMeta { + safeConv := func(u *url.URL) *string { + if u == nil { + return nil + } + s := u.String() + return &s + } + return resources.EventStaticMeta{ Name: e.Name, Description: e.Description, @@ -59,8 +67,8 @@ func (e EventConfig) Resource() resources.EventStaticMeta { Frequency: e.Frequency.String(), StartsAt: e.StartsAt, ExpiresAt: e.ExpiresAt, - ActionUrl: e.ActionURL, - Logo: e.Logo, + ActionUrl: safeConv(e.ActionURL), + Logo: safeConv(e.Logo), } } diff --git a/resources/model_event_static_meta.go b/resources/model_event_static_meta.go index 1096896..0827410 100644 --- a/resources/model_event_static_meta.go +++ b/resources/model_event_static_meta.go @@ -4,20 +4,19 @@ package resources -import ( - "net/url" - "time" -) +import "time" // Primary event metadata in plain JSON. This is a template to be filled by `dynamic` when it's present. type EventStaticMeta struct { - ActionUrl *url.URL `json:"action_url,omitempty"` - Description string `json:"description"` + // Page where you can fulfill the event + ActionUrl *string `json:"action_url,omitempty"` + Description string `json:"description"` // General event expiration date (UTC RFC3339) ExpiresAt *time.Time `json:"expires_at,omitempty"` // Event frequency, which means how often you can fulfill certain task and claim the reward. - Frequency string `json:"frequency"` - Logo *url.URL `json:"logo,omitempty"` + Frequency string `json:"frequency"` + // Event logo + Logo *string `json:"logo,omitempty"` // Unique event code name Name string `json:"name"` // Reward amount in points From 0b205b593c23642d7730695d7f01297425bd2017 Mon Sep 17 00:00:00 2001 From: Zaptoss Date: Mon, 25 Mar 2024 10:28:30 +0200 Subject: [PATCH 36/42] Fix dynamic passport scan reward --- internal/service/handlers/claim_event.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/internal/service/handlers/claim_event.go b/internal/service/handlers/claim_event.go index 55c3331..1ab632d 100644 --- a/internal/service/handlers/claim_event.go +++ b/internal/service/handlers/claim_event.go @@ -6,6 +6,7 @@ import ( "github.com/rarimo/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" @@ -47,6 +48,15 @@ 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).FilterByDID(event.UserDID).FilterDisabled().Get() if err != nil { From aca4035da577703406548f4e3f9c7bc4223c66aa Mon Sep 17 00:00:00 2001 From: Zaptoss Date: Mon, 25 Mar 2024 13:57:17 +0200 Subject: [PATCH 37/42] Fix event opener --- internal/service/workers/reopener/init.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/service/workers/reopener/init.go b/internal/service/workers/reopener/init.go index 765a4c7..839f65a 100644 --- a/internal/service/workers/reopener/init.go +++ b/internal/service/workers/reopener/init.go @@ -124,10 +124,10 @@ func runStartingWatchers(ctx context.Context, cfg config.Config) error { log := cfg.Log().WithField("who", "opener[initializer]") notStartedEv := cfg.EventTypes().List(func(ev evtypes.EventConfig) bool { - return ev.Disabled || !evtypes.FilterNotStarted(ev) + return ev.Disabled || !evtypes.FilterNotStarted(ev) || evtypes.FilterExpired(ev) }) - if len(notStartedEv) != 0 { + if len(notStartedEv) == 0 { log.Info("No events to open at Start time: all types already opened or there no types with StartAt") return nil } @@ -155,7 +155,7 @@ func startingWatcher(cfg config.Config, name string) func(context.Context) { var err error running.WithThreshold(ctx, log, fmt.Sprintf("opener[%s]", name), func(context.Context) (bool, error) { - if balances, err = pg.NewBalances(cfg.DB().Clone()).FilterDisabled().Select(); err == nil { + if balances, err = pg.NewBalances(cfg.DB().Clone()).Select(); err != nil { return false, err } return true, nil @@ -172,7 +172,7 @@ func startingWatcher(cfg config.Config, name string) func(context.Context) { } running.WithThreshold(ctx, log, fmt.Sprintf("opener[%s]", name), func(context.Context) (bool, error) { - if err = pg.NewEvents(cfg.DB().Clone()).Insert(events...); err == nil { + if err = pg.NewEvents(cfg.DB().Clone()).Insert(events...); err != nil { return false, err } return true, nil From 86b291bd33b6596d20711b1575ccf267e2ba10c7 Mon Sep 17 00:00:00 2001 From: Zaptoss Date: Mon, 25 Mar 2024 16:46:50 +0200 Subject: [PATCH 38/42] Fix json unmarshal --- internal/data/jsonb.go | 32 +++++++++++++++++++- internal/service/handlers/verify_passport.go | 4 +-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/internal/data/jsonb.go b/internal/data/jsonb.go index f3759ae..dcc63a1 100644 --- a/internal/data/jsonb.go +++ b/internal/data/jsonb.go @@ -5,6 +5,7 @@ import ( "encoding/json" "gitlab.com/distributed_lab/kit/pgdb" + "gitlab.com/distributed_lab/logan/v3/errors" ) type Jsonb json.RawMessage @@ -16,6 +17,35 @@ func (j *Jsonb) Value() (driver.Value, error) { return pgdb.JSONValue(j) } +// func (j *Jsonb) Scan(src interface{}) error { +// return pgdb.JSONScan(src, j) +// } + +func (j *Jsonb) UnmarshalJSON(data []byte) error { + if j == nil { + return errors.New("UnmarshalJSON on nil pointer") + } + *j = append((*j)[0:0], data...) + return nil +} + func (j *Jsonb) Scan(src interface{}) error { - return pgdb.JSONScan(src, j) + var data []byte + switch rawData := src.(type) { + case []byte: + data = rawData + case string: + data = []byte(rawData) + case nil: + data = []byte("null") + default: + return errors.New("Unexpected type for jsonb") + } + + err := json.Unmarshal(data, j) + if err != nil { + return errors.Wrap(err, "failed to unmarshal") + } + + return nil } diff --git a/internal/service/handlers/verify_passport.go b/internal/service/handlers/verify_passport.go index d8ec207..aa10b4f 100644 --- a/internal/service/handlers/verify_passport.go +++ b/internal/service/handlers/verify_passport.go @@ -2,7 +2,6 @@ package handlers import ( "database/sql" - "encoding/json" "fmt" "net/http" "time" @@ -212,11 +211,10 @@ func fulfillPassportScanEvent(r *http.Request, req connector.VerifyPassportReque if passportScanEvent != nil { log.Debug("PassportScan event open") - _, err = EventsQ(r). FilterByUserDID(req.UserDID). FilterByType(evtypes.TypePassportScan). - Update(data.EventFulfilled, json.RawMessage(passportScanEvent.Meta), reward) + Update(data.EventFulfilled, nil, reward) if err != nil { return fmt.Errorf("update reward for passport_scan event by DID: %w", err) } From 2519bedf4a7e469f1f427fff61daecee01368670 Mon Sep 17 00:00:00 2001 From: Zaptoss Date: Mon, 25 Mar 2024 17:28:09 +0200 Subject: [PATCH 39/42] Fix fulfill verified proof event --- internal/service/handlers/fulfill_verify_proof_event.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/service/handlers/fulfill_verify_proof_event.go b/internal/service/handlers/fulfill_verify_proof_event.go index 18ab58f..a09f242 100644 --- a/internal/service/handlers/fulfill_verify_proof_event.go +++ b/internal/service/handlers/fulfill_verify_proof_event.go @@ -77,7 +77,7 @@ func FulfillVerifyProofEvent(w http.ResponseWriter, r *http.Request) { } err = EventsQ(r).Transaction(func() (err error) { - passportValid := verifier.PassportHash.Valid && verifier.PassportExpires.Time.Before(time.Now().UTC()) + passportValid := verifier.PassportHash.Valid && verifier.PassportExpires.Time.After(time.Now().UTC()) if passportValid { log.Debugf("Verifier have valid passport.") } From 427ee9e86d16de00e16db40628e4bf53b7913905 Mon Sep 17 00:00:00 2001 From: Zaptoss Date: Mon, 25 Mar 2024 19:14:57 +0200 Subject: [PATCH 40/42] Change error message in jsonb --- internal/data/jsonb.go | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/internal/data/jsonb.go b/internal/data/jsonb.go index dcc63a1..e7abfd9 100644 --- a/internal/data/jsonb.go +++ b/internal/data/jsonb.go @@ -3,9 +3,9 @@ package data import ( "database/sql/driver" "encoding/json" + "fmt" "gitlab.com/distributed_lab/kit/pgdb" - "gitlab.com/distributed_lab/logan/v3/errors" ) type Jsonb json.RawMessage @@ -17,13 +17,9 @@ func (j *Jsonb) Value() (driver.Value, error) { return pgdb.JSONValue(j) } -// func (j *Jsonb) Scan(src interface{}) error { -// return pgdb.JSONScan(src, j) -// } - func (j *Jsonb) UnmarshalJSON(data []byte) error { if j == nil { - return errors.New("UnmarshalJSON on nil pointer") + return fmt.Errorf("json.RawMessage: UnmarshalJSON on nil pointer") } *j = append((*j)[0:0], data...) return nil @@ -39,12 +35,12 @@ func (j *Jsonb) Scan(src interface{}) error { case nil: data = []byte("null") default: - return errors.New("Unexpected type for jsonb") + return fmt.Errorf("unexpected type for jsonb: %T", src) } err := json.Unmarshal(data, j) if err != nil { - return errors.Wrap(err, "failed to unmarshal") + return fmt.Errorf("failed to unmarshal: %w", err) } return nil From ae058c9075107822e3ddd9f3d276756161796438 Mon Sep 17 00:00:00 2001 From: Zaptoss Date: Tue, 26 Mar 2024 17:27:07 +0200 Subject: [PATCH 41/42] Add support StartAt logic to PoH event in sbtcheck --- internal/service/workers/sbtcheck/main.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/internal/service/workers/sbtcheck/main.go b/internal/service/workers/sbtcheck/main.go index e2be16c..854d7f2 100644 --- a/internal/service/workers/sbtcheck/main.go +++ b/internal/service/workers/sbtcheck/main.go @@ -54,7 +54,11 @@ type extConfig interface { func Run(ctx context.Context, cfg extConfig) { log := cfg.Log().WithField("who", "sbt-checker") - getPoh := cfg.EventTypes().Get(evtypes.TypeGetPoH, evtypes.FilterInactive) + + // FilterInactive filter also events which hasn't start yet. Then we need filter only events which will not be active. + // Events with StartsAt will be open in specified time + getPoh := cfg.EventTypes().Get(evtypes.TypeGetPoH, + func(ev evtypes.EventConfig) bool { return ev.Disabled || evtypes.FilterExpired(ev) }) if getPoh == nil { log.Warn("PoH event is disabled or expired, SBT check will not run") return @@ -71,6 +75,12 @@ func Run(ctx context.Context, cfg extConfig) { }) } + if start := getPoh.StartsAt; start != nil && start.After(time.Now().UTC()) { + until := start.Sub(time.Now().UTC()) + timer := time.NewTimer(until) + <-timer.C + } + var wg sync.WaitGroup for name, net := range cfg.SbtCheck().networks { if net.disabled { From b1b396744cb9fafbcf85cee35c51b8936be92022 Mon Sep 17 00:00:00 2001 From: Zaptoss Date: Wed, 27 Mar 2024 15:19:46 +0200 Subject: [PATCH 42/42] VerifyPassport connector become deprecated. Change private endpoint for verification to public --- internal/service/handlers/verify_passport.go | 6 ++++++ internal/service/router.go | 2 +- pkg/connector/main.go | 2 ++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/internal/service/handlers/verify_passport.go b/internal/service/handlers/verify_passport.go index aa10b4f..13dbbc4 100644 --- a/internal/service/handlers/verify_passport.go +++ b/internal/service/handlers/verify_passport.go @@ -15,6 +15,12 @@ import ( ) func VerifyPassport(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNoContent) + return + + // TODO: New logic. User go to public endpoint with proof that verify passport + // need logic for check proof and accruing points for verification. + req, err := requests.NewVerifyPassport(r) if err != nil { Log(r).WithError(err).Debug("Bad request") diff --git a/internal/service/router.go b/internal/service/router.go index 887ac42..0ecff25 100644 --- a/internal/service/router.go +++ b/internal/service/router.go @@ -31,6 +31,7 @@ func Run(ctx context.Context, cfg config.Config) { r.Route("/{did}", func(r chi.Router) { r.Get("/", handlers.GetBalance) r.Patch("/", handlers.ActivateBalance) + r.Patch("/verifypassport", handlers.VerifyPassport) r.Get("/withdrawals", handlers.ListWithdrawals) r.Post("/withdrawals", handlers.Withdraw) }) @@ -46,7 +47,6 @@ func Run(ctx context.Context, cfg config.Config) { }) // must be accessible only within the cluster r.Route("/private", func(r chi.Router) { - r.Patch("/balances", handlers.VerifyPassport) r.Patch("/events", handlers.FulfillEvent) r.Patch("/proofs", handlers.FulfillVerifyProofEvent) r.Post("/referrals", handlers.EditReferrals) diff --git a/pkg/connector/main.go b/pkg/connector/main.go index de4c31d..ae51ead 100644 --- a/pkg/connector/main.go +++ b/pkg/connector/main.go @@ -81,6 +81,8 @@ func (c *Client) FulfillVerifyProofEvent(ctx context.Context, req FulfillVerifyP } func (c *Client) VerifyPassport(ctx context.Context, req VerifyPassportRequest) error { + // Deprecated: VerifyPassport whould be public endpoint + // and that connector currently not used if c.disabled { c.log.Info("Points connector disabled") return nil