Voltar ao Painel

    Guia de Configuração Supabase

    ✅ CÓDIGO 100% BLINDADO CONTRA ERRO DE POLÍTICA DUPLICADA:Todas as políticas agora usam o bloco de segurança DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_policies ...) THEN CREATE POLICY ... END IF; END $$;. Isso significa que você pode rodar 1, 10 ou 100 vezes a consulta no Supabase que nunca mais dará erro 42710 (policy already exists)!👉 Clique no botão verde "Copiar Código Limpo" em cada parte. O código puro vai para o seu Ctrl+V.

    Guia Completo MSK Uniformes — Passo a Passo

    Tudo que você precisa para o sistema ficar 100% funcional. Os códigos estão completos abaixo — é só copiar e colar.


    Índice

    1. Criar conta no Supabase
    2. Criar o Banco de Dados (SQL completo abaixo — 3 partes)
    3. Criar os Buckets de Armazenamento (Storage)
    4. Ativar o Realtime (tempo real do chat)
    5. Fazer o Deploy das Edge Functions (códigos completos abaixo)
    6. Configurar as Secrets
    7. Criar o App no Mercado Livre
    8. Conectar o Webhook do Mercado Livre
    9. Automações e Notificações WhatsApp
    10. Checklist Final

    1. Criar Conta no Supabase

    Se você já tem o projeto, pule para o passo 2.

    1. Acesse https://supabase.com → clique em Start your project
    2. Faça login com GitHub ou email
    3. Clique em New Project
    4. Preencha: Name: MSK Uniformes | Database Password: crie uma senha forte e ANOTE | Region: South America (São Paulo)
    5. Clique em Create new project e aguarde ~2 minutos
    Importante: O projeto já está conectado ao Supabase. Se criar um projeto NOVO, avise para atualizar o arquivo src/lib/supabase.ts com o novo URL e a nova chave.

    2. Criar o Banco de Dados (SQL)

    No painel do Supabase → SQL EditorNew query → cole as 3 partes abaixo (uma de cada vez ou todas juntas) → clique RUN.

    SQL — Parte 1 de 3 (cole no SQL Editor)
    -- ==========================================
    -- SCHEMA PROFISSIONAL SUPABASE - MSK UNIFORMES
    -- ==========================================
    -- Execute este código completo no SQL Editor do Supabase
    
    CREATE TABLE IF NOT EXISTS msk_produtos_estoque (
      id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
      nome TEXT NOT NULL,
      sku TEXT UNIQUE,
      categoria TEXT,
      tamanhos TEXT,
      cor TEXT,
      tecido TEXT,
      quantidade INTEGER DEFAULT 0,
      preco DECIMAL(10,2) DEFAULT 0,
      preco_custo DECIMAL(10,2) DEFAULT 0,
      loja TEXT DEFAULT 'Fábrica',
      imagem_url TEXT,
      descricao TEXT,
      is_kit BOOLEAN DEFAULT false,
      kit_quantidade INTEGER DEFAULT 1,
      med_comprimento DECIMAL(8,1),
      med_cintura DECIMAL(8,1),
      med_quadril DECIMAL(8,1),
      med_colarinho DECIMAL(8,1),
      med_manga DECIMAL(8,1),
      peso_kit DECIMAL(8,1),
      tipo_tecido TEXT,
      composicao_tecido TEXT,
      created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
      updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()
    );
    
    ALTER TABLE msk_produtos_estoque ADD COLUMN IF NOT EXISTS descricao TEXT;
    ALTER TABLE msk_produtos_estoque ADD COLUMN IF NOT EXISTS is_kit BOOLEAN DEFAULT false;
    ALTER TABLE msk_produtos_estoque ADD COLUMN IF NOT EXISTS kit_quantidade INTEGER DEFAULT 1;
    ALTER TABLE msk_produtos_estoque ADD COLUMN IF NOT EXISTS med_comprimento DECIMAL(8,1);
    ALTER TABLE msk_produtos_estoque ADD COLUMN IF NOT EXISTS med_cintura DECIMAL(8,1);
    ALTER TABLE msk_produtos_estoque ADD COLUMN IF NOT EXISTS med_quadril DECIMAL(8,1);
    ALTER TABLE msk_produtos_estoque ADD COLUMN IF NOT EXISTS med_colarinho DECIMAL(8,1);
    ALTER TABLE msk_produtos_estoque ADD COLUMN IF NOT EXISTS med_manga DECIMAL(8,1);
    ALTER TABLE msk_produtos_estoque ADD COLUMN IF NOT EXISTS peso_kit DECIMAL(8,1);
    ALTER TABLE msk_produtos_estoque ADD COLUMN IF NOT EXISTS tipo_tecido TEXT;
    ALTER TABLE msk_produtos_estoque ADD COLUMN IF NOT EXISTS composicao_tecido TEXT;
    
    CREATE TABLE IF NOT EXISTS msk_estoque_movimentacoes (
      id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
      produto_id UUID REFERENCES msk_produtos_estoque(id) ON DELETE CASCADE,
      tipo TEXT NOT NULL,
      quantidade INTEGER NOT NULL,
      motivo TEXT,
      loja TEXT,
      responsavel TEXT,
      created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
    );
    
    CREATE TABLE IF NOT EXISTS msk_transferencias_estoque (
      id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
      produto_id UUID REFERENCES msk_produtos_estoque(id),
      sku TEXT,
      nome_produto TEXT,
      origem TEXT,
      destino TEXT,
      quantidade INTEGER,
      status TEXT DEFAULT 'pendente',
      solicitante TEXT,
      recebido_por TEXT,
      recebido_em TIMESTAMP WITH TIME ZONE,
      created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
    );
    
    CREATE TABLE IF NOT EXISTS msk_notificacoes_admin (
      id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
      titulo TEXT,
      mensagem TEXT,
      loja_destino TEXT,
      tipo TEXT,
      lida BOOLEAN DEFAULT false,
      link TEXT,
      created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
    );
    
    ALTER TABLE msk_produtos_estoque ENABLE ROW LEVEL SECURITY;
    DO $$ BEGIN
      IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_produtos_estoque' AND policyname = 'allow_all_msk_produtos_estoque') THEN
        CREATE POLICY "allow_all_msk_produtos_estoque" ON msk_produtos_estoque FOR ALL USING (true);
      END IF;
    END $$;
    
    ALTER TABLE msk_estoque_movimentacoes ENABLE ROW LEVEL SECURITY;
    DO $$ BEGIN
      IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_estoque_movimentacoes' AND policyname = 'allow_all_msk_estoque_movimentacoes') THEN
        CREATE POLICY "allow_all_msk_estoque_movimentacoes" ON msk_estoque_movimentacoes FOR ALL USING (true);
      END IF;
    END $$;
    
    ALTER TABLE msk_transferencias_estoque ENABLE ROW LEVEL SECURITY;
    DO $$ BEGIN
      IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_transferencias_estoque' AND policyname = 'allow_all_msk_transferencias') THEN
        CREATE POLICY "allow_all_msk_transferencias" ON msk_transferencias_estoque FOR ALL USING (true);
      END IF;
    END $$;
    
    ALTER TABLE msk_notificacoes_admin ENABLE ROW LEVEL SECURITY;
    DO $$ BEGIN
      IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_notificacoes_admin' AND policyname = 'allow_all_msk_notificacoes') THEN
        CREATE POLICY "allow_all_msk_notificacoes" ON msk_notificacoes_admin FOR ALL USING (true);
      END IF;
    END $$;
    
    CREATE TABLE IF NOT EXISTS msk_conferencia_fotografica (
      id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
      op_id UUID,
      op_numero TEXT,
      produto_nome TEXT,
      quantidade_esperada INTEGER DEFAULT 0,
      quantidade_conferida INTEGER DEFAULT 0,
      conferente TEXT,
      fotos JSONB DEFAULT '[]',
      status TEXT DEFAULT 'pendente',
      observacao TEXT,
      created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
    );
    
    ALTER TABLE msk_conferencia_fotografica ENABLE ROW LEVEL SECURITY;
    DROP POLICY IF EXISTS "allow_all_msk_conferencia" ON msk_conferencia_fotografica;
    CREATE POLICY "allow_all_msk_conferencia" ON msk_conferencia_fotografica FOR ALL USING (true);
    
    CREATE TABLE IF NOT EXISTS categorias (
      id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
      nome TEXT NOT NULL UNIQUE,
      descricao TEXT,
      imagem_url TEXT
    );
    
    CREATE TABLE IF NOT EXISTS parceiros (
      id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
      nome TEXT NOT NULL,
      tipo TEXT NOT NULL,
      especialidade TEXT,
      contato TEXT,
      cidade TEXT,
      cnpj TEXT,
      avaliacao DECIMAL DEFAULT 5.0,
      ativo BOOLEAN DEFAULT true,
      valor_servico DECIMAL(10,2) DEFAULT 0,
      forma_pagamento TEXT DEFAULT 'Pix',
      prazo_entrega TEXT,
      observacao TEXT,
      created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now())
    );
    
    ALTER TABLE parceiros ADD COLUMN IF NOT EXISTS especialidade TEXT;
    ALTER TABLE parceiros ADD COLUMN IF NOT EXISTS cnpj TEXT;
    ALTER TABLE parceiros ADD COLUMN IF NOT EXISTS valor_servico DECIMAL(10,2) DEFAULT 0;
    ALTER TABLE parceiros ADD COLUMN IF NOT EXISTS forma_pagamento TEXT DEFAULT 'Pix';
    ALTER TABLE parceiros ADD COLUMN IF NOT EXISTS prazo_entrega TEXT;
    ALTER TABLE parceiros ADD COLUMN IF NOT EXISTS observacao TEXT;
    
    CREATE TABLE IF NOT EXISTS produtos (
      id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
      sku TEXT UNIQUE NOT NULL,
      nome TEXT NOT NULL,
      descricao TEXT,
      categoria_id UUID REFERENCES categorias(id),
      tipo_peca TEXT,
      material TEXT,
      preco_venda DECIMAL(10,2),
      preco_custo DECIMAL(10,2),
      tamanhos_disponiveis JSONB,
      imagens JSONB,
      qr_video_url TEXT,
      fornecedor_id UUID REFERENCES parceiros(id),
      created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now())
    );
    
    CREATE TABLE IF NOT EXISTS estoque (
      id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
      produto_id UUID REFERENCES produtos(id) ON DELETE CASCADE,
      tamanho TEXT NOT NULL,
      localizacao TEXT NOT NULL,
      quantidade INTEGER DEFAULT 0,
      ultima_atualizacao TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()),
      UNIQUE(produto_id, tamanho, localizacao)
    );
    
    CREATE TABLE IF NOT EXISTS escolas (
      id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
      nome TEXT NOT NULL,
      slug TEXT UNIQUE NOT NULL,
      logo_url TEXT,
      cor_primaria TEXT,
      percentual_royalties DECIMAL(5,2) DEFAULT 10.00,
      email_financeiro TEXT,
      created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now())
    );
    
    CREATE TABLE IF NOT EXISTS produtos_escola (
      escola_id UUID REFERENCES escolas(id) ON DELETE CASCADE,
      produto_id UUID REFERENCES produtos(id) ON DELETE CASCADE,
      preco_especial DECIMAL(10,2),
      PRIMARY KEY (escola_id, produto_id)
    );
    
    CREATE TABLE IF NOT EXISTS ordens_producao (
      id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
      numero_op SERIAL,
      produto_id UUID REFERENCES produtos(id),
      produto_nome TEXT,
      cliente TEXT,
      prioridade TEXT DEFAULT 'Normal',
      responsavel TEXT,
      quantidade INTEGER NOT NULL,
      tamanho TEXT,
      status TEXT DEFAULT 'CORTE',
      parceiro_id UUID REFERENCES parceiros(id),
      data_inicio TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()),
      data_previsao TIMESTAMP WITH TIME ZONE,
      data_conclusao TIMESTAMP WITH TIME ZONE,
      observacoes TEXT,
      anexos TEXT,
      comentarios JSONB DEFAULT '[]'::jsonb
    );
    
    CREATE TABLE IF NOT EXISTS avaliacoes_biometricas (
      id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
      nome_cliente TEXT NOT NULL,
      idade INTEGER,
      altura DECIMAL,
      peso DECIMAL,
      produto_id UUID REFERENCES produtos(id),
      tamanho_recomendado TEXT,
      created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now())
    );
    
    CREATE TABLE IF NOT EXISTS transacoes_financeiras (
      id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
      escola_id UUID REFERENCES escolas(id),
      produto_id UUID REFERENCES produtos(id),
      valor_total DECIMAL(10,2) NOT NULL,
      valor_repasse DECIMAL(10,2) NOT NULL,
      metodo_pagamento TEXT,
      status_repasse TEXT DEFAULT 'AGUARDANDO',
      data_venda TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now())
    );
    
    ALTER TABLE categorias ENABLE ROW LEVEL SECURITY;
    ALTER TABLE parceiros ENABLE ROW LEVEL SECURITY;
    ALTER TABLE produtos ENABLE ROW LEVEL SECURITY;
    ALTER TABLE estoque ENABLE ROW LEVEL SECURITY;
    ALTER TABLE escolas ENABLE ROW LEVEL SECURITY;
    ALTER TABLE produtos_escola ENABLE ROW LEVEL SECURITY;
    ALTER TABLE ordens_producao ENABLE ROW LEVEL SECURITY;
    ALTER TABLE avaliacoes_biometricas ENABLE ROW LEVEL SECURITY;
    ALTER TABLE transacoes_financeiras ENABLE ROW LEVEL SECURITY;
    
    DO $$ BEGIN
      IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'categorias' AND policyname = 'Leitura publica de categorias') THEN
        CREATE POLICY "Leitura publica de categorias" ON categorias FOR SELECT USING (true);
      END IF;
      IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'produtos' AND policyname = 'Leitura publica de produtos') THEN
        CREATE POLICY "Leitura publica de produtos" ON produtos FOR SELECT USING (true);
      END IF;
      IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'estoque' AND policyname = 'Leitura publica de estoque') THEN
        CREATE POLICY "Leitura publica de estoque" ON estoque FOR SELECT USING (true);
      END IF;
      IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'escolas' AND policyname = 'Leitura publica de escolas') THEN
        CREATE POLICY "Leitura publica de escolas" ON escolas FOR SELECT USING (true);
      END IF;
      IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'produtos_escola' AND policyname = 'Leitura publica de produtos_escola') THEN
        CREATE POLICY "Leitura publica de produtos_escola" ON produtos_escola FOR SELECT USING (true);
      END IF;
      IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'avaliacoes_biometricas' AND policyname = 'Insercao publica no provador') THEN
        CREATE POLICY "Insercao publica no provador" ON avaliacoes_biometricas FOR INSERT WITH CHECK (true);
      END IF;
      IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'parceiros' AND policyname = 'Acesso total admin parceiros') THEN
        CREATE POLICY "Acesso total admin parceiros" ON parceiros FOR ALL USING (true);
      END IF;
      IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'ordens_producao' AND policyname = 'Acesso total admin OPs') THEN
        CREATE POLICY "Acesso total admin OPs" ON ordens_producao FOR ALL USING (true);
      END IF;
      IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'transacoes_financeiras' AND policyname = 'Acesso total admin financeiro') THEN
        CREATE POLICY "Acesso total admin financeiro" ON transacoes_financeiras FOR ALL USING (true);
      END IF;
      IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'produtos' AND policyname = 'Acesso total admin produtos') THEN
        CREATE POLICY "Acesso total admin produtos" ON produtos FOR ALL USING (true);
      END IF;
    END $$;
    SQL — Parte 2 de 3
    -- 10. CENTRO DE CUSTOS
    CREATE TABLE IF NOT EXISTS msk_centro_custos (
      id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
      data DATE NOT NULL,
      descricao TEXT NOT NULL,
      categoria TEXT NOT NULL,
      subcategoria TEXT,
      forma_pagamento TEXT NOT NULL,
      valor NUMERIC(12,2) NOT NULL,
      responsavel TEXT NOT NULL,
      loja TEXT DEFAULT 'Santos',
      fornecedor TEXT,
      nota_fiscal TEXT,
      observacao TEXT,
      aprovado_por TEXT,
      status TEXT DEFAULT 'pendente',
      comprovante_url TEXT,
      created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
      updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()
    );
    
    -- 11. PRECIFICACAO DE PRODUCAO
    CREATE TABLE IF NOT EXISTS msk_precificacao (
      id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
      nome_produto TEXT NOT NULL,
      tipo_peca TEXT NOT NULL,
      segmento TEXT NOT NULL,
      unidade TEXT DEFAULT 'Santos',
      tecido_nome TEXT,
      tecido_gramatura NUMERIC(6,2),
      tecido_consumo_metros NUMERIC(6,3),
      tecido_preco_metro NUMERIC(10,2),
      aviamentos_valor NUMERIC(10,2) DEFAULT 0,
      embalagem_valor NUMERIC(10,2) DEFAULT 0,
      etiqueta_valor NUMERIC(10,2) DEFAULT 0,
      tempo_corte_min NUMERIC(6,2) DEFAULT 0,
      tempo_costura_min NUMERIC(6,2) DEFAULT 0,
      tempo_acabamento_min NUMERIC(6,2) DEFAULT 0,
      custo_hora_corte NUMERIC(10,2) DEFAULT 25,
      custo_hora_costura NUMERIC(10,2) DEFAULT 30,
      custo_hora_acabamento NUMERIC(10,2) DEFAULT 20,
      bordado_valor NUMERIC(10,2) DEFAULT 0,
      silk_valor NUMERIC(10,2) DEFAULT 0,
      dtf_valor NUMERIC(10,2) DEFAULT 0,
      sublimacao_valor NUMERIC(10,2) DEFAULT 0,
      overhead_pct NUMERIC(5,2) DEFAULT 15,
      comissao_pct NUMERIC(5,2) DEFAULT 5,
      impostos_pct NUMERIC(5,2) DEFAULT 6,
      marketplace_pct NUMERIC(5,2) DEFAULT 0,
      frete_valor NUMERIC(10,2) DEFAULT 0,
      motoboy_valor NUMERIC(10,2) DEFAULT 0,
      transporte_valor NUMERIC(10,2) DEFAULT 0,
      terceirizacao_valor NUMERIC(10,2) DEFAULT 0,
      linha_valor NUMERIC(10,2) DEFAULT 0,
      botoes_valor NUMERIC(10,2) DEFAULT 0,
      ziper_valor NUMERIC(10,2) DEFAULT 0,
      insumos JSONB,
      insumos_total NUMERIC(12,2) DEFAULT 0,
      margem_lucro_pct NUMERIC(5,2) DEFAULT 30,
      preco_venda_sugerido NUMERIC(10,2),
      preco_venda_praticado NUMERIC(10,2),
      observacao TEXT,
      created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
      updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()
    );
    
    ALTER TABLE msk_precificacao ADD COLUMN IF NOT EXISTS motoboy_valor NUMERIC(10,2) DEFAULT 0;
    ALTER TABLE msk_precificacao ADD COLUMN IF NOT EXISTS transporte_valor NUMERIC(10,2) DEFAULT 0;
    ALTER TABLE msk_precificacao ADD COLUMN IF NOT EXISTS terceirizacao_valor NUMERIC(10,2) DEFAULT 0;
    ALTER TABLE msk_precificacao ADD COLUMN IF NOT EXISTS linha_valor NUMERIC(10,2) DEFAULT 0;
    ALTER TABLE msk_precificacao ADD COLUMN IF NOT EXISTS botoes_valor NUMERIC(10,2) DEFAULT 0;
    ALTER TABLE msk_precificacao ADD COLUMN IF NOT EXISTS ziper_valor NUMERIC(10,2) DEFAULT 0;
    ALTER TABLE msk_precificacao ADD COLUMN IF NOT EXISTS insumos JSONB;
    ALTER TABLE msk_precificacao ADD COLUMN IF NOT EXISTS insumos_total NUMERIC(12,2) DEFAULT 0;
    
    CREATE TABLE IF NOT EXISTS msk_precificacao_insumos (
      id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
      precificacao_id UUID REFERENCES msk_precificacao(id) ON DELETE CASCADE,
      materia_prima_id UUID REFERENCES msk_materia_prima(id) ON DELETE SET NULL,
      nome_insumo TEXT,
      categoria TEXT,
      quantidade NUMERIC(12,3) NOT NULL DEFAULT 1,
      unidade_medida TEXT,
      preco_unitario NUMERIC(12,2) NOT NULL DEFAULT 0,
      custo_total NUMERIC(12,2) NOT NULL DEFAULT 0,
      ordem INTEGER DEFAULT 0,
      created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
    );
    
    ALTER TABLE msk_centro_custos ENABLE ROW LEVEL SECURITY;
    DO $$ BEGIN
      IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_centro_custos' AND policyname = 'allow_all_centro_custos') THEN
        CREATE POLICY "allow_all_centro_custos" ON msk_centro_custos FOR ALL USING (true);
      END IF;
    END $$;
    
    ALTER TABLE msk_precificacao ENABLE ROW LEVEL SECURITY;
    DO $$ BEGIN
      IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_precificacao' AND policyname = 'allow_all_precificacao') THEN
        CREATE POLICY "allow_all_precificacao" ON msk_precificacao FOR ALL USING (true);
      END IF;
    END $$;
    
    ALTER TABLE msk_precificacao_insumos ENABLE ROW LEVEL SECURITY;
    DO $$ BEGIN
      IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_precificacao_insumos' AND policyname = 'allow_all_precificacao_insumos') THEN
        CREATE POLICY "allow_all_precificacao_insumos" ON msk_precificacao_insumos FOR ALL USING (true);
      END IF;
    END $$;
    
    -- MATERIA-PRIMA MSK
    CREATE TABLE IF NOT EXISTS msk_materia_prima (
      id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
      codigo TEXT UNIQUE NOT NULL,
      nome TEXT NOT NULL,
      categoria TEXT NOT NULL,
      subcategoria TEXT,
      cor TEXT,
      unidade_medida TEXT NOT NULL DEFAULT 'kg',
      quantidade_atual NUMERIC(12,3) NOT NULL DEFAULT 0,
      quantidade_minima NUMERIC(12,3) DEFAULT 0,
      preco_custo NUMERIC(12,2),
      fornecedor TEXT,
      localizacao TEXT DEFAULT 'Fabrica',
      status TEXT DEFAULT 'ativo',
      observacao TEXT,
      lote TEXT,
      data_entrada DATE,
      imagem_url TEXT,
      created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
      updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()
    );
    
    CREATE TABLE IF NOT EXISTS msk_materia_prima_movimentacoes (
      id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
      materia_prima_id UUID REFERENCES msk_materia_prima(id) ON DELETE CASCADE,
      tipo TEXT NOT NULL,
      quantidade NUMERIC(12,3) NOT NULL,
      unidade_medida TEXT,
      motivo TEXT,
      ordem_producao TEXT,
      responsavel TEXT,
      custo_total NUMERIC(12,2),
      observacao TEXT,
      created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
    );
    
    ALTER TABLE msk_materia_prima ENABLE ROW LEVEL SECURITY;
    DO $$ BEGIN
      IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_materia_prima' AND policyname = 'allow_all_materia_prima') THEN
        CREATE POLICY "allow_all_materia_prima" ON msk_materia_prima FOR ALL USING (true);
      END IF;
    END $$;
    
    ALTER TABLE msk_materia_prima_movimentacoes ENABLE ROW LEVEL SECURITY;
    DO $$ BEGIN
      IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_materia_prima_movimentacoes' AND policyname = 'allow_all_materia_prima_mov') THEN
        CREATE POLICY "allow_all_materia_prima_mov" ON msk_materia_prima_movimentacoes FOR ALL USING (true);
      END IF;
    END $$;
    
    CREATE INDEX IF NOT EXISTS idx_materia_prima_categoria ON msk_materia_prima(categoria);
    CREATE INDEX IF NOT EXISTS idx_materia_prima_localizacao ON msk_materia_prima(localizacao);
    
    -- PATRIMONIO MSK
    CREATE TABLE IF NOT EXISTS msk_patrimonio (
      id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
      codigo TEXT UNIQUE NOT NULL,
      nome TEXT NOT NULL,
      categoria TEXT NOT NULL,
      marca TEXT,
      modelo TEXT,
      numero_serie TEXT,
      ano_aquisicao INTEGER,
      data_aquisicao DATE,
      valor_aquisicao NUMERIC(12,2),
      valor_atual NUMERIC(12,2),
      fornecedor TEXT,
      localizacao TEXT DEFAULT 'Fabrica',
      setor TEXT,
      responsavel TEXT,
      status TEXT DEFAULT 'ativo',
      condicao TEXT DEFAULT 'bom',
      data_ultima_manutencao DATE,
      data_proxima_manutencao DATE,
      vida_util_anos INTEGER DEFAULT 5,
      depreciacao_anual NUMERIC(5,2) DEFAULT 20,
      observacao TEXT,
      imagem_url TEXT,
      nota_fiscal TEXT,
      created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
      updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()
    );
    
    CREATE TABLE IF NOT EXISTS msk_patrimonio_manutencao (
      id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
      patrimonio_id UUID REFERENCES msk_patrimonio(id) ON DELETE CASCADE,
      tipo TEXT NOT NULL,
      descricao TEXT NOT NULL,
      custo NUMERIC(12,2),
      responsavel TEXT,
      fornecedor_servico TEXT,
      data_realizacao DATE,
      proxima_manutencao DATE,
      observacao TEXT,
      created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
    );
    
    ALTER TABLE msk_patrimonio ENABLE ROW LEVEL SECURITY;
    DO $$ BEGIN
      IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_patrimonio' AND policyname = 'allow_all_patrimonio') THEN
        CREATE POLICY "allow_all_patrimonio" ON msk_patrimonio FOR ALL USING (true);
      END IF;
    END $$;
    
    ALTER TABLE msk_patrimonio_manutencao ENABLE ROW LEVEL SECURITY;
    DO $$ BEGIN
      IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_patrimonio_manutencao' AND policyname = 'allow_all_patrimonio_manut') THEN
        CREATE POLICY "allow_all_patrimonio_manut" ON msk_patrimonio_manutencao FOR ALL USING (true);
      END IF;
    END $$;
    
    CREATE INDEX IF NOT EXISTS idx_patrimonio_categoria ON msk_patrimonio(categoria);
    CREATE INDEX IF NOT EXISTS idx_patrimonio_status ON msk_patrimonio(status);
    CREATE INDEX IF NOT EXISTS idx_patrimonio_localizacao ON msk_patrimonio(localizacao);
    
    -- AUTOMACOES MSK
    CREATE TABLE IF NOT EXISTS msk_automacoes_logs (
      id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
      automacao_nome TEXT NOT NULL,
      descricao TEXT,
      status TEXT DEFAULT 'sucesso',
      tempo_economizado_min INTEGER DEFAULT 0,
      detalhes JSONB,
      created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
    );
    
    CREATE TABLE IF NOT EXISTS msk_automacoes_templates (
      id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
      nome TEXT NOT NULL UNIQUE,
      tipo TEXT NOT NULL,
      assunto TEXT,
      corpo TEXT NOT NULL,
      variaveis JSONB,
      ativo BOOLEAN DEFAULT true,
      updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()
    );
    
    ALTER TABLE msk_automacoes_logs ENABLE ROW LEVEL SECURITY;
    DO $$ BEGIN
      IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_automacoes_logs' AND policyname = 'allow_all_automacoes_logs') THEN
        CREATE POLICY "allow_all_automacoes_logs" ON msk_automacoes_logs FOR ALL USING (true);
      END IF;
    END $$;
    
    ALTER TABLE msk_automacoes_templates ENABLE ROW LEVEL SECURITY;
    DO $$ BEGIN
      IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_automacoes_templates' AND policyname = 'allow_all_automacoes_templates') THEN
        CREATE POLICY "allow_all_automacoes_templates" ON msk_automacoes_templates FOR ALL USING (true);
      END IF;
    END $$;
    SQL — Parte 3 de 3
    -- 12. CONSENTIMENTOS DE FOTOS (LGPD)
    CREATE TABLE IF NOT EXISTS consentimentos_fotos (
      id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
      nome_responsavel TEXT,
      nome_crianca TEXT,
      numero_pedido TEXT,
      ip_address TEXT,
      data_hora TIMESTAMP WITH TIME ZONE DEFAULT now(),
      status_exclusao TEXT DEFAULT 'pendente',
      data_exclusao_prevista TIMESTAMP WITH TIME ZONE DEFAULT (now() + interval '48 hours')
    );
    
    ALTER TABLE consentimentos_fotos ENABLE ROW LEVEL SECURITY;
    DO $$ BEGIN
      IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'consentimentos_fotos' AND policyname = 'allow_public_insert_consentimentos') THEN
        CREATE POLICY "allow_public_insert_consentimentos" ON consentimentos_fotos FOR INSERT WITH CHECK (true);
      END IF;
      IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'consentimentos_fotos' AND policyname = 'allow_admin_select_consentimentos') THEN
        CREATE POLICY "allow_admin_select_consentimentos" ON consentimentos_fotos FOR SELECT USING (true);
      END IF;
    END $$;
    
    -- 13. HISTORICO DE PRECOS
    CREATE TABLE IF NOT EXISTS msk_historico_precos (
      id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
      produto_id UUID REFERENCES msk_produtos_estoque(id) ON DELETE CASCADE,
      preco_antigo DECIMAL(10,2),
      preco_novo DECIMAL(10,2),
      custo_antigo DECIMAL(10,2),
      custo_novo DECIMAL(10,2),
      motivo TEXT,
      responsavel TEXT,
      created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
    );
    
    -- 14. ORDENS DE COMPRA
    CREATE TABLE IF NOT EXISTS msk_ordens_compra (
      id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
      numero_pedido TEXT,
      fornecedor TEXT,
      itens JSONB,
      valor_total DECIMAL(10,2),
      status TEXT DEFAULT 'pendente',
      data_pedido TIMESTAMP WITH TIME ZONE DEFAULT now(),
      data_prevista TIMESTAMP WITH TIME ZONE,
      observacoes TEXT,
      created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
    );
    
    ALTER TABLE msk_historico_precos ENABLE ROW LEVEL SECURITY;
    DO $$ BEGIN
      IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_historico_precos' AND policyname = 'allow_all_historico_precos') THEN
        CREATE POLICY "allow_all_historico_precos" ON msk_historico_precos FOR ALL USING (true);
      END IF;
    END $$;
    
    ALTER TABLE msk_ordens_compra ENABLE ROW LEVEL SECURITY;
    DO $$ BEGIN
      IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_ordens_compra' AND policyname = 'allow_all_ordens_compra') THEN
        CREATE POLICY "allow_all_ordens_compra" ON msk_ordens_compra FOR ALL USING (true);
      END IF;
    END $$;
    
    -- 15. EDITOR VISUAL RECURSOS
    CREATE TABLE IF NOT EXISTS msk_editor_logos (
      id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
      nome TEXT,
      url TEXT NOT NULL,
      created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
    );
    
    CREATE TABLE IF NOT EXISTS msk_editor_templates (
      id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
      nome TEXT NOT NULL,
      configuracao JSONB NOT NULL,
      created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
    );
    
    CREATE TABLE IF NOT EXISTS msk_editor_texturas (
      id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
      nome TEXT NOT NULL,
      url TEXT NOT NULL,
      created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
    );
    
    ALTER TABLE msk_editor_logos ENABLE ROW LEVEL SECURITY;
    DO $$ BEGIN
      IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_editor_logos' AND policyname = 'allow_all_editor_logos') THEN
        CREATE POLICY "allow_all_editor_logos" ON msk_editor_logos FOR ALL USING (true);
      END IF;
    END $$;
    
    ALTER TABLE msk_editor_templates ENABLE ROW LEVEL SECURITY;
    DO $$ BEGIN
      IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_editor_templates' AND policyname = 'allow_all_editor_templates') THEN
        CREATE POLICY "allow_all_editor_templates" ON msk_editor_templates FOR ALL USING (true);
      END IF;
    END $$;
    
    ALTER TABLE msk_editor_texturas ENABLE ROW LEVEL SECURITY;
    DO $$ BEGIN
      IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_editor_texturas' AND policyname = 'allow_all_editor_texturas') THEN
        CREATE POLICY "allow_all_editor_texturas" ON msk_editor_texturas FOR ALL USING (true);
      END IF;
    END $$;
    
    -- 16. MOCKUPS PNG (CorelDRAW)
    CREATE TABLE IF NOT EXISTS msk_editor_mockups (
      id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
      nome TEXT NOT NULL,
      url TEXT NOT NULL,
      created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
    );
    
    ALTER TABLE msk_editor_mockups ENABLE ROW LEVEL SECURITY;
    DO $$ BEGIN
      IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_editor_mockups' AND policyname = 'allow_all_editor_mockups') THEN
        CREATE POLICY "allow_all_editor_mockups" ON msk_editor_mockups FOR ALL USING (true);
      END IF;
    END $$;
    
    -- 17. COLUNAS DE INTEGRACAO COM MERCADO LIVRE
    ALTER TABLE msk_produtos_estoque ADD COLUMN IF NOT EXISTS ml_item_id TEXT;
    ALTER TABLE msk_produtos_estoque ADD COLUMN IF NOT EXISTS ml_permalink TEXT;
    ALTER TABLE msk_produtos_estoque ADD COLUMN IF NOT EXISTS ml_status TEXT;
    ALTER TABLE msk_produtos_estoque ADD COLUMN IF NOT EXISTS ml_published_at TIMESTAMP WITH TIME ZONE;
    
    -- 18. COLUNA DE RESERVA DE ESTOQUE
    ALTER TABLE msk_produtos_estoque ADD COLUMN IF NOT EXISTS quantidade_reservada INTEGER DEFAULT 0;
    
    -- 19. FORNECEDORES DE PRODUTOS DE TERCEIROS
    CREATE TABLE IF NOT EXISTS msk_fornecedores_produtos (
      id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
      fornecedor_nome TEXT NOT NULL,
      fornecedor_contato TEXT,
      fornecedor_cidade TEXT,
      produto_nome TEXT NOT NULL,
      sku TEXT,
      categoria TEXT DEFAULT 'terceirizado',
      preco_custo DECIMAL(10,2) NOT NULL DEFAULT 0,
      preco_venda DECIMAL(10,2) NOT NULL DEFAULT 0,
      margem_lucro_pct DECIMAL(5,2) DEFAULT 0,
      quantidade_estoque INTEGER DEFAULT 0,
      unidade TEXT DEFAULT 'Fabrica',
      tipo_produto TEXT DEFAULT 'terceirizado',
      imagem_url TEXT,
      observacao TEXT,
      created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
      updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()
    );
    
    ALTER TABLE msk_fornecedores_produtos ENABLE ROW LEVEL SECURITY;
    DO $$ BEGIN
      IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_fornecedores_produtos' AND policyname = 'allow_all_fornecedores_produtos') THEN
        CREATE POLICY "allow_all_fornecedores_produtos" ON msk_fornecedores_produtos FOR ALL USING (true);
      END IF;
    END $$;
    
    -- 20. CONSIGNADO
    CREATE TABLE IF NOT EXISTS msk_consignado (
      id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
      fabrica_parceira TEXT NOT NULL,
      produto_nome TEXT NOT NULL,
      sku TEXT,
      categoria TEXT DEFAULT '3D',
      quantidade_consignada INTEGER DEFAULT 0,
      quantidade_vendida INTEGER DEFAULT 0,
      quantidade_devolvida INTEGER DEFAULT 0,
      preco_custo DECIMAL(10,2) DEFAULT 0,
      preco_venda DECIMAL(10,2) DEFAULT 0,
      margem_lucro_pct DECIMAL(5,2) DEFAULT 0,
      data_consignacao DATE,
      data_validade_consignacao DATE,
      status TEXT DEFAULT 'ativo',
      observacao TEXT,
      created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
      updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()
    );
    
    ALTER TABLE msk_consignado ENABLE ROW LEVEL SECURITY;
    DO $$ BEGIN
      IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_consignado' AND policyname = 'allow_all_consignado') THEN
        CREATE POLICY "allow_all_consignado" ON msk_consignado FOR ALL USING (true);
      END IF;
    END $$;
    
    -- CHAT INTERNO
    CREATE TABLE IF NOT EXISTS msk_chat_conversas (
      id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
      tipo TEXT NOT NULL DEFAULT 'individual',
      nome TEXT,
      participante_id TEXT,
      participante_tipo TEXT NOT NULL DEFAULT 'parceiro',
      participante_avatar TEXT,
      op_vinculada TEXT,
      ultima_mensagem TEXT,
      ultima_mensagem_data TIMESTAMP WITH TIME ZONE DEFAULT now(),
      nao_lidas INTEGER DEFAULT 0,
      ativo BOOLEAN DEFAULT true,
      created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
      updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()
    );
    
    CREATE TABLE IF NOT EXISTS msk_chat_mensagens (
      id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
      conversa_id UUID REFERENCES msk_chat_conversas(id) ON DELETE CASCADE,
      remetente TEXT NOT NULL DEFAULT 'MSK',
      remetente_role TEXT NOT NULL DEFAULT 'admin',
      conteudo TEXT NOT NULL,
      anexo_url TEXT,
      anexo_tipo TEXT,
      lida BOOLEAN DEFAULT false,
      created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
    );
    
    ALTER TABLE msk_chat_conversas ADD COLUMN IF NOT EXISTS participante_avatar TEXT;
    ALTER TABLE msk_chat_conversas ADD COLUMN IF NOT EXISTS op_vinculada TEXT;
    ALTER TABLE msk_chat_conversas ADD COLUMN IF NOT EXISTS nao_lidas INTEGER DEFAULT 0;
    ALTER TABLE msk_chat_mensagens ADD COLUMN IF NOT EXISTS anexo_url TEXT;
    ALTER TABLE msk_chat_mensagens ADD COLUMN IF NOT EXISTS anexo_tipo TEXT;
    
    CREATE INDEX IF NOT EXISTS idx_chat_conversas_participante ON msk_chat_conversas(participante_id);
    CREATE INDEX IF NOT EXISTS idx_chat_mensagens_conversa ON msk_chat_mensagens(conversa_id);
    CREATE INDEX IF NOT EXISTS idx_chat_mensagens_created ON msk_chat_mensagens(created_at);
    
    ALTER TABLE msk_chat_conversas REPLICA IDENTITY FULL;
    ALTER TABLE msk_chat_mensagens REPLICA IDENTITY FULL;
    
    ALTER TABLE msk_chat_conversas ENABLE ROW LEVEL SECURITY;
    DO $$ BEGIN
      IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_chat_conversas' AND policyname = 'allow_all_chat_conversas') THEN
        CREATE POLICY "allow_all_chat_conversas" ON msk_chat_conversas FOR ALL USING (true);
      END IF;
    END $$;
    
    ALTER TABLE msk_chat_mensagens ENABLE ROW LEVEL SECURITY;
    DO $$ BEGIN
      IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_chat_mensagens' AND policyname = 'allow_all_chat_mensagens') THEN
        CREATE POLICY "allow_all_chat_mensagens" ON msk_chat_mensagens FOR ALL USING (true);
      END IF;
    END $$;
    
    -- CARD LOGOS (posicionamento da logo nos cards da home)
    CREATE TABLE IF NOT EXISTS msk_card_logos (
      card_key TEXT PRIMARY KEY,
      top NUMERIC DEFAULT 50,
      left NUMERIC DEFAULT 50,
      width NUMERIC DEFAULT 12,
      rotate NUMERIC DEFAULT 0,
      blend TEXT DEFAULT 'multiply',
      opacity NUMERIC DEFAULT 0.9,
      enabled BOOLEAN DEFAULT true,
      updated_at TIMESTAMPTZ DEFAULT now()
    );
    
    ALTER TABLE msk_card_logos ENABLE ROW LEVEL SECURITY;
    DO $$ BEGIN
      IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename = 'msk_card_logos' AND policyname = 'allow_all_card_logos') THEN
        CREATE POLICY "allow_all_card_logos" ON msk_card_logos FOR ALL USING (true) WITH CHECK (true);
      END IF;
    END $$;

    3. Criar os Buckets de Armazenamento (Storage)

    No painel → StorageNew bucket para cada um:

    Nome do BucketPúblico?Para que serve
    produtosSimFotos dos produtos/uniformes
    mockupsSimMockups PNG do CorelDRAW
    logosSimLogos dos clientes no editor visual
    editor-texturasSimTexturas de tecido personalizadas
    chat-anexosSimImagens e documentos do chat
    conferencia-fotosSimFotos da conferência de peças
    provador-fotosSimFotos dos alunos no provador
    technical-documentsNãoFichas técnicas em PDF
    production-qrcodesSimQR Codes das ordens de produção

    Para cada bucket público, cole no SQL Editor:

    Política de acesso público (repita para cada bucket)
    -- Substitua 'produtos' pelo nome de cada bucket
    CREATE POLICY "allow_public_read_produtos" ON storage.objects FOR SELECT USING (bucket_id = 'produtos');
    CREATE POLICY "allow_public_upload_produtos" ON storage.objects FOR INSERT WITH CHECK (bucket_id = 'produtos');
    CREATE POLICY "allow_public_update_produtos" ON storage.objects FOR UPDATE USING (bucket_id = 'produtos');

    4. Ativar o Realtime (Tempo Real do Chat)

    1. Painel → Database → aba Replication
    2. Ative o switch das tabelas msk_chat_conversas e msk_chat_mensagens

    5. Deploy das Edge Functions

    Painel → Edge FunctionsCreate a new function. Para cada função, digite o nome e cole o código correspondente:

    5.1 — ml-publish

    Publica/atualiza produtos no Mercado Livre.

    ml-publish/index.ts
    import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
    import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
    
    const corsHeaders = {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
      "Access-Control-Allow-Methods": "POST, OPTIONS",
    };
    
    const ML_API_BASE = "https://api.mercadolibre.com";
    
    async function getAccessToken(): Promise<string> {
      const clientId = Deno.env.get("ML_CLIENT_ID");
      const clientSecret = Deno.env.get("ML_CLIENT_SECRET");
      const refreshToken = Deno.env.get("ML_REFRESH_TOKEN");
    
      if (!clientId || !clientSecret || !refreshToken) {
        throw new Error("Credenciais do ML ausentes. Configure as secrets.");
      }
    
      const res = await fetch(`${ML_API_BASE}/oauth/token`, {
        method: "POST",
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
        body: new URLSearchParams({
          grant_type: "refresh_token",
          client_id: clientId,
          client_secret: clientSecret,
          refresh_token: refreshToken,
        }),
      });
    
      if (!res.ok) {
        const body = await res.text();
        throw new Error(`Falha ao obter token ML: ${body}`);
      }
    
      const data = await res.json();
      return data.access_token;
    }
    
    serve(async (req) => {
      if (req.method === "OPTIONS") {
        return new Response("ok", { headers: corsHeaders });
      }
    
      try {
        const supabaseClient = createClient(
          Deno.env.get("SUPABASE_URL") ?? "",
          Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "",
        );
    
        const body = await req.json();
        const { produto_id, acao } = body;
    
        if (!produto_id) {
          return new Response(
            JSON.stringify({ error: "produto_id e obrigatorio" }),
            { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } },
          );
        }
    
        const { data: produto, error: prodError } = await supabaseClient
          .from("msk_produtos_estoque")
          .select("*")
          .eq("id", produto_id)
          .single();
    
        if (prodError || !produto) {
          return new Response(
            JSON.stringify({ error: "Produto nao encontrado" }),
            { status: 404, headers: { ...corsHeaders, "Content-Type": "application/json" } },
          );
        }
    
        const accessToken = await getAccessToken();
    
        const mlItem: any = {
          title: String(produto.nome).substring(0, 60),
          description: {
            plain_text:
              (produto.descricao || produto.nome) +
              `\n\nCategoria: ${produto.categoria || "N/A"}` +
              `\nTecido: ${produto.tipo_tecido || produto.tecido || "N/A"}` +
              `\nComposicao: ${produto.composicao_tecido || "N/A"}` +
              `\nTamanhos: ${produto.tamanhos || "N/A"}` +
              `\nCor: ${produto.cor || "N/A"}`,
          },
          price: Number(produto.preco) || 0,
          currency_id: "BRL",
          available_quantity: Number(produto.quantidade) || 1,
          buying_mode: "buy_it_now",
          condition: "new",
          listing_type_id: "gold_special",
          shipping: { mode: "me2", local_pick_up: true, free_shipping: false },
          attributes: [{ id: "BRAND", value_name: "MSK" }],
        };
    
        if (produto.imagem_url) {
          mlItem.pictures = [{ source: produto.imagem_url }];
        }
    
        const mlItemId = produto.ml_item_id;
        let response: Response;
    
        if (mlItemId && acao !== "republicar") {
          const res = await fetch(`${ML_API_BASE}/items/${mlItemId}`, {
            method: "PUT",
            headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
            body: JSON.stringify({
              price: mlItem.price,
              available_quantity: mlItem.available_quantity,
              title: mlItem.title,
            }),
          });
          const result = await res.json();
          if (!res.ok) throw new Error(`Erro ao atualizar ML: ${JSON.stringify(result)}`);
          response = new Response(
            JSON.stringify({ success: true, action: "atualizado", ml_item_id: mlItemId, ml_result: result }),
            { status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" } },
          );
        } else {
          const res = await fetch(`${ML_API_BASE}/items`, {
            method: "POST",
            headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
            body: JSON.stringify(mlItem),
          });
          const result = await res.json();
          if (!res.ok) throw new Error(`Erro ao publicar ML: ${JSON.stringify(result)}`);
    
          await supabaseClient
            .from("msk_produtos_estoque")
            .update({
              ml_item_id: result.id,
              ml_permalink: result.permalink || null,
              ml_status: result.status || "active",
              ml_published_at: new Date().toISOString(),
            })
            .eq("id", produto_id);
    
          response = new Response(
            JSON.stringify({ success: true, action: "publicado", ml_item_id: result.id, ml_permalink: result.permalink, ml_result: result }),
            { status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" } },
          );
        }
    
        await supabaseClient.from("msk_automacoes_logs").insert([
          {
            automacao_nome: "Publicacao Mercado Livre",
            status: "sucesso",
            descricao: `Produto ${produto.nome} (${produto.sku}) publicado/atualizado no ML`,
          },
        ]);
    
        return response;
      } catch (error) {
        console.error("ERRO ML:", error);
        return new Response(
          JSON.stringify({ error: error.message }),
          { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } },
        );
      }
    });

    5.2 — marketplace-webhook

    Recebe pedidos do ML/Shopee e salva no banco.

    marketplace-webhook/index.ts
    import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
    import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
    
    const corsHeaders = {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
    };
    
    const GHL_API = "https://services.leadconnectorhq.com";
    const GHL_VERSION = "2021-07-28";
    
    serve(async (req) => {
      if (req.method === "OPTIONS") {
        return new Response("ok", { headers: corsHeaders });
      }
    
      try {
        const supabaseClient = createClient(
          Deno.env.get("SUPABASE_URL") ?? "",
          Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? ""
        );
    
        const payload = await req.json();
        const { source, order } = payload; // source: 'mercadolivre' | 'shopee'
    
        if (!source || !order) {
          throw new Error("Invalid payload: missing source or order data");
        }
    
        const externalOrderId = String(order.id);
    
        // 1. IDEMPOTÊNCIA: se o pedido já existe, NÃO recriar oportunidade
        const { data: existingOrder } = await supabaseClient
          .from("marketplace_orders")
          .select("id, ghl_contact_id, ghl_opportunity_id")
          .eq("marketplace", source)
          .eq("external_order_id", externalOrderId)
          .maybeSingle();
    
        if (existingOrder) {
          if (order.status) {
            await supabaseClient
              .from("marketplace_orders")
              .update({ status: order.status, updated_at: new Date().toISOString() })
              .eq("id", existingOrder.id);
          }
          return new Response(JSON.stringify({
            success: true,
            message: "Pedido ja existente - nenhuma oportunidade duplicada criada",
            orderId: existingOrder.id,
            ghl_opportunity_id: existingOrder.ghl_opportunity_id ?? null,
          }), { headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 200 });
        }
    
        // 2. Salvar pedido no Supabase
        const { data: dbOrder, error: dbError } = await supabaseClient
          .from("marketplace_orders")
          .insert([{
            marketplace: source,
            external_order_id: externalOrderId,
            customer_name: order.customer.name,
            customer_email: order.customer.email || null,
            customer_phone: order.customer.phone || null,
            total_amount: order.total,
            status: order.status,
            items: order.items,
          }])
          .select().single();
    
        if (dbError) throw dbError;
    
        // 3. Integração CRM
        const crmApiKey = Deno.env.get("CRM_API_KEY");
        const locationId = Deno.env.get("CRM_LOCATION_ID");
        const pipelineId = Deno.env.get("CRM_PIPELINE_ID");
        const stageId = Deno.env.get("CRM_PIPELINE_STAGE_ID");
    
        let ghlContactId = null;
        let ghlOpportunityId = null;
    
        if (crmApiKey && locationId && pipelineId && stageId) {
          const fullName = (order.customer.name || "").trim();
          const firstName = fullName.split(" ")[0] || fullName;
          const lastName = fullName.split(" ").slice(1).join(" ") || "";
          const email = order.customer.email || "";
          const phone = order.customer.phone || "";
    
          // 3a. DEDUPLICAÇÃO: buscar contato por email
          if (email) {
            const searchRes = await fetch(
              `${GHL_API}/contacts/?locationId=${locationId}&email=${encodeURIComponent(email)}`,
              { headers: { Authorization: `Bearer ${crmApiKey}`, Version: GHL_VERSION, Accept: "application/json" } }
            );
            if (searchRes.ok) {
              const searchData = await searchRes.json();
              if (searchData.contacts && searchData.contacts.length > 0) {
                ghlContactId = searchData.contacts[0].id;
              }
            }
          }
    
          // 3a2. Se não achou por email, buscar por telefone
          if (!ghlContactId && phone) {
            const searchRes = await fetch(
              `${GHL_API}/contacts/?locationId=${locationId}&phone=${encodeURIComponent(phone)}`,
              { headers: { Authorization: `Bearer ${crmApiKey}`, Version: GHL_VERSION, Accept: "application/json" } }
            );
            if (searchRes.ok) {
              const searchData = await searchRes.json();
              if (searchData.contacts && searchData.contacts.length > 0) {
                ghlContactId = searchData.contacts[0].id;
              }
            }
          }
    
          // 3b. Se não existe, criar contato
          if (!ghlContactId) {
            const contactBody = { firstName, lastName, locationId, tags: ["marketplace", source] };
            if (email) contactBody.email = email;
            if (phone) contactBody.phone = phone;
    
            const contactRes = await fetch(`${GHL_API}/contacts/`, {
              method: "POST",
              headers: { Authorization: `Bearer ${crmApiKey}`, Version: GHL_VERSION, "Content-Type": "application/json", Accept: "application/json" },
              body: JSON.stringify(contactBody),
            });
            const contactData = await contactRes.json();
            if (contactData.contact?.id) ghlContactId = contactData.contact.id;
            else if (contactData.id) ghlContactId = contactData.id;
          }
    
          // 3c. Criar oportunidade no pipeline
          if (ghlContactId) {
            const oppRes = await fetch(`${GHL_API}/opportunities/`, {
              method: "POST",
              headers: { Authorization: `Bearer ${crmApiKey}`, Version: GHL_VERSION, "Content-Type": "application/json", Accept: "application/json" },
              body: JSON.stringify({
                pipelineId, locationId,
                name: `Pedido ${source} - ${externalOrderId}`,
                pipelineStageId: stageId,
                status: "open",
                contactId: ghlContactId,
                monetaryValue: Number(order.total) || 0,
              }),
            });
            const oppData = await oppRes.json();
            if (oppData.id) ghlOpportunityId = oppData.id;
            else if (oppData.opportunity?.id) ghlOpportunityId = oppData.opportunity.id;
          }
        }
    
        // 4. Salvar IDs no Supabase
        if (ghlContactId || ghlOpportunityId) {
          await supabaseClient
            .from("marketplace_orders")
            .update({ ghl_contact_id: ghlContactId, ghl_opportunity_id: ghlOpportunityId, updated_at: new Date().toISOString() })
            .eq("id", dbOrder.id);
        }
    
        await supabaseClient.from("msk_automacoes_logs").insert([{
          automacao_nome: "Webhook Marketplace -> CRM",
          status: "sucesso",
          descricao: `Pedido ${source} #${externalOrderId} salvo. Contato: ${ghlContactId ?? "n/a"}. Oportunidade: ${ghlOpportunityId ?? "n/a"}.`,
        }]);
    
        return new Response(JSON.stringify({
          success: true, orderId: dbOrder.id,
          ghl_contact_id: ghlContactId, ghl_opportunity_id: ghlOpportunityId,
        }), { headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 200 });
      } catch (error) {
        console.error("ERRO webhook:", error);
        return new Response(JSON.stringify({ error: error.message }),
          { headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 400 });
      }
    });

    5.3 — generate-production-package

    Gera PDF e QR Code das ordens de produção.

    generate-production-package/index.ts
    import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
    import { createClient } from "https://esm.sh/@supabase/supabase-js@2.39.3"
    import * as qrcode from "https://deno.land/x/qrcode_generator@v1.0.1/mod.ts"
    
    const corsHeaders = {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
    }
    
    serve(async (req) => {
      if (req.method === 'OPTIONS') {
        return new Response('ok', { headers: corsHeaders })
      }
    
      try {
        const { production_order_id } = await req.json()
    
        if (!production_order_id) {
          return new Response(JSON.stringify({ error: 'production_order_id is required' }), {
            status: 400,
            headers: { ...corsHeaders, 'Content-Type': 'application/json' },
          })
        }
    
        const supabaseClient = createClient(
          Deno.env.get('SUPABASE_URL') ?? '',
          Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
        )
    
        const { data: order, error: orderError } = await supabaseClient
          .from('production_orders')
          .select('*, technical_sheets(*)')
          .eq('id', production_order_id)
          .single()
    
        if (orderError || !order) {
          throw new Error('Production order not found')
        }
    
        const technicalSheet = order.technical_sheets[0]
        if (!technicalSheet) {
          throw new Error('Technical sheet not found for this order')
        }
    
        if (technicalSheet.status !== 'approved') {
          throw new Error('Technical sheet must be approved before generating package')
        }
    
        if (technicalSheet.pdf_url && technicalSheet.technical_image_url) {
          return new Response(JSON.stringify({
            message: 'Package already generated for this version',
            pdf_url: technicalSheet.pdf_url,
            qr_code_url: technicalSheet.qr_code_url,
          }), {
            status: 200,
            headers: { ...corsHeaders, 'Content-Type': 'application/json' },
          })
        }
    
        const { data: sizes } = await supabaseClient
          .from('production_order_sizes')
          .select('*')
          .eq('production_order_id', production_order_id)
    
        const { data: personalizations } = await supabaseClient
          .from('personalizations')
          .select('*')
          .eq('technical_sheet_id', technicalSheet.id)
    
        const { error: tasksError } = await supabaseClient.rpc('create_production_tasks', {
          p_order_id: production_order_id
        })
    
        if (tasksError) {
          console.error("Error creating tasks:", tasksError)
        }
    
        const pdfContent = `Ficha Tecnica - Ordem: ${order.order_number}\nStatus: Aprovado\n`
        const pdfBlob = new Blob([pdfContent], { type: 'application/pdf' })
        const pdfFileName = `${production_order_id}/ficha-tecnica-v${technicalSheet.version || 1}.pdf`
    
        const orderUrl = `${Deno.env.get('PUBLIC_SITE_URL') || 'https://mskonline.com.br'}/admin/producao/caixa/${production_order_id}`
        const qrCodeBase64 = qrcode.qrcode(4, 'L').addData(orderUrl).make().createDataURL(4)
        const qrFetch = await fetch(qrCodeBase64)
        const qrBlob = await qrFetch.blob()
        const qrFileName = `${production_order_id}/qr-code.png`
    
        const { error: pdfUploadError } = await supabaseClient.storage
          .from('technical-documents')
          .upload(pdfFileName, pdfBlob, { upsert: true, contentType: 'application/pdf' })
    
        if (pdfUploadError) throw pdfUploadError
    
        const { error: qrUploadError } = await supabaseClient.storage
          .from('production-qrcodes')
          .upload(qrFileName, qrBlob, { upsert: true, contentType: 'image/png' })
    
        if (qrUploadError) throw qrUploadError
    
        const { data: pdfUrlData } = supabaseClient.storage.from('technical-documents').getPublicUrl(pdfFileName)
        const { data: qrUrlData } = supabaseClient.storage.from('production-qrcodes').getPublicUrl(qrFileName)
    
        await supabaseClient
          .from('technical_sheets')
          .update({
            pdf_url: pdfUrlData.publicUrl,
            qr_code_url: qrUrlData.publicUrl,
          })
          .eq('id', technicalSheet.id)
    
        await supabaseClient
          .from('production_history')
          .insert({
            production_order_id: production_order_id,
            action: 'package_generated',
            details: 'PDF, QR Code e tarefas geradas com sucesso',
            created_by: 'system'
          })
    
        return new Response(JSON.stringify({
          success: true,
          pdf_url: pdfUrlData.publicUrl,
          qr_code_url: qrUrlData.publicUrl,
        }), {
          headers: { ...corsHeaders, 'Content-Type': 'application/json' },
          status: 200,
        })
    
      } catch (error: any) {
        return new Response(JSON.stringify({ error: error.message }), {
          headers: { ...corsHeaders, 'Content-Type': 'application/json' },
          status: 400,
        })
      }
    })

    6. Configurar as Secrets

    Painel → Project Settings (engrenagem) → Edge FunctionsSecretsAdd secret:

    Nome da SecretValor
    ML_CLIENT_ID(obtido no passo 7)
    ML_CLIENT_SECRET(obtido no passo 7)
    ML_REFRESH_TOKEN(obtido no passo 7)
    SUPABASE_URLhttps://gcpuoafrpveyrtiifqde.supabase.co
    SUPABASE_SERVICE_ROLE_KEY(pegue em Project Settings → API → service_role)
    CRM_API_KEYPrivate Integration Token da sua subconta MSK (ver passo 6b)
    CRM_LOCATION_IDLocation ID da subconta MSK (ver passo 6b)
    CRM_PIPELINE_IDID do pipeline "MSK | Marketplace" (ver passo 6b)
    CRM_PIPELINE_STAGE_IDID do estágio "Novo Pedido" (ver passo 6b)

    6b. Configurar o CRM (Pipeline MSK | Marketplace)

    A integração com o CRM já existe no código. Você só precisa obter 4 valores na sua subconta MSK e colocar nas secrets acima.

    Passo A — Criar o Private Integration Token

    1. Na sua subconta MSK, vá em Settings → Integrations
    2. Clique em Add Integration → Private Integration
    3. Marque as permissões: Contacts (read/write) e Opportunities (read/write)
    4. Salve e copie o API Token → este é o CRM_API_KEY

    Passo B — Obter o Location ID

    1. Em Settings → Business Profile ou na URL do painel: .../location/LOCATION_ID/...
    2. Copie esse ID → este é o CRM_LOCATION_ID

    Passo C — Criar o Pipeline e obter os IDs

    1. Vá em Opportunities → Pipelines → crie um pipeline chamado MSK | Marketplace
    2. Crie os estágios nesta ordem exata:
      • Novo Pedido
      • Pagamento Confirmado
      • Em Separação
      • Em Produção
      • Pronto para Envio
      • Enviado
      • Entregue
      • Cancelado
    3. Para pegar os IDs: abra o pipeline no navegador, clique em cada estágio. A URL mostra pipelineId=XXX e stageId=YYY.
    4. O pipelineIdCRM_PIPELINE_ID
    5. O stageId do estágio "Novo Pedido" → CRM_PIPELINE_STAGE_ID
    Não invente chaves. Os 4 valores (API_KEY, LOCATION_ID, PIPELINE_ID, STAGE_ID) só existem na sua subconta. Sem eles, o webhook salva o pedido no Supabase mas não cria o contato nem a oportunidade no CRM.


    6c. Criar o Pipeline automaticamente (recomendado)

    Em vez de criar o pipeline "MSK | Marketplace" e os 8 estágios manualmente no CRM, use a Edge Function setup-crm-pipeline. Ela consulta a API, cria o pipeline com os estágios na ordem correta (se ainda não existir) e devolve os IDs prontos para colar nas secrets.

    1. No Supabase → Edge Functions → Deploy new function
    2. Nome: setup-crm-pipeline
    3. Cole o código abaixo e clique em Deploy
    4. Depois de deployar, clique em Test (ou rode o curl abaixo). A função usa CRM_API_KEY e CRM_LOCATION_ID que já estão configuradas.
    5. A resposta contém CRM_PIPELINE_ID e CRM_PIPELINE_STAGE_ID — copie esses dois valores.
    6. Vá em Edge Functions → Secrets e adicione:
      • CRM_PIPELINE_ID = valor retornado
      • CRM_PIPELINE_STAGE_ID = valor retornado
    supabase/functions/setup-crm-pipeline/index.ts
    import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
    
    const corsHeaders = {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
    };
    
    const GHL_API = "https://services.leadconnectorhq.com";
    const PIPELINE_VERSION = "v3";
    const PIPELINE_NAME = "MSK | Marketplace";
    const STAGES = [
      "Novo Pedido",
      "Pagamento Confirmado",
      "Em Separação",
      "Em Produção",
      "Pronto para Envio",
      "Enviado",
      "Entregue",
      "Cancelado",
    ];
    
    serve(async (req) => {
      if (req.method === "OPTIONS") return new Response("ok", { headers: corsHeaders });
    
      try {
        const apiKey = Deno.env.get("CRM_API_KEY");
        const locationId = Deno.env.get("CRM_LOCATION_ID");
        if (!apiKey || !locationId) {
          return new Response(JSON.stringify({ success: false, error: "Faltam CRM_API_KEY ou CRM_LOCATION_ID" }),
            { headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 400 });
        }
    
        const authHeaders = { Authorization: `Bearer ${apiKey}`, Accept: "application/json" };
    
        // 1. Consultar pipelines existentes
        const listRes = await fetch(`${GHL_API}/opportunities/pipelines?locationId=${encodeURIComponent(locationId)}`,
          { headers: { ...authHeaders, Version: PIPELINE_VERSION } });
        if (!listRes.ok) {
          const t = await listRes.text();
          return new Response(JSON.stringify({ success: false, error: `Erro ao consultar (${listRes.status}): ${t}` }),
            { headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 502 });
        }
        const listData = await listRes.json();
        const pipelines = listData.pipelines || [];
        let target = pipelines.find((p) => p.name && p.name.toLowerCase() === PIPELINE_NAME.toLowerCase());
    
        // 2. Criar se não existir
        if (!target) {
          const createRes = await fetch(`${GHL_API}/opportunities/pipelines`, {
            method: "POST",
            headers: { ...authHeaders, Version: PIPELINE_VERSION, "Content-Type": "application/json" },
            body: JSON.stringify({
              name: PIPELINE_NAME, locationId,
              stages: STAGES.map((name, i) => ({ name, position: i + 1, showInFunnel: true })),
              showInFunnel: true, showInPieChart: false, colorRenderMode: "dot",
            }),
          });
          if (!createRes.ok) {
            const t = await createRes.text();
            return new Response(JSON.stringify({ success: false, error: `Erro ao criar (${createRes.status}): ${t}` }),
              { headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 502 });
          }
          const created = await createRes.json();
          target = { id: created.id, name: created.name || PIPELINE_NAME, stages: created.stages || [] };
        }
    
        // 3. Obter estágio "Novo Pedido"
        let stage = (target.stages || []).find((s) => s.name && s.name.toLowerCase() === "novo pedido");
        if (!stage && target.id) {
          const dRes = await fetch(`${GHL_API}/opportunities/pipelines/${target.id}`, { headers: { ...authHeaders, Version: PIPELINE_VERSION } });
          if (dRes.ok) {
            const d = await dRes.json();
            target.stages = d.stages || [];
            stage = target.stages.find((s) => s.name && s.name.toLowerCase() === "novo pedido");
          }
        }
        if (!stage) {
          return new Response(JSON.stringify({ success: false, error: "Estágio Novo Pedido não encontrado", pipelineId: target.id, stages: target.stages }),
            { headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 404 });
        }
    
        return new Response(JSON.stringify({
          success: true, message: "Pipeline configurado.",
          CRM_PIPELINE_ID: target.id, CRM_PIPELINE_STAGE_ID: stage.id,
          pipelineName: target.name, stages: target.stages,
        }), { headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 200 });
      } catch (e) {
        return new Response(JSON.stringify({ success: false, error: e.message }),
          { headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 500 });
      }
    });

    Como testar (após deploy):

    Comando para executar o setup
    curl -X POST https://gcpuoafrpveyrtiifqde.supabase.co/functions/v1/setup-crm-pipeline \
      -H "Authorization: Bearer SUA_ANON_KEY" \
      -H "Content-Type: application/json" \
      -d '{}'
    Resposta esperada: a função retorna success: true com CRM_PIPELINE_ID e CRM_PIPELINE_STAGE_ID preenchidos. Se o pipeline já existir, ela apenas consulta e retorna os IDs existentes (não duplica).

    6d. Portal da Família com Login (Supabase Auth + RLS)

    O Portal da Família agora exige login real. Cada cliente vê apenas seus próprios pedidos — protegido no banco por RLS. O admin continua vendo tudo (via papel admin).

    1. Rode a migration no SQL Editor: supabase/migrations/20260914_portal_auth.sql (cria coluna user_id, tabela app_roles, função is_admin() e RLS por usuário).
    2. Em Authentication → Users → Add user, crie o usuário admin (email + senha).
    3. Copie o UID dele e rode no SQL Editor:
      INSERT INTO public.app_roles (user_id, role)
      VALUES ('UID_DO_ADMIN', 'admin')
      ON CONFLICT (user_id) DO UPDATE SET role = 'admin';
    4. Deploy das Edge Functions:
      • track-order — rastreamento público seguro (aceita só tracking_token, retorna só status/etapa/previsão).
      • create-checkout-order — cria pedido do checkout (visitante = user_id NULL via service_role; autenticado = user_id do JWT).
      • upload-provador-foto — upload seguro de fotos do provador para o bucket privado provador-fotos (o bucket não aceita INSERT público; só esta função grava).
    5. Em Authentication → Providers, certifique-se de que Email está habilitado.
    Como testar com 2 usuários: crie dois usuários (ex: cliente1@teste.com e cliente2@teste.com) em Authentication → Users. Faça login com cada um em /portal — cada um verá apenas os próprios pedidos. O admin (com papel admin) vê todos.


    7. Criar o App no Mercado Livre

    1. Acesse https://developers.mercadolivre.com.br
    2. Clique em Criar aplicação
    3. Preencha: Nome: MSK Uniformes | Redirect URI: https://gcpuoafrpveyrtiifqde.supabase.co/functions/v1/ml-publish | Scopes: read, write, offline_access
    4. Anote o App ID (= ML_CLIENT_ID) e o Client Secret (= ML_CLIENT_SECRET)

    Obter o Refresh Token:

    Abra no navegador e autorize
    https://auth.mercadolivre.com.br/authorization?response_type=code&client_id=SEU_CLIENT_ID&redirect_uri=SUA_REDIRECT_URI

    Copie o ?code=XXXX da URL de retorno e faça um POST para:

    Corpo da requisição (x-www-form-urlencoded)
    POST https://api.mercadolibre.com/oauth/token
    grant_type=authorization_code
    client_id=SEU_CLIENT_ID
    client_secret=SEU_CLIENT_SECRET
    code=CODIGO_COPIADO
    redirect_uri=SUA_REDIRECT_URI

    A resposta contém refresh_token → este é o ML_REFRESH_TOKEN.


    8. Conectar o Webhook do Mercado Livre

    1. No painel do ML → Configurações → Notificações
    2. Cole no campo Webhook URL:
    URL do Webhook
    https://gcpuoafrpveyrtiifqde.supabase.co/functions/v1/marketplace-webhook

    Selecione os eventos Orders e Items → Salve.


    9. Automações e Notificações WhatsApp

    Opção 1 — WhatsApp Business API (recomendado):

    1. Crie conta em https://business.whatsapp.com
    2. Obtenha o token da API
    3. Adicione as secrets: WHATSAPP_TOKEN e WHATSAPP_PHONE_ID

    Opção 2 — Link do WhatsApp (mais simples):

    O sistema já gera links wa.me com mensagem pronta. Funciona sem configuração.


    10. Checklist Final

    • Rodei as 3 partes do SQL no SQL Editor (passo 2)
    • Criei todos os 9 buckets no Storage (passo 3)
    • Apliquei as políticas de acesso público nos buckets
    • Ativei Realtime para msk_chat_conversas e msk_chat_mensagens
    • Fiz deploy das 3 Edge Functions (passo 5)
    • Configurei as 5 secrets (passo 6)
    • Criei o app no ML Developers e obtive os tokens (passo 7)
    • Configurei o webhook URL no ML (passo 8)
    • Testei publicar um produto no ML

    11. Testar a Integração (Pedido Fictício)

    Envie um POST para a Edge Function com um pedido de exemplo (sem criar venda real). Pode usar terminal ou o site reqbin.com.

    Teste com curl (pedido fictício)
    curl -X POST https://gcpuoafrpveyrtiifqde.supabase.co/functions/v1/marketplace-webhook \
      -H "Authorization: Bearer SUA_ANON_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "source": "mercadolivre",
        "order": {
          "id": "TESTE-001",
          "status": "pago",
          "total": 159.90,
          "customer": {
            "name": "Joao da Silva Teste",
            "email": "teste@mskonline.com.br",
            "phone": "+5513999999999"
          },
          "items": [
            { "nome": "Kit Escolar 3 Pecas", "quantidade": 1, "preco": 159.90 }
          ]
        }
      }'

    Resultado esperado: success: true com ghl_contact_id e ghl_opportunity_id preenchidos. No CRM aparecerá o contato com tags marketplace e mercadolivre, e a oportunidade "Pedido mercadolivre - TESTE-001" no estágio "Novo Pedido" com R$ 159,90.

    Idempotência: rode o mesmo comando de novo — a resposta dirá "Pedido ja existente - nenhuma oportunidade duplicada criada". O mesmo pedido nunca cria duas oportunidades.


    Senhas e Acessos do Sistema

    OndeSenha / Acesso
    Painel Admin/admin → senha: simioni2026
    Portal da Transparência/escola/:slug/financeiro → senha: simioni2026
    Portal da Família/portal
    Portal do Representante/representante
    Escolher Escola/escolha-sua-escola
    Editor de Logos/admin/card-logos
    Editor Visual/admin/dashboard → Editor Visual

    Conclusão

    Após completar todos os passos, o sistema estará 100% funcional: cadastro de produtos/uniformes/kits, matéria-prima, ordens de serviço, precificação, chat em tempo real, provador virtual com IA, publicação no Mercado Livre, notificações por WhatsApp, conferência fotográfica, transferência de estoque, editor visual, portal da transparência, portal da família e catálogo escolar.

    Tempo estimado: 2 a 4 horas.