Skip to content

Commit

Permalink
Merge pull request #12 from midday-ai/feature/connect-bank-v2
Browse files Browse the repository at this point in the history
Feature/connect bank v2
  • Loading branch information
pontusab authored Feb 26, 2024
2 parents d290758 + 406d9dd commit 0a21bd2
Show file tree
Hide file tree
Showing 24 changed files with 587 additions and 319 deletions.
97 changes: 30 additions & 67 deletions apps/dashboard/src/actions/connect-bank-account-action.ts
Original file line number Diff line number Diff line change
@@ -1,90 +1,53 @@
"use server";

import { processPromisesBatch } from "@/utils/process";
import { LogEvents } from "@midday/events/events";
import { logsnag } from "@midday/events/server";
import { getTransactions, transformTransactions } from "@midday/gocardless";
import { scheduler } from "@midday/jobs";
import { Events, client } from "@midday/jobs";
import { getUser } from "@midday/supabase/cached-queries";
import { createBankAccounts } from "@midday/supabase/mutations";
import { createClient } from "@midday/supabase/server";
import { revalidateTag } from "next/cache";
import { action } from "./safe-action";
import { connectBankAccountSchema } from "./schema";

const BATCH_LIMIT = 500;

export const connectBankAccountAction = action(
connectBankAccountSchema,
async (accounts) => {
async ({ provider, accounts }) => {
const user = await getUser();
const supabase = createClient();
const teamId = user.data.team_id;

const { data } = await createBankAccounts(supabase, accounts);

const promises = data?.map(async (account) => {
// Fetch transactions for each account
const { transactions } = await getTransactions({
accountId: account.account_id,
});
try {
const { data } = await createBankAccounts(
supabase,
accounts.map((account) => ({
...account,
provider,
}))
);

// Schedule sync for each account
await scheduler.register(account.id, {
type: "interval",
options: {
seconds: 3600, // every 1h
const event = await client.sendEvent({
name: Events.TRANSACTIONS_SETUP,
payload: {
teamId: user.data.team_id,
provider,
accounts: data.map((account) => ({
id: account.id,
account_id: account.account_id,
})),
},
});

// Update bank account last_accessed
await supabase
.from("bank_accounts")
.update({
last_accessed: new Date().toISOString(),
})
.eq("id", account.id);

const formattedTransactions = transformTransactions(
transactions?.booked,
{
accountId: account.id, // Bank account row id
teamId,
}
);

// NOTE: We will get all the transactions at once so
// we need to guard against massive payloads
await processPromisesBatch(
formattedTransactions,
BATCH_LIMIT,
async (batch) => {
await supabase.from("transactions").upsert(batch, {
onConflict: "internal_id",
ignoreDuplicates: true,
});
}
);

return;
});

await Promise.all(promises);

revalidateTag(`bank_connections_${teamId}`);
revalidateTag(`transactions_${teamId}`);
revalidateTag(`spending_${teamId}`);
revalidateTag(`metrics_${teamId}`);
revalidateTag(`bank_accounts_${teamId}`);
revalidateTag(`insights_${teamId}`);
logsnag.track({
event: LogEvents.ConnectBankCompleted.name,
icon: LogEvents.ConnectBankCompleted.icon,
user_id: user.data.email,
channel: LogEvents.ConnectBankCompleted.channel,
});

logsnag.track({
event: LogEvents.ConnectBankCompleted.name,
icon: LogEvents.ConnectBankCompleted.icon,
user_id: user.data.email,
channel: LogEvents.ConnectBankCompleted.channel,
});
return event;
} catch (err) {
console.log(err);

return;
throw new Error("Something went wrong");
}
}
);
6 changes: 4 additions & 2 deletions apps/dashboard/src/actions/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,12 @@ const bankAccount = z.object({
institution_id: z.string(),
name: z.string(),
logo_url: z.string().optional(),
owner_name: z.string().optional(),
});

export const connectBankAccountSchema = z.array(bankAccount);
export const connectBankAccountSchema = z.object({
accounts: z.array(bankAccount),
provider: z.enum(["gocardless", "plaid", "teller"]),
});

export const sendFeedbackSchema = z.object({
feedback: z.string(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { CommandMenu } from "@/components/command-menu";
import { ExportStatus } from "@/components/export-status";
import { Header } from "@/components/header";
import { HotKeys } from "@/components/hot-keys";
import { ConnectBankModal } from "@/components/modals/connect-bank-modal";
import { SelectAccountModal } from "@/components/modals/select-account-modal";
import { ConnectGoCardLessModal } from "@/components/modals/connect-gocardless-modal";
import { ConnectTransactionsModal } from "@/components/modals/connect-transactions-modal";
import { SelectAccountGoCardLessModal } from "@/components/modals/select-account-gocardless-modal";
import { Sidebar } from "@/components/sidebar";
import { getCountryCode } from "@midday/location";
import { getUser } from "@midday/supabase/cached-queries";
Expand All @@ -30,8 +31,9 @@ export default async function Layout({
{children}
</div>

<ConnectBankModal countryCode={countryCode} />
<SelectAccountModal countryCode={countryCode} />
<ConnectTransactionsModal countryCode={countryCode} />
<ConnectGoCardLessModal countryCode={countryCode} />
<SelectAccountGoCardLessModal countryCode={countryCode} />
<ExportStatus />
<CommandMenu />
<HotKeys />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const defaultValue = {
};

export default async function Overview({ searchParams }) {
// TODO: Check if there are transactions instead
const bankConnections = await getBankConnectionsByTeamId();
const chartPeriod = cookies().has(Cookies.ChartPeriod)
? JSON.parse(cookies().get(Cookies.ChartPeriod)?.value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default async function Transactions({
}: {
searchParams: { [key: string]: string | string[] | undefined };
}) {
// TODO: Check if there are transactions instead
const bankConnections = await getBankConnectionsByTeamId();
const page = typeof searchParams.page === "string" ? +searchParams.page : 0;
const transactionId = searchParams?.id;
Expand Down
2 changes: 0 additions & 2 deletions apps/dashboard/src/components/activity-list.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { Button } from "@midday/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@midday/ui/card";
Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/src/components/bank-account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function BankAccount({
<div className="ml-4 flex flex-col">
<p className="text-sm font-medium leading-none mb-1">{name}</p>
<span className="text-xs font-medium text-[#606060]">
{bank_name} - {currency}
{bank_name} ({currency})
</span>
<span className="text-xs text-[#606060]">
Last accessed {formatDistanceToNow(new Date(last_accessed))} ago
Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/src/components/connected-accounts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export async function ConnectedAccounts() {

<CardFooter className="flex justify-between">
<div />
<Link href="?step=bank">
<Link href="?step=connect">
<Button data-event="Connect Bank" data-icon="🏦" data-channel="bank">
Connect bank
</Button>
Expand Down
37 changes: 37 additions & 0 deletions apps/dashboard/src/components/loading-transactions-event.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"use client";

import { Button } from "@midday/ui/button";
import { useEventDetails } from "@trigger.dev/react";
import { Loader2 } from "lucide-react";
import { useEffect } from "react";

export function LoadingTransactionsEvent({
eventId,
setEventId,
onClose,
}: {
eventId: string;
}) {
const { data } = useEventDetails(eventId);
const firstRun = data?.runs?.at(0);

useEffect(() => {
if (firstRun?.status === "SUCCESS") {
onClose();
}
}, [firstRun]);

if (firstRun?.status === "FAILURE") {
return (
<Button onClick={() => setEventId(undefined)} className="w-full">
Try again
</Button>
);
}

return (
<Button disabled className="w-full">
<Loader2 className="w-4 h-4 animate-spin pointer-events-none" />
</Button>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
DialogHeader,
DialogTitle,
} from "@midday/ui/dialog";
import { Icons } from "@midday/ui/icons";
import { Input } from "@midday/ui/input";
import { Skeleton } from "@midday/ui/skeleton";
import { isDesktopApp } from "@todesktop/client-core/platform/todesktop";
Expand Down Expand Up @@ -91,14 +92,14 @@ function Row({ id, name, logo, onSelect }) {
);
}

export function ConnectBankModal({ countryCode }) {
export function ConnectGoCardLessModal({ countryCode }) {
const searchParams = useSearchParams();
const router = useRouter();
const pathname = usePathname();
const [loading, setLoading] = useState(true);
const [results, setResults] = useState([]);
const [filteredResults, setFilteredResults] = useState([]);
const isOpen = searchParams.get("step") === "bank";
const isOpen = searchParams.get("step") === "gocardless";

useEffect(() => {
async function fetchData() {
Expand All @@ -122,7 +123,7 @@ export function ConnectBankModal({ countryCode }) {
const redirectBase = isDesktopApp() ? "midday://" : location.origin;

const { link } = await buildLink({
redirect: `${redirectBase}/${pathname}?step=account`,
redirect: `${redirectBase}/${pathname}?step=select-account-gocardless`,
institutionId,
agreement: data.id,
});
Expand All @@ -147,16 +148,24 @@ export function ConnectBankModal({ countryCode }) {
<DialogContent>
<div className="p-4">
<DialogHeader>
<DialogTitle>Connect bank</DialogTitle>
<div className="flex space-x-4 items-center mb-4">
<button
type="button"
className="items-center rounded border bg-accent p-1"
onClick={() => router.back()}
>
<Icons.ArrowBack />
</button>
<DialogTitle className="m-0 p-0">Search Bank</DialogTitle>
</div>
<DialogDescription>
Start by selecting your business bank, once authenticated you can
select which accounts you want to link to your account.
select which accounts you want to link to Midday.
</DialogDescription>

<div>
<Input
placeholder="Search bank"
autoComplete={false}
type="search"
className="my-2"
onChange={(evt) => handleFilterBanks(evt.target.value)}
Expand Down
Loading

0 comments on commit 0a21bd2

Please sign in to comment.