DatabaseCreation: dml.sql

File dml.sql, 58.9 KB (added by 231090, 5 days ago)
Line 
1INSERT INTO company_category (category_name)
2VALUES ('Hair Salon'),
3 ('Nail Salon'),
4 ('Spa'),
5 ('Beauty Salon'),
6 ('Barbershop'),
7 ('Massage Therapy'),
8 ('Skin Care'),
9 ('Eyebrow & Lash Studio'),
10 ('Tanning Salon'),
11 ('Waxing Studio'),
12 ('Makeup Studio'),
13 ('Hair Removal'),
14 ('Wellness Center'),
15 ('Med Spa');
16
17
18-- ============================================================
19-- 11. SEED — COMPANY
20-- ============================================================
21
22WITH salon_names(n) AS (SELECT * FROM beauty_salon_names)
23INSERT
24INTO company (name, email, logo_url, description, created_at)
25SELECT n,
26 LOWER(REPLACE(n, ' ', '')) || '@mail.com',
27 'https://img.com/' || ROW_NUMBER() OVER (),
28 'Beauty salon services',
29 NOW() - (random() * INTERVAL '25 years')
30FROM salon_names;
31
32
33-- ============================================================
34-- 12. SEED — COMPANY → CATEGORY
35-- ============================================================
36
37INSERT INTO company_company_category (company_id, company_category_id)
38SELECT c.company_id, cat.company_category_id
39FROM company c
40 CROSS JOIN company_category cat
41WHERE random() < 0.3
42ON CONFLICT DO NOTHING;
43
44
45-- ============================================================
46-- 13. SEED — COMPANY PHONE
47-- ============================================================
48
49UPDATE company
50SET phone =
51 CASE
52 WHEN random() < 0.70 THEN '+389 7' || floor(random() * 3)::int::text
53 || ' ' || floor(random() * 900 + 100)::int::text
54 || ' ' || floor(random() * 900 + 100)::int::text
55 WHEN random() < 0.33 THEN '+30 69' || floor(random() * 8 + 1)::int::text
56 || ' ' || floor(random() * 900 + 100)::int::text
57 || ' ' || floor(random() * 9000 + 1000)::int::text
58 WHEN random() < 0.50 THEN '+359 8' || floor(random() * 9)::int::text
59 || ' ' || floor(random() * 900 + 100)::int::text
60 || ' ' || floor(random() * 9000 + 1000)::int::text
61 ELSE '+381 6' || floor(random() * 9)::int::text
62 || ' ' || floor(random() * 900 + 100)::int::text
63 || ' ' || floor(random() * 9000 + 1000)::int::text
64 END
65where true;
66
67
68-- ============================================================
69-- 14. SEED — COMPANY LOCATIONS
70-- ============================================================
71
72INSERT INTO company_location (company_id, address, city, phone)
73SELECT c.company_id,
74 (ARRAY ['Ilindenska','Partizanska','Jane Sandanski',
75 'Bulevar Makedonija','Vodno','Goce Delchev',
76 'Dimitar Vlahov'])[FLOOR(RANDOM() * 7 + 1)]
77 || ' No. ' || FLOOR(RANDOM() * 200)::int,
78 (ARRAY ['Skopje','Bitola','Ohrid','Tetovo',
79 'Kumanovo','Prilep','Strumica'])[FLOOR(RANDOM() * 7 + 1)],
80 '+3897' || FLOOR(1000000 + RANDOM() * 8999999)::text
81FROM company c
82 JOIN LATERAL generate_series(1, (1 + FLOOR(RANDOM() * 20))::int) gs ON TRUE;
83
84
85-- ============================================================
86-- 15. SEED — BUSINESS HOURS
87-- ============================================================
88
89INSERT INTO business_hours (location_id, day_of_week, is_closed, open_time, close_time)
90SELECT l.location_id,
91 d.day_of_week,
92 (d.day_of_week = 'sunday') AS is_closed,
93 CASE
94 WHEN d.day_of_week = 'sunday' THEN NULL
95 WHEN d.day_of_week = 'saturday' THEN TIME '10:00' + (FLOOR(RANDOM() * 2) * INTERVAL '1 hour')
96 ELSE TIME '08:00' + (FLOOR(RANDOM() * 5) * INTERVAL '1 hour')
97 END AS open_time,
98 CASE
99 WHEN d.day_of_week = 'sunday' THEN NULL
100 WHEN d.day_of_week = 'saturday' THEN TIME '10:00' + (FLOOR(RANDOM() * 2) * INTERVAL '1 hour')
101 + (4 + FLOOR(RANDOM() * 2)) * INTERVAL '1 hour'
102 ELSE TIME '08:00' + (FLOOR(RANDOM() * 5) * INTERVAL '1 hour')
103 + (8 + FLOOR(RANDOM() * 3)) * INTERVAL '1 hour'
104 END AS close_time
105FROM company_location l
106 CROSS JOIN (SELECT unnest(enum_range(NULL::day_of_week_enum)) AS day_of_week) d;
107
108
109-- ============================================================
110-- 16. SEED — PROMO CODES
111-- ============================================================
112
113INSERT INTO promo_code (company_id, code, discount_type, discount_value, valid_from, valid_until)
114SELECT c.company_id,
115 UPPER(SUBSTRING(md5(RANDOM()::text), 1, 8)),
116 (ARRAY ['percentage', 'fixed'])[FLOOR(RANDOM() * 2 + 1)]::discount_type_enum,
117 CASE
118 WHEN RANDOM() < 0.5 THEN ROUND((5 + RANDOM() * 45)::numeric, 2)
119 ELSE ROUND((5 + RANDOM() * 50)::numeric, 2)
120 END,
121 CURRENT_DATE - (RANDOM() * INTERVAL '365 days'),
122 CURRENT_DATE + (RANDOM() * INTERVAL '180 days')
123FROM company c
124 JOIN LATERAL generate_series(1, (1 + FLOOR(RANDOM() * 5))::int) gs ON TRUE;
125
126
127-- ============================================================
128-- 17. SEED — USER TABLE
129-- ============================================================
130
131INSERT INTO "user" (first_name, last_name, email, password_hash, role, is_active, created_at)
132SELECT fn,
133 ln,
134 lower(fn) || '.' || lower(ln) || row_number() OVER () || '@example.com',
135 md5(random()::text),
136 CASE
137 WHEN rnd < 0.003 THEN 'admin'
138 WHEN rnd < 0.053 THEN 'staff'
139 WHEN rnd < 0.083 THEN 'owner'
140 ELSE 'client'
141 END::user_role_enum,
142 random() < 0.85,
143 NOW() - (pow(random(), 2) * INTERVAL '5 years')
144FROM (SELECT fn,
145 ln,
146 random() AS rnd
147 FROM first_names_200_mf AS f(fn)
148 CROSS JOIN last_names_40 AS l(ln)
149 CROSS JOIN generate_series(1, 1000)
150 ORDER BY random()
151 LIMIT 1000000) base;
152
153
154-- ============================================================
155-- 18. SEED — CLIENT TABLE
156-- ============================================================
157
158INSERT INTO client (client_id, loyalty_points, date_of_birth, phone, notes)
159SELECT user_id,
160 floor(random() * 5000)::int,
161 CURRENT_DATE - (floor(random() * 18628 + 6570)::int) * INTERVAL '1 day',
162 CASE
163 WHEN random() < 0.70 THEN '+389 7' || floor(random() * 3)::int::text
164 || ' ' || floor(random() * 900 + 100)::int::text
165 || ' ' || floor(random() * 900 + 100)::int::text
166 WHEN random() < 0.33 THEN '+30 69' || floor(random() * 8 + 1)::int::text
167 || ' ' || floor(random() * 900 + 100)::int::text
168 || ' ' || floor(random() * 9000 + 1000)::int::text
169 WHEN random() < 0.50 THEN '+359 8' || floor(random() * 9)::int::text
170 || ' ' || floor(random() * 900 + 100)::int::text
171 || ' ' || floor(random() * 9000 + 1000)::int::text
172 ELSE '+381 6' || floor(random() * 9)::int::text
173 || ' ' || floor(random() * 900 + 100)::int::text
174 || ' ' || floor(random() * 9000 + 1000)::int::text
175 END,
176 NULL
177FROM "user"
178WHERE role = 'client';
179
180
181-- ============================================================
182-- 19. SEED — OWNER TABLE
183-- ============================================================
184
185WITH company_slots AS (SELECT company_id,
186 generate_series(1, (1 + FLOOR(RANDOM() * 3))::int) AS slot
187 FROM company),
188 numbered_slots AS (SELECT company_id,
189 ROW_NUMBER() OVER () AS rn
190 FROM company_slots),
191 shuffled_owners AS (SELECT user_id,
192 ROW_NUMBER() OVER (ORDER BY RANDOM()) AS rn
193 FROM "user"
194 WHERE role = 'owner'
195 LIMIT (SELECT COUNT(*) FROM numbered_slots))
196INSERT
197INTO owner (owner_id, company_id)
198SELECT o.user_id, s.company_id
199FROM shuffled_owners o
200 JOIN numbered_slots s ON o.rn = s.rn;
201
202UPDATE owner
203SET owner_since = NOW() - random() * INTERVAL '10 years'
204where true;
205
206
207-- ============================================================
208-- 20. SEED — STAFF TABLE
209-- ============================================================
210
211INSERT INTO staff (staff_id, location_id, date_hired, hourly_rate)
212SELECT u.user_id,
213 cl.location_id,
214 CURRENT_DATE - (RANDOM() * INTERVAL '5 years'),
215 ROUND((10 + RANDOM() * 40)::numeric, 2)
216FROM (SELECT user_id,
217 ROW_NUMBER() OVER (ORDER BY user_id) AS rn
218 FROM "user"
219 WHERE role = 'staff') u
220 JOIN (SELECT location_id,
221 ROW_NUMBER() OVER (ORDER BY location_id) AS rn,
222 COUNT(*) OVER () AS total
223 FROM company_location) cl ON ((u.rn - 1) % cl.total) = (cl.rn - 1);
224
225
226
227-- ============================================================
228-- 21. SEED — STAFF TYPE
229-- ============================================================
230
231INSERT INTO staff_type (staff_id, role_type, specialty, certification, years_experience, extra_info)
232SELECT s.staff_id,
233 (ARRAY ['hairdresser','nail_tech','esthetician',
234 'makeup_artist','receptionist'])[FLOOR(RANDOM() * 5 + 1)]::staff_role_type_enum,
235 'General Specialty',
236 'Certified Professional',
237 FLOOR(RANDOM() * 15)::int,
238 'Staff member'
239FROM staff s
240ON CONFLICT (staff_id, role_type) DO NOTHING;
241
242
243-- ============================================================
244-- 22. SEED — SERVICE CATEGORIES
245-- ============================================================
246
247INSERT INTO service_category (category_name, description)
248VALUES ('Haircuts', 'Basic and premium haircut services for men and women.'),
249 ('Hair Coloring', 'Highlights, full color, balayage, root touch-ups, and color correction.'),
250 ('Extensions & Weaves', 'Hair extensions, tape-ins, clip-ins, and weaves for added length and volume.'),
251 ('Styling & Blowouts', 'Blowouts, updos, curls, and everyday styling for events and special occasions.'),
252 ('Nail Manicure', 'Basic nail care, shaping, cuticle treatment, and polish application.'),
253 ('Nail Pedicure', 'Foot soak, exfoliation, callus removal, and nail polish application.'),
254 ('Nail Art & Design', 'Decorative nail art, patterns, and custom designs.'),
255 ('Acrylic & Gel Nails', 'Acrylic, gel, and hard-gel overlay services for long-lasting nails.'),
256 ('Natural Nail Care', 'Gentle care focused on strengthening and maintaining healthy natural nails.'),
257 ('Facials', 'Deep cleansing, exfoliation, masks, and moisturizing treatments for the face.'),
258 ('Body Treatments', 'Body scrubs, wraps, and hydrating treatments for the whole body.'),
259 ('Massages', 'Relaxing and therapeutic massages including neck, shoulder, back, and full-body.'),
260 ('Manicure & Pedicure', 'Combined hand and foot nail services for relaxation and grooming.'),
261 ('Waxing', 'Hair removal using wax for brows, legs, arms, bikini, and other body areas.'),
262 ('Laser Hair Removal', 'Laser-based permanent or semi-permanent hair reduction.'),
263 ('Eyebrow Shaping', 'Brow waxing, threading, or trimming for well-defined shape.'),
264 ('Eyebrow Tinting', 'Enhancing brow color for a fuller, more defined look.'),
265 ('Lash Extensions', 'Individual or volume lash extensions for longer, fuller lashes.'),
266 ('Lash Lift & Tint', 'Lifting and tinting natural lashes for a wide-eyed look.'),
267 ('Spray Tanning', 'Professional spray tanning for an even, sun-kissed glow.'),
268 ('Bronzing & Contouring', 'Makeup contouring for facial definition and sculpting.'),
269 ('Makeup Application', 'Day, evening, or special-event makeup application.'),
270 ('Bridal Makeup', 'Full bridal makeup trial and wedding-day application.'),
271 ('Skincare Consultation', 'Personalized skin analysis and tailored treatment plans.'),
272 ('Chemical Peels', 'Exfoliating treatments to improve texture, tone, and clarity of the skin.'),
273 ('Microneedling', 'Collagen-stimulating procedure to reduce fine lines, scars, and pores.'),
274 ('Facial Hair Removal', 'Precision removal of facial hair using wax, threading, or threading-based methods.'),
275 ('Body Hair Removal', 'Waxing and laser-assisted removal for larger body areas.'),
276 ('Manicure & Pedicure Men', 'Manicure and pedicure services tailored for men.'),
277 ('Men Haircuts', 'Haircut and styling services designed specifically for men.'),
278 ('Men Beard Grooming', 'Beard trimming, shaping, oiling, and styling.'),
279 ('Men Skincare', 'Facial and body skincare routines for men.'),
280 ('Detox & Wellness Massage', 'Massage focused on detoxification and overall wellness.'),
281 ('Aromatherapy Massage', 'Massage using essential oils to enhance relaxation and mood.'),
282 ('Hot Stone Massage', 'Massage using heated stones to relieve muscle tension.'),
283 ('Couples Massage', 'Simultaneous massage for two people in a relaxing environment.'),
284 ('Prenatal Massage', 'Gentle massage tailored for pregnant women.'),
285 ('Deep Tissue Massage', 'Intensive massage targeting deeper muscle layers and tension.'),
286 ('Sports Massage', 'Massage focused on athletes and performance recovery.'),
287 ('Body Polish & Exfoliation', 'Full-body exfoliation to remove dead skin and leave skin smooth.'),
288 ('Hydration & Nourishing Treatments', 'Intensive moisturizing masks and treatments for dry or tired skin.'),
289 ('Acne & Problem Skin Care', 'Targeted treatments for acne, breakouts, and oily or sensitive skin.'),
290 ('Anti-Aging Treatments', 'Procedures and masks aimed at reducing signs of aging and wrinkles.'),
291 ('Brightening & Even Skin Tone', 'Treatments designed to lighten dark spots and even out skin tone.'),
292 ('LED Light Therapy', 'Non-invasive light treatments to support skin healing and rejuvenation.'),
293 ('Microdermabrasion', 'Mechanical exfoliation to improve skin texture and brightness.'),
294 ('Facial Cleansing', 'Deep cleansing and pore extraction for clearer skin.'),
295 ('Facial Hydration', 'Intensive hydration masks and serums for dry or dehydrated skin.'),
296 ('Body Sculpting & Wraps', 'Wraps and treatments aimed at temporary contouring and detox.'),
297 ('Threading & Eyebrow Services', 'Precision threading for brows and facial hair lineups.'),
298 ('Lash & Brow Tinting Combo', 'Combined tinting service for both lashes and brows.'),
299 ('Express Manicure', 'Quick manicure focusing on basic shaping and polish.'),
300 ('Express Pedicure', 'Fast pedicure service for basic nail care and polish.'),
301 ('Express Facial', 'Short facial treatment for busy clients.'),
302 ('Express Waxing', 'Quick waxing for small areas like brows or upper lip.'),
303 ('On-Site Services', 'Mobile or on-site beauty services for events and home visits.'),
304 ('Glow-Up Packages', 'Curated packages designed to enhance overall appearance and confidence.'),
305 ('Consultation Only', 'Initial consultation without treatment for plan and pricing discussion.'),
306 ('Hair Treatment', 'Keratin treatments, deep conditioning, scalp therapy, and repair treatments.'),
307 ('Botox', 'Botulinum toxin injections to reduce wrinkles and fine lines.'),
308 ('Fillers', 'Dermal filler injections for volume, contouring, and wrinkle correction.'),
309 ('Body Contouring', 'Non-surgical body shaping treatments to reduce fat and tighten skin.');
310
311WITH category_mapping(comp_cat, svc_cat) AS (VALUES ('Hair Salon', 'Haircuts'),
312 ('Hair Salon', 'Hair Coloring'),
313 ('Hair Salon', 'Extensions & Weaves'),
314 ('Hair Salon', 'Styling & Blowouts'),
315 ('Hair Salon', 'Hair Treatment'),
316 ('Nail Salon', 'Nail Manicure'),
317 ('Nail Salon', 'Nail Pedicure'),
318 ('Nail Salon', 'Nail Art & Design'),
319 ('Nail Salon', 'Acrylic & Gel Nails'),
320 ('Nail Salon', 'Express Manicure'),
321 ('Barbershop', 'Men Haircuts'),
322 ('Barbershop', 'Men Beard Grooming'),
323 ('Barbershop', 'Men Skincare'),
324 ('Spa', 'Massages'),
325 ('Spa', 'Body Treatments'),
326 ('Spa', 'Hot Stone Massage'),
327 ('Spa', 'Facial Hydration'),
328 ('Skin Care', 'Facials'),
329 ('Skin Care', 'Chemical Peels'),
330 ('Skin Care', 'Microneedling'),
331 ('Skin Care', 'Skincare Consultation'),
332 ('Waxing Studio', 'Waxing'),
333 ('Waxing Studio', 'Facial Hair Removal'),
334 ('Waxing Studio', 'Express Waxing'),
335 ('Eyebrow & Lash Studio', 'Eyebrow Shaping'),
336 ('Eyebrow & Lash Studio', 'Eyebrow Tinting'),
337 ('Eyebrow & Lash Studio', 'Lash Extensions'),
338 ('Med Spa', 'Botox'),
339 ('Med Spa', 'Fillers'),
340 ('Med Spa', 'Body Contouring'),
341 ('Med Spa', 'Chemical Peels'),
342 ('Massage Therapy', 'Massages'),
343 ('Massage Therapy', 'Deep Tissue Massage'),
344 ('Massage Therapy', 'Sports Massage')),
345
346-- 2. Дефинираме конкретни услуги за секоја категорија на услуга
347 service_blueprints(svc_cat_name, svc_name, duration, base_price) AS (VALUES ('Haircuts', 'Basic Haircut', 30,
348 35.00),
349 ('Hair Coloring', 'Full Hair Color',
350 90,
351 120.00),
352 ('Styling & Blowouts',
353 'Blowout Styling', 45,
354 55.00),
355 ('Men Haircuts', 'Men’s Fade & Style',
356 45,
357 40.00),
358 ('Men Beard Grooming',
359 'Full Beard Grooming',
360 30, 30.00),
361 ('Hair Treatment',
362 'Deep Conditioning Mask',
363 45, 60.00),
364
365
366 ('Nail Manicure', 'Classic Manicure',
367 30,
368 25.00),
369 ('Nail Pedicure', 'Spa Pedicure', 45,
370 40.00),
371 ('Nail Art & Design',
372 'Custom Nail Art (Full Set)', 60,
373 50.00),
374 ('Acrylic & Gel Nails',
375 'Gel Extensions', 90,
376 85.00),
377
378
379 ('Facials', 'Signature Facial', 60,
380 85.00),
381 ('Body Treatments',
382 'Sea Salt Body Scrub', 60,
383 95.00),
384 ('Chemical Peels', 'Glycolic Peel', 45,
385 85.00),
386 ('Microneedling',
387 'Microneedling Session',
388 120, 250.00),
389 ('Skincare Consultation',
390 'Skin Analysis & Plan', 30, 45.00),
391
392
393 ('Waxing', 'Full Body Wax', 120,
394 140.00),
395 ('Laser Hair Removal',
396 'Laser Hair Removal - Face', 30,
397 150.00),
398 ('Eyebrow Shaping',
399 'Signature Brow Shaping',
400 30, 25.00),
401
402
403 ('Lash Extensions',
404 'Lash Extensions - Classic', 90,
405 120.00),
406 ('Lash Lift & Tint',
407 'Lash Lift & Tint Combo',
408 60, 110.00),
409 ('Spray Tanning',
410 'Spray Tan - Full Body', 30,
411 50.00),
412
413
414 ('Makeup Application',
415 'Full Face Glam', 60,
416 75.00),
417 ('Bridal Makeup',
418 'Bridal Trial & Day-of',
419 120, 250.00),
420 ('Bronzing & Contouring',
421 'Express Sculpt & Glow', 30, 40.00),
422
423
424 ('Massages', 'Swedish Massage', 60,
425 80.00),
426 ('Deep Tissue Massage',
427 'Deep Tissue Relief',
428 60, 110.00),
429 ('Hot Stone Massage',
430 'Hot Stone Therapy', 90,
431 130.00),
432 ('Aromatherapy Massage',
433 'Essential Oil Relaxer', 60, 95.00),
434
435
436 ('Botox', 'Botox Treatment (per area)',
437 30,
438 200.00),
439 ('Fillers', 'Dermal Filler Session',
440 45,
441 500.00),
442
443
444 ('Consultation Only',
445 'Professional Consultation', 15,
446 0.00))
447
448INSERT
449INTO service (company_id, service_category_id, service_name, duration_minutes, price, is_active)
450SELECT ccc.company_id,
451 sc.service_category_id,
452 sb.svc_name,
453 sb.duration,
454 sb.base_price,
455 TRUE
456FROM company_company_category ccc
457 JOIN company_category cc ON ccc.company_category_id = cc.company_category_id
458 JOIN category_mapping cm ON cc.category_name = cm.comp_cat
459 JOIN service_category sc ON sc.category_name = cm.svc_cat
460 JOIN service_blueprints sb ON sb.svc_cat_name = sc.category_name
461ON CONFLICT DO NOTHING;
462
463
464-- ============================================================
465-- 24. SEED — STAFF SERVICE
466-- ============================================================
467
468INSERT INTO staff_service (staff_id, service_id)
469SELECT DISTINCT ON (s.staff_id, sv.service_id) s.staff_id,
470 sv.service_id
471FROM staff s
472 JOIN staff_type st ON st.staff_id = s.staff_id
473 JOIN company_location cl ON cl.location_id = s.location_id
474 JOIN service sv ON sv.company_id = cl.company_id AND sv.is_active = TRUE
475 JOIN service_category sc ON sc.service_category_id = sv.service_category_id
476WHERE (
477 (st.role_type = 'hairdresser' AND sc.category_name IN (
478 'Haircuts', 'Hair Coloring', 'Extensions & Weaves',
479 'Styling & Blowouts', 'Men Haircuts', 'Men Beard Grooming',
480 'Hair Treatment'))
481 OR (st.role_type = 'nail_tech' AND sc.category_name IN (
482 'Nail Manicure', 'Nail Pedicure', 'Nail Art & Design',
483 'Acrylic & Gel Nails', 'Natural Nail Care',
484 'Manicure & Pedicure',
485 'Express Manicure', 'Express Pedicure'))
486 OR (st.role_type = 'esthetician' AND sc.category_name IN (
487 'Facials', 'Body Treatments', 'Waxing',
488 'Laser Hair Removal',
489 'Chemical Peels', 'Microneedling', 'Eyebrow Shaping',
490 'Eyebrow Tinting',
491 'Lash Extensions', 'Lash Lift & Tint',
492 'Spray Tanning', 'Massages',
493 'Deep Tissue Massage', 'Sports Massage',
494 'Hot Stone Massage',
495 'Couples Massage', 'Aromatherapy Massage',
496 'Prenatal Massage',
497 'Detox & Wellness Massage',
498 'Acne & Problem Skin Care',
499 'Anti-Aging Treatments', 'Facial Hair Removal',
500 'Body Hair Removal',
501 'Skincare Consultation', 'Skin Care',
502 'LED Light Therapy', 'Microdermabrasion',
503 'Body Contouring',
504 'Botox', 'Fillers'))
505 OR (st.role_type = 'makeup_artist' AND sc.category_name IN (
506 'Makeup Application', 'Bridal Makeup',
507 'Bronzing & Contouring'))
508 OR (st.role_type = 'receptionist' AND sc.category_name IN (
509 'Skincare Consultation', 'Consultation Only'))
510 )
511 AND (SELECT COUNT(*) FROM staff_service ss WHERE ss.staff_id = s.staff_id) < 3
512ON CONFLICT DO NOTHING;
513
514
515-- ============================================================
516-- 25. SEED — STAFF AVAILABILITY
517-- ============================================================
518
519WITH location_hours AS (SELECT bh.location_id,
520 bh.day_of_week,
521 bh.open_time,
522 bh.close_time,
523 EXTRACT(EPOCH FROM (bh.close_time - bh.open_time)) / 60 AS available_minutes
524 FROM business_hours bh
525 WHERE bh.is_closed = FALSE
526 AND bh.day_of_week::text <> 'sunday'),
527 staff_days AS (SELECT s.staff_id,
528 lh.day_of_week,
529 lh.open_time,
530 lh.available_minutes
531 FROM staff s
532 JOIN location_hours lh ON lh.location_id = s.location_id),
533 staff_shifts AS (SELECT staff_id,
534 day_of_week,
535 open_time,
536 available_minutes,
537 LEAST(
538 (ARRAY [240, 360, 480])[FLOOR(RANDOM() * 3 + 1)::int],
539 available_minutes::int
540 ) AS shift_minutes
541 FROM staff_days),
542 staff_timed AS (SELECT staff_id,
543 day_of_week,
544 open_time,
545 available_minutes,
546 shift_minutes,
547 SQRT(-2.0 * LN(NULLIF(RANDOM(), 0))) * COS(2 * PI() * RANDOM()) AS z
548 FROM staff_shifts),
549 staff_timed_clamped AS (SELECT staff_id,
550 day_of_week,
551 shift_minutes,
552 (open_time + (
553 GREATEST(0, LEAST(
554 available_minutes - shift_minutes,
555 ROUND(
556 ((available_minutes - shift_minutes) / 2.0)
557 + z * ((available_minutes - shift_minutes) / 6.0)
558 )
559 )) || ' minutes')::interval)::time AS start_time
560 FROM staff_timed)
561INSERT
562INTO staff_availability (staff_id, day_of_week, start_time, end_time)
563SELECT staff_id,
564 day_of_week,
565 start_time,
566 (start_time + (shift_minutes || ' minutes')::interval)::time AS end_time
567FROM staff_timed_clamped
568ON CONFLICT (staff_id, day_of_week) DO NOTHING;
569
570
571
572-- ============================================================
573-- 26. SEED — BLOCKED TIME
574-- ============================================================
575
576WITH block_series AS (SELECT s.staff_id,
577 gs.block_num,
578 random() < 0.60 AS is_single_day,
579 (NOW() - (random() * INTERVAL '2 years'))::date AS block_date,
580 random() AS reason_rnd
581 FROM staff s
582 CROSS JOIN generate_series(1, (2 + FLOOR(RANDOM() * 4))::int) AS gs(block_num)),
583 blocks_computed AS (SELECT staff_id,
584 (block_date::timestamp + TIME '09:00')::timestamptz AS start_datetime,
585 CASE
586 WHEN is_single_day
587 THEN (block_date::timestamp + TIME '17:00')::timestamptz
588 ELSE (block_date::timestamp
589 + ((3 + FLOOR(RANDOM() * 5)) || ' days')::interval
590 + TIME '17:00')::timestamptz
591 END AS end_datetime,
592 CASE
593 WHEN reason_rnd < 0.20 THEN 'Sick day'
594 WHEN reason_rnd < 0.40 THEN 'Personal leave'
595 WHEN reason_rnd < 0.55 THEN 'Vacation'
596 WHEN reason_rnd < 0.70 THEN 'Medical appointment'
597 WHEN reason_rnd < 0.80 THEN 'Family emergency'
598 WHEN reason_rnd < 0.90 THEN 'Training / workshop'
599 END AS reason
600 FROM block_series)
601INSERT
602INTO blocked_time (staff_id, start_datetime, end_datetime, reason)
603SELECT staff_id, start_datetime, end_datetime, reason
604FROM blocks_computed
605WHERE end_datetime > start_datetime;
606
607
608-- ============================================================
609-- 27. SEED — APPOINTMENTS + APPOINTMENT_SERVICE
610-- ============================================================
611
612-- COMPLETED ROWS
613CREATE TEMP TABLE tmp_clients AS
614SELECT client_id,
615 ROW_NUMBER() OVER (ORDER BY random()) AS rn
616FROM client;
617
618CREATE TEMP TABLE tmp_staff_service AS
619SELECT ss.staff_id,
620 ss.service_id,
621 sv.duration_minutes,
622 sv.price,
623 s.location_id,
624 ROW_NUMBER() OVER (ORDER BY random()) AS rn
625FROM staff_service ss
626 JOIN service sv ON sv.service_id = ss.service_id
627 JOIN staff s ON s.staff_id = ss.staff_id;
628
629INSERT INTO appointment (client_id, staff_id, location_id,
630 appointment_date, appointment_time, end_time,
631 status, booked_at, cancelled_at, cancellation_reason)
632SELECT DISTINCT ON (gen.staff_id, gen.appt_date, gen.start_time) t.client_id,
633 gen.staff_id,
634 gen.location_id,
635 gen.appt_date,
636 gen.start_time,
637 (gen.start_time + (gen.duration_minutes || ' minutes')::interval)::time,
638 gen.status_val,
639 (gen.appt_date::timestamp - (random() * INTERVAL '30 days'))::timestamptz,
640 CASE
641 WHEN gen.status_val = 'cancelled'
642 THEN (gen.appt_date::timestamp - (random() * INTERVAL '5 days'))::timestamptz
643 END,
644 CASE
645 WHEN gen.status_val = 'cancelled'
646 THEN (ARRAY ['Client no-show','Staff unavailable','Personal reasons',
647 'Rescheduled','Weather conditions'])[FLOOR(random() * 5 + 1)::int]
648 END
649FROM (SELECT ss.staff_id,
650 ss.location_id,
651 ss.duration_minutes,
652 (CURRENT_DATE - (random() * 1825)::int) AS appt_date,
653 (TIME '08:00' + (FLOOR(random() * 40) * 15) * INTERVAL '1 minute')::time AS start_time,
654 CASE
655 WHEN random() < 0.60 THEN 'completed'
656 WHEN random() < 0.75 THEN 'confirmed'
657 WHEN random() < 0.90 THEN 'cancelled'
658 ELSE 'pending'
659 END::appointment_status_enum AS status_val,
660 ROW_NUMBER() OVER () AS rn
661 FROM tmp_staff_service ss
662 CROSS JOIN generate_series(1, 10)
663 ORDER BY random()
664 LIMIT 500000) gen
665 JOIN tmp_clients t ON (gen.rn % (SELECT COUNT(*) FROM tmp_clients)) = (t.rn - 1)
666ON CONFLICT (staff_id, appointment_date, appointment_time) DO NOTHING;
667
668DROP TABLE tmp_clients;
669DROP TABLE tmp_staff_service;
670
671select count(*) from appointment a ;
672---- PENDING ROWS
673
674CREATE TEMP TABLE tmp_clients_p AS
675SELECT client_id,
676 ROW_NUMBER() OVER (ORDER BY random()) AS rn
677FROM client;
678
679CREATE TEMP TABLE tmp_staff_service_p AS
680SELECT ss.staff_id,
681 ss.service_id,
682 sv.duration_minutes,
683 sv.price,
684 s.location_id,
685 ROW_NUMBER() OVER (ORDER BY random()) AS rn
686FROM staff_service ss
687 JOIN service sv ON sv.service_id = ss.service_id
688 JOIN staff s ON s.staff_id = ss.staff_id;
689
690INSERT INTO appointment (client_id, staff_id, location_id,
691 appointment_date, appointment_time, end_time,
692 status, booked_at, cancelled_at, cancellation_reason)
693SELECT DISTINCT ON (gen.staff_id, gen.appt_date, gen.start_time) t.client_id,
694 gen.staff_id,
695 gen.location_id,
696 gen.appt_date,
697 gen.start_time,
698 (gen.start_time + (gen.duration_minutes || ' minutes')::interval)::time,
699 'pending'::appointment_status_enum,
700 (NOW() - (random() * INTERVAL '14 days'))::timestamptz,
701 NULL,
702 NULL
703FROM (SELECT tss.staff_id,
704 tss.location_id,
705 tss.duration_minutes,
706 (CURRENT_DATE + (14 + (random() * 106)::int)) AS appt_date,
707 (TIME '08:00' + (FLOOR(random() * 40) * 15) * INTERVAL '1 minute')::time AS start_time,
708 ROW_NUMBER() OVER () AS rn
709 FROM tmp_staff_service_p tss
710 CROSS JOIN generate_series(1, 10)
711 ORDER BY random()
712 LIMIT 500000) gen
713 JOIN tmp_clients_p t ON (gen.rn % (SELECT COUNT(*) FROM tmp_clients_p)) = (t.rn - 1)
714ON CONFLICT (staff_id, appointment_date, appointment_time) DO NOTHING;
715
716DROP TABLE tmp_clients_p;
717DROP TABLE tmp_staff_service_p;
718
719
720---CANCELLED ROWS
721
722CREATE TEMP TABLE tmp_clients_c AS
723SELECT client_id,
724 ROW_NUMBER() OVER (ORDER BY random()) AS rn
725FROM client;
726
727CREATE TEMP TABLE tmp_staff_service_c AS
728SELECT ss.staff_id,
729 ss.service_id,
730 sv.duration_minutes,
731 sv.price,
732 s.location_id,
733 ROW_NUMBER() OVER (ORDER BY random()) AS rn
734FROM staff_service ss
735 JOIN service sv ON sv.service_id = ss.service_id
736 JOIN staff s ON s.staff_id = ss.staff_id;
737
738INSERT INTO appointment (client_id, staff_id, location_id,
739 appointment_date, appointment_time, end_time,
740 status, booked_at, cancelled_at, cancellation_reason)
741SELECT DISTINCT ON (gen.staff_id, gen.appt_date, gen.start_time) t.client_id,
742 gen.staff_id,
743 gen.location_id,
744 gen.appt_date,
745 gen.start_time,
746 (gen.start_time + (gen.duration_minutes || ' minutes')::interval)::time,
747 'cancelled'::appointment_status_enum,
748 -- booked_at before the appointment date
749 (gen.appt_date::timestamp - (random() * INTERVAL '30 days'))::timestamptz,
750 -- canceled_at: between booked_at and the appointment date (1–5 days before appt)
751 (gen.appt_date::timestamp - (random() * INTERVAL '5 days'))::timestamptz,
752 (ARRAY ['Client no-show','Staff unavailable','Personal reasons',
753 'Rescheduled','Weather conditions'])[FLOOR(random() * 5 + 1)::int]
754FROM (SELECT tssc.staff_id,
755 tssc.location_id,
756 tssc.duration_minutes,
757 -- any point in the past 5 years
758 (CURRENT_DATE - (random() * 1825)::int) AS appt_date,
759 (TIME '08:00' + (FLOOR(random() * 40) * 15) * INTERVAL '1 minute')::time AS start_time,
760 ROW_NUMBER() OVER () AS rn
761 FROM tmp_staff_service_c tssc
762 CROSS JOIN generate_series(1, 10)
763 ORDER BY random()
764 LIMIT 100000) gen
765 JOIN tmp_clients_c t ON (gen.rn % (SELECT COUNT(*) FROM tmp_clients_c)) = (t.rn - 1)
766ON CONFLICT (staff_id, appointment_date, appointment_time) DO NOTHING;
767
768DROP TABLE tmp_clients_c;
769
770DROP TABLE tmp_staff_service_c;
771
772
773-- APPOINTMENT SERVICE
774INSERT INTO appointment_service (appointment_id, service_id, duration_minutes, price)
775SELECT appointment_id, service_id, duration_minutes, price
776FROM (SELECT a.appointment_id,
777 s.service_id,
778 s.duration_minutes,
779 s.price,
780 ROW_NUMBER() OVER (
781 PARTITION BY a.appointment_id
782 ORDER BY random()
783 ) AS rn
784 FROM appointment a
785 JOIN staff_service ss ON ss.staff_id = a.staff_id
786 JOIN service s ON s.service_id = ss.service_id
787 WHERE random() < 0.6) sub
788WHERE rn <= 3
789ON CONFLICT DO NOTHING;
790
791
792
793-- UPDATE na end_time spored sum od services vo Appointment
794
795UPDATE appointment a
796SET end_time = (
797 a.appointment_time + (sub.total_minutes || ' minutes')::interval
798 )::time
799FROM (SELECT appointment_id, SUM(duration_minutes) AS total_minutes
800 FROM appointment_service
801 GROUP BY appointment_id) sub
802WHERE a.appointment_id = sub.appointment_id;
803select * from appointment;
804
805
806-- ============================================================
807-- 28. SEED — PRODUCTS
808-- ============================================================
809
810INSERT INTO product (product_name, brand, unit_price)
811SELECT p AS product_name,
812 b AS brand,
813 ROUND((RANDOM() * 140 + 10)::NUMERIC(10, 2), 2) AS unit_price
814FROM beauty_products_name p
815 CROSS JOIN brands_name b
816 CROSS JOIN generate_series(1, 1667)
817ORDER BY RANDOM()
818LIMIT 1000;
819
820
821
822
823UPDATE product
824SET unit_price = ROUND((RANDOM() * 140 + 10)::NUMERIC(10, 2), 2)
825where true;
826
827
828-- ============================================================
829-- 29. SEED — INVENTORY
830-- ============================================================
831
832ALTER TABLE inventory
833 SET UNLOGGED;
834
835WITH product_totals AS (SELECT product_id,
836 (RANDOM() * 9997 + 3) AS total_units
837 FROM product)
838INSERT
839INTO inventory (product_id, location_id, quantity_on_hand)
840SELECT p.product_id,
841 l.location_id,
842 GREATEST(1, ROUND((p.total_units * (0.5 + RANDOM()) / 2)::NUMERIC, 3))
843FROM product_totals p
844 JOIN LATERAL (
845 SELECT location_id
846 FROM company_location
847 ORDER BY RANDOM()
848 LIMIT (1 + FLOOR(RANDOM() * 3))::int
849 ) l ON TRUE;
850
851ALTER TABLE inventory
852 SET LOGGED;
853
854
855-- ============================================================
856-- 30. SEED — APPOINTMENT PRODUCTS
857-- ============================================================
858
859INSERT INTO appointment_product (appointment_id, product_id, quantity_used)
860SELECT a.appointment_id,
861 ((ROW_NUMBER() OVER (ORDER BY RANDOM()) - 1) % (SELECT MAX(product_id) FROM product)) + 1,
862 ROUND((0.1 + RANDOM() * 2.9)::numeric, 3)
863FROM appointment a
864WHERE RANDOM() < 0.40
865ON CONFLICT DO NOTHING;
866
867
868-- ============================================================
869-- 31. SEED — INVOICES + PROMO DISCOUNTS
870-- ============================================================
871
872
873-- insert invoices for all completed appointments
874INSERT INTO invoice (appointment_id, client_id, invoice_date, subtotal, tax, payment_method)
875SELECT a.appointment_id,
876 a.client_id,
877 a.appointment_date AS invoice_date,
878 SUM(as2.price) AS subtotal,
879 ROUND(SUM(as2.price) * 0.18, 2) AS tax,
880 CASE
881 WHEN random() < 0.60 THEN 'card'
882 WHEN random() < 0.75 THEN 'cash'
883 ELSE 'loyalty_points'
884 END::payment_method_enum
885FROM appointment a
886 JOIN appointment_service as2 ON as2.appointment_id = a.appointment_id
887WHERE a.status = 'completed'
888GROUP BY a.appointment_id, a.client_id, a.appointment_date;
889
890-- match valid promos (~20% of invoices)
891CREATE TEMP TABLE temp_matched_promos AS
892WITH invoice_company AS (SELECT i.invoice_id,
893 i.invoice_date,
894 i.subtotal,
895 cl.company_id
896 FROM invoice i
897 JOIN appointment a ON a.appointment_id = i.appointment_id
898 JOIN company_location cl ON cl.location_id = a.location_id
899 WHERE random() < 0.20),
900 matched AS (SELECT DISTINCT ON (ic.invoice_id) ic.invoice_id,
901 p.promo_id,
902 p.discount_type,
903 p.discount_value,
904 ic.subtotal,
905 CASE
906 WHEN p.discount_type = 'percentage'
907 THEN ROUND(ic.subtotal * p.discount_value / 100.0, 2)
908 ELSE LEAST(p.discount_value, ic.subtotal)
909 END AS discount_amount
910 FROM invoice_company ic
911 JOIN promo_code p
912 ON p.company_id = ic.company_id
913 AND p.valid_from <= ic.invoice_date
914 AND p.valid_until >= ic.invoice_date
915 ORDER BY ic.invoice_id, RANDOM())
916SELECT *
917FROM matched;
918
919-- apply discounts
920UPDATE invoice i
921SET discount_total = t.discount_amount
922FROM temp_matched_promos t
923WHERE i.invoice_id = t.invoice_id;
924
925-- audit trail
926INSERT INTO invoice_promo (invoice_id, promo_id)
927SELECT invoice_id, promo_id
928FROM temp_matched_promos
929ON CONFLICT DO NOTHING;
930
931DROP TABLE temp_matched_promos;
932
933
934-- ============================================================
935-- 32. SEED — REVIEWS
936-- ============================================================
937
938WITH completed AS (SELECT a.appointment_id, a.client_id, a.appointment_date
939 FROM appointment a
940 WHERE a.status = 'completed'
941 AND random() < 0.50)
942INSERT
943INTO review (appointment_id, client_id, rating, comment, created_at)
944SELECT appointment_id,
945 client_id,
946 rating,
947 CASE
948 WHEN random() < 0.30 THEN
949 CASE rating
950 WHEN 5 THEN (ARRAY ['Fantastic, best salon in town!','Exceeded my expectations!',
951 'Great service, very satisfied!','Will definitely come back.',
952 'Very relaxing, highly recommend.'])[FLOOR(RANDOM() * 5 + 1)::int]
953 WHEN 4 THEN (ARRAY ['Staff was friendly and professional.','Good experience overall.',
954 'Nice atmosphere and great results.','Quick and efficient service.',
955 'Very clean and welcoming space.'])[FLOOR(RANDOM() * 5 + 1)::int]
956 WHEN 3 THEN (ARRAY ['Average service, nothing special.','A bit rushed but acceptable.',
957 'Decent but room for improvement.','Was okay, not great not bad.',
958 'Expected a bit more for the price.'])[FLOOR(RANDOM() * 5 + 1)::int]
959 WHEN 2 THEN (ARRAY ['Not what I expected.','Disappointed with the results.',
960 'Service was below average.','Staff seemed uninterested.',
961 'Would think twice before returning.'])[FLOOR(RANDOM() * 5 + 1)::int]
962 WHEN 1 THEN (ARRAY ['Would not recommend.','Very disappointed with the service.',
963 'Terrible experience overall.','Staff was rude and unprofessional.',
964 'Complete waste of money.'])[FLOOR(RANDOM() * 5 + 1)::int]
965 END
966 END AS comment,
967 (appointment_date + (random() * INTERVAL '7 days'))::timestamptz AS created_at
968FROM (SELECT appointment_id,
969 client_id,
970 appointment_date,
971 CASE
972 WHEN random() < 0.40 THEN 5
973 WHEN random() < 0.70 THEN 4
974 WHEN random() < 0.85 THEN 3
975 WHEN random() < 0.95 THEN 2
976 ELSE 1
977 END::SMALLINT AS rating
978 FROM completed) r;
979
980
981-- ============================================================
982-- 33. SEED — LOYALTY TRANSACTIONS + POINT SYNC
983-- ============================================================
984
985-- earning rows
986INSERT INTO loyalty_transaction (client_id, appointment_id, points_earned, points_spent, created_at)
987SELECT i.client_id,
988 i.appointment_id,
989 FLOOR(i.total)::int AS points_earned,
990 0 AS points_spent,
991 (i.invoice_date + (random() * INTERVAL '1 hour'))::timestamptz
992FROM invoice i
993WHERE i.payment_method <> 'loyalty_points'
994 AND FLOOR(i.total) > 0;
995
996-- spending rows
997INSERT INTO loyalty_transaction (client_id, appointment_id, points_earned, points_spent, created_at)
998SELECT i.client_id,
999 i.appointment_id,
1000 0 AS points_earned,
1001 FLOOR(i.subtotal)::int AS points_spent,
1002 (i.invoice_date + (random() * INTERVAL '1 hour'))::timestamptz
1003FROM invoice i
1004WHERE i.payment_method = 'loyalty_points'
1005 AND FLOOR(i.total) > 0;
1006
1007-- zero phantom balances
1008UPDATE client
1009SET loyalty_points = 0
1010where true;
1011
1012-- sync net balance
1013UPDATE client c
1014SET loyalty_points = sub.net_points
1015FROM (SELECT client_id,
1016 GREATEST(0, COALESCE(SUM(points_earned), 0) - COALESCE(SUM(points_spent), 0))::int AS net_points
1017 FROM loyalty_transaction
1018 GROUP BY client_id) sub
1019WHERE c.client_id = sub.client_id;
1020
1021
1022
1023-- ============================================================
1024-- 34. SEED — SERVICE PRICE HISTORY
1025-- ============================================================
1026
1027WITH RECURSIVE
1028 service_base AS (SELECT service_id,
1029 price,
1030 duration_minutes,
1031 FLOOR(RANDOM() * 4)::INT AS change_count
1032 FROM service
1033 WHERE is_active = TRUE),
1034 price_changes AS (SELECT service_id,
1035 1 AS iteration,
1036 change_count,
1037 price::NUMERIC(10, 2) AS new_price,
1038 ROUND((price * 0.9)::NUMERIC, 2)::NUMERIC(10, 2) AS old_price,
1039 (NOW() - (RANDOM() * INTERVAL '6 months'))::TIMESTAMPTZ AS changed_at
1040 FROM service_base
1041 WHERE change_count >= 1
1042
1043 UNION ALL
1044
1045 SELECT pc.service_id,
1046 pc.iteration + 1,
1047 pc.change_count,
1048 pc.old_price,
1049 ROUND((pc.old_price * 0.9)::NUMERIC, 2)::NUMERIC(10, 2),
1050 (pc.changed_at - (RANDOM() * INTERVAL '6 months'))::TIMESTAMPTZ
1051 FROM price_changes pc
1052 WHERE pc.iteration < pc.change_count)
1053INSERT
1054INTO service_price_history (service_id, old_price, new_price, changed_at)
1055SELECT service_id,
1056 GREATEST(old_price, 5.00),
1057 GREATEST(new_price, 5.00),
1058 changed_at
1059FROM price_changes;
1060
1061
1062-- ============================================================
1063-- 35. SEED — STAFF TIME SLOTS (WITH ADJUSTABLE LIMIT)
1064-- ============================================================
1065
1066ALTER TABLE staff_time_slot SET UNLOGGED;
1067
1068INSERT INTO staff_time_slot (staff_id, slot_start)
1069SELECT staff_id, slot_start
1070FROM (SELECT sa.staff_id,
1071 (
1072 d.slot_date
1073 + sa.start_time
1074 + (gs * INTERVAL '15 minutes')
1075 )::timestamp AS slot_start
1076 FROM staff_availability sa
1077 JOIN generate_series(
1078 CURRENT_DATE - INTERVAL '7 days',
1079 CURRENT_DATE + INTERVAL '30 days',
1080 INTERVAL '1 day'
1081 ) AS d(slot_date)
1082 ON EXTRACT(DOW FROM d.slot_date) =
1083 CASE sa.day_of_week
1084 WHEN 'sunday' THEN 0
1085 WHEN 'monday' THEN 1
1086 WHEN 'tuesday' THEN 2
1087 WHEN 'wednesday' THEN 3
1088 WHEN 'thursday' THEN 4
1089 WHEN 'friday' THEN 5
1090 WHEN 'saturday' THEN 6
1091 END
1092 CROSS JOIN LATERAL generate_series(
1093 0,
1094 FLOOR(EXTRACT(EPOCH FROM (sa.end_time - sa.start_time)) / 900) - 1
1095 ) AS gs
1096 LIMIT 1000000) AS limited_slots
1097ON CONFLICT (staff_id, slot_start) DO NOTHING;
1098
1099-- remove slots that overlap blocked_time
1100DELETE FROM staff_time_slot ts
1101WHERE EXISTS (SELECT 1
1102 FROM blocked_time b
1103 WHERE b.staff_id = ts.staff_id
1104 AND ts.slot_start < b.end_datetime
1105 AND ts.slot_start + INTERVAL '15 minutes' > b.start_datetime);
1106
1107ALTER TABLE staff_time_slot SET LOGGED;