Skip to content
Merged
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
584 changes: 291 additions & 293 deletions app/(api)/api/jobs/account/mark-for-deletion/route.ts

Large diffs are not rendered by default.

330 changes: 165 additions & 165 deletions app/(api)/api/jobs/daily-summary/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,186 +2,186 @@ import { serve } from "@upstash/workflow/nextjs";
import { gte } from "drizzle-orm";
import { Resend } from "resend";
import {
DailySummary,
dailySummaryPlainText,
DailySummary,
dailySummaryPlainText,
} from "@/components/emails/daily-summary";
import { getTodayDataForUser } from "@/lib/utils/todayData";
import { getDatabaseForOwner } from "@/lib/utils/useDatabase";
import { opsUser } from "@/ops/drizzle/schema";
import { getOpsDatabase } from "@/ops/useOps";

function isCurrentlySevenAM(timezone: string): boolean {
try {
const now = new Date();
const userTime = new Intl.DateTimeFormat("en-US", {
timeZone: timezone,
hour: "numeric",
hour12: false,
}).format(now);

const hour = Number.parseInt(userTime.split(" ")[0] || userTime, 10);
return hour === 7;
} catch (error) {
console.error(
`[DailySummary] Error checking time for timezone ${timezone}:`,
error,
);
return false;
}
try {
const now = new Date();
const userTime = new Intl.DateTimeFormat("en-US", {
timeZone: timezone,
hour: "numeric",
hour12: false,
}).format(now);

const hour = Number.parseInt(userTime.split(" ")[0] || userTime, 10);
return hour === 7;
} catch (error) {
console.error(
`[DailySummary] Error checking time for timezone ${timezone}:`,
error,
);
return false;
}
}

export const { POST } = serve(async (context) => {
const resend = new Resend(process.env.RESEND_API_KEY);
console.log(
`[DailySummary] Starting daily summary job at ${new Date().toISOString()}`,
);

// Step 1: Get active users from ops database (active in last 7 days) and filter by 7AM timezone
const users = await context.run("fetch-users", async () => {
const opsDb = await getOpsDatabase();
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);

const allUsers = await opsDb
.select()
.from(opsUser)
.where(gte(opsUser.lastActiveAt, sevenDaysAgo));
console.log(
`[DailySummary] Found ${allUsers.length} active users (last 7 days)`,
);

// Filter users where it's currently 7AM in their timezone
const usersAt7AM = allUsers.filter((user) => {
const userTimezone = user.timeZone || "UTC";
return isCurrentlySevenAM(userTimezone);
});

console.log(
`[DailySummary] Found ${usersAt7AM.length} users where it's currently 7AM`,
);
return usersAt7AM;
});

// Step 2: Process users
await context.run("process-users", async () => {
console.log(`[DailySummary] Processing ${users.length} users`);

for (const userData of users) {
try {
const userTimezone = userData.timeZone || "UTC";
await processUserSummary(
userData.id,
userData.email,
userData.firstName,
userTimezone,
resend,
);
} catch (error) {
console.error(
`[DailySummary] Error processing user ${userData.email}:`,
error,
);
}
}
});

console.log(
`[DailySummary] Daily summary job completed at ${new Date().toISOString()}`,
);
const resend = new Resend(process.env.RESEND_API_KEY);
console.log(
`[DailySummary] Starting daily summary job at ${new Date().toISOString()}`,
);

// Step 1: Get active users from ops database (active in last 7 days) and filter by 7AM timezone
const users = await context.run("fetch-users", async () => {
const opsDb = await getOpsDatabase();
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);

const allUsers = await opsDb
.select()
.from(opsUser)
.where(gte(opsUser.lastActiveAt, sevenDaysAgo));
console.log(
`[DailySummary] Found ${allUsers.length} active users (last 7 days)`,
);

// Filter users where it's currently 7AM in their timezone
const usersAt7AM = allUsers.filter((user) => {
const userTimezone = user.timeZone || "UTC";
return isCurrentlySevenAM(userTimezone);
});

console.log(
`[DailySummary] Found ${usersAt7AM.length} users where it's currently 7AM`,
);
return usersAt7AM;
});

