Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions apps/admin-panel/generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,17 @@ export type AccountDetailPayload = {
readonly errors: ReadonlyArray<Error>;
};

export type AccountForceDeleteInput = {
readonly accountId: Scalars['AccountId']['input'];
readonly cancelIfPositiveBalance?: InputMaybe<Scalars['Boolean']['input']>;
};

export type AccountForceDeletePayload = {
readonly __typename: 'AccountForceDeletePayload';
readonly errors: ReadonlyArray<Error>;
readonly success: Scalars['Boolean']['output'];
};

export const AccountLevel = {
One: 'ONE',
Three: 'THREE',
Expand Down Expand Up @@ -393,6 +404,7 @@ export type MerchantPayload = {

export type Mutation = {
readonly __typename: 'Mutation';
readonly accountForceDelete: AccountForceDeletePayload;
readonly accountUpdateLevel: AccountDetailPayload;
readonly accountUpdateStatus: AccountDetailPayload;
readonly marketingNotificationTrigger: SuccessPayload;
Expand All @@ -403,6 +415,11 @@ export type Mutation = {
};


export type MutationAccountForceDeleteArgs = {
input: AccountForceDeleteInput;
};


export type MutationAccountUpdateLevelArgs = {
input: AccountUpdateLevelInput;
};
Expand Down
8 changes: 8 additions & 0 deletions bats/admin-gql/account-force-delete.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
mutation accountForceDelete($input: AccountForceDeleteInput!) {
accountForceDelete(input: $input) {
errors {
message
}
success
}
}
18 changes: 18 additions & 0 deletions bats/core/api/admin.bats
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,24 @@ getEmailCode() {
[[ "$num_errors" == "0" && "$success" == "true" ]] || exit 1
}

@test "admin: can force delete account" {
admin_token="$(read_value 'admin.token')"

create_user 'tester_delete'
id="$(read_value 'tester_delete.account_id')"

variables=$(
jq -n \
--arg accountId "$id" \
'{input: {accountId: $accountId}}'
)
exec_admin_graphql "$admin_token" 'account-force-delete' "$variables"
num_errors="$(graphql_output '.data.accountForceDelete.errors | length')"
success="$(graphql_output '.data.accountForceDelete.success')"

[[ "$num_errors" == "0" && "$success" == "true" ]] || exit 1
}

# TODO: add check by email

# TODO: business update map info
4 changes: 3 additions & 1 deletion core/api/src/app/accounts/mark-account-for-deletion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ export const markAccountForDeletion = async ({
accountId,
cancelIfPositiveBalance = false,
updatedByPrivilegedClientId,
bypassMaxDeletions = false,
}: {
accountId: AccountId
cancelIfPositiveBalance?: boolean
updatedByPrivilegedClientId?: PrivilegedClientId
bypassMaxDeletions?: boolean
}): Promise<true | ApplicationError> => {
const accountsRepo = AccountsRepository()
const account = await accountsRepo.findById(accountId)
Expand Down Expand Up @@ -59,7 +61,7 @@ export const markAccountForDeletion = async ({
if (user.deletedPhones) {
deletedPhones.push(...user.deletedPhones)
}
if (deletedPhones.length > 0) {
if (deletedPhones.length > 0 && !bypassMaxDeletions) {
const usersByPhones = await usersRepo.findByDeletedPhones(deletedPhones)
if (usersByPhones instanceof Error) return usersByPhones
if (usersByPhones.length >= maxDeletions) return new InvalidAccountForDeletionError()
Expand Down
1 change: 1 addition & 0 deletions core/api/src/app/admin/update-user-email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const updateUserEmail = async ({
const result = await markAccountForDeletion({
accountId: newAccount.id,
cancelIfPositiveBalance: true,
bypassMaxDeletions: true,
updatedByPrivilegedClientId,
})
if (result instanceof Error) return result
Expand Down
1 change: 1 addition & 0 deletions core/api/src/app/admin/update-user-phone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const updateUserPhone = async ({
const result = await markAccountForDeletion({
accountId: newAccount.id,
cancelIfPositiveBalance: true,
bypassMaxDeletions: true,
updatedByPrivilegedClientId,
})
if (result instanceof Error) return result
Expand Down
36 changes: 36 additions & 0 deletions core/api/src/debug/force-delete-account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* how to run:
*
* pnpm tsx src/debug/force-delete-account.ts <account id>
*
* <account id>: ID of the account to force delete (bypasses max deletions limit)
*/

import { Accounts } from "@/app"

import { setupMongoConnection } from "@/services/mongodb"

const main = async () => {
const args = process.argv.slice(-1)
const accountId = args[0] as AccountId

const result = await Accounts.markAccountForDeletion({
accountId,
cancelIfPositiveBalance: true,
bypassMaxDeletions: true,
updatedByPrivilegedClientId: "admin" as PrivilegedClientId,
})

if (result instanceof Error) {
console.error("Error:", result)
return
}
console.log(`Successfully force deleted account ${accountId}`)
}

setupMongoConnection()
.then(async (mongoose) => {
await main()
if (mongoose) await mongoose.connection.close()
})
.catch((err) => console.log(err))
2 changes: 2 additions & 0 deletions core/api/src/graphql/admin/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import MerchantMapValidateMutation from "./root/mutation/merchant-map-validate"

import AccountUpdateLevelMutation from "./root/mutation/account-update-level"
import AccountUpdateStatusMutation from "./root/mutation/account-update-status"
import AccountForceDeleteMutation from "./root/mutation/account-force-delete"

import TriggerMarketingNotificationMutation from "./root/mutation/marketing-notification-trigger"

Expand All @@ -18,6 +19,7 @@ export const mutationFields = {
userUpdateEmail: UserUpdateEmailMutation,
accountUpdateLevel: AccountUpdateLevelMutation,
accountUpdateStatus: AccountUpdateStatusMutation,
accountForceDelete: AccountForceDeleteMutation,
merchantMapValidate: MerchantMapValidateMutation,
merchantMapDelete: MerchantMapDeleteMutation,
marketingNotificationTrigger: TriggerMarketingNotificationMutation,
Expand Down
59 changes: 59 additions & 0 deletions core/api/src/graphql/admin/root/mutation/account-force-delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Accounts } from "@/app"

import { GT } from "@/graphql/index"
import AccountId from "@/graphql/shared/types/scalar/account-id"
import { mapAndParseErrorForGqlResponse } from "@/graphql/error-map"
import AccountForceDeletePayload from "@/graphql/admin/types/payload/account-force-delete"

const AccountForceDeleteInput = GT.Input({
name: "AccountForceDeleteInput",
fields: () => ({
accountId: {
type: GT.NonNull(AccountId),
},
cancelIfPositiveBalance: {
type: GT.Boolean,
defaultValue: true,
},
}),
})

const AccountForceDeleteMutation = GT.Field<
null,
GraphQLAdminContext,
{
input: {
accountId: AccountId | Error
cancelIfPositiveBalance?: boolean
}
}
>({
extensions: {
complexity: 120,
},
type: GT.NonNull(AccountForceDeletePayload),
args: {
input: { type: GT.NonNull(AccountForceDeleteInput) },
},
resolve: async (_, args, { privilegedClientId }) => {
const { accountId, cancelIfPositiveBalance = true } = args.input

if (accountId instanceof Error)
return { errors: [{ message: accountId.message }], success: false }

const result = await Accounts.markAccountForDeletion({
accountId,
cancelIfPositiveBalance,
bypassMaxDeletions: true,
updatedByPrivilegedClientId: privilegedClientId,
})

if (result instanceof Error) {
return { errors: [mapAndParseErrorForGqlResponse(result)], success: false }
}

return { errors: [], success: true }
},
})

export default AccountForceDeleteMutation
11 changes: 11 additions & 0 deletions core/api/src/graphql/admin/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@ type AccountDetailPayload {
errors: [Error!]!
}

input AccountForceDeleteInput {
accountId: AccountId!
cancelIfPositiveBalance: Boolean = true
}

type AccountForceDeletePayload {
errors: [Error!]!
success: Boolean!
}

"""Unique identifier of an account"""
scalar AccountId

Expand Down Expand Up @@ -345,6 +355,7 @@ type MerchantPayload {
}

type Mutation {
accountForceDelete(input: AccountForceDeleteInput!): AccountForceDeletePayload!
accountUpdateLevel(input: AccountUpdateLevelInput!): AccountDetailPayload!
accountUpdateStatus(input: AccountUpdateStatusInput!): AccountDetailPayload!
marketingNotificationTrigger(input: MarketingNotificationTriggerInput!): SuccessPayload!
Expand Down
16 changes: 16 additions & 0 deletions core/api/src/graphql/admin/types/payload/account-force-delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { GT } from "@/graphql/index"
import IError from "@/graphql/shared/types/abstract/error"

const AccountForceDeletePayload = GT.Object({
name: "AccountForceDeletePayload",
fields: () => ({
errors: {
type: GT.NonNullList(IError),
},
success: {
type: GT.NonNull(GT.Boolean),
},
}),
})

export default AccountForceDeletePayload
Loading