From 477bd8476c94c54327f518ad4ccb47a1254341ca Mon Sep 17 00:00:00 2001 From: Rohan Thakkar <552rthakkar@gmail.com> Date: Mon, 6 Apr 2026 16:27:25 +0000 Subject: [PATCH 1/3] 1 to 1ness enforced using task status --- src/services/sponsors/sponsor-router.ts | 66 ++++++++++++++++++++++--- src/services/tasks/task-router.ts | 22 ++++++++- 2 files changed, 80 insertions(+), 8 deletions(-) diff --git a/src/services/sponsors/sponsor-router.ts b/src/services/sponsors/sponsor-router.ts index f77dd33..45e4b9b 100644 --- a/src/services/sponsors/sponsor-router.ts +++ b/src/services/sponsors/sponsor-router.ts @@ -2,11 +2,34 @@ import { Router, Request, Response, NextFunction } from "express"; import { isValidSponsorInsertFormat, isValidSponsorUpdateFormat, SponsorInsert, SponsorUpdate } from "./sponsor-formats"; import { RouterError } from "../../middleware/error-handler"; import StatusCode from "status-code-enum"; -import { Roles, Tables } from "../../lib/db/strings"; +import { EmailStatus, Roles, Tables } from "../../lib/db/strings"; import { createUser, requireMemberRole } from "../../middleware/auth"; const sponsorRouter: Router = Router(); +/** + * Helper function to filterout inactive tasks from the sponsors array + * @param sponsors Array of sponsors with contact_tasks + * @returns Array of sponsors with only active tasks + */ +const filterForActive = (sponsors: any[]) => { + return sponsors.map((sponsor) => { + const { contact_tasks, ...rest } = sponsor; + const active_task = + (contact_tasks || []).find( + (task: any) => + task.status !== EmailStatus.REJECTED && + task.status !== EmailStatus.GHOSTED && + task.status !== EmailStatus.INVALID_CONTACT && + task.status !== EmailStatus.DEFERRED, + ) || null; + return { + ...rest, + active_task, + }; + }); +}; + /** * POST /sponsors/create * @@ -92,6 +115,9 @@ sponsorRouter.get("/", createUser, requireMemberRole, async (req: Request, res: contact_tasks ( id, status, + notes, + due_date, + owner_id, profiles ( id, name @@ -103,7 +129,7 @@ sponsorRouter.get("/", createUser, requireMemberRole, async (req: Request, res: return next(new RouterError(StatusCode.ServerErrorInternal, "Error fetching sponsors", null, error)); } - return res.status(StatusCode.SuccessOK).json(data); + return res.status(StatusCode.SuccessOK).json(filterForActive(data)); }); /** @@ -147,7 +173,20 @@ sponsorRouter.get("/:email", createUser, requireMemberRole, async (req: Request, return next(new RouterError(StatusCode.ClientErrorBadRequest, "Email is required")); } - const { data, error } = await supabase.from(Tables.SPONSORS).select("*").eq("sponsor_email", email); + const { data, error } = await supabase.from(Tables.SPONSORS).select(` + *, + contact_tasks ( + id, + status, + notes, + due_date, + owner_id, + profiles ( + id, + name + ) + ) + `).eq("sponsor_email", email); if (error) { return next(new RouterError(StatusCode.ServerErrorInternal, "Error fetching sponsor", null, error)); @@ -157,7 +196,7 @@ sponsorRouter.get("/:email", createUser, requireMemberRole, async (req: Request, return next(new RouterError(StatusCode.ClientErrorNotFound, "Sponsor not found")); } - return res.status(StatusCode.SuccessOK).json(data); + return res.status(StatusCode.SuccessOK).json(filterForActive(data)); }); /** @@ -203,13 +242,26 @@ sponsorRouter.get( return next(new RouterError(StatusCode.ClientErrorBadRequest, "Company name is required")); } const supabase = (req as any).supabase; - const { data, error } = await supabase.from(Tables.SPONSORS).select("*").ilike("company_name", `%${companyName}%`); + const { data, error } = await supabase.from(Tables.SPONSORS).select(` + *, + contact_tasks ( + id, + status, + notes, + due_date, + owner_id, + profiles ( + id, + name + ) + ) + `).ilike("company_name", `%${companyName}%`); if (error) { return next(new RouterError(StatusCode.ServerErrorInternal, "Error fetching sponsor", null, error)); } - return res.status(StatusCode.SuccessOK).json(data); + return res.status(StatusCode.SuccessOK).json(filterForActive(data)); }, ); @@ -250,4 +302,4 @@ sponsorRouter.patch("/:email", createUser, requireMemberRole, async (req: Reques } }); -export default sponsorRouter; +export default sponsorRouter; \ No newline at end of file diff --git a/src/services/tasks/task-router.ts b/src/services/tasks/task-router.ts index 554d720..fe9e2b0 100644 --- a/src/services/tasks/task-router.ts +++ b/src/services/tasks/task-router.ts @@ -152,6 +152,26 @@ taskRouter.post("/create", createUser, requireMemberRole, async (req: Request, r return next(new RouterError(StatusCode.ClientErrorBadRequest, "Invalid task format")); } + const { data: existingTasks, error: checkError } = await supabase + .from(Tables.CONTACT_TASKS) + .select("id, status") + .eq("sponsor_email", task.sponsor_email); + + if (checkError) { + return next(new RouterError(StatusCode.ServerErrorInternal, "Error checking existing tasks", null, checkError)); + } + + const hasActiveTask = existingTasks?.some( + (t: any) => + t.status !== EmailStatus.REJECTED && + t.status !== EmailStatus.GHOSTED && + t.status !== EmailStatus.INVALID_CONTACT && + t.status !== EmailStatus.DEFERRED + ); + if (hasActiveTask) { + return next(new RouterError(StatusCode.ClientErrorBadRequest, "Sponsor already has an active task")); + } + const { data: insertedTask, error: dbErr } = await supabase.from(Tables.CONTACT_TASKS).insert(task).select().single(); if (dbErr) { @@ -321,4 +341,4 @@ taskRouter.patch("/:id", createUser, requireMemberRole, async (req: Request, res return next(new RouterError(StatusCode.ServerErrorInternal, "Error updating task", null, error)); } }); -export default taskRouter; +export default taskRouter; \ No newline at end of file From 1f930b5075a1fa89b9ef207d47a613364ecf8d23 Mon Sep 17 00:00:00 2001 From: Advita Gelli Date: Tue, 7 Apr 2026 16:37:52 -0500 Subject: [PATCH 2/3] Sponsor status defaults to NOT_CONTACTED upon new task creation --- src/services/sponsors/sponsor-router.ts | 25 +++++++++++++++++++------ src/services/tasks/task-router.ts | 10 ++++++++++ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/services/sponsors/sponsor-router.ts b/src/services/sponsors/sponsor-router.ts index 45e4b9b..89001d3 100644 --- a/src/services/sponsors/sponsor-router.ts +++ b/src/services/sponsors/sponsor-router.ts @@ -43,7 +43,7 @@ const filterForActive = (sponsors: any[]) => { * - sponsor_name: string - Name of the sponsor contact * - company_name: string - Name of the sponsor's company * - notes: string (optional) - Additional notes about the sponsor - * - status: "PENDING_EMAIL" | "CONTACTED" | "REJECTED" | "NEED_PAYMENT" | "CONFIRMED" (optional, defaults to "PENDING_EMAIL") + * - status: "NOT_CONTACTED" | "CONTACTED" | "REJECTED" | "NEED_PAYMENT" | "CONFIRMED" | "INVALID_CONTACT" | "DEFERRED" (optional, defaults to "NOT_CONTACTED") * * * @returns {Object} JSON response containing: @@ -96,7 +96,7 @@ sponsorRouter.post("/create", createUser, requireMemberRole, async (req: Request * sponsor_name: string, * company_name: string, * notes: string, - * status: "PENDING_EMAIL" | "CONTACTED" | "REJECTED" | "NEED_PAYMENT" | "CONFIRMED", + * status: "NOT_CONTACTED" | "CONTACTED" | "REJECTED" | "NEED_PAYMENT" | "CONFIRMED" | "INVALID_CONTACT" | "DEFERRED", * created_at: string, * updated_at: string * } @@ -150,7 +150,7 @@ sponsorRouter.get("/", createUser, requireMemberRole, async (req: Request, res: * sponsor_name: string, * company_name: string, * notes: string, - * status: "PENDING_EMAIL" | "CONTACTED" | "REJECTED" | "NEED_PAYMENT" | "CONFIRMED", + * status: "NOT_CONTACTED" | "CONTACTED" | "REJECTED" | "NEED_PAYMENT" | "CONFIRMED" | "INVALID_CONTACT" | "DEFERRED", * created_at: string, * updated_at: string * } @@ -218,7 +218,7 @@ sponsorRouter.get("/:email", createUser, requireMemberRole, async (req: Request, * sponsor_name: string, * company_name: string, * notes: string, - * status: "PENDING_EMAIL" | "CONTACTED" | "REJECTED" | "NEED_PAYMENT" | "CONFIRMED", + * status: "NOT_CONTACTED" | "CONTACTED" | "REJECTED" | "NEED_PAYMENT" | "CONFIRMED" | "INVALID_CONTACT" | "DEFERRED", * created_at: string, * updated_at: string * } @@ -285,7 +285,20 @@ sponsorRouter.patch("/:email", createUser, requireMemberRole, async (req: Reques .from(Tables.SPONSORS) .update(updatePayload) .eq("sponsor_email", email) - .select() + .select(` + *, + contact_tasks ( + id, + status, + notes, + due_date, + owner_id, + profiles ( + id, + name + ) + ) + `) .single(); if (error) { @@ -296,7 +309,7 @@ sponsorRouter.patch("/:email", createUser, requireMemberRole, async (req: Reques return next(new RouterError(StatusCode.ServerErrorInternal, "Error updating sponsor", null, error)); } - return res.status(StatusCode.SuccessOK).json(data); + return res.status(StatusCode.SuccessOK).json(filterForActive([data])[0]); } catch (error) { return next(new RouterError(StatusCode.ServerErrorInternal, "Error updating sponsor", null, error)); } diff --git a/src/services/tasks/task-router.ts b/src/services/tasks/task-router.ts index fe9e2b0..8287a66 100644 --- a/src/services/tasks/task-router.ts +++ b/src/services/tasks/task-router.ts @@ -178,6 +178,16 @@ taskRouter.post("/create", createUser, requireMemberRole, async (req: Request, r return next(new RouterError(StatusCode.ServerErrorInternal, "Error creating task", null, dbErr)); } + // Reset sponsor status to NOT_CONTACTED so it reflects the fresh active task + const { error: sponsorResetError } = await supabase + .from(Tables.SPONSORS) + .update({ status: SponsorStatus.NOT_CONTACTED, updated_at: new Date().toISOString() }) + .eq("sponsor_email", task.sponsor_email); + + if (sponsorResetError) { + console.error(`Task ${insertedTask.id} created, but failed to reset sponsor ${task.sponsor_email} status:`, sponsorResetError); + } + return res.status(StatusCode.SuccessOK).json({ message: "Task created successfully", task_id: insertedTask.id }); }); From bdd2ab854c98c49b85be6322a8b9d26f53327976 Mon Sep 17 00:00:00 2001 From: Advita Gelli Date: Tue, 7 Apr 2026 19:19:30 -0500 Subject: [PATCH 3/3] Added new enums to task status --- src/lib/db/schemas.ts | 32 ++++---------------------------- src/lib/db/strings.ts | 2 ++ 2 files changed, 6 insertions(+), 28 deletions(-) diff --git a/src/lib/db/schemas.ts b/src/lib/db/schemas.ts index d552d2c..e91087b 100644 --- a/src/lib/db/schemas.ts +++ b/src/lib/db/schemas.ts @@ -12,31 +12,6 @@ export type Database = { __InternalSupabase: { PostgrestVersion: "14.4" } - graphql_public: { - Tables: { - [_ in never]: never - } - Views: { - [_ in never]: never - } - Functions: { - graphql: { - Args: { - extensions?: Json - operationName?: string - query?: string - variables?: Json - } - Returns: Json - } - } - Enums: { - [_ in never]: never - } - CompositeTypes: { - [_ in never]: never - } - } public: { Tables: { contact_tasks: { @@ -430,6 +405,8 @@ export type Database = { | "GHOSTED" | "INVALID_CONTACT" | "DEFERRED" + | "NEED_PAYMENT" + | "CONFIRMED" user_role: "LEAD" | "MEMBER" } CompositeTypes: { @@ -556,9 +533,6 @@ export type CompositeTypes< : never export const Constants = { - graphql_public: { - Enums: {}, - }, public: { Enums: { email_direction: ["OUTBOUND", "INBOUND"], @@ -583,6 +557,8 @@ export const Constants = { "GHOSTED", "INVALID_CONTACT", "DEFERRED", + "NEED_PAYMENT", + "CONFIRMED", ], user_role: ["LEAD", "MEMBER"], }, diff --git a/src/lib/db/strings.ts b/src/lib/db/strings.ts index d9bf85c..4129539 100644 --- a/src/lib/db/strings.ts +++ b/src/lib/db/strings.ts @@ -30,6 +30,8 @@ export const EmailStatus = { GHOSTED: "GHOSTED", INVALID_CONTACT: "INVALID_CONTACT", DEFERRED: "DEFERRED", + NEED_PAYMENT: "NEED_PAYMENT", + CONFIRMED: "CONFIRMED", } as const; export const EmailReplyTypes = {