{"data":[{"name":"universal_app_notifications","version":"20260906005000","rollback":null,"created_by":"canonical-production-release","statements":["create table if not exists public.app_notifications (\n  id uuid primary key default gen_random_uuid(),\n  tenant_id uuid not null references public.tenants(id) on delete cascade,\n  user_id uuid not null references public.profiles(id) on delete cascade,\n  source_app text not null,\n  event_type text not null,\n  title text not null,\n  body text,\n  severity text not null default 'info' check (severity in ('info', 'success', 'warning', 'urgent')),\n  destination_path text,\n  actor_user_id uuid references public.profiles(id) on delete set null,\n  entity_type text,\n  entity_id text,\n  metadata jsonb not null default '{}'::jsonb,\n  dedupe_key text not null,\n  read_at timestamptz,\n  created_at timestamptz not null default now(),\n  updated_at timestamptz not null default now()\n);\n\ncreate unique index if not exists app_notifications_dedupe_idx\n  on public.app_notifications (tenant_id, user_id, source_app, dedupe_key);\ncreate index if not exists app_notifications_user_feed_idx\n  on public.app_notifications (tenant_id, user_id, created_at desc);\ncreate index if not exists app_notifications_unread_idx\n  on public.app_notifications (tenant_id, user_id, created_at desc)\n  where read_at is null;\n\n-- This is deliberately separate from the legacy public.notification_preferences\n-- table, which stores per-event preferences under an older global contract.\ncreate table if not exists public.app_notification_preferences (\n  id uuid primary key default gen_random_uuid(),\n  tenant_id uuid not null references public.tenants(id) on delete cascade,\n  user_id uuid not null references public.profiles(id) on delete cascade,\n  in_app_enabled boolean not null default true,\n  browser_enabled boolean not null default false,\n  email_enabled boolean not null default false,\n  sms_enabled boolean not null default false,\n  source_preferences jsonb not null default '{}'::jsonb,\n  quiet_hours jsonb not null default '{}'::jsonb,\n  created_at timestamptz not null default now(),\n  updated_at timestamptz not null default now(),\n  unique (tenant_id, user_id)\n);\n\nalter table public.app_notifications enable row level security;\nalter table public.app_notification_preferences enable row level security;\n\ndrop policy if exists \"app_notifications_select_own_tenant\" on public.app_notifications;\ncreate policy \"app_notifications_select_own_tenant\"\n  on public.app_notifications for select to authenticated\n  using (\n    user_id = auth.uid()\n    and exists (\n      select 1 from public.user_tenant_memberships m\n      where m.user_id = auth.uid()\n        and m.tenant_id = app_notifications.tenant_id\n        and coalesce(m.status, 'active') = 'active'\n    )\n  );\n\ndrop policy if exists \"app_notifications_insert_self\" on public.app_notifications;\ncreate policy \"app_notifications_insert_self\"\n  on public.app_notifications for insert to authenticated\n  with check (\n    user_id = auth.uid()\n    and exists (\n      select 1 from public.user_tenant_memberships m\n      where m.user_id = auth.uid()\n        and m.tenant_id = app_notifications.tenant_id\n        and coalesce(m.status, 'active') = 'active'\n    )\n  );\n\ndrop policy if exists \"app_notifications_update_self\" on public.app_notifications;\ncreate policy \"app_notifications_update_self\"\n  on public.app_notifications for update to authenticated\n  using (\n    user_id = auth.uid()\n    and exists (\n      select 1 from public.user_tenant_memberships m\n      where m.user_id = auth.uid()\n        and m.tenant_id = app_notifications.tenant_id\n        and coalesce(m.status, 'active') = 'active'\n    )\n  )\n  with check (\n    user_id = auth.uid()\n    and exists (\n      select 1 from public.user_tenant_memberships m\n      where m.user_id = auth.uid()\n        and m.tenant_id = app_notifications.tenant_id\n        and coalesce(m.status, 'active') = 'active'\n    )\n  );\n\ndrop policy if exists \"app_notifications_delete_self\" on public.app_notifications;\ncreate policy \"app_notifications_delete_self\"\n  on public.app_notifications for delete to authenticated\n  using (\n    user_id = auth.uid()\n    and exists (\n      select 1 from public.user_tenant_memberships m\n      where m.user_id = auth.uid()\n        and m.tenant_id = app_notifications.tenant_id\n        and coalesce(m.status, 'active') = 'active'\n    )\n  );\n\ndrop policy if exists \"app_notification_preferences_select_self\" on public.app_notification_preferences;\ncreate policy \"app_notification_preferences_select_self\"\n  on public.app_notification_preferences for select to authenticated\n  using (\n    user_id = auth.uid()\n    and exists (\n      select 1 from public.user_tenant_memberships m\n      where m.user_id = auth.uid()\n        and m.tenant_id = app_notification_preferences.tenant_id\n        and coalesce(m.status, 'active') = 'active'\n    )\n  );\n\ndrop policy if exists \"app_notification_preferences_insert_self\" on public.app_notification_preferences;\ncreate policy \"app_notification_preferences_insert_self\"\n  on public.app_notification_preferences for insert to authenticated\n  with check (\n    user_id = auth.uid()\n    and exists (\n      select 1 from public.user_tenant_memberships m\n      where m.user_id = auth.uid()\n        and m.tenant_id = app_notification_preferences.tenant_id\n        and coalesce(m.status, 'active') = 'active'\n    )\n  );\n\ndrop policy if exists \"app_notification_preferences_update_self\" on public.app_notification_preferences;\ncreate policy \"app_notification_preferences_update_self\"\n  on public.app_notification_preferences for update to authenticated\n  using (\n    user_id = auth.uid()\n    and exists (\n      select 1 from public.user_tenant_memberships m\n      where m.user_id = auth.uid()\n        and m.tenant_id = app_notification_preferences.tenant_id\n        and coalesce(m.status, 'active') = 'active'\n    )\n  )\n  with check (\n    user_id = auth.uid()\n    and exists (\n      select 1 from public.user_tenant_memberships m\n      where m.user_id = auth.uid()\n        and m.tenant_id = app_notification_preferences.tenant_id\n        and coalesce(m.status, 'active') = 'active'\n    )\n  );\n\ngrant select, insert, update, delete on public.app_notifications to authenticated;\ngrant select, insert, update on public.app_notification_preferences to authenticated;\n\nalter table public.app_notifications replica identity full;\ndo $$\nbegin\n  alter publication supabase_realtime add table public.app_notifications;\nexception\n  when duplicate_object then null;\n  when undefined_object then null;\nend $$;\n"],"idempotency_key":"ef25f7d3514758a37cc42e8870f6d79499ce967b:20260906005000"},{"name":"book_me_directly_public_profile_and_provisioning_reconciliation","version":"20260906013000","rollback":null,"created_by":"canonical-production-release","statements":["-- Canonicalize the tenant provisioning repairs applied during Book Me Directly tenant creation\n-- and expose only a narrow, whitelisted public profile for Book Me Directly pages.\n\ncreate or replace function public.ensure_tenant_provisioning_foundation(p_tenant_id uuid,p_owner_user_id uuid default null::uuid,p_owner_role text default 'Tenant Owner'::text)\nreturns jsonb language plpgsql security definer set search_path to 'public' as $function$\ndeclare\n  v_caller uuid := auth.uid(); v_uid uuid := coalesce(p_owner_user_id,auth.uid()); v_tenant public.tenants%rowtype; v_type text; v_membership_id uuid; v_track text; v_authorized boolean := false; v_owner_role text := coalesce(nullif(trim(p_owner_role),''),'Tenant Owner');\nbegin\n  if p_tenant_id is null then raise exception 'Tenant id is required'; end if;\n  select * into v_tenant from public.tenants where id=p_tenant_id; if not found then raise exception 'Tenant not found'; end if;\n  v_authorized := session_user in ('postgres','supabase_admin') or coalesce(auth.role(),'')='service_role' or public.is_system_admin(v_caller) or public.is_admin_or_higher(v_caller,p_tenant_id) or (v_tenant.owner_id=v_caller and coalesce(p_owner_user_id,v_caller)=v_caller);\n  if not v_authorized then raise exception 'Not authorized to provision this tenant' using errcode='42501'; end if;\n  if p_owner_user_id is not null and v_caller is not null and p_owner_user_id<>v_caller and session_user not in ('postgres','supabase_admin') and coalesce(auth.role(),'')<>'service_role' and not public.is_system_admin(v_caller) then raise exception 'Only platform administrators may provision another user as tenant owner' using errcode='42501'; end if;\n  v_type:=public.normalize_declared_business_type(v_tenant.tenant_type); if v_type is null or v_type is distinct from v_tenant.tenant_type then raise exception 'Tenant has unresolved business identity'; end if;\n  if v_uid is null then v_uid:=v_tenant.owner_id; end if; if v_uid is null then raise exception 'Tenant owner is required before provisioning'; end if;\n  update public.tenants set owner_id=coalesce(owner_id,v_uid),updated_at=now() where id=p_tenant_id;\n  insert into public.user_tenant_memberships(user_id,tenant_id,is_owner,status) values(v_uid,p_tenant_id,true,'active') on conflict(user_id,tenant_id) do update set status='active',is_owner=true,last_accessed_at=now() returning id into v_membership_id;\n  if not exists(select 1 from public.user_roles where user_id=v_uid and tenant_id=p_tenant_id) then insert into public.user_roles(user_id,tenant_id,role,assigned_by) values(v_uid,p_tenant_id,v_owner_role,v_caller); end if;\n  v_track:=coalesce(public.academy_track_for_type(v_type),'universal');\n  insert into public.tenant_readiness_checks(tenant_id,check_key,status,detail) values(p_tenant_id,'owner_foundation','ready',jsonb_build_object('owner_id',v_uid,'membership_id',v_membership_id,'academy_track',v_track,'academy_enrollment_mode','existing_course_system')) on conflict(tenant_id,check_key) do update set status='ready',detail=excluded.detail,updated_at=now();\n  return jsonb_build_object('ok',true,'tenant_id',p_tenant_id,'tenant_type',v_type,'owner_id',v_uid,'membership_id',v_membership_id,'academy_track',v_track,'owner_role',v_owner_role);\nend;\n$function$;\n\ncreate or replace function public.get_public_book_me_directly_profile(p_company_slug text)\nreturns jsonb language plpgsql stable security definer set search_path to 'public' as $function$\ndeclare\n  v_tenant public.tenants%rowtype; v_settings jsonb; v_services_source jsonb := '[]'::jsonb; v_services jsonb := '[]'::jsonb; v_enabled boolean := true; v_address text;\nbegin\n  if nullif(trim(p_company_slug),'') is null then return null; end if;\n  select * into v_tenant from public.tenants where lower(company_slug)=lower(trim(p_company_slug)) and coalesce(is_active,true)=true and coalesce(status,'active')='active' limit 1;\n  if not found then return null; end if;\n  v_settings := coalesce(v_tenant.settings,'{}'::jsonb);\n  v_enabled := lower(coalesce(v_settings->>'book_me_directly_enabled',v_settings#>>'{customer_channels,book_me_directly,enabled}','true')) not in ('false','0','off','no');\n  v_services_source := coalesce(v_settings->'services',v_settings#>'{business_operating_blueprint,services}',v_settings#>'{tenant_manifest,services}','[]'::jsonb);\n  if jsonb_typeof(v_services_source)='array' then\n    select coalesce(jsonb_agg(label order by ord),'[]'::jsonb) into v_services from (select ord,case when jsonb_typeof(value)='string' then nullif(value #>> '{}','') else coalesce(nullif(value->>'name',''),nullif(value->>'label',''),nullif(value->>'title','')) end as label from jsonb_array_elements(v_services_source) with ordinality as source(value,ord) where ord<=8) labels where label is not null and label<>'';\n  end if;\n  v_address := coalesce(nullif(v_settings->>'public_address',''),nullif(v_settings->>'address',''),nullif(v_tenant.company_address,''),nullif(concat_ws(', ',nullif(v_tenant.company_city,''),nullif(v_tenant.company_state,''),nullif(v_tenant.company_zip,'')),''));\n  return jsonb_build_object('company_slug',v_tenant.company_slug,'name',coalesce(nullif(v_tenant.company_dba_name,''),nullif(v_tenant.company_legal_name,''),nullif(v_tenant.name,''),v_tenant.company_slug),'logo_url',nullif(v_tenant.company_logo_url,''),'background_url',coalesce(nullif(v_settings->>'public_background_url',''),nullif(v_settings->>'workspace_background_url',''),nullif(v_settings->>'background_url','')),'phone',coalesce(nullif(v_settings->>'public_phone',''),nullif(v_settings->>'phone',''),nullif(v_tenant.company_phone,'')),'website',coalesce(nullif(v_settings->>'website',''),nullif(v_settings->>'website_url',''),nullif(v_tenant.company_website,'')),'booking_url',coalesce(nullif(v_settings->>'booking_url',''),nullif(v_settings->>'public_booking_url',''),nullif(v_settings->>'book_url','')),'address',v_address,'instagram_url',coalesce(nullif(v_settings#>>'{socials,instagram}',''),nullif(v_settings#>>'{social_links,instagram}',''),nullif(v_settings#>>'{social,instagram}',''),nullif(v_settings->>'instagram_url',''),nullif(v_settings->>'instagram','')),'facebook_url',coalesce(nullif(v_settings#>>'{socials,facebook}',''),nullif(v_settings#>>'{social_links,facebook}',''),nullif(v_settings#>>'{social,facebook}',''),nullif(v_settings->>'facebook_url',''),nullif(v_settings->>'facebook','')),'primary_label',coalesce(nullif(v_settings->>'book_me_directly_primary_label',''),nullif(v_settings->>'booking_cta_label',''),'Book or request service'),'services',v_services,'enabled',v_enabled);\nend;\n$function$;\n\nrevoke all on function public.get_public_book_me_directly_profile(text) from public;\ngrant execute on function public.get_public_book_me_directly_profile(text) to anon, authenticated, service_role;\ncomment on function public.get_public_book_me_directly_profile(text) is 'Returns only whitelisted public business identity and booking fields for Book Me Directly pages.';\n"],"idempotency_key":"029b67da98b34a517740a38209c3718837eea336:20260906013000"},{"name":"storage_v3_retire_legacy_bucket_policies","version":"20260906021400","rollback":null,"created_by":"canonical-production-release","statements":["-- Storage Architecture V3: retire policies whose bucket namespaces are removed after verified migration.\n-- Canonical migration-only retry after prior rename release included a deleted path.\ndrop policy if exists \"Tenant members can delete job photos\" on storage.objects;\ndrop policy if exists \"Tenant members can update job photos\" on storage.objects;\ndrop policy if exists \"Tenant members can upload job photos\" on storage.objects;\ndrop policy if exists \"Tenant members can view job photos\" on storage.objects;\ndrop policy if exists \"authenticated users can read demo request logos\" on storage.objects;\ndrop policy if exists \"public can upload demo request logos\" on storage.objects;\ndrop policy if exists \"contact_files_delete_tenant_members\" on storage.objects;\ndrop policy if exists \"contact_files_insert_tenant_members\" on storage.objects;\ndrop policy if exists \"contact_files_select_tenant_members\" on storage.objects;\ndrop policy if exists \"contact_files_update_tenant_members\" on storage.objects;\n"],"idempotency_key":"55a00a7067a14aa1b6851cb7873d806109e6bb4b:20260906021400"},{"name":"fix_proposal_editor_storage_rls_path","version":"20260906033000","rollback":null,"created_by":"canonical-production-release","statements":["-- Align Proposal Editor storage authorization with the canonical tenant app namespace.\n-- Proposal Editor assets live only under apps/proposal-editor/* inside each tenant's private bucket.\n\nDROP POLICY IF EXISTS proposal_editor_tenant_assets_select ON storage.objects;\nDROP POLICY IF EXISTS proposal_editor_tenant_assets_insert ON storage.objects;\nDROP POLICY IF EXISTS proposal_editor_tenant_assets_update ON storage.objects;\nDROP POLICY IF EXISTS proposal_editor_tenant_assets_delete ON storage.objects;\n\nCREATE POLICY proposal_editor_tenant_assets_select\nON storage.objects FOR SELECT TO authenticated\nUSING (\n  name LIKE 'apps/proposal-editor/%'\n  AND (\n    public.is_system_admin(auth.uid())\n    OR EXISTS (\n      SELECT 1\n      FROM public.tenants t\n      JOIN public.security_active_user_tenant_memberships m ON m.tenant_id = t.id\n      WHERE m.user_id = auth.uid()\n        AND coalesce(m.status, 'active') = 'active'\n        AND bucket_id = nullif(btrim(coalesce(t.settings->>'storage_bucket', '')), '')\n    )\n  )\n);\n\nCREATE POLICY proposal_editor_tenant_assets_insert\nON storage.objects FOR INSERT TO authenticated\nWITH CHECK (\n  name LIKE 'apps/proposal-editor/%'\n  AND (\n    public.is_system_admin(auth.uid())\n    OR EXISTS (\n      SELECT 1\n      FROM public.tenants t\n      JOIN public.security_active_user_tenant_memberships m ON m.tenant_id = t.id\n      WHERE m.user_id = auth.uid()\n        AND coalesce(m.status, 'active') = 'active'\n        AND bucket_id = nullif(btrim(coalesce(t.settings->>'storage_bucket', '')), '')\n    )\n  )\n);\n\nCREATE POLICY proposal_editor_tenant_assets_update\nON storage.objects FOR UPDATE TO authenticated\nUSING (\n  name LIKE 'apps/proposal-editor/%'\n  AND (\n    public.is_system_admin(auth.uid())\n    OR EXISTS (\n      SELECT 1\n      FROM public.tenants t\n      JOIN public.security_active_user_tenant_memberships m ON m.tenant_id = t.id\n      WHERE m.user_id = auth.uid()\n        AND coalesce(m.status, 'active') = 'active'\n        AND bucket_id = nullif(btrim(coalesce(t.settings->>'storage_bucket', '')), '')\n    )\n  )\n)\nWITH CHECK (\n  name LIKE 'apps/proposal-editor/%'\n  AND (\n    public.is_system_admin(auth.uid())\n    OR EXISTS (\n      SELECT 1\n      FROM public.tenants t\n      JOIN public.security_active_user_tenant_memberships m ON m.tenant_id = t.id\n      WHERE m.user_id = auth.uid()\n        AND coalesce(m.status, 'active') = 'active'\n        AND bucket_id = nullif(btrim(coalesce(t.settings->>'storage_bucket', '')), '')\n    )\n  )\n);\n\nCREATE POLICY proposal_editor_tenant_assets_delete\nON storage.objects FOR DELETE TO authenticated\nUSING (\n  name LIKE 'apps/proposal-editor/%'\n  AND (\n    public.is_system_admin(auth.uid())\n    OR EXISTS (\n      SELECT 1\n      FROM public.tenants t\n      JOIN public.security_active_user_tenant_memberships m ON m.tenant_id = t.id\n      WHERE m.user_id = auth.uid()\n        AND coalesce(m.status, 'active') = 'active'\n        AND bucket_id = nullif(btrim(coalesce(t.settings->>'storage_bucket', '')), '')\n    )\n  )\n);\n"],"idempotency_key":"e1126a9ed2633789641c643e92ceab9fd789da94:20260906033000"},{"name":"sync_primary_tenant_logo_pointer","version":"20260906222732","rollback":null,"created_by":"nysomenterprises@gmail.com","statements":["create or replace function public.sync_primary_tenant_logo_pointer()\nreturns trigger\nlanguage plpgsql\nsecurity definer\nset search_path = public\nas $$\nbegin\n  if new.collection_key = 'branding'\n     and new.is_primary = true\n     and new.deleted_at is null then\n    update public.tenants\n    set company_logo_url = case\n          when company_logo_url like '%/functions/v1/tenant-media-library?mode=asset&id=%'\n            then regexp_replace(company_logo_url, 'id=[^&]+', 'id=' || new.id::text)\n          else 'https://ymntrajfubicyxwwdxxa.supabase.co/functions/v1/tenant-media-library?mode=asset&id=' || new.id::text\n        end,\n        settings = jsonb_set(\n          jsonb_set(coalesce(settings, '{}'::jsonb), '{media_branding,resolved_logo_asset_id}', to_jsonb(new.id::text), true),\n          '{media_branding,resolved_logo_version}',\n          to_jsonb(new.updated_at::text),\n          true\n        ),\n        updated_at = now()\n    where id = new.tenant_id;\n  end if;\n  return new;\nend;\n$$;\n\ndrop trigger if exists trg_sync_primary_tenant_logo_pointer on public.tenant_media_assets;\ncreate trigger trg_sync_primary_tenant_logo_pointer\nafter insert or update of is_primary, deleted_at, updated_at, storage_key, original_key\non public.tenant_media_assets\nfor each row\nexecute function public.sync_primary_tenant_logo_pointer();"],"idempotency_key":null},{"name":"capability_layer_foundation","version":"20260907015000","rollback":null,"created_by":"canonical-production-release","statements":["create table if not exists public.tenant_capability_providers (\n  id uuid primary key default gen_random_uuid(),\n  tenant_id uuid not null,\n  capability text not null check (capability ~ '^[a-z][a-z0-9_]*\\.[a-z][a-z0-9_]*$'),\n  provider_key text not null check (length(trim(provider_key)) > 0),\n  adapter_key text not null check (length(trim(adapter_key)) > 0),\n  connection_ref text null,\n  enabled boolean not null default true,\n  priority integer not null default 100 check (priority between 0 and 10000),\n  metadata jsonb not null default '{}'::jsonb,\n  created_at timestamptz not null default now(),\n  updated_at timestamptz not null default now(),\n  unique (tenant_id, capability, provider_key)\n);\n\ncreate index if not exists tenant_capability_providers_resolution_idx\n  on public.tenant_capability_providers (tenant_id, capability, enabled, priority, created_at);\n\nalter table public.tenant_capability_providers enable row level security;\n\ncreate table if not exists public.capability_execution_audit (\n  id uuid primary key default gen_random_uuid(),\n  tenant_id uuid not null,\n  capability text not null,\n  provider_key text null,\n  adapter_key text null,\n  status text not null check (status in ('started','succeeded','blocked','failed')),\n  actor_type text not null check (actor_type in ('user','command_center')),\n  actor_id uuid null,\n  error_code text null,\n  created_at timestamptz not null default now()\n);\n\ncreate index if not exists capability_execution_audit_tenant_created_idx\n  on public.capability_execution_audit (tenant_id, created_at desc);\n\nalter table public.capability_execution_audit enable row level security;\n\ncomment on table public.tenant_capability_providers is 'Tenant-scoped vendor-neutral capability bindings. Credentials are intentionally excluded; connection_ref is an opaque pointer to an existing provider connection.';\ncomment on table public.capability_execution_audit is 'Secret-free execution receipts for the KnockLaunch Capability Layer.';\n"],"idempotency_key":"9782c4bca642807f64efaa6625a2758d4f38c2cb:20260907015000"},{"name":"capability_mail_contacts_bindings","version":"20260907021500","rollback":null,"created_by":"canonical-production-release","statements":["-- Bind only proven existing Mail360 and native KnockLaunch Contacts service rails.\n-- Opaque connection references only; no provider credentials or tokens are copied into the registry.\n\nwith chosen_mailboxes as (\n  select distinct on (tenant_org_id)\n    tenant_org_id as tenant_id,\n    id\n  from public.tenant_mailboxes\n  where provider = 'zoho_mail'\n    and status = 'active'\n  order by tenant_org_id, is_default desc nulls last, id\n), mail_capabilities(capability, adapter_key) as (\n  values\n    ('mail.read'::text, 'mail360_read'::text),\n    ('mail.reply'::text, 'mail360_reply'::text)\n)\ninsert into public.tenant_capability_providers\n  (tenant_id, capability, provider_key, adapter_key, connection_ref, enabled, priority, metadata)\nselect\n  mailbox.tenant_id,\n  capability.capability,\n  'zoho_mail',\n  capability.adapter_key,\n  'tenant_mailbox:' || mailbox.id::text,\n  true,\n  100,\n  jsonb_build_object('seeded_from', 'active_zoho_mailbox', 'service', 'mail360')\nfrom chosen_mailboxes mailbox\ncross join mail_capabilities capability\non conflict (tenant_id, capability, provider_key)\ndo update set\n  adapter_key = excluded.adapter_key,\n  connection_ref = excluded.connection_ref,\n  enabled = true,\n  priority = excluded.priority,\n  metadata = excluded.metadata,\n  updated_at = now();\n\nwith active_tenants as (\n  select id as tenant_id\n  from public.tenants\n  where status = 'active'\n    and is_active = true\n), contact_capabilities(capability) as (\n  values\n    ('contacts.find'::text),\n    ('contacts.create'::text),\n    ('contacts.update'::text)\n)\ninsert into public.tenant_capability_providers\n  (tenant_id, capability, provider_key, adapter_key, connection_ref, enabled, priority, metadata)\nselect\n  tenant.tenant_id,\n  capability.capability,\n  'knocklaunch_contacts',\n  'contacts_domain',\n  'tenant:' || tenant.tenant_id::text,\n  true,\n  100,\n  jsonb_build_object('seeded_from', 'active_tenant', 'service', 'contacts')\nfrom active_tenants tenant\ncross join contact_capabilities capability\non conflict (tenant_id, capability, provider_key)\ndo update set\n  adapter_key = excluded.adapter_key,\n  connection_ref = excluded.connection_ref,\n  enabled = true,\n  priority = excluded.priority,\n  metadata = excluded.metadata,\n  updated_at = now();\n"],"idempotency_key":"3b73b2f34881eaff5b2d2a69a722dda640345c7a:20260907021500"},{"name":"capability_delegation_foundation","version":"20260907023000","rollback":null,"created_by":"canonical-production-release","statements":["-- Explicit, tenant-scoped Capability Layer delegation.\n-- No provider secrets are stored here. Delegations are opt-in and leases are single-use.\n\ncreate table if not exists public.tenant_capability_delegations (\n  id uuid primary key default gen_random_uuid(),\n  tenant_id uuid not null,\n  capability text not null check (capability ~ '^[a-z][a-z0-9_]*\\.[a-z][a-z0-9_]*$'),\n  principal_key text not null default 'command_center' check (principal_key = 'command_center'),\n  delegated_user_id uuid not null,\n  granted_by_user_id uuid not null,\n  enabled boolean not null default true,\n  expires_at timestamptz not null,\n  metadata jsonb not null default '{}'::jsonb,\n  created_at timestamptz not null default now(),\n  updated_at timestamptz not null default now(),\n  unique (tenant_id, capability, principal_key)\n);\n\ncreate index if not exists tenant_capability_delegations_lookup_idx\n  on public.tenant_capability_delegations (tenant_id, capability, principal_key, enabled, expires_at);\n\nalter table public.tenant_capability_delegations enable row level security;\n\ncreate table if not exists public.capability_delegation_leases (\n  id uuid primary key default gen_random_uuid(),\n  tenant_id uuid not null,\n  capability text not null check (capability ~ '^[a-z][a-z0-9_]*\\.[a-z][a-z0-9_]*$'),\n  adapter_key text not null check (length(trim(adapter_key)) > 0),\n  principal_key text not null default 'command_center' check (principal_key = 'command_center'),\n  delegated_user_id uuid not null,\n  expires_at timestamptz not null,\n  created_at timestamptz not null default now()\n);\n\ncreate index if not exists capability_delegation_leases_expiry_idx\n  on public.capability_delegation_leases (expires_at);\n\nalter table public.capability_delegation_leases enable row level security;\n\ncreate or replace function public.consume_capability_delegation_lease(\n  _lease_id uuid,\n  _tenant_id uuid,\n  _capability text,\n  _adapter_key text\n)\nreturns uuid\nlanguage plpgsql\nsecurity definer\nset search_path = public, pg_temp\nas $$\ndeclare\n  v_user_id uuid;\nbegin\n  delete from public.capability_delegation_leases\n  where id = _lease_id\n    and tenant_id = _tenant_id\n    and capability = _capability\n    and adapter_key = _adapter_key\n    and principal_key = 'command_center'\n    and expires_at > now()\n  returning delegated_user_id into v_user_id;\n\n  return v_user_id;\nend;\n$$;\n\nrevoke all on function public.consume_capability_delegation_lease(uuid, uuid, text, text) from public;\nrevoke all on function public.consume_capability_delegation_lease(uuid, uuid, text, text) from anon;\nrevoke all on function public.consume_capability_delegation_lease(uuid, uuid, text, text) from authenticated;\ngrant execute on function public.consume_capability_delegation_lease(uuid, uuid, text, text) to service_role;\n\n-- Correct the v0.3 mail capability bindings to the active tenant mail owner.\nupdate public.tenant_capability_providers\nset adapter_key = case capability\n    when 'mail.read' then 'zoho_mail_read'\n    when 'mail.reply' then 'zoho_mail_reply'\n    else adapter_key\n  end,\n  metadata = jsonb_build_object('seeded_from', 'active_zoho_mailbox', 'service', 'zoho-mail'),\n  updated_at = now()\nwhere provider_key = 'zoho_mail'\n  and capability in ('mail.read', 'mail.reply');\n\ncomment on table public.tenant_capability_delegations is 'Explicit tenant-owner grants allowing a named KnockLaunch service principal to execute one capability as the granting user for a bounded period.';\ncomment on table public.capability_delegation_leases is 'Short-lived single-use leases created by capability-router after validating an active tenant capability delegation.';\ncomment on function public.consume_capability_delegation_lease(uuid, uuid, text, text) is 'Atomically consumes one matching, unexpired Capability Layer delegation lease. Executable by service_role only.';\n"],"idempotency_key":"2585172352dc9d8cd092044ae106d02b02bcb674:20260907023000"},{"name":"realtor_transaction_demo_identity","version":"20260907023500","rollback":null,"created_by":"canonical-production-release","statements":["-- Explicitly separate sample REALTOR transactions from production business.\nALTER TABLE public.realtor_transactions\n  ADD COLUMN IF NOT EXISTS is_demo boolean;\n\nUPDATE public.realtor_transactions\nSET is_demo = false\nWHERE is_demo IS NULL;\n\nUPDATE public.realtor_transactions\nSET is_demo = true\nWHERE tenant_id = '90ed5bf4-8abd-4d3a-af7c-024664b5b99d'::uuid\n  AND (\n    coalesce(notes, '') ~* '^\\s*DEMO\\b'\n    OR coalesce(assigned_to, '') ~* '\\bCurri Demo Agent\\b'\n    OR (coalesce(property_address, '') ~* '\\bDemo\\b' AND coalesce(notes, '') ~* '\\bDEMO\\b')\n  );\n\nALTER TABLE public.realtor_transactions\n  ALTER COLUMN is_demo SET DEFAULT false,\n  ALTER COLUMN is_demo SET NOT NULL;\n\nCREATE INDEX IF NOT EXISTS realtor_transactions_tenant_demo_status_idx\n  ON public.realtor_transactions (tenant_id, is_demo, status);\n\nCOMMENT ON COLUMN public.realtor_transactions.is_demo IS\n  'True only for explicit sample/demo transactions. Demo rows must never contribute to live brokerage metrics or production operating claims.';\n"],"idempotency_key":"a93b30bbf5abc2b372d02177b7b814385dd0c8d8:20260907023500"},{"name":"repair_realtor_demo_backfill","version":"20260907024500","rollback":null,"created_by":"canonical-production-release","statements":["-- Repair Curri sample transaction identity after the original PostgreSQL regex\n-- predicate used JavaScript-style word boundaries and matched zero rows.\nUPDATE public.realtor_transactions\nSET is_demo = true\nWHERE tenant_id = '90ed5bf4-8abd-4d3a-af7c-024664b5b99d'::uuid\n  AND (\n    upper(btrim(coalesce(notes, ''))) LIKE 'DEMO%'\n    OR coalesce(assigned_to, '') ILIKE '%Curri Demo Agent%'\n    OR (\n      coalesce(property_address, '') ILIKE '%Demo%'\n      AND coalesce(notes, '') ILIKE '%DEMO%'\n    )\n  );\n\nDO $$\nBEGIN\n  IF EXISTS (\n    SELECT 1\n    FROM public.realtor_transactions\n    WHERE tenant_id = '90ed5bf4-8abd-4d3a-af7c-024664b5b99d'::uuid\n      AND (\n        upper(btrim(coalesce(notes, ''))) LIKE 'DEMO%'\n        OR coalesce(assigned_to, '') ILIKE '%Curri Demo Agent%'\n        OR (\n          coalesce(property_address, '') ILIKE '%Demo%'\n          AND coalesce(notes, '') ILIKE '%DEMO%'\n        )\n      )\n      AND is_demo IS NOT TRUE\n  ) THEN\n    RAISE EXCEPTION 'Curri REALTOR demo transaction backfill incomplete';\n  END IF;\nEND $$;\n"],"idempotency_key":"43d383eaf34e423f29f805ef8ae944887e2f99c3:20260907024500"},{"name":"capability_native_calendar_bindings","version":"20260907031100","rollback":null,"created_by":"canonical-production-release","statements":["-- Native KnockLaunch Calendar capability bindings for active tenants only.\n-- No external provider or credential claim is made; connection_ref is an opaque tenant pointer.\n\nwith active_tenants as (\n  select id as tenant_id\n  from public.tenants\n  where status = 'active'\n    and is_active = true\n), calendar_capabilities(capability) as (\n  values\n    ('calendar.create_event'::text),\n    ('calendar.update_event'::text),\n    ('calendar.delete_event'::text),\n    ('calendar.find_availability'::text)\n)\ninsert into public.tenant_capability_providers\n  (tenant_id, capability, provider_key, adapter_key, connection_ref, enabled, priority, metadata)\nselect\n  tenant.tenant_id,\n  capability.capability,\n  'knocklaunch_calendar',\n  'calendar_domain',\n  'tenant:' || tenant.tenant_id::text,\n  true,\n  100,\n  jsonb_build_object('seeded_from', 'active_tenant', 'service', 'calendar-domain', 'provider_scope', 'native')\nfrom active_tenants tenant\ncross join calendar_capabilities capability\non conflict (tenant_id, capability, provider_key)\ndo update set\n  adapter_key = excluded.adapter_key,\n  connection_ref = excluded.connection_ref,\n  enabled = true,\n  priority = excluded.priority,\n  metadata = excluded.metadata,\n  updated_at = now();\n"],"idempotency_key":"523ff5250d1b5188a007d09990c103da64fc91a1:20260907031100"},{"name":"application_capability_layer","version":"20260907031200","rollback":null,"created_by":"canonical-production-release","statements":["-- KNOCKLAUNCH Application Capability Layer. Application records remain in their existing canonical tables.\n-- Release replay: canonical migration runner filters removed changed migration paths before applying extant migrations.\ncreate table if not exists public.application_catalog (\n  id text primary key,\n  slug text not null unique,\n  label text not null,\n  audience text not null check (audience in ('tenant-user','customer-public')),\n  workspace_surface text,\n  bootstrap_mode text not null check (bootstrap_mode in ('default','vertical','token')),\n  default_installed boolean not null default true,\n  capabilities jsonb not null default '[]'::jsonb,\n  data_owners jsonb not null default '[]'::jsonb,\n  created_at timestamptz not null default now(),\n  updated_at timestamptz not null default now()\n);\n\ncreate table if not exists public.tenant_applications (\n  id uuid primary key default gen_random_uuid(),\n  tenant_id uuid not null references public.tenants(id) on delete cascade,\n  application_id text not null references public.application_catalog(id) on delete cascade,\n  enabled boolean not null default true,\n  installed_at timestamptz not null default now(),\n  settings jsonb not null default '{}'::jsonb,\n  bootstrap_version integer not null default 1,\n  updated_at timestamptz not null default now(),\n  unique (tenant_id, application_id)\n);\ncreate index if not exists tenant_applications_tenant_idx on public.tenant_applications(tenant_id);\ncreate index if not exists tenant_applications_app_idx on public.tenant_applications(application_id);\n\ncreate table if not exists public.application_action_registry (\n  application_id text not null references public.application_catalog(id) on delete cascade,\n  action text not null,\n  implementation_kind text not null default 'declared' check (implementation_kind in ('declared','edge-function','client-service','database')),\n  implementation_ref text,\n  callable boolean not null default false,\n  created_at timestamptz not null default now(),\n  updated_at timestamptz not null default now(),\n  primary key (application_id, action)\n);\n\nalter table public.application_catalog enable row level security;\nalter table public.tenant_applications enable row level security;\nalter table public.application_action_registry enable row level security;\n\ndrop policy if exists application_catalog_authenticated_read on public.application_catalog;\ncreate policy application_catalog_authenticated_read on public.application_catalog for select to authenticated using (true);\ndrop policy if exists application_action_registry_authenticated_read on public.application_action_registry;\ncreate policy application_action_registry_authenticated_read on public.application_action_registry for select to authenticated using (true);\ndrop policy if exists tenant_applications_member_read on public.tenant_applications;\ncreate policy tenant_applications_member_read on public.tenant_applications for select to authenticated using (\n  exists (\n    select 1 from public.user_tenant_access uta\n    where uta.user_id = auth.uid()\n      and uta.tenant_id = tenant_applications.tenant_id\n      and coalesce(uta.membership_status, 'active') = 'active'\n  )\n);\n\ninsert into public.application_catalog (id,slug,label,audience,workspace_surface,bootstrap_mode,default_installed,capabilities,data_owners) values\n('crm','crm','CRM','tenant-user','dashboard','default',true,'[\"read-workspace\",\"manage-pipeline\"]','[\"unified_contacts\",\"pipeline_stages\",\"pipeline_transition_rules\"]'),\n('contacts','contacts','Contacts','tenant-user','contacts','default',true,'[\"list\",\"create\",\"update\",\"import\"]','[\"unified_contacts\",\"contact_notes\",\"contact_files\"]'),\n('communications','communications','Communications','tenant-user','communications','default',true,'[\"read\",\"send-email\",\"send-sms\"]','[\"unified_inbox\",\"email_messages\",\"sms_history\"]'),\n('mail','mail','Mail','tenant-user','communications/email','default',true,'[\"read\",\"compose\",\"send\",\"reply\",\"forward\"]','[\"unified_inbox\",\"email_messages\",\"tenant_mailboxes\",\"mail_webhook_events\"]'),\n('calendar','calendar','Calendar','tenant-user','calendar','default',true,'[\"list-events\",\"create-event\",\"update-event\"]','[\"user_calendar_events\"]'),\n('invoices','invoices','Invoices','tenant-user','invoices','default',true,'[\"list\",\"create\",\"send\",\"void\",\"record-payment\"]','[\"invoices\",\"invoice_templates\"]'),\n('payments','payments','Payments','tenant-user','billing/payments','default',true,'[\"list\",\"collect\",\"refund\",\"reconcile\"]','[\"finance_transactions\",\"finance_accounts\",\"invoices\"]'),\n('proposals','proposals','Proposals','tenant-user','proposals','default',true,'[\"list\",\"create\",\"send\",\"accept\"]','[\"proposals\",\"proposal_versions\",\"proposal_profiles\"]'),\n('documents','documents','Documents','tenant-user','documents','default',true,'[\"list\",\"upload\",\"generate\",\"share\"]','[\"documents\",\"document_versions\",\"document_share_links\"]'),\n('forms','forms','Forms','tenant-user','documents','default',true,'[\"create-template\",\"fill\",\"request-signature\"]','[\"document_templates\",\"document_field_templates\",\"document_requests\"]'),\n('booking','booking','Booking','tenant-user','booking','default',true,'[\"list\",\"create\",\"reschedule\",\"cancel\"]','[\"event_bookings\",\"appointment_settings\"]'),\n('tasks','tasks','Tasks','tenant-user','tasks','default',true,'[\"list\",\"create\",\"complete\",\"assign\"]','[\"tasks\"]'),\n('jobs','jobs','Jobs','tenant-user','jobs','default',true,'[\"list\",\"create\",\"assign\",\"update-status\"]','[\"jobs\",\"job_activity_log\",\"job_time_logs\"]'),\n('reports','reports','Reports','tenant-user','reports','default',true,'[\"list\",\"generate\",\"share\"]','[\"report_definitions\",\"scheduled_reports\"]'),\n('analytics','analytics','Analytics','tenant-user','analytics','default',true,'[\"read-metrics\",\"export\"]','[\"analytics_metrics\",\"analytics_snapshots\"]'),\n('opportunity-radar','opportunity-radar','Opportunity Radar','tenant-user','opportunity-radar','default',true,'[\"scan\",\"review\",\"convert\"]','[\"opportunity_radar_opportunities\",\"opportunity_radar_runs\",\"opportunity_radar_seed_state\"]'),\n('canvassing','canvassing','Canvassing','tenant-user','canvassing','vertical',false,'[\"map\",\"territories\",\"doors\",\"import\"]','[\"door_objects\",\"territories\",\"canvassing_routes\",\"door_visits\"]'),\n('qr-suite','qr-suite','QR Suite','tenant-user','qr-suite','default',true,'[\"create\",\"list\",\"track\"]','[\"qr_codes\",\"qr_campaigns\",\"qr_events\"]'),\n('roof-estimator','roof-estimator','Roof Estimator','tenant-user','roof-estimator/admin','vertical',false,'[\"measure\",\"estimate\",\"approve\",\"generate-proposal\"]','[\"roof_estimates\",\"roof_estimate_versions\",\"roof_price_book\",\"roof_price_book_items\"]'),\n('academy','academy','Academy','tenant-user','academy','default',true,'[\"list-courses\",\"train\",\"roleplay\"]','[\"academy_courses\",\"academy_trainee_progress\"]'),\n('customer-portal','customer-portal','Customer Portal','customer-public',null,'token',true,'[\"view-progress\",\"view-documents\",\"upload-documents\"]','[\"customer_portal_access\",\"portal_links\",\"document_requests\"]')\non conflict (id) do update set\n  slug=excluded.slug,\n  label=excluded.label,\n  audience=excluded.audience,\n  workspace_surface=excluded.workspace_surface,\n  bootstrap_mode=excluded.bootstrap_mode,\n  default_installed=excluded.default_installed,\n  capabilities=excluded.capabilities,\n  data_owners=excluded.data_owners,\n  updated_at=now();\n\ninsert into public.application_action_registry(application_id, action)\nselect c.id, capability.value\nfrom public.application_catalog c\ncross join lateral jsonb_array_elements_text(c.capabilities) capability(value)\non conflict (application_id, action) do nothing;\n\ncreate or replace function public.bootstrap_tenant_applications(target_tenant_id uuid)\nreturns void\nlanguage sql\nsecurity definer\nset search_path = public\nas $$\n  insert into public.tenant_applications(tenant_id, application_id, enabled)\n  select target_tenant_id, id, true\n  from public.application_catalog\n  where audience = 'tenant-user' and default_installed = true\n  on conflict (tenant_id, application_id) do nothing;\n$$;\nrevoke all on function public.bootstrap_tenant_applications(uuid) from public;\ngrant execute on function public.bootstrap_tenant_applications(uuid) to service_role;\n\ncreate or replace function public.bootstrap_tenant_applications_after_insert()\nreturns trigger\nlanguage plpgsql\nsecurity definer\nset search_path = public\nas $$\nbegin\n  perform public.bootstrap_tenant_applications(new.id);\n  return new;\nend;\n$$;\n\ndrop trigger if exists bootstrap_tenant_applications_trigger on public.tenants;\ncreate trigger bootstrap_tenant_applications_trigger\nafter insert on public.tenants\nfor each row execute function public.bootstrap_tenant_applications_after_insert();\n\ndo $$\ndeclare tenant_record record;\nbegin\n  for tenant_record in select id from public.tenants loop\n    perform public.bootstrap_tenant_applications(tenant_record.id);\n  end loop;\nend $$;\n"],"idempotency_key":"e66e015152be9b8e7b0fa89c9888690558e3ce77:20260907031200"},{"name":"repair_owner_membership_role_sync","version":"20260911021500","rollback":null,"created_by":"canonical-production-release","statements":["create or replace function public.sync_membership_to_role()\nreturns trigger\nlanguage plpgsql\nsecurity definer\nset search_path to 'public', 'pg_temp'\nas $function$\ndeclare\n  v_role public.app_role;\nbegin\n  if coalesce(new.status, '') <> 'active' then\n    delete from public.user_roles ur\n    where ur.user_id = new.user_id\n      and ur.tenant_id = new.tenant_id;\n    return new;\n  end if;\n\n  if exists (\n    select 1\n    from public.user_roles ur\n    where ur.user_id = new.user_id\n      and ur.tenant_id = new.tenant_id\n  ) then\n    return new;\n  end if;\n\n  if exists (\n    select 1\n    from public.user_roles ur\n    where ur.user_id = new.user_id\n      and ur.tenant_id is null\n      and ur.role in ('GodMode'::public.app_role, 'PlatformAdmin'::public.app_role)\n  ) then\n    return new;\n  end if;\n\n  v_role := (\n    case\n      when coalesce(new.is_owner, false) then 'Tenant Owner'\n      else 'Rep'\n    end\n  )::public.app_role;\n\n  insert into public.user_roles(user_id, tenant_id, role)\n  values (new.user_id, new.tenant_id, v_role)\n  on conflict (user_id, tenant_id) do nothing;\n\n  return new;\nend;\n$function$;\n\ncreate or replace function public.sync_role_to_membership()\nreturns trigger\nlanguage plpgsql\nsecurity definer\nset search_path to 'public', 'pg_temp'\nas $function$\nbegin\n  if new.tenant_id is null then\n    return new;\n  end if;\n\n  insert into public.user_tenant_memberships (\n    user_id,\n    tenant_id,\n    is_owner,\n    status\n  )\n  values (\n    new.user_id,\n    new.tenant_id,\n    new.role::text in ('Owner', 'Tenant Owner'),\n    'active'\n  )\n  on conflict (user_id, tenant_id) do update\n    set status = 'active',\n        is_owner = excluded.is_owner;\n\n  return new;\nend;\n$function$;\n\ndrop trigger if exists trg_sync_role_to_membership on public.user_roles;\ncreate trigger trg_sync_role_to_membership\nafter insert or update of role, user_id, tenant_id on public.user_roles\nfor each row execute function public.sync_role_to_membership();\n"],"idempotency_key":"fc96d367aa91254549c3672ac57ca171dd115a44:20260911021500"},{"name":"application_api_bindings","version":"20260907040500","rollback":null,"created_by":"canonical-production-release","statements":["-- Bind the Application Capability Layer to existing canonical domain owners.\n-- Unsupported declared actions intentionally remain callable=false.\n\ninsert into public.application_action_registry (application_id, action, implementation_kind, implementation_ref, callable)\nvalues\n  ('crm','read-workspace','database','crm:read-workspace',true),\n  ('contacts','list','edge-function','contacts:list',true),\n  ('contacts','create','edge-function','contacts:create',true),\n  ('contacts','update','edge-function','contacts:update',true),\n  ('communications','send-email','edge-function','capability-router:mail.send',true),\n  ('communications','send-sms','edge-function','capability-router:phone.message',true),\n  ('mail','read','edge-function','capability-router:mail.read',true),\n  ('mail','send','edge-function','capability-router:mail.send',true),\n  ('mail','reply','edge-function','capability-router:mail.reply',true),\n  ('calendar','list-events','database','calendar:list-events',true),\n  ('calendar','create-event','edge-function','calendar-domain:create',true),\n  ('calendar','update-event','edge-function','calendar-domain:update',true),\n  ('invoices','list','edge-function','invoices:list',true),\n  ('invoices','create','edge-function','invoices:create',true),\n  ('invoices','void','edge-function','invoices-manage:void',true),\n  ('payments','list','database','payments:list',true),\n  ('payments','collect','edge-function','capability-router:payments.create_checkout',true),\n  ('proposals','list','edge-function','proposal-engine:list',true),\n  ('proposals','create','edge-function','proposal-engine:create',true),\n  ('proposals','accept','edge-function','proposal-engine:approve',true),\n  ('documents','list','edge-function','documents:list',true),\n  ('documents','generate','edge-function','documents:generate_from_template',true),\n  ('documents','share','edge-function','documents:create_share_link',true),\n  ('forms','create-template','edge-function','documents:create_template',true),\n  ('booking','list','database','booking:list',true),\n  ('booking','create','database','booking:create',true),\n  ('booking','reschedule','database','booking:reschedule',true),\n  ('booking','cancel','database','booking:cancel',true),\n  ('tasks','list','database','tasks:list',true),\n  ('tasks','create','database','tasks:create',true),\n  ('tasks','complete','database','tasks:complete',true),\n  ('tasks','assign','database','tasks:assign',true),\n  ('jobs','list','database','jobs:list',true),\n  ('jobs','create','database','jobs:create',true),\n  ('jobs','assign','database','jobs:assign',true),\n  ('jobs','update-status','database','jobs:update-status',true),\n  ('reports','list','database','reports:list',true),\n  ('analytics','read-metrics','database','analytics:read-metrics',true),\n  ('opportunity-radar','scan','edge-function','opportunity-radar:scan',true),\n  ('canvassing','territories','edge-function','canvassing-territories:list',true),\n  ('canvassing','doors','edge-function','canvassing-doors:list',true),\n  ('qr-suite','create','edge-function','qr-suite:create_code',true),\n  ('qr-suite','list','edge-function','qr-suite:list',true),\n  ('qr-suite','track','edge-function','qr-suite:analytics_overview',true),\n  ('roof-estimator','measure','edge-function','roof-estimator:run',true),\n  ('roof-estimator','estimate','edge-function','roof-estimator:run',true),\n  ('academy','list-courses','database','academy:list-courses',true),\n  ('customer-portal','view-progress','edge-function','customer-portal:view_portal',true),\n  ('customer-portal','view-documents','edge-function','customer-portal:view_portal',true)\non conflict (application_id, action) do update\nset implementation_kind = excluded.implementation_kind,\n    implementation_ref = excluded.implementation_ref,\n    callable = excluded.callable,\n    updated_at = now();\n\ncomment on table public.application_action_registry is 'Canonical KNOCKLAUNCH application action contract. callable=true only when implementation_ref resolves to a live domain Edge Function, delegated capability, or bounded canonical database handler.';\n"],"idempotency_key":"0c3cab6116754520713879576744bd15d3a008e7:20260907040500"},{"name":"application_demo_orchestration","version":"20260907040100","rollback":null,"created_by":"canonical-production-release","statements":["-- KNOCKLAUNCH application domains + demo orchestration.\n-- Booking owns BookMeDirectly.com. Progress owns WhatsMyProgress.com.\n-- Public demo writes remain server-side through self-serve-demo; GodMode receives read-only inspection access.\n\nalter table public.application_catalog add column if not exists external_url text;\n\ninsert into public.application_catalog (\n  id, slug, label, audience, workspace_surface, bootstrap_mode, default_installed,\n  capabilities, data_owners, external_url\n) values (\n  'progress', 'progress', 'Progress', 'customer-public', null, 'token', true,\n  '[\"view-progress\",\"view-documents\",\"upload-documents\"]'::jsonb,\n  '[\"customer_portal_access\",\"portal_links\",\"document_requests\"]'::jsonb,\n  'https://whatsmyprogress.com'\n)\non conflict (id) do update set\n  slug = excluded.slug,\n  label = excluded.label,\n  audience = excluded.audience,\n  workspace_surface = excluded.workspace_surface,\n  bootstrap_mode = excluded.bootstrap_mode,\n  default_installed = excluded.default_installed,\n  capabilities = excluded.capabilities,\n  data_owners = excluded.data_owners,\n  external_url = excluded.external_url,\n  updated_at = now();\n\ninsert into public.application_action_registry(application_id, action, implementation_kind, implementation_ref, callable, created_at, updated_at)\nselect 'progress', action, implementation_kind, implementation_ref, callable, created_at, now()\nfrom public.application_action_registry\nwhere application_id = 'customer-portal'\non conflict (application_id, action) do update set\n  implementation_kind = excluded.implementation_kind,\n  implementation_ref = excluded.implementation_ref,\n  callable = excluded.callable,\n  updated_at = now();\n\ninsert into public.tenant_applications(tenant_id, application_id, enabled, installed_at, settings, bootstrap_version, updated_at)\nselect tenant_id, 'progress', enabled, installed_at, settings, bootstrap_version, now()\nfrom public.tenant_applications\nwhere application_id = 'customer-portal'\non conflict (tenant_id, application_id) do update set\n  enabled = excluded.enabled,\n  settings = excluded.settings,\n  bootstrap_version = greatest(public.tenant_applications.bootstrap_version, excluded.bootstrap_version),\n  updated_at = now();\n\ndelete from public.application_catalog where id = 'customer-portal';\n\nupdate public.application_catalog\nset external_url = 'https://bookmedirectly.com', updated_at = now()\nwhere id = 'booking';\n\ninsert into public.application_action_registry(application_id, action)\nselect c.id, capability.value\nfrom public.application_catalog c\ncross join lateral jsonb_array_elements_text(c.capabilities) capability(value)\nwhere c.id = 'progress'\non conflict (application_id, action) do nothing;\n\ncreate table if not exists public.demo_environments (\n  id uuid primary key default gen_random_uuid(),\n  public_id uuid not null default gen_random_uuid() unique,\n  request_id uuid references public.knocklaunch_demo_requests(id) on delete set null,\n  opportunity_id uuid references public.business_opportunities(id) on delete set null,\n  business_name text not null,\n  demo_mode text not null default 'basic' check (demo_mode in ('basic','applications')),\n  template_key text,\n  status text not null default 'draft' check (status in ('draft','human_review','ready','archived')),\n  configuration jsonb not null default '{}'::jsonb,\n  created_by uuid,\n  expires_at timestamptz,\n  created_at timestamptz not null default now(),\n  updated_at timestamptz not null default now()\n);\ncreate index if not exists demo_environments_status_created_idx on public.demo_environments(status, created_at desc);\ncreate index if not exists demo_environments_request_idx on public.demo_environments(request_id) where request_id is not null;\n\ncreate table if not exists public.demo_environment_applications (\n  environment_id uuid not null references public.demo_environments(id) on delete cascade,\n  application_id text not null references public.application_catalog(id) on delete restrict,\n  enabled boolean not null default true,\n  sort_order integer not null default 0,\n  configuration jsonb not null default '{}'::jsonb,\n  created_at timestamptz not null default now(),\n  updated_at timestamptz not null default now(),\n  primary key (environment_id, application_id)\n);\ncreate index if not exists demo_environment_applications_app_idx on public.demo_environment_applications(application_id, environment_id);\n\ncreate table if not exists public.demo_personas (\n  id uuid primary key default gen_random_uuid(),\n  environment_id uuid not null references public.demo_environments(id) on delete cascade,\n  persona_key text not null,\n  display_name text not null,\n  role_key text not null check (role_key in ('owner','manager','sales','staff','customer')),\n  title text,\n  synthetic_email text,\n  permissions jsonb not null default '{}'::jsonb,\n  is_default boolean not null default false,\n  sort_order integer not null default 0,\n  metadata jsonb not null default '{}'::jsonb,\n  created_at timestamptz not null default now(),\n  updated_at timestamptz not null default now(),\n  unique (environment_id, persona_key)\n);\ncreate unique index if not exists demo_personas_one_default_idx on public.demo_personas(environment_id) where is_default;\n\ncreate table if not exists public.demo_records (\n  id uuid primary key default gen_random_uuid(),\n  environment_id uuid not null references public.demo_environments(id) on delete cascade,\n  application_id text not null references public.application_catalog(id) on delete restrict,\n  persona_id uuid not null references public.demo_personas(id) on delete cascade,\n  record_type text not null,\n  record_key text not null,\n  title text not null,\n  subtitle text,\n  status text,\n  sort_order integer not null default 0,\n  data jsonb not null default '{}'::jsonb,\n  created_at timestamptz not null default now(),\n  updated_at timestamptz not null default now(),\n  unique (environment_id, application_id, persona_id, record_key)\n);\ncreate index if not exists demo_records_lookup_idx on public.demo_records(environment_id, persona_id, application_id, sort_order);\n\ncreate table if not exists public.demo_sessions (\n  id uuid primary key default gen_random_uuid(),\n  environment_id uuid not null references public.demo_environments(id) on delete cascade,\n  persona_id uuid references public.demo_personas(id) on delete set null,\n  session_token uuid not null default gen_random_uuid() unique,\n  source text not null default 'public-demo',\n  started_at timestamptz not null default now(),\n  last_seen_at timestamptz not null default now(),\n  ended_at timestamptz,\n  metadata jsonb not null default '{}'::jsonb\n);\ncreate index if not exists demo_sessions_environment_idx on public.demo_sessions(environment_id, started_at desc);\n\ncreate table if not exists public.demo_activity_log (\n  id uuid primary key default gen_random_uuid(),\n  environment_id uuid not null references public.demo_environments(id) on delete restrict,\n  session_id uuid references public.demo_sessions(id) on delete set null,\n  persona_id uuid references public.demo_personas(id) on delete set null,\n  application_id text references public.application_catalog(id) on delete set null,\n  event_type text not null,\n  action text not null,\n  route text,\n  record_type text,\n  record_key text,\n  severity text not null default 'info' check (severity in ('debug','info','warning','error')),\n  payload jsonb not null default '{}'::jsonb,\n  created_at timestamptz not null default now()\n);\ncreate index if not exists demo_activity_environment_time_idx on public.demo_activity_log(environment_id, created_at desc);\ncreate index if not exists demo_activity_application_time_idx on public.demo_activity_log(application_id, created_at desc) where application_id is not null;\n\ncreate or replace function public.prevent_demo_activity_log_mutation()\nreturns trigger\nlanguage plpgsql\nset search_path = public\nas $$\nbegin\n  raise exception 'demo_activity_log is append-only';\nend;\n$$;\n\ndrop trigger if exists demo_activity_log_immutable on public.demo_activity_log;\ncreate trigger demo_activity_log_immutable\nbefore update or delete on public.demo_activity_log\nfor each row execute function public.prevent_demo_activity_log_mutation();\n\nalter table public.demo_environments enable row level security;\nalter table public.demo_environment_applications enable row level security;\nalter table public.demo_personas enable row level security;\nalter table public.demo_records enable row level security;\nalter table public.demo_sessions enable row level security;\nalter table public.demo_activity_log enable row level security;\n\ndrop policy if exists demo_environments_platform_admin_read on public.demo_environments;\ncreate policy demo_environments_platform_admin_read on public.demo_environments for select to authenticated using (public.is_platform_admin(auth.uid()));\ndrop policy if exists demo_environment_applications_platform_admin_read on public.demo_environment_applications;\ncreate policy demo_environment_applications_platform_admin_read on public.demo_environment_applications for select to authenticated using (public.is_platform_admin(auth.uid()));\ndrop policy if exists demo_personas_platform_admin_read on public.demo_personas;\ncreate policy demo_personas_platform_admin_read on public.demo_personas for select to authenticated using (public.is_platform_admin(auth.uid()));\ndrop policy if exists demo_records_platform_admin_read on public.demo_records;\ncreate policy demo_records_platform_admin_read on public.demo_records for select to authenticated using (public.is_platform_admin(auth.uid()));\ndrop policy if exists demo_sessions_platform_admin_read on public.demo_sessions;\ncreate policy demo_sessions_platform_admin_read on public.demo_sessions for select to authenticated using (public.is_platform_admin(auth.uid()));\ndrop policy if exists demo_activity_log_platform_admin_read on public.demo_activity_log;\ncreate policy demo_activity_log_platform_admin_read on public.demo_activity_log for select to authenticated using (public.is_platform_admin(auth.uid()));\n\nrevoke insert, update, delete on public.demo_environments from anon, authenticated;\nrevoke insert, update, delete on public.demo_environment_applications from anon, authenticated;\nrevoke insert, update, delete on public.demo_personas from anon, authenticated;\nrevoke insert, update, delete on public.demo_records from anon, authenticated;\nrevoke insert, update, delete on public.demo_sessions from anon, authenticated;\nrevoke insert, update, delete on public.demo_activity_log from anon, authenticated;\n"],"idempotency_key":"50c610320cb6cdcd2a4ef649704ad33088dc8c19:20260907040100"},{"name":"progress_application_api_bridge","version":"20260907040400","rollback":null,"created_by":"canonical-production-release","statements":["-- Fresh-bootstrap bridge between the Progress rename (40100) and immutable Application API bindings (40500).\n-- Production already has 40500 recorded, so this is intentionally a no-op there.\ndo $progress_bridge$\nbegin\n  if not exists (\n    select 1\n    from supabase_migrations.schema_migrations\n    where version = '20260907040500'\n  )\n  and exists (select 1 from public.application_catalog where id = 'progress')\n  and not exists (select 1 from public.application_catalog where id = 'customer-portal') then\n    insert into public.application_catalog (\n      id, slug, label, audience, workspace_surface, bootstrap_mode, default_installed,\n      capabilities, data_owners\n    ) values (\n      'customer-portal', 'customer-portal', 'Customer Portal', 'customer-public', null, 'token', false,\n      '[\"view-progress\",\"view-documents\",\"upload-documents\"]'::jsonb,\n      '[\"customer_portal_access\",\"portal_links\",\"document_requests\"]'::jsonb\n    )\n    on conflict (id) do nothing;\n  end if;\nend\n$progress_bridge$;\n"],"idempotency_key":"8d617b744f2deef75612da8c6220d444988c8c28:20260907040400"},{"name":"progress_application_api_finalize","version":"20260907040600","rollback":null,"created_by":"canonical-production-release","statements":["-- Finalize the Progress rename after immutable 40500 has had a valid legacy FK target on fresh bootstraps.\n-- On established production the temporary legacy row does not exist, making this migration data-neutral.\ninsert into public.application_action_registry (\n  application_id, action, implementation_kind, implementation_ref, callable, created_at, updated_at\n)\nselect 'progress', action, implementation_kind, implementation_ref, callable, created_at, now()\nfrom public.application_action_registry\nwhere application_id = 'customer-portal'\non conflict (application_id, action) do update set\n  implementation_kind = excluded.implementation_kind,\n  implementation_ref = excluded.implementation_ref,\n  callable = excluded.callable,\n  updated_at = now();\n\ninsert into public.tenant_applications (\n  tenant_id, application_id, enabled, installed_at, settings, bootstrap_version, updated_at\n)\nselect tenant_id, 'progress', enabled, installed_at, settings, bootstrap_version, now()\nfrom public.tenant_applications\nwhere application_id = 'customer-portal'\non conflict (tenant_id, application_id) do update set\n  enabled = excluded.enabled,\n  settings = excluded.settings,\n  bootstrap_version = greatest(public.tenant_applications.bootstrap_version, excluded.bootstrap_version),\n  updated_at = now();\n\ndelete from public.application_catalog where id = 'customer-portal';\n"],"idempotency_key":"8d617b744f2deef75612da8c6220d444988c8c28:20260907040600"},{"name":"application_capability_gap_closure","version":"20260907051600","rollback":null,"created_by":"canonical-production-release","statements":["-- Close the gap between first-class KNOCKLAUNCH application surfaces and the canonical capability catalog.\n-- Existing domain tables remain authoritative; this migration creates no shadow application data tables.\n-- Vertical bootstrap is driven only by explicit supported tenant types from proven application families.\n\nalter table public.application_catalog\n  add column if not exists supported_tenant_types jsonb not null default '[]'::jsonb;\n\ninsert into public.application_catalog\n  (id,slug,label,audience,workspace_surface,bootstrap_mode,default_installed,capabilities,data_owners,supported_tenant_types)\nvalues\n  ('billing','billing','Billing','tenant-user','billing','default',true,'[\"list-receivables\",\"list-invoices\",\"list-payments\"]','[\"invoices\",\"invoice_templates\",\"finance_transactions\",\"finance_accounts\",\"settlement_invoices\",\"tarping_invoices\"]','[]'),\n  ('field-intelligence','field-intelligence','Field Intelligence','tenant-user','field-intelligence','vertical',false,'[\"list-properties\",\"list-targets\",\"territories\"]','[\"property_intelligence\",\"territories\",\"door_objects\",\"canvassing_routes\",\"door_visits\"]','[\"MITIGATION\",\"ROOFING_COMPANY\",\"PUBLIC_ADJUSTER_FIRM\",\"REALTOR\"]'),\n  ('route-service-operations','route-service-operations','Route Service Operations','tenant-user','lawn-care','vertical',false,'[\"list-visits\",\"list-routes\",\"service-workspace\"]','[\"lawn_service_visits\",\"lawn_service_routes\",\"lawn_service_plans\",\"lawn_service_requests\"]','[\"LAWN_CARE\"]'),\n  ('operational-command','operational-command','Operational Command','tenant-user','dashboard','vertical',false,'[\"read-overview\",\"priority-work\",\"vertical-operations\"]','[\"unified_contacts\",\"jobs\",\"contact_claims\",\"realtor_transactions\",\"analytics_metrics\"]','[\"MITIGATION\",\"ROOFING_COMPANY\",\"PUBLIC_ADJUSTER_FIRM\",\"REALTOR\",\"TIRE_SHOP\"]'),\n  ('claim-operations','claim-operations','Claim Operations','tenant-user','claim-operations','vertical',false,'[\"list-claims\",\"claim-file\",\"representation-packets\"]','[\"contact_claims\",\"claim_deadlines\",\"signed_agreements\",\"claim_negotiation_events\",\"claim_payment_status_history\"]','[\"PUBLIC_ADJUSTER_FIRM\"]'),\n  ('realtor-transaction-operations','realtor-transaction-operations','Realtor Transaction Operations','tenant-user','realtor-operations','vertical',false,'[\"list-transactions\",\"list-properties\",\"transaction-workspace\"]','[\"realtor_transactions\",\"realtor_properties\",\"realtor_property_contacts\",\"realtor_campaigns\"]','[\"REALTOR\"]')\non conflict (id) do update set\n  slug=excluded.slug,\n  label=excluded.label,\n  audience=excluded.audience,\n  workspace_surface=excluded.workspace_surface,\n  bootstrap_mode=excluded.bootstrap_mode,\n  default_installed=excluded.default_installed,\n  capabilities=excluded.capabilities,\n  data_owners=excluded.data_owners,\n  supported_tenant_types=excluded.supported_tenant_types,\n  updated_at=now();\n\ninsert into public.application_action_registry(application_id, action)\nselect c.id, capability.value\nfrom public.application_catalog c\ncross join lateral jsonb_array_elements_text(c.capabilities) capability(value)\nwhere c.id in ('billing','field-intelligence','route-service-operations','operational-command','claim-operations','realtor-transaction-operations')\non conflict (application_id, action) do nothing;\n\ninsert into public.application_action_registry(application_id,action,implementation_kind,implementation_ref,callable)\nvalues\n  ('billing','list-receivables','database','billing:list-receivables',true),\n  ('field-intelligence','list-properties','database','field-intelligence:list-properties',true),\n  ('route-service-operations','list-visits','database','route-service-operations:list-visits',true),\n  ('operational-command','read-overview','database','operational-command:read-overview',true),\n  ('claim-operations','list-claims','database','claim-operations:list-claims',true),\n  ('realtor-transaction-operations','list-transactions','database','realtor-transaction-operations:list-transactions',true)\non conflict (application_id,action) do update set\n  implementation_kind=excluded.implementation_kind,\n  implementation_ref=excluded.implementation_ref,\n  callable=excluded.callable,\n  updated_at=now();\n\ncreate or replace function public.bootstrap_tenant_applications(target_tenant_id uuid)\nreturns void\nlanguage sql\nsecurity definer\nset search_path = public\nas $$\n  insert into public.tenant_applications(tenant_id, application_id, enabled)\n  select target_tenant_id, c.id, true\n  from public.application_catalog c\n  join public.tenants t on t.id = target_tenant_id\n  where c.audience = 'tenant-user'\n    and (\n      c.default_installed = true\n      or (\n        c.bootstrap_mode = 'vertical'\n        and jsonb_array_length(coalesce(c.supported_tenant_types, '[]'::jsonb)) > 0\n        and c.supported_tenant_types ? coalesce(t.tenant_type::text, '')\n      )\n    )\n  on conflict (tenant_id, application_id) do nothing;\n$$;\nrevoke all on function public.bootstrap_tenant_applications(uuid) from public;\ngrant execute on function public.bootstrap_tenant_applications(uuid) to service_role;\n\ndo $$\ndeclare tenant_record record;\nbegin\n  for tenant_record in select id from public.tenants loop\n    perform public.bootstrap_tenant_applications(tenant_record.id);\n  end loop;\nend $$;\n"],"idempotency_key":"572dd11a4cf679b82399bdee34c8b5c4c9d93f49:20260907051600"},{"name":"create_call_center_application","version":"20260907121500","rollback":null,"created_by":"canonical-production-release","statements":["create table if not exists public.call_center_workflows (\n  id uuid primary key default gen_random_uuid(),\n  tenant_id uuid not null references public.tenants(id) on delete cascade,\n  name text not null default 'Guided Call Workflow',\n  active boolean not null default true,\n  is_default boolean not null default true,\n  created_at timestamptz not null default now(),\n  updated_at timestamptz not null default now(),\n  unique (tenant_id, name)\n);\n\ncreate table if not exists public.call_center_stages (\n  id uuid primary key default gen_random_uuid(),\n  tenant_id uuid not null references public.tenants(id) on delete cascade,\n  workflow_id uuid not null references public.call_center_workflows(id) on delete cascade,\n  stage_key text not null,\n  label text not null,\n  prompt text,\n  sort_order integer not null default 0,\n  active boolean not null default true,\n  created_at timestamptz not null default now(),\n  updated_at timestamptz not null default now(),\n  unique (workflow_id, stage_key)\n);\n\ncreate table if not exists public.call_center_dispositions (\n  id uuid primary key default gen_random_uuid(),\n  tenant_id uuid not null references public.tenants(id) on delete cascade,\n  key text not null,\n  label text not null,\n  requires_follow_up boolean not null default false,\n  active boolean not null default true,\n  sort_order integer not null default 0,\n  created_at timestamptz not null default now(),\n  updated_at timestamptz not null default now(),\n  unique (tenant_id, key)\n);\n\ncreate table if not exists public.call_center_queue (\n  id uuid primary key default gen_random_uuid(),\n  tenant_id uuid not null references public.tenants(id) on delete cascade,\n  contact_id uuid not null references public.unified_contacts(id) on delete cascade,\n  assigned_user_id uuid null references auth.users(id) on delete set null,\n  status text not null default 'queued' check (status in ('queued','assigned','in_progress','completed','paused','removed')),\n  priority integer not null default 0,\n  next_action_at timestamptz,\n  last_disposition_key text,\n  source text,\n  created_at timestamptz not null default now(),\n  updated_at timestamptz not null default now(),\n  unique (tenant_id, contact_id)\n);\n\ncreate table if not exists public.call_center_sessions (\n  id uuid primary key default gen_random_uuid(),\n  tenant_id uuid not null references public.tenants(id) on delete cascade,\n  queue_item_id uuid null references public.call_center_queue(id) on delete set null,\n  contact_id uuid null references public.unified_contacts(id) on delete set null,\n  agent_user_id uuid null references auth.users(id) on delete set null,\n  workflow_id uuid null references public.call_center_workflows(id) on delete set null,\n  current_stage_key text,\n  channel text not null default 'phone',\n  direction text not null default 'outbound',\n  status text not null default 'in_progress' check (status in ('queued','ringing','in_progress','completed','failed','cancelled')),\n  disposition_key text,\n  notes text,\n  follow_up_at timestamptz,\n  provider_name text,\n  provider_call_id text,\n  provider_parent_call_id text,\n  recording_url text,\n  transcript_url text,\n  started_at timestamptz not null default now(),\n  answered_at timestamptz,\n  ended_at timestamptz,\n  duration_seconds integer,\n  created_at timestamptz not null default now(),\n  updated_at timestamptz not null default now()\n);\n\ncreate table if not exists public.call_center_notes (\n  id uuid primary key default gen_random_uuid(),\n  tenant_id uuid not null references public.tenants(id) on delete cascade,\n  session_id uuid not null references public.call_center_sessions(id) on delete cascade,\n  contact_id uuid null references public.unified_contacts(id) on delete set null,\n  author_user_id uuid null references auth.users(id) on delete set null,\n  body text not null,\n  created_at timestamptz not null default now()\n);\n\ncreate index if not exists call_center_queue_tenant_status_priority_idx on public.call_center_queue(tenant_id,status,priority desc,created_at);\ncreate index if not exists call_center_queue_assignee_idx on public.call_center_queue(tenant_id,assigned_user_id,status);\ncreate index if not exists call_center_sessions_tenant_started_idx on public.call_center_sessions(tenant_id,started_at desc);\ncreate index if not exists call_center_sessions_contact_idx on public.call_center_sessions(tenant_id,contact_id,started_at desc);\ncreate index if not exists call_center_sessions_provider_call_idx on public.call_center_sessions(provider_name,provider_call_id) where provider_call_id is not null;\ncreate index if not exists call_center_notes_session_idx on public.call_center_notes(tenant_id,session_id,created_at);\n\nalter table public.call_center_workflows enable row level security;\nalter table public.call_center_stages enable row level security;\nalter table public.call_center_dispositions enable row level security;\nalter table public.call_center_queue enable row level security;\nalter table public.call_center_sessions enable row level security;\nalter table public.call_center_notes enable row level security;\n\ncreate or replace function public.call_center_tenant_access(target_tenant uuid)\nreturns boolean language sql stable security definer set search_path=public as $$\n  select exists (\n    select 1 from public.user_tenant_memberships tm\n    where tm.tenant_id = target_tenant\n      and tm.user_id = auth.uid()\n      and coalesce(tm.status,'active') = 'active'\n  );\n$$;\nrevoke all on function public.call_center_tenant_access(uuid) from public;\ngrant execute on function public.call_center_tenant_access(uuid) to authenticated;\n\ncreate policy call_center_workflows_tenant on public.call_center_workflows for all to authenticated using (public.call_center_tenant_access(tenant_id)) with check (public.call_center_tenant_access(tenant_id));\ncreate policy call_center_stages_tenant on public.call_center_stages for all to authenticated using (public.call_center_tenant_access(tenant_id)) with check (public.call_center_tenant_access(tenant_id));\ncreate policy call_center_dispositions_tenant on public.call_center_dispositions for all to authenticated using (public.call_center_tenant_access(tenant_id)) with check (public.call_center_tenant_access(tenant_id));\ncreate policy call_center_queue_tenant on public.call_center_queue for all to authenticated using (public.call_center_tenant_access(tenant_id)) with check (public.call_center_tenant_access(tenant_id));\ncreate policy call_center_sessions_tenant on public.call_center_sessions for all to authenticated using (public.call_center_tenant_access(tenant_id)) with check (public.call_center_tenant_access(tenant_id));\ncreate policy call_center_notes_tenant on public.call_center_notes for all to authenticated using (public.call_center_tenant_access(tenant_id)) with check (public.call_center_tenant_access(tenant_id));\n\ninsert into public.call_center_workflows (tenant_id,name,active,is_default)\nselect t.id,'Guided Call Workflow',true,true from public.tenants t\non conflict (tenant_id,name) do update set active=excluded.active,is_default=excluded.is_default,updated_at=now();\n\ninsert into public.call_center_stages (tenant_id,workflow_id,stage_key,label,prompt,sort_order)\nselect w.tenant_id,w.id,s.stage_key,s.label,s.prompt,s.sort_order\nfrom public.call_center_workflows w\ncross join (values\n ('open','Open','Introduce yourself, confirm who you reached, and earn permission to continue.',10),\n ('discover','Discover','Ask focused questions and listen for the customer''s real need, timing, and constraints.',20),\n ('help','Help','Connect what you learned to the clearest next step your company can actually provide.',30),\n ('commit','Commit','Ask for the next action: appointment, transfer, follow-up, payment, or another concrete commitment.',40),\n ('close','Close','Confirm the details, set expectations, document the outcome, and leave the customer knowing what happens next.',50)\n) as s(stage_key,label,prompt,sort_order)\nwhere w.is_default=true\non conflict (workflow_id,stage_key) do update set label=excluded.label,prompt=excluded.prompt,sort_order=excluded.sort_order,active=true,updated_at=now();\n\ninsert into public.call_center_dispositions (tenant_id,key,label,requires_follow_up,sort_order)\nselect t.id,d.key,d.label,d.requires_follow_up,d.sort_order\nfrom public.tenants t\ncross join (values\n ('appointment_set','Appointment set',false,10),\n ('callback','Callback requested',true,20),\n ('no_answer','No answer',true,30),\n ('voicemail','Left voicemail',true,40),\n ('not_interested','Not interested',false,50),\n ('wrong_number','Wrong number',false,60),\n ('do_not_call','Do not call',false,70)\n) as d(key,label,requires_follow_up,sort_order)\non conflict (tenant_id,key) do update set label=excluded.label,requires_follow_up=excluded.requires_follow_up,sort_order=excluded.sort_order,active=true,updated_at=now();\n\ninsert into public.application_catalog (id,slug,label,audience,workspace_surface,bootstrap_mode,default_installed,capabilities,data_owners)\nvalues ('call-center','call-center','Call Center','tenant-user','call-center','default',true,\n  '[\"guided-calls\",\"manage-queue\",\"call-history\",\"dispositions\",\"follow-up\",\"telephony\"]'::jsonb,\n  '[\"unified_contacts\",\"call_center_workflows\",\"call_center_stages\",\"call_center_queue\",\"call_center_sessions\",\"call_center_dispositions\"]'::jsonb)\non conflict (id) do update set slug=excluded.slug,label=excluded.label,audience=excluded.audience,workspace_surface=excluded.workspace_surface,bootstrap_mode=excluded.bootstrap_mode,default_installed=excluded.default_installed,capabilities=excluded.capabilities,data_owners=excluded.data_owners;\n\ninsert into public.application_action_registry (application_id,action,implementation_kind,implementation_ref,callable)\nvalues\n ('call-center','queue.list','client-service','/call-center',true),\n ('call-center','session.start','client-service','/call-center',true),\n ('call-center','session.complete','client-service','/call-center',true),\n ('call-center','history.list','client-service','/call-center',true),\n ('call-center','follow-up.set','client-service','/call-center',true)\non conflict (application_id,action) do update set implementation_kind=excluded.implementation_kind,implementation_ref=excluded.implementation_ref,callable=excluded.callable;\n\ninsert into public.tenant_applications (tenant_id,application_id,enabled)\nselect t.id,'call-center',true from public.tenants t\non conflict (tenant_id,application_id) do update set enabled=excluded.enabled;\n"],"idempotency_key":"303ce50cee012c4e5d06d175f4fc16b1d6fbe4ec:20260907121500"},{"name":"call_center_future_tenant_bootstrap","version":"20260907133000","rollback":null,"created_by":"canonical-production-release","statements":["create or replace function public.bootstrap_call_center_tenant(target_tenant uuid)\nreturns void\nlanguage plpgsql\nsecurity definer\nset search_path=public\nas $$\ndeclare\n  workflow_uuid uuid;\nbegin\n  insert into public.call_center_workflows (tenant_id,name,active,is_default)\n  values (target_tenant,'Guided Call Workflow',true,true)\n  on conflict (tenant_id,name) do update set active=true,is_default=true,updated_at=now()\n  returning id into workflow_uuid;\n\n  insert into public.call_center_stages (tenant_id,workflow_id,stage_key,label,prompt,sort_order)\n  select target_tenant,workflow_uuid,s.stage_key,s.label,s.prompt,s.sort_order\n  from (values\n    ('open','Open','Introduce yourself, confirm who you reached, and earn permission to continue.',10),\n    ('discover','Discover','Ask focused questions and listen for the customer''s real need, timing, and constraints.',20),\n    ('help','Help','Connect what you learned to the clearest next step your company can actually provide.',30),\n    ('commit','Commit','Ask for the next action: appointment, transfer, follow-up, payment, or another concrete commitment.',40),\n    ('close','Close','Confirm the details, set expectations, document the outcome, and leave the customer knowing what happens next.',50)\n  ) as s(stage_key,label,prompt,sort_order)\n  on conflict (workflow_id,stage_key) do update set label=excluded.label,prompt=excluded.prompt,sort_order=excluded.sort_order,active=true,updated_at=now();\n\n  insert into public.call_center_dispositions (tenant_id,key,label,requires_follow_up,sort_order)\n  select target_tenant,d.key,d.label,d.requires_follow_up,d.sort_order\n  from (values\n    ('appointment_set','Appointment set',false,10),\n    ('callback','Callback requested',true,20),\n    ('no_answer','No answer',true,30),\n    ('voicemail','Left voicemail',true,40),\n    ('not_interested','Not interested',false,50),\n    ('wrong_number','Wrong number',false,60),\n    ('do_not_call','Do not call',false,70)\n  ) as d(key,label,requires_follow_up,sort_order)\n  on conflict (tenant_id,key) do update set label=excluded.label,requires_follow_up=excluded.requires_follow_up,sort_order=excluded.sort_order,active=true,updated_at=now();\n\n  insert into public.tenant_applications (tenant_id,application_id,enabled)\n  values (target_tenant,'call-center',true)\n  on conflict (tenant_id,application_id) do update set enabled=true,updated_at=now();\nend;\n$$;\n\nrevoke all on function public.bootstrap_call_center_tenant(uuid) from public;\n\ncreate or replace function public.bootstrap_call_center_tenant_after_insert()\nreturns trigger\nlanguage plpgsql\nsecurity definer\nset search_path=public\nas $$\nbegin\n  perform public.bootstrap_call_center_tenant(new.id);\n  return new;\nend;\n$$;\n\ndrop trigger if exists bootstrap_call_center_tenant_on_create on public.tenants;\ncreate trigger bootstrap_call_center_tenant_on_create\nafter insert on public.tenants\nfor each row execute function public.bootstrap_call_center_tenant_after_insert();\n\nselect public.bootstrap_call_center_tenant(id) from public.tenants;\n"],"idempotency_key":"303ce50cee012c4e5d06d175f4fc16b1d6fbe4ec:20260907133000"},{"name":"calendar_caldav_provider","version":"20260907190500","rollback":null,"created_by":"canonical-production-release","statements":["alter table public.calendar_connections\n  drop constraint if exists calendar_connections_provider_check;\n\nalter table public.calendar_connections\n  add constraint calendar_connections_provider_check\n  check (provider = any (array[\n    'google_calendar'::text,\n    'microsoft_calendar'::text,\n    'zoho_calendar'::text,\n    'caldav'::text,\n    'ics'::text\n  ]));\n\nupdate public.application_catalog\nset\n  capabilities = '[\"list-events\",\"create-event\",\"update-event\",\"list-connections\",\"authorize-provider\",\"add-ics\",\"add-caldav\",\"sync-connection\",\"revoke-connection\"]'::jsonb,\n  updated_at = now()\nwhere id = 'calendar';\n\ninsert into public.application_action_registry(application_id, action, implementation_kind, implementation_ref, callable)\nvalues ('calendar','add-caldav','edge-function','calendar-provider-connections:add_caldav',true)\non conflict (application_id, action) do update\nset\n  implementation_kind = excluded.implementation_kind,\n  implementation_ref = excluded.implementation_ref,\n  callable = excluded.callable,\n  updated_at = now();\n\nupdate public.tenant_applications\nset\n  bootstrap_version = greatest(coalesce(bootstrap_version, 1), 3),\n  updated_at = now()\nwhere application_id = 'calendar';\n"],"idempotency_key":"6783b70c33c44dc04efc321f081c5cbfff29da52:20260907190500"},{"name":"knocklaunch_call_center_tenant_specific","version":"20260907143000","rollback":null,"created_by":"canonical-production-release","statements":["do $$\ndeclare\n  target_tenant uuid := 'a3ffa31f-0c88-4fdd-b872-051db013ca3e'::uuid;\n  workflow_uuid uuid;\nbegin\n  if not exists (\n    select 1 from public.tenants\n    where id = target_tenant\n      and lower(coalesce(company_dba_name,name,'')) like '%knock%launch%'\n  ) then\n    return;\n  end if;\n\n  select id into workflow_uuid\n  from public.call_center_workflows\n  where tenant_id = target_tenant and is_default = true\n  order by updated_at desc\n  limit 1;\n\n  if workflow_uuid is null then\n    insert into public.call_center_workflows (tenant_id,name,active,is_default)\n    values (target_tenant,'KNOCKLAUNCH Sales & Success Workflow',true,true)\n    returning id into workflow_uuid;\n  else\n    update public.call_center_workflows\n    set name='KNOCKLAUNCH Sales & Success Workflow',active=true,is_default=true,updated_at=now()\n    where id=workflow_uuid;\n  end if;\n\n  update public.call_center_stages set active=false,updated_at=now() where tenant_id=target_tenant and workflow_id=workflow_uuid;\n\n  insert into public.call_center_stages (tenant_id,workflow_id,stage_key,label,prompt,sort_order,active)\n  values\n    (target_tenant,workflow_uuid,'open','Open with context','Introduce KNOCKLAUNCH, confirm who you reached, and frame the call around the business or workflow they want to improve.',10,true),\n    (target_tenant,workflow_uuid,'discover','Find the gap','Qualify the business, current tools, pain points, users, urgency, and the outcome they want from KNOCKLAUNCH.',20,true),\n    (target_tenant,workflow_uuid,'help','Show the fit','Demonstrate or explain the KNOCKLAUNCH applications and capabilities that directly solve the stated gap. Keep the conversation tied to their actual tenant and workflow.',30,true),\n    (target_tenant,workflow_uuid,'commit','Set the next move','Convert interest into a concrete KNOCKLAUNCH next step: live demo, application demo, tenant build, onboarding, proposal, or scheduled follow-up.',40,true),\n    (target_tenant,workflow_uuid,'close','Own the follow-up','Confirm who owns the next action, timing, deliverables, and what KNOCKLAUNCH will send, configure, or build next.',50,true)\n  on conflict (workflow_id,stage_key) do update set label=excluded.label,prompt=excluded.prompt,sort_order=excluded.sort_order,active=true,updated_at=now();\n\n  update public.call_center_dispositions set active=false,updated_at=now() where tenant_id=target_tenant;\n\n  insert into public.call_center_dispositions (tenant_id,key,label,requires_follow_up,active,sort_order)\n  values\n    (target_tenant,'demo_booked','Demo booked',false,true,10),\n    (target_tenant,'tenant_build_requested','Tenant build requested',false,true,20),\n    (target_tenant,'onboarding_next_step','Onboarding next step',true,true,30),\n    (target_tenant,'follow_up','Follow-up scheduled',true,true,40),\n    (target_tenant,'no_answer','No answer',true,true,50),\n    (target_tenant,'not_ready','Not ready yet',true,true,60),\n    (target_tenant,'do_not_contact','Do not contact',false,true,70)\n  on conflict (tenant_id,key) do update set label=excluded.label,requires_follow_up=excluded.requires_follow_up,active=true,sort_order=excluded.sort_order,updated_at=now();\nend\n$$;\n"],"idempotency_key":"a0a05628b654e59101c3e92971303f4c4ab77071:20260907143000"},{"name":"expand_call_center_campaigns_reporting","version":"20260907170000","rollback":null,"created_by":"canonical-production-release","statements":["create table if not exists public.call_center_campaigns (\n  id uuid primary key default gen_random_uuid(),\n  tenant_id uuid not null references public.tenants(id) on delete cascade,\n  name text not null,\n  description text,\n  status text not null default 'active' check (status in ('draft','active','paused','completed','archived')),\n  workflow_id uuid null references public.call_center_workflows(id) on delete set null,\n  default_assigned_user_id uuid null references auth.users(id) on delete set null,\n  default_priority integer not null default 0,\n  starts_at timestamptz,\n  ends_at timestamptz,\n  created_by_user_id uuid null references auth.users(id) on delete set null,\n  created_at timestamptz not null default now(),\n  updated_at timestamptz not null default now(),\n  unique (tenant_id,name)\n);\n\ncreate table if not exists public.call_center_campaign_members (\n  id uuid primary key default gen_random_uuid(),\n  tenant_id uuid not null references public.tenants(id) on delete cascade,\n  campaign_id uuid not null references public.call_center_campaigns(id) on delete cascade,\n  contact_id uuid not null references public.unified_contacts(id) on delete cascade,\n  status text not null default 'ready' check (status in ('ready','queued','in_progress','follow_up','completed','skipped','do_not_call','removed')),\n  import_id uuid,\n  assigned_user_id uuid null references auth.users(id) on delete set null,\n  priority integer not null default 0,\n  last_disposition_key text,\n  last_called_at timestamptz,\n  next_action_at timestamptz,\n  created_at timestamptz not null default now(),\n  updated_at timestamptz not null default now(),\n  unique (campaign_id,contact_id)\n);\n\ncreate table if not exists public.call_center_imports (\n  id uuid primary key default gen_random_uuid(),\n  tenant_id uuid not null references public.tenants(id) on delete cascade,\n  campaign_id uuid null references public.call_center_campaigns(id) on delete set null,\n  file_name text,\n  source_kind text not null default 'csv',\n  mapping jsonb not null default '{}'::jsonb,\n  total_rows integer not null default 0,\n  imported_rows integer not null default 0,\n  duplicate_rows integer not null default 0,\n  invalid_rows integer not null default 0,\n  skipped_rows integer not null default 0,\n  imported_by_user_id uuid null references auth.users(id) on delete set null,\n  created_at timestamptz not null default now()\n);\n\nalter table public.call_center_campaign_members\n  add constraint call_center_campaign_members_import_id_fkey\n  foreign key (import_id) references public.call_center_imports(id) on delete set null;\n\ncreate table if not exists public.call_center_weekly_reports (\n  id uuid primary key default gen_random_uuid(),\n  tenant_id uuid not null references public.tenants(id) on delete cascade,\n  campaign_id uuid null references public.call_center_campaigns(id) on delete cascade,\n  report_scope text not null check (report_scope in ('tenant','campaign')),\n  scope_key text not null,\n  period_start timestamptz not null,\n  period_end timestamptz not null,\n  report_data jsonb not null default '{}'::jsonb,\n  generated_at timestamptz not null default now(),\n  unique (tenant_id,scope_key,period_start,period_end)\n);\n\nalter table public.call_center_queue add column if not exists campaign_id uuid null references public.call_center_campaigns(id) on delete set null;\nalter table public.call_center_sessions add column if not exists campaign_id uuid null references public.call_center_campaigns(id) on delete set null;\n\nalter table public.call_center_queue drop constraint if exists call_center_queue_tenant_id_contact_id_key;\ncreate unique index if not exists call_center_queue_campaign_contact_unique on public.call_center_queue(tenant_id,campaign_id,contact_id) where campaign_id is not null;\ncreate unique index if not exists call_center_queue_general_contact_unique on public.call_center_queue(tenant_id,contact_id) where campaign_id is null;\ncreate index if not exists call_center_campaigns_tenant_status_idx on public.call_center_campaigns(tenant_id,status,updated_at desc);\ncreate index if not exists call_center_campaign_members_campaign_status_idx on public.call_center_campaign_members(tenant_id,campaign_id,status,priority desc);\ncreate index if not exists call_center_campaign_members_contact_idx on public.call_center_campaign_members(tenant_id,contact_id,updated_at desc);\ncreate index if not exists call_center_queue_campaign_idx on public.call_center_queue(tenant_id,campaign_id,status,priority desc);\ncreate index if not exists call_center_sessions_campaign_idx on public.call_center_sessions(tenant_id,campaign_id,started_at desc);\ncreate index if not exists call_center_imports_tenant_idx on public.call_center_imports(tenant_id,created_at desc);\ncreate index if not exists call_center_weekly_reports_tenant_idx on public.call_center_weekly_reports(tenant_id,period_end desc);\n\nalter table public.call_center_campaigns enable row level security;\nalter table public.call_center_campaign_members enable row level security;\nalter table public.call_center_imports enable row level security;\nalter table public.call_center_weekly_reports enable row level security;\n\ncreate policy call_center_campaigns_tenant on public.call_center_campaigns for all to authenticated using (public.call_center_tenant_access(tenant_id)) with check (public.call_center_tenant_access(tenant_id));\ncreate policy call_center_campaign_members_tenant on public.call_center_campaign_members for all to authenticated using (public.call_center_tenant_access(tenant_id)) with check (public.call_center_tenant_access(tenant_id));\ncreate policy call_center_imports_tenant on public.call_center_imports for all to authenticated using (public.call_center_tenant_access(tenant_id)) with check (public.call_center_tenant_access(tenant_id));\ncreate policy call_center_weekly_reports_tenant on public.call_center_weekly_reports for select to authenticated using (public.call_center_tenant_access(tenant_id));\n\ncreate or replace function private.generate_call_center_weekly_reports(p_period_end timestamptz default date_trunc('week',now()))\nreturns integer\nlanguage plpgsql\nsecurity definer\nset search_path=public,private\nas $$\ndeclare\n  t record;\n  c record;\n  p_start timestamptz := p_period_end - interval '7 days';\n  metrics jsonb;\n  generated integer := 0;\nbegin\n  for t in select id from public.tenants loop\n    select jsonb_build_object(\n      'calls_attempted', count(*),\n      'calls_completed', count(*) filter (where s.status='completed'),\n      'answered_count', count(*) filter (where s.answered_at is not null),\n      'completion_rate', case when count(*)>0 then round((count(*) filter (where s.status='completed'))::numeric*100/count(*),1) else null end,\n      'answer_rate', case when count(*)>0 and count(*) filter (where s.answered_at is not null)>0 then round((count(*) filter (where s.answered_at is not null))::numeric*100/count(*),1) else null end,\n      'average_duration_seconds', round(avg(s.duration_seconds) filter (where s.duration_seconds is not null),1),\n      'follow_ups_created', count(*) filter (where s.follow_up_at is not null),\n      'dispositions', coalesce((select jsonb_object_agg(x.disposition_key,x.cnt) from (select coalesce(s2.disposition_key,'unclassified') disposition_key,count(*) cnt from public.call_center_sessions s2 where s2.tenant_id=t.id and s2.started_at>=p_start and s2.started_at<p_period_end group by 1) x),'{}'::jsonb),\n      'agents', coalesce((select jsonb_object_agg(x.agent_key,x.cnt) from (select coalesce(s3.agent_user_id::text,'unassigned') agent_key,count(*) cnt from public.call_center_sessions s3 where s3.tenant_id=t.id and s3.started_at>=p_start and s3.started_at<p_period_end group by 1) x),'{}'::jsonb),\n      'campaigns', coalesce((select jsonb_object_agg(x.campaign_key,x.cnt) from (select coalesce(s4.campaign_id::text,'uncampaigned') campaign_key,count(*) cnt from public.call_center_sessions s4 where s4.tenant_id=t.id and s4.started_at>=p_start and s4.started_at<p_period_end group by 1) x),'{}'::jsonb),\n      'follow_ups_due', (select count(*) from public.call_center_queue q where q.tenant_id=t.id and q.next_action_at>=p_start and q.next_action_at<p_period_end)\n    ) into metrics\n    from public.call_center_sessions s\n    where s.tenant_id=t.id and s.started_at>=p_start and s.started_at<p_period_end;\n\n    insert into public.call_center_weekly_reports(tenant_id,campaign_id,report_scope,scope_key,period_start,period_end,report_data,generated_at)\n    values(t.id,null,'tenant','tenant',p_start,p_period_end,coalesce(metrics,'{}'::jsonb),now())\n    on conflict(tenant_id,scope_key,period_start,period_end) do update set report_data=excluded.report_data,generated_at=now();\n    generated := generated + 1;\n\n    for c in select id,name from public.call_center_campaigns where tenant_id=t.id loop\n      select jsonb_build_object(\n        'campaign_name', c.name,\n        'calls_attempted', count(*),\n        'calls_completed', count(*) filter (where s.status='completed'),\n        'answered_count', count(*) filter (where s.answered_at is not null),\n        'completion_rate', case when count(*)>0 then round((count(*) filter (where s.status='completed'))::numeric*100/count(*),1) else null end,\n        'answer_rate', case when count(*)>0 and count(*) filter (where s.answered_at is not null)>0 then round((count(*) filter (where s.answered_at is not null))::numeric*100/count(*),1) else null end,\n        'average_duration_seconds', round(avg(s.duration_seconds) filter (where s.duration_seconds is not null),1),\n        'follow_ups_created', count(*) filter (where s.follow_up_at is not null),\n        'dispositions', coalesce((select jsonb_object_agg(x.disposition_key,x.cnt) from (select coalesce(s2.disposition_key,'unclassified') disposition_key,count(*) cnt from public.call_center_sessions s2 where s2.tenant_id=t.id and s2.campaign_id=c.id and s2.started_at>=p_start and s2.started_at<p_period_end group by 1) x),'{}'::jsonb),\n        'agents', coalesce((select jsonb_object_agg(x.agent_key,x.cnt) from (select coalesce(s3.agent_user_id::text,'unassigned') agent_key,count(*) cnt from public.call_center_sessions s3 where s3.tenant_id=t.id and s3.campaign_id=c.id and s3.started_at>=p_start and s3.started_at<p_period_end group by 1) x),'{}'::jsonb),\n        'member_count', (select count(*) from public.call_center_campaign_members m where m.tenant_id=t.id and m.campaign_id=c.id),\n        'follow_ups_due', (select count(*) from public.call_center_queue q where q.tenant_id=t.id and q.campaign_id=c.id and q.next_action_at>=p_start and q.next_action_at<p_period_end)\n      ) into metrics\n      from public.call_center_sessions s\n      where s.tenant_id=t.id and s.campaign_id=c.id and s.started_at>=p_start and s.started_at<p_period_end;\n\n      insert into public.call_center_weekly_reports(tenant_id,campaign_id,report_scope,scope_key,period_start,period_end,report_data,generated_at)\n      values(t.id,c.id,'campaign',c.id::text,p_start,p_period_end,coalesce(metrics,'{}'::jsonb),now())\n      on conflict(tenant_id,scope_key,period_start,period_end) do update set report_data=excluded.report_data,generated_at=now();\n      generated := generated + 1;\n    end loop;\n  end loop;\n  return generated;\nend;\n$$;\n\nrevoke all on function private.generate_call_center_weekly_reports(timestamptz) from public,anon,authenticated;\ngrant execute on function private.generate_call_center_weekly_reports(timestamptz) to service_role;\n\nselect private.generate_call_center_weekly_reports(date_trunc('week',now()));\n\ndo $$\nbegin\n  if exists (select 1 from pg_extension where extname='pg_cron') and not exists (select 1 from cron.job where jobname='call-center-weekly-reports') then\n    perform cron.schedule('call-center-weekly-reports','5 0 * * 1','select private.generate_call_center_weekly_reports(date_trunc(''week'',now()));');\n  end if;\nend $$;\n\nupdate public.application_catalog\nset capabilities='[\"guided-calls\",\"manage-queue\",\"call-history\",\"dispositions\",\"follow-up\",\"telephony\",\"campaigns\",\"csv-import\",\"csv-export\",\"weekly-reporting\",\"performance\"]'::jsonb,\n    data_owners='[\"unified_contacts\",\"call_center_workflows\",\"call_center_stages\",\"call_center_queue\",\"call_center_sessions\",\"call_center_dispositions\",\"call_center_campaigns\",\"call_center_campaign_members\",\"call_center_imports\",\"call_center_weekly_reports\"]'::jsonb,\n    updated_at=now()\nwhere id='call-center';\n\ninsert into public.application_action_registry(application_id,action,implementation_kind,implementation_ref,callable)\nvalues\n ('call-center','campaign.list','client-service','/call-center',true),\n ('call-center','campaign.create','client-service','/call-center',true),\n ('call-center','campaign.update','client-service','/call-center',true),\n ('call-center','campaign.import-csv','client-service','/call-center',true),\n ('call-center','campaign.export-csv','client-service','/call-center',true),\n ('call-center','report.weekly.list','client-service','/call-center',true),\n ('call-center','report.weekly.generate','database','private.generate_call_center_weekly_reports',false)\non conflict(application_id,action) do update set implementation_kind=excluded.implementation_kind,implementation_ref=excluded.implementation_ref,callable=excluded.callable;\n"],"idempotency_key":"cc085eadb0dceae5fca79645d7718651200fb13a:20260907170000"},{"name":"advance_call_center_operations","version":"20260907183000","rollback":null,"created_by":"canonical-production-release","statements":["alter table public.call_center_campaigns add column if not exists timezone text not null default 'America/New_York';\nalter table public.call_center_campaigns add column if not exists calling_window_start time not null default '09:00';\nalter table public.call_center_campaigns add column if not exists calling_window_end time not null default '19:00';\nalter table public.call_center_campaigns add column if not exists max_attempts integer not null default 3 check (max_attempts between 1 and 25);\nalter table public.call_center_campaigns add column if not exists retry_delay_minutes integer not null default 1440 check (retry_delay_minutes >= 5);\nalter table public.call_center_campaigns add column if not exists goal_calls integer;\nalter table public.call_center_campaigns add column if not exists goal_completions integer;\nalter table public.call_center_campaigns add column if not exists archived_at timestamptz;\nalter table public.call_center_queue add column if not exists attempt_count integer not null default 0;\nalter table public.call_center_queue add column if not exists last_attempt_at timestamptz;\nalter table public.call_center_campaign_members add column if not exists attempt_count integer not null default 0;\nalter table public.call_center_campaign_members add column if not exists exhausted_at timestamptz;\n\ncreate table if not exists public.call_center_playbooks (\n id uuid primary key default gen_random_uuid(), tenant_id uuid not null references public.tenants(id) on delete cascade, campaign_id uuid not null references public.call_center_campaigns(id) on delete cascade,\n opener text not null default '', discovery text not null default '', objections text not null default '', offer text not null default '', close_text text not null default '', notes text not null default '', updated_by_user_id uuid references auth.users(id) on delete set null,\n created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique(tenant_id,campaign_id)\n);\ncreate table if not exists public.call_center_qa_reviews (\n id uuid primary key default gen_random_uuid(), tenant_id uuid not null references public.tenants(id) on delete cascade, session_id uuid not null references public.call_center_sessions(id) on delete cascade,\n reviewer_user_id uuid references auth.users(id) on delete set null, rating integer not null check (rating between 1 and 5), review_status text not null default 'reviewed' check (review_status in ('reviewed','coaching','resolved')),\n coaching_note text, reviewed_at timestamptz not null default now(), created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique(tenant_id,session_id)\n);\ncreate table if not exists public.call_center_import_mappings (\n id uuid primary key default gen_random_uuid(), tenant_id uuid not null references public.tenants(id) on delete cascade, name text not null, mapping jsonb not null default '{}'::jsonb, created_by_user_id uuid references auth.users(id) on delete set null,\n created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique(tenant_id,name)\n);\ncreate index if not exists call_center_qa_reviews_tenant_status_idx on public.call_center_qa_reviews(tenant_id,review_status,reviewed_at desc);\ncreate index if not exists call_center_playbooks_tenant_campaign_idx on public.call_center_playbooks(tenant_id,campaign_id);\nalter table public.call_center_playbooks enable row level security;\nalter table public.call_center_qa_reviews enable row level security;\nalter table public.call_center_import_mappings enable row level security;\ncreate policy call_center_playbooks_tenant on public.call_center_playbooks for all to authenticated using (public.call_center_tenant_access(tenant_id)) with check (public.call_center_tenant_access(tenant_id));\ncreate policy call_center_qa_reviews_tenant on public.call_center_qa_reviews for all to authenticated using (public.call_center_tenant_access(tenant_id)) with check (public.call_center_tenant_access(tenant_id));\ncreate policy call_center_import_mappings_tenant on public.call_center_import_mappings for all to authenticated using (public.call_center_tenant_access(tenant_id)) with check (public.call_center_tenant_access(tenant_id));\n\nupdate public.application_catalog set capabilities='[\"guided-calls\",\"manage-queue\",\"call-history\",\"dispositions\",\"follow-up\",\"telephony\",\"campaigns\",\"csv-import\",\"csv-export\",\"weekly-reporting\",\"performance\",\"campaign-controls\",\"playbooks\",\"supervisor-queue\",\"qa-coaching\"]'::jsonb,\n data_owners='[\"unified_contacts\",\"call_center_workflows\",\"call_center_stages\",\"call_center_queue\",\"call_center_sessions\",\"call_center_dispositions\",\"call_center_campaigns\",\"call_center_campaign_members\",\"call_center_imports\",\"call_center_weekly_reports\",\"call_center_playbooks\",\"call_center_qa_reviews\",\"call_center_import_mappings\"]'::jsonb where id='call-center';\ninsert into public.application_action_registry(application_id,action,implementation_kind,implementation_ref,callable) values\n ('call-center','campaign.rules.update','client-service','/call-center',true),('call-center','campaign.duplicate','client-service','/call-center',true),('call-center','playbook.update','client-service','/call-center',true),('call-center','queue.bulk-update','client-service','/call-center',true),('call-center','qa.review','client-service','/call-center',true)\non conflict(application_id,action) do update set implementation_kind=excluded.implementation_kind,implementation_ref=excluded.implementation_ref,callable=excluded.callable;\n"],"idempotency_key":"c1ff6c5dad4b6d0fda2ca54e9590bc2b18d57d26:20260907183000"},{"name":"finish_call_center_operating_floor","version":"20260907155300","rollback":null,"created_by":"canonical-production-release","statements":["create table if not exists public.call_center_agents (id uuid primary key default gen_random_uuid(),tenant_id uuid not null references public.tenants(id) on delete cascade,user_id uuid not null references auth.users(id) on delete cascade,preferred_language text not null default 'en' check (preferred_language in ('en','fil')),timezone text not null default 'America/New_York',status text not null default 'active' check (status in ('active','paused')),ready_for_calls boolean not null default false,created_at timestamptz not null default now(),updated_at timestamptz not null default now(),unique(tenant_id,user_id));alter table public.call_center_agents enable row level security;drop policy if exists call_center_agents_tenant_access on public.call_center_agents;create policy call_center_agents_tenant_access on public.call_center_agents for all using(public.call_center_tenant_access(tenant_id)) with check(public.call_center_tenant_access(tenant_id));grant select,insert,update,delete on public.call_center_agents to authenticated;create table if not exists public.call_center_campaign_agents (id uuid primary key default gen_random_uuid(),tenant_id uuid not null references public.tenants(id) on delete cascade,campaign_id uuid not null references public.call_center_campaigns(id) on delete cascade,agent_user_id uuid not null references auth.users(id) on delete cascade,active boolean not null default true,assigned_at timestamptz not null default now(),assigned_by_user_id uuid references auth.users(id) on delete set null,unique(tenant_id,campaign_id,agent_user_id));alter table public.call_center_campaign_agents enable row level security;drop policy if exists call_center_campaign_agents_tenant_access on public.call_center_campaign_agents;create policy call_center_campaign_agents_tenant_access on public.call_center_campaign_agents for all using(public.call_center_tenant_access(tenant_id)) with check(public.call_center_tenant_access(tenant_id));grant select,insert,update,delete on public.call_center_campaign_agents to authenticated;do $$ declare v_tenant uuid;v_workflow uuid;v_campaign uuid;begin select id into v_tenant from public.tenants where status='active' and ((settings->>'workspace_variant')='platform_admin' or upper(coalesce(settings->>'routing_family',''))='PLATFORM') order by created_at asc limit 1;if v_tenant is null then return;end if;select id into v_workflow from public.call_center_workflows where tenant_id=v_tenant and active=true and is_default=true order by updated_at desc limit 1;select id into v_campaign from public.call_center_campaigns where tenant_id=v_tenant and name='KNOCKLAUNCH Demo & Tenant Growth' limit 1;if v_campaign is null then insert into public.call_center_campaigns(tenant_id,name,description,status,workflow_id,default_priority,timezone,calling_window_start,calling_window_end,max_attempts,retry_delay_minutes) values(v_tenant,'KNOCKLAUNCH Demo & Tenant Growth','Outbound demos and onboarding conversations for businesses evaluating KNOCKLAUNCH applications.','active',v_workflow,10,'America/New_York','09:00','19:00',4,1440) returning id into v_campaign;else update public.call_center_campaigns set workflow_id=coalesce(v_workflow,workflow_id),status='active',timezone='America/New_York',calling_window_start='09:00',calling_window_end='19:00',max_attempts=4,retry_delay_minutes=1440,updated_at=now() where id=v_campaign;end if;insert into public.call_center_playbooks(tenant_id,campaign_id,opener,discovery,objections,offer,close_text,notes) values(v_tenant,v_campaign,'Introduce yourself and KNOCKLAUNCH, explain that it is business operating software, and ask permission for a brief conversation about how they currently run the parts of the business that create the most friction.','Ask how they currently handle new leads, scheduling, phone calls, payments, follow-up, and keeping customers informed about progress. Find the one or two operational gaps that actually cost them time or money.','Already have software: acknowledge it and ask what still falls through the cracks. Too busy: offer a short tailored demo at a better time. Price: do not invent pricing; explain that the next step is matching only the applications they need. Send info: agree, then ask what problem the information should focus on so the follow-up is useful.','Offer a tailored KNOCKLAUNCH demonstration built around their actual business and the applications they need rather than forcing a generic CRM or oversized software package.','Ask for one concrete next step: schedule the tailored demo, identify the decision-maker who should attend, or agree on an onboarding follow-up if they are ready.','Keep the conversation useful and low-pressure. Do not invent customer results, savings, integrations, pricing, or capabilities. Demonstrate tenant-specific value and document the agreed next step.') on conflict(tenant_id,campaign_id) do update set opener=excluded.opener,discovery=excluded.discovery,objections=excluded.objections,offer=excluded.offer,close_text=excluded.close_text,notes=excluded.notes,updated_at=now();end $$;\n"],"idempotency_key":"6d5e32ef635175270427838e3adc3e47a5f126fc:20260907155300"},{"name":"calendar_oversight_sources_policy_repair","version":"20260907164730","rollback":null,"created_by":"canonical-production-release","statements":["create table if not exists public.calendar_connections (\n  id uuid primary key default gen_random_uuid(),\n  tenant_id uuid not null references public.tenants(id) on delete cascade,\n  owner_user_id uuid references auth.users(id) on delete cascade,\n  scope text not null check (scope in ('personal','business')),\n  provider text not null check (provider in ('google_calendar','microsoft_calendar','zoho_calendar','ics')),\n  display_name text not null,\n  account_label text,\n  external_calendar_id text,\n  status text not null default 'active' check (status in ('active','needs_attention','disabled')),\n  enabled boolean not null default true,\n  read_only boolean not null default true,\n  last_synced_at timestamptz,\n  last_sync_error text,\n  metadata jsonb not null default '{}'::jsonb,\n  created_by uuid not null references auth.users(id),\n  created_at timestamptz not null default now(),\n  updated_at timestamptz not null default now(),\n  constraint calendar_connections_scope_owner check ((scope='personal' and owner_user_id is not null) or scope='business')\n);\ncreate unique index if not exists calendar_connections_identity_idx on public.calendar_connections(tenant_id,scope,provider,coalesce(owner_user_id,'00000000-0000-0000-0000-000000000000'::uuid),coalesce(external_calendar_id,''));\n\ncreate table if not exists public.calendar_connection_secrets (\n  connection_id uuid primary key references public.calendar_connections(id) on delete cascade,\n  ciphertext text not null,\n  iv text not null,\n  key_version integer not null default 1,\n  updated_at timestamptz not null default now()\n);\n\ncreate table if not exists public.calendar_connection_oauth_states (\n  state_hash text primary key,\n  tenant_id uuid not null references public.tenants(id) on delete cascade,\n  user_id uuid not null references auth.users(id) on delete cascade,\n  provider text not null,\n  scope text not null check(scope in ('personal','business')),\n  return_path text not null,\n  code_verifier text,\n  expires_at timestamptz not null,\n  consumed_at timestamptz,\n  created_at timestamptz not null default now()\n);\n\ncreate table if not exists public.calendar_external_events (\n  id uuid primary key default gen_random_uuid(),\n  connection_id uuid not null references public.calendar_connections(id) on delete cascade,\n  tenant_id uuid not null references public.tenants(id) on delete cascade,\n  owner_user_id uuid references auth.users(id) on delete cascade,\n  scope text not null check(scope in ('personal','business')),\n  provider text not null,\n  external_event_id text not null,\n  calendar_id text,\n  title text not null,\n  description text,\n  location text,\n  start_time timestamptz not null,\n  end_time timestamptz,\n  all_day boolean not null default false,\n  status text,\n  etag text,\n  metadata jsonb not null default '{}'::jsonb,\n  synced_at timestamptz not null default now(),\n  unique(connection_id,external_event_id)\n);\ncreate index if not exists calendar_external_events_tenant_time_idx on public.calendar_external_events(tenant_id,start_time);\ncreate index if not exists calendar_external_events_owner_time_idx on public.calendar_external_events(owner_user_id,start_time) where owner_user_id is not null;\n\nalter table public.calendar_connections enable row level security;\nalter table public.calendar_connection_secrets enable row level security;\nalter table public.calendar_connection_oauth_states enable row level security;\nalter table public.calendar_external_events enable row level security;\n\ndrop policy if exists calendar_connections_select on public.calendar_connections;\ncreate policy calendar_connections_select on public.calendar_connections for select to authenticated using (\n  (scope='personal' and owner_user_id=auth.uid()) or\n  (scope='business' and public.is_tenant_member(auth.uid(),tenant_id))\n);\ndrop policy if exists calendar_connections_update_personal on public.calendar_connections;\ncreate policy calendar_connections_update_personal on public.calendar_connections for update to authenticated using(scope='personal' and owner_user_id=auth.uid()) with check(scope='personal' and owner_user_id=auth.uid());\n\ndrop policy if exists calendar_external_events_select on public.calendar_external_events;\ncreate policy calendar_external_events_select on public.calendar_external_events for select to authenticated using (\n  (scope='personal' and owner_user_id=auth.uid()) or\n  (scope='business' and public.is_tenant_member(auth.uid(),tenant_id))\n);\n\nrevoke all on public.calendar_connection_secrets from anon, authenticated;\nrevoke all on public.calendar_connection_oauth_states from anon, authenticated;\ngrant select,update on public.calendar_connections to authenticated;\ngrant select on public.calendar_external_events to authenticated;\n"],"idempotency_key":"81d5fc41a9fe66714d11ebcb08d1e02a23e2d922:20260907164730"},{"name":"calendar_first_class_application_bootstrap","version":"20260907171500","rollback":null,"created_by":"canonical-production-release","statements":["update public.application_catalog\nset\n  slug = 'calendar',\n  label = 'Calendar',\n  audience = 'tenant-user',\n  workspace_surface = 'calendar',\n  bootstrap_mode = 'default',\n  default_installed = true,\n  capabilities = '[\"list-events\",\"create-event\",\"update-event\",\"list-connections\",\"authorize-provider\",\"add-ics\",\"sync-connection\",\"revoke-connection\"]'::jsonb,\n  data_owners = '[\"user_calendar_events\",\"calendar_connections\",\"calendar_external_events\",\"calendar_connection_secrets\",\"calendar_connection_oauth_states\"]'::jsonb,\n  supported_tenant_types = '[]'::jsonb,\n  updated_at = now()\nwhere id = 'calendar';\n\ninsert into public.application_action_registry(application_id, action, implementation_kind, implementation_ref, callable)\nvalues\n  ('calendar','list-events','database','calendar:list-events',true),\n  ('calendar','create-event','edge-function','calendar-domain:create',true),\n  ('calendar','update-event','edge-function','calendar-domain:update',true),\n  ('calendar','list-connections','edge-function','calendar-provider-connections:list',true),\n  ('calendar','authorize-provider','edge-function','calendar-provider-connections:authorize_url',true),\n  ('calendar','add-ics','edge-function','calendar-provider-connections:add_ics',true),\n  ('calendar','sync-connection','edge-function','calendar-provider-connections:sync',true),\n  ('calendar','revoke-connection','edge-function','calendar-provider-connections:revoke',true)\non conflict (application_id, action) do update\nset\n  implementation_kind = excluded.implementation_kind,\n  implementation_ref = excluded.implementation_ref,\n  callable = excluded.callable,\n  updated_at = now();\n\ninsert into public.tenant_applications(tenant_id, application_id, enabled, settings, bootstrap_version)\nselect t.id, 'calendar', true, '{}'::jsonb, 2\nfrom public.tenants t\non conflict (tenant_id, application_id) do update\nset\n  enabled = true,\n  bootstrap_version = greatest(coalesce(public.tenant_applications.bootstrap_version, 1), 2),\n  updated_at = now();\n"],"idempotency_key":"8f714fabcddfd95e56501a4abe713d3e09424365:20260907171500"},{"name":"first_class_application_routes_bootstrap","version":"20260907170500","rollback":null,"created_by":"canonical-production-release","statements":["-- First-class application routing + bootstrap contract.\n-- Every application_catalog row owns one canonical standalone path and bootstrap identity.\n\nalter table public.application_catalog add column if not exists standalone_path text;\nalter table public.application_catalog add column if not exists login_path text;\nalter table public.application_catalog add column if not exists bootstrap_version integer not null default 1;\nalter table public.application_catalog add column if not exists route_adapter text not null default 'tenant-workspace';\nalter table public.application_catalog add column if not exists config_contract jsonb not null default '{}'::jsonb;\n\ninsert into public.application_catalog (\n  id, slug, label, audience, workspace_surface, bootstrap_mode, default_installed,\n  capabilities, data_owners, external_url, supported_tenant_types,\n  standalone_path, login_path, bootstrap_version, route_adapter, config_contract\n) values (\n  'proposal-editor', 'proposal-editor', 'Proposal Editor', 'tenant-user', null, 'vertical', false,\n  '[\"design\",\"create\",\"manage\",\"render\",\"approve\",\"tenant-presentation\"]'::jsonb,\n  '[\"proposal_profiles\",\"proposals\",\"proposal_versions\",\"proposal_sequences\"]'::jsonb,\n  null,\n  '[\"MOBILE_VENDOR\",\"REALTOR\",\"ROOFING_COMPANY\",\"PUBLIC_ADJUSTER_FIRM\",\"MITIGATION\",\"LAWN_CARE\",\"MISC\"]'::jsonb,\n  '/proposal-editor', '/proposal-editor/login', 1, 'proposal-editor',\n  '{\"profileOwnership\":\"tenant\",\"flagshipProfile\":\"big_papis_editorial_v2\"}'::jsonb\n)\non conflict (id) do update set\n  slug = excluded.slug,\n  label = excluded.label,\n  audience = excluded.audience,\n  bootstrap_mode = excluded.bootstrap_mode,\n  default_installed = excluded.default_installed,\n  capabilities = excluded.capabilities,\n  data_owners = excluded.data_owners,\n  supported_tenant_types = excluded.supported_tenant_types,\n  standalone_path = excluded.standalone_path,\n  login_path = excluded.login_path,\n  bootstrap_version = greatest(public.application_catalog.bootstrap_version, excluded.bootstrap_version),\n  route_adapter = excluded.route_adapter,\n  config_contract = coalesce(public.application_catalog.config_contract, '{}'::jsonb) || excluded.config_contract,\n  updated_at = now();\n\nupdate public.application_catalog\nset\n  standalone_path = case when id = 'crm' then '/knock-launch-crm' else '/' || slug end,\n  login_path = case when audience = 'tenant-user' then case when id = 'crm' then '/crm/login' else '/' || slug || '/login' end else null end,\n  bootstrap_version = greatest(coalesce(bootstrap_version, 1), 1),\n  route_adapter = case\n    when id = 'crm' then 'platform-workspace'\n    when id = 'call-center' then 'call-center'\n    when id = 'proposal-editor' then 'proposal-editor'\n    when audience = 'customer-public' then 'external-public'\n    else 'tenant-workspace'\n  end,\n  config_contract = coalesce(config_contract, '{}'::jsonb) || jsonb_build_object(\n    'schema', 'application-bootstrap.v1',\n    'applicationId', id,\n    'applicationSlug', slug\n  ),\n  updated_at = now();\n\nalter table public.application_catalog alter column standalone_path set not null;\nalter table public.application_catalog alter column bootstrap_version set not null;\nalter table public.application_catalog alter column route_adapter set not null;\n\ncreate unique index if not exists application_catalog_standalone_path_uidx\n  on public.application_catalog (standalone_path);\ncreate unique index if not exists application_catalog_login_path_uidx\n  on public.application_catalog (login_path)\n  where login_path is not null;\n\nupdate public.tenant_applications ta\nset\n  bootstrap_version = greatest(coalesce(ta.bootstrap_version, 1), c.bootstrap_version),\n  settings = coalesce(ta.settings, '{}'::jsonb) || jsonb_strip_nulls(jsonb_build_object(\n    'application_slug', c.slug,\n    'standalone_path', c.standalone_path,\n    'login_path', c.login_path,\n    'bootstrap_contract', 'application-bootstrap.v' || c.bootstrap_version::text,\n    'route_adapter', c.route_adapter\n  )),\n  updated_at = now()\nfrom public.application_catalog c\nwhere c.id = ta.application_id;\n\ncreate or replace function public.bootstrap_tenant_applications_for_tenant(p_tenant_id uuid)\nreturns integer\nlanguage plpgsql\nsecurity definer\nset search_path = public\nas $$\ndeclare\n  v_tenant_type text;\n  v_count integer := 0;\nbegin\n  select upper(replace(replace(coalesce(t.tenant_type::text, ''), '-', '_'), ' ', '_'))\n    into v_tenant_type\n  from public.tenants t\n  where t.id = p_tenant_id;\n\n  if not found then\n    raise exception 'tenant_not_found';\n  end if;\n\n  insert into public.tenant_applications (\n    tenant_id, application_id, enabled, settings, bootstrap_version, updated_at\n  )\n  select\n    p_tenant_id,\n    c.id,\n    true,\n    jsonb_strip_nulls(jsonb_build_object(\n      'application_slug', c.slug,\n      'standalone_path', c.standalone_path,\n      'login_path', c.login_path,\n      'bootstrap_contract', 'application-bootstrap.v' || c.bootstrap_version::text,\n      'route_adapter', c.route_adapter,\n      'bootstrap_source', 'tenant-create-default-eligible'\n    )),\n    c.bootstrap_version,\n    now()\n  from public.application_catalog c\n  where c.default_installed = true\n    and c.audience = 'tenant-user'\n    and (\n      jsonb_typeof(c.supported_tenant_types) <> 'array'\n      or jsonb_array_length(c.supported_tenant_types) = 0\n      or c.supported_tenant_types ? v_tenant_type\n    )\n  on conflict (tenant_id, application_id) do update set\n    -- Preserve explicit tenant enable/disable state. Bootstrap only reconciles contract metadata/version.\n    settings = coalesce(public.tenant_applications.settings, '{}'::jsonb) || excluded.settings,\n    bootstrap_version = greatest(public.tenant_applications.bootstrap_version, excluded.bootstrap_version),\n    updated_at = now();\n\n  get diagnostics v_count = row_count;\n  return v_count;\nend;\n$$;\n\nrevoke all on function public.bootstrap_tenant_applications_for_tenant(uuid) from public;\ngrant execute on function public.bootstrap_tenant_applications_for_tenant(uuid) to service_role;\n\ncreate or replace function public.bootstrap_tenant_applications_after_insert()\nreturns trigger\nlanguage plpgsql\nsecurity definer\nset search_path = public\nas $$\nbegin\n  perform public.bootstrap_tenant_applications_for_tenant(new.id);\n  return new;\nend;\n$$;\n\nrevoke all on function public.bootstrap_tenant_applications_after_insert() from public;\n\ndrop trigger if exists trg_bootstrap_tenant_applications_after_insert on public.tenants;\ncreate trigger trg_bootstrap_tenant_applications_after_insert\nafter insert on public.tenants\nfor each row execute function public.bootstrap_tenant_applications_after_insert();\n"],"idempotency_key":"752729e0fb72387ad2ce6d27bdb12e3e492f7990:20260907170500"},{"name":"tenant_applications_platform_admin_read","version":"20260907182500","rollback":null,"created_by":"canonical-production-release","statements":["-- Allow canonical platform administrators to read tenant application installation state\n-- while preserving the existing active-member read contract.\n\ndrop policy if exists tenant_applications_member_read on public.tenant_applications;\n\ncreate policy tenant_applications_member_read\non public.tenant_applications\nfor select\nto authenticated\nusing (\n  public.is_platform_admin(auth.uid())\n  or exists (\n    select 1\n    from public.user_tenant_access uta\n    where uta.user_id = auth.uid()\n      and uta.tenant_id = tenant_applications.tenant_id\n      and coalesce(uta.membership_status, 'active') = 'active'\n  )\n);\n"],"idempotency_key":"db625bd0fcd727a4f3f2834ae9e47b48804f0771:20260907182500"},{"name":"curri_multi_tenant_control_center","version":"20260907190000","rollback":null,"created_by":"canonical-production-release","statements":["-- Make Curri Control Center's existing parent/child tenant contract explicit and tenant-qualified.\n-- No new tenants are created and child data remains isolated in its owning tenant.\n\ndo $$\ndeclare\n  v_parent uuid;\n  v_brokerage uuid;\n  v_one_call uuid;\n  v_apps jsonb := '[\"analytics\",\"calendar\",\"communications\",\"documents\",\"mail\",\"opportunity-radar\",\"reports\",\"tasks\"]'::jsonb;\nbegin\n  select id into v_parent from public.tenants where company_slug = 'curri-control-center' limit 1;\n  select id into v_brokerage from public.tenants where company_slug = 'curri-properties' limit 1;\n  select id into v_one_call from public.tenants where company_slug = 'one-call' limit 1;\n\n  if v_parent is null or v_brokerage is null or v_one_call is null then\n    raise exception 'curri_multi_tenant_contract_requires_existing_parent_and_children';\n  end if;\n\n  update public.tenants\n  set settings = coalesce(settings, '{}'::jsonb)\n    || jsonb_build_object(\n      'enabled_applications', v_apps,\n      'multi_tenant_contract', jsonb_build_object(\n        'schema', 'curri-portfolio.v1',\n        'role', 'parent_control_center',\n        'parent_tenant_id', v_parent::text,\n        'child_tenant_ids', jsonb_build_array(v_brokerage::text, v_one_call::text),\n        'child_slugs', jsonb_build_array('curri-properties', 'one-call'),\n        'routing', 'tenant-qualified',\n        'data_isolation', 'child-tenant-owned'\n      ),\n      'control_center_profile', coalesce(settings->'control_center_profile', '{}'::jsonb)\n        || jsonb_build_object(\n          'role', 'parent_control_center',\n          'child_tenants', jsonb_build_array(\n            jsonb_build_object('tenant_id', v_brokerage::text, 'slug', 'curri-properties', 'name', 'Realty World Curri Properties', 'role', 'realtor_flagship', 'route', '/knock-launch-crm?tenant=curri-properties'),\n            jsonb_build_object('tenant_id', v_one_call::text, 'slug', 'one-call', 'name', 'One Call Maintenance & Property Management', 'role', 'property_maintenance_management', 'route', '/knock-launch-crm?tenant=one-call')\n          )\n        ),\n      'tenant_manifest', coalesce(settings->'tenant_manifest', '{}'::jsonb) || jsonb_build_object('applications', v_apps)\n    ),\n    updated_at = now()\n  where id = v_parent;\n\n  update public.tenants\n  set settings = coalesce(settings, '{}'::jsonb) || jsonb_build_object(\n      'control_center_parent_slug', 'curri-control-center',\n      'control_center_parent_tenant_id', v_parent::text,\n      'control_center_role', 'business_tenant',\n      'portfolio_family', 'curri',\n      'portfolio_data_owner', company_slug\n    ),\n    updated_at = now()\n  where id in (v_brokerage, v_one_call);\nend $$;\n"],"idempotency_key":"b7afa13681a28ff82cd419cbbbdb4ddf997da00f:20260907190000"},{"name":"curri_three_tenant_demo_readiness","version":"20260907193000","rollback":null,"created_by":"canonical-production-release","statements":["-- Curri three-tenant demo readiness.\n-- Keeps Curri Control Center, Realty World Curri Properties, and One Call as separate data owners.\n-- No tenant rows are created. Sample operational scenarios are explicitly demo-only.\n\ndo $$\ndeclare\n  v_control uuid;\n  v_realtor uuid;\n  v_one_call uuid;\n  v_background text;\n  v_control_apps text[] := array['analytics','calendar','communications','contacts','documents','mail','opportunity-radar','reports','tasks'];\n  v_realtor_apps text[] := array['analytics','calendar','call-center','communications','contacts','crm','documents','field-intelligence','forms','mail','opportunity-radar','proposal-editor','qr-suite','realtor-transaction-operations','reports','tasks'];\n  v_one_call_apps text[] := array['analytics','calendar','communications','contacts','crm','documents','forms','invoices','jobs','mail','opportunity-radar','payments','tasks'];\nbegin\n  select id, coalesce(settings->>'backgroundImageUrl', settings->>'background_image_url')\n    into v_realtor, v_background\n  from public.tenants where company_slug = 'curri-properties' limit 1;\n  select id into v_control from public.tenants where company_slug = 'curri-control-center' limit 1;\n  select id into v_one_call from public.tenants where company_slug = 'one-call' limit 1;\n\n  if v_control is null or v_realtor is null or v_one_call is null then\n    raise exception 'curri_three_tenant_demo_readiness_requires_existing_tenants';\n  end if;\n\n  update public.tenants\n  set settings = coalesce(settings, '{}'::jsonb)\n    || jsonb_build_object(\n      'enabled_applications', to_jsonb(v_control_apps),\n      'tenant_manifest', coalesce(settings->'tenant_manifest', '{}'::jsonb) || jsonb_build_object('applications', to_jsonb(v_control_apps)),\n      'demo_readiness', jsonb_build_object('version', 1, 'state', 'ready', 'mode', 'executive_meeting_demo', 'child_slugs', jsonb_build_array('curri-properties','one-call'))\n    ),\n    updated_at = now()\n  where id = v_control;\n\n  update public.tenants\n  set settings = coalesce(settings, '{}'::jsonb)\n    || jsonb_build_object(\n      'enabled_applications', to_jsonb(v_realtor_apps),\n      'tenant_manifest', coalesce(settings->'tenant_manifest', '{}'::jsonb) || jsonb_build_object('applications', to_jsonb(v_realtor_apps)),\n      'demo_readiness', jsonb_build_object(\n        'version', 1,\n        'state', 'ready',\n        'mode', 'brokerage_sample_demo',\n        'sample_transaction_count', (select count(*) from public.realtor_transactions where tenant_id = v_realtor and is_demo is true),\n        'agent_roster_source', 'settings.demo_agent_roster',\n        'system_of_record', 'BoldTrail'\n      )\n    ),\n    updated_at = now()\n  where id = v_realtor;\n\n  update public.tenants\n  set settings = coalesce(settings, '{}'::jsonb)\n    || case when v_background is null then '{}'::jsonb else jsonb_build_object('backgroundImageUrl', v_background, 'background_image_url', v_background, 'shared_visual_source_slug', 'curri-properties') end\n    || jsonb_build_object(\n      'enabled_applications', to_jsonb(v_one_call_apps),\n      'tenant_manifest', coalesce(settings->'tenant_manifest', '{}'::jsonb) || jsonb_build_object('applications', to_jsonb(v_one_call_apps)),\n      'demo_readiness', jsonb_build_object('version', 1, 'state', 'ready', 'mode', 'property_operations_demo', 'parent_slug', 'curri-control-center'),\n      'demo_operations', jsonb_build_object(\n        'demo_only', true,\n        'label', 'Meeting demo · sample operations',\n        'metrics', jsonb_build_object('open_work_orders', 5, 'due_today', 2, 'waiting_approval', 1, 'weekly_billable', '$6,840', 'response_time_minutes', 18),\n        'work_orders', jsonb_build_array(\n          jsonb_build_object('title','Pre-listing make-ready','property','Beachside seller property','status','Scheduled','amount','$3,200','due','Tue · 9:00 AM','source','Realty World Curri Properties'),\n          jsonb_build_object('title','Turnover punch list','property','Melbourne rental','status','In Progress','amount','$1,850','due','Today · 1:30 PM'),\n          jsonb_build_object('title','HVAC vendor coordination','property','Palm Bay rental','status','Waiting','amount','$420','due','Approval needed'),\n          jsonb_build_object('title','Quarterly property inspection','property','Satellite Beach rental','status','Assigned','amount','$295','due','Wed · 10:00 AM'),\n          jsonb_build_object('title','Emergency leak response','property','Viera investment property','status','Complete','amount','$780','due','Closed today')\n        ),\n        'schedule', jsonb_build_array(\n          jsonb_build_object('time','9:00 AM','title','Make-ready kickoff','detail','Beachside seller property'),\n          jsonb_build_object('time','1:30 PM','title','Turnover walkthrough','detail','Melbourne rental'),\n          jsonb_build_object('time','4:00 PM','title','Vendor approval review','detail','HVAC coordination')\n        )\n      )\n    ),\n    updated_at = now()\n  where id = v_one_call;\n\n  update public.tenant_applications\n  set enabled = application_id = any(v_control_apps)\n  where tenant_id = v_control;\n\n  update public.tenant_applications\n  set enabled = application_id = any(v_realtor_apps)\n  where tenant_id = v_realtor;\n\n  update public.tenant_applications\n  set enabled = application_id = any(v_one_call_apps)\n  where tenant_id = v_one_call;\nend $$;\n"],"idempotency_key":"9371269dd3f20c37bc9409fe97eb26ad75a30e51:20260907193000"},{"name":"curri_opportunity_radar_profiles","version":"20260907203000","rollback":null,"created_by":"canonical-production-release","statements":["-- Curri child-business Opportunity Radar profiles.\n-- Keep the executive parent as an aggregation workspace; only operating child tenants seed public demand.\n\ndo $$\nbegin\n  update public.tenants\n  set settings = coalesce(settings, '{}'::jsonb)\n    || jsonb_build_object(\n      'opportunity_radar_profile', 'REALTOR',\n      'opportunity_territory', jsonb_build_object(\n        'label', 'Brevard brokerage demo territory',\n        'terms', jsonb_build_array(\n          'Satellite Beach',\n          'Indian Harbour Beach',\n          'Viera',\n          'Melbourne Beach',\n          'Titusville',\n          '32937',\n          '32940',\n          '32951',\n          '32780'\n        )\n      ),\n      'service_profile', jsonb_build_object(\n        'category', 'real-estate',\n        'services', jsonb_build_array(\n          'Buyer Representation',\n          'Listing Representation',\n          'Home Search',\n          'Seller Consultation'\n        ),\n        'service_area', jsonb_build_array(\n          'Satellite Beach',\n          'Indian Harbour Beach',\n          'Viera',\n          'Melbourne Beach',\n          'Titusville'\n        )\n      )\n    ),\n      updated_at = now()\n  where company_slug = 'curri-properties';\n\n  update public.tenants\n  set settings = coalesce(settings, '{}'::jsonb)\n    || jsonb_build_object(\n      'opportunity_radar_keywords', jsonb_build_array(\n        'property maintenance',\n        'property management',\n        'handyman',\n        'make ready',\n        'turnover',\n        'rental property maintenance',\n        'property inspection',\n        'home repair',\n        'maintenance service'\n      ),\n      'opportunity_territory', jsonb_build_object(\n        'label', 'Brevard property operations demo territory',\n        'terms', jsonb_build_array(\n          'Satellite Beach',\n          'Melbourne',\n          'Palm Bay',\n          'Viera',\n          '32937'\n        )\n      ),\n      'service_profile', jsonb_build_object(\n        'category', 'property-maintenance-management',\n        'services', jsonb_build_array(\n          'Property Maintenance',\n          'Property Management',\n          'Make-Ready Work',\n          'Turnover Work',\n          'Property Inspections',\n          'Home Repair'\n        ),\n        'service_area', jsonb_build_array(\n          'Satellite Beach',\n          'Melbourne',\n          'Palm Bay',\n          'Viera'\n        )\n      )\n    ),\n      updated_at = now()\n  where company_slug = 'one-call';\nend;\n$$;\n"],"idempotency_key":"a7c0bde1cc5a3ff3afa27b95ce54e279ad20e2e9:20260907203000"},{"name":"calendar_bootstrap_v3_contract_reconcile","version":"20260907215200","rollback":null,"created_by":"canonical-production-release","statements":["-- Reconcile Calendar's first-class bootstrap identity with its completed provider evolution.\n-- Calendar installs reached v2 in calendar_first_class_application_bootstrap and v3 in calendar_caldav_provider.\n-- Preserve tenant enablement and all Calendar domain data; only align bootstrap contract metadata.\n\nupdate public.application_catalog\nset\n  bootstrap_version = greatest(coalesce(bootstrap_version, 1), 3),\n  config_contract = coalesce(config_contract, '{}'::jsonb) || jsonb_build_object(\n    'schema', 'application-bootstrap.v3',\n    'applicationId', 'calendar',\n    'applicationSlug', 'calendar'\n  ),\n  updated_at = now()\nwhere id = 'calendar';\n\nupdate public.tenant_applications\nset\n  bootstrap_version = greatest(coalesce(bootstrap_version, 1), 3),\n  settings = coalesce(settings, '{}'::jsonb) || jsonb_build_object(\n    'application_slug', 'calendar',\n    'standalone_path', '/calendar',\n    'login_path', '/calendar/login',\n    'bootstrap_contract', 'application-bootstrap.v3',\n    'route_adapter', 'tenant-workspace'\n  ),\n  updated_at = now()\nwhere application_id = 'calendar';\n"],"idempotency_key":"1d6c04db992da9102143ad081a5bf61a835b2a71:20260907215200"},{"name":"sync_primary_tenant_logo_pointer","version":"20260906223000","rollback":null,"created_by":"canonical-production-release","statements":["create or replace function public.sync_primary_tenant_logo_pointer()\nreturns trigger\nlanguage plpgsql\nsecurity definer\nset search_path = public\nas $$\nbegin\n  if new.collection_key = 'branding'\n     and new.is_primary = true\n     and new.deleted_at is null then\n    update public.tenants\n    set company_logo_url = case\n          when company_logo_url like '%/functions/v1/tenant-media-library?mode=asset&id=%'\n            then regexp_replace(company_logo_url, 'id=[^&]+', 'id=' || new.id::text)\n          else 'https://ymntrajfubicyxwwdxxa.supabase.co/functions/v1/tenant-media-library?mode=asset&id=' || new.id::text\n        end,\n        settings = jsonb_set(\n          jsonb_set(coalesce(settings, '{}'::jsonb), '{media_branding,resolved_logo_asset_id}', to_jsonb(new.id::text), true),\n          '{media_branding,resolved_logo_version}',\n          to_jsonb(new.updated_at::text),\n          true\n        ),\n        updated_at = now()\n    where id = new.tenant_id;\n  end if;\n  return new;\nend;\n$$;\n\ndrop trigger if exists trg_sync_primary_tenant_logo_pointer on public.tenant_media_assets;\ncreate trigger trg_sync_primary_tenant_logo_pointer\nafter insert or update of is_primary, deleted_at, updated_at, storage_key, original_key\non public.tenant_media_assets\nfor each row\nexecute function public.sync_primary_tenant_logo_pointer();\n"],"idempotency_key":"bb0db422407859dbe047e3083a1ba50961c14567:20260906223000"},{"name":"resolve_approved_oauth_authorization_redirect","version":"20260908130234","rollback":null,"created_by":"nysomenterprises@gmail.com","statements":["create or replace function public.resolve_approved_oauth_authorization_redirect(p_authorization_id text)\nreturns jsonb\nlanguage plpgsql\nstable\nsecurity definer\nset search_path = ''\nas $$\ndeclare\n  v_row record;\nbegin\n  if auth.uid() is null then\n    return null;\n  end if;\n\n  select a.redirect_uri, a.authorization_code, a.state, a.expires_at\n    into v_row\n  from auth.oauth_authorizations a\n  where a.authorization_id = p_authorization_id\n    and a.user_id = auth.uid()\n    and a.status::text = 'approved'\n    and a.authorization_code is not null\n    and a.expires_at > now()\n  limit 1;\n\n  if not found then\n    return null;\n  end if;\n\n  return jsonb_build_object(\n    'redirect_uri', v_row.redirect_uri,\n    'authorization_code', v_row.authorization_code,\n    'state', v_row.state,\n    'expires_at', v_row.expires_at\n  );\nend;\n$$;\n\nrevoke all on function public.resolve_approved_oauth_authorization_redirect(text) from public;\ngrant execute on function public.resolve_approved_oauth_authorization_redirect(text) to authenticated;"],"idempotency_key":null},{"name":"fix_invitation_delivery_history","version":"20260909164000","rollback":null,"created_by":"canonical-production-release","statements":["alter table public.user_invitations drop constraint if exists user_invitations_status_check;\nalter table public.user_invitations add constraint user_invitations_status_check\n  check (status = any (array['pending'::text, 'sent'::text, 'opened'::text, 'accepted'::text, 'expired'::text, 'cancelled'::text, 'failed'::text]));\n\ncreate or replace function public.capture_owner_invitation_history()\nreturns trigger\nlanguage plpgsql\nsecurity definer\nset search_path to 'public'\nas $function$\ndeclare\n  v_relationship_id uuid;\n  v_display_name text;\nbegin\n  insert into public.owner_tenant_relationships (tenant_id)\n  values (new.tenant_id)\n  on conflict (tenant_id) do nothing;\n\n  select id into v_relationship_id\n  from public.owner_tenant_relationships\n  where tenant_id = new.tenant_id;\n\n  v_display_name := coalesce(nullif(new.name, ''), nullif(new.email, ''), 'Tenant user');\n\n  if lower(coalesce(new.status, '')) = 'sent'\n     and (tg_op = 'INSERT' or lower(coalesce(old.status, '')) is distinct from 'sent') then\n    insert into public.owner_relationship_events (\n      relationship_id, tenant_id, event_type, channel, direction, title, summary,\n      participants, source, source_id, metadata, occurred_at, created_by\n    ) values (\n      v_relationship_id, new.tenant_id, 'invitation_sent', 'platform', 'outbound',\n      'Invitation sent to ' || v_display_name,\n      concat_ws(' • ', nullif(new.email,''), nullif(new.role,'')),\n      jsonb_build_array(jsonb_build_object('name', new.name, 'email', new.email, 'role', new.role)),\n      'user_invitations', new.id::text || ':sent',\n      jsonb_build_object('invitation_id', new.id, 'status', new.status, 'delivery_receipt', new.metadata->'delivery_receipt'),\n      coalesce((new.metadata->>'sent_at')::timestamptz, new.updated_at, now()), new.invited_by\n    )\n    on conflict (tenant_id, source, source_id) where source_id is not null do nothing;\n  end if;\n\n  if lower(coalesce(new.status, '')) = 'failed'\n     and (tg_op = 'INSERT' or lower(coalesce(old.status, '')) is distinct from 'failed') then\n    insert into public.owner_relationship_events (\n      relationship_id, tenant_id, event_type, channel, direction, title, summary,\n      participants, source, source_id, metadata, occurred_at, created_by\n    ) values (\n      v_relationship_id, new.tenant_id, 'invitation_failed', 'platform', 'outbound',\n      'Invitation delivery failed for ' || v_display_name,\n      concat_ws(' • ', nullif(new.email,''), nullif(new.role,'')),\n      jsonb_build_array(jsonb_build_object('name', new.name, 'email', new.email, 'role', new.role)),\n      'user_invitations', new.id::text || ':failed',\n      jsonb_build_object('invitation_id', new.id, 'status', new.status, 'delivery_error', new.metadata->'delivery_error'),\n      new.updated_at, new.invited_by\n    )\n    on conflict (tenant_id, source, source_id) where source_id is not null do nothing;\n  end if;\n\n  if new.accepted_at is not null or lower(coalesce(new.status,'')) = 'accepted' then\n    insert into public.owner_relationship_events (\n      relationship_id, tenant_id, event_type, channel, direction, title, summary,\n      participants, source, source_id, metadata, occurred_at, created_by\n    ) values (\n      v_relationship_id, new.tenant_id, 'invitation_accepted', 'platform', 'inbound',\n      'Invitation accepted by ' || v_display_name,\n      concat_ws(' • ', nullif(new.email,''), nullif(new.role,'')),\n      jsonb_build_array(jsonb_build_object('name', new.name, 'email', new.email, 'role', new.role)),\n      'user_invitations', new.id::text || ':accepted',\n      jsonb_build_object('invitation_id', new.id, 'status', new.status),\n      coalesce(new.accepted_at, new.updated_at, now()), new.invited_by\n    )\n    on conflict (tenant_id, source, source_id) where source_id is not null do nothing;\n  end if;\n\n  return new;\nend;\n$function$;\n"],"idempotency_key":"cc7f9971944d522b074e73625659deb79858c84f:20260909164000"},{"name":"harden_system_backup_recovery","version":"20260909173000","rollback":null,"created_by":"canonical-production-release","statements":["-- Harden the existing KNOCKLAUNCH independent backup primitive after the 2026-09-03 throttled run.\n-- Supabase native backups remain the authoritative physical restore layer.\n-- Forgejo generic packages remain the independent logical database + Storage recovery layer.\n\ncreate or replace function public.system_backup_export_chunk(p_item_id bigint)\nreturns jsonb\nlanguage plpgsql\nsecurity definer\nset search_path=public,private,auth,storage,supabase_migrations\nset statement_timeout='20s'\nas $$\ndeclare\n  i public.system_backup_items%rowtype;\n  v_data jsonb;\n  v_rows integer;\nbegin\n  select * into i from public.system_backup_items where id=p_item_id;\n  if i.id is null or i.kind<>'database_chunk' then raise exception 'backup_item_not_database_chunk'; end if;\n  if i.source_schema not in ('public','private','auth','storage','supabase_migrations') then raise exception 'backup_schema_not_allowed'; end if;\n  if i.source_schema='public' and i.source_table in ('system_backup_runs','system_backup_items') then raise exception 'backup_internal_table_excluded'; end if;\n  if coalesce(i.row_limit,0)=0 then v_data:='[]'::jsonb;\n  else\n    execute format('select coalesce(jsonb_agg(to_jsonb(t)), ''[]''::jsonb) from (select * from %I.%I order by ctid offset %s limit %s) t',i.source_schema,i.source_table,coalesce(i.row_offset,0),i.row_limit) into v_data;\n  end if;\n  v_rows:=jsonb_array_length(v_data);\n  return jsonb_build_object('format','knocklaunch-table-chunk-v1','schema',i.source_schema,'table',i.source_table,'offset',coalesce(i.row_offset,0),'limit',coalesce(i.row_limit,0),'rows',v_rows,'data',v_data);\nend;\n$$;\nrevoke all on function public.system_backup_export_chunk(bigint) from public, anon, authenticated;\ngrant execute on function public.system_backup_export_chunk(bigint) to service_role;\n\nselect cron.unschedule(jobid) from cron.job where jobname in ('knocklaunch-weekly-full-backup','knocklaunch-daily-full-backup');\nselect cron.schedule('knocklaunch-daily-full-backup','15 8 * * *',$$select private.system_backup_edge_request('start','daily',null,false);$$);\nselect cron.unschedule(jobid) from cron.job where jobname='knocklaunch-backup-recovery-pulse';\nselect cron.schedule('knocklaunch-backup-recovery-pulse','*/15 * * * *',$$select private.system_backup_edge_request('resume','recovery-pulse',null,false);$$);\n"],"idempotency_key":"ea6af0f1bed804239a80deaed1a508d80a070247:20260909173000"},{"name":"system_backup_3am_eastern","version":"20260909172457","rollback":null,"created_by":"nysomenterprises@gmail.com","statements":["select cron.unschedule(jobid)\nfrom cron.job\nwhere jobname='knocklaunch-daily-full-backup';\n\nselect cron.schedule(\n  'knocklaunch-daily-full-backup',\n  '0 * * * *',\n  $$\n  select private.system_backup_edge_request('start','daily-3am-america-new-york',null,false)\n  where extract(hour from timezone('America/New_York', now())) = 3;\n  $$\n);"],"idempotency_key":null},{"name":"create_platform_godmode_media_bucket","version":"20260910044620","rollback":null,"created_by":"nysomenterprises@gmail.com","statements":["insert into storage.buckets (id, name, public, file_size_limit)\nvalues ('platform-godmode', 'platform-godmode', true, 262144000)\non conflict (id) do update set public = excluded.public, file_size_limit = excluded.file_size_limit;\n\ndrop policy if exists \"temporary_godmode_media_upload\" on storage.objects;\ncreate policy \"temporary_godmode_media_upload\"\non storage.objects for insert\nto anon\nwith check (bucket_id = 'platform-godmode' and name = 'backgrounds/godmode-video-background.mp4');"],"idempotency_key":null},{"name":"remove_temporary_godmode_media_upload_policy","version":"20260910044644","rollback":null,"created_by":"nysomenterprises@gmail.com","statements":["drop policy if exists \"temporary_godmode_media_upload\" on storage.objects;"],"idempotency_key":null},{"name":"schedule_system_backup_3am_eastern","version":"20260909174500","rollback":null,"created_by":"canonical-production-release","statements":["-- Keep the independent full-data backup at 03:00 America/New_York year-round.\n-- pg_cron runs in UTC, so an hourly pulse uses an explicit DST-safe local-hour guard.\n\ncreate or replace function private.system_backup_edge_request_at_local_hour(\n  p_local_hour integer,\n  p_action text default 'start',\n  p_reason text default 'daily',\n  p_source_main_sha text default null,\n  p_force boolean default false\n)\nreturns bigint\nlanguage plpgsql\nsecurity definer\nset search_path=private\nas $$\ndeclare\n  v_local timestamp;\nbegin\n  if p_local_hour < 0 or p_local_hour > 23 then\n    raise exception 'system_backup_local_hour_invalid';\n  end if;\n  v_local := now() at time zone 'America/New_York';\n  if extract(hour from v_local)::integer <> p_local_hour then\n    return null;\n  end if;\n  return private.system_backup_edge_request(p_action,p_reason,p_source_main_sha,p_force);\nend;\n$$;\nrevoke all on function private.system_backup_edge_request_at_local_hour(integer,text,text,text,boolean) from public, anon, authenticated;\n\ncreate or replace function private.system_backup_health(p_max_age interval default interval '36 hours')\nreturns jsonb\nlanguage sql\nsecurity definer\nset search_path=public,private\nas $$\n  with latest as (\n    select id,status,requested_at,completed_at,failed_items,manifest_key,metadata,error\n    from public.system_backup_runs\n    order by requested_at desc\n    limit 1\n  ), success as (\n    select id,completed_at,failed_items,manifest_key,metadata\n    from public.system_backup_runs\n    where status='completed'\n      and failed_items=0\n      and manifest_key is not null\n      and coalesce((metadata->>'verified')::boolean,false)=true\n    order by completed_at desc\n    limit 1\n  )\n  select jsonb_build_object(\n    'healthy', exists(select 1 from success where completed_at >= now()-p_max_age),\n    'maxAgeSeconds', extract(epoch from p_max_age)::bigint,\n    'lastSuccessfulRunId', (select id from success),\n    'lastSuccessfulAt', (select completed_at from success),\n    'latestRunId', (select id from latest),\n    'latestStatus', (select status from latest),\n    'latestError', (select error from latest)\n  );\n$$;\nrevoke all on function private.system_backup_health(interval) from public, anon, authenticated;\ngrant execute on function private.system_backup_health(interval) to service_role;\n\ncreate or replace function private.system_backup_assert_recent_verified(p_max_age interval default interval '36 hours')\nreturns void\nlanguage plpgsql\nsecurity definer\nset search_path=public,private\nas $$\ndeclare\n  v_health jsonb;\nbegin\n  v_health := private.system_backup_health(p_max_age);\n  if coalesce((v_health->>'healthy')::boolean,false) is not true then\n    raise exception 'recent_verified_system_backup_required' using detail=v_health::text;\n  end if;\nend;\n$$;\nrevoke all on function private.system_backup_assert_recent_verified(interval) from public, anon, authenticated;\ngrant execute on function private.system_backup_assert_recent_verified(interval) to service_role;\n\nselect cron.unschedule(jobid)\nfrom cron.job\nwhere jobname in ('knocklaunch-weekly-full-backup','knocklaunch-daily-full-backup','knocklaunch-daily-full-backup-3am-eastern');\n\nselect cron.schedule(\n  'knocklaunch-daily-full-backup-3am-eastern',\n  '0 * * * *',\n  $$select private.system_backup_edge_request_at_local_hour(3,'start','daily-3am-eastern',null,false);$$\n);\n\nselect cron.unschedule(jobid) from cron.job where jobname='knocklaunch-backup-recovery-pulse';\nselect cron.schedule(\n  'knocklaunch-backup-recovery-pulse',\n  '*/15 * * * *',\n  $$select private.system_backup_edge_request('resume','recovery-pulse',null,false);$$\n);\n"],"idempotency_key":"a59cf843b4e4238ccf50ee704f24892c1fdb8f25:20260909174500"},{"name":"temporary_godmode_media_upload_policy","version":"20260910045653","rollback":null,"created_by":"nysomenterprises@gmail.com","statements":["create policy \"temporary_godmode_media_upload\" on storage.objects for insert to anon with check (bucket_id = 'platform-godmode' and name = 'backgrounds/godmode-video-background.mp4');"],"idempotency_key":null},{"name":"remove_temporary_godmode_media_upload_policy","version":"20260910051325","rollback":null,"created_by":"nysomenterprises@gmail.com","statements":["drop policy if exists \"temporary_godmode_media_upload\" on storage.objects;"],"idempotency_key":null},{"name":"user_activity_observability","version":"20260911012500","rollback":null,"created_by":"canonical-production-release","statements":["create table if not exists public.user_activity_events (\n  id uuid primary key default gen_random_uuid(),\n  user_id uuid not null references auth.users(id) on delete cascade,\n  tenant_id text null,\n  session_id text not null,\n  event_type text not null check (event_type in ('login', 'session_start', 'page_view', 'interaction')),\n  route_path text null,\n  route_label text null,\n  action_label text null,\n  human_summary text not null,\n  device_family text null,\n  metadata jsonb not null default '{}'::jsonb,\n  occurred_at timestamptz not null default now(),\n  created_at timestamptz not null default now()\n);\n\ncreate table if not exists public.user_activity_presence (\n  user_id uuid primary key references auth.users(id) on delete cascade,\n  tenant_id text null,\n  session_id text not null,\n  current_path text null,\n  current_label text null,\n  device_family text null,\n  started_at timestamptz not null default now(),\n  last_seen_at timestamptz not null default now(),\n  updated_at timestamptz not null default now()\n);\n\ncreate index if not exists user_activity_events_user_time_idx\n  on public.user_activity_events (user_id, occurred_at desc);\ncreate index if not exists user_activity_events_tenant_time_idx\n  on public.user_activity_events (tenant_id, occurred_at desc);\ncreate index if not exists user_activity_events_type_time_idx\n  on public.user_activity_events (event_type, occurred_at desc);\ncreate index if not exists user_activity_events_route_time_idx\n  on public.user_activity_events (route_path, occurred_at desc)\n  where event_type = 'page_view';\ncreate index if not exists user_activity_presence_last_seen_idx\n  on public.user_activity_presence (last_seen_at desc);\ncreate index if not exists user_activity_presence_tenant_seen_idx\n  on public.user_activity_presence (tenant_id, last_seen_at desc);\n\nalter table public.user_activity_events enable row level security;\nalter table public.user_activity_presence enable row level security;\nalter table public.user_activity_presence replica identity full;\n\ndrop policy if exists user_activity_events_insert_own on public.user_activity_events;\ncreate policy user_activity_events_insert_own\n  on public.user_activity_events\n  for insert\n  to authenticated\n  with check (auth.uid() = user_id);\n\ndrop policy if exists user_activity_events_platform_admin_read on public.user_activity_events;\ncreate policy user_activity_events_platform_admin_read\n  on public.user_activity_events\n  for select\n  to authenticated\n  using (public.is_platform_admin(auth.uid()));\n\ndrop policy if exists user_activity_presence_insert_own on public.user_activity_presence;\ncreate policy user_activity_presence_insert_own\n  on public.user_activity_presence\n  for insert\n  to authenticated\n  with check (auth.uid() = user_id);\n\ndrop policy if exists user_activity_presence_update_own on public.user_activity_presence;\ncreate policy user_activity_presence_update_own\n  on public.user_activity_presence\n  for update\n  to authenticated\n  using (auth.uid() = user_id)\n  with check (auth.uid() = user_id);\n\ndrop policy if exists user_activity_presence_platform_admin_read on public.user_activity_presence;\ncreate policy user_activity_presence_platform_admin_read\n  on public.user_activity_presence\n  for select\n  to authenticated\n  using (public.is_platform_admin(auth.uid()));\n\ngrant select, insert on public.user_activity_events to authenticated;\ngrant select, insert, update on public.user_activity_presence to authenticated;\n\ndo $$\nbegin\n  if exists (select 1 from pg_publication where pubname = 'supabase_realtime') then\n    if not exists (\n      select 1 from pg_publication_tables\n      where pubname = 'supabase_realtime'\n        and schemaname = 'public'\n        and tablename = 'user_activity_events'\n    ) then\n      execute 'alter publication supabase_realtime add table public.user_activity_events';\n    end if;\n    if not exists (\n      select 1 from pg_publication_tables\n      where pubname = 'supabase_realtime'\n        and schemaname = 'public'\n        and tablename = 'user_activity_presence'\n    ) then\n      execute 'alter publication supabase_realtime add table public.user_activity_presence';\n    end if;\n  end if;\nend $$;\n\ncomment on table public.user_activity_events is 'Human-facing product activity events. Deliberately excludes typed field values, query strings, message bodies, credentials, and invitation tokens.';\ncomment on table public.user_activity_presence is 'Latest authenticated user presence for Godmode operator visibility.';\n"],"idempotency_key":"2cd9c3e636796ba0fc5aa2bfea23b098dbe7b98f:20260911012500"},{"name":"repair_owner_membership_role_sync","version":"20260911020123","rollback":null,"created_by":"nysomenterprises@gmail.com","statements":["create or replace function public.sync_membership_to_role()\nreturns trigger\nlanguage plpgsql\nsecurity definer\nset search_path to 'public', 'pg_temp'\nas $function$\ndeclare\n  v_role public.app_role;\nbegin\n  if coalesce(new.status, '') <> 'active' then\n    delete from public.user_roles ur\n    where ur.user_id = new.user_id\n      and ur.tenant_id = new.tenant_id;\n    return new;\n  end if;\n\n  -- An explicit tenant role is authoritative. Membership synchronization\n  -- must never overwrite or conflict with it during owner handoff.\n  if exists (\n    select 1\n    from public.user_roles ur\n    where ur.user_id = new.user_id\n      and ur.tenant_id = new.tenant_id\n  ) then\n    return new;\n  end if;\n\n  -- Platform-wide administrators do not need a synthetic tenant role.\n  if exists (\n    select 1\n    from public.user_roles ur\n    where ur.user_id = new.user_id\n      and ur.tenant_id is null\n      and ur.role in ('GodMode'::public.app_role, 'PlatformAdmin'::public.app_role)\n  ) then\n    return new;\n  end if;\n\n  v_role := (\n    case\n      when coalesce(new.is_owner, false) then 'Tenant Owner'\n      else 'Rep'\n    end\n  )::public.app_role;\n\n  insert into public.user_roles(user_id, tenant_id, role)\n  values (new.user_id, new.tenant_id, v_role)\n  on conflict (user_id, tenant_id) do nothing;\n\n  return new;\nend;\n$function$;\n\ncreate or replace function public.sync_role_to_membership()\nreturns trigger\nlanguage plpgsql\nsecurity definer\nset search_path to 'public', 'pg_temp'\nas $function$\nbegin\n  -- Global/platform roles intentionally have tenant_id = NULL and must not\n  -- create or alter tenant membership rows.\n  if new.tenant_id is null then\n    return new;\n  end if;\n\n  insert into public.user_tenant_memberships (\n    user_id,\n    tenant_id,\n    is_owner,\n    status\n  )\n  values (\n    new.user_id,\n    new.tenant_id,\n    new.role::text in ('Owner', 'Tenant Owner'),\n    'active'\n  )\n  on conflict (user_id, tenant_id) do update\n    set status = 'active',\n        is_owner = excluded.is_owner;\n\n  return new;\nend;\n$function$;\n\ndrop trigger if exists trg_sync_role_to_membership on public.user_roles;\ncreate trigger trg_sync_role_to_membership\nafter insert or update of role, user_id, tenant_id on public.user_roles\nfor each row execute function public.sync_role_to_membership();"],"idempotency_key":null},{"name":"bootstrap_redfish_proposal_profile","version":"20260911153500","rollback":null,"created_by":"canonical-production-release","statements":["-- Give the Redfish tenant a valid universal proposal-editor profile without changing its operational roofing proposal data owner.\nINSERT INTO public.proposal_profiles (id, tenant_id, profile_key, tenant_type, status, number_prefix, renderer_key, version, config, created_at, updated_at)\nSELECT gen_random_uuid(), t.id, 'default', 'ROOFING_COMPANY', 'active', 'RF', 'tenant_profile_v1', 1,\n  jsonb_build_object(\n    'contract', 'PREHISTORIC-002-UNIVERSAL-PROPOSALS',\n    'application', 'proposal-editor',\n    'businessCategory', 'roofing',\n    'contextKind', 'property',\n    'documentTitle', 'Roofing Proposal',\n    'defaultOfferKey', 'roofing_project',\n    'services', COALESCE(t.settings->'services', '[]'::jsonb),\n    'terminology', jsonb_build_object('workspaceTitle', 'Proposal Editor', 'workspaceEyebrow', 'Proposal design', 'customerLabel', 'Prepared for', 'summaryLabel', 'Property and scope'),\n    'offers', jsonb_build_array(jsonb_build_object('key', 'roofing_project', 'label', 'Roofing project', 'pricingMode', 'combined', 'lineKeys', jsonb_build_array('roofing_project'))),\n    'commercial', jsonb_build_object('pricing', jsonb_build_object('roofing_project', jsonb_build_object('kind', 'service', 'label', 'Roofing project', 'quantity', 1, 'quantityRule', 'fixed', 'unitLabel', 'project', 'unitPrice', 0)))\n  ), now(), now()\nFROM public.tenants t\nWHERE t.company_slug = 'redfish-roofing' AND t.tenant_type = 'ROOFING_COMPANY'\nON CONFLICT (tenant_id, profile_key) DO UPDATE\nSET tenant_type = EXCLUDED.tenant_type,\n    status = 'active',\n    number_prefix = EXCLUDED.number_prefix,\n    renderer_key = EXCLUDED.renderer_key,\n    config = CASE WHEN public.proposal_profiles.config IS NULL OR public.proposal_profiles.config = '{}'::jsonb THEN EXCLUDED.config ELSE public.proposal_profiles.config END,\n    updated_at = now();\n"],"idempotency_key":"4787c82d479b4ec96a3702e19282abdb076db4d5:20260911153500"},{"name":"proposal_operating_contracts","version":"20260911163000","rollback":null,"created_by":"canonical-production-release","statements":["-- Canonical accepted-proposal operating contracts.\n-- Accepted proposals become deterministic, two-sided operating inputs for issuer and acceptor tenants.\n\nalter table public.proposals add column if not exists counterparty_tenant_id uuid references public.tenants(id) on delete set null;\nalter table public.owner_relationship_proposals add column if not exists canonical_proposal_id uuid references public.proposals(id) on delete set null;\n\ncreate table if not exists public.proposal_operating_contracts (\n  id uuid primary key default gen_random_uuid(),\n  proposal_id uuid not null unique references public.proposals(id) on delete cascade,\n  proposal_version integer not null check (proposal_version > 0),\n  issuer_tenant_id uuid not null references public.tenants(id) on delete cascade,\n  acceptor_tenant_id uuid references public.tenants(id) on delete set null,\n  lifecycle_status text not null default 'draft' check (lifecycle_status in ('draft','sent','received','accepted','activated','declined','void')),\n  pending_agreement_id uuid references public.pending_agreements(id) on delete set null,\n  signed_agreement_id uuid references public.signed_agreements(id) on delete set null,\n  signed_pdf_path text,\n  signed_at timestamptz,\n  accepted_at timestamptz,\n  activated_at timestamptz,\n  terms jsonb not null default '{}'::jsonb,\n  metadata jsonb not null default '{}'::jsonb,\n  created_at timestamptz not null default now(),\n  updated_at timestamptz not null default now()\n);\n\ncreate table if not exists public.proposal_operating_commitments (\n  id uuid primary key default gen_random_uuid(),\n  contract_id uuid not null references public.proposal_operating_contracts(id) on delete cascade,\n  proposal_id uuid not null references public.proposals(id) on delete cascade,\n  proposal_version integer not null check (proposal_version > 0),\n  issuer_tenant_id uuid not null references public.tenants(id) on delete cascade,\n  acceptor_tenant_id uuid references public.tenants(id) on delete set null,\n  party_role text not null check (party_role in ('issuer','acceptor','shared')),\n  responsible_tenant_id uuid references public.tenants(id) on delete set null,\n  commitment_key text not null,\n  title text not null,\n  description text,\n  category text not null default 'operating',\n  status text not null default 'proposed' check (status in ('proposed','active','blocked','completed','waived','ended')),\n  cadence text,\n  due_at timestamptz,\n  review_at timestamptz,\n  terms jsonb not null default '{}'::jsonb,\n  evidence jsonb not null default '{}'::jsonb,\n  activated_at timestamptz,\n  completed_at timestamptz,\n  created_at timestamptz not null default now(),\n  updated_at timestamptz not null default now(),\n  unique (contract_id, party_role, commitment_key)\n);\n\ncreate table if not exists public.proposal_operating_contract_events (\n  id uuid primary key default gen_random_uuid(),\n  contract_id uuid not null references public.proposal_operating_contracts(id) on delete cascade,\n  proposal_id uuid not null references public.proposals(id) on delete cascade,\n  event_type text not null check (event_type in ('drafted','sent','received','accepted','activated','declined','voided')),\n  source_type text not null default 'system',\n  source_id text,\n  event_data jsonb not null default '{}'::jsonb,\n  occurred_at timestamptz not null default now(),\n  dedupe_key text not null unique\n);\n\ncreate index if not exists proposal_operating_contracts_issuer_idx on public.proposal_operating_contracts (issuer_tenant_id,lifecycle_status,updated_at desc);\ncreate index if not exists proposal_operating_contracts_acceptor_idx on public.proposal_operating_contracts (acceptor_tenant_id,lifecycle_status,updated_at desc);\ncreate index if not exists proposal_operating_commitments_responsible_idx on public.proposal_operating_commitments (responsible_tenant_id,status,review_at,due_at);\ncreate index if not exists proposal_operating_commitments_proposal_idx on public.proposal_operating_commitments (proposal_id,proposal_version);\n\nalter table public.proposal_operating_contracts enable row level security;\nalter table public.proposal_operating_commitments enable row level security;\nalter table public.proposal_operating_contract_events enable row level security;\n\ndrop policy if exists proposal_operating_contracts_read on public.proposal_operating_contracts;\ncreate policy proposal_operating_contracts_read on public.proposal_operating_contracts for select using (public.is_platform_admin(auth.uid()) or public.is_tenant_member(auth.uid(),issuer_tenant_id) or (acceptor_tenant_id is not null and public.is_tenant_member(auth.uid(),acceptor_tenant_id)));\ndrop policy if exists proposal_operating_commitments_read on public.proposal_operating_commitments;\ncreate policy proposal_operating_commitments_read on public.proposal_operating_commitments for select using (public.is_platform_admin(auth.uid()) or public.is_tenant_member(auth.uid(),issuer_tenant_id) or (acceptor_tenant_id is not null and public.is_tenant_member(auth.uid(),acceptor_tenant_id)));\ndrop policy if exists proposal_operating_contract_events_read on public.proposal_operating_contract_events;\ncreate policy proposal_operating_contract_events_read on public.proposal_operating_contract_events for select using (public.is_platform_admin(auth.uid()) or exists (select 1 from public.proposal_operating_contracts c where c.id=proposal_operating_contract_events.contract_id and (public.is_tenant_member(auth.uid(),c.issuer_tenant_id) or (c.acceptor_tenant_id is not null and public.is_tenant_member(auth.uid(),c.acceptor_tenant_id)))));\n\ncreate or replace function public.activate_proposal_operating_contract(p_proposal_id uuid,p_signed_agreement_id uuid,p_pending_agreement_id uuid default null,p_signed_pdf_path text default null,p_signed_at timestamptz default now()) returns uuid language plpgsql security definer set search_path=public as $$\ndeclare v_contract public.proposal_operating_contracts%rowtype; v_owner_proposal_id uuid; v_signed_at timestamptz:=coalesce(p_signed_at,now()); v_settings_entry jsonb;\nbegin\n select * into v_contract from public.proposal_operating_contracts where proposal_id=p_proposal_id for update;\n if not found then raise exception 'proposal_operating_contract_not_found'; end if;\n if v_contract.signed_agreement_id is not null and v_contract.lifecycle_status='activated' then return v_contract.id; end if;\n update public.proposals set status='accepted',accepted_at=coalesce(accepted_at,v_signed_at),updated_at=now() where id=p_proposal_id and current_version=v_contract.proposal_version and status not in ('declined','void');\n if not found then raise exception 'proposal_version_or_status_not_activatable'; end if;\n update public.proposal_operating_contracts set lifecycle_status='activated',pending_agreement_id=coalesce(p_pending_agreement_id,pending_agreement_id),signed_agreement_id=p_signed_agreement_id,signed_pdf_path=coalesce(p_signed_pdf_path,signed_pdf_path),signed_at=coalesce(signed_at,v_signed_at),accepted_at=coalesce(accepted_at,v_signed_at),activated_at=coalesce(activated_at,v_signed_at),updated_at=now() where id=v_contract.id;\n update public.proposal_operating_commitments set status=case when status='proposed' then 'active' else status end,activated_at=coalesce(activated_at,v_signed_at),review_at=coalesce(review_at,case when coalesce((terms->>'offset_days')::integer,0)>0 then v_signed_at+make_interval(days=>(terms->>'offset_days')::integer) else null end),updated_at=now() where contract_id=v_contract.id;\n insert into public.proposal_operating_contract_events(contract_id,proposal_id,event_type,source_type,source_id,event_data,occurred_at,dedupe_key) values\n (v_contract.id,p_proposal_id,'received','signed_agreement',p_signed_agreement_id::text,jsonb_build_object('pending_agreement_id',p_pending_agreement_id,'signed_pdf_path',p_signed_pdf_path),v_signed_at,v_contract.id::text||':received'),\n (v_contract.id,p_proposal_id,'accepted','signed_agreement',p_signed_agreement_id::text,'{}'::jsonb,v_signed_at,v_contract.id::text||':accepted'),\n (v_contract.id,p_proposal_id,'activated','signed_agreement',p_signed_agreement_id::text,'{}'::jsonb,v_signed_at,v_contract.id::text||':activated') on conflict(dedupe_key) do nothing;\n select id into v_owner_proposal_id from public.owner_relationship_proposals where canonical_proposal_id=p_proposal_id order by version desc limit 1;\n if v_owner_proposal_id is not null then\n  update public.owner_relationship_proposals set status='accepted',accepted_at=coalesce(accepted_at,v_signed_at),updated_at=now() where id=v_owner_proposal_id and status not in ('declined','superseded');\n  update public.owner_compensation_terms set status='active',effective_start=coalesce(effective_start,v_signed_at::date),updated_at=now() where proposal_id=v_owner_proposal_id and status='proposed';\n end if;\n insert into public.owner_relationship_followups(relationship_id,tenant_id,title,due_at,status,related_proposal_id,metadata)\n select rp.relationship_id,rp.tenant_id,c.title,c.review_at,'open',rp.id,jsonb_build_object('proposal_operating_contract_id',v_contract.id,'proposal_commitment_id',c.id,'source','accepted_proposal') from public.proposal_operating_commitments c join public.owner_relationship_proposals rp on rp.id=v_owner_proposal_id where c.contract_id=v_contract.id and c.party_role in ('issuer','shared') and c.review_at is not null and not exists(select 1 from public.owner_relationship_followups f where f.metadata->>'proposal_commitment_id'=c.id::text);\n insert into public.tasks(tenant_id,user_id,assigned_to,title,description,priority,status,due_date,source_kind,source_id,metadata)\n select c.responsible_tenant_id::text,u.user_id,u.user_id,c.title,c.description,'medium','pending',coalesce(c.due_at,c.review_at),'accepted_proposal_commitment',c.id::text,jsonb_build_object('proposal_id',c.proposal_id,'proposal_version',c.proposal_version,'proposal_operating_contract_id',v_contract.id,'party_role',c.party_role,'category',c.category,'cadence',c.cadence,'source','accepted_proposal') from public.proposal_operating_commitments c join lateral (select vu.user_id from public.v_active_tenant_users vu where vu.tenant_id=c.responsible_tenant_id and coalesce(vu.is_owner,false)=true and coalesce(vu.profile_active,true)=true and coalesce(vu.membership_status,'active')='active' order by vu.joined_at nulls last,vu.user_id limit 1) u on true where c.contract_id=v_contract.id and c.status='active' and c.responsible_tenant_id is not null and not exists(select 1 from public.tasks t where t.source_kind='accepted_proposal_commitment' and t.source_id=c.id::text);\n v_settings_entry:=jsonb_build_object('proposalId',p_proposal_id,'contractId',v_contract.id,'status','active','proposalVersion',v_contract.proposal_version,'activatedAt',v_signed_at);\n update public.tenants set settings=jsonb_set(coalesce(settings,'{}'::jsonb),'{operating_contracts}',coalesce(settings->'operating_contracts','{}'::jsonb)||jsonb_build_object(v_contract.id::text,v_settings_entry||jsonb_build_object('role',case when id=v_contract.issuer_tenant_id then 'issuer' else 'acceptor' end,'counterpartyTenantId',case when id=v_contract.issuer_tenant_id then v_contract.acceptor_tenant_id else v_contract.issuer_tenant_id end)),true) where id=v_contract.issuer_tenant_id or id=v_contract.acceptor_tenant_id;\n return v_contract.id;\nend;$$;\n\ncreate or replace function public.activate_proposal_operating_contract_from_signed_agreement() returns trigger language plpgsql security definer set search_path=public as $$\ndeclare v_vars jsonb:=coalesce(new.metadata->'source_variables','{}'::jsonb); v_proposal_id_text text; v_proposal_id uuid; v_contract public.proposal_operating_contracts%rowtype;\nbegin\n if lower(coalesce(v_vars->>'PROPOSAL_OPERATING_CONTRACT',v_vars->>'proposal_operating_contract','')) not in ('true','1','yes') then return new; end if;\n v_proposal_id_text:=coalesce(v_vars->>'PROPOSAL_ID',v_vars->>'proposal_id','');\n if v_proposal_id_text !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' then raise exception 'proposal_operating_contract_proposal_id_invalid'; end if;\n v_proposal_id:=v_proposal_id_text::uuid; select * into v_contract from public.proposal_operating_contracts where proposal_id=v_proposal_id;\n if not found then raise exception 'proposal_operating_contract_not_found'; end if;\n if new.tenant_id is distinct from v_contract.issuer_tenant_id then raise exception 'proposal_operating_contract_issuer_mismatch'; end if;\n if coalesce(v_vars->>'PROPOSAL_VERSION',v_vars->>'proposal_version','')<>v_contract.proposal_version::text then raise exception 'proposal_operating_contract_version_mismatch'; end if;\n perform public.activate_proposal_operating_contract(v_proposal_id,new.id,nullif(coalesce(v_vars->>'PENDING_AGREEMENT_ID',v_vars->>'pending_agreement_id',''),'')::uuid,coalesce(new.metadata->>'filled_pdf_path',new.pdf_url),new.signed_at); return new;\nend;$$;\ndrop trigger if exists tg_activate_proposal_operating_contract on public.signed_agreements;\ncreate trigger tg_activate_proposal_operating_contract after insert on public.signed_agreements for each row execute function public.activate_proposal_operating_contract_from_signed_agreement();\n\ncreate or replace function public.sync_proposal_render_to_document_family() returns trigger language plpgsql security definer set search_path=public as $$\ndeclare v_document_id uuid;\nbegin\n if new.render_path is null or new.render_bucket is null then return new; end if;\n begin v_document_id:=nullif(new.metadata->>'canonicalDocumentId','')::uuid; exception when others then v_document_id:=null; end;\n if v_document_id is null then\n  insert into public.documents(scope,category,name,mime_type,uploaded_by,metadata,tenant_id) values('tenant','proposal',new.proposal_number||'.pdf','application/pdf',new.created_by,jsonb_build_object('documentFamily','proposal','proposalId',new.id,'proposalVersion',new.current_version,'storageBucket',new.render_bucket,'storagePath',new.render_path,'renderHash',new.render_hash),new.tenant_id) returning id into v_document_id;\n  new.metadata:=coalesce(new.metadata,'{}'::jsonb)||jsonb_build_object('canonicalDocumentId',v_document_id,'documentFamily','proposal');\n else\n  update public.documents set name=new.proposal_number||'.pdf',mime_type='application/pdf',metadata=coalesce(metadata,'{}'::jsonb)||jsonb_build_object('proposalVersion',new.current_version,'storageBucket',new.render_bucket,'storagePath',new.render_path,'renderHash',new.render_hash) where id=v_document_id;\n end if;\n insert into public.document_activity(tenant_org_id,document_id,document_type,activity_type,activity_data,user_id) values(new.tenant_id::text,v_document_id::text,'proposal','proposal_rendered',jsonb_build_object('proposal_id',new.id,'proposal_version',new.current_version,'render_hash',new.render_hash),new.created_by); return new;\nend;$$;\ndrop trigger if exists tg_sync_proposal_render_to_document_family on public.proposals;\ncreate trigger tg_sync_proposal_render_to_document_family before insert or update of render_path,render_hash on public.proposals for each row when(new.render_path is not null and new.render_hash is not null) execute function public.sync_proposal_render_to_document_family();\n\n-- Revised Redfish partnership seed: draft only. No message or signature request is sent by this migration.\ndo $$\ndeclare v_issuer uuid; v_acceptor uuid; v_profile uuid; v_proposal uuid; v_contract uuid; v_relationship uuid; v_old_owner_proposal uuid; v_owner_proposal uuid;\nv_commitments jsonb:=$JSON$[\n{\"party\":\"issuer\",\"key\":\"knocklaunch-os\",\"title\":\"Maintain and evolve Redfish's KnockLaunch OS workspace\",\"description\":\"Configure, improve and support Redfish-specific workflows, automation and practical custom development as the business changes.\",\"category\":\"technology\",\"cadence\":\"ongoing\"},\n{\"party\":\"issuer\",\"key\":\"website-seo\",\"title\":\"Website, SEO and digital growth management\",\"description\":\"Manage the website, search visibility, landing paths, source tracking and measurable digital acquisition work.\",\"category\":\"growth\",\"cadence\":\"ongoing\"},\n{\"party\":\"issuer\",\"key\":\"attribution-reporting\",\"title\":\"Track attribution and growth performance\",\"description\":\"Keep source, opportunity, conversion, collected revenue and campaign performance visible so decisions are based on evidence.\",\"category\":\"growth\",\"cadence\":\"monthly\"},\n{\"party\":\"issuer\",\"key\":\"aerial-media\",\"title\":\"Aerial and field media production\",\"description\":\"Provide drone/aerial imagery and video plus on-site capture when appropriate and scheduled.\",\"category\":\"media\",\"cadence\":\"as-needed\"},\n{\"party\":\"issuer\",\"key\":\"video-production\",\"title\":\"Video production, editing and repurposing\",\"description\":\"Shoot, edit and repurpose useful Redfish video for campaigns, social channels, sales and brand storytelling.\",\"category\":\"media\",\"cadence\":\"ongoing\"},\n{\"party\":\"issuer\",\"key\":\"studio-audio\",\"title\":\"Voiceover and audio production\",\"description\":\"Use Noel's recording-studio capability for voiceovers, audio polish and related production when useful.\",\"category\":\"media\",\"cadence\":\"as-needed\"},\n{\"party\":\"issuer\",\"key\":\"podcast-pilot\",\"title\":\"Produce the first Redfish podcast pilot at no charge\",\"description\":\"Plan and produce the initial pilot podcast session as a complimentary test before deciding on an ongoing format.\",\"category\":\"media\",\"cadence\":\"one-time\"},\n{\"party\":\"issuer\",\"key\":\"marketing-coordination\",\"title\":\"Coordinate with Redfish's other marketing resources\",\"description\":\"Work with approved outside marketing resources so video, SEO, social, AccuLynx and KnockLaunch efforts complement rather than duplicate each other.\",\"category\":\"coordination\",\"cadence\":\"ongoing\"},\n{\"party\":\"issuer\",\"key\":\"trade-workflow\",\"title\":\"Support the full Roofing, Windows and Exteriors workflow\",\"description\":\"Keep Redfish workflows useful for roofing, windows and exteriors, including roof-type capture, contractor/supplier relationships, canvassing, photo/document organization and distinct proposal versus estimate workflows.\",\"category\":\"technology\",\"cadence\":\"ongoing\"},\n{\"party\":\"issuer\",\"key\":\"working-rhythm\",\"title\":\"Maintain an active working rhythm with Jimmy\",\"description\":\"Execute, communicate and reprioritize often enough that Jimmy can see what is moving, Noel can see what Redfish needs, and useful work does not stall between departments or vendors.\",\"category\":\"coordination\",\"cadence\":\"weekly\"},\n{\"party\":\"issuer\",\"key\":\"review-90\",\"title\":\"90-day partnership review\",\"description\":\"Review results, workload, lead quality, media output and operating priorities after 90 days.\",\"category\":\"review\",\"cadence\":\"one-time\",\"offset_days\":90},\n{\"party\":\"issuer\",\"key\":\"review-180\",\"title\":\"Six-month partnership review\",\"description\":\"Review actual production and decide the long-term structure at the end of the initial six-month term.\",\"category\":\"review\",\"cadence\":\"one-time\",\"offset_days\":180},\n{\"party\":\"acceptor\",\"key\":\"weekly-retainer\",\"title\":\"Pay the $1,000 weekly partnership retainer\",\"description\":\"The weekly base pays for the ongoing growth, media and technology partnership and is separate from performance compensation.\",\"category\":\"compensation\",\"cadence\":\"weekly\"},\n{\"party\":\"acceptor\",\"key\":\"personal-sales\",\"title\":\"Honor the personal roofing sales lane\",\"description\":\"For roofing jobs Noel personally sources: $1,000 draw at signed contract, then 15% total sales commission when the job is fully collected, with the draw credited against that commission.\",\"category\":\"compensation\",\"cadence\":\"per-qualifying-job\"},\n{\"party\":\"acceptor\",\"key\":\"digital-performance\",\"title\":\"Honor the 5% digital-originated revenue lane\",\"description\":\"Pay 5% of qualifying collected revenue from non-personal jobs first attributable to the website, KnockLaunch, SEO or other digital channels Noel builds or manages. This lane does not overlap the 15% personal-sales lane and is not netted against the weekly retainer.\",\"category\":\"compensation\",\"cadence\":\"per-qualifying-collection\"},\n{\"party\":\"acceptor\",\"key\":\"roofing-operations\",\"title\":\"Own licensed roofing operations and fulfillment\",\"description\":\"Redfish remains responsible for inspections, professional roofing determinations, estimates/final scope, crews, permitting, customer service, warranty and licensed trade work.\",\"category\":\"operations\",\"cadence\":\"ongoing\"},\n{\"party\":\"acceptor\",\"key\":\"approved-external-costs\",\"title\":\"Cover approved third-party campaign and production costs\",\"description\":\"Paid advertising, printing, subscriptions, travel or other third-party costs require Redfish approval before purchase and remain Redfish expenses.\",\"category\":\"operations\",\"cadence\":\"as-needed\"},\n{\"party\":\"acceptor\",\"key\":\"field-materials\",\"title\":\"Provide professional Redfish field materials\",\"description\":\"When Noel is meeting customers or representing Redfish, provide branded shirts or polos plus business cards and approved sales materials so he can present as part of the company.\",\"category\":\"operations\",\"cadence\":\"at-start\"},\n{\"party\":\"acceptor\",\"key\":\"system-access\",\"title\":\"Provide approved system access when needed\",\"description\":\"Provide the access or credentials Noel reasonably needs for approved Redfish systems and connected workflows, subject to Redfish approval and security controls.\",\"category\":\"operations\",\"cadence\":\"as-needed\"},\n{\"party\":\"shared\",\"key\":\"no-separate-platform-fee\",\"title\":\"No separate KnockLaunch OS core platform fee during the initial term\",\"description\":\"The core Redfish KnockLaunch OS platform and ongoing configuration are included in the weekly partnership during the six-month initial term; approved external services remain separate.\",\"category\":\"commercial-term\",\"cadence\":\"six-month-term\"},\n{\"party\":\"shared\",\"key\":\"redfish-data-ownership\",\"title\":\"Redfish keeps ownership of its customer and business records\",\"description\":\"KnockLaunch OS is the working operating layer for Redfish and does not claim ownership of Redfish customer or business records.\",\"category\":\"business-term\",\"cadence\":\"ongoing\"}\n]$JSON$::jsonb;\nbegin\n select id into v_issuer from public.tenants where company_slug='knocklaunch-crm' limit 1; select id into v_acceptor from public.tenants where company_slug='redfish-roofing' limit 1; if v_issuer is null or v_acceptor is null then return; end if;\n insert into public.proposal_profiles(tenant_id,profile_key,tenant_type,status,number_prefix,renderer_key,version,config)\n select v_issuer,'growth-partnership',t.tenant_type,'active','KLG','tenant_profile_v1',1,jsonb_build_object('application','proposal-editor','businessCategory','growth-partnership','documentTitle','Integrated Growth, Media & Technology Partnership','contextKind','business-partnership','defaultOfferKey','integrated-partnership','terminology',jsonb_build_object('workspaceTitle','Partnership Proposals','customerLabel','Prepared for','summaryLabel','Partnership scope'),'offers',jsonb_build_array(jsonb_build_object('key','integrated-partnership','label','Integrated partnership','lineKeys',jsonb_build_array('weekly_retainer'),'pricingMode','combined')),'commercial',jsonb_build_object('pricing',jsonb_build_object('weekly_retainer',jsonb_build_object('kind','service','label','Integrated growth, media & technology partnership','quantity',1,'unitLabel','week','unitPrice',1000,'quantityRule','fixed')),'compensationLanes',jsonb_build_array(jsonb_build_object('key','weekly_retainer','label','$1,000 weekly base retainer','separate',true),jsonb_build_object('key','personal_sales','label','$1,000 signing draw + 15% personally sourced roofing jobs','separate',true),jsonb_build_object('key','digital_originated','label','5% qualifying digitally originated collected revenue','separate',true)))) from public.tenants t where t.id=v_issuer\n on conflict(tenant_id,profile_key) do update set config=excluded.config,status='active',updated_at=now();\n select id into v_profile from public.proposal_profiles where tenant_id=v_issuer and profile_key='growth-partnership'; select id into v_proposal from public.proposals where tenant_id=v_issuer and idempotency_key='knocklaunch:redfish:growth-media-technology:v2';\n if v_proposal is null then\n  insert into public.proposals(tenant_id,profile_id,proposal_number,status,current_version,idempotency_key,counterparty_tenant_id,offer_key,pricing_mode,currency,recipient_snapshot,context_snapshot,line_items,subtotal_amount,adjustment_amount,tax_amount,total_amount,balance_amount,terms,profile_snapshot,brand_snapshot,commercial_snapshot,created_by,metadata)\n  select v_issuer,v_profile,'KLG-REDFISH-2026-01','draft',1,'knocklaunch:redfish:growth-media-technology:v2',v_acceptor,'integrated-partnership','combined','USD',jsonb_build_object('name','Jimmy Eldredge','email','jeldredge@redfishroofingfl.com','company','Redfish Roofing'),jsonb_build_object('kind','business-partnership','counterpartyTenantId',v_acceptor,'counterpartySlug','redfish-roofing','trades',jsonb_build_array('Roofing','Windows','Exteriors'),'termMonths',6,'reviewDay',90),jsonb_build_array(jsonb_build_object('id',gen_random_uuid(),'key','weekly_retainer','kind','service','label','Integrated growth, media & technology partnership','description','Ongoing embedded partnership across KnockLaunch OS, growth, SEO, media production and coordination.','quantity',1,'unitLabel','week','unitPrice',1000)),1000,0,0,1000,1000,'Initial six-month working structure. $1,000/week base retainer is separate from the $1,000 signing draw + 15% personal roofing sales lane and the 5% qualifying digital-originated collected-revenue lane. First podcast pilot is complimentary. Review at 90 days and month six.',p.config,jsonb_build_object('businessName','Knock Launch OS','legalName',coalesce(t.company_legal_name,t.name),'logoUrl',t.company_logo_url,'email',t.company_email,'website',t.company_website),p.config->'commercial',null,jsonb_build_object('documentFamily','proposal','documentArchitecture','canonical-document-family','operatingContract',jsonb_build_object('version',1,'acceptanceMode','knocklaunch-sign','issuerTenantId',v_issuer,'acceptorTenantId',v_acceptor,'commitments',v_commitments,'signatureFields',jsonb_build_array(jsonb_build_object('id','acceptor-signature','type','signature','label','Signature','required',true,'page',15,'x',8,'y',80,'width',42,'height',7),jsonb_build_object('id','acceptor-date','type','date','label','Date','required',true,'page',15,'x',55,'y',80,'width',28,'height',5)))) from public.proposal_profiles p join public.tenants t on t.id=v_issuer where p.id=v_profile returning id into v_proposal;\n end if;\n insert into public.proposal_versions(proposal_id,tenant_id,version,reason,snapshot,snapshot_hash,created_by) select v_proposal,v_issuer,1,'seed_revised_redfish_partnership',jsonb_build_object('proposalId',v_proposal,'tenantId',v_issuer,'status','draft','version',1,'counterpartyTenantId',v_acceptor),encode(digest(v_proposal::text||':1:redfish-growth-partnership-v2','sha256'),'hex'),null where not exists(select 1 from public.proposal_versions where proposal_id=v_proposal and version=1);\n insert into public.proposal_operating_contracts(proposal_id,proposal_version,issuer_tenant_id,acceptor_tenant_id,lifecycle_status,terms,metadata) values(v_proposal,1,v_issuer,v_acceptor,'draft',jsonb_build_object('termMonths',6,'reviewDay',90,'commercialLanes',jsonb_build_array('weekly_retainer','personal_sales','digital_originated')),jsonb_build_object('seed','redfish-growth-partnership-v2')) on conflict(proposal_id) do update set acceptor_tenant_id=excluded.acceptor_tenant_id,terms=excluded.terms,metadata=excluded.metadata,updated_at=now(); select id into v_contract from public.proposal_operating_contracts where proposal_id=v_proposal;\n insert into public.proposal_operating_commitments(contract_id,proposal_id,proposal_version,issuer_tenant_id,acceptor_tenant_id,party_role,responsible_tenant_id,commitment_key,title,description,category,status,cadence,terms) select v_contract,v_proposal,1,v_issuer,v_acceptor,x.party,case when x.party='issuer' then v_issuer when x.party='acceptor' then v_acceptor else null end,x.key,x.title,x.description,x.category,'proposed',x.cadence,case when x.offset_days is not null then jsonb_build_object('offset_days',x.offset_days) else '{}'::jsonb end from jsonb_to_recordset(v_commitments) as x(party text,key text,title text,description text,category text,cadence text,offset_days integer) on conflict(contract_id,party_role,commitment_key) do update set title=excluded.title,description=excluded.description,category=excluded.category,cadence=excluded.cadence,terms=excluded.terms,updated_at=now();\n insert into public.proposal_operating_contract_events(contract_id,proposal_id,event_type,source_type,source_id,event_data,dedupe_key) values(v_contract,v_proposal,'drafted','seed','redfish-growth-partnership-v2',jsonb_build_object('accepted',false),v_contract::text||':drafted') on conflict(dedupe_key) do nothing;\n select id into v_relationship from public.owner_tenant_relationships where tenant_id=v_acceptor order by created_at asc limit 1;\n if v_relationship is not null then\n  select id into v_old_owner_proposal from public.owner_relationship_proposals where tenant_id=v_acceptor and source_id='redfish-jimmy-six-month-growth-partnership-v1' order by version desc limit 1; select id into v_owner_proposal from public.owner_relationship_proposals where tenant_id=v_acceptor and source_id='redfish-jimmy-integrated-growth-media-technology-v2' order by version desc limit 1;\n  if v_owner_proposal is null then insert into public.owner_relationship_proposals(relationship_id,tenant_id,title,recipient_name,recipient_email,status,terms_summary,attachment_ref,version,supersedes_id,source,source_id,metadata,canonical_proposal_id) values(v_relationship,v_acceptor,'Integrated Growth, Media & Technology Partnership','Jimmy Eldredge','jeldredge@redfishroofingfl.com','draft','$1,000/week base partnership retainer, separate $1,000 signing draw + 15% personal roofing sales lane, separate 5% qualifying digital-originated collected revenue, six-month initial term with 90-day review.',jsonb_build_object('status','attach_enhanced_final_artifact','filename','Redfish_Roofing_Integrated_Growth_Media_Technology_Partnership_2026.pdf'),2,v_old_owner_proposal,'owner_seed','redfish-jimmy-integrated-growth-media-technology-v2',jsonb_build_object('term_months',6,'review_day',90,'platform_access','Knock Launch OS included in the integrated partnership','canonical_proposal_id',v_proposal,'acceptance_activation','automatic_two_sided_operating_contract'),v_proposal) returning id into v_owner_proposal; else update public.owner_relationship_proposals set canonical_proposal_id=v_proposal,updated_at=now() where id=v_owner_proposal; end if;\n  if v_old_owner_proposal is not null then update public.owner_relationship_proposals set status='superseded',updated_at=now() where id=v_old_owner_proposal and status<>'accepted'; update public.owner_compensation_terms set status='superseded',updated_at=now() where proposal_id=v_old_owner_proposal and status='proposed'; end if;\n  insert into public.owner_compensation_terms(relationship_id,tenant_id,proposal_id,lane_key,label,status,earning_type,rate_pct,fixed_amount,currency,trigger_event,settlement_rule,version,notes,metadata)\n  select v_relationship,v_acceptor,v_owner_proposal,x.lane_key,x.label,'proposed',x.earning_type,x.rate_pct,x.fixed_amount,'USD',x.trigger_event,x.settlement_rule,2,x.notes,x.metadata from(values\n   ('redfish_weekly_partnership','$1,000 weekly partnership retainer','custom'::text,null::numeric,1000::numeric,'Each active service week during the partnership.','Separate base compensation for the integrated growth, media and technology role. It is not credited against or netted from the 15% personal-sales lane or the 5% digital-originated lane.','Base partnership compensation.',jsonb_build_object('cadence','weekly','separate_from_performance',true)),\n   ('redfish_personal_roofing_sales','Personal roofing sales','personal_sales_commission',15::numeric,null::numeric,'A roofing lead Noel personally sources signs a Redfish contract; final commission settles when that specific job is fully collected.','$1,000 signing draw, then 15% total commission on collected job revenue with the draw credited against the final commission. No overlap with the 5% digital lane.','Personal roofing sales only.',jsonb_build_object('signing_draw',1000,'draw_is_credit_against_commission',true,'no_digital_stacking',true)),\n   ('redfish_digital_originated_revenue','Qualifying digital-originated revenue','digital_originated_percentage',5::numeric,null::numeric,'Redfish collects revenue on a qualifying non-personal job first attributable to Noel-managed website, KnockLaunch, SEO, landing page, tracked form/call or campaign channels.','5% of qualifying collected revenue. The weekly retainer is separate. Personal jobs stay exclusively in the 15% personal-sales lane.','Digital performance lane only.',jsonb_build_object('collected_revenue_basis',true,'weekly_retainer_is_separate',true,'no_personal_commission_stacking',true))\n  ) as x(lane_key,label,earning_type,rate_pct,fixed_amount,trigger_event,settlement_rule,notes,metadata) where not exists(select 1 from public.owner_compensation_terms e where e.proposal_id=v_owner_proposal and e.lane_key=x.lane_key and e.version=2);\n end if;\nend;$$;\n"],"idempotency_key":"10fd9c44137283bfac92ebb0e180c2a5f9f8c6f7:20260911163000"},{"name":"fix_proposal_operating_contract_task_source","version":"20260911170000","rollback":null,"created_by":"canonical-production-release","statements":["-- Correct accepted-proposal task creation for generated source columns.\n-- tasks.source_kind and tasks.source_id are GENERATED ALWAYS from metadata.\n\ncreate or replace function public.activate_proposal_operating_contract(\n  p_proposal_id uuid,\n  p_signed_agreement_id uuid,\n  p_pending_agreement_id uuid default null,\n  p_signed_pdf_path text default null,\n  p_signed_at timestamptz default now()\n) returns uuid\nlanguage plpgsql\nsecurity definer\nset search_path=public\nas $$\ndeclare\n  v_contract public.proposal_operating_contracts%rowtype;\n  v_owner_proposal_id uuid;\n  v_signed_at timestamptz:=coalesce(p_signed_at,now());\n  v_settings_entry jsonb;\nbegin\n  select * into v_contract from public.proposal_operating_contracts where proposal_id=p_proposal_id for update;\n  if not found then raise exception 'proposal_operating_contract_not_found'; end if;\n  if v_contract.signed_agreement_id is not null and v_contract.lifecycle_status='activated' then return v_contract.id; end if;\n\n  update public.proposals set status='accepted',accepted_at=coalesce(accepted_at,v_signed_at),updated_at=now()\n  where id=p_proposal_id and current_version=v_contract.proposal_version and status not in ('declined','void');\n  if not found then raise exception 'proposal_version_or_status_not_activatable'; end if;\n\n  update public.proposal_operating_contracts set lifecycle_status='activated',pending_agreement_id=coalesce(p_pending_agreement_id,pending_agreement_id),signed_agreement_id=p_signed_agreement_id,signed_pdf_path=coalesce(p_signed_pdf_path,signed_pdf_path),signed_at=coalesce(signed_at,v_signed_at),accepted_at=coalesce(accepted_at,v_signed_at),activated_at=coalesce(activated_at,v_signed_at),updated_at=now() where id=v_contract.id;\n\n  update public.proposal_operating_commitments set status=case when status='proposed' then 'active' else status end,activated_at=coalesce(activated_at,v_signed_at),review_at=coalesce(review_at,case when coalesce((terms->>'offset_days')::integer,0)>0 then v_signed_at+make_interval(days=>(terms->>'offset_days')::integer) else null end),updated_at=now() where contract_id=v_contract.id;\n\n  insert into public.proposal_operating_contract_events(contract_id,proposal_id,event_type,source_type,source_id,event_data,occurred_at,dedupe_key) values\n  (v_contract.id,p_proposal_id,'received','signed_agreement',p_signed_agreement_id::text,jsonb_build_object('pending_agreement_id',p_pending_agreement_id,'signed_pdf_path',p_signed_pdf_path),v_signed_at,v_contract.id::text||':received'),\n  (v_contract.id,p_proposal_id,'accepted','signed_agreement',p_signed_agreement_id::text,'{}'::jsonb,v_signed_at,v_contract.id::text||':accepted'),\n  (v_contract.id,p_proposal_id,'activated','signed_agreement',p_signed_agreement_id::text,'{}'::jsonb,v_signed_at,v_contract.id::text||':activated') on conflict(dedupe_key) do nothing;\n\n  select id into v_owner_proposal_id from public.owner_relationship_proposals where canonical_proposal_id=p_proposal_id order by version desc limit 1;\n  if v_owner_proposal_id is not null then\n    update public.owner_relationship_proposals set status='accepted',accepted_at=coalesce(accepted_at,v_signed_at),updated_at=now() where id=v_owner_proposal_id and status not in ('declined','superseded');\n    update public.owner_compensation_terms set status='active',effective_start=coalesce(effective_start,v_signed_at::date),updated_at=now() where proposal_id=v_owner_proposal_id and status='proposed';\n  end if;\n\n  insert into public.owner_relationship_followups(relationship_id,tenant_id,title,due_at,status,related_proposal_id,metadata)\n  select rp.relationship_id,rp.tenant_id,c.title,c.review_at,'open',rp.id,jsonb_build_object('proposal_operating_contract_id',v_contract.id,'proposal_commitment_id',c.id,'source','accepted_proposal')\n  from public.proposal_operating_commitments c join public.owner_relationship_proposals rp on rp.id=v_owner_proposal_id\n  where c.contract_id=v_contract.id and c.party_role in ('issuer','shared') and c.review_at is not null\n    and not exists(select 1 from public.owner_relationship_followups f where f.metadata->>'proposal_commitment_id'=c.id::text);\n\n  insert into public.tasks(tenant_id,user_id,assigned_to,title,description,priority,status,due_date,metadata)\n  select c.responsible_tenant_id::text,u.user_id,u.user_id,c.title,c.description,'medium','pending',coalesce(c.due_at,c.review_at),\n    jsonb_build_object('proposal_id',c.proposal_id,'proposal_version',c.proposal_version,'proposal_operating_contract_id',v_contract.id,'proposal_commitment_id',c.id,'party_role',c.party_role,'category',c.category,'cadence',c.cadence,'source','accepted_proposal','source_kind','accepted_proposal_commitment','source_id',c.id::text)\n  from public.proposal_operating_commitments c\n  join lateral (\n    select vu.user_id from public.v_active_tenant_users vu\n    where vu.tenant_id=c.responsible_tenant_id and coalesce(vu.is_owner,false)=true and coalesce(vu.profile_active,true)=true and coalesce(vu.membership_status,'active')='active'\n    order by vu.joined_at nulls last,vu.user_id limit 1\n  ) u on true\n  where c.contract_id=v_contract.id and c.status='active' and c.responsible_tenant_id is not null\n    and not exists(select 1 from public.tasks t where t.source_kind='accepted_proposal_commitment' and t.source_id=c.id::text);\n\n  v_settings_entry:=jsonb_build_object('proposalId',p_proposal_id,'contractId',v_contract.id,'status','active','proposalVersion',v_contract.proposal_version,'activatedAt',v_signed_at);\n  update public.tenants set settings=jsonb_set(coalesce(settings,'{}'::jsonb),'{operating_contracts}',coalesce(settings->'operating_contracts','{}'::jsonb)||jsonb_build_object(v_contract.id::text,v_settings_entry||jsonb_build_object('role',case when id=v_contract.issuer_tenant_id then 'issuer' else 'acceptor' end,'counterpartyTenantId',case when id=v_contract.issuer_tenant_id then v_contract.acceptor_tenant_id else v_contract.issuer_tenant_id end)),true)\n  where id=v_contract.issuer_tenant_id or id=v_contract.acceptor_tenant_id;\n  return v_contract.id;\nend;\n$$;\n"],"idempotency_key":"035de58129e96650964a99158f4eade22405487c:20260911170000"},{"name":"platform_business_activity_ledger","version":"20260911183000","rollback":null,"created_by":"canonical-production-release","statements":["-- KNOCKLAUNCH platform Business Activity Ledger.\n-- Specialized systems keep operational state; material business activity projects into this append-only spine.\n\nalter table public.activity_timeline\n  add column if not exists event_domain text not null default 'activity',\n  add column if not exists source_channel text,\n  add column if not exists touch_channel text,\n  add column if not exists source_detail text,\n  add column if not exists source_campaign text,\n  add column if not exists source_referrer text,\n  add column if not exists source_system text not null default 'knocklaunch',\n  add column if not exists source_record_type text,\n  add column if not exists source_record_id text,\n  add column if not exists correlation_key text,\n  add column if not exists actor_role text,\n  add column if not exists previous_state text,\n  add column if not exists new_state text,\n  add column if not exists reason_code text,\n  add column if not exists monetary_value numeric(14,2),\n  add column if not exists currency text not null default 'USD',\n  add column if not exists outcome text,\n  add column if not exists corrects_event_id uuid references public.activity_timeline(id) on delete restrict,\n  add column if not exists dedupe_key text,\n  add column if not exists recorded_at timestamptz not null default now();\n\n-- The legacy timeline check predates the canonical ledger vocabulary. Replace it\n-- before any trigger or historical backfill can emit the new event families.\nalter table public.activity_timeline\n  drop constraint if exists activity_timeline_activity_type_check;\nalter table public.activity_timeline\n  add constraint activity_timeline_activity_type_check check (\n    activity_type = any (array[\n      'call','email','sms','note','task','meeting','status_change','stage_change',\n      'assignment','document','photo','automation','score_change','merge','import',\n      'export','field_update','contact_created','contact_updated','job_created',\n      'job_updated','invoice_created','invoice_paid','invoice_updated',\n      'settlement_invoice_created','settlement_invoice_paid','settlement_invoice_updated',\n      'business_joined_platform','business_lifecycle_updated'\n    ]::text[])\n    or activity_type ~ '^(opportunity|communication|document|user)_[A-Za-z0-9_.:-]+$'\n  );\n\ncreate index if not exists activity_timeline_tenant_created_idx on public.activity_timeline (tenant_org_id, created_at desc);\ncreate index if not exists activity_timeline_contact_created_idx on public.activity_timeline (tenant_org_id, contact_id, created_at desc) where contact_id is not null;\ncreate index if not exists activity_timeline_source_created_idx on public.activity_timeline (tenant_org_id, source_channel, created_at desc) where source_channel is not null;\ncreate index if not exists activity_timeline_touch_created_idx on public.activity_timeline (tenant_org_id, touch_channel, created_at desc) where touch_channel is not null;\ncreate index if not exists activity_timeline_correlation_idx on public.activity_timeline (tenant_org_id, correlation_key, created_at desc) where correlation_key is not null;\ncreate unique index if not exists activity_timeline_tenant_dedupe_uidx on public.activity_timeline (tenant_org_id, dedupe_key) where dedupe_key is not null;\n\ncreate or replace function public.business_activity_safe_uuid(_value text)\nreturns uuid language plpgsql immutable set search_path = public, pg_temp as $$\nbegin\n  if nullif(btrim(coalesce(_value, '')), '') is null then return null; end if;\n  return _value::uuid;\nexception when invalid_text_representation then return null;\nend;\n$$;\n\ncreate or replace function public.normalize_business_source(_source text, _metadata jsonb default '{}'::jsonb)\nreturns text language plpgsql immutable set search_path = public, pg_temp as $$\ndeclare raw text := nullif(btrim(coalesce(_source, '')), ''); evidence text;\nbegin\n  evidence := lower(concat_ws(' ', raw, _metadata->>'source', _metadata->>'utm_source', _metadata->>'utm_medium', _metadata->>'medium', _metadata->>'provider', _metadata->>'referrer'));\n  if evidence ~ '(google[ _-]?ads|ppc|paid[ _-]?search|cpc|sem)' then return 'PPC'; end if;\n  if evidence ~ '(organic|seo|organic[ _-]?search)' then return 'SEO'; end if;\n  if evidence ~ 'roof[ _-]?help[ _-]?finder' then return 'RoofHelpFinder'; end if;\n  if evidence ~ '(canvass|door[ _-]?knock|field[ _-]?sales)' then return 'Canvassing'; end if;\n  if evidence ~ '(referr|partner)' then return 'Referral'; end if;\n  if evidence ~ '(facebook|instagram|tiktok|linkedin|nextdoor|yelp|twitter|social)' then return 'Social'; end if;\n  if evidence ~ '(phone|call|twilio)' then return 'Phone'; end if;\n  if evidence ~ '(email|mail)' then return 'Email'; end if;\n  if evidence ~ '(manual|entered[ _-]?by|admin)' then return 'Manual'; end if;\n  if raw is null then return 'Unknown'; end if;\n  return initcap(regexp_replace(raw, '[_-]+', ' ', 'g'));\nend;\n$$;\n\ncreate or replace function public.business_activity_assert_tenant_access(_tenant_id uuid)\nreturns void language plpgsql security invoker set search_path = public, auth, pg_temp as $$\nbegin\n  if coalesce(auth.role(), '') = 'service_role' then return; end if;\n  if auth.uid() is null then raise exception 'business_activity_authentication_required' using errcode = '42501'; end if;\n  if public.is_platform_admin(auth.uid()) or public.ai_user_in_tenant(_tenant_id) then return; end if;\n  raise exception 'business_activity_tenant_access_denied' using errcode = '42501';\nend;\n$$;\n\ncreate or replace function public.activity_timeline_normalize_insert()\nreturns trigger language plpgsql security definer set search_path = public, auth, pg_temp as $$\nbegin\n  new.event_domain := coalesce(nullif(btrim(new.event_domain), ''), 'activity');\n  if nullif(btrim(coalesce(new.source_channel, '')), '') is not null then\n    new.source_channel := public.normalize_business_source(new.source_channel, coalesce(new.metadata, '{}'::jsonb));\n  end if;\n  new.touch_channel := nullif(btrim(coalesce(new.touch_channel, '')), '');\n  new.currency := upper(coalesce(nullif(btrim(new.currency), ''), 'USD'));\n  new.source_system := coalesce(nullif(btrim(new.source_system), ''), 'knocklaunch');\n  new.recorded_at := coalesce(new.recorded_at, now());\n  if nullif(new.actor_role, '') is null and new.user_id is not null then\n    select ur.role::text into new.actor_role\n    from public.user_tenant_memberships m\n    join public.user_roles ur on ur.user_id = m.user_id and ur.tenant_id = m.tenant_id\n    where m.user_id = new.user_id and m.tenant_id = new.tenant_org_id and m.status = 'active'\n    order by m.is_owner desc nulls last, ur.assigned_at desc nulls last\n    limit 1;\n  end if;\n  new.actor_role := coalesce(new.actor_role, case when new.is_system_generated then 'system' else 'member' end);\n  return new;\nend;\n$$;\ndrop trigger if exists activity_timeline_normalize_insert on public.activity_timeline;\ncreate trigger activity_timeline_normalize_insert before insert on public.activity_timeline for each row execute function public.activity_timeline_normalize_insert();\n\ncreate or replace function public.prevent_business_activity_mutation()\nreturns trigger language plpgsql set search_path = public, pg_temp as $$\nbegin raise exception 'business_activity_ledger_is_append_only'; end;\n$$;\ndrop trigger if exists activity_timeline_append_only on public.activity_timeline;\ncreate trigger activity_timeline_append_only before update or delete on public.activity_timeline for each row execute function public.prevent_business_activity_mutation();\n\nalter table public.activity_timeline enable row level security;\ndrop policy if exists \"Tenant isolation for activity_timeline\" on public.activity_timeline;\ndrop policy if exists activity_timeline_tenant_select on public.activity_timeline;\ndrop policy if exists activity_timeline_platform_admin_select on public.activity_timeline;\ncreate policy activity_timeline_tenant_select on public.activity_timeline for select to authenticated using (public.ai_user_in_tenant(tenant_org_id));\ncreate policy activity_timeline_platform_admin_select on public.activity_timeline for select to authenticated using (public.is_platform_admin(auth.uid()));\ngrant select on public.activity_timeline to authenticated;\nrevoke insert, update, delete on public.activity_timeline from authenticated, anon;\n\ncreate or replace function public.business_activity_project(_tenant_id uuid, _activity_type text, _title text, _event jsonb default '{}'::jsonb)\nreturns uuid language plpgsql security definer set search_path = public, auth, pg_temp as $$\ndeclare\n  v_id uuid;\n  v_contact uuid := public.business_activity_safe_uuid(_event->>'contact_id');\n  v_actor uuid := public.business_activity_safe_uuid(_event->>'actor_user_id');\n  v_related uuid := public.business_activity_safe_uuid(_event->>'related_entity_id');\n  v_correction uuid := public.business_activity_safe_uuid(_event->>'corrects_event_id');\n  v_money numeric := case when nullif(_event->>'monetary_value','') is null then null else (_event->>'monetary_value')::numeric end;\n  v_occurred timestamptz := case when nullif(_event->>'occurred_at','') is null then now() else (_event->>'occurred_at')::timestamptz end;\nbegin\n  if nullif(btrim(coalesce(_activity_type,'')), '') is null then raise exception 'business_activity_type_required'; end if;\n  if nullif(btrim(coalesce(_title,'')), '') is null then raise exception 'business_activity_title_required'; end if;\n  if v_correction is not null and not exists (select 1 from public.activity_timeline where id=v_correction and tenant_org_id=_tenant_id) then\n    raise exception 'business_activity_correction_target_invalid';\n  end if;\n  insert into public.activity_timeline (\n    tenant_org_id, contact_id, user_id, activity_type, title, description, metadata,\n    related_entity_type, related_entity_id, importance, is_system_generated, event_domain,\n    source_channel, touch_channel, source_detail, source_campaign, source_referrer,\n    source_system, source_record_type, source_record_id, correlation_key, actor_role,\n    previous_state, new_state, reason_code, monetary_value, currency, outcome,\n    corrects_event_id, dedupe_key, created_at\n  ) values (\n    _tenant_id, v_contact, v_actor, _activity_type, _title, _event->>'description', coalesce(_event->'metadata','{}'::jsonb),\n    _event->>'related_entity_type', v_related, coalesce(nullif(_event->>'importance',''),'normal'), coalesce((_event->>'is_system_generated')::boolean,true), coalesce(nullif(_event->>'event_domain',''),'activity'),\n    nullif(_event->>'source_channel',''), nullif(_event->>'touch_channel',''), nullif(_event->>'source_detail',''), nullif(_event->>'source_campaign',''), nullif(_event->>'source_referrer',''),\n    coalesce(nullif(_event->>'source_system',''),'knocklaunch'), nullif(_event->>'source_record_type',''), nullif(_event->>'source_record_id',''), nullif(_event->>'correlation_key',''), nullif(_event->>'actor_role',''),\n    nullif(_event->>'previous_state',''), nullif(_event->>'new_state',''), nullif(_event->>'reason_code',''), v_money, coalesce(nullif(_event->>'currency',''),'USD'), nullif(_event->>'outcome',''),\n    v_correction, nullif(_event->>'dedupe_key',''), v_occurred\n  ) on conflict (tenant_org_id,dedupe_key) where dedupe_key is not null do nothing returning id into v_id;\n  if v_id is null and nullif(_event->>'dedupe_key','') is not null then\n    select id into v_id from public.activity_timeline where tenant_org_id=_tenant_id and dedupe_key=_event->>'dedupe_key' order by created_at desc limit 1;\n  end if;\n  return v_id;\nend;\n$$;\nrevoke all on function public.business_activity_project(uuid,text,text,jsonb) from public, anon, authenticated;\n\ncreate or replace function public.record_business_activity(p_tenant_org_id uuid, p_activity_type text, p_title text, p_event jsonb default '{}'::jsonb)\nreturns uuid language plpgsql security definer set search_path = public, auth, pg_temp as $$\ndeclare v_event jsonb;\nbegin\n  perform public.business_activity_assert_tenant_access(p_tenant_org_id);\n  v_event := coalesce(p_event,'{}'::jsonb) || jsonb_build_object('actor_user_id',auth.uid(),'is_system_generated',false,'source_system',coalesce(p_event->>'source_system','knocklaunch'));\n  return public.business_activity_project(p_tenant_org_id,p_activity_type,p_title,v_event);\nend;\n$$;\nrevoke all on function public.record_business_activity(uuid,text,text,jsonb) from public, anon;\ngrant execute on function public.record_business_activity(uuid,text,text,jsonb) to authenticated, service_role;\n\ncreate or replace function public.log_activity_timeline(\n  p_tenant_org_id uuid, p_contact_id uuid, p_user_id uuid, p_activity_type text, p_title text,\n  p_description text default null, p_metadata jsonb default '{}'::jsonb, p_related_entity_type text default null,\n  p_related_entity_id uuid default null, p_importance text default 'normal', p_is_system boolean default false\n) returns uuid language plpgsql security definer set search_path = public, auth, pg_temp as $$\ndeclare v_actor uuid; v_system boolean;\nbegin\n  perform public.business_activity_assert_tenant_access(p_tenant_org_id);\n  v_actor := case when coalesce(auth.role(),'')='service_role' then p_user_id else auth.uid() end;\n  v_system := case when coalesce(auth.role(),'')='service_role' then p_is_system else false end;\n  return public.business_activity_project(p_tenant_org_id,p_activity_type,p_title,\n    jsonb_build_object('contact_id',p_contact_id,'actor_user_id',v_actor,'description',p_description,'metadata',coalesce(p_metadata,'{}'::jsonb),\n      'related_entity_type',p_related_entity_type,'related_entity_id',p_related_entity_id,'importance',p_importance,'is_system_generated',v_system,\n      'event_domain',coalesce(nullif(p_metadata->>'event_domain',''),'activity'),'source_channel',coalesce(p_metadata->>'source_channel',p_metadata->>'source'),\n      'touch_channel',p_metadata->>'touch_channel','source_detail',p_metadata->>'source_detail','source_campaign',coalesce(p_metadata->>'source_campaign',p_metadata->>'campaign'),\n      'source_referrer',coalesce(p_metadata->>'source_referrer',p_metadata->>'referrer'),'correlation_key',coalesce(p_metadata->>'correlation_key',case when p_contact_id is not null then 'contact:'||p_contact_id::text else null end),\n      'source_system',coalesce(p_metadata->>'source_system','knocklaunch'),'source_record_type',coalesce(p_metadata->>'source_record_type','legacy_activity')));\nend;\n$$;\nrevoke all on function public.log_activity_timeline(uuid,uuid,uuid,text,text,text,jsonb,text,uuid,text,boolean) from public, anon;\ngrant execute on function public.log_activity_timeline(uuid,uuid,uuid,text,text,text,jsonb,text,uuid,text,boolean) to authenticated, service_role;\n\ncreate or replace function public.project_contact_business_activity()\nreturns trigger language plpgsql security definer set search_path = public, pg_temp as $$\ndeclare source_raw text; changed text[]:=array[]::text[]; event_name text;\nbegin\n  if public.business_activity_safe_uuid(new.tenant_org_id) is null then return new; end if;\n  source_raw:=coalesce(nullif(new.raw_data #>> '{acquisition,source}',''),nullif(new.source,''),nullif(new.lead_origin,''),nullif(new.referral_source,''),nullif(new.import_source,''));\n  if tg_op='INSERT' then event_name:='contact_created'; else\n    if new.source is distinct from old.source or new.lead_origin is distinct from old.lead_origin or new.referral_source is distinct from old.referral_source then changed:=array_append(changed,'source'); end if;\n    if new.lead_stage is distinct from old.lead_stage or new.status is distinct from old.status or new.contact_status is distinct from old.contact_status or new.service_status is distinct from old.service_status then changed:=array_append(changed,'stage'); end if;\n    if new.assigned_to is distinct from old.assigned_to or new.assigned_user is distinct from old.assigned_user or new.assigned_rep is distinct from old.assigned_rep then changed:=array_append(changed,'assignment'); end if;\n    if new.graduated_at is distinct from old.graduated_at or new.lost_at is distinct from old.lost_at then changed:=array_append(changed,'outcome'); end if;\n    if cardinality(changed)=0 then return new; end if; event_name:='contact_updated';\n  end if;\n  perform public.business_activity_project(new.tenant_org_id::uuid,event_name,case when tg_op='INSERT' then 'Contact entered business' else 'Contact activity updated' end,\n    jsonb_build_object('contact_id',new.id,'actor_user_id',coalesce(auth.uid(),new.created_by_user_id),'description',case when tg_op='INSERT' then coalesce(new.name,trim(concat_ws(' ',new.first_name,new.last_name)),'Contact') else array_to_string(changed,', ') end,\n      'metadata',jsonb_build_object('changed_fields',changed,'raw_source',source_raw),'related_entity_type','contact','related_entity_id',new.id,'event_domain',case when tg_op='INSERT' then 'acquisition' else 'relationship' end,\n      'source_channel',source_raw,'source_detail',coalesce(new.raw_data #>> '{acquisition,medium}',new.raw_data->>'utm_medium'),'source_campaign',coalesce(new.raw_data #>> '{acquisition,campaign}',new.raw_data->>'utm_campaign'),\n      'source_referrer',coalesce(new.raw_data #>> '{acquisition,referrer}',new.raw_data->>'referrer'),'correlation_key','contact:'||new.id::text,'previous_state',case when tg_op='UPDATE' then coalesce(old.lead_stage,old.status,old.contact_status) else null end,\n      'new_state',coalesce(new.lead_stage,new.status,new.contact_status),'outcome',case when new.lost_at is not null then 'lost' when new.graduated_at is not null then 'converted' else null end,\n      'dedupe_key',case when tg_op='INSERT' then 'contact-created:'||new.id::text else null end,'source_record_type','unified_contacts','source_record_id',new.id,'occurred_at',coalesce(new.created_at,now())));\n  return new;\nend;\n$$;\ndrop trigger if exists unified_contacts_business_activity on public.unified_contacts;\ncreate trigger unified_contacts_business_activity after insert or update of source,lead_origin,referral_source,lead_stage,status,contact_status,service_status,assigned_to,assigned_user,assigned_rep,graduated_at,lost_at on public.unified_contacts for each row execute function public.project_contact_business_activity();\n\ncreate or replace function public.project_opportunity_business_activity()\nreturns trigger language plpgsql security definer set search_path = public, pg_temp as $$\ndeclare o public.business_opportunities%rowtype;\nbegin\n  select * into o from public.business_opportunities where id=new.opportunity_id; if not found then return new; end if;\n  perform public.business_activity_project(new.tenant_id,'opportunity_'||new.dimension,'Opportunity '||replace(new.dimension,'_',' ')||': '||coalesce(new.from_state,'new')||' → '||coalesce(new.to_state,'updated'),\n    jsonb_build_object('contact_id',o.contact_id,'actor_user_id',new.actor_user_id,'description',o.title,'metadata',coalesce(new.metadata,'{}'::jsonb),'related_entity_type','opportunity','related_entity_id',new.opportunity_id,\n      'event_domain','sales','source_channel',o.source_module,'source_detail',o.source_entity_type,'correlation_key','opportunity:'||new.opportunity_id::text,'previous_state',new.from_state,'new_state',new.to_state,'reason_code',new.reason_code,\n      'outcome',case when new.dimension='lifecycle' and new.to_state in ('won','lost','cancelled') then new.to_state when new.dimension='payment' and new.to_state='paid' then 'paid' else null end,\n      'dedupe_key','opportunity-event:'||new.id::text,'source_record_type','business_opportunity_events','source_record_id',new.id,'occurred_at',new.created_at));\n  return new;\nend;\n$$;\ndrop trigger if exists business_opportunity_events_activity_projection on public.business_opportunity_events;\ncreate trigger business_opportunity_events_activity_projection after insert on public.business_opportunity_events for each row execute function public.project_opportunity_business_activity();\n\ncreate or replace function public.project_communication_business_activity()\nreturns trigger language plpgsql security definer set search_path = public, pg_temp as $$\ndeclare tid uuid:=public.business_activity_safe_uuid(new.tenant_org_id); channel text;\nbegin\n  if tid is null then return new; end if;\n  channel:=case when lower(coalesce(new.event_type,'')) like '%email%' then 'Email' when lower(coalesce(new.event_type,'')) like '%sms%' or lower(coalesce(new.event_type,'')) like '%text%' then 'SMS' else 'Phone' end;\n  perform public.business_activity_project(tid,'communication_'||coalesce(new.event_type,'activity'),initcap(coalesce(new.direction,'Business'))||' '||replace(coalesce(new.event_type,'communication'),'_',' '),\n    jsonb_build_object('contact_id',coalesce(new.contact_id,new.lead_id),'actor_user_id',new.rep_id,'description',coalesce(new.outcome,new.result,new.status,new.disposition),'metadata',jsonb_build_object('provider',new.provider,'status',new.status,'duration_seconds',new.duration_seconds),\n      'related_entity_type','communication','related_entity_id',new.id,'event_domain','communication','touch_channel',channel,'source_detail',new.provider,'correlation_key',case when coalesce(new.contact_id,new.lead_id) is not null then 'contact:'||coalesce(new.contact_id,new.lead_id)::text else 'communication:'||new.id::text end,\n      'outcome',coalesce(new.outcome,new.result,new.disposition),'dedupe_key','communication-event:'||new.id::text,'source_record_type','communication_events','source_record_id',new.id,'occurred_at',new.created_at));\n  return new;\nend;\n$$;\ndrop trigger if exists communication_events_activity_projection on public.communication_events;\ncreate trigger communication_events_activity_projection after insert on public.communication_events for each row execute function public.project_communication_business_activity();\n\ncreate or replace function public.project_document_business_activity()\nreturns trigger language plpgsql security definer set search_path = public, pg_temp as $$\ndeclare tid uuid:=public.business_activity_safe_uuid(new.tenant_org_id);\nbegin\n  if tid is null then return new; end if;\n  perform public.business_activity_project(tid,'document_'||coalesce(new.activity_type,'activity'),initcap(replace(coalesce(new.activity_type,'Document activity'),'_',' ')),\n    jsonb_build_object('contact_id',public.business_activity_safe_uuid(new.activity_data->>'contact_id'),'actor_user_id',new.user_id,'description',new.document_type,'metadata',coalesce(new.activity_data,'{}'::jsonb),\n      'related_entity_type','document','event_domain','document','correlation_key',coalesce('document:'||nullif(new.document_id,''),'document-activity:'||new.id::text),'dedupe_key','document-activity:'||new.id::text,\n      'source_record_type','document_activity','source_record_id',new.id,'occurred_at',new.created_at));\n  return new;\nend;\n$$;\ndrop trigger if exists document_activity_business_projection on public.document_activity;\ncreate trigger document_activity_business_projection after insert on public.document_activity for each row execute function public.project_document_business_activity();\n\ncreate or replace function public.project_job_business_activity()\nreturns trigger language plpgsql security definer set search_path = public, pg_temp as $$\ndeclare changed text[]:=array[]::text[]; event_name text;\nbegin\n  if tg_op='INSERT' then event_name:='job_created'; else\n    if new.status is distinct from old.status then changed:=array_append(changed,'status'); end if;\n    if new.assigned_crew_id is distinct from old.assigned_crew_id or new.assigned_pm_id is distinct from old.assigned_pm_id then changed:=array_append(changed,'assignment'); end if;\n    if new.scheduled_date is distinct from old.scheduled_date then changed:=array_append(changed,'schedule'); end if;\n    if new.completed_at is distinct from old.completed_at or new.paid_at is distinct from old.paid_at then changed:=array_append(changed,'outcome'); end if;\n    if new.financing_mode is distinct from old.financing_mode or new.financed_amount is distinct from old.financed_amount then changed:=array_append(changed,'finance'); end if;\n    if cardinality(changed)=0 then return new; end if; event_name:='job_updated';\n  end if;\n  perform public.business_activity_project(new.tenant_id,event_name,case when tg_op='INSERT' then 'Job created' else 'Job activity updated' end,\n    jsonb_build_object('contact_id',new.contact_id,'actor_user_id',coalesce(auth.uid(),new.claim_payment_updated_by,new.verified_by,new.manager_approved_by,new.created_by),'description',coalesce(new.customer_name,new.job_number,'Job')||case when cardinality(changed)>0 then ' · '||array_to_string(changed,', ') else '' end,\n      'metadata',jsonb_build_object('job_number',new.job_number,'job_type',new.job_type),'related_entity_type','job','related_entity_id',new.id,'event_domain','fulfillment','correlation_key',coalesce('contact:'||new.contact_id::text,'job:'||new.id::text),\n      'previous_state',case when tg_op='UPDATE' then old.status else null end,'new_state',new.status,'outcome',case when new.paid_at is not null then 'paid' when new.completed_at is not null then 'completed' else null end,\n      'dedupe_key',case when tg_op='INSERT' then 'job-created:'||new.id::text else null end,'source_record_type','jobs','source_record_id',new.id,'occurred_at',coalesce(new.created_at,now())));\n  return new;\nend;\n$$;\ndrop trigger if exists jobs_business_activity on public.jobs;\ncreate trigger jobs_business_activity after insert or update of status,assigned_crew_id,assigned_pm_id,scheduled_date,completed_at,paid_at,financing_mode,financed_amount on public.jobs for each row execute function public.project_job_business_activity();\n\ncreate or replace function public.project_invoice_business_activity()\nreturns trigger language plpgsql security definer set search_path = public, pg_temp as $$\ndeclare tid uuid:=public.business_activity_safe_uuid(new.tenant_org_id); cid uuid:=coalesce(new.canonical_contact_id,public.business_activity_safe_uuid(new.contact_id)); event_name text;\nbegin\n  if tid is null then return new; end if;\n  if tg_op='UPDATE' and new.status is not distinct from old.status and new.amount is not distinct from old.amount and new.paid_at is not distinct from old.paid_at then return new; end if;\n  event_name:=case when tg_op='INSERT' then 'invoice_created' when new.paid_at is not null and old.paid_at is null then 'invoice_paid' else 'invoice_updated' end;\n  perform public.business_activity_project(tid,event_name,case when event_name='invoice_paid' then 'Invoice paid' when event_name='invoice_created' then 'Invoice created' else 'Invoice updated' end,\n    jsonb_build_object('contact_id',cid,'actor_user_id',public.business_activity_safe_uuid(new.created_by),'description',coalesce(new.customer_name,new.invoice_id,'Invoice'),'metadata',jsonb_build_object('invoice_number',new.invoice_id),\n      'related_entity_type','invoice','related_entity_id',new.id,'event_domain','revenue','correlation_key',coalesce('contact:'||cid::text,'invoice:'||new.id::text),'previous_state',case when tg_op='UPDATE' then old.status else null end,'new_state',new.status,\n      'monetary_value',case when event_name='invoice_paid' then new.amount else null end,'outcome',case when event_name='invoice_paid' then 'paid' else null end,'dedupe_key',case when tg_op='INSERT' then 'invoice-created:'||new.id::text else null end,\n      'source_record_type','invoices','source_record_id',new.id,'occurred_at',coalesce(new.created_at,now())));\n  return new;\nend;\n$$;\ndrop trigger if exists invoices_business_activity on public.invoices;\ncreate trigger invoices_business_activity after insert or update of status,amount,paid_at on public.invoices for each row execute function public.project_invoice_business_activity();\n\ncreate or replace function public.project_settlement_invoice_business_activity()\nreturns trigger language plpgsql security definer set search_path = public, pg_temp as $$\ndeclare tid uuid:=public.business_activity_safe_uuid(new.tenant_org_id); cid uuid:=public.business_activity_safe_uuid(new.contact_id); event_name text;\nbegin\n  if tid is null then return new; end if;\n  if tg_op='UPDATE' and new.status is not distinct from old.status and new.settlement_amount is not distinct from old.settlement_amount and new.remaining_balance is not distinct from old.remaining_balance and new.paid_at is not distinct from old.paid_at then return new; end if;\n  event_name:=case when tg_op='INSERT' then 'settlement_invoice_created' when new.paid_at is not null and old.paid_at is null then 'settlement_invoice_paid' else 'settlement_invoice_updated' end;\n  perform public.business_activity_project(tid,event_name,case when event_name='settlement_invoice_paid' then 'Invoice paid' when event_name='settlement_invoice_created' then 'Invoice created' else 'Invoice updated' end,\n    jsonb_build_object('contact_id',cid,'actor_user_id',new.created_by_user_id,'description',coalesce(new.homeowner_name,new.invoice_number,'Invoice'),'metadata',jsonb_build_object('invoice_number',new.invoice_number,'remaining_balance',new.remaining_balance),\n      'related_entity_type','settlement_invoice','related_entity_id',new.id,'event_domain','revenue','correlation_key',coalesce('contact:'||cid::text,'invoice:'||new.id::text),'previous_state',case when tg_op='UPDATE' then old.status else null end,'new_state',new.status,\n      'monetary_value',case when event_name='settlement_invoice_paid' then new.settlement_amount else null end,'outcome',case when event_name='settlement_invoice_paid' then 'paid' else null end,'dedupe_key',case when tg_op='INSERT' then 'settlement-invoice-created:'||new.id::text else null end,\n      'source_record_type','settlement_invoices','source_record_id',new.id,'occurred_at',coalesce(new.created_at,now())));\n  return new;\nend;\n$$;\ndrop trigger if exists settlement_invoices_business_activity on public.settlement_invoices;\ncreate trigger settlement_invoices_business_activity after insert or update of status,settlement_amount,remaining_balance,paid_at on public.settlement_invoices for each row execute function public.project_settlement_invoice_business_activity();\n\ncreate or replace function public.project_tenant_business_activity()\nreturns trigger language plpgsql security definer set search_path = public, pg_temp as $$\ndeclare event_name text;\nbegin\n  if tg_op='UPDATE' and new.status is not distinct from old.status and new.is_active is not distinct from old.is_active and new.is_trial is not distinct from old.is_trial then return new; end if;\n  event_name:=case when tg_op='INSERT' then 'business_joined_platform' else 'business_lifecycle_updated' end;\n  perform public.business_activity_project(new.id,event_name,case when tg_op='INSERT' then 'Business joined KnockLaunch' else 'Business lifecycle updated' end,\n    jsonb_build_object('actor_user_id',new.owner_id,'description',coalesce(new.company_dba_name,new.company_legal_name,new.name),'metadata',jsonb_build_object('tenant_type',new.tenant_type,'is_trial',new.is_trial),\n      'related_entity_type','tenant','related_entity_id',new.id,'importance','high','event_domain','platform','source_channel',coalesce(new.settings #>> '{acquisition,source}',new.settings->>'lead_source'),\n      'source_detail',coalesce(new.settings #>> '{acquisition,medium}',new.settings->>'utm_medium'),'source_campaign',coalesce(new.settings #>> '{acquisition,campaign}',new.settings->>'utm_campaign'),\n      'source_referrer',coalesce(new.settings #>> '{acquisition,referrer}',new.settings->>'referrer'),'correlation_key','tenant:'||new.id::text,'previous_state',case when tg_op='UPDATE' then old.status else null end,'new_state',new.status,\n      'outcome',case when new.is_active=false then 'inactive' when new.is_trial=false and tg_op='UPDATE' and old.is_trial=true then 'converted' else null end,'dedupe_key',case when tg_op='INSERT' then 'tenant-created:'||new.id::text else null end,\n      'source_record_type','tenants','source_record_id',new.id,'occurred_at',coalesce(new.created_at,now())));\n  return new;\nend;\n$$;\ndrop trigger if exists tenants_business_activity on public.tenants;\ncreate trigger tenants_business_activity after insert or update of status,is_active,is_trial on public.tenants for each row execute function public.project_tenant_business_activity();\n\ncreate or replace function public.project_user_presence_business_activity()\nreturns trigger language plpgsql security definer set search_path = public, pg_temp as $$\ndeclare tid uuid:=public.business_activity_safe_uuid(new.tenant_id);\nbegin\n  if tid is null or new.event_type not in ('login','session_start') then return new; end if;\n  perform public.business_activity_project(tid,'user_'||new.event_type,case when new.event_type='login' then 'User signed in' else 'User session started' end,\n    jsonb_build_object('actor_user_id',new.user_id,'description',new.human_summary,'metadata',jsonb_build_object('device_family',new.device_family,'route_path',new.route_path),'related_entity_type','user','related_entity_id',new.user_id,\n      'event_domain','usage','touch_channel','App','correlation_key','user:'||new.user_id::text,'dedupe_key','user-activity:'||new.id::text,'source_record_type','user_activity_events','source_record_id',new.id,'occurred_at',new.occurred_at));\n  return new;\nend;\n$$;\ndrop trigger if exists user_activity_events_business_projection on public.user_activity_events;\ncreate trigger user_activity_events_business_projection after insert on public.user_activity_events for each row execute function public.project_user_presence_business_activity();\n\n-- Historical baseline. The same helper and dedupe keys are used so this is deterministic and safe to reconcile.\nselect public.business_activity_project(c.tenant_org_id::uuid,'contact_created','Contact entered business',jsonb_build_object(\n  'contact_id',c.id,'actor_user_id',c.created_by_user_id,'description',coalesce(c.name,trim(concat_ws(' ',c.first_name,c.last_name)),'Contact'),'metadata',jsonb_build_object('backfill',true),\n  'related_entity_type','contact','related_entity_id',c.id,'event_domain','acquisition','source_channel',coalesce(c.raw_data #>> '{acquisition,source}',c.source,c.lead_origin,c.referral_source,c.import_source),\n  'source_detail',coalesce(c.raw_data #>> '{acquisition,medium}',c.raw_data->>'utm_medium'),'source_campaign',coalesce(c.raw_data #>> '{acquisition,campaign}',c.raw_data->>'utm_campaign'),'source_referrer',coalesce(c.raw_data #>> '{acquisition,referrer}',c.raw_data->>'referrer'),\n  'correlation_key','contact:'||c.id::text,'new_state',coalesce(c.lead_stage,c.status,c.contact_status),'outcome',case when c.lost_at is not null then 'lost' when c.graduated_at is not null then 'converted' else null end,\n  'dedupe_key','contact-created:'||c.id::text,'source_record_type','unified_contacts','source_record_id',c.id,'occurred_at',coalesce(c.source_created_date,c.created_at,now())))\nfrom public.unified_contacts c join public.tenants t on t.id::text=c.tenant_org_id;\n\nselect public.business_activity_project(t.id,'business_joined_platform','Business joined KnockLaunch',jsonb_build_object(\n  'actor_user_id',t.owner_id,'description',coalesce(t.company_dba_name,t.company_legal_name,t.name),'metadata',jsonb_build_object('backfill',true,'tenant_type',t.tenant_type),\n  'related_entity_type','tenant','related_entity_id',t.id,'importance','high','event_domain','platform','source_channel',coalesce(t.settings #>> '{acquisition,source}',t.settings->>'lead_source'),\n  'source_detail',coalesce(t.settings #>> '{acquisition,medium}',t.settings->>'utm_medium'),'source_campaign',coalesce(t.settings #>> '{acquisition,campaign}',t.settings->>'utm_campaign'),'source_referrer',coalesce(t.settings #>> '{acquisition,referrer}',t.settings->>'referrer'),\n  'correlation_key','tenant:'||t.id::text,'new_state',t.status,'outcome',case when t.is_active=false then 'inactive' else null end,'dedupe_key','tenant-created:'||t.id::text,'source_record_type','tenants','source_record_id',t.id,'occurred_at',coalesce(t.created_at,now())))\nfrom public.tenants t;\n\nselect public.business_activity_project(e.tenant_id,'opportunity_'||e.dimension,'Opportunity '||replace(e.dimension,'_',' ')||': '||coalesce(e.from_state,'new')||' → '||coalesce(e.to_state,'updated'),jsonb_build_object(\n  'contact_id',o.contact_id,'actor_user_id',e.actor_user_id,'description',o.title,'metadata',coalesce(e.metadata,'{}'::jsonb)||jsonb_build_object('backfill',true),'related_entity_type','opportunity','related_entity_id',e.opportunity_id,\n  'event_domain','sales','correlation_key','opportunity:'||e.opportunity_id::text,'previous_state',e.from_state,'new_state',e.to_state,'reason_code',e.reason_code,'outcome',case when e.dimension='lifecycle' and e.to_state in ('won','lost','cancelled') then e.to_state when e.dimension='payment' and e.to_state='paid' then 'paid' else null end,\n  'dedupe_key','opportunity-event:'||e.id::text,'source_record_type','business_opportunity_events','source_record_id',e.id,'occurred_at',e.created_at))\nfrom public.business_opportunity_events e join public.business_opportunities o on o.id=e.opportunity_id;\n\nselect public.business_activity_project(j.tenant_id,'job_created','Job created',jsonb_build_object(\n  'contact_id',j.contact_id,'actor_user_id',j.created_by,'description',coalesce(j.customer_name,j.job_number,'Job'),'metadata',jsonb_build_object('backfill',true,'job_number',j.job_number,'job_type',j.job_type),\n  'related_entity_type','job','related_entity_id',j.id,'event_domain','fulfillment','correlation_key',coalesce('contact:'||j.contact_id::text,'job:'||j.id::text),'new_state',j.status,\n  'outcome',case when j.paid_at is not null then 'paid' when j.completed_at is not null then 'completed' else null end,'dedupe_key','job-created:'||j.id::text,'source_record_type','jobs','source_record_id',j.id,'occurred_at',j.created_at))\nfrom public.jobs j;\n\nselect public.business_activity_project(i.tenant_org_id::uuid,case when i.paid_at is not null then 'invoice_paid' else 'invoice_created' end,case when i.paid_at is not null then 'Invoice paid' else 'Invoice created' end,jsonb_build_object(\n  'contact_id',coalesce(i.canonical_contact_id,public.business_activity_safe_uuid(i.contact_id)),'actor_user_id',public.business_activity_safe_uuid(i.created_by),'description',coalesce(i.customer_name,i.invoice_id,'Invoice'),'metadata',jsonb_build_object('backfill',true,'invoice_number',i.invoice_id),\n  'related_entity_type','invoice','related_entity_id',i.id,'event_domain','revenue','correlation_key',coalesce('contact:'||coalesce(i.canonical_contact_id,public.business_activity_safe_uuid(i.contact_id))::text,'invoice:'||i.id::text),'new_state',i.status,\n  'monetary_value',case when i.paid_at is not null then i.amount else null end,'outcome',case when i.paid_at is not null then 'paid' else null end,'dedupe_key','invoice-created:'||i.id::text,'source_record_type','invoices','source_record_id',i.id,'occurred_at',i.created_at))\nfrom public.invoices i join public.tenants t on t.id::text=i.tenant_org_id;\n\nselect public.business_activity_project(s.tenant_org_id::uuid,case when s.paid_at is not null then 'settlement_invoice_paid' else 'settlement_invoice_created' end,case when s.paid_at is not null then 'Invoice paid' else 'Invoice created' end,jsonb_build_object(\n  'contact_id',public.business_activity_safe_uuid(s.contact_id),'actor_user_id',s.created_by_user_id,'description',coalesce(s.homeowner_name,s.invoice_number,'Invoice'),'metadata',jsonb_build_object('backfill',true,'invoice_number',s.invoice_number,'remaining_balance',s.remaining_balance),\n  'related_entity_type','settlement_invoice','related_entity_id',s.id,'event_domain','revenue','correlation_key',coalesce('contact:'||public.business_activity_safe_uuid(s.contact_id)::text,'invoice:'||s.id::text),'new_state',s.status,\n  'monetary_value',case when s.paid_at is not null then s.settlement_amount else null end,'outcome',case when s.paid_at is not null then 'paid' else null end,'dedupe_key','settlement-invoice-created:'||s.id::text,'source_record_type','settlement_invoices','source_record_id',s.id,'occurred_at',s.created_at))\nfrom public.settlement_invoices s join public.tenants t on t.id::text=s.tenant_org_id;\n\nselect public.business_activity_project(c.tenant_org_id::uuid,'communication_'||coalesce(c.event_type,'activity'),initcap(coalesce(c.direction,'Business'))||' '||replace(coalesce(c.event_type,'communication'),'_',' '),jsonb_build_object(\n  'contact_id',coalesce(c.contact_id,c.lead_id),'actor_user_id',c.rep_id,'description',coalesce(c.outcome,c.result,c.status,c.disposition),'metadata',jsonb_build_object('backfill',true,'provider',c.provider,'duration_seconds',c.duration_seconds),\n  'related_entity_type','communication','related_entity_id',c.id,'event_domain','communication','touch_channel',case when lower(coalesce(c.event_type,'')) like '%email%' then 'Email' when lower(coalesce(c.event_type,'')) like '%sms%' or lower(coalesce(c.event_type,'')) like '%text%' then 'SMS' else 'Phone' end,\n  'source_detail',c.provider,'correlation_key',case when coalesce(c.contact_id,c.lead_id) is not null then 'contact:'||coalesce(c.contact_id,c.lead_id)::text else 'communication:'||c.id::text end,'outcome',coalesce(c.outcome,c.result,c.disposition),\n  'dedupe_key','communication-event:'||c.id::text,'source_record_type','communication_events','source_record_id',c.id,'occurred_at',c.created_at))\nfrom public.communication_events c join public.tenants t on t.id::text=c.tenant_org_id;\n\nselect public.business_activity_project(d.tenant_org_id::uuid,'document_'||coalesce(d.activity_type,'activity'),initcap(replace(coalesce(d.activity_type,'Document activity'),'_',' ')),jsonb_build_object(\n  'contact_id',public.business_activity_safe_uuid(d.activity_data->>'contact_id'),'actor_user_id',d.user_id,'description',d.document_type,'metadata',coalesce(d.activity_data,'{}'::jsonb)||jsonb_build_object('backfill',true),\n  'related_entity_type','document','event_domain','document','correlation_key',coalesce('document:'||nullif(d.document_id,''),'document-activity:'||d.id::text),'dedupe_key','document-activity:'||d.id::text,\n  'source_record_type','document_activity','source_record_id',d.id,'occurred_at',d.created_at))\nfrom public.document_activity d join public.tenants t on t.id::text=d.tenant_org_id;\n\nselect public.business_activity_project(u.tenant_id::uuid,'user_'||u.event_type,case when u.event_type='login' then 'User signed in' else 'User session started' end,jsonb_build_object(\n  'actor_user_id',u.user_id,'description',u.human_summary,'metadata',jsonb_build_object('backfill',true,'device_family',u.device_family,'route_path',u.route_path),'related_entity_type','user','related_entity_id',u.user_id,\n  'event_domain','usage','touch_channel','App','correlation_key','user:'||u.user_id::text,'dedupe_key','user-activity:'||u.id::text,'source_record_type','user_activity_events','source_record_id',u.id,'occurred_at',u.occurred_at))\nfrom public.user_activity_events u join public.tenants t on t.id::text=u.tenant_id where u.event_type in ('login','session_start');\n\ncreate or replace view public.business_activity_ledger with (security_invoker=true) as\nselect a.id,a.tenant_org_id,t.name as tenant_name,a.contact_id,a.user_id,a.actor_role,a.event_domain,a.activity_type,a.title,a.description,\n  a.related_entity_type,a.related_entity_id,a.correlation_key,a.source_channel,a.touch_channel,a.source_detail,a.source_campaign,a.source_referrer,\n  a.source_system,a.source_record_type,a.source_record_id,a.previous_state,a.new_state,a.reason_code,a.monetary_value,a.currency,a.outcome,\n  a.corrects_event_id,a.dedupe_key,a.importance,a.is_system_generated,a.metadata,a.created_at as occurred_at,a.recorded_at\nfrom public.activity_timeline a join public.tenants t on t.id=a.tenant_org_id;\n\ncreate or replace view public.business_contact_attribution with (security_invoker=true) as\nwith first_source as (\n  select distinct on (tenant_org_id,contact_id) tenant_org_id,contact_id,source_channel as first_touch_source,source_detail as first_touch_detail,source_campaign as first_campaign,source_referrer as first_referrer,created_at as first_touch_at\n  from public.activity_timeline where contact_id is not null and source_channel is not null and source_channel<>'Unknown' order by tenant_org_id,contact_id,created_at,id\n), latest_source as (\n  select distinct on (tenant_org_id,contact_id) tenant_org_id,contact_id,source_channel as latest_touch_source,source_detail as latest_touch_detail,source_campaign as latest_campaign,source_referrer as latest_referrer,created_at as latest_source_at\n  from public.activity_timeline where contact_id is not null and source_channel is not null and source_channel<>'Unknown' order by tenant_org_id,contact_id,created_at desc,id desc\n), latest_touch as (\n  select distinct on (tenant_org_id,contact_id) tenant_org_id,contact_id,touch_channel as latest_touch_channel,created_at as latest_touch_at\n  from public.activity_timeline where contact_id is not null and touch_channel is not null order by tenant_org_id,contact_id,created_at desc,id desc\n), activity as (\n  select tenant_org_id,contact_id,count(*) as activity_count,max(created_at) as last_activity_at,\n    coalesce(sum(monetary_value) filter (where event_domain='revenue' and lower(coalesce(outcome,'')) in ('paid','collected')),0)::numeric(14,2) as collected_revenue\n  from public.activity_timeline where contact_id is not null group by tenant_org_id,contact_id\n)\nselect c.tenant_org_id::uuid as tenant_org_id,c.id as contact_id,\n  coalesce(f.first_touch_source,public.normalize_business_source(coalesce(c.raw_data #>> '{acquisition,source}',c.source,c.lead_origin,c.referral_source,c.import_source),c.raw_data)) as first_touch_source,\n  f.first_touch_detail,f.first_campaign,f.first_referrer,coalesce(f.first_touch_at,c.source_created_date,c.created_at) as first_touch_at,\n  coalesce(l.latest_touch_source,f.first_touch_source,public.normalize_business_source(coalesce(c.raw_data #>> '{acquisition,source}',c.source,c.lead_origin,c.referral_source,c.import_source),c.raw_data)) as latest_touch_source,\n  l.latest_touch_detail,l.latest_campaign,l.latest_referrer,coalesce(l.latest_source_at,f.first_touch_at,c.source_created_date,c.created_at) as latest_source_at,\n  x.latest_touch_channel,x.latest_touch_at,coalesce(a.activity_count,0) as activity_count,coalesce(a.last_activity_at,c.updated_at,c.created_at) as last_activity_at,\n  coalesce(a.collected_revenue,0)::numeric(14,2) as collected_revenue\nfrom public.unified_contacts c join public.tenants t on t.id::text=c.tenant_org_id\nleft join first_source f on f.tenant_org_id=t.id and f.contact_id=c.id\nleft join latest_source l on l.tenant_org_id=t.id and l.contact_id=c.id\nleft join latest_touch x on x.tenant_org_id=t.id and x.contact_id=c.id\nleft join activity a on a.tenant_org_id=t.id and a.contact_id=c.id;\n\ncreate or replace view public.business_tenant_activity_summary with (security_invoker=true) as\nselect t.id as tenant_id,t.name as tenant_name,count(a.id)::bigint as activity_count,\n  count(distinct a.contact_id) filter (where a.contact_id is not null)::bigint as tracked_contacts,\n  count(distinct a.contact_id) filter (where a.contact_id is not null and a.source_channel is not null and a.source_channel<>'Unknown')::bigint as attributed_contacts,\n  max(a.created_at) as last_activity_at,\n  coalesce(sum(a.monetary_value) filter (where a.event_domain='revenue' and lower(coalesce(a.outcome,'')) in ('paid','collected')),0)::numeric(14,2) as collected_revenue\nfrom public.tenants t left join public.activity_timeline a on a.tenant_org_id=t.id group by t.id,t.name;\n\ngrant select on public.business_activity_ledger to authenticated;\ngrant select on public.business_contact_attribution to authenticated;\ngrant select on public.business_tenant_activity_summary to authenticated;\ncomment on table public.activity_timeline is 'Canonical append-only KNOCKLAUNCH Business Activity Ledger spine. Material business actions project here with provenance, attribution and correlation.';\ncomment on view public.business_activity_ledger is 'Role-aware human-readable projection of the canonical Business Activity Ledger.';\ncomment on view public.business_contact_attribution is 'First-source, latest-source, latest-touch, activity and collected-revenue attribution per tenant contact.';\ncomment on view public.business_tenant_activity_summary is 'Tenant-safe activity and revenue rollup; platform administrators receive cross-tenant oversight through ledger RLS.';\n"],"idempotency_key":"73ddb27088505c58ca5f717fd997b41a5e8fd934:20260911183000"},{"name":"preserve_modes_media_upload_contract","version":"20260911213000","rollback":null,"created_by":"canonical-production-release","statements":["-- Preserve specialized MODES media ownership when canonical storage sync observes an upload.\n-- The storage trigger remains the generic readability/indexing owner, but it must not\n-- downgrade a mode-specific asset prepared by tenant-media-library.\n\ncreate or replace function public.kl_tenant_media_collection(p_name text)\nreturns text\nlanguage sql\nimmutable\nset search_path to 'public', 'pg_temp'\nas $function$\n  select case lower(split_part(coalesce(p_name,''), '/', 1))\n    when 'brand' then case lower(split_part(coalesce(p_name,''), '/', 2))\n      when 'logos' then 'branding'\n      when 'backgrounds' then 'backgrounds'\n      else trim(both '-' from regexp_replace(lower(split_part(coalesce(p_name,''), '/', 2)), '[^a-z0-9]+', '-', 'g'))\n    end\n    when 'media' then case lower(split_part(coalesce(p_name,''), '/', 2))\n      when 'videos' then 'videos'\n      when 'modes' then case\n        when trim(both '-' from regexp_replace(lower(split_part(coalesce(p_name,''), '/', 3)), '[^a-z0-9]+', '-', 'g')) <> ''\n          then 'mode-' || trim(both '-' from regexp_replace(lower(split_part(coalesce(p_name,''), '/', 3)), '[^a-z0-9]+', '-', 'g'))\n        else 'modes'\n      end\n      else trim(both '-' from regexp_replace(lower(split_part(coalesce(p_name,''), '/', 2)), '[^a-z0-9]+', '-', 'g'))\n    end\n    when 'apps' then trim(both '-' from regexp_replace(lower(split_part(coalesce(p_name,''), '/', 2)), '[^a-z0-9]+', '-', 'g'))\n    when 'library' then trim(both '-' from regexp_replace(lower(split_part(coalesce(p_name,''), '/', 2)), '[^a-z0-9]+', '-', 'g'))\n    when 'logo' then 'branding'\n    when 'branding' then 'branding'\n    when 'background' then 'backgrounds'\n    when 'backgrounds' then 'backgrounds'\n    when 'video' then 'videos'\n    when 'videos' then 'videos'\n    when 'advertisement' then 'marketing'\n    when 'advertisements' then 'marketing'\n    when 'marketing' then 'marketing'\n    when 'documents' then 'documents'\n    when 'property-photos' then 'property-photos'\n    when 'public-intake' then 'public-intake'\n    else trim(both '-' from regexp_replace(lower(split_part(coalesce(p_name,''), '/', 1)), '[^a-z0-9]+', '-', 'g'))\n  end;\n$function$;\n\ncreate or replace function public.kl_sync_tenant_media_storage_object()\nreturns trigger\nlanguage plpgsql\nsecurity definer\nset search_path to 'public', 'storage', 'pg_temp'\nas $function$\ndeclare\n  v_object storage.objects%rowtype;\n  v_tenant_id uuid;\n  v_old_tenant_id uuid;\n  v_collection text;\n  v_mime text;\n  v_size bigint;\n  v_preserved_id uuid;\nbegin\n  if tg_op='DELETE' then\n    v_old_tenant_id:=public.kl_tenant_media_bucket_tenant_id(old.bucket_id);\n    if v_old_tenant_id is not null then\n      update public.tenant_media_assets\n      set deleted_at=now(),published=false,is_primary=false,updated_at=now()\n      where tenant_id=v_old_tenant_id and bucket_name=old.bucket_id and original_key=old.name and deleted_at is null;\n      perform public.kl_refresh_tenant_media_primaries(v_old_tenant_id);\n    end if;\n    return old;\n  end if;\n\n  v_object:=new;\n  v_tenant_id:=public.kl_tenant_media_bucket_tenant_id(v_object.bucket_id);\n  v_mime:=public.kl_tenant_media_mime(v_object.name,coalesce(v_object.metadata->>'mimetype',v_object.metadata->>'contentType',''));\n  v_collection:=public.kl_tenant_media_collection(v_object.name);\n  begin\n    v_size:=nullif(coalesce(v_object.metadata->>'size',v_object.metadata->>'contentLength',''),'')::bigint;\n  exception when invalid_text_representation then\n    v_size:=null;\n  end;\n\n  if tg_op='UPDATE' and (old.bucket_id is distinct from new.bucket_id or old.name is distinct from new.name) then\n    v_old_tenant_id:=public.kl_tenant_media_bucket_tenant_id(old.bucket_id);\n    if v_old_tenant_id is not null\n       and v_tenant_id is not null\n       and v_old_tenant_id=v_tenant_id\n       and public.kl_tenant_media_supported(v_object.name,v_mime)\n       and coalesce(v_collection,'')<>'' then\n      update public.tenant_media_assets\n      set collection_key=case\n            when coalesce(metadata->>'usage','')='mode_card' then collection_key\n            else v_collection\n          end,\n          bucket_name=v_object.bucket_id,\n          original_key=v_object.name,\n          original_content_type=nullif(v_mime,''),\n          mime_type=nullif(v_mime,''),\n          size_bytes=v_size,\n          storage_provider='supabase',\n          storage_key=v_object.name,\n          is_primary=false,\n          deleted_at=null,\n          metadata=case\n            when coalesce(metadata->>'usage','')='mode_card' then\n              jsonb_build_object(\n                'existing_storage',true,\n                'existing_origin','supabase_storage',\n                'source_bucket',v_object.bucket_id,\n                'storage_object_id',v_object.id::text,\n                'storage_created_at',v_object.created_at::text,\n                'storage_updated_at',v_object.updated_at::text,\n                'contract','READABLE-TENANT-STORAGE-V2') || coalesce(metadata,'{}'::jsonb)\n            else\n              coalesce(metadata,'{}'::jsonb) || jsonb_build_object(\n                'existing_storage',true,\n                'existing_origin','supabase_storage',\n                'source_bucket',v_object.bucket_id,\n                'storage_object_id',v_object.id::text,\n                'storage_created_at',v_object.created_at::text,\n                'storage_updated_at',v_object.updated_at::text,\n                'contract','READABLE-TENANT-STORAGE-V2')\n          end,\n          updated_at=coalesce(v_object.updated_at,now())\n      where tenant_id=v_old_tenant_id\n        and bucket_name=old.bucket_id\n        and original_key=old.name\n        and deleted_at is null\n      returning id into v_preserved_id;\n      if v_preserved_id is not null then\n        perform public.kl_refresh_tenant_media_primaries(v_old_tenant_id);\n        return new;\n      end if;\n    end if;\n\n    if v_old_tenant_id is not null then\n      update public.tenant_media_assets\n      set deleted_at=now(),published=false,is_primary=false,updated_at=now()\n      where tenant_id=v_old_tenant_id and bucket_name=old.bucket_id and original_key=old.name and deleted_at is null;\n      perform public.kl_refresh_tenant_media_primaries(v_old_tenant_id);\n    end if;\n  end if;\n\n  if v_tenant_id is null then return new; end if;\n  if not public.kl_tenant_media_supported(v_object.name,v_mime) then return new; end if;\n  if coalesce(v_collection,'')='' then return new; end if;\n\n  insert into public.tenant_media_assets(\n    tenant_id,collection_key,bucket_name,original_key,original_content_type,source_kind,status,review_status,published,is_primary,title,mime_type,size_bytes,storage_provider,storage_key,deleted_at,metadata,created_at,updated_at)\n  values(\n    v_tenant_id,v_collection,v_object.bucket_id,v_object.name,nullif(v_mime,''),'existing','ready','approved',true,false,\n    split_part(v_object.name,'/',array_length(string_to_array(v_object.name,'/'),1)),nullif(v_mime,''),v_size,'supabase',v_object.name,null,\n    jsonb_build_object('existing_storage',true,'existing_origin','supabase_storage','source_bucket',v_object.bucket_id,'storage_object_id',v_object.id::text,'storage_created_at',v_object.created_at::text,'storage_updated_at',v_object.updated_at::text,'contract','READABLE-TENANT-STORAGE-V2'),\n    coalesce(v_object.created_at,now()),coalesce(v_object.updated_at,now()))\n  on conflict(tenant_id,bucket_name,original_key) do update\n  set collection_key=case\n        when coalesce(tenant_media_assets.metadata->>'usage','')='mode_card' then tenant_media_assets.collection_key\n        else excluded.collection_key\n      end,\n      original_content_type=excluded.original_content_type,\n      status='ready',\n      review_status='approved',\n      published=true,\n      mime_type=excluded.mime_type,\n      size_bytes=excluded.size_bytes,\n      storage_provider='supabase',\n      storage_key=excluded.storage_key,\n      deleted_at=null,\n      metadata=case\n        when coalesce(tenant_media_assets.metadata->>'usage','')='mode_card'\n          then excluded.metadata || coalesce(tenant_media_assets.metadata,'{}'::jsonb)\n        else coalesce(tenant_media_assets.metadata,'{}'::jsonb) || excluded.metadata\n      end,\n      updated_at=excluded.updated_at;\n  perform public.kl_refresh_tenant_media_primaries(v_tenant_id);\n  return new;\nend;\n$function$;\n\nupdate public.tenant_media_assets\nset collection_key='mode-' || trim(both '-' from regexp_replace(lower(coalesce(nullif(metadata->>'storage_slug',''),split_part(coalesce(storage_key,original_key,''),'/',3))), '[^a-z0-9]+', '-', 'g')),\n    metadata=jsonb_set(coalesce(metadata,'{}'::jsonb),'{contract}',to_jsonb('MODES_MEDIA_V1'::text),true),\n    updated_at=now()\nwhere deleted_at is null\n  and coalesce(metadata->>'usage','')='mode_card'\n  and coalesce(storage_key,original_key,'') like 'media/modes/%/%'\n  and trim(both '-' from regexp_replace(lower(coalesce(nullif(metadata->>'storage_slug',''),split_part(coalesce(storage_key,original_key,''),'/',3))), '[^a-z0-9]+', '-', 'g'))<>'';\n"],"idempotency_key":"3f2fb334ec40cbf76c4d373c594be30b3296abab:20260911213000"},{"name":"business_activity_ledger_primitive_projection_coverage","version":"20260912011031","rollback":null,"created_by":"nysomenterprises@gmail.com","statements":["create or replace function public.project_proposal_business_activity()\nreturns trigger\nlanguage plpgsql\nsecurity definer\nset search_path to 'public','auth','pg_temp'\nas $$\ndeclare\n  v_event text;\n  v_prev text;\n  v_dedupe text;\nbegin\n  if new.tenant_id is null then return new; end if;\n  if tg_op = 'INSERT' then\n    v_event := 'proposal_created';\n    v_prev := null;\n    v_dedupe := 'proposal-created:' || new.id::text;\n  else\n    if new.status is not distinct from old.status\n       and new.current_version is not distinct from old.current_version\n       and new.selected_option_key is not distinct from old.selected_option_key\n       and new.total_amount is not distinct from old.total_amount\n       and new.approved_at is not distinct from old.approved_at\n       and new.sent_at is not distinct from old.sent_at\n       and new.accepted_at is not distinct from old.accepted_at\n       and new.declined_at is not distinct from old.declined_at\n       and new.paid_at is not distinct from old.paid_at\n       and new.voided_at is not distinct from old.voided_at then\n      return new;\n    end if;\n    v_prev := old.status;\n    if new.paid_at is not null and old.paid_at is null then\n      v_event := 'proposal_paid'; v_dedupe := 'proposal-paid:' || new.id::text;\n    elsif new.accepted_at is not null and old.accepted_at is null then\n      v_event := 'proposal_accepted'; v_dedupe := 'proposal-accepted:' || new.id::text;\n    elsif new.declined_at is not null and old.declined_at is null then\n      v_event := 'proposal_declined'; v_dedupe := 'proposal-declined:' || new.id::text;\n    elsif new.voided_at is not null and old.voided_at is null then\n      v_event := 'proposal_voided'; v_dedupe := 'proposal-voided:' || new.id::text;\n    elsif new.sent_at is not null and old.sent_at is null then\n      v_event := 'proposal_sent'; v_dedupe := null;\n    elsif new.approved_at is not null and old.approved_at is null then\n      v_event := 'proposal_approved'; v_dedupe := null;\n    else\n      v_event := 'proposal_updated'; v_dedupe := null;\n    end if;\n  end if;\n\n  perform public.business_activity_project(\n    new.tenant_id,\n    v_event,\n    initcap(replace(v_event, '_', ' ')),\n    jsonb_build_object(\n      'contact_id', new.source_contact_id,\n      'actor_user_id', coalesce(auth.uid(), new.created_by),\n      'description', coalesce(new.proposal_number, 'Proposal'),\n      'metadata', jsonb_build_object('current_version', new.current_version, 'offer_key', new.offer_key, 'selected_option_key', new.selected_option_key),\n      'related_entity_type', 'proposal',\n      'related_entity_id', new.id,\n      'event_domain', 'sales',\n      'correlation_key', case when new.source_contact_id is not null then 'contact:' || new.source_contact_id::text else 'proposal:' || new.id::text end,\n      'previous_state', v_prev,\n      'new_state', new.status,\n      'monetary_value', new.total_amount,\n      'currency', coalesce(new.currency, 'USD'),\n      'outcome', case when v_event='proposal_paid' then 'paid' when v_event='proposal_accepted' then 'accepted' when v_event='proposal_declined' then 'declined' when v_event='proposal_voided' then 'voided' else null end,\n      'dedupe_key', v_dedupe,\n      'source_record_type', 'proposals',\n      'source_record_id', new.id,\n      'occurred_at', coalesce(new.paid_at, new.accepted_at, new.declined_at, new.voided_at, new.sent_at, new.approved_at, new.updated_at, new.created_at, now())\n    )\n  );\n  return new;\nend;\n$$;\n\ndrop trigger if exists proposals_business_activity on public.proposals;\ncreate trigger proposals_business_activity\nafter insert or update on public.proposals\nfor each row execute function public.project_proposal_business_activity();\n\ncreate or replace function public.project_pending_agreement_business_activity()\nreturns trigger\nlanguage plpgsql\nsecurity definer\nset search_path to 'public','auth','pg_temp'\nas $$\ndeclare\n  v_event text;\n  v_prev text;\n  v_dedupe text;\nbegin\n  if new.tenant_id is null then return new; end if;\n  if tg_op = 'INSERT' then\n    v_event := 'agreement_created'; v_prev := null; v_dedupe := 'agreement-created:' || new.id::text;\n  else\n    if new.status is not distinct from old.status\n       and new.sent_at is not distinct from old.sent_at\n       and new.viewed_at is not distinct from old.viewed_at\n       and new.signed_at is not distinct from old.signed_at then\n      return new;\n    end if;\n    v_prev := old.status;\n    if new.signed_at is not null and old.signed_at is null then\n      v_event := 'agreement_signed'; v_dedupe := 'agreement-signed:' || new.id::text;\n    elsif lower(coalesce(new.status,'')) = 'declined' and lower(coalesce(old.status,'')) <> 'declined' then\n      v_event := 'agreement_declined'; v_dedupe := 'agreement-declined:' || new.id::text;\n    elsif new.viewed_at is not null and old.viewed_at is null then\n      v_event := 'agreement_viewed'; v_dedupe := null;\n    elsif new.sent_at is not null and old.sent_at is null then\n      v_event := 'agreement_sent'; v_dedupe := null;\n    else\n      v_event := 'agreement_updated'; v_dedupe := null;\n    end if;\n  end if;\n\n  perform public.business_activity_project(\n    new.tenant_id,\n    v_event,\n    initcap(replace(v_event, '_', ' ')),\n    jsonb_build_object(\n      'contact_id', new.contact_id,\n      'actor_user_id', coalesce(auth.uid(), new.sent_by_user_id),\n      'description', coalesce(new.template_type, 'Agreement') || case when new.recipient_name is not null then ' · ' || new.recipient_name else '' end,\n      'metadata', jsonb_build_object('recipient_email', new.recipient_email, 'template_type', new.template_type, 'event_booking_id', new.event_booking_id),\n      'related_entity_type', 'agreement',\n      'related_entity_id', new.id,\n      'event_domain', 'document',\n      'correlation_key', case when new.contact_id is not null then 'contact:' || new.contact_id::text else 'agreement:' || new.id::text end,\n      'previous_state', v_prev,\n      'new_state', new.status,\n      'outcome', case when v_event='agreement_signed' then 'signed' when v_event='agreement_declined' then 'declined' else null end,\n      'dedupe_key', v_dedupe,\n      'source_record_type', 'pending_agreements',\n      'source_record_id', new.id,\n      'occurred_at', coalesce(new.signed_at, new.viewed_at, new.sent_at, new.updated_at, new.created_at, now())\n    )\n  );\n  return new;\nend;\n$$;\n\ndrop trigger if exists pending_agreements_business_activity on public.pending_agreements;\ncreate trigger pending_agreements_business_activity\nafter insert or update on public.pending_agreements\nfor each row execute function public.project_pending_agreement_business_activity();\n\ncreate or replace function public.project_signed_agreement_business_activity()\nreturns trigger\nlanguage plpgsql\nsecurity definer\nset search_path to 'public','auth','pg_temp'\nas $$\ndeclare\n  v_event text;\n  v_prev text;\n  v_dedupe text;\nbegin\n  if new.tenant_id is null then return new; end if;\n  if tg_op = 'INSERT' then\n    v_event := 'signed_agreement_recorded'; v_prev := null; v_dedupe := 'signed-agreement:' || new.id::text;\n  else\n    if new.status is not distinct from old.status\n       and new.declined_at is not distinct from old.declined_at\n       and new.signed_at is not distinct from old.signed_at then\n      return new;\n    end if;\n    v_prev := old.status;\n    if new.declined_at is not null and old.declined_at is null then\n      v_event := 'signed_agreement_declined'; v_dedupe := 'signed-agreement-declined:' || new.id::text;\n    else\n      v_event := 'signed_agreement_updated'; v_dedupe := null;\n    end if;\n  end if;\n\n  perform public.business_activity_project(\n    new.tenant_id,\n    v_event,\n    initcap(replace(v_event, '_', ' ')),\n    jsonb_build_object(\n      'contact_id', new.contact_id,\n      'actor_user_id', coalesce(auth.uid(), new.user_id),\n      'description', coalesce(new.agreement_type, 'Signed agreement'),\n      'metadata', jsonb_build_object('agreement_version', new.agreement_version, 'signer_role', new.signer_role, 'event_booking_id', new.event_booking_id),\n      'related_entity_type', 'signed_agreement',\n      'related_entity_id', new.id,\n      'event_domain', 'document',\n      'correlation_key', case when new.contact_id is not null then 'contact:' || new.contact_id::text else 'signed-agreement:' || new.id::text end,\n      'previous_state', v_prev,\n      'new_state', new.status,\n      'outcome', case when v_event='signed_agreement_recorded' then 'signed' when v_event='signed_agreement_declined' then 'declined' else null end,\n      'dedupe_key', v_dedupe,\n      'source_record_type', 'signed_agreements',\n      'source_record_id', new.id,\n      'occurred_at', coalesce(new.declined_at, new.signed_at, now())\n    )\n  );\n  return new;\nend;\n$$;\n\ndrop trigger if exists signed_agreements_business_activity on public.signed_agreements;\ncreate trigger signed_agreements_business_activity\nafter insert or update on public.signed_agreements\nfor each row execute function public.project_signed_agreement_business_activity();\n\ncreate or replace function public.project_task_business_activity()\nreturns trigger\nlanguage plpgsql\nsecurity definer\nset search_path to 'public','auth','pg_temp'\nas $$\ndeclare\n  v_tid uuid := public.business_activity_safe_uuid(new.tenant_id);\n  v_contact uuid := coalesce(new.canonical_contact_id, public.business_activity_safe_uuid(new.contact_id));\n  v_event text;\n  v_prev text;\n  v_dedupe text;\nbegin\n  if v_tid is null then return new; end if;\n  if tg_op = 'INSERT' then\n    v_event := 'task_created'; v_prev := null; v_dedupe := 'task-created:' || new.id::text;\n  else\n    if new.status is not distinct from old.status\n       and new.assigned_to is not distinct from old.assigned_to\n       and new.due_date is not distinct from old.due_date\n       and new.completed_at is not distinct from old.completed_at\n       and new.priority is not distinct from old.priority then\n      return new;\n    end if;\n    v_prev := old.status;\n    if (new.completed_at is not null and old.completed_at is null) or (lower(coalesce(new.status,''))='completed' and lower(coalesce(old.status,''))<>'completed') then\n      v_event := 'task_completed'; v_dedupe := 'task-completed:' || new.id::text;\n    else\n      v_event := 'task_updated'; v_dedupe := null;\n    end if;\n  end if;\n\n  perform public.business_activity_project(\n    v_tid,\n    v_event,\n    initcap(replace(v_event, '_', ' ')),\n    jsonb_build_object(\n      'contact_id', v_contact,\n      'actor_user_id', coalesce(auth.uid(), new.user_id),\n      'description', new.title,\n      'metadata', jsonb_build_object('priority', new.priority, 'assigned_to', new.assigned_to, 'due_date', new.due_date, 'source_kind', new.source_kind, 'source_id', new.source_id),\n      'related_entity_type', 'task',\n      'related_entity_id', new.id,\n      'event_domain', 'workflow',\n      'correlation_key', case when v_contact is not null then 'contact:' || v_contact::text else 'task:' || new.id::text end,\n      'previous_state', v_prev,\n      'new_state', new.status,\n      'outcome', case when v_event='task_completed' then 'completed' else null end,\n      'dedupe_key', v_dedupe,\n      'source_record_type', 'tasks',\n      'source_record_id', new.id,\n      'occurred_at', coalesce(new.completed_at, new.updated_at, new.created_at, now())\n    )\n  );\n  return new;\nend;\n$$;\n\ndrop trigger if exists tasks_business_activity on public.tasks;\ncreate trigger tasks_business_activity\nafter insert or update on public.tasks\nfor each row execute function public.project_task_business_activity();\n\ncreate or replace function public.project_claim_business_activity()\nreturns trigger\nlanguage plpgsql\nsecurity definer\nset search_path to 'public','auth','pg_temp'\nas $$\ndeclare\n  v_tid uuid := public.business_activity_safe_uuid(new.tenant_org_id);\n  v_contact uuid := coalesce(new.canonical_contact_id, public.business_activity_safe_uuid(new.contact_id));\n  v_event text;\n  v_prev text;\n  v_state text := coalesce(new.claim_stage, new.claim_status, new.claim_payment_status::text, new.payment_pipeline_status::text);\n  v_dedupe text;\nbegin\n  if v_tid is null then return new; end if;\n  if tg_op = 'INSERT' then\n    v_event := 'claim_created'; v_prev := null; v_dedupe := 'claim-created:' || new.id::text;\n  else\n    if new.claim_status is not distinct from old.claim_status\n       and new.claim_stage is not distinct from old.claim_stage\n       and new.claim_payment_status is not distinct from old.claim_payment_status\n       and new.payment_pipeline_status is not distinct from old.payment_pipeline_status\n       and new.settlement_amount is not distinct from old.settlement_amount\n       and new.fee_status is not distinct from old.fee_status\n       and new.check_issued_at is not distinct from old.check_issued_at\n       and new.check_received_by_homeowner_at is not distinct from old.check_received_by_homeowner_at then\n      return new;\n    end if;\n    v_prev := coalesce(old.claim_stage, old.claim_status, old.claim_payment_status::text, old.payment_pipeline_status::text);\n    if new.settlement_amount is not null and old.settlement_amount is null then\n      v_event := 'claim_settled'; v_dedupe := 'claim-settled:' || new.id::text;\n    elsif new.claim_payment_status is distinct from old.claim_payment_status or new.payment_pipeline_status is distinct from old.payment_pipeline_status then\n      v_event := 'claim_payment_updated'; v_dedupe := null;\n    else\n      v_event := 'claim_updated'; v_dedupe := null;\n    end if;\n  end if;\n\n  perform public.business_activity_project(\n    v_tid,\n    v_event,\n    initcap(replace(v_event, '_', ' ')),\n    jsonb_build_object(\n      'contact_id', v_contact,\n      'actor_user_id', coalesce(auth.uid(), new.claim_payment_status_updated_by, new.check_issued_by),\n      'description', coalesce(new.claim_number, new.carrier, 'Claim'),\n      'metadata', jsonb_build_object('carrier', new.carrier, 'claim_type', new.claim_type, 'fee_status', new.fee_status, 'claim_payment_status', new.claim_payment_status, 'payment_pipeline_status', new.payment_pipeline_status),\n      'related_entity_type', 'claim',\n      'related_entity_id', new.id,\n      'event_domain', 'claims',\n      'correlation_key', case when v_contact is not null then 'contact:' || v_contact::text else 'claim:' || new.id::text end,\n      'previous_state', v_prev,\n      'new_state', v_state,\n      'monetary_value', new.settlement_amount,\n      'outcome', case when v_event='claim_settled' then 'settled' else null end,\n      'dedupe_key', v_dedupe,\n      'source_record_type', 'contact_claims',\n      'source_record_id', new.id,\n      'occurred_at', coalesce(new.claim_payment_status_updated_at, new.check_received_by_homeowner_at, new.check_issued_at, new.updated_at, new.created_at, now())\n    )\n  );\n  return new;\nend;\n$$;\n\ndrop trigger if exists contact_claims_business_activity on public.contact_claims;\ncreate trigger contact_claims_business_activity\nafter insert or update on public.contact_claims\nfor each row execute function public.project_claim_business_activity();\n\ncreate or replace function public.project_event_booking_business_activity()\nreturns trigger\nlanguage plpgsql\nsecurity definer\nset search_path to 'public','auth','pg_temp'\nas $$\ndeclare\n  v_tid uuid := public.business_activity_safe_uuid(new.tenant_org_id);\n  v_event text;\n  v_prev text;\n  v_dedupe text;\nbegin\n  if v_tid is null then return new; end if;\n  if tg_op = 'INSERT' then\n    v_event := 'booking_created'; v_prev := null; v_dedupe := 'booking-created:' || new.id::text;\n  else\n    if new.status is not distinct from old.status\n       and new.stage is not distinct from old.stage\n       and new.event_date is not distinct from old.event_date\n       and new.start_time is not distinct from old.start_time\n       and new.quote_amount is not distinct from old.quote_amount\n       and new.deposit_amount is not distinct from old.deposit_amount\n       and new.balance_due is not distinct from old.balance_due\n       and new.confirmed_at is not distinct from old.confirmed_at\n       and new.completed_at is not distinct from old.completed_at then\n      return new;\n    end if;\n    v_prev := coalesce(old.status, old.stage);\n    if new.completed_at is not null and old.completed_at is null then\n      v_event := 'booking_completed'; v_dedupe := 'booking-completed:' || new.id::text;\n    elsif lower(coalesce(new.status,'')) = 'cancelled' and lower(coalesce(old.status,'')) <> 'cancelled' then\n      v_event := 'booking_cancelled'; v_dedupe := 'booking-cancelled:' || new.id::text;\n    elsif new.confirmed_at is not null and old.confirmed_at is null then\n      v_event := 'booking_confirmed'; v_dedupe := 'booking-confirmed:' || new.id::text;\n    else\n      v_event := 'booking_updated'; v_dedupe := null;\n    end if;\n  end if;\n\n  perform public.business_activity_project(\n    v_tid,\n    v_event,\n    initcap(replace(v_event, '_', ' ')),\n    jsonb_build_object(\n      'contact_id', new.contact_id,\n      'actor_user_id', coalesce(auth.uid(), new.created_by),\n      'description', coalesce(new.event_name, new.venue_name, 'Event booking'),\n      'metadata', jsonb_build_object('event_type', new.event_type, 'venue_name', new.venue_name, 'event_date', new.event_date, 'start_time', new.start_time, 'guest_count', new.guest_count, 'service_package', new.service_package),\n      'related_entity_type', 'event_booking',\n      'related_entity_id', new.id,\n      'event_domain', 'booking',\n      'source_channel', new.source,\n      'source_detail', new.source_ref,\n      'correlation_key', case when new.contact_id is not null then 'contact:' || new.contact_id::text else 'booking:' || new.id::text end,\n      'previous_state', v_prev,\n      'new_state', coalesce(new.status, new.stage),\n      'monetary_value', new.quote_amount,\n      'outcome', case when v_event='booking_completed' then 'completed' when v_event='booking_cancelled' then 'cancelled' when v_event='booking_confirmed' then 'confirmed' else null end,\n      'dedupe_key', v_dedupe,\n      'source_record_type', 'event_bookings',\n      'source_record_id', new.id,\n      'occurred_at', coalesce(new.completed_at, new.confirmed_at, new.updated_at, new.created_at, now())\n    )\n  );\n  return new;\nend;\n$$;\n\ndrop trigger if exists event_bookings_business_activity on public.event_bookings;\ncreate trigger event_bookings_business_activity\nafter insert or update on public.event_bookings\nfor each row execute function public.project_event_booking_business_activity();\n\ncreate or replace function public.project_roof_estimate_business_activity()\nreturns trigger\nlanguage plpgsql\nsecurity definer\nset search_path to 'public','auth','pg_temp'\nas $$\ndeclare\n  v_tid uuid := public.business_activity_safe_uuid(new.tenant_org_id);\n  v_contact uuid := public.business_activity_safe_uuid(new.contact_id);\n  v_event text;\n  v_prev text;\n  v_state text := coalesce(new.approval_status, new.status);\n  v_dedupe text;\nbegin\n  if v_tid is null then return new; end if;\n  if tg_op = 'INSERT' then\n    v_event := 'estimate_created'; v_prev := null; v_dedupe := 'roof-estimate-created:' || new.id::text;\n  else\n    if new.status is not distinct from old.status\n       and new.approval_status is not distinct from old.approval_status\n       and new.approved_at is not distinct from old.approved_at\n       and new.total_job_price is not distinct from old.total_job_price\n       and new.total_squares is not distinct from old.total_squares\n       and new.report_pdf_path is not distinct from old.report_pdf_path then\n      return new;\n    end if;\n    v_prev := coalesce(old.approval_status, old.status);\n    if (new.approved_at is not null and old.approved_at is null) or (lower(coalesce(new.approval_status,''))='approved' and lower(coalesce(old.approval_status,''))<>'approved') then\n      v_event := 'estimate_approved'; v_dedupe := 'roof-estimate-approved:' || new.id::text;\n    elsif lower(coalesce(new.approval_status,''))='rejected' and lower(coalesce(old.approval_status,''))<>'rejected' then\n      v_event := 'estimate_rejected'; v_dedupe := 'roof-estimate-rejected:' || new.id::text;\n    else\n      v_event := 'estimate_updated'; v_dedupe := null;\n    end if;\n  end if;\n\n  perform public.business_activity_project(\n    v_tid,\n    v_event,\n    initcap(replace(v_event, '_', ' ')),\n    jsonb_build_object(\n      'contact_id', v_contact,\n      'actor_user_id', coalesce(auth.uid(), new.approved_by, new.created_by_user_id),\n      'description', coalesce(new.property_address, 'Roof estimate'),\n      'metadata', jsonb_build_object('roof_type', new.roof_type, 'total_squares', new.total_squares, 'pricing_mode', new.pricing_mode, 'report_pdf_path', new.report_pdf_path),\n      'related_entity_type', 'roof_estimate',\n      'related_entity_id', new.id,\n      'event_domain', 'sales',\n      'correlation_key', case when v_contact is not null then 'contact:' || v_contact::text else 'roof-estimate:' || new.id::text end,\n      'previous_state', v_prev,\n      'new_state', v_state,\n      'monetary_value', new.total_job_price,\n      'outcome', case when v_event='estimate_approved' then 'approved' when v_event='estimate_rejected' then 'rejected' else null end,\n      'dedupe_key', v_dedupe,\n      'source_record_type', 'roof_estimates',\n      'source_record_id', new.id,\n      'occurred_at', coalesce(new.approved_at, new.updated_at, new.created_at, now())\n    )\n  );\n  return new;\nend;\n$$;\n\ndrop trigger if exists roof_estimates_business_activity on public.roof_estimates;\ncreate trigger roof_estimates_business_activity\nafter insert or update on public.roof_estimates\nfor each row execute function public.project_roof_estimate_business_activity();"],"idempotency_key":null},{"name":"portfolio_control_center_classification","version":"20260912142000","rollback":null,"created_by":"canonical-production-release","statements":["create or replace function public.normalize_declared_business_type(p_value text)\nreturns text\nlanguage plpgsql\nimmutable\nset search_path to 'public'\nas $function$\ndeclare\n  v_token text := trim(both '_' from regexp_replace(upper(trim(coalesce(p_value, ''))), '[^A-Z0-9]+', '_', 'g'));\nbegin\n  if v_token = '' or v_token in ('PLATFORM','PLATFORM_ADMIN','GODMODE','ADMIN','SYSTEM_ADMIN') then return null; end if;\n  return case\n    when v_token in ('MITIGATION','EMERGENCY_MITIGATION','RESTORATION','WATER_MITIGATION','TARPING','SCETARPING','SCE_TARPING') then 'MITIGATION'\n    when v_token in ('ROOFING','ROOFER','ROOFING_COMPANY','ROOFINGCOMPANY') then 'ROOFING_COMPANY'\n    when v_token in ('PUBLIC_ADJUSTER','PUBLIC_ADJUSTER_FIRM','PUBLICADJUSTERFIRM','PA') then 'PUBLIC_ADJUSTER_FIRM'\n    when v_token in ('REALTOR','REAL_ESTATE','REAL_ESTATE_AGENT','BROKERAGE') then 'REALTOR'\n    when v_token in ('LAWN_CARE','LAWNCARE','LAWN','LANDSCAPING','ROUTE_BASED','ROUTE_BASED_SERVICE','FIELD_SERVICE') then 'LAWN_CARE'\n    when v_token in ('TIRE_SHOP','TIRESHOP','TIRE_STORE','TIRE_AND_AUTO','TIRE_AUTO') then 'TIRE_SHOP'\n    when v_token in ('AUTO_DETAILING','AUTODETAILING','AUTO_DETAILER','MOBILE_DETAILING','CAR_DETAILING','DETAILING') then 'AUTO_DETAILING'\n    when v_token in ('CAFE_QSR','CAFE','QSR','QUICK_SERVICE','QUICK_SERVICE_RESTAURANT','FOOD_BEVERAGE','FOOD_AND_BEVERAGE','RESTAURANT') then 'CAFE_QSR'\n    when v_token in ('MOBILE_VENDOR','MOBILEVENDOR','FOOD_TRUCK','FOODTRUCK','MOBILE_KITCHEN','MOBILEKITCHEN','POP_UP_VENDOR') then 'MOBILE_VENDOR'\n    when v_token in ('SHORT_TERM_RENTAL','SHORT_TERM_RENTALS','VACATION_RENTAL','VACATION_RENTALS','STR') then 'SHORT_TERM_RENTAL'\n    when v_token in ('PORTFOLIO_CONTROL_CENTER','PORTFOLIO_CONTROL','EXECUTIVE_CONTROL_CENTER','EXECUTIVE_OVERSIGHT') then 'PORTFOLIO_CONTROL_CENTER'\n    when v_token in ('MISC','MISCELLANEOUS') then 'MISC'\n    when v_token ~ '^[A-Z][A-Z0-9_]{1,95}$' then v_token\n    else null\n  end;\nend;\n$function$;\n\nupdate public.tenants\nset\n  tenant_type = 'PORTFOLIO_CONTROL_CENTER',\n  settings = jsonb_set(\n    jsonb_set(\n      jsonb_set(\n        jsonb_set(\n          coalesce(settings, '{}'::jsonb) || jsonb_build_object(\n            'declared_business_type', 'PORTFOLIO_CONTROL_CENTER',\n            'experience_family', 'modes',\n            'shell_template', 'MODES'\n          ),\n          '{tenant_manifest,businessTypeKey}', to_jsonb('PORTFOLIO_CONTROL_CENTER'::text), true\n        ),\n        '{tenant_manifest,moduleTenantType}', to_jsonb('PORTFOLIO_CONTROL_CENTER'::text), true\n      ),\n      '{tenant_manifest,businessOperatingBlueprint,businessTypeKey}', to_jsonb('PORTFOLIO_CONTROL_CENTER'::text), true\n    ),\n    '{business_operating_blueprint,businessTypeKey}', to_jsonb('PORTFOLIO_CONTROL_CENTER'::text), true\n  )\nwhere id = '60911b66-2940-4f68-9b88-cd17d1bdf5b6'::uuid\n  and company_slug = 'curri-control-center';\n"],"idempotency_key":"767ebb136efefaf2ced5653e72690ec13d8fb7da:20260912142000"},{"name":"private_owner_search_console_provider","version":"20260912151000","rollback":null,"created_by":"canonical-production-release","statements":["alter table public.tenant_provider_credentials\n  drop constraint if exists tenant_provider_credentials_provider_check;\nalter table public.tenant_provider_credentials\n  add constraint tenant_provider_credentials_provider_check\n  check (provider = any (array['twilio'::text,'resend'::text,'stripe'::text,'square'::text,'zoho_mail'::text,'zoho_mail_admin'::text,'google_workspace'::text,'google_search_console'::text,'microsoft_365'::text,'monday'::text,'quickbooks'::text,'slack'::text,'dropbox'::text,'hubspot'::text,'docusign'::text,'other'::text]));\n\nalter table public.tenant_provider_secrets\n  drop constraint if exists tenant_provider_secrets_provider_check;\nalter table public.tenant_provider_secrets\n  add constraint tenant_provider_secrets_provider_check\n  check (provider = any (array['twilio'::text,'resend'::text,'stripe'::text,'square'::text,'zoho_mail'::text,'zoho_mail_admin'::text,'google_workspace'::text,'google_search_console'::text,'microsoft_365'::text,'monday'::text,'quickbooks'::text,'slack'::text,'dropbox'::text,'hubspot'::text,'docusign'::text]));\n\nalter table public.tenant_provider_oauth_states\n  drop constraint if exists tenant_provider_oauth_states_provider_check;\nalter table public.tenant_provider_oauth_states\n  add constraint tenant_provider_oauth_states_provider_check\n  check (provider = any (array['google_workspace'::text,'google_search_console'::text,'microsoft_365'::text,'monday'::text,'quickbooks'::text,'slack'::text,'dropbox'::text,'hubspot'::text,'docusign'::text]));\n"],"idempotency_key":"20350c5abaffeba22ba3fae993bf328865c4f15f:20260912151000"},{"name":"owner_proposals_personal_tenant","version":"20260912152000","rollback":null,"created_by":"canonical-production-release","statements":["-- Owner-authored proposal ownership belongs to Knock Launch OS; counterparty tenant records are projections only.\n-- Align the unsent Redfish draft with the 16-page BONUS artifact while preserving all commercial lanes.\n-- The Business Activity Ledger projector emits proposal_* events, so its governing timeline constraint must accept that family.\n\nalter table public.activity_timeline drop constraint if exists activity_timeline_activity_type_check;\nalter table public.activity_timeline add constraint activity_timeline_activity_type_check check (\n  activity_type = any(array[\n    'call'::text,'email'::text,'sms'::text,'note'::text,'task'::text,'meeting'::text,'status_change'::text,'stage_change'::text,'assignment'::text,'document'::text,'photo'::text,'automation'::text,'score_change'::text,'merge'::text,'import'::text,'export'::text,'field_update'::text,'contact_created'::text,'contact_updated'::text,'job_created'::text,'job_updated'::text,'invoice_created'::text,'invoice_paid'::text,'invoice_updated'::text,'settlement_invoice_created'::text,'settlement_invoice_paid'::text,'settlement_invoice_updated'::text,'business_joined_platform'::text,'business_lifecycle_updated'::text\n  ]) or activity_type ~ '^(opportunity|communication|document|user|proposal)_[A-Za-z0-9_.:-]+$'::text\n);\n\ndo $$\ndeclare\n  v_owner_tenant_id uuid;\n  v_redfish_tenant_id uuid;\n  v_proposal_id uuid;\nbegin\n  select id into v_owner_tenant_id from public.tenants where company_slug='knocklaunch-crm' limit 1;\n  select id into v_redfish_tenant_id from public.tenants where company_slug='redfish-roofing' limit 1;\n  select id into v_proposal_id from public.proposals where idempotency_key='knocklaunch:redfish:growth-media-technology:v2' limit 1;\n  if v_owner_tenant_id is null or v_redfish_tenant_id is null or v_proposal_id is null then return; end if;\n\n  update public.proposals\n  set tenant_id=v_owner_tenant_id,\n      counterparty_tenant_id=v_redfish_tenant_id,\n      current_version=2,\n      metadata=jsonb_set(\n        jsonb_set(\n          jsonb_set(coalesce(metadata,'{}'::jsonb),'{personalTenantOwnership}',jsonb_build_object('ownerTenantId',v_owner_tenant_id,'ownerTenantSlug','knocklaunch-crm','workspace','/knock-launch-os/proposals','counterpartyProjectionOnly',true),true),\n          '{finalArtifact}',jsonb_build_object('filename','Redfish_Roofing_Integrated_Growth_Media_Technology_Partnership_2026_BONUS.pdf','pageCount',16,'status','ready_for_tenant_attachment'),true\n        ),\n        '{bonusConcept}',jsonb_build_object('title','Storm Response Partnership Funnel','partner','Space Coast Emergency Tarping','status','optional_non_binding','activationRule','Only activates if separately agreed with Space Coast Emergency Tarping.','purpose','Emergency tarping revenue can create immediate cash flow, strengthen advertising capacity and open downstream roofing opportunities.'),true\n      ),\n      updated_at=now()\n  where id=v_proposal_id and status='draft' and sent_at is null and accepted_at is null;\n\n  update public.proposals\n  set metadata=jsonb_set(metadata,'{operatingContract,signatureFields}',jsonb_build_array(\n    jsonb_build_object('id','acceptor-signature','type','signature','label','Signature','required',true,'page',16,'x',8,'y',80,'width',42,'height',7),\n    jsonb_build_object('id','acceptor-date','type','date','label','Date','required',true,'page',16,'x',55,'y',80,'width',28,'height',5)\n  ),true), updated_at=now()\n  where id=v_proposal_id and status='draft' and sent_at is null and accepted_at is null;\n\n  insert into public.proposal_versions(proposal_id,tenant_id,version,reason,snapshot,snapshot_hash,created_by)\n  select p.id,p.tenant_id,2,'bonus_storm_response_and_personal_tenant_ownership',to_jsonb(p),encode(digest(to_jsonb(p)::text,'sha256'),'hex'),p.created_by\n  from public.proposals p where p.id=v_proposal_id\n  on conflict(proposal_id,version) do update set reason=excluded.reason,snapshot=excluded.snapshot,snapshot_hash=excluded.snapshot_hash;\n\n  update public.proposal_operating_contracts set proposal_version=2,updated_at=now() where proposal_id=v_proposal_id and lifecycle_status='draft';\n  update public.proposal_operating_commitments set proposal_version=2,updated_at=now() where proposal_id=v_proposal_id and status='proposed';\n\n  update public.owner_relationship_proposals\n  set attachment_ref=jsonb_build_object('status','ready_for_personal_tenant_attachment','filename','Redfish_Roofing_Integrated_Growth_Media_Technology_Partnership_2026_BONUS.pdf'),\n      metadata=coalesce(metadata,'{}'::jsonb)||jsonb_build_object('projectionOnly',true,'canonicalOwnerTenantId',v_owner_tenant_id,'canonicalOwnerTenantSlug','knocklaunch-crm','canonicalWorkspace','/knock-launch-os/proposals','bonusConcept','Storm Response Partnership Funnel'),\n      updated_at=now()\n  where canonical_proposal_id=v_proposal_id and status='draft';\n\n  update public.owner_relationship_proposals\n  set metadata=coalesce(metadata,'{}'::jsonb)||jsonb_build_object('projectionOnly',true,'canonicalOwnerTenantId',v_owner_tenant_id,'canonicalOwnerTenantSlug','knocklaunch-crm','canonicalWorkspace','/knock-launch-os/proposals'),updated_at=now()\n  where tenant_id=v_redfish_tenant_id and source_id='redfish-jimmy-six-month-growth-partnership-v1';\nend;$$;\n"],"idempotency_key":"9ecdb6cdeba660c5beef111dbf7a44a510b22689:20260912152000"},{"name":"event_rentals_tenant_type","version":"20260912163500","rollback":null,"created_by":"canonical-production-release","statements":["-- Event Rentals is a first-class declared tenant blueprint layered on canonical event, contact, document, billing, task, media, and reporting primitives.\n\n\nCREATE OR REPLACE FUNCTION public.event_rentals_operating_contract()\nRETURNS jsonb LANGUAGE sql IMMUTABLE SET search_path TO 'public' AS $$\nSELECT $json${\n  \"contractVersion\": 1,\n  \"serviceLevels\": [\n    {\"key\":\"rent_it\",\"label\":\"Rent It\",\"description\":\"Equipment rental with customer-managed event execution.\"},\n    {\"key\":\"set_it_up\",\"label\":\"Set It Up\",\"description\":\"Delivery, setup, breakdown, pickup, return, and inspection.\"},\n    {\"key\":\"activate_my_business\",\"label\":\"Activate My Business\",\"description\":\"Turn a client property into a measurable promotional or community event.\"},\n    {\"key\":\"produce_my_event\",\"label\":\"Produce My Event\",\"description\":\"Full event production with infrastructure, vendors, readiness, execution, and outcomes.\"}\n  ],\n  \"modes\": [\n    {\"key\":\"rentals\",\"label\":\"Rentals\",\"applications\":[\"inventory\",\"availability\",\"quotes_bookings\",\"delivery_pickup\",\"returns\",\"damage_condition\",\"packages\"]},\n    {\"key\":\"events\",\"label\":\"Events\",\"applications\":[\"event_calendar\",\"run_of_show\",\"site_plan\",\"attendance\",\"crew\",\"weather_contingency\",\"setup_teardown\",\"event_readiness\"]},\n    {\"key\":\"activations\",\"label\":\"Activations\",\"applications\":[\"client_account\",\"event_objective\",\"promotion_registration\",\"lead_capture\",\"attendance\",\"sponsors\",\"roi_outcomes\",\"follow_up\"]},\n    {\"key\":\"vendors\",\"label\":\"Vendors\",\"applications\":[\"vendor_directory\",\"assignments\",\"food_service\",\"entertainment\",\"security\",\"decor\",\"compliance_licenses\",\"insurance_coi\",\"documents\",\"performance_history\"]}\n  ],\n  \"assetCategories\": [\"commercial_tents_canopies\",\"chairs\",\"rectangular_tables\",\"round_tables\",\"cocktail_high_top_tables\",\"sidewalls\",\"lighting\",\"linens\",\"signage_backdrops\",\"stanchions_check_in\",\"trash_recycling\",\"coolers_beverage_stations\",\"carts_dollies\",\"storage_bins\",\"cleaning_maintenance\",\"trailers_transportation\",\"custom\"],\n  \"assetLifecycleStatuses\": [\"available\",\"inspection_needed\",\"damaged\",\"repair\",\"retired\"],\n  \"allocationStatuses\": [\"reserved\",\"deployed\",\"returned\",\"cancelled\"],\n  \"deliveryWorkflow\": [\"load_plan\",\"delivery\",\"setup\",\"event_live\",\"breakdown\",\"pickup_return\",\"inspection\",\"cleaning_maintenance\",\"damage_capture\",\"complete\"],\n  \"readinessCategories\": [\"site_authorization\",\"temporary_structure_documents\",\"insurance_coi\",\"permits\",\"weather_contingency\",\"power_electrical\",\"sanitation_trash\",\"emergency_access\",\"security\",\"food_vendor_compliance\",\"alcohol_provider\",\"vendor_confirmations\",\"inventory_allocation\",\"crew_assignments\",\"setup_breakdown_plan\",\"required_documents\"],\n  \"vendorFields\": [\"category\",\"contact_reference\",\"event_assignments\",\"status\",\"license_metadata\",\"document_metadata\",\"insurance_coi_metadata\",\"insurance_expiration\",\"notes\",\"performance_history\"],\n  \"activationMetrics\": [\"registrations\",\"attendance\",\"leads_captured\",\"appointments_or_conversions_entered\",\"sponsor_participation\",\"sponsor_revenue\",\"event_expenses\",\"follow_up_state\"],\n  \"eventOwnershipTypes\": [\"client_event\",\"owned_event\"],\n  \"packageModel\": \"outcome_oriented_with_internal_asset_allocation\",\n  \"financeCategories\": [\"equipment_rental\",\"delivery\",\"setup_breakdown\",\"tent_infrastructure\",\"decor_lighting\",\"event_management_production\",\"vendor_coordination\",\"sponsorship\",\"media_content\",\"signage_branding\",\"deposit\",\"damage_charge\",\"taxes_fees\",\"event_specific_expense\"],\n  \"homePriorities\": [\"upcoming_bookings_events\",\"readiness_risks\",\"inventory_commitments_conflicts_utilization\",\"deliveries_pickups\",\"outstanding_deposits_balances\",\"vendor_compliance_expirations\",\"next_operational_tasks\"]\n}$json$::jsonb;\n$$;\n\nCREATE OR REPLACE FUNCTION public.prepare_declared_tenant_blueprint()\nRETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$\nDECLARE v_declared text; v_model text; v_blueprint jsonb; v_apps jsonb; v_manifest jsonb; v_custom boolean; v_slug text;\nBEGIN\n  v_declared:=public.normalize_declared_business_type(coalesce(nullif(new.settings->>'declared_business_type',''),nullif(new.settings->>'business_type',''),nullif(new.settings->>'business_category',''),nullif(new.settings#>>'{business_operating_blueprint,businessTypeKey}',''),nullif(new.tenant_type,'')));\n  IF v_declared IS NULL THEN RAISE EXCEPTION 'A valid declared business type is required before tenant creation' USING ERRCODE='22023'; END IF;\n  new.tenant_type:=v_declared;\n  v_slug:=nullif(trim(coalesce(new.company_slug,'')),'');\n  IF v_slug IS NULL THEN\n    v_slug:=trim(both '-' from regexp_replace(lower(new.name),'[^a-z0-9]+','-','g'));\n    IF v_slug='' THEN v_slug:='tenant'; END IF;\n    IF EXISTS(SELECT 1 FROM public.tenants existing WHERE existing.company_slug=v_slug) THEN v_slug:=v_slug||'-'||substr(replace(new.id::text,'-',''),1,6); END IF;\n  END IF;\n  new.company_slug:=v_slug;\n  v_model:=public.tenant_operating_model_for(v_declared,coalesce(new.settings,'{}'::jsonb));\n  IF v_model IS NULL THEN RAISE EXCEPTION 'Operating model is required for declared business type %; tenant creation will not generalize it to MISC',v_declared USING ERRCODE='22023'; END IF;\n  v_blueprint:=public.build_tenant_business_blueprint(v_declared,coalesce(new.settings,'{}'::jsonb),new.name);\n  IF v_declared='EVENT_RENTALS' THEN\n    v_blueprint:=v_blueprint||public.event_rentals_operating_contract()||jsonb_build_object(\n      'businessTypeKey','EVENT_RENTALS','operatingModel','events',\n      'primaryRelationship',jsonb_build_object('singular','Client','plural','Clients','kind','client'),\n      'workObjects',jsonb_build_array(\n        jsonb_build_object('singular','Event','plural','Events','kind','events'),\n        jsonb_build_object('singular','Rental Booking','plural','Rental Bookings','kind','rentals'),\n        jsonb_build_object('singular','Activation','plural','Activations','kind','activations')\n      ),\n      'workflowStages',jsonb_build_array('Inquiry','Quote','Reserved','Prep','Setup','Live','Teardown','Return & Inspection','Complete','Paid'),\n      'vocabulary',jsonb_build_object('workspace','Event Operations','work','Event','workPlural','Events')\n    );\n  END IF;\n  v_apps:=coalesce(v_blueprint->'applications',public.tenant_blueprint_applications(v_model));\n  v_custom:=v_declared NOT IN ('MITIGATION','ROOFING_COMPANY','PUBLIC_ADJUSTER_FIRM','REALTOR','LAWN_CARE','TIRE_SHOP','AUTO_DETAILING','CAFE_QSR','MOBILE_VENDOR','SHORT_TERM_RENTAL','EVENT_RENTALS','MISC');\n  v_manifest:=coalesce(new.settings->'tenant_manifest','{}'::jsonb)||jsonb_build_object('schemaVersion',2,'businessTypeKey',v_declared,'moduleTenantType',CASE WHEN v_custom THEN 'CUSTOM_BLUEPRINT' ELSE v_declared END,'applications',v_apps,'businessOperatingBlueprint',v_blueprint);\n  new.settings:=coalesce(new.settings,'{}'::jsonb)||jsonb_build_object('declared_business_type',v_declared,'operating_model',v_model,'business_operating_blueprint',v_blueprint,'enabled_applications',v_apps,'tenant_manifest',v_manifest,'route_namespace',new.company_slug,'canonical_workspace_route','/'||new.company_slug||'/dashboard','canonical_owner_route','/'||new.company_slug||'/dashboard','canonical_public_route','https://whatsmyprogress.com/'||new.company_slug||'/','bootstrap_state','provisioning')||CASE WHEN v_custom THEN jsonb_build_object('archetype','ADAPTIVE_BUSINESS','experience_family','adaptive-business-operations','workspace_variant',v_model||'_business') ELSE '{}'::jsonb END;\n  IF v_declared='EVENT_RENTALS' AND new.settings->'modes' IS NULL THEN\n    new.settings:=new.settings||jsonb_build_object('modes',jsonb_build_object('version',1,'cards',jsonb_build_array(\n      jsonb_build_object('key','rentals','title','Rentals','subtitle','Inventory, availability, delivery & pickup','color','#4F46E5','route','/'||new.company_slug||'/dashboard?mode=rentals','storage_slug','rentals'),\n      jsonb_build_object('key','events','title','Events','subtitle','Plan, prepare, execute & close','color','#0F766E','route','/'||new.company_slug||'/dashboard?mode=events','storage_slug','events'),\n      jsonb_build_object('key','activations','title','Activations','subtitle','Promote, capture, measure & follow up','color','#B45309','route','/'||new.company_slug||'/dashboard?mode=activations','storage_slug','activations'),\n      jsonb_build_object('key','vendors','title','Vendors','subtitle','Coordinate, verify, assign & review','color','#7C3AED','route','/'||new.company_slug||'/dashboard?mode=vendors','storage_slug','vendors')\n    )));\n  END IF;\n  IF v_model='appointments' THEN new.settings:=new.settings||jsonb_build_object('enabled_automations',coalesce(new.settings->'enabled_automations','[\"booking-confirmation\",\"appointment-reminder\",\"queue-up-next\",\"queue-your-turn\",\"rebook-followup\"]'::jsonb)); END IF;\n  new.status:='inactive'; new.is_active:=false; RETURN new;\nEND;\n$$;\n\nCREATE TABLE IF NOT EXISTS public.event_rental_assets(\n  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n  tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n  asset_code text NOT NULL,\n  name text NOT NULL,\n  category text NOT NULL,\n  description text,\n  total_quantity integer NOT NULL DEFAULT 1 CHECK(total_quantity>0),\n  lifecycle_status text NOT NULL DEFAULT 'available' CHECK(lifecycle_status IN('available','inspection_needed','damaged','repair','retired')),\n  condition_notes text,\n  metadata jsonb NOT NULL DEFAULT '{}'::jsonb,\n  created_at timestamptz NOT NULL DEFAULT now(),\n  updated_at timestamptz NOT NULL DEFAULT now(),\n  UNIQUE(tenant_id,asset_code)\n);\n\nCREATE TABLE IF NOT EXISTS public.event_rental_asset_allocations(\n  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n  tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n  event_booking_id uuid NOT NULL REFERENCES public.event_bookings(id) ON DELETE CASCADE,\n  asset_id uuid NOT NULL REFERENCES public.event_rental_assets(id) ON DELETE RESTRICT,\n  quantity integer NOT NULL CHECK(quantity>0),\n  status text NOT NULL DEFAULT 'reserved' CHECK(status IN('reserved','deployed','returned','cancelled')),\n  starts_at timestamptz NOT NULL,\n  ends_at timestamptz NOT NULL,\n  notes text,\n  metadata jsonb NOT NULL DEFAULT '{}'::jsonb,\n  created_at timestamptz NOT NULL DEFAULT now(),\n  updated_at timestamptz NOT NULL DEFAULT now(),\n  CHECK(ends_at>starts_at)\n);\n\nCREATE TABLE IF NOT EXISTS public.event_rental_vendors(\n  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n  tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n  contact_id uuid,\n  organization_name text NOT NULL,\n  category text NOT NULL,\n  status text NOT NULL DEFAULT 'active' CHECK(status IN('active','inactive','needs_review','blocked')),\n  license_metadata jsonb NOT NULL DEFAULT '{}'::jsonb,\n  document_metadata jsonb NOT NULL DEFAULT '{}'::jsonb,\n  insurance_coi_metadata jsonb NOT NULL DEFAULT '{}'::jsonb,\n  insurance_expires_on date,\n  notes text,\n  performance_history jsonb NOT NULL DEFAULT '{}'::jsonb,\n  metadata jsonb NOT NULL DEFAULT '{}'::jsonb,\n  created_at timestamptz NOT NULL DEFAULT now(),\n  updated_at timestamptz NOT NULL DEFAULT now()\n);\n\nCREATE TABLE IF NOT EXISTS public.event_rental_event_vendors(\n  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n  tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n  event_booking_id uuid NOT NULL REFERENCES public.event_bookings(id) ON DELETE CASCADE,\n  vendor_id uuid NOT NULL REFERENCES public.event_rental_vendors(id) ON DELETE RESTRICT,\n  assignment_role text,\n  status text NOT NULL DEFAULT 'planned' CHECK(status IN('planned','invited','confirmed','onsite','complete','cancelled','blocked')),\n  financial_metadata jsonb NOT NULL DEFAULT '{}'::jsonb,\n  notes text,\n  metadata jsonb NOT NULL DEFAULT '{}'::jsonb,\n  created_at timestamptz NOT NULL DEFAULT now(),\n  updated_at timestamptz NOT NULL DEFAULT now(),\n  UNIQUE(event_booking_id,vendor_id)\n);\n\nCREATE TABLE IF NOT EXISTS public.event_rental_readiness_items(\n  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n  tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n  event_booking_id uuid NOT NULL REFERENCES public.event_bookings(id) ON DELETE CASCADE,\n  item_key text NOT NULL,\n  category text NOT NULL,\n  label text NOT NULL,\n  required boolean NOT NULL DEFAULT true,\n  status text NOT NULL DEFAULT 'pending' CHECK(status IN('pending','ready','blocked','not_applicable')),\n  due_at timestamptz,\n  blocker_reason text,\n  metadata jsonb NOT NULL DEFAULT '{}'::jsonb,\n  created_at timestamptz NOT NULL DEFAULT now(),\n  updated_at timestamptz NOT NULL DEFAULT now(),\n  UNIQUE(event_booking_id,item_key)\n);\n\nCREATE INDEX IF NOT EXISTS event_rental_assets_tenant_category_idx ON public.event_rental_assets(tenant_id,category,lifecycle_status);\nCREATE INDEX IF NOT EXISTS event_rental_allocations_tenant_event_idx ON public.event_rental_asset_allocations(tenant_id,event_booking_id);\nCREATE INDEX IF NOT EXISTS event_rental_allocations_asset_time_idx ON public.event_rental_asset_allocations(asset_id,starts_at,ends_at) WHERE status IN('reserved','deployed');\nCREATE INDEX IF NOT EXISTS event_rental_vendors_tenant_category_idx ON public.event_rental_vendors(tenant_id,category,status);\nCREATE INDEX IF NOT EXISTS event_rental_event_vendors_event_idx ON public.event_rental_event_vendors(tenant_id,event_booking_id,status);\nCREATE INDEX IF NOT EXISTS event_rental_readiness_event_idx ON public.event_rental_readiness_items(tenant_id,event_booking_id,status);\nCREATE INDEX IF NOT EXISTS event_rental_vendor_insurance_idx ON public.event_rental_vendors(tenant_id,insurance_expires_on) WHERE insurance_expires_on IS NOT NULL;\n\nCREATE OR REPLACE FUNCTION public.assert_event_rental_booking_tenant()\nRETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$\nBEGIN\n  IF NOT EXISTS(SELECT 1 FROM public.event_bookings b WHERE b.id=NEW.event_booking_id AND b.tenant_org_id=NEW.tenant_id::text) THEN RAISE EXCEPTION 'Event booking does not belong to the event-rental tenant' USING ERRCODE='23514'; END IF;\n  RETURN NEW;\nEND; $$;\n\nCREATE OR REPLACE FUNCTION public.assert_event_rental_vendor_tenant()\nRETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$\nBEGIN\n  IF NOT EXISTS(SELECT 1 FROM public.event_rental_vendors v WHERE v.id=NEW.vendor_id AND v.tenant_id=NEW.tenant_id) THEN RAISE EXCEPTION 'Event vendor does not belong to the event-rental tenant' USING ERRCODE='23514'; END IF;\n  RETURN NEW;\nEND; $$;\n\nCREATE OR REPLACE FUNCTION public.check_event_rental_allocation_capacity()\nRETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$\nDECLARE v_total integer; v_lifecycle text; v_committed integer;\nBEGIN\n  SELECT a.total_quantity,a.lifecycle_status INTO v_total,v_lifecycle FROM public.event_rental_assets a WHERE a.id=NEW.asset_id AND a.tenant_id=NEW.tenant_id FOR UPDATE;\n  IF NOT FOUND THEN RAISE EXCEPTION 'Event-rental asset does not belong to tenant' USING ERRCODE='23514'; END IF;\n  IF NEW.status IN('reserved','deployed') AND v_lifecycle IN('damaged','repair','retired') THEN RAISE EXCEPTION 'Asset is not allocatable while status is %',v_lifecycle USING ERRCODE='23514'; END IF;\n  IF NEW.status IN('reserved','deployed') THEN\n    SELECT coalesce(sum(x.quantity),0)::integer INTO v_committed FROM public.event_rental_asset_allocations x WHERE x.asset_id=NEW.asset_id AND x.tenant_id=NEW.tenant_id AND x.status IN('reserved','deployed') AND x.id IS DISTINCT FROM NEW.id AND tstzrange(x.starts_at,x.ends_at,'[)')&&tstzrange(NEW.starts_at,NEW.ends_at,'[)');\n    IF v_committed+NEW.quantity>v_total THEN RAISE EXCEPTION 'Asset allocation exceeds available quantity for the requested time window' USING ERRCODE='23514'; END IF;\n  END IF;\n  RETURN NEW;\nEND; $$;\n\nDROP TRIGGER IF EXISTS event_rental_allocations_tenant_guard ON public.event_rental_asset_allocations;\nCREATE TRIGGER event_rental_allocations_tenant_guard BEFORE INSERT OR UPDATE OF tenant_id,event_booking_id ON public.event_rental_asset_allocations FOR EACH ROW EXECUTE FUNCTION public.assert_event_rental_booking_tenant();\nDROP TRIGGER IF EXISTS event_rental_allocations_capacity_guard ON public.event_rental_asset_allocations;\nCREATE TRIGGER event_rental_allocations_capacity_guard BEFORE INSERT OR UPDATE OF tenant_id,event_booking_id,asset_id,quantity,status,starts_at,ends_at ON public.event_rental_asset_allocations FOR EACH ROW EXECUTE FUNCTION public.check_event_rental_allocation_capacity();\nDROP TRIGGER IF EXISTS event_rental_event_vendors_tenant_guard ON public.event_rental_event_vendors;\nCREATE TRIGGER event_rental_event_vendors_tenant_guard BEFORE INSERT OR UPDATE OF tenant_id,event_booking_id ON public.event_rental_event_vendors FOR EACH ROW EXECUTE FUNCTION public.assert_event_rental_booking_tenant();\nDROP TRIGGER IF EXISTS event_rental_event_vendors_vendor_guard ON public.event_rental_event_vendors;\nCREATE TRIGGER event_rental_event_vendors_vendor_guard BEFORE INSERT OR UPDATE OF tenant_id,vendor_id ON public.event_rental_event_vendors FOR EACH ROW EXECUTE FUNCTION public.assert_event_rental_vendor_tenant();\nDROP TRIGGER IF EXISTS event_rental_readiness_tenant_guard ON public.event_rental_readiness_items;\nCREATE TRIGGER event_rental_readiness_tenant_guard BEFORE INSERT OR UPDATE OF tenant_id,event_booking_id ON public.event_rental_readiness_items FOR EACH ROW EXECUTE FUNCTION public.assert_event_rental_booking_tenant();\n\nCREATE OR REPLACE FUNCTION public.event_rental_asset_available_quantity(p_tenant_id uuid,p_asset_id uuid,p_starts_at timestamptz,p_ends_at timestamptz,p_exclude_event_booking_id uuid DEFAULT NULL)\nRETURNS integer LANGUAGE sql STABLE SECURITY INVOKER SET search_path TO 'public' AS $$\nSELECT greatest(0,a.total_quantity-coalesce((SELECT sum(x.quantity)::integer FROM public.event_rental_asset_allocations x WHERE x.tenant_id=p_tenant_id AND x.asset_id=p_asset_id AND x.status IN('reserved','deployed') AND(p_exclude_event_booking_id IS NULL OR x.event_booking_id<>p_exclude_event_booking_id) AND tstzrange(x.starts_at,x.ends_at,'[)')&&tstzrange(p_starts_at,p_ends_at,'[)')),0)) FROM public.event_rental_assets a WHERE a.id=p_asset_id AND a.tenant_id=p_tenant_id;\n$$;\n\nCREATE OR REPLACE FUNCTION public.event_rental_event_readiness_summary(p_tenant_id uuid,p_event_booking_id uuid)\nRETURNS jsonb LANGUAGE sql STABLE SECURITY INVOKER SET search_path TO 'public' AS $$\nWITH items AS(SELECT * FROM public.event_rental_readiness_items r WHERE r.tenant_id=p_tenant_id AND r.event_booking_id=p_event_booking_id), scored AS(SELECT count(*) FILTER(WHERE required AND status<>'not_applicable') AS required_count,count(*) FILTER(WHERE required AND status='ready') AS ready_count,count(*) FILTER(WHERE status='blocked') AS blocker_count,coalesce(jsonb_agg(jsonb_build_object('key',item_key,'label',label,'reason',blocker_reason) ORDER BY label) FILTER(WHERE status='blocked'),'[]'::jsonb) AS blockers FROM items)\nSELECT jsonb_build_object('required',required_count,'ready',ready_count,'blocked',blocker_count,'score',CASE WHEN required_count=0 THEN 100 ELSE round((ready_count::numeric/required_count::numeric)*100)::integer END,'blockers',blockers) FROM scored;\n$$;\n\nALTER TABLE public.event_rental_assets ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.event_rental_asset_allocations ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.event_rental_vendors ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.event_rental_event_vendors ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.event_rental_readiness_items ENABLE ROW LEVEL SECURITY;\n\nDROP POLICY IF EXISTS event_rental_assets_tenant_access ON public.event_rental_assets;\nCREATE POLICY event_rental_assets_tenant_access ON public.event_rental_assets FOR ALL USING(public.is_tenant_member(tenant_id::text) OR public.is_system_admin()) WITH CHECK(public.is_tenant_member(tenant_id::text) OR public.is_system_admin());\nDROP POLICY IF EXISTS event_rental_allocations_tenant_access ON public.event_rental_asset_allocations;\nCREATE POLICY event_rental_allocations_tenant_access ON public.event_rental_asset_allocations FOR ALL USING(public.is_tenant_member(tenant_id::text) OR public.is_system_admin()) WITH CHECK(public.is_tenant_member(tenant_id::text) OR public.is_system_admin());\nDROP POLICY IF EXISTS event_rental_vendors_tenant_access ON public.event_rental_vendors;\nCREATE POLICY event_rental_vendors_tenant_access ON public.event_rental_vendors FOR ALL USING(public.is_tenant_member(tenant_id::text) OR public.is_system_admin()) WITH CHECK(public.is_tenant_member(tenant_id::text) OR public.is_system_admin());\nDROP POLICY IF EXISTS event_rental_event_vendors_tenant_access ON public.event_rental_event_vendors;\nCREATE POLICY event_rental_event_vendors_tenant_access ON public.event_rental_event_vendors FOR ALL USING(public.is_tenant_member(tenant_id::text) OR public.is_system_admin()) WITH CHECK(public.is_tenant_member(tenant_id::text) OR public.is_system_admin());\nDROP POLICY IF EXISTS event_rental_readiness_tenant_access ON public.event_rental_readiness_items;\nCREATE POLICY event_rental_readiness_tenant_access ON public.event_rental_readiness_items FOR ALL USING(public.is_tenant_member(tenant_id::text) OR public.is_system_admin()) WITH CHECK(public.is_tenant_member(tenant_id::text) OR public.is_system_admin());\n\nDO $$\nBEGIN\n  IF public.normalize_declared_business_type('EVENT_RENTALS') IS DISTINCT FROM 'EVENT_RENTALS' THEN RAISE EXCEPTION 'Event Rentals normalization regression'; END IF;\n  IF public.tenant_operating_model_for('EVENT_RENTALS','{}'::jsonb) IS DISTINCT FROM 'events' THEN RAISE EXCEPTION 'Event Rentals operating model regression'; END IF;\n  IF jsonb_array_length(public.event_rentals_operating_contract()->'modes')<>4 THEN RAISE EXCEPTION 'Event Rentals mode contract must contain exactly four modes'; END IF;\n  IF jsonb_array_length(public.event_rentals_operating_contract()->'serviceLevels')<>4 THEN RAISE EXCEPTION 'Event Rentals service-level contract must contain exactly four offers'; END IF;\n  IF public.normalize_declared_business_type('MOBILE_VENDOR') IS DISTINCT FROM 'MOBILE_VENDOR' THEN RAISE EXCEPTION 'Sibling tenant-type normalization changed'; END IF;\nEND $$;\n\n\n"],"idempotency_key":"ee3d2e4340dade4ba88d42b131e3ec4ad0a230fe:20260912163500"},{"name":"normalize_bookmedirectly_slug","version":"20260914012400","rollback":null,"created_by":"canonical-production-release","statements":["do $$\nbegin\n  if exists (\n    select 1\n    from public.tenants\n    where company_slug = 'bookmedirectly'\n      and id <> 'b6cd1709-089e-423d-b09a-31d3181d0021'::uuid\n  ) then\n    raise exception 'canonical BookMeDirectly slug is already in use';\n  end if;\n\n  update public.tenants\n  set\n    company_slug = 'bookmedirectly',\n    settings = jsonb_set(\n      jsonb_set(\n        jsonb_set(\n          jsonb_set(\n            coalesce(settings, '{}'::jsonb),\n            '{route_namespace}',\n            to_jsonb('bookmedirectly'::text),\n            true\n          ),\n          '{canonical_owner_route}',\n          to_jsonb('/bookmedirectly/dashboard'::text),\n          true\n        ),\n        '{canonical_workspace_route}',\n        to_jsonb('/bookmedirectly/dashboard'::text),\n        true\n      ),\n      '{canonical_public_route}',\n      to_jsonb('https://whatsmyprogress.com/bookmedirectly/'::text),\n      true\n    )\n  where id = 'b6cd1709-089e-423d-b09a-31d3181d0021'::uuid\n    and company_slug in ('bookmedirectly-com', 'bookmedirectly');\nend\n$$;\n"],"idempotency_key":"fc62f12835d48e19583480b80164dc9dc6a4066f:20260914012400"},{"name":"harden_proposal_esign_invariants","version":"20260912180000","rollback":null,"created_by":"canonical-production-release","statements":["-- Harden proposal e-sign activation so a proposal cannot become operational\n-- without explicit e-sign consent and a durable final signed PDF artifact.\n\ncreate or replace function public.enforce_proposal_operating_contract_activation_artifact()\nreturns trigger\nlanguage plpgsql\nsecurity definer\nset search_path to 'public'\nas $function$\ndeclare\n  v_signed public.signed_agreements%rowtype;\n  v_pending public.pending_agreements%rowtype;\nbegin\n  if new.lifecycle_status is distinct from 'activated' then\n    return new;\n  end if;\n\n  if new.signed_agreement_id is null then\n    raise exception 'proposal_operating_contract_signed_agreement_required';\n  end if;\n  if new.pending_agreement_id is null then\n    raise exception 'proposal_operating_contract_pending_agreement_required';\n  end if;\n  if nullif(btrim(coalesce(new.signed_pdf_path, '')), '') is null then\n    raise exception 'proposal_operating_contract_signed_pdf_required';\n  end if;\n\n  select * into v_signed\n  from public.signed_agreements\n  where id = new.signed_agreement_id;\n  if not found then\n    raise exception 'proposal_operating_contract_signed_agreement_not_found';\n  end if;\n  if coalesce(v_signed.esign_consent, false) is not true then\n    raise exception 'proposal_operating_contract_esign_consent_required';\n  end if;\n  if coalesce(v_signed.metadata->>'filled_pdf_path', '') <> new.signed_pdf_path then\n    raise exception 'proposal_operating_contract_signed_pdf_mismatch';\n  end if;\n  if coalesce(v_signed.metadata->>'pending_agreement_id', '') <> new.pending_agreement_id::text then\n    raise exception 'proposal_operating_contract_pending_agreement_mismatch';\n  end if;\n\n  select * into v_pending\n  from public.pending_agreements\n  where id = new.pending_agreement_id;\n  if not found then\n    raise exception 'proposal_operating_contract_pending_agreement_not_found';\n  end if;\n  if v_pending.status <> 'signed'\n     or coalesce(v_pending.signed_pdf_status, '') <> 'ready'\n     or v_pending.signed_pdf_path is distinct from new.signed_pdf_path then\n    raise exception 'proposal_operating_contract_signed_pdf_not_ready';\n  end if;\n  if lower(coalesce(v_pending.completed_fields->>'esign_consent', '')) <> 'true' then\n    raise exception 'proposal_operating_contract_server_consent_required';\n  end if;\n\n  return new;\nend;\n$function$;\n\ndrop trigger if exists tg_enforce_proposal_operating_contract_activation_artifact\n  on public.proposal_operating_contracts;\ncreate trigger tg_enforce_proposal_operating_contract_activation_artifact\nbefore insert or update on public.proposal_operating_contracts\nfor each row execute function public.enforce_proposal_operating_contract_activation_artifact();\n\ncreate or replace function public.activate_proposal_operating_contract_from_signed_agreement()\nreturns trigger\nlanguage plpgsql\nsecurity definer\nset search_path to 'public'\nas $function$\ndeclare\n  v_vars jsonb := coalesce(new.metadata->'source_variables', '{}'::jsonb);\n  v_proposal_id_text text;\n  v_proposal_id uuid;\n  v_contract public.proposal_operating_contracts%rowtype;\n  v_pending_id_text text;\n  v_pending_id uuid;\n  v_filled_pdf_path text;\n  v_pending public.pending_agreements%rowtype;\nbegin\n  if lower(coalesce(v_vars->>'PROPOSAL_OPERATING_CONTRACT', v_vars->>'proposal_operating_contract', '')) not in ('true', '1', 'yes') then\n    return new;\n  end if;\n\n  if coalesce(new.esign_consent, false) is not true then\n    raise exception 'proposal_operating_contract_esign_consent_required';\n  end if;\n\n  v_proposal_id_text := coalesce(v_vars->>'PROPOSAL_ID', v_vars->>'proposal_id', '');\n  if v_proposal_id_text !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' then\n    raise exception 'proposal_operating_contract_proposal_id_invalid';\n  end if;\n  v_proposal_id := v_proposal_id_text::uuid;\n\n  select * into v_contract\n  from public.proposal_operating_contracts\n  where proposal_id = v_proposal_id;\n  if not found then\n    raise exception 'proposal_operating_contract_not_found';\n  end if;\n  if new.tenant_id is distinct from v_contract.issuer_tenant_id then\n    raise exception 'proposal_operating_contract_issuer_mismatch';\n  end if;\n  if coalesce(v_vars->>'PROPOSAL_VERSION', v_vars->>'proposal_version', '') <> v_contract.proposal_version::text then\n    raise exception 'proposal_operating_contract_version_mismatch';\n  end if;\n  if nullif(coalesce(v_vars->>'PROPOSAL_CONTRACT_ID', v_vars->>'proposal_contract_id', ''), '') is not null\n     and coalesce(v_vars->>'PROPOSAL_CONTRACT_ID', v_vars->>'proposal_contract_id', '') <> v_contract.id::text then\n    raise exception 'proposal_operating_contract_contract_id_mismatch';\n  end if;\n\n  v_pending_id_text := coalesce(new.metadata->>'pending_agreement_id', '');\n  if v_pending_id_text !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' then\n    raise exception 'proposal_operating_contract_pending_agreement_id_invalid';\n  end if;\n  v_pending_id := v_pending_id_text::uuid;\n\n  v_filled_pdf_path := nullif(btrim(coalesce(new.metadata->>'filled_pdf_path', '')), '');\n  if v_filled_pdf_path is null then\n    raise exception 'proposal_operating_contract_signed_pdf_required';\n  end if;\n\n  select * into v_pending\n  from public.pending_agreements\n  where id = v_pending_id;\n  if not found then\n    raise exception 'proposal_operating_contract_pending_agreement_not_found';\n  end if;\n  if v_pending.tenant_id is distinct from v_contract.issuer_tenant_id then\n    raise exception 'proposal_operating_contract_pending_agreement_tenant_mismatch';\n  end if;\n  if v_pending.status <> 'signed'\n     or coalesce(v_pending.signed_pdf_status, '') <> 'ready'\n     or v_pending.signed_pdf_path is distinct from v_filled_pdf_path then\n    raise exception 'proposal_operating_contract_signed_pdf_not_ready';\n  end if;\n  if lower(coalesce(v_pending.completed_fields->>'esign_consent', '')) <> 'true' then\n    raise exception 'proposal_operating_contract_server_consent_required';\n  end if;\n\n  perform public.activate_proposal_operating_contract(\n    v_proposal_id,\n    new.id,\n    v_pending_id,\n    v_filled_pdf_path,\n    new.signed_at\n  );\n  return new;\nend;\n$function$;\n"],"idempotency_key":"cebdf4456280220a1b1dcb72727b699785308b7f:20260912180000"},{"name":"event_rentals_modes_shell_contract","version":"20260912184000","rollback":null,"created_by":"canonical-production-release","statements":["-- Promote Event Rentals from a data-only four-card seed to the canonical MODES tenant shell.\n-- This migration is intentionally configuration-only: no fake business records are created.\n\nCREATE OR REPLACE FUNCTION public.prepare_declared_tenant_blueprint()\nRETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$\nDECLARE v_declared text; v_model text; v_blueprint jsonb; v_apps jsonb; v_manifest jsonb; v_custom boolean; v_slug text;\nBEGIN\n  v_declared:=public.normalize_declared_business_type(coalesce(nullif(new.settings->>'declared_business_type',''),nullif(new.settings->>'business_type',''),nullif(new.settings->>'business_category',''),nullif(new.settings#>>'{business_operating_blueprint,businessTypeKey}',''),nullif(new.tenant_type,'')));\n  IF v_declared IS NULL THEN RAISE EXCEPTION 'A valid declared business type is required before tenant creation' USING ERRCODE='22023'; END IF;\n  new.tenant_type:=v_declared;\n  v_slug:=nullif(trim(coalesce(new.company_slug,'')),'');\n  IF v_slug IS NULL THEN\n    v_slug:=trim(both '-' from regexp_replace(lower(new.name),'[^a-z0-9]+','-','g'));\n    IF v_slug='' THEN v_slug:='tenant'; END IF;\n    IF EXISTS(SELECT 1 FROM public.tenants existing WHERE existing.company_slug=v_slug) THEN v_slug:=v_slug||'-'||substr(replace(new.id::text,'-',''),1,6); END IF;\n  END IF;\n  new.company_slug:=v_slug;\n  v_model:=public.tenant_operating_model_for(v_declared,coalesce(new.settings,'{}'::jsonb));\n  IF v_model IS NULL THEN RAISE EXCEPTION 'Operating model is required for declared business type %; tenant creation will not generalize it to MISC',v_declared USING ERRCODE='22023'; END IF;\n  v_blueprint:=public.build_tenant_business_blueprint(v_declared,coalesce(new.settings,'{}'::jsonb),new.name);\n  IF v_declared='EVENT_RENTALS' THEN\n    v_blueprint:=v_blueprint||public.event_rentals_operating_contract()||jsonb_build_object(\n      'contractVersion',2,\n      'businessTypeKey','EVENT_RENTALS','operatingModel','events',\n      'primaryRelationship',jsonb_build_object('singular','Client','plural','Clients','kind','client'),\n      'workObjects',jsonb_build_array(\n        jsonb_build_object('singular','Event','plural','Events','kind','events'),\n        jsonb_build_object('singular','Rental Booking','plural','Rental Bookings','kind','rentals'),\n        jsonb_build_object('singular','Activation','plural','Activations','kind','activations')\n      ),\n      'workflowStages',jsonb_build_array('Inquiry','Quote','Reserved','Prep','Setup','Live','Teardown','Return & Inspection','Complete','Paid'),\n      'vocabulary',jsonb_build_object('workspace','Event Operations','work','Event','workPlural','Events'),\n      'dashboardPriorities',jsonb_build_array('Upcoming bookings and events','Readiness risks and blockers','Inventory conflicts and utilization','Deliveries and pickups','Outstanding deposits and balances','Vendor compliance expirations','Next operational tasks'),\n      'modules',jsonb_build_array(\n        jsonb_build_object('id','today','path','dashboard','label','Operations','emphasis','primary'),\n        jsonb_build_object('id','booking','path','calendar','label','Bookings'),\n        jsonb_build_object('id','calendar','path','calendar','label','Event Calendar'),\n        jsonb_build_object('id','contacts','path','contacts','label','Customers'),\n        jsonb_build_object('id','proposals','path','proposals','label','Quotes & Proposals'),\n        jsonb_build_object('id','payments','path','payments','label','Payments'),\n        jsonb_build_object('id','jobs','path','jobs','label','Crews & Work'),\n        jsonb_build_object('id','communications','path','communications','label','Messages'),\n        jsonb_build_object('id','documents','path','documents','label','Documents'),\n        jsonb_build_object('id','analytics','path','analytics','label','Performance'),\n        jsonb_build_object('id','opportunity-radar','path','opportunity-radar','label','Opportunities'),\n        jsonb_build_object('id','settings','path','settings','label','Settings')\n      )\n    );\n  END IF;\n  v_apps:=coalesce(v_blueprint->'applications',public.tenant_blueprint_applications(v_model));\n  IF v_declared='EVENT_RENTALS' THEN\n    v_apps:=jsonb_build_array('booking','contacts','calendar','proposals','payments','invoices','documents','tasks','jobs','communications','analytics','opportunity-radar','settings');\n  END IF;\n  v_custom:=v_declared NOT IN ('MITIGATION','ROOFING_COMPANY','PUBLIC_ADJUSTER_FIRM','REALTOR','LAWN_CARE','TIRE_SHOP','AUTO_DETAILING','CAFE_QSR','MOBILE_VENDOR','SHORT_TERM_RENTAL','EVENT_RENTALS','MISC');\n  v_manifest:=coalesce(new.settings->'tenant_manifest','{}'::jsonb)||jsonb_build_object('schemaVersion',2,'businessTypeKey',v_declared,'moduleTenantType',CASE WHEN v_custom THEN 'CUSTOM_BLUEPRINT' ELSE v_declared END,'applications',v_apps,'businessOperatingBlueprint',v_blueprint);\n  new.settings:=coalesce(new.settings,'{}'::jsonb)||jsonb_build_object('declared_business_type',v_declared,'operating_model',v_model,'business_operating_blueprint',v_blueprint,'enabled_applications',v_apps,'tenant_manifest',v_manifest,'route_namespace',new.company_slug,'canonical_workspace_route','/'||new.company_slug||'/dashboard','canonical_owner_route','/'||new.company_slug||'/dashboard','canonical_public_route','https://whatsmyprogress.com/'||new.company_slug||'/','bootstrap_state','provisioning')||CASE WHEN v_custom THEN jsonb_build_object('archetype','ADAPTIVE_BUSINESS','experience_family','adaptive-business-operations','workspace_variant',v_model||'_business') ELSE '{}'::jsonb END;\n  IF v_declared='EVENT_RENTALS' THEN\n    new.settings:=new.settings||jsonb_build_object('shell_template','MODES','experience_family','modes','routing_family','EVENT_RENTALS_OPS','workspace_type','event_rentals','workspace_variant','event_operations','tenant_platform_contract','KNOCKLAUNCH-TENANT-v1');\n  END IF;\n  IF v_declared='EVENT_RENTALS' AND new.settings->'modes' IS NULL THEN\n    new.settings:=new.settings||jsonb_build_object('modes',jsonb_build_object('version',3,'cards',jsonb_build_array(\n      jsonb_build_object('key','rentals','title','Rentals','headline','Know what is available and get it booked','subtitle','Inventory, availability, quotes, delivery & returns','color','#4F46E5','route','/'||new.company_slug||'/dashboard?mode=rentals','storage_slug','rentals','roles',jsonb_build_array('Owner','Admin','GodMode'),'primary_applications',jsonb_build_array('booking','proposals','payments','documents','tasks'),'quick_actions',jsonb_build_array(jsonb_build_object('label','New Rental','application','booking','action','create'),jsonb_build_object('label','Build Proposal','application','proposals','action','create'),jsonb_build_object('label','Collect Payment','application','payments','action','collect'))),\n      jsonb_build_object('key','events','title','Events','headline','Get every event ready before showtime','subtitle','Calendar, crews, readiness, setup & teardown','color','#0F766E','route','/'||new.company_slug||'/dashboard?mode=events','storage_slug','events','roles',jsonb_build_array('Owner','Admin','GodMode'),'primary_applications',jsonb_build_array('calendar','booking','jobs','tasks','documents'),'quick_actions',jsonb_build_array(jsonb_build_object('label','New Event','application','calendar','action','create-event'),jsonb_build_object('label','Create Work','application','jobs','action','create'),jsonb_build_object('label','Create Task','application','tasks','action','create'))),\n      jsonb_build_object('key','activations','title','Activations','headline','Turn events into measurable business','subtitle','Promote, capture leads, measure ROI & follow up','color','#B45309','route','/'||new.company_slug||'/dashboard?mode=activations','storage_slug','activations','roles',jsonb_build_array('Owner','Admin','GodMode'),'primary_applications',jsonb_build_array('contacts','calendar','opportunity-radar','communications','analytics'),'quick_actions',jsonb_build_array(jsonb_build_object('label','Add Contact','application','contacts','action','create'),jsonb_build_object('label','Create Event','application','calendar','action','create-event'),jsonb_build_object('label','Scan Opportunities','application','opportunity-radar','action','scan'))),\n      jsonb_build_object('key','vendors','title','Vendors','headline','Keep every vendor confirmed and compliant','subtitle','Assignments, documents, COIs, expirations & history','color','#7C3AED','route','/'||new.company_slug||'/dashboard?mode=vendors','storage_slug','vendors','roles',jsonb_build_array('Owner','Admin','GodMode'),'primary_applications',jsonb_build_array('contacts','documents','tasks','communications'),'quick_actions',jsonb_build_array(jsonb_build_object('label','Add Contact','application','contacts','action','create'),jsonb_build_object('label','Create Task','application','tasks','action','create')))\n    )));\n  END IF;\n  IF v_model='appointments' THEN new.settings:=new.settings||jsonb_build_object('enabled_automations',coalesce(new.settings->'enabled_automations','[\"booking-confirmation\",\"appointment-reminder\",\"queue-up-next\",\"queue-your-turn\",\"rebook-followup\"]'::jsonb)); END IF;\n  new.status:='inactive'; new.is_active:=false; RETURN new;\nEND;\n$$;\n\nWITH event_rentals AS (\n  SELECT t.id,t.company_slug,\n    coalesce(t.settings->'business_operating_blueprint','{}'::jsonb)||public.event_rentals_operating_contract()||jsonb_build_object(\n      'contractVersion',2,'businessTypeKey','EVENT_RENTALS','operatingModel','events',\n      'dashboardPriorities',jsonb_build_array('Upcoming bookings and events','Readiness risks and blockers','Inventory conflicts and utilization','Deliveries and pickups','Outstanding deposits and balances','Vendor compliance expirations','Next operational tasks'),\n      'modules',jsonb_build_array(\n        jsonb_build_object('id','today','path','dashboard','label','Operations','emphasis','primary'),\n        jsonb_build_object('id','booking','path','calendar','label','Bookings'),\n        jsonb_build_object('id','calendar','path','calendar','label','Event Calendar'),\n        jsonb_build_object('id','contacts','path','contacts','label','Customers'),\n        jsonb_build_object('id','proposals','path','proposals','label','Quotes & Proposals'),\n        jsonb_build_object('id','payments','path','payments','label','Payments'),\n        jsonb_build_object('id','jobs','path','jobs','label','Crews & Work'),\n        jsonb_build_object('id','communications','path','communications','label','Messages'),\n        jsonb_build_object('id','documents','path','documents','label','Documents'),\n        jsonb_build_object('id','analytics','path','analytics','label','Performance'),\n        jsonb_build_object('id','opportunity-radar','path','opportunity-radar','label','Opportunities'),\n        jsonb_build_object('id','settings','path','settings','label','Settings')\n      )\n    ) AS blueprint\n  FROM public.tenants t WHERE t.tenant_type='EVENT_RENTALS'\n), normalized AS (\n  SELECT id,company_slug,blueprint,jsonb_build_array('booking','contacts','calendar','proposals','payments','invoices','documents','tasks','jobs','communications','analytics','opportunity-radar','settings') AS apps FROM event_rentals\n)\nUPDATE public.tenants t\nSET settings=coalesce(t.settings,'{}'::jsonb)||jsonb_build_object(\n  'shell_template','MODES','experience_family','modes','routing_family','EVENT_RENTALS_OPS','workspace_type','event_rentals','workspace_variant','event_operations','tenant_platform_contract','KNOCKLAUNCH-TENANT-v1',\n  'enabled_applications',n.apps,\n  'business_operating_blueprint',n.blueprint,\n  'tenant_manifest',coalesce(t.settings->'tenant_manifest','{}'::jsonb)||jsonb_build_object('schemaVersion',2,'businessTypeKey','EVENT_RENTALS','moduleTenantType','EVENT_RENTALS','applications',n.apps,'businessOperatingBlueprint',n.blueprint),\n  'modes',jsonb_build_object('version',3,'cards',jsonb_build_array(\n    jsonb_build_object('key','rentals','title','Rentals','headline','Know what is available and get it booked','subtitle','Inventory, availability, quotes, delivery & returns','color','#4F46E5','route','/'||n.company_slug||'/dashboard?mode=rentals','storage_slug','rentals','roles',jsonb_build_array('Owner','Admin','GodMode'),'primary_applications',jsonb_build_array('booking','proposals','payments','documents','tasks'),'quick_actions',jsonb_build_array(jsonb_build_object('label','New Rental','application','booking','action','create'),jsonb_build_object('label','Build Proposal','application','proposals','action','create'),jsonb_build_object('label','Collect Payment','application','payments','action','collect'))),\n    jsonb_build_object('key','events','title','Events','headline','Get every event ready before showtime','subtitle','Calendar, crews, readiness, setup & teardown','color','#0F766E','route','/'||n.company_slug||'/dashboard?mode=events','storage_slug','events','roles',jsonb_build_array('Owner','Admin','GodMode'),'primary_applications',jsonb_build_array('calendar','booking','jobs','tasks','documents'),'quick_actions',jsonb_build_array(jsonb_build_object('label','New Event','application','calendar','action','create-event'),jsonb_build_object('label','Create Work','application','jobs','action','create'),jsonb_build_object('label','Create Task','application','tasks','action','create'))),\n    jsonb_build_object('key','activations','title','Activations','headline','Turn events into measurable business','subtitle','Promote, capture leads, measure ROI & follow up','color','#B45309','route','/'||n.company_slug||'/dashboard?mode=activations','storage_slug','activations','roles',jsonb_build_array('Owner','Admin','GodMode'),'primary_applications',jsonb_build_array('contacts','calendar','opportunity-radar','communications','analytics'),'quick_actions',jsonb_build_array(jsonb_build_object('label','Add Contact','application','contacts','action','create'),jsonb_build_object('label','Create Event','application','calendar','action','create-event'),jsonb_build_object('label','Scan Opportunities','application','opportunity-radar','action','scan'))),\n    jsonb_build_object('key','vendors','title','Vendors','headline','Keep every vendor confirmed and compliant','subtitle','Assignments, documents, COIs, expirations & history','color','#7C3AED','route','/'||n.company_slug||'/dashboard?mode=vendors','storage_slug','vendors','roles',jsonb_build_array('Owner','Admin','GodMode'),'primary_applications',jsonb_build_array('contacts','documents','tasks','communications'),'quick_actions',jsonb_build_array(jsonb_build_object('label','Add Contact','application','contacts','action','create'),jsonb_build_object('label','Create Task','application','tasks','action','create')))\n  ))\n),updated_at=now()\nFROM normalized n WHERE t.id=n.id;\n\nDO $$\nDECLARE v_settings jsonb;\nBEGIN\n  SELECT settings INTO v_settings FROM public.tenants WHERE company_slug='salty-panda-events';\n  IF v_settings IS NULL THEN RAISE EXCEPTION 'Salty Panda Events tenant missing'; END IF;\n  IF v_settings->>'shell_template' IS DISTINCT FROM 'MODES' THEN RAISE EXCEPTION 'Event Rentals MODES shell backfill failed'; END IF;\n  IF v_settings->>'routing_family' IS DISTINCT FROM 'EVENT_RENTALS_OPS' THEN RAISE EXCEPTION 'Event Rentals routing family backfill failed'; END IF;\n  IF coalesce((v_settings#>>'{modes,version}')::integer,0)<>3 THEN RAISE EXCEPTION 'Event Rentals modes contract must be version 3'; END IF;\n  IF jsonb_array_length(v_settings#>'{modes,cards}')<>4 THEN RAISE EXCEPTION 'Event Rentals must expose exactly four Modes'; END IF;\n  IF NOT (v_settings->'enabled_applications') @> '[\"proposals\",\"payments\",\"jobs\"]'::jsonb THEN RAISE EXCEPTION 'Event Rentals primary applications missing operational dependencies'; END IF;\nEND $$;\n"],"idempotency_key":"08eb39685a6b0854007b65ba0c92f95ba22088f8:20260912184000"},{"name":"add_esign_health_canary","version":"20260912184500","rollback":null,"created_by":"canonical-production-release","statements":["-- Additive health ledger for the KNOCKLAUNCH Sign synthetic production canary.\n-- Customer agreements are not migrated or rewritten by this migration.\n\ncreate table if not exists public.esign_health_checks (\n  id uuid primary key default gen_random_uuid(),\n  check_type text not null default 'synthetic_canary',\n  status text not null,\n  started_at timestamptz not null default now(),\n  completed_at timestamptz,\n  runtime_identity text,\n  evidence jsonb not null default '{}'::jsonb,\n  error_code text,\n  created_at timestamptz not null default now(),\n  constraint esign_health_checks_check_type_check check (check_type in ('synthetic_canary')),\n  constraint esign_health_checks_status_check check (status in ('running', 'passed', 'failed'))\n);\n\ncomment on table public.esign_health_checks is\n  'Service-role-only bounded health ledger for KNOCKLAUNCH Sign synthetic canary proof. Contains no customer agreement payloads.';\n\ncreate index if not exists esign_health_checks_created_at_idx\n  on public.esign_health_checks (created_at desc);\n\nalter table public.esign_health_checks enable row level security;\n\nrevoke all on table public.esign_health_checks from anon, authenticated;\ngrant select, insert, update, delete on table public.esign_health_checks to service_role;\n"],"idempotency_key":"a604e829f398b5b5770b1c845a63bc02a1587d0a:20260912184500"},{"name":"allow_agreement_activity_events","version":"20260912190000","rollback":null,"created_by":"canonical-production-release","statements":["-- Allow the canonical agreement lifecycle namespace emitted by\n-- project_pending_agreement_business_activity without weakening the existing\n-- activity type contract.\n\nalter table public.activity_timeline\n  drop constraint if exists activity_timeline_activity_type_check;\n\nalter table public.activity_timeline\n  add constraint activity_timeline_activity_type_check\n  check (\n    activity_type = any (array[\n      'call'::text,\n      'email'::text,\n      'sms'::text,\n      'note'::text,\n      'task'::text,\n      'meeting'::text,\n      'status_change'::text,\n      'stage_change'::text,\n      'assignment'::text,\n      'document'::text,\n      'photo'::text,\n      'automation'::text,\n      'score_change'::text,\n      'merge'::text,\n      'import'::text,\n      'export'::text,\n      'field_update'::text,\n      'contact_created'::text,\n      'contact_updated'::text,\n      'job_created'::text,\n      'job_updated'::text,\n      'invoice_created'::text,\n      'invoice_paid'::text,\n      'invoice_updated'::text,\n      'settlement_invoice_created'::text,\n      'settlement_invoice_paid'::text,\n      'settlement_invoice_updated'::text,\n      'business_joined_platform'::text,\n      'business_lifecycle_updated'::text\n    ])\n    or activity_type ~ '^(opportunity|communication|document|user|proposal|agreement)_[A-Za-z0-9_.:-]+$'::text\n  );\n"],"idempotency_key":"5aa8c48cf18a5a10de12410e5c01ca750e7cfba0:20260912190000"},{"name":"allow_signed_agreement_activity_events","version":"20260912193000","rollback":null,"created_by":"canonical-production-release","statements":["-- Allow the signed-agreement business activity projector to write its canonical\n-- activity namespace without weakening the existing activity type allowlist.\n\nalter table public.activity_timeline\n  drop constraint if exists activity_timeline_activity_type_check;\n\nalter table public.activity_timeline\n  add constraint activity_timeline_activity_type_check\n  check (\n    activity_type = any (array[\n      'call'::text,\n      'email'::text,\n      'sms'::text,\n      'note'::text,\n      'task'::text,\n      'meeting'::text,\n      'status_change'::text,\n      'stage_change'::text,\n      'assignment'::text,\n      'document'::text,\n      'photo'::text,\n      'automation'::text,\n      'score_change'::text,\n      'merge'::text,\n      'import'::text,\n      'export'::text,\n      'field_update'::text,\n      'contact_created'::text,\n      'contact_updated'::text,\n      'job_created'::text,\n      'job_updated'::text,\n      'invoice_created'::text,\n      'invoice_paid'::text,\n      'invoice_updated'::text,\n      'settlement_invoice_created'::text,\n      'settlement_invoice_paid'::text,\n      'settlement_invoice_updated'::text,\n      'business_joined_platform'::text,\n      'business_lifecycle_updated'::text\n    ])\n    or activity_type ~ '^(opportunity|communication|document|user|proposal|agreement|signed_agreement)_[A-Za-z0-9_.:-]+$'::text\n  );\n"],"idempotency_key":"2785976c9b55b70c9ba1a728ffc3e68a0ab1c6bd:20260912193000"},{"name":"platform_mode_default_media","version":"20260912194500","rollback":null,"created_by":"canonical-production-release","statements":["-- Shared default imagery for MODES tenants. Default bytes are seeded once as\n-- platform operational media after this schema/runtime contract is live;\n-- tenant-specific replacements continue to use canonical private tenant media.\n\nCREATE TABLE IF NOT EXISTS public.platform_mode_default_media (\n  mode_key text PRIMARY KEY,\n  title text NOT NULL,\n  mime_type text NOT NULL DEFAULT 'image/webp',\n  content_base64 text NOT NULL,\n  width integer NOT NULL CHECK (width > 0),\n  height integer NOT NULL CHECK (height > 0),\n  pack_version integer NOT NULL DEFAULT 1 CHECK (pack_version > 0),\n  sha256 text NOT NULL CHECK (length(sha256) = 64),\n  active boolean NOT NULL DEFAULT true,\n  created_at timestamptz NOT NULL DEFAULT now(),\n  updated_at timestamptz NOT NULL DEFAULT now(),\n  CONSTRAINT platform_mode_default_media_key_check CHECK (mode_key IN ('operations','planning','growth','financials','back-office','field','rentals','events','activations','vendors'))\n);\nALTER TABLE public.platform_mode_default_media ENABLE ROW LEVEL SECURITY;\nREVOKE ALL ON public.platform_mode_default_media FROM anon, authenticated;\nGRANT SELECT ON public.platform_mode_default_media TO service_role;\nCOMMENT ON TABLE public.platform_mode_default_media IS 'MODE_DEFAULT_MEDIA_V1 shared platform image pack. Tenant custom images remain in tenant-private media/modes storage.';\n\nCREATE OR REPLACE FUNCTION public.kl_mode_default_key(p_key text, p_title text DEFAULT NULL)\nRETURNS text LANGUAGE plpgsql IMMUTABLE SET search_path TO 'public' AS $$\nDECLARE\n  v_key text := trim(both '-' from regexp_replace(lower(coalesce(p_key,'')), '[^a-z0-9]+', '-', 'g'));\n  v_title text := trim(both '-' from regexp_replace(lower(coalesce(p_title,'')), '[^a-z0-9]+', '-', 'g'));\n  v text;\nBEGIN\n  FOREACH v IN ARRAY ARRAY[v_key, v_title] LOOP\n    IF v IN ('rentals','rental') THEN RETURN 'rentals'; END IF;\n    IF v IN ('events','event','booked-event') THEN RETURN 'events'; END IF;\n    IF v IN ('activations','activation') THEN RETURN 'activations'; END IF;\n    IF v IN ('vendors','vendor') THEN RETURN 'vendors'; END IF;\n    IF v IN ('financials','financial','finance','money','performance','reports','analytics') THEN RETURN 'financials'; END IF;\n    IF v IN ('planning','plan','schedule','calendar','prep','prepare') THEN RETURN 'planning'; END IF;\n    IF v IN ('growth','opportunity','opportunities','pipeline','sales','customers','customer','follow-up','followup') THEN RETURN 'growth'; END IF;\n    IF v IN ('back-office','backoffice','office','admin','administration','documents','document') THEN RETURN 'back-office'; END IF;\n    IF v IN ('field','popup','pop-up','on-site','onsite') THEN RETURN 'field'; END IF;\n    IF v IN ('operations','operation','command','today','work','work-queue','jobs','job','team','overview','focus') THEN RETURN 'operations'; END IF;\n  END LOOP;\n  RETURN 'operations';\nEND;\n$$;\n\nCREATE OR REPLACE FUNCTION public.kl_mode_default_url(p_default_key text)\nRETURNS text LANGUAGE sql IMMUTABLE SET search_path TO 'public' AS $$\nSELECT 'https://ymntrajfubicyxwwdxxa.supabase.co/functions/v1/tenant-media-library?mode=default-mode&key=' || public.kl_mode_default_key(p_default_key, NULL) || '&v=1';\n$$;\n\nCREATE OR REPLACE FUNCTION public.kl_apply_mode_default_media(p_settings jsonb)\nRETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$\nDECLARE\n  v_settings jsonb := coalesce(p_settings, '{}'::jsonb);\n  v_modes jsonb; v_cards jsonb; v_next_cards jsonb := '[]'::jsonb; v_card jsonb;\n  v_default_key text; v_default_url text; v_image_url text; v_fallback_url text;\n  v_has_default boolean; v_custom boolean; v_any_default boolean := false;\nBEGIN\n  IF upper(coalesce(v_settings->>'shell_template','')) <> 'MODES' THEN RETURN v_settings; END IF;\n  v_modes := coalesce(v_settings->'modes', '{}'::jsonb);\n  v_cards := coalesce(v_modes->'cards', '[]'::jsonb);\n  IF jsonb_typeof(v_cards) <> 'array' THEN RETURN v_settings; END IF;\n  FOR v_card IN SELECT value FROM jsonb_array_elements(v_cards) LOOP\n    v_default_key := public.kl_mode_default_key(v_card->>'key', v_card->>'title');\n    v_default_url := public.kl_mode_default_url(v_default_key);\n    SELECT EXISTS (SELECT 1 FROM public.platform_mode_default_media d WHERE d.mode_key=v_default_key AND d.active=true) INTO v_has_default;\n    v_image_url := btrim(coalesce(v_card->>'image_url',''));\n    v_fallback_url := btrim(coalesce(v_card->>'fallback_image_url',''));\n    v_custom := btrim(coalesce(v_card->>'image_asset_id','')) <> '' OR lower(coalesce(v_card->>'image_source',''))='custom' OR (v_image_url <> '' AND position('mode=default-mode' in v_image_url)=0) OR v_fallback_url <> '';\n    IF v_has_default THEN\n      v_any_default := true;\n      IF v_custom THEN\n        v_card := v_card || jsonb_build_object('default_image_key',v_default_key,'default_image_url',v_default_url,'image_source','custom');\n      ELSE\n        v_card := v_card || jsonb_build_object('default_image_key',v_default_key,'default_image_url',v_default_url,'image_url',v_default_url,'image_source','default');\n      END IF;\n    END IF;\n    v_next_cards := v_next_cards || jsonb_build_array(v_card);\n  END LOOP;\n  IF NOT v_any_default THEN RETURN v_settings; END IF;\n  v_modes := v_modes || jsonb_build_object('cards',v_next_cards,'default_media_contract','MODE_DEFAULT_MEDIA_V1','default_media_pack_version',1,'default_media_customization',jsonb_build_object('upload_action','create_mode_card_upload','finalize_action','finalize_upload','promote_action','promote_mode_card_asset','position_action','update_mode_card_position','tenant_overrides_are_private',true));\n  RETURN jsonb_set(v_settings,'{modes}',v_modes,true);\nEND;\n$$;\nREVOKE ALL ON FUNCTION public.kl_apply_mode_default_media(jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION public.kl_apply_mode_default_media(jsonb) TO service_role;\n\nCREATE OR REPLACE FUNCTION public.apply_mode_default_media_to_tenant()\nRETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public' AS $$\nBEGIN NEW.settings := public.kl_apply_mode_default_media(NEW.settings); RETURN NEW; END;\n$$;\nREVOKE ALL ON FUNCTION public.apply_mode_default_media_to_tenant() FROM PUBLIC, anon, authenticated;\nDROP TRIGGER IF EXISTS zz_modes_default_media_contract ON public.tenants;\nCREATE TRIGGER zz_modes_default_media_contract BEFORE INSERT OR UPDATE OF settings ON public.tenants FOR EACH ROW EXECUTE FUNCTION public.apply_mode_default_media_to_tenant();\n\nDO $$\nBEGIN\n  IF public.kl_mode_default_key('command','Command') <> 'operations' THEN RAISE EXCEPTION 'Command alias regression'; END IF;\n  IF public.kl_mode_default_key('plan','Plan') <> 'planning' THEN RAISE EXCEPTION 'Plan alias regression'; END IF;\n  IF public.kl_mode_default_key('popup','Pop-Up') <> 'field' THEN RAISE EXCEPTION 'Pop-Up alias regression'; END IF;\n  IF public.kl_mode_default_key('booked-event','Booked Event') <> 'events' THEN RAISE EXCEPTION 'Booked Event alias regression'; END IF;\n  IF public.kl_mode_default_key('office','Back Office') <> 'back-office' THEN RAISE EXCEPTION 'Back Office alias regression'; END IF;\n  IF public.kl_mode_default_key('performance','Performance') <> 'financials' THEN RAISE EXCEPTION 'Performance alias regression'; END IF;\n  IF public.kl_mode_default_key('documents','Documents') <> 'back-office' THEN RAISE EXCEPTION 'Documents alias regression'; END IF;\nEND $$;\n"],"idempotency_key":"5ebcab0458a5ad1fa5769597ddaf4c8e9a7eefba:20260912194500"},{"name":"create_app_background_jobs","version":"20260914065000","rollback":null,"created_by":"canonical-production-release","statements":["-- Restore the shared background-job contract consumed by NotificationBell.\ncreate table if not exists public.app_background_jobs (\n  id uuid primary key default gen_random_uuid(),\n  tenant_id uuid not null references public.tenants(id) on delete cascade,\n  user_id uuid not null references public.profiles(id) on delete cascade,\n  source_app text not null,\n  job_type text not null,\n  title text not null,\n  detail text,\n  status text not null default 'queued' check (status in ('queued', 'running', 'waiting', 'needs_attention', 'completed', 'failed', 'canceled')),\n  progress_percentage integer not null default 0 check (progress_percentage between 0 and 100),\n  progress_label text,\n  destination_path text,\n  entity_type text,\n  entity_id text,\n  metadata jsonb not null default '{}'::jsonb,\n  dedupe_key text not null,\n  error_message text,\n  started_at timestamptz not null default now(),\n  finished_at timestamptz,\n  created_at timestamptz not null default now(),\n  updated_at timestamptz not null default now(),\n  constraint app_background_jobs_tenant_user_source_dedupe_key unique (tenant_id, user_id, source_app, dedupe_key)\n);\n\ncreate index if not exists idx_app_background_jobs_active\n  on public.app_background_jobs (tenant_id, user_id, status, created_at desc);\n\nalter table public.app_background_jobs enable row level security;\n\ndrop policy if exists app_background_jobs_select_self on public.app_background_jobs;\ncreate policy app_background_jobs_select_self\n  on public.app_background_jobs\n  for select\n  to authenticated\n  using (\n    user_id = auth.uid()\n    and exists (\n      select 1\n      from public.user_tenant_memberships m\n      where m.user_id = auth.uid()\n        and m.tenant_id = app_background_jobs.tenant_id\n        and coalesce(m.status, 'active') = 'active'\n    )\n  );\n\ndrop policy if exists app_background_jobs_insert_self on public.app_background_jobs;\ncreate policy app_background_jobs_insert_self\n  on public.app_background_jobs\n  for insert\n  to authenticated\n  with check (\n    user_id = auth.uid()\n    and exists (\n      select 1\n      from public.user_tenant_memberships m\n      where m.user_id = auth.uid()\n        and m.tenant_id = app_background_jobs.tenant_id\n        and coalesce(m.status, 'active') = 'active'\n    )\n  );\n\ndrop policy if exists app_background_jobs_update_self on public.app_background_jobs;\ncreate policy app_background_jobs_update_self\n  on public.app_background_jobs\n  for update\n  to authenticated\n  using (\n    user_id = auth.uid()\n    and exists (\n      select 1\n      from public.user_tenant_memberships m\n      where m.user_id = auth.uid()\n        and m.tenant_id = app_background_jobs.tenant_id\n        and coalesce(m.status, 'active') = 'active'\n    )\n  )\n  with check (\n    user_id = auth.uid()\n    and exists (\n      select 1\n      from public.user_tenant_memberships m\n      where m.user_id = auth.uid()\n        and m.tenant_id = app_background_jobs.tenant_id\n        and coalesce(m.status, 'active') = 'active'\n    )\n  );\n\ngrant select, insert, update on public.app_background_jobs to authenticated;\ngrant all on public.app_background_jobs to service_role;\n\ndo $$\nbegin\n  if not exists (\n    select 1\n    from pg_publication_tables\n    where pubname = 'supabase_realtime'\n      and schemaname = 'public'\n      and tablename = 'app_background_jobs'\n  ) then\n    alter publication supabase_realtime add table public.app_background_jobs;\n  end if;\nend\n$$;\n"],"idempotency_key":"b52b1aa71a323327aeea6dc3ab248fcf3746a209:20260914065000"},{"name":"modes_tenant_capability_contract","version":"20260912220000","rollback":null,"created_by":"canonical-production-release","statements":["create or replace function public.apply_modes_tenant_capability_contract(\n  p_settings jsonb,\n  p_tenant_type text\n)\nreturns jsonb\nlanguage plpgsql\nstable\nset search_path = public\nas $$\ndeclare\n  v_settings jsonb := coalesce(p_settings, '{}'::jsonb);\n  v_modes jsonb := coalesce(p_settings -> 'modes', '{}'::jsonb);\n  v_family text := lower(coalesce(p_settings ->> 'experience_family', ''));\n  v_shell text := upper(coalesce(p_settings ->> 'shell_template', ''));\n  v_type text := upper(coalesce(p_tenant_type, ''));\n  v_default_count integer := 0;\n  v_ready boolean := false;\n  v_namespaces jsonb := jsonb_build_array(\n    'media/modes/operations',\n    'media/modes/planning',\n    'media/modes/growth',\n    'media/modes/financials',\n    'media/modes/back-office',\n    'media/modes/field',\n    'media/modes/rentals',\n    'media/modes/events',\n    'media/modes/activations',\n    'media/modes/vendors'\n  );\nbegin\n  if v_family = 'standard' then\n    return v_settings;\n  end if;\n  if v_family <> 'modes' and not (v_family = '' and (v_shell = 'MODES' or v_type = 'PORTFOLIO_CONTROL_CENTER')) then\n    return v_settings;\n  end if;\n\n  if to_regclass('public.platform_mode_default_media') is not null then\n    execute $q$\n      select count(distinct mode_key)\n      from public.platform_mode_default_media\n      where active = true\n        and mode_key = any(array['operations','planning','growth','financials','back-office','field','rentals','events','activations','vendors'])\n    $q$ into v_default_count;\n  end if;\n  v_ready := v_default_count = 10;\n\n  if jsonb_typeof(v_modes -> 'storage_namespaces') = 'array' then\n    select coalesce(jsonb_agg(value order by value), '[]'::jsonb)\n      into v_namespaces\n      from (\n        select distinct value\n        from jsonb_array_elements(\n          (v_modes -> 'storage_namespaces') || v_namespaces\n        )\n      ) namespaces;\n  end if;\n\n  v_modes := v_modes || jsonb_build_object(\n    'version', 3,\n    'capability_contract', 'MODES_TENANT_V1',\n    'default_media_contract', 'MODE_DEFAULT_MEDIA_V1',\n    'activation_state', case when v_ready then 'ready' else 'prerequisite_required' end,\n    'storage_namespaces', v_namespaces,\n    'reconciled_at', now()\n  );\n\n  return v_settings || jsonb_build_object(\n    'experience_family', 'modes',\n    'shell_template', 'MODES',\n    'modes', v_modes\n  );\nend;\n$$;\n\ncreate or replace function public.enforce_modes_tenant_capability_contract()\nreturns trigger\nlanguage plpgsql\nset search_path = public\nas $$\nbegin\n  new.settings := public.apply_modes_tenant_capability_contract(new.settings, new.tenant_type);\n  return new;\nend;\n$$;\n\ndrop trigger if exists tenants_enforce_modes_capability on public.tenants;\ncreate trigger tenants_enforce_modes_capability\nbefore insert or update of settings, tenant_type\non public.tenants\nfor each row\nexecute function public.enforce_modes_tenant_capability_contract();\n\ncreate or replace function public.reconcile_modes_tenant_capability(p_tenant_id uuid default null)\nreturns jsonb\nlanguage plpgsql\nsecurity definer\nset search_path = public\nas $$\ndeclare\n  v_count integer := 0;\n  v_ready integer := 0;\n  v_prerequisite integer := 0;\nbegin\n  update public.tenants as tenant\n  set settings = public.apply_modes_tenant_capability_contract(tenant.settings, tenant.tenant_type),\n      updated_at = now()\n  where (p_tenant_id is null or tenant.id = p_tenant_id)\n    and (\n      lower(coalesce(tenant.settings ->> 'experience_family', '')) = 'modes'\n      or (\n        coalesce(tenant.settings ->> 'experience_family', '') = ''\n        and (\n          upper(coalesce(tenant.settings ->> 'shell_template', '')) = 'MODES'\n          or upper(coalesce(tenant.tenant_type, '')) = 'PORTFOLIO_CONTROL_CENTER'\n        )\n      )\n    );\n  get diagnostics v_count = row_count;\n\n  select count(*) filter (where tenant.settings #>> '{modes,activation_state}' = 'ready'),\n         count(*) filter (where tenant.settings #>> '{modes,activation_state}' = 'prerequisite_required')\n    into v_ready, v_prerequisite\n    from public.tenants as tenant\n    where (p_tenant_id is null or tenant.id = p_tenant_id)\n      and lower(coalesce(tenant.settings ->> 'experience_family', '')) = 'modes';\n\n  return jsonb_build_object(\n    'reconciled', v_count,\n    'ready', v_ready,\n    'prerequisite_required', v_prerequisite,\n    'contract', 'MODES_TENANT_V1',\n    'default_media_contract', 'MODE_DEFAULT_MEDIA_V1'\n  );\nend;\n$$;\n\nrevoke all on function public.reconcile_modes_tenant_capability(uuid) from public;\nrevoke all on function public.reconcile_modes_tenant_capability(uuid) from anon;\nrevoke all on function public.reconcile_modes_tenant_capability(uuid) from authenticated;\ngrant execute on function public.reconcile_modes_tenant_capability(uuid) to service_role;\n\nselect public.reconcile_modes_tenant_capability(null);\n"],"idempotency_key":"a5d545575f980a25fbe9e4ba2f6dfda80963d8f8:20260912220000"},{"name":"modes_card_driven_namespaces","version":"20260912224718","rollback":null,"created_by":"nysomenterprises@gmail.com","statements":["create or replace function public.apply_modes_tenant_capability_contract(p_settings jsonb, p_tenant_type text)\nreturns jsonb\nlanguage plpgsql\nstable\nset search_path to 'public'\nas $function$\ndeclare\n  v_settings jsonb := coalesce(p_settings, '{}'::jsonb);\n  v_modes jsonb := coalesce(p_settings -> 'modes', '{}'::jsonb);\n  v_family text := lower(coalesce(p_settings ->> 'experience_family', ''));\n  v_shell text := upper(coalesce(p_settings ->> 'shell_template', ''));\n  v_type text := upper(replace(coalesce(p_tenant_type, ''), '-', '_'));\n  v_default_count integer := 0;\n  v_card_count integer := 0;\n  v_ready boolean := false;\n  v_namespaces jsonb := '[]'::jsonb;\nbegin\n  if v_family = 'standard' then return v_settings; end if;\n  if v_family <> 'modes' and not (v_family = '' and (v_shell = 'MODES' or v_type = 'PORTFOLIO_CONTROL_CENTER')) then return v_settings; end if;\n\n  if to_regclass('public.platform_mode_default_media') is not null then\n    select count(distinct mode_key) into v_default_count\n      from public.platform_mode_default_media\n     where active = true\n       and mode_key = any(array['operations','planning','growth','financials','back-office','field','rentals','events','activations','vendors']);\n  end if;\n\n  if jsonb_typeof(v_modes -> 'cards') = 'array' then\n    select count(*), coalesce(jsonb_agg('media/modes/' || mode_slug order by mode_slug), '[]'::jsonb)\n      into v_card_count, v_namespaces\n      from (\n        select distinct trim(both '-' from regexp_replace(\n          lower(coalesce(nullif(card ->> 'storage_slug',''), nullif(card ->> 'key',''), nullif(card ->> 'title',''))),\n          '[^a-z0-9]+','-','g'\n        )) as mode_slug\n        from jsonb_array_elements(v_modes -> 'cards') as card\n      ) x\n     where mode_slug <> '';\n  end if;\n\n  v_ready := v_default_count = 10 and v_card_count > 0;\n  v_modes := v_modes || jsonb_build_object(\n    'version',4,\n    'capability_contract','MODES_TENANT_V1',\n    'default_media_contract','MODE_DEFAULT_MEDIA_V1',\n    'activation_state',case when v_ready then 'ready' else 'prerequisite_required' end,\n    'storage_namespaces',v_namespaces,\n    'reconciled_at',now()\n  );\n\n  return v_settings || jsonb_build_object('experience_family','modes','shell_template','MODES','modes',v_modes);\nend;\n$function$;\n\nupdate public.tenants as tenant\n   set settings=public.apply_modes_tenant_capability_contract(tenant.settings,tenant.tenant_type),\n       updated_at=now()\n where lower(coalesce(tenant.settings->>'experience_family',''))='modes'\n    or (coalesce(tenant.settings->>'experience_family','')='' and (\n         upper(coalesce(tenant.settings->>'shell_template',''))='MODES'\n         or upper(replace(coalesce(tenant.tenant_type,''),'-','_'))='PORTFOLIO_CONTROL_CENTER'\n       ));\n"],"idempotency_key":null},{"name":"automatic_modes_storage_reconciliation","version":"20260913022000","rollback":null,"created_by":"canonical-production-release","statements":["-- Automatic tenant-scoped MODES storage reconciliation.\n-- Uses the same Vault + digest authorization + pg_net dispatch pattern as system backup.\n\ndo $$\nbegin\n  if not exists (select 1 from vault.secrets where name = 'knocklaunch_modes_storage_reconcile_trigger') then\n    perform vault.create_secret(\n      encode(extensions.gen_random_bytes(32), 'hex'),\n      'knocklaunch_modes_storage_reconcile_trigger',\n      'Machine-only token for automatic MODES tenant storage reconciliation.'\n    );\n  end if;\nend\n$$;\n\ncreate or replace function public.modes_storage_reconcile_authorize(p_token text)\nreturns boolean\nlanguage sql\nsecurity definer\nset search_path = public, vault, extensions\nas $$\n  select coalesce((\n    select extensions.digest(ds.decrypted_secret, 'sha256') = extensions.digest(coalesce(p_token,''), 'sha256')\n      from vault.decrypted_secrets ds\n     where ds.name = 'knocklaunch_modes_storage_reconcile_trigger'\n     limit 1\n  ), false);\n$$;\nrevoke all on function public.modes_storage_reconcile_authorize(text) from public, anon, authenticated;\ngrant execute on function public.modes_storage_reconcile_authorize(text) to service_role;\n\ncreate or replace function private.modes_storage_edge_request(p_tenant_id uuid default null)\nreturns bigint\nlanguage plpgsql\nsecurity definer\nset search_path = private, vault, net\nas $$\ndeclare\n  v_token text;\n  v_request_id bigint;\nbegin\n  select decrypted_secret into v_token\n    from vault.decrypted_secrets\n   where name = 'knocklaunch_modes_storage_reconcile_trigger'\n   limit 1;\n  if v_token is null then\n    raise exception 'modes_storage_reconcile_secret_missing';\n  end if;\n  select net.http_post(\n    url := 'https://ymntrajfubicyxwwdxxa.supabase.co/functions/v1/tenant-media-library',\n    body := jsonb_strip_nulls(jsonb_build_object('action','reconcile_modes_internal','tenantId',p_tenant_id)),\n    headers := jsonb_build_object('Content-Type','application/json','x-modes-storage-token',v_token),\n    timeout_milliseconds := 30000\n  ) into v_request_id;\n  return v_request_id;\nend;\n$$;\nrevoke all on function private.modes_storage_edge_request(uuid) from public, anon, authenticated;\n\ncreate or replace function private.enqueue_modes_storage_reconcile_trigger()\nreturns trigger\nlanguage plpgsql\nsecurity definer\nset search_path = private, public\nas $$\ndeclare\n  v_new_family text;\n  v_new_shell text;\n  v_new_type text;\n  v_new_modes boolean;\n  v_old_family text;\n  v_old_shell text;\n  v_old_type text;\n  v_old_modes boolean;\nbegin\n  v_new_family := lower(coalesce(new.settings->>'experience_family',''));\n  v_new_shell := upper(coalesce(new.settings->>'shell_template',''));\n  v_new_type := upper(replace(coalesce(new.tenant_type,''),'-','_'));\n  v_new_modes := v_new_family = 'modes'\n    or (v_new_family <> 'standard' and (v_new_shell = 'MODES' or v_new_type = 'PORTFOLIO_CONTROL_CENTER'));\n\n  if not v_new_modes then\n    return new;\n  end if;\n\n  if tg_op = 'INSERT' then\n    perform private.modes_storage_edge_request(new.id);\n    return new;\n  end if;\n\n  v_old_family := lower(coalesce(old.settings->>'experience_family',''));\n  v_old_shell := upper(coalesce(old.settings->>'shell_template',''));\n  v_old_type := upper(replace(coalesce(old.tenant_type,''),'-','_'));\n  v_old_modes := v_old_family = 'modes'\n    or (v_old_family <> 'standard' and (v_old_shell = 'MODES' or v_old_type = 'PORTFOLIO_CONTROL_CENTER'));\n\n  if not v_old_modes\n     or coalesce(old.settings#>'{modes,cards}','[]'::jsonb) is distinct from coalesce(new.settings#>'{modes,cards}','[]'::jsonb)\n     or coalesce(old.settings->>'storage_bucket','') is distinct from coalesce(new.settings->>'storage_bucket','') then\n    perform private.modes_storage_edge_request(new.id);\n  end if;\n  return new;\nend;\n$$;\nrevoke all on function private.enqueue_modes_storage_reconcile_trigger() from public, anon, authenticated;\n\ndrop trigger if exists tenant_modes_storage_reconcile on public.tenants;\ncreate trigger tenant_modes_storage_reconcile\nafter insert or update of tenant_type, settings on public.tenants\nfor each row execute function private.enqueue_modes_storage_reconcile_trigger();\n"],"idempotency_key":"928a131559cfee66f81092220fcdda034fbf8ec2:20260913022000"},{"name":"modes_hq_image_library","version":"20260913032000","rollback":null,"created_by":"canonical-production-release","statements":["-- Canonical high-quality source library for MODES card imagery.\ninsert into storage.buckets (id, name, public, file_size_limit, allowed_mime_types)\nvalues (\n  'platform-modes',\n  'platform-modes',\n  false,\n  262144000,\n  array['image/webp', 'image/jpeg', 'image/png', 'image/avif']::text[]\n)\non conflict (id) do update\nset\n  name = excluded.name,\n  public = false,\n  file_size_limit = excluded.file_size_limit,\n  allowed_mime_types = excluded.allowed_mime_types,\n  updated_at = now();\n\ninsert into public.platform_storage_registry (\n  bucket_id,\n  namespace,\n  domain,\n  purpose,\n  owner_tenant_id,\n  owner_tenant_slug,\n  canonical_path_contract,\n  visibility,\n  lifecycle,\n  retention_class,\n  managed_by,\n  status,\n  architecture_version,\n  last_audited_at,\n  updated_at\n)\nvalues (\n  'platform-modes',\n  'platform',\n  'modes-media',\n  'Canonical private source pack for high-quality MODES card imagery',\n  null,\n  null,\n  'v2/{mode-key}.webp',\n  'private',\n  'active',\n  'business',\n  'tenant-media-library',\n  'active',\n  'modes-media-v2',\n  now(),\n  now()\n)\non conflict (bucket_id) do update\nset\n  namespace = excluded.namespace,\n  domain = excluded.domain,\n  purpose = excluded.purpose,\n  owner_tenant_id = null,\n  owner_tenant_slug = null,\n  canonical_path_contract = excluded.canonical_path_contract,\n  visibility = 'private',\n  lifecycle = 'active',\n  retention_class = excluded.retention_class,\n  managed_by = excluded.managed_by,\n  status = 'active',\n  architecture_version = excluded.architecture_version,\n  last_audited_at = now(),\n  updated_at = now();\n"],"idempotency_key":"27c810b3c2b27eedfc27d5dc794751a628195f73:20260913032000"},{"name":"allow_default_pack_media_source_kind","version":"20260913033000","rollback":null,"created_by":"canonical-production-release","statements":["-- MODES V2 default-pack assets are canonical system-provided tenant media.\nalter table public.tenant_media_assets\n  drop constraint if exists tenant_media_assets_source_kind_check;\n\nalter table public.tenant_media_assets\n  add constraint tenant_media_assets_source_kind_check\n  check (source_kind = any (array['upload'::text, 'existing'::text, 'default_pack'::text]));\n"],"idempotency_key":"a621f500bb5ce3dddc4f43f0470a3a8030e61c8f:20260913033000"},{"name":"canonical_tenant_workspace_family","version":"20260914015000","rollback":null,"created_by":"canonical-production-release","statements":["create or replace function public.canonical_tenant_experience_family(p_settings jsonb, p_tenant_type text)\nreturns text\nlanguage plpgsql\nimmutable\nset search_path = public\nas $$\ndeclare\n  v_settings jsonb := coalesce(p_settings, '{}'::jsonb);\n  v_explicit text := lower(trim(coalesce(v_settings ->> 'experience_family', '')));\n  v_type text := upper(regexp_replace(trim(coalesce(p_tenant_type, '')), '[\\s-]+', '_', 'g'));\n  v_legacy_shell text := upper(trim(coalesce(\n    v_settings ->> 'shell_template',\n    v_settings ->> 'shellTemplate',\n    v_settings ->> 'tenant_shell_template',\n    ''\n  )));\nbegin\n  if v_explicit in ('standard', 'modes') then\n    return v_explicit;\n  end if;\n  if v_type = 'PORTFOLIO_CONTROL_CENTER' or v_legacy_shell = 'MODES' then\n    return 'modes';\n  end if;\n  return 'standard';\nend;\n$$;\n\nupdate public.tenants as tenant\nset settings = jsonb_set(\n      coalesce(tenant.settings, '{}'::jsonb),\n      '{experience_family}',\n      to_jsonb(public.canonical_tenant_experience_family(tenant.settings, tenant.tenant_type)),\n      true\n    ),\n    updated_at = now()\nwhere coalesce(tenant.settings ->> 'experience_family', '') is distinct from public.canonical_tenant_experience_family(tenant.settings, tenant.tenant_type);\n\ncreate or replace function public.enforce_tenant_experience_family()\nreturns trigger\nlanguage plpgsql\nset search_path = public\nas $$\nbegin\n  new.settings := jsonb_set(\n    coalesce(new.settings, '{}'::jsonb),\n    '{experience_family}',\n    to_jsonb(public.canonical_tenant_experience_family(new.settings, new.tenant_type)),\n    true\n  );\n  return new;\nend;\n$$;\n\ndrop trigger if exists tenants_enforce_experience_family on public.tenants;\ncreate trigger tenants_enforce_experience_family\nbefore insert or update of settings, tenant_type on public.tenants\nfor each row execute function public.enforce_tenant_experience_family();\n\nalter table public.tenants drop constraint if exists tenants_experience_family_valid;\nalter table public.tenants\n  add constraint tenants_experience_family_valid\n  check (coalesce(settings ->> 'experience_family', '') in ('standard', 'modes'));\n\ncreate or replace function public.apply_modes_tenant_capability_contract(p_settings jsonb, p_tenant_type text)\nreturns jsonb\nlanguage plpgsql\nstable\nset search_path = public\nas $$\ndeclare\n  v_settings jsonb := coalesce(p_settings, '{}'::jsonb);\n  v_modes jsonb := coalesce(p_settings -> 'modes', '{}'::jsonb);\n  v_family text := public.canonical_tenant_experience_family(p_settings, p_tenant_type);\n  v_default_count integer := 0;\n  v_ready boolean := false;\n  v_namespaces jsonb := jsonb_build_array(\n    'media/modes/today',\n    'media/modes/operations',\n    'media/modes/opportunities',\n    'media/modes/performance'\n  );\nbegin\n  v_settings := jsonb_set(v_settings, '{experience_family}', to_jsonb(v_family), true);\n  if v_family <> 'modes' then\n    return v_settings;\n  end if;\n\n  if to_regclass('public.platform_mode_default_media') is not null then\n    select count(distinct mode_key) into v_default_count\n      from public.platform_mode_default_media\n     where active = true\n       and mode_key = any(array['operations','planning','growth','financials','back-office','field','rentals','events','activations','vendors']);\n  end if;\n\n  if jsonb_typeof(v_modes -> 'cards') = 'array' then\n    select coalesce(jsonb_agg(namespace order by namespace), v_namespaces)\n      into v_namespaces\n      from (\n        select distinct namespace\n        from (\n          select jsonb_array_elements_text(v_namespaces) as namespace\n          union all\n          select 'media/modes/' || trim(both '-' from regexp_replace(\n            lower(coalesce(nullif(card ->> 'storage_slug',''), nullif(card ->> 'key',''), nullif(card ->> 'title',''))),\n            '[^a-z0-9]+','-','g'\n          )) as namespace\n          from jsonb_array_elements(v_modes -> 'cards') as card\n        ) candidates\n        where namespace <> 'media/modes/'\n      ) unique_namespaces;\n  end if;\n\n  v_ready := v_default_count = 10;\n  v_modes := v_modes || jsonb_build_object(\n    'version', 4,\n    'capability_contract', 'MODES_TENANT_V1',\n    'default_media_contract', 'MODE_DEFAULT_MEDIA_V2',\n    'activation_state', case when v_ready then 'ready' else 'prerequisite_required' end,\n    'storage_namespaces', v_namespaces,\n    'reconciled_at', now()\n  );\n\n  return v_settings || jsonb_build_object('experience_family','modes','modes',v_modes);\nend;\n$$;\n\ncreate or replace function public.reconcile_modes_tenant_capability(p_tenant_id uuid default null::uuid)\nreturns jsonb\nlanguage plpgsql\nsecurity definer\nset search_path = public\nas $$\ndeclare\n  v_count integer := 0;\n  v_ready integer := 0;\n  v_prerequisite integer := 0;\nbegin\n  update public.tenants as tenant\n  set settings = public.apply_modes_tenant_capability_contract(tenant.settings, tenant.tenant_type),\n      updated_at = now()\n  where (p_tenant_id is null or tenant.id = p_tenant_id)\n    and public.canonical_tenant_experience_family(tenant.settings, tenant.tenant_type) = 'modes';\n  get diagnostics v_count = row_count;\n\n  select count(*) filter (where tenant.settings #>> '{modes,activation_state}' = 'ready'),\n         count(*) filter (where tenant.settings #>> '{modes,activation_state}' = 'prerequisite_required')\n    into v_ready, v_prerequisite\n    from public.tenants as tenant\n    where (p_tenant_id is null or tenant.id = p_tenant_id)\n      and lower(coalesce(tenant.settings ->> 'experience_family', '')) = 'modes';\n\n  return jsonb_build_object(\n    'reconciled', v_count,\n    'ready', v_ready,\n    'prerequisite_required', v_prerequisite,\n    'contract', 'MODES_TENANT_V1',\n    'default_media_contract', 'MODE_DEFAULT_MEDIA_V2'\n  );\nend;\n$$;\n\n"],"idempotency_key":"493a439732b3cab69cc0dd6c0f0f76a04097c01e:20260914015000"},{"name":"fix_tenant_type_blueprint_contract","version":"20260914031800","rollback":null,"created_by":"canonical-production-release","statements":["-- Repair adaptive tenant classification and blueprint application contracts.\n\ncreate or replace function public.canonical_tenant_application_ids(p_apps jsonb)\nreturns jsonb\nlanguage sql\nstable\nset search_path to 'public'\nas $function$\nwith source as (\n  select value, ordinality\n  from jsonb_array_elements_text(\n    case when jsonb_typeof(p_apps) = 'array' then p_apps else '[]'::jsonb end\n  ) with ordinality as x(value, ordinality)\n), mapped as (\n  select\n    case value\n      when 'operations' then 'jobs'\n      when 'pipeline' then 'opportunity-radar'\n      when 'queue' then 'booking'\n      when 'orders' then 'jobs'\n      when 'route' then 'route-service-operations'\n      when 'settings' then null\n      else value\n    end as application_id,\n    ordinality\n  from source\n), valid as (\n  select m.application_id, min(m.ordinality) as ordinality\n  from mapped m\n  join public.application_catalog c on c.id = m.application_id\n  where m.application_id is not null\n  group by m.application_id\n)\nselect coalesce(jsonb_agg(application_id order by ordinality), '[]'::jsonb)\nfrom valid;\n$function$;\n\ncreate or replace function public.tenant_blueprint_applications(p_model text)\nreturns jsonb\nlanguage sql\nimmutable\nset search_path to 'public'\nas $function$\nselect case p_model\n  when 'appointments' then '[\"booking\",\"contacts\",\"calendar\",\"communications\",\"invoices\",\"documents\",\"tasks\",\"analytics\",\"opportunity-radar\"]'::jsonb\n  when 'recurring_routes' then '[\"route-service-operations\",\"jobs\",\"contacts\",\"calendar\",\"communications\",\"invoices\",\"documents\",\"tasks\",\"analytics\",\"opportunity-radar\"]'::jsonb\n  when 'events' then '[\"booking\",\"jobs\",\"contacts\",\"calendar\",\"communications\",\"invoices\",\"documents\",\"tasks\",\"analytics\",\"opportunity-radar\",\"proposals\",\"payments\"]'::jsonb\n  when 'orders' then '[\"jobs\",\"contacts\",\"communications\",\"invoices\",\"documents\",\"analytics\",\"opportunity-radar\",\"payments\"]'::jsonb\n  when 'professional_cases' then '[\"contacts\",\"calendar\",\"communications\",\"documents\",\"tasks\",\"analytics\",\"opportunity-radar\"]'::jsonb\n  when 'service_jobs' then '[\"jobs\",\"contacts\",\"calendar\",\"communications\",\"invoices\",\"documents\",\"tasks\",\"analytics\",\"opportunity-radar\",\"forms\",\"proposals\",\"payments\"]'::jsonb\n  when 'general' then '[\"jobs\",\"contacts\",\"calendar\",\"communications\",\"invoices\",\"documents\",\"tasks\",\"analytics\",\"opportunity-radar\"]'::jsonb\n  else '[\"jobs\",\"contacts\",\"calendar\",\"communications\",\"invoices\",\"documents\",\"tasks\",\"analytics\",\"opportunity-radar\"]'::jsonb\nend;\n$function$;\n\ncreate or replace function public.tenant_operating_model_for(p_business_type text, p_settings jsonb default '{}'::jsonb)\nreturns text\nlanguage plpgsql\nimmutable\nset search_path to 'public'\nas $function$\ndeclare\n  v_type text := public.normalize_declared_business_type(p_business_type);\n  v_model text := lower(trim(coalesce(\n    p_settings #>> '{business_operating_blueprint,operatingModel}',\n    p_settings #>> '{tenant_manifest,businessOperatingBlueprint,operatingModel}',\n    p_settings #>> '{tenant_manifest,adaptiveBusinessProfile,operatingModel}',\n    p_settings #>> '{adaptive_business_profile,operatingModel}',\n    p_settings ->> 'operating_model',\n    ''\n  )));\nbegin\n  if v_model in ('service_jobs','recurring_routes','appointments','professional_cases','orders','events','general') then return v_model; end if;\n  if v_type is null then return null; end if;\n  if v_type = 'MISC' then return 'general'; end if;\n  if v_type in ('CUSTOMER_PROGRESS_PLATFORM','ROOFING_LEAD_FUNNEL') or v_type ~ '(LEAD_FUNNEL|PROGRESS_PLATFORM|SAAS_PLATFORM)' then return 'general'; end if;\n  if v_type ~ '(BARBER|SALON|SPA|NAIL|MASSAGE|TATTOO|MEDSPA|DENTAL|DENTIST|CHIROPRACT|THERAP|APPOINTMENT)' then return 'appointments'; end if;\n  if v_type in ('LAWN_CARE') or v_type ~ '(POOL|PEST|ROUTE|RECURRING_SERVICE)' then return 'recurring_routes'; end if;\n  if v_type in ('MOBILE_VENDOR') or v_type ~ '(CATER|EVENT|WEDDING|VENUE)' then return 'events'; end if;\n  if v_type in ('CAFE_QSR') or v_type ~ '(RETAIL|BAKERY|ORDER|RESTAURANT)' then return 'orders'; end if;\n  if v_type in ('PUBLIC_ADJUSTER_FIRM','REALTOR') or v_type ~ '(LAW|LEGAL|ACCOUNT|CONSULT|CASE_MANAGEMENT)' then return 'professional_cases'; end if;\n  if v_type in ('MITIGATION','ROOFING_COMPANY','TIRE_SHOP','AUTO_DETAILING','MOBILE_LOCKSMITH') or v_type ~ '(LOCKSMITH|LOCKOUT|AUTO_LOCK|ROADSIDE|HVAC|PLUMB|ELECTRIC|CONTRACTOR|REPAIR|SERVICE_JOB)' then return 'service_jobs'; end if;\n  if v_type = 'SHORT_TERM_RENTAL' then return 'appointments'; end if;\n  return null;\nend;\n$function$;\n\ncreate or replace function public.bootstrap_tenant_applications_for_tenant(p_tenant_id uuid)\nreturns integer\nlanguage plpgsql\nsecurity definer\nset search_path to 'public'\nas $function$\ndeclare\n  v_tenant_type text;\n  v_settings jsonb;\n  v_declared_apps jsonb;\n  v_count integer := 0;\nbegin\n  select upper(replace(replace(coalesce(t.tenant_type::text, ''), '-', '_'), ' ', '_')), coalesce(t.settings, '{}'::jsonb)\n    into v_tenant_type, v_settings\n  from public.tenants t\n  where t.id = p_tenant_id;\n\n  if not found then\n    raise exception 'tenant_not_found';\n  end if;\n\n  v_declared_apps := case\n    when jsonb_typeof(v_settings->'enabled_applications') = 'array' and jsonb_array_length(v_settings->'enabled_applications') > 0 then v_settings->'enabled_applications'\n    when jsonb_typeof(v_settings#>'{tenant_manifest,applications}') = 'array' and jsonb_array_length(v_settings#>'{tenant_manifest,applications}') > 0 then v_settings#>'{tenant_manifest,applications}'\n    when jsonb_typeof(v_settings#>'{business_operating_blueprint,applications}') = 'array' and jsonb_array_length(v_settings#>'{business_operating_blueprint,applications}') > 0 then v_settings#>'{business_operating_blueprint,applications}'\n    else public.tenant_blueprint_applications(public.tenant_operating_model_for(v_tenant_type, v_settings))\n  end;\n  v_declared_apps := public.canonical_tenant_application_ids(v_declared_apps);\n\n  insert into public.tenant_applications (\n    tenant_id, application_id, enabled, settings, bootstrap_version, updated_at\n  )\n  select\n    p_tenant_id,\n    c.id,\n    true,\n    jsonb_strip_nulls(jsonb_build_object(\n      'application_slug', c.slug,\n      'standalone_path', c.standalone_path,\n      'login_path', c.login_path,\n      'bootstrap_contract', 'application-bootstrap.v' || c.bootstrap_version::text,\n      'route_adapter', c.route_adapter,\n      'bootstrap_source', case when v_declared_apps ? c.id then 'tenant-blueprint' else 'tenant-create-default-eligible' end\n    )),\n    c.bootstrap_version,\n    now()\n  from public.application_catalog c\n  where c.audience = 'tenant-user'\n    and (\n      (\n        c.default_installed = true\n        and (\n          jsonb_typeof(c.supported_tenant_types) <> 'array'\n          or jsonb_array_length(c.supported_tenant_types) = 0\n          or c.supported_tenant_types ? v_tenant_type\n        )\n      )\n      or v_declared_apps ? c.id\n    )\n  on conflict (tenant_id, application_id) do update set\n    settings = coalesce(public.tenant_applications.settings, '{}'::jsonb) || excluded.settings,\n    bootstrap_version = greatest(public.tenant_applications.bootstrap_version, excluded.bootstrap_version),\n    updated_at = now();\n\n  get diagnostics v_count = row_count;\n  return v_count;\nend;\n$function$;\n\nupdate public.tenants\nset settings = jsonb_set(settings, '{enabled_applications}', public.canonical_tenant_application_ids(settings->'enabled_applications'), true),\n    updated_at = now()\nwhere jsonb_typeof(settings->'enabled_applications') = 'array'\n  and public.canonical_tenant_application_ids(settings->'enabled_applications') is distinct from settings->'enabled_applications';\n\nupdate public.tenants\nset settings = jsonb_set(settings, '{tenant_manifest,applications}', public.canonical_tenant_application_ids(settings#>'{tenant_manifest,applications}'), true),\n    updated_at = now()\nwhere jsonb_typeof(settings#>'{tenant_manifest,applications}') = 'array'\n  and public.canonical_tenant_application_ids(settings#>'{tenant_manifest,applications}') is distinct from settings#>'{tenant_manifest,applications}';\n\nupdate public.tenants\nset settings = jsonb_set(settings, '{business_operating_blueprint,applications}', public.canonical_tenant_application_ids(settings#>'{business_operating_blueprint,applications}'), true),\n    updated_at = now()\nwhere jsonb_typeof(settings#>'{business_operating_blueprint,applications}') = 'array'\n  and public.canonical_tenant_application_ids(settings#>'{business_operating_blueprint,applications}') is distinct from settings#>'{business_operating_blueprint,applications}';\n\ndo $do$\ndeclare\n  r record;\n  v_label text;\n  v_settings jsonb;\n  v_model text;\n  v_blueprint jsonb;\n  v_manifest jsonb;\n  v_apps jsonb;\n  v_wmp_blueprint jsonb;\nbegin\n  select settings->'business_operating_blueprint'\n    into v_wmp_blueprint\n  from public.tenants\n  where company_slug = 'whatsmyprogress-com';\n\n  update public.tenants\n  set tenant_type = 'MOBILE_LOCKSMITH',\n      settings = coalesce(settings, '{}'::jsonb) || jsonb_build_object(\n        'declared_business_type', 'MOBILE_LOCKSMITH',\n        'operating_model', 'service_jobs',\n        'archetype', 'ADAPTIVE_BUSINESS'\n      ),\n      updated_at = now()\n  where company_slug = '321-auto-unlock';\n\n  select coalesce(settings, '{}'::jsonb), coalesce(company_dba_name, company_legal_name, name)\n    into v_settings, v_label\n  from public.tenants\n  where company_slug = '321-auto-unlock';\n  if found then\n    v_blueprint := public.build_tenant_business_blueprint('MOBILE_LOCKSMITH', v_settings, v_label);\n    v_apps := public.canonical_tenant_application_ids(coalesce(v_blueprint->'applications', public.tenant_blueprint_applications('service_jobs')));\n    v_blueprint := jsonb_set(v_blueprint, '{applications}', v_apps, true);\n    v_manifest := coalesce(v_settings->'tenant_manifest', '{}'::jsonb) || jsonb_build_object(\n      'schemaVersion', 2,\n      'businessTypeKey', 'MOBILE_LOCKSMITH',\n      'moduleTenantType', 'CUSTOM_BLUEPRINT',\n      'applications', v_apps,\n      'businessOperatingBlueprint', v_blueprint\n    );\n    update public.tenants\n    set settings = v_settings || jsonb_build_object(\n          'business_operating_blueprint', v_blueprint,\n          'enabled_applications', v_apps,\n          'tenant_manifest', v_manifest\n        ),\n        updated_at = now()\n    where company_slug = '321-auto-unlock';\n  end if;\n\n  select coalesce(settings, '{}'::jsonb)\n    into v_settings\n  from public.tenants\n  where company_slug = 'whatsmyprogress-com';\n  if found then\n    v_apps := public.canonical_tenant_application_ids(coalesce(v_settings#>'{tenant_manifest,applications}', v_settings->'enabled_applications', '[]'::jsonb));\n    v_manifest := coalesce(v_settings->'tenant_manifest', '{}'::jsonb) || jsonb_build_object(\n      'businessTypeKey', 'CUSTOMER_PROGRESS_PLATFORM',\n      'moduleTenantType', 'CUSTOM_BLUEPRINT',\n      'applications', v_apps\n    );\n    update public.tenants\n    set tenant_type = 'CUSTOMER_PROGRESS_PLATFORM',\n        settings = v_settings || jsonb_build_object(\n          'declared_business_type', 'CUSTOMER_PROGRESS_PLATFORM',\n          'operating_model', 'general',\n          'archetype', 'ADAPTIVE_BUSINESS',\n          'enabled_applications', v_apps,\n          'tenant_manifest', v_manifest\n        ),\n        updated_at = now()\n    where company_slug = 'whatsmyprogress-com';\n  end if;\n\n  if exists (\n    select 1 from public.tenants\n    where company_slug = 'whatsmyprogress-com'\n      and settings->'business_operating_blueprint' is distinct from v_wmp_blueprint\n  ) then\n    raise exception 'whatsmyprogress_blueprint_changed';\n  end if;\n\n  v_apps := '[\"opportunity-radar\",\"crm\",\"contacts\",\"forms\",\"communications\",\"call-center\",\"analytics\",\"documents\",\"tasks\"]'::jsonb;\n  v_blueprint := jsonb_build_object(\n    'version', 1,\n    'businessTypeKey', 'ROOFING_LEAD_FUNNEL',\n    'businessType', 'Roofing Lead Funnel',\n    'businessDescription', 'Roofing lead intake, qualification, follow-up, and handoff workspace',\n    'operatingModel', 'general',\n    'primaryRelationship', jsonb_build_object('singular','Lead','plural','Leads','kind','lead'),\n    'workObjects', jsonb_build_array(jsonb_build_object('singular','Lead','plural','Leads','kind','lead')),\n    'workflowStages', jsonb_build_array('New Lead','Contacted','Qualified','Estimate Requested','Sent to Roofer','Closed Won','Closed Lost'),\n    'applications', v_apps,\n    'capabilities', v_apps,\n    'modules', jsonb_build_array(\n      jsonb_build_object('id','opportunity-radar','path','opportunity-radar','label','Lead Radar','emphasis','primary'),\n      jsonb_build_object('id','crm','path','crm','label','Lead Pipeline'),\n      jsonb_build_object('id','contacts','path','contacts','label','Contacts'),\n      jsonb_build_object('id','forms','path','forms','label','Lead Intake'),\n      jsonb_build_object('id','communications','path','communications','label','Follow-up'),\n      jsonb_build_object('id','call-center','path','call-center','label','Call Center'),\n      jsonb_build_object('id','analytics','path','analytics','label','Lead Analytics'),\n      jsonb_build_object('id','documents','path','documents','label','Documents'),\n      jsonb_build_object('id','tasks','path','tasks','label','Tasks')\n    ),\n    'vocabulary', jsonb_build_object('workspace','Roof Help Finder','work','Lead','workPlural','Leads'),\n    'schedulingModel', 'lead_followup',\n    'billingModel', 'lead_generation',\n    'recurrenceModel', 'lead_nurture',\n    'dashboardPriorities', jsonb_build_array('New leads','Follow-ups due','Qualified leads','Roofer handoffs'),\n    'assumptions', '[]'::jsonb\n  );\n\n  select coalesce(settings, '{}'::jsonb)\n    into v_settings\n  from public.tenants\n  where company_slug = 'roof-help-finder';\n  if found then\n    v_manifest := coalesce(v_settings->'tenant_manifest', '{}'::jsonb) || jsonb_build_object(\n      'schemaVersion', 2,\n      'businessTypeKey', 'ROOFING_LEAD_FUNNEL',\n      'moduleTenantType', 'CUSTOM_BLUEPRINT',\n      'applications', v_apps,\n      'businessOperatingBlueprint', v_blueprint\n    );\n    update public.tenants\n    set tenant_type = 'ROOFING_LEAD_FUNNEL',\n        settings = v_settings || jsonb_build_object(\n          'declared_business_type', 'ROOFING_LEAD_FUNNEL',\n          'operating_model', 'general',\n          'archetype', 'ADAPTIVE_BUSINESS',\n          'business_operating_blueprint', v_blueprint,\n          'enabled_applications', v_apps,\n          'tenant_manifest', v_manifest,\n          'capability_packs', case\n            when jsonb_typeof(v_settings->'capability_packs') = 'array' and v_settings->'capability_packs' ? 'lead-funnel' then v_settings->'capability_packs'\n            when jsonb_typeof(v_settings->'capability_packs') = 'array' then (v_settings->'capability_packs') || '[\"lead-funnel\"]'::jsonb\n            else '[\"lead-funnel\"]'::jsonb\n          end\n        ),\n        updated_at = now()\n    where company_slug = 'roof-help-finder';\n  end if;\n\n  for r in\n    select id, company_slug, tenant_type, coalesce(settings, '{}'::jsonb) as settings,\n           coalesce(company_dba_name, company_legal_name, name) as label\n    from public.tenants\n    where company_slug in ('tranzformer-auto-detailing','cafe-qsr','la-vista-del-mar')\n  loop\n    v_settings := r.settings;\n    v_model := public.tenant_operating_model_for(r.tenant_type, v_settings);\n    if v_model is null then\n      raise exception 'operating_model_missing:%', r.company_slug;\n    end if;\n    v_blueprint := public.build_tenant_business_blueprint(r.tenant_type, v_settings || jsonb_build_object('operating_model', v_model), r.label);\n    v_apps := public.canonical_tenant_application_ids(coalesce(v_blueprint->'applications', public.tenant_blueprint_applications(v_model)));\n    v_blueprint := jsonb_set(v_blueprint, '{applications}', v_apps, true);\n    v_manifest := coalesce(v_settings->'tenant_manifest', '{}'::jsonb) || jsonb_build_object(\n      'schemaVersion', 2,\n      'businessTypeKey', r.tenant_type,\n      'moduleTenantType', r.tenant_type,\n      'applications', v_apps,\n      'businessOperatingBlueprint', v_blueprint\n    );\n    update public.tenants\n    set settings = v_settings || jsonb_build_object(\n          'declared_business_type', r.tenant_type,\n          'operating_model', v_model,\n          'business_operating_blueprint', v_blueprint,\n          'enabled_applications', v_apps,\n          'tenant_manifest', v_manifest\n        ),\n        updated_at = now()\n    where id = r.id;\n  end loop;\n\n  for r in\n    select id\n    from public.tenants\n    where company_slug in (\n      '321-auto-unlock','whatsmyprogress-com','roof-help-finder',\n      'tranzformer-auto-detailing','cafe-qsr','la-vista-del-mar',\n      'jj-barber-spa','salty-panda-events','bookmedirectly-com',\n      'demo-c-c-carpet-cleaning-1b5e576a','demo-space-coast-seal-and-sign-6010714f'\n    )\n  loop\n    perform public.bootstrap_tenant_applications_for_tenant(r.id);\n  end loop;\n\n  if not exists (select 1 from public.tenants where company_slug='321-auto-unlock' and tenant_type='MOBILE_LOCKSMITH' and settings->>'route_namespace'='321-auto-unlock') then\n    raise exception '321_auto_unlock_contract_not_verified';\n  end if;\n  if not exists (select 1 from public.tenants where company_slug='whatsmyprogress-com' and tenant_type='CUSTOMER_PROGRESS_PLATFORM' and settings->>'route_namespace'='whatsmyprogress-com') then\n    raise exception 'whatsmyprogress_contract_not_verified';\n  end if;\n  if not exists (select 1 from public.tenants where company_slug='roof-help-finder' and tenant_type='ROOFING_LEAD_FUNNEL' and settings->>'route_namespace'='roof-help-finder') then\n    raise exception 'roof_help_finder_contract_not_verified';\n  end if;\n\n  if exists (\n    with declared as (\n      select t.id, app_id\n      from public.tenants t\n      cross join lateral jsonb_array_elements_text(coalesce(t.settings#>'{tenant_manifest,applications}', '[]'::jsonb)) app_id\n      where t.is_active = true\n      union all\n      select t.id, app_id\n      from public.tenants t\n      cross join lateral jsonb_array_elements_text(coalesce(t.settings->'enabled_applications', '[]'::jsonb)) app_id\n      where t.is_active = true\n    )\n    select 1 from declared d left join public.application_catalog c on c.id=d.app_id where c.id is null\n  ) then\n    raise exception 'active_tenant_invalid_application_reference';\n  end if;\n\n  if exists (\n    select 1\n    from (values\n      ('jj-barber-spa','BARBERSHOP_SPA'),\n      ('salty-panda-events','EVENT_RENTALS'),\n      ('curri-control-center','PORTFOLIO_CONTROL_CENTER'),\n      ('knocklaunch-crm','PLATFORM')\n    ) as expected(company_slug, tenant_type)\n    left join public.tenants t on t.company_slug=expected.company_slug\n    where t.id is null or t.tenant_type<>expected.tenant_type\n  ) then\n    raise exception 'representative_tenant_type_isolation_failed';\n  end if;\nend;\n$do$;\n"],"idempotency_key":"ca61db9c6cc0542ee5914e8ce435063ce21c88fc:20260914031800"},{"name":"roof_help_finder_tenant_native_apps","version":"20260914032000","rollback":null,"created_by":"canonical-production-release","statements":["-- Keep Roof Help Finder on tenant-native application routes.\ndo $do$\ndeclare\n  v_tenant_id uuid;\n  v_settings jsonb;\n  v_apps jsonb := '[\"opportunity-radar\",\"contacts\",\"forms\",\"communications\",\"call-center\",\"analytics\",\"documents\",\"tasks\"]'::jsonb;\n  v_manifest jsonb;\n  v_blueprint jsonb;\nbegin\n  select id, coalesce(settings, '{}'::jsonb)\n    into v_tenant_id, v_settings\n  from public.tenants\n  where company_slug = 'roof-help-finder'\n    and tenant_type = 'ROOFING_LEAD_FUNNEL';\n\n  if v_tenant_id is null then\n    raise exception 'roof_help_finder_not_found_or_wrong_type';\n  end if;\n\n  v_blueprint := coalesce(v_settings->'business_operating_blueprint', '{}'::jsonb);\n  v_blueprint := jsonb_set(v_blueprint, '{applications}', v_apps, true);\n  v_blueprint := jsonb_set(v_blueprint, '{capabilities}', v_apps, true);\n  v_blueprint := jsonb_set(v_blueprint, '{modules}', jsonb_build_array(\n    jsonb_build_object('id','opportunity-radar','path','opportunity-radar','label','Lead Radar','emphasis','primary'),\n    jsonb_build_object('id','contacts','path','contacts','label','Contacts'),\n    jsonb_build_object('id','forms','path','forms','label','Lead Intake'),\n    jsonb_build_object('id','communications','path','communications','label','Follow-up'),\n    jsonb_build_object('id','call-center','path','call-center','label','Call Center'),\n    jsonb_build_object('id','analytics','path','analytics','label','Lead Analytics'),\n    jsonb_build_object('id','documents','path','documents','label','Documents'),\n    jsonb_build_object('id','tasks','path','tasks','label','Tasks')\n  ), true);\n\n  v_manifest := coalesce(v_settings->'tenant_manifest', '{}'::jsonb) || jsonb_build_object(\n    'businessTypeKey', 'ROOFING_LEAD_FUNNEL',\n    'moduleTenantType', 'CUSTOM_BLUEPRINT',\n    'applications', v_apps,\n    'businessOperatingBlueprint', v_blueprint\n  );\n\n  update public.tenants\n  set settings = v_settings || jsonb_build_object(\n        'enabled_applications', v_apps,\n        'business_operating_blueprint', v_blueprint,\n        'tenant_manifest', v_manifest\n      ),\n      updated_at = now()\n  where id = v_tenant_id;\n\n  update public.tenant_applications\n  set enabled = false,\n      updated_at = now()\n  where tenant_id = v_tenant_id\n    and application_id = 'crm';\n\n  perform public.bootstrap_tenant_applications_for_tenant(v_tenant_id);\n\n  if exists (\n    select 1\n    from jsonb_array_elements_text(v_apps) app_id\n    left join public.application_catalog c on c.id = app_id\n    where c.id is null\n       or coalesce(c.route_adapter, '') = 'platform-workspace'\n  ) then\n    raise exception 'roof_help_finder_non_tenant_native_app_detected';\n  end if;\n\n  if exists (\n    select 1\n    from public.tenant_applications\n    where tenant_id = v_tenant_id\n      and application_id = 'crm'\n      and enabled = true\n  ) then\n    raise exception 'roof_help_finder_crm_still_enabled';\n  end if;\nend;\n$do$;\n"],"idempotency_key":"319c4002fe9972799ea290e75a7f4016d017dea9:20260914032000"},{"name":"prepare_modes_workspace_activation","version":"20260914032800","rollback":null,"created_by":"canonical-production-release","statements":["create or replace function public.prepare_modes_tenant_workspace(p_tenant_id uuid)\nreturns jsonb\nlanguage plpgsql\nsecurity definer\nset search_path to 'public'\nas $function$\ndeclare\n  v_settings jsonb;\n  v_modes jsonb;\n  v_cards jsonb;\n  v_namespaces jsonb;\n  v_default_count integer := 0;\n  v_now timestamptz := now();\nbegin\n  select coalesce(settings, '{}'::jsonb)\n    into v_settings\n    from public.tenants\n   where id = p_tenant_id\n   for update;\n\n  if not found then\n    raise exception 'tenant_not_found';\n  end if;\n\n  if to_regclass('public.platform_mode_default_media') is null then\n    raise exception 'mode_capability_prerequisite_required';\n  end if;\n\n  select count(distinct mode_key)\n    into v_default_count\n    from public.platform_mode_default_media\n   where active = true\n     and mode_key = any(array['operations','planning','growth','financials','back-office','field','rentals','events','activations','vendors']);\n\n  if v_default_count <> 10 then\n    raise exception 'mode_capability_prerequisite_required';\n  end if;\n\n  v_modes := coalesce(v_settings -> 'modes', '{}'::jsonb);\n  if jsonb_typeof(v_modes -> 'cards') = 'array' and jsonb_array_length(v_modes -> 'cards') > 0 then\n    v_cards := v_modes -> 'cards';\n  else\n    v_cards := jsonb_build_array(\n      jsonb_build_object('key','today','title','Today','subtitle','Plan the day and keep priorities visible.','storage_slug','today','default_image_key','planning','color','#38bdf8'),\n      jsonb_build_object('key','operations','title','Operations','subtitle','Run the work that keeps the business moving.','storage_slug','operations','default_image_key','operations','color','#34d399'),\n      jsonb_build_object('key','opportunities','title','Opportunities','subtitle','Work leads, follow-up, and growth.','storage_slug','opportunities','default_image_key','growth','color','#f59e0b'),\n      jsonb_build_object('key','performance','title','Performance','subtitle','See money, results, and business health.','storage_slug','performance','default_image_key','financials','color','#a78bfa')\n    );\n  end if;\n\n  select coalesce(jsonb_agg(namespace order by namespace), '[]'::jsonb)\n    into v_namespaces\n    from (\n      select distinct 'media/modes/' || trim(both '-' from regexp_replace(\n        lower(coalesce(nullif(card ->> 'storage_slug',''), nullif(card ->> 'key',''), nullif(card ->> 'title',''))),\n        '[^a-z0-9]+','-','g'\n      )) as namespace\n      from jsonb_array_elements(v_cards) as card\n    ) prepared\n   where namespace <> 'media/modes/';\n\n  v_modes := v_modes || jsonb_build_object(\n    'version', 4,\n    'capability_contract', 'MODES_TENANT_V1',\n    'default_media_contract', 'MODE_DEFAULT_MEDIA_V2',\n    'activation_state', 'prepare',\n    'storage_namespaces', v_namespaces,\n    'cards', v_cards,\n    'reconciled_at', v_now\n  );\n\n  update public.tenants\n     set settings = jsonb_set(v_settings, '{modes}', v_modes, true),\n         updated_at = v_now\n   where id = p_tenant_id;\n\n  return jsonb_build_object(\n    'ok', true,\n    'tenantId', p_tenant_id,\n    'activation_state', 'prepare',\n    'card_count', jsonb_array_length(v_cards),\n    'storage_namespaces', v_namespaces,\n    'prepared_at', v_now\n  );\nend;\n$function$;\n\nrevoke all on function public.prepare_modes_tenant_workspace(uuid) from public, anon, authenticated;\ngrant execute on function public.prepare_modes_tenant_workspace(uuid) to service_role;\n"],"idempotency_key":"27a07712a53c19b0eba1cd63a2ee4a0916703840:20260914032800"},{"name":"business_blueprint_authority","version":"20260914033800","rollback":null,"created_by":"canonical-production-release","statements":["create or replace function public.merge_tenant_application_ids(variadic p_sources jsonb[])\nreturns jsonb\nlanguage plpgsql\nstable\nset search_path = public\nas $$\ndeclare\n  v_source jsonb;\n  v_combined jsonb := '[]'::jsonb;\nbegin\n  foreach v_source in array p_sources loop\n    if jsonb_typeof(v_source) = 'array' then\n      v_combined := v_combined || v_source;\n    end if;\n  end loop;\n  return public.canonical_tenant_application_ids(v_combined);\nend;\n$$;\n\ncreate or replace function public.business_blueprint_module_tenant_type(p_business_type text)\nreturns text\nlanguage sql\nimmutable\nset search_path = public\nas $$\n  select case public.normalize_declared_business_type(p_business_type)\n    when 'MITIGATION' then 'MITIGATION'\n    when 'ROOFING_COMPANY' then 'ROOFING_COMPANY'\n    when 'PUBLIC_ADJUSTER_FIRM' then 'PUBLIC_ADJUSTER_FIRM'\n    when 'REALTOR' then 'REALTOR'\n    when 'LAWN_CARE' then 'LAWN_CARE'\n    when 'TIRE_SHOP' then 'TIRE_SHOP'\n    when 'AUTO_DETAILING' then 'AUTO_DETAILING'\n    when 'CAFE_QSR' then 'CAFE_QSR'\n    when 'MOBILE_VENDOR' then 'MOBILE_VENDOR'\n    when 'SHORT_TERM_RENTAL' then 'SHORT_TERM_RENTAL'\n    when 'EVENT_RENTALS' then 'EVENT_RENTALS'\n    when 'PORTFOLIO_CONTROL_CENTER' then 'PORTFOLIO_CONTROL_CENTER'\n    when 'PLATFORM_ADMIN' then 'PLATFORM_ADMIN'\n    when 'MISC' then 'MISC'\n    else 'CUSTOM_BLUEPRINT'\n  end;\n$$;\n\ncreate or replace function public.compile_tenant_business_blueprint_settings(\n  p_tenant_type text,\n  p_name text,\n  p_settings jsonb,\n  p_merge_legacy boolean default false,\n  p_legacy_apps jsonb default '[]'::jsonb\n)\nreturns jsonb\nlanguage plpgsql\nsecurity definer\nset search_path = public\nas $$\ndeclare\n  v_settings jsonb := coalesce(p_settings, '{}'::jsonb);\n  v_manifest jsonb := case when jsonb_typeof(coalesce(p_settings, '{}'::jsonb)->'tenant_manifest') = 'object' then coalesce(p_settings, '{}'::jsonb)->'tenant_manifest' else '{}'::jsonb end;\n  v_onboarding jsonb := case when jsonb_typeof(coalesce(p_settings, '{}'::jsonb)->'onboarding_contract') = 'object' then coalesce(p_settings, '{}'::jsonb)->'onboarding_contract' else '{}'::jsonb end;\n  v_existing_blueprint jsonb;\n  v_generated_blueprint jsonb;\n  v_blueprint jsonb;\n  v_apps jsonb;\n  v_business_type text;\n  v_raw_business_type text;\n  v_model text;\n  v_module_tenant_type text;\n  v_minimal boolean;\nbegin\n  v_existing_blueprint := case\n    when jsonb_typeof(v_settings->'business_operating_blueprint') = 'object' then v_settings->'business_operating_blueprint'\n    when jsonb_typeof(v_manifest->'businessOperatingBlueprint') = 'object' then v_manifest->'businessOperatingBlueprint'\n    else '{}'::jsonb\n  end;\n\n  v_raw_business_type := coalesce(\n    nullif(v_existing_blueprint->>'businessTypeKey', ''),\n    nullif(v_manifest#>>'{businessOperatingBlueprint,businessTypeKey}', ''),\n    nullif(v_manifest->>'businessTypeKey', ''),\n    nullif(v_settings->>'declared_business_type', ''),\n    nullif(v_settings->>'business_type', ''),\n    nullif(p_tenant_type, '')\n  );\n  v_business_type := public.normalize_declared_business_type(v_raw_business_type);\n  if v_business_type is null then\n    if upper(trim(coalesce(v_raw_business_type, ''))) in ('PLATFORM','PLATFORM_ADMIN','GODMODE','ADMIN','SYSTEM_ADMIN') then\n      return v_settings;\n    end if;\n    raise exception 'business_blueprint_business_type_required' using errcode = '22023';\n  end if;\n\n  v_model := coalesce(\n    nullif(v_existing_blueprint->>'operatingModel', ''),\n    nullif(v_settings->>'operating_model', ''),\n    public.tenant_operating_model_for(v_business_type, v_settings),\n    'general'\n  );\n\n  if jsonb_typeof(v_existing_blueprint) = 'object'\n     and v_existing_blueprint <> '{}'::jsonb\n     and nullif(v_existing_blueprint->>'operatingModel', '') is not null then\n    -- A rich persisted blueprint is business truth. Its operating model may be\n    -- intentionally more specific than the generic blueprint factory's model\n    -- families (for example executive_oversight). Do not flatten it through\n    -- the generic generator; only compile projections from it below.\n    v_generated_blueprint := '{}'::jsonb;\n    v_blueprint := v_existing_blueprint;\n  else\n    v_generated_blueprint := public.build_tenant_business_blueprint(\n      v_business_type,\n      v_settings || jsonb_build_object(\n        'declared_business_type', v_business_type,\n        'business_type', v_business_type,\n        'operating_model', v_model\n      ),\n      coalesce(nullif(p_name, ''), v_business_type)\n    );\n    v_blueprint := coalesce(v_generated_blueprint, '{}'::jsonb) || v_existing_blueprint;\n  end if;\n\n  v_apps := public.canonical_tenant_application_ids(v_blueprint->'applications');\n  if p_merge_legacy then\n    v_apps := public.merge_tenant_application_ids(\n      v_blueprint->'applications',\n      v_settings->'enabled_applications',\n      v_manifest->'applications',\n      v_onboarding->'applications',\n      p_legacy_apps\n    );\n  end if;\n\n  v_minimal := lower(coalesce(v_settings->>'minimal_workspace', 'false')) = 'true';\n  if jsonb_array_length(v_apps) = 0 and not v_minimal then\n    v_apps := public.canonical_tenant_application_ids(public.tenant_blueprint_applications(v_model));\n  end if;\n\n  v_module_tenant_type := public.business_blueprint_module_tenant_type(v_business_type);\n  v_blueprint := v_blueprint || jsonb_build_object(\n    'businessTypeKey', v_business_type,\n    'operatingModel', v_model,\n    'applications', v_apps\n  );\n\n  v_manifest := v_manifest || jsonb_build_object(\n    'businessTypeKey', v_business_type,\n    'moduleTenantType', v_module_tenant_type,\n    'applications', v_apps,\n    'businessOperatingBlueprint', v_blueprint,\n    'source', 'persisted'\n  );\n\n  v_onboarding := v_onboarding || jsonb_build_object(\n    'declared_tenant_type', v_business_type,\n    'module_tenant_type', v_module_tenant_type,\n    'business_operating_blueprint', v_blueprint,\n    'applications', v_apps\n  );\n\n  return v_settings || jsonb_build_object(\n    'business_type', v_business_type,\n    'declared_business_type', v_business_type,\n    'operating_model', v_model,\n    'business_operating_blueprint', v_blueprint,\n    'enabled_applications', v_apps,\n    'module_tenant_type', v_module_tenant_type,\n    'tenant_manifest', v_manifest,\n    'onboarding_contract', v_onboarding\n  );\nend;\n$$;\n\ncreate or replace function public.reconcile_tenant_business_blueprint(\n  p_tenant_id uuid,\n  p_merge_legacy boolean default false\n)\nreturns jsonb\nlanguage plpgsql\nsecurity definer\nset search_path = public\nas $$\ndeclare\n  v_tenant public.tenants%rowtype;\n  v_legacy_apps jsonb := '[]'::jsonb;\n  v_settings jsonb;\n  v_apps jsonb;\n  v_enabled_count integer := 0;\nbegin\n  select * into v_tenant from public.tenants where id = p_tenant_id for update;\n  if not found then raise exception 'tenant_not_found'; end if;\n\n  if public.normalize_declared_business_type(coalesce(\n      v_tenant.settings#>>'{business_operating_blueprint,businessTypeKey}',\n      v_tenant.settings->>'declared_business_type',\n      v_tenant.settings->>'business_type',\n      v_tenant.tenant_type\n    )) is null then\n    return jsonb_build_object(\n      'ok', true,\n      'tenant_id', p_tenant_id,\n      'skipped', true,\n      'reason', 'non_business_platform_identity'\n    );\n  end if;\n\n  if p_merge_legacy\n     and jsonb_array_length(public.canonical_tenant_application_ids(v_tenant.settings->'enabled_applications')) = 0\n     and jsonb_array_length(public.canonical_tenant_application_ids(v_tenant.settings#>'{tenant_manifest,applications}')) = 0 then\n    select coalesce(jsonb_agg(ta.application_id order by ta.installed_at, ta.application_id), '[]'::jsonb)\n      into v_legacy_apps\n    from public.tenant_applications ta\n    where ta.tenant_id = p_tenant_id and ta.enabled = true;\n  end if;\n\n  v_settings := public.compile_tenant_business_blueprint_settings(\n    v_tenant.tenant_type,\n    v_tenant.name,\n    coalesce(v_tenant.settings, '{}'::jsonb),\n    p_merge_legacy,\n    v_legacy_apps\n  );\n  v_apps := public.canonical_tenant_application_ids(v_settings#>'{business_operating_blueprint,applications}');\n\n  update public.tenants\n  set settings = v_settings,\n      updated_at = now()\n  where id = p_tenant_id;\n\n  insert into public.tenant_applications (\n    tenant_id, application_id, enabled, settings, bootstrap_version, updated_at\n  )\n  select\n    p_tenant_id,\n    c.id,\n    true,\n    jsonb_build_object(\n      'application_slug', c.slug,\n      'standalone_path', c.standalone_path,\n      'login_path', c.login_path,\n      'bootstrap_contract', 'application-bootstrap.v' || c.bootstrap_version::text,\n      'route_adapter', c.route_adapter,\n      'bootstrap_source', 'business-operating-blueprint'\n    ),\n    c.bootstrap_version,\n    now()\n  from jsonb_array_elements_text(v_apps) app(application_id)\n  join public.application_catalog c on c.id = app.application_id\n  on conflict (tenant_id, application_id) do update set\n    enabled = true,\n    settings = coalesce(public.tenant_applications.settings, '{}'::jsonb) || excluded.settings,\n    bootstrap_version = greatest(public.tenant_applications.bootstrap_version, excluded.bootstrap_version),\n    updated_at = now();\n\n  update public.tenant_applications ta\n  set enabled = false,\n      updated_at = now(),\n      settings = coalesce(ta.settings, '{}'::jsonb) || jsonb_build_object('disabled_by', 'business-operating-blueprint')\n  where ta.tenant_id = p_tenant_id\n    and ta.enabled = true\n    and not (v_apps ? ta.application_id)\n    and exists (\n      select 1 from public.application_catalog c\n      where c.id = ta.application_id and c.audience = 'tenant-user'\n    );\n\n  select count(*) into v_enabled_count\n  from public.tenant_applications\n  where tenant_id = p_tenant_id and enabled = true;\n\n  return jsonb_build_object(\n    'ok', true,\n    'tenant_id', p_tenant_id,\n    'business_type_key', v_settings#>>'{business_operating_blueprint,businessTypeKey}',\n    'operating_model', v_settings#>>'{business_operating_blueprint,operatingModel}',\n    'module_tenant_type', v_settings->>'module_tenant_type',\n    'applications', v_apps,\n    'enabled_application_count', v_enabled_count,\n    'authority', 'business-operating-blueprint'\n  );\nend;\n$$;\n\nrevoke all on function public.reconcile_tenant_business_blueprint(uuid, boolean) from public, anon, authenticated;\ngrant execute on function public.reconcile_tenant_business_blueprint(uuid, boolean) to service_role;\n\ncreate or replace function public.enforce_tenant_business_blueprint_authority()\nreturns trigger\nlanguage plpgsql\nsecurity definer\nset search_path = public\nas $$\nbegin\n  new.settings := public.compile_tenant_business_blueprint_settings(\n    new.tenant_type,\n    new.name,\n    coalesce(new.settings, '{}'::jsonb),\n    tg_op = 'INSERT',\n    '[]'::jsonb\n  );\n  return new;\nend;\n$$;\n\ndrop trigger if exists aab_tenant_business_blueprint_authority on public.tenants;\ncreate trigger aab_tenant_business_blueprint_authority\nbefore insert or update of tenant_type, settings on public.tenants\nfor each row execute function public.enforce_tenant_business_blueprint_authority();\n\ncreate or replace function public.bootstrap_tenant_applications_for_tenant(p_tenant_id uuid)\nreturns integer\nlanguage plpgsql\nsecurity definer\nset search_path = public\nas $$\ndeclare\n  v_count integer;\nbegin\n  perform public.reconcile_tenant_business_blueprint(p_tenant_id, false);\n  select count(*) into v_count\n  from public.tenant_applications\n  where tenant_id = p_tenant_id and enabled = true;\n  return v_count;\nend;\n$$;\n\n-- One-time lossless adoption: enrich older/incomplete blueprints with every\n-- currently valid working app before making the blueprint authoritative.\ndo $$\ndeclare\n  r record;\nbegin\n  for r in\n    select id\n    from public.tenants\n    where is_active = true\n       or jsonb_typeof(coalesce(settings, '{}'::jsonb)->'business_operating_blueprint') = 'object'\n  loop\n    perform public.reconcile_tenant_business_blueprint(r.id, true);\n  end loop;\nend;\n$$;\n\n-- Contract assertions: persisted projections must be compiled from the same\n-- canonical application list, and semantic module IDs must never leak into it.\ndo $$\ndeclare\n  v_drift integer;\n  v_unknown integer;\n  v_semantic_aliases integer;\nbegin\n  select count(*) into v_drift\n  from public.tenants t\n  where (t.is_active = true or jsonb_typeof(coalesce(t.settings, '{}'::jsonb)->'business_operating_blueprint') = 'object')\n    and (\n      coalesce(t.settings->'enabled_applications', '[]'::jsonb) is distinct from public.canonical_tenant_application_ids(t.settings#>'{business_operating_blueprint,applications}')\n      or coalesce(t.settings#>'{tenant_manifest,applications}', '[]'::jsonb) is distinct from public.canonical_tenant_application_ids(t.settings#>'{business_operating_blueprint,applications}')\n      or coalesce(t.settings#>'{tenant_manifest,businessOperatingBlueprint,applications}', '[]'::jsonb) is distinct from public.canonical_tenant_application_ids(t.settings#>'{business_operating_blueprint,applications}')\n    );\n  if v_drift > 0 then raise exception 'business_blueprint_projection_drift:%', v_drift; end if;\n\n  select count(*) into v_unknown\n  from public.tenants t\n  cross join lateral jsonb_array_elements_text(coalesce(t.settings#>'{business_operating_blueprint,applications}', '[]'::jsonb)) app(application_id)\n  left join public.application_catalog c on c.id = app.application_id\n  where (t.is_active = true or jsonb_typeof(coalesce(t.settings, '{}'::jsonb)->'business_operating_blueprint') = 'object')\n    and c.id is null;\n  if v_unknown > 0 then raise exception 'business_blueprint_unknown_application_ids:%', v_unknown; end if;\n\n  select count(*) into v_semantic_aliases\n  from public.tenants t\n  cross join lateral jsonb_array_elements_text(coalesce(t.settings#>'{business_operating_blueprint,applications}', '[]'::jsonb)) app(application_id)\n  where app.application_id in ('settings','operations','pipeline','queue','orders','route');\n  if v_semantic_aliases > 0 then raise exception 'business_blueprint_semantic_module_leaked_into_applications:%', v_semantic_aliases; end if;\nend;\n$$;\n\ncomment on function public.compile_tenant_business_blueprint_settings(text, text, jsonb, boolean, jsonb)\nis 'Canonical compiler: business operating blueprint is authoritative; manifests, enabled apps and compatibility metadata are projections.';\ncomment on function public.reconcile_tenant_business_blueprint(uuid, boolean)\nis 'Reconciles tenant application assignments and persisted projections from the canonical business operating blueprint.';\n"],"idempotency_key":"c76a72dfde4082242cfaf985750ccb2906c50a2c:20260914033800"},{"name":"canonicalize_big_papis_mode_card_routes","version":"20260914035800","rollback":null,"created_by":"canonical-production-release","statements":["-- Canonicalize only Big Papi's persisted MODES card route fields.\n-- jsonb_set(card, '{route}', ...) preserves every other property on each card.\nupdate public.tenants as t\nset settings = jsonb_set(\n  coalesce(t.settings, '{}'::jsonb),\n  '{modes,cards}',\n  coalesce(\n    (\n      select jsonb_agg(\n        case card->>'key'\n          when 'plan' then jsonb_set(card, '{route}', to_jsonb('/big-papis/owner/dashboard?bpMode=plan'::text), true)\n          when 'popup' then jsonb_set(card, '{route}', to_jsonb('/big-papis/owner/dashboard?bpMode=popup'::text), true)\n          when 'event' then jsonb_set(card, '{route}', to_jsonb('/big-papis/owner/dashboard?bpMode=event'::text), true)\n          when 'office' then jsonb_set(card, '{route}', to_jsonb('/big-papis/owner/dashboard?bpMode=office'::text), true)\n          else card\n        end\n        order by ordinality\n      )\n      from jsonb_array_elements(t.settings->'modes'->'cards')\n        with ordinality as entries(card, ordinality)\n    ),\n    t.settings->'modes'->'cards'\n  ),\n  true\n)\nwhere lower(coalesce(t.company_slug, '')) in ('big-papis', 'big-papis-coquito-iceys')\n  and jsonb_typeof(t.settings->'modes'->'cards') = 'array';\n"],"idempotency_key":"f1890c34e477fef2c2a7746a88b03409cc571ec6:20260914035800"},{"name":"mobile_catering_contract","version":"20260914103200","rollback":null,"created_by":"canonical-production-release","statements":["-- MOBILE_CATERING is the first-class event-driven archetype for mobile catering and chef-led food-truck operations.\n\ncreate or replace function public.business_blueprint_module_tenant_type(p_business_type text)\nreturns text\nlanguage sql\nimmutable\nset search_path = public\nas $$\n  select case public.normalize_declared_business_type(p_business_type)\n    when 'MITIGATION' then 'MITIGATION'\n    when 'ROOFING_COMPANY' then 'ROOFING_COMPANY'\n    when 'PUBLIC_ADJUSTER_FIRM' then 'PUBLIC_ADJUSTER_FIRM'\n    when 'REALTOR' then 'REALTOR'\n    when 'LAWN_CARE' then 'LAWN_CARE'\n    when 'TIRE_SHOP' then 'TIRE_SHOP'\n    when 'AUTO_DETAILING' then 'AUTO_DETAILING'\n    when 'CAFE_QSR' then 'CAFE_QSR'\n    when 'MOBILE_VENDOR' then 'MOBILE_VENDOR'\n    when 'MOBILE_CATERING' then 'MOBILE_CATERING'\n    when 'SHORT_TERM_RENTAL' then 'SHORT_TERM_RENTAL'\n    when 'EVENT_RENTALS' then 'EVENT_RENTALS'\n    when 'PORTFOLIO_CONTROL_CENTER' then 'PORTFOLIO_CONTROL_CENTER'\n    when 'PLATFORM_ADMIN' then 'PLATFORM_ADMIN'\n    when 'MISC' then 'MISC'\n    else 'CUSTOM_BLUEPRINT'\n  end;\n$$;\n\ncreate or replace function public.prepare_mobile_catering_first_class()\nreturns trigger\nlanguage plpgsql\nsecurity definer\nset search_path = public\nas $$\ndeclare\n  v_family text;\nbegin\n  if public.normalize_declared_business_type(new.tenant_type) <> 'MOBILE_CATERING' then\n    return new;\n  end if;\n\n  v_family := public.canonical_tenant_experience_family(coalesce(new.settings, '{}'::jsonb), 'MOBILE_CATERING');\n  new.tenant_type := 'MOBILE_CATERING';\n  new.settings := coalesce(new.settings, '{}'::jsonb) || jsonb_build_object(\n    'business_type', 'MOBILE_CATERING',\n    'declared_business_type', 'MOBILE_CATERING',\n    'declared_tenant_type', 'MOBILE_CATERING',\n    'archetype', 'MOBILE_CATERING',\n    'module_tenant_type', 'MOBILE_CATERING',\n    'operating_model', 'events',\n    'experience_family', v_family,\n    'factory_experience_family', 'mobile-catering-operations',\n    'blueprint_key', 'mobile-catering-standard',\n    'workspace_type', 'mobile_catering',\n    'routing_family', 'MOBILE_CATERING_OPS'\n  );\n  return new;\nend;\n$$;\n\ndrop trigger if exists aaa_prepare_mobile_catering_first_class on public.tenants;\ncreate trigger aaa_prepare_mobile_catering_first_class\nbefore insert or update on public.tenants\nfor each row execute function public.prepare_mobile_catering_first_class();\n\nupdate public.application_catalog\nset supported_tenant_types = supported_tenant_types || '[\"MOBILE_CATERING\"]'::jsonb,\n    updated_at = now()\nwhere jsonb_typeof(supported_tenant_types) = 'array'\n  and supported_tenant_types ? 'MOBILE_VENDOR'\n  and not (supported_tenant_types ? 'MOBILE_CATERING');\n"],"idempotency_key":"d740263d78b8df795e6c21e8756783fb09cd34a1:20260914103200"},{"name":"creative_studio","version":"20260914202000","rollback":null,"created_by":"canonical-production-release","statements":["create table if not exists public.creative_profiles (\n  tenant_id uuid primary key references public.tenants(id) on delete cascade,\n  favorite_fonts jsonb not null default '[]'::jsonb,\n  design_preferences jsonb not null default '{}'::jsonb,\n  preferred_media_ids jsonb not null default '[]'::jsonb,\n  created_at timestamptz not null default now(),\n  updated_at timestamptz not null default now()\n);\ncreate table if not exists public.creative_templates (\n  id uuid primary key default gen_random_uuid(),\n  tenant_id uuid not null references public.tenants(id) on delete cascade,\n  kind text not null check (kind in ('flyer','proposal','invoice')),\n  label text not null,\n  format_key text not null default 'custom',\n  presentation jsonb not null default '{}'::jsonb,\n  content jsonb not null default '{}'::jsonb,\n  metadata jsonb not null default '{}'::jsonb,\n  created_at timestamptz not null default now(),\n  updated_at timestamptz not null default now()\n);\ncreate index if not exists creative_templates_tenant_kind_updated_idx on public.creative_templates(tenant_id,kind,updated_at desc);\nalter table public.creative_profiles enable row level security;\nalter table public.creative_templates enable row level security;\ndrop policy if exists creative_profiles_member_select on public.creative_profiles;\ncreate policy creative_profiles_member_select on public.creative_profiles for select using (is_system_admin(auth.uid()) or exists (select 1 from public.security_active_user_tenant_memberships m where m.user_id=auth.uid() and m.status='active' and m.tenant_id=creative_profiles.tenant_id));\ndrop policy if exists creative_profiles_member_insert on public.creative_profiles;\ncreate policy creative_profiles_member_insert on public.creative_profiles for insert with check (is_system_admin(auth.uid()) or exists (select 1 from public.security_active_user_tenant_memberships m where m.user_id=auth.uid() and m.status='active' and m.tenant_id=creative_profiles.tenant_id));\ndrop policy if exists creative_profiles_member_update on public.creative_profiles;\ncreate policy creative_profiles_member_update on public.creative_profiles for update using (is_system_admin(auth.uid()) or exists (select 1 from public.security_active_user_tenant_memberships m where m.user_id=auth.uid() and m.status='active' and m.tenant_id=creative_profiles.tenant_id)) with check (is_system_admin(auth.uid()) or exists (select 1 from public.security_active_user_tenant_memberships m where m.user_id=auth.uid() and m.status='active' and m.tenant_id=creative_profiles.tenant_id));\ndrop policy if exists creative_templates_member_select on public.creative_templates;\ncreate policy creative_templates_member_select on public.creative_templates for select using (is_system_admin(auth.uid()) or exists (select 1 from public.security_active_user_tenant_memberships m where m.user_id=auth.uid() and m.status='active' and m.tenant_id=creative_templates.tenant_id));\ndrop policy if exists creative_templates_member_insert on public.creative_templates;\ncreate policy creative_templates_member_insert on public.creative_templates for insert with check (is_system_admin(auth.uid()) or exists (select 1 from public.security_active_user_tenant_memberships m where m.user_id=auth.uid() and m.status='active' and m.tenant_id=creative_templates.tenant_id));\ndrop policy if exists creative_templates_member_update on public.creative_templates;\ncreate policy creative_templates_member_update on public.creative_templates for update using (is_system_admin(auth.uid()) or exists (select 1 from public.security_active_user_tenant_memberships m where m.user_id=auth.uid() and m.status='active' and m.tenant_id=creative_templates.tenant_id)) with check (is_system_admin(auth.uid()) or exists (select 1 from public.security_active_user_tenant_memberships m where m.user_id=auth.uid() and m.status='active' and m.tenant_id=creative_templates.tenant_id));\ndrop policy if exists creative_templates_member_delete on public.creative_templates;\ncreate policy creative_templates_member_delete on public.creative_templates for delete using (is_system_admin(auth.uid()) or exists (select 1 from public.security_active_user_tenant_memberships m where m.user_id=auth.uid() and m.status='active' and m.tenant_id=creative_templates.tenant_id));\ninsert into public.application_catalog (id,slug,label,audience,workspace_surface,bootstrap_mode,default_installed,capabilities,data_owners,supported_tenant_types,standalone_path,login_path,bootstrap_version,route_adapter,config_contract)\nvalues ('creative-studio','creative-studio','Creative Studio','tenant-user','creative-studio','default',false,'[\"creative-library\",\"flyers\",\"templates\",\"proposal-design\",\"invoice-presentation\"]'::jsonb,'[\"creative_profiles\",\"creative_templates\",\"tenant_media_assets\",\"proposal_profiles\",\"invoice_templates\"]'::jsonb,'[]'::jsonb,'/creative-studio','/creative-studio/login',1,'creative-studio','{\"contract\":\"creative-studio.v1\",\"document_modes\":[\"flyer\",\"proposal\",\"invoice\"]}'::jsonb)\non conflict (id) do update set label=excluded.label,audience=excluded.audience,workspace_surface=excluded.workspace_surface,capabilities=excluded.capabilities,data_owners=excluded.data_owners,supported_tenant_types=excluded.supported_tenant_types,standalone_path=excluded.standalone_path,login_path=excluded.login_path,bootstrap_version=excluded.bootstrap_version,route_adapter=excluded.route_adapter,config_contract=excluded.config_contract,updated_at=now();\ninsert into public.tenant_applications (tenant_id,application_id,enabled,settings,bootstrap_version)\nselect t.id,'creative-studio',true,'{\"application_slug\":\"creative-studio\",\"route_adapter\":\"creative-studio\",\"standalone_path\":\"/creative-studio\",\"bootstrap_source\":\"creative-studio-v1\"}'::jsonb,1 from public.tenants t where t.company_slug='bychefap'\non conflict (tenant_id,application_id) do update set enabled=true,settings=excluded.settings,bootstrap_version=excluded.bootstrap_version,updated_at=now();\nwith rewritten as (select t.id,jsonb_agg(case when c.card->>'key'='office' then jsonb_set(c.card,'{primary_applications}',case when coalesce(c.card->'primary_applications','[]'::jsonb)?'creative-studio' then coalesce(c.card->'primary_applications','[]'::jsonb) else coalesce(c.card->'primary_applications','[]'::jsonb)||'[\"creative-studio\"]'::jsonb end,true) else c.card end order by c.ordinality) cards from public.tenants t cross join lateral jsonb_array_elements(coalesce(t.settings->'modes'->'cards','[]'::jsonb)) with ordinality c(card,ordinality) where t.company_slug='bychefap' group by t.id) update public.tenants t set settings=jsonb_set(t.settings,'{modes,cards}',rewritten.cards,true),updated_at=now() from rewritten where t.id=rewritten.id;\nupdate public.tenants t set settings=jsonb_set(jsonb_set(t.settings,'{business_operating_blueprint,applications}',case when coalesce(t.settings->'business_operating_blueprint'->'applications','[]'::jsonb)?'creative-studio' then coalesce(t.settings->'business_operating_blueprint'->'applications','[]'::jsonb) else coalesce(t.settings->'business_operating_blueprint'->'applications','[]'::jsonb)||'[\"creative-studio\"]'::jsonb end,true),'{business_operating_blueprint,capabilities}',case when coalesce(t.settings->'business_operating_blueprint'->'capabilities','[]'::jsonb)?'creative-studio' then coalesce(t.settings->'business_operating_blueprint'->'capabilities','[]'::jsonb) else coalesce(t.settings->'business_operating_blueprint'->'capabilities','[]'::jsonb)||'[\"creative-studio\"]'::jsonb end,true),updated_at=now() where t.company_slug='bychefap';\n"],"idempotency_key":"082f7835777cb9b45b0d1274571dbefd2970a92d:20260914202000"},{"name":"mobile_catering_modes_preservation","version":"20260914205500","rollback":null,"created_by":"canonical-production-release","statements":["create or replace function public.prepare_modes_tenant_workspace(p_tenant_id uuid)\nreturns jsonb\nlanguage plpgsql\nsecurity definer\nset search_path to 'public'\nas $function$\ndeclare\n  v_settings jsonb;\n  v_modes jsonb;\n  v_cards jsonb;\n  v_namespaces jsonb;\n  v_default_count integer := 0;\n  v_now timestamptz := now();\n  v_tenant_type text;\n  v_normalized_type text;\n  v_is_generic_seed boolean := false;\nbegin\n  select coalesce(settings, '{}'::jsonb), tenant_type\n    into v_settings, v_tenant_type\n    from public.tenants\n   where id = p_tenant_id\n   for update;\n\n  if not found then raise exception 'tenant_not_found'; end if;\n  if to_regclass('public.platform_mode_default_media') is null then raise exception 'mode_capability_prerequisite_required'; end if;\n\n  select count(distinct mode_key) into v_default_count\n    from public.platform_mode_default_media\n   where active = true\n     and mode_key = any(array['operations','planning','growth','financials','back-office','field','rentals','events','activations','vendors']);\n  if v_default_count <> 10 then raise exception 'mode_capability_prerequisite_required'; end if;\n\n  v_modes := coalesce(v_settings -> 'modes', '{}'::jsonb);\n  v_normalized_type := public.normalize_declared_business_type(coalesce(\n    v_settings #>> '{business_operating_blueprint,businessTypeKey}',\n    v_settings ->> 'declared_business_type',\n    v_settings ->> 'business_type',\n    v_tenant_type\n  ));\n\n  if jsonb_typeof(v_modes -> 'cards') = 'array' and jsonb_array_length(v_modes -> 'cards') = 4 then\n    select count(*) = 4 into v_is_generic_seed\n      from jsonb_array_elements(v_modes -> 'cards') with ordinality as c(card, ord)\n     where (ord = 1 and coalesce(card->>'key','') = 'today' and coalesce(card->>'title','') = 'Today')\n        or (ord = 2 and coalesce(card->>'key','') = 'operations' and coalesce(card->>'title','') = 'Operations')\n        or (ord = 3 and coalesce(card->>'key','') = 'opportunities' and coalesce(card->>'title','') = 'Opportunities')\n        or (ord = 4 and coalesce(card->>'key','') = 'performance' and coalesce(card->>'title','') = 'Performance');\n  end if;\n\n  if v_normalized_type = 'MOBILE_CATERING' and (\n       jsonb_typeof(v_modes -> 'cards') <> 'array'\n       or jsonb_array_length(coalesce(v_modes -> 'cards', '[]'::jsonb)) = 0\n       or v_is_generic_seed\n     ) then\n    v_cards := jsonb_build_array(\n      jsonb_build_object('key','plan','title','Plan & Book','subtitle','Find catering leads, food-truck events, festivals, and turn them into booked work.','storage_slug','plan','default_image_key','planning','color','#18b8ff','primary_applications',jsonb_build_array('opportunity-radar','booking','calendar','proposals','contacts','communications')),\n      jsonb_build_object('key','prep','title','Kitchen Prep','subtitle','Turn booked work into prep lists, production tasks, documents, and a ready kitchen.','storage_slug','kitchen-prep','default_image_key','planning','color','#13ed86','primary_applications',jsonb_build_array('jobs','tasks','documents','calendar','contacts','communications')),\n      jsonb_build_object('key','events','title','Catering & Events','subtitle','Run private catering, food-truck festivals, pop-ups, and booked mobile events.','storage_slug','catering-events','default_image_key','events','color','#ff7b22','primary_applications',jsonb_build_array('booking','calendar','jobs','tasks','documents','communications')),\n      jsonb_build_object('key','office','title','Back Office','subtitle','Build proposals, create flyers, send invoices, collect payments, follow up, and watch the numbers.','storage_slug','back-office','default_image_key','back-office','color','#ff3f97','primary_applications',jsonb_build_array('creative-studio','invoices','payments','proposals','analytics','documents','communications'))\n    );\n  elsif jsonb_typeof(v_modes -> 'cards') = 'array' and jsonb_array_length(v_modes -> 'cards') > 0 then\n    v_cards := v_modes -> 'cards';\n  else\n    v_cards := jsonb_build_array(\n      jsonb_build_object('key','today','title','Today','subtitle','Plan the day and keep priorities visible.','storage_slug','today','default_image_key','planning','color','#38bdf8'),\n      jsonb_build_object('key','operations','title','Operations','subtitle','Run the work that keeps the business moving.','storage_slug','operations','default_image_key','operations','color','#34d399'),\n      jsonb_build_object('key','opportunities','title','Opportunities','subtitle','Work leads, follow-up, and growth.','storage_slug','opportunities','default_image_key','growth','color','#f59e0b'),\n      jsonb_build_object('key','performance','title','Performance','subtitle','See money, results, and business health.','storage_slug','performance','default_image_key','financials','color','#a78bfa')\n    );\n  end if;\n\n  select coalesce(jsonb_agg(namespace order by namespace), '[]'::jsonb) into v_namespaces\n    from (\n      select distinct 'media/modes/' || trim(both '-' from regexp_replace(lower(coalesce(nullif(card ->> 'storage_slug',''), nullif(card ->> 'key',''), nullif(card ->> 'title',''))), '[^a-z0-9]+','-','g')) as namespace\n      from jsonb_array_elements(v_cards) as card\n    ) prepared\n   where namespace <> 'media/modes/';\n\n  v_modes := v_modes || jsonb_build_object('version',4,'capability_contract','MODES_TENANT_V1','default_media_contract','MODE_DEFAULT_MEDIA_V2','activation_state','prepare','storage_namespaces',v_namespaces,'cards',v_cards,'reconciled_at',v_now);\n\n  update public.tenants set settings = jsonb_set(v_settings, '{modes}', v_modes, true), updated_at = v_now where id = p_tenant_id;\n\n  return jsonb_build_object('ok',true,'tenantId',p_tenant_id,'activation_state','prepare','card_count',jsonb_array_length(v_cards),'storage_namespaces',v_namespaces,'prepared_at',v_now,'mobile_catering_preserved',v_normalized_type='MOBILE_CATERING');\nend;\n$function$;\n"],"idempotency_key":"d3e8057efb655aad45783ec4700c763413e32f7c:20260914205500"},{"name":"mobile_catering_modes_enforcement","version":"20260914210000","rollback":null,"created_by":"canonical-production-release","statements":["create or replace function public.apply_modes_tenant_capability_contract(p_settings jsonb, p_tenant_type text)\nreturns jsonb\nlanguage plpgsql\nstable\nset search_path to 'public'\nas $function$\ndeclare\n  v_settings jsonb := coalesce(p_settings, '{}'::jsonb);\n  v_modes jsonb := coalesce(p_settings -> 'modes', '{}'::jsonb);\n  v_family text := public.canonical_tenant_experience_family(p_settings, p_tenant_type);\n  v_default_count integer := 0;\n  v_ready boolean := false;\n  v_normalized_type text;\n  v_cards jsonb;\n  v_has_cards_array boolean := jsonb_typeof(coalesce(p_settings -> 'modes', '{}'::jsonb) -> 'cards') = 'array';\n  v_is_generic_seed boolean := false;\n  v_namespaces jsonb;\nbegin\n  v_settings := jsonb_set(v_settings, '{experience_family}', to_jsonb(v_family), true);\n  if v_family <> 'modes' then\n    return v_settings;\n  end if;\n\n  v_normalized_type := public.normalize_declared_business_type(coalesce(\n    v_settings #>> '{business_operating_blueprint,businessTypeKey}',\n    v_settings ->> 'declared_business_type',\n    v_settings ->> 'business_type',\n    p_tenant_type\n  ));\n\n  if v_has_cards_array then\n    v_cards := v_modes -> 'cards';\n    if jsonb_array_length(v_cards) = 4 then\n      select count(*) = 4\n        into v_is_generic_seed\n        from jsonb_array_elements(v_cards) with ordinality as c(card, ord)\n       where (ord = 1 and coalesce(card->>'key','') = 'today' and coalesce(card->>'title','') = 'Today')\n          or (ord = 2 and coalesce(card->>'key','') = 'operations' and coalesce(card->>'title','') = 'Operations')\n          or (ord = 3 and coalesce(card->>'key','') = 'opportunities' and coalesce(card->>'title','') = 'Opportunities')\n          or (ord = 4 and coalesce(card->>'key','') = 'performance' and coalesce(card->>'title','') = 'Performance');\n    end if;\n  end if;\n\n  if v_normalized_type = 'MOBILE_CATERING'\n     and (not v_has_cards_array or jsonb_array_length(v_cards) = 0 or v_is_generic_seed) then\n    v_cards := jsonb_build_array(\n      jsonb_build_object(\n        'key','plan','title','Plan & Book',\n        'subtitle','Find catering leads, food-truck events, festivals, and turn them into booked work.',\n        'storage_slug','plan','default_image_key','planning','color','#18b8ff',\n        'primary_applications',jsonb_build_array('opportunity-radar','booking','calendar','proposals','contacts','communications')\n      ),\n      jsonb_build_object(\n        'key','prep','title','Kitchen Prep',\n        'subtitle','Turn booked work into prep lists, production tasks, documents, and a ready kitchen.',\n        'storage_slug','kitchen-prep','default_image_key','planning','color','#13ed86',\n        'primary_applications',jsonb_build_array('jobs','tasks','documents','calendar','contacts','communications')\n      ),\n      jsonb_build_object(\n        'key','events','title','Catering & Events',\n        'subtitle','Run private catering, food-truck festivals, pop-ups, and booked mobile events.',\n        'storage_slug','catering-events','default_image_key','events','color','#ff7b22',\n        'primary_applications',jsonb_build_array('booking','calendar','jobs','tasks','documents','communications')\n      ),\n      jsonb_build_object(\n        'key','office','title','Back Office',\n        'subtitle','Build proposals, create flyers, send invoices, collect payments, follow up, and watch the numbers.',\n        'storage_slug','back-office','default_image_key','back-office','color','#ff3f97',\n        'primary_applications',jsonb_build_array('creative-studio','invoices','payments','proposals','analytics','documents','communications')\n      )\n    );\n    v_has_cards_array := true;\n  end if;\n\n  if to_regclass('public.platform_mode_default_media') is not null then\n    select count(distinct mode_key)\n      into v_default_count\n      from public.platform_mode_default_media\n     where active = true\n       and mode_key = any(array['operations','planning','growth','financials','back-office','field','rentals','events','activations','vendors']);\n  end if;\n\n  if v_has_cards_array and jsonb_array_length(v_cards) > 0 then\n    select coalesce(jsonb_agg(namespace order by namespace), '[]'::jsonb)\n      into v_namespaces\n      from (\n        select distinct 'media/modes/' || trim(both '-' from regexp_replace(\n          lower(coalesce(nullif(card ->> 'storage_slug',''), nullif(card ->> 'key',''), nullif(card ->> 'title',''))),\n          '[^a-z0-9]+','-','g'\n        )) as namespace\n        from jsonb_array_elements(v_cards) as card\n      ) prepared\n     where namespace <> 'media/modes/';\n  else\n    v_namespaces := jsonb_build_array(\n      'media/modes/today',\n      'media/modes/operations',\n      'media/modes/opportunities',\n      'media/modes/performance'\n    );\n  end if;\n\n  v_ready := v_default_count = 10;\n  v_modes := v_modes || jsonb_build_object(\n    'version', 4,\n    'capability_contract', 'MODES_TENANT_V1',\n    'default_media_contract', 'MODE_DEFAULT_MEDIA_V2',\n    'activation_state', case when v_ready then 'ready' else 'prerequisite_required' end,\n    'storage_namespaces', v_namespaces,\n    'reconciled_at', now()\n  );\n  if v_has_cards_array then\n    v_modes := v_modes || jsonb_build_object('cards', v_cards);\n  end if;\n\n  return v_settings || jsonb_build_object('experience_family','modes','modes',v_modes);\nend;\n$function$;\n\ndo $assert$\ndeclare\n  v_generic jsonb;\n  v_result jsonb;\n  v_custom jsonb;\n  v_custom_result jsonb;\nbegin\n  v_generic := jsonb_build_object(\n    'experience_family','modes',\n    'business_operating_blueprint',jsonb_build_object('businessTypeKey','MOBILE_CATERING'),\n    'modes',jsonb_build_object('cards',jsonb_build_array(\n      jsonb_build_object('key','today','title','Today','storage_slug','today'),\n      jsonb_build_object('key','operations','title','Operations','storage_slug','operations'),\n      jsonb_build_object('key','opportunities','title','Opportunities','storage_slug','opportunities'),\n      jsonb_build_object('key','performance','title','Performance','storage_slug','performance')\n    ))\n  );\n  v_result := public.apply_modes_tenant_capability_contract(v_generic, 'MOBILE_CATERING');\n  if v_result #>> '{modes,cards,0,title}' <> 'Plan & Book'\n     or v_result #>> '{modes,cards,1,title}' <> 'Kitchen Prep'\n     or v_result #>> '{modes,cards,2,title}' <> 'Catering & Events'\n     or v_result #>> '{modes,cards,3,title}' <> 'Back Office' then\n    raise exception 'mobile_catering_generic_modes_normalization_failed';\n  end if;\n  if not coalesce((v_result #> '{modes,cards,3,primary_applications}') ? 'creative-studio', false) then\n    raise exception 'mobile_catering_creative_studio_missing';\n  end if;\n  if coalesce((v_result #> '{modes,storage_namespaces}') ? 'media/modes/today', false)\n     or coalesce((v_result #> '{modes,storage_namespaces}') ? 'media/modes/operations', false)\n     or coalesce((v_result #> '{modes,storage_namespaces}') ? 'media/modes/opportunities', false)\n     or coalesce((v_result #> '{modes,storage_namespaces}') ? 'media/modes/performance', false) then\n    raise exception 'mobile_catering_generic_namespaces_leaked';\n  end if;\n\n  v_custom := jsonb_build_object(\n    'experience_family','modes',\n    'business_operating_blueprint',jsonb_build_object('businessTypeKey','MOBILE_CATERING'),\n    'modes',jsonb_build_object('cards',jsonb_build_array(\n      jsonb_build_object('key','chef-choice','title','Chef Choice','storage_slug','chef-choice')\n    ))\n  );\n  v_custom_result := public.apply_modes_tenant_capability_contract(v_custom, 'MOBILE_CATERING');\n  if jsonb_array_length(v_custom_result #> '{modes,cards}') <> 1\n     or v_custom_result #>> '{modes,cards,0,title}' <> 'Chef Choice'\n     or v_custom_result #>> '{modes,cards,0,storage_slug}' <> 'chef-choice' then\n    raise exception 'mobile_catering_custom_modes_preservation_failed';\n  end if;\nend;\n$assert$;\n\nupdate public.tenants as tenant\n   set settings = public.apply_modes_tenant_capability_contract(tenant.settings, tenant.tenant_type),\n       updated_at = now()\n where public.canonical_tenant_experience_family(tenant.settings, tenant.tenant_type) = 'modes'\n   and public.normalize_declared_business_type(coalesce(\n         tenant.settings #>> '{business_operating_blueprint,businessTypeKey}',\n         tenant.settings ->> 'declared_business_type',\n         tenant.settings ->> 'business_type',\n         tenant.tenant_type\n       )) = 'MOBILE_CATERING';\n"],"idempotency_key":"961e8dd498c91c4ab76be7749a8236fd38b9e00d:20260914210000"},{"name":"reconcile_tenant_blueprint_machine_type","version":"20260914213500","rollback":null,"created_by":"canonical-production-release","statements":["-- Keep rich persisted business blueprints aligned with the canonical declared\n-- tenant identity without flattening owner-facing human business labels.\n\ncreate or replace function public.enforce_tenant_business_blueprint_authority()\nreturns trigger\nlanguage plpgsql\nsecurity definer\nset search_path to 'public'\nas $function$\ndeclare\n  v_canonical_type text;\n  v_business_label text;\nbegin\n  new.settings := public.compile_tenant_business_blueprint_settings(\n    new.tenant_type,\n    new.name,\n    coalesce(new.settings, '{}'::jsonb),\n    tg_op = 'INSERT',\n    '[]'::jsonb\n  );\n\n  v_canonical_type := public.normalize_declared_business_type(new.tenant_type);\n  v_business_label := nullif(new.settings #>> '{tenant_manifest,businessOperatingBlueprint,businessType}', '');\n\n  -- A legacy machine token (for example MOBILE_VENDOR) may survive inside a\n  -- rich blueprint after the tenant is promoted to a new first-class type.\n  -- Repair only machine-shaped labels. Human labels intentionally remain owner\n  -- facing and are never normalized away by this authority.\n  if v_canonical_type is not null\n     and v_business_label ~ '^[A-Z][A-Z0-9_]{1,95}$'\n     and public.normalize_declared_business_type(v_business_label) is distinct from v_canonical_type then\n    new.settings := jsonb_set(\n      jsonb_set(\n        jsonb_set(\n          new.settings,\n          '{tenant_manifest,businessOperatingBlueprint,businessType}',\n          to_jsonb(v_canonical_type),\n          true\n        ),\n        '{business_operating_blueprint,businessType}',\n        to_jsonb(v_canonical_type),\n        true\n      ),\n      '{onboarding_contract,business_operating_blueprint,businessType}',\n      to_jsonb(v_canonical_type),\n      true\n    );\n  end if;\n\n  return new;\nend;\n$function$;\n\n-- Recompile the one production tenant proven to carry this legacy machine\n-- label. The predicate makes the migration a no-op once the row is healthy.\nupdate public.tenants\nset settings = settings\nwhere id = 'ea91e6fb-0528-40d6-a5dc-72a2a73e8c96'::uuid\n  and public.normalize_declared_business_type(tenant_type) = 'MOBILE_CATERING'\n  and settings #>> '{tenant_manifest,businessOperatingBlueprint,businessType}' = 'MOBILE_VENDOR'\n  and settings #>> '{tenant_manifest,businessOperatingBlueprint,businessTypeKey}' = 'MOBILE_CATERING';\n\ndo $verify$\nbegin\n  if exists (\n    select 1\n    from public.tenants\n    where id = 'ea91e6fb-0528-40d6-a5dc-72a2a73e8c96'::uuid\n      and (\n        public.normalize_declared_business_type(tenant_type) is distinct from 'MOBILE_CATERING'\n        or settings->>'declared_business_type' is distinct from 'MOBILE_CATERING'\n        or settings #>> '{tenant_manifest,businessOperatingBlueprint,businessTypeKey}' is distinct from 'MOBILE_CATERING'\n        or settings #>> '{tenant_manifest,businessOperatingBlueprint,businessType}' is distinct from 'MOBILE_CATERING'\n      )\n  ) then\n    raise exception 'bychefap_mobile_catering_blueprint_reconciliation_failed';\n  end if;\nend;\n$verify$;\n"],"idempotency_key":"26decb2ae0d0bacfef50a35368a40e51e13fc457:20260914213500"},{"name":"current_user_workspace_bootstrap","version":"20260914234500","rollback":null,"created_by":"canonical-production-release","statements":["-- Collapse authenticated workspace startup into one server-owned read.\ncreate or replace function public.get_current_user_workspace_bootstrap()\nreturns jsonb\nlanguage sql\nstable\nsecurity definer\nset search_path = 'public', 'auth', 'pg_temp'\nas $function$\nwith caller as (\n  select auth.uid() as user_id\n), authority as (\n  select c.user_id, coalesce(public.is_system_admin(c.user_id), false) as platform_admin\n  from caller c\n), visible_tenants as (\n  select t.*\n  from public.tenants t\n  cross join authority a\n  where a.user_id is not null\n    and t.status = 'active'\n    and (\n      a.platform_admin\n      or exists (\n        select 1\n        from public.user_tenant_memberships m\n        where m.user_id = a.user_id\n          and m.tenant_id = t.id\n          and m.status = 'active'\n      )\n      or exists (\n        select 1\n        from public.user_roles r\n        where r.user_id = a.user_id\n          and r.tenant_id = t.id\n      )\n    )\n), access_rows as (\n  select\n    t.id as tenant_id,\n    jsonb_build_object(\n      'user_id', a.user_id,\n      'tenant_id', t.id,\n      'effective_role', coalesce(\n        role_row.role,\n        case when coalesce(membership_row.is_owner, false) then 'Tenant Owner' end,\n        case when a.platform_admin then 'GodMode' else 'Rep' end\n      ),\n      'is_owner', coalesce(membership_row.is_owner, false),\n      'membership_status', coalesce(membership_row.status, 'active')\n    ) as access_row\n  from visible_tenants t\n  cross join authority a\n  left join lateral (\n    select m.is_owner, m.status\n    from public.user_tenant_memberships m\n    where m.user_id = a.user_id and m.tenant_id = t.id\n    order by m.joined_at desc nulls last\n    limit 1\n  ) membership_row on true\n  left join lateral (\n    select r.role::text as role\n    from public.user_roles r\n    where r.user_id = a.user_id and r.tenant_id = t.id\n    order by case r.role::text\n      when 'Owner' then 1\n      when 'Tenant Owner' then 2\n      when 'Admin' then 3\n      when 'Manager' then 4\n      when 'PM' then 5\n      when 'Rep' then 6\n      else 20\n    end\n    limit 1\n  ) role_row on true\n)\nselect jsonb_build_object(\n  'user_id', a.user_id,\n  'platform_admin', a.platform_admin,\n  'access', coalesce((select jsonb_agg(ar.access_row order by ar.tenant_id) from access_rows ar), '[]'::jsonb),\n  'tenants', coalesce((select jsonb_agg(to_jsonb(vt) order by vt.company_slug nulls last, vt.name) from visible_tenants vt), '[]'::jsonb)\n)\nfrom authority a;\n$function$;\n\nrevoke all on function public.get_current_user_workspace_bootstrap() from public;\ngrant execute on function public.get_current_user_workspace_bootstrap() to authenticated;\n"],"idempotency_key":"d4ce95282b4b5495f60dd0bece4c841d22501205:20260914234500"},{"name":"mail_continuity_primitive","version":"20260915130000","rollback":null,"created_by":"canonical-production-release","statements":["-- KNOCKLAUNCH Mail Continuity primitive.\n-- Provider-level fallback delivery survives KnockLaunch UI/login degradation and\n-- never treats an individual's failed login as an outage signal.\n\ncreate table if not exists public.tenant_mail_continuity (\n  id uuid primary key default gen_random_uuid(),\n  tenant_id uuid not null references public.tenants(id) on delete cascade,\n  business_email text not null,\n  fallback_email text,\n  fallback_identity_verified_at timestamptz,\n  provider_verification_status text not null default 'unconfigured'\n    check (provider_verification_status in ('unconfigured','pending','verified','error','prerequisite_required')),\n  provider_verified_at timestamptz,\n  provider_forward_enabled boolean not null default false,\n  provider_org_id text,\n  provider_user_id text,\n  provider_account_id text,\n  mailbox_ready boolean not null default false,\n  mail_launched boolean not null default false,\n  desired_mode text not null default 'prelaunch' check (desired_mode in ('normal','prelaunch','degraded')),\n  effective_mode text not null default 'normal' check (effective_mode in ('normal','prelaunch','degraded')),\n  activation_reason text,\n  degraded_since timestamptz,\n  recovered_at timestamptz,\n  last_provider_reconciled_at timestamptz,\n  last_mail_reconciled_at timestamptz,\n  last_error text,\n  created_at timestamptz not null default now(),\n  updated_at timestamptz not null default now(),\n  unique (tenant_id, business_email),\n  check (fallback_email is null or lower(btrim(fallback_email)) <> lower(btrim(business_email)))\n);\n\ncreate index if not exists tenant_mail_continuity_tenant_idx on public.tenant_mail_continuity(tenant_id);\ncreate index if not exists tenant_mail_continuity_mode_idx on public.tenant_mail_continuity(desired_mode, provider_forward_enabled);\n\nalter table public.tenant_mail_continuity enable row level security;\nrevoke all on public.tenant_mail_continuity from public, anon, authenticated;\ngrant select, insert, update, delete on public.tenant_mail_continuity to service_role;\n\ncreate table if not exists public.mail_continuity_health_state (\n  scope text primary key default 'platform' check (scope = 'platform'),\n  state text not null default 'healthy' check (state in ('healthy','degraded')),\n  consecutive_failures integer not null default 0 check (consecutive_failures >= 0),\n  consecutive_healthy integer not null default 0 check (consecutive_healthy >= 0),\n  last_checked_at timestamptz,\n  last_changed_at timestamptz not null default now(),\n  last_probe jsonb not null default '{}'::jsonb,\n  updated_at timestamptz not null default now()\n);\ninsert into public.mail_continuity_health_state(scope) values ('platform') on conflict (scope) do nothing;\nalter table public.mail_continuity_health_state enable row level security;\nrevoke all on public.mail_continuity_health_state from public, anon, authenticated;\ngrant select, insert, update on public.mail_continuity_health_state to service_role;\n\ncreate table if not exists private.mail_continuity_machine_auth (\n  key text primary key,\n  secret_hash text not null,\n  updated_at timestamptz not null default now()\n);\nrevoke all on private.mail_continuity_machine_auth from public, anon, authenticated;\n\ncreate or replace function public.validate_mail_continuity_machine_token(p_token text)\nreturns boolean\nlanguage sql\nsecurity definer\nset search_path=public,private,extensions\nas $$\n  select exists (\n    select 1\n    from private.mail_continuity_machine_auth\n    where key='primary'\n      and secret_hash=encode(extensions.digest(coalesce(p_token,''),'sha256'),'hex')\n  );\n$$;\nrevoke all on function public.validate_mail_continuity_machine_token(text) from public, anon, authenticated;\ngrant execute on function public.validate_mail_continuity_machine_token(text) to service_role;\n\ncreate or replace function private.seed_mail_continuity_from_email_setup()\nreturns trigger\nlanguage plpgsql\nsecurity definer\nset search_path=public,private,auth\nas $$\ndeclare\n  v_business text;\n  v_owner_id uuid;\n  v_fallback text;\n  v_confirmed timestamptz;\nbegin\n  if new.channel <> 'email' then return new; end if;\n  select lower(btrim(coalesce(new.requested_value,t.company_email,''))), t.owner_id\n    into v_business, v_owner_id\n  from public.tenants t where t.id=new.tenant_id;\n  if v_business is null or v_business='' then return new; end if;\n  select lower(btrim(u.email)), u.email_confirmed_at into v_fallback, v_confirmed\n  from auth.users u where u.id=v_owner_id;\n  insert into public.tenant_mail_continuity(\n    tenant_id,business_email,fallback_email,fallback_identity_verified_at,\n    desired_mode,effective_mode,activation_reason,mail_launched,updated_at\n  ) values (\n    new.tenant_id,v_business,\n    case when v_confirmed is not null and v_fallback is not null and v_fallback<>v_business then v_fallback else null end,\n    case when v_confirmed is not null and v_fallback is not null and v_fallback<>v_business then v_confirmed else null end,\n    'prelaunch','normal','mail_not_launched',false,now()\n  )\n  on conflict (tenant_id,business_email) do update set\n    fallback_email=case when excluded.fallback_email is not null then excluded.fallback_email else public.tenant_mail_continuity.fallback_email end,\n    fallback_identity_verified_at=case when excluded.fallback_email is not null then excluded.fallback_identity_verified_at else public.tenant_mail_continuity.fallback_identity_verified_at end,\n    updated_at=now();\n  return new;\nend;\n$$;\nrevoke all on function private.seed_mail_continuity_from_email_setup() from public, anon, authenticated;\n\ndrop trigger if exists communication_setup_mail_continuity_seed on public.communication_setup_requests;\ncreate trigger communication_setup_mail_continuity_seed\nafter insert or update of requested_value,status on public.communication_setup_requests\nfor each row execute function private.seed_mail_continuity_from_email_setup();\n\ncreate or replace function private.sync_mail_continuity_mailbox()\nreturns trigger\nlanguage plpgsql\nsecurity definer\nset search_path=public,private\nas $$\nbegin\n  insert into public.tenant_mail_continuity(\n    tenant_id,business_email,provider_account_id,mailbox_ready,mail_launched,\n    desired_mode,effective_mode,activation_reason,updated_at\n  ) values (\n    new.tenant_org_id,lower(btrim(new.email_address)),new.account_key,new.status='active',false,\n    'prelaunch','normal','mail_not_launched',now()\n  )\n  on conflict (tenant_id,business_email) do update set\n    provider_account_id=excluded.provider_account_id,\n    mailbox_ready=excluded.mailbox_ready,\n    updated_at=now();\n  return new;\nend;\n$$;\nrevoke all on function private.sync_mail_continuity_mailbox() from public, anon, authenticated;\n\ndrop trigger if exists tenant_mailbox_continuity_sync on public.tenant_mailboxes;\ncreate trigger tenant_mailbox_continuity_sync\nafter insert or update of account_key,email_address,status,provider on public.tenant_mailboxes\nfor each row execute function private.sync_mail_continuity_mailbox();\n\nwith latest_email_request as (\n  select distinct on (r.tenant_id)\n    r.tenant_id,\n    lower(btrim(coalesce(r.requested_value,t.company_email,''))) as business_email,\n    lower(btrim(u.email)) as fallback_email,\n    u.email_confirmed_at\n  from public.communication_setup_requests r\n  join public.tenants t on t.id=r.tenant_id\n  left join auth.users u on u.id=t.owner_id\n  where r.channel='email'\n    and coalesce(r.status,'requested') not in ('cancelled','rejected')\n  order by r.tenant_id,r.updated_at desc\n)\ninsert into public.tenant_mail_continuity(\n  tenant_id,business_email,fallback_email,fallback_identity_verified_at,\n  desired_mode,effective_mode,activation_reason,mail_launched,updated_at\n)\nselect tenant_id,business_email,\n       case when email_confirmed_at is not null and fallback_email<>business_email then fallback_email else null end,\n       case when email_confirmed_at is not null and fallback_email<>business_email then email_confirmed_at else null end,\n       'prelaunch','normal','mail_not_launched',false,now()\nfrom latest_email_request\nwhere nullif(business_email,'') is not null\non conflict (tenant_id,business_email) do update set\n  fallback_email=coalesce(public.tenant_mail_continuity.fallback_email,excluded.fallback_email),\n  fallback_identity_verified_at=coalesce(public.tenant_mail_continuity.fallback_identity_verified_at,excluded.fallback_identity_verified_at),\n  updated_at=now();\n\ninsert into public.tenant_mail_continuity(\n  tenant_id,business_email,provider_account_id,mailbox_ready,mail_launched,\n  desired_mode,effective_mode,activation_reason,updated_at\n)\nselect m.tenant_org_id,lower(btrim(m.email_address)),m.account_key,m.status='active',m.status='active',\n       'normal','normal','existing_mailbox_backfill',now()\nfrom public.tenant_mailboxes m\nwhere nullif(btrim(m.email_address),'') is not null\non conflict (tenant_id,business_email) do update set\n  provider_account_id=excluded.provider_account_id,\n  mailbox_ready=excluded.mailbox_ready,\n  mail_launched=true,\n  desired_mode='normal',\n  effective_mode='normal',\n  activation_reason='existing_mailbox_backfill',\n  updated_at=now();\n\nupdate public.tenant_mail_continuity c\nset fallback_email=lower(btrim(u.email)), fallback_identity_verified_at=u.email_confirmed_at, updated_at=now()\nfrom public.tenants t\njoin auth.users u on u.id=t.owner_id\nwhere c.tenant_id=t.id\n  and c.fallback_email is null\n  and u.email_confirmed_at is not null\n  and nullif(btrim(u.email),'') is not null\n  and lower(btrim(u.email))<>lower(btrim(c.business_email));\n\ndo $$\ndeclare\n  v_secret text;\nbegin\n  select decrypted_secret into v_secret from vault.decrypted_secrets where name='knocklaunch_mail_continuity_trigger' limit 1;\n  if v_secret is null then\n    v_secret := gen_random_uuid()::text || gen_random_uuid()::text;\n    perform vault.create_secret(v_secret,'knocklaunch_mail_continuity_trigger','Machine-only token for KNOCKLAUNCH Mail Continuity health reconciliation.',null);\n  end if;\n  insert into private.mail_continuity_machine_auth(key,secret_hash,updated_at)\n  values ('primary',encode(extensions.digest(v_secret,'sha256'),'hex'),now())\n  on conflict (key) do update set secret_hash=excluded.secret_hash,updated_at=now();\nend;\n$$;\n\ncreate or replace function private.mail_continuity_edge_request()\nreturns bigint\nlanguage plpgsql\nsecurity definer\nset search_path=private,vault,net\nas $$\ndeclare\n  v_token text;\n  v_request_id bigint;\nbegin\n  select decrypted_secret into v_token from vault.decrypted_secrets where name='knocklaunch_mail_continuity_trigger' limit 1;\n  if v_token is null then raise exception 'mail_continuity_trigger_secret_missing'; end if;\n  select net.http_post(\n    url:='https://ymntrajfubicyxwwdxxa.supabase.co/functions/v1/mail-continuity',\n    body:='{\"action\":\"health_tick\",\"source\":\"pg_cron\"}'::jsonb,\n    headers:=jsonb_build_object('Content-Type','application/json','x-mail-continuity-token',v_token),\n    timeout_milliseconds:=10000\n  ) into v_request_id;\n  return v_request_id;\nend;\n$$;\nrevoke all on function private.mail_continuity_edge_request() from public, anon, authenticated;\n\nselect cron.unschedule(jobid) from cron.job where jobname='knocklaunch-mail-continuity-health';\nselect cron.schedule(\n  'knocklaunch-mail-continuity-health',\n  '* * * * *',\n  $$select private.mail_continuity_edge_request();$$\n);\n"],"idempotency_key":"aaa434cd1c868878c3b6bbb0bddf4479b2a6be0d:20260915130000"},{"name":"rehome_mail_continuity_pulse","version":"20260915133000","rollback":null,"created_by":"canonical-production-release","statements":["-- Move Mail Continuity health scheduling to the existing independent\n-- cc-execution Cloudflare minute cron. The previous pg_net path is retained as\n-- an inactive compatibility helper but must not remain an authoritative pulse.\nselect cron.unschedule(jobid)\nfrom cron.job\nwhere jobname = 'knocklaunch-mail-continuity-health';\n"],"idempotency_key":"1bda8c7441fd097ff1a25f53adfce2c9ec307bf3:20260915133000"},{"name":"contact_price_overrides_and_order_requests","version":"20260915135000","rollback":null,"created_by":"canonical-production-release","statements":["alter table public.invoices\n  add column if not exists document_kind text not null default 'invoice';\n\nupdate public.invoices\nset document_kind = 'invoice'\nwhere document_kind is null;\n\nalter table public.invoices\n  alter column document_kind set default 'invoice',\n  alter column document_kind set not null;\n\ndo $$\nbegin\n  if not exists (\n    select 1 from pg_constraint\n    where conrelid = 'public.invoices'::regclass\n      and conname = 'invoices_document_kind_check'\n  ) then\n    alter table public.invoices\n      add constraint invoices_document_kind_check\n      check (document_kind in ('invoice', 'order_request'));\n  end if;\nend $$;\n\ncreate table if not exists public.contact_price_overrides (\n  id uuid primary key default gen_random_uuid(),\n  tenant_org_id text not null,\n  contact_id uuid not null references public.unified_contacts(id) on delete cascade,\n  item_key text not null,\n  item_label text not null,\n  unit_price numeric(12,2) not null check (unit_price >= 0),\n  currency text not null default 'USD',\n  active boolean not null default true,\n  notes text,\n  created_at timestamptz not null default now(),\n  updated_at timestamptz not null default now(),\n  unique (tenant_org_id, contact_id, item_key)\n);\n\ncreate index if not exists contact_price_overrides_tenant_contact_idx\n  on public.contact_price_overrides (tenant_org_id, contact_id)\n  where active = true;\n\ncreate or replace function public.contact_price_overrides_validate_tenant()\nreturns trigger\nlanguage plpgsql\nsecurity definer\nset search_path = public, pg_temp\nas $$\nbegin\n  if not exists (\n    select 1\n    from public.unified_contacts c\n    where c.id = new.contact_id\n      and c.tenant_org_id = new.tenant_org_id\n      and c.deleted_at is null\n  ) then\n    raise exception 'Contact price override tenant/contact mismatch';\n  end if;\n  new.item_key := lower(regexp_replace(trim(new.item_key), '[^a-zA-Z0-9]+', '_', 'g'));\n  new.currency := upper(coalesce(nullif(trim(new.currency), ''), 'USD'));\n  new.updated_at := now();\n  return new;\nend;\n$$;\n\ndrop trigger if exists contact_price_overrides_validate_tenant_trg on public.contact_price_overrides;\ncreate trigger contact_price_overrides_validate_tenant_trg\nbefore insert or update on public.contact_price_overrides\nfor each row execute function public.contact_price_overrides_validate_tenant();\n\nalter table public.contact_price_overrides enable row level security;\n\ndrop policy if exists contact_price_overrides_select on public.contact_price_overrides;\ncreate policy contact_price_overrides_select on public.contact_price_overrides\nfor select using (is_tenant_member(tenant_org_id) or is_system_admin());\n\ndrop policy if exists contact_price_overrides_insert on public.contact_price_overrides;\ncreate policy contact_price_overrides_insert on public.contact_price_overrides\nfor insert with check (is_tenant_member(tenant_org_id) or is_system_admin());\n\ndrop policy if exists contact_price_overrides_update on public.contact_price_overrides;\ncreate policy contact_price_overrides_update on public.contact_price_overrides\nfor update using (is_tenant_member(tenant_org_id) or is_system_admin())\nwith check (is_tenant_member(tenant_org_id) or is_system_admin());\n\ndrop policy if exists contact_price_overrides_delete on public.contact_price_overrides;\ncreate policy contact_price_overrides_delete on public.contact_price_overrides\nfor delete using (is_tenant_member(tenant_org_id) or is_system_admin());\n\ngrant select, insert, update, delete on public.contact_price_overrides to authenticated;\n\ncomment on table public.contact_price_overrides is 'Tenant-isolated per-contact pricing overrides. Blank/missing items use tenant normal pricing.';\ncomment on column public.invoices.document_kind is 'invoice = payable billing document; order_request = order initiation document with no payment due until confirmation.';\n"],"idempotency_key":"3712b1eccc93035efd76de11a6331919c9d1db10:20260915135000"},{"name":"tenant_workspace_search","version":"20260915182000","rollback":null,"created_by":"canonical-production-release","statements":["-- Canonical deterministic tenant business search. Tenant scope is enforced here,\n-- while the client passes only application IDs already resolved for the active tenant.\ncreate or replace function public.search_tenant_workspace(\n  p_tenant_id uuid,\n  p_query text,\n  p_application_ids text[] default array[]::text[],\n  p_limit integer default 28\n)\nreturns table (\n  domain text,\n  entity_type text,\n  entity_id text,\n  title text,\n  subtitle text,\n  occurred_at timestamptz,\n  score double precision,\n  app_id text,\n  metadata jsonb\n)\nlanguage plpgsql\nsecurity definer\nset search_path = public, extensions, pg_temp\nas $$\ndeclare\n  v_user_id uuid := auth.uid();\n  v_query text := lower(trim(regexp_replace(coalesce(p_query, ''), '\\s+', ' ', 'g')));\n  v_apps text[] := coalesce(p_application_ids, array[]::text[]);\n  v_limit integer := least(40, greatest(1, coalesce(p_limit, 28)));\n  v_next_work boolean := v_query = any(array['next stop','next event','next appointment','next job','what''s next','whats next','what is next']);\nbegin\n  if v_user_id is null then\n    raise exception 'Sign in to search this business.' using errcode = '42501';\n  end if;\n  if p_tenant_id is null then\n    raise exception 'Business scope is required.' using errcode = '22023';\n  end if;\n  if not coalesce(public.is_system_admin(v_user_id), false)\n     and not exists (\n       select 1\n       from public.user_tenant_access uta\n       where uta.user_id = v_user_id\n         and uta.tenant_id = p_tenant_id\n         and (coalesce(uta.has_membership, false) or lower(coalesce(uta.membership_status, '')) = 'active')\n     ) then\n    raise exception 'This business is not available to the signed-in user.' using errcode = '42501';\n  end if;\n  if length(v_query) < 2 and not v_next_work then\n    return;\n  end if;\n\n  return query\n  with raw as (\n    select\n      'people'::text as domain,\n      'contact'::text as entity_type,\n      c.id::text as entity_id,\n      coalesce(nullif(c.name, ''), nullif(trim(concat_ws(' ', c.first_name, c.last_name)), ''), 'Customer')::text as title,\n      nullif(concat_ws(' • ', nullif(c.email, ''), nullif(c.phone, ''), nullif(c.city, '')), '')::text as subtitle,\n      coalesce(c.updated_at, c.created_at) as occurred_at,\n      'contacts'::text as app_id,\n      lower(concat_ws(' ', c.name, c.first_name, c.last_name, c.email, c.phone, c.address, c.city, c.state, c.zip)) as search_text,\n      0::double precision as base_score,\n      false as next_only,\n      jsonb_build_object('contactId', c.id, 'email', c.email, 'phone', c.phone) as metadata\n    from public.unified_contacts c\n    where 'contacts' = any(v_apps)\n      and c.tenant_org_id = p_tenant_id\n      and c.deleted_at is null\n\n    union all\n\n    select\n      'sales', 'invoice', i.id::text,\n      coalesce(nullif(i.invoice_id, ''), 'Invoice ' || left(i.id::text, 8)),\n      nullif(concat_ws(' • ', nullif(i.customer_name, ''), nullif(i.status, ''), case when i.amount is not null then '$' || trim(to_char(i.amount, 'FM999999990.00')) end), ''),\n      coalesce(i.updated_at, i.created_at), 'invoices',\n      lower(concat_ws(' ', i.invoice_id, i.customer_name, i.customer_email, i.status, i.amount::text)),\n      0::double precision, false,\n      jsonb_build_object('invoiceId', i.id, 'contactId', i.contact_id, 'eventId', i.event_booking_id, 'status', i.status)\n    from public.invoices i\n    where 'invoices' = any(v_apps)\n      and i.tenant_org_id = p_tenant_id\n\n    union all\n\n    select\n      'schedule', 'event', e.id::text,\n      coalesce(nullif(e.event_name, ''), nullif(e.venue_name, ''), 'Scheduled event'),\n      nullif(concat_ws(' • ', nullif(e.venue_name, ''), nullif(e.location, ''), e.event_date::text), ''),\n      coalesce(e.start_time, e.event_date::timestamptz, e.updated_at, e.created_at),\n      case when 'calendar' = any(v_apps) then 'calendar' else 'booking' end,\n      lower(concat_ws(' ', e.event_name, e.venue_name, e.location, e.event_type, e.status, e.event_date::text)),\n      0::double precision, false,\n      jsonb_build_object('eventId', e.id, 'contactId', e.contact_id, 'eventDate', e.event_date, 'startTime', e.start_time)\n    from public.event_bookings e\n    where ('calendar' = any(v_apps) or 'booking' = any(v_apps))\n      and e.tenant_org_id = p_tenant_id\n\n    union all\n\n    select\n      'work', 'job', j.id::text,\n      coalesce(nullif(j.job_number, ''), nullif(j.customer_name, ''), 'Job'),\n      nullif(concat_ws(' • ', nullif(j.customer_name, ''), nullif(j.property_address, ''), j.scheduled_date::text, nullif(j.status, '')), ''),\n      coalesce(j.updated_at, j.created_at), 'jobs',\n      lower(concat_ws(' ', j.job_number, j.customer_name, j.customer_email, j.customer_phone, j.property_address, j.status, j.scheduled_date::text)),\n      0::double precision, false,\n      jsonb_build_object('jobId', j.id, 'contactId', j.contact_id, 'scheduledDate', j.scheduled_date, 'scheduledTime', j.scheduled_time)\n    from public.jobs j\n    where 'jobs' = any(v_apps)\n      and j.tenant_id = p_tenant_id\n\n    union all\n\n    select\n      'documents', 'document', d.id::text,\n      coalesce(nullif(d.name, ''), 'Document'),\n      nullif(concat_ws(' • ', nullif(d.category, ''), nullif(d.mime_type, '')), ''),\n      d.uploaded_at, 'documents',\n      lower(concat_ws(' ', d.name, d.category, d.mime_type, d.scope)),\n      0::double precision, false,\n      jsonb_build_object('documentId', d.id, 'eventId', d.event_booking_id, 'category', d.category)\n    from public.documents d\n    where 'documents' = any(v_apps)\n      and d.tenant_id = p_tenant_id\n\n    union all\n\n    select\n      'work', 'route_stop', s.id::text,\n      coalesce(nullif(d.address, ''), 'Route stop ' || s.stop_order::text),\n      nullif(concat_ws(' • ', nullif(d.city, ''), nullif(d.state, ''), 'Stop ' || s.stop_order::text), ''),\n      coalesce(rs.started_at, s.created_at), 'route-service-operations',\n      lower(concat_ws(' ', d.address, d.city, d.state, d.zip, s.status)),\n      (130 - least(coalesce(s.stop_order, 0), 99)::double precision / 100), true,\n      jsonb_build_object('routeSessionId', s.route_session_id, 'stopId', s.id, 'doorId', s.door_id, 'contactId', d.contact_id, 'stopOrder', s.stop_order)\n    from public.route_session_stops s\n    join public.route_sessions rs on rs.id = s.route_session_id and rs.tenant_org_id = p_tenant_id\n    left join public.door_objects d on d.id = s.door_id and d.tenant_org_id = p_tenant_id and d.deleted_at is null\n    where v_next_work\n      and 'route-service-operations' = any(v_apps)\n      and s.tenant_org_id = p_tenant_id\n      and rs.ended_at is null\n      and lower(coalesce(s.status, 'pending')) not in ('completed','skipped','cancelled','canceled')\n\n    union all\n\n    select\n      'work', 'service_visit', v.id::text,\n      coalesce(nullif(c.name, ''), 'Service stop ' || coalesce(v.stop_order, 0)::text),\n      nullif(concat_ws(' • ', v.service_date::text, 'Stop ' || coalesce(v.stop_order, 0)::text, nullif(v.status, '')), ''),\n      v.service_date::timestamptz, 'route-service-operations',\n      lower(concat_ws(' ', c.name, c.email, c.phone, v.service_date::text, v.status)),\n      (125 - least(coalesce(v.stop_order, 0), 99)::double precision / 100), true,\n      jsonb_build_object('visitId', v.id, 'routeId', v.route_id, 'contactId', v.contact_id, 'stopOrder', v.stop_order)\n    from public.lawn_service_visits v\n    left join public.unified_contacts c on c.id = v.contact_id and c.tenant_org_id = p_tenant_id and c.deleted_at is null\n    where v_next_work\n      and 'route-service-operations' = any(v_apps)\n      and v.tenant_id = p_tenant_id\n      and v.service_date >= current_date\n      and lower(coalesce(v.status, 'scheduled')) not in ('completed','cancelled','canceled','skipped')\n\n    union all\n\n    select\n      'work', 'job', j.id::text,\n      coalesce(nullif(j.job_number, ''), nullif(j.customer_name, ''), 'Upcoming job'),\n      nullif(concat_ws(' • ', nullif(j.customer_name, ''), nullif(j.property_address, ''), j.scheduled_date::text, nullif(j.scheduled_time, '')), ''),\n      j.scheduled_date::timestamptz, 'jobs',\n      lower(concat_ws(' ', j.job_number, j.customer_name, j.property_address, j.scheduled_date::text)),\n      120::double precision, true,\n      jsonb_build_object('jobId', j.id, 'contactId', j.contact_id, 'scheduledDate', j.scheduled_date, 'scheduledTime', j.scheduled_time)\n    from public.jobs j\n    where v_next_work\n      and 'jobs' = any(v_apps)\n      and j.tenant_id = p_tenant_id\n      and j.scheduled_date >= current_date\n      and lower(coalesce(j.status, 'scheduled')) not in ('completed','cancelled','canceled')\n\n    union all\n\n    select\n      'schedule', 'event', e.id::text,\n      coalesce(nullif(e.event_name, ''), nullif(e.venue_name, ''), 'Upcoming event'),\n      nullif(concat_ws(' • ', nullif(e.venue_name, ''), nullif(e.location, ''), e.event_date::text), ''),\n      coalesce(e.start_time, e.event_date::timestamptz),\n      case when 'calendar' = any(v_apps) then 'calendar' else 'booking' end,\n      lower(concat_ws(' ', e.event_name, e.venue_name, e.location, e.event_date::text)),\n      115::double precision, true,\n      jsonb_build_object('eventId', e.id, 'contactId', e.contact_id, 'eventDate', e.event_date, 'startTime', e.start_time)\n    from public.event_bookings e\n    where v_next_work\n      and ('calendar' = any(v_apps) or 'booking' = any(v_apps))\n      and e.tenant_org_id = p_tenant_id\n      and e.event_date >= current_date\n      and lower(coalesce(e.status, 'scheduled')) not in ('cancelled','canceled')\n  ),\n  scored as (\n    select\n      r.domain, r.entity_type, r.entity_id, r.title, r.subtitle, r.occurred_at, r.app_id, r.metadata,\n      case\n        when r.next_only then r.base_score\n        when lower(r.title) = v_query then 100::double precision\n        when lower(r.title) like v_query || '%' then 90::double precision\n        when lower(r.title) like '%' || v_query || '%' then 75::double precision\n        else 50::double precision + greatest(similarity(lower(r.title), v_query), similarity(r.search_text, v_query)) * 20\n      end as score\n    from raw r\n    where (v_next_work and r.next_only)\n       or (not v_next_work and not r.next_only and (\n         r.search_text like '%' || v_query || '%'\n         or similarity(r.search_text, v_query) >= 0.22\n         or similarity(lower(r.title), v_query) >= 0.28\n       ))\n  ),\n  deduped as (\n    select distinct on (s.domain, s.entity_type, s.entity_id)\n      s.domain, s.entity_type, s.entity_id, s.title, s.subtitle, s.occurred_at, s.score, s.app_id, s.metadata\n    from scored s\n    order by s.domain, s.entity_type, s.entity_id, s.score desc, s.occurred_at desc nulls last\n  ),\n  ranked as (\n    select d.*, row_number() over (partition by d.domain order by d.score desc, d.occurred_at desc nulls last, d.title, d.entity_id) as domain_rank\n    from deduped d\n  )\n  select r.domain, r.entity_type, r.entity_id, r.title, r.subtitle, r.occurred_at, r.score, r.app_id, r.metadata\n  from ranked r\n  where r.domain_rank <= 8\n  order by r.score desc, r.occurred_at desc nulls last, r.title, r.entity_id\n  limit v_limit;\nend;\n$$;\n\nrevoke all on function public.search_tenant_workspace(uuid, text, text[], integer) from public, anon;\ngrant execute on function public.search_tenant_workspace(uuid, text, text[], integer) to authenticated;\n\ncomment on function public.search_tenant_workspace(uuid, text, text[], integer)\nis 'Tenant-guarded deterministic workspace search for shared KnockLaunch business records. Application scope is supplied by the resolved tenant application contract.';\n"],"idempotency_key":"607773a773c5c7e5d88622eb69be09716e584c72:20260915182000"},{"name":"repair_deterministic_tenant_search_scope","version":"20260915191500","rollback":null,"created_by":"canonical-production-release","statements":["-- Repair the deterministic tenant workspace search contract without rewriting deployed history.\n-- Legacy tenant_org_id columns remain text in several canonical tables, while p_tenant_id is UUID.\ncreate or replace function public.search_tenant_workspace(\n  p_tenant_id uuid,\n  p_query text,\n  p_application_ids text[] default array[]::text[],\n  p_limit integer default 28\n)\nreturns table (\n  domain text,\n  entity_type text,\n  entity_id text,\n  title text,\n  subtitle text,\n  occurred_at timestamptz,\n  score double precision,\n  app_id text,\n  metadata jsonb\n)\nlanguage plpgsql\nas $$\ndeclare\n  v_user_id uuid := auth.uid();\n  v_query text := lower(trim(regexp_replace(coalesce(p_query, ''), '\\s+', ' ', 'g')));\n  v_apps text[] := coalesce(p_application_ids, array[]::text[]);\n  v_limit integer := least(40, greatest(1, coalesce(p_limit, 28)));\n  v_next_work boolean := v_query = any(array['next stop','next event','next appointment','next job','what''s next','whats next','what is next']);\nbegin\n  if v_user_id is null then\n    raise exception 'Sign in to search this business.' using errcode = '42501';\n  end if;\n  if p_tenant_id is null then\n    raise exception 'Business scope is required.' using errcode = '22023';\n  end if;\n  if not coalesce(public.is_system_admin(v_user_id), false)\n     and not exists (\n       select 1\n       from public.user_tenant_access uta\n       where uta.user_id = v_user_id\n         and uta.tenant_id = p_tenant_id\n         and (coalesce(uta.has_membership, false) or lower(coalesce(uta.membership_status, '')) = 'active')\n     ) then\n    raise exception 'This business is not available to the signed-in user.' using errcode = '42501';\n  end if;\n  if length(v_query) < 2 and not v_next_work then\n    return;\n  end if;\n\n  return query\n  with raw as (\n    select\n      'people'::text as domain,\n      'contact'::text as entity_type,\n      c.id::text as entity_id,\n      coalesce(nullif(c.name, ''), nullif(trim(concat_ws(' ', c.first_name, c.last_name)), ''), 'Customer')::text as title,\n      nullif(concat_ws(' • ', nullif(c.email, ''), nullif(c.phone, ''), nullif(c.city, '')), '')::text as subtitle,\n      coalesce(c.updated_at, c.created_at) as occurred_at,\n      'contacts'::text as app_id,\n      lower(concat_ws(' ', c.name, c.first_name, c.last_name, c.email, c.phone, c.address, c.city, c.state, c.zip)) as search_text,\n      0::double precision as base_score,\n      false as next_only,\n      jsonb_build_object('contactId', c.id, 'email', c.email, 'phone', c.phone) as metadata\n    from public.unified_contacts c\n    where 'contacts' = any(v_apps)\n      and c.tenant_org_id = p_tenant_id::text\n      and c.deleted_at is null\n\n    union all\n\n    select\n      'sales', 'invoice', i.id::text,\n      coalesce(nullif(i.invoice_id, ''), 'Invoice ' || left(i.id::text, 8)),\n      nullif(concat_ws(' • ', nullif(i.customer_name, ''), nullif(i.status, ''), case when i.amount is not null then '$' || trim(to_char(i.amount, 'FM999999990.00')) end), ''),\n      coalesce(i.updated_at, i.created_at), 'invoices',\n      lower(concat_ws(' ', i.invoice_id, i.customer_name, i.customer_email, i.status, i.amount::text)),\n      0::double precision, false,\n      jsonb_build_object('invoiceId', i.id, 'contactId', i.contact_id, 'eventId', i.event_booking_id, 'status', i.status)\n    from public.invoices i\n    where 'invoices' = any(v_apps)\n      and i.tenant_org_id = p_tenant_id::text\n\n    union all\n\n    select\n      'schedule', 'event', e.id::text,\n      coalesce(nullif(e.event_name, ''), nullif(e.venue_name, ''), 'Scheduled event'),\n      nullif(concat_ws(' • ', nullif(e.venue_name, ''), nullif(e.location, ''), e.event_date::text), ''),\n      coalesce(e.start_time, e.event_date::timestamptz, e.updated_at, e.created_at),\n      case when 'calendar' = any(v_apps) then 'calendar' else 'booking' end,\n      lower(concat_ws(' ', e.event_name, e.venue_name, e.location, e.event_type, e.status, e.event_date::text)),\n      0::double precision, false,\n      jsonb_build_object('eventId', e.id, 'contactId', e.contact_id, 'eventDate', e.event_date, 'startTime', e.start_time)\n    from public.event_bookings e\n    where ('calendar' = any(v_apps) or 'booking' = any(v_apps))\n      and e.tenant_org_id = p_tenant_id::text\n\n    union all\n\n    select\n      'work', 'job', j.id::text,\n      coalesce(nullif(j.job_number, ''), nullif(j.customer_name, ''), 'Job'),\n      nullif(concat_ws(' • ', nullif(j.customer_name, ''), nullif(j.property_address, ''), j.scheduled_date::text, nullif(j.status, '')), ''),\n      coalesce(j.updated_at, j.created_at), 'jobs',\n      lower(concat_ws(' ', j.job_number, j.customer_name, j.customer_email, j.customer_phone, j.property_address, j.status, j.scheduled_date::text)),\n      0::double precision, false,\n      jsonb_build_object('jobId', j.id, 'contactId', j.contact_id, 'scheduledDate', j.scheduled_date, 'scheduledTime', j.scheduled_time)\n    from public.jobs j\n    where 'jobs' = any(v_apps)\n      and j.tenant_id = p_tenant_id\n\n    union all\n\n    select\n      'documents', 'document', d.id::text,\n      coalesce(nullif(d.name, ''), 'Document'),\n      nullif(concat_ws(' • ', nullif(d.category, ''), nullif(d.mime_type, '')), ''),\n      d.uploaded_at, 'documents',\n      lower(concat_ws(' ', d.name, d.category, d.mime_type, d.scope)),\n      0::double precision, false,\n      jsonb_build_object('documentId', d.id, 'eventId', d.event_booking_id, 'category', d.category)\n    from public.documents d\n    where 'documents' = any(v_apps)\n      and d.tenant_id = p_tenant_id\n\n    union all\n\n    select\n      'work', 'route_stop', s.id::text,\n      coalesce(nullif(d.address, ''), 'Route stop ' || s.stop_order::text),\n      nullif(concat_ws(' • ', nullif(d.city, ''), nullif(d.state, ''), 'Stop ' || s.stop_order::text), ''),\n      coalesce(rs.started_at, s.created_at), 'route-service-operations',\n      lower(concat_ws(' ', d.address, d.city, d.state, d.zip, s.status)),\n      (130 - least(coalesce(s.stop_order, 0), 99)::double precision / 100), true,\n      jsonb_build_object('routeSessionId', s.route_session_id, 'stopId', s.id, 'doorId', s.door_id, 'contactId', d.contact_id, 'stopOrder', s.stop_order)\n    from public.route_session_stops s\n    join public.route_sessions rs on rs.id = s.route_session_id and rs.tenant_org_id = p_tenant_id\n    left join public.door_objects d on d.id = s.door_id and d.tenant_org_id = p_tenant_id::text and d.deleted_at is null\n    where v_next_work\n      and 'route-service-operations' = any(v_apps)\n      and s.tenant_org_id = p_tenant_id\n      and rs.ended_at is null\n      and lower(coalesce(s.status, 'pending')) not in ('completed','skipped','cancelled','canceled')\n\n    union all\n\n    select\n      'work', 'service_visit', v.id::text,\n      coalesce(nullif(c.name, ''), 'Service stop ' || coalesce(v.stop_order, 0)::text),\n      nullif(concat_ws(' • ', v.service_date::text, 'Stop ' || coalesce(v.stop_order, 0)::text, nullif(v.status, '')), ''),\n      v.service_date::timestamptz, 'route-service-operations',\n      lower(concat_ws(' ', c.name, c.email, c.phone, v.service_date::text, v.status)),\n      (125 - least(coalesce(v.stop_order, 0), 99)::double precision / 100), true,\n      jsonb_build_object('visitId', v.id, 'routeId', v.route_id, 'contactId', v.contact_id, 'stopOrder', v.stop_order)\n    from public.lawn_service_visits v\n    left join public.unified_contacts c on c.id = v.contact_id and c.tenant_org_id = p_tenant_id::text and c.deleted_at is null\n    where v_next_work\n      and 'route-service-operations' = any(v_apps)\n      and v.tenant_id = p_tenant_id\n      and v.service_date >= current_date\n      and lower(coalesce(v.status, 'scheduled')) not in ('completed','cancelled','canceled','skipped')\n\n    union all\n\n    select\n      'work', 'job', j.id::text,\n      coalesce(nullif(j.job_number, ''), nullif(j.customer_name, ''), 'Upcoming job'),\n      nullif(concat_ws(' • ', nullif(j.customer_name, ''), nullif(j.property_address, ''), j.scheduled_date::text, nullif(j.scheduled_time, '')), ''),\n      j.scheduled_date::timestamptz, 'jobs',\n      lower(concat_ws(' ', j.job_number, j.customer_name, j.property_address, j.scheduled_date::text)),\n      120::double precision, true,\n      jsonb_build_object('jobId', j.id, 'contactId', j.contact_id, 'scheduledDate', j.scheduled_date, 'scheduledTime', j.scheduled_time)\n    from public.jobs j\n    where v_next_work\n      and 'jobs' = any(v_apps)\n      and j.tenant_id = p_tenant_id\n      and j.scheduled_date >= current_date\n      and lower(coalesce(j.status, 'scheduled')) not in ('completed','cancelled','canceled')\n\n    union all\n\n    select\n      'schedule', 'event', e.id::text,\n      coalesce(nullif(e.event_name, ''), nullif(e.venue_name, ''), 'Upcoming event'),\n      nullif(concat_ws(' • ', nullif(e.venue_name, ''), nullif(e.location, ''), e.event_date::text), ''),\n      coalesce(e.start_time, e.event_date::timestamptz),\n      case when 'calendar' = any(v_apps) then 'calendar' else 'booking' end,\n      lower(concat_ws(' ', e.event_name, e.venue_name, e.location, e.event_date::text)),\n      115::double precision, true,\n      jsonb_build_object('eventId', e.id, 'contactId', e.contact_id, 'eventDate', e.event_date, 'startTime', e.start_time)\n    from public.event_bookings e\n    where v_next_work\n      and ('calendar' = any(v_apps) or 'booking' = any(v_apps))\n      and e.tenant_org_id = p_tenant_id::text\n      and e.event_date >= current_date\n      and lower(coalesce(e.status, 'scheduled')) not in ('cancelled','canceled')\n  ),\n  scored as (\n    select\n      r.domain, r.entity_type, r.entity_id, r.title, r.subtitle, r.occurred_at, r.app_id, r.metadata,\n      case\n        when r.next_only then r.base_score\n        when lower(r.title) = v_query then 100::double precision\n        when lower(r.title) like v_query || '%' then 90::double precision\n        when lower(r.title) like '%' || v_query || '%' then 75::double precision\n        else 50::double precision + greatest(similarity(lower(r.title), v_query), similarity(r.search_text, v_query)) * 20\n      end as score\n    from raw r\n    where (v_next_work and r.next_only)\n       or (not v_next_work and not r.next_only and (\n         r.search_text like '%' || v_query || '%'\n         or similarity(r.search_text, v_query) >= 0.22\n         or similarity(lower(r.title), v_query) >= 0.28\n       ))\n  ),\n  deduped as (\n    select distinct on (s.domain, s.entity_type, s.entity_id)\n      s.domain, s.entity_type, s.entity_id, s.title, s.subtitle, s.occurred_at, s.score, s.app_id, s.metadata\n    from scored s\n    order by s.domain, s.entity_type, s.entity_id, s.score desc, s.occurred_at desc nulls last\n  ),\n  ranked as (\n    select d.*, row_number() over (partition by d.domain order by d.score desc, d.occurred_at desc nulls last, d.title, d.entity_id) as domain_rank\n    from deduped d\n  )\n  select r.domain, r.entity_type, r.entity_id, r.title, r.subtitle, r.occurred_at, r.score, r.app_id, r.metadata\n  from ranked r\n  where r.domain_rank <= 8\n  order by r.score desc, r.occurred_at desc nulls last, r.title, r.entity_id\n  limit v_limit;\nend;\n$$;\n"],"idempotency_key":"9fe8166588082b6a8e7166ca9d0dfdb40d91ef1e:20260915191500"},{"name":"platform_application_registry_release_recovery","version":"20260916000000","rollback":null,"created_by":"canonical-production-release","statements":["-- Corrective release generation for application-registry.v2.\n-- This migration is intentionally idempotent and scoped: Call Center becomes opt-in,\n-- Big Papi's stale historical assignment is removed, and other tenant assignments remain untouched.\n\nupdate public.application_catalog\nset default_installed = false,\n    config_contract = coalesce(config_contract, '{}'::jsonb) || jsonb_build_object(\n      'registry_contract', 'application-registry.v2',\n      'registry_authority', 'source-product-catalog',\n      'projection_role', 'bootstrap-metadata',\n      'availability_policy', 'assigned',\n      'lifecycle_status', 'active'\n    ),\n    updated_at = now()\nwhere id = 'call-center';\n\ndo $$\ndeclare\n  v_tenant_id uuid;\n  v_settings jsonb;\nbegin\n  select id, coalesce(settings, '{}'::jsonb)\n    into v_tenant_id, v_settings\n  from public.tenants\n  where company_slug = 'big-papis'\n  limit 1\n  for update;\n\n  if v_tenant_id is null then\n    return;\n  end if;\n\n  v_settings := jsonb_set(v_settings, '{enabled_applications}', coalesce(v_settings->'enabled_applications', '[]'::jsonb) - 'call-center', true);\n  v_settings := jsonb_set(v_settings, '{tenant_manifest,applications}', coalesce(v_settings#>'{tenant_manifest,applications}', '[]'::jsonb) - 'call-center', true);\n  v_settings := jsonb_set(v_settings, '{tenant_manifest,businessOperatingBlueprint,applications}', coalesce(v_settings#>'{tenant_manifest,businessOperatingBlueprint,applications}', '[]'::jsonb) - 'call-center', true);\n  v_settings := jsonb_set(v_settings, '{onboarding_contract,applications}', coalesce(v_settings#>'{onboarding_contract,applications}', '[]'::jsonb) - 'call-center', true);\n  v_settings := jsonb_set(v_settings, '{onboarding_contract,business_operating_blueprint,applications}', coalesce(v_settings#>'{onboarding_contract,business_operating_blueprint,applications}', '[]'::jsonb) - 'call-center', true);\n  v_settings := jsonb_set(v_settings, '{business_operating_blueprint,applications}', coalesce(v_settings#>'{business_operating_blueprint,applications}', '[]'::jsonb) - 'call-center', true);\n\n  update public.tenants\n  set settings = v_settings,\n      updated_at = now()\n  where id = v_tenant_id;\n\n  update public.tenant_applications\n  set enabled = false,\n      settings = coalesce(settings, '{}'::jsonb) || jsonb_build_object(\n        'disabled_by', 'application-registry.v2',\n        'availability_policy', 'assigned'\n      ),\n      updated_at = now()\n  where tenant_id = v_tenant_id\n    and application_id = 'call-center';\n\n  if (coalesce(v_settings->'enabled_applications', '[]'::jsonb) ? 'call-center')\n     or (coalesce(v_settings#>'{tenant_manifest,applications}', '[]'::jsonb) ? 'call-center')\n     or (coalesce(v_settings#>'{tenant_manifest,businessOperatingBlueprint,applications}', '[]'::jsonb) ? 'call-center')\n     or (coalesce(v_settings#>'{onboarding_contract,applications}', '[]'::jsonb) ? 'call-center')\n     or (coalesce(v_settings#>'{onboarding_contract,business_operating_blueprint,applications}', '[]'::jsonb) ? 'call-center')\n     or (coalesce(v_settings#>'{business_operating_blueprint,applications}', '[]'::jsonb) ? 'call-center') then\n    raise exception 'big_papis_call_center_projection_reconciliation_failed';\n  end if;\nend $$;\n\ncreate or replace function public.bootstrap_call_center_tenant(target_tenant uuid)\nreturns void\nlanguage plpgsql\nsecurity definer\nset search_path to 'public'\nas $function$\ndeclare\n  workflow_uuid uuid;\n  v_apps jsonb;\nbegin\n  insert into public.call_center_workflows (tenant_id,name,active,is_default)\n  values (target_tenant,'Guided Call Workflow',true,true)\n  on conflict (tenant_id,name) do update set active=true,is_default=true,updated_at=now()\n  returning id into workflow_uuid;\n\n  insert into public.call_center_stages (tenant_id,workflow_id,stage_key,label,prompt,sort_order)\n  select target_tenant,workflow_uuid,s.stage_key,s.label,s.prompt,s.sort_order\n  from (values\n    ('open','Open','Introduce yourself, confirm who you reached, and earn permission to continue.',10),\n    ('discover','Discover','Ask focused questions and listen for the customer''s real need, timing, and constraints.',20),\n    ('help','Help','Connect what you learned to the clearest next step your company can actually provide.',30),\n    ('commit','Commit','Ask for the next action: appointment, transfer, follow-up, payment, or another concrete commitment.',40),\n    ('close','Close','Confirm the details, set expectations, document the outcome, and leave the customer knowing what happens next.',50)\n  ) as s(stage_key,label,prompt,sort_order)\n  on conflict (workflow_id,stage_key) do update set label=excluded.label,prompt=excluded.prompt,sort_order=excluded.sort_order,active=true,updated_at=now();\n\n  insert into public.call_center_dispositions (tenant_id,key,label,requires_follow_up,sort_order)\n  select target_tenant,d.key,d.label,d.requires_follow_up,d.sort_order\n  from (values\n    ('appointment_set','Appointment set',false,10),\n    ('callback','Callback requested',true,20),\n    ('no_answer','No answer',true,30),\n    ('voicemail','Left voicemail',true,40),\n    ('not_interested','Not interested',false,50),\n    ('wrong_number','Wrong number',false,60),\n    ('do_not_call','Do not call',false,70)\n  ) as d(key,label,requires_follow_up,sort_order)\n  on conflict (tenant_id,key) do update set label=excluded.label,requires_follow_up=excluded.requires_follow_up,sort_order=excluded.sort_order,active=true,updated_at=now();\n\n  perform public.reconcile_tenant_business_blueprint(target_tenant, false);\n\n  select public.merge_tenant_application_ids(\n      coalesce(settings#>'{business_operating_blueprint,applications}', '[]'::jsonb),\n      jsonb_build_array('call-center')\n    )\n    into v_apps\n  from public.tenants\n  where id = target_tenant\n  for update;\n\n  if v_apps is null then\n    raise exception 'call_center_tenant_blueprint_unavailable';\n  end if;\n\n  update public.tenants\n  set settings = jsonb_set(coalesce(settings, '{}'::jsonb), '{business_operating_blueprint,applications}', v_apps, true),\n      updated_at = now()\n  where id = target_tenant;\n\n  perform public.reconcile_tenant_business_blueprint(target_tenant, false);\n\n  update public.tenant_applications\n  set enabled = true,\n      settings = coalesce(settings, '{}'::jsonb) || jsonb_build_object(\n        'assignment_source', 'explicit-call-center-bootstrap',\n        'availability_policy', 'assigned'\n      ),\n      updated_at = now()\n  where tenant_id = target_tenant\n    and application_id = 'call-center';\nend;\n$function$;\n"],"idempotency_key":"e26fee174b0b3c2a7f49e25aa72bccac89e26f1a:20260916000000"}],"rows":86,"limit":200,"table":"schema_migrations","format":"knocklaunch-table-chunk-v1","offset":200,"schema":"supabase_migrations"}