From 76310b9d6a9d61982be66af9dc152e332f6f4f6c Mon Sep 17 00:00:00 2001 From: shanoysinc Date: Fri, 23 Jan 2026 15:37:33 -0500 Subject: [PATCH 1/3] feat: Prevent admins from accepting applications before the job runs --- .../app/routes/_dashboard.applications.tsx | 32 ++++++++++- .../src/modules/applications/applications.ts | 55 ++++++++++++++++++- .../applications/applications.types.ts | 8 +++ .../20260123134234_auto_review_job_status.ts | 21 +++++++ 4 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 packages/db/src/migrations/20260123134234_auto_review_job_status.ts diff --git a/apps/admin-dashboard/app/routes/_dashboard.applications.tsx b/apps/admin-dashboard/app/routes/_dashboard.applications.tsx index 6931e1ced..abe07ebbb 100644 --- a/apps/admin-dashboard/app/routes/_dashboard.applications.tsx +++ b/apps/admin-dashboard/app/routes/_dashboard.applications.tsx @@ -14,7 +14,10 @@ import { z } from 'zod'; import { ListSearchParams } from '@oyster/core/admin-dashboard/ui'; import { listApplications } from '@oyster/core/applications'; -import { type ApplicationRejectionReason } from '@oyster/core/applications/types'; +import type { + ApplicationRejectionReason, + AutoReviewJobStatus, +} from '@oyster/core/applications/types'; import { ApplicationStatus } from '@oyster/core/applications/ui'; import { Application } from '@oyster/types'; import { @@ -213,6 +216,33 @@ function ApplicationsTable() { return `${reviewedByFirstName} ${reviewedByLastName}`; }, }, + { + displayName: 'Auto Review Job Status', + render: (application) => { + const jobStatus = application.autoReviewJobStatus as + | AutoReviewJobStatus + | undefined; + + if (!jobStatus) { + return '-'; + } + + const JobStatusColor: Record = { + DONE: 'lime-100', + FAILED: 'red-100', + QUEUED: 'amber-100', + }; + + const color = JobStatusColor[jobStatus]; + + return ( + + {toTitleCase(jobStatus)} + + ); + }, + size: '200', + }, { show: () => ['pending', 'rejected'].includes(status), size: '48', diff --git a/packages/core/src/modules/applications/applications.ts b/packages/core/src/modules/applications/applications.ts index 0df49ae61..782730dcc 100644 --- a/packages/core/src/modules/applications/applications.ts +++ b/packages/core/src/modules/applications/applications.ts @@ -20,6 +20,7 @@ import { type ApplicationRejectionReason, ApplicationStatus, type ApplyInput, + AutoReviewJobStatus, } from '@/modules/applications/applications.types'; import { ReferralStatus } from '@/modules/referrals/referrals.types'; import { STUDENT_PROFILE_URL } from '@/shared/env'; @@ -31,6 +32,29 @@ export async function countPendingApplications() { .selectFrom('applications') .select((eb) => eb.fn.countAll().as('count')) .where('status', '=', ApplicationStatus.PENDING) + .where((eb) => + eb.or([ + eb('applications.autoReviewJobStatus', '=', AutoReviewJobStatus.DONE), + eb('applications.autoReviewJobStatus', '=', AutoReviewJobStatus.FAILED), + ]) + ) + .executeTakeFirstOrThrow(); + + const count = Number(result.count); + + return count; +} + +export async function countReviewedApplications() { + const result = await db + .selectFrom('applications') + .select((eb) => eb.fn.countAll().as('count')) + .where((eb) => + eb.or([ + eb('applications.autoReviewJobStatus', '=', AutoReviewJobStatus.DONE), + eb('applications.autoReviewJobStatus', '=', AutoReviewJobStatus.FAILED), + ]) + ) .executeTakeFirstOrThrow(); const count = Number(result.count); @@ -121,7 +145,14 @@ export async function listApplications({ }) .$if(status !== 'all', (qb) => { return qb.where('applications.status', '=', status); - }); + }) + // Only show applications that have been reviewed (DONE or FAILED) + .where((eb) => + eb.or([ + eb('applications.autoReviewJobStatus', '=', AutoReviewJobStatus.DONE), + eb('applications.autoReviewJobStatus', '=', AutoReviewJobStatus.FAILED), + ]) + ); const orderDirection = status === 'pending' ? 'asc' : 'desc'; @@ -130,6 +161,7 @@ export async function listApplications({ .leftJoin('schools', 'schools.id', 'applications.schoolId') .leftJoin('admins', 'admins.id', 'applications.reviewedById') .select([ + 'applications.autoReviewJobStatus', 'applications.createdAt', 'applications.email', 'applications.firstName', @@ -410,6 +442,7 @@ export async function apply(input: ApplyInput) { await trx .insertInto('applications') .values({ + autoReviewJobStatus: AutoReviewJobStatus.QUEUED, contribution: input.contribution, educationLevel: input.educationLevel, email: input.email, @@ -592,8 +625,24 @@ export const applicationWorker = registerWorker( ApplicationBullJob, async (job) => { return match(job) - .with({ name: 'application.review' }, ({ data }) => { - return reviewApplication(data); + .with({ name: 'application.review' }, async ({ data }) => { + try { + await reviewApplication(data); + // Job completed successfully - mark as DONE + await db + .updateTable('applications') + .set({ autoReviewJobStatus: AutoReviewJobStatus.DONE }) + .where('id', '=', data.applicationId) + .execute(); + } catch (error) { + // Job failed - mark as FAILED + await db + .updateTable('applications') + .set({ autoReviewJobStatus: AutoReviewJobStatus.FAILED }) + .where('id', '=', data.applicationId) + .execute(); + throw error; + } }) .exhaustive(); } diff --git a/packages/core/src/modules/applications/applications.types.ts b/packages/core/src/modules/applications/applications.types.ts index 527ab4b6f..18d3f8e8d 100644 --- a/packages/core/src/modules/applications/applications.types.ts +++ b/packages/core/src/modules/applications/applications.types.ts @@ -21,12 +21,20 @@ export const ApplicationStatus = { REJECTED: 'rejected', } as const; +export const AutoReviewJobStatus = { + DONE: 'DONE', + FAILED: 'FAILED', + QUEUED: 'QUEUED', +} as const; + export type ApplicationRejectionReason = ExtractValue< typeof ApplicationRejectionReason >; export type ApplicationStatus = ExtractValue; +export type AutoReviewJobStatus = ExtractValue; + // Use Cases export const ApplyInput = Application.pick({ diff --git a/packages/db/src/migrations/20260123134234_auto_review_job_status.ts b/packages/db/src/migrations/20260123134234_auto_review_job_status.ts new file mode 100644 index 000000000..a08a1a1d2 --- /dev/null +++ b/packages/db/src/migrations/20260123134234_auto_review_job_status.ts @@ -0,0 +1,21 @@ +import type { Kysely } from 'kysely'; + +export async function up(db: Kysely) { + await db.schema + .alterTable('applications') + .addColumn('auto_review_job_status', 'text') + .execute(); + + // Set all existing applications to DONE + await db + .updateTable('applications') + .set({ auto_review_job_status: 'DONE' }) + .execute(); +} + +export async function down(db: Kysely) { + await db.schema + .alterTable('applications') + .dropColumn('auto_review_job_status') + .execute(); +} From 2e70bb46a5264bc1e04562c03e6d64a96c142bdd Mon Sep 17 00:00:00 2001 From: shanoysinc Date: Fri, 23 Jan 2026 16:04:36 -0500 Subject: [PATCH 2/3] fix: remove pdf attach from onboarding email --- .../modules/notifications/use-cases/send-email.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/core/src/modules/notifications/use-cases/send-email.ts b/packages/core/src/modules/notifications/use-cases/send-email.ts index c5424f53f..945b988d7 100644 --- a/packages/core/src/modules/notifications/use-cases/send-email.ts +++ b/packages/core/src/modules/notifications/use-cases/send-email.ts @@ -233,23 +233,13 @@ async function getAttachments( { name: 'referral-sent' }, { name: 'resume-submitted' }, { name: 'student-anniversary' }, + { name: 'student-attended-onboarding' }, { name: 'student-graduation' }, { name: 'student-removed' }, () => { return undefined; } ) - .with({ name: 'student-attended-onboarding' }, async () => { - const file = await getObject({ key: 'onboarding-deck.pdf' }); - - return [ - { - content: file.base64, - contentType: 'application/pdf', - name: 'ColorStack Onboarding Deck.pdf', - } as EmailAttachment, - ]; - }) .exhaustive(); return attachments; From ca6d56b722ee7adfb696fc58811b45f28f577fd6 Mon Sep 17 00:00:00 2001 From: shanoysinc Date: Mon, 9 Feb 2026 16:45:24 -0500 Subject: [PATCH 3/3] fix: prevent application from being rejected for students not in the system --- .../src/modules/applications/applications.ts | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/packages/core/src/modules/applications/applications.ts b/packages/core/src/modules/applications/applications.ts index 782730dcc..0281356e2 100644 --- a/packages/core/src/modules/applications/applications.ts +++ b/packages/core/src/modules/applications/applications.ts @@ -796,22 +796,12 @@ async function shouldReject( return [false]; } - const [memberWithSameLinkedIn, applicationAcceptedWithSameLinkedIn] = - await Promise.all([ - db - .selectFrom('students') - .where('linkedInUrl', 'ilike', application.linkedInUrl) - .executeTakeFirst(), - - db - .selectFrom('applications') - .where('id', '!=', application.id) - .where('linkedInUrl', 'ilike', application.linkedInUrl) - .where('status', '=', ApplicationStatus.ACCEPTED) - .executeTakeFirst(), - ]); + const memberWithSameLinkedIn = await db + .selectFrom('students') + .where('linkedInUrl', 'ilike', application.linkedInUrl) + .executeTakeFirst(); - if (memberWithSameLinkedIn || applicationAcceptedWithSameLinkedIn) { + if (memberWithSameLinkedIn) { return [true, 'linkedin_already_used']; }