Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

kyc: add quiz #70

Merged
merged 4 commits into from
Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ kyc/social:
5: https://www.facebook.com/reel/1463272597773681
6: https://www.facebook.com/reel/1463272597773681
allow-long-live-tokens: true
kyc/quiz:
wintr/connectors/storage/v2: *db
maxSessionDurationSeconds: 600
maxQuestionsPerSession: 3
maxWrongAnswersPerSession: 1
sessionCoolDownSeconds: 3600
auth/email-link:
wintr/connectors/storage/v2: *db
fromEmailAddress: [email protected]
Expand Down
9 changes: 5 additions & 4 deletions cmd/eskimo-hut/contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/pkg/errors"

emaillink "github.com/ice-blockchain/eskimo/auth/email_link"
kycquiz "github.com/ice-blockchain/eskimo/kyc/quiz"
kycsocial "github.com/ice-blockchain/eskimo/kyc/social"
"github.com/ice-blockchain/eskimo/users"
)
Expand Down Expand Up @@ -142,9 +143,9 @@ type (
Authorization string `header:"Authorization" swaggerignore:"true" required:"true" allowForbiddenWriteOperation:"true" allowUnauthorized:"true"`
}
StartOrContinueKYCStep4SessionRequestBody struct {
QuestionNumber *uint8 `form:"questionNumber" required:"true" swaggerignore:"true" example:"11"`
SelectedOption *uint8 `form:"selectedOption" required:"true" swaggerignore:"true" example:"0"`
Language string `form:"language" required:"true" swaggerignore:"true" example:"en"`
QuestionNumber uint8 `form:"questionNumber" required:"true" swaggerignore:"true" example:"11"`
}
TryResetKYCStepsRequestBody struct {
Authorization string `header:"Authorization" swaggerignore:"true" required:"true" example:"some token"`
Expand Down Expand Up @@ -188,9 +189,8 @@ const (

noPendingLoginSessionErrorCode = "NO_PENDING_LOGIN_SESSION"

quizAlreadyCompletedSuccessfullyErrorCode = "QUIZ_ALREADY_COMPLETED_SUCCESSFULLY"
questionAlreadyAnsweredErrorCode = "QUESTION_ALREADY_ANSWERED"
quizNotAvailableErrorCode = "QUIZ_NOT_AVAILABLE"
quizAlreadyRunningErrorCode = "QUIZ_ALREADY_RUNNING"
quizUnknownQuestionNumErrorCode = "QUIZ_UNKNOWN_QUESTION_NUM"

socialKYCStepAlreadyCompletedSuccessfullyErrorCode = "SOCIAL_KYC_STEP_ALREADY_COMPLETED_SUCCESSFULLY"
socialKYCStepNotAvailableErrorCode = "SOCIAL_KYC_STEP_NOT_AVAILABLE"
Expand All @@ -211,6 +211,7 @@ type (
// | service implements server.State and is responsible for managing the state and lifecycle of the package.
service struct {
usersProcessor users.Processor
quizRepository kycquiz.Repository
authEmailLinkClient emaillink.Client
socialRepository kycsocial.Repository
}
Expand Down
3 changes: 3 additions & 0 deletions cmd/eskimo-hut/eskimo_hut.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

emaillink "github.com/ice-blockchain/eskimo/auth/email_link"
"github.com/ice-blockchain/eskimo/cmd/eskimo-hut/api"
kycquiz "github.com/ice-blockchain/eskimo/kyc/quiz"
"github.com/ice-blockchain/eskimo/kyc/social"
"github.com/ice-blockchain/eskimo/users"
appcfg "github.com/ice-blockchain/wintr/config"
Expand Down Expand Up @@ -51,6 +52,7 @@ func (s *service) Init(ctx context.Context, cancel context.CancelFunc) {
s.usersProcessor = users.StartProcessor(ctx, cancel)
s.authEmailLinkClient = emaillink.NewClient(ctx, s.usersProcessor, server.Auth(ctx))
s.socialRepository = social.New(ctx, s.usersProcessor)
s.quizRepository = kycquiz.NewRepository(ctx, s.usersProcessor)
}

func (s *service) Close(ctx context.Context) error {
Expand All @@ -59,6 +61,7 @@ func (s *service) Close(ctx context.Context) error {
}

return multierror.Append( //nolint:wrapcheck // Not needed.
errors.Wrap(s.quizRepository.Close(), "could not close quiz repository"),
errors.Wrap(s.socialRepository.Close(), "could not close socialRepository"),
errors.Wrap(s.authEmailLinkClient.Close(), "could not close authEmailLinkClient"),
errors.Wrap(s.usersProcessor.Close(), "could not close usersProcessor"),
Expand Down
111 changes: 66 additions & 45 deletions cmd/eskimo-hut/kyc.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@ package main

import (
"context"
"fmt"
"math/rand"
"slices"
"strings"
stdlibtime "time"

"github.com/pkg/errors"

Expand All @@ -17,7 +13,6 @@ import (
"github.com/ice-blockchain/eskimo/users"
"github.com/ice-blockchain/wintr/log"
"github.com/ice-blockchain/wintr/server"
"github.com/ice-blockchain/wintr/time"
)

func (s *service) setupKYCRoutes(router *server.Router) {
Expand Down Expand Up @@ -52,51 +47,67 @@ func (s *service) setupKYCRoutes(router *server.Router) {
// @Failure 500 {object} server.ErrorResponse
// @Failure 504 {object} server.ErrorResponse "if request times out"
// @Router /kyc/startOrContinueKYCStep4Session/users/{userId} [POST].
func (s *service) StartOrContinueKYCStep4Session( //nolint:gocritic,funlen,revive // .
_ context.Context,
func (s *service) StartOrContinueKYCStep4Session( //nolint:gocritic,funlen,revive,gocognit,cyclop // .
ctx context.Context,
req *server.Request[StartOrContinueKYCStep4SessionRequestBody, kycquiz.Quiz],
) (*server.Response[kycquiz.Quiz], *server.Response[server.ErrorResponse]) {
//nolint:godox // .
// TODO add validations for "selectedOption" && "questionNumber".
// TODO if we don`t support a specific language, default to 'en'.
// TODO return 404 USER_NOT_FOUND if user is not found.
// TODO implement the proper logic for the use cases bellow.
if req.Data.QuestionNumber != 222 { //nolint:gomnd // .
switch rand.Intn(10) { //nolint:gosec,gomnd // .
case 0:
return server.OK(&kycquiz.Quiz{Result: kycquiz.FailureResult}), nil
case 1:
return server.OK(&kycquiz.Quiz{Result: kycquiz.SuccessResult}), nil
case 2: //nolint:gomnd // .
return nil, server.Conflict(errors.Errorf("question already answered, retry with fresh a call (222)"), questionAlreadyAnsweredErrorCode)
const (
magicNumberQuizStart = 222
defaultLanguage = "en"
)

// Handle the session start.
if *req.Data.QuestionNumber == magicNumberQuizStart && *req.Data.SelectedOption == magicNumberQuizStart {
var (
err error
quiz *kycquiz.Quiz
)
langLoop:
for _, lang := range []string{req.Data.Language, defaultLanguage} {
if quiz, err = s.quizRepository.StartQuizSession(ctx, req.AuthenticatedUser.UserID, lang); err != nil {
err = errors.Wrapf(err, "failed to StartQuizSession for userID:%v,language:%v", req.AuthenticatedUser.UserID, lang)
switch {
case errors.Is(err, kycquiz.ErrUnknownLanguage):
continue langLoop

case errors.Is(err, kycquiz.ErrUnknownUser):
return nil, server.NotFound(err, userNotFoundErrorCode)

case errors.Is(err, kycquiz.ErrSessionIsAlreadyRunning):
return nil, server.ForbiddenWithCode(errors.Errorf("another quiz session is already running"), quizAlreadyRunningErrorCode)

case errors.Is(err, kycquiz.ErrSessionFinished), errors.Is(err, kycquiz.ErrSessionFinishedWithError), errors.Is(err, kycquiz.ErrInvalidKYCState): //nolint:lll // .
return nil, server.BadRequest(err, raceConditionErrorCode)

default:
return nil, server.Unexpected(err)
}
}
}

return server.OK(quiz), nil
}
switch rand.Intn(10) { //nolint:gosec,gomnd // .
case 0:
return nil, server.Conflict(errors.Errorf("quiz already finished successfully, ignore it and proceed with mining"), quizAlreadyCompletedSuccessfullyErrorCode)
case 1:
return nil, server.ForbiddenWithCode(errors.Errorf("quiz not available, ignore it and proceed with mining"), quizNotAvailableErrorCode)

// Handle the session continuation.
session, err := s.quizRepository.ContinueQuizSession(ctx, req.AuthenticatedUser.UserID, *req.Data.QuestionNumber, *req.Data.SelectedOption)
if err != nil {
err = errors.Wrapf(err, "failed to ContinueQuizSession for userID:%v,question:%v,option:%v", req.AuthenticatedUser.UserID, *req.Data.QuestionNumber, *req.Data.SelectedOption) //nolint:lll // .
switch {
case errors.Is(err, kycquiz.ErrUnknownUser) || errors.Is(err, kycquiz.ErrUnknownSession):
return nil, server.NotFound(err, userNotFoundErrorCode)

case errors.Is(err, kycquiz.ErrSessionExpired), errors.Is(err, kycquiz.ErrSessionFinished), errors.Is(err, kycquiz.ErrSessionFinishedWithError), errors.Is(err, kycquiz.ErrInvalidKYCState): //nolint:lll // .
return nil, server.BadRequest(err, raceConditionErrorCode)

case errors.Is(err, kycquiz.ErrUnknownQuestionNumber):
return nil, server.BadRequest(err, quizUnknownQuestionNumErrorCode)

default:
return nil, server.Unexpected(err)
}
}

//nolint:lll // .
return server.OK(&kycquiz.Quiz{
Progress: &kycquiz.Progress{
ExpiresAt: time.New(stdlibtime.Now().Add(stdlibtime.Hour)),
NextQuestion: &kycquiz.Question{
Options: []string{
fmt.Sprintf("[%v]You don't need to do anything and the ice is mined automatically", strings.Repeat("bogus", rand.Intn(20))), //nolint:gosec,gomnd,lll // .
fmt.Sprintf("[%v]You need to check in every 24 hours by tapping the Ice button to begin your daily mining session", strings.Repeat("bogus", rand.Intn(20))), //nolint:gosec,gomnd,lll // .
fmt.Sprintf("[%v]Ice is not mined, but it turns out immediately after registration", strings.Repeat("bogus", rand.Intn(20))), //nolint:gosec,gomnd,lll // .
fmt.Sprintf("[%v]Ice is cool", strings.Repeat("bogus", rand.Intn(20))), //nolint:gosec,gomnd // .
},
Number: uint8(11 + rand.Intn(20)), //nolint:gosec,gomnd // .
Text: fmt.Sprintf("[%v][%v] What are the major differences between Ice, Pi and Bee?", req.Data.Language, strings.Repeat("bogus", rand.Intn(20))), //nolint:gosec,gomnd,lll // .
},
MaxQuestions: uint8(30 + rand.Intn(40)), //nolint:gosec,gomnd // .
CorrectAnswers: uint8(1 + rand.Intn(6)), //nolint:gosec,gomnd // .
IncorrectAnswers: uint8(1 + rand.Intn(3)), //nolint:gosec,gomnd // .
},
}), nil
return server.OK(session), nil
}

// VerifySocialKYCStep godoc
Expand Down Expand Up @@ -197,7 +208,7 @@ func validateVerifySocialKYCStep(req *server.Request[kycsocial.VerificationMetad
// @Failure 500 {object} server.ErrorResponse
// @Failure 504 {object} server.ErrorResponse "if request times out"
// @Router /kyc/tryResetKYCSteps/users/{userId} [POST].
func (s *service) TryResetKYCSteps( //nolint:gocritic,funlen,gocognit,revive // .
func (s *service) TryResetKYCSteps( //nolint:gocritic,funlen,gocognit,revive,cyclop,gocyclo // .
ctx context.Context,
req *server.Request[TryResetKYCStepsRequestBody, User],
) (*server.Response[User], *server.Response[server.ErrorResponse]) {
Expand All @@ -219,6 +230,16 @@ func (s *service) TryResetKYCSteps( //nolint:gocritic,funlen,gocognit,revive //
return nil, server.Unexpected(errors.Wrapf(err, "failed to skip kycStep %v", kycStep))
}
}
case users.QuizKYCStep:
if err := s.quizRepository.SkipQuizSession(ctx, req.Data.UserID); err != nil {
if errors.Is(err, kycquiz.ErrInvalidKYCState) || errors.Is(err, kycquiz.ErrSessionFinished) || errors.Is(err, kycquiz.ErrSessionFinishedWithError) || errors.Is(err, kycquiz.ErrSessionExpired) { //nolint:lll // .
log.Error(errors.Wrapf(err, "skipQuizSession failed unexpectedly during tryResetKYCSteps for userID:%v", req.Data.UserID))
err = nil
}
if err != nil {
return nil, server.Unexpected(errors.Wrapf(err, "failed to SkipQuizSession for userID:%v", req.Data.UserID))
}
}
}
}
resp, err := s.usersProcessor.TryResetKYCSteps(ctx, req.Data.UserID)
Expand Down
16 changes: 8 additions & 8 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ require (
github.com/ice-blockchain/go-tarantool-client v0.0.0-20230327200757-4fc71fa3f7bb
github.com/ice-blockchain/wintr v1.128.0
github.com/imroc/req/v3 v3.42.3
github.com/ip2location/ip2location-go/v9 v9.6.1
github.com/ip2location/ip2location-go/v9 v9.7.0
github.com/jackc/pgx/v5 v5.5.1
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.8.4
Expand Down Expand Up @@ -62,7 +62,7 @@ require (
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.20.2 // indirect
github.com/go-openapi/jsonreference v0.20.4 // indirect
github.com/go-openapi/spec v0.20.13 // indirect
github.com/go-openapi/spec v0.20.14 // indirect
github.com/go-openapi/swag v0.22.7 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
Expand Down Expand Up @@ -104,7 +104,7 @@ require (
github.com/opencontainers/image-spec v1.1.0-rc5 // indirect
github.com/opencontainers/runc v1.1.11 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/pierrec/lz4/v4 v4.1.19 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
Expand Down Expand Up @@ -141,8 +141,8 @@ require (
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/arch v0.7.0 // indirect
golang.org/x/crypto v0.18.0 // indirect
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc // indirect
golang.org/x/oauth2 v0.15.0 // indirect
golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e // indirect
golang.org/x/oauth2 v0.16.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
Expand All @@ -152,9 +152,9 @@ require (
google.golang.org/api v0.155.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/appengine/v2 v2.0.5 // indirect
google.golang.org/genproto v0.0.0-20240102182953-50ed04b92917 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 // indirect
google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 // indirect
google.golang.org/grpc v1.60.1 // indirect
google.golang.org/protobuf v1.32.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
Expand Down
Loading
Loading