Files
route-commerce/src/actions/import-products.ts
T

97 lines
3.1 KiB
TypeScript

"use server";
import { getAdminUser } from "@/lib/admin-permissions";
import { withBrand } from "@/db/client";
import { products } from "@/db/schema";
import { getSession } from "@/lib/auth";
export type ImportProductsResult =
| { success: true; created: number; updated: number; errors: { product: string; error: string }[] }
| { success: false; error: string };
/**
* Bulk-import products. Replaces the legacy `bulk_upsert_products` SECURITY
* DEFINER RPC. The new `products` schema drops the legacy `type`, `is_taxable`,
* `pickup_type`, and `image_url` columns; we keep `name`, `description`,
* `price_cents`, and `active`. Without an id we always INSERT (no upsert
* key for matching — the caller can run an update path separately if
* deduplication is needed).
*/
export async function importProductsBatch(
brandId: string,
productsToImport: Array<{
name: string;
description: string;
price: number;
type: string;
active: boolean;
image_url?: string;
}>
): Promise<ImportProductsResult> {
await getSession(); const adminUser = await getAdminUser();
if (!adminUser) return { success: false, error: "Not authenticated" };
if (!adminUser.can_manage_products) return { success: false, error: "Not authorized" };
if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) {
return { success: false, error: "Not authorized for this brand" };
}
let created = 0;
const errors: { product: string; error: string }[] = [];
const validProducts: Array<{ name: string; description: string; price: number; type: string; active: boolean; image_url?: string }> = [];
const skipped: { product: { name: string; description: string; price: number; type: string; active: boolean; image_url?: string }; error: string }[] = [];
for (const p of productsToImport) {
const priceCents = Math.round(Number(p.price) * 100);
if (!Number.isFinite(priceCents) || priceCents < 0) {
skipped.push({ product: p, error: "Invalid price" });
} else {
validProducts.push(p);
}
}
const settled = await Promise.allSettled(
validProducts.map((p) =>
withBrand(brandId, (db) =>
db.insert(products).values({
brandId: brandId,
name: p.name,
description: p.description ?? null,
priceCents: Math.round(Number(p.price) * 100),
active: p.active,
}),
).then(
() => p,
(err) => ({ p, err }),
),
),
);
for (const entry of settled) {
if (entry.status === "rejected") {
const err = (entry as PromiseRejectedResult).reason;
errors.push({
product: "<unknown>",
error: err instanceof Error ? err.message : String(err),
});
continue;
}
const value = entry.value;
if ("err" in value) {
errors.push({
product: value.p.name,
error: value.err instanceof Error ? value.err.message : String(value.err),
});
} else {
created++;
}
}
for (const s of skipped) {
errors.push({ product: s.product.name, error: s.error });
}
return { success: true, created, updated: 0, errors };
}