Skip to content

Commit

Permalink
chore: addressing comments
Browse files Browse the repository at this point in the history
  • Loading branch information
Nicolas Burtey committed Dec 26, 2023
1 parent e1fba3a commit 24daa64
Show file tree
Hide file tree
Showing 15 changed files with 128 additions and 47 deletions.
2 changes: 1 addition & 1 deletion bats/core/api/quiz.bats
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ setup_file() {
[[ "$btc_balance_after_retry" == "$btc_balance_after_quiz" ]] || exit 1
}

@test "quiz: new mutation" {
@test "quiz: completes a quiz question and gets paid once - time based quiz mutation" {
token_name="alice"
question_id="whatIsBitcoin"

Expand Down
2 changes: 1 addition & 1 deletion core/api/dev/apollo-federation/supergraph.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1536,7 +1536,7 @@ type QuizClaimPayload
@join__type(graph: PUBLIC)
{
errors: [Error!]!
quizzes: [Quiz]
quizzes: [Quiz!]!
}

input QuizCompletedInput
Expand Down
119 changes: 102 additions & 17 deletions core/api/src/app/quiz/claim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ import {
NoBtcWalletExistsForAccountError,
NotEnoughBalanceForQuizError,
QuizClaimedTooEarlyError,
UnauthorizedIPError,
UnknownRepositoryError,
} from "@/domain/errors"
import { WalletCurrency } from "@/domain/shared"
import { RateLimitConfig } from "@/domain/rate-limit"
Expand All @@ -35,7 +33,6 @@ import {
import { consumeLimiter } from "@/services/rate-limit"
import { getFunderWalletId } from "@/services/ledger/caching"
import { AccountsIpsRepository } from "@/services/mongoose/accounts-ips"
import { QuizQuestionId } from "@/domain/quiz/index.types"

type ClaimQuizResult = {
id: QuizQuestionId
Expand All @@ -44,16 +41,14 @@ type ClaimQuizResult = {
notBefore: Date | undefined
}[]

export const claimQuiz = async ({
export const claimQuizLegacy = async ({
quizQuestionId: quizQuestionIdString,
accountId: accountIdRaw,
ip,
legacy,
}: {
quizQuestionId: string
accountId: string
ip: IpAddress | undefined
legacy: boolean
}): Promise<ClaimQuizResult | ApplicationError> => {
const check = await checkAddQuizAttemptPerIpLimits(ip)
if (check instanceof Error) return check
Expand Down Expand Up @@ -99,22 +94,12 @@ export const claimQuiz = async ({
if (validatedIPMetadata instanceof MissingIPMetadataError)
return new InvalidIpMetadataError(validatedIPMetadata)

if (validatedIPMetadata instanceof UnauthorizedIPError) return validatedIPMetadata

return new UnknownRepositoryError("claim quiz error")
return validatedIPMetadata
}

const quizzesBefore = await listQuizzesByAccountId(accountId)
if (quizzesBefore instanceof Error) return quizzesBefore

if (!legacy) {
const quiz = quizzesBefore.find((quiz) => quiz.id === quizId)
if (quiz === undefined) return new InvalidQuizQuestionIdError()

if (quiz.notBefore && quiz.notBefore > new Date())
return new QuizClaimedTooEarlyError()
}

const recipientWallets = await WalletsRepository().listByAccountId(accountId)
if (recipientWallets instanceof Error) return recipientWallets

Expand Down Expand Up @@ -162,6 +147,106 @@ const checkAddQuizAttemptPerIpLimits = async (
})
}

export const claimQuiz = async ({
quizQuestionId: quizQuestionIdString,
accountId: accountIdRaw,
ip,
}: {
quizQuestionId: string
accountId: string
ip: IpAddress | undefined
}): Promise<ClaimQuizResult | ApplicationError> => {
const check = await checkAddQuizAttemptPerIpLimits(ip)
if (check instanceof Error) return check

const accountId = checkedToAccountId(accountIdRaw)
if (accountId instanceof Error) return accountId

const quizzesConfig = getQuizzesConfig()

// TODO: quizQuestionId checkedFor
const quizId = quizQuestionIdString as QuizQuestionId

const amount = QuizzesValue[quizId]
if (!amount) return new InvalidQuizQuestionIdError()

const funderWalletId = await getFunderWalletId()
const funderWallet = await WalletsRepository().findById(funderWalletId)
if (funderWallet instanceof Error) return funderWallet
const funderAccount = await AccountsRepository().findById(funderWallet.accountId)
if (funderAccount instanceof Error) return funderAccount

const recipientAccount = await AccountsRepository().findById(accountId)
if (recipientAccount instanceof Error) return recipientAccount

const user = await UsersRepository().findById(recipientAccount.kratosUserId)
if (user instanceof Error) return user

const validatedPhoneMetadata = PhoneMetadataAuthorizer(
quizzesConfig.phoneMetadataValidationSettings,
).authorize(user.phoneMetadata)

if (validatedPhoneMetadata instanceof Error) {
return new InvalidPhoneForQuizError(validatedPhoneMetadata.name)
}

const accountIP = await AccountsIpsRepository().findLastByAccountId(recipientAccount.id)
if (accountIP instanceof Error) return accountIP

const validatedIPMetadata = IPMetadataAuthorizer(
quizzesConfig.ipMetadataValidationSettings,
).authorize(accountIP.metadata)
if (validatedIPMetadata instanceof Error) {
if (validatedIPMetadata instanceof MissingIPMetadataError)
return new InvalidIpMetadataError(validatedIPMetadata)

return validatedIPMetadata
}

const quizzesBefore = await listQuizzesByAccountId(accountId)
if (quizzesBefore instanceof Error) return quizzesBefore

const quiz = quizzesBefore.find((quiz) => quiz.id === quizId)
if (quiz === undefined) return new InvalidQuizQuestionIdError()

if (quiz.notBefore && quiz.notBefore > new Date()) return new QuizClaimedTooEarlyError()

const recipientWallets = await WalletsRepository().listByAccountId(accountId)
if (recipientWallets instanceof Error) return recipientWallets

const recipientBtcWallet = recipientWallets.find(
(wallet) => wallet.currency === WalletCurrency.Btc,
)
if (recipientBtcWallet === undefined) return new NoBtcWalletExistsForAccountError()
const recipientWalletId = recipientBtcWallet.id

const shouldGiveSats = await QuizRepository().add({ quizId, accountId })
if (shouldGiveSats instanceof Error) return shouldGiveSats

const funderBalance = await getBalanceForWallet({ walletId: funderWalletId })
if (funderBalance instanceof Error) return funderBalance

const sendCheck = FunderBalanceChecker().check({
balance: funderBalance as Satoshis,
amountToSend: amount,
})
if (sendCheck instanceof Error) return sendCheck

const payment = await intraledgerPaymentSendWalletIdForBtcWallet({
senderWalletId: funderWalletId,
recipientWalletId,
amount,
memo: quizId,
senderAccount: funderAccount,
})
if (payment instanceof Error) return payment

const quizzesAfter = await listQuizzesByAccountId(accountId)
if (quizzesAfter instanceof Error) return quizzesAfter

return quizzesAfter
}

const FunderBalanceChecker = () => {
const check = ({
balance,
Expand Down
2 changes: 0 additions & 2 deletions core/api/src/domain/quiz/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { QuizQuestionId } from "./index.types"

export const milliSecondsBetweenSections = 60 * 60 * 12 * 1000

export const QuizzesValue: Record<QuizQuestionId, Satoshis> = {
Expand Down
1 change: 0 additions & 1 deletion core/api/src/domain/quiz/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
export * from "./config"

import { QuizzesValue, milliSecondsBetweenSections } from "./config"
import { QuizQuestionId } from "./index.types"
import { QuizzesSectionsConfig } from "./sections"

export interface QuizCompleted {
Expand Down
2 changes: 1 addition & 1 deletion core/api/src/domain/quiz/index.types.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
type QuizzesSectionsConfig = typeof import("./sections").QuizzesSectionsConfig
export type QuizQuestionId = QuizzesSectionsConfig[number]["quiz"][number]
type QuizQuestionId = QuizzesSectionsConfig[number]["quiz"][number]
3 changes: 1 addition & 2 deletions core/api/src/graphql/public/root/mutation/quiz-claim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,9 @@ const QuizClaimMutation = GT.Field<
quizQuestionId: id,
accountId: domainAccount.id,
ip,
legacy: false,
})
if (quizzes instanceof Error) {
return { errors: [mapAndParseErrorForGqlResponse(quizzes)] }
return { errors: [mapAndParseErrorForGqlResponse(quizzes)], quizzes: [] }
}

return {
Expand Down
3 changes: 1 addition & 2 deletions core/api/src/graphql/public/root/mutation/quiz-completed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,10 @@ const QuizCompletedMutation = GT.Field<
resolve: async (_, args, { domainAccount, ip }) => {
const { id } = args.input

const quizzes = await Quiz.claimQuiz({
const quizzes = await Quiz.claimQuizLegacy({
quizQuestionId: id,
accountId: domainAccount.id,
ip,
legacy: true,
})
if (quizzes instanceof Error) {
return { errors: [mapAndParseErrorForGqlResponse(quizzes)] }
Expand Down
2 changes: 1 addition & 1 deletion core/api/src/graphql/public/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1191,7 +1191,7 @@ input QuizClaimInput {

type QuizClaimPayload {
errors: [Error!]!
quizzes: [Quiz]
quizzes: [Quiz!]!
}

input QuizCompletedInput {
Expand Down
2 changes: 1 addition & 1 deletion core/api/src/graphql/public/types/payload/quiz-claim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const QuizClaimPayload = GT.Object({
type: GT.NonNullList(IError),
},
quizzes: {
type: GT.List(Quiz),
type: GT.NonNullList(Quiz),
},
}),
})
Expand Down
1 change: 0 additions & 1 deletion core/api/src/services/mongoose/quiz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { Quiz } from "./schema"
import { QuizCompleted } from "@/domain/quiz"

import { QuizAlreadyPresentError, UnknownRepositoryError } from "@/domain/errors"
import { QuizQuestionId } from "@/domain/quiz/index.types"

interface ExtendedError extends Error {
code?: number
Expand Down
2 changes: 0 additions & 2 deletions core/api/test/integration/app/quizzes/add-quiz.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ describe("addQuiz", () => {
accountId: crypto.randomUUID() as AccountId,
quizQuestionId: "fakeQuizQuestionId",
ip: undefined,
legacy: true,
})
expect(result).toBeInstanceOf(InvalidIpMetadataError)
})
Expand All @@ -44,7 +43,6 @@ describe("addQuiz", () => {
accountId: crypto.randomUUID() as AccountId,
quizQuestionId: "fakeQuizQuestionId",
ip: "192.168.13.13" as IpAddress,
legacy: true,
})

expect(result).toBeInstanceOf(UserAddQuizAttemptIpRateLimiterExceededError)
Expand Down
1 change: 0 additions & 1 deletion core/api/test/integration/services/quiz.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import crypto from "crypto"

import { QuizAlreadyPresentError } from "@/domain/errors"
import { QuizRepository } from "@/services/mongoose"
import { QuizQuestionId } from "@/domain/quiz/index.types"

describe("QuizRepository", () => {
const accountId = crypto.randomUUID() as AccountId
Expand Down
31 changes: 18 additions & 13 deletions core/api/test/unit/domain/quiz/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { fillQuizInformation } from "@/domain/quiz"
import { QuizQuestionId } from "@/domain/quiz/index.types"

describe("quiz", () => {
it("completed is false by default", () => {
Expand All @@ -13,7 +12,7 @@ describe("quiz", () => {
expect(filledInfo.quizzes[0].notBefore).toBe(undefined)
})

it("one element completed", () => {
it("test that we are on section 0 after 1 element has been completed", () => {
const quizCompleted = { quizId: "sat" as QuizQuestionId, createdAt: new Date() }
const filledInfo = fillQuizInformation([quizCompleted])

Expand All @@ -28,20 +27,29 @@ describe("quiz", () => {
expect(filledInfo.currentSection).toBe(0)
})

it("two elements completed", () => {
it("test that we are on section 0 after 2 elements has been completed", () => {
const quizzesCompleted = [
{ quizId: "sat" as QuizQuestionId, createdAt: new Date() },
{ quizId: "whereBitcoinExist" as QuizQuestionId, createdAt: new Date() },
]
const filledInfo = fillQuizInformation(quizzesCompleted)

expect(filledInfo.quizzes.find((quiz) => quiz.completed === true)).toEqual({
amount: 1,
completed: true,
id: "sat",
section: 0,
notBefore: undefined,
})
expect(filledInfo.quizzes.filter((quiz) => quiz.completed === true)).toEqual([
{
amount: 1,
completed: true,
id: "sat",
section: 0,
notBefore: undefined,
},
{
amount: 1,
completed: true,
id: "whereBitcoinExist",
section: 0,
notBefore: undefined,
},
])

expect(filledInfo.currentSection).toBe(0)
})
Expand All @@ -57,9 +65,6 @@ describe("quiz", () => {
const filledInfo = fillQuizInformation(quizzesCompleted)
expect(filledInfo.currentSection).toBe(1)

console.log(filledInfo.quizzes[4])
console.log(filledInfo.quizzes[5])

expect(filledInfo.quizzes[4].notBefore).toBeUndefined()
expect(filledInfo.quizzes[5].notBefore?.getTime()).toBeCloseTo(
new Date().getTime() + 12 * 60 * 60 * 1000,
Expand Down
2 changes: 1 addition & 1 deletion dev/config/apollo-federation/supergraph.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1536,7 +1536,7 @@ type QuizClaimPayload
@join__type(graph: PUBLIC)
{
errors: [Error!]!
quizzes: [Quiz]
quizzes: [Quiz!]!
}

input QuizCompletedInput
Expand Down

0 comments on commit 24daa64

Please sign in to comment.