Initial commit - Route Commerce platform

This commit is contained in:
2026-06-01 19:40:55 +00:00
commit 53a9671461
617 changed files with 106132 additions and 0 deletions
+435
View File
@@ -0,0 +1,435 @@
"use server";
import { getAdminUser } from "@/lib/admin-permissions";
import { svcHeaders } from "@/lib/svc-headers";
export async function registerWholesaleCustomer(params: {
brandId: string;
companyName: string;
contactName?: string;
email: string;
phone?: string;
}): Promise<{ success: boolean; error?: string; requiresApproval?: boolean }> {
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
const response = await fetch(
`${supabaseUrl}/rest/v1/rpc/register_wholesale_customer`,
{
method: "POST",
headers: {
...svcHeaders(supabaseKey),
"Content-Type": "application/json",
},
body: JSON.stringify({
p_brand_id: params.brandId,
p_company_name: params.companyName,
p_contact_name: params.contactName ?? null,
p_email: params.email,
p_phone: params.phone ?? null,
}),
}
);
if (!response.ok) {
const err = await response.json();
// Supabase error format: { "message": "...", "code": "...", ... }
// Our RPC error format: { "success": false, "error": "..." }
return { success: false, error: err.message ?? err.error ?? "Registration failed." };
}
const data = await response.json();
// Normalize: RPC may return an array (single row) or a plain object
const result = Array.isArray(data) ? data[0] : data;
if (!result.success) {
return { success: false, error: result.error ?? "Registration failed." };
}
return {
success: true,
requiresApproval: result.requires_approval ?? false,
};
}
export async function getPendingWholesaleRegistrations(brandId: string) {
const adminUser = await getAdminUser();
if (!adminUser) return [];
if (!adminUser.can_manage_orders) return [];
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
const response = await fetch(
`${supabaseUrl}/rest/v1/rpc/get_pending_wholesale_registrations`,
{
method: "POST",
headers: {
...svcHeaders(supabaseKey),
"Content-Type": "application/json",
},
body: JSON.stringify({ p_brand_id: brandId }),
}
);
if (!response.ok) return [];
return response.json();
}
export async function approveWholesaleRegistration(
registrationId: string,
brandId: string,
action: "approve" | "reject"
): Promise<{ success: boolean; error?: string }> {
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 !== "platform_admin" && adminUser.brand_id !== brandId) {
return { success: false, error: "Not authorized to operate on this brand" };
}
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
const response = await fetch(
`${supabaseUrl}/rest/v1/rpc/approve_wholesale_registration`,
{
method: "POST",
headers: {
...svcHeaders(supabaseKey),
"Content-Type": "application/json",
},
body: JSON.stringify({
p_registration_id: registrationId,
p_brand_id: brandId,
p_action: action,
}),
}
);
if (!response.ok) return { success: false, error: "Failed to process registration." };
const data = await response.json();
// Normalize array response (RETURNING single row) to plain object
const result = Array.isArray(data) ? data[0] : data;
if (!result.success) {
return { success: false, error: result.error ?? "Failed to process registration." };
}
return { success: true };
}
export async function getWholesaleCustomerByUser(
brandId: string,
userId: string
): Promise<{
id: string;
user_id: string;
company_name: string;
contact_name: string;
email: string;
phone: string;
account_status: string;
role: string;
brand_id: string;
} | null> {
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
const response = await fetch(
`${supabaseUrl}/rest/v1/rpc/get_wholesale_customer_by_user`,
{
method: "POST",
headers: {
...svcHeaders(supabaseKey),
"Content-Type": "application/json",
},
body: JSON.stringify({ p_brand_id: brandId, p_user_id: userId }),
}
);
if (!response.ok) return null;
const data = await response.json();
return data ?? null;
}
// Fetch a wholesale customer directly by their customer ID (used for admin preview mode)
export async function getWholesaleCustomer(
customerId: string
): Promise<{
id: string;
user_id: string;
company_name: string;
contact_name: string;
email: string;
phone: string;
account_status: string;
role: string;
brand_id: string;
} | null> {
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
const response = await fetch(
`${supabaseUrl}/rest/v1/wholesale_customers?id=eq.${customerId}&select=id,user_id,company_name,contact_name,email,phone,account_status,role,brand_id`,
{
headers: svcHeaders(supabaseKey),
}
);
if (!response.ok) return null;
const data = await response.json();
return data?.[0] ?? null;
}
export async function getWholesaleProducts(brandId: string) {
// Uses SECURITY DEFINER RPC — no auth required for admins, anon key works for customers too
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
const response = await fetch(
`${supabaseUrl}/rest/v1/rpc/get_wholesale_products`,
{
method: "POST",
headers: {
...svcHeaders(supabaseKey),
"Content-Type": "application/json",
},
body: JSON.stringify({ p_brand_id: brandId }),
}
);
if (!response.ok) return [];
return response.json();
}
export async function submitWholesaleOrder(params: {
brandId: string;
customerId: string;
anticipatedPickupDate?: string;
items: Array<{ product_id: string; quantity: number; unit_price: number }>;
notes?: string;
checkoutSessionId?: string; // pass explicitly for caller-generated UUID
}): Promise<{
success: boolean;
error?: string;
orderId?: string;
invoiceNumber?: string;
status?: string;
depositRequired?: number;
creditLimit?: number;
outstandingBalance?: number;
orderTotal?: number;
idempotent?: boolean;
}> {
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
// Generate UUID at the start of checkout — before RPC call for true idempotency
const checkoutSessionId = params.checkoutSessionId ?? crypto.randomUUID();
const itemsJson = params.items.map(i => ({
product_id: i.product_id,
quantity: i.quantity,
unit_price: i.unit_price,
}));
const response = await fetch(
`${supabaseUrl}/rest/v1/rpc/create_wholesale_order`,
{
method: "POST",
headers: {
...svcHeaders(supabaseKey),
"Content-Type": "application/json",
},
body: JSON.stringify({
p_brand_id: params.brandId,
p_customer_id: params.customerId,
p_anticipated_pickup_date: params.anticipatedPickupDate ?? null,
p_items: itemsJson,
p_notes: params.notes ?? null,
p_checkout_session_id: checkoutSessionId,
}),
}
);
if (!response.ok) return { success: false, error: "Failed to create order." };
const data = await response.json();
// Normalize array response (RETURNING single row) to plain object
const result = Array.isArray(data) ? data[0] : data;
if (!result.success) {
return {
success: false,
error: result.error ?? "Failed to create order.",
creditLimit: result.credit_limit,
outstandingBalance: result.outstanding_balance,
orderTotal: result.order_total,
};
}
// Fire webhook — fire-and-forget, don't block the response
enqueueWholesaleWebhookForOrderCreated(
result.order_id,
params.brandId,
params.customerId,
result.invoice_number,
Number(result.deposit_required) || 0
).catch(() => {});
return {
success: true,
orderId: result.order_id,
invoiceNumber: result.invoice_number,
status: result.status,
depositRequired: result.deposit_required,
idempotent: result.idempotent ?? false,
};
}
export async function enqueueWholesaleWebhookForOrderCreated(orderId: string, brandId: string, customerId: string, invoiceNumber: string | null, subtotal: number) {
try {
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY ?? process.env.SUPABASE_ANON_KEY!;
await fetch(`${supabaseUrl}/rest/v1/rpc/enqueue_wholesale_webhook`, {
method: "POST",
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
body: JSON.stringify({
p_event_type: "order_created",
p_order_id: orderId,
p_brand_id: brandId,
p_payload: { order_id: orderId, brand_id: brandId, customer_id: customerId, invoice_number: invoiceNumber, subtotal },
}),
});
} catch (_) {}
}
export type WholesaleProduct = {
id: string;
name: string;
description: string | null;
unit_type: string;
availability: string;
qty_available: number;
price_tiers: Array<{ min_qty: number; max_qty: number; price: number }>;
hp_sku: string | null;
created_at: string;
};
export type WholesaleCustomerOrder = {
id: string;
status: string;
fulfillment_status: string;
payment_status: string;
anticipated_pickup_date: string | null;
subtotal: number;
deposit_required: number;
deposit_paid: number;
balance_due: number;
invoice_number: string | null;
invoice_token: string | null;
created_at: string;
updated_at: string;
items: Array<{
id: string;
product_name: string;
quantity: number;
unit_price: number;
line_total: number;
}>;
};
export type WholesalePricingOverride = {
product_id: string;
custom_unit_price: number;
product_name: string;
};
export async function getWholesaleCustomerPricing(customerId: string): Promise<WholesalePricingOverride[]> {
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
const response = await fetch(
`${supabaseUrl}/rest/v1/rpc/get_wholesale_customer_pricing`,
{
method: "POST",
headers: {
...svcHeaders(supabaseKey),
"Content-Type": "application/json",
},
body: JSON.stringify({ p_customer_id: customerId }),
}
);
if (!response.ok) return [];
return response.json();
}
export async function upsertWholesaleCustomerPricing(params: {
customerId: string;
productId: string;
customUnitPrice: number;
}): Promise<{ success: boolean; error?: string }> {
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
const response = await fetch(
`${supabaseUrl}/rest/v1/rpc/upsert_wholesale_customer_pricing`,
{
method: "POST",
headers: {
...svcHeaders(supabaseKey),
"Content-Type": "application/json",
},
body: JSON.stringify({
p_customer_id: params.customerId,
p_product_id: params.productId,
p_custom_unit_price: params.customUnitPrice,
}),
}
);
if (!response.ok) return { success: false, error: "Failed to save pricing override" };
return { success: true };
}
export async function deleteWholesaleCustomerPricing(params: {
customerId: string;
productId: string;
}): Promise<{ success: boolean; error?: string }> {
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
const response = await fetch(
`${supabaseUrl}/rest/v1/rpc/delete_wholesale_customer_pricing`,
{
method: "POST",
headers: {
...svcHeaders(supabaseKey),
"Content-Type": "application/json",
},
body: JSON.stringify({
p_customer_id: params.customerId,
p_product_id: params.productId,
}),
}
);
if (!response.ok) return { success: false, error: "Failed to delete pricing override" };
return { success: true };
}
export async function getWholesaleCustomerOrders(customerId: string): Promise<WholesaleCustomerOrder[]> {
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
const response = await fetch(
`${supabaseUrl}/rest/v1/rpc/get_wholesale_customer_orders`,
{
method: "POST",
headers: {
...svcHeaders(supabaseKey),
"Content-Type": "application/json",
},
body: JSON.stringify({ p_customer_id: customerId }),
}
);
if (!response.ok) return [];
return response.json();
}