0ac4beaaa8
- 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)
123 lines
4.9 KiB
TypeScript
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 };
|
|
}
|