Skip to content

Commit

Permalink
WIP - feat: referral
Browse files Browse the repository at this point in the history
  • Loading branch information
Nicolas Burtey committed Dec 8, 2023
1 parent edacbb8 commit 92b265b
Show file tree
Hide file tree
Showing 13 changed files with 82 additions and 13 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"
}
4 changes: 3 additions & 1 deletion bats/helpers/user.bash
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ 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 +14,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
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 | undefined
}): 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/domain/authentication/index.types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ interface IAuthWithPhonePasswordlessService {
createIdentityWithSession(args: {
phone: PhoneNumber
phoneMetadata?: PhoneMetadata
referral: Referral | undefined
}): 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 @@ -676,7 +676,8 @@ export const mapError = (error: ApplicationError): CustomGraphQLError => {
case "InvalidErrorCodeForPhoneMetadataError":
case "InvalidMobileCountryCodeForPhoneMetadataError":
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
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
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
10 changes: 7 additions & 3 deletions core/api/test/integration/services/wallets-repository.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { randomUUID } from "crypto"

import {
CouldNotFindWalletFromAccountIdAndCurrencyError,
MultipleWalletsFoundForAccountIdAndCurrency,
MultipleWalletsFoundForAccountIdAndCurrencyError,
RepositoryError,
} from "@/domain/errors"
import { WalletCurrency } from "@/domain/shared"
Expand Down Expand Up @@ -68,7 +68,9 @@ describe("WalletsRepository", () => {
await newWallet(WalletCurrency.Usd)

const accountWallets = await wallets.findAccountWalletsByAccountId(accountId)
expect(accountWallets).toBeInstanceOf(MultipleWalletsFoundForAccountIdAndCurrency)
expect(accountWallets).toBeInstanceOf(
MultipleWalletsFoundForAccountIdAndCurrencyError,
)
expect((accountWallets as RepositoryError).message).toBe(WalletCurrency.Btc)
})

Expand All @@ -78,7 +80,9 @@ describe("WalletsRepository", () => {
await newWallet(WalletCurrency.Usd)

const accountWallets = await wallets.findAccountWalletsByAccountId(accountId)
expect(accountWallets).toBeInstanceOf(MultipleWalletsFoundForAccountIdAndCurrency)
expect(accountWallets).toBeInstanceOf(
MultipleWalletsFoundForAccountIdAndCurrencyError,
)
expect((accountWallets as RepositoryError).message).toBe(WalletCurrency.Usd)
})
})
Expand Down

0 comments on commit 92b265b

Please sign in to comment.