mirror of
https://git.meshkee.com/Meshkee/backend.git
synced 2026-08-12 06:40:58 +04:30
Initial commit: Meshkee CMS API
NestJS backend with Prisma, Docker Compose for Postgres/Redis, and deploy docs for the production VM.
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
-- Meshkee CMS — initial schema
|
||||
-- Tables: users, businesses, domains
|
||||
|
||||
-- Reusable trigger to keep updated_at in sync
|
||||
CREATE OR REPLACE FUNCTION set_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- users
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE users (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
cell_number VARCHAR(20) NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255),
|
||||
first_name VARCHAR(100),
|
||||
last_name VARCHAR(100),
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
cell_verified_at TIMESTAMPTZ,
|
||||
last_login_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT users_cell_number_unique UNIQUE (cell_number),
|
||||
CONSTRAINT users_cell_number_format CHECK (cell_number ~ '^\+[1-9]\d{6,14}$'),
|
||||
CONSTRAINT users_email_format_optional CHECK (
|
||||
email IS NULL OR email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_users_cell_number ON users (cell_number);
|
||||
CREATE INDEX idx_users_is_active ON users (is_active) WHERE is_active = TRUE;
|
||||
|
||||
CREATE TRIGGER users_set_updated_at
|
||||
BEFORE UPDATE ON users
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- businesses (created by super admin — no direct user owner column)
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE businesses (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
slug VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
settings JSONB NOT NULL DEFAULT '{}',
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT businesses_slug_unique UNIQUE (slug),
|
||||
CONSTRAINT businesses_slug_format CHECK (slug ~ '^[a-z0-9]+(?:-[a-z0-9]+)*$')
|
||||
);
|
||||
|
||||
CREATE INDEX idx_businesses_is_active ON businesses (is_active) WHERE is_active = TRUE;
|
||||
|
||||
CREATE TRIGGER businesses_set_updated_at
|
||||
BEFORE UPDATE ON businesses
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- domains (many per business)
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE domains (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
business_id BIGINT NOT NULL,
|
||||
host VARCHAR(253) NOT NULL,
|
||||
is_primary BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
is_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
verified_at TIMESTAMPTZ,
|
||||
ssl_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT domains_host_unique UNIQUE (host),
|
||||
CONSTRAINT domains_business_id_fkey
|
||||
FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE,
|
||||
CONSTRAINT domains_host_format CHECK (
|
||||
host ~ '^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$'
|
||||
OR host ~ '^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$'
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_domains_business_id ON domains (business_id);
|
||||
CREATE INDEX idx_domains_host ON domains (host);
|
||||
CREATE INDEX idx_domains_business_primary ON domains (business_id) WHERE is_primary = TRUE;
|
||||
|
||||
CREATE UNIQUE INDEX idx_domains_one_primary_per_business
|
||||
ON domains (business_id)
|
||||
WHERE is_primary = TRUE;
|
||||
|
||||
CREATE TRIGGER domains_set_updated_at
|
||||
BEFORE UPDATE ON domains
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
@@ -0,0 +1,324 @@
|
||||
-- Meshkee CMS — RBAC, content tables, media
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- enums
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TYPE content_status AS ENUM ('draft', 'published', 'archived');
|
||||
CREATE TYPE media_type AS ENUM ('image', 'video');
|
||||
CREATE TYPE media_entity_type AS ENUM ('product', 'blog', 'portfolio');
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- permissions (RBAC)
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE permissions (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
slug VARCHAR(100) NOT NULL,
|
||||
group_name VARCHAR(50) NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT permissions_slug_unique UNIQUE (slug)
|
||||
);
|
||||
|
||||
CREATE TABLE roles (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
slug VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
is_system BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT roles_slug_unique UNIQUE (slug)
|
||||
);
|
||||
|
||||
CREATE TABLE role_permissions (
|
||||
role_id BIGINT NOT NULL,
|
||||
permission_id BIGINT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
PRIMARY KEY (role_id, permission_id),
|
||||
CONSTRAINT role_permissions_role_id_fkey
|
||||
FOREIGN KEY (role_id) REFERENCES roles (id) ON DELETE CASCADE,
|
||||
CONSTRAINT role_permissions_permission_id_fkey
|
||||
FOREIGN KEY (permission_id) REFERENCES permissions (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE user_roles (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL,
|
||||
role_id BIGINT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT user_roles_user_role_unique UNIQUE (user_id, role_id),
|
||||
CONSTRAINT user_roles_user_id_fkey
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
CONSTRAINT user_roles_role_id_fkey
|
||||
FOREIGN KEY (role_id) REFERENCES roles (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_user_roles_user_id ON user_roles (user_id);
|
||||
CREATE INDEX idx_role_permissions_permission_id ON role_permissions (permission_id);
|
||||
|
||||
CREATE TRIGGER roles_set_updated_at
|
||||
BEFORE UPDATE ON roles
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- media (images & videos, scoped per business)
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE media (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
business_id BIGINT NOT NULL,
|
||||
uploaded_by BIGINT,
|
||||
media_type media_type NOT NULL,
|
||||
storage_disk VARCHAR(50) NOT NULL DEFAULT 'local',
|
||||
storage_path TEXT NOT NULL,
|
||||
public_url TEXT NOT NULL,
|
||||
file_name VARCHAR(255) NOT NULL,
|
||||
original_file_name VARCHAR(255) NOT NULL,
|
||||
mime_type VARCHAR(100) NOT NULL,
|
||||
file_size_bytes BIGINT NOT NULL,
|
||||
width INTEGER,
|
||||
height INTEGER,
|
||||
duration_seconds NUMERIC(10, 2),
|
||||
alt_text VARCHAR(255),
|
||||
caption TEXT,
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT media_business_id_fkey
|
||||
FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE,
|
||||
CONSTRAINT media_uploaded_by_fkey
|
||||
FOREIGN KEY (uploaded_by) REFERENCES users (id) ON DELETE SET NULL,
|
||||
CONSTRAINT media_file_size_positive CHECK (file_size_bytes > 0),
|
||||
CONSTRAINT media_image_dimensions CHECK (
|
||||
media_type <> 'image' OR (width IS NOT NULL AND height IS NOT NULL)
|
||||
),
|
||||
CONSTRAINT media_video_duration CHECK (
|
||||
media_type <> 'video' OR duration_seconds IS NOT NULL
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_media_business_id ON media (business_id);
|
||||
CREATE INDEX idx_media_business_type ON media (business_id, media_type);
|
||||
CREATE INDEX idx_media_created_at ON media (business_id, created_at DESC);
|
||||
|
||||
CREATE TRIGGER media_set_updated_at
|
||||
BEFORE UPDATE ON media
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- products
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE products (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
business_id BIGINT NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
slug VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
content JSONB NOT NULL DEFAULT '{}',
|
||||
price NUMERIC(12, 2),
|
||||
compare_at_price NUMERIC(12, 2),
|
||||
sku VARCHAR(100),
|
||||
stock_quantity INTEGER,
|
||||
status content_status NOT NULL DEFAULT 'draft',
|
||||
featured_media_id BIGINT,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
published_at TIMESTAMPTZ,
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT products_business_slug_unique UNIQUE (business_id, slug),
|
||||
CONSTRAINT products_business_id_fkey
|
||||
FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE,
|
||||
CONSTRAINT products_featured_media_id_fkey
|
||||
FOREIGN KEY (featured_media_id) REFERENCES media (id) ON DELETE SET NULL,
|
||||
CONSTRAINT products_price_non_negative CHECK (price IS NULL OR price >= 0),
|
||||
CONSTRAINT products_compare_price_non_negative CHECK (compare_at_price IS NULL OR compare_at_price >= 0),
|
||||
CONSTRAINT products_stock_non_negative CHECK (stock_quantity IS NULL OR stock_quantity >= 0)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_products_business_id ON products (business_id);
|
||||
CREATE INDEX idx_products_business_status ON products (business_id, status);
|
||||
CREATE INDEX idx_products_business_published ON products (business_id, published_at DESC);
|
||||
|
||||
CREATE TRIGGER products_set_updated_at
|
||||
BEFORE UPDATE ON products
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- blogs
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE blogs (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
business_id BIGINT NOT NULL,
|
||||
author_id BIGINT,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
slug VARCHAR(255) NOT NULL,
|
||||
excerpt TEXT,
|
||||
content JSONB NOT NULL DEFAULT '{}',
|
||||
status content_status NOT NULL DEFAULT 'draft',
|
||||
featured_media_id BIGINT,
|
||||
published_at TIMESTAMPTZ,
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT blogs_business_slug_unique UNIQUE (business_id, slug),
|
||||
CONSTRAINT blogs_business_id_fkey
|
||||
FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE,
|
||||
CONSTRAINT blogs_author_id_fkey
|
||||
FOREIGN KEY (author_id) REFERENCES users (id) ON DELETE SET NULL,
|
||||
CONSTRAINT blogs_featured_media_id_fkey
|
||||
FOREIGN KEY (featured_media_id) REFERENCES media (id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_blogs_business_id ON blogs (business_id);
|
||||
CREATE INDEX idx_blogs_business_status ON blogs (business_id, status);
|
||||
CREATE INDEX idx_blogs_business_published ON blogs (business_id, published_at DESC);
|
||||
|
||||
CREATE TRIGGER blogs_set_updated_at
|
||||
BEFORE UPDATE ON blogs
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- portfolios
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE portfolios (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
business_id BIGINT NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
slug VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
content JSONB NOT NULL DEFAULT '{}',
|
||||
client_name VARCHAR(255),
|
||||
project_url TEXT,
|
||||
status content_status NOT NULL DEFAULT 'draft',
|
||||
featured_media_id BIGINT,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
published_at TIMESTAMPTZ,
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT portfolios_business_slug_unique UNIQUE (business_id, slug),
|
||||
CONSTRAINT portfolios_business_id_fkey
|
||||
FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE,
|
||||
CONSTRAINT portfolios_featured_media_id_fkey
|
||||
FOREIGN KEY (featured_media_id) REFERENCES media (id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_portfolios_business_id ON portfolios (business_id);
|
||||
CREATE INDEX idx_portfolios_business_status ON portfolios (business_id, status);
|
||||
CREATE INDEX idx_portfolios_business_published ON portfolios (business_id, published_at DESC);
|
||||
|
||||
CREATE TRIGGER portfolios_set_updated_at
|
||||
BEFORE UPDATE ON portfolios
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- media attachments (galleries, inline images/videos on content)
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE media_attachments (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
business_id BIGINT NOT NULL,
|
||||
media_id BIGINT NOT NULL,
|
||||
entity_type media_entity_type NOT NULL,
|
||||
entity_id BIGINT NOT NULL,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
is_featured BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT media_attachments_business_id_fkey
|
||||
FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE,
|
||||
CONSTRAINT media_attachments_media_id_fkey
|
||||
FOREIGN KEY (media_id) REFERENCES media (id) ON DELETE CASCADE,
|
||||
CONSTRAINT media_attachments_unique UNIQUE (media_id, entity_type, entity_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_media_attachments_entity
|
||||
ON media_attachments (business_id, entity_type, entity_id, sort_order);
|
||||
CREATE INDEX idx_media_attachments_media_id ON media_attachments (media_id);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- seed: default permissions & roles
|
||||
-- ---------------------------------------------------------------------------
|
||||
INSERT INTO permissions (name, slug, group_name, description) VALUES
|
||||
('View business', 'business.read', 'business', 'View business profile and settings'),
|
||||
('Update business', 'business.update', 'business', 'Edit business profile and settings'),
|
||||
('View domains', 'domains.read', 'domains', 'View connected domains'),
|
||||
('Manage domains', 'domains.manage', 'domains', 'Add, edit, and remove domains'),
|
||||
('View products', 'products.read', 'products', 'View products'),
|
||||
('Create products', 'products.create', 'products', 'Create products'),
|
||||
('Update products', 'products.update', 'products', 'Edit products'),
|
||||
('Delete products', 'products.delete', 'products', 'Delete products'),
|
||||
('Publish products', 'products.publish', 'products', 'Publish and unpublish products'),
|
||||
('View blogs', 'blogs.read', 'blogs', 'View blog posts'),
|
||||
('Create blogs', 'blogs.create', 'blogs', 'Create blog posts'),
|
||||
('Update blogs', 'blogs.update', 'blogs', 'Edit blog posts'),
|
||||
('Delete blogs', 'blogs.delete', 'blogs', 'Delete blog posts'),
|
||||
('Publish blogs', 'blogs.publish', 'blogs', 'Publish and unpublish blog posts'),
|
||||
('View portfolios', 'portfolios.read', 'portfolios', 'View portfolio items'),
|
||||
('Create portfolios', 'portfolios.create', 'portfolios', 'Create portfolio items'),
|
||||
('Update portfolios', 'portfolios.update', 'portfolios', 'Edit portfolio items'),
|
||||
('Delete portfolios', 'portfolios.delete', 'portfolios', 'Delete portfolio items'),
|
||||
('Publish portfolios', 'portfolios.publish', 'portfolios', 'Publish and unpublish portfolio items'),
|
||||
('View media', 'media.read', 'media', 'View uploaded media'),
|
||||
('Upload media', 'media.create', 'media', 'Upload images and videos'),
|
||||
('Update media', 'media.update', 'media', 'Edit media metadata'),
|
||||
('Delete media', 'media.delete', 'media', 'Delete media files'),
|
||||
('View users', 'users.read', 'users', 'View user accounts'),
|
||||
('Manage users', 'users.manage', 'users', 'Create and manage user accounts'),
|
||||
('Manage roles', 'roles.manage', 'users', 'Assign roles and permissions');
|
||||
|
||||
INSERT INTO roles (name, slug, description, is_system) VALUES
|
||||
('Owner', 'owner', 'Full access to everything', TRUE),
|
||||
('Administrator', 'admin', 'Manage content, media, and settings', TRUE),
|
||||
('Editor', 'editor', 'Create and edit content', TRUE),
|
||||
('Viewer', 'viewer', 'Read-only access', TRUE);
|
||||
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
CROSS JOIN permissions p
|
||||
WHERE r.slug = 'owner';
|
||||
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.slug <> 'roles.manage'
|
||||
WHERE r.slug = 'admin';
|
||||
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.slug IN (
|
||||
'business.read',
|
||||
'products.read', 'products.create', 'products.update', 'products.publish',
|
||||
'blogs.read', 'blogs.create', 'blogs.update', 'blogs.publish',
|
||||
'portfolios.read', 'portfolios.create', 'portfolios.update', 'portfolios.publish',
|
||||
'media.read', 'media.create', 'media.update'
|
||||
)
|
||||
WHERE r.slug = 'editor';
|
||||
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.slug IN (
|
||||
'business.read',
|
||||
'domains.read',
|
||||
'products.read',
|
||||
'blogs.read',
|
||||
'portfolios.read',
|
||||
'media.read'
|
||||
)
|
||||
WHERE r.slug = 'viewer';
|
||||
@@ -0,0 +1,79 @@
|
||||
-- Meshkee CMS — categories for products, blogs, portfolios
|
||||
|
||||
CREATE TABLE categories (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
business_id BIGINT NOT NULL,
|
||||
entity_type media_entity_type NOT NULL,
|
||||
parent_id BIGINT,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
slug VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT categories_business_entity_slug_unique
|
||||
UNIQUE (business_id, entity_type, slug),
|
||||
CONSTRAINT categories_business_id_fkey
|
||||
FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE,
|
||||
CONSTRAINT categories_parent_id_fkey
|
||||
FOREIGN KEY (parent_id) REFERENCES categories (id) ON DELETE SET NULL,
|
||||
CONSTRAINT categories_slug_format CHECK (slug ~ '^[a-z0-9]+(?:-[a-z0-9]+)*$')
|
||||
);
|
||||
|
||||
CREATE INDEX idx_categories_business_entity
|
||||
ON categories (business_id, entity_type, sort_order);
|
||||
CREATE INDEX idx_categories_parent_id ON categories (parent_id);
|
||||
CREATE INDEX idx_categories_active
|
||||
ON categories (business_id, entity_type) WHERE is_active = TRUE;
|
||||
|
||||
CREATE TRIGGER categories_set_updated_at
|
||||
BEFORE UPDATE ON categories
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
CREATE TABLE category_assignments (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
business_id BIGINT NOT NULL,
|
||||
category_id BIGINT NOT NULL,
|
||||
entity_type media_entity_type NOT NULL,
|
||||
entity_id BIGINT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT category_assignments_unique
|
||||
UNIQUE (category_id, entity_type, entity_id),
|
||||
CONSTRAINT category_assignments_business_id_fkey
|
||||
FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE,
|
||||
CONSTRAINT category_assignments_category_id_fkey
|
||||
FOREIGN KEY (category_id) REFERENCES categories (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_category_assignments_entity
|
||||
ON category_assignments (business_id, entity_type, entity_id);
|
||||
CREATE INDEX idx_category_assignments_category_id
|
||||
ON category_assignments (category_id);
|
||||
|
||||
INSERT INTO permissions (name, slug, group_name, description) VALUES
|
||||
('View categories', 'categories.read', 'categories', 'View categories'),
|
||||
('Create categories', 'categories.create', 'categories', 'Create categories'),
|
||||
('Update categories', 'categories.update', 'categories', 'Edit categories'),
|
||||
('Delete categories', 'categories.delete', 'categories', 'Delete categories');
|
||||
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.slug LIKE 'categories.%'
|
||||
WHERE r.slug IN ('owner', 'admin');
|
||||
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.slug IN ('categories.read', 'categories.create', 'categories.update')
|
||||
WHERE r.slug = 'editor';
|
||||
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.slug = 'categories.read'
|
||||
WHERE r.slug = 'viewer';
|
||||
@@ -0,0 +1,128 @@
|
||||
-- Meshkee CMS — three user types: super_admin, business_owner, customer
|
||||
-- Businesses are created by super admin (no longer tied 1:1 to user on register)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- business_users (owners/staff assigned to a business by super admin)
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS business_users (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
business_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
is_owner BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT business_users_business_user_unique UNIQUE (business_id, user_id),
|
||||
CONSTRAINT business_users_business_id_fkey
|
||||
FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE,
|
||||
CONSTRAINT business_users_user_id_fkey
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_business_users_user_id ON business_users (user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_business_users_business_id ON business_users (business_id);
|
||||
|
||||
-- Migrate legacy businesses.user_id links (only when upgrading old databases)
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'businesses'
|
||||
AND column_name = 'user_id'
|
||||
) THEN
|
||||
INSERT INTO business_users (business_id, user_id, is_owner)
|
||||
SELECT id, user_id, TRUE
|
||||
FROM businesses
|
||||
WHERE user_id IS NOT NULL
|
||||
ON CONFLICT (business_id, user_id) DO NOTHING;
|
||||
|
||||
ALTER TABLE businesses DROP CONSTRAINT IF EXISTS businesses_user_id_fkey;
|
||||
ALTER TABLE businesses DROP CONSTRAINT IF EXISTS businesses_user_id_unique;
|
||||
ALTER TABLE businesses DROP COLUMN user_id;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- business_customers (users who registered on a business website)
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS business_customers (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
business_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT business_customers_business_user_unique UNIQUE (business_id, user_id),
|
||||
CONSTRAINT business_customers_business_id_fkey
|
||||
FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE,
|
||||
CONSTRAINT business_customers_user_id_fkey
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_business_customers_user_id ON business_customers (user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_business_customers_business_id ON business_customers (business_id);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- roles: super_admin, business_owner, customer
|
||||
-- ---------------------------------------------------------------------------
|
||||
INSERT INTO roles (name, slug, description, is_system) VALUES
|
||||
('Super Admin', 'super_admin', 'Full platform control — manages businesses, domains, and owners', TRUE),
|
||||
('Business Owner', 'business_owner', 'Manages assigned business dashboard', TRUE),
|
||||
('Customer', 'customer', 'Registered user on a business website', TRUE)
|
||||
ON CONFLICT (slug) DO NOTHING;
|
||||
|
||||
-- Migrate legacy owner role assignments to business_owner
|
||||
UPDATE user_roles ur
|
||||
SET role_id = (SELECT id FROM roles WHERE slug = 'business_owner')
|
||||
WHERE role_id = (SELECT id FROM roles WHERE slug = 'owner');
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- permissions: platform-level (super admin)
|
||||
-- ---------------------------------------------------------------------------
|
||||
INSERT INTO permissions (name, slug, group_name, description) VALUES
|
||||
('View all businesses', 'businesses.read', 'businesses', 'View all businesses'),
|
||||
('Create businesses', 'businesses.create', 'businesses', 'Create new businesses'),
|
||||
('Update businesses', 'businesses.update', 'businesses', 'Edit businesses'),
|
||||
('Delete businesses', 'businesses.delete', 'businesses', 'Delete businesses'),
|
||||
('Assign business owners', 'businesses.assign', 'businesses', 'Assign owners to businesses'),
|
||||
('View all users', 'platform.users.read', 'platform', 'View all platform users'),
|
||||
('Manage all users', 'platform.users.manage', 'platform', 'Create and manage platform users')
|
||||
ON CONFLICT (slug) DO NOTHING;
|
||||
|
||||
-- super_admin: all permissions
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
CROSS JOIN permissions p
|
||||
WHERE r.slug = 'super_admin'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- business_owner: same as legacy owner (business + content + media + categories + domains read)
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.slug IN (
|
||||
'business.read', 'business.update',
|
||||
'domains.read', 'domains.manage',
|
||||
'products.read', 'products.create', 'products.update', 'products.delete', 'products.publish',
|
||||
'blogs.read', 'blogs.create', 'blogs.update', 'blogs.delete', 'blogs.publish',
|
||||
'portfolios.read', 'portfolios.create', 'portfolios.update', 'portfolios.delete', 'portfolios.publish',
|
||||
'media.read', 'media.create', 'media.update', 'media.delete',
|
||||
'categories.read', 'categories.create', 'categories.update', 'categories.delete',
|
||||
'users.read'
|
||||
)
|
||||
WHERE r.slug = 'business_owner'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- customer: no CMS permissions for now (orders/favorites added later)
|
||||
INSERT INTO permissions (name, slug, group_name, description) VALUES
|
||||
('View own orders', 'orders.read', 'orders', 'View own orders'),
|
||||
('View own favorites', 'favorites.read', 'favorites', 'View own favorites')
|
||||
ON CONFLICT (slug) DO NOTHING;
|
||||
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.slug IN ('orders.read', 'favorites.read')
|
||||
WHERE r.slug = 'customer'
|
||||
ON CONFLICT DO NOTHING;
|
||||
@@ -0,0 +1,79 @@
|
||||
-- Business team: owners can add staff with limited per-business roles
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- business_users: add role_id for staff permissions (owners use is_owner=true)
|
||||
-- ---------------------------------------------------------------------------
|
||||
ALTER TABLE business_users
|
||||
ADD COLUMN IF NOT EXISTS role_id BIGINT,
|
||||
ADD COLUMN IF NOT EXISTS invited_by BIGINT,
|
||||
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW();
|
||||
|
||||
ALTER TABLE business_users DROP CONSTRAINT IF EXISTS business_users_role_id_fkey;
|
||||
ALTER TABLE business_users
|
||||
ADD CONSTRAINT business_users_role_id_fkey
|
||||
FOREIGN KEY (role_id) REFERENCES roles (id) ON DELETE RESTRICT;
|
||||
|
||||
ALTER TABLE business_users DROP CONSTRAINT IF EXISTS business_users_invited_by_fkey;
|
||||
ALTER TABLE business_users
|
||||
ADD CONSTRAINT business_users_invited_by_fkey
|
||||
FOREIGN KEY (invited_by) REFERENCES users (id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE business_users DROP CONSTRAINT IF EXISTS business_users_member_role_check;
|
||||
ALTER TABLE business_users
|
||||
ADD CONSTRAINT business_users_member_role_check CHECK (
|
||||
(is_owner = TRUE AND role_id IS NULL)
|
||||
OR (is_owner = FALSE AND role_id IS NOT NULL)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_business_users_role_id ON business_users (role_id);
|
||||
|
||||
DROP TRIGGER IF EXISTS business_users_set_updated_at ON business_users;
|
||||
CREATE TRIGGER business_users_set_updated_at
|
||||
BEFORE UPDATE ON business_users
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- business_staff global role (dashboard access for invited team members)
|
||||
-- ---------------------------------------------------------------------------
|
||||
INSERT INTO roles (name, slug, description, is_system) VALUES
|
||||
('Business Staff', 'business_staff', 'Team member on a business with limited permissions', TRUE)
|
||||
ON CONFLICT (slug) DO NOTHING;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- team management permissions (for business owners)
|
||||
-- ---------------------------------------------------------------------------
|
||||
INSERT INTO permissions (name, slug, group_name, description) VALUES
|
||||
('View business team', 'business.team.read', 'business_team', 'View team members of a business'),
|
||||
('Invite business team', 'business.team.invite', 'business_team', 'Add team members to a business'),
|
||||
('Update business team', 'business.team.update', 'business_team', 'Change team member roles'),
|
||||
('Remove business team', 'business.team.remove', 'business_team', 'Remove team members from a business')
|
||||
ON CONFLICT (slug) DO NOTHING;
|
||||
|
||||
-- business_owner gets team management permissions
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.slug LIKE 'business.team.%'
|
||||
WHERE r.slug = 'business_owner'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- admin staff role: almost full business access + team read (not invite/remove owners)
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.slug IN (
|
||||
'business.read', 'business.update',
|
||||
'domains.read',
|
||||
'products.read', 'products.create', 'products.update', 'products.delete', 'products.publish',
|
||||
'blogs.read', 'blogs.create', 'blogs.update', 'blogs.delete', 'blogs.publish',
|
||||
'portfolios.read', 'portfolios.create', 'portfolios.update', 'portfolios.delete', 'portfolios.publish',
|
||||
'media.read', 'media.create', 'media.update', 'media.delete',
|
||||
'categories.read', 'categories.create', 'categories.update', 'categories.delete',
|
||||
'business.team.read'
|
||||
)
|
||||
WHERE r.slug = 'admin'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- editor & viewer already seeded in 002 — ensure business_staff has no extra perms
|
||||
-- business_staff global role: no permissions (permissions come from business_users.role_id)
|
||||
@@ -0,0 +1,75 @@
|
||||
-- System-wide business categories (not scoped to any business)
|
||||
-- Businesses are tagged with one or more of these categories
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- business_categories (platform / system level)
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE business_categories (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
parent_id BIGINT,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
slug VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
icon VARCHAR(100),
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT business_categories_slug_unique UNIQUE (slug),
|
||||
CONSTRAINT business_categories_parent_id_fkey
|
||||
FOREIGN KEY (parent_id) REFERENCES business_categories (id) ON DELETE SET NULL,
|
||||
CONSTRAINT business_categories_slug_format CHECK (slug ~ '^[a-z0-9]+(?:-[a-z0-9]+)*$')
|
||||
);
|
||||
|
||||
CREATE INDEX idx_business_categories_parent_id ON business_categories (parent_id);
|
||||
CREATE INDEX idx_business_categories_sort_order ON business_categories (sort_order);
|
||||
CREATE INDEX idx_business_categories_active
|
||||
ON business_categories (is_active) WHERE is_active = TRUE;
|
||||
|
||||
CREATE TRIGGER business_categories_set_updated_at
|
||||
BEFORE UPDATE ON business_categories
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- business_category_assignments (business ↔ system category, many-to-many)
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE business_category_assignments (
|
||||
business_id BIGINT NOT NULL,
|
||||
category_id BIGINT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
PRIMARY KEY (business_id, category_id),
|
||||
CONSTRAINT business_category_assignments_business_id_fkey
|
||||
FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE,
|
||||
CONSTRAINT business_category_assignments_category_id_fkey
|
||||
FOREIGN KEY (category_id) REFERENCES business_categories (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_business_category_assignments_category_id
|
||||
ON business_category_assignments (category_id);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- permissions (super admin manages business categories)
|
||||
-- ---------------------------------------------------------------------------
|
||||
INSERT INTO permissions (name, slug, group_name, description) VALUES
|
||||
('View business categories', 'business_categories.read', 'business_categories', 'View system business categories'),
|
||||
('Create business categories', 'business_categories.create', 'business_categories', 'Create system business categories'),
|
||||
('Update business categories', 'business_categories.update', 'business_categories', 'Edit system business categories'),
|
||||
('Delete business categories', 'business_categories.delete', 'business_categories', 'Delete system business categories')
|
||||
ON CONFLICT (slug) DO NOTHING;
|
||||
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.slug LIKE 'business_categories.%'
|
||||
WHERE r.slug = 'super_admin'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.slug = 'business_categories.read'
|
||||
WHERE r.slug IN ('business_owner', 'admin')
|
||||
ON CONFLICT DO NOTHING;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE users
|
||||
ADD COLUMN IF NOT EXISTS profile JSONB NOT NULL DEFAULT '{}'::jsonb;
|
||||
@@ -0,0 +1,9 @@
|
||||
-- Business i18n fields: name_fa, about (English name uses existing `name` column)
|
||||
|
||||
ALTER TABLE businesses
|
||||
ADD COLUMN IF NOT EXISTS name_fa VARCHAR(255),
|
||||
ADD COLUMN IF NOT EXISTS about TEXT;
|
||||
|
||||
UPDATE businesses
|
||||
SET name_fa = COALESCE(name_fa, name)
|
||||
WHERE name_fa IS NULL;
|
||||
@@ -0,0 +1,9 @@
|
||||
ALTER TABLE domains
|
||||
ADD COLUMN IF NOT EXISTS expires_at TIMESTAMPTZ,
|
||||
ADD COLUMN IF NOT EXISTS is_active BOOLEAN NOT NULL DEFAULT TRUE;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_domains_is_active ON domains (is_active);
|
||||
|
||||
UPDATE domains
|
||||
SET expires_at = NOW() + INTERVAL '365 days'
|
||||
WHERE expires_at IS NULL;
|
||||
@@ -0,0 +1,7 @@
|
||||
-- Remove redundant name_en (use name for English/default name)
|
||||
|
||||
UPDATE businesses
|
||||
SET name = COALESCE(name, name_en)
|
||||
WHERE name IS NULL AND name_en IS NOT NULL;
|
||||
|
||||
ALTER TABLE businesses DROP COLUMN IF EXISTS name_en;
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Persian display name for product/blog/portfolio categories
|
||||
|
||||
ALTER TABLE categories
|
||||
ADD COLUMN IF NOT EXISTS name_fa VARCHAR(255);
|
||||
@@ -0,0 +1,104 @@
|
||||
-- Category variations & options (product categories only)
|
||||
-- Variation types: color (predefined palette), size (user-defined), custom (user-defined name + values)
|
||||
|
||||
CREATE TYPE variation_type AS ENUM ('color', 'size', 'custom');
|
||||
|
||||
CREATE TABLE category_variations (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
business_id BIGINT NOT NULL,
|
||||
category_id BIGINT NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
variation_type variation_type NOT NULL,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT category_variations_business_id_fkey
|
||||
FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE,
|
||||
CONSTRAINT category_variations_category_id_fkey
|
||||
FOREIGN KEY (category_id) REFERENCES categories (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX category_variations_one_color_per_category
|
||||
ON category_variations (category_id) WHERE variation_type = 'color';
|
||||
|
||||
CREATE UNIQUE INDEX category_variations_one_size_per_category
|
||||
ON category_variations (category_id) WHERE variation_type = 'size';
|
||||
|
||||
CREATE UNIQUE INDEX category_variations_custom_name_per_category
|
||||
ON category_variations (category_id, name) WHERE variation_type = 'custom';
|
||||
|
||||
CREATE INDEX idx_category_variations_category_id ON category_variations (category_id);
|
||||
CREATE INDEX idx_category_variations_business_id ON category_variations (business_id);
|
||||
|
||||
CREATE TRIGGER category_variations_set_updated_at
|
||||
BEFORE UPDATE ON category_variations
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
CREATE TABLE category_variation_options (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
variation_id BIGINT NOT NULL,
|
||||
label VARCHAR(255) NOT NULL,
|
||||
value VARCHAR(255) NOT NULL,
|
||||
color_hex VARCHAR(7),
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT category_variation_options_variation_id_fkey
|
||||
FOREIGN KEY (variation_id) REFERENCES category_variations (id) ON DELETE CASCADE,
|
||||
CONSTRAINT category_variation_options_unique_value
|
||||
UNIQUE (variation_id, value)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_category_variation_options_variation_id
|
||||
ON category_variation_options (variation_id);
|
||||
|
||||
-- Product variants (combinations of category variation options)
|
||||
CREATE TABLE product_variants (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
business_id BIGINT NOT NULL,
|
||||
product_id BIGINT NOT NULL,
|
||||
sku VARCHAR(100),
|
||||
price NUMERIC(12, 2),
|
||||
compare_at_price NUMERIC(12, 2),
|
||||
stock_quantity INTEGER,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT product_variants_business_id_fkey
|
||||
FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE,
|
||||
CONSTRAINT product_variants_product_id_fkey
|
||||
FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE CASCADE,
|
||||
CONSTRAINT product_variants_stock_non_negative
|
||||
CHECK (stock_quantity IS NULL OR stock_quantity >= 0)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_product_variants_product_id ON product_variants (product_id);
|
||||
CREATE INDEX idx_product_variants_business_id ON product_variants (business_id);
|
||||
|
||||
CREATE TRIGGER product_variants_set_updated_at
|
||||
BEFORE UPDATE ON product_variants
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
CREATE TABLE product_variant_selections (
|
||||
variant_id BIGINT NOT NULL,
|
||||
variation_id BIGINT NOT NULL,
|
||||
option_id BIGINT NOT NULL,
|
||||
|
||||
CONSTRAINT product_variant_selections_pkey PRIMARY KEY (variant_id, variation_id),
|
||||
CONSTRAINT product_variant_selections_variant_id_fkey
|
||||
FOREIGN KEY (variant_id) REFERENCES product_variants (id) ON DELETE CASCADE,
|
||||
CONSTRAINT product_variant_selections_variation_id_fkey
|
||||
FOREIGN KEY (variation_id) REFERENCES category_variations (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT product_variant_selections_option_id_fkey
|
||||
FOREIGN KEY (option_id) REFERENCES category_variation_options (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT product_variant_selections_unique_option
|
||||
UNIQUE (variant_id, option_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_product_variant_selections_option_id
|
||||
ON product_variant_selections (option_id);
|
||||
@@ -0,0 +1,118 @@
|
||||
-- Category technical forms: dynamic form definitions per product category
|
||||
-- Field types: text, textarea, select, multi_select
|
||||
|
||||
CREATE TYPE technical_field_type AS ENUM ('text', 'textarea', 'select', 'multi_select');
|
||||
|
||||
CREATE TABLE category_technical_forms (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
business_id BIGINT NOT NULL,
|
||||
category_id BIGINT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT category_technical_forms_business_id_fkey
|
||||
FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE,
|
||||
CONSTRAINT category_technical_forms_category_id_fkey
|
||||
FOREIGN KEY (category_id) REFERENCES categories (id) ON DELETE CASCADE,
|
||||
CONSTRAINT category_technical_forms_category_unique
|
||||
UNIQUE (category_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_category_technical_forms_business_id
|
||||
ON category_technical_forms (business_id);
|
||||
|
||||
CREATE TRIGGER category_technical_forms_set_updated_at
|
||||
BEFORE UPDATE ON category_technical_forms
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
CREATE TABLE category_technical_form_fields (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
form_id BIGINT NOT NULL,
|
||||
label VARCHAR(255) NOT NULL,
|
||||
field_key VARCHAR(255) NOT NULL,
|
||||
field_type technical_field_type NOT NULL,
|
||||
is_required BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT category_technical_form_fields_form_id_fkey
|
||||
FOREIGN KEY (form_id) REFERENCES category_technical_forms (id) ON DELETE CASCADE,
|
||||
CONSTRAINT category_technical_form_fields_unique_key
|
||||
UNIQUE (form_id, field_key)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_category_technical_form_fields_form_id
|
||||
ON category_technical_form_fields (form_id);
|
||||
|
||||
CREATE TRIGGER category_technical_form_fields_set_updated_at
|
||||
BEFORE UPDATE ON category_technical_form_fields
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
CREATE TABLE category_technical_form_field_options (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
field_id BIGINT NOT NULL,
|
||||
label VARCHAR(255) NOT NULL,
|
||||
value VARCHAR(255) NOT NULL,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT category_technical_form_field_options_field_id_fkey
|
||||
FOREIGN KEY (field_id) REFERENCES category_technical_form_fields (id) ON DELETE CASCADE,
|
||||
CONSTRAINT category_technical_form_field_options_unique_value
|
||||
UNIQUE (field_id, value)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_category_technical_form_field_options_field_id
|
||||
ON category_technical_form_field_options (field_id);
|
||||
|
||||
-- Product technical data values (one row per product per field)
|
||||
CREATE TABLE product_technical_field_values (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
business_id BIGINT NOT NULL,
|
||||
product_id BIGINT NOT NULL,
|
||||
field_id BIGINT NOT NULL,
|
||||
text_value TEXT,
|
||||
option_id BIGINT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT product_technical_field_values_business_id_fkey
|
||||
FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE,
|
||||
CONSTRAINT product_technical_field_values_product_id_fkey
|
||||
FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE CASCADE,
|
||||
CONSTRAINT product_technical_field_values_field_id_fkey
|
||||
FOREIGN KEY (field_id) REFERENCES category_technical_form_fields (id) ON DELETE CASCADE,
|
||||
CONSTRAINT product_technical_field_values_option_id_fkey
|
||||
FOREIGN KEY (option_id) REFERENCES category_technical_form_field_options (id) ON DELETE SET NULL,
|
||||
CONSTRAINT product_technical_field_values_unique
|
||||
UNIQUE (product_id, field_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_product_technical_field_values_product_id
|
||||
ON product_technical_field_values (product_id);
|
||||
CREATE INDEX idx_product_technical_field_values_business_id
|
||||
ON product_technical_field_values (business_id);
|
||||
|
||||
CREATE TRIGGER product_technical_field_values_set_updated_at
|
||||
BEFORE UPDATE ON product_technical_field_values
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
-- Multi-select option selections
|
||||
CREATE TABLE product_technical_field_value_options (
|
||||
field_value_id BIGINT NOT NULL,
|
||||
option_id BIGINT NOT NULL,
|
||||
|
||||
CONSTRAINT product_technical_field_value_options_pkey
|
||||
PRIMARY KEY (field_value_id, option_id),
|
||||
CONSTRAINT product_technical_field_value_options_field_value_id_fkey
|
||||
FOREIGN KEY (field_value_id) REFERENCES product_technical_field_values (id) ON DELETE CASCADE,
|
||||
CONSTRAINT product_technical_field_value_options_option_id_fkey
|
||||
FOREIGN KEY (option_id) REFERENCES category_technical_form_field_options (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_product_technical_field_value_options_option_id
|
||||
ON product_technical_field_value_options (option_id);
|
||||
@@ -0,0 +1,83 @@
|
||||
-- Meshkee CMS — polymorphic comments (product, blog, portfolio)
|
||||
|
||||
CREATE TABLE comments (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
business_id BIGINT NOT NULL,
|
||||
entity_type media_entity_type NOT NULL,
|
||||
entity_id BIGINT NOT NULL,
|
||||
author_name VARCHAR(255) NOT NULL,
|
||||
author_email VARCHAR(255),
|
||||
text TEXT NOT NULL,
|
||||
is_approved BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
approved_at TIMESTAMPTZ,
|
||||
approved_by BIGINT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT comments_business_id_fkey
|
||||
FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE,
|
||||
CONSTRAINT comments_approved_by_fkey
|
||||
FOREIGN KEY (approved_by) REFERENCES users (id) ON DELETE SET NULL,
|
||||
CONSTRAINT comments_text_nonempty CHECK (char_length(trim(text)) > 0),
|
||||
CONSTRAINT comments_author_name_nonempty CHECK (char_length(trim(author_name)) > 0)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_comments_business_approval
|
||||
ON comments (business_id, is_approved, created_at DESC);
|
||||
|
||||
CREATE INDEX idx_comments_entity
|
||||
ON comments (business_id, entity_type, entity_id, is_approved, created_at DESC);
|
||||
|
||||
CREATE TRIGGER comments_set_updated_at
|
||||
BEFORE UPDATE ON comments
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- permissions
|
||||
-- ---------------------------------------------------------------------------
|
||||
INSERT INTO permissions (name, slug, group_name, description) VALUES
|
||||
('View comments', 'comments.read', 'comments', 'View comments on business content'),
|
||||
('Approve comments', 'comments.approve', 'comments', 'Approve or reject comments'),
|
||||
('Delete comments', 'comments.delete', 'comments', 'Delete comments')
|
||||
ON CONFLICT (slug) DO NOTHING;
|
||||
|
||||
-- business_owner
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.slug LIKE 'comments.%'
|
||||
WHERE r.slug = 'business_owner'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- owner (legacy global role)
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.slug LIKE 'comments.%'
|
||||
WHERE r.slug = 'owner'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- admin
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.slug IN ('comments.read', 'comments.approve', 'comments.delete')
|
||||
WHERE r.slug = 'admin'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- editor: read + approve (moderate), no delete
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.slug IN ('comments.read', 'comments.approve')
|
||||
WHERE r.slug = 'editor'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- viewer: read only
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.slug = 'comments.read'
|
||||
WHERE r.slug = 'viewer'
|
||||
ON CONFLICT DO NOTHING;
|
||||
@@ -0,0 +1,83 @@
|
||||
-- Meshkee CMS — expert product reviews
|
||||
|
||||
CREATE TABLE expert_reviews (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
business_id BIGINT NOT NULL,
|
||||
product_id BIGINT NOT NULL,
|
||||
author_name VARCHAR(255) NOT NULL,
|
||||
author_email VARCHAR(255),
|
||||
rate SMALLINT NOT NULL,
|
||||
positive_points TEXT[] NOT NULL DEFAULT '{}',
|
||||
negative_points TEXT[] NOT NULL DEFAULT '{}',
|
||||
text TEXT NOT NULL,
|
||||
is_approved BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
approved_at TIMESTAMPTZ,
|
||||
approved_by BIGINT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT expert_reviews_business_id_fkey
|
||||
FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE,
|
||||
CONSTRAINT expert_reviews_product_id_fkey
|
||||
FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE CASCADE,
|
||||
CONSTRAINT expert_reviews_approved_by_fkey
|
||||
FOREIGN KEY (approved_by) REFERENCES users (id) ON DELETE SET NULL,
|
||||
CONSTRAINT expert_reviews_rate_range CHECK (rate >= 1 AND rate <= 10),
|
||||
CONSTRAINT expert_reviews_text_nonempty CHECK (char_length(trim(text)) > 0),
|
||||
CONSTRAINT expert_reviews_author_name_nonempty CHECK (char_length(trim(author_name)) > 0)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_expert_reviews_business_approval
|
||||
ON expert_reviews (business_id, is_approved, created_at DESC);
|
||||
|
||||
CREATE INDEX idx_expert_reviews_product
|
||||
ON expert_reviews (business_id, product_id, is_approved, created_at DESC);
|
||||
|
||||
CREATE TRIGGER expert_reviews_set_updated_at
|
||||
BEFORE UPDATE ON expert_reviews
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- permissions
|
||||
-- ---------------------------------------------------------------------------
|
||||
INSERT INTO permissions (name, slug, group_name, description) VALUES
|
||||
('View expert reviews', 'expert_reviews.read', 'expert_reviews', 'View expert product reviews'),
|
||||
('Approve expert reviews', 'expert_reviews.approve', 'expert_reviews', 'Approve or reject expert reviews'),
|
||||
('Delete expert reviews', 'expert_reviews.delete', 'expert_reviews', 'Delete expert reviews')
|
||||
ON CONFLICT (slug) DO NOTHING;
|
||||
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.slug LIKE 'expert_reviews.%'
|
||||
WHERE r.slug = 'business_owner'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.slug LIKE 'expert_reviews.%'
|
||||
WHERE r.slug = 'owner'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.slug IN ('expert_reviews.read', 'expert_reviews.approve', 'expert_reviews.delete')
|
||||
WHERE r.slug = 'admin'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.slug IN ('expert_reviews.read', 'expert_reviews.approve')
|
||||
WHERE r.slug = 'editor'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.slug = 'expert_reviews.read'
|
||||
WHERE r.slug = 'viewer'
|
||||
ON CONFLICT DO NOTHING;
|
||||
@@ -0,0 +1,38 @@
|
||||
-- Meshkee CMS — addresses owned by exactly one user or business
|
||||
|
||||
CREATE TABLE addresses (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
user_id BIGINT,
|
||||
business_id BIGINT,
|
||||
province VARCHAR(100) NOT NULL,
|
||||
city VARCHAR(100) NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
postal_code VARCHAR(20) NOT NULL,
|
||||
landline VARCHAR(30),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT addresses_user_id_fkey
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
CONSTRAINT addresses_business_id_fkey
|
||||
FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE,
|
||||
CONSTRAINT addresses_exactly_one_owner CHECK (
|
||||
(user_id IS NOT NULL AND business_id IS NULL)
|
||||
OR (user_id IS NULL AND business_id IS NOT NULL)
|
||||
),
|
||||
CONSTRAINT addresses_province_nonempty CHECK (char_length(trim(province)) > 0),
|
||||
CONSTRAINT addresses_city_nonempty CHECK (char_length(trim(city)) > 0),
|
||||
CONSTRAINT addresses_address_nonempty CHECK (char_length(trim(address)) > 0),
|
||||
CONSTRAINT addresses_postal_code_nonempty CHECK (char_length(trim(postal_code)) > 0),
|
||||
CONSTRAINT addresses_landline_nonempty_optional CHECK (
|
||||
landline IS NULL OR char_length(trim(landline)) > 0
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_addresses_user_id ON addresses (user_id) WHERE user_id IS NOT NULL;
|
||||
CREATE INDEX idx_addresses_business_id ON addresses (business_id) WHERE business_id IS NOT NULL;
|
||||
|
||||
CREATE TRIGGER addresses_set_updated_at
|
||||
BEFORE UPDATE ON addresses
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
@@ -0,0 +1,12 @@
|
||||
-- Business profile fields for dashboard / public storefront
|
||||
|
||||
ALTER TABLE businesses
|
||||
ADD COLUMN IF NOT EXISTS vision TEXT,
|
||||
ADD COLUMN IF NOT EXISTS emails JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
ADD COLUMN IF NOT EXISTS phone_numbers JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
ADD COLUMN IF NOT EXISTS social_media JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
ADD COLUMN IF NOT EXISTS logo_media_id BIGINT;
|
||||
|
||||
ALTER TABLE businesses
|
||||
ADD CONSTRAINT businesses_logo_media_id_fkey
|
||||
FOREIGN KEY (logo_media_id) REFERENCES media (id) ON DELETE SET NULL;
|
||||
@@ -0,0 +1,73 @@
|
||||
-- Meshkee CMS — location reference tree (country → province → city)
|
||||
|
||||
CREATE TYPE city_level AS ENUM ('country', 'province', 'city');
|
||||
|
||||
CREATE TABLE cities (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
parent_id BIGINT,
|
||||
level city_level NOT NULL,
|
||||
name_fa VARCHAR(255) NOT NULL,
|
||||
name_en VARCHAR(255) NOT NULL,
|
||||
landline_code VARCHAR(10),
|
||||
slug VARCHAR(100) NOT NULL,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT cities_slug_unique UNIQUE (slug),
|
||||
CONSTRAINT cities_parent_id_fkey
|
||||
FOREIGN KEY (parent_id) REFERENCES cities (id) ON DELETE CASCADE,
|
||||
CONSTRAINT cities_slug_format CHECK (slug ~ '^[a-z0-9]+(?:-[a-z0-9]+)*$'),
|
||||
CONSTRAINT cities_name_fa_nonempty CHECK (char_length(trim(name_fa)) > 0),
|
||||
CONSTRAINT cities_name_en_nonempty CHECK (char_length(trim(name_en)) > 0),
|
||||
CONSTRAINT cities_landline_code_nonempty_optional CHECK (
|
||||
landline_code IS NULL OR char_length(trim(landline_code)) > 0
|
||||
),
|
||||
CONSTRAINT cities_country_root CHECK (
|
||||
(level = 'country' AND parent_id IS NULL)
|
||||
OR (level <> 'country' AND parent_id IS NOT NULL)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_cities_parent_id ON cities (parent_id);
|
||||
CREATE INDEX idx_cities_level ON cities (level);
|
||||
CREATE INDEX idx_cities_level_parent ON cities (level, parent_id, sort_order);
|
||||
CREATE INDEX idx_cities_active ON cities (is_active) WHERE is_active = TRUE;
|
||||
|
||||
CREATE TRIGGER cities_set_updated_at
|
||||
BEFORE UPDATE ON cities
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
CREATE OR REPLACE FUNCTION cities_validate_parent_level()
|
||||
RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
parent_level city_level;
|
||||
BEGIN
|
||||
IF NEW.level = 'country' THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
SELECT level INTO parent_level FROM cities WHERE id = NEW.parent_id;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'parent city not found';
|
||||
END IF;
|
||||
|
||||
IF NEW.level = 'province' AND parent_level <> 'country' THEN
|
||||
RAISE EXCEPTION 'province parent must be a country';
|
||||
END IF;
|
||||
|
||||
IF NEW.level = 'city' AND parent_level <> 'province' THEN
|
||||
RAISE EXCEPTION 'city parent must be a province';
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER cities_validate_parent_level
|
||||
BEFORE INSERT OR UPDATE ON cities
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION cities_validate_parent_level();
|
||||
@@ -0,0 +1,20 @@
|
||||
-- Product-level variation value selections (which category options apply to a product).
|
||||
-- Store item variants are created later from these values.
|
||||
|
||||
CREATE TABLE product_variation_values (
|
||||
product_id BIGINT NOT NULL,
|
||||
variation_id BIGINT NOT NULL,
|
||||
option_id BIGINT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT product_variation_values_pkey PRIMARY KEY (product_id, option_id),
|
||||
CONSTRAINT product_variation_values_product_id_fkey
|
||||
FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE CASCADE,
|
||||
CONSTRAINT product_variation_values_variation_id_fkey
|
||||
FOREIGN KEY (variation_id) REFERENCES category_variations (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT product_variation_values_option_id_fkey
|
||||
FOREIGN KEY (option_id) REFERENCES category_variation_options (id) ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
CREATE INDEX idx_product_variation_values_variation_id
|
||||
ON product_variation_values (variation_id);
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Festival flag for store item variants
|
||||
ALTER TABLE product_variants
|
||||
ADD COLUMN is_festival BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
@@ -0,0 +1,7 @@
|
||||
-- Reward points earned when purchasing a store item variant (festival rewards — usage later)
|
||||
ALTER TABLE product_variants
|
||||
ADD COLUMN reward_points INTEGER;
|
||||
|
||||
ALTER TABLE product_variants
|
||||
ADD CONSTRAINT product_variants_reward_points_non_negative
|
||||
CHECK (reward_points IS NULL OR reward_points >= 0);
|
||||
@@ -0,0 +1,205 @@
|
||||
-- Meshkee CMS — shopping cart and orders
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- enums
|
||||
-- ---------------------------------------------------------------------------
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE order_status AS ENUM (
|
||||
'pending',
|
||||
'confirmed',
|
||||
'processing',
|
||||
'shipped',
|
||||
'delivered',
|
||||
'cancelled'
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE order_source AS ENUM ('website', 'admin');
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- carts (one per customer per business)
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE carts (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
business_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT carts_business_id_fkey
|
||||
FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE,
|
||||
CONSTRAINT carts_user_id_fkey
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
CONSTRAINT carts_business_user_unique UNIQUE (business_id, user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_carts_business_id ON carts (business_id);
|
||||
CREATE INDEX idx_carts_user_id ON carts (user_id);
|
||||
|
||||
CREATE TRIGGER carts_set_updated_at
|
||||
BEFORE UPDATE ON carts
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- cart items (product variants in cart)
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE cart_items (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
cart_id BIGINT NOT NULL,
|
||||
variant_id BIGINT NOT NULL,
|
||||
quantity INT NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT cart_items_cart_id_fkey
|
||||
FOREIGN KEY (cart_id) REFERENCES carts (id) ON DELETE CASCADE,
|
||||
CONSTRAINT cart_items_variant_id_fkey
|
||||
FOREIGN KEY (variant_id) REFERENCES product_variants (id) ON DELETE CASCADE,
|
||||
CONSTRAINT cart_items_cart_variant_unique UNIQUE (cart_id, variant_id),
|
||||
CONSTRAINT cart_items_quantity_positive CHECK (quantity > 0)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_cart_items_cart_id ON cart_items (cart_id);
|
||||
CREATE INDEX idx_cart_items_variant_id ON cart_items (variant_id);
|
||||
|
||||
CREATE TRIGGER cart_items_set_updated_at
|
||||
BEFORE UPDATE ON cart_items
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- orders
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE orders (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
business_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
order_number VARCHAR(30) NOT NULL,
|
||||
status order_status NOT NULL DEFAULT 'pending',
|
||||
source order_source NOT NULL DEFAULT 'website',
|
||||
subtotal NUMERIC(12, 2) NOT NULL DEFAULT 0,
|
||||
shipping_total NUMERIC(12, 2) NOT NULL DEFAULT 0,
|
||||
discount_total NUMERIC(12, 2) NOT NULL DEFAULT 0,
|
||||
total NUMERIC(12, 2) NOT NULL DEFAULT 0,
|
||||
shipping_address JSONB NOT NULL DEFAULT '{}',
|
||||
address_id BIGINT,
|
||||
customer_notes TEXT,
|
||||
admin_notes TEXT,
|
||||
created_by BIGINT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT orders_business_id_fkey
|
||||
FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE,
|
||||
CONSTRAINT orders_user_id_fkey
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT orders_address_id_fkey
|
||||
FOREIGN KEY (address_id) REFERENCES addresses (id) ON DELETE SET NULL,
|
||||
CONSTRAINT orders_created_by_fkey
|
||||
FOREIGN KEY (created_by) REFERENCES users (id) ON DELETE SET NULL,
|
||||
CONSTRAINT orders_business_order_number_unique UNIQUE (business_id, order_number),
|
||||
CONSTRAINT orders_subtotal_non_negative CHECK (subtotal >= 0),
|
||||
CONSTRAINT orders_shipping_total_non_negative CHECK (shipping_total >= 0),
|
||||
CONSTRAINT orders_discount_total_non_negative CHECK (discount_total >= 0),
|
||||
CONSTRAINT orders_total_non_negative CHECK (total >= 0)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_orders_business_created
|
||||
ON orders (business_id, created_at DESC);
|
||||
|
||||
CREATE INDEX idx_orders_business_user
|
||||
ON orders (business_id, user_id, created_at DESC);
|
||||
|
||||
CREATE INDEX idx_orders_business_status
|
||||
ON orders (business_id, status, created_at DESC);
|
||||
|
||||
CREATE TRIGGER orders_set_updated_at
|
||||
BEFORE UPDATE ON orders
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- order items (line items with price snapshots)
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE order_items (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
order_id BIGINT NOT NULL,
|
||||
variant_id BIGINT,
|
||||
product_id BIGINT NOT NULL,
|
||||
product_title VARCHAR(255) NOT NULL,
|
||||
variant_sku VARCHAR(100),
|
||||
unit_price NUMERIC(12, 2) NOT NULL,
|
||||
compare_at_price NUMERIC(12, 2),
|
||||
quantity INT NOT NULL,
|
||||
line_total NUMERIC(12, 2) NOT NULL,
|
||||
selections_snapshot JSONB NOT NULL DEFAULT '[]',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT order_items_order_id_fkey
|
||||
FOREIGN KEY (order_id) REFERENCES orders (id) ON DELETE CASCADE,
|
||||
CONSTRAINT order_items_variant_id_fkey
|
||||
FOREIGN KEY (variant_id) REFERENCES product_variants (id) ON DELETE SET NULL,
|
||||
CONSTRAINT order_items_product_id_fkey
|
||||
FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT order_items_quantity_positive CHECK (quantity > 0),
|
||||
CONSTRAINT order_items_unit_price_non_negative CHECK (unit_price >= 0),
|
||||
CONSTRAINT order_items_line_total_non_negative CHECK (line_total >= 0)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_order_items_order_id ON order_items (order_id);
|
||||
CREATE INDEX idx_order_items_variant_id ON order_items (variant_id);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- permissions
|
||||
-- ---------------------------------------------------------------------------
|
||||
INSERT INTO permissions (name, slug, group_name, description) VALUES
|
||||
('Create orders', 'orders.create', 'orders', 'Create orders on behalf of customers'),
|
||||
('Update orders', 'orders.update', 'orders', 'Update order status and admin notes')
|
||||
ON CONFLICT (slug) DO NOTHING;
|
||||
|
||||
-- business_owner
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.slug IN ('orders.read', 'orders.create', 'orders.update')
|
||||
WHERE r.slug = 'business_owner'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- owner (legacy global role)
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.slug IN ('orders.read', 'orders.create', 'orders.update')
|
||||
WHERE r.slug = 'owner'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- admin
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.slug IN ('orders.read', 'orders.create', 'orders.update')
|
||||
WHERE r.slug = 'admin'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- editor: read + update status
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.slug IN ('orders.read', 'orders.update')
|
||||
WHERE r.slug = 'editor'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- viewer: read only
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.slug = 'orders.read'
|
||||
WHERE r.slug = 'viewer'
|
||||
ON CONFLICT DO NOTHING;
|
||||
@@ -0,0 +1,198 @@
|
||||
-- Meshkee CMS — store items (one per product) and store item variants (purchasable SKUs)
|
||||
-- Replaces product_variants / product_variant_selections
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- store_items (one listing per product in the shop)
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE store_items (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
business_id BIGINT NOT NULL,
|
||||
product_id BIGINT NOT NULL,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT store_items_business_id_fkey
|
||||
FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE,
|
||||
CONSTRAINT store_items_product_id_fkey
|
||||
FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE CASCADE,
|
||||
CONSTRAINT store_items_business_product_unique UNIQUE (business_id, product_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_store_items_business_id ON store_items (business_id);
|
||||
CREATE INDEX idx_store_items_product_id ON store_items (product_id);
|
||||
|
||||
CREATE TRIGGER store_items_set_updated_at
|
||||
BEFORE UPDATE ON store_items
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- store_item_variants (purchasable combinations with price & stock)
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE store_item_variants (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
store_item_id BIGINT NOT NULL,
|
||||
business_id BIGINT NOT NULL,
|
||||
sku VARCHAR(100),
|
||||
price NUMERIC(12, 2),
|
||||
compare_at_price NUMERIC(12, 2),
|
||||
stock_quantity INTEGER,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
is_festival BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
reward_points INTEGER,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
legacy_product_variant_id BIGINT,
|
||||
|
||||
CONSTRAINT store_item_variants_store_item_id_fkey
|
||||
FOREIGN KEY (store_item_id) REFERENCES store_items (id) ON DELETE CASCADE,
|
||||
CONSTRAINT store_item_variants_business_id_fkey
|
||||
FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE,
|
||||
CONSTRAINT store_item_variants_stock_non_negative
|
||||
CHECK (stock_quantity IS NULL OR stock_quantity >= 0),
|
||||
CONSTRAINT store_item_variants_reward_points_non_negative
|
||||
CHECK (reward_points IS NULL OR reward_points >= 0)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_store_item_variants_store_item_id ON store_item_variants (store_item_id);
|
||||
CREATE INDEX idx_store_item_variants_business_id ON store_item_variants (business_id);
|
||||
CREATE UNIQUE INDEX idx_store_item_variants_legacy_id
|
||||
ON store_item_variants (legacy_product_variant_id)
|
||||
WHERE legacy_product_variant_id IS NOT NULL;
|
||||
|
||||
CREATE TRIGGER store_item_variants_set_updated_at
|
||||
BEFORE UPDATE ON store_item_variants
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- store_item_variant_selections
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE store_item_variant_selections (
|
||||
variant_id BIGINT NOT NULL,
|
||||
variation_id BIGINT NOT NULL,
|
||||
option_id BIGINT NOT NULL,
|
||||
|
||||
CONSTRAINT store_item_variant_selections_pkey PRIMARY KEY (variant_id, variation_id),
|
||||
CONSTRAINT store_item_variant_selections_variant_id_fkey
|
||||
FOREIGN KEY (variant_id) REFERENCES store_item_variants (id) ON DELETE CASCADE,
|
||||
CONSTRAINT store_item_variant_selections_variation_id_fkey
|
||||
FOREIGN KEY (variation_id) REFERENCES category_variations (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT store_item_variant_selections_option_id_fkey
|
||||
FOREIGN KEY (option_id) REFERENCES category_variation_options (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT store_item_variant_selections_unique_option
|
||||
UNIQUE (variant_id, option_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_store_item_variant_selections_option_id
|
||||
ON store_item_variant_selections (option_id);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- migrate product_variants → store_items + store_item_variants
|
||||
-- ---------------------------------------------------------------------------
|
||||
INSERT INTO store_items (business_id, product_id, is_active, sort_order, created_at, updated_at)
|
||||
SELECT DISTINCT
|
||||
pv.business_id,
|
||||
pv.product_id,
|
||||
TRUE,
|
||||
0,
|
||||
NOW(),
|
||||
NOW()
|
||||
FROM product_variants pv;
|
||||
|
||||
INSERT INTO store_item_variants (
|
||||
store_item_id,
|
||||
business_id,
|
||||
sku,
|
||||
price,
|
||||
compare_at_price,
|
||||
stock_quantity,
|
||||
is_active,
|
||||
is_festival,
|
||||
reward_points,
|
||||
sort_order,
|
||||
created_at,
|
||||
updated_at,
|
||||
legacy_product_variant_id
|
||||
)
|
||||
SELECT
|
||||
si.id,
|
||||
pv.business_id,
|
||||
pv.sku,
|
||||
pv.price,
|
||||
pv.compare_at_price,
|
||||
pv.stock_quantity,
|
||||
pv.is_active,
|
||||
pv.is_festival,
|
||||
pv.reward_points,
|
||||
pv.sort_order,
|
||||
pv.created_at,
|
||||
pv.updated_at,
|
||||
pv.id
|
||||
FROM product_variants pv
|
||||
JOIN store_items si
|
||||
ON si.business_id = pv.business_id
|
||||
AND si.product_id = pv.product_id;
|
||||
|
||||
INSERT INTO store_item_variant_selections (variant_id, variation_id, option_id)
|
||||
SELECT
|
||||
siv.id,
|
||||
pvs.variation_id,
|
||||
pvs.option_id
|
||||
FROM product_variant_selections pvs
|
||||
JOIN store_item_variants siv
|
||||
ON siv.legacy_product_variant_id = pvs.variant_id;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- repoint cart_items and order_items to store_item_variants
|
||||
-- ---------------------------------------------------------------------------
|
||||
ALTER TABLE cart_items DROP CONSTRAINT cart_items_variant_id_fkey;
|
||||
ALTER TABLE cart_items RENAME COLUMN variant_id TO store_item_variant_id;
|
||||
|
||||
UPDATE cart_items ci
|
||||
SET store_item_variant_id = siv.id
|
||||
FROM store_item_variants siv
|
||||
WHERE siv.legacy_product_variant_id = ci.store_item_variant_id;
|
||||
|
||||
ALTER TABLE cart_items
|
||||
ADD CONSTRAINT cart_items_store_item_variant_id_fkey
|
||||
FOREIGN KEY (store_item_variant_id) REFERENCES store_item_variants (id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE cart_items
|
||||
DROP CONSTRAINT IF EXISTS cart_items_cart_variant_unique;
|
||||
|
||||
ALTER TABLE cart_items
|
||||
ADD CONSTRAINT cart_items_cart_store_item_variant_unique
|
||||
UNIQUE (cart_id, store_item_variant_id);
|
||||
|
||||
DROP INDEX IF EXISTS idx_cart_items_variant_id;
|
||||
CREATE INDEX idx_cart_items_store_item_variant_id
|
||||
ON cart_items (store_item_variant_id);
|
||||
|
||||
ALTER TABLE order_items DROP CONSTRAINT order_items_variant_id_fkey;
|
||||
ALTER TABLE order_items RENAME COLUMN variant_id TO store_item_variant_id;
|
||||
|
||||
UPDATE order_items oi
|
||||
SET store_item_variant_id = siv.id
|
||||
FROM store_item_variants siv
|
||||
WHERE siv.legacy_product_variant_id = oi.store_item_variant_id;
|
||||
|
||||
ALTER TABLE order_items
|
||||
ADD CONSTRAINT order_items_store_item_variant_id_fkey
|
||||
FOREIGN KEY (store_item_variant_id) REFERENCES store_item_variants (id) ON DELETE SET NULL;
|
||||
|
||||
DROP INDEX IF EXISTS idx_order_items_variant_id;
|
||||
CREATE INDEX idx_order_items_store_item_variant_id
|
||||
ON order_items (store_item_variant_id);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- drop legacy tables
|
||||
-- ---------------------------------------------------------------------------
|
||||
DROP TABLE product_variant_selections;
|
||||
DROP TABLE product_variants;
|
||||
|
||||
ALTER TABLE store_item_variants DROP COLUMN legacy_product_variant_id;
|
||||
DROP INDEX IF EXISTS idx_store_item_variants_legacy_id;
|
||||
@@ -0,0 +1,6 @@
|
||||
-- Business-scoped customer enable/disable (does not deactivate the global user account)
|
||||
ALTER TABLE business_customers
|
||||
ADD COLUMN IF NOT EXISTS is_enabled BOOLEAN NOT NULL DEFAULT TRUE;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_business_customers_is_enabled
|
||||
ON business_customers (business_id, is_enabled);
|
||||
@@ -0,0 +1,114 @@
|
||||
-- Meshkee CMS — payment transactions
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- enums
|
||||
-- ---------------------------------------------------------------------------
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE transaction_type AS ENUM (
|
||||
'pos',
|
||||
'cash',
|
||||
'transfer',
|
||||
'e_payment_gate'
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE transaction_status AS ENUM (
|
||||
'pending',
|
||||
'completed',
|
||||
'failed',
|
||||
'refunded'
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- transactions
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE transactions (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
business_id BIGINT NOT NULL,
|
||||
order_id BIGINT,
|
||||
user_id BIGINT NOT NULL,
|
||||
type transaction_type NOT NULL,
|
||||
status transaction_status NOT NULL DEFAULT 'pending',
|
||||
amount NUMERIC(12, 2) NOT NULL,
|
||||
pos_type VARCHAR(100),
|
||||
gateway_type VARCHAR(100),
|
||||
transfer_account VARCHAR(255),
|
||||
transfer_ref_number VARCHAR(100),
|
||||
notes TEXT,
|
||||
created_by BIGINT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT transactions_business_id_fkey
|
||||
FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE,
|
||||
CONSTRAINT transactions_order_id_fkey
|
||||
FOREIGN KEY (order_id) REFERENCES orders (id) ON DELETE SET NULL,
|
||||
CONSTRAINT transactions_user_id_fkey
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT transactions_created_by_fkey
|
||||
FOREIGN KEY (created_by) REFERENCES users (id) ON DELETE SET NULL,
|
||||
CONSTRAINT transactions_amount_non_negative CHECK (amount >= 0),
|
||||
CONSTRAINT transactions_type_fields_check CHECK (
|
||||
(type = 'pos'
|
||||
AND pos_type IS NOT NULL
|
||||
AND gateway_type IS NULL
|
||||
AND transfer_account IS NULL
|
||||
AND transfer_ref_number IS NULL)
|
||||
OR (type = 'cash'
|
||||
AND pos_type IS NULL
|
||||
AND gateway_type IS NULL
|
||||
AND transfer_account IS NULL
|
||||
AND transfer_ref_number IS NULL)
|
||||
OR (type = 'transfer'
|
||||
AND transfer_account IS NOT NULL
|
||||
AND transfer_ref_number IS NOT NULL
|
||||
AND pos_type IS NULL
|
||||
AND gateway_type IS NULL)
|
||||
OR (type = 'e_payment_gate'
|
||||
AND gateway_type IS NOT NULL
|
||||
AND pos_type IS NULL
|
||||
AND transfer_account IS NULL
|
||||
AND transfer_ref_number IS NULL)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_transactions_business_created
|
||||
ON transactions (business_id, created_at DESC);
|
||||
|
||||
CREATE INDEX idx_transactions_order_id
|
||||
ON transactions (order_id);
|
||||
|
||||
CREATE INDEX idx_transactions_business_user
|
||||
ON transactions (business_id, user_id, created_at DESC);
|
||||
|
||||
CREATE TRIGGER transactions_set_updated_at
|
||||
BEFORE UPDATE ON transactions
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- permissions
|
||||
-- ---------------------------------------------------------------------------
|
||||
INSERT INTO permissions (name, slug, group_name, description) VALUES
|
||||
('View transactions', 'transactions.read', 'transactions', 'View payment transactions')
|
||||
ON CONFLICT (slug) DO NOTHING;
|
||||
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.slug = 'transactions.read'
|
||||
WHERE r.slug IN ('business_owner', 'owner', 'admin')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.slug = 'transactions.read'
|
||||
WHERE r.slug = 'editor'
|
||||
ON CONFLICT DO NOTHING;
|
||||
@@ -0,0 +1,7 @@
|
||||
-- Meshkee CMS — order fulfillment process step (from business store settings)
|
||||
|
||||
ALTER TABLE orders
|
||||
ADD COLUMN IF NOT EXISTS process_step_id VARCHAR(64) NOT NULL DEFAULT 'processing';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_orders_business_process_step
|
||||
ON orders (business_id, process_step_id);
|
||||
@@ -0,0 +1,63 @@
|
||||
-- Meshkee CMS — saved operator shopping cards (draft orders)
|
||||
|
||||
CREATE TABLE shopping_cards (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
business_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
subtotal NUMERIC(12, 2) NOT NULL DEFAULT 0,
|
||||
total NUMERIC(12, 2) NOT NULL DEFAULT 0,
|
||||
created_by BIGINT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT shopping_cards_business_id_fkey
|
||||
FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE,
|
||||
CONSTRAINT shopping_cards_user_id_fkey
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT shopping_cards_created_by_fkey
|
||||
FOREIGN KEY (created_by) REFERENCES users (id) ON DELETE SET NULL,
|
||||
CONSTRAINT shopping_cards_subtotal_non_negative CHECK (subtotal >= 0),
|
||||
CONSTRAINT shopping_cards_total_non_negative CHECK (total >= 0)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_shopping_cards_business_created
|
||||
ON shopping_cards (business_id, created_at DESC);
|
||||
|
||||
CREATE INDEX idx_shopping_cards_business_user
|
||||
ON shopping_cards (business_id, user_id, created_at DESC);
|
||||
|
||||
CREATE TRIGGER shopping_cards_set_updated_at
|
||||
BEFORE UPDATE ON shopping_cards
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
CREATE TABLE shopping_card_items (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
shopping_card_id BIGINT NOT NULL,
|
||||
store_item_variant_id BIGINT,
|
||||
product_id BIGINT NOT NULL,
|
||||
product_title VARCHAR(255) NOT NULL,
|
||||
variant_sku VARCHAR(100),
|
||||
unit_price NUMERIC(12, 2) NOT NULL,
|
||||
compare_at_price NUMERIC(12, 2),
|
||||
quantity INT NOT NULL,
|
||||
line_total NUMERIC(12, 2) NOT NULL,
|
||||
selections_snapshot JSONB NOT NULL DEFAULT '[]',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT shopping_card_items_card_id_fkey
|
||||
FOREIGN KEY (shopping_card_id) REFERENCES shopping_cards (id) ON DELETE CASCADE,
|
||||
CONSTRAINT shopping_card_items_variant_id_fkey
|
||||
FOREIGN KEY (store_item_variant_id) REFERENCES store_item_variants (id) ON DELETE SET NULL,
|
||||
CONSTRAINT shopping_card_items_product_id_fkey
|
||||
FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE RESTRICT,
|
||||
CONSTRAINT shopping_card_items_quantity_positive CHECK (quantity > 0),
|
||||
CONSTRAINT shopping_card_items_unit_price_non_negative CHECK (unit_price >= 0),
|
||||
CONSTRAINT shopping_card_items_line_total_non_negative CHECK (line_total >= 0)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_shopping_card_items_card_id
|
||||
ON shopping_card_items (shopping_card_id);
|
||||
|
||||
CREATE INDEX idx_shopping_card_items_variant_id
|
||||
ON shopping_card_items (store_item_variant_id);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- Meshkee CMS — blog post type (news | article | blog)
|
||||
|
||||
CREATE TYPE blog_post_type AS ENUM ('news', 'article', 'blog');
|
||||
|
||||
ALTER TABLE blogs
|
||||
ADD COLUMN post_type blog_post_type NOT NULL DEFAULT 'blog';
|
||||
|
||||
CREATE INDEX idx_blogs_business_post_type
|
||||
ON blogs (business_id, post_type);
|
||||
@@ -0,0 +1,37 @@
|
||||
-- Meshkee CMS — curated store specials (e.g. special sale, best sellers)
|
||||
|
||||
CREATE TABLE store_specials (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
business_id BIGINT NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT store_specials_business_id_fkey
|
||||
FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_store_specials_business_id ON store_specials (business_id);
|
||||
|
||||
CREATE TRIGGER store_specials_set_updated_at
|
||||
BEFORE UPDATE ON store_specials
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
CREATE TABLE store_special_items (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
special_id BIGINT NOT NULL,
|
||||
store_item_id BIGINT NOT NULL,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
CONSTRAINT store_special_items_special_id_fkey
|
||||
FOREIGN KEY (special_id) REFERENCES store_specials (id) ON DELETE CASCADE,
|
||||
CONSTRAINT store_special_items_store_item_id_fkey
|
||||
FOREIGN KEY (store_item_id) REFERENCES store_items (id) ON DELETE CASCADE,
|
||||
CONSTRAINT store_special_items_unique UNIQUE (special_id, store_item_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_store_special_items_special_id ON store_special_items (special_id);
|
||||
CREATE INDEX idx_store_special_items_store_item_id ON store_special_items (store_item_id);
|
||||
@@ -0,0 +1,27 @@
|
||||
-- Website contact form submissions (per business)
|
||||
|
||||
CREATE TABLE contact_submissions (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
business_id BIGINT NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255),
|
||||
cell_number VARCHAR(20),
|
||||
text TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT contact_submissions_business_id_fkey
|
||||
FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE,
|
||||
CONSTRAINT contact_submissions_title_nonempty CHECK (char_length(trim(title)) > 0),
|
||||
CONSTRAINT contact_submissions_name_nonempty CHECK (char_length(trim(name)) > 0),
|
||||
CONSTRAINT contact_submissions_text_nonempty CHECK (char_length(trim(text)) > 0)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_contact_submissions_business_created
|
||||
ON contact_submissions (business_id, created_at DESC);
|
||||
|
||||
CREATE TRIGGER contact_submissions_set_updated_at
|
||||
BEFORE UPDATE ON contact_submissions
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
@@ -0,0 +1,42 @@
|
||||
-- Customer product favorites (per business)
|
||||
|
||||
CREATE TABLE favorites (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
business_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
product_id BIGINT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT favorites_business_id_fkey
|
||||
FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE,
|
||||
CONSTRAINT favorites_user_id_fkey
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
CONSTRAINT favorites_product_id_fkey
|
||||
FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE CASCADE,
|
||||
CONSTRAINT favorites_business_user_product_unique
|
||||
UNIQUE (business_id, user_id, product_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_favorites_business_user_created
|
||||
ON favorites (business_id, user_id, created_at DESC);
|
||||
|
||||
CREATE INDEX idx_favorites_product_id
|
||||
ON favorites (product_id);
|
||||
|
||||
CREATE TRIGGER favorites_set_updated_at
|
||||
BEFORE UPDATE ON favorites
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
INSERT INTO permissions (name, slug, group_name, description) VALUES
|
||||
('Add own favorites', 'favorites.create', 'favorites', 'Add products to own favorites'),
|
||||
('Remove own favorites', 'favorites.delete', 'favorites', 'Remove products from own favorites')
|
||||
ON CONFLICT (slug) DO NOTHING;
|
||||
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.slug IN ('favorites.create', 'favorites.delete')
|
||||
WHERE r.slug = 'customer'
|
||||
ON CONFLICT DO NOTHING;
|
||||
@@ -0,0 +1,66 @@
|
||||
-- Meshkee CMS — product brands (per business)
|
||||
|
||||
CREATE TABLE brands (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
business_id BIGINT NOT NULL,
|
||||
name_en VARCHAR(255) NOT NULL,
|
||||
name_fa VARCHAR(255),
|
||||
image_media_id BIGINT,
|
||||
about TEXT,
|
||||
slug VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT brands_business_id_fkey
|
||||
FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE,
|
||||
CONSTRAINT brands_image_media_id_fkey
|
||||
FOREIGN KEY (image_media_id) REFERENCES media (id) ON DELETE SET NULL,
|
||||
CONSTRAINT brands_business_slug_unique UNIQUE (business_id, slug),
|
||||
CONSTRAINT brands_name_en_nonempty CHECK (char_length(trim(name_en)) > 0)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_brands_business_id ON brands (business_id);
|
||||
CREATE INDEX idx_brands_image_media_id ON brands (image_media_id);
|
||||
|
||||
CREATE TRIGGER brands_set_updated_at
|
||||
BEFORE UPDATE ON brands
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
ALTER TABLE products
|
||||
ADD COLUMN brand_id BIGINT,
|
||||
ADD CONSTRAINT products_brand_id_fkey
|
||||
FOREIGN KEY (brand_id) REFERENCES brands (id) ON DELETE SET NULL;
|
||||
|
||||
CREATE INDEX idx_products_brand_id ON products (brand_id);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- permissions
|
||||
-- ---------------------------------------------------------------------------
|
||||
INSERT INTO permissions (name, slug, group_name, description) VALUES
|
||||
('View brands', 'brands.read', 'brands', 'View product brands'),
|
||||
('Create brands', 'brands.create', 'brands', 'Create product brands'),
|
||||
('Update brands', 'brands.update', 'brands', 'Edit product brands'),
|
||||
('Delete brands', 'brands.delete', 'brands', 'Delete product brands')
|
||||
ON CONFLICT (slug) DO NOTHING;
|
||||
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.slug LIKE 'brands.%'
|
||||
WHERE r.slug IN ('business_owner', 'owner', 'admin')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.slug IN ('brands.read', 'brands.create', 'brands.update')
|
||||
WHERE r.slug = 'editor'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.slug = 'brands.read'
|
||||
WHERE r.slug = 'viewer'
|
||||
ON CONFLICT DO NOTHING;
|
||||
@@ -0,0 +1,177 @@
|
||||
-- Meshkee CMS — website homepage widgets: category/brand groups and sliders
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- brands: user-defined list order
|
||||
-- ---------------------------------------------------------------------------
|
||||
ALTER TABLE brands
|
||||
ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
CREATE INDEX idx_brands_business_sort_order
|
||||
ON brands (business_id, sort_order);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- website category groups (curated category rows for the storefront)
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE website_category_groups (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
business_id BIGINT NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT website_category_groups_business_id_fkey
|
||||
FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_website_category_groups_business_id
|
||||
ON website_category_groups (business_id);
|
||||
|
||||
CREATE TRIGGER website_category_groups_set_updated_at
|
||||
BEFORE UPDATE ON website_category_groups
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
CREATE TABLE website_category_group_items (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
group_id BIGINT NOT NULL,
|
||||
category_id BIGINT NOT NULL,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
CONSTRAINT website_category_group_items_group_id_fkey
|
||||
FOREIGN KEY (group_id) REFERENCES website_category_groups (id) ON DELETE CASCADE,
|
||||
CONSTRAINT website_category_group_items_category_id_fkey
|
||||
FOREIGN KEY (category_id) REFERENCES categories (id) ON DELETE CASCADE,
|
||||
CONSTRAINT website_category_group_items_unique UNIQUE (group_id, category_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_website_category_group_items_group_id
|
||||
ON website_category_group_items (group_id);
|
||||
|
||||
CREATE INDEX idx_website_category_group_items_category_id
|
||||
ON website_category_group_items (category_id);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- website brand groups (curated brand rows for the storefront)
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE website_brand_groups (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
business_id BIGINT NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT website_brand_groups_business_id_fkey
|
||||
FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_website_brand_groups_business_id
|
||||
ON website_brand_groups (business_id);
|
||||
|
||||
CREATE TRIGGER website_brand_groups_set_updated_at
|
||||
BEFORE UPDATE ON website_brand_groups
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
CREATE TABLE website_brand_group_items (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
group_id BIGINT NOT NULL,
|
||||
brand_id BIGINT NOT NULL,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
CONSTRAINT website_brand_group_items_group_id_fkey
|
||||
FOREIGN KEY (group_id) REFERENCES website_brand_groups (id) ON DELETE CASCADE,
|
||||
CONSTRAINT website_brand_group_items_brand_id_fkey
|
||||
FOREIGN KEY (brand_id) REFERENCES brands (id) ON DELETE CASCADE,
|
||||
CONSTRAINT website_brand_group_items_unique UNIQUE (group_id, brand_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_website_brand_group_items_group_id
|
||||
ON website_brand_group_items (group_id);
|
||||
|
||||
CREATE INDEX idx_website_brand_group_items_brand_id
|
||||
ON website_brand_group_items (brand_id);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- website sliders (multiple sliders per business, each with ordered slides)
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE website_sliders (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
business_id BIGINT NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT website_sliders_business_id_fkey
|
||||
FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_website_sliders_business_id
|
||||
ON website_sliders (business_id);
|
||||
|
||||
CREATE TRIGGER website_sliders_set_updated_at
|
||||
BEFORE UPDATE ON website_sliders
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
CREATE TABLE website_slider_slides (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
slider_id BIGINT NOT NULL,
|
||||
image_media_id BIGINT NOT NULL,
|
||||
title VARCHAR(255),
|
||||
link_url VARCHAR(2048),
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT website_slider_slides_slider_id_fkey
|
||||
FOREIGN KEY (slider_id) REFERENCES website_sliders (id) ON DELETE CASCADE,
|
||||
CONSTRAINT website_slider_slides_image_media_id_fkey
|
||||
FOREIGN KEY (image_media_id) REFERENCES media (id) ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
CREATE INDEX idx_website_slider_slides_slider_id
|
||||
ON website_slider_slides (slider_id);
|
||||
|
||||
CREATE INDEX idx_website_slider_slides_image_media_id
|
||||
ON website_slider_slides (image_media_id);
|
||||
|
||||
CREATE TRIGGER website_slider_slides_set_updated_at
|
||||
BEFORE UPDATE ON website_slider_slides
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- permissions
|
||||
-- ---------------------------------------------------------------------------
|
||||
INSERT INTO permissions (name, slug, group_name, description) VALUES
|
||||
('View website widgets', 'website.read', 'website', 'View homepage category/brand groups and sliders'),
|
||||
('Manage website widgets', 'website.update', 'website', 'Create and edit homepage category/brand groups and sliders')
|
||||
ON CONFLICT (slug) DO NOTHING;
|
||||
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.slug LIKE 'website.%'
|
||||
WHERE r.slug IN ('business_owner', 'owner', 'admin')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.slug IN ('website.read', 'website.update')
|
||||
WHERE r.slug = 'editor'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.slug = 'website.read'
|
||||
WHERE r.slug = 'viewer'
|
||||
ON CONFLICT DO NOTHING;
|
||||
@@ -0,0 +1,4 @@
|
||||
-- User-defined label for saved addresses (e.g. home, office)
|
||||
|
||||
ALTER TABLE addresses
|
||||
ADD COLUMN IF NOT EXISTS label VARCHAR(100);
|
||||
@@ -0,0 +1,7 @@
|
||||
-- Postal code is optional for saved addresses
|
||||
|
||||
ALTER TABLE addresses
|
||||
DROP CONSTRAINT IF EXISTS addresses_postal_code_nonempty;
|
||||
|
||||
ALTER TABLE addresses
|
||||
ALTER COLUMN postal_code DROP NOT NULL;
|
||||
@@ -0,0 +1,17 @@
|
||||
-- Favicon generated from business logo for dashboards and storefront
|
||||
|
||||
ALTER TABLE businesses
|
||||
ADD COLUMN IF NOT EXISTS favicon_media_id BIGINT;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'businesses_favicon_media_id_fkey'
|
||||
) THEN
|
||||
ALTER TABLE businesses
|
||||
ADD CONSTRAINT businesses_favicon_media_id_fkey
|
||||
FOREIGN KEY (favicon_media_id) REFERENCES media (id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
END $$;
|
||||
Reference in New Issue
Block a user