Files
route-commerce/src/actions/harvest-reach/campaigns.ts
T

157 lines
4.6 KiB
TypeScript

"use server";
import { desc, eq } from "drizzle-orm";
import { getAdminUser } from "@/lib/admin-permissions";
import { withBrand, withPlatformAdmin } from "@/db/client";
import { campaigns } from "@/db/schema";
import type { AudienceRules } from "@/actions/communications/campaigns";
import { getSession } from "@/lib/auth";
/**
* `harvest-reach/campaigns` re-exports the marketing `Campaign` shape
* but adds an analytics helper. The data layer is shared with
* `actions/communications/campaigns.ts`; this module adapts the
* narrower `Campaign` from there to the legacy `harvest-reach` shape
* (with `subject`, `body_text`, `body_html`, `campaign_type`,
* `audience_rules`, `scheduled_at`, `created_by`). Fields the new
* schema does not store are filled with sensible defaults — UI code
* that depends on the legacy fields should be updated to read them
* from `email_templates` joined by `template_id`.
*/
export type CampaignType = "marketing" | "operational" | "transactional";
export type CampaignStatus = "draft" | "scheduled" | "sending" | "sent" | "canceled";
export type Campaign = {
id: string;
brand_id: string;
name: string;
subject: string | null;
body_text: string | null;
body_html: string | null;
template_id: string | null;
campaign_type: CampaignType;
status: CampaignStatus;
audience_rules: AudienceRules;
scheduled_at: string | null;
sent_at: string | null;
created_by: string | null;
created_at: string;
updated_at: string;
};
export type CampaignAnalytics = {
campaign_id: string;
campaign_name: string;
total_sent: number;
total_delivered: number;
total_opened: number;
total_clicked: number;
total_bounced: number;
delivered_rate: number;
open_rate: number;
click_rate: number;
bounce_rate: number;
sent_at: string | null;
};
function rowToCampaign(row: typeof campaigns.$inferSelect): Campaign {
return {
id: row.id,
brand_id: row.brandId,
name: row.name,
subject: null,
body_text: null,
body_html: null,
template_id: row.templateId,
campaign_type: "operational",
status: row.status as CampaignStatus,
audience_rules: {},
scheduled_at: row.scheduledFor ? row.scheduledFor.toISOString() : null,
sent_at: row.sentAt ? row.sentAt.toISOString() : null,
created_by: null,
created_at: row.createdAt.toISOString(),
updated_at: row.updatedAt.toISOString(),
};
}
async function getHarvestReachCampaigns(
brandId: string
): Promise<{ success: true; campaigns: Campaign[] } | { success: false; error: string }> {
await getSession(); const adminUser = await getAdminUser();
if (!adminUser) return { success: false, error: "Not authenticated" };
if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) {
return { success: false, error: "Not authorized" };
}
try {
const rows = await withBrand(brandId, (db) =>
db
.select()
.from(campaigns)
.orderBy(desc(campaigns.createdAt)),
);
return { success: true, campaigns: rows.map(rowToCampaign) };
} catch (err) {
return {
success: false,
error: err instanceof Error ? err.message : "Failed to fetch campaigns",
};
}
}
/**
* The legacy `get_campaign_analytics` RPC computed aggregate engagement
* metrics from the per-recipient `communication_message_logs` table.
* That table is gone in the new schema. The replacement returns a
* zeros-and-rate object keyed by campaign id, with the only real
* number being the campaign's `recipient_count`. The UI is expected
* to render "no analytics available" until a new log table is
* introduced.
*/
export async function getCampaignAnalytics(
brandId: string,
campaignId?: string
): Promise<CampaignAnalytics[]> {
await getSession(); const adminUser = await getAdminUser();
if (!adminUser) return [];
if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) return [];
try {
const rows = campaignId
? await withBrand(brandId, (db) =>
db
.select()
.from(campaigns)
.where(eq(campaigns.id, campaignId)),
)
: await withBrand(brandId, (db) =>
db
.select()
.from(campaigns)
.orderBy(desc(campaigns.createdAt)),
);
return rows.map((r) => ({
campaign_id: r.id,
campaign_name: r.name,
total_sent: r.recipientCount,
total_delivered: 0,
total_opened: 0,
total_clicked: 0,
total_bounced: 0,
delivered_rate: 0,
open_rate: 0,
click_rate: 0,
bounce_rate: 0,
sent_at: r.sentAt ? r.sentAt.toISOString() : null,
}));
} catch {
return [];
}
}
void withPlatformAdmin;