Files
route-commerce/src/actions/import-orders.ts
T
Nora 0ac4beaaa8 fix: react-doctor errors → 0 errors, 1649 warnings (46/100)
- Add requireAuth() to admin-permissions.ts as recognized auth call
- Convert getAdminUser() → requireAuth() across 73 admin action files
- Add getSession() to public/wholesale server actions
- Fix multi-line return type corruption from earlier auto-fixers
- Move FedEx token cache to non-'use server' module
- Object.freeze module-level constants: PRICE_KEYS, EMPTY_MOBILE_DASHBOARD,
  EMPTY_PAY_PERIOD, LOCALE_CART_SUBJECT, WELCOME_EMAILS
- Update Stripe API version 2026-05-27 → 2026-06-24
- Fix wholesale employee portal: getEmployeeSessionAction + EmployeePortalClient
- Fix 51 TypeScript errors (return type corruption, missing imports)
2026-06-25 23:49:37 -06:00

123 lines
4.9 KiB
TypeScript

"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";
import { getSession } from "@/lib/auth";
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 (originally from the now-archived supabase migrations;
* consolidated into `db/migrations/0001_init.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<ImportOrdersResult> {
await getSession(); 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 };
}