diff --git a/internal/assets/migrations/006_one_time_qr_code.sql b/internal/assets/migrations/006_one_time_qr_code.sql new file mode 100644 index 0000000..b004c51 --- /dev/null +++ b/internal/assets/migrations/006_one_time_qr_code.sql @@ -0,0 +1,27 @@ +-- +migrate Up +CREATE OR REPLACE FUNCTION trigger_set_updated_at_ts() RETURNS trigger + LANGUAGE plpgsql +AS $$ BEGIN NEW.updated_at = (NOW() AT TIME ZONE 'utc'); RETURN NEW; END; $$; + +CREATE TABLE IF NOT EXISTS qr_codes ( + id TEXT PRIMARY KEY, + nullifier TEXT REFERENCES balances (nullifier), + reward BIGINT NOT NULL, + usage_count BIGINT NOT NULL DEFAULT 0, + infinity BOOLEAN NOT NULL DEFAULT FALSE, + + updated_at TIMESTAMP NOT NULL DEFAULT NOW(), + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); + +DROP TRIGGER IF EXISTS set_updated_at ON qr_codes; +CREATE TRIGGER set_updated_at + BEFORE UPDATE + ON qr_codes + FOR EACH ROW +EXECUTE FUNCTION trigger_set_updated_at_ts(); + + +-- +migrate Down +DROP TABLE IF EXISTS qr_codes; +DROP FUNCTION IF EXISTS trigger_set_updated_at_ts(); diff --git a/internal/data/events.go b/internal/data/events.go index 827a5d9..70e480f 100644 --- a/internal/data/events.go +++ b/internal/data/events.go @@ -66,6 +66,7 @@ type EventsQ interface { FilterByType(...string) EventsQ FilterByNotType(types ...string) EventsQ FilterByUpdatedAtBefore(int64) EventsQ + FilterByQRCode(string) EventsQ FilterTodayEvents(offset int) EventsQ diff --git a/internal/data/evtypes/models/extra.go b/internal/data/evtypes/models/extra.go index c48d00a..1642d1e 100644 --- a/internal/data/evtypes/models/extra.go +++ b/internal/data/evtypes/models/extra.go @@ -23,6 +23,7 @@ const ( TypePollParticipation = "poll_participation" TypeEarlyTest = "early_test" TypeDailyQuestion = "daily_question" + TypeQRCode = "qr_code" ) const ( diff --git a/internal/data/pg/events.go b/internal/data/pg/events.go index 1921f57..1d2d510 100644 --- a/internal/data/pg/events.go +++ b/internal/data/pg/events.go @@ -260,6 +260,10 @@ func (q *events) FilterByQuestionID(id int) data.EventsQ { return q.applyCondition(squirrel.Eq{"meta->>'question_id'": id}) } +func (q *events) FilterByQRCode(qrCode string) data.EventsQ { + return q.applyCondition(squirrel.Eq{"meta->>'qr_code'": qrCode}) +} + func (q *events) FilterInactiveNotClaimed(types ...string) data.EventsQ { if len(types) == 0 { return q diff --git a/internal/data/pg/qr_codes.go b/internal/data/pg/qr_codes.go new file mode 100644 index 0000000..b70ba4d --- /dev/null +++ b/internal/data/pg/qr_codes.go @@ -0,0 +1,114 @@ +package pg + +import ( + "database/sql" + "errors" + "fmt" + + "github.com/Masterminds/squirrel" + "github.com/rarimo/geo-points-svc/internal/data" + "gitlab.com/distributed_lab/kit/pgdb" +) + +const qrCodesTable = "qr_codes" + +type qrCodesQ struct { + db *pgdb.DB + selector squirrel.SelectBuilder + updater squirrel.UpdateBuilder + counter squirrel.SelectBuilder +} + +func NewQRCodesQ(db *pgdb.DB) data.QRCodesQ { + return &qrCodesQ{ + db: db, + selector: squirrel.Select("id", qrCodesTable+".nullifier AS nullifier", "usage_count", "infinity", "reward").From(qrCodesTable), + updater: squirrel.Update(qrCodesTable), + counter: squirrel.Select("COUNT(*) as count").From(qrCodesTable), + } +} + +func (q *qrCodesQ) New() data.QRCodesQ { + return NewQRCodesQ(q.db) +} + +func (q *qrCodesQ) Insert(qrCodes ...data.QRCode) error { + if len(qrCodes) == 0 { + return nil + } + + stmt := squirrel.Insert(qrCodesTable).Columns("id", "nullifier", "reward", "usage_count", "infinity") + for _, qr := range qrCodes { + stmt = stmt.Values(qr.ID, qr.Nullifier, qr.Reward, qr.UsageCount, qr.Infinity) + } + + if err := q.db.Exec(stmt); err != nil { + return fmt.Errorf("insert qr codes: %w", err) + } + + return nil +} + +func (q *qrCodesQ) Update(values map[string]any) error { + + if err := q.db.Exec(q.updater.SetMap(values)); err != nil { + return fmt.Errorf("update qrCode: %w", err) + } + + return nil +} + +func (q *qrCodesQ) Select() ([]data.QRCode, error) { + var res []data.QRCode + + if err := q.db.Select(&res, q.selector); err != nil { + return nil, fmt.Errorf("select qrCodes: %w", err) + } + + return res, nil +} + +func (q *qrCodesQ) Get() (*data.QRCode, error) { + var res data.QRCode + + if err := q.db.Get(&res, q.selector); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + return nil, fmt.Errorf("get qrCode: %w", err) + } + + return &res, nil +} + +func (q *qrCodesQ) Page(page *pgdb.OffsetPageParams) data.QRCodesQ { + q.selector = page.ApplyTo(q.selector, "updated_at") + return q +} + +func (q *qrCodesQ) Count() (uint64, error) { + var res struct { + Count uint64 `db:"count"` + } + + if err := q.db.Get(&res, q.counter); err != nil { + return 0, fmt.Errorf("count qrCodes: %w", err) + } + + return res.Count, nil +} + +func (q *qrCodesQ) FilterByNullifier(nullifiers ...string) data.QRCodesQ { + return q.applyCondition(squirrel.Eq{fmt.Sprintf("%s.nullifier", qrCodesTable): nullifiers}) +} + +func (q *qrCodesQ) FilterByID(ids ...string) data.QRCodesQ { + return q.applyCondition(squirrel.Eq{fmt.Sprintf("%s.id", qrCodesTable): ids}) +} + +func (q *qrCodesQ) applyCondition(cond squirrel.Sqlizer) data.QRCodesQ { + q.selector = q.selector.Where(cond) + q.updater = q.updater.Where(cond) + q.counter = q.counter.Where(cond) + return q +} diff --git a/internal/data/qr_codes.go b/internal/data/qr_codes.go new file mode 100644 index 0000000..5ceb117 --- /dev/null +++ b/internal/data/qr_codes.go @@ -0,0 +1,40 @@ +package data + +import ( + "database/sql" + "time" + + "gitlab.com/distributed_lab/kit/pgdb" +) + +const ( + ColNullifier = "nullifier" + ColUsageCount = "usage_count" + ColInfinity = "infinity" +) + +type QRCode struct { + ID string `db:"id"` + Nullifier sql.NullString `db:"nullifier"` + Reward int `db:"reward"` + UsageCount int `db:"usage_count"` + Infinity bool `db:"infinity"` + + UpdatedAt time.Time `db:"updated_at"` + CreatedAt time.Time `db:"created_at"` +} + +type QRCodesQ interface { + New() QRCodesQ + Insert(...QRCode) error + Update(map[string]any) error + + Page(*pgdb.OffsetPageParams) QRCodesQ + + Get() (*QRCode, error) + Select() ([]QRCode, error) + Count() (uint64, error) + + FilterByID(...string) QRCodesQ + FilterByNullifier(...string) QRCodesQ +} diff --git a/internal/service/handlers/create_qr_code.go b/internal/service/handlers/create_qr_code.go new file mode 100644 index 0000000..77ee5db --- /dev/null +++ b/internal/service/handlers/create_qr_code.go @@ -0,0 +1,79 @@ +package handlers + +import ( + "crypto/rand" + "database/sql" + "encoding/hex" + "net/http" + + "github.com/rarimo/geo-auth-svc/pkg/auth" + "github.com/rarimo/geo-points-svc/internal/data" + "github.com/rarimo/geo-points-svc/internal/service/requests" + "github.com/rarimo/geo-points-svc/resources" + "gitlab.com/distributed_lab/ape" + "gitlab.com/distributed_lab/ape/problems" +) + +func CreateQRCode(w http.ResponseWriter, r *http.Request) { + req, err := requests.NewCreateQRCode(r) + if err != nil { + ape.RenderErr(w, problems.BadRequest(err)...) + return + } + + if !auth.Authenticates(UserClaims(r), auth.AdminGrant) { + ape.RenderErr(w, problems.Unauthorized()) + return + } + + var ( + _ = req.Data.Attributes.Nullifier + dataUsageCount = req.Data.Attributes.UsageCount + dataReward = req.Data.Attributes.Reward + usageCount = 1 + reward = 10 + ) + + qrValue := make([]byte, 10) + _, err = rand.Read(qrValue[:]) + if err != nil { + Log(r).WithError(err).Error("Failed to get rand bytes") + ape.RenderErr(w, problems.InternalError()) + return + } + + if dataUsageCount != nil { + usageCount = *dataUsageCount + } + + if dataReward != nil { + reward = *dataReward + } + + qr := data.QRCode{ + ID: "one_time_" + hex.EncodeToString(qrValue), + Nullifier: sql.NullString{}, + UsageCount: usageCount, + Reward: reward, + } + + if err = QRCodesQ(r).Insert(qr); err != nil { + Log(r).WithError(err).Error("Failed to insert qr code") + ape.RenderErr(w, problems.InternalError()) + return + } + + ape.Render(w, resources.QrCodeRequest{ + Data: resources.QrCode{ + Key: resources.Key{ + ID: qr.ID, + Type: resources.QR_CODE, + }, + Attributes: resources.QrCodeAttributes{ + Reward: &reward, + UsageCount: &usageCount, + }, + }, + }) + +} diff --git a/internal/service/handlers/ctx.go b/internal/service/handlers/ctx.go index 0ccff85..ec2da65 100644 --- a/internal/service/handlers/ctx.go +++ b/internal/service/handlers/ctx.go @@ -30,6 +30,7 @@ const ( dailyQuestionsCtxKey dailyQuestionsCfgCtxKey abstractionCtxKey + qrCodesQCtxKey ) func CtxLog(entry *logan.Entry) func(context.Context) context.Context { @@ -112,6 +113,16 @@ func WithdrawalsQ(r *http.Request) data.WithdrawalsQ { return r.Context().Value(withdrawalsQCtxKey).(data.WithdrawalsQ).New() } +func CtxQRCodesQ(q data.QRCodesQ) func(context.Context) context.Context { + return func(ctx context.Context) context.Context { + return context.WithValue(ctx, qrCodesQCtxKey, q) + } +} + +func QRCodesQ(r *http.Request) data.QRCodesQ { + return r.Context().Value(qrCodesQCtxKey).(data.QRCodesQ).New() +} + func CtxUserClaims(claim []resources.Claim) func(context.Context) context.Context { return func(ctx context.Context) context.Context { return context.WithValue(ctx, userClaimsCtxKey, claim) diff --git a/internal/service/handlers/middleware.go b/internal/service/handlers/middleware.go index ffd4646..4a31bb7 100644 --- a/internal/service/handlers/middleware.go +++ b/internal/service/handlers/middleware.go @@ -52,6 +52,7 @@ func DBCloneMiddleware(db *pgdb.DB) func(http.Handler) http.Handler { CtxEventTypesQ(pg.NewEventTypes(clone)), CtxDailyQuestionsQ(pg.NewDailyQuestionsQ(clone)), CtxWithdrawalsQ(pg.NewWithdrawals(clone)), + CtxQRCodesQ(pg.NewQRCodesQ(clone)), } for _, extender := range extenders { diff --git a/internal/service/handlers/submit_qr_code.go b/internal/service/handlers/submit_qr_code.go new file mode 100644 index 0000000..1688613 --- /dev/null +++ b/internal/service/handlers/submit_qr_code.go @@ -0,0 +1,156 @@ +package handlers + +import ( + "fmt" + "net/http" + "regexp" + + "github.com/go-chi/chi" + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/rarimo/geo-points-svc/internal/data" + "github.com/rarimo/geo-points-svc/internal/data/evtypes" + "github.com/rarimo/geo-points-svc/internal/data/evtypes/models" + "github.com/rarimo/geo-points-svc/internal/data/pg" + "gitlab.com/distributed_lab/ape" + "gitlab.com/distributed_lab/ape/problems" +) + +var qrCodeRegexp = regexp.MustCompile("[0-9A-Za-z_]+") + +func SubmitQRCode(w http.ResponseWriter, r *http.Request) { + qrCode := chi.URLParam(r, "qr_code") + if !qrCodeRegexp.MatchString(qrCode) { + ape.RenderErr(w, problems.BadRequest(validation.Errors{ + "path": fmt.Errorf("invalid qr_code format"), + })...) + return + } + + // never panic because of auth validation + nullifier := UserClaims(r)[0].Nullifier + + log := Log(r).WithFields(map[string]any{ + "nullifier": nullifier, + "qr_code": qrCode, + }) + + balance, err := BalancesQ(r).FilterDisabled().FilterByNullifier(nullifier).Get() + if err != nil { + log.WithError(err).Error("Failed to get balance") + ape.RenderErr(w, problems.InternalError()) + return + } + if balance == nil { + log.Debug("Balance not found") + ape.RenderErr(w, problems.NotFound()) + return + } + if !balance.IsVerified() { + log.Debug("Balance not verified") + ape.RenderErr(w, problems.Forbidden()) + return + } + + qr, err := QRCodesQ(r).FilterByID(qrCode).Get() + if err != nil { + log.WithError(err).Error("Failed to get qr code") + ape.RenderErr(w, problems.InternalError()) + return + } + if qr == nil || (!qr.Infinity && qr.UsageCount <= 0) { + if qr == nil { + log.Debug("QR code absent in db") + ape.RenderErr(w, problems.NotFound()) + return + } + log.Debug("QR code usage count exceed") + ape.RenderErr(w, problems.NotFound()) + return + } + + ev, err := EventsQ(r).FilterByNullifier(nullifier).FilterByType(qrCode).Get() + if err != nil { + log.WithError(err).Error("Failed to get event by type") + ape.RenderErr(w, problems.InternalError()) + return + } + if ev != nil { + log.Debug("User already scan old qr code") + ape.RenderErr(w, problems.Conflict()) + return + } + + ev, err = EventsQ(r).FilterByNullifier(nullifier).FilterByType(models.TypeQRCode).FilterByQRCode(qrCode).Get() + if err != nil { + log.WithError(err).Error("Failed to get event by qrcode") + ape.RenderErr(w, problems.InternalError()) + return + } + if ev != nil { + log.Debug("User already scan qr code") + ape.RenderErr(w, problems.Conflict()) + return + } + + evType := EventTypes(r).Get(models.TypeQRCode, evtypes.FilterInactive) + if evType == nil { + log.Debug("Event QR code absent or inactive") + ape.RenderErr(w, problems.Forbidden()) + return + } + + reward := int64(qr.Reward) + err = EventsQ(r).Transaction(func() error { + if !evType.AutoClaim { + return EventsQ(r).Insert(data.Event{ + Nullifier: nullifier, + Type: models.TypeQRCode, + Status: data.EventFulfilled, + PointsAmount: &reward, + Meta: data.Jsonb(fmt.Sprintf(`{"qr_code": "%s"}`, qr.ID)), + }) + } + + err = EventsQ(r).Insert(data.Event{ + Nullifier: nullifier, + Type: models.TypeQRCode, + Status: data.EventClaimed, + PointsAmount: &reward, + Meta: data.Jsonb(fmt.Sprintf(`{"qr_code": "%s"}`, qr.ID)), + }) + if err != nil { + return fmt.Errorf("failed to insert event: %w", err) + } + + level, err := DoLevelRefUpgrade(Levels(r), ReferralsQ(r), balance, reward) + if err != nil { + return fmt.Errorf("failed to do lvlup and referrals updates: %w", err) + } + + err = BalancesQ(r).FilterByNullifier(balance.Nullifier).Update(map[string]any{ + data.ColAmount: pg.AddToValue(data.ColAmount, reward), + data.ColLevel: level, + }) + if err != nil { + return fmt.Errorf("update balance amount and level: %w", err) + } + + if !qr.Infinity { + err = QRCodesQ(r).FilterByID(qrCode).Update(map[string]any{ + data.ColUsageCount: qr.UsageCount - 1, + }) + if err != nil { + return fmt.Errorf("failed to update qr code: %w", err) + } + } + + return nil + }) + if err != nil { + log.WithError(err).Error("Failed to exec tx") + ape.RenderErr(w, problems.InternalError()) + return + } + + ape.Render(w, newEventClaimingStateResponse(balance.Nullifier, true)) +} diff --git a/internal/service/requests/create_qr_code.go b/internal/service/requests/create_qr_code.go new file mode 100644 index 0000000..66b96f3 --- /dev/null +++ b/internal/service/requests/create_qr_code.go @@ -0,0 +1,28 @@ +package requests + +import ( + "encoding/json" + "net/http" + "strings" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/rarimo/geo-points-svc/resources" +) + +func NewCreateQRCode(r *http.Request) (req resources.QrCodeRequest, err error) { + if err = json.NewDecoder(r.Body).Decode(&req); err != nil { + err = newDecodeError("body", err) + return + } + + req.Data.ID = strings.ToLower(req.Data.ID) + + errs := validation.Errors{ + "data/type": validation.Validate(req.Data.Type, validation.Required, validation.In(resources.QR_CODE)), + "data/attributes/nullifier": validation.Validate(req.Data.Attributes.Nullifier, validation.Match(nullifierRegexp)), + "data/attributes/usage_count": validation.Validate(req.Data.Attributes.UsageCount, validation.Min(int(0))), + "data/attributes/reward": validation.Validate(req.Data.Attributes.Reward, validation.Min(int(1))), + } + + return req, errs.Filter() +} diff --git a/internal/service/router.go b/internal/service/router.go index 527bc08..4ccc129 100644 --- a/internal/service/router.go +++ b/internal/service/router.go @@ -31,68 +31,79 @@ func Run(ctx context.Context, cfg config.Config) { ) authMW := handlers.AuthMiddleware(cfg.Auth(), cfg.Log()) - r.Route("/integrations/geo-points-svc/v1", func(r chi.Router) { - r.Route("/public", func(r chi.Router) { - r.Route("/abstraction", func(r chi.Router) { - r.Route("/accounts", func(r chi.Router) { + r.Route("/integrations/geo-points-svc", func(r chi.Router) { + r.Route("/v2", func(r chi.Router) { + r.Route("/public", func(r chi.Router) { + r.Route("/qrcodes", func(r chi.Router) { r.Use(authMW) - r.Post("/", handlers.CreateAbstractionAccount) - r.Get("/{nullifier}", handlers.GetAbstractionAccount) + r.Post("/", handlers.CreateQRCode) + r.Post("/{qr_code}", handlers.SubmitQRCode) }) }) + }) + r.Route("/v1", func(r chi.Router) { + r.Route("/public", func(r chi.Router) { + r.Route("/abstraction", func(r chi.Router) { + r.Route("/accounts", func(r chi.Router) { + r.Use(authMW) + r.Post("/", handlers.CreateAbstractionAccount) + r.Get("/{nullifier}", handlers.GetAbstractionAccount) + }) + }) - r.Route("/balances", func(r chi.Router) { - r.Use(authMW) - r.Post("/", handlers.CreateBalance) - r.Route("/{nullifier}", func(r chi.Router) { - r.Get("/", handlers.GetBalance) - r.Patch("/", handlers.ActivateBalance) - r.Post("/verifypassport", handlers.VerifyInternalPassport) - r.Post("/join_program", handlers.VerifyInternalPassport) - r.Route("/verify", func(r chi.Router) { - r.Post("/external", handlers.VerifyExternalPassport) + r.Route("/balances", func(r chi.Router) { + r.Use(authMW) + r.Post("/", handlers.CreateBalance) + r.Route("/{nullifier}", func(r chi.Router) { + r.Get("/", handlers.GetBalance) + r.Patch("/", handlers.ActivateBalance) + r.Post("/verifypassport", handlers.VerifyInternalPassport) + r.Post("/join_program", handlers.VerifyInternalPassport) + r.Route("/verify", func(r chi.Router) { + r.Post("/external", handlers.VerifyExternalPassport) + }) + r.Post("/withdrawals", handlers.Withdraw) }) - r.Post("/withdrawals", handlers.Withdraw) }) - }) - r.Route("/daily_questions", func(r chi.Router) { - r.Use(authMW) - r.Route("/admin", func(r chi.Router) { - r.Delete("/{question_id}", handlers.DeleteDailyQuestion) - r.Patch("/{question_id}", handlers.EditDailyQuestion) - r.Post("/", handlers.CreateDailyQuestion) - r.Get("/", handlers.FilterStartAtDailyQuestions) + r.Route("/daily_questions", func(r chi.Router) { + r.Use(authMW) + r.Route("/admin", func(r chi.Router) { + r.Delete("/{question_id}", handlers.DeleteDailyQuestion) + r.Patch("/{question_id}", handlers.EditDailyQuestion) + r.Post("/", handlers.CreateDailyQuestion) + r.Get("/", handlers.FilterStartAtDailyQuestions) + }) + r.Route("/{nullifier}", func(r chi.Router) { + r.Get("/status", handlers.GetDailyQuestionsStatus) + r.Get("/", handlers.GetDailyQuestion) + r.Post("/", handlers.CheckDailyQuestion) + }) }) - r.Route("/{nullifier}", func(r chi.Router) { - r.Get("/status", handlers.GetDailyQuestionsStatus) - r.Get("/", handlers.GetDailyQuestion) - r.Post("/", handlers.CheckDailyQuestion) + r.Route("/events", func(r chi.Router) { + r.Use(authMW) + r.Get("/", handlers.ListEvents) + r.Post("/poll", handlers.FulfillPollEvent) + r.Route("/{id}", func(r chi.Router) { + r.Get("/", handlers.GetEvent) + r.Patch("/", handlers.ClaimEvent) + r.Patch("/qrcode", handlers.FulfillQREvent) + }) }) - }) - r.Route("/events", func(r chi.Router) { - r.Use(authMW) - r.Get("/", handlers.ListEvents) - r.Post("/poll", handlers.FulfillPollEvent) - r.Route("/{id}", func(r chi.Router) { - r.Get("/", handlers.GetEvent) - r.Patch("/", handlers.ClaimEvent) - r.Patch("/qrcode", handlers.FulfillQREvent) + r.Get("/balances", handlers.Leaderboard) + r.Route("/event_types", func(r chi.Router) { + r.Get("/", handlers.ListEventTypes) + r.Get("/{name}", handlers.GetEventType) + r.With(authMW).Get("/qr", handlers.ListQREventTypes) + r.With(authMW).Post("/", handlers.CreateEventType) + r.With(authMW).Put("/{name}", handlers.UpdateEventType) }) }) - r.Get("/balances", handlers.Leaderboard) - r.Route("/event_types", func(r chi.Router) { - r.Get("/", handlers.ListEventTypes) - r.Get("/{name}", handlers.GetEventType) - r.With(authMW).Get("/qr", handlers.ListQREventTypes) - r.With(authMW).Post("/", handlers.CreateEventType) - r.With(authMW).Put("/{name}", handlers.UpdateEventType) + // must be accessible only within the cluster + r.Route("/private", func(r chi.Router) { + r.Post("/referrals", handlers.EditReferrals) }) }) - // must be accessible only within the cluster - r.Route("/private", func(r chi.Router) { - r.Post("/referrals", handlers.EditReferrals) - }) }) cfg.Log().Info("Service started") diff --git a/resources/model_qr_code.go b/resources/model_qr_code.go new file mode 100644 index 0000000..b7f5ca8 --- /dev/null +++ b/resources/model_qr_code.go @@ -0,0 +1,43 @@ +/* + * GENERATED. Do not modify. Your changes might be overwritten! + */ + +package resources + +import "encoding/json" + +type QrCode struct { + Key + Attributes QrCodeAttributes `json:"attributes"` +} +type QrCodeRequest struct { + Data QrCode `json:"data"` + Included Included `json:"included"` +} + +type QrCodeListRequest struct { + Data []QrCode `json:"data"` + Included Included `json:"included"` + Links *Links `json:"links"` + Meta json.RawMessage `json:"meta,omitempty"` +} + +func (r *QrCodeListRequest) PutMeta(v interface{}) (err error) { + r.Meta, err = json.Marshal(v) + return err +} + +func (r *QrCodeListRequest) GetMeta(out interface{}) error { + return json.Unmarshal(r.Meta, out) +} + +// MustQrCode - returns QrCode from include collection. +// if entry with specified key does not exist - returns nil +// if entry with specified key exists but type or ID mismatches - panics +func (c *Included) MustQrCode(key Key) *QrCode { + var qRCode QrCode + if c.tryFindEntry(key, &qRCode) { + return &qRCode + } + return nil +} diff --git a/resources/model_qr_code_attributes.go b/resources/model_qr_code_attributes.go new file mode 100644 index 0000000..57a0a23 --- /dev/null +++ b/resources/model_qr_code_attributes.go @@ -0,0 +1,14 @@ +/* + * GENERATED. Do not modify. Your changes might be overwritten! + */ + +package resources + +type QrCodeAttributes struct { + // For creating personal qr-codes + Nullifier *string `json:"nullifier,omitempty"` + // Reward for this qr-code + Reward *int `json:"reward,omitempty"` + // Specify how many times qr-code can be scaned. Omit if qr-code must have infinity usage count + UsageCount *int `json:"usage_count,omitempty"` +} diff --git a/resources/model_resource_type.go b/resources/model_resource_type.go index 83fbfc8..ada5832 100644 --- a/resources/model_resource_type.go +++ b/resources/model_resource_type.go @@ -20,6 +20,7 @@ const ( EVENT_TYPE ResourceType = "event_type" FULFILL_POLL_EVENT ResourceType = "fulfill_poll_event" FULFILL_QR_EVENT ResourceType = "fulfill_qr_event" + QR_CODE ResourceType = "qr_code" VERIFY_PASSPORT ResourceType = "verify_passport" WITHDRAW ResourceType = "withdraw" )