From 546c39eb0c3e9e45ac4d5e1a20b6c22184a1d4e5 Mon Sep 17 00:00:00 2001 From: Vincent You <113566592+Vinceyou1@users.noreply.github.com> Date: Mon, 1 Sep 2025 11:45:10 -0500 Subject: [PATCH 1/2] automatic deal screening on ingestion --- app/actions/bulk-screen.ts | 117 ++++++++++++++++++++++ app/actions/bulk-upload-deal.ts | 31 +++--- components/Dialogs/bulk-import-dialog.tsx | 44 ++++++-- components/Dialogs/bulk-screen-dialog.tsx | 5 +- 4 files changed, 174 insertions(+), 23 deletions(-) create mode 100644 app/actions/bulk-screen.ts diff --git a/app/actions/bulk-screen.ts b/app/actions/bulk-screen.ts new file mode 100644 index 0000000..ee98f92 --- /dev/null +++ b/app/actions/bulk-screen.ts @@ -0,0 +1,117 @@ +"use server"; + +import { Deal } from "@prisma/client"; +import { redisClient } from "@/lib/redis"; +import { auth } from "@/auth"; + +// Copied from worker code +interface Submission { + id: string; + userId: string; + name: string; + screenerContent: string; + brokerage: string; + firstName: string; + lastName: string; + linkedinUrl: string; + workPhone: string; + dealCaption: string; + dealType: string; + revenue: number; + ebitda: number; + ebitdaMargin: number; + industry: string; + sourceWebsite: string; + companyLocation: string; +} + +export default async function BulkScreenDeals( + deals: Deal[], + screenerContent: string, // could be screener id/name instead? +): Promise<{ status: number; message: string }> { + // don't think this is necessary + const userSession = await auth(); + + if (!userSession) { + return { + message: "Unauthorized", + status: 400, + }; + } + + if (deals.length === 0) { + return { + status: 400, + message: "No deals were selected", + }; + } + + // if (!screenerId || !screenerContent || !screenerName) { + // console.log( + // "screener information is not present inside screen all function", + // ); + + // return NextResponse.json({ message: "Invalid screener" }, { status: 400 }); + // } + + // console.log("inside api route"); + // console.log(dealListings); + // console.log(screenerId); + + // TODO: is this even necessary? + try { + console.log("connecting to redis"); + // if (!redisClient.isOpen) { + // await redisClient.connect(); + // } + console.log("connected to redis"); + } catch (error) { + console.error("Error connecting to Redis:", error); + return { + message: "Internal Server Error", + status: 500, + }; + } + + try { + console.log("sending all deals to AI screener"); + + deals.forEach(async (dealListing: any) => { + const dealListingWithUserId = { + ...dealListing, + userId: userSession.user.id, + }; + + // typing doesn't actually do anything here + const submission: Submission = { + ...dealListingWithUserId, + screenerContent, + } + + await redisClient.lpush( + "dealListings", + JSON.stringify(submission), + ); + }); + + // publish the message that a new screening call request was made + await redisClient.publish( + "new_screen_call", + JSON.stringify({ + userId: userSession.user.id, + }), + ); + } catch (error) { + console.error("Error pushing to Redis:", error); + + return { + message: "Error pushing to Redis", + status: 500, + }; + } + + return { + message: "Products successfully pushed on to the backend", + status: 200 + } +} diff --git a/app/actions/bulk-upload-deal.ts b/app/actions/bulk-upload-deal.ts index 0e7ebb2..c4a644f 100644 --- a/app/actions/bulk-upload-deal.ts +++ b/app/actions/bulk-upload-deal.ts @@ -2,7 +2,7 @@ import { TransformedDeal } from "../types"; import prismaDB from "@/lib/prisma"; -import { DealType } from "@prisma/client"; +import { Deal, DealType } from "@prisma/client"; import { auth } from "@/auth"; import { rateLimit } from "@/lib/redis"; import { headers } from "next/headers"; @@ -14,15 +14,16 @@ import { headers } from "next/headers"; * Each deal is added to the "manual-deals" collection with a timestamp. * * @param {TransformedDeal[]} deals - An array of deals conforming to the `TransformedDeal` type. - * @returns {Promise<{ type: string; message: string; failedDeals?: TransformedDeal[] }>} + * @returns {Promise<{ status: number; message: string; dbDeals?: Deal[] }>} * Returns an object indicating success or failure and lists any deals that failed to upload. */ -const BulkUploadDealsToDB = async (deals: TransformedDeal[]) => { +const BulkUploadDealsToDB = async (deals: TransformedDeal[]): Promise<{ status: number; message: string; dbDeals?: Deal[]; }> => { const userSession = await auth(); if (!userSession) { return { - error: "unauthorized user", + status: 401, + message: "Unauthorized user", }; } @@ -38,20 +39,22 @@ const BulkUploadDealsToDB = async (deals: TransformedDeal[]) => { console.log("Rate limit excedded for bulk upload db"); return { - error: "Too many requests", + status: 429, + message: "Too many requests", }; } - if (!Array.isArray(deals) || deals.length === 0) { + if (deals.length === 0) { return { - error: "No deals or a valid array provided for bulk upload.", + status: 400, + message: "No deals provided for bulk upload.", }; } console.log("deals received", deals); - + let dbDeals: Deal[] = []; try { - await prismaDB.deal.createMany({ + dbDeals = await prismaDB.deal.createManyAndReturn({ data: deals.map((deal) => ({ title: deal.dealCaption || null, // Title is optional in schema, use null as fallback dealCaption: deal.dealCaption || "", // Required in schema, use empty string as fallback @@ -73,13 +76,15 @@ const BulkUploadDealsToDB = async (deals: TransformedDeal[]) => { }); return { - success: `${deals.length} deals uploaded successfully.`, + status: 200, + message: "Bulk upload successful", + dbDeals, }; } catch (error) { - console.error("Bulk upload error:", error); - + console.error("Bulk upload message:", error); return { - error: "Bulk upload failed due to a server error.", + status: 500, + message: "Bulk upload failed due to a server error.", }; } }; diff --git a/components/Dialogs/bulk-import-dialog.tsx b/components/Dialogs/bulk-import-dialog.tsx index 832dd2a..f3fa3c7 100644 --- a/components/Dialogs/bulk-import-dialog.tsx +++ b/components/Dialogs/bulk-import-dialog.tsx @@ -23,6 +23,7 @@ import { ScrollArea } from "../ui/scroll-area"; import { TransformedDeal } from "@/app/types"; import { useToast } from "@/hooks/use-toast"; import BulkUploadDealsToDB from "@/app/actions/bulk-upload-deal"; +import BulkScreenDeals from "@/app/actions/bulk-screen"; type SheetDeal = { Brokerage: string; @@ -153,24 +154,49 @@ export function BulkImportDialog() { const formattedDeals = transformDeals(deals); console.log("formattedDeals", formattedDeals); - const response = await BulkUploadDealsToDB(formattedDeals); + const uploadResponse = await BulkUploadDealsToDB(formattedDeals); - if (response.error) { - setError(response.error); + // TODO: no console logs past this point seem to have any effect + // toasting and closing the dialog doesn't work + + if (uploadResponse.status != 200) { + setError(uploadResponse.message); toast({ title: "Error uploading deals", variant: "destructive", - description: response.error, + description: uploadResponse.message, + }); + + setUploading(false); + setUploadComplete(true); + return; + } + + const screenResponse = await BulkScreenDeals(uploadResponse.dbDeals!, "").finally(() => console.log("HERE")); + if (screenResponse.status != 200) { + setError(screenResponse.message); + toast({ + title: "Error screening deals", + variant: "destructive", + description: screenResponse.message, }); - } else { - setSuccess("Deals uploaded successfully"); - setDeals([]); - setFile(null); - toast({ title: "Deals uploaded successfully" }); + + setUploading(false); + setUploadComplete(true); + console.log("ERROR") + return; } + setSuccess("Deals uploaded successfully"); + setDeals([]); + setFile(null); + + // toasting doesn't appear to work + toast({ title: "Deals uploaded successfully" }); + setUploading(false); setUploadComplete(true); + setIsOpen(false); }; return ( diff --git a/components/Dialogs/bulk-screen-dialog.tsx b/components/Dialogs/bulk-screen-dialog.tsx index 1444bad..9091748 100644 --- a/components/Dialogs/bulk-screen-dialog.tsx +++ b/components/Dialogs/bulk-screen-dialog.tsx @@ -169,7 +169,10 @@ function BulkScreenComponent({ > {data && - data.map((screener) => ( + [...data, { + id: "blablabla", + name: "testing 123" + }].map((screener) => ( From deab446676e64838c0f3353bf60f3907c6e2a68f Mon Sep 17 00:00:00 2001 From: Vincent You <113566592+Vinceyou1@users.noreply.github.com> Date: Sun, 7 Sep 2025 22:18:41 -0500 Subject: [PATCH 2/2] optional screening on import --- components/Dialogs/bulk-import-dialog.tsx | 71 ++++++++++++++++++++--- components/Dialogs/bulk-screen-dialog.tsx | 5 +- 2 files changed, 65 insertions(+), 11 deletions(-) diff --git a/components/Dialogs/bulk-import-dialog.tsx b/components/Dialogs/bulk-import-dialog.tsx index f3fa3c7..44b2123 100644 --- a/components/Dialogs/bulk-import-dialog.tsx +++ b/components/Dialogs/bulk-import-dialog.tsx @@ -25,6 +25,10 @@ import { useToast } from "@/hooks/use-toast"; import BulkUploadDealsToDB from "@/app/actions/bulk-upload-deal"; import BulkScreenDeals from "@/app/actions/bulk-screen"; +import useSWR from "swr"; +import { DealScreenersGET } from "@/app/types"; +import { fetcher } from "@/lib/utils"; + type SheetDeal = { Brokerage: string; "First Name"?: string; @@ -53,6 +57,15 @@ export function BulkImportDialog() { const [uploading, setUploading] = useState(false); const [uploadComplete, setUploadComplete] = useState(false); + const { + data: screeners, + error: screenerFetchError, + isLoading, + } = useSWR(`/api/deal-screeners`, fetcher); + const [selectedScreenerId, setSelectedScreenerId] = React.useState< + string | null + >(null); + const expectedHeaders = [ "Brokerage", "First Name", @@ -156,9 +169,6 @@ export function BulkImportDialog() { console.log("formattedDeals", formattedDeals); const uploadResponse = await BulkUploadDealsToDB(formattedDeals); - // TODO: no console logs past this point seem to have any effect - // toasting and closing the dialog doesn't work - if (uploadResponse.status != 200) { setError(uploadResponse.message); toast({ @@ -171,8 +181,25 @@ export function BulkImportDialog() { setUploadComplete(true); return; } + console.log(selectedScreenerId) + if (selectedScreenerId === null) { + setSuccess("Deals uploaded successfully"); + setDeals([]); + setFile(null); + + setUploading(false); + setUploadComplete(true); + return; + } + + const screenerContent = screeners?.find( + (screener) => screener.id === selectedScreenerId, + )?.content; - const screenResponse = await BulkScreenDeals(uploadResponse.dbDeals!, "").finally(() => console.log("HERE")); + const screenResponse = await BulkScreenDeals( + uploadResponse.dbDeals!, + screenerContent!, + ); if (screenResponse.status != 200) { setError(screenResponse.message); toast({ @@ -183,7 +210,6 @@ export function BulkImportDialog() { setUploading(false); setUploadComplete(true); - console.log("ERROR") return; } @@ -196,12 +222,17 @@ export function BulkImportDialog() { setUploading(false); setUploadComplete(true); - setIsOpen(false); }; return ( <> - + { + setSuccess(null); + setIsOpen(open); + }} + >