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:
Ali Reza
2026-07-21 17:52:36 +03:30
commit bb59d5e9ba
254 changed files with 37031 additions and 0 deletions
+183
View File
@@ -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));
+35
View File
@@ -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));
+113
View File
@@ -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));
+178
View File
@@ -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)
);