"use server"; import { and, desc, eq } from "drizzle-orm"; import { getAdminUser } from "@/lib/admin-permissions"; import { withBrand, withPlatformAdmin } from "@/db/client"; import { files } from "@/db/schema"; import { importContactsBatch, previewContactImport, type ContactImportEntry, type ImportPreviewResult, } from "./contacts"; export type UploadContactsResult = | { success: true; fileId: string; fileUrl: string; recordCount: number } | { success: false; error: string }; /** * Records a CSV file as an uploaded contact-import asset. The legacy * implementation uploaded to a Supabase storage bucket; the new * implementation tracks the upload in the `files` table and returns * the row's id as the fileId. Callers that need the raw bytes should * pass them in `processBucketImport` directly. */ export async function uploadContactsToBucket( brandId: string, file: File ): Promise { const adminUser = await getAdminUser(); if (!adminUser) return { success: false, error: "Not authenticated" }; // Validate file type if (!file.name.endsWith(".csv")) { return { success: false, error: "Only CSV files are supported" }; } // For very large files (farmers with tons of data), check size const maxSize = 50 * 1024 * 1024; // 50MB max if (file.size > maxSize) { return { success: false, error: "File too large. Max 50MB for large imports." }; } const timestamp = Date.now(); const safeName = file.name.replace(/[^a-zA-Z0-9.-]/g, "_"); const storageKey = `imports/${brandId}/${timestamp}-${safeName}`; try { const [row] = await withBrand(brandId, (db) => db .insert(files) .values({ brandId: brandId, storageKey, mimeType: "text/csv", sizeBytes: file.size, purpose: "contact_import", uploadedBy: adminUser.id, }) .returning({ id: files.id }), ); if (!row) return { success: false, error: "Failed to record upload" }; // Get rough row count from file size (approx 200 bytes per row) const estimatedRows = Math.floor(file.size / 200); return { success: true, fileId: row.id, fileUrl: storageKey, recordCount: estimatedRows, }; } catch (err) { return { success: false, error: err instanceof Error ? err.message : "Upload failed", }; } } export type ProcessImportResult = | { success: true; created: number; updated: number; skipped: number; errors: number } | { success: false; error: string }; /** * The legacy `process_contact_import_from_url` RPC downloaded a CSV * from a Supabase storage URL and ran the import. Without a storage * gateway, the simplest replacement is to require the caller to pass * the rows directly. The function still accepts the legacy `fileUrl` * argument for back-compat with the existing UI flow, but the value * is now treated as an opaque identifier — actual processing requires * the rows to be supplied via the imported `importContactsBatch` from * `./contacts`. */ export async function processBucketImport( brandId: string, fileUrl: string, allowOptInOverride: boolean = false, rows?: ContactImportEntry[] ): Promise { const adminUser = await getAdminUser(); if (!adminUser) return { success: false, error: "Not authenticated" }; if (!rows || rows.length === 0) { return { success: false, error: "No rows supplied for import" }; } void fileUrl; void allowOptInOverride; const res = await importContactsBatch({ brandId, contacts: rows, }); if (!res.success) return { success: false, error: res.error }; return { success: true, created: res.result.created, updated: res.result.updated, skipped: res.result.skipped, errors: res.result.errors.length, }; } export async function listImportHistory( brandId: string, limit: number = 10 ): Promise<{ success: true; imports: ImportHistoryItem[] } | { success: false; error: string }> { const adminUser = await getAdminUser(); if (!adminUser) return { success: false, error: "Not authenticated" }; try { const rows = await withBrand(brandId, (db) => db .select({ storageKey: files.storageKey, sizeBytes: files.sizeBytes, createdAt: files.createdAt, }) .from(files) .where(and(eq(files.brandId, brandId), eq(files.purpose, "contact_import"))) .orderBy(desc(files.createdAt)) .limit(limit), ); return { success: true, imports: rows.map((r) => ({ filename: r.storageKey.split("/").pop() ?? "", size: r.sizeBytes, createdAt: r.createdAt.toISOString(), url: r.storageKey, })), }; } catch (err) { return { success: false, error: err instanceof Error ? err.message : "Failed to list imports", }; } } export type ImportHistoryItem = { filename: string; size: number; createdAt: string; url: string; }; export { previewContactImport, type ImportPreviewResult };