migrate: replace Supabase REST with Drizzle/pg in core admin (wave 1)

This commit is contained in:
2026-06-07 03:14:59 +00:00
parent b8317a200e
commit eb9621d238
17 changed files with 911 additions and 1053 deletions
+26 -37
View File
@@ -2,7 +2,7 @@
import { getAdminUser } from "@/lib/admin-permissions";
import { getActiveBrandId } from "@/lib/brand-scope";
import { svcHeaders } from "@/lib/svc-headers";
import { pool } from "@/lib/db";
import { randomUUID } from "crypto";
export type AdminCreateOrderItem = {
@@ -58,9 +58,6 @@ export async function createAdminOrder(
return { success: false, error: "At least one item is required" };
}
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
// Build items for RPC (match checkout shape)
const rpcItems = input.items.map((i) => ({
product_id: i.product_id,
@@ -77,33 +74,23 @@ export async function createAdminOrder(
const taxLocation = null;
try {
const response = await fetch(
`${supabaseUrl}/rest/v1/rpc/create_order_with_items`,
{
method: "POST",
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json", Prefer: "return=representation" },
body: JSON.stringify({
p_idempotency_key: idempotencyKey,
p_customer_name: input.customer_name.trim(),
p_customer_email: input.customer_email?.trim() || null,
p_customer_phone: input.customer_phone?.trim() || null,
p_stop_id: input.stop_id || null,
p_items: rpcItems,
p_tax_amount: taxAmount,
p_tax_rate: taxRate,
p_tax_location: taxLocation,
// The RPC may also accept brand scoping internally via stop or we can extend later.
}),
}
const { rows } = await pool.query<{ id: string }>(
`SELECT * FROM create_order_with_items(
$1, $2, $3, $4, $5, $6::jsonb, $7, $8, $9
)`,
[
idempotencyKey,
input.customer_name.trim(),
input.customer_email?.trim() || null,
input.customer_phone?.trim() || null,
input.stop_id || null,
JSON.stringify(rpcItems),
taxAmount,
taxRate,
taxLocation,
],
);
if (!response.ok) {
const errText = await response.text().catch(() => "Unknown error");
return { success: false, error: `Failed to create order: ${errText}` };
}
const data = await response.json();
const data = rows[0];
if (!data || !data.id) {
return { success: false, error: "Order created but no ID returned" };
}
@@ -112,18 +99,20 @@ export async function createAdminOrder(
if (input.internal_notes?.trim()) {
// Best-effort; don't fail the whole create if this secondary update fails.
try {
await fetch(`${supabaseUrl}/rest/v1/orders?id=eq.${data.id}`, {
method: "PATCH",
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
body: JSON.stringify({ internal_notes: input.internal_notes.trim() }),
});
await pool.query(
"UPDATE orders SET internal_notes = $1 WHERE id = $2",
[input.internal_notes.trim(), data.id],
);
} catch {
// ignore
}
}
return { success: true, orderId: data.id, order: data };
} catch (err: any) {
return { success: false, error: err?.message ?? "Unexpected error creating order" };
} catch (err) {
return {
success: false,
error: err instanceof Error ? err.message : "Unexpected error creating order",
};
}
}
+26 -24
View File
@@ -1,7 +1,7 @@
"use server";
import { getAdminUser } from "@/lib/admin-permissions";
import { svcHeaders } from "@/lib/svc-headers";
import { pool } from "@/lib/db";
import { logAuditEvent } from "@/actions/audit";
export type CreateRefundResult =
@@ -22,37 +22,39 @@ export async function createRefund(
if (!adminUser) return { success: false, error: "Not authenticated" };
if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" };
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
const res = await fetch(`${supabaseUrl}/rest/v1/refunds`, {
method: "POST",
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json", Prefer: "return=representation" },
body: JSON.stringify({
order_id: orderId,
amount: data.amount,
reason: data.reason ?? null,
processor: data.processor ?? null,
processor_refund_id: data.processor_refund_id ?? null,
status: "pending",
}),
});
if (!res.ok) {
const err = await res.text();
return { success: false, error: `Failed: ${err}` };
let inserted: { id: string } | null = null;
try {
const { rows } = await pool.query<{ id: string }>(
`INSERT INTO refunds (order_id, amount, reason, processor, processor_refund_id, status)
VALUES ($1, $2, $3, $4, $5, 'pending')
RETURNING id`,
[
orderId,
data.amount,
data.reason ?? null,
data.processor ?? null,
data.processor_refund_id ?? null,
],
);
inserted = rows[0] ?? null;
} catch (err) {
return {
success: false,
error: err instanceof Error ? err.message : "Failed",
};
}
if (!inserted) {
return { success: false, error: "Insert returned no row" };
}
const inserted = await res.json();
logAuditEvent({
table_name: "refunds",
record_id: inserted[0]?.id ?? "",
record_id: inserted.id,
action: "INSERT",
old_data: {},
new_data: { order_id: orderId, amount: data.amount },
brand_id: brandId,
});
return { success: true, id: inserted[0]?.id ?? "" };
return { success: true, id: inserted.id };
}
+74 -53
View File
@@ -1,7 +1,7 @@
"use server";
import { getAdminUser } from "@/lib/admin-permissions";
import { svcHeaders } from "@/lib/svc-headers";
import { pool } from "@/lib/db";
import { logAuditEvent } from "@/actions/audit";
export type UpdateOrderResult =
@@ -36,33 +36,51 @@ export async function updateOrder(
return { success: false, error: "Not authorized for this brand" };
}
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
// Build a partial SET clause. Each set column is added in the order
// the caller passed it; we don't care about column ordering.
const sets: string[] = [];
const params: unknown[] = [];
const push = (col: string, val: unknown) => {
params.push(val);
sets.push(`${col} = $${params.length}`);
};
const patchData: Record<string, unknown> = {};
if (data.customer_name !== undefined) patchData.customer_name = data.customer_name;
if (data.customer_email !== undefined) patchData.customer_email = data.customer_email;
if (data.customer_phone !== undefined) patchData.customer_phone = data.customer_phone;
if (data.status !== undefined) patchData.status = data.status;
if (data.discount_amount !== undefined) patchData.discount_amount = data.discount_amount;
if (data.discount_reason !== undefined) patchData.discount_reason = data.discount_reason;
if (data.internal_notes !== undefined) patchData.internal_notes = data.internal_notes;
if (data.pickup_complete !== undefined) patchData.pickup_complete = data.pickup_complete;
if (data.pickup_completed_at !== undefined) patchData.pickup_completed_at = data.pickup_completed_at;
if (data.subtotal !== undefined) patchData.subtotal = data.subtotal;
if (data.payment_processor !== undefined) patchData.payment_processor = data.payment_processor;
if (data.payment_status !== undefined) patchData.payment_status = data.payment_status;
if (data.payment_transaction_id !== undefined) patchData.payment_transaction_id = data.payment_transaction_id;
if (data.customer_name !== undefined) push("customer_name", data.customer_name);
if (data.customer_email !== undefined) push("customer_email", data.customer_email);
if (data.customer_phone !== undefined) push("customer_phone", data.customer_phone);
if (data.status !== undefined) push("status", data.status);
if (data.discount_amount !== undefined) push("discount_amount", data.discount_amount);
if (data.discount_reason !== undefined) push("discount_reason", data.discount_reason);
if (data.internal_notes !== undefined) push("internal_notes", data.internal_notes);
if (data.pickup_complete !== undefined) push("pickup_complete", data.pickup_complete);
if (data.pickup_completed_at !== undefined) push("pickup_completed_at", data.pickup_completed_at);
if (data.subtotal !== undefined) push("subtotal", data.subtotal);
if (data.payment_processor !== undefined) push("payment_processor", data.payment_processor);
if (data.payment_status !== undefined) push("payment_status", data.payment_status);
if (data.payment_transaction_id !== undefined) push("payment_transaction_id", data.payment_transaction_id);
const res = await fetch(`${supabaseUrl}/rest/v1/orders?id=eq.${orderId}`, {
method: "PATCH",
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
body: JSON.stringify(patchData),
});
if (sets.length === 0) {
return { success: true };
}
if (!res.ok) {
const err = await res.text();
return { success: false, error: `Failed: ${err}` };
params.push(orderId);
const patchData = Object.fromEntries(
sets.map((s, i) => {
const col = s.split(" = ")[0];
return [col, params[i]];
}),
);
try {
await pool.query(
`UPDATE orders SET ${sets.join(", ")} WHERE id = $${params.length}`,
params,
);
} catch (err) {
return {
success: false,
error: err instanceof Error ? err.message : "Failed",
};
}
logAuditEvent({
@@ -85,25 +103,33 @@ export async function updateOrderItem(
if (!adminUser) return { success: false, error: "Not authenticated" };
if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" };
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
const sets: string[] = [];
const params: unknown[] = [];
const push = (col: string, val: unknown) => {
params.push(val);
sets.push(`${col} = $${params.length}`);
};
const patchData: Record<string, unknown> = {};
if (data.quantity !== undefined) patchData.quantity = data.quantity;
if (data.price !== undefined) patchData.price = data.price;
if (data.quantity !== undefined) push("quantity", data.quantity);
if (data.price !== undefined) push("price", data.price);
const res = await fetch(`${supabaseUrl}/rest/v1/order_items?id=eq.${itemId}`, {
method: "PATCH",
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
body: JSON.stringify(patchData),
});
if (!res.ok) {
const err = await res.text();
return { success: false, error: `Failed: ${err}` };
if (sets.length === 0) {
return { success: true };
}
return { success: true };
params.push(itemId);
try {
await pool.query(
`UPDATE order_items SET ${sets.join(", ")} WHERE id = $${params.length}`,
params,
);
return { success: true };
} catch (err) {
return {
success: false,
error: err instanceof Error ? err.message : "Failed",
};
}
}
export async function deleteOrderItem(itemId: string): Promise<UpdateOrderResult> {
@@ -111,18 +137,13 @@ export async function deleteOrderItem(itemId: string): Promise<UpdateOrderResult
if (!adminUser) return { success: false, error: "Not authenticated" };
if (!adminUser.can_manage_orders) return { success: false, error: "Not authorized" };
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
const res = await fetch(`${supabaseUrl}/rest/v1/order_items?id=eq.${itemId}`, {
method: "DELETE",
headers: { ...svcHeaders(supabaseKey) },
});
if (!res.ok) {
const err = await res.text();
return { success: false, error: `Failed: ${err}` };
try {
await pool.query("DELETE FROM order_items WHERE id = $1", [itemId]);
return { success: true };
} catch (err) {
return {
success: false,
error: err instanceof Error ? err.message : "Failed",
};
}
return { success: true };
}