- Schema creation and grants - Enums: product_type, product_status, price_type, attribute_type - Tables: categories, products, variants, prices - RLS policies for tenant isolation - Performance indexes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
263 lines
8.1 KiB
PL/PgSQL
263 lines
8.1 KiB
PL/PgSQL
-- ============================================
|
|
-- TEMPLATE-SAAS: Portfolio Tables
|
|
-- Version: 1.0.0
|
|
-- Module: SAAS-019
|
|
-- ============================================
|
|
|
|
-- ============================================
|
|
-- Categories (hierarchical product categories)
|
|
-- ============================================
|
|
CREATE TABLE portfolio.categories (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE,
|
|
|
|
-- Hierarchy
|
|
parent_id UUID REFERENCES portfolio.categories(id) ON DELETE SET NULL,
|
|
|
|
-- Category info
|
|
name VARCHAR(100) NOT NULL,
|
|
slug VARCHAR(120) NOT NULL,
|
|
description TEXT,
|
|
|
|
-- Display
|
|
position INT NOT NULL DEFAULT 0,
|
|
image_url VARCHAR(500),
|
|
color VARCHAR(7) DEFAULT '#3B82F6',
|
|
icon VARCHAR(50),
|
|
|
|
-- Status
|
|
is_active BOOLEAN DEFAULT TRUE,
|
|
|
|
-- SEO
|
|
meta_title VARCHAR(200),
|
|
meta_description TEXT,
|
|
|
|
-- Custom fields
|
|
custom_fields JSONB DEFAULT '{}'::jsonb,
|
|
|
|
-- Audit
|
|
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
|
updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
|
created_by UUID REFERENCES users.users(id) ON DELETE SET NULL,
|
|
deleted_at TIMESTAMPTZ,
|
|
|
|
-- Constraints
|
|
CONSTRAINT unique_category_slug UNIQUE (tenant_id, slug),
|
|
CONSTRAINT unique_category_position UNIQUE (tenant_id, parent_id, position)
|
|
);
|
|
|
|
COMMENT ON TABLE portfolio.categories IS 'Hierarchical product categories per tenant';
|
|
COMMENT ON COLUMN portfolio.categories.parent_id IS 'Parent category for hierarchical structure';
|
|
|
|
-- ============================================
|
|
-- Products (main product catalog)
|
|
-- ============================================
|
|
CREATE TABLE portfolio.products (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE,
|
|
|
|
-- Category reference
|
|
category_id UUID REFERENCES portfolio.categories(id) ON DELETE SET NULL,
|
|
|
|
-- Basic info
|
|
name VARCHAR(255) NOT NULL,
|
|
slug VARCHAR(280) NOT NULL,
|
|
sku VARCHAR(100),
|
|
barcode VARCHAR(100),
|
|
description TEXT,
|
|
short_description VARCHAR(500),
|
|
|
|
-- Product type
|
|
product_type portfolio.product_type DEFAULT 'physical' NOT NULL,
|
|
status portfolio.product_status DEFAULT 'draft' NOT NULL,
|
|
|
|
-- Pricing (base price, can be overridden by variants/prices)
|
|
base_price DECIMAL(15, 2) DEFAULT 0,
|
|
cost_price DECIMAL(15, 2),
|
|
compare_at_price DECIMAL(15, 2),
|
|
currency VARCHAR(3) DEFAULT 'USD',
|
|
|
|
-- Inventory
|
|
track_inventory BOOLEAN DEFAULT TRUE,
|
|
stock_quantity INT DEFAULT 0,
|
|
low_stock_threshold INT DEFAULT 5,
|
|
allow_backorder BOOLEAN DEFAULT FALSE,
|
|
|
|
-- Physical properties
|
|
weight DECIMAL(10, 3),
|
|
weight_unit VARCHAR(10) DEFAULT 'kg',
|
|
length DECIMAL(10, 2),
|
|
width DECIMAL(10, 2),
|
|
height DECIMAL(10, 2),
|
|
dimension_unit VARCHAR(10) DEFAULT 'cm',
|
|
|
|
-- Media
|
|
images JSONB DEFAULT '[]'::jsonb,
|
|
featured_image_url VARCHAR(500),
|
|
|
|
-- SEO
|
|
meta_title VARCHAR(200),
|
|
meta_description TEXT,
|
|
tags JSONB DEFAULT '[]'::jsonb,
|
|
|
|
-- Visibility
|
|
is_visible BOOLEAN DEFAULT TRUE,
|
|
is_featured BOOLEAN DEFAULT FALSE,
|
|
|
|
-- Attributes for variants
|
|
has_variants BOOLEAN DEFAULT FALSE,
|
|
variant_attributes JSONB DEFAULT '[]'::jsonb,
|
|
|
|
-- Custom fields
|
|
custom_fields JSONB DEFAULT '{}'::jsonb,
|
|
|
|
-- Audit
|
|
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
|
updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
|
created_by UUID REFERENCES users.users(id) ON DELETE SET NULL,
|
|
published_at TIMESTAMPTZ,
|
|
deleted_at TIMESTAMPTZ,
|
|
|
|
-- Constraints
|
|
CONSTRAINT unique_product_slug UNIQUE (tenant_id, slug),
|
|
CONSTRAINT unique_product_sku UNIQUE (tenant_id, sku),
|
|
CONSTRAINT check_prices CHECK (base_price >= 0 AND (cost_price IS NULL OR cost_price >= 0))
|
|
);
|
|
|
|
COMMENT ON TABLE portfolio.products IS 'Product catalog with multi-tenant support';
|
|
COMMENT ON COLUMN portfolio.products.images IS 'JSON array of image URLs';
|
|
COMMENT ON COLUMN portfolio.products.variant_attributes IS 'JSON array of attribute names used for variants';
|
|
|
|
-- ============================================
|
|
-- Variants (product variants: size, color, etc.)
|
|
-- ============================================
|
|
CREATE TABLE portfolio.variants (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE,
|
|
product_id UUID NOT NULL REFERENCES portfolio.products(id) ON DELETE CASCADE,
|
|
|
|
-- Variant identification
|
|
sku VARCHAR(100),
|
|
barcode VARCHAR(100),
|
|
name VARCHAR(255),
|
|
|
|
-- Attributes (e.g., {"color": "red", "size": "L"})
|
|
attributes JSONB DEFAULT '{}'::jsonb NOT NULL,
|
|
|
|
-- Pricing
|
|
price DECIMAL(15, 2),
|
|
cost_price DECIMAL(15, 2),
|
|
compare_at_price DECIMAL(15, 2),
|
|
|
|
-- Inventory
|
|
stock_quantity INT DEFAULT 0,
|
|
low_stock_threshold INT,
|
|
|
|
-- Physical properties (can override product)
|
|
weight DECIMAL(10, 3),
|
|
|
|
-- Media
|
|
image_url VARCHAR(500),
|
|
|
|
-- Status
|
|
is_active BOOLEAN DEFAULT TRUE,
|
|
position INT DEFAULT 0,
|
|
|
|
-- Audit
|
|
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
|
updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
|
deleted_at TIMESTAMPTZ,
|
|
|
|
-- Constraints
|
|
CONSTRAINT unique_variant_sku UNIQUE (tenant_id, sku),
|
|
CONSTRAINT check_variant_prices CHECK (price IS NULL OR price >= 0)
|
|
);
|
|
|
|
COMMENT ON TABLE portfolio.variants IS 'Product variants (size, color, etc.)';
|
|
COMMENT ON COLUMN portfolio.variants.attributes IS 'JSON object with attribute key-value pairs';
|
|
|
|
-- ============================================
|
|
-- Prices (multi-currency pricing)
|
|
-- ============================================
|
|
CREATE TABLE portfolio.prices (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE,
|
|
|
|
-- Reference (either product or variant)
|
|
product_id UUID REFERENCES portfolio.products(id) ON DELETE CASCADE,
|
|
variant_id UUID REFERENCES portfolio.variants(id) ON DELETE CASCADE,
|
|
|
|
-- Pricing
|
|
price_type portfolio.price_type DEFAULT 'one_time' NOT NULL,
|
|
currency VARCHAR(3) NOT NULL DEFAULT 'USD',
|
|
amount DECIMAL(15, 2) NOT NULL,
|
|
compare_at_amount DECIMAL(15, 2),
|
|
|
|
-- Recurring pricing (for subscriptions)
|
|
billing_period VARCHAR(20),
|
|
billing_interval INT,
|
|
|
|
-- Tiered pricing
|
|
min_quantity INT DEFAULT 1,
|
|
max_quantity INT,
|
|
|
|
-- Validity
|
|
valid_from TIMESTAMPTZ,
|
|
valid_until TIMESTAMPTZ,
|
|
|
|
-- Priority (for overlapping prices)
|
|
priority INT DEFAULT 0,
|
|
|
|
-- Status
|
|
is_active BOOLEAN DEFAULT TRUE,
|
|
|
|
-- Audit
|
|
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
|
updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
|
deleted_at TIMESTAMPTZ,
|
|
|
|
-- Constraints
|
|
CONSTRAINT check_price_reference CHECK (
|
|
(product_id IS NOT NULL AND variant_id IS NULL) OR
|
|
(product_id IS NULL AND variant_id IS NOT NULL)
|
|
),
|
|
CONSTRAINT check_amount CHECK (amount >= 0),
|
|
CONSTRAINT check_quantity_range CHECK (
|
|
min_quantity > 0 AND (max_quantity IS NULL OR max_quantity >= min_quantity)
|
|
)
|
|
);
|
|
|
|
COMMENT ON TABLE portfolio.prices IS 'Multi-currency pricing for products and variants';
|
|
COMMENT ON COLUMN portfolio.prices.billing_period IS 'day, week, month, year for recurring prices';
|
|
|
|
-- ============================================
|
|
-- Triggers for updated_at
|
|
-- ============================================
|
|
CREATE OR REPLACE FUNCTION portfolio.update_updated_at()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
NEW.updated_at = NOW();
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
CREATE TRIGGER trg_categories_updated_at
|
|
BEFORE UPDATE ON portfolio.categories
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION portfolio.update_updated_at();
|
|
|
|
CREATE TRIGGER trg_products_updated_at
|
|
BEFORE UPDATE ON portfolio.products
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION portfolio.update_updated_at();
|
|
|
|
CREATE TRIGGER trg_variants_updated_at
|
|
BEFORE UPDATE ON portfolio.variants
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION portfolio.update_updated_at();
|
|
|
|
CREATE TRIGGER trg_prices_updated_at
|
|
BEFORE UPDATE ON portfolio.prices
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION portfolio.update_updated_at();
|