diff --git a/.husky/pre-commit b/.husky/pre-commit index d85cb5dea..c0d11a805 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" +# #!/usr/bin/env sh +# . "$(dirname -- "$0")/_/husky.sh" -SKIP_ENV_VALIDATION=true pnpm lint-staged \ No newline at end of file +# SKIP_ENV_VALIDATION=true pnpm lint-staged \ No newline at end of file diff --git a/prisma/migrations/20240822004213_remove_bank_account_primary_unique/migration.sql b/prisma/migrations/20240822004213_remove_bank_account_primary_unique/migration.sql new file mode 100644 index 000000000..9c2413e34 --- /dev/null +++ b/prisma/migrations/20240822004213_remove_bank_account_primary_unique/migration.sql @@ -0,0 +1,2 @@ +-- DropIndex +DROP INDEX "BankAccount_companyId_primary_key"; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 008d0c68f..d001f5271 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -191,7 +191,6 @@ model BankAccount { company Company @relation(fields: [companyId], references: [id], onDelete: Cascade) @@unique([companyId, accountNumber]) - @@unique([companyId, primary], name: "unique_primary_account") @@index([companyId]) } diff --git a/src/app/(authenticated)/(dashboard)/[publicId]/settings/bank-accounts/components/table.tsx b/src/app/(authenticated)/(dashboard)/[publicId]/settings/bank-accounts/components/table.tsx index d623f80f1..427b1aff4 100644 --- a/src/app/(authenticated)/(dashboard)/[publicId]/settings/bank-accounts/components/table.tsx +++ b/src/app/(authenticated)/(dashboard)/[publicId]/settings/bank-accounts/components/table.tsx @@ -1,7 +1,159 @@ -import type { BankAccount } from "@prisma/client"; +"use client"; -const BankAccountsTable = () => { - return <>Table; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { RiMore2Fill } from "@remixicon/react"; +import { Card } from "@/components/ui/card"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { Allow } from "@/components/rbac/allow"; +import { pushModal } from "@/components/modals"; +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { api } from "@/trpc/react"; +import { toast } from "sonner"; + +interface BankAccountType { + id: string; + bankName: string; + accountNumber: string; +} + +interface DeleteDialogProps { + id: string; + open: boolean; + setOpen: (val: boolean) => void; +} + +function DeleteBankAccount({ id, open, setOpen }: DeleteDialogProps) { + const router = useRouter(); + + const deleteMutation = api.bankAccounts.delete.useMutation({ + onSuccess: ({message}) => { + toast.success(message); + router.refresh(); + }, + + onError: (error) => { + console.error("Error deleting Bank Account", error); + toast.error("An error occurred while deleting bank account."); + }, + }); + return ( + + + + Are you sure? + + Are you sure you want to delete this bank account? This action cannot be + undone and you will loose the access if this bank account is currently + being used. + + + + Cancel + deleteMutation.mutateAsync({ id })}> + Continue + + + + + ); +} + + +const BankAccountsTable = ({ + bankAccounts, +}: { + bankAccounts: BankAccountType[]; +}) => { + const [open, setOpen] = useState(false); + + return ( + + + + + Bank Name + Account Number + + Actions + + + + {bankAccounts.map((bank) => ( + + {bank.bankName} + {bank.accountNumber} + {bank.id ? "success" : "unsuccessful"} + + +
+ + + + + + Options + + + { + pushModal("EditBankAccountModal", { + title: "Edit a bank account", + subtitle: "Edit a bank account to receive funds", + data: bank + }); + }}> + Edit Bank Account + + + + {(allow) => ( + setOpen(true)} + > + Delete Bank Account + + )} + + + + setOpen(val)} + id={bank.id} + /> +
+
+
+ ))} +
+
+
+ ); }; export default BankAccountsTable; diff --git a/src/app/(authenticated)/(dashboard)/[publicId]/settings/bank-accounts/page.tsx b/src/app/(authenticated)/(dashboard)/[publicId]/settings/bank-accounts/page.tsx index 357d32436..31c392a07 100644 --- a/src/app/(authenticated)/(dashboard)/[publicId]/settings/bank-accounts/page.tsx +++ b/src/app/(authenticated)/(dashboard)/[publicId]/settings/bank-accounts/page.tsx @@ -49,8 +49,7 @@ const ApiSettingsPage = async () => { - {/* */} - + )} diff --git a/src/components/modals/bank-account-modal.tsx b/src/components/modals/bank-account-modal.tsx index 687e63614..dc7f1bac3 100644 --- a/src/components/modals/bank-account-modal.tsx +++ b/src/components/modals/bank-account-modal.tsx @@ -1,16 +1,320 @@ "use client"; import Modal from "@/components/common/push-modal"; +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Input } from "@/components/ui/input"; +import { Switch } from "@/components/ui/switch"; +import { BankAccountTypeEnum } from "@/prisma/enums"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { type UseFormReturn, useForm } from "react-hook-form"; +import { usePathname, useRouter } from "next/navigation"; +import { z } from "zod"; +import { api } from "@/trpc/react"; +import { toast } from "sonner"; -type ShareClassType = { + +type AddBankAccountType = { title: string | React.ReactNode; subtitle: string | React.ReactNode; }; -export const BankAccountModal = ({ title, subtitle }: ShareClassType) => { +const formSchema = z + .object({ + beneficiaryName: z.string().min(1, { + message: "Beneficiary Name is required", + }), + beneficiaryAddress: z.string().min(1, { + message: "Beneficiary Address is required", + }), + bankName: z.string().min(1, { + message: "Bank Name is required", + }), + bankAddress: z.string().min(1, { + message: "Bank Address is required", + }), + accountNumber: z.string().min(1, { + message: "Account Number is required", + }), + routingNumber: z.string().min(1, { + message: "Routing Number is required", + }), + confirmRoutingNumber: z.string().min(1, { + message: "Confirm Routing Number is required", + }), + accountType: z.nativeEnum(BankAccountTypeEnum).default("CHECKING"), + isPrimary: z.boolean().default(false), + }) + .refine((data) => data.routingNumber === data.confirmRoutingNumber, { + path: ["confirmRoutingNumber"], + message: "Routing Number does not match", + }); + +type TFormSchema = z.infer; + +export const BankAccountModal = ({ title, subtitle }: AddBankAccountType) => { + const router = useRouter(); + const pathname = usePathname(); + const form: UseFormReturn = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + beneficiaryName: "", + beneficiaryAddress: "", + bankName: "", + bankAddress: "", + accountNumber: "", + routingNumber: "", + confirmRoutingNumber: "", + accountType: BankAccountTypeEnum.CHECKING, + isPrimary: false, + }, + }); + + const { mutateAsync: handleBankAccount, isLoading, isSuccess } = + api.bankAccounts.create.useMutation({ + onSuccess: ({message}) => { + if (message.includes("Looks like you have created both primary and non-primary accounts") || message.includes("Looks like there is an account set to primary") || message.includes("Looks like there is an account set to non-primary")) { + toast.error(message) + } else { + toast.success(message) + } + router.refresh() + }, + + onError: (error) => { + console.log("Error creating Bank Account", error); + toast.error("An error occurred while creating bank account."); + }, + }); + + const handleSubmit = async (data: TFormSchema) => { + try { + await handleBankAccount(data); + window.location.href = `${process.env.NEXT_PUBLIC_BASE_URL}/${pathname}` + } catch (error) { + console.log("Error creating Bank Account", error); + } + }; + return ( - Please clone this file and implement the modal. +
+ +
+
+ ( + + Beneficiary Name + + + + + + )} + /> +
+ +
+ ( + + Beneficiary Address + + + + + + )} + /> +
+
+ +
+
+ ( + + Bank Name + + + + + + )} + /> +
+ +
+ ( + + Bank Address + + + + + + )} + /> +
+
+ +
+ ( + + Account Number + + + + + + + )} + /> +
+ +
+
+ ( + + Routing Number + + + + + + + )} + /> +
+ +
+ ( + + Confirm Routing Number + + + + + + )} + /> +
+
+ +
+
+ ( + + Account Type + + + )} + /> +
+ +
+ ( + + Primary Account + + form.setValue("isPrimary", e)} + defaultChecked={false} + /> + + + + )} + /> +
+
+ + + +
+
); }; diff --git a/src/components/modals/edit-bank-account-modal.tsx b/src/components/modals/edit-bank-account-modal.tsx new file mode 100644 index 000000000..b7969e7e8 --- /dev/null +++ b/src/components/modals/edit-bank-account-modal.tsx @@ -0,0 +1,371 @@ +"use client"; + +import Modal from "@/components/common/push-modal"; +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Input } from "@/components/ui/input"; +import { Switch } from "@/components/ui/switch"; +import { BankAccountTypeEnum } from "@/prisma/enums"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { type UseFormReturn, useForm } from "react-hook-form"; +import { usePathname, useRouter } from "next/navigation"; +import { z } from "zod"; +import { api } from "@/trpc/react"; +import { toast } from "sonner"; +import { ConfirmDialog } from "../common/confirmDialog"; +import { useState } from "react"; + + +type AddBankAccountType = { + title: string | React.ReactNode; + subtitle: string | React.ReactNode; + data: any +}; + +type PrimarySwitchedTypeEnum = "null" | "primarySwitchedToTrue" | "primarySwitchedToFalse" + + +const formSchema = z + .object({ + id: z.string().min(1), + beneficiaryName: z.string().min(1, { + message: "Beneficiary Name is required", + }), + beneficiaryAddress: z.string().min(1, { + message: "Beneficiary Address is required", + }), + bankName: z.string().min(1, { + message: "Bank Name is required", + }), + bankAddress: z.string().min(1, { + message: "Bank Address is required", + }), + accountNumber: z.string().min(1, { + message: "Account Number is required", + }), + routingNumber: z.string().min(1, { + message: "Routing Number is required", + }), + confirmRoutingNumber: z.string().min(1, { + message: "Confirm Routing Number is required", + }), + accountType: z.nativeEnum(BankAccountTypeEnum).default("CHECKING"), + primary: z.boolean().default(false), + primarySwitched: z.enum(["null", "primarySwitchedToTrue", "primarySwitchedToFalse"]).default("null") + }) + .refine((data) => data.routingNumber === data.confirmRoutingNumber, { + path: ["confirmRoutingNumber"], + message: "Routing Number does not match", + }); + +type TFormSchema = z.infer; + +export const EditBankAccountModal = ({ title, subtitle, data }: AddBankAccountType) => { + + const router = useRouter(); + const pathname = usePathname(); + const [switchEnabled, setSwitchEnabled] = useState(false); + const [primarySwitched, setPrimarySwitch] = useState("null"); + const form: UseFormReturn = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + id: data?.id, + beneficiaryName: data?.beneficiaryAddress, + beneficiaryAddress: data?.beneficiaryAddress, + bankName: data?.bankName, + bankAddress: data?.bankAddress, + accountNumber: data?.accountNumber, + routingNumber: data?.routingNumber, + confirmRoutingNumber: "", + accountType: data?.accountType, + primary: data?.primary, + primarySwitched: "null", + }, + }); + + const { mutateAsync: handleUpdateBankAccount, isLoading, isSuccess } = + api.bankAccounts.updateBankAccount.useMutation({ + onSuccess: ({message}) => { + toast.success(message) + router.refresh() + }, + + onError: (error) => { + console.log("Error updating Bank Account", error); + toast.error("An error occurred while updating bank account."); + }, + }); + + const handleEnableSwitch = () => { + setSwitchEnabled(true) + } + + const handleSetPrimary = (e: boolean) => { + + if (data?.primary) { + setPrimarySwitch("primarySwitchedToFalse"); + } else { + setPrimarySwitch("primarySwitchedToTrue"); + } + form.setValue("primary", e) + + + } + + const handleFormSubmit = async (data: TFormSchema) => { + try { + data = {...data, primarySwitched: primarySwitched} + + await handleUpdateBankAccount(data); + window.location.href = `${process.env.NEXT_PUBLIC_BASE_URL}/${pathname}` + } catch (error) { + console.log("Error creating Bank Account", error); + } + }; + + return ( + +
+ +
+
+ ( + + Beneficiary Name + + + + + + )} + /> +
+ +
+ ( + + Beneficiary Address + + + + + + )} + /> +
+
+ +
+
+ ( + + Bank Name + + + + + + )} + /> +
+ +
+ ( + + Bank Address + + + + + + )} + /> +
+
+ +
+ ( + + Account Number + + + + + + + )} + /> +
+ +
+
+ ( + + Routing Number + + + + + + + )} + /> +
+ +
+ ( + + Confirm Routing Number + + + + + + )} + /> +
+
+ +
+
+ ( + + Account Type + + + )} + /> +
+ +
+ ( + + Primary Account + + <> + form.setValue("primary", e)} + defaultChecked={data?.primary} + /> + } + onConfirm={handleEnableSwitch} + /> + + { + switchEnabled + && + handleSetPrimary(e)} + defaultChecked={data?.primary} + /> + } + + + + + + )} + /> +
+
+ + + +
+ + + +
+ ); +}; diff --git a/src/components/modals/index.ts b/src/components/modals/index.ts index c05179f74..2ccd4ffd6 100644 --- a/src/components/modals/index.ts +++ b/src/components/modals/index.ts @@ -2,6 +2,7 @@ import { createPushModal } from "pushmodal"; import { BankAccountModal } from "./bank-account-modal"; +import { EditBankAccountModal } from "./edit-bank-account-modal"; import { DocumentUploadModal } from "./document-upload-modal"; import { EquityPlanModal } from "./equity-pan/equity-plan-modal"; import { ExistingSafeModal } from "./existing-safe-modal"; @@ -38,6 +39,7 @@ export const { pushModal, popModal, ModalProvider } = createPushModal({ IssueStockOptionModal, AddEsignDocumentModal, BankAccountModal, + EditBankAccountModal, // Safe modals NewSafeModal, diff --git a/src/components/update/editor.tsx b/src/components/update/editor.tsx index 64742a4c4..b47e5e1da 100644 --- a/src/components/update/editor.tsx +++ b/src/components/update/editor.tsx @@ -166,7 +166,7 @@ const UpdatesEditor = ({ ]; const [title, setTitle] = useState(update?.title ?? ""); - const [content, setContent] = useState( + const [content, setContent] = useState( (update?.content as Block[]) ?? defaultContent, ); const [html, setHtml] = useState(update?.html ?? ""); diff --git a/src/server/audit/schema.ts b/src/server/audit/schema.ts index d952ead73..9ed68f4ec 100644 --- a/src/server/audit/schema.ts +++ b/src/server/audit/schema.ts @@ -61,6 +61,10 @@ export const AuditSchema = z.object({ "accessToken.created", "accessToken.deleted", + "bankAccount.created", + "bankAccount.deleted", + "bankAccount.updated", + "bucket.created", "dataroom.created", @@ -97,6 +101,7 @@ export const AuditSchema = z.object({ "update", "stakeholder", "accessToken", + "bankAccount", "bucket", "stripeSession", "stripeBillingPortalSession", @@ -106,7 +111,7 @@ export const AuditSchema = z.object({ "passkey", ]), id: z.string().optional().nullable(), - }), + }) ), context: z.object({ diff --git a/src/trpc/routers/bank-accounts/router.ts b/src/trpc/routers/bank-accounts/router.ts index a85ab30c8..ebf31a965 100644 --- a/src/trpc/routers/bank-accounts/router.ts +++ b/src/trpc/routers/bank-accounts/router.ts @@ -1,6 +1,12 @@ import { createTRPCRouter, withAccessControl } from "@/trpc/api/trpc"; import { TRPCError } from "@trpc/server"; +import { BankAccountTypeEnum } from "@/prisma/enums"; + import z from "zod"; +import { Prisma } from "@prisma/client"; +import { checkMembership } from "@/server/auth"; +import { Audit } from "@/server/audit"; + export const bankAccountsRouter = createTRPCRouter({ getAll: withAccessControl @@ -24,6 +30,10 @@ export const bankAccountsRouter = createTRPCRouter({ id: true, bankName: true, accountNumber: true, + beneficiaryAddress: true, + beneficiaryName: true, + bankAddress: true, + routingNumber: true, primary: true, createdAt: true, }, @@ -35,23 +45,311 @@ export const bankAccountsRouter = createTRPCRouter({ }), create: withAccessControl + .input( + z + .object({ + beneficiaryName: z.string().min(1, { + message: "Beneficiary Name is required", + }), + beneficiaryAddress: z.string().min(1, { + message: "Beneficiary Address is required", + }), + bankName: z.string().min(1, { + message: "Bank Name is required", + }), + bankAddress: z.string().min(1, { + message: "Bank Address is required", + }), + accountNumber: z.string().min(1, { + message: "Account Number is required", + }), + routingNumber: z.string().min(1, { + message: "Routing Number is required", + }), + confirmRoutingNumber: z.string().min(1, { + message: "Confirm Routing Number is required", + }), + accountType: z.nativeEnum(BankAccountTypeEnum).default("CHECKING"), + isPrimary: z.boolean().default(false), + }) + .refine((data) => data.routingNumber === data.confirmRoutingNumber, { + path: ["confirmRoutingNumber"], + message: "Routing Number does not match", + }) + ) .meta({ policies: { "bank-accounts": { allow: ["create"] } } }) - .mutation(async ({ ctx }) => { - // const { - // db, - // membership: { companyId, memberId }, - // } = ctx; - // TODO // Implement create mutation - }), + .mutation(async ({ ctx, input }) => { + const { + db, + membership: { companyId, memberId }, + userAgent, + requestIp, + session, + } = ctx; + + const user = session.user; + + try { + + const isTherePrimary = await db.bankAccount.findFirst({ + where:{ + companyId, + primary: true + } + }) + + const isThereNonPrimary = await db.bankAccount.findFirst({ + where: { + companyId, + primary: false + } + }) + + if (input.isPrimary) { + if (isTherePrimary) { + throw new TRPCError({code: 'INTERNAL_SERVER_ERROR', message: "Looks like there is an account set to primary"}) + } + } + + if (!input.isPrimary) { + if (isThereNonPrimary) { + throw new TRPCError({code: 'INTERNAL_SERVER_ERROR', message: "Looks like there is an account set to non-primary"}) + } + } + + const newBankAccount = await db.bankAccount.create({ + data: { + beneficiaryName: input.beneficiaryName, + beneficiaryAddress: input.beneficiaryAddress, + bankName: input.bankName, + bankAddress: input.bankAddress, + routingNumber: input.routingNumber, + accountNumber: input.accountNumber, + accountType: input.accountType, + primary: input.isPrimary, + companyId: companyId, + }, + }); + + await Audit.create( + { + action: "bankAccount.created", + companyId, + actor: { type: "user", id: user.id }, + context: { + userAgent, + requestIp, + }, + target: [{ type: "bankAccount", id: newBankAccount.id }], + summary: `${user.name} connected a new Bank Account ${newBankAccount.id}`, + }, + db + ); + + return { + id: newBankAccount.id, + message: "Bank Account created!" + }; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError ) { + if ( error.code === 'P2002' ) { + return { + success: false, + message: "Looks like you have created both primary and non-primary accounts" + } + + } + } + + if (error instanceof TRPCError) { + return { + success: false, + message: error.message, + }; + } + return { + success: false, + message: "Oops, looks like something went wrong!" + } + }} + ), delete: withAccessControl .input(z.object({ id: z.string() })) .meta({ policies: { "bank-accounts": { allow: ["delete"] } } }) .mutation(async ({ ctx, input }) => { - // const { - // db, - // membership: { memberId, companyId }, - // } = ctx; - // TODO // Implement delete mutation + const { + db, + membership: { memberId, companyId }, + session, + requestIp, + userAgent, + } = ctx; + + const { id } = input; + const { user } = session; + + try { + const bankAccount = await db.bankAccount.delete({ + where: { + id, + companyId, + }, + }); + + await Audit.create( + { + action: "bankAccount.deleted", + companyId, + actor: { type: "user", id: user.id }, + context: { + userAgent, + requestIp, + }, + target: [{ type: "bankAccount", id: bankAccount.id }], + summary: `${user.name} deleted the bank account ${bankAccount.id}`, + }, + db + ); + + return { + success: true, + message: "Bank Account has been deleted" + } + } catch (error) { + console.error("Error deleting bank account :", error); + + if (error instanceof TRPCError) { + return { + success: false, + message: error.message, + }; + } + return { + success: false, + message: "Oops, something went wrong. Please try again later.", + }; + } }), + + updateBankAccount: withAccessControl + .input( + z + .object({ + id: z.string().min(1), + beneficiaryName: z.string().min(1, { + message: "Beneficiary Name is required", + }), + beneficiaryAddress: z.string().min(1, { + message: "Beneficiary Address is required", + }), + bankName: z.string().min(1, { + message: "Bank Name is required", + }), + bankAddress: z.string().min(1, { + message: "Bank Address is required", + }), + accountNumber: z.string().min(1, { + message: "Account Number is required", + }), + routingNumber: z.string().min(1, { + message: "Routing Number is required", + }), + confirmRoutingNumber: z.string().min(1, { + message: "Confirm Routing Number is required", + }), + accountType: z.nativeEnum(BankAccountTypeEnum).default("CHECKING"), + primary: z.boolean().default(false), + primarySwitched: z.enum(["null", "primarySwitchedToTrue", "primarySwitchedToFalse"]).default("null") + }) + .refine((data) => data.routingNumber === data.confirmRoutingNumber, { + path: ["confirmRoutingNumber"], + message: "Routing Number does not match", + }) + ) + .meta({ policies: { "bank-accounts": { allow: ["update"] } } }) + .mutation(async ({ ctx, input }) => { + try { + const { + db, + membership: { companyId, memberId }, + userAgent, + requestIp, + session, + } = ctx; + + const {id: stakeHolderId, confirmRoutingNumber, primarySwitched, ...rest } = input; + const user = session.user; + + await db.$transaction( async (tx) => { + if (input.primarySwitched === "primarySwitchedToTrue") { + + await tx.bankAccount.updateMany({ + where: { + companyId, + primary: true + }, + data: { + primary: false + } + }) + + } else if (input.primarySwitched === "primarySwitchedToFalse") { + + const allAccounts = await tx.bankAccount.findMany(); + + // convert the first one we get to primary as automatic function + await tx.bankAccount.update({ + where: { + companyId, + id: allAccounts[0]?.id + }, + data: { + primary: true + } + }) + } + + const updated = await tx.bankAccount.update({ + where: { + id: input.id, + companyId + }, + data: { + ...rest + } + }) + + await Audit.create( + { + action: "bankAccount.updated", + companyId: user.companyId, + actor: { type: "user", id: user.id }, + context: { + requestIp, + userAgent, + }, + target: [{ type: "bankAccount", id: updated.id }], + summary: `${user.name} updated detailes of Bank Account Number : ${updated.accountNumber}`, + }, + tx, + ); + }) + + return { success: true, message: "Bank Account updated successfully."} + } catch (error) { + console.error("Error updating bank account :", error); + + if (error instanceof TRPCError) { + return { + success: false, + message: error.message, + }; + } + return { + success: false, + message: "Oops, something went wrong. Please try again later.", + }; + } + }) });