From 1cf485dbc4dadb2446a30146016de821525c4283 Mon Sep 17 00:00:00 2001 From: Pontus Abrahamsson Date: Fri, 24 Nov 2023 09:55:58 +0100 Subject: [PATCH] Update --- apps/dashboard/package.json | 6 +- .../components/charts/transactions-list.tsx | 12 +- .../tables/transactions/data-table-row.tsx | 12 +- .../src/components/transaction-details.tsx | 52 ++++---- apps/dashboard/src/jobs/transactions.ts | 95 +++++++++++++- apps/website/package.json | 2 +- bun.lockb | Bin 438384 -> 438384 bytes packages/gocardless/src/index.ts | 28 +++++ packages/supabase/src/queries/index.ts | 26 ++-- packages/supabase/src/types/db.ts | 118 ++++++++++++++++-- 10 files changed, 300 insertions(+), 51 deletions(-) diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index a2cf0bf1d..854b3fb2c 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -31,9 +31,9 @@ "framer-motion": "^10.16.5", "next": "14.0.4-canary.4", "next-international": "^1.1.4", - "next-safe-action": "^5.1.2", + "next-safe-action": "^5.1.3", "next-themes": "^0.2.1", - "next-usequerystate": "^1.12.2", + "next-usequerystate": "^1.13.0", "react": "18.2.0", "react-dom": "18.2.0", "react-dropzone": "^14.2.3", @@ -45,7 +45,7 @@ "devDependencies": { "@midday/tsconfig": "workspace:*", "@t3-oss/env-nextjs": "^0.7.1", - "@types/node": "^20.9.4", + "@types/node": "^20.9.5", "@types/react": "^18.2.38", "@types/react-dom": "^18.2.17", "typescript": "^5.3.2" diff --git a/apps/dashboard/src/components/charts/transactions-list.tsx b/apps/dashboard/src/components/charts/transactions-list.tsx index 967330545..febf225a2 100644 --- a/apps/dashboard/src/components/charts/transactions-list.tsx +++ b/apps/dashboard/src/components/charts/transactions-list.tsx @@ -1,5 +1,6 @@ import { formatAmount } from "@/utils/format"; import { getTransactions } from "@midday/supabase/cached-queries"; +import { Avatar, AvatarFallback, AvatarImage } from "@midday/ui/avatar"; import { Icons } from "@midday/ui/icons"; import { Skeleton } from "@midday/ui/skeleton"; import { cn } from "@midday/ui/utils"; @@ -58,7 +59,16 @@ export async function TransactionsList({ type, disabled }) { href={`/transactions?id=${transaction.id}`} className="flex p-3" > -
+
+ + + + + {transaction?.name?.charAt(0)} + + + + - {data.name} + + + + + {data?.name?.charAt(0)} + + + + {data.name} + 0 && "text-[#00C969]")}> diff --git a/apps/dashboard/src/components/transaction-details.tsx b/apps/dashboard/src/components/transaction-details.tsx index 060148b62..f786b6f3f 100644 --- a/apps/dashboard/src/components/transaction-details.tsx +++ b/apps/dashboard/src/components/transaction-details.tsx @@ -8,6 +8,7 @@ import { AccordionItem, AccordionTrigger, } from "@midday/ui/accordion"; +import { Avatar, AvatarFallback, AvatarImage } from "@midday/ui/avatar"; import { Button } from "@midday/ui/button"; import { Icons } from "@midday/ui/icons"; import { Skeleton } from "@midday/ui/skeleton"; @@ -42,33 +43,38 @@ export function TransactionDetails({ transactionId, onClose, data }) {

{isLoading ? ( - + ) : ( data?.name )}

-
- {isLoading ? ( -
- - -
- ) : ( -
- 0 && "text-[#00C969]" - )} - > - {formatAmount({ - amount: data?.amount, - currency: data?.currency, - locale, - })} - -
- )} +
+
+ {isLoading ? ( + + ) : ( +
+ 0 && "text-[#00C969]" + )} + > + {formatAmount({ + amount: data?.amount, + currency: data?.currency, + locale, + })} + +
+ )} +
+ + + + {data?.name?.charAt(0)} + +
diff --git a/apps/dashboard/src/jobs/transactions.ts b/apps/dashboard/src/jobs/transactions.ts index dbbd3045a..953b91060 100644 --- a/apps/dashboard/src/jobs/transactions.ts +++ b/apps/dashboard/src/jobs/transactions.ts @@ -12,6 +12,23 @@ import { capitalCase } from "change-case"; import { revalidateTag } from "next/cache"; import { z } from "zod"; +export async function processPromisesBatch( + items: Array, + limit: number, + fn: (item: any) => Promise +): Promise { + let results = []; + for (let start = 0; start < items.length; start += limit) { + const end = start + limit > items.length ? items.length : start + limit; + + const slicedResults = await Promise.all(items.slice(start, end).map(fn)); + + results = [...results, ...slicedResults]; + } + + return results; +} + const mapTransactionMethod = (method: string) => { switch (method) { case "Payment": @@ -44,7 +61,7 @@ const transformTransactions = (transactions, { teamId, accountId }) => amount: data.transactionAmount.amount, currency: data.transactionAmount.currency, bank_account_id: accountId, - category: data.transactionAmount.amount > 0 ? "income" : "uncategorized", + category: data.transactionAmount.amount > 0 ? "income" : null, team_id: teamId, })); @@ -227,6 +244,13 @@ client.defineJob({ } } + await io.sendEvent("Enrich Transactions", { + name: "transactions.encrichment", + payload: { + teamId: data?.team_id, + }, + }); + if (error) { await io.logger.error(JSON.stringify(error, null, 2)); } @@ -279,6 +303,13 @@ client.defineJob({ revalidateTag(`transactions_${teamId}`); revalidateTag(`spending_${teamId}`); revalidateTag(`metrics_${teamId}`); + + await io.sendEvent("Enrich Transactions", { + name: "transactions.encrichment", + payload: { + teamId, + }, + }); } if (error) { @@ -289,6 +320,68 @@ client.defineJob({ }, }); +client.defineJob({ + id: "transactions-encrichment", + name: "Transactions - Enrichment", + version: "1.0.0", + trigger: eventTrigger({ + name: "transactions.encrichment", + schema: z.object({ + teamId: z.string(), + }), + }), + integrations: { supabase }, + run: async (payload, io) => { + const { teamId } = payload; + + const { data: transactionsData } = await io.supabase.client + .from("transactions") + .select("id, name") + .eq("team_id", teamId) + .is("category", null) + .is("logo_url", null) + .is("enrichment_id", null) + .select(); + + async function enrichTransactions(transaction) { + const { data } = await io.supabase.client + .rpc("search_enriched_transactions", { term: transaction.name }) + .single(); + + return { + ...data, + enriched_id: data?.id, + }; + } + + const result = await processPromisesBatch( + transactionsData, + 5, + enrichTransactions + ); + + if (result.length > 0) { + const { data: updatedTransactions } = await io.supabase.client + .from("transactions") + .upsert(result, { + onConflict: "internal_id", + ignoreDuplicates: true, + }) + .select(); + + if (updatedTransactions?.length > 0) { + revalidateTag(`transactions_${teamId}`); + revalidateTag(`spending_${teamId}`); + revalidateTag(`metrics_${teamId}`); + + await io.logger.info( + `Transactions Enriched: ${updatedTransactions?.length}` + ); + } + } + }, +}); + client.defineJob({ id: "transactions-export", name: "Transactions - Export", diff --git a/apps/website/package.json b/apps/website/package.json index 42dd251f5..972d7ca2f 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -23,7 +23,7 @@ }, "devDependencies": { "@midday/tsconfig": "workspace:*", - "@types/node": "^20.9.4", + "@types/node": "^20.9.5", "@types/react": "^18.2.38", "@types/react-dom": "^18.2.17" } diff --git a/bun.lockb b/bun.lockb index 84f7b3489ed5c83e32c257383cd76b0d3e2d111b..383e09c012ef6147f34e0f52de45d54fb72ece98 100755 GIT binary patch delta 368 zcmV-$0gwLh;2QAY8jvm^c_O)jqN~Sc`@Zi!v1dVGgZSn#tN;e>0*+_#7;Y4Eu}<3B zlW2n|vryYqUjYr1(L^hgkj@FW**XCa29s!0FozgH0k;@I0`(_AQmwIU+O%>Jp!@ZZ z5lssvG_3zw%Evyg)+eSd!Kj)lXB0lHKY?@6IM4GaK0M=I9X{YY)Xoa$_;)ARNvCr>=_#!krNFy z+?;j~jIWRklre*hwRCFu#Z*MMo$LZw2tZ!pNwHmx7swM`zk)liV0p&vl%=j_{3nY9 z`)B@sk$z#47c3*RB9w`!LS{xe<>U36GCk5O4<7zoDjhV`Yifrz^#Zpw^#g;t0X3J; zJq0zl?ym&kjR7^c&pib?0s%9Z3P1%i0W+8J!~_$!P(TIh)B!cOI~4}{bptalFt<31 O2Hpn&Gq)F$23Z{rPM=Ny delta 365 zcmV-z0h0dk;2QAY8jvm^W&HvN$)wXugUcexQsy0fD!jeq7m%s;O=pF}u4F}vu}<3B zlcY~5vryYqUjYo0AxSHfkj@FW**XCa29xMXFozgH0k;@I0`(_A!fc@Mbs=(ofC@V& z@`9L-Lx0Uo(0JQS%Z@O4GEy_DcreVwI^vrHbE7A2ySVUcBP9ZJ8qPBGFZ#!bX7FT~DTzU6s-Zb&pib?0s%6Y3P1%i12Qf$w?9Ay`_ut6w>uRE`gH>`E;6?`iw52Y L0W!B2lLlEG!}_4H diff --git a/packages/gocardless/src/index.ts b/packages/gocardless/src/index.ts index 9871b4585..60d9b65c2 100644 --- a/packages/gocardless/src/index.ts +++ b/packages/gocardless/src/index.ts @@ -252,3 +252,31 @@ export async function getTransactions(accountId: string) { return result.json(); } + +export async function getRequisitions() { + const token = await getAccessToken(); + + const result = await fetch(`${baseUrl}/api/v2/requisitions/`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + }); + + return result.json(); +} + +export async function deleteRequisition(id: string) { + const token = await getAccessToken(); + + const result = await fetch(`${baseUrl}/api/v2/requisitions/${id}/`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + }); + + return result.json(); +} diff --git a/packages/supabase/src/queries/index.ts b/packages/supabase/src/queries/index.ts index 1c658b00e..1dd22d8b5 100644 --- a/packages/supabase/src/queries/index.ts +++ b/packages/supabase/src/queries/index.ts @@ -169,11 +169,14 @@ export async function getSpendingQuery( currency: data?.at(0)?.currency, }, data: Object.entries(combinedValues).map( - ([category, { amount, currency }]) => ({ - category, - currency, - amount: +Math.abs(amount).toFixed(2), - }) + ([category, { amount, currency }]) => { + return { + category: + !category || category === "null" ? "uncategorized" : category, + currency, + amount: +Math.abs(amount).toFixed(2), + }; + } ), }; } @@ -219,6 +222,7 @@ export async function getTransactionsQuery( ` *, assigned:assigned_id(*), + enrichment:enrichment_id(id,category,logo_url), attachments(id,size,name) `, { count: "exact" } @@ -290,7 +294,13 @@ export async function getTransactionsQuery( totalAmount, currency: data?.at(0)?.currency, }, - data, + data: data?.map((transaction) => ({ + ...transaction, + category: + transaction?.category || + transaction?.enrichment?.category || + "uncategorized", + })), }; } @@ -300,9 +310,9 @@ export async function getTransaction(supabase: Client, id: string) { .select( ` *, - account:bank_account_id(*), assigned:assigned_id(*), - attachments(*) + enrichment:enrichment_id(id,category,logo_url), + attachments(id,size,name) ` ) .eq("id", id) diff --git a/packages/supabase/src/types/db.ts b/packages/supabase/src/types/db.ts index b861c0a77..810ed23bd 100644 --- a/packages/supabase/src/types/db.ts +++ b/packages/supabase/src/types/db.ts @@ -54,7 +54,7 @@ export interface Database { isOneToOne: false; referencedRelation: "transactions"; referencedColumns: ["id"]; - }, + } ]; }; bank_accounts: { @@ -124,7 +124,7 @@ export interface Database { isOneToOne: false; referencedRelation: "teams"; referencedColumns: ["id"]; - }, + } ]; }; bank_connections: { @@ -165,7 +165,7 @@ export interface Database { isOneToOne: false; referencedRelation: "teams"; referencedColumns: ["id"]; - }, + } ]; }; teams: { @@ -189,6 +189,34 @@ export interface Database { }; Relationships: []; }; + transaction_enrichments: { + Row: { + category: Database["public"]["Enums"]["transactionCategories"] | null; + created_at: string; + id: string; + logo_url: string | null; + value: string | null; + }; + Insert: { + category?: + | Database["public"]["Enums"]["transactionCategories"] + | null; + created_at?: string; + id?: string; + logo_url?: string | null; + value?: string | null; + }; + Update: { + category?: + | Database["public"]["Enums"]["transactionCategories"] + | null; + created_at?: string; + id?: string; + logo_url?: string | null; + value?: string | null; + }; + Relationships: []; + }; transactions: { Row: { amount: number | null; @@ -199,17 +227,18 @@ export interface Database { created_at: string; currency: string | null; date: string | null; + enrichment_id: string | null; id: string; + internal_id: string | null; + logo_url: string | null; method: Database["public"]["Enums"]["transactionMethods"] | null; name: string | null; note: string | null; order: number; original: string | null; - internal_id: string | null; reference: string; team_id: string | null; transaction_id: string; - vat: Database["public"]["Enums"]["vatRates"] | null; }; Insert: { amount?: number | null; @@ -222,17 +251,18 @@ export interface Database { created_at?: string; currency?: string | null; date?: string | null; + enrichment_id?: string | null; id?: string; + internal_id?: string | null; + logo_url?: string | null; method?: Database["public"]["Enums"]["transactionMethods"] | null; name?: string | null; note?: string | null; order?: number; original?: string | null; - internal_id?: string | null; reference: string; team_id?: string | null; transaction_id: string; - vat?: Database["public"]["Enums"]["vatRates"] | null; }; Update: { amount?: number | null; @@ -245,17 +275,18 @@ export interface Database { created_at?: string; currency?: string | null; date?: string | null; + enrichment_id?: string | null; id?: string; + internal_id?: string | null; + logo_url?: string | null; method?: Database["public"]["Enums"]["transactionMethods"] | null; name?: string | null; note?: string | null; order?: number; original?: string | null; - internal_id?: string | null; reference?: string; team_id?: string | null; transaction_id?: string; - vat?: Database["public"]["Enums"]["vatRates"] | null; }; Relationships: [ { @@ -272,13 +303,20 @@ export interface Database { referencedRelation: "bank_accounts"; referencedColumns: ["id"]; }, + { + foreignKeyName: "transactions_enrichment_id_fkey"; + columns: ["enrichment_id"]; + isOneToOne: false; + referencedRelation: "transaction_enrichments"; + referencedColumns: ["id"]; + }, { foreignKeyName: "transactions_team_id_fkey"; columns: ["team_id"]; isOneToOne: false; referencedRelation: "teams"; referencedColumns: ["id"]; - }, + } ]; }; users: { @@ -323,7 +361,7 @@ export interface Database { isOneToOne: false; referencedRelation: "teams"; referencedColumns: ["id"]; - }, + } ]; }; users_on_team: { @@ -359,7 +397,7 @@ export interface Database { isOneToOne: false; referencedRelation: "users"; referencedColumns: ["id"]; - }, + } ]; }; }; @@ -367,7 +405,60 @@ export interface Database { [_ in never]: never; }; Functions: { - [_ in never]: never; + gtrgm_compress: { + Args: { + "": unknown; + }; + Returns: unknown; + }; + gtrgm_decompress: { + Args: { + "": unknown; + }; + Returns: unknown; + }; + gtrgm_in: { + Args: { + "": unknown; + }; + Returns: unknown; + }; + gtrgm_options: { + Args: { + "": unknown; + }; + Returns: undefined; + }; + gtrgm_out: { + Args: { + "": unknown; + }; + Returns: unknown; + }; + search_enriched_transactions: { + Args: { + term: string; + }; + Returns: { + id: string; + }[]; + }; + set_limit: { + Args: { + "": number; + }; + Returns: number; + }; + show_limit: { + Args: Record; + Returns: number; + }; + show_trgm: { + Args: { + "": string; + }; + Returns: unknown; + }; }; Enums: { bankProviders: "gocardless" | "plaid"; @@ -382,6 +473,7 @@ export interface Database { | "meals" | "equipment" | "activity" + | "uncategorized" | "other"; transactionMethods: | "payment"