-- ============================================ -- 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();