mirror of
https://git.meshkee.com/Meshkee/backend.git
synced 2026-08-11 22:30:59 +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:
Executable
+27
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
MIGRATIONS_DIR="$ROOT_DIR/database/migrations"
|
||||
CONTAINER="${POSTGRES_CONTAINER:-meshkee-postgres}"
|
||||
DB_USER="${POSTGRES_USER:-meshkee}"
|
||||
DB_NAME="${POSTGRES_DB:-meshkee_cms}"
|
||||
|
||||
"$ROOT_DIR/database/wait-for-postgres.sh"
|
||||
|
||||
run_migration() {
|
||||
local file="$1"
|
||||
echo "→ Running $(basename "$file")"
|
||||
docker exec -i "$CONTAINER" psql -U "$DB_USER" -d "$DB_NAME" < "$file"
|
||||
}
|
||||
|
||||
if [[ $# -gt 0 ]]; then
|
||||
run_migration "$1"
|
||||
else
|
||||
for file in "$MIGRATIONS_DIR"/*.sql; do
|
||||
[[ -f "$file" ]] || continue
|
||||
run_migration "$file"
|
||||
done
|
||||
fi
|
||||
|
||||
echo "Done."
|
||||
@@ -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 $$;
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
SEEDS_DIR="$ROOT_DIR/database/seeds"
|
||||
CONTAINER="${POSTGRES_CONTAINER:-meshkee-postgres}"
|
||||
DB_USER="${POSTGRES_USER:-meshkee}"
|
||||
DB_NAME="${POSTGRES_DB:-meshkee_cms}"
|
||||
|
||||
"$ROOT_DIR/database/wait-for-postgres.sh"
|
||||
|
||||
run_seed() {
|
||||
local file="$1"
|
||||
echo "→ Seeding $(basename "$file")"
|
||||
docker exec -i "$CONTAINER" psql -U "$DB_USER" -d "$DB_NAME" < "$file"
|
||||
}
|
||||
|
||||
if [[ $# -gt 0 ]]; then
|
||||
run_seed "$1"
|
||||
else
|
||||
for file in "$SEEDS_DIR"/*.sql; do
|
||||
[[ -f "$file" ]] || continue
|
||||
run_seed "$file"
|
||||
done
|
||||
fi
|
||||
|
||||
echo "Seed complete."
|
||||
@@ -0,0 +1,183 @@
|
||||
-- Sample seed data for local development / DataGrip testing
|
||||
-- Password for all users: password
|
||||
--
|
||||
-- User types:
|
||||
-- 1 Ali — super_admin
|
||||
-- 2 Reza — business_owner (Meshkee Demo Shop)
|
||||
-- 3 Sara — business_owner (Creative Studio)
|
||||
-- 4 Mina — customer on shop-a.local
|
||||
-- 5 Amir — customer on studio-b.local
|
||||
|
||||
INSERT INTO users (id, cell_number, password_hash, email, first_name, last_name, cell_verified_at) VALUES
|
||||
(1, '+989121111111', '$2b$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'ali@meshkee.demo', 'Ali', 'Hassani', NOW()),
|
||||
(2, '+989122222222', '$2b$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'reza@shop.demo', 'Reza', 'Ahmadi', NOW()),
|
||||
(3, '+989123333333', '$2b$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'sara@studio.demo', 'Sara', 'Karimi', NOW()),
|
||||
(4, '+989124444444', '$2b$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'mina@customer.demo', 'Mina', 'Salehi', NOW()),
|
||||
(5, '+989125555555', '$2b$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'amir@customer.demo', 'Amir', 'Jafari', NOW())
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO businesses (id, name, name_fa, about, slug, description) VALUES
|
||||
(1, 'Meshkee Demo Shop', 'فروشگاه دمو مشکی', 'فروشگاه آنلاین نمونه برای تست سیستم', 'meshkee-demo-shop', 'Sample e-commerce business'),
|
||||
(2, 'Creative Studio', 'استودیو خلاق', 'آژانس طراحی و برندینگ', 'creative-studio', 'Design and branding agency')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- System business categories are seeded in 005_business_categories.sql
|
||||
|
||||
INSERT INTO domains (id, business_id, host, is_primary, is_verified, verified_at, ssl_enabled) VALUES
|
||||
(1, 1, 'shop-a.local', TRUE, TRUE, NOW(), FALSE),
|
||||
(2, 1, 'www.shop-a.local', FALSE, TRUE, NOW(), FALSE),
|
||||
(3, 2, 'studio-b.local', TRUE, TRUE, NOW(), FALSE)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- Roles
|
||||
INSERT INTO user_roles (user_id, role_id)
|
||||
SELECT 1, r.id FROM roles r WHERE r.slug = 'super_admin'
|
||||
ON CONFLICT (user_id, role_id) DO NOTHING;
|
||||
|
||||
INSERT INTO user_roles (user_id, role_id)
|
||||
SELECT 2, r.id FROM roles r WHERE r.slug = 'business_owner'
|
||||
ON CONFLICT (user_id, role_id) DO NOTHING;
|
||||
|
||||
INSERT INTO user_roles (user_id, role_id)
|
||||
SELECT 3, r.id FROM roles r WHERE r.slug = 'business_owner'
|
||||
ON CONFLICT (user_id, role_id) DO NOTHING;
|
||||
|
||||
INSERT INTO user_roles (user_id, role_id)
|
||||
SELECT 4, r.id FROM roles r WHERE r.slug = 'customer'
|
||||
ON CONFLICT (user_id, role_id) DO NOTHING;
|
||||
|
||||
INSERT INTO user_roles (user_id, role_id)
|
||||
SELECT 5, r.id FROM roles r WHERE r.slug = 'customer'
|
||||
ON CONFLICT (user_id, role_id) DO NOTHING;
|
||||
|
||||
-- Business owners (assigned by super admin)
|
||||
INSERT INTO business_users (id, business_id, user_id, is_owner) VALUES
|
||||
(1, 1, 2, TRUE),
|
||||
(2, 2, 3, TRUE)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- Team staff: editor on business 1 (invited by business owner)
|
||||
INSERT INTO users (id, cell_number, password_hash, email, first_name, last_name, cell_verified_at) VALUES
|
||||
(6, '+989126666667', '$2b$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'editor@shop.demo', 'Nima', 'Editori', NOW())
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO business_users (id, business_id, user_id, is_owner, role_id, invited_by)
|
||||
SELECT 3, 1, 6, FALSE, r.id, 2
|
||||
FROM roles r WHERE r.slug = 'editor'
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO user_roles (user_id, role_id)
|
||||
SELECT 6, r.id FROM roles r WHERE r.slug = 'business_staff'
|
||||
ON CONFLICT (user_id, role_id) DO NOTHING;
|
||||
|
||||
-- Website customers
|
||||
INSERT INTO business_customers (id, business_id, user_id) VALUES
|
||||
(1, 1, 4),
|
||||
(2, 2, 5)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO media (
|
||||
id, business_id, uploaded_by, media_type, storage_disk, storage_path, public_url,
|
||||
file_name, original_file_name, mime_type, file_size_bytes, width, height, duration_seconds, alt_text
|
||||
) VALUES
|
||||
(1, 1, 2, 'image', 'local', '/uploads/shop/hero-phone.jpg', 'https://cdn.meshkee.demo/shop/hero-phone.jpg', 'hero-phone.jpg', 'hero-phone.jpg', 'image/jpeg', 245000, 1200, 800, NULL, 'Smartphone hero'),
|
||||
(2, 1, 2, 'image', 'local', '/uploads/shop/laptop.jpg', 'https://cdn.meshkee.demo/shop/laptop.jpg', 'laptop.jpg', 'laptop.jpg', 'image/jpeg', 198000, 1200, 800, NULL, 'Laptop product shot'),
|
||||
(3, 1, 2, 'video', 'local', '/uploads/shop/unboxing.mp4', 'https://cdn.meshkee.demo/shop/unboxing.mp4', 'unboxing.mp4', 'unboxing.mp4', 'video/mp4', 5200000, NULL, NULL, 42.50, 'Product unboxing'),
|
||||
(4, 2, 3, 'image', 'local', '/uploads/studio/brand-cover.jpg', 'https://cdn.meshkee.demo/studio/brand-cover.jpg', 'brand-cover.jpg', 'brand-cover.jpg', 'image/jpeg', 310000, 1600, 900, NULL, 'Branding project cover'),
|
||||
(5, 2, 3, 'image', 'local', '/uploads/studio/web-ui.jpg', 'https://cdn.meshkee.demo/studio/web-ui.jpg', 'web-ui.jpg', 'web-ui.jpg', 'image/jpeg', 275000, 1440, 900, NULL, 'Web design mockup')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO categories (id, business_id, entity_type, parent_id, name, slug, description, sort_order) VALUES
|
||||
(1, 1, 'product', NULL, 'Electronics', 'electronics', 'Electronic devices', 1),
|
||||
(2, 1, 'product', 1, 'Phones', 'phones', 'Mobile phones', 1),
|
||||
(3, 1, 'product', 1, 'Laptops', 'laptops', 'Laptops and notebooks', 2),
|
||||
(4, 1, 'product', NULL, 'Accessories', 'accessories', 'Phone and laptop accessories', 2),
|
||||
(5, 1, 'blog', NULL, 'Tutorials', 'tutorials', 'How-to guides', 1),
|
||||
(6, 1, 'blog', NULL, 'News', 'news', 'Store news and updates', 2),
|
||||
(7, 1, 'portfolio', NULL, 'Product Photography', 'product-photography', 'Commercial product shoots', 1),
|
||||
(8, 2, 'product', NULL, 'Design Packages', 'design-packages', 'Service packages', 1),
|
||||
(9, 2, 'blog', NULL, 'Case Studies', 'case-studies', 'Client success stories', 1),
|
||||
(10, 2, 'blog', NULL, 'Design Tips', 'design-tips', 'Tips for better design', 2),
|
||||
(11, 2, 'portfolio', NULL, 'Branding', 'branding', 'Logo and identity work', 1),
|
||||
(12, 2, 'portfolio', NULL, 'Web Design', 'web-design', 'Websites and web apps', 2)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO products (
|
||||
id, business_id, title, slug, description, content, price, compare_at_price, sku,
|
||||
stock_quantity, status, featured_media_id, sort_order, published_at
|
||||
) VALUES
|
||||
(1, 1, 'Meshkee X Phone', 'meshkee-x-phone', 'Flagship smartphone with OLED display.',
|
||||
'{"blocks":[{"type":"paragraph","text":"6.5 inch OLED, 256GB storage."}]}',
|
||||
12990000, 14990000, 'MXP-001', 25, 'published', 1, 1, NOW()),
|
||||
(2, 1, 'Meshkee Book Pro', 'meshkee-book-pro', 'Lightweight laptop for creators.',
|
||||
'{"blocks":[{"type":"paragraph","text":"14 inch, 16GB RAM, 512GB SSD."}]}',
|
||||
28990000, NULL, 'MBP-001', 10, 'published', 2, 2, NOW()),
|
||||
(3, 2, 'Brand Identity Package', 'brand-identity-package', 'Logo, color palette, and brand guidelines.',
|
||||
'{"blocks":[{"type":"paragraph","text":"Includes 3 logo concepts."}]}',
|
||||
15000000, NULL, 'PKG-BRAND-01', NULL, 'published', 4, 1, NOW())
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO blogs (
|
||||
id, business_id, author_id, title, slug, excerpt, content, status, featured_media_id, published_at
|
||||
) VALUES
|
||||
(1, 1, 2, 'How to Choose the Right Phone', 'how-to-choose-phone',
|
||||
'A quick guide to picking your next smartphone.',
|
||||
'{"blocks":[{"type":"heading","text":"Battery life"},{"type":"paragraph","text":"Look for 4000mAh or more."}]}',
|
||||
'published', 1, NOW()),
|
||||
(2, 1, 2, 'Summer Sale Starts Next Week', 'summer-sale-next-week',
|
||||
'Up to 30% off on selected electronics.',
|
||||
'{"blocks":[{"type":"paragraph","text":"Sale runs Monday through Sunday."}]}',
|
||||
'draft', NULL, NULL),
|
||||
(3, 2, 3, 'Rebranding a Local Café', 'rebranding-local-cafe',
|
||||
'How we refreshed a neighborhood café brand.',
|
||||
'{"blocks":[{"type":"paragraph","text":"We started with customer interviews."}]}',
|
||||
'published', 4, NOW())
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO portfolios (
|
||||
id, business_id, title, slug, description, content, client_name, project_url,
|
||||
status, featured_media_id, sort_order, published_at
|
||||
) VALUES
|
||||
(1, 1, 'Phone Launch Campaign', 'phone-launch-campaign',
|
||||
'Product photos and video for Meshkee X Phone launch.',
|
||||
'{"blocks":[{"type":"paragraph","text":"Shot in studio with 3 lighting setups."}]}',
|
||||
'Meshkee', 'https://shop-a.local/products/meshkee-x-phone', 'published', 1, 1, NOW()),
|
||||
(2, 2, 'Nova Café Rebrand', 'nova-cafe-rebrand',
|
||||
'Full brand identity for Nova Café.',
|
||||
'{"blocks":[{"type":"paragraph","text":"Logo, menu design, and signage."}]}',
|
||||
'Nova Café', 'https://novacafe.example.com', 'published', 4, 1, NOW()),
|
||||
(3, 2, 'FinTech Dashboard UI', 'fintech-dashboard-ui',
|
||||
'Dashboard design for a financial startup.',
|
||||
'{"blocks":[{"type":"paragraph","text":"Dark mode first design system."}]}',
|
||||
'PayFlow', 'https://payflow.example.com', 'published', 5, 2, NOW())
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO category_assignments (id, business_id, category_id, entity_type, entity_id) VALUES
|
||||
(1, 1, 2, 'product', 1),
|
||||
(2, 1, 3, 'product', 2),
|
||||
(3, 2, 8, 'product', 3),
|
||||
(4, 1, 5, 'blog', 1),
|
||||
(5, 1, 6, 'blog', 2),
|
||||
(6, 2, 9, 'blog', 3),
|
||||
(7, 1, 7, 'portfolio', 1),
|
||||
(8, 2, 11, 'portfolio', 2),
|
||||
(9, 2, 12, 'portfolio', 3)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO media_attachments (id, business_id, media_id, entity_type, entity_id, sort_order, is_featured) VALUES
|
||||
(1, 1, 3, 'product', 1, 1, FALSE),
|
||||
(2, 2, 5, 'portfolio', 3, 1, FALSE)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
SELECT setval(pg_get_serial_sequence('users', 'id'), COALESCE((SELECT MAX(id) FROM users), 1));
|
||||
SELECT setval(pg_get_serial_sequence('businesses', 'id'), COALESCE((SELECT MAX(id) FROM businesses), 1));
|
||||
SELECT setval(pg_get_serial_sequence('domains', 'id'), COALESCE((SELECT MAX(id) FROM domains), 1));
|
||||
SELECT setval(pg_get_serial_sequence('business_users', 'id'), COALESCE((SELECT MAX(id) FROM business_users), 1));
|
||||
SELECT setval(pg_get_serial_sequence('business_customers', 'id'), COALESCE((SELECT MAX(id) FROM business_customers), 1));
|
||||
SELECT setval(pg_get_serial_sequence('media', 'id'), COALESCE((SELECT MAX(id) FROM media), 1));
|
||||
SELECT setval(pg_get_serial_sequence('categories', 'id'), COALESCE((SELECT MAX(id) FROM categories), 1));
|
||||
SELECT setval(pg_get_serial_sequence('products', 'id'), COALESCE((SELECT MAX(id) FROM products), 1));
|
||||
SELECT setval(pg_get_serial_sequence('blogs', 'id'), COALESCE((SELECT MAX(id) FROM blogs), 1));
|
||||
SELECT setval(pg_get_serial_sequence('portfolios', 'id'), COALESCE((SELECT MAX(id) FROM portfolios), 1));
|
||||
SELECT setval(pg_get_serial_sequence('category_assignments', 'id'), COALESCE((SELECT MAX(id) FROM category_assignments), 1));
|
||||
SELECT setval(pg_get_serial_sequence('media_attachments', 'id'), COALESCE((SELECT MAX(id) FROM media_attachments), 1));
|
||||
@@ -0,0 +1,35 @@
|
||||
-- Super admin account for production / staging
|
||||
-- Cell: +989127004945 (09127004945)
|
||||
-- Password: Ali2reza
|
||||
|
||||
INSERT INTO users (cell_number, password_hash, email, first_name, last_name, cell_verified_at, is_active)
|
||||
VALUES (
|
||||
'+989127004945',
|
||||
'$2b$10$avrNkn5W5gWkZNrupUAXwePyj4FiwQM4H5hvHp.btPA1L0I0NqgAi',
|
||||
'ali@meshkee.app',
|
||||
'Ali',
|
||||
'Reza',
|
||||
NOW(),
|
||||
TRUE
|
||||
)
|
||||
ON CONFLICT (cell_number) DO UPDATE SET
|
||||
password_hash = EXCLUDED.password_hash,
|
||||
first_name = EXCLUDED.first_name,
|
||||
last_name = EXCLUDED.last_name,
|
||||
is_active = TRUE,
|
||||
cell_verified_at = COALESCE(users.cell_verified_at, NOW());
|
||||
|
||||
-- Ensure only super_admin as global role (removes business_owner/customer if present)
|
||||
DELETE FROM user_roles ur
|
||||
USING users u, roles r
|
||||
WHERE ur.user_id = u.id
|
||||
AND ur.role_id = r.id
|
||||
AND u.cell_number = '+989127004945'
|
||||
AND r.slug IN ('super_admin', 'business_owner', 'business_staff', 'customer');
|
||||
|
||||
INSERT INTO user_roles (user_id, role_id)
|
||||
SELECT u.id, r.id
|
||||
FROM users u
|
||||
JOIN roles r ON r.slug = 'super_admin'
|
||||
WHERE u.cell_number = '+989127004945'
|
||||
ON CONFLICT (user_id, role_id) DO NOTHING;
|
||||
@@ -0,0 +1,181 @@
|
||||
-- Sample comments and expert reviews for product 4
|
||||
-- Resolves business_id and approver from the product / business owner automatically.
|
||||
-- Requires migrations 012_comments.sql and 013_expert_reviews.sql
|
||||
|
||||
WITH product_ctx AS (
|
||||
SELECT
|
||||
p.id AS product_id,
|
||||
p.business_id,
|
||||
owner.user_id AS owner_user_id
|
||||
FROM products p
|
||||
JOIN business_users owner
|
||||
ON owner.business_id = p.business_id
|
||||
AND owner.is_owner = TRUE
|
||||
WHERE p.id = 4
|
||||
),
|
||||
comment_rows AS (
|
||||
SELECT *
|
||||
FROM (VALUES
|
||||
(
|
||||
1::bigint,
|
||||
'Mina Salehi'::varchar,
|
||||
'mina@customer.demo'::varchar,
|
||||
'Camera quality is outstanding, especially in low light. Very happy with the upgrade.'::text,
|
||||
TRUE,
|
||||
NOW() - INTERVAL '2 days',
|
||||
NOW() - INTERVAL '3 days'
|
||||
),
|
||||
(
|
||||
2,
|
||||
'Arash Mohammadi',
|
||||
'arash@example.com',
|
||||
'Smooth performance and the display looks fantastic. Battery could last a bit longer though.',
|
||||
TRUE,
|
||||
NOW() - INTERVAL '1 day',
|
||||
NOW() - INTERVAL '2 days'
|
||||
),
|
||||
(
|
||||
3,
|
||||
'Leila Karimi',
|
||||
'leila@example.com',
|
||||
'Premium build and fast delivery from Sanihome. Setup was seamless.',
|
||||
TRUE,
|
||||
NOW() - INTERVAL '5 hours',
|
||||
NOW() - INTERVAL '1 day'
|
||||
),
|
||||
(
|
||||
4,
|
||||
'Hossein Rahimi',
|
||||
'hossein@example.com',
|
||||
'Just placed my order — excited to try the new Pro model.',
|
||||
FALSE,
|
||||
NULL::timestamptz,
|
||||
NOW() - INTERVAL '3 hours'
|
||||
),
|
||||
(
|
||||
5,
|
||||
'Nazanin Azizi',
|
||||
NULL,
|
||||
'Does this model support dual SIM for Iran?',
|
||||
FALSE,
|
||||
NULL::timestamptz,
|
||||
NOW() - INTERVAL '1 hour'
|
||||
)
|
||||
) AS rows(
|
||||
id,
|
||||
author_name,
|
||||
author_email,
|
||||
text,
|
||||
is_approved,
|
||||
approved_at,
|
||||
created_at
|
||||
)
|
||||
)
|
||||
INSERT INTO comments (
|
||||
id, business_id, entity_type, entity_id, author_name, author_email, text,
|
||||
is_approved, approved_at, approved_by, created_at
|
||||
)
|
||||
SELECT
|
||||
r.id,
|
||||
ctx.business_id,
|
||||
'product'::media_entity_type,
|
||||
ctx.product_id,
|
||||
r.author_name,
|
||||
r.author_email,
|
||||
r.text,
|
||||
r.is_approved,
|
||||
CASE WHEN r.is_approved THEN r.approved_at ELSE NULL END,
|
||||
CASE WHEN r.is_approved THEN ctx.owner_user_id ELSE NULL END,
|
||||
r.created_at
|
||||
FROM comment_rows r
|
||||
CROSS JOIN product_ctx ctx
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
WITH product_ctx AS (
|
||||
SELECT
|
||||
p.id AS product_id,
|
||||
p.business_id,
|
||||
owner.user_id AS owner_user_id
|
||||
FROM products p
|
||||
JOIN business_users owner
|
||||
ON owner.business_id = p.business_id
|
||||
AND owner.is_owner = TRUE
|
||||
WHERE p.id = 4
|
||||
),
|
||||
review_rows AS (
|
||||
SELECT *
|
||||
FROM (VALUES
|
||||
(
|
||||
1::bigint,
|
||||
'MobileTech Review'::varchar,
|
||||
'reviews@mobiletech.demo'::varchar,
|
||||
9::smallint,
|
||||
ARRAY['Excellent camera system', 'Top-tier performance', 'Premium display', 'Strong build quality']::text[],
|
||||
ARRAY['High price point', 'No charger in box']::text[],
|
||||
'The iPhone 17 Pro remains a benchmark flagship. Photo and video capabilities are class-leading, and day-to-day performance is flawless for power users.'::text,
|
||||
TRUE,
|
||||
NOW() - INTERVAL '4 days',
|
||||
NOW() - INTERVAL '5 days'
|
||||
),
|
||||
(
|
||||
2,
|
||||
'Gadget Iran',
|
||||
'editor@gadgetiran.demo',
|
||||
8,
|
||||
ARRAY['Bright ProMotion display', 'Reliable iOS updates', 'Great video stabilization'],
|
||||
ARRAY['Heavy for one-handed use', 'Storage upgrades are expensive'],
|
||||
'A compelling Pro model for creators and professionals. The camera and display are the main reasons to choose it over the standard line.',
|
||||
TRUE,
|
||||
NOW() - INTERVAL '2 days',
|
||||
NOW() - INTERVAL '3 days'
|
||||
),
|
||||
(
|
||||
3,
|
||||
'PhoneLab',
|
||||
'lab@phonelab.demo',
|
||||
7,
|
||||
ARRAY['Fast A-series chip', 'Solid battery for its class', 'Excellent ecosystem integration'],
|
||||
ARRAY['Incremental design changes', 'Pro price without major leaps for casual users'],
|
||||
'A polished flagship that makes sense for Apple loyalists and mobile photographers, though casual upgraders may find better value elsewhere.',
|
||||
FALSE,
|
||||
NULL::timestamptz,
|
||||
NOW() - INTERVAL '6 hours'
|
||||
)
|
||||
) AS rows(
|
||||
id,
|
||||
author_name,
|
||||
author_email,
|
||||
rate,
|
||||
positive_points,
|
||||
negative_points,
|
||||
text,
|
||||
is_approved,
|
||||
approved_at,
|
||||
created_at
|
||||
)
|
||||
)
|
||||
INSERT INTO expert_reviews (
|
||||
id, business_id, product_id, author_name, author_email, rate,
|
||||
positive_points, negative_points, text,
|
||||
is_approved, approved_at, approved_by, created_at
|
||||
)
|
||||
SELECT
|
||||
r.id,
|
||||
ctx.business_id,
|
||||
ctx.product_id,
|
||||
r.author_name,
|
||||
r.author_email,
|
||||
r.rate,
|
||||
r.positive_points,
|
||||
r.negative_points,
|
||||
r.text,
|
||||
r.is_approved,
|
||||
CASE WHEN r.is_approved THEN r.approved_at ELSE NULL END,
|
||||
CASE WHEN r.is_approved THEN ctx.owner_user_id ELSE NULL END,
|
||||
r.created_at
|
||||
FROM review_rows r
|
||||
CROSS JOIN product_ctx ctx
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
SELECT setval(pg_get_serial_sequence('comments', 'id'), COALESCE((SELECT MAX(id) FROM comments), 1));
|
||||
SELECT setval(pg_get_serial_sequence('expert_reviews', 'id'), COALESCE((SELECT MAX(id) FROM expert_reviews), 1));
|
||||
@@ -0,0 +1,113 @@
|
||||
-- Iran location reference data (country → provinces → provincial capitals)
|
||||
-- Requires migration 015_cities.sql
|
||||
|
||||
INSERT INTO cities (parent_id, level, name_fa, name_en, landline_code, slug, sort_order) VALUES
|
||||
(NULL, 'country', 'ایران', 'Iran', '98', 'iran', 1);
|
||||
|
||||
INSERT INTO cities (parent_id, level, name_fa, name_en, landline_code, slug, sort_order)
|
||||
SELECT c.id, 'province', v.name_fa, v.name_en, v.landline_code, v.slug, v.sort_order
|
||||
FROM cities c
|
||||
CROSS JOIN (
|
||||
VALUES
|
||||
('آذربایجان شرقی', 'East Azerbaijan', '041', 'east-azerbaijan', 1),
|
||||
('آذربایجان غربی', 'West Azerbaijan', '044', 'west-azerbaijan', 2),
|
||||
('اردبیل', 'Ardabil', '045', 'ardabil', 3),
|
||||
('اصفهان', 'Isfahan', '031', 'isfahan', 4),
|
||||
('البرز', 'Alborz', '026', 'alborz', 5),
|
||||
('ایلام', 'Ilam', '084', 'ilam', 6),
|
||||
('بوشهر', 'Bushehr', '077', 'bushehr', 7),
|
||||
('تهران', 'Tehran', '021', 'tehran-province', 8),
|
||||
('چهارمحال و بختیاری', 'Chaharmahal and Bakhtiari', '038', 'chaharmahal-bakhtiari', 9),
|
||||
('خراسان جنوبی', 'South Khorasan', '056', 'south-khorasan', 10),
|
||||
('خراسان رضوی', 'Razavi Khorasan', '051', 'razavi-khorasan', 11),
|
||||
('خراسان شمالی', 'North Khorasan', '058', 'north-khorasan', 12),
|
||||
('خوزستان', 'Khuzestan', '061', 'khuzestan', 13),
|
||||
('زنجان', 'Zanjan', '024', 'zanjan', 14),
|
||||
('سمنان', 'Semnan', '023', 'semnan', 15),
|
||||
('سیستان و بلوچستان', 'Sistan and Baluchestan', '054', 'sistan-baluchestan', 16),
|
||||
('فارس', 'Fars', '071', 'fars', 17),
|
||||
('قزوین', 'Qazvin', '028', 'qazvin', 18),
|
||||
('قم', 'Qom', '025', 'qom', 19),
|
||||
('کردستان', 'Kurdistan', '087', 'kurdistan', 20),
|
||||
('کرمان', 'Kerman', '034', 'kerman', 21),
|
||||
('کرمانشاه', 'Kermanshah', '083', 'kermanshah', 22),
|
||||
('کهگیلویه و بویراحمد', 'Kohgiluyeh and Boyer-Ahmad', '074', 'kohgiluyeh-boyer-ahmad', 23),
|
||||
('گلستان', 'Golestan', '017', 'golestan', 24),
|
||||
('گیلان', 'Gilan', '013', 'gilan', 25),
|
||||
('لرستان', 'Lorestan', '066', 'lorestan', 26),
|
||||
('مازندران', 'Mazandaran', '011', 'mazandaran', 27),
|
||||
('مرکزی', 'Markazi', '086', 'markazi', 28),
|
||||
('هرمزگان', 'Hormozgan', '076', 'hormozgan', 29),
|
||||
('همدان', 'Hamadan', '081', 'hamadan', 30),
|
||||
('یزد', 'Yazd', '035', 'yazd', 31)
|
||||
) AS v(name_fa, name_en, landline_code, slug, sort_order)
|
||||
WHERE c.slug = 'iran';
|
||||
|
||||
INSERT INTO cities (parent_id, level, name_fa, name_en, landline_code, slug, sort_order)
|
||||
SELECT p.id, 'city', v.name_fa, v.name_en, v.landline_code, v.slug, v.sort_order
|
||||
FROM cities p
|
||||
JOIN (
|
||||
VALUES
|
||||
('east-azerbaijan', 'تبریز', 'Tabriz', '041', 'tabriz', 1),
|
||||
('west-azerbaijan', 'ارومیه', 'Urmia', '044', 'urmia', 1),
|
||||
('ardabil', 'اردبیل', 'Ardabil', '045', 'ardabil-city', 1),
|
||||
('isfahan', 'اصفهان', 'Isfahan', '031', 'isfahan-city', 1),
|
||||
('alborz', 'کرج', 'Karaj', '026', 'karaj', 1),
|
||||
('ilam', 'ایلام', 'Ilam', '084', 'ilam-city', 1),
|
||||
('bushehr', 'بوشهر', 'Bushehr', '077', 'bushehr-city', 1),
|
||||
('tehran-province', 'تهران', 'Tehran', '021', 'tehran', 1),
|
||||
('chaharmahal-bakhtiari', 'شهرکرد', 'Shahrekord', '038', 'shahrekord', 1),
|
||||
('south-khorasan', 'بیرجند', 'Birjand', '056', 'birjand', 1),
|
||||
('razavi-khorasan', 'مشهد', 'Mashhad', '051', 'mashhad', 1),
|
||||
('north-khorasan', 'بجنورد', 'Bojnord', '058', 'bojnord', 1),
|
||||
('khuzestan', 'اهواز', 'Ahvaz', '061', 'ahvaz', 1),
|
||||
('zanjan', 'زنجان', 'Zanjan', '024', 'zanjan-city', 1),
|
||||
('semnan', 'سمنان', 'Semnan', '023', 'semnan-city', 1),
|
||||
('sistan-baluchestan', 'زاهدان', 'Zahedan', '054', 'zahedan', 1),
|
||||
('fars', 'شیراز', 'Shiraz', '071', 'shiraz', 1),
|
||||
('qazvin', 'قزوین', 'Qazvin', '028', 'qazvin-city', 1),
|
||||
('qom', 'قم', 'Qom', '025', 'qom-city', 1),
|
||||
('kurdistan', 'سنندج', 'Sanandaj', '087', 'sanandaj', 1),
|
||||
('kerman', 'کرمان', 'Kerman', '034', 'kerman-city', 1),
|
||||
('kermanshah', 'کرمانشاه', 'Kermanshah', '083', 'kermanshah-city', 1),
|
||||
('kohgiluyeh-boyer-ahmad', 'یاسوج', 'Yasuj', '074', 'yasuj', 1),
|
||||
('golestan', 'گرگان', 'Gorgan', '017', 'gorgan', 1),
|
||||
('gilan', 'رشت', 'Rasht', '013', 'rasht', 1),
|
||||
('lorestan', 'خرمآباد', 'Khorramabad', '066', 'khorramabad', 1),
|
||||
('mazandaran', 'ساری', 'Sari', '011', 'sari', 1),
|
||||
('markazi', 'اراک', 'Arak', '086', 'arak', 1),
|
||||
('hormozgan', 'بندرعباس', 'Bandar Abbas', '076', 'bandar-abbas', 1),
|
||||
('hamadan', 'همدان', 'Hamadan', '081', 'hamadan-city', 1),
|
||||
('yazd', 'یزد', 'Yazd', '035', 'yazd-city', 1)
|
||||
) AS v(province_slug, name_fa, name_en, landline_code, slug, sort_order)
|
||||
ON p.slug = v.province_slug
|
||||
WHERE p.level = 'province';
|
||||
|
||||
-- Additional major cities
|
||||
INSERT INTO cities (parent_id, level, name_fa, name_en, landline_code, slug, sort_order)
|
||||
SELECT p.id, 'city', v.name_fa, v.name_en, v.landline_code, v.slug, v.sort_order
|
||||
FROM cities p
|
||||
JOIN (
|
||||
VALUES
|
||||
('tehran-province', 'ری', 'Rey', '021', 'rey', 2),
|
||||
('tehran-province', 'شهریار', 'Shahriar', '021', 'shahriar', 3),
|
||||
('tehran-province', 'ورامین', 'Varamin', '021', 'varamin', 4),
|
||||
('isfahan', 'کاشان', 'Kashan', '031', 'kashan', 2),
|
||||
('isfahan', 'نجفآباد', 'Najafabad', '031', 'najafabad', 3),
|
||||
('fars', 'مرودشت', 'Marvdasht', '071', 'marvdasht', 2),
|
||||
('fars', 'جهرم', 'Jahrom', '071', 'jahrom', 3),
|
||||
('khuzestan', 'آبادان', 'Abadan', '061', 'abadan', 2),
|
||||
('khuzestan', 'دزفول', 'Dezful', '061', 'dezful', 3),
|
||||
('razavi-khorasan', 'نیشابور', 'Neyshabur', '051', 'neyshabur', 2),
|
||||
('razavi-khorasan', 'سبزوار', 'Sabzevar', '051', 'sabzevar', 3),
|
||||
('mazandaran', 'آمل', 'Amol', '011', 'amol', 2),
|
||||
('mazandaran', 'بابل', 'Babol', '011', 'babol', 3),
|
||||
('gilan', 'انزلی', 'Bandar Anzali', '013', 'bandar-anzali', 2),
|
||||
('east-azerbaijan', 'مراغه', 'Maragheh', '041', 'maragheh', 2),
|
||||
('kerman', 'رفسنجان', 'Rafsanjan', '034', 'rafsanjan', 2),
|
||||
('alborz', 'فردیس', 'Fardis', '026', 'fardis', 2)
|
||||
) AS v(province_slug, name_fa, name_en, landline_code, slug, sort_order)
|
||||
ON p.slug = v.province_slug
|
||||
WHERE p.level = 'province';
|
||||
|
||||
SELECT setval(pg_get_serial_sequence('cities', 'id'), COALESCE((SELECT MAX(id) FROM cities), 1));
|
||||
@@ -0,0 +1,178 @@
|
||||
-- System business categories: retail, industry, and services (up to 3 levels)
|
||||
-- Run after 001_sample_data.sql (replaces the minimal categories seeded there)
|
||||
|
||||
DELETE FROM business_category_assignments;
|
||||
DELETE FROM business_categories;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Level 1 — top-level industries
|
||||
-- ---------------------------------------------------------------------------
|
||||
INSERT INTO business_categories (parent_id, name, slug, description, sort_order) VALUES
|
||||
(NULL, 'Retail & Shopping', 'retail-shopping', 'Physical and online retail businesses', 1),
|
||||
(NULL, 'Manufacturing & Industry', 'manufacturing-industry', 'Production, factories, and industrial businesses', 2),
|
||||
(NULL, 'Food & Beverage', 'food-beverage', 'Restaurants, food production, and beverage brands', 3),
|
||||
(NULL, 'Professional Services', 'professional-services', 'Consulting, creative, and business services', 4),
|
||||
(NULL, 'Technology & Digital', 'technology-digital', 'Software, IT, and digital businesses', 5),
|
||||
(NULL, 'Health & Wellness', 'health-wellness', 'Healthcare, beauty, and fitness businesses', 6),
|
||||
(NULL, 'Home & Living', 'home-living', 'Furniture, décor, and home improvement', 7),
|
||||
(NULL, 'Automotive', 'automotive', 'Vehicle sales, parts, and services', 8);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Level 2 — sectors
|
||||
-- ---------------------------------------------------------------------------
|
||||
INSERT INTO business_categories (parent_id, name, slug, description, sort_order)
|
||||
SELECT p.id, v.name, v.slug, v.description, v.sort_order
|
||||
FROM business_categories p
|
||||
JOIN (
|
||||
VALUES
|
||||
-- Retail & Shopping
|
||||
('retail-shopping', 'Fashion & Apparel', 'fashion-apparel', 'Clothing, footwear, and fashion accessories', 1),
|
||||
('retail-shopping', 'Electronics & Tech Retail', 'electronics-retail', 'Consumer electronics and technology retail', 2),
|
||||
('retail-shopping', 'Grocery & Supermarket', 'grocery-supermarket', 'Supermarkets, grocery, and convenience stores', 3),
|
||||
('retail-shopping', 'Home & Furniture Retail', 'home-furniture-retail', 'Furniture, décor, and home goods stores', 4),
|
||||
('retail-shopping', 'Sports & Outdoors', 'sports-outdoors-retail', 'Sporting goods and outdoor equipment', 5),
|
||||
('retail-shopping', 'Jewelry & Accessories', 'jewelry-accessories', 'Jewelry, watches, and fashion accessories', 6),
|
||||
('retail-shopping', 'Books & Stationery', 'books-stationery', 'Bookstores, stationery, and office supplies', 7),
|
||||
('retail-shopping', 'E-commerce & Online', 'e-commerce-online', 'Online-only and omnichannel retail', 8),
|
||||
|
||||
-- Manufacturing & Industry
|
||||
('manufacturing-industry', 'Textile & Apparel', 'textile-apparel-mfg', 'Garment, fabric, and textile production', 1),
|
||||
('manufacturing-industry', 'Food Processing', 'food-processing-mfg', 'Packaged food and beverage manufacturing', 2),
|
||||
('manufacturing-industry', 'Metal & Machinery', 'metal-machinery-mfg', 'Metalwork, machinery, and industrial equipment', 3),
|
||||
('manufacturing-industry', 'Chemicals & Materials', 'chemicals-materials', 'Chemicals, plastics, and raw materials', 4),
|
||||
('manufacturing-industry', 'Electronics Manufacturing', 'electronics-manufacturing', 'Electronic components and device manufacturing', 5),
|
||||
('manufacturing-industry', 'Packaging & Printing', 'packaging-printing', 'Packaging, labels, and commercial printing', 6),
|
||||
|
||||
-- Food & Beverage
|
||||
('food-beverage', 'Restaurants & Cafés', 'restaurants-cafes', 'Dining, cafés, and hospitality', 1),
|
||||
('food-beverage', 'Bakery & Confectionery', 'bakery-confectionery', 'Bakeries, pastries, and sweets', 2),
|
||||
('food-beverage', 'Beverage Production', 'beverage-production', 'Juice, soft drinks, tea, and coffee production', 3),
|
||||
('food-beverage', 'Food Wholesale & Distribution', 'food-wholesale', 'Food distribution and wholesale supply', 4),
|
||||
|
||||
-- Professional Services
|
||||
('professional-services', 'Design & Creative', 'design-creative', 'Design, branding, and creative agencies', 1),
|
||||
('professional-services', 'Consulting & Advisory', 'consulting-advisory', 'Business, legal, and management consulting', 2),
|
||||
('professional-services', 'Education & Training', 'education-training', 'Schools, courses, and training providers', 3),
|
||||
('professional-services', 'Marketing & Advertising', 'marketing-advertising', 'Marketing agencies and advertising services', 4),
|
||||
|
||||
-- Technology & Digital
|
||||
('technology-digital', 'Software & IT Services', 'software-it-services', 'Software development and IT consulting', 1),
|
||||
('technology-digital', 'Digital Media & Content', 'digital-media', 'Media, content, and publishing platforms', 2),
|
||||
('technology-digital', 'Hardware & Devices', 'hardware-devices', 'Hardware products and device companies', 3),
|
||||
|
||||
-- Health & Wellness
|
||||
('health-wellness', 'Beauty & Personal Care', 'beauty-personal-care', 'Salons, cosmetics, and personal care retail', 1),
|
||||
('health-wellness', 'Pharmacy & Medical Supply', 'pharmacy-medical-supply', 'Pharmacies and medical supply stores', 2),
|
||||
('health-wellness', 'Fitness & Sports Clubs', 'fitness-sports-clubs', 'Gyms, fitness studios, and sports clubs', 3),
|
||||
|
||||
-- Home & Living
|
||||
('home-living', 'Furniture & Décor', 'furniture-decor', 'Furniture stores and interior décor', 1),
|
||||
('home-living', 'Building Materials', 'building-materials', 'Construction and building supply', 2),
|
||||
('home-living', 'Garden & Outdoor Living', 'garden-outdoor-living', 'Garden centers and outdoor living products', 3),
|
||||
|
||||
-- Automotive
|
||||
('automotive', 'Vehicle Dealers', 'auto-dealers', 'Car, motorcycle, and vehicle dealerships', 1),
|
||||
('automotive', 'Parts & Service', 'auto-parts-service', 'Auto parts, repair, and maintenance services', 2)
|
||||
) AS v(parent_slug, name, slug, description, sort_order)
|
||||
ON p.slug = v.parent_slug
|
||||
WHERE p.parent_id IS NULL;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Level 3 — specific business / store types
|
||||
-- ---------------------------------------------------------------------------
|
||||
INSERT INTO business_categories (parent_id, name, slug, description, sort_order)
|
||||
SELECT p.id, v.name, v.slug, v.description, v.sort_order
|
||||
FROM business_categories p
|
||||
JOIN (
|
||||
VALUES
|
||||
-- Fashion & Apparel
|
||||
('fashion-apparel', 'Women''s Clothing Store', 'womens-clothing-store', 'Retail stores focused on women''s apparel', 1),
|
||||
('fashion-apparel', 'Men''s Clothing Store', 'mens-clothing-store', 'Retail stores focused on men''s apparel', 2),
|
||||
('fashion-apparel', 'Children''s Clothing Store', 'children-clothing-store', 'Apparel for infants, kids, and teens', 3),
|
||||
('fashion-apparel', 'Footwear Store', 'footwear-store', 'Shoes, boots, and footwear retail', 4),
|
||||
('fashion-apparel', 'Luxury Fashion Boutique', 'luxury-fashion-boutique', 'High-end and designer fashion retail', 5),
|
||||
|
||||
-- Electronics & Tech Retail
|
||||
('electronics-retail', 'Mobile & Accessories Store', 'mobile-accessories-store', 'Phones, tablets, and mobile accessories', 1),
|
||||
('electronics-retail', 'Computer & Laptop Store', 'computer-laptop-store', 'Computers, laptops, and peripherals', 2),
|
||||
('electronics-retail', 'Home Appliances Store', 'home-appliances-store', 'Large and small home appliances', 3),
|
||||
('electronics-retail', 'Consumer Electronics Store', 'consumer-electronics-store', 'General electronics and gadgets retail', 4),
|
||||
|
||||
-- Grocery & Supermarket
|
||||
('grocery-supermarket', 'Supermarket & Hypermarket', 'supermarket-hypermarket', 'Large-format grocery and hypermarket chains', 1),
|
||||
('grocery-supermarket', 'Convenience Store', 'convenience-store', 'Neighborhood and convenience grocery', 2),
|
||||
('grocery-supermarket', 'Organic & Health Food Store', 'organic-health-food-store', 'Organic, natural, and health food retail', 3),
|
||||
|
||||
-- E-commerce & Online
|
||||
('e-commerce-online', 'General E-commerce Store', 'general-e-commerce-store', 'Multi-category online retail stores', 1),
|
||||
('e-commerce-online', 'Online Fashion Store', 'online-fashion-store', 'Fashion-focused online retailers', 2),
|
||||
('e-commerce-online', 'Online Electronics Store', 'online-electronics-store', 'Electronics-focused online retailers', 3),
|
||||
('e-commerce-online', 'Marketplace Seller', 'marketplace-seller', 'Businesses selling primarily on marketplaces', 4),
|
||||
|
||||
-- Textile & Apparel Manufacturing
|
||||
('textile-apparel-mfg', 'Garment Factory', 'garment-factory', 'Clothing and garment mass production', 1),
|
||||
('textile-apparel-mfg', 'Fabric & Textile Mill', 'fabric-textile-mill', 'Fabric weaving, knitting, and textile mills', 2),
|
||||
('textile-apparel-mfg', 'Leather Goods Manufacturing', 'leather-goods-manufacturing', 'Bags, belts, and leather products', 3),
|
||||
|
||||
-- Food Processing
|
||||
('food-processing-mfg', 'Dairy Processing', 'dairy-processing', 'Milk, cheese, and dairy product manufacturing', 1),
|
||||
('food-processing-mfg', 'Meat Processing', 'meat-processing', 'Meat packing and processed meat products', 2),
|
||||
('food-processing-mfg', 'Snack Foods Manufacturing', 'snack-foods-manufacturing', 'Chips, nuts, and packaged snack production', 3),
|
||||
|
||||
-- Metal & Machinery
|
||||
('metal-machinery-mfg', 'Industrial Machinery', 'industrial-machinery', 'Heavy machinery and industrial equipment', 1),
|
||||
('metal-machinery-mfg', 'Metal Fabrication', 'metal-fabrication', 'Sheet metal, welding, and metal parts', 2),
|
||||
('metal-machinery-mfg', 'Tools & Hardware Manufacturing', 'tools-hardware-manufacturing', 'Hand tools and hardware production', 3),
|
||||
|
||||
-- Restaurants & Cafés
|
||||
('restaurants-cafes', 'Fast Food', 'fast-food', 'Quick-service and fast food restaurants', 1),
|
||||
('restaurants-cafes', 'Café & Coffee Shop', 'cafe-coffee-shop', 'Cafés, coffee shops, and tea houses', 2),
|
||||
('restaurants-cafes', 'Fine Dining Restaurant', 'fine-dining-restaurant', 'Full-service and upscale dining', 3),
|
||||
('restaurants-cafes', 'Bakery & Pastry Shop', 'bakery-pastry-shop', 'Retail bakeries and pastry shops', 4),
|
||||
|
||||
-- Design & Creative
|
||||
('design-creative', 'Graphic Design Studio', 'graphic-design-studio', 'Visual design and print-focused studios', 1),
|
||||
('design-creative', 'Branding Agency', 'branding-agency', 'Brand strategy, identity, and positioning', 2),
|
||||
('design-creative', 'Web Design Agency', 'web-design-agency', 'Website and digital experience design', 3),
|
||||
('design-creative', 'Photography Studio', 'photography-studio', 'Commercial and studio photography', 4),
|
||||
|
||||
-- Software & IT Services
|
||||
('software-it-services', 'Software Development', 'software-development', 'Custom software and application development', 1),
|
||||
('software-it-services', 'SaaS Company', 'saas-company', 'Software-as-a-service product companies', 2),
|
||||
('software-it-services', 'IT Consulting', 'it-consulting', 'IT strategy, integration, and support services', 3),
|
||||
|
||||
-- Beauty & Personal Care
|
||||
('beauty-personal-care', 'Cosmetics Store', 'cosmetics-store', 'Makeup and skincare retail', 1),
|
||||
('beauty-personal-care', 'Hair & Beauty Salon', 'hair-beauty-salon', 'Salons and beauty service providers', 2),
|
||||
('beauty-personal-care', 'Perfume & Fragrance Store', 'perfume-fragrance-store', 'Perfume and fragrance specialty retail', 3),
|
||||
|
||||
-- Furniture & Décor
|
||||
('furniture-decor', 'Furniture Store', 'furniture-store', 'Home and office furniture retail', 1),
|
||||
('furniture-decor', 'Home Décor Store', 'home-decor-store', 'Decorative items and home accessories', 2),
|
||||
('furniture-decor', 'Lighting Store', 'lighting-store', 'Lamps, fixtures, and lighting retail', 3),
|
||||
|
||||
-- Automotive
|
||||
('auto-dealers', 'Car Dealership', 'car-dealership', 'New and used passenger car dealers', 1),
|
||||
('auto-dealers', 'Motorcycle Dealer', 'motorcycle-dealer', 'Motorcycle and scooter dealerships', 2),
|
||||
('auto-parts-service', 'Auto Parts Store', 'auto-parts-store', 'Spare parts and accessories retail', 1),
|
||||
('auto-parts-service', 'Auto Repair & Service', 'auto-repair-service', 'Vehicle maintenance and repair workshops', 2)
|
||||
) AS v(parent_slug, name, slug, description, sort_order)
|
||||
ON p.slug = v.parent_slug;
|
||||
|
||||
-- Demo business category assignments (by slug)
|
||||
INSERT INTO business_category_assignments (business_id, category_id)
|
||||
SELECT 1, c.id
|
||||
FROM business_categories c
|
||||
WHERE c.slug IN ('general-e-commerce-store', 'electronics-retail', 'retail-shopping')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO business_category_assignments (business_id, category_id)
|
||||
SELECT 2, c.id
|
||||
FROM business_categories c
|
||||
WHERE c.slug IN ('branding-agency', 'graphic-design-studio', 'design-creative')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
SELECT setval(
|
||||
pg_get_serial_sequence('business_categories', 'id'),
|
||||
COALESCE((SELECT MAX(id) FROM business_categories), 1)
|
||||
);
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
cd "$ROOT_DIR"
|
||||
docker compose down -v
|
||||
docker compose up -d
|
||||
"$ROOT_DIR/database/seed.sh"
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
CONTAINER="${POSTGRES_CONTAINER:-meshkee-postgres}"
|
||||
DB_USER="${POSTGRES_USER:-meshkee}"
|
||||
DB_NAME="${POSTGRES_DB:-meshkee_cms}"
|
||||
MAX_ATTEMPTS="${WAIT_MAX_ATTEMPTS:-60}"
|
||||
SLEEP_SECONDS="${WAIT_SLEEP_SECONDS:-1}"
|
||||
|
||||
echo "Waiting for PostgreSQL in container '$CONTAINER'..."
|
||||
|
||||
for ((attempt = 1; attempt <= MAX_ATTEMPTS; attempt++)); do
|
||||
if docker exec "$CONTAINER" pg_isready -U "$DB_USER" -d "$DB_NAME" >/dev/null 2>&1; then
|
||||
if docker exec "$CONTAINER" psql -U "$DB_USER" -d "$DB_NAME" -tAc \
|
||||
"SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'users'" \
|
||||
2>/dev/null | grep -q 1; then
|
||||
echo "PostgreSQL is ready."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$attempt" -eq "$MAX_ATTEMPTS" ]]; then
|
||||
echo "PostgreSQL did not become ready within ${MAX_ATTEMPTS}s." >&2
|
||||
echo "Check: docker compose ps && docker compose logs postgres" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sleep "$SLEEP_SECONDS"
|
||||
done
|
||||
Reference in New Issue
Block a user