Skip to content

Commit

Permalink
feat: add the ability to add a referral when creating an account
Browse files Browse the repository at this point in the history
  • Loading branch information
Nicolas Burtey committed Mar 3, 2024
1 parent e7c8306 commit 728830f
Show file tree
Hide file tree
Showing 24 changed files with 111 additions and 17 deletions.
9 changes: 9 additions & 0 deletions bats/core/api/referral.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env bats
load "../../helpers/user.bash"

@test "referral: alice is using bob referral" {
local token_name=$1
local phone=$(random_phone)

login_user "$token_name" "$phone" "bob"
}
5 changes: 4 additions & 1 deletion bats/helpers/user.bash
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
CURRENT_FILE=${BASH_SOURCE:-bats/helpers/.}
echo "Sourcing file: $CURRENT_FILE"
source "$(dirname "$CURRENT_FILE")/_common.bash"
source "$(dirname "$CURRENT_FILE")/cli.bash"

login_user() {
local token_name=$1
local phone=$2
local referral=$3

local code="000000"

Expand All @@ -13,7 +15,8 @@ login_user() {
jq -n \
--arg phone "$phone" \
--arg code "$code" \
'{input: {phone: $phone, code: $code}}'
--arg referral "$referral" \
'{input: {phone: $phone, code: $code, referral: $referral}}'
)
exec_graphql 'anon' 'user-login' "$variables"
auth_token="$(graphql_output '.data.userLogin.authToken')"
Expand Down
7 changes: 7 additions & 0 deletions core/api/src/app/accounts/create-account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ const initializeCreatedAccount = async ({
account,
config,
phone,
referral,
}: {
account: Account
config: AccountsConfig
phone?: PhoneNumber
referral: Referral | undefined
}): Promise<Account | ApplicationError> => {
const walletsEnabledConfig = config.initialWallets

Expand Down Expand Up @@ -50,6 +52,7 @@ const initializeCreatedAccount = async ({

account.statusHistory = [{ status: config.initialStatus, comment: "Initial Status" }]
account.level = config.initialLevel
account.referral = referral

const updatedAccount = await AccountsRepository().update(account)
if (updatedAccount instanceof Error) return updatedAccount
Expand All @@ -76,17 +79,20 @@ export const createAccountForDeviceAccount = async ({
return initializeCreatedAccount({
account: accountNew,
config: levelZeroAccountsConfig,
referral: undefined,
})
}

export const createAccountWithPhoneIdentifier = async ({
newAccountInfo: { kratosUserId, phone },
config,
phoneMetadata,
referral,
}: {
newAccountInfo: NewAccountWithPhoneIdentifier
config: AccountsConfig
phoneMetadata?: PhoneMetadata
referral: Referral | undefined
}): Promise<Account | RepositoryError> => {
const user = await UsersRepository().update({ id: kratosUserId, phone, phoneMetadata })
if (user instanceof Error) return user
Expand All @@ -98,6 +104,7 @@ export const createAccountWithPhoneIdentifier = async ({
account: accountNew,
config,
phone,
referral,
})
if (account instanceof Error) return account

Expand Down
4 changes: 3 additions & 1 deletion core/api/src/app/authentication/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,12 @@ export const loginWithPhoneToken = async ({
phone,
code,
ip,
referral,
}: {
phone: PhoneNumber
code: PhoneCode
ip: IpAddress
referral?: Referral
}): Promise<LoginWithPhoneTokenResult | ApplicationError> => {
{
const limitOk = await checkFailedLoginAttemptPerIpLimits(ip)
Expand Down Expand Up @@ -103,7 +105,6 @@ export const loginWithPhoneToken = async ({

const kratosResult = await authService.createIdentityWithSession({
phone,
phoneMetadata,
})
if (kratosResult instanceof Error) return kratosResult
const { kratosUserId } = kratosResult
Expand All @@ -112,6 +113,7 @@ export const loginWithPhoneToken = async ({
newAccountInfo: { phone, kratosUserId },
config: getDefaultAccountsConfig(),
phoneMetadata,
referral,
})
if (account instanceof Error) {
recordExceptionInCurrentSpan({
Expand Down
1 change: 1 addition & 0 deletions core/api/src/app/bootstrap/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export const bootstrap = async () => {
account = await createAccountWithPhoneIdentifier({
newAccountInfo: { phone, kratosUserId },
config: getDefaultAccountsConfig(),
referral: undefined,
})
}
if (account instanceof Error) return account
Expand Down
1 change: 1 addition & 0 deletions core/api/src/domain/accounts/index.types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ type Account = {
readonly contacts: AccountContact[]
kratosUserId: UserId
displayCurrency: DisplayCurrency
referral?: Referral
// temp
role?: string
}
Expand Down
1 change: 0 additions & 1 deletion core/api/src/domain/authentication/index.types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ interface IAuthWithPhonePasswordlessService {
logoutToken(args: { sessionId: SessionId }): Promise<void | AuthenticationError>
createIdentityWithSession(args: {
phone: PhoneNumber
phoneMetadata?: PhoneMetadata
}): Promise<CreateKratosUserForPhoneNoPasswordSchemaResponse | AuthenticationError>
updateIdentityFromDeviceAccount(args: {
phone: PhoneNumber
Expand Down
3 changes: 2 additions & 1 deletion core/api/src/domain/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class CannotConnectToDbError extends RepositoryError {
export class DbConnectionClosedError extends RepositoryError {
level = ErrorLevel.Critical
}
export class MultipleWalletsFoundForAccountIdAndCurrency extends RepositoryError {}
export class MultipleWalletsFoundForAccountIdAndCurrencyError extends RepositoryError {}
export class WalletInvoiceMissingLnInvoiceError extends RepositoryError {}

export class CouldNotUnsetPhoneFromUserError extends CouldNotUpdateError {}
Expand Down Expand Up @@ -87,6 +87,7 @@ export class InvalidPubKeyError extends ValidationError {}
export class InvalidScanDepthAmount extends ValidationError {}
export class SatoshiAmountRequiredError extends ValidationError {}
export class InvalidUsername extends ValidationError {}
export class InvalidReferralError extends ValidationError {}
export class InvalidDeviceId extends ValidationError {}
export class InvalidIdentityPassword extends ValidationError {}
export class InvalidIdentityUsername extends ValidationError {}
Expand Down
8 changes: 8 additions & 0 deletions core/api/src/domain/users/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
InvalidIdentityUsername,
InvalidLanguageError,
InvalidPhoneNumber,
InvalidReferralError,
} from "@/domain/errors"

export * from "./phone-metadata-authorizer"
Expand Down Expand Up @@ -89,4 +90,11 @@ export const checkedToIdentityPassword = (
return password as IdentityPassword
}

export const checkedToReferral = (referral: string): Referral | ValidationError => {
if (referral.length > 12) {
return new InvalidReferralError(referral)
}
return referral as Referral
}

export { Languages }
2 changes: 2 additions & 0 deletions core/api/src/domain/users/index.types.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
type PhoneNumber = string & { readonly brand: unique symbol }
type PhoneCode = string & { readonly brand: unique symbol }

type Referral = string & { readonly brand: unique symbol }

type EmailAddress = string & { readonly brand: unique symbol }
type EmailCode = string & { readonly brand: unique symbol }

Expand Down
3 changes: 2 additions & 1 deletion core/api/src/graphql/error-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -710,7 +710,8 @@ export const mapError = (error: ApplicationError): CustomGraphQLError => {
case "InvalidCarrierTypeForPhoneMetadataError":
case "InvalidErrorCodeForPhoneMetadataError":
case "InvalidCountryCodeForPhoneMetadataError":
case "MultipleWalletsFoundForAccountIdAndCurrency":
case "MultipleWalletsFoundForAccountIdAndCurrencyError":
case "InvalidReferralError":
message = `Unexpected error occurred, please try again or contact support if it persists (code: ${
error.name
}${error.message ? ": " + error.message : ""})`
Expand Down
12 changes: 11 additions & 1 deletion core/api/src/graphql/public/root/mutation/user-login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Phone from "@/graphql/shared/types/scalar/phone"
import AuthTokenPayload from "@/graphql/shared/types/payload/auth-token"
import { mapAndParseErrorForGqlResponse } from "@/graphql/error-map"
import { Authentication } from "@/app"
import Referral from "@/graphql/shared/types/scalar/referral"

const UserLoginInput = GT.Input({
name: "UserLoginInput",
Expand All @@ -15,6 +16,9 @@ const UserLoginInput = GT.Input({
code: {
type: GT.NonNull(OneTimeAuthCode),
},
referral: {
type: Referral,
},
}),
})

Expand All @@ -25,6 +29,7 @@ const UserLoginMutation = GT.Field<
input: {
phone: PhoneNumber | InputValidationError
code: PhoneCode | InputValidationError
referral?: Referral | InputValidationError
}
}
>({
Expand All @@ -36,7 +41,7 @@ const UserLoginMutation = GT.Field<
input: { type: GT.NonNull(UserLoginInput) },
},
resolve: async (_, args, { ip }) => {
const { phone, code } = args.input
const { phone, code, referral } = args.input

if (phone instanceof Error) {
return { errors: [{ message: phone.message }] }
Expand All @@ -46,6 +51,10 @@ const UserLoginMutation = GT.Field<
return { errors: [{ message: code.message }] }
}

if (referral instanceof Error) {
return { errors: [{ message: referral.message }] }
}

if (ip === undefined) {
return { errors: [{ message: "ip is undefined" }] }
}
Expand All @@ -54,6 +63,7 @@ const UserLoginMutation = GT.Field<
phone,
code,
ip,
referral,
})

if (res instanceof Error) {
Expand Down
4 changes: 4 additions & 0 deletions core/api/src/graphql/public/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1262,6 +1262,9 @@ type RealtimePricePayload {
realtimePrice: RealtimePrice
}

"""Referral code provided by a community"""
scalar Referral

"""
Non-fractional signed whole numeric value between -(2^53) + 1 and 2^53 - 1
"""
Expand Down Expand Up @@ -1583,6 +1586,7 @@ type UserEmailRegistrationValidatePayload {
input UserLoginInput {
code: OneTimeAuthCode!
phone: Phone!
referral: Referral
}

input UserLoginUpgradeInput {
Expand Down
31 changes: 31 additions & 0 deletions core/api/src/graphql/shared/types/scalar/referral.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { checkedToReferral } from "@/domain/users"
import { InputValidationError } from "@/graphql/error"
import { GT } from "@/graphql/index"

const Referral = GT.Scalar({
name: "Referral",
description: "Referral code provided by a community",
parseValue(value) {
if (typeof value !== "string") {
return new InputValidationError({ message: "Invalid type for Referral" })
}
return validReferralValue(value)
},
parseLiteral(ast) {
if (ast.kind === GT.Kind.STRING) {
return validReferralValue(ast.value)
}
return new InputValidationError({ message: "Invalid type for Referral" })
},
})

function validReferralValue(value: string) {
const ReferralNumberValid = checkedToReferral(value)
if (ReferralNumberValid instanceof Error)
return new InputValidationError({
message: "Referral is not a valid",
})
return ReferralNumberValid
}

export default Referral
3 changes: 0 additions & 3 deletions core/api/src/services/kratos/auth-phone-no-password.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,8 @@ export const AuthWithPhonePasswordlessService = (): IAuthWithPhonePasswordlessSe

const createIdentityWithSession = async ({
phone,
phoneMetadata,
}: {
phone: PhoneNumber
phoneMetadata?: PhoneMetadata
}): Promise<CreateKratosUserForPhoneNoPasswordSchemaResponse | KratosError> => {
const traits = { phone }
const method = "password"
Expand All @@ -91,7 +89,6 @@ export const AuthWithPhonePasswordlessService = (): IAuthWithPhonePasswordlessSe
traits,
method,
password,
transient_payload: { phoneMetadata },
},
})
const authToken = result.data.session_token as AuthToken
Expand Down
4 changes: 2 additions & 2 deletions core/api/src/services/mongoose/accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export const AccountsRepository = (): IAccountsRepository => {
withdrawFee,
kratosUserId,
displayCurrency,

referral,
role,
}: Account): Promise<Account | RepositoryError> => {
try {
Expand All @@ -87,7 +87,7 @@ export const AccountsRepository = (): IAccountsRepository => {
withdrawFee,
kratosUserId,
displayCurrency,

referral,
role,
},
{
Expand Down
2 changes: 2 additions & 0 deletions core/api/src/services/mongoose/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,8 @@ const AccountSchema = new Schema<AccountRecord>(
index: true,
},

referral: String,

displayCurrency: String, // FIXME: should be an enum
},
{ id: false },
Expand Down
1 change: 1 addition & 0 deletions core/api/src/services/mongoose/schema.types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ interface AccountRecord {
onchain: OnChainObjectForUser[]
defaultWalletId: WalletId
displayCurrency?: string
referral?: Referral

// mongoose in-built functions
save: () => Promise<AccountRecord>
Expand Down
6 changes: 3 additions & 3 deletions core/api/src/services/mongoose/wallets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
CouldNotFindWalletFromOnChainAddressesError,
CouldNotListWalletsFromAccountIdError,
CouldNotListWalletsFromWalletCurrencyError,
MultipleWalletsFoundForAccountIdAndCurrency,
MultipleWalletsFoundForAccountIdAndCurrencyError,
} from "@/domain/errors"

export const WalletsRepository = (): IWalletsRepository => {
Expand Down Expand Up @@ -99,7 +99,7 @@ export const WalletsRepository = (): IWalletsRepository => {
return new CouldNotFindWalletFromAccountIdAndCurrencyError(WalletCurrency.Btc)
}
if (btcWallets.length > 1) {
return new MultipleWalletsFoundForAccountIdAndCurrency(WalletCurrency.Btc)
return new MultipleWalletsFoundForAccountIdAndCurrencyError(WalletCurrency.Btc)
}
const btcWallet = btcWallets[0]

Expand All @@ -108,7 +108,7 @@ export const WalletsRepository = (): IWalletsRepository => {
return new CouldNotFindWalletFromAccountIdAndCurrencyError(WalletCurrency.Usd)
}
if (usdWallets.length > 1) {
return new MultipleWalletsFoundForAccountIdAndCurrency(WalletCurrency.Usd)
return new MultipleWalletsFoundForAccountIdAndCurrencyError(WalletCurrency.Usd)
}
const usdWallet = usdWallets[0]

Expand Down
2 changes: 2 additions & 0 deletions core/api/test/helpers/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export const createUserAndWalletFromPhone = async (
account = await createAccountWithPhoneIdentifier({
newAccountInfo: { phone, kratosUserId },
config: getDefaultAccountsConfig(),
referral: undefined,
})
if (account instanceof Error) throw account

Expand Down Expand Up @@ -164,6 +165,7 @@ export const createUserAndWallet = async (
account = await createAccountWithPhoneIdentifier({
newAccountInfo: { phone, kratosUserId },
config: getDefaultAccountsConfig(),
referral: undefined,
})
if (account instanceof Error) throw account

Expand Down
Loading

0 comments on commit 728830f

Please sign in to comment.