Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 4 additions & 28 deletions src/lib/db/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -430,6 +405,8 @@ export type Database = {
| "GHOSTED"
| "INVALID_CONTACT"
| "DEFERRED"
| "NEED_PAYMENT"
| "CONFIRMED"
user_role: "LEAD" | "MEMBER"
}
CompositeTypes: {
Expand Down Expand Up @@ -556,9 +533,6 @@ export type CompositeTypes<
: never

export const Constants = {
graphql_public: {
Enums: {},
},
public: {
Enums: {
email_direction: ["OUTBOUND", "INBOUND"],
Expand All @@ -583,6 +557,8 @@ export const Constants = {
"GHOSTED",
"INVALID_CONTACT",
"DEFERRED",
"NEED_PAYMENT",
"CONFIRMED",
],
user_role: ["LEAD", "MEMBER"],
},
Expand Down
2 changes: 2 additions & 0 deletions src/lib/db/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
91 changes: 78 additions & 13 deletions src/services/sponsors/sponsor-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Comment thread
r-thak marked this conversation as resolved.
import { createUser, requireMemberRole } from "../../middleware/auth";

const sponsorRouter: Router = Router();

/**
* Helper function to filterout inactive tasks from the sponsors array
Comment thread
r-thak marked this conversation as resolved.
* @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;
Comment thread
r-thak marked this conversation as resolved.
return {
...rest,
active_task,
};
});
Comment thread
r-thak marked this conversation as resolved.
};

/**
* POST /sponsors/create
*
Expand All @@ -20,7 +43,7 @@ const sponsorRouter: Router = Router();
* - 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:
Expand Down Expand Up @@ -73,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
* }
Expand All @@ -92,6 +115,9 @@ sponsorRouter.get("/", createUser, requireMemberRole, async (req: Request, res:
contact_tasks (
id,
status,
notes,
due_date,
owner_id,
profiles (
id,
name
Expand All @@ -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));
Comment thread
r-thak marked this conversation as resolved.
});

/**
Expand All @@ -124,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
* }
Expand All @@ -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));
Expand All @@ -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));
});

/**
Expand All @@ -179,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
* }
Expand All @@ -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));
},
);

Expand All @@ -233,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) {
Expand All @@ -244,10 +309,10 @@ 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));
}
});

export default sponsorRouter;
export default sponsorRouter;
32 changes: 31 additions & 1 deletion src/services/tasks/task-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,12 +152,42 @@ 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);
Comment thread
r-thak marked this conversation as resolved.

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
);
Comment thread
r-thak marked this conversation as resolved.
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) {
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 });
});

Expand Down Expand Up @@ -321,4 +351,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;