"use server"; import { getAdminUser } from "@/lib/admin-permissions"; import { withTx, pool } from "@/lib/db"; import { orders, orderItems, customers } from "@/db/schema"; import { eq } from "drizzle-orm"; export type ImportOrdersResult = | { success: true; imported: number; errors: { row: number; error: string }[] } | { success: false; error: string }; /** * Bulk-import orders. Replaces the legacy `create_order_with_items` SECURITY * DEFINER RPC (`supabase/migrations/021_shipping_only_brand.sql`). The new * `orders` schema doesn't have `subtotal`, `customer_name`, `customer_email`, * `customer_phone`, or `stop_id` columns — totals are stored in * `total_cents` and customers are referenced by `customer_id`. For each * imported order we upsert a `customers` row keyed on email+tenant, then * insert the `orders` + `order_items` rows. */ export async function importOrdersBatch( brandId: string, ordersToImport: Array<{ customer_name: string; customer_email: string; customer_phone: string; stop_id: string; items: Array<{ product_id: string; quantity: number; fulfillment: string }>; }> ): Promise { const adminUser = await getAdminUser(); if (!adminUser) return { success: false, error: "Not authenticated" }; if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" }; if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) { return { success: false, error: "Not authorized for this brand" }; } const results = { imported: 0, errors: [] as { row: number; error: string }[] }; for (let i = 0; i < ordersToImport.length; i++) { const order = ordersToImport[i]; try { // Compute total_cents server-side from current product prices. const productIds = order.items.map((it) => it.product_id); if (productIds.length === 0) { results.errors.push({ row: i, error: "Order has no items" }); continue; } // Fetch product prices for the brand. const productRes = await pool.query<{ id: string; price_cents: number }>( `SELECT id, price_cents FROM products WHERE brand_id = $1 AND id = ANY($2::uuid[])`, [brandId, productIds] ); const priceMap = new Map(productRes.rows.map((p) => [p.id, p.price_cents])); let totalCents = 0; for (const it of order.items) { const unit = priceMap.get(it.product_id); if (typeof unit !== "number") { results.errors.push({ row: i, error: `Unknown product: ${it.product_id}` }); totalCents = -1; break; } totalCents += unit * it.quantity; } if (totalCents < 0) continue; // Determine fulfillment: pickup | ship | mixed based on items. const fulfillments = new Set(order.items.map((it) => it.fulfillment)); const fulfillment: "pickup" | "ship" | "mixed" = fulfillments.size > 1 ? "mixed" : (Array.from(fulfillments)[0] as "pickup" | "ship" | "mixed") ?? "pickup"; // Insert in a single transaction: customers + orders + order_items. await withTx(async (client) => { // Upsert customer by (brand_id, email). const customerRes = await client.query<{ id: string }>( `INSERT INTO customers (brand_id, name, email, phone, email_opt_in) VALUES ($1, $2, $3, $4, true) ON CONFLICT (brand_id, email) WHERE email IS NOT NULL DO UPDATE SET name = EXCLUDED.name, phone = EXCLUDED.phone, updated_at = now() RETURNING id`, [brandId, order.customer_name, order.customer_email || null, order.customer_phone || null] ); const customerId = customerRes.rows[0]?.id ?? null; const orderRes = await client.query<{ id: string }>( `INSERT INTO orders (brand_id, customer_id, total_cents, status, fulfillment) VALUES ($1, $2, $3, 'pending', $4) RETURNING id`, [brandId, customerId, totalCents, fulfillment] ); const orderId = orderRes.rows[0]?.id; if (!orderId) throw new Error("Order insert returned no id"); for (const it of order.items) { const unit = priceMap.get(it.product_id) ?? 0; await client.query( `INSERT INTO order_items (order_id, product_id, quantity, price_cents, fulfillment) VALUES ($1, $2, $3, $4, $5)`, [orderId, it.product_id, it.quantity, unit, it.fulfillment === "shipping" ? "ship" : it.fulfillment] ); } }); results.imported++; } catch (err) { results.errors.push({ row: i, error: `Failed to import order for ${order.customer_name}: ${err instanceof Error ? err.message : String(err)}`, }); } } return { success: true, imported: results.imported, errors: results.errors }; }