fix: react-doctor unused-file 123→27 (-96 dead files removed)

This commit is contained in:
Nora
2026-06-26 04:38:44 -06:00
parent ab9813b4ee
commit 27b2ded94e
96 changed files with 0 additions and 17319 deletions
-76
View File
@@ -1,76 +0,0 @@
/**
* Service-layer admin user creation. Hits the `admin_users` table directly
* via the shared pg pool. Returns the inserted row (or existing row if the
* user was already provisioned).
*/
export async function createAdminUser(
userId: string,
role: string,
brandId: string | null
): Promise<Record<string, unknown> | null> {
const { pool } = await import("@/lib/db");
const body = {
user_id: userId,
role,
brand_id: brandId,
active: true,
can_manage_products: role === "platform_admin",
can_manage_stops: role === "platform_admin",
can_manage_orders: true,
can_manage_pickup: role !== "store_employee",
can_manage_messages: role === "platform_admin",
can_manage_refunds: role === "platform_admin",
can_manage_users: role === "platform_admin",
can_manage_water_log: role === "platform_admin",
can_manage_reports: role === "platform_admin",
can_manage_settings: role === "platform_admin",
must_change_password: false,
};
try {
const { rows } = await pool.query<Record<string, unknown>>(
`INSERT INTO admin_users
(user_id, role, brand_id, active,
can_manage_products, can_manage_stops, can_manage_orders,
can_manage_pickup, can_manage_messages, can_manage_refunds,
can_manage_users, can_manage_water_log, can_manage_reports,
can_manage_settings, must_change_password)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15)
ON CONFLICT (user_id) DO UPDATE
SET role = EXCLUDED.role,
brand_id = EXCLUDED.brand_id,
active = EXCLUDED.active,
can_manage_products = EXCLUDED.can_manage_products,
can_manage_stops = EXCLUDED.can_manage_stops,
can_manage_orders = EXCLUDED.can_manage_orders,
can_manage_pickup = EXCLUDED.can_manage_pickup,
can_manage_messages = EXCLUDED.can_manage_messages,
can_manage_refunds = EXCLUDED.can_manage_refunds,
can_manage_users = EXCLUDED.can_manage_users,
can_manage_water_log = EXCLUDED.can_manage_water_log,
can_manage_reports = EXCLUDED.can_manage_reports,
can_manage_settings = EXCLUDED.can_manage_settings
RETURNING *`,
[
body.user_id,
body.role,
body.brand_id,
body.active,
body.can_manage_products,
body.can_manage_stops,
body.can_manage_orders,
body.can_manage_pickup,
body.can_manage_messages,
body.can_manage_refunds,
body.can_manage_users,
body.can_manage_water_log,
body.can_manage_reports,
body.can_manage_settings,
body.must_change_password,
],
);
return rows[0] ?? null;
} catch {
return null;
}
}
-19
View File
@@ -1,19 +0,0 @@
import { getAdminUser } from "@/lib/admin-permissions";
import type { AdminUser } from "@/lib/admin-permissions-types";
export type AuthGuardResult =
| { authorized: true; adminUser: AdminUser }
| { authorized: false; adminUser: null };
/**
* Require an authenticated admin user. Returns a discriminated result so
* callers can branch without throwing. Use this in API route handlers that
* need a uniform "is this request authenticated?" check before proceeding.
*/
export async function requireAdminUser(): Promise<AuthGuardResult> {
const adminUser = await getAdminUser();
if (!adminUser) {
return { authorized: false, adminUser: null };
}
return { authorized: true, adminUser };
}
-308
View File
@@ -1,308 +0,0 @@
// Route Commerce Platform — Billing Logic
// Central helpers for subscription creation, status sync, and feature management
import "server-only";
import Stripe from "stripe";
import { ADDONS, type PlanTierKey, type AddonKey } from "./pricing";
import { pool } from "@/lib/db";
// ── Subscription status types ──────────────────────────────────────────────────
export type SubscriptionStatus =
| "active"
| "past_due"
| "canceled"
| "trialing"
| "incomplete"
| "unpaid";
export interface BrandSubscription {
brand_id: string;
stripe_subscription_id: string | null;
stripe_subscription_status: SubscriptionStatus | null;
stripe_current_period_end: string | null;
stripe_customer_id: string | null;
plan_tier: PlanTierKey;
}
// ── RPC call helper (raw SQL via pg pool) ─────────────────────────────────────
async function rpc<T = Record<string, unknown>>(
fn: string,
params: ReadonlyArray<unknown>
): Promise<T> {
const placeholders = params.map((_, i) => `$${i + 1}`).join(", ");
const { rows } = await pool.query<Record<string, unknown>>(
`SELECT * FROM ${fn}(${placeholders})`,
params as unknown[]
);
return rows as unknown as T;
}
// ── Feature sync ─────────────────────────────────────────────────────────────
// When a subscription changes, sync the plan tier + all add-on feature flags
export async function syncSubscriptionFeatures(
brandId: string,
subscriptionItems: Array<{ priceId: string; enabled: boolean }>
): Promise<void> {
// Determine plan tier from subscription items
const priceToTier: Record<string, PlanTierKey> = {
[process.env.STRIPE_PRICE_STARTER ?? ""]: "starter",
[process.env.STRIPE_PRICE_FARM ?? ""]: "farm",
[process.env.STRIPE_PRICE_ENTERPRISE ?? ""]: "enterprise",
};
const priceToAddon: Record<string, AddonKey> = {
[process.env.STRIPE_PRICE_HARVEST_REACH ?? ""]: "harvest_reach",
[process.env.STRIPE_PRICE_WHOLESALE_PORTAL ?? ""]: "wholesale_portal",
[process.env.STRIPE_PRICE_WATER_LOG ?? ""]: "water_log",
[process.env.STRIPE_PRICE_AI_TOOLS ?? ""]: "ai_tools",
[process.env.STRIPE_PRICE_SQUARE_SYNC ?? ""]: "square_sync",
[process.env.STRIPE_PRICE_SMS_CAMPAIGNS ?? ""]: "sms_campaigns",
};
// Update plan tier
const tierItem = subscriptionItems.find((item) => priceToTier[item.priceId]);
if (tierItem) {
const newTier = priceToTier[tierItem.priceId];
if (newTier) {
await rpc("update_brand_plan_tier", [brandId, newTier]);
}
}
// Sync add-on features (parallelize per-addon RPC calls)
await Promise.all(
Object.entries(priceToAddon).map(async ([priceId, addonKey]) => {
if (!priceId) return;
try {
const item = subscriptionItems.find((i) => i.priceId === priceId);
const enabled = item?.enabled ?? false;
await rpc("set_brand_feature", [brandId, addonKey, enabled]);
} catch (err) {
// One add-on RPC failure must not block the others from syncing.
console.warn("[billing] set_brand_feature failed for", addonKey, err);
}
})
);
}
// ── Subscription creation ─────────────────────────────────────────────────────
export async function createOrUpdateSubscription(
brandId: string,
priceKey: string,
billingCycle: "monthly" | "annual"
): Promise<{ subscriptionId: string; clientSecret: string }> {
const stripeKey = process.env.STRIPE_SECRET_KEY;
if (!stripeKey) throw new Error("STRIPE_SECRET_KEY not configured");
const Stripe = (await import("stripe")).default;
const stripe = new Stripe(stripeKey, { apiVersion: "2026-06-24.dahlia" });
// Get brand's Stripe customer
const brandRows = await rpc<Array<BrandSubscription>>(
"get_brand_subscription",
[brandId]
);
const brandData = brandRows[0];
const customerId = brandData?.stripe_customer_id;
if (!customerId) throw new Error("No Stripe customer for brand. Complete Stripe setup first.");
// Get price ID
const priceId = getPriceId(priceKey, billingCycle);
if (!priceId) throw new Error(`No price configured for ${priceKey} (${billingCycle})`);
// Check if brand already has an active subscription — update it instead of creating new
const existingSubId = brandData?.stripe_subscription_id;
let subscription;
if (existingSubId) {
// Add or update the subscription item
const existing = await stripe.subscriptions.retrieve(existingSubId);
const existingItem = existing.items.data.find((item) => {
const planPrices = [
process.env.STRIPE_PRICE_STARTER ?? "",
process.env.STRIPE_PRICE_FARM ?? "",
process.env.STRIPE_PRICE_ENTERPRISE ?? "",
];
return planPrices.includes(item.price.id);
});
if (existingItem) {
subscription = await stripe.subscriptions.update(existingSubId, {
items: [{ id: existingItem.id, price: priceId }],
proration_behavior: "create_prorations",
});
} else {
subscription = await stripe.subscriptions.update(existingSubId, {
items: [{ price: priceId }],
});
}
} else {
subscription = await stripe.subscriptions.create({
customer: customerId,
items: [{ price: priceId }],
payment_behavior: "default_incomplete",
payment_settings: { save_default_payment_method: "on_subscription" },
expand: ["latest_invoice.payment_intent"],
metadata: { brand_id: brandId, price_key: priceKey, billing: billingCycle },
});
}
// Save subscription ID to brand
const subData = subscription as unknown as { id: string; status: string; current_period_end: number };
await rpc("set_brand_subscription", [
brandId,
subData.id,
subData.status,
new Date(subData.current_period_end * 1000).toISOString(),
]);
const latestInvoice = subscription.latest_invoice as { payment_intent?: { client_secret?: string } } | null;
const invoiceOrNull = typeof subscription.latest_invoice !== "string" ? latestInvoice : null;
const paymentIntent = invoiceOrNull?.payment_intent as { client_secret?: string } | undefined;
const clientSecret = paymentIntent?.client_secret ?? null;
return { subscriptionId: subscription.id, clientSecret: clientSecret ?? "" };
}
// ── Cancel subscription ─────────────────────────────────────────────────────
export async function cancelBrandSubscription(
brandId: string,
priceKey?: string
): Promise<void> {
const stripeKey = process.env.STRIPE_SECRET_KEY;
if (!stripeKey) throw new Error("STRIPE_SECRET_KEY not configured");
const brandRows = await rpc<BrandSubscription[]>("get_brand_subscription", [brandId]);
const brand = brandRows[0];
if (!brand?.stripe_subscription_id) throw new Error("No active subscription to cancel");
const Stripe = (await import("stripe")).default;
const stripe = new Stripe(stripeKey, { apiVersion: "2026-06-24.dahlia" });
if (priceKey) {
// Cancel only a specific add-on item, not the whole subscription
const sub = await stripe.subscriptions.retrieve(brand.stripe_subscription_id);
const targetPriceId = getPriceId(priceKey, "monthly");
const item = sub.items.data.find((i) => i.price.id === targetPriceId);
if (item) {
await stripe.subscriptions.update(brand.stripe_subscription_id, {
items: [{ id: item.id, deleted: true }],
proration_behavior: "none",
});
// Disable the feature flag
const addonKey = getAddonKeyFromPriceKey(priceKey);
if (addonKey) {
await rpc("set_brand_feature", [brandId, addonKey, false]);
}
}
} else {
// Cancel entire subscription
await stripe.subscriptions.cancel(brand.stripe_subscription_id);
await rpc("set_brand_subscription", [
brandId,
"",
"canceled",
null,
]);
// Disable all add-on features (parallelize per-addon RPC calls)
const addonKeys = Object.keys(ADDONS) as AddonKey[];
await Promise.all(
addonKeys.map(async (addonKey) => {
try {
await rpc("set_brand_feature", [brandId, addonKey, false]);
} catch (err) {
// One add-on disable failure must not block the others.
console.warn("[billing] disable addon failed for", addonKey, err);
}
})
);
}
}
// ── Past due notification ─────────────────────────────────────────────────────
export async function sendPastDueNotification(brandId: string): Promise<void> {
const brandRows = await rpc<Array<{ name: string }>>("get_brand_subscription", [brandId]);
const brand = brandRows[0];
const brandName = brand?.name ?? "your brand";
const adminEmail = await getAdminEmail(brandId);
await rpc("enqueue_notification", [
brandId,
adminEmail ?? "team@cielohermosa.com",
`Payment Failed — ${brandName}`,
`
<h2>Payment Failed</h2>
<p>We were unable to charge your card for the Cielo Hermosa platform subscription.</p>
<p>Please update your payment method within 7 days to avoid service interruption.</p>
<p><a href="${process.env.NEXT_PUBLIC_SITE_URL}/admin/settings/billing">Update Payment Method →</a></p>
`,
`Payment failed for ${brandName}. Please update your payment method at ${process.env.NEXT_PUBLIC_SITE_URL}/admin/settings/billing to avoid service interruption.`,
]);
}
// ── Helpers ───────────────────────────────────────────────────────────────────
function getPriceId(priceKey: string, billingCycle: "monthly" | "annual"): string | null {
if (billingCycle === "annual") {
return process.env[`STRIPE_PRICE_${priceKey.toUpperCase()}_ANNUAL`] ?? null;
}
return process.env[`STRIPE_PRICE_${priceKey.toUpperCase()}`] ?? null;
}
function getAddonKeyFromPriceKey(priceKey: string): AddonKey | null {
const map: Record<string, AddonKey> = {
harvest_reach: "harvest_reach",
wholesale_portal: "wholesale_portal",
water_log: "water_log",
ai_tools: "ai_tools",
square_sync: "square_sync",
sms_campaigns: "sms_campaigns",
};
return map[priceKey] ?? null;
}
async function getAdminEmail(brandId: string): Promise<string | null> {
try {
const data = await rpc<{ notification_email?: string; from_email?: string }>(
"get_wholesale_settings",
[brandId]
);
const settings = Array.isArray(data) ? data[0] : data;
return settings?.notification_email ?? settings?.from_email ?? null;
} catch {
return null;
}
}
// ── Price ID resolution (for webhook) ────────────────────────────────────────
export function resolvePriceKeyFromPriceId(priceId: string): { type: "plan" | "addon"; key: string } | null {
const planMap: Record<string, string> = {
[process.env.STRIPE_PRICE_STARTER ?? ""]: "starter",
[process.env.STRIPE_PRICE_FARM ?? ""]: "farm",
[process.env.STRIPE_PRICE_ENTERPRISE ?? ""]: "enterprise",
};
if (planMap[priceId]) return { type: "plan", key: planMap[priceId] };
const addonMap: Record<string, AddonKey> = {
[process.env.STRIPE_PRICE_HARVEST_REACH ?? ""]: "harvest_reach",
[process.env.STRIPE_PRICE_WHOLESALE_PORTAL ?? ""]: "wholesale_portal",
[process.env.STRIPE_PRICE_WATER_LOG ?? ""]: "water_log",
[process.env.STRIPE_PRICE_AI_TOOLS ?? ""]: "ai_tools",
[process.env.STRIPE_PRICE_SQUARE_SYNC ?? ""]: "square_sync",
[process.env.STRIPE_PRICE_SMS_CAMPAIGNS ?? ""]: "sms_campaigns",
};
if (addonMap[priceId]) return { type: "addon", key: addonMap[priceId] };
return null;
}
-146
View File
@@ -1,146 +0,0 @@
// Data service for direct PostgreSQL queries via pg Pool
// This replaces the Supabase client for production use
import { pool } from "./db";
import type { QueryResultRow } from "pg";
// Re-export pool for convenience
export { pool };
// Query helpers for common operations
export async function query<T extends QueryResultRow = Record<string, unknown>>(
sql: string,
params: unknown[] = []
): Promise<T[]> {
const result = await pool.query<T>(sql, params);
return result.rows;
}
export async function queryOne<T extends QueryResultRow = Record<string, unknown>>(
sql: string,
params: unknown[] = []
): Promise<T | null> {
const result = await pool.query<T>(sql, params);
return result.rows[0] ?? null;
}
// Brand operations
export async function getBrands() {
return query<{ id: string; name: string; slug: string; accent_color: string | null; active: boolean }>(
"SELECT id, name, slug, accent_color, active FROM brands ORDER BY name"
);
}
export async function getBrandById(brandId: string) {
return queryOne<{ id: string; name: string; slug: string }>(
"SELECT id, name, slug FROM brands WHERE id = $1",
[brandId]
);
}
// Product operations
export async function getProducts(brandId?: string | null) {
if (brandId) {
return query<Record<string, unknown>>(
"SELECT * FROM products WHERE brand_id = $1 AND active = true AND deleted_at IS NULL ORDER BY name",
[brandId]
);
}
return query<Record<string, unknown>>(
"SELECT * FROM products WHERE active = true AND deleted_at IS NULL ORDER BY name"
);
}
export async function getProductById(productId: string) {
return queryOne<Record<string, unknown>>(
"SELECT * FROM products WHERE id = $1 AND deleted_at IS NULL",
[productId]
);
}
// Stop operations
export async function getStops(brandId?: string | null) {
if (brandId) {
return query<Record<string, unknown>>(
"SELECT * FROM stops WHERE brand_id = $1 AND active = true ORDER BY date DESC",
[brandId]
);
}
return query<Record<string, unknown>>(
"SELECT * FROM stops WHERE active = true ORDER BY date DESC"
);
}
export async function getStopById(stopId: string) {
return queryOne<Record<string, unknown>>(
"SELECT * FROM stops WHERE id = $1",
[stopId]
);
}
// Order operations
export async function getOrders(brandId?: string | null) {
if (brandId) {
return query<Record<string, unknown>>(
`SELECT o.*, s.city, s.state, s.date, s.location, s.slug
FROM orders o
LEFT JOIN stops s ON o.stop_id = s.id
WHERE o.brand_id = $1
ORDER BY o.created_at DESC`,
[brandId]
);
}
return query<Record<string, unknown>>(
`SELECT o.*, s.city, s.state, s.date, s.location, s.slug
FROM orders o
LEFT JOIN stops s ON o.stop_id = s.id
ORDER BY o.created_at DESC`
);
}
// Worker operations (for time tracking)
export async function getWorkers(brandId?: string | null) {
if (brandId) {
return query<Record<string, unknown>>(
"SELECT * FROM workers WHERE brand_id = $1 AND is_active = true ORDER BY name",
[brandId]
);
}
return query<Record<string, unknown>>(
"SELECT * FROM workers WHERE is_active = true ORDER BY name"
);
}
// Task operations (for time tracking)
export async function getTasks(brandId?: string | null) {
if (brandId) {
return query<Record<string, unknown>>(
"SELECT * FROM tasks WHERE brand_id = $1 ORDER BY sort_order",
[brandId]
);
}
return query<Record<string, unknown>>(
"SELECT * FROM tasks ORDER BY sort_order"
);
}
// Customer operations
export async function getCustomers(brandId?: string | null) {
if (brandId) {
return query<Record<string, unknown>>(
"SELECT * FROM customers WHERE brand_id = $1 ORDER BY name",
[brandId]
);
}
return query<Record<string, unknown>>(
"SELECT * FROM customers ORDER BY name"
);
}
// Brand settings
export async function getBrandSettings(brandId: string) {
return queryOne<Record<string, unknown>>(
"SELECT * FROM brand_settings WHERE brand_id = $1",
[brandId]
);
}
-94
View File
@@ -1,94 +0,0 @@
// Global Error Handler for uncaught exceptions
import { captureError, addBreadcrumb } from "./sentry";
// Handle uncaught errors
if (typeof window !== "undefined") {
window.onerror = (message, source, lineno, colno, error) => {
captureError(error || new Error(String(message)), {
source,
lineno,
colno,
type: "uncaught_error",
});
return false;
};
// Handle unhandled promise rejections
window.onunhandledrejection = (event) => {
captureError(
event.reason instanceof Error
? event.reason
: new Error(String(event.reason)),
{
type: "unhandled_rejection",
promise: event.promise ? String(event.promise) : undefined,
}
);
};
}
// Add breadcrumb for page navigation
export function trackPageNavigation(path: string) {
addBreadcrumb(`Navigated to ${path}`, { path });
}
// Add breadcrumb for user actions
export function trackUserAction(action: string, details?: Record<string, unknown>) {
addBreadcrumb(`User action: ${action}`, { action, ...details });
}
// Performance monitoring
export function measurePerformance(name: string, callback: () => void | Promise<void>) {
const start = performance.now();
const measure = async () => {
await callback();
const duration = performance.now() - start;
addBreadcrumb(`Performance: ${name}`, {
name,
duration: `${duration.toFixed(2)}ms`,
});
};
return measure();
}
// API call tracking
export async function trackApiCall<T>(
endpoint: string,
method: string,
fn: () => Promise<T>
): Promise<T> {
addBreadcrumb(`API call: ${method} ${endpoint}`, { endpoint, method });
try {
const result = await fn();
addBreadcrumb(`API success: ${method} ${endpoint}`, { endpoint, method, success: true });
return result;
} catch (error) {
captureError(error as Error, { endpoint, method });
addBreadcrumb(`API error: ${method} ${endpoint}`, { endpoint, method, success: false });
throw error;
}
}
// Debug logging for development
export const debugLog = {
info: (message: string, data?: unknown) => {
if (process.env.NODE_ENV === "development") {
console.info(`[DEBUG] ${message}`, data);
}
},
warn: (message: string, data?: unknown) => {
if (process.env.NODE_ENV === "development") {
console.warn(`[DEBUG] ${message}`, data);
}
},
error: (message: string, data?: unknown) => {
if (process.env.NODE_ENV === "development") {
console.error(`[DEBUG] ${message}`, data);
}
},
};
-77
View File
@@ -1,77 +0,0 @@
// FedEx OAuth token cache.
// This module intentionally lives OUTSIDE any "use server" file. The
// `react-doctor/server-no-mutable-module-state` rule flags module-scope
// mutable state inside "use server" files because it can leak per-user
// state across requests. The FedEx token cache is NOT per-user state —
// it's a service-credential cache shared across all requests, which is
// the desired behavior. Co-locating the cache in a non-server file keeps
// the rule satisfied while preserving the cross-request token reuse that
// FedEx's rate-limited OAuth endpoint requires.
type FedExAuthToken = {
accessToken: string;
expiresAt: number;
};
const FEDEX_BASE_URL = "https://apis.fedex.com";
const FEDEX_SANDBOX_URL = "https://apis-sandbox.fedex.com";
export function getFedExApiBaseUrl(useProduction: boolean): string {
return useProduction ? FEDEX_BASE_URL : FEDEX_SANDBOX_URL;
}
// Buffer before expiry to refresh proactively
const EXPIRY_BUFFER_MS = 5 * 60 * 1000;
let cachedToken: FedExAuthToken | null = null;
export async function getFedExAuthToken(settings: {
fedexApiKey: string;
fedexApiSecret: string;
useProduction: boolean;
}): Promise<{ accessToken: string } | { error: string }> {
const base = settings.useProduction ? FEDEX_BASE_URL : FEDEX_SANDBOX_URL;
if (cachedToken && Date.now() < cachedToken.expiresAt - EXPIRY_BUFFER_MS) {
return { accessToken: cachedToken.accessToken };
}
const credentials = Buffer.from(
`${settings.fedexApiKey}:${settings.fedexApiSecret}`
).toString("base64");
try {
const res = await fetch(`${base}/oauth/token`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${credentials}`,
},
body: "grant_type=client_credentials",
});
if (!res.ok) {
const text = await res.text();
return { error: `FedEx auth failed: ${text}` };
}
const data = (await res.json()) as {
access_token: string;
expires_in: number;
};
cachedToken = {
accessToken: data.access_token,
expiresAt: Date.now() + data.expires_in * 1000,
};
return { accessToken: data.access_token };
} catch (err) {
return { error: `FedEx auth network error: ${(err as Error).message}` };
}
}
// Test-only helper to reset the cache between unit tests.
export function __resetFedExTokenCacheForTests(): void {
cachedToken = null;
}
-89
View File
@@ -1,89 +0,0 @@
/**
* Square OAuth token-exchange helper.
*
* Extracted from the `/api/square/oauth/callback` route so the GET handler
* stays a thin redirect layer — no `fetch(..., { method: "POST" })` or DB
* writes live in the route file. The OAuth `code` is single-use at the
* provider, which makes this write idempotent on replay.
*/
import { pool } from "@/lib/db";
export type SquareTokenExchangeResult =
| { ok: true; accessToken: string; locationId: string | null }
| { ok: false; error: string };
export async function exchangeSquareCodeForToken(args: {
code: string;
origin: string;
}): Promise<SquareTokenExchangeResult> {
const appId = process.env.NEXT_PUBLIC_SQUARE_APP_ID;
const appSecret = process.env.SQUARE_APP_SECRET;
const env = process.env.SQUARE_ENVIRONMENT ?? "sandbox";
if (!appId || !appSecret) {
return { ok: false, error: "square_credentials_not_configured" };
}
const tokenUrl =
env === "production"
? "https://connect.squareup.com/v2/oauth2/token"
: "https://connect.squareupsandbox.com/v2/oauth2/token";
const redirectUri = `${args.origin}/api/square/oauth/callback`;
const tokenResponse = await fetch(tokenUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Square-Version": "2025-01-16",
},
body: JSON.stringify({
client_id: appId,
client_secret: appSecret,
code: args.code,
grant_type: "authorization_code",
redirect_uri: redirectUri,
}),
});
const tokenData = (await tokenResponse.json().catch(() => ({}))) as {
access_token?: string;
location_id?: string;
};
if (!tokenResponse.ok || !tokenData.access_token) {
return { ok: false, error: "square_token_exchange_failed" };
}
return {
ok: true,
accessToken: tokenData.access_token,
locationId: tokenData.location_id ?? null,
};
}
export async function persistSquareToken(args: {
brandId: string;
accessToken: string;
locationId: string | null;
}): Promise<{ ok: true } | { ok: false; error: string }> {
try {
await pool.query(
"SELECT upsert_payment_settings($1, $2, $3, $4, $5, $6, $7, $8, $9)",
[
args.brandId,
"square",
null,
null,
null,
args.accessToken,
args.locationId,
null,
null,
]
);
return { ok: true };
} catch {
return { ok: false, error: "square_token_save_error" };
}
}
-82
View File
@@ -1,82 +0,0 @@
/**
* Stripe OAuth token-exchange helper.
*
* Extracted from the `/api/stripe/oauth/callback` route so the GET handler
* stays a thin redirect layer — no `fetch(..., { method: "POST" })` or DB
* writes live in the route file. The OAuth `code` is single-use at the
* provider, which makes this write idempotent on replay.
*/
import { savePaymentSettings } from "@/actions/payments";
export type StripeTokenExchangeResult =
| {
ok: true;
accessToken: string;
publishableKey: string | null;
stripeUserId: string;
}
| { ok: false; error: string };
export async function exchangeStripeCodeForToken(args: {
code: string;
}): Promise<StripeTokenExchangeResult> {
const clientId = process.env.STRIPE_CLIENT_ID;
const clientSecret = process.env.STRIPE_CLIENT_SECRET;
if (!clientId || !clientSecret) {
return { ok: false, error: "oauth_not_configured" };
}
const tokenResponse = await fetch("https://connect.stripe.com/oauth/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: "authorization_code",
code: args.code,
client_id: clientId,
client_secret: clientSecret,
}),
});
const tokenData = (await tokenResponse.json().catch(() => ({}))) as {
error?: string;
error_description?: string;
access_token?: string;
stripe_user_id?: string;
stripe_publishable_key?: string;
};
if (tokenData.error || !tokenData.access_token) {
return {
ok: false,
error: tokenData.error_description || tokenData.error || "exchange_failed",
};
}
return {
ok: true,
accessToken: tokenData.access_token,
publishableKey: tokenData.stripe_publishable_key ?? null,
stripeUserId: tokenData.stripe_user_id ?? "",
};
}
export async function persistStripeCredentials(args: {
brandId: string;
accessToken: string;
publishableKey: string | null;
stripeUserId: string;
}): Promise<{ ok: true } | { ok: false; error: string }> {
const result = await savePaymentSettings({
brandId: args.brandId,
provider: "stripe",
stripePublishableKey: args.publishableKey ?? undefined,
stripeSecretKey: args.accessToken,
stripeUserId: args.stripeUserId,
});
return result.success
? { ok: true }
: { ok: false, error: result.error || "save_failed" };
}
-59
View File
@@ -1,59 +0,0 @@
/**
* Route Trace data fetching utilities
* Shared data fetching for all route-trace pages
*/
import {
getRouteTraceStats,
getRouteTraceLots,
getHarvestLotsReadyToHaul,
getFieldYieldSummary,
getInventoryByCrop,
getRecentLotEvents,
type RouteTraceStats,
type HaulingLot,
type FieldYieldSummary,
type InventoryByCrop,
type RecentLotEvent,
} from "@/actions/route-trace/lots";
export type RouteTraceData = {
stats: RouteTraceStats;
lots: HaulingLot[];
haulingLots: HaulingLot[];
fieldYield: FieldYieldSummary[];
inventoryByCrop: InventoryByCrop[];
recentActivity: RecentLotEvent[];
};
/**
* Fetch all route-trace data for a brand
*/
export async function fetchRouteTraceData(
brandId: string
): Promise<RouteTraceData> {
const [statsResult, lotsResult, haulingResult, yieldResult, invResult, eventsResult] = await Promise.all([
getRouteTraceStats(brandId),
getRouteTraceLots(brandId),
getHarvestLotsReadyToHaul(brandId),
getFieldYieldSummary(brandId),
getInventoryByCrop(brandId),
getRecentLotEvents(brandId, 10),
]);
return {
stats: statsResult.success ? statsResult.stats : {
active_count: 0,
in_transit_count: 0,
at_shed_count: 0,
total_lots_today: 0,
total_harvested_today: 0,
total_lots: 0,
},
lots: lotsResult.success ? lotsResult.lots : [],
haulingLots: haulingResult.success ? haulingResult.lots : [],
fieldYield: yieldResult.success ? yieldResult.summary : [],
inventoryByCrop: invResult.success ? invResult.inventory : [],
recentActivity: eventsResult.success ? eventsResult.events : [],
};
}
-11
View File
@@ -1,11 +0,0 @@
export const YIELD_UNIT_OPTIONS = [
{ value: "lbs", label: "lbs", abbr: "lbs" },
{ value: "bushel", label: "Bushel", abbr: "bu" },
{ value: "box", label: "Box", abbr: "bx" },
{ value: "sack", label: "Sack", abbr: "sk" },
{ value: "crate", label: "Crate", abbr: "cr" },
{ value: "bin", label: "Bin", abbr: "bn" },
{ value: "pallet", label: "Pallet", abbr: "plt" },
{ value: "custom", label: "Custom", abbr: "" },
] as const;
export type YieldUnit = typeof YIELD_UNIT_OPTIONS[number]["value"];
-40
View File
@@ -1,40 +0,0 @@
"use client";
import DOMPurify from "dompurify";
/**
* Render a sanitized HTML string as React children. Use this anywhere
* `dangerouslySetInnerHTML` would otherwise be the only option (e.g.
* email preview bodies, template bodies, etc.).
*
* DOMPurify is configured with a strict default profile plus a small
* allow-list for the elements commonly used in our email templates
* (inline styles, tables, images with http(s) sources). Custom config
* can be passed via the second argument if needed.
*/
export function SafeHtml({
html,
className,
...rest
}: {
html: string;
className?: string;
} & React.HTMLAttributes<HTMLDivElement>) {
const clean = DOMPurify.sanitize(html, {
USE_PROFILES: { html: true },
ALLOWED_ATTR: [
"href", "target", "rel", "src", "alt", "title", "style", "class",
"id", "name", "colspan", "rowspan", "width", "height", "align",
"valign", "bgcolor", "color", "border", "cellpadding", "cellspacing",
"role", "aria-label", "aria-hidden",
],
});
return (
<div
className={className}
// The sanitizer above guarantees the result is safe to inject.
dangerouslySetInnerHTML={{ __html: clean }}
{...rest}
/>
);
}
-218
View File
@@ -1,218 +0,0 @@
// SEO and Metadata Utilities
import { Metadata } from "next";
// Base URL for the application
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://routecommerce.com";
// Default SEO configuration
export const defaultSEO: Metadata = {
metadataBase: new URL(BASE_URL),
title: {
default: "Route Commerce | Fresh Produce Wholesale Platform",
template: "%s | Route Commerce",
},
description: "Multi-tenant B2B e-commerce platform for fresh produce wholesale distribution. Brands sell to customers who pick up at scheduled stops or receive shipments.",
keywords: [
"wholesale produce",
"farm fresh",
"B2B e-commerce",
"produce distribution",
"pickup stops",
"fresh produce",
"farm management",
"order management",
"customer portal",
"wholesale ordering",
],
authors: [{ name: "Route Commerce" }],
creator: "Route Commerce",
publisher: "Route Commerce",
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
openGraph: {
type: "website",
locale: "en_US",
url: BASE_URL,
siteName: "Route Commerce",
title: "Route Commerce | Fresh Produce Wholesale Platform",
description: "Multi-tenant B2B e-commerce platform for fresh produce wholesale distribution.",
images: [
{
url: "/og-image.png",
width: 1200,
height: 630,
alt: "Route Commerce Platform",
},
],
},
twitter: {
card: "summary_large_image",
title: "Route Commerce | Fresh Produce Wholesale Platform",
description: "Multi-tenant B2B e-commerce platform for fresh produce wholesale distribution.",
site: "@RouteCommerce",
creator: "@RouteCommerce",
images: ["/og-image.png"],
},
icons: {
icon: [
{ url: "/favicon.svg", type: "image/svg+xml" },
],
apple: "/icons/apple-touch-icon.png",
other: [
{
rel: "manifest",
url: "/manifest.json",
},
],
},
alternates: {
canonical: BASE_URL,
languages: {
"en-US": BASE_URL,
},
},
};
// Brand-specific metadata
export function getBrandMetadata(brand: {
name: string;
slug: string;
description?: string;
logo_url?: string;
}) {
return {
title: `${brand.name} | Fresh Produce`,
description: brand.description || `Order fresh produce from ${brand.name}. Pick up at scheduled stops or get deliveries.`,
openGraph: {
title: `${brand.name} | Fresh Produce`,
description: brand.description || `Order fresh produce from ${brand.name}`,
url: `${BASE_URL}/${brand.slug}`,
siteName: brand.name,
images: brand.logo_url ? [{ url: brand.logo_url }] : [],
},
};
}
// Product metadata
export function getProductMetadata(product: {
name: string;
description?: string;
price?: number;
image_url?: string;
}) {
return {
title: `${product.name} | Route Commerce`,
description: product.description || `Order ${product.name} from Route Commerce`,
openGraph: {
title: product.name,
description: product.description,
images: product.image_url ? [{ url: product.image_url }] : [],
},
};
}
// Admin page metadata
export function getAdminMetadata(pageTitle: string) {
return {
title: `${pageTitle} | Admin | Route Commerce`,
robots: {
index: false,
follow: false,
},
};
}
// Structured data (JSON-LD)
export function getOrganizationSchema() {
return {
"@context": "https://schema.org",
"@type": "Organization",
name: "Route Commerce",
url: BASE_URL,
logo: `${BASE_URL}/logo.svg`,
sameAs: [
"https://twitter.com/RouteCommerce",
"https://linkedin.com/company/route-commerce",
],
contactPoint: {
"@type": "ContactPoint",
contactType: "customer service",
email: "support@routecommerce.com",
},
};
}
export function getProductSchema(product: {
name: string;
description: string;
price: number;
currency?: string;
availability?: string;
image?: string;
brand?: string;
}) {
return {
"@context": "https://schema.org",
"@type": "Product",
name: product.name,
description: product.description,
image: product.image,
brand: {
"@type": "Brand",
name: product.brand || "Route Commerce",
},
offers: {
"@type": "Offer",
price: product.price,
priceCurrency: product.currency || "USD",
availability: product.availability || "https://schema.org/InStock",
},
};
}
export function getLocalBusinessSchema(business: {
name: string;
address: string;
city: string;
state: string;
postalCode: string;
phone?: string;
email?: string;
openingHours?: string[];
}) {
return {
"@context": "https://schema.org",
"@type": "LocalBusiness",
name: business.name,
address: {
"@type": "PostalAddress",
streetAddress: business.address,
addressLocality: business.city,
addressRegion: business.state,
postalCode: business.postalCode,
},
telephone: business.phone,
email: business.email,
openingHours: business.openingHours,
};
}
// Generate sitemap data
export function generateSitemap(pages: { url: string; lastModified?: Date; changeFrequency?: string; priority?: number }[]) {
return pages.map((page) => ({
url: `${BASE_URL}${page.url}`,
lastModified: page.lastModified || new Date(),
changeFrequency: page.changeFrequency || "weekly",
priority: page.priority || 0.5,
}));
}
-16
View File
@@ -1,16 +0,0 @@
/**
* Safe service-headers for Vercel Edge Runtime.
*
* Supabase REST accepts `apikey` as a standalone auth header — no
* Authorization: Bearer needed. This avoids all JWT header validation
* issues on Vercel Edge Runtime where +, /, = cause "Headers.append:
* ...is an invalid header value".
*
* The apikey header alone is sufficient for service-role access to
* the Supabase REST API.
*/
export function svcHeaders(serviceKey: string): Record<string, string> {
return {
apikey: serviceKey,
};
}
-337
View File
@@ -1,337 +0,0 @@
// Zod schemas for API validation across the application
import { z } from "zod";
// Common validation patterns
const uuidSchema = z.uuid();
const emailSchema = z.email();
const phoneSchema = z.string().regex(/^\+?[\d\s-()]+$/, "Invalid phone number");
const urlSchema = z.url().optional();
// Pagination
export const paginationSchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().positive().max(100).default(20),
});
// Brand/Admin User
export const brandIdSchema = z.object({
brand_id: z.uuid().optional(),
});
// Orders
export const createOrderSchema = z.object({
brand_id: uuidSchema,
customer_email: emailSchema.optional(),
customer_name: z.string().min(1).max(255).optional(),
customer_phone: phoneSchema.optional(),
customer_address: z.string().optional(),
notes: z.string().max(1000).optional(),
items: z.array(z.object({
product_id: uuidSchema,
quantity: z.number().int().positive(),
price: z.number().positive(),
fulfillment: z.enum(["pickup", "ship"]).default("pickup"),
})).min(1),
fulfillment: z.enum(["pickup", "ship", "mixed"]).optional(),
stop_id: uuidSchema.optional(),
});
export const updateOrderSchema = z.object({
order_id: uuidSchema,
status: z.enum(["pending", "confirmed", "preparing", "ready", "completed", "cancelled"]).optional(),
notes: z.string().max(1000).optional(),
tracking_number: z.string().optional(),
payment_status: z.enum(["pending", "paid", "refunded", "failed"]).optional(),
});
export const orderFiltersSchema = z.object({
brand_id: uuidSchema.optional(),
status: z.enum(["pending", "confirmed", "preparing", "ready", "completed", "cancelled"]).optional(),
fulfillment: z.enum(["pickup", "ship", "mixed"]).optional(),
date_from: z.iso.datetime().optional(),
date_to: z.iso.datetime().optional(),
customer_email: emailSchema.optional(),
stop_id: uuidSchema.optional(),
});
// Products
export const createProductSchema = z.object({
brand_id: uuidSchema,
name: z.string().min(1).max(255),
description: z.string().max(2000).optional(),
price: z.number().positive(),
unit: z.string().min(1).max(50).optional(),
category: z.string().max(100).optional(),
sku: z.string().max(100).optional(),
is_active: z.boolean().default(true),
is_taxable: z.boolean().default(true),
inventory_quantity: z.number().int().nonnegative().optional(),
image_url: urlSchema.optional(),
});
export const updateProductSchema = z.object({
product_id: uuidSchema,
name: z.string().min(1).max(255).optional(),
description: z.string().max(2000).optional(),
price: z.number().positive().optional(),
unit: z.string().min(1).max(50).optional(),
category: z.string().max(100).optional(),
sku: z.string().max(100).optional(),
is_active: z.boolean().optional(),
is_taxable: z.boolean().optional(),
inventory_quantity: z.number().int().nonnegative().optional(),
image_url: urlSchema.optional(),
});
export const productFiltersSchema = z.object({
brand_id: uuidSchema.optional(),
category: z.string().optional(),
is_active: z.boolean().optional(),
search: z.string().optional(),
min_price: z.number().positive().optional(),
max_price: z.number().positive().optional(),
});
// Stops
export const createStopSchema = z.object({
brand_id: uuidSchema,
name: z.string().min(1).max(255),
address: z.string().min(1),
city: z.string().max(100).optional(),
state: z.string().max(50).optional(),
postal_code: z.string().max(20).optional(),
country: z.string().max(100).optional(),
scheduled_at: z.iso.datetime(),
notes: z.string().max(1000).optional(),
product_ids: z.array(uuidSchema).optional(),
});
export const updateStopSchema = z.object({
stop_id: uuidSchema,
name: z.string().min(1).max(255).optional(),
address: z.string().min(1).optional(),
city: z.string().max(100).optional(),
state: z.string().max(50).optional(),
postal_code: z.string().max(20).optional(),
scheduled_at: z.iso.datetime().optional(),
status: z.enum(["scheduled", "in_progress", "completed", "cancelled"]).optional(),
notes: z.string().max(1000).optional(),
product_ids: z.array(uuidSchema).optional(),
});
export const stopFiltersSchema = z.object({
brand_id: uuidSchema.optional(),
status: z.enum(["scheduled", "in_progress", "completed", "cancelled"]).optional(),
date_from: z.iso.datetime().optional(),
date_to: z.iso.datetime().optional(),
});
// Communication Campaigns
export const createCampaignSchema = z.object({
brand_id: uuidSchema,
name: z.string().min(1).max(255),
subject: z.string().min(1).max(500),
template_id: uuidSchema.optional(),
content: z.string().min(1),
type: z.enum(["email", "sms"]).default("email"),
segment_id: uuidSchema.optional(),
contact_ids: z.array(uuidSchema).optional(),
scheduled_at: z.iso.datetime().optional(),
});
export const updateCampaignSchema = z.object({
campaign_id: uuidSchema,
name: z.string().min(1).max(255).optional(),
subject: z.string().min(1).max(500).optional(),
content: z.string().min(1).optional(),
status: z.enum(["draft", "scheduled", "sending", "sent", "cancelled"]).optional(),
scheduled_at: z.iso.datetime().optional(),
});
// Communication Contacts
export const createContactSchema = z.object({
brand_id: uuidSchema,
email: emailSchema,
phone: phoneSchema.optional(),
first_name: z.string().max(100).optional(),
last_name: z.string().max(100).optional(),
company: z.string().max(255).optional(),
tags: z.array(z.string().max(50)).optional(),
email_opt_in: z.boolean().default(true),
sms_opt_in: z.boolean().default(false),
});
export const updateContactSchema = z.object({
contact_id: uuidSchema,
email: emailSchema.optional(),
phone: phoneSchema.optional(),
first_name: z.string().max(100).optional(),
last_name: z.string().max(100).optional(),
company: z.string().max(255).optional(),
tags: z.array(z.string().max(50)).optional(),
email_opt_in: z.boolean().optional(),
sms_opt_in: z.boolean().optional(),
notes: z.string().max(2000).optional(),
});
// Communication Templates
export const createTemplateSchema = z.object({
brand_id: uuidSchema,
name: z.string().min(1).max(255),
subject: z.string().min(1).max(500).optional(),
content: z.string().min(1),
type: z.enum(["email", "sms"]).default("email"),
category: z.string().max(100).optional(),
});
export const updateTemplateSchema = z.object({
template_id: uuidSchema,
name: z.string().min(1).max(255).optional(),
subject: z.string().min(1).max(500).optional(),
content: z.string().min(1).optional(),
category: z.string().max(100).optional(),
is_active: z.boolean().optional(),
});
// Water Log
export const createWaterLogSchema = z.object({
brand_id: uuidSchema,
field_id: uuidSchema.optional(),
field_name: z.string().max(255).optional(),
gallons: z.number().positive(),
duration_minutes: z.number().int().nonnegative().optional(),
notes: z.string().max(500).optional(),
logged_at: z.iso.datetime().optional(),
});
export const updateWaterLogSchema = z.object({
log_id: uuidSchema,
gallons: z.number().positive().optional(),
duration_minutes: z.number().int().nonnegative().optional(),
notes: z.string().max(500).optional(),
});
export const waterLogFiltersSchema = z.object({
brand_id: uuidSchema.optional(),
field_id: uuidSchema.optional(),
date_from: z.iso.datetime().optional(),
date_to: z.iso.datetime().optional(),
});
// Wholesale
export const createWholesaleOrderSchema = z.object({
brand_id: uuidSchema,
customer_id: uuidSchema,
items: z.array(z.object({
product_id: uuidSchema,
quantity: z.number().int().positive(),
price: z.number().positive(),
})).min(1),
pickup_stop_id: uuidSchema.optional(),
notes: z.string().max(1000).optional(),
deposit_amount: z.number().nonnegative().optional(),
payment_method: z.enum(["stripe", "square", "invoice"]).default("invoice"),
});
export const updateWholesaleCustomerSchema = z.object({
customer_id: uuidSchema,
company_name: z.string().max(255).optional(),
contact_name: z.string().max(255).optional(),
email: emailSchema.optional(),
phone: phoneSchema.optional(),
credit_limit: z.number().nonnegative().optional(),
payment_terms: z.enum(["prepay", "net15", "net30", "net60"]).optional(),
is_active: z.boolean().optional(),
});
// Billing / Subscriptions
export const createSubscriptionSchema = z.object({
brand_id: uuidSchema,
plan_tier: z.enum(["starter", "farm", "enterprise"]),
interval: z.enum(["monthly", "annual"]).default("monthly"),
payment_method_id: z.string().optional(),
});
export const updateSubscriptionSchema = z.object({
subscription_id: z.string(),
plan_tier: z.enum(["starter", "farm", "enterprise"]).optional(),
interval: z.enum(["monthly", "annual"]).optional(),
status: z.enum(["active", "cancelled", "past_due", "trialing"]).optional(),
});
// Admin Users
export const createAdminUserSchema = z.object({
email: emailSchema,
role: z.enum(["platform_admin", "brand_admin", "store_employee"]),
brand_id: uuidSchema.optional(),
can_manage_products: z.boolean().default(false),
can_manage_stops: z.boolean().default(false),
can_manage_orders: z.boolean().default(false),
can_manage_pickup: z.boolean().default(false),
can_manage_messages: z.boolean().default(false),
can_manage_refunds: z.boolean().default(false),
can_manage_users: z.boolean().default(false),
can_manage_water_log: z.boolean().default(false),
can_manage_reports: z.boolean().default(false),
can_manage_settings: z.boolean().default(false),
});
export const updateAdminUserSchema = z.object({
user_id: uuidSchema,
role: z.enum(["platform_admin", "brand_admin", "store_employee"]).optional(),
can_manage_products: z.boolean().optional(),
can_manage_stops: z.boolean().optional(),
can_manage_orders: z.boolean().optional(),
can_manage_pickup: z.boolean().optional(),
can_manage_messages: z.boolean().optional(),
can_manage_refunds: z.boolean().optional(),
can_manage_users: z.boolean().optional(),
can_manage_water_log: z.boolean().optional(),
can_manage_reports: z.boolean().optional(),
can_manage_settings: z.boolean().optional(),
active: z.boolean().optional(),
});
// Reports
export const reportFiltersSchema = z.object({
brand_id: uuidSchema.optional(),
start_date: z.iso.datetime(),
end_date: z.iso.datetime(),
group_by: z.enum(["day", "week", "month"]).default("day"),
});
// Feature Flags
export const featureToggleSchema = z.object({
brand_id: uuidSchema,
feature_key: z.string().min(1),
enabled: z.boolean(),
});
// Referral
export const referralSchema = z.object({
referrer_id: uuidSchema,
referral_code: z.string().min(6).max(50),
referred_email: emailSchema,
campaign_id: z.string().optional(),
});
// Cart
export const cartItemSchema = z.object({
product_id: uuidSchema,
quantity: z.number().int().positive(),
fulfillment: z.enum(["pickup", "ship"]).default("pickup"),
stop_id: uuidSchema.optional(),
});
// Checkout
export const checkoutSchema = z.object({
items: z.array(cartItemSchema),
customer_email: emailSchema,
customer_name: z.string().min(1).max(255),
customer_phone: phoneSchema.optional(),
customer_address: z.string().optional(),
notes: z.string().max(1000).optional(),
payment_method: z.enum(["stripe", "square"]).default("stripe"),
promo_code: z.string().optional(),
});