Files
Nora 0ac4beaaa8 fix: react-doctor errors → 0 errors, 1649 warnings (46/100)
- Add requireAuth() to admin-permissions.ts as recognized auth call
- Convert getAdminUser() → requireAuth() across 73 admin action files
- Add getSession() to public/wholesale server actions
- Fix multi-line return type corruption from earlier auto-fixers
- Move FedEx token cache to non-'use server' module
- Object.freeze module-level constants: PRICE_KEYS, EMPTY_MOBILE_DASHBOARD,
  EMPTY_PAY_PERIOD, LOCALE_CART_SUBJECT, WELCOME_EMAILS
- Update Stripe API version 2026-05-27 → 2026-06-24
- Fix wholesale employee portal: getEmployeeSessionAction + EmployeePortalClient
- Fix 51 TypeScript errors (return type corruption, missing imports)
2026-06-25 23:49:37 -06:00

748 lines
30 KiB
PL/PgSQL

-- Migration 046: Wholesale Portal Tables
-- Core wholesale ordering portal for Route Commerce.
-- Idempotent — all statements use IF NOT EXISTS / CREATE OR REPLACE.
BEGIN;
-- ── 1. Wholesale Customers ─────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS public.wholesale_customers (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID, -- FK added after auth.users exists
brand_id UUID NOT NULL REFERENCES brands(id) ON DELETE CASCADE,
company_name TEXT,
contact_name TEXT,
email TEXT,
phone TEXT,
billing_address TEXT,
shipping_address TEXT,
account_status TEXT NOT NULL DEFAULT 'active',
credit_limit NUMERIC(10,2) NOT NULL DEFAULT 0,
deposits_enabled BOOLEAN NOT NULL DEFAULT false,
deposit_threshold NUMERIC(10,2),
deposit_percentage INTEGER CHECK (deposit_percentage IS NULL OR deposit_percentage BETWEEN 1 AND 100),
order_email TEXT,
invoice_email TEXT,
admin_notes TEXT,
role TEXT NOT NULL DEFAULT 'buyer',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Add the FK after the table exists. Use DO block so it's idempotent.
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'wholesale_customers_user_id_fkey'
) THEN
ALTER TABLE public.wholesale_customers
ADD CONSTRAINT wholesale_customers_user_id_fkey
FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE SET NULL
NOT VALID;
END IF;
END $$;
-- Individual unique constraint on user_id (allows FK reference from other tables)
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'wholesale_customers_user_id_unique'
) THEN
ALTER TABLE public.wholesale_customers
ADD CONSTRAINT wholesale_customers_user_id_unique
UNIQUE (user_id);
END IF;
END $$;
-- Composite unique: one user + one brand (still useful for the upsert ON CONFLICT)
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'wholesale_customers_brand_user_unique'
) THEN
ALTER TABLE public.wholesale_customers
ADD CONSTRAINT wholesale_customers_brand_user_unique
UNIQUE (brand_id, user_id);
END IF;
END $$;
-- RLS
ALTER TABLE public.wholesale_customers ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "brand_admin_manage_wholesale_customers" ON wholesale_customers;
CREATE POLICY "brand_admin_manage_wholesale_customers" ON wholesale_customers
FOR ALL USING (
current_setting('app.settings.role', true)::TEXT IN ('brand_admin', 'platform_admin')
AND (
current_setting('app.settings.role', true)::TEXT = 'platform_admin'
OR brand_id = current_setting('app.settings.brand_id', true)::UUID
)
);
DROP POLICY IF EXISTS "wholesale_customer_read_own" ON wholesale_customers;
CREATE POLICY "wholesale_customer_read_own" ON wholesale_customers
FOR SELECT USING (
current_setting('app.settings.role', true)::TEXT = 'wholesale_customer'
AND user_id = current_setting('app.settings.user_id', true)::UUID
);
-- ── 2. Wholesale Products ───────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS public.wholesale_products (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
brand_id UUID NOT NULL REFERENCES brands(id) ON DELETE CASCADE,
rc_product_id UUID REFERENCES products(id) ON DELETE SET NULL,
name TEXT NOT NULL,
description TEXT,
unit_type TEXT NOT NULL DEFAULT 'each',
unit_type_custom TEXT,
availability TEXT NOT NULL DEFAULT 'unavailable',
qty_available NUMERIC(10,2) DEFAULT 0,
season_start DATE,
season_end DATE,
price_tiers JSONB NOT NULL DEFAULT '[]',
hp_sku TEXT,
hp_item_id TEXT,
internal_notes TEXT,
handling_instructions TEXT,
transport_temp TEXT,
storage_warning TEXT,
loading_notes TEXT,
product_label TEXT,
pack_style TEXT,
container_type TEXT,
container_size_code TEXT,
units_per_container INTEGER,
container_notes TEXT,
default_pickup_location TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE public.wholesale_products ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "brand_admin_manage_wholesale_products" ON wholesale_products;
CREATE POLICY "brand_admin_manage_wholesale_products" ON wholesale_products
FOR ALL USING (
current_setting('app.settings.role', true)::TEXT IN ('brand_admin', 'platform_admin')
AND (
current_setting('app.settings.role', true)::TEXT = 'platform_admin'
OR brand_id = current_setting('app.settings.brand_id', true)::UUID
)
);
-- ── 3. Wholesale Orders ────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS public.wholesale_orders (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
brand_id UUID NOT NULL REFERENCES brands(id) ON DELETE CASCADE,
customer_id UUID NOT NULL, -- FK added below after customers table exists
wc_order_id BIGINT,
status TEXT NOT NULL DEFAULT 'pending',
fulfillment_status TEXT NOT NULL DEFAULT 'unfulfilled',
payment_status TEXT NOT NULL DEFAULT 'unpaid',
anticipated_pickup_date DATE,
subtotal NUMERIC(10,2) NOT NULL DEFAULT 0,
deposit_required NUMERIC(10,2) DEFAULT 0,
deposit_paid NUMERIC(10,2) NOT NULL DEFAULT 0,
balance_due NUMERIC(10,2) NOT NULL DEFAULT 0,
assigned_employee_id UUID REFERENCES auth.users(id) ON DELETE SET NULL,
fulfillment_notes TEXT,
internal_notes TEXT,
invoice_number TEXT,
invoice_pdf_path TEXT,
invoice_token TEXT,
invoice_type TEXT,
deposit_percentage INTEGER,
fulfilled_at TIMESTAMPTZ,
fulfilled_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Add customer FK now that customers table definitely exists
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'wholesale_orders_customer_id_fkey'
) THEN
ALTER TABLE public.wholesale_orders
ADD CONSTRAINT wholesale_orders_customer_id_fkey
FOREIGN KEY (customer_id) REFERENCES wholesale_customers(id) ON DELETE RESTRICT
NOT VALID;
END IF;
END $$;
ALTER TABLE public.wholesale_orders ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "brand_admin_manage_wholesale_orders" ON wholesale_orders;
CREATE POLICY "brand_admin_manage_wholesale_orders" ON wholesale_orders
FOR ALL USING (
current_setting('app.settings.role', true)::TEXT IN ('brand_admin', 'platform_admin')
AND (
current_setting('app.settings.role', true)::TEXT = 'platform_admin'
OR brand_id = current_setting('app.settings.brand_id', true)::UUID
)
);
-- ── 4. Wholesale Order Items ───────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS public.wholesale_order_items (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
wholesale_order_id UUID NOT NULL, -- FK added below
product_id UUID NOT NULL, -- FK added below
quantity NUMERIC(10,2) NOT NULL,
unit_price NUMERIC(10,2) NOT NULL,
line_total NUMERIC(10,2) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'wholesale_order_items_order_id_fkey'
) THEN
ALTER TABLE public.wholesale_order_items
ADD CONSTRAINT wholesale_order_items_order_id_fkey
FOREIGN KEY (wholesale_order_id) REFERENCES wholesale_orders(id) ON DELETE CASCADE
NOT VALID;
END IF;
END $$;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'wholesale_order_items_product_id_fkey'
) THEN
ALTER TABLE public.wholesale_order_items
ADD CONSTRAINT wholesale_order_items_product_id_fkey
FOREIGN KEY (product_id) REFERENCES wholesale_products(id) ON DELETE RESTRICT
NOT VALID;
END IF;
END $$;
ALTER TABLE public.wholesale_order_items ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "brand_admin_manage_wholesale_order_items" ON wholesale_order_items;
CREATE POLICY "brand_admin_manage_wholesale_order_items" ON wholesale_order_items
FOR ALL USING (
current_setting('app.settings.role', true)::TEXT IN ('brand_admin', 'platform_admin')
);
-- ── 5. Deposit Transactions ─────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS public.wholesale_deposits (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
wholesale_order_id UUID NOT NULL, -- FK added below
amount NUMERIC(10,2) NOT NULL,
payment_method TEXT,
reference TEXT,
recorded_by UUID NOT NULL REFERENCES auth.users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'wholesale_deposits_order_id_fkey'
) THEN
ALTER TABLE public.wholesale_deposits
ADD CONSTRAINT wholesale_deposits_order_id_fkey
FOREIGN KEY (wholesale_order_id) REFERENCES wholesale_orders(id) ON DELETE CASCADE
NOT VALID;
END IF;
END $$;
ALTER TABLE public.wholesale_deposits ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "brand_admin_manage_deposits" ON wholesale_deposits;
CREATE POLICY "brand_admin_manage_deposits" ON wholesale_deposits
FOR ALL USING (
current_setting('app.settings.role', true)::TEXT IN ('brand_admin', 'platform_admin')
);
-- ── 6. Wholesale Settings ───────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS public.wholesale_settings (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
brand_id UUID NOT NULL UNIQUE REFERENCES brands(id) ON DELETE CASCADE,
portal_page_id UUID,
price_sheet_page_id UUID,
require_approval BOOLEAN NOT NULL DEFAULT true,
min_order_amount NUMERIC(10,2),
online_payment_enabled BOOLEAN NOT NULL DEFAULT false,
pickup_location TEXT,
fob_location TEXT,
from_email TEXT,
invoice_business_name TEXT,
last_invoice_number INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE public.wholesale_settings ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "brand_admin_manage_wholesale_settings" ON wholesale_settings;
CREATE POLICY "brand_admin_manage_wholesale_settings" ON wholesale_settings
FOR ALL USING (
current_setting('app.settings.role', true)::TEXT IN ('brand_admin', 'platform_admin')
AND (
current_setting('app.settings.role', true)::TEXT = 'platform_admin'
OR brand_id = current_setting('app.settings.brand_id', true)::UUID
)
);
DROP FUNCTION IF EXISTS public.get_wholesale_orders(UUID);
DROP FUNCTION IF EXISTS public.upsert_wholesale_settings(UUID,UUID,UUID,BOOLEAN,NUMERIC,BOOLEAN,TEXT,TEXT,TEXT,TEXT);
DROP FUNCTION IF EXISTS public.get_wholesale_settings(UUID);
DROP FUNCTION IF EXISTS public.get_wholesale_customers(UUID);
DROP FUNCTION IF EXISTS public.upsert_wholesale_customer(UUID,UUID,TEXT,TEXT,TEXT,TEXT,TEXT,NUMERIC,BOOLEAN,NUMERIC,INTEGER,TEXT,TEXT,TEXT,TEXT);
DROP FUNCTION IF EXISTS public.get_wholesale_products(UUID);
DROP FUNCTION IF EXISTS public.upsert_wholesale_product(UUID,UUID,TEXT,UUID,UUID,UUID,UUID,UUID,UUID,UUID,UUID,UUID,UUID,UUID,UUID,UUID,UUID,UUID,UUID,UUID,UUID,UUID,UUID);
DROP FUNCTION IF EXISTS public.create_wholesale_order(UUID,UUID,DATE,JSONB,INTEGER,TEXT);
DROP FUNCTION IF EXISTS public.record_wholesale_deposit(UUID,NUMERIC,TEXT,TEXT,UUID);
DROP FUNCTION IF EXISTS public.mark_wholesale_order_fulfilled(UUID,UUID);
-- ── 7. RPCs ─────────────────────────────────────────────────────────────────
CREATE OR REPLACE FUNCTION public.get_wholesale_orders(p_brand_id UUID DEFAULT NULL)
RETURNS JSONB
LANGUAGE plpgsql SECURITY DEFINER SET search_path = public
AS $$
DECLARE
v_result JSONB;
BEGIN
SELECT jsonb_agg(t ORDER BY t.created_at DESC) INTO v_result
FROM (
SELECT
wo.id, wo.status, wo.fulfillment_status, wo.payment_status,
wo.anticipated_pickup_date, wo.subtotal, wo.deposit_required,
wo.deposit_paid, wo.balance_due, wo.created_at, wo.updated_at,
wo.invoice_number, wo.assigned_employee_id,
wc.company_name, wc.contact_name, wc.email AS customer_email,
COALESCE(jsonb_agg(CASE WHEN woi.id IS NOT NULL THEN jsonb_build_object(
'id', woi.id,
'product_name', wp.name,
'quantity', woi.quantity,
'unit_price', woi.unit_price,
'line_total', woi.line_total
) END), '[]'::JSONB) AS items,
wo.fulfilled_at
FROM wholesale_orders wo
JOIN wholesale_customers wc ON wo.customer_id = wc.id
LEFT JOIN wholesale_order_items woi ON woi.wholesale_order_id = wo.id
LEFT JOIN wholesale_products wp ON woi.product_id = wp.id
WHERE wo.brand_id = p_brand_id
GROUP BY wo.id, wc.id
ORDER BY wo.created_at DESC
LIMIT 500
) t;
RETURN COALESCE(v_result, '[]'::JSONB);
END;
$$;
CREATE OR REPLACE FUNCTION public.upsert_wholesale_settings(
p_brand_id UUID DEFAULT NULL,
p_portal_page_id UUID DEFAULT NULL,
p_price_sheet_page_id UUID DEFAULT NULL,
p_require_approval BOOLEAN DEFAULT NULL,
p_min_order_amount NUMERIC DEFAULT NULL,
p_online_payment_enabled BOOLEAN DEFAULT NULL,
p_pickup_location TEXT DEFAULT NULL,
p_fob_location TEXT DEFAULT NULL,
p_from_email TEXT DEFAULT NULL,
p_invoice_business_name TEXT DEFAULT NULL
)
RETURNS JSONB
LANGUAGE plpgsql SECURITY DEFINER SET search_path = public
AS $$
BEGIN
INSERT INTO wholesale_settings (
brand_id, portal_page_id, price_sheet_page_id,
require_approval, min_order_amount, online_payment_enabled,
pickup_location, fob_location, from_email, invoice_business_name
)
VALUES (
p_brand_id, p_portal_page_id, p_price_sheet_page_id,
COALESCE(p_require_approval, true),
p_min_order_amount, COALESCE(p_online_payment_enabled, false),
p_pickup_location, p_fob_location, p_from_email, p_invoice_business_name
)
ON CONFLICT (brand_id) DO UPDATE SET
portal_page_id = COALESCE(p_portal_page_id, wholesale_settings.portal_page_id),
price_sheet_page_id = COALESCE(p_price_sheet_page_id, wholesale_settings.price_sheet_page_id),
require_approval = COALESCE(p_require_approval, wholesale_settings.require_approval),
min_order_amount = COALESCE(p_min_order_amount, wholesale_settings.min_order_amount),
online_payment_enabled = COALESCE(p_online_payment_enabled, wholesale_settings.online_payment_enabled),
pickup_location = COALESCE(p_pickup_location, wholesale_settings.pickup_location),
fob_location = COALESCE(p_fob_location, wholesale_settings.fob_location),
from_email = COALESCE(p_from_email, wholesale_settings.from_email),
invoice_business_name = COALESCE(p_invoice_business_name, wholesale_settings.invoice_business_name),
updated_at = now()
RETURNING jsonb_build_object('success', true);
END;
$$;
CREATE OR REPLACE FUNCTION public.get_wholesale_settings(p_brand_id UUID DEFAULT NULL)
RETURNS JSONB
LANGUAGE plpgsql SECURITY DEFINER SET search_path = public
AS $$
DECLARE
v_result JSONB;
BEGIN
SELECT jsonb_build_object(
'id', id,
'brand_id', brand_id,
'portal_page_id', portal_page_id,
'price_sheet_page_id', price_sheet_page_id,
'require_approval', require_approval,
'min_order_amount', min_order_amount,
'online_payment_enabled', online_payment_enabled,
'pickup_location', pickup_location,
'fob_location', fob_location,
'from_email', from_email,
'invoice_business_name', invoice_business_name,
'last_invoice_number', last_invoice_number
) INTO v_result
FROM wholesale_settings
WHERE brand_id = p_brand_id;
RETURN v_result;
END;
$$;
CREATE OR REPLACE FUNCTION public.get_wholesale_customers(p_brand_id UUID DEFAULT NULL)
RETURNS JSONB
LANGUAGE plpgsql SECURITY DEFINER SET search_path = public
AS $$
DECLARE
v_result JSONB;
BEGIN
SELECT jsonb_agg(t ORDER BY t.company_name NULLS LAST) INTO v_result
FROM (
SELECT
id, user_id, company_name, contact_name, email, phone,
account_status, credit_limit,
deposits_enabled, deposit_threshold, deposit_percentage,
order_email, invoice_email, admin_notes, role,
created_at
FROM wholesale_customers
WHERE brand_id = p_brand_id
ORDER BY company_name NULLS LAST
) t;
RETURN COALESCE(v_result, '[]'::JSONB);
END;
$$;
CREATE OR REPLACE FUNCTION public.upsert_wholesale_customer(
p_brand_id UUID DEFAULT NULL,
p_user_id UUID DEFAULT NULL,
p_company_name TEXT DEFAULT NULL,
p_contact_name TEXT DEFAULT NULL,
p_email TEXT DEFAULT NULL,
p_phone TEXT DEFAULT NULL,
p_account_status TEXT DEFAULT NULL,
p_credit_limit NUMERIC DEFAULT NULL,
p_deposits_enabled BOOLEAN DEFAULT NULL,
p_deposit_threshold NUMERIC DEFAULT NULL,
p_deposit_percentage INTEGER DEFAULT NULL,
p_order_email TEXT DEFAULT NULL,
p_invoice_email TEXT DEFAULT NULL,
p_admin_notes TEXT DEFAULT NULL,
p_role TEXT DEFAULT NULL
)
RETURNS JSONB
LANGUAGE plpgsql SECURITY DEFINER SET search_path = public
AS $$
DECLARE
v_id UUID;
BEGIN
INSERT INTO wholesale_customers (
brand_id, user_id, company_name, contact_name, email, phone,
account_status, credit_limit, deposits_enabled, deposit_threshold,
deposit_percentage, order_email, invoice_email, admin_notes, role
)
VALUES (
p_brand_id, p_user_id, p_company_name, p_contact_name, p_email, p_phone,
COALESCE(p_account_status, 'active'),
COALESCE(p_credit_limit, 0),
COALESCE(p_deposits_enabled, false),
p_deposit_threshold, p_deposit_percentage,
p_order_email, p_invoice_email, p_admin_notes,
COALESCE(p_role, 'buyer')
)
ON CONFLICT (brand_id, user_id) DO UPDATE SET
company_name = COALESCE(p_company_name, wholesale_customers.company_name),
contact_name = COALESCE(p_contact_name, wholesale_customers.contact_name),
email = COALESCE(p_email, wholesale_customers.email),
phone = COALESCE(p_phone, wholesale_customers.phone),
account_status = COALESCE(p_account_status, wholesale_customers.account_status),
credit_limit = COALESCE(p_credit_limit, wholesale_customers.credit_limit),
deposits_enabled = COALESCE(p_deposits_enabled, wholesale_customers.deposits_enabled),
deposit_threshold = COALESCE(p_deposit_threshold, wholesale_customers.deposit_threshold),
deposit_percentage= COALESCE(p_deposit_percentage, wholesale_customers.deposit_percentage),
order_email = COALESCE(p_order_email, wholesale_customers.order_email),
invoice_email = COALESCE(p_invoice_email, wholesale_customers.invoice_email),
admin_notes = COALESCE(p_admin_notes, wholesale_customers.admin_notes),
role = COALESCE(p_role, wholesale_customers.role),
updated_at = now()
RETURNING id INTO v_id;
RETURN jsonb_build_object('success', true, 'id', v_id);
END;
$$;
CREATE OR REPLACE FUNCTION public.get_wholesale_products(p_brand_id UUID DEFAULT NULL)
RETURNS JSONB
LANGUAGE plpgsql SECURITY DEFINER SET search_path = public
AS $$
DECLARE
v_result JSONB;
BEGIN
SELECT jsonb_agg(t ORDER BY t.name) INTO v_result
FROM (
SELECT
id, name, description, unit_type, unit_type_custom,
availability, qty_available, season_start, season_end,
price_tiers, hp_sku, hp_item_id,
handling_instructions, storage_warning, loading_notes,
product_label, pack_style, container_type, container_size_code,
units_per_container, default_pickup_location,
created_at
FROM wholesale_products
WHERE brand_id = p_brand_id
ORDER BY name
) t;
RETURN COALESCE(v_result, '[]'::JSONB);
END;
$$;
CREATE OR REPLACE FUNCTION public.upsert_wholesale_product(
p_brand_id UUID DEFAULT NULL,
p_id UUID DEFAULT NULL,
p_name TEXT DEFAULT NULL,
p_description TEXT DEFAULT NULL,
p_unit_type TEXT DEFAULT NULL,
p_unit_type_custom TEXT DEFAULT NULL,
p_availability TEXT DEFAULT NULL,
p_qty_available NUMERIC DEFAULT NULL,
p_season_start DATE DEFAULT NULL,
p_season_end DATE DEFAULT NULL,
p_price_tiers JSONB DEFAULT NULL,
p_hp_sku TEXT DEFAULT NULL,
p_hp_item_id TEXT DEFAULT NULL,
p_internal_notes TEXT DEFAULT NULL,
p_handling_instructions TEXT DEFAULT NULL,
p_storage_warning TEXT DEFAULT NULL,
p_loading_notes TEXT DEFAULT NULL,
p_product_label TEXT DEFAULT NULL,
p_pack_style TEXT DEFAULT NULL,
p_container_type TEXT DEFAULT NULL,
p_container_size_code TEXT DEFAULT NULL,
p_units_per_container INTEGER DEFAULT NULL,
p_default_pickup_location TEXT DEFAULT NULL
)
RETURNS JSONB
LANGUAGE plpgsql SECURITY DEFINER SET search_path = public
AS $$
DECLARE
v_id UUID;
BEGIN
IF p_id IS NOT NULL THEN
UPDATE wholesale_products SET
name = COALESCE(p_name, name),
description = COALESCE(p_description, description),
unit_type = COALESCE(p_unit_type, unit_type),
unit_type_custom = COALESCE(p_unit_type_custom, unit_type_custom),
availability = COALESCE(p_availability, availability),
qty_available = COALESCE(p_qty_available, qty_available),
season_start = COALESCE(p_season_start, season_start),
season_end = COALESCE(p_season_end, season_end),
price_tiers = COALESCE(p_price_tiers, price_tiers),
hp_sku = COALESCE(p_hp_sku, hp_sku),
hp_item_id = COALESCE(p_hp_item_id, hp_item_id),
internal_notes = COALESCE(p_internal_notes, internal_notes),
handling_instructions = COALESCE(p_handling_instructions, handling_instructions),
storage_warning = COALESCE(p_storage_warning, storage_warning),
loading_notes = COALESCE(p_loading_notes, loading_notes),
product_label = COALESCE(p_product_label, product_label),
pack_style = COALESCE(p_pack_style, pack_style),
container_type = COALESCE(p_container_type, container_type),
container_size_code = COALESCE(p_container_size_code, container_size_code),
units_per_container = COALESCE(p_units_per_container, units_per_container),
default_pickup_location = COALESCE(p_default_pickup_location, default_pickup_location),
updated_at = now()
WHERE id = p_id AND brand_id = p_brand_id
RETURNING id INTO v_id;
ELSE
INSERT INTO wholesale_products (
brand_id, name, description, unit_type, unit_type_custom,
availability, qty_available, season_start, season_end,
price_tiers, hp_sku, hp_item_id, internal_notes,
handling_instructions, storage_warning, loading_notes,
product_label, pack_style, container_type, container_size_code,
units_per_container, default_pickup_location
) VALUES (
p_brand_id, p_name, p_description,
COALESCE(p_unit_type, 'each'),
p_unit_type_custom,
COALESCE(p_availability, 'unavailable'),
COALESCE(p_qty_available, 0),
p_season_start, p_season_end,
COALESCE(p_price_tiers, '[]'::JSONB),
p_hp_sku, p_hp_item_id, p_internal_notes,
p_handling_instructions, p_storage_warning, p_loading_notes,
p_product_label, p_pack_style, p_container_type, p_container_size_code,
p_units_per_container, p_default_pickup_location
)
RETURNING id INTO v_id;
END IF;
RETURN jsonb_build_object('success', true, 'id', v_id);
END;
$$;
CREATE OR REPLACE FUNCTION public.create_wholesale_order(
p_brand_id UUID DEFAULT NULL,
p_customer_id UUID DEFAULT NULL,
p_anticipated_pickup_date DATE DEFAULT NULL,
p_items JSONB DEFAULT '[]'::JSONB,
p_deposit_percentage INTEGER DEFAULT NULL,
p_notes TEXT DEFAULT NULL
)
RETURNS JSONB
LANGUAGE plpgsql SECURITY DEFINER SET search_path = public
AS $$
DECLARE
v_order_id UUID;
v_subtotal NUMERIC(10,2);
v_dep_required NUMERIC(10,2) := 0;
v_customer RECORD;
v_settings RECORD;
v_next_inv INTEGER;
v_status TEXT;
BEGIN
-- Resolve customer deposit rules
SELECT deposits_enabled, deposit_threshold, deposit_percentage
INTO v_customer
FROM wholesale_customers
WHERE id = p_customer_id;
-- Resolve invoice counter
SELECT COALESCE(last_invoice_number, 0) INTO v_settings
FROM wholesale_settings
WHERE brand_id = p_brand_id;
-- Calculate subtotal
v_subtotal := COALESCE(
(SELECT SUM(
((item->>'quantity')::NUMERIC) * ((item->>'unit_price')::NUMERIC)
) FROM jsonb_array_elements(p_items) AS item),
0
);
-- Determine if deposit required
IF v_customer.deposits_enabled THEN
IF v_customer.deposit_threshold IS NULL OR v_subtotal >= v_customer.deposit_threshold THEN
v_dep_required := ROUND(v_subtotal * COALESCE(v_customer.deposit_percentage, 0) / 100.0, 2);
END IF;
END IF;
v_status := CASE WHEN v_dep_required > 0 THEN 'awaiting_deposit' ELSE 'pending' END;
v_next_inv := COALESCE(v_settings.last_invoice_number, 0) + 1;
INSERT INTO wholesale_orders (
brand_id, customer_id, status, anticipated_pickup_date,
subtotal, deposit_required, deposit_paid, balance_due,
deposit_percentage, internal_notes, invoice_number
) VALUES (
p_brand_id, p_customer_id, v_status, p_anticipated_pickup_date,
v_subtotal, v_dep_required, 0, v_subtotal,
p_deposit_percentage, p_notes,
'INV-' || LPAD(v_next_inv::TEXT, 5, '0')
) RETURNING id INTO v_order_id;
-- Insert line items
IF jsonb_array_length(p_items) > 0 THEN
INSERT INTO wholesale_order_items (wholesale_order_id, product_id, quantity, unit_price, line_total)
SELECT
v_order_id,
(item->>'product_id')::UUID,
(item->>'quantity')::NUMERIC,
(item->>'unit_price')::NUMERIC,
((item->>'quantity')::NUMERIC) * ((item->>'unit_price')::NUMERIC)
FROM jsonb_array_elements(p_items) AS item;
END IF;
-- Advance invoice counter
UPDATE wholesale_settings
SET last_invoice_number = v_next_inv, updated_at = now()
WHERE brand_id = p_brand_id;
RETURN jsonb_build_object(
'success', true,
'order_id', v_order_id,
'status', v_status,
'deposit_required', v_dep_required,
'subtotal', v_subtotal
);
END;
$$;
CREATE OR REPLACE FUNCTION public.record_wholesale_deposit(
p_order_id UUID DEFAULT NULL,
p_amount NUMERIC DEFAULT NULL,
p_method TEXT DEFAULT 'cash',
p_reference TEXT DEFAULT NULL,
p_recorded_by UUID DEFAULT NULL
)
RETURNS JSONB
LANGUAGE plpgsql SECURITY DEFINER SET search_path = public
AS $$
DECLARE
v_new_paid NUMERIC(10,2);
BEGIN
-- Compute new deposit_paid first so balance_due uses the correct updated value
SELECT deposit_paid + p_amount INTO v_new_paid
FROM wholesale_orders
WHERE id = p_order_id;
UPDATE wholesale_orders
SET
deposit_paid = v_new_paid,
balance_due = subtotal - v_new_paid,
updated_at = now()
WHERE id = p_order_id;
INSERT INTO wholesale_deposits (wholesale_order_id, amount, payment_method, reference, recorded_by)
VALUES (p_order_id, p_amount, p_method, p_reference, p_recorded_by);
-- Advance to pending once deposit covers requirement
UPDATE wholesale_orders
SET status = 'pending'
WHERE id = p_order_id
AND v_new_paid >= deposit_required
AND status = 'awaiting_deposit';
RETURN jsonb_build_object('success', true);
END;
$$;
CREATE OR REPLACE FUNCTION public.mark_wholesale_order_fulfilled(
p_order_id UUID DEFAULT NULL,
p_by UUID DEFAULT NULL
)
RETURNS JSONB
LANGUAGE plpgsql SECURITY DEFINER SET search_path = public
AS $$
BEGIN
UPDATE wholesale_orders
SET
fulfillment_status = 'fulfilled',
payment_status = CASE WHEN balance_due <= 0 THEN 'paid' ELSE payment_status END,
status = 'fulfilled',
fulfilled_at = now(),
fulfilled_by = p_by,
updated_at = now()
WHERE id = p_order_id;
RETURN jsonb_build_object('success', true);
END;
$$;
COMMIT;
NOTIFY pgrst, 'reload schema';