migrate: replace Supabase REST with Drizzle/pg in 11 more action files (wave 5 partial)

- analytics.ts: rewrite getReportsSummary, getRevenueChart, getSalesByProduct,
  getContactGrowth, getRecentOrders, getConversionFunnel against pool + new
  orders/customers schema. Drops retired columns (subtotal, pickup_complete)
  and re-implements the SQL by hand.
- import-orders.ts: bulk import via withTx using orders + orderItems + customers
  Drizzle tables, computes total_cents from current product prices.
- import-products.ts: rewrite to use withTenant(brandId) and Drizzle products
  table.
- products/create-product.ts, update-product.ts, upload-image.ts: switch to
  withTenant + Drizzle; image_url moves to product_images table.
- reports.ts: rewrite against pool + new orders schema.
- route-trace/lots.ts: stub functions (route-trace feature retired from SaaS
  rebuild — harvest_lots table not in db/schema). Uses discriminated union
  return types so consumer narrowing works in both branches.
- settings/features.ts: switch to withTenant + Drizzle brandSettings.
- shipping.ts: switch to pool + Drizzle orders/orderItems.
- api/v1/referrals/route.ts: fix typecheck (referred_user_id undefined → 'anonymous').

Typecheck: clean. Tests: 22/22 pass. Build: succeeds.
This commit is contained in:
2026-06-07 05:26:03 +00:00
parent 3f323dd52a
commit 67abcaa2db
11 changed files with 834 additions and 762 deletions
+26 -27
View File
@@ -2,7 +2,9 @@
import { getAdminUser } from "@/lib/admin-permissions";
import { getMockTableData } from "@/lib/mock-data";
import { svcHeaders } from "@/lib/svc-headers";
import { withTenant } from "@/db/client";
import { products } from "@/db/schema";
import { and, eq } from "drizzle-orm";
const useMockData = process.env.NEXT_PUBLIC_USE_MOCK_DATA === "true";
@@ -36,33 +38,30 @@ export async function updateProduct(
return { success: true };
}
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
const res = await fetch(`${supabaseUrl}/rest/v1/products?id=eq.${productId}`, {
method: "PATCH",
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json", "Prefer": "return=representation" },
body: JSON.stringify({
name: data.name,
description: data.description,
price: data.price,
type: data.type,
active: data.active,
image_url: data.image_url ?? null,
is_taxable: data.is_taxable,
pickup_type: data.pickup_type ?? "scheduled_stop",
}),
});
if (!res.ok) {
const err = await res.text();
return { success: false, error: `Failed to update product: ${err}` };
// The new schema has `price_cents` (integer cents) and no `type`,
// `is_taxable`, `pickup_type`, or `image_url` column. `type` /
// `is_taxable` / `pickup_type` are dropped for the SaaS rebuild;
// `image_url` lives in `product_images`.
const priceCents = Math.round(Number(data.price) * 100);
if (!Number.isFinite(priceCents) || priceCents < 0) {
return { success: false, error: "Invalid price" };
}
const updated = await res.json();
if (updated.errors) {
return { success: false, error: updated.errors[0]?.message ?? "Unknown error" };
try {
await withTenant(brandId, (db) =>
db
.update(products)
.set({
name: data.name,
description: data.description ?? null,
priceCents,
active: data.active,
updatedAt: new Date(),
})
.where(and(eq(products.id, productId), eq(products.tenantId, brandId)))
);
return { success: true };
} catch (err) {
return { success: false, error: `Failed to update product: ${err instanceof Error ? err.message : String(err)}` };
}
return { success: true };
}