// Step 2: Process users
await context.run("process-users", async () => {
console.log(`[DailySummary] Processing ${users.length} users`);

for (const userData of users) {
try {
const userTimezone = userData.timeZone || "UTC";
await processUserSummary(
userData.id,
userData.email,
userData.firstName,
userTimezone,
resend,
);
} catch (error) {
console.error(
`[DailySummary] Error processing user ${userData.email}:`,
error,
);
}
}
});

console.log(
`[DailySummary] Daily summary job completed at ${new Date().toISOString()}`,
);
});

async function processUserSummary(
userId: string,
email: string,
firstName: string | undefined | null,
timezone: string,
resend: Resend,
userId: string,
email: string,
firstName: string | undefined | null,
timezone: string,
resend: Resend,
) {
try {
console.log(
`[DailySummary] Processing summary for user ${email} (${userId}) at timezone ${timezone}`,
);

// Get user's database
const db = await getDatabaseForOwner(userId);

// Get today's data using the same logic as getTodayData
const today = new Date();
const { dueToday, overDue, events } = await getTodayDataForUser(
db,
timezone,
today,
);

// Only send email if user has relevant content
const hasContent =
dueToday.length > 0 || overDue.length > 0 || events.length > 0;

if (!hasContent) {
console.log(
`[DailySummary] No content for user ${email}, skipping email`,
);
return;
}

console.log(
`[DailySummary] Sending summary to ${email}: ${overDue.length} overdue, ${dueToday.length} due today, ${events.length} events`,
);

// Send daily summary email
await resend.emails.send({
from: "Manage Daily Summary <daily-summary@email.managee.xyz>",
to: email,
subject: `🌅 Your Daily Summary - ${getFormattedDate(today, timezone)} ✨`,
react: DailySummary({
firstName: firstName || undefined,
email,
timezone,
date: today,
overdueTasks: overDue,
dueToday: dueToday,
events: events,
}),
text: dailySummaryPlainText({
firstName: firstName || undefined,
email,
timezone,
date: today,
overdueTasks: overDue,
dueToday: dueToday,
events: events,
}),
});

console.log(`[DailySummary] Successfully sent summary email to ${email}`);
} catch (error) {
console.error(
`[DailySummary] Error processing summary for ${email}:`,
error,
);
}
try {
console.log(
`[DailySummary] Processing summary for user ${email} (${userId}) at timezone ${timezone}`,
);

// Get user's database
const db = await getDatabaseForOwner(userId);

// Get today's data using the same logic as getTodayData
const today = new Date();
const { dueToday, overDue, events } = await getTodayDataForUser(
db,
timezone,
today,
);

// Only send email if user has relevant content
const hasContent =
dueToday.length > 0 || overDue.length > 0 || events.length > 0;

if (!hasContent) {
console.log(
`[DailySummary] No content for user ${email}, skipping email`,
);
return;
}

console.log(
`[DailySummary] Sending summary to ${email}: ${overDue.length} overdue, ${dueToday.length} due today, ${events.length} events`,
);

// Send daily summary email
await resend.emails.send({
from: "Manage Daily Summary",
to: email,
subject: `🌅 Your Daily Summary - ${getFormattedDate(today, timezone)} ✨`,
react: DailySummary({
firstName: firstName || undefined,
email,
timezone,
date: today,
overdueTasks: overDue,
dueToday: dueToday,
events: events,
}),
text: dailySummaryPlainText({
firstName: firstName || undefined,
email,
timezone,
date: today,
overdueTasks: overDue,
dueToday: dueToday,
events: events,
}),
});

console.log(`[DailySummary] Successfully sent summary email to ${email}`);
} catch (error) {
console.error(
`[DailySummary] Error processing summary for ${email}:`,
error,
);
}
}

function getFormattedDate(date: Date, timezone: string): string {
try {
return new Intl.DateTimeFormat("en-US", {
timeZone: timezone,
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
}).format(date);
} catch (error) {
console.error(
`[DailySummary] Error formatting date for timezone ${timezone}:`,
error,
);
// Fallback to UTC if timezone is invalid
return new Intl.DateTimeFormat("en-US", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
}).format(date);
}
try {
return new Intl.DateTimeFormat("en-US", {
timeZone: timezone,
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
}).format(date);
} catch (error) {
console.error(
`[DailySummary] Error formatting date for timezone ${timezone}:`,
error,
);
// Fallback to UTC if timezone is invalid
return new Intl.DateTimeFormat("en-US", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
}).format(date);
}
}
Loading
Loading