feat(backend): implement v2 domain slice and live smoke
This commit is contained in:
639
backend/command-api/sql/v2/001_v2_domain_foundation.sql
Normal file
639
backend/command-api/sql/v2/001_v2_domain_foundation.sql
Normal file
@@ -0,0 +1,639 @@
|
||||
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);
|
||||
Reference in New Issue
Block a user