From 2da42453494d42dcc8222dfc7ea959532b33c6c5 Mon Sep 17 00:00:00 2001 From: Sam Peters Date: Wed, 25 Oct 2023 14:34:17 -0500 Subject: [PATCH] feat: enable quiz completion without reward --- .../dev/apollo-federation/supergraph.graphql | 1 + core/api/src/app/payments/add-earn.ts | 99 ++++++++++++++++--- .../public/root/mutation/quiz-completed.ts | 16 ++- core/api/src/graphql/public/schema.graphql | 1 + .../public/types/object/consumer-account.ts | 16 ++- 5 files changed, 109 insertions(+), 24 deletions(-) diff --git a/core/api/dev/apollo-federation/supergraph.graphql b/core/api/dev/apollo-federation/supergraph.graphql index fa9fd4a6568..cae3ab17b3b 100644 --- a/core/api/dev/apollo-federation/supergraph.graphql +++ b/core/api/dev/apollo-federation/supergraph.graphql @@ -309,6 +309,7 @@ type ConsumerAccount implements Account """List the quiz questions of the consumer account""" quiz: [Quiz!]! + quizRewardsEnabled: Boolean! realtimePrice: RealtimePrice! """ diff --git a/core/api/src/app/payments/add-earn.ts b/core/api/src/app/payments/add-earn.ts index 74c14a476b4..4efa539880c 100644 --- a/core/api/src/app/payments/add-earn.ts +++ b/core/api/src/app/payments/add-earn.ts @@ -29,7 +29,13 @@ export const addEarn = async ({ }: { quizQuestionId: string accountId: string -}): Promise => { +}): Promise< + | { + quizQuestion: Quiz + rewardPaymentErrror?: ApplicationError + } + | ApplicationError +> => { const accountId = checkedToAccountId(accountIdRaw) if (accountId instanceof Error) return accountId @@ -53,12 +59,25 @@ export const addEarn = async ({ const user = await UsersRepository().findById(recipientAccount.kratosUserId) if (user instanceof Error) return user + const isFirstTimeAnsweringQuestion = + await RewardsRepository(accountId).add(quizQuestionId) + if (isFirstTimeAnsweringQuestion instanceof Error) return isFirstTimeAnsweringQuestion + + const quizQuestion: Quiz = { + id: quizQuestionId, + amount: amount, + completed: true, + } + const validatedPhoneMetadata = PhoneMetadataAuthorizer( rewardsConfig.phoneMetadataValidationSettings, ).authorize(user.phoneMetadata) if (validatedPhoneMetadata instanceof Error) { - return new InvalidPhoneForRewardError(validatedPhoneMetadata.name) + return { + quizQuestion, + rewardPaymentErrror: new InvalidPhoneForRewardError(validatedPhoneMetadata.name), + } } const accountIP = await AccountsIpsRepository().findLastByAccountId(recipientAccount.id) @@ -67,26 +86,43 @@ export const addEarn = async ({ const validatedIPMetadata = IPMetadataAuthorizer( rewardsConfig.ipMetadataValidationSettings, ).authorize(accountIP.metadata) + if (validatedIPMetadata instanceof Error) { if (validatedIPMetadata instanceof MissingIPMetadataError) - return new InvalidIpMetadataError(validatedIPMetadata) - - if (validatedIPMetadata instanceof UnauthorizedIPError) return validatedIPMetadata - - return new UnknownRepositoryError("add earn error") + return { + quizQuestion, + rewardPaymentErrror: new InvalidIpMetadataError(validatedIPMetadata), + } + + if (validatedIPMetadata instanceof UnauthorizedIPError) + return { + quizQuestion, + rewardPaymentErrror: validatedIPMetadata, + } + + return { + quizQuestion, + rewardPaymentErrror: new UnknownRepositoryError("add earn error"), + } } const recipientWallets = await WalletsRepository().listByAccountId(accountId) - if (recipientWallets instanceof Error) return recipientWallets + if (recipientWallets instanceof Error) + return { + quizQuestion, + rewardPaymentErrror: recipientWallets, + } const recipientBtcWallet = recipientWallets.find( (wallet) => wallet.currency === WalletCurrency.Btc, ) - if (recipientBtcWallet === undefined) return new NoBtcWalletExistsForAccountError() - const recipientWalletId = recipientBtcWallet.id + if (recipientBtcWallet === undefined) + return { + quizQuestion, + rewardPaymentErrror: new NoBtcWalletExistsForAccountError(), + } - const shouldGiveReward = await RewardsRepository(accountId).add(quizQuestionId) - if (shouldGiveReward instanceof Error) return shouldGiveReward + const recipientWalletId = recipientBtcWallet.id const payment = await intraledgerPaymentSendWalletIdForBtcWallet({ senderWalletId: funderWalletId, @@ -95,7 +131,42 @@ export const addEarn = async ({ memo: quizQuestionId, senderAccount: funderAccount, }) - if (payment instanceof Error) return payment - return { id: quizQuestionId, earnAmount: amount } + return { + quizQuestion, + rewardPaymentErrror: payment instanceof Error ? payment : undefined, + } +} + +export const isAccountEligibleForEarnPayment = async ({ + accountId, +}: { + accountId: AccountId +}): Promise => { + 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 rewardsConfig = getRewardsConfig() + + const validatedPhoneMetadata = PhoneMetadataAuthorizer( + rewardsConfig.phoneMetadataValidationSettings, + ).authorize(user.phoneMetadata) + + if (validatedPhoneMetadata instanceof Error) { + return false + } + + const accountIP = await AccountsIpsRepository().findLastByAccountId(recipientAccount.id) + if (accountIP instanceof Error) return accountIP + + const validatedIPMetadata = IPMetadataAuthorizer( + rewardsConfig.ipMetadataValidationSettings, + ).authorize(accountIP.metadata) + + if (validatedIPMetadata instanceof Error) return false + + return true } diff --git a/core/api/src/graphql/public/root/mutation/quiz-completed.ts b/core/api/src/graphql/public/root/mutation/quiz-completed.ts index 9656e44f15a..6076fd55167 100644 --- a/core/api/src/graphql/public/root/mutation/quiz-completed.ts +++ b/core/api/src/graphql/public/root/mutation/quiz-completed.ts @@ -26,21 +26,19 @@ const QuizCompletedMutation = GT.Field< resolve: async (_, args, { domainAccount }) => { const { id } = args.input - const question = await Payments.addEarn({ + const res = await Payments.addEarn({ quizQuestionId: id, accountId: domainAccount.id, }) - if (question instanceof Error) { - return { errors: [mapAndParseErrorForGqlResponse(question)] } + if (res instanceof Error) { + return { errors: [mapAndParseErrorForGqlResponse(res)] } } return { - errors: [], - quiz: { - id: question.id, - amount: question.earnAmount, - completed: true, - }, + errors: res.rewardPaymentErrror + ? [mapAndParseErrorForGqlResponse(res.rewardPaymentErrror)] + : [], + quiz: res.quizQuestion, } }, }) diff --git a/core/api/src/graphql/public/schema.graphql b/core/api/src/graphql/public/schema.graphql index 0a133b24eea..b4fef7f1080 100644 --- a/core/api/src/graphql/public/schema.graphql +++ b/core/api/src/graphql/public/schema.graphql @@ -230,6 +230,7 @@ type ConsumerAccount implements Account { """List the quiz questions of the consumer account""" quiz: [Quiz!]! + quizRewardsEnabled: Boolean! realtimePrice: RealtimePrice! """ diff --git a/core/api/src/graphql/public/types/object/consumer-account.ts b/core/api/src/graphql/public/types/object/consumer-account.ts index 6d7d740bc7f..a6d602c4c1d 100644 --- a/core/api/src/graphql/public/types/object/consumer-account.ts +++ b/core/api/src/graphql/public/types/object/consumer-account.ts @@ -10,7 +10,7 @@ import CallbackEndpoint from "./callback-endpoint" import { NotificationSettings } from "./notification-settings" -import { Accounts, Prices, Wallets } from "@/app" +import { Accounts, Payments, Prices, Wallets } from "@/app" import { majorToMinorUnit, @@ -158,6 +158,20 @@ const ConsumerAccount = GT.Object({ resolve: (source) => source.quiz, }, + quizRewardsEnabled: { + type: GT.NonNull(GT.Boolean), + resolve: async (source) => { + const rewardsEnabled = await Payments.isAccountEligibleForEarnPayment({ + accountId: source.id, + }) + + if (rewardsEnabled instanceof Error) { + throw mapError(rewardsEnabled) + } + + return rewardsEnabled + }, + }, transactions: { description: "A list of all transactions associated with walletIds optionally passed.",