Files
route-commerce/src/actions/route-trace/lots.ts
T
tyler 67abcaa2db 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.
2026-06-07 05:26:03 +00:00

347 lines
11 KiB
TypeScript

"use server";
import { getAdminUser } from "@/lib/admin-permissions";
import { getActiveBrandId, assertBrandAccess } from "@/lib/brand-scope";
// TODO(migration): the `harvest_lots` / `harvest_lot_events` / `lot_orders`
// tables from the route-trace feature (defined across
// supabase/migrations/143_enable_route_trace.sql and friends) were retired
// from the SaaS rebuild — they don't exist in `db/schema/`. The functions
// below all return empty results so the public trace page and FSMA reports
// 404 gracefully. If route-trace comes back, re-introduce the tables in
// `db/schema/` and replace these stubs with real Drizzle queries.
export interface HarvestLot {
id: string;
brand_id: string;
lot_number: string;
crop_type: string;
variety: string | null;
harvest_date: string;
field_location: string | null;
worker_name: string | null;
packer_name: string | null;
quantity_lbs: number | null;
quantity_used_lbs: number | null;
status: "active" | "in_transit" | "at_shed" | "packed" | "delivered";
notes: string | null;
source_stop_id: string | null;
destination_stop_id: string | null;
created_at: string;
updated_at: string;
bin_id: string | null;
container_id: string | null;
field_block: string | null;
pallets: number | null;
yield_estimate_lbs: number | null;
yield_unit: string | null;
}
export interface LotEvent {
id: string;
event_type: string;
event_time: string;
location: string | null;
notes: string | null;
created_by_name: string | null;
created_at: string;
bin_id: string | null;
}
export interface LotDetail {
lot_id: string;
lot_number: string;
crop_type: string;
variety: string | null;
harvest_date: string;
field_location: string | null;
worker_name: string | null;
packer_name: string | null;
quantity_lbs: number | null;
quantity_used_lbs: number | null;
status: string;
notes: string | null;
source_stop_id: string | null;
destination_stop_id: string | null;
created_at: string;
updated_at: string;
bin_id: string | null;
container_id: string | null;
field_block: string | null;
pallets: number | null;
yield_estimate_lbs: number | null;
yield_unit: string | null;
events: LotEvent[];
}
export interface RouteTraceStats {
active_count: number;
in_transit_count: number;
at_shed_count: number;
total_lots_today: number;
total_harvested_today: number;
total_lots: number;
}
export interface RecentLotEvent {
event_id: string;
event_type: string;
event_time: string;
location: string | null;
bin_id: string | null;
notes: string | null;
created_by_name: string | null;
lot_id: string;
lot_number: string;
crop_type: string;
status: string;
}
export interface LotOrder {
id: string;
customer_name: string;
order_date: string;
stop_name: string;
item_quantity: number | null;
item_notes: string | null;
fulfillment: string | null;
lot_quantity_used: number | null;
}
export interface TraceChain {
lot: HarvestLot;
events: LotEvent[];
orders: Array<{ id: string; customer_name: string; order_date: string; stop_name: string }>;
}
export interface HaulingLot {
lot_id: string;
lot_number: string;
crop_type: string;
harvest_date: string;
status: string;
field_location: string | null;
worker_name: string | null;
quantity_lbs: number | null;
yield_unit: string | null;
bin_id: string | null;
container_id: string | null;
field_block: string | null;
pallets: number | null;
destination_stop_id: string | null;
destination_stop_name: string | null;
destination_stop_time: string | null;
}
export interface FieldYieldSummary {
field_location: string;
field_block: string;
total_yield_estimate: number;
total_quantity_lbs: number;
active_lots: number;
yield_unit: string | null;
}
// Discriminated unions so TypeScript narrows correctly for consumer patterns:
// - `result.success ? result.X : defaultValue` (X is defined on success)
// - `if (result.success && result.X) { ... } else { result.error }` (error readable in else)
type LotsResult =
| { success: true; lots: HaulingLot[]; error: null }
| { success: false; lots: null; error: string };
type HarvestLotsResult =
| { success: true; lots: HarvestLot[]; error: null }
| { success: false; lots: null; error: string };
type StatsResult =
| { success: true; stats: RouteTraceStats; error: null }
| { success: false; stats: null; error: string };
type LotDetailResult =
| { success: true; lot: LotDetail; error: null }
| { success: false; lot: null; error: string };
type CreatedLotResult =
| { success: true; lot: HarvestLot; error: null }
| { success: false; lot: null; error: string };
type UpdatedLotResult =
| { success: true; lot: HarvestLot | null; error: null }
| { success: false; lot: null; error: string };
type InventoryResult =
| { success: true; inventory: InventoryByCrop[]; error: null }
| { success: false; inventory: null; error: string };
type YieldResult =
| { success: true; summary: FieldYieldSummary[]; error: null }
| { success: false; summary: null; error: string };
type EventsResult =
| { success: true; events: RecentLotEvent[]; error: null }
| { success: false; events: null; error: string };
type OrdersResult =
| { success: true; orders: LotOrder[]; error: null }
| { success: false; orders: null; error: string };
type ChainResult =
| { success: true; chain: TraceChain; error: null }
| { success: false; chain: null; error: string };
async function assertCanAccessBrand(brandId: string): Promise<
{ ok: true; adminUser: NonNullable<Awaited<ReturnType<typeof getAdminUser>>> } | { ok: false; error: string }
> {
const adminUser = await getAdminUser();
if (!adminUser) return { ok: false, error: "Unauthorized" };
const activeBrandId = await getActiveBrandId(adminUser, brandId);
if (!activeBrandId && adminUser.role !== "platform_admin") {
return { ok: false, error: "Brand access required" };
}
if (activeBrandId) {
try {
assertBrandAccess(adminUser, activeBrandId);
} catch {
return { ok: false, error: "Brand access denied" };
}
}
return { ok: true, adminUser };
}
export async function getRouteTraceLots(brandId: string, _status?: string): Promise<LotsResult> {
const auth = await assertCanAccessBrand(brandId);
if (!auth.ok) return { success: false, lots: null, error: auth.error };
return { success: true, lots: [], error: null };
}
export async function getRouteTraceLotDetail(_lotId: string): Promise<LotDetailResult> {
const adminUser = await getAdminUser();
if (!adminUser) return { success: false, lot: null, error: "Unauthorized" };
return { success: false, lot: null, error: "Route-trace feature not configured" };
}
export interface CreateLotData {
crop_type: string;
variety?: string;
harvest_date: string;
field_location?: string;
worker_name?: string;
packer_name?: string;
quantity_lbs?: number;
notes?: string;
destination_stop_id?: string;
bin_id?: string;
container_id?: string;
field_block?: string;
yield_estimate_lbs?: number;
yield_unit?: string;
pallets?: number;
}
export async function createHarvestLot(
brandId: string,
_data: CreateLotData
): Promise<CreatedLotResult> {
const auth = await assertCanAccessBrand(brandId);
if (!auth.ok) return { success: false, lot: null, error: auth.error };
return { success: false, lot: null, error: "Route-trace feature not configured" };
}
export async function updateHarvestLotStatus(
_lotId: string,
_status: string,
_location?: string,
_notes?: string,
_binId?: string
): Promise<UpdatedLotResult> {
const adminUser = await getAdminUser();
if (!adminUser) return { success: false, lot: null, error: "Unauthorized" };
return { success: false, lot: null, error: "Route-trace feature not configured" };
}
export async function getRouteTraceStats(brandId: string): Promise<StatsResult> {
const auth = await assertCanAccessBrand(brandId);
if (!auth.ok) return { success: false, stats: null, error: auth.error };
return {
success: true,
stats: {
active_count: 0,
in_transit_count: 0,
at_shed_count: 0,
total_lots_today: 0,
total_harvested_today: 0,
total_lots: 0,
},
error: null,
};
}
export async function searchHarvestLots(brandId: string, _query: string): Promise<HarvestLotsResult> {
const auth = await assertCanAccessBrand(brandId);
if (!auth.ok) return { success: false, lots: null, error: auth.error };
return { success: true, lots: [], error: null };
}
export async function getTraceChain(_lotId: string): Promise<ChainResult> {
const adminUser = await getAdminUser();
if (!adminUser) return { success: false, chain: null, error: "Unauthorized" };
return { success: false, chain: null, error: "Route-trace feature not configured" };
}
export async function getHarvestLotsReadyToHaul(brandId: string): Promise<LotsResult> {
const auth = await assertCanAccessBrand(brandId);
if (!auth.ok) return { success: false, lots: null, error: auth.error };
return { success: true, lots: [], error: null };
}
export async function getFieldYieldSummary(brandId: string): Promise<YieldResult> {
const auth = await assertCanAccessBrand(brandId);
if (!auth.ok) return { success: false, summary: null, error: auth.error };
return { success: true, summary: [], error: null };
}
export async function getLotOrders(_lotId: string): Promise<OrdersResult> {
const adminUser = await getAdminUser();
if (!adminUser) return { success: false, orders: null, error: "Unauthorized" };
return { success: true, orders: [], error: null };
}
export interface InventoryByCrop {
crop_type: string;
status: string;
total_lbs: number;
total_estimate: number;
lot_count: number;
yield_unit: string | null;
}
export async function getInventoryByCrop(brandId: string): Promise<InventoryResult> {
const auth = await assertCanAccessBrand(brandId);
if (!auth.ok) return { success: false, inventory: null, error: auth.error };
return { success: true, inventory: [], error: null };
}
export async function markLotUsedInOrder(
_lotId: string,
_orderId: string,
_quantityToAdd?: number,
_notes?: string
): Promise<{ success: true } | { success: false; error: string }> {
const adminUser = await getAdminUser();
if (!adminUser) return { success: false, error: "Unauthorized" };
return { success: false, error: "Route-trace feature not configured" };
}
export async function getRecentLotEvents(
brandId: string,
_limit = 10
): Promise<EventsResult> {
const auth = await assertCanAccessBrand(brandId);
if (!auth.ok) return { success: false, events: null, error: auth.error };
return { success: true, events: [], error: null };
}
/**
* Look up a harvest lot by its public-facing lot number (e.g. "FL-2026-001").
* Returns the lot's UUID or `null` if no such lot exists.
*
* NOTE: the `harvest_lots` table is not in the new Drizzle schema (it's a
* niche traceability feature that was retired from the SaaS rebuild). This
* stub returns `null` so the public trace page gracefully 404s. If the
* feature comes back, re-introduce the table in `db/schema/` and replace
* this with a real Drizzle query.
*/
export async function getLotIdByNumber(_lotNumber: string): Promise<string | null> {
return null;
}