Files
route-commerce/src/actions/import-products.ts
T
openclaw 916ad39176
Deploy to route.crispygoat.com / deploy (push) Failing after 3m1s
feat(storage): MinIO object storage, Neon Auth, Supabase removal
- Add MinIO/S3-compatible storage client (src/lib/storage.ts) with uploadObject,
  deleteObject, presigned URL helpers, and BUCKETS constant
- Wire product images, brand logos, and water log photos to MinIO via the
  new storage client
- Migrate forgot-password to Neon Auth (remove Supabase /auth/v1/recover call)
- Migrate send-scheduled cron to direct Postgres + Resend (remove Supabase Edge
  Function proxy)
- Add logoUrl to email types (OrderReceipt, Welcome, PasswordReset) and pass
  brand_settings.logo_url from all call sites
- Update email templates to use dynamic logoUrl instead of hardcoded Supabase
  bucket URLs
- Remove hardcoded Supabase URLs from TuxedoVideoHero, TuxedoAboutPage,
  TimeTrackingFieldClient; use brand_settings props + local public/ fallback
- Download brand logos (3) and tuxedo-hero.mp4 (36MB) from Supabase bucket to
  public/ for local development
- Add MinIO env vars to .env.example (endpoint, access key, secret, buckets)
- Fix TimeTrackingFieldClient to destructure logoUrl and brandAccent props
- Fix admin/users.ts logoUrl type (null → undefined for optional string)
- Remove stale sb- cookie from wholesale-auth
- Migrate tuxedo/about page to remove supabase import and use pool query for
  wholesale_settings lookup
2026-06-09 12:23:37 -06:00

68 lines
2.1 KiB
TypeScript

"use server";
import { getAdminUser } from "@/lib/admin-permissions";
import { withBrand } from "@/db/client";
import { products } from "@/db/schema";
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> {
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 }[] = [];
for (const p of productsToImport) {
const priceCents = Math.round(Number(p.price) * 100);
if (!Number.isFinite(priceCents) || priceCents < 0) {
errors.push({ product: p.name, error: "Invalid price" });
continue;
}
try {
await withBrand(brandId, (db) =>
db.insert(products).values({
brandId: brandId,
name: p.name,
description: p.description ?? null,
priceCents,
active: p.active,
})
);
created++;
} catch (err) {
errors.push({
product: p.name,
error: err instanceof Error ? err.message : String(err),
});
}
}
return { success: true, created, updated: 0, errors };
}