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 Dec 11, 2023
1 parent 88e52f4 commit ca0234a
Show file tree
Hide file tree
Showing 26 changed files with 123 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
3 changes: 0 additions & 3 deletions core/api/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,5 @@ redis-cli:
redis-flush:
docker-compose exec redis redis-cli FLUSHDB

codegen:
pnpm run write-sdl

gen-test-jwt:
pnpm run gen-test-jwt
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 newWallet = (currency: WalletCurrency) =>
WalletsRepository().persistNew({
Expand Down Expand Up @@ -53,6 +55,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 @@ -79,17 +82,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 @@ -101,6 +107,7 @@ export const createAccountWithPhoneIdentifier = async ({
account: accountNew,
config,
phone,
referral,
})
if (account instanceof Error) return account

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,12 @@ export const createAccountFromRegistrationPayload = async ({
return regPayload
}

const { userId, phone, phoneMetadata } = regPayload
const { userId, phone, phoneMetadata, referral } = regPayload
const account = await createAccountWithPhoneIdentifier({
newAccountInfo: { phone, kratosUserId: userId },
config: getDefaultAccountsConfig(),
phoneMetadata,
referral,
})
if (account instanceof Error) {
recordExceptionInCurrentSpan({
Expand Down
3 changes: 3 additions & 0 deletions core/api/src/app/authentication/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,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,6 +105,7 @@ export const loginWithPhoneToken = async ({
const kratosResult = await authService.createIdentityWithSession({
phone,
phoneMetadata,
referral,
})
if (kratosResult instanceof Error) return kratosResult

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 @@ -70,6 +70,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 @@ -112,6 +112,7 @@ type Account = {
notificationSettings: NotificationSettings
kratosUserId: UserId
displayCurrency: DisplayCurrency
referral?: Referral
// temp
role?: string
}
Expand Down
10 changes: 9 additions & 1 deletion core/api/src/domain/authentication/index.types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,20 @@ type RegistrationPayload = {
userId: UserId
phone: PhoneNumber
phoneMetadata: PhoneMetadata | undefined
referral: Referral | undefined
}

type TransientPayload = {
phoneMetadata?: Record<string, Record<string, string>>
referral?: string
}

type RegistrationPayloadValidator = {
validate(rawBody: {
identity_id?: string
phone?: string
schema_id?: string
transient_payload?: { phoneMetadata?: Record<string, Record<string, string>> }
transient_payload?: TransientPayload
}): RegistrationPayload | ValidationError
}

Expand All @@ -83,6 +90,7 @@ interface IAuthWithPhonePasswordlessService {
createIdentityWithSession(args: {
phone: PhoneNumber
phoneMetadata?: PhoneMetadata
referral?: Referral | undefined
}): Promise<CreateKratosUserForPhoneNoPasswordSchemaResponse | AuthenticationError>
updateIdentityFromDeviceAccount(args: {
phone: PhoneNumber
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const RegistrationPayloadValidator = (
identity_id?: string
phone?: string
schema_id?: string
transient_payload?: { phoneMetadata?: Record<string, Record<string, string>> }
transient_payload?: TransientPayload
}): RegistrationPayload | ValidationError => {
const {
identity_id: userIdRaw,
Expand All @@ -40,6 +40,7 @@ export const RegistrationPayloadValidator = (
if (phone instanceof Error) return phone

const rawPhoneMetadata = transient_payload?.phoneMetadata
const referral = transient_payload?.referral as Referral | undefined

let phoneMetadata: PhoneMetadata | undefined = undefined

Expand All @@ -56,6 +57,7 @@ export const RegistrationPayloadValidator = (
userId: userIdChecked,
phone,
phoneMetadata,
referral,
}
}

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 @@ -675,7 +675,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 @@ -1189,6 +1189,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 @@ -1509,6 +1512,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
4 changes: 3 additions & 1 deletion core/api/src/services/kratos/auth-phone-no-password.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,11 @@ export const AuthWithPhonePasswordlessService = (): IAuthWithPhonePasswordlessSe
const createIdentityWithSession = async ({
phone,
phoneMetadata,
referral,
}: {
phone: PhoneNumber
phoneMetadata?: PhoneMetadata
referral?: Referral
}): Promise<CreateKratosUserForPhoneNoPasswordSchemaResponse | KratosError> => {
const traits = { phone }
const method = "password"
Expand All @@ -91,7 +93,7 @@ export const AuthWithPhonePasswordlessService = (): IAuthWithPhonePasswordlessSe
traits,
method,
password,
transient_payload: { phoneMetadata },
transient_payload: { phoneMetadata, referral },
},
})
const authToken = result.data.session_token as AuthToken
Expand Down
2 changes: 2 additions & 0 deletions core/api/src/services/mongoose/accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export const AccountsRepository = (): IAccountsRepository => {
kratosUserId,
displayCurrency,
notificationSettings,
referral,

role,
}: Account): Promise<Account | RepositoryError> => {
Expand All @@ -130,6 +131,7 @@ export const AccountsRepository = (): IAccountsRepository => {
kratosUserId,
displayCurrency,
notificationSettings,
referral,

role,
},
Expand Down
Loading

0 comments on commit ca0234a

Please sign in to comment.