-- Dual approval (agency + client) + threaded comments for calendar posts & assets -- Idempotent. Private portal only (no public publish). -- --------------------------------------------------------------------------- -- Calendar posts (normalized; private per tenant) -- --------------------------------------------------------------------------- create table if not exists public.calendar_posts ( id uuid primary key default gen_random_uuid(), tenant_id uuid not null references public.tenants(id) on delete cascade, external_key text, month text, date date not null, time_slot text, platform text not null check (platform in ( 'instagram', 'facebook', 'linkedin', 'tiktok', 'youtube', 'x', 'other' )), format text, pillar text, title text not null, hook text, body text, cta text, hashtags text, objective_metric text, status text not null default 'draft' check (status in ('draft', 'in_review', 'scheduled', 'published', 'archived')), agency_approved_at timestamptz, agency_approved_by uuid references auth.users(id), client_approved_at timestamptz, client_approved_by uuid references auth.users(id), meta jsonb not null default '{}'::jsonb, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique (tenant_id, external_key) ); create index if not exists calendar_posts_tenant_month_idx on public.calendar_posts (tenant_id, month, date); create index if not exists calendar_posts_tenant_platform_idx on public.calendar_posts (tenant_id, platform, date); alter table public.calendar_posts enable row level security; drop policy if exists calendar_posts_tenant_select on public.calendar_posts; drop policy if exists calendar_posts_tenant_write on public.calendar_posts; create policy calendar_posts_tenant_select on public.calendar_posts for select using ( tenant_id::text = coalesce( auth.jwt() -> 'app_metadata' ->> 'tenant_id', auth.jwt() -> 'user_metadata' ->> 'tenant_id' ) ); create policy calendar_posts_tenant_write on public.calendar_posts for all using ( tenant_id::text = coalesce( auth.jwt() -> 'app_metadata' ->> 'tenant_id', auth.jwt() -> 'user_metadata' ->> 'tenant_id' ) ) with check ( tenant_id::text = coalesce( auth.jwt() -> 'app_metadata' ->> 'tenant_id', auth.jwt() -> 'user_metadata' ->> 'tenant_id' ) ); -- --------------------------------------------------------------------------- -- Shared comments (calendar_post | asset) -- --------------------------------------------------------------------------- create table if not exists public.content_comments ( id uuid primary key default gen_random_uuid(), tenant_id uuid not null references public.tenants(id) on delete cascade, resource_type text not null check (resource_type in ('calendar_post', 'asset')), resource_id text not null, body text not null, author_id uuid references auth.users(id), author_email text, author_party text not null check (author_party in ('agency', 'client')), created_at timestamptz not null default now() ); create index if not exists content_comments_resource_idx on public.content_comments (tenant_id, resource_type, resource_id, created_at); alter table public.content_comments enable row level security; drop policy if exists content_comments_tenant_select on public.content_comments; drop policy if exists content_comments_tenant_insert on public.content_comments; create policy content_comments_tenant_select on public.content_comments for select using ( tenant_id::text = coalesce( auth.jwt() -> 'app_metadata' ->> 'tenant_id', auth.jwt() -> 'user_metadata' ->> 'tenant_id' ) ); create policy content_comments_tenant_insert on public.content_comments for insert with check ( tenant_id::text = coalesce( auth.jwt() -> 'app_metadata' ->> 'tenant_id', auth.jwt() -> 'user_metadata' ->> 'tenant_id' ) ); -- --------------------------------------------------------------------------- -- Dual approval events (calendar_post | asset) -- --------------------------------------------------------------------------- create table if not exists public.content_approvals ( id uuid primary key default gen_random_uuid(), tenant_id uuid not null references public.tenants(id) on delete cascade, resource_type text not null check (resource_type in ('calendar_post', 'asset')), resource_id text not null, party text not null check (party in ('agency', 'client')), approved boolean not null default true, note text, actor_id uuid references auth.users(id), actor_email text, created_at timestamptz not null default now(), unique (tenant_id, resource_type, resource_id, party) ); alter table public.content_approvals enable row level security; drop policy if exists content_approvals_tenant_select on public.content_approvals; drop policy if exists content_approvals_tenant_write on public.content_approvals; create policy content_approvals_tenant_select on public.content_approvals for select using ( tenant_id::text = coalesce( auth.jwt() -> 'app_metadata' ->> 'tenant_id', auth.jwt() -> 'user_metadata' ->> 'tenant_id' ) ); create policy content_approvals_tenant_write on public.content_approvals for all using ( tenant_id::text = coalesce( auth.jwt() -> 'app_metadata' ->> 'tenant_id', auth.jwt() -> 'user_metadata' ->> 'tenant_id' ) ) with check ( tenant_id::text = coalesce( auth.jwt() -> 'app_metadata' ->> 'tenant_id', auth.jwt() -> 'user_metadata' ->> 'tenant_id' ) ); -- --------------------------------------------------------------------------- -- Assets: dual validation columns -- --------------------------------------------------------------------------- alter table public.asset_files add column if not exists review_status text not null default 'in_review'; alter table public.asset_files drop constraint if exists asset_files_review_status_check; alter table public.asset_files add constraint asset_files_review_status_check check (review_status in ('draft', 'in_review', 'validated', 'archived')); alter table public.asset_files add column if not exists agency_approved_at timestamptz; alter table public.asset_files add column if not exists agency_approved_by uuid references auth.users(id); alter table public.asset_files add column if not exists client_approved_at timestamptz; alter table public.asset_files add column if not exists client_approved_by uuid references auth.users(id); comment on table public.calendar_posts is 'Private RRSS calendar per tenant. Dual agency/client approval. No public publish.'; comment on table public.content_comments is 'Comments on calendar posts or assets (agency + client).'; comment on table public.content_approvals is 'Dual-check approvals: agency and client must both approve for full OK.';