CREATE EXTENSION IF NOT EXISTS pgcrypto; CREATE TABLE IF NOT EXISTS tenants ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), slug TEXT NOT NULL UNIQUE, name TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'ACTIVE' CHECK (status IN ('ACTIVE', 'INACTIVE')), metadata JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, email TEXT, display_name TEXT, phone TEXT, status TEXT NOT NULL DEFAULT 'ACTIVE' CHECK (status IN ('ACTIVE', 'INVITED', 'DISABLED')), metadata JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email_unique ON users (LOWER(email)) WHERE email IS NOT NULL; CREATE TABLE IF NOT EXISTS tenant_memberships ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, user_id TEXT REFERENCES users(id) ON DELETE SET NULL, invited_email TEXT, membership_status TEXT NOT NULL DEFAULT 'ACTIVE' CHECK (membership_status IN ('INVITED', 'ACTIVE', 'SUSPENDED', 'REMOVED')), base_role TEXT NOT NULL DEFAULT 'member' CHECK (base_role IN ('admin', 'manager', 'member', 'viewer')), metadata JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT chk_tenant_membership_identity CHECK (user_id IS NOT NULL OR invited_email IS NOT NULL) ); CREATE UNIQUE INDEX IF NOT EXISTS idx_tenant_memberships_tenant_user ON tenant_memberships (tenant_id, user_id) WHERE user_id IS NOT NULL; CREATE UNIQUE INDEX IF NOT EXISTS idx_tenant_memberships_tenant_invited_email ON tenant_memberships (tenant_id, LOWER(invited_email)) WHERE invited_email IS NOT NULL; CREATE TABLE IF NOT EXISTS businesses ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, slug TEXT NOT NULL, business_name TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'ACTIVE' CHECK (status IN ('ACTIVE', 'INACTIVE', 'ARCHIVED')), contact_name TEXT, contact_email TEXT, contact_phone TEXT, metadata JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE UNIQUE INDEX IF NOT EXISTS idx_businesses_tenant_slug ON businesses (tenant_id, slug); CREATE TABLE IF NOT EXISTS business_memberships ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, business_id UUID NOT NULL REFERENCES businesses(id) ON DELETE CASCADE, user_id TEXT REFERENCES users(id) ON DELETE SET NULL, invited_email TEXT, membership_status TEXT NOT NULL DEFAULT 'ACTIVE' CHECK (membership_status IN ('INVITED', 'ACTIVE', 'SUSPENDED', 'REMOVED')), business_role TEXT NOT NULL DEFAULT 'member' CHECK (business_role IN ('owner', 'manager', 'member', 'viewer')), metadata JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT chk_business_membership_identity CHECK (user_id IS NOT NULL OR invited_email IS NOT NULL) ); CREATE UNIQUE INDEX IF NOT EXISTS idx_business_memberships_business_user ON business_memberships (business_id, user_id) WHERE user_id IS NOT NULL; CREATE UNIQUE INDEX IF NOT EXISTS idx_business_memberships_business_invited_email ON business_memberships (business_id, LOWER(invited_email)) WHERE invited_email IS NOT NULL; CREATE TABLE IF NOT EXISTS vendors ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, slug TEXT NOT NULL, company_name TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'ACTIVE' CHECK (status IN ('ACTIVE', 'INACTIVE', 'ARCHIVED')), contact_name TEXT, contact_email TEXT, contact_phone TEXT, metadata JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE UNIQUE INDEX IF NOT EXISTS idx_vendors_tenant_slug ON vendors (tenant_id, slug); CREATE TABLE IF NOT EXISTS vendor_memberships ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, vendor_id UUID NOT NULL REFERENCES vendors(id) ON DELETE CASCADE, user_id TEXT REFERENCES users(id) ON DELETE SET NULL, invited_email TEXT, membership_status TEXT NOT NULL DEFAULT 'ACTIVE' CHECK (membership_status IN ('INVITED', 'ACTIVE', 'SUSPENDED', 'REMOVED')), vendor_role TEXT NOT NULL DEFAULT 'member' CHECK (vendor_role IN ('owner', 'manager', 'member', 'viewer')), metadata JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT chk_vendor_membership_identity CHECK (user_id IS NOT NULL OR invited_email IS NOT NULL) ); CREATE UNIQUE INDEX IF NOT EXISTS idx_vendor_memberships_vendor_user ON vendor_memberships (vendor_id, user_id) WHERE user_id IS NOT NULL; CREATE UNIQUE INDEX IF NOT EXISTS idx_vendor_memberships_vendor_invited_email ON vendor_memberships (vendor_id, LOWER(invited_email)) WHERE invited_email IS NOT NULL; CREATE TABLE IF NOT EXISTS staffs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, user_id TEXT REFERENCES users(id) ON DELETE SET NULL, full_name TEXT NOT NULL, email TEXT, phone TEXT, status TEXT NOT NULL DEFAULT 'ACTIVE' CHECK (status IN ('ACTIVE', 'INVITED', 'INACTIVE', 'BLOCKED')), primary_role TEXT, onboarding_status TEXT NOT NULL DEFAULT 'PENDING' CHECK (onboarding_status IN ('PENDING', 'IN_PROGRESS', 'COMPLETED')), average_rating NUMERIC(3, 2) NOT NULL DEFAULT 0 CHECK (average_rating >= 0 AND average_rating <= 5), rating_count INTEGER NOT NULL DEFAULT 0 CHECK (rating_count >= 0), metadata JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE UNIQUE INDEX IF NOT EXISTS idx_staffs_tenant_user ON staffs (tenant_id, user_id) WHERE user_id IS NOT NULL; CREATE TABLE IF NOT EXISTS workforce ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, vendor_id UUID NOT NULL REFERENCES vendors(id) ON DELETE CASCADE, staff_id UUID NOT NULL REFERENCES staffs(id) ON DELETE CASCADE, workforce_number TEXT NOT NULL, employment_type TEXT NOT NULL CHECK (employment_type IN ('W2', 'W1099', 'TEMP', 'CONTRACT')), status TEXT NOT NULL DEFAULT 'ACTIVE' CHECK (status IN ('ACTIVE', 'INACTIVE', 'SUSPENDED')), metadata JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE UNIQUE INDEX IF NOT EXISTS idx_workforce_vendor_staff ON workforce (vendor_id, staff_id); CREATE UNIQUE INDEX IF NOT EXISTS idx_workforce_number_tenant ON workforce (tenant_id, workforce_number); CREATE TABLE IF NOT EXISTS roles_catalog ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, code TEXT NOT NULL, name TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'ACTIVE' CHECK (status IN ('ACTIVE', 'INACTIVE')), metadata JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE UNIQUE INDEX IF NOT EXISTS idx_roles_catalog_tenant_code ON roles_catalog (tenant_id, code); CREATE TABLE IF NOT EXISTS staff_roles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), staff_id UUID NOT NULL REFERENCES staffs(id) ON DELETE CASCADE, role_id UUID NOT NULL REFERENCES roles_catalog(id) ON DELETE CASCADE, is_primary BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE UNIQUE INDEX IF NOT EXISTS idx_staff_roles_staff_role ON staff_roles (staff_id, role_id); CREATE TABLE IF NOT EXISTS clock_points ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, business_id UUID REFERENCES businesses(id) ON DELETE SET NULL, label TEXT NOT NULL, address TEXT, latitude NUMERIC(9, 6), longitude NUMERIC(9, 6), geofence_radius_meters INTEGER NOT NULL DEFAULT 100 CHECK (geofence_radius_meters > 0), nfc_tag_uid TEXT, status TEXT NOT NULL DEFAULT 'ACTIVE' CHECK (status IN ('ACTIVE', 'INACTIVE')), metadata JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE UNIQUE INDEX IF NOT EXISTS idx_clock_points_tenant_nfc_tag ON clock_points (tenant_id, nfc_tag_uid) WHERE nfc_tag_uid IS NOT NULL; CREATE TABLE IF NOT EXISTS orders ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, business_id UUID NOT NULL REFERENCES businesses(id) ON DELETE RESTRICT, vendor_id UUID REFERENCES vendors(id) ON DELETE SET NULL, order_number TEXT NOT NULL, title TEXT NOT NULL, description TEXT, status TEXT NOT NULL DEFAULT 'DRAFT' CHECK (status IN ('DRAFT', 'OPEN', 'FILLED', 'ACTIVE', 'COMPLETED', 'CANCELLED')), service_type TEXT NOT NULL DEFAULT 'EVENT' CHECK (service_type IN ('EVENT', 'CATERING', 'HOTEL', 'RESTAURANT', 'OTHER')), starts_at TIMESTAMPTZ, ends_at TIMESTAMPTZ, location_name TEXT, location_address TEXT, latitude NUMERIC(9, 6), longitude NUMERIC(9, 6), notes TEXT, created_by_user_id TEXT REFERENCES users(id) ON DELETE SET NULL, metadata JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT chk_orders_time_window CHECK (starts_at IS NULL OR ends_at IS NULL OR starts_at < ends_at) ); CREATE UNIQUE INDEX IF NOT EXISTS idx_orders_tenant_order_number ON orders (tenant_id, order_number); CREATE INDEX IF NOT EXISTS idx_orders_tenant_business_status ON orders (tenant_id, business_id, status, created_at DESC); CREATE TABLE IF NOT EXISTS shifts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE, business_id UUID NOT NULL REFERENCES businesses(id) ON DELETE RESTRICT, vendor_id UUID REFERENCES vendors(id) ON DELETE SET NULL, clock_point_id UUID REFERENCES clock_points(id) ON DELETE SET NULL, shift_code TEXT NOT NULL, title TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'OPEN' CHECK (status IN ('DRAFT', 'OPEN', 'PENDING_CONFIRMATION', 'ASSIGNED', 'ACTIVE', 'COMPLETED', 'CANCELLED')), starts_at TIMESTAMPTZ NOT NULL, ends_at TIMESTAMPTZ NOT NULL, timezone TEXT NOT NULL DEFAULT 'UTC', location_name TEXT, location_address TEXT, latitude NUMERIC(9, 6), longitude NUMERIC(9, 6), geofence_radius_meters INTEGER CHECK (geofence_radius_meters IS NULL OR geofence_radius_meters > 0), required_workers INTEGER NOT NULL DEFAULT 1 CHECK (required_workers > 0), assigned_workers INTEGER NOT NULL DEFAULT 0 CHECK (assigned_workers >= 0), notes TEXT, metadata JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT chk_shifts_time_window CHECK (starts_at < ends_at), CONSTRAINT chk_shifts_assigned_workers CHECK (assigned_workers <= required_workers) ); CREATE UNIQUE INDEX IF NOT EXISTS idx_shifts_order_shift_code ON shifts (order_id, shift_code); CREATE INDEX IF NOT EXISTS idx_shifts_tenant_time ON shifts (tenant_id, starts_at, ends_at); CREATE TABLE IF NOT EXISTS shift_roles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), shift_id UUID NOT NULL REFERENCES shifts(id) ON DELETE CASCADE, role_id UUID REFERENCES roles_catalog(id) ON DELETE SET NULL, role_code TEXT NOT NULL, role_name TEXT NOT NULL, workers_needed INTEGER NOT NULL CHECK (workers_needed > 0), assigned_count INTEGER NOT NULL DEFAULT 0 CHECK (assigned_count >= 0), pay_rate_cents INTEGER NOT NULL DEFAULT 0 CHECK (pay_rate_cents >= 0), bill_rate_cents INTEGER NOT NULL DEFAULT 0 CHECK (bill_rate_cents >= 0), metadata JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT chk_shift_roles_assigned_count CHECK (assigned_count <= workers_needed) ); CREATE UNIQUE INDEX IF NOT EXISTS idx_shift_roles_shift_role_code ON shift_roles (shift_id, role_code); CREATE TABLE IF NOT EXISTS applications ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, shift_id UUID NOT NULL REFERENCES shifts(id) ON DELETE CASCADE, shift_role_id UUID NOT NULL REFERENCES shift_roles(id) ON DELETE CASCADE, staff_id UUID NOT NULL REFERENCES staffs(id) ON DELETE CASCADE, status TEXT NOT NULL DEFAULT 'PENDING' CHECK (status IN ('PENDING', 'CONFIRMED', 'CHECKED_IN', 'LATE', 'NO_SHOW', 'COMPLETED', 'REJECTED', 'CANCELLED')), origin TEXT NOT NULL DEFAULT 'STAFF' CHECK (origin IN ('STAFF', 'BUSINESS', 'VENDOR', 'SYSTEM')), applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), metadata JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE UNIQUE INDEX IF NOT EXISTS idx_applications_shift_role_staff ON applications (shift_role_id, staff_id); CREATE INDEX IF NOT EXISTS idx_applications_staff_status ON applications (staff_id, status, applied_at DESC); CREATE TABLE IF NOT EXISTS assignments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, business_id UUID NOT NULL REFERENCES businesses(id) ON DELETE RESTRICT, vendor_id UUID REFERENCES vendors(id) ON DELETE SET NULL, shift_id UUID NOT NULL REFERENCES shifts(id) ON DELETE CASCADE, shift_role_id UUID NOT NULL REFERENCES shift_roles(id) ON DELETE CASCADE, workforce_id UUID NOT NULL REFERENCES workforce(id) ON DELETE RESTRICT, staff_id UUID NOT NULL REFERENCES staffs(id) ON DELETE RESTRICT, application_id UUID REFERENCES applications(id) ON DELETE SET NULL, status TEXT NOT NULL DEFAULT 'ASSIGNED' CHECK (status IN ('ASSIGNED', 'ACCEPTED', 'CHECKED_IN', 'CHECKED_OUT', 'COMPLETED', 'CANCELLED', 'NO_SHOW')), assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), accepted_at TIMESTAMPTZ, checked_in_at TIMESTAMPTZ, checked_out_at TIMESTAMPTZ, metadata JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE UNIQUE INDEX IF NOT EXISTS idx_assignments_shift_role_workforce ON assignments (shift_role_id, workforce_id); CREATE INDEX IF NOT EXISTS idx_assignments_staff_status ON assignments (staff_id, status, assigned_at DESC); CREATE TABLE IF NOT EXISTS attendance_events ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, assignment_id UUID NOT NULL REFERENCES assignments(id) ON DELETE CASCADE, shift_id UUID NOT NULL REFERENCES shifts(id) ON DELETE CASCADE, staff_id UUID NOT NULL REFERENCES staffs(id) ON DELETE RESTRICT, clock_point_id UUID REFERENCES clock_points(id) ON DELETE SET NULL, event_type TEXT NOT NULL CHECK (event_type IN ('CLOCK_IN', 'CLOCK_OUT', 'MANUAL_ADJUSTMENT')), source_type TEXT NOT NULL CHECK (source_type IN ('NFC', 'GEO', 'QR', 'MANUAL', 'SYSTEM')), source_reference TEXT, nfc_tag_uid TEXT, device_id TEXT, latitude NUMERIC(9, 6), longitude NUMERIC(9, 6), accuracy_meters INTEGER CHECK (accuracy_meters IS NULL OR accuracy_meters >= 0), distance_to_clock_point_meters INTEGER CHECK (distance_to_clock_point_meters IS NULL OR distance_to_clock_point_meters >= 0), within_geofence BOOLEAN, validation_status TEXT NOT NULL DEFAULT 'ACCEPTED' CHECK (validation_status IN ('ACCEPTED', 'FLAGGED', 'REJECTED')), validation_reason TEXT, captured_at TIMESTAMPTZ NOT NULL, raw_payload JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_attendance_events_assignment_time ON attendance_events (assignment_id, captured_at DESC); CREATE INDEX IF NOT EXISTS idx_attendance_events_staff_time ON attendance_events (staff_id, captured_at DESC); CREATE TABLE IF NOT EXISTS attendance_sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, assignment_id UUID NOT NULL UNIQUE REFERENCES assignments(id) ON DELETE CASCADE, staff_id UUID NOT NULL REFERENCES staffs(id) ON DELETE RESTRICT, clock_in_event_id UUID REFERENCES attendance_events(id) ON DELETE SET NULL, clock_out_event_id UUID REFERENCES attendance_events(id) ON DELETE SET NULL, status TEXT NOT NULL DEFAULT 'OPEN' CHECK (status IN ('OPEN', 'CLOSED', 'DISPUTED')), check_in_at TIMESTAMPTZ, check_out_at TIMESTAMPTZ, worked_minutes INTEGER NOT NULL DEFAULT 0 CHECK (worked_minutes >= 0), metadata JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS timesheets ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, assignment_id UUID NOT NULL UNIQUE REFERENCES assignments(id) ON DELETE CASCADE, staff_id UUID NOT NULL REFERENCES staffs(id) ON DELETE RESTRICT, status TEXT NOT NULL DEFAULT 'PENDING' CHECK (status IN ('PENDING', 'SUBMITTED', 'APPROVED', 'REJECTED', 'PAID')), regular_minutes INTEGER NOT NULL DEFAULT 0 CHECK (regular_minutes >= 0), overtime_minutes INTEGER NOT NULL DEFAULT 0 CHECK (overtime_minutes >= 0), break_minutes INTEGER NOT NULL DEFAULT 0 CHECK (break_minutes >= 0), gross_pay_cents BIGINT NOT NULL DEFAULT 0 CHECK (gross_pay_cents >= 0), metadata JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS documents ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, document_type TEXT NOT NULL, name TEXT NOT NULL, required_for_role_code TEXT, metadata JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE UNIQUE INDEX IF NOT EXISTS idx_documents_tenant_type_name ON documents (tenant_id, document_type, name); CREATE TABLE IF NOT EXISTS staff_documents ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, staff_id UUID NOT NULL REFERENCES staffs(id) ON DELETE CASCADE, document_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE, file_uri TEXT, status TEXT NOT NULL DEFAULT 'PENDING' CHECK (status IN ('PENDING', 'VERIFIED', 'REJECTED', 'EXPIRED')), expires_at TIMESTAMPTZ, metadata JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE UNIQUE INDEX IF NOT EXISTS idx_staff_documents_staff_document ON staff_documents (staff_id, document_id); CREATE TABLE IF NOT EXISTS certificates ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, staff_id UUID NOT NULL REFERENCES staffs(id) ON DELETE CASCADE, certificate_type TEXT NOT NULL, certificate_number TEXT, issued_at TIMESTAMPTZ, expires_at TIMESTAMPTZ, status TEXT NOT NULL DEFAULT 'PENDING' CHECK (status IN ('PENDING', 'VERIFIED', 'REJECTED', 'EXPIRED')), metadata JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS verification_jobs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, staff_id UUID REFERENCES staffs(id) ON DELETE SET NULL, document_id UUID REFERENCES documents(id) ON DELETE SET NULL, type TEXT NOT NULL, file_uri TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'PENDING' CHECK (status IN ('PENDING', 'PROCESSING', 'AUTO_PASS', 'AUTO_FAIL', 'NEEDS_REVIEW', 'APPROVED', 'REJECTED', 'ERROR')), idempotency_key TEXT, provider_name TEXT, provider_reference TEXT, confidence NUMERIC(4, 3), reasons JSONB NOT NULL DEFAULT '[]'::jsonb, extracted JSONB NOT NULL DEFAULT '{}'::jsonb, review JSONB NOT NULL DEFAULT '{}'::jsonb, metadata JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE UNIQUE INDEX IF NOT EXISTS idx_verification_jobs_tenant_idempotency ON verification_jobs (tenant_id, idempotency_key) WHERE idempotency_key IS NOT NULL; CREATE TABLE IF NOT EXISTS verification_reviews ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), verification_job_id UUID NOT NULL REFERENCES verification_jobs(id) ON DELETE CASCADE, reviewer_user_id TEXT REFERENCES users(id) ON DELETE SET NULL, decision TEXT NOT NULL CHECK (decision IN ('APPROVED', 'REJECTED')), note TEXT, reason_code TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS verification_events ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), verification_job_id UUID NOT NULL REFERENCES verification_jobs(id) ON DELETE CASCADE, from_status TEXT, to_status TEXT NOT NULL, actor_type TEXT NOT NULL, actor_id TEXT, details JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS accounts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, owner_type TEXT NOT NULL CHECK (owner_type IN ('BUSINESS', 'VENDOR', 'STAFF')), owner_business_id UUID REFERENCES businesses(id) ON DELETE CASCADE, owner_vendor_id UUID REFERENCES vendors(id) ON DELETE CASCADE, owner_staff_id UUID REFERENCES staffs(id) ON DELETE CASCADE, provider_name TEXT NOT NULL, provider_reference TEXT NOT NULL, last4 TEXT, is_primary BOOLEAN NOT NULL DEFAULT FALSE, metadata JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT chk_accounts_single_owner CHECK ( (owner_business_id IS NOT NULL)::INTEGER + (owner_vendor_id IS NOT NULL)::INTEGER + (owner_staff_id IS NOT NULL)::INTEGER = 1 ) ); CREATE UNIQUE INDEX IF NOT EXISTS idx_accounts_owner_primary_business ON accounts (owner_business_id) WHERE owner_business_id IS NOT NULL AND is_primary = TRUE; CREATE UNIQUE INDEX IF NOT EXISTS idx_accounts_owner_primary_vendor ON accounts (owner_vendor_id) WHERE owner_vendor_id IS NOT NULL AND is_primary = TRUE; CREATE UNIQUE INDEX IF NOT EXISTS idx_accounts_owner_primary_staff ON accounts (owner_staff_id) WHERE owner_staff_id IS NOT NULL AND is_primary = TRUE; CREATE TABLE IF NOT EXISTS invoices ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE, business_id UUID NOT NULL REFERENCES businesses(id) ON DELETE RESTRICT, vendor_id UUID REFERENCES vendors(id) ON DELETE SET NULL, invoice_number TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'PENDING' CHECK (status IN ('DRAFT', 'PENDING', 'PENDING_REVIEW', 'APPROVED', 'PAID', 'OVERDUE', 'DISPUTED', 'VOID')), currency_code TEXT NOT NULL DEFAULT 'USD', subtotal_cents BIGINT NOT NULL DEFAULT 0 CHECK (subtotal_cents >= 0), tax_cents BIGINT NOT NULL DEFAULT 0 CHECK (tax_cents >= 0), total_cents BIGINT NOT NULL DEFAULT 0 CHECK (total_cents >= 0), due_at TIMESTAMPTZ, metadata JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE UNIQUE INDEX IF NOT EXISTS idx_invoices_tenant_invoice_number ON invoices (tenant_id, invoice_number); CREATE TABLE IF NOT EXISTS recent_payments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, invoice_id UUID NOT NULL REFERENCES invoices(id) ON DELETE CASCADE, assignment_id UUID REFERENCES assignments(id) ON DELETE SET NULL, staff_id UUID REFERENCES staffs(id) ON DELETE SET NULL, status TEXT NOT NULL DEFAULT 'PENDING' CHECK (status IN ('PENDING', 'PROCESSING', 'PAID', 'FAILED')), amount_cents BIGINT NOT NULL CHECK (amount_cents >= 0), process_date TIMESTAMPTZ, metadata JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS staff_reviews ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, business_id UUID NOT NULL REFERENCES businesses(id) ON DELETE CASCADE, staff_id UUID NOT NULL REFERENCES staffs(id) ON DELETE CASCADE, assignment_id UUID NOT NULL REFERENCES assignments(id) ON DELETE CASCADE, reviewer_user_id TEXT REFERENCES users(id) ON DELETE SET NULL, rating SMALLINT NOT NULL CHECK (rating BETWEEN 1 AND 5), review_text TEXT, tags JSONB NOT NULL DEFAULT '[]'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE UNIQUE INDEX IF NOT EXISTS idx_staff_reviews_business_assignment_staff ON staff_reviews (business_id, assignment_id, staff_id); CREATE INDEX IF NOT EXISTS idx_staff_reviews_staff_created_at ON staff_reviews (staff_id, created_at DESC); CREATE TABLE IF NOT EXISTS staff_favorites ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, business_id UUID NOT NULL REFERENCES businesses(id) ON DELETE CASCADE, staff_id UUID NOT NULL REFERENCES staffs(id) ON DELETE CASCADE, created_by_user_id TEXT REFERENCES users(id) ON DELETE SET NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE UNIQUE INDEX IF NOT EXISTS idx_staff_favorites_business_staff ON staff_favorites (business_id, staff_id); CREATE TABLE IF NOT EXISTS domain_events ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, aggregate_type TEXT NOT NULL, aggregate_id UUID NOT NULL, sequence INTEGER NOT NULL CHECK (sequence > 0), event_type TEXT NOT NULL, actor_user_id TEXT REFERENCES users(id) ON DELETE SET NULL, payload JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE UNIQUE INDEX IF NOT EXISTS idx_domain_events_aggregate_sequence ON domain_events (tenant_id, aggregate_type, aggregate_id, sequence);