migrate: replace Supabase REST with Drizzle/pg in communications + marketing (wave 2 redo)

This commit is contained in:
2026-06-07 03:58:26 +00:00
parent 99a3d66636
commit 3ad2a48fc3
19 changed files with 1738 additions and 1089 deletions
+9 -74
View File
@@ -1,6 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import crypto from "crypto";
import { svcHeaders } from "@/lib/svc-headers";
// Resend webhook events: email.delivered, email.opened, email.clicked,
// email.bounced, email.failed, email.unsubscribed
@@ -48,78 +47,14 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
}
const { type, data } = event;
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
// Look up by customer_email + subject (event_id is not populated by send_campaign)
// Filter to recent logs (last 7 days) to avoid stale matches
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
const findRes = await fetch(
`${supabaseUrl}/rest/v1/communication_message_logs?` +
`customer_email=eq.${encodeURIComponent(data.to)}` +
`&subject=eq.${encodeURIComponent(data.subject ?? "")}` +
`&delivery_method=eq.email` +
`&created_at=gt.${sevenDaysAgo.toISOString()}` +
`&order=created_at.desc` +
`&limit=5&select=id,brand_id`,
{
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
}
);
if (!findRes.ok) {
return NextResponse.json({ error: "Failed to look up message" }, { status: 500 });
}
const logs: { id: string; brand_id: string }[] = await findRes.json();
if (logs.length === 0) {
// No matching log found — silently acknowledge to avoid Resend retries
return NextResponse.json({ ok: true });
}
// Update the most recent matching log
const log = logs[0];
const updates: Record<string, string | undefined> = {};
switch (type) {
case "email.delivered":
updates.delivered_at = data.delivered_at ?? new Date().toISOString();
updates.status = "delivered";
break;
case "email.opened":
updates.opened_at = data.opened_at ?? new Date().toISOString();
break;
case "email.clicked":
updates.clicked_at = data.clicked_at ?? new Date().toISOString();
break;
case "email.bounced":
case "email.failed":
updates.bounced_at = data.bounced_at ?? new Date().toISOString();
updates.bounce_reason = data.bounce_reason ?? undefined;
updates.status = "bounced";
break;
case "email.unsubscribed":
updates.status = "unsubscribed";
break;
default:
return NextResponse.json({ ok: true });
}
const patchRes = await fetch(
`${supabaseUrl}/rest/v1/communication_message_logs?id=eq.${log.id}`,
{
method: "PATCH",
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json", Prefer: "return=minimal" },
body: JSON.stringify(updates),
}
);
if (!patchRes.ok) {
return NextResponse.json({ error: "Failed to update log" }, { status: 500 });
}
// The new schema does not have a `communication_message_logs` table
// — the per-recipient delivery log has been retired. We still
// acknowledge the event so Resend does not retry, but there is
// nothing to update. When a new log table is added, this is the
// place to look up by `customer_email + subject` (the event_id is
// never populated by `send_campaign`) and write delivered/opened/
// clicked/bounced timestamps.
void event;
return NextResponse.json({ ok: true });
}
}