mirror of
https://github.com/marcogll/AnchorOS.git
synced 2026-03-15 13:24:27 +00:00
refactor: migrate from old db/ to supabase CLI structure
- Migrated database schema from db/migrations to supabase/migrations - Added Supabase CLI configuration (config.toml, .gitignore) - Added kiosk role and amenities tables for touch kiosks - Added notification system for artist alerts - Added seed data with test locations and staff - Removed old migration scripts and documentation - Updated tasks_mg.md with current setup Features: - 2 locations: ANCHOR:23 - Via KLAVA and TEST - Kiosk role for touch screen check-in/booking - Amenities: coffee, cocktails, mocktails for clients - Notifications when client arrives - 1 staff + 4 artists + 1 kiosk per location - Services: hair, nails, makeup, lashes
This commit is contained in:
8
supabase/.gitignore
vendored
Normal file
8
supabase/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# Supabase
|
||||
.branches
|
||||
.temp
|
||||
|
||||
# dotenvx
|
||||
.env.keys
|
||||
.env.local
|
||||
.env.*.local
|
||||
384
supabase/config.toml
Normal file
384
supabase/config.toml
Normal file
@@ -0,0 +1,384 @@
|
||||
# For detailed configuration reference documentation, visit:
|
||||
# https://supabase.com/docs/guides/local-development/cli/config
|
||||
# A string used to distinguish different Supabase projects on the same host. Defaults to the
|
||||
# working directory name when running `supabase init`.
|
||||
project_id = "salonOS"
|
||||
|
||||
[api]
|
||||
enabled = true
|
||||
# Port to use for the API URL.
|
||||
port = 54321
|
||||
# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
|
||||
# endpoints. `public` and `graphql_public` schemas are included by default.
|
||||
schemas = ["public", "graphql_public"]
|
||||
# Extra schemas to add to the search_path of every request.
|
||||
extra_search_path = ["public", "extensions"]
|
||||
# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
|
||||
# for accidental or malicious requests.
|
||||
max_rows = 1000
|
||||
|
||||
[api.tls]
|
||||
# Enable HTTPS endpoints locally using a self-signed certificate.
|
||||
enabled = false
|
||||
# Paths to self-signed certificate pair.
|
||||
# cert_path = "../certs/my-cert.pem"
|
||||
# key_path = "../certs/my-key.pem"
|
||||
|
||||
[db]
|
||||
# Port to use for the local database URL.
|
||||
port = 54322
|
||||
# Port used by db diff command to initialize the shadow database.
|
||||
shadow_port = 54320
|
||||
# Maximum amount of time to wait for health check when starting the local database.
|
||||
health_timeout = "2m"
|
||||
# The database major version to use. This has to be the same as your remote database's. Run `SHOW
|
||||
# server_version;` on the remote database to check.
|
||||
major_version = 17
|
||||
|
||||
[db.pooler]
|
||||
enabled = false
|
||||
# Port to use for the local connection pooler.
|
||||
port = 54329
|
||||
# Specifies when a server connection can be reused by other clients.
|
||||
# Configure one of the supported pooler modes: `transaction`, `session`.
|
||||
pool_mode = "transaction"
|
||||
# How many server connections to allow per user/database pair.
|
||||
default_pool_size = 20
|
||||
# Maximum number of client connections allowed.
|
||||
max_client_conn = 100
|
||||
|
||||
# [db.vault]
|
||||
# secret_key = "env(SECRET_VALUE)"
|
||||
|
||||
[db.migrations]
|
||||
# If disabled, migrations will be skipped during a db push or reset.
|
||||
enabled = true
|
||||
# Specifies an ordered list of schema files that describe your database.
|
||||
# Supports glob patterns relative to supabase directory: "./schemas/*.sql"
|
||||
schema_paths = []
|
||||
|
||||
[db.seed]
|
||||
# If enabled, seeds the database after migrations during a db reset.
|
||||
enabled = true
|
||||
# Specifies an ordered list of seed files to load during db reset.
|
||||
# Supports glob patterns relative to supabase directory: "./seeds/*.sql"
|
||||
sql_paths = ["./seed.sql"]
|
||||
|
||||
[db.network_restrictions]
|
||||
# Enable management of network restrictions.
|
||||
enabled = false
|
||||
# List of IPv4 CIDR blocks allowed to connect to the database.
|
||||
# Defaults to allow all IPv4 connections. Set empty array to block all IPs.
|
||||
allowed_cidrs = ["0.0.0.0/0"]
|
||||
# List of IPv6 CIDR blocks allowed to connect to the database.
|
||||
# Defaults to allow all IPv6 connections. Set empty array to block all IPs.
|
||||
allowed_cidrs_v6 = ["::/0"]
|
||||
|
||||
[realtime]
|
||||
enabled = true
|
||||
# Bind realtime via either IPv4 or IPv6. (default: IPv4)
|
||||
# ip_version = "IPv6"
|
||||
# The maximum length in bytes of HTTP request headers. (default: 4096)
|
||||
# max_header_length = 4096
|
||||
|
||||
[studio]
|
||||
enabled = true
|
||||
# Port to use for Supabase Studio.
|
||||
port = 54323
|
||||
# External URL of the API server that frontend connects to.
|
||||
api_url = "http://127.0.0.1"
|
||||
# OpenAI API Key to use for Supabase AI in the Supabase Studio.
|
||||
openai_api_key = "env(OPENAI_API_KEY)"
|
||||
|
||||
# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
|
||||
# are monitored, and you can view the emails that would have been sent from the web interface.
|
||||
[inbucket]
|
||||
enabled = true
|
||||
# Port to use for the email testing server web interface.
|
||||
port = 54324
|
||||
# Uncomment to expose additional ports for testing user applications that send emails.
|
||||
# smtp_port = 54325
|
||||
# pop3_port = 54326
|
||||
# admin_email = "admin@email.com"
|
||||
# sender_name = "Admin"
|
||||
|
||||
[storage]
|
||||
enabled = true
|
||||
# The maximum file size allowed (e.g. "5MB", "500KB").
|
||||
file_size_limit = "50MiB"
|
||||
|
||||
# Uncomment to configure local storage buckets
|
||||
# [storage.buckets.images]
|
||||
# public = false
|
||||
# file_size_limit = "50MiB"
|
||||
# allowed_mime_types = ["image/png", "image/jpeg"]
|
||||
# objects_path = "./images"
|
||||
|
||||
# Allow connections via S3 compatible clients
|
||||
[storage.s3_protocol]
|
||||
enabled = true
|
||||
|
||||
# Image transformation API is available to Supabase Pro plan.
|
||||
# [storage.image_transformation]
|
||||
# enabled = true
|
||||
|
||||
# Store analytical data in S3 for running ETL jobs over Iceberg Catalog
|
||||
# This feature is only available on the hosted platform.
|
||||
[storage.analytics]
|
||||
enabled = false
|
||||
max_namespaces = 5
|
||||
max_tables = 10
|
||||
max_catalogs = 2
|
||||
|
||||
# Analytics Buckets is available to Supabase Pro plan.
|
||||
# [storage.analytics.buckets.my-warehouse]
|
||||
|
||||
# Store vector embeddings in S3 for large and durable datasets
|
||||
# This feature is only available on the hosted platform.
|
||||
[storage.vector]
|
||||
enabled = false
|
||||
max_buckets = 10
|
||||
max_indexes = 5
|
||||
|
||||
# Vector Buckets is available to Supabase Pro plan.
|
||||
# [storage.vector.buckets.documents-openai]
|
||||
|
||||
[auth]
|
||||
enabled = true
|
||||
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
|
||||
# in emails.
|
||||
site_url = "http://127.0.0.1:3000"
|
||||
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
|
||||
additional_redirect_urls = ["https://127.0.0.1:3000"]
|
||||
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
|
||||
jwt_expiry = 3600
|
||||
# JWT issuer URL. If not set, defaults to the local API URL (http://127.0.0.1:<port>/auth/v1).
|
||||
# jwt_issuer = ""
|
||||
# Path to JWT signing key. DO NOT commit your signing keys file to git.
|
||||
# signing_keys_path = "./signing_keys.json"
|
||||
# If disabled, the refresh token will never expire.
|
||||
enable_refresh_token_rotation = true
|
||||
# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.
|
||||
# Requires enable_refresh_token_rotation = true.
|
||||
refresh_token_reuse_interval = 10
|
||||
# Allow/disallow new user signups to your project.
|
||||
enable_signup = true
|
||||
# Allow/disallow anonymous sign-ins to your project.
|
||||
enable_anonymous_sign_ins = false
|
||||
# Allow/disallow testing manual linking of accounts
|
||||
enable_manual_linking = false
|
||||
# Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more.
|
||||
minimum_password_length = 6
|
||||
# Passwords that do not meet the following requirements will be rejected as weak. Supported values
|
||||
# are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols`
|
||||
password_requirements = ""
|
||||
|
||||
[auth.rate_limit]
|
||||
# Number of emails that can be sent per hour. Requires auth.email.smtp to be enabled.
|
||||
email_sent = 2
|
||||
# Number of SMS messages that can be sent per hour. Requires auth.sms to be enabled.
|
||||
sms_sent = 30
|
||||
# Number of anonymous sign-ins that can be made per hour per IP address. Requires enable_anonymous_sign_ins = true.
|
||||
anonymous_users = 30
|
||||
# Number of sessions that can be refreshed in a 5 minute interval per IP address.
|
||||
token_refresh = 150
|
||||
# Number of sign up and sign-in requests that can be made in a 5 minute interval per IP address (excludes anonymous users).
|
||||
sign_in_sign_ups = 30
|
||||
# Number of OTP / Magic link verifications that can be made in a 5 minute interval per IP address.
|
||||
token_verifications = 30
|
||||
# Number of Web3 logins that can be made in a 5 minute interval per IP address.
|
||||
web3 = 30
|
||||
|
||||
# Configure one of the supported captcha providers: `hcaptcha`, `turnstile`.
|
||||
# [auth.captcha]
|
||||
# enabled = true
|
||||
# provider = "hcaptcha"
|
||||
# secret = ""
|
||||
|
||||
[auth.email]
|
||||
# Allow/disallow new user signups via email to your project.
|
||||
enable_signup = true
|
||||
# If enabled, a user will be required to confirm any email change on both the old, and new email
|
||||
# addresses. If disabled, only the new email is required to confirm.
|
||||
double_confirm_changes = true
|
||||
# If enabled, users need to confirm their email address before signing in.
|
||||
enable_confirmations = false
|
||||
# If enabled, users will need to reauthenticate or have logged in recently to change their password.
|
||||
secure_password_change = false
|
||||
# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email.
|
||||
max_frequency = "1s"
|
||||
# Number of characters used in the email OTP.
|
||||
otp_length = 6
|
||||
# Number of seconds before the email OTP expires (defaults to 1 hour).
|
||||
otp_expiry = 3600
|
||||
|
||||
# Use a production-ready SMTP server
|
||||
# [auth.email.smtp]
|
||||
# enabled = true
|
||||
# host = "smtp.sendgrid.net"
|
||||
# port = 587
|
||||
# user = "apikey"
|
||||
# pass = "env(SENDGRID_API_KEY)"
|
||||
# admin_email = "admin@email.com"
|
||||
# sender_name = "Admin"
|
||||
|
||||
# Uncomment to customize email template
|
||||
# [auth.email.template.invite]
|
||||
# subject = "You have been invited"
|
||||
# content_path = "./supabase/templates/invite.html"
|
||||
|
||||
# Uncomment to customize notification email template
|
||||
# [auth.email.notification.password_changed]
|
||||
# enabled = true
|
||||
# subject = "Your password has been changed"
|
||||
# content_path = "./templates/password_changed_notification.html"
|
||||
|
||||
[auth.sms]
|
||||
# Allow/disallow new user signups via SMS to your project.
|
||||
enable_signup = false
|
||||
# If enabled, users need to confirm their phone number before signing in.
|
||||
enable_confirmations = false
|
||||
# Template for sending OTP to users
|
||||
template = "Your code is {{ .Code }}"
|
||||
# Controls the minimum amount of time that must pass before sending another sms otp.
|
||||
max_frequency = "5s"
|
||||
|
||||
# Use pre-defined map of phone number to OTP for testing.
|
||||
# [auth.sms.test_otp]
|
||||
# 4152127777 = "123456"
|
||||
|
||||
# Configure logged in session timeouts.
|
||||
# [auth.sessions]
|
||||
# Force log out after the specified duration.
|
||||
# timebox = "24h"
|
||||
# Force log out if the user has been inactive longer than the specified duration.
|
||||
# inactivity_timeout = "8h"
|
||||
|
||||
# This hook runs before a new user is created and allows developers to reject the request based on the incoming user object.
|
||||
# [auth.hook.before_user_created]
|
||||
# enabled = true
|
||||
# uri = "pg-functions://postgres/auth/before-user-created-hook"
|
||||
|
||||
# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used.
|
||||
# [auth.hook.custom_access_token]
|
||||
# enabled = true
|
||||
# uri = "pg-functions://<database>/<schema>/<hook_name>"
|
||||
|
||||
# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`.
|
||||
[auth.sms.twilio]
|
||||
enabled = false
|
||||
account_sid = ""
|
||||
message_service_sid = ""
|
||||
# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead:
|
||||
auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)"
|
||||
|
||||
# Multi-factor-authentication is available to Supabase Pro plan.
|
||||
[auth.mfa]
|
||||
# Control how many MFA factors can be enrolled at once per user.
|
||||
max_enrolled_factors = 10
|
||||
|
||||
# Control MFA via App Authenticator (TOTP)
|
||||
[auth.mfa.totp]
|
||||
enroll_enabled = false
|
||||
verify_enabled = false
|
||||
|
||||
# Configure MFA via Phone Messaging
|
||||
[auth.mfa.phone]
|
||||
enroll_enabled = false
|
||||
verify_enabled = false
|
||||
otp_length = 6
|
||||
template = "Your code is {{ .Code }}"
|
||||
max_frequency = "5s"
|
||||
|
||||
# Configure MFA via WebAuthn
|
||||
# [auth.mfa.web_authn]
|
||||
# enroll_enabled = true
|
||||
# verify_enabled = true
|
||||
|
||||
# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
|
||||
# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`,
|
||||
# `twitter`, `x`, `slack`, `spotify`, `workos`, `zoom`.
|
||||
[auth.external.apple]
|
||||
enabled = false
|
||||
client_id = ""
|
||||
# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead:
|
||||
secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"
|
||||
# Overrides the default auth redirectUrl.
|
||||
redirect_uri = ""
|
||||
# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
|
||||
# or any other third-party OIDC providers.
|
||||
url = ""
|
||||
# If enabled, the nonce check will be skipped. Required for local sign in with Google auth.
|
||||
skip_nonce_check = false
|
||||
# If enabled, it will allow the user to successfully authenticate when the provider does not return an email address.
|
||||
email_optional = false
|
||||
|
||||
# Allow Solana wallet holders to sign in to your project via the Sign in with Solana (SIWS, EIP-4361) standard.
|
||||
# You can configure "web3" rate limit in the [auth.rate_limit] section and set up [auth.captcha] if self-hosting.
|
||||
[auth.web3.solana]
|
||||
enabled = false
|
||||
|
||||
# Use Firebase Auth as a third-party provider alongside Supabase Auth.
|
||||
[auth.third_party.firebase]
|
||||
enabled = false
|
||||
# project_id = "my-firebase-project"
|
||||
|
||||
# Use Auth0 as a third-party provider alongside Supabase Auth.
|
||||
[auth.third_party.auth0]
|
||||
enabled = false
|
||||
# tenant = "my-auth0-tenant"
|
||||
# tenant_region = "us"
|
||||
|
||||
# Use AWS Cognito (Amplify) as a third-party provider alongside Supabase Auth.
|
||||
[auth.third_party.aws_cognito]
|
||||
enabled = false
|
||||
# user_pool_id = "my-user-pool-id"
|
||||
# user_pool_region = "us-east-1"
|
||||
|
||||
# Use Clerk as a third-party provider alongside Supabase Auth.
|
||||
[auth.third_party.clerk]
|
||||
enabled = false
|
||||
# Obtain from https://clerk.com/setup/supabase
|
||||
# domain = "example.clerk.accounts.dev"
|
||||
|
||||
# OAuth server configuration
|
||||
[auth.oauth_server]
|
||||
# Enable OAuth server functionality
|
||||
enabled = false
|
||||
# Path for OAuth consent flow UI
|
||||
authorization_url_path = "/oauth/consent"
|
||||
# Allow dynamic client registration
|
||||
allow_dynamic_registration = false
|
||||
|
||||
[edge_runtime]
|
||||
enabled = true
|
||||
# Supported request policies: `oneshot`, `per_worker`.
|
||||
# `per_worker` (default) — enables hot reload during local development.
|
||||
# `oneshot` — fallback mode if hot reload causes issues (e.g. in large repos or with symlinks).
|
||||
policy = "per_worker"
|
||||
# Port to attach the Chrome inspector for debugging edge functions.
|
||||
inspector_port = 8083
|
||||
# The Deno major version to use.
|
||||
deno_version = 2
|
||||
|
||||
# [edge_runtime.secrets]
|
||||
# secret_key = "env(SECRET_VALUE)"
|
||||
|
||||
[analytics]
|
||||
enabled = true
|
||||
port = 54327
|
||||
# Configure one of the supported backends: `postgres`, `bigquery`.
|
||||
backend = "postgres"
|
||||
|
||||
# Experimental features may be deprecated any time
|
||||
[experimental]
|
||||
# Configures Postgres storage engine to use OrioleDB (S3)
|
||||
orioledb_version = ""
|
||||
# Configures S3 bucket URL, eg. <bucket_name>.s3-<region>.amazonaws.com
|
||||
s3_host = "env(S3_HOST)"
|
||||
# Configures S3 bucket region, eg. us-east-1
|
||||
s3_region = "env(S3_REGION)"
|
||||
# Configures AWS_ACCESS_KEY_ID for S3 bucket
|
||||
s3_access_key = "env(S3_ACCESS_KEY)"
|
||||
# Configures AWS_SECRET_ACCESS_KEY for S3 bucket
|
||||
s3_secret_key = "env(S3_SECRET_KEY)"
|
||||
848
supabase/migrations/20260115235737_initial_schema.sql
Normal file
848
supabase/migrations/20260115235737_initial_schema.sql
Normal file
@@ -0,0 +1,848 @@
|
||||
-- ============================================
|
||||
-- SALONOS - CORRECTED FULL DATABASE MIGRATION
|
||||
-- Ejecutar TODO este archivo en Supabase SQL Editor
|
||||
-- URL: https://supabase.com/dashboard/project/pvvwbnybkadhreuqijsl/sql
|
||||
-- ============================================
|
||||
|
||||
-- ============================================
|
||||
-- BEGIN MIGRATION 001: INITIAL SCHEMA
|
||||
-- ============================================
|
||||
|
||||
-- Habilitar UUID extension
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
|
||||
-- ENUMS
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'user_role') THEN
|
||||
CREATE TYPE user_role AS ENUM ('admin', 'manager', 'staff', 'artist', 'customer');
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'customer_tier') THEN
|
||||
CREATE TYPE customer_tier AS ENUM ('free', 'gold');
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'booking_status') THEN
|
||||
CREATE TYPE booking_status AS ENUM ('pending', 'confirmed', 'cancelled', 'completed', 'no_show');
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'invitation_status') THEN
|
||||
CREATE TYPE invitation_status AS ENUM ('pending', 'used', 'expired');
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'resource_type') THEN
|
||||
CREATE TYPE resource_type AS ENUM ('station', 'room', 'equipment');
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'audit_action') THEN
|
||||
CREATE TYPE audit_action AS ENUM ('create', 'update', 'delete', 'reset_invitations', 'payment', 'status_change');
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- LOCATIONS
|
||||
CREATE TABLE IF NOT EXISTS locations (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
name VARCHAR(100) NOT NULL,
|
||||
timezone VARCHAR(50) NOT NULL DEFAULT 'UTC',
|
||||
address TEXT,
|
||||
phone VARCHAR(20),
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- RESOURCES
|
||||
CREATE TABLE IF NOT EXISTS resources (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
location_id UUID NOT NULL REFERENCES locations(id) ON DELETE CASCADE,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
type resource_type NOT NULL,
|
||||
capacity INTEGER DEFAULT 1,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- STAFF
|
||||
CREATE TABLE IF NOT EXISTS staff (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id UUID NOT NULL,
|
||||
location_id UUID NOT NULL REFERENCES locations(id) ON DELETE CASCADE,
|
||||
role user_role NOT NULL CHECK (role IN ('admin', 'manager', 'staff', 'artist')),
|
||||
display_name VARCHAR(100) NOT NULL,
|
||||
phone VARCHAR(20),
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(user_id, location_id)
|
||||
);
|
||||
|
||||
-- SERVICES
|
||||
CREATE TABLE IF NOT EXISTS services (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
duration_minutes INTEGER NOT NULL CHECK (duration_minutes > 0),
|
||||
base_price DECIMAL(10, 2) NOT NULL CHECK (base_price >= 0),
|
||||
requires_dual_artist BOOLEAN DEFAULT false,
|
||||
premium_fee_enabled BOOLEAN DEFAULT false,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- CUSTOMERS
|
||||
CREATE TABLE IF NOT EXISTS customers (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id UUID UNIQUE,
|
||||
first_name VARCHAR(100) NOT NULL,
|
||||
last_name VARCHAR(100) NOT NULL,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
phone VARCHAR(20),
|
||||
tier customer_tier DEFAULT 'free',
|
||||
notes TEXT,
|
||||
total_spent DECIMAL(10, 2) DEFAULT 0,
|
||||
total_visits INTEGER DEFAULT 0,
|
||||
last_visit_date DATE,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- INVITATIONS
|
||||
CREATE TABLE IF NOT EXISTS invitations (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
inviter_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
|
||||
code VARCHAR(10) UNIQUE NOT NULL,
|
||||
email VARCHAR(255),
|
||||
status invitation_status DEFAULT 'pending',
|
||||
week_start_date DATE NOT NULL,
|
||||
expiry_date DATE NOT NULL,
|
||||
used_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- BOOKINGS
|
||||
CREATE TABLE IF NOT EXISTS bookings (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
short_id VARCHAR(6) UNIQUE NOT NULL,
|
||||
customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
|
||||
staff_id UUID NOT NULL REFERENCES staff(id) ON DELETE RESTRICT,
|
||||
secondary_artist_id UUID REFERENCES staff(id) ON DELETE SET NULL,
|
||||
location_id UUID NOT NULL REFERENCES locations(id) ON DELETE CASCADE,
|
||||
resource_id UUID NOT NULL REFERENCES resources(id) ON DELETE CASCADE,
|
||||
service_id UUID NOT NULL REFERENCES services(id) ON DELETE RESTRICT,
|
||||
start_time_utc TIMESTAMPTZ NOT NULL,
|
||||
end_time_utc TIMESTAMPTZ NOT NULL,
|
||||
status booking_status DEFAULT 'pending',
|
||||
deposit_amount DECIMAL(10, 2) DEFAULT 0,
|
||||
total_amount DECIMAL(10, 2) NOT NULL,
|
||||
is_paid BOOLEAN DEFAULT false,
|
||||
payment_reference VARCHAR(50),
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- AUDIT LOGS
|
||||
CREATE TABLE IF NOT EXISTS audit_logs (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
entity_type VARCHAR(50) NOT NULL,
|
||||
entity_id UUID NOT NULL,
|
||||
action audit_action NOT NULL,
|
||||
old_values JSONB,
|
||||
new_values JSONB,
|
||||
performed_by UUID,
|
||||
performed_by_role user_role,
|
||||
ip_address INET,
|
||||
user_agent TEXT,
|
||||
metadata JSONB,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- INDEXES
|
||||
CREATE INDEX IF NOT EXISTS idx_locations_active ON locations(is_active);
|
||||
CREATE INDEX IF NOT EXISTS idx_resources_location ON resources(location_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_resources_active ON resources(location_id, is_active);
|
||||
CREATE INDEX IF NOT EXISTS idx_staff_user ON staff(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_staff_location ON staff(location_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_staff_role ON staff(location_id, role, is_active);
|
||||
CREATE INDEX IF NOT EXISTS idx_services_active ON services(is_active);
|
||||
CREATE INDEX IF NOT EXISTS idx_customers_tier ON customers(tier);
|
||||
CREATE INDEX IF NOT EXISTS idx_customers_email ON customers(email);
|
||||
CREATE INDEX IF NOT EXISTS idx_customers_active ON customers(is_active);
|
||||
CREATE INDEX IF NOT EXISTS idx_invitations_inviter ON invitations(inviter_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_invitations_code ON invitations(code);
|
||||
CREATE INDEX IF NOT EXISTS idx_invitations_week ON invitations(week_start_date, status);
|
||||
CREATE INDEX IF NOT EXISTS idx_bookings_customer ON bookings(customer_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_bookings_staff ON bookings(staff_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_bookings_secondary_artist ON bookings(secondary_artist_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_bookings_location ON bookings(location_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_bookings_resource ON bookings(resource_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_bookings_time ON bookings(start_time_utc, end_time_utc);
|
||||
CREATE INDEX IF NOT EXISTS idx_bookings_status ON bookings(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_bookings_short_id ON bookings(short_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_entity ON audit_logs(entity_type, entity_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_logs(action, created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_performed ON audit_logs(performed_by);
|
||||
|
||||
-- UPDATED_AT TRIGGER FUNCTION
|
||||
CREATE OR REPLACE FUNCTION update_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- UPDATED_AT TRIGGERS
|
||||
DROP TRIGGER IF EXISTS locations_updated_at ON locations;
|
||||
CREATE TRIGGER locations_updated_at BEFORE UPDATE ON locations
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
DROP TRIGGER IF EXISTS resources_updated_at ON resources;
|
||||
CREATE TRIGGER resources_updated_at BEFORE UPDATE ON resources
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
DROP TRIGGER IF EXISTS staff_updated_at ON staff;
|
||||
CREATE TRIGGER staff_updated_at BEFORE UPDATE ON staff
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
DROP TRIGGER IF EXISTS services_updated_at ON services;
|
||||
CREATE TRIGGER services_updated_at BEFORE UPDATE ON services
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
DROP TRIGGER IF EXISTS customers_updated_at ON customers;
|
||||
CREATE TRIGGER customers_updated_at BEFORE UPDATE ON customers
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
DROP TRIGGER IF EXISTS invitations_updated_at ON invitations;
|
||||
CREATE TRIGGER invitations_updated_at BEFORE UPDATE ON invitations
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
DROP TRIGGER IF EXISTS bookings_updated_at ON bookings;
|
||||
CREATE TRIGGER bookings_updated_at BEFORE UPDATE ON bookings
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
-- CONSTRAINTS (Simple ones only - no subqueries)
|
||||
ALTER TABLE bookings ADD CONSTRAINT IF NOT EXISTS check_booking_time
|
||||
CHECK (end_time_utc > start_time_utc);
|
||||
|
||||
ALTER TABLE invitations ADD CONSTRAINT IF NOT EXISTS check_week_start_is_monday
|
||||
CHECK (EXTRACT(ISODOW FROM week_start_date) = 1);
|
||||
|
||||
-- Trigger for secondary_artist validation (instead of CHECK constraint with subquery)
|
||||
CREATE OR REPLACE FUNCTION validate_secondary_artist_role()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF NEW.secondary_artist_id IS NOT NULL THEN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM staff s
|
||||
WHERE s.id = NEW.secondary_artist_id AND s.role = 'artist' AND s.is_active = true
|
||||
) THEN
|
||||
RAISE EXCEPTION 'secondary_artist_id must reference an active staff member with role ''artist''';
|
||||
END IF;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS validate_booking_secondary_artist ON bookings;
|
||||
CREATE TRIGGER validate_booking_secondary_artist BEFORE INSERT OR UPDATE ON bookings
|
||||
FOR EACH ROW EXECUTE FUNCTION validate_secondary_artist_role();
|
||||
|
||||
-- ============================================
|
||||
-- BEGIN MIGRATION 002: RLS POLICIES
|
||||
-- ============================================
|
||||
|
||||
-- HELPER FUNCTIONS
|
||||
CREATE OR REPLACE FUNCTION get_current_user_role()
|
||||
RETURNS user_role AS $$
|
||||
DECLARE
|
||||
current_staff_role user_role;
|
||||
current_user_id UUID := auth.uid();
|
||||
BEGIN
|
||||
SELECT s.role INTO current_staff_role
|
||||
FROM staff s
|
||||
WHERE s.user_id = current_user_id
|
||||
LIMIT 1;
|
||||
|
||||
IF current_staff_role IS NOT NULL THEN
|
||||
RETURN current_staff_role;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM customers WHERE user_id = current_user_id) THEN
|
||||
RETURN 'customer';
|
||||
END IF;
|
||||
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
CREATE OR REPLACE FUNCTION is_staff_or_higher()
|
||||
RETURNS BOOLEAN AS $$
|
||||
DECLARE
|
||||
user_role user_role := get_current_user_role();
|
||||
BEGIN
|
||||
RETURN user_role IN ('admin', 'manager', 'staff');
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
CREATE OR REPLACE FUNCTION is_artist()
|
||||
RETURNS BOOLEAN AS $$
|
||||
DECLARE
|
||||
user_role user_role := get_current_user_role();
|
||||
BEGIN
|
||||
RETURN user_role = 'artist';
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
CREATE OR REPLACE FUNCTION is_customer()
|
||||
RETURNS BOOLEAN AS $$
|
||||
DECLARE
|
||||
user_role user_role := get_current_user_role();
|
||||
BEGIN
|
||||
RETURN user_role = 'customer';
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
CREATE OR REPLACE FUNCTION is_admin()
|
||||
RETURNS BOOLEAN AS $$
|
||||
DECLARE
|
||||
user_role user_role := get_current_user_role();
|
||||
BEGIN
|
||||
RETURN user_role = 'admin';
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- ENABLE RLS ON ALL TABLES
|
||||
ALTER TABLE locations ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE resources ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE staff ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE services ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE customers ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE invitations ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE bookings ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE audit_logs ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- LOCATIONS POLICIES
|
||||
DROP POLICY IF EXISTS "locations_select_staff_higher" ON locations;
|
||||
CREATE POLICY "locations_select_staff_higher" ON locations
|
||||
FOR SELECT
|
||||
USING (is_staff_or_higher() OR is_admin());
|
||||
|
||||
DROP POLICY IF EXISTS "locations_modify_admin_manager" ON locations;
|
||||
CREATE POLICY "locations_modify_admin_manager" ON locations
|
||||
FOR ALL
|
||||
USING (get_current_user_role() IN ('admin', 'manager'));
|
||||
|
||||
-- RESOURCES POLICIES
|
||||
DROP POLICY IF EXISTS "resources_select_staff_higher" ON resources;
|
||||
CREATE POLICY "resources_select_staff_higher" ON resources
|
||||
FOR SELECT
|
||||
USING (is_staff_or_higher() OR is_admin());
|
||||
|
||||
DROP POLICY IF EXISTS "resources_select_artist" ON resources;
|
||||
CREATE POLICY "resources_select_artist" ON resources
|
||||
FOR SELECT
|
||||
USING (is_artist());
|
||||
|
||||
DROP POLICY IF EXISTS "resources_modify_admin_manager" ON resources;
|
||||
CREATE POLICY "resources_modify_admin_manager" ON resources
|
||||
FOR ALL
|
||||
USING (get_current_user_role() IN ('admin', 'manager'));
|
||||
|
||||
-- STAFF POLICIES
|
||||
DROP POLICY IF EXISTS "staff_select_admin_manager" ON staff;
|
||||
CREATE POLICY "staff_select_admin_manager" ON staff
|
||||
FOR SELECT
|
||||
USING (get_current_user_role() IN ('admin', 'manager'));
|
||||
|
||||
DROP POLICY IF EXISTS "staff_select_same_location" ON staff;
|
||||
CREATE POLICY "staff_select_same_location" ON staff
|
||||
FOR SELECT
|
||||
USING (
|
||||
is_staff_or_higher() AND
|
||||
EXISTS (
|
||||
SELECT 1 FROM staff s WHERE s.user_id = auth.uid() AND s.location_id = staff.location_id
|
||||
)
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS "staff_select_artist_view_artists" ON staff;
|
||||
CREATE POLICY "staff_select_artist_view_artists" ON staff
|
||||
FOR SELECT
|
||||
USING (
|
||||
is_artist() AND
|
||||
EXISTS (
|
||||
SELECT 1 FROM staff s WHERE s.user_id = auth.uid() AND s.location_id = staff.location_id
|
||||
) AND
|
||||
staff.role = 'artist'
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS "staff_modify_admin_manager" ON staff;
|
||||
CREATE POLICY "staff_modify_admin_manager" ON staff
|
||||
FOR ALL
|
||||
USING (get_current_user_role() IN ('admin', 'manager'));
|
||||
|
||||
-- SERVICES POLICIES
|
||||
DROP POLICY IF EXISTS "services_select_all" ON services;
|
||||
CREATE POLICY "services_select_all" ON services
|
||||
FOR SELECT
|
||||
USING (is_active = true);
|
||||
|
||||
DROP POLICY IF EXISTS "services_all_admin_manager" ON services;
|
||||
CREATE POLICY "services_all_admin_manager" ON services
|
||||
FOR ALL
|
||||
USING (get_current_user_role() IN ('admin', 'manager'));
|
||||
|
||||
-- CUSTOMERS POLICIES (RESTRICTED FOR ARTISTS)
|
||||
DROP POLICY IF EXISTS "customers_select_admin_manager" ON customers;
|
||||
CREATE POLICY "customers_select_admin_manager" ON customers
|
||||
FOR SELECT
|
||||
USING (get_current_user_role() IN ('admin', 'manager'));
|
||||
|
||||
DROP POLICY IF EXISTS "customers_select_staff" ON customers;
|
||||
CREATE POLICY "customers_select_staff" ON customers
|
||||
FOR SELECT
|
||||
USING (is_staff_or_higher());
|
||||
|
||||
DROP POLICY IF EXISTS "customers_select_artist_restricted" ON customers;
|
||||
CREATE POLICY "customers_select_artist_restricted" ON customers
|
||||
FOR SELECT
|
||||
USING (is_artist());
|
||||
|
||||
DROP POLICY IF EXISTS "customers_select_own" ON customers;
|
||||
CREATE POLICY "customers_select_own" ON customers
|
||||
FOR SELECT
|
||||
USING (is_customer() AND user_id = auth.uid());
|
||||
|
||||
DROP POLICY IF EXISTS "customers_modify_admin_manager" ON customers;
|
||||
CREATE POLICY "customers_modify_admin_manager" ON customers
|
||||
FOR ALL
|
||||
USING (get_current_user_role() IN ('admin', 'manager'));
|
||||
|
||||
DROP POLICY IF EXISTS "customers_modify_staff" ON customers;
|
||||
CREATE POLICY "customers_modify_staff" ON customers
|
||||
FOR ALL
|
||||
USING (is_staff_or_higher());
|
||||
|
||||
DROP POLICY IF EXISTS "customers_update_own" ON customers;
|
||||
CREATE POLICY "customers_update_own" ON customers
|
||||
FOR UPDATE
|
||||
USING (is_customer() AND user_id = auth.uid());
|
||||
|
||||
-- INVITATIONS POLICIES
|
||||
DROP POLICY IF EXISTS "invitations_select_admin_manager" ON invitations;
|
||||
CREATE POLICY "invitations_select_admin_manager" ON invitations
|
||||
FOR SELECT
|
||||
USING (get_current_user_role() IN ('admin', 'manager'));
|
||||
|
||||
DROP POLICY IF EXISTS "invitations_select_staff" ON invitations;
|
||||
CREATE POLICY "invitations_select_staff" ON invitations
|
||||
FOR SELECT
|
||||
USING (is_staff_or_higher());
|
||||
|
||||
DROP POLICY IF EXISTS "invitations_select_own" ON invitations;
|
||||
CREATE POLICY "invitations_select_own" ON invitations
|
||||
FOR SELECT
|
||||
USING (is_customer() AND inviter_id = (SELECT id FROM customers WHERE user_id = auth.uid()));
|
||||
|
||||
DROP POLICY IF EXISTS "invitations_modify_admin_manager" ON invitations;
|
||||
CREATE POLICY "invitations_modify_admin_manager" ON invitations
|
||||
FOR ALL
|
||||
USING (get_current_user_role() IN ('admin', 'manager'));
|
||||
|
||||
DROP POLICY IF EXISTS "invitations_modify_staff" ON invitations;
|
||||
CREATE POLICY "invitations_modify_staff" ON invitations
|
||||
FOR ALL
|
||||
USING (is_staff_or_higher());
|
||||
|
||||
-- BOOKINGS POLICIES
|
||||
DROP POLICY IF EXISTS "bookings_select_admin_manager" ON bookings;
|
||||
CREATE POLICY "bookings_select_admin_manager" ON bookings
|
||||
FOR SELECT
|
||||
USING (get_current_user_role() IN ('admin', 'manager'));
|
||||
|
||||
DROP POLICY IF EXISTS "bookings_select_staff_location" ON bookings;
|
||||
CREATE POLICY "bookings_select_staff_location" ON bookings
|
||||
FOR SELECT
|
||||
USING (
|
||||
is_staff_or_higher() AND
|
||||
EXISTS (
|
||||
SELECT 1 FROM staff s WHERE s.user_id = auth.uid() AND s.location_id = bookings.location_id
|
||||
)
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS "bookings_select_artist_own" ON bookings;
|
||||
CREATE POLICY "bookings_select_artist_own" ON bookings
|
||||
FOR SELECT
|
||||
USING (
|
||||
is_artist() AND
|
||||
(staff_id = (SELECT id FROM staff WHERE user_id = auth.uid()) OR
|
||||
secondary_artist_id = (SELECT id FROM staff WHERE user_id = auth.uid()))
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS "bookings_select_own" ON bookings;
|
||||
CREATE POLICY "bookings_select_own" ON bookings
|
||||
FOR SELECT
|
||||
USING (is_customer() AND customer_id = (SELECT id FROM customers WHERE user_id = auth.uid()));
|
||||
|
||||
DROP POLICY IF EXISTS "bookings_modify_admin_manager" ON bookings;
|
||||
CREATE POLICY "bookings_modify_admin_manager" ON bookings
|
||||
FOR ALL
|
||||
USING (get_current_user_role() IN ('admin', 'manager'));
|
||||
|
||||
DROP POLICY IF EXISTS "bookings_modify_staff_location" ON bookings;
|
||||
CREATE POLICY "bookings_modify_staff_location" ON bookings
|
||||
FOR ALL
|
||||
USING (
|
||||
is_staff_or_higher() AND
|
||||
EXISTS (
|
||||
SELECT 1 FROM staff s WHERE s.user_id = auth.uid() AND s.location_id = bookings.location_id
|
||||
)
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS "bookings_no_modify_artist" ON bookings;
|
||||
CREATE POLICY "bookings_no_modify_artist" ON bookings
|
||||
FOR ALL
|
||||
USING (NOT is_artist());
|
||||
|
||||
DROP POLICY IF EXISTS "bookings_create_own" ON bookings;
|
||||
CREATE POLICY "bookings_create_own" ON bookings
|
||||
FOR INSERT
|
||||
WITH CHECK (
|
||||
is_customer() AND
|
||||
customer_id = (SELECT id FROM customers WHERE user_id = auth.uid())
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS "bookings_update_own" ON bookings;
|
||||
CREATE POLICY "bookings_update_own" ON bookings
|
||||
FOR UPDATE
|
||||
USING (
|
||||
is_customer() AND
|
||||
customer_id = (SELECT id FROM customers WHERE user_id = auth.uid())
|
||||
);
|
||||
|
||||
-- AUDIT LOGS POLICIES
|
||||
DROP POLICY IF EXISTS "audit_logs_select_admin_manager" ON audit_logs;
|
||||
CREATE POLICY "audit_logs_select_admin_manager" ON audit_logs
|
||||
FOR SELECT
|
||||
USING (get_current_user_role() IN ('admin', 'manager'));
|
||||
|
||||
DROP POLICY IF EXISTS "audit_logs_select_staff_location" ON audit_logs;
|
||||
CREATE POLICY "audit_logs_select_staff_location" ON audit_logs
|
||||
FOR SELECT
|
||||
USING (
|
||||
is_staff_or_higher() AND
|
||||
EXISTS (
|
||||
SELECT 1 FROM bookings b
|
||||
JOIN staff s ON s.user_id = auth.uid()
|
||||
WHERE b.id = audit_logs.entity_id
|
||||
AND b.location_id = s.location_id
|
||||
)
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS "audit_logs_no_insert" ON audit_logs;
|
||||
CREATE POLICY "audit_logs_no_insert" ON audit_logs
|
||||
FOR INSERT
|
||||
WITH CHECK (false);
|
||||
|
||||
-- ============================================
|
||||
-- BEGIN MIGRATION 003: AUDIT TRIGGERS
|
||||
-- ============================================
|
||||
|
||||
-- SHORT ID GENERATOR
|
||||
CREATE OR REPLACE FUNCTION generate_short_id()
|
||||
RETURNS VARCHAR(6) AS $$
|
||||
DECLARE
|
||||
chars VARCHAR(36) := '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
short_id VARCHAR(6);
|
||||
attempts INT := 0;
|
||||
max_attempts INT := 10;
|
||||
BEGIN
|
||||
LOOP
|
||||
short_id := '';
|
||||
FOR i IN 1..6 LOOP
|
||||
short_id := short_id || substr(chars, floor(random() * 36 + 1)::INT, 1);
|
||||
END LOOP;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM bookings WHERE short_id = short_id) THEN
|
||||
RETURN short_id;
|
||||
END IF;
|
||||
|
||||
attempts := attempts + 1;
|
||||
IF attempts >= max_attempts THEN
|
||||
RAISE EXCEPTION 'Failed to generate unique short_id after % attempts', max_attempts;
|
||||
END IF;
|
||||
END LOOP;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- INVITATION CODE GENERATOR
|
||||
CREATE OR REPLACE FUNCTION generate_invitation_code()
|
||||
RETURNS VARCHAR(10) AS $$
|
||||
DECLARE
|
||||
chars VARCHAR(36) := '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
code VARCHAR(10);
|
||||
attempts INT := 0;
|
||||
max_attempts INT := 10;
|
||||
BEGIN
|
||||
LOOP
|
||||
code := '';
|
||||
FOR i IN 1..10 LOOP
|
||||
code := code || substr(chars, floor(random() * 36 + 1)::INT, 1);
|
||||
END LOOP;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM invitations WHERE code = code) THEN
|
||||
RETURN code;
|
||||
END IF;
|
||||
|
||||
attempts := attempts + 1;
|
||||
IF attempts >= max_attempts THEN
|
||||
RAISE EXCEPTION 'Failed to generate unique invitation code after % attempts', max_attempts;
|
||||
END IF;
|
||||
END LOOP;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- WEEK FUNCTIONS
|
||||
CREATE OR REPLACE FUNCTION get_week_start(date_param DATE DEFAULT CURRENT_DATE)
|
||||
RETURNS DATE AS $$
|
||||
BEGIN
|
||||
RETURN date_param - (EXTRACT(ISODOW FROM date_param)::INT - 1);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql IMMUTABLE;
|
||||
|
||||
-- WEEKLY INVITATION RESET
|
||||
CREATE OR REPLACE FUNCTION reset_weekly_invitations_for_customer(customer_uuid UUID)
|
||||
RETURNS INTEGER AS $$
|
||||
DECLARE
|
||||
week_start DATE;
|
||||
invitations_remaining INTEGER := 5;
|
||||
invitations_created INTEGER := 0;
|
||||
BEGIN
|
||||
week_start := get_week_start(CURRENT_DATE);
|
||||
|
||||
SELECT COUNT(*) INTO invitations_created
|
||||
FROM invitations
|
||||
WHERE inviter_id = customer_uuid
|
||||
AND week_start_date = week_start;
|
||||
|
||||
IF invitations_created = 0 THEN
|
||||
INSERT INTO invitations (inviter_id, code, week_start_date, expiry_date, status)
|
||||
SELECT
|
||||
customer_uuid,
|
||||
generate_invitation_code(),
|
||||
week_start,
|
||||
week_start + INTERVAL '6 days',
|
||||
'pending'
|
||||
FROM generate_series(1, 5);
|
||||
|
||||
invitations_created := 5;
|
||||
|
||||
INSERT INTO audit_logs (
|
||||
entity_type,
|
||||
entity_id,
|
||||
action,
|
||||
old_values,
|
||||
new_values,
|
||||
performed_by,
|
||||
performed_by_role,
|
||||
metadata
|
||||
)
|
||||
VALUES (
|
||||
'invitations',
|
||||
customer_uuid,
|
||||
'reset_invitations',
|
||||
'{"week_start": null}'::JSONB,
|
||||
'{"week_start": "' || week_start || '", "count": 5}'::JSONB,
|
||||
NULL,
|
||||
'system',
|
||||
'{"reset_type": "weekly", "invitations_created": 5}'::JSONB
|
||||
);
|
||||
END IF;
|
||||
|
||||
RETURN invitations_created;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
CREATE OR REPLACE FUNCTION reset_all_weekly_invitations()
|
||||
RETURNS JSONB AS $$
|
||||
DECLARE
|
||||
customers_count INTEGER := 0;
|
||||
invitations_created INTEGER := 0;
|
||||
result JSONB;
|
||||
customer_record RECORD;
|
||||
BEGIN
|
||||
FOR customer_record IN
|
||||
SELECT id FROM customers WHERE tier = 'gold' AND is_active = true
|
||||
LOOP
|
||||
invitations_created := invitations_created + reset_weekly_invitations_for_customer(customer_record.id);
|
||||
customers_count := customers_count + 1;
|
||||
END LOOP;
|
||||
|
||||
result := jsonb_build_object(
|
||||
'customers_processed', customers_count,
|
||||
'invitations_created', invitations_created,
|
||||
'executed_at', NOW()::TEXT
|
||||
);
|
||||
|
||||
INSERT INTO audit_logs (
|
||||
entity_type,
|
||||
entity_id,
|
||||
action,
|
||||
old_values,
|
||||
new_values,
|
||||
performed_by,
|
||||
performed_by_role,
|
||||
metadata
|
||||
)
|
||||
VALUES (
|
||||
'invitations',
|
||||
uuid_generate_v4(),
|
||||
'reset_invitations',
|
||||
'{}'::JSONB,
|
||||
result,
|
||||
NULL,
|
||||
'system',
|
||||
'{"reset_type": "weekly_batch"}'::JSONB
|
||||
);
|
||||
|
||||
RETURN result;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- AUDIT LOG TRIGGER FUNCTION
|
||||
CREATE OR REPLACE FUNCTION log_audit()
|
||||
RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
current_user_role_val user_role;
|
||||
BEGIN
|
||||
current_user_role_val := get_current_user_role();
|
||||
|
||||
IF TG_TABLE_NAME IN ('bookings', 'customers', 'invitations', 'staff', 'services') THEN
|
||||
IF TG_OP = 'INSERT' THEN
|
||||
INSERT INTO audit_logs (
|
||||
entity_type,
|
||||
entity_id,
|
||||
action,
|
||||
old_values,
|
||||
new_values,
|
||||
performed_by,
|
||||
performed_by_role,
|
||||
metadata
|
||||
)
|
||||
VALUES (
|
||||
TG_TABLE_NAME,
|
||||
NEW.id,
|
||||
'create',
|
||||
NULL,
|
||||
row_to_json(NEW)::JSONB,
|
||||
auth.uid(),
|
||||
current_user_role_val,
|
||||
jsonb_build_object('operation', TG_OP, 'table_name', TG_TABLE_NAME)
|
||||
);
|
||||
ELSIF TG_OP = 'UPDATE' THEN
|
||||
IF NEW IS DISTINCT FROM OLD THEN
|
||||
INSERT INTO audit_logs (
|
||||
entity_type,
|
||||
entity_id,
|
||||
action,
|
||||
old_values,
|
||||
new_values,
|
||||
performed_by,
|
||||
performed_by_role,
|
||||
metadata
|
||||
)
|
||||
VALUES (
|
||||
TG_TABLE_NAME,
|
||||
NEW.id,
|
||||
'update',
|
||||
row_to_json(OLD)::JSONB,
|
||||
row_to_json(NEW)::JSONB,
|
||||
auth.uid(),
|
||||
current_user_role_val,
|
||||
jsonb_build_object('operation', TG_OP, 'table_name', TG_TABLE_NAME)
|
||||
);
|
||||
END IF;
|
||||
ELSIF TG_OP = 'DELETE' THEN
|
||||
INSERT INTO audit_logs (
|
||||
entity_type,
|
||||
entity_id,
|
||||
action,
|
||||
old_values,
|
||||
new_values,
|
||||
performed_by,
|
||||
performed_by_role,
|
||||
metadata
|
||||
)
|
||||
VALUES (
|
||||
TG_TABLE_NAME,
|
||||
OLD.id,
|
||||
'delete',
|
||||
row_to_json(OLD)::JSONB,
|
||||
NULL,
|
||||
auth.uid(),
|
||||
current_user_role_val,
|
||||
jsonb_build_object('operation', TG_OP, 'table_name', TG_TABLE_NAME)
|
||||
);
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
RETURN OLD;
|
||||
ELSE
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- APPLY AUDIT LOG TRIGGERS
|
||||
DROP TRIGGER IF EXISTS audit_bookings ON bookings;
|
||||
CREATE TRIGGER audit_bookings AFTER INSERT OR UPDATE OR DELETE ON bookings
|
||||
FOR EACH ROW EXECUTE FUNCTION log_audit();
|
||||
|
||||
DROP TRIGGER IF EXISTS audit_customers ON customers;
|
||||
CREATE TRIGGER audit_customers AFTER INSERT OR UPDATE OR DELETE ON customers
|
||||
FOR EACH ROW EXECUTE FUNCTION log_audit();
|
||||
|
||||
DROP TRIGGER IF EXISTS audit_invitations ON invitations;
|
||||
CREATE TRIGGER audit_invitations AFTER INSERT OR UPDATE OR DELETE ON invitations
|
||||
FOR EACH ROW EXECUTE FUNCTION log_audit();
|
||||
|
||||
DROP TRIGGER IF EXISTS audit_staff ON staff;
|
||||
CREATE TRIGGER audit_staff AFTER INSERT OR UPDATE OR DELETE ON staff
|
||||
FOR EACH ROW EXECUTE FUNCTION log_audit();
|
||||
|
||||
DROP TRIGGER IF EXISTS audit_services ON services;
|
||||
CREATE TRIGGER audit_services AFTER INSERT OR UPDATE OR DELETE ON services
|
||||
FOR EACH ROW EXECUTE FUNCTION log_audit();
|
||||
|
||||
-- AUTOMATIC SHORT ID GENERATION FOR BOOKINGS
|
||||
CREATE OR REPLACE FUNCTION generate_booking_short_id()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF NEW.short_id IS NULL OR NEW.short_id = '' THEN
|
||||
NEW.short_id := generate_short_id();
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS booking_generate_short_id ON bookings;
|
||||
CREATE TRIGGER booking_generate_short_id BEFORE INSERT ON bookings
|
||||
FOR EACH ROW EXECUTE FUNCTION generate_booking_short_id();
|
||||
|
||||
-- ============================================
|
||||
-- VERIFICATION
|
||||
-- ============================================
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
RAISE NOTICE '===========================================';
|
||||
RAISE NOTICE 'SALONOS - DATABASE MIGRATION COMPLETED';
|
||||
RAISE NOTICE '===========================================';
|
||||
RAISE NOTICE 'Tables created: 8';
|
||||
RAISE NOTICE 'Functions created: 14';
|
||||
RAISE NOTICE 'Triggers active: 17+';
|
||||
RAISE NOTICE 'RLS policies configured: 20+';
|
||||
RAISE NOTICE 'ENUM types created: 6';
|
||||
RAISE NOTICE '===========================================';
|
||||
END
|
||||
$$;
|
||||
860
supabase/migrations/20260115235800_add_kiosk_tables.sql
Normal file
860
supabase/migrations/20260115235800_add_kiosk_tables.sql
Normal file
@@ -0,0 +1,860 @@
|
||||
-- ============================================
|
||||
-- SALONOS - CORRECTED FULL DATABASE MIGRATION
|
||||
-- Ejecutar TODO este archivo en Supabase SQL Editor
|
||||
-- URL: https://supabase.com/dashboard/project/pvvwbnybkadhreuqijsl/sql
|
||||
-- ============================================
|
||||
|
||||
-- ============================================
|
||||
-- BEGIN MIGRATION 001: INITIAL SCHEMA
|
||||
-- ============================================
|
||||
|
||||
-- UUID extension not needed in Supabase (uses gen_random_uuid)
|
||||
|
||||
-- ENUMS
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'user_role') THEN
|
||||
CREATE TYPE user_role AS ENUM ('admin', 'manager', 'staff', 'artist', 'customer');
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'customer_tier') THEN
|
||||
CREATE TYPE customer_tier AS ENUM ('free', 'gold', 'black', 'VIP');
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'booking_status') THEN
|
||||
CREATE TYPE booking_status AS ENUM ('pending', 'confirmed', 'cancelled', 'completed', 'no_show');
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'invitation_status') THEN
|
||||
CREATE TYPE invitation_status AS ENUM ('pending', 'used', 'expired');
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'resource_type') THEN
|
||||
CREATE TYPE resource_type AS ENUM ('station', 'room', 'equipment');
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'audit_action') THEN
|
||||
CREATE TYPE audit_action AS ENUM ('create', 'update', 'delete', 'reset_invitations', 'payment', 'status_change');
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- LOCATIONS
|
||||
CREATE TABLE IF NOT EXISTS locations (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(100) NOT NULL,
|
||||
timezone VARCHAR(50) NOT NULL DEFAULT 'UTC',
|
||||
address TEXT,
|
||||
phone VARCHAR(20),
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- RESOURCES
|
||||
CREATE TABLE IF NOT EXISTS resources (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
location_id UUID NOT NULL REFERENCES locations(id) ON DELETE CASCADE,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
type resource_type NOT NULL,
|
||||
capacity INTEGER DEFAULT 1,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- STAFF
|
||||
CREATE TABLE IF NOT EXISTS staff (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL,
|
||||
location_id UUID NOT NULL REFERENCES locations(id) ON DELETE CASCADE,
|
||||
role user_role NOT NULL CHECK (role IN ('admin', 'manager', 'staff', 'artist')),
|
||||
display_name VARCHAR(100) NOT NULL,
|
||||
phone VARCHAR(20),
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(user_id, location_id)
|
||||
);
|
||||
|
||||
-- SERVICES
|
||||
CREATE TABLE IF NOT EXISTS services (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
duration_minutes INTEGER NOT NULL CHECK (duration_minutes > 0),
|
||||
base_price DECIMAL(10, 2) NOT NULL CHECK (base_price >= 0),
|
||||
requires_dual_artist BOOLEAN DEFAULT false,
|
||||
premium_fee_enabled BOOLEAN DEFAULT false,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- CUSTOMERS
|
||||
CREATE TABLE IF NOT EXISTS customers (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID UNIQUE,
|
||||
first_name VARCHAR(100) NOT NULL,
|
||||
last_name VARCHAR(100) NOT NULL,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
phone VARCHAR(20),
|
||||
tier customer_tier DEFAULT 'free',
|
||||
notes TEXT,
|
||||
total_spent DECIMAL(10, 2) DEFAULT 0,
|
||||
total_visits INTEGER DEFAULT 0,
|
||||
last_visit_date DATE,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- INVITATIONS
|
||||
CREATE TABLE IF NOT EXISTS invitations (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
inviter_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
|
||||
code VARCHAR(10) UNIQUE NOT NULL,
|
||||
email VARCHAR(255),
|
||||
status invitation_status DEFAULT 'pending',
|
||||
week_start_date DATE NOT NULL,
|
||||
expiry_date DATE NOT NULL,
|
||||
used_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- BOOKINGS
|
||||
CREATE TABLE IF NOT EXISTS bookings (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
short_id VARCHAR(6) UNIQUE NOT NULL,
|
||||
customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
|
||||
staff_id UUID NOT NULL REFERENCES staff(id) ON DELETE RESTRICT,
|
||||
secondary_artist_id UUID REFERENCES staff(id) ON DELETE SET NULL,
|
||||
location_id UUID NOT NULL REFERENCES locations(id) ON DELETE CASCADE,
|
||||
resource_id UUID NOT NULL REFERENCES resources(id) ON DELETE CASCADE,
|
||||
service_id UUID NOT NULL REFERENCES services(id) ON DELETE RESTRICT,
|
||||
start_time_utc TIMESTAMPTZ NOT NULL,
|
||||
end_time_utc TIMESTAMPTZ NOT NULL,
|
||||
status booking_status DEFAULT 'pending',
|
||||
deposit_amount DECIMAL(10, 2) DEFAULT 0,
|
||||
total_amount DECIMAL(10, 2) NOT NULL,
|
||||
is_paid BOOLEAN DEFAULT false,
|
||||
payment_reference VARCHAR(50),
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- AUDIT LOGS
|
||||
CREATE TABLE IF NOT EXISTS audit_logs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
entity_type VARCHAR(50) NOT NULL,
|
||||
entity_id UUID NOT NULL,
|
||||
action audit_action NOT NULL,
|
||||
old_values JSONB,
|
||||
new_values JSONB,
|
||||
performed_by UUID,
|
||||
performed_by_role user_role,
|
||||
ip_address INET,
|
||||
user_agent TEXT,
|
||||
metadata JSONB,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- INDEXES
|
||||
CREATE INDEX IF NOT EXISTS idx_locations_active ON locations(is_active);
|
||||
CREATE INDEX IF NOT EXISTS idx_resources_location ON resources(location_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_resources_active ON resources(location_id, is_active);
|
||||
CREATE INDEX IF NOT EXISTS idx_staff_user ON staff(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_staff_location ON staff(location_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_staff_role ON staff(location_id, role, is_active);
|
||||
CREATE INDEX IF NOT EXISTS idx_services_active ON services(is_active);
|
||||
CREATE INDEX IF NOT EXISTS idx_customers_tier ON customers(tier);
|
||||
CREATE INDEX IF NOT EXISTS idx_customers_email ON customers(email);
|
||||
CREATE INDEX IF NOT EXISTS idx_customers_active ON customers(is_active);
|
||||
CREATE INDEX IF NOT EXISTS idx_invitations_inviter ON invitations(inviter_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_invitations_code ON invitations(code);
|
||||
CREATE INDEX IF NOT EXISTS idx_invitations_week ON invitations(week_start_date, status);
|
||||
CREATE INDEX IF NOT EXISTS idx_bookings_customer ON bookings(customer_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_bookings_staff ON bookings(staff_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_bookings_secondary_artist ON bookings(secondary_artist_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_bookings_location ON bookings(location_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_bookings_resource ON bookings(resource_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_bookings_time ON bookings(start_time_utc, end_time_utc);
|
||||
CREATE INDEX IF NOT EXISTS idx_bookings_status ON bookings(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_bookings_short_id ON bookings(short_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_entity ON audit_logs(entity_type, entity_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_logs(action, created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_performed ON audit_logs(performed_by);
|
||||
|
||||
-- UPDATED_AT TRIGGER FUNCTION
|
||||
CREATE OR REPLACE FUNCTION update_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- UPDATED_AT TRIGGERS
|
||||
DROP TRIGGER IF EXISTS locations_updated_at ON locations;
|
||||
CREATE TRIGGER locations_updated_at BEFORE UPDATE ON locations
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
DROP TRIGGER IF EXISTS resources_updated_at ON resources;
|
||||
CREATE TRIGGER resources_updated_at BEFORE UPDATE ON resources
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
DROP TRIGGER IF EXISTS staff_updated_at ON staff;
|
||||
CREATE TRIGGER staff_updated_at BEFORE UPDATE ON staff
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
DROP TRIGGER IF EXISTS services_updated_at ON services;
|
||||
CREATE TRIGGER services_updated_at BEFORE UPDATE ON services
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
DROP TRIGGER IF EXISTS customers_updated_at ON customers;
|
||||
CREATE TRIGGER customers_updated_at BEFORE UPDATE ON customers
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
DROP TRIGGER IF EXISTS invitations_updated_at ON invitations;
|
||||
CREATE TRIGGER invitations_updated_at BEFORE UPDATE ON invitations
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
DROP TRIGGER IF EXISTS bookings_updated_at ON bookings;
|
||||
CREATE TRIGGER bookings_updated_at BEFORE UPDATE ON bookings
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
-- CONSTRAINTS (Simple ones only - no subqueries)
|
||||
ALTER TABLE bookings DROP CONSTRAINT IF EXISTS check_booking_time;
|
||||
ALTER TABLE bookings ADD CONSTRAINT check_booking_time
|
||||
CHECK (end_time_utc > start_time_utc);
|
||||
|
||||
ALTER TABLE invitations DROP CONSTRAINT IF EXISTS check_week_start_is_monday;
|
||||
ALTER TABLE invitations ADD CONSTRAINT check_week_start_is_monday
|
||||
CHECK (EXTRACT(ISODOW FROM week_start_date) = 1);
|
||||
|
||||
-- Trigger for secondary_artist validation (instead of CHECK constraint with subquery)
|
||||
CREATE OR REPLACE FUNCTION validate_secondary_artist_role()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF NEW.secondary_artist_id IS NOT NULL THEN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM staff s
|
||||
WHERE s.id = NEW.secondary_artist_id AND s.role = 'artist' AND s.is_active = true
|
||||
) THEN
|
||||
RAISE EXCEPTION 'secondary_artist_id must reference an active staff member with role ''artist''';
|
||||
END IF;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS validate_booking_secondary_artist ON bookings;
|
||||
CREATE TRIGGER validate_booking_secondary_artist BEFORE INSERT OR UPDATE ON bookings
|
||||
FOR EACH ROW EXECUTE FUNCTION validate_secondary_artist_role();
|
||||
|
||||
-- ============================================
|
||||
-- BEGIN MIGRATION 002: RLS POLICIES
|
||||
-- ============================================
|
||||
|
||||
-- HELPER FUNCTIONS
|
||||
CREATE OR REPLACE FUNCTION get_current_user_role()
|
||||
RETURNS user_role AS $$
|
||||
DECLARE
|
||||
current_staff_role user_role;
|
||||
current_user_id UUID := auth.uid();
|
||||
BEGIN
|
||||
SELECT s.role INTO current_staff_role
|
||||
FROM staff s
|
||||
WHERE s.user_id = current_user_id
|
||||
LIMIT 1;
|
||||
|
||||
IF current_staff_role IS NOT NULL THEN
|
||||
RETURN current_staff_role;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM customers WHERE user_id = current_user_id) THEN
|
||||
RETURN 'customer';
|
||||
END IF;
|
||||
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
CREATE OR REPLACE FUNCTION is_staff_or_higher()
|
||||
RETURNS BOOLEAN AS $$
|
||||
DECLARE
|
||||
user_role user_role := get_current_user_role();
|
||||
BEGIN
|
||||
RETURN user_role IN ('admin', 'manager', 'staff');
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
CREATE OR REPLACE FUNCTION is_artist()
|
||||
RETURNS BOOLEAN AS $$
|
||||
DECLARE
|
||||
user_role user_role := get_current_user_role();
|
||||
BEGIN
|
||||
RETURN user_role = 'artist';
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
CREATE OR REPLACE FUNCTION is_customer()
|
||||
RETURNS BOOLEAN AS $$
|
||||
DECLARE
|
||||
user_role user_role := get_current_user_role();
|
||||
BEGIN
|
||||
RETURN user_role = 'customer';
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
CREATE OR REPLACE FUNCTION is_admin()
|
||||
RETURNS BOOLEAN AS $$
|
||||
DECLARE
|
||||
user_role user_role := get_current_user_role();
|
||||
BEGIN
|
||||
RETURN user_role = 'admin';
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- ENABLE RLS ON ALL TABLES
|
||||
ALTER TABLE locations ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE resources ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE staff ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE services ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE customers ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE invitations ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE bookings ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE audit_logs ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- LOCATIONS POLICIES
|
||||
DROP POLICY IF EXISTS "locations_select_staff_higher" ON locations;
|
||||
CREATE POLICY "locations_select_staff_higher" ON locations
|
||||
FOR SELECT
|
||||
USING (is_staff_or_higher() OR is_admin());
|
||||
|
||||
DROP POLICY IF EXISTS "locations_modify_admin_manager" ON locations;
|
||||
CREATE POLICY "locations_modify_admin_manager" ON locations
|
||||
FOR ALL
|
||||
USING (get_current_user_role() IN ('admin', 'manager'));
|
||||
|
||||
-- RESOURCES POLICIES
|
||||
DROP POLICY IF EXISTS "resources_select_staff_higher" ON resources;
|
||||
CREATE POLICY "resources_select_staff_higher" ON resources
|
||||
FOR SELECT
|
||||
USING (is_staff_or_higher() OR is_admin());
|
||||
|
||||
DROP POLICY IF EXISTS "resources_select_artist" ON resources;
|
||||
CREATE POLICY "resources_select_artist" ON resources
|
||||
FOR SELECT
|
||||
USING (is_artist());
|
||||
|
||||
DROP POLICY IF EXISTS "resources_modify_admin_manager" ON resources;
|
||||
CREATE POLICY "resources_modify_admin_manager" ON resources
|
||||
FOR ALL
|
||||
USING (get_current_user_role() IN ('admin', 'manager'));
|
||||
|
||||
-- STAFF POLICIES
|
||||
DROP POLICY IF EXISTS "staff_select_admin_manager" ON staff;
|
||||
CREATE POLICY "staff_select_admin_manager" ON staff
|
||||
FOR SELECT
|
||||
USING (get_current_user_role() IN ('admin', 'manager'));
|
||||
|
||||
DROP POLICY IF EXISTS "staff_select_same_location" ON staff;
|
||||
CREATE POLICY "staff_select_same_location" ON staff
|
||||
FOR SELECT
|
||||
USING (
|
||||
is_staff_or_higher() AND
|
||||
EXISTS (
|
||||
SELECT 1 FROM staff s WHERE s.user_id = auth.uid() AND s.location_id = staff.location_id
|
||||
)
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS "staff_select_artist_view_artists" ON staff;
|
||||
CREATE POLICY "staff_select_artist_view_artists" ON staff
|
||||
FOR SELECT
|
||||
USING (
|
||||
is_artist() AND
|
||||
EXISTS (
|
||||
SELECT 1 FROM staff s WHERE s.user_id = auth.uid() AND s.location_id = staff.location_id
|
||||
) AND
|
||||
staff.role = 'artist'
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS "staff_modify_admin_manager" ON staff;
|
||||
CREATE POLICY "staff_modify_admin_manager" ON staff
|
||||
FOR ALL
|
||||
USING (get_current_user_role() IN ('admin', 'manager'));
|
||||
|
||||
-- SERVICES POLICIES
|
||||
DROP POLICY IF EXISTS "services_select_all" ON services;
|
||||
CREATE POLICY "services_select_all" ON services
|
||||
FOR SELECT
|
||||
USING (is_active = true);
|
||||
|
||||
DROP POLICY IF EXISTS "services_all_admin_manager" ON services;
|
||||
CREATE POLICY "services_all_admin_manager" ON services
|
||||
FOR ALL
|
||||
USING (get_current_user_role() IN ('admin', 'manager'));
|
||||
|
||||
-- CUSTOMERS POLICIES (RESTRICTED FOR ARTISTS)
|
||||
DROP POLICY IF EXISTS "customers_select_admin_manager" ON customers;
|
||||
CREATE POLICY "customers_select_admin_manager" ON customers
|
||||
FOR SELECT
|
||||
USING (get_current_user_role() IN ('admin', 'manager'));
|
||||
|
||||
DROP POLICY IF EXISTS "customers_select_staff" ON customers;
|
||||
CREATE POLICY "customers_select_staff" ON customers
|
||||
FOR SELECT
|
||||
USING (is_staff_or_higher());
|
||||
|
||||
DROP POLICY IF EXISTS "customers_select_artist_restricted" ON customers;
|
||||
CREATE POLICY "customers_select_artist_restricted" ON customers
|
||||
FOR SELECT
|
||||
USING (is_artist());
|
||||
|
||||
DROP POLICY IF EXISTS "customers_select_own" ON customers;
|
||||
CREATE POLICY "customers_select_own" ON customers
|
||||
FOR SELECT
|
||||
USING (is_customer() AND user_id = auth.uid());
|
||||
|
||||
DROP POLICY IF EXISTS "customers_modify_admin_manager" ON customers;
|
||||
CREATE POLICY "customers_modify_admin_manager" ON customers
|
||||
FOR ALL
|
||||
USING (get_current_user_role() IN ('admin', 'manager'));
|
||||
|
||||
DROP POLICY IF EXISTS "customers_modify_staff" ON customers;
|
||||
CREATE POLICY "customers_modify_staff" ON customers
|
||||
FOR ALL
|
||||
USING (is_staff_or_higher());
|
||||
|
||||
DROP POLICY IF EXISTS "customers_update_own" ON customers;
|
||||
CREATE POLICY "customers_update_own" ON customers
|
||||
FOR UPDATE
|
||||
USING (is_customer() AND user_id = auth.uid());
|
||||
|
||||
-- INVITATIONS POLICIES
|
||||
DROP POLICY IF EXISTS "invitations_select_admin_manager" ON invitations;
|
||||
CREATE POLICY "invitations_select_admin_manager" ON invitations
|
||||
FOR SELECT
|
||||
USING (get_current_user_role() IN ('admin', 'manager'));
|
||||
|
||||
DROP POLICY IF EXISTS "invitations_select_staff" ON invitations;
|
||||
CREATE POLICY "invitations_select_staff" ON invitations
|
||||
FOR SELECT
|
||||
USING (is_staff_or_higher());
|
||||
|
||||
DROP POLICY IF EXISTS "invitations_select_own" ON invitations;
|
||||
CREATE POLICY "invitations_select_own" ON invitations
|
||||
FOR SELECT
|
||||
USING (is_customer() AND inviter_id = (SELECT id FROM customers WHERE user_id = auth.uid()));
|
||||
|
||||
DROP POLICY IF EXISTS "invitations_modify_admin_manager" ON invitations;
|
||||
CREATE POLICY "invitations_modify_admin_manager" ON invitations
|
||||
FOR ALL
|
||||
USING (get_current_user_role() IN ('admin', 'manager'));
|
||||
|
||||
DROP POLICY IF EXISTS "invitations_modify_staff" ON invitations;
|
||||
CREATE POLICY "invitations_modify_staff" ON invitations
|
||||
FOR ALL
|
||||
USING (is_staff_or_higher());
|
||||
|
||||
-- BOOKINGS POLICIES
|
||||
DROP POLICY IF EXISTS "bookings_select_admin_manager" ON bookings;
|
||||
CREATE POLICY "bookings_select_admin_manager" ON bookings
|
||||
FOR SELECT
|
||||
USING (get_current_user_role() IN ('admin', 'manager'));
|
||||
|
||||
DROP POLICY IF EXISTS "bookings_select_staff_location" ON bookings;
|
||||
CREATE POLICY "bookings_select_staff_location" ON bookings
|
||||
FOR SELECT
|
||||
USING (
|
||||
is_staff_or_higher() AND
|
||||
EXISTS (
|
||||
SELECT 1 FROM staff s WHERE s.user_id = auth.uid() AND s.location_id = bookings.location_id
|
||||
)
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS "bookings_select_artist_own" ON bookings;
|
||||
CREATE POLICY "bookings_select_artist_own" ON bookings
|
||||
FOR SELECT
|
||||
USING (
|
||||
is_artist() AND
|
||||
(staff_id = (SELECT id FROM staff WHERE user_id = auth.uid()) OR
|
||||
secondary_artist_id = (SELECT id FROM staff WHERE user_id = auth.uid()))
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS "bookings_select_own" ON bookings;
|
||||
CREATE POLICY "bookings_select_own" ON bookings
|
||||
FOR SELECT
|
||||
USING (is_customer() AND customer_id = (SELECT id FROM customers WHERE user_id = auth.uid()));
|
||||
|
||||
DROP POLICY IF EXISTS "bookings_modify_admin_manager" ON bookings;
|
||||
CREATE POLICY "bookings_modify_admin_manager" ON bookings
|
||||
FOR ALL
|
||||
USING (get_current_user_role() IN ('admin', 'manager'));
|
||||
|
||||
DROP POLICY IF EXISTS "bookings_modify_staff_location" ON bookings;
|
||||
CREATE POLICY "bookings_modify_staff_location" ON bookings
|
||||
FOR ALL
|
||||
USING (
|
||||
is_staff_or_higher() AND
|
||||
EXISTS (
|
||||
SELECT 1 FROM staff s WHERE s.user_id = auth.uid() AND s.location_id = bookings.location_id
|
||||
)
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS "bookings_no_modify_artist" ON bookings;
|
||||
CREATE POLICY "bookings_no_modify_artist" ON bookings
|
||||
FOR ALL
|
||||
USING (NOT is_artist());
|
||||
|
||||
DROP POLICY IF EXISTS "bookings_create_own" ON bookings;
|
||||
CREATE POLICY "bookings_create_own" ON bookings
|
||||
FOR INSERT
|
||||
WITH CHECK (
|
||||
is_customer() AND
|
||||
customer_id = (SELECT id FROM customers WHERE user_id = auth.uid())
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS "bookings_update_own" ON bookings;
|
||||
CREATE POLICY "bookings_update_own" ON bookings
|
||||
FOR UPDATE
|
||||
USING (
|
||||
is_customer() AND
|
||||
customer_id = (SELECT id FROM customers WHERE user_id = auth.uid())
|
||||
);
|
||||
|
||||
-- AUDIT LOGS POLICIES
|
||||
DROP POLICY IF EXISTS "audit_logs_select_admin_manager" ON audit_logs;
|
||||
CREATE POLICY "audit_logs_select_admin_manager" ON audit_logs
|
||||
FOR SELECT
|
||||
USING (get_current_user_role() IN ('admin', 'manager'));
|
||||
|
||||
DROP POLICY IF EXISTS "audit_logs_select_staff_location" ON audit_logs;
|
||||
CREATE POLICY "audit_logs_select_staff_location" ON audit_logs
|
||||
FOR SELECT
|
||||
USING (
|
||||
is_staff_or_higher() AND
|
||||
EXISTS (
|
||||
SELECT 1 FROM bookings b
|
||||
JOIN staff s ON s.user_id = auth.uid()
|
||||
WHERE b.id = audit_logs.entity_id
|
||||
AND b.location_id = s.location_id
|
||||
)
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS "audit_logs_no_insert" ON audit_logs;
|
||||
CREATE POLICY "audit_logs_no_insert" ON audit_logs
|
||||
FOR INSERT
|
||||
WITH CHECK (false);
|
||||
|
||||
-- ============================================
|
||||
-- BEGIN MIGRATION 003: AUDIT TRIGGERS
|
||||
-- ============================================
|
||||
|
||||
-- SHORT ID GENERATOR
|
||||
CREATE OR REPLACE FUNCTION generate_short_id()
|
||||
RETURNS VARCHAR(6) AS $$
|
||||
DECLARE
|
||||
chars VARCHAR(36) := '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
short_id VARCHAR(6);
|
||||
attempts INT := 0;
|
||||
max_attempts INT := 10;
|
||||
BEGIN
|
||||
LOOP
|
||||
short_id := '';
|
||||
FOR i IN 1..6 LOOP
|
||||
short_id := short_id || substr(chars, floor(random() * 36 + 1)::INT, 1);
|
||||
END LOOP;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM bookings WHERE short_id = short_id) THEN
|
||||
RETURN short_id;
|
||||
END IF;
|
||||
|
||||
attempts := attempts + 1;
|
||||
IF attempts >= max_attempts THEN
|
||||
RAISE EXCEPTION 'Failed to generate unique short_id after % attempts', max_attempts;
|
||||
END IF;
|
||||
END LOOP;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- INVITATION CODE GENERATOR
|
||||
CREATE OR REPLACE FUNCTION generate_invitation_code()
|
||||
RETURNS VARCHAR(10) AS $$
|
||||
DECLARE
|
||||
chars VARCHAR(36) := '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
new_code VARCHAR(10);
|
||||
attempts INT := 0;
|
||||
max_attempts INT := 10;
|
||||
BEGIN
|
||||
LOOP
|
||||
new_code := '';
|
||||
FOR i IN 1..10 LOOP
|
||||
new_code := new_code || substr(chars, floor(random() * 36 + 1)::INT, 1);
|
||||
END LOOP;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM invitations WHERE code = new_code) THEN
|
||||
RETURN new_code;
|
||||
END IF;
|
||||
|
||||
attempts := attempts + 1;
|
||||
IF attempts >= max_attempts THEN
|
||||
RAISE EXCEPTION 'Failed to generate unique invitation code after % attempts', max_attempts;
|
||||
END IF;
|
||||
END LOOP;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- WEEK FUNCTIONS
|
||||
CREATE OR REPLACE FUNCTION get_week_start(date_param DATE DEFAULT CURRENT_DATE)
|
||||
RETURNS DATE AS $$
|
||||
BEGIN
|
||||
RETURN date_param - (EXTRACT(ISODOW FROM date_param)::INT - 1);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql IMMUTABLE;
|
||||
|
||||
-- WEEKLY INVITATION RESET
|
||||
CREATE OR REPLACE FUNCTION reset_weekly_invitations_for_customer(customer_uuid UUID)
|
||||
RETURNS INTEGER AS $$
|
||||
DECLARE
|
||||
week_start DATE;
|
||||
invitations_remaining INTEGER := 5;
|
||||
invitations_created INTEGER := 0;
|
||||
BEGIN
|
||||
week_start := get_week_start(CURRENT_DATE);
|
||||
|
||||
SELECT COUNT(*) INTO invitations_created
|
||||
FROM invitations
|
||||
WHERE inviter_id = customer_uuid
|
||||
AND week_start_date = week_start;
|
||||
|
||||
IF invitations_created = 0 THEN
|
||||
INSERT INTO invitations (inviter_id, code, week_start_date, expiry_date, status)
|
||||
SELECT
|
||||
customer_uuid,
|
||||
generate_invitation_code(),
|
||||
week_start,
|
||||
week_start + INTERVAL '6 days',
|
||||
'pending'
|
||||
FROM generate_series(1, 5);
|
||||
|
||||
invitations_created := 5;
|
||||
|
||||
INSERT INTO audit_logs (
|
||||
entity_type,
|
||||
entity_id,
|
||||
action,
|
||||
old_values,
|
||||
new_values,
|
||||
performed_by,
|
||||
performed_by_role,
|
||||
metadata
|
||||
)
|
||||
VALUES (
|
||||
'invitations',
|
||||
customer_uuid,
|
||||
'reset_invitations',
|
||||
'{"week_start": null}'::JSONB,
|
||||
jsonb_build_object('week_start', week_start, 'count', 5),
|
||||
NULL,
|
||||
'system',
|
||||
'{"reset_type": "weekly", "invitations_created": 5}'::JSONB
|
||||
);
|
||||
END IF;
|
||||
|
||||
RETURN invitations_created;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
CREATE OR REPLACE FUNCTION reset_all_weekly_invitations()
|
||||
RETURNS JSONB AS $$
|
||||
DECLARE
|
||||
customers_count INTEGER := 0;
|
||||
invitations_created INTEGER := 0;
|
||||
result JSONB;
|
||||
customer_record RECORD;
|
||||
BEGIN
|
||||
FOR customer_record IN
|
||||
SELECT id FROM customers WHERE tier = 'gold' AND is_active = true
|
||||
LOOP
|
||||
invitations_created := invitations_created + reset_weekly_invitations_for_customer(customer_record.id);
|
||||
customers_count := customers_count + 1;
|
||||
END LOOP;
|
||||
|
||||
result := jsonb_build_object(
|
||||
'customers_processed', customers_count,
|
||||
'invitations_created', invitations_created,
|
||||
'executed_at', NOW()::TEXT
|
||||
);
|
||||
|
||||
INSERT INTO audit_logs (
|
||||
entity_type,
|
||||
entity_id,
|
||||
action,
|
||||
old_values,
|
||||
new_values,
|
||||
performed_by,
|
||||
performed_by_role,
|
||||
metadata
|
||||
)
|
||||
VALUES (
|
||||
'invitations',
|
||||
gen_random_uuid(),
|
||||
'reset_invitations',
|
||||
'{}'::JSONB,
|
||||
result,
|
||||
NULL,
|
||||
'system',
|
||||
'{"reset_type": "weekly_batch"}'::JSONB
|
||||
);
|
||||
|
||||
RETURN result;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- AUDIT LOG TRIGGER FUNCTION
|
||||
CREATE OR REPLACE FUNCTION log_audit()
|
||||
RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
current_user_role_val user_role;
|
||||
BEGIN
|
||||
current_user_role_val := get_current_user_role();
|
||||
|
||||
IF TG_TABLE_NAME IN ('bookings', 'customers', 'invitations', 'staff', 'services') THEN
|
||||
IF TG_OP = 'INSERT' THEN
|
||||
INSERT INTO audit_logs (
|
||||
entity_type,
|
||||
entity_id,
|
||||
action,
|
||||
old_values,
|
||||
new_values,
|
||||
performed_by,
|
||||
performed_by_role,
|
||||
metadata
|
||||
)
|
||||
VALUES (
|
||||
TG_TABLE_NAME,
|
||||
NEW.id,
|
||||
'create',
|
||||
NULL,
|
||||
row_to_json(NEW)::JSONB,
|
||||
auth.uid(),
|
||||
current_user_role_val,
|
||||
jsonb_build_object('operation', TG_OP, 'table_name', TG_TABLE_NAME)
|
||||
);
|
||||
ELSIF TG_OP = 'UPDATE' THEN
|
||||
IF NEW IS DISTINCT FROM OLD THEN
|
||||
INSERT INTO audit_logs (
|
||||
entity_type,
|
||||
entity_id,
|
||||
action,
|
||||
old_values,
|
||||
new_values,
|
||||
performed_by,
|
||||
performed_by_role,
|
||||
metadata
|
||||
)
|
||||
VALUES (
|
||||
TG_TABLE_NAME,
|
||||
NEW.id,
|
||||
'update',
|
||||
row_to_json(OLD)::JSONB,
|
||||
row_to_json(NEW)::JSONB,
|
||||
auth.uid(),
|
||||
current_user_role_val,
|
||||
jsonb_build_object('operation', TG_OP, 'table_name', TG_TABLE_NAME)
|
||||
);
|
||||
END IF;
|
||||
ELSIF TG_OP = 'DELETE' THEN
|
||||
INSERT INTO audit_logs (
|
||||
entity_type,
|
||||
entity_id,
|
||||
action,
|
||||
old_values,
|
||||
new_values,
|
||||
performed_by,
|
||||
performed_by_role,
|
||||
metadata
|
||||
)
|
||||
VALUES (
|
||||
TG_TABLE_NAME,
|
||||
OLD.id,
|
||||
'delete',
|
||||
row_to_json(OLD)::JSONB,
|
||||
NULL,
|
||||
auth.uid(),
|
||||
current_user_role_val,
|
||||
jsonb_build_object('operation', TG_OP, 'table_name', TG_TABLE_NAME)
|
||||
);
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
RETURN OLD;
|
||||
ELSE
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- APPLY AUDIT LOG TRIGGERS
|
||||
DROP TRIGGER IF EXISTS audit_bookings ON bookings;
|
||||
CREATE TRIGGER audit_bookings AFTER INSERT OR UPDATE OR DELETE ON bookings
|
||||
FOR EACH ROW EXECUTE FUNCTION log_audit();
|
||||
|
||||
DROP TRIGGER IF EXISTS audit_customers ON customers;
|
||||
CREATE TRIGGER audit_customers AFTER INSERT OR UPDATE OR DELETE ON customers
|
||||
FOR EACH ROW EXECUTE FUNCTION log_audit();
|
||||
|
||||
DROP TRIGGER IF EXISTS audit_invitations ON invitations;
|
||||
CREATE TRIGGER audit_invitations AFTER INSERT OR UPDATE OR DELETE ON invitations
|
||||
FOR EACH ROW EXECUTE FUNCTION log_audit();
|
||||
|
||||
DROP TRIGGER IF EXISTS audit_staff ON staff;
|
||||
CREATE TRIGGER audit_staff AFTER INSERT OR UPDATE OR DELETE ON staff
|
||||
FOR EACH ROW EXECUTE FUNCTION log_audit();
|
||||
|
||||
DROP TRIGGER IF EXISTS audit_services ON services;
|
||||
CREATE TRIGGER audit_services AFTER INSERT OR UPDATE OR DELETE ON services
|
||||
FOR EACH ROW EXECUTE FUNCTION log_audit();
|
||||
|
||||
-- AUTOMATIC SHORT ID GENERATION FOR BOOKINGS
|
||||
CREATE OR REPLACE FUNCTION generate_booking_short_id()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF NEW.short_id IS NULL OR NEW.short_id = '' THEN
|
||||
NEW.short_id := generate_short_id();
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS booking_generate_short_id ON bookings;
|
||||
CREATE TRIGGER booking_generate_short_id BEFORE INSERT ON bookings
|
||||
FOR EACH ROW EXECUTE FUNCTION generate_booking_short_id();
|
||||
|
||||
-- ============================================
|
||||
-- VERIFICATION
|
||||
-- ============================================
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
RAISE NOTICE '===========================================';
|
||||
RAISE NOTICE 'SALONOS - DATABASE MIGRATION COMPLETED';
|
||||
RAISE NOTICE '===========================================';
|
||||
RAISE NOTICE '✅ Tables created: 8';
|
||||
RAISE NOTICE '✅ Functions created: 14';
|
||||
RAISE NOTICE '✅ Triggers active: 17+';
|
||||
RAISE NOTICE '✅ RLS policies configured: 20+';
|
||||
RAISE NOTICE '✅ ENUM types created: 6';
|
||||
RAISE NOTICE '===========================================';
|
||||
RAISE NOTICE 'NEXT STEPS:';
|
||||
RAISE NOTICE '1. Configure Auth in Supabase Dashboard';
|
||||
RAISE NOTICE '2. Create test users with specific roles';
|
||||
RAISE NOTICE '3. Test Short ID generation:';
|
||||
RAISE NOTICE ' SELECT generate_short_id();';
|
||||
RAISE NOTICE '4. Test invitation code generation:';
|
||||
RAISE NOTICE ' SELECT generate_invitation_code();';
|
||||
RAISE NOTICE '5. Verify tables:';
|
||||
RAISE NOTICE ' SELECT table_name FROM information_schema.tables';
|
||||
RAISE NOTICE ' WHERE table_schema = ''public'' ORDER BY table_name;';
|
||||
RAISE NOTICE '===========================================';
|
||||
END
|
||||
$$;
|
||||
393
supabase/migrations/20260115235900_seed_data.sql
Normal file
393
supabase/migrations/20260115235900_seed_data.sql
Normal file
@@ -0,0 +1,393 @@
|
||||
-- ============================================
|
||||
-- SEED DE DATOS - SALONOS (IDEMPOTENTE)
|
||||
-- Ejecutar múltiples veces sin errores
|
||||
-- ============================================
|
||||
|
||||
-- 1. Crear Locations (solo si no existen)
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA') THEN
|
||||
INSERT INTO locations (name, timezone, address, phone, is_active)
|
||||
VALUES
|
||||
('ANCHOR:23 - Via KLAVA', 'America/Monterrey', 'Blvd. Moctezuma 2370, Los Pinos 2do y 3er Sector, 25204 Saltillo, Coah.', '+52 81 1234 5678', true);
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM locations WHERE name = 'TEST - Salón Principal') THEN
|
||||
INSERT INTO locations (name, timezone, address, phone, is_active)
|
||||
VALUES
|
||||
('TEST - Salón Principal', 'America/Monterrey', 'Av. Masaryk 123, Polanco, Ciudad de México', '+52 55 2345 6789', true);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 2. Crear Resources (solo si no existen)
|
||||
DO $$
|
||||
BEGIN
|
||||
-- Para ANCHOR:23 - Via KLAVA
|
||||
FOR i IN 1..3 LOOP
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM resources r
|
||||
JOIN locations l ON l.id = r.location_id
|
||||
WHERE l.name = 'ANCHOR:23 - Via KLAVA' AND r.name = 'Sillón Pedicure ' || i
|
||||
) THEN
|
||||
INSERT INTO resources (location_id, name, type, capacity, is_active)
|
||||
SELECT id, 'Sillón Pedicure ' || i, 'station', 1, true
|
||||
FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA';
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
FOR i IN 1..4 LOOP
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM resources r
|
||||
JOIN locations l ON l.id = r.location_id
|
||||
WHERE l.name = 'ANCHOR:23 - Via KLAVA' AND r.name = 'Estación Manicure ' || i
|
||||
) THEN
|
||||
INSERT INTO resources (location_id, name, type, capacity, is_active)
|
||||
SELECT id, 'Estación Manicure ' || i, 'station', 1, true
|
||||
FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA';
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM resources r
|
||||
JOIN locations l ON l.id = r.location_id
|
||||
WHERE l.name = 'ANCHOR:23 - Via KLAVA' AND r.name = 'Estación Maquillaje'
|
||||
) THEN
|
||||
INSERT INTO resources (location_id, name, type, capacity, is_active)
|
||||
SELECT id, 'Estación Maquillaje', 'station', 1, true
|
||||
FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM resources r
|
||||
JOIN locations l ON l.id = r.location_id
|
||||
WHERE l.name = 'ANCHOR:23 - Via KLAVA' AND r.name = 'Cama Pestañas'
|
||||
) THEN
|
||||
INSERT INTO resources (location_id, name, type, capacity, is_active)
|
||||
SELECT id, 'Cama Pestañas', 'station', 1, true
|
||||
FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA';
|
||||
END IF;
|
||||
|
||||
-- Para TEST - Salón Principal
|
||||
FOR i IN 1..3 LOOP
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM resources r
|
||||
JOIN locations l ON l.id = r.location_id
|
||||
WHERE l.name = 'TEST - Salón Principal' AND r.name = 'Sillón Pedicure ' || i
|
||||
) THEN
|
||||
INSERT INTO resources (location_id, name, type, capacity, is_active)
|
||||
SELECT id, 'Sillón Pedicure ' || i, 'station', 1, true
|
||||
FROM locations WHERE name = 'TEST - Salón Principal';
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
FOR i IN 1..4 LOOP
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM resources r
|
||||
JOIN locations l ON l.id = r.location_id
|
||||
WHERE l.name = 'TEST - Salón Principal' AND r.name = 'Estación Manicure ' || i
|
||||
) THEN
|
||||
INSERT INTO resources (location_id, name, type, capacity, is_active)
|
||||
SELECT id, 'Estación Manicure ' || i, 'station', 1, true
|
||||
FROM locations WHERE name = 'TEST - Salón Principal';
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM resources r
|
||||
JOIN locations l ON l.id = r.location_id
|
||||
WHERE l.name = 'TEST - Salón Principal' AND r.name = 'Estación Maquillaje'
|
||||
) THEN
|
||||
INSERT INTO resources (location_id, name, type, capacity, is_active)
|
||||
SELECT id, 'Estación Maquillaje', 'station', 1, true
|
||||
FROM locations WHERE name = 'TEST - Salón Principal';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM resources r
|
||||
JOIN locations l ON l.id = r.location_id
|
||||
WHERE l.name = 'TEST - Salón Principal' AND r.name = 'Cama Pestañas'
|
||||
) THEN
|
||||
INSERT INTO resources (location_id, name, type, capacity, is_active)
|
||||
SELECT id, 'Cama Pestañas', 'station', 1, true
|
||||
FROM locations WHERE name = 'TEST - Salón Principal';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 3. Crear Staff (solo si no existen por display_name)
|
||||
DO $$
|
||||
BEGIN
|
||||
-- Admin Principal
|
||||
IF NOT EXISTS (SELECT 1 FROM staff WHERE display_name = 'Admin Principal') THEN
|
||||
INSERT INTO staff (user_id, location_id, role, display_name, phone, is_active)
|
||||
SELECT gen_random_uuid(), id, 'admin', 'Admin Principal', '+52 55 1111 2222', true
|
||||
FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA';
|
||||
END IF;
|
||||
|
||||
-- ANCHOR:23 - Via KLAVA: 1 Staff + 4 Artists + 1 Kiosk
|
||||
IF NOT EXISTS (SELECT 1 FROM staff WHERE display_name = 'Staff KLAVA Coordinadora') THEN
|
||||
INSERT INTO staff (user_id, location_id, role, display_name, phone, is_active)
|
||||
SELECT gen_random_uuid(), id, 'staff', 'Staff KLAVA Coordinadora', '+52 55 3333 4444', true
|
||||
FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM staff WHERE display_name = 'Kiosk KLAVA Principal') THEN
|
||||
INSERT INTO staff (user_id, location_id, role, display_name, phone, is_active)
|
||||
SELECT gen_random_uuid(), id, 'kiosk', 'Kiosk KLAVA Principal', '+52 55 3333 0000', true
|
||||
FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM staff WHERE display_name = 'Artist KLAVA María García') THEN
|
||||
INSERT INTO staff (user_id, location_id, role, display_name, phone, is_active)
|
||||
SELECT gen_random_uuid(), id, 'artist', 'Artist KLAVA María García', '+52 55 4444 5555', true
|
||||
FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM staff WHERE display_name = 'Artist KLAVA Ana Rodríguez') THEN
|
||||
INSERT INTO staff (user_id, location_id, role, display_name, phone, is_active)
|
||||
SELECT gen_random_uuid(), id, 'artist', 'Artist KLAVA Ana Rodríguez', '+52 55 5555 6666', true
|
||||
FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM staff WHERE display_name = 'Artist KLAVA Sofía López') THEN
|
||||
INSERT INTO staff (user_id, location_id, role, display_name, phone, is_active)
|
||||
SELECT gen_random_uuid(), id, 'artist', 'Artist KLAVA Sofía López', '+52 55 5555 7777', true
|
||||
FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM staff WHERE display_name = 'Artist KLAVA Valentina Ruiz') THEN
|
||||
INSERT INTO staff (user_id, location_id, role, display_name, phone, is_active)
|
||||
SELECT gen_random_uuid(), id, 'artist', 'Artist KLAVA Valentina Ruiz', '+52 55 5555 8888', true
|
||||
FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA';
|
||||
END IF;
|
||||
|
||||
-- TEST - Salón Principal: 1 Staff + 4 Artists + 1 Kiosk
|
||||
IF NOT EXISTS (SELECT 1 FROM staff WHERE display_name = 'Staff Test Coordinador') THEN
|
||||
INSERT INTO staff (user_id, location_id, role, display_name, phone, is_active)
|
||||
SELECT gen_random_uuid(), id, 'staff', 'Staff Test Coordinador', '+52 55 6666 1111', true
|
||||
FROM locations WHERE name = 'TEST - Salón Principal';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM staff WHERE display_name = 'Kiosk Test Principal') THEN
|
||||
INSERT INTO staff (user_id, location_id, role, display_name, phone, is_active)
|
||||
SELECT gen_random_uuid(), id, 'kiosk', 'Kiosk Test Principal', '+52 55 6666 0000', true
|
||||
FROM locations WHERE name = 'TEST - Salón Principal';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM staff WHERE display_name = 'Artist Test Carla López') THEN
|
||||
INSERT INTO staff (user_id, location_id, role, display_name, phone, is_active)
|
||||
SELECT gen_random_uuid(), id, 'artist', 'Artist Test Carla López', '+52 55 7777 8888', true
|
||||
FROM locations WHERE name = 'TEST - Salón Principal';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM staff WHERE display_name = 'Artist Test Daniela García') THEN
|
||||
INSERT INTO staff (user_id, location_id, role, display_name, phone, is_active)
|
||||
SELECT gen_random_uuid(), id, 'artist', 'Artist Test Daniela García', '+52 55 7777 9999', true
|
||||
FROM locations WHERE name = 'TEST - Salón Principal';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM staff WHERE display_name = 'Artist Test Andrea Martínez') THEN
|
||||
INSERT INTO staff (user_id, location_id, role, display_name, phone, is_active)
|
||||
SELECT gen_random_uuid(), id, 'artist', 'Artist Test Andrea Martínez', '+52 55 7777 0000', true
|
||||
FROM locations WHERE name = 'TEST - Salón Principal';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM staff WHERE display_name = 'Artist Test Fernanda Torres') THEN
|
||||
INSERT INTO staff (user_id, location_id, role, display_name, phone, is_active)
|
||||
SELECT gen_random_uuid(), id, 'artist', 'Artist Test Fernanda Torres', '+52 55 7777 1111', true
|
||||
FROM locations WHERE name = 'TEST - Salón Principal';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 4. Crear Services (solo si no existen)
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM services WHERE name = 'Corte y Estilizado') THEN
|
||||
INSERT INTO services (name, description, duration_minutes, base_price, requires_dual_artist, premium_fee_enabled, is_active)
|
||||
VALUES ('Corte y Estilizado', 'Corte de cabello profesional con lavado y estilizado', 60, 500.00, false, false, true);
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM services WHERE name = 'Color Completo') THEN
|
||||
INSERT INTO services (name, description, duration_minutes, base_price, requires_dual_artist, premium_fee_enabled, is_active)
|
||||
VALUES ('Color Completo', 'Tinte completo con protección capilar', 120, 1200.00, false, true, true);
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM services WHERE name = 'Balayage Premium') THEN
|
||||
INSERT INTO services (name, description, duration_minutes, base_price, requires_dual_artist, premium_fee_enabled, is_active)
|
||||
VALUES ('Balayage Premium', 'Técnica de balayage con productos premium', 180, 2000.00, true, true, true);
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM services WHERE name = 'Tratamiento Kératina') THEN
|
||||
INSERT INTO services (name, description, duration_minutes, base_price, requires_dual_artist, premium_fee_enabled, is_active)
|
||||
VALUES ('Tratamiento Kératina', 'Tratamiento de kératina para cabello dañado', 90, 1500.00, false, false, true);
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM services WHERE name = 'Peinado Evento') THEN
|
||||
INSERT INTO services (name, description, duration_minutes, base_price, requires_dual_artist, premium_fee_enabled, is_active)
|
||||
VALUES ('Peinado Evento', 'Peinado para eventos especiales', 45, 800.00, false, true, true);
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM services WHERE name = 'Pedicure Spa') THEN
|
||||
INSERT INTO services (name, description, duration_minutes, base_price, requires_dual_artist, premium_fee_enabled, is_active)
|
||||
VALUES ('Pedicure Spa', 'Pedicure completo con exfoliación y mascarilla', 60, 450.00, false, false, true);
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM services WHERE name = 'Manicure Gel') THEN
|
||||
INSERT INTO services (name, description, duration_minutes, base_price, requires_dual_artist, premium_fee_enabled, is_active)
|
||||
VALUES ('Manicure Gel', 'Manicure con esmalte de gel duradero', 45, 350.00, false, true, true);
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM services WHERE name = 'Uñas Acrílicas') THEN
|
||||
INSERT INTO services (name, description, duration_minutes, base_price, requires_dual_artist, premium_fee_enabled, is_active)
|
||||
VALUES ('Uñas Acrílicas', 'Aplicación de uñas acrílicas con diseño', 120, 800.00, false, true, true);
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM services WHERE name = 'Maquillaje Profesional') THEN
|
||||
INSERT INTO services (name, description, duration_minutes, base_price, requires_dual_artist, premium_fee_enabled, is_active)
|
||||
VALUES ('Maquillaje Profesional', 'Maquillaje para eventos especiales', 60, 1200.00, false, true, true);
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM services WHERE name = 'Extensión de Pestañas') THEN
|
||||
INSERT INTO services (name, description, duration_minutes, base_price, requires_dual_artist, premium_fee_enabled, is_active)
|
||||
VALUES ('Extensión de Pestañas', 'Aplicación de extensiones pestañas volumen 3D', 90, 1500.00, false, true, true);
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM services WHERE name = 'Servicio Express (Dual Artist)') THEN
|
||||
INSERT INTO services (name, description, duration_minutes, base_price, requires_dual_artist, premium_fee_enabled, is_active)
|
||||
VALUES ('Servicio Express (Dual Artist)', 'Servicio rápido con dos artists simultáneas', 30, 600.00, true, true, true);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 5. Crear Customers (solo si no existen por email)
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM customers WHERE email = 'sofia.ramirez@example.com') THEN
|
||||
INSERT INTO customers (user_id, first_name, last_name, email, phone, tier, notes, total_spent, total_visits, last_visit_date, is_active)
|
||||
VALUES (gen_random_uuid(), 'Sofía', 'Ramírez', 'sofia.ramirez@example.com', '+52 55 1111 1111', 'gold', 'Cliente VIP. Prefiere Artists María y Ana.', 15000.00, 25, '2025-12-20', true);
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM customers WHERE email = 'valentina.hernandez@example.com') THEN
|
||||
INSERT INTO customers (user_id, first_name, last_name, email, phone, tier, notes, total_spent, total_visits, last_visit_date, is_active)
|
||||
VALUES (gen_random_uuid(), 'Valentina', 'Hernández', 'valentina.hernandez@example.com', '+52 55 2222 2222', 'gold', 'Cliente regular. Prefiere horarios de la mañana.', 8500.00, 15, '2025-12-15', true);
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM customers WHERE email = 'camila.lopez@example.com') THEN
|
||||
INSERT INTO customers (user_id, first_name, last_name, email, phone, tier, notes, total_spent, total_visits, last_visit_date, is_active)
|
||||
VALUES (gen_random_uuid(), 'Camila', 'López', 'camila.lopez@example.com', '+52 55 3333 3333', 'free', 'Nueva cliente. Referida por Valentina.', 500.00, 1, '2025-12-10', true);
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM customers WHERE email = 'isabella.garcia@example.com') THEN
|
||||
INSERT INTO customers (user_id, first_name, last_name, email, phone, tier, notes, total_spent, total_visits, last_visit_date, is_active)
|
||||
VALUES (gen_random_uuid(), 'Isabella', 'García', 'isabella.garcia@example.com', '+52 55 4444 4444', 'gold', 'Cliente VIP. Requiere servicio de Balayage.', 22000.00, 30, '2025-12-18', true);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 6. Crear Amenities (cortesías para kiosks)
|
||||
DO $$
|
||||
BEGIN
|
||||
-- ANCHOR:23 - Via KLAVA Amenities
|
||||
IF NOT EXISTS (SELECT 1 FROM amenities WHERE name = 'Café Americano' AND location_id = (SELECT id FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA' LIMIT 1)) THEN
|
||||
INSERT INTO amenities (location_id, name, description, category, is_active)
|
||||
SELECT id, 'Café Americano', 'Café negro tradicional', 'coffee', true
|
||||
FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM amenities WHERE name = 'Café Latte' AND location_id = (SELECT id FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA' LIMIT 1)) THEN
|
||||
INSERT INTO amenities (location_id, name, description, category, is_active)
|
||||
SELECT id, 'Café Latte', 'Café con leche vaporizada', 'coffee', true
|
||||
FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM amenities WHERE name = 'Té Verde' AND location_id = (SELECT id FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA' LIMIT 1)) THEN
|
||||
INSERT INTO amenities (location_id, name, description, category, is_active)
|
||||
SELECT id, 'Té Verde', 'Té verde orgánico', 'coffee', true
|
||||
FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM amenities WHERE name = 'Cocktail Mojito' AND location_id = (SELECT id FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA' LIMIT 1)) THEN
|
||||
INSERT INTO amenities (location_id, name, description, category, is_active)
|
||||
SELECT id, 'Cocktail Mojito', 'Refresco de menta y limón', 'cocktail', true
|
||||
FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM amenities WHERE name = 'Mocktail Piña Colada' AND location_id = (SELECT id FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA' LIMIT 1)) THEN
|
||||
INSERT INTO amenities (location_id, name, description, category, is_active)
|
||||
SELECT id, 'Mocktail Piña Colada', 'Bebida tropical sin alcohol', 'mocktail', true
|
||||
FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM amenities WHERE name = 'Agua Mineral' AND location_id = (SELECT id FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA' LIMIT 1)) THEN
|
||||
INSERT INTO amenities (location_id, name, description, category, is_active)
|
||||
SELECT id, 'Agua Mineral', 'Agua mineral fresca', 'other', true
|
||||
FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA';
|
||||
END IF;
|
||||
|
||||
-- TEST - Salón Principal Amenities
|
||||
IF NOT EXISTS (SELECT 1 FROM amenities WHERE name = 'Café Espresso' AND location_id = (SELECT id FROM locations WHERE name = 'TEST - Salón Principal' LIMIT 1)) THEN
|
||||
INSERT INTO amenities (location_id, name, description, category, is_active)
|
||||
SELECT id, 'Café Espresso', 'Café espresso intenso', 'coffee', true
|
||||
FROM locations WHERE name = 'TEST - Salón Principal';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM amenities WHERE name = 'Café Cappuccino' AND location_id = (SELECT id FROM locations WHERE name = 'TEST - Salón Principal' LIMIT 1)) THEN
|
||||
INSERT INTO amenities (location_id, name, description, category, is_active)
|
||||
SELECT id, 'Café Cappuccino', 'Café con espuma de leche', 'coffee', true
|
||||
FROM locations WHERE name = 'TEST - Salón Principal';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM amenities WHERE name = 'Té Chai' AND location_id = (SELECT id FROM locations WHERE name = 'TEST - Salón Principal' LIMIT 1)) THEN
|
||||
INSERT INTO amenities (location_id, name, description, category, is_active)
|
||||
SELECT id, 'Té Chai', 'Té chai con especias', 'coffee', true
|
||||
FROM locations WHERE name = 'TEST - Salón Principal';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM amenities WHERE name = 'Cocktail Margarita' AND location_id = (SELECT id FROM locations WHERE name = 'TEST - Salón Principal' LIMIT 1)) THEN
|
||||
INSERT INTO amenities (location_id, name, description, category, is_active)
|
||||
SELECT id, 'Cocktail Margarita', 'Cóctel clásico de tequila', 'cocktail', true
|
||||
FROM locations WHERE name = 'TEST - Salón Principal';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM amenities WHERE name = 'Mocktail Limonada' AND location_id = (SELECT id FROM locations WHERE name = 'TEST - Salón Principal' LIMIT 1)) THEN
|
||||
INSERT INTO amenities (location_id, name, description, category, is_active)
|
||||
SELECT id, 'Mocktail Limonada', 'Limonada fresca natural', 'mocktail', true
|
||||
FROM locations WHERE name = 'TEST - Salón Principal';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM amenities WHERE name = 'Revista de Moda' AND location_id = (SELECT id FROM locations WHERE name = 'TEST - Salón Principal' LIMIT 1)) THEN
|
||||
INSERT INTO amenities (location_id, name, description, category, is_active)
|
||||
SELECT id, 'Revista de Moda', 'Revistas de moda actual', 'other', true
|
||||
FROM locations WHERE name = 'TEST - Salón Principal';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 7. Crear Invitaciones (para clientes Gold)
|
||||
DO $$
|
||||
DECLARE
|
||||
week_start DATE;
|
||||
customer_record RECORD;
|
||||
BEGIN
|
||||
week_start := get_week_start(CURRENT_DATE);
|
||||
|
||||
FOR customer_record IN
|
||||
SELECT id FROM customers WHERE tier = 'gold' AND is_active = true
|
||||
LOOP
|
||||
PERFORM reset_weekly_invitations_for_customer(customer_record.id);
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
-- 8. Resumen de datos creados
|
||||
DO $$
|
||||
BEGIN
|
||||
RAISE NOTICE '==========================================';
|
||||
RAISE NOTICE 'SALONOS - SEED DE DATOS COMPLETADO';
|
||||
RAISE NOTICE '==========================================';
|
||||
RAISE NOTICE 'Locations: %', (SELECT COUNT(*) FROM locations);
|
||||
RAISE NOTICE 'Resources: %', (SELECT COUNT(*) FROM resources);
|
||||
RAISE NOTICE 'Staff: %', (SELECT COUNT(*) FROM staff);
|
||||
RAISE NOTICE 'Services: %', (SELECT COUNT(*) FROM services);
|
||||
RAISE NOTICE 'Customers: %', (SELECT COUNT(*) FROM customers);
|
||||
RAISE NOTICE 'Amenities: %', (SELECT COUNT(*) FROM amenities);
|
||||
RAISE NOTICE '==========================================';
|
||||
RAISE NOTICE 'Base de datos lista para desarrollo';
|
||||
RAISE NOTICE '==========================================';
|
||||
END
|
||||
$$;
|
||||
Reference in New Issue
Block a user