@fayz-ai/plugin-agenda 0.13.4 → 0.13.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/locales/en.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CA6NrC,CAAA"}
1
+ {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/locales/en.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CA4NrC,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"pt-BR.d.ts","sourceRoot":"","sources":["../../src/locales/pt-BR.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CA6NvC,CAAA"}
1
+ {"version":3,"file":"pt-BR.d.ts","sourceRoot":"","sources":["../../src/locales/pt-BR.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CA4NvC,CAAA"}
@@ -5,6 +5,9 @@ export declare const MIGRATION_003_THE_CLIENT_ARRIVES_BEFORE_BEING_SERVED = "--
5
5
  export declare const MIGRATION_004_THE_APPOINTMENT_CARRIES_ITS_UNIT = "-- 004_the_appointment_carries_its_unit.sql\n--\n-- Um agendamento criado PELA TELA nasce com `unit_id` NULO: o formul\u00E1rio coleta\n-- o local e o provider nunca envia a unidade. E `location_id` j\u00E1 \u00C9 uma unidade \u2014\n-- aponta para o mesmo `app.units`.\n--\n-- O efeito n\u00E3o \u00E9 cosm\u00E9tico. O escopo por unidade \u00E9 o que faz a recep\u00E7\u00E3o de uma\n-- filial ver a fila DELA; uma linha sem unidade n\u00E3o \u00E9 filtrada por unidade\n-- nenhuma, ent\u00E3o ela aparece para TODAS. Num tenant de franquia \u2014 e a rede\n-- sint\u00E9tica tem seis unidades \u2014 isso \u00E9 atendimento de uma filial vis\u00EDvel na\n-- recep\u00E7\u00E3o da outra.\n--\n-- Apareceu numa jornada real: duas clientes no mesmo dia, uma marcada pela tela\n-- (sem unidade) e outra com unidade; a recep\u00E7\u00E3o de Norte \u2014 Centro via a primeira\n-- e n\u00E3o a segunda. O filtro estava certo. O dado \u00E9 que chegava incompleto.\n--\n-- POR QUE GATILHO, E N\u00C3O S\u00D3 O CAMPO NO PROVIDER. O provider \u00E9 um escritor entre\n-- v\u00E1rios: o agente cria agendamento, a importa\u00E7\u00E3o cria, o totem cria. Corrigir\n-- s\u00F3 a tela deixaria os outros escrevendo linha sem unidade \u2014 e o furo \u00E9 de\n-- VISIBILIDADE, que n\u00E3o se conserta pela metade.\nCREATE OR REPLACE FUNCTION public.trg_agenda_stamp_unit()\n RETURNS trigger\n LANGUAGE plpgsql\nAS $$\nBEGIN\n IF NEW.unit_id IS NULL AND NEW.location_id IS NOT NULL THEN\n NEW.unit_id := NEW.location_id;\n END IF;\n RETURN NEW;\nEND $$;\n\nCOMMENT ON FUNCTION public.trg_agenda_stamp_unit() IS\n 'Carimba a unidade do agendamento a partir do local, que \u00E9 a mesma app.units. Sem ela a linha escapa do escopo por unidade e aparece para todas as filiais (004).';\n\nDROP TRIGGER IF EXISTS appointments_stamp_unit ON public.appointments;\nCREATE TRIGGER appointments_stamp_unit\n BEFORE INSERT OR UPDATE OF location_id, unit_id ON public.appointments\n FOR EACH ROW\n EXECUTE FUNCTION public.trg_agenda_stamp_unit();\n\n-- \u2500\u2500 e os que j\u00E1 nasceram sem \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nUPDATE public.appointments\n SET unit_id = location_id\n WHERE unit_id IS NULL AND location_id IS NOT NULL;\n";
6
6
  export declare const MIGRATION_005_THE_QUEUE_KNOWS_HOW_LONG_SHE_WAITED = "-- 005_the_queue_knows_how_long_she_waited.sql\n--\n-- A fila mostra QUEM chegou e n\u00E3o H\u00C1 QUANTO TEMPO. Quem est\u00E1 esperando h\u00E1\n-- quarenta minutos e quem acabou de entrar aparecem iguais, e \u00E9 justamente essa\n-- diferen\u00E7a que decide quem a recep\u00E7\u00E3o chama primeiro.\n--\n-- O V1 mede por `checked_in_at`, e o coment\u00E1rio dele explica por que n\u00E3o serve\n-- `updated_at`: qualquer edi\u00E7\u00E3o no agendamento o reescreve, ent\u00E3o trocar uma\n-- observa\u00E7\u00E3o \u00E0s 14h faria a cliente que chegou \u00E0s 13h parecer rec\u00E9m-chegada. Um\n-- rel\u00F3gio que qualquer coisa reinicia n\u00E3o \u00E9 rel\u00F3gio.\n--\n-- O carimbo \u00E9 do BANCO, e n\u00E3o da tela: quem faz check-in pode ser a recep\u00E7\u00E3o\n-- pelo navegador, o totem, ou o pr\u00F3prio agente. Tr\u00EAs escritores, um s\u00F3 instante\n-- verdadeiro \u2014 e o rel\u00F3gio do servidor \u00E9 o \u00FAnico que os tr\u00EAs compartilham.\nALTER TABLE public.appointments\n ADD COLUMN IF NOT EXISTS checked_in_at timestamptz;\n\nCOMMENT ON COLUMN public.appointments.checked_in_at IS\n 'Quando a pessoa CHEGOU. N\u00E3o use updated_at para isso: qualquer edi\u00E7\u00E3o o reescreve e o tempo de espera mente (005).';\n\n-- \u2500\u2500 e o carimbo acontece na transi\u00E7\u00E3o, n\u00E3o no cliente \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nCREATE OR REPLACE FUNCTION public.trg_agenda_stamp_checkin()\n RETURNS trigger\n LANGUAGE plpgsql\nAS $$\nBEGIN\n -- S\u00F3 na ENTRADA em waiting: reentrar no estado (uma corre\u00E7\u00E3o de status, um\n -- salvamento qualquer) n\u00E3o pode reiniciar a espera de quem j\u00E1 est\u00E1 sentada.\n IF NEW.status = 'waiting' AND (TG_OP = 'INSERT' OR OLD.status IS DISTINCT FROM 'waiting')\n AND NEW.checked_in_at IS NULL THEN\n NEW.checked_in_at := now();\n END IF;\n RETURN NEW;\nEND $$;\n\nCOMMENT ON FUNCTION public.trg_agenda_stamp_checkin() IS\n 'Carimba a chegada ao ENTRAR em waiting, uma vez. Reentrar no estado n\u00E3o reinicia a espera (005).';\n\nDROP TRIGGER IF EXISTS appointments_stamp_checkin ON public.appointments;\nCREATE TRIGGER appointments_stamp_checkin\n BEFORE INSERT OR UPDATE OF status ON public.appointments\n FOR EACH ROW\n EXECUTE FUNCTION public.trg_agenda_stamp_checkin();\n\n-- Quem j\u00E1 est\u00E1 esperando desde antes deste arquivo herda o \u00FAltimo instante\n-- conhecido \u2014 impreciso, e ainda assim melhor que um travess\u00E3o.\nUPDATE public.appointments\n SET checked_in_at = updated_at\n WHERE status = 'waiting' AND checked_in_at IS NULL;\n";
7
7
  export declare const MIGRATION_006_THE_AGENDA_STOPS_PRICING_EVERY_INVOICE = "-- ---------------------------------------------------------------------------\n-- 006_the_agenda_stops_pricing_every_invoice.sql \u2014 ler DOIS agendamentos deixa\n-- de custar a apura\u00E7\u00E3o de 8.134 faturas.\n--\n-- O SINTOMA: `GET /rest/v1/v_appointments?...` devolvendo 500 com\n-- `57014: canceling statement due to statement timeout`, de forma reprodut\u00EDvel,\n-- ao abrir a agenda depois que existe agendamento na semana. Enquanto a semana\n-- estava vazia a mesma consulta respondia em 12ms \u2014 o que escondeu o problema e\n-- fez ele parecer intermitente.\n--\n-- \u2500\u2500 A medi\u00E7\u00E3o \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- EXPLAIN ANALYZE da consulta exata, com 2 agendamentos na janela:\n--\n-- Nested Loop Left Join actual time=17137..21252 rows=2\n-- Join Filter: (o_1.id = b.order_id)\n-- Rows Removed by Join Filter: 16268 \u2190 dezesseis mil\n-- -> Nested Loop ... actual time=5619..10590 rows=8134 loops=2\n-- Execution Time: 21272 ms\n--\n-- `v_invoice_balances` foi calculada POR INTEIRO \u2014 as 8.134 faturas do tenant \u2014\n-- e DUAS VEZES, uma por agendamento. S\u00F3 depois o `Join Filter` jogou fora\n-- 16.268 das 16.270 linhas para ficar com as 2 que interessavam.\n--\n-- \u2500\u2500 Por que o filtro n\u00E3o desceu \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- A view fazia:\n--\n-- LEFT JOIN v_invoice_balances inv ON inv.invoice_id = b.order_id\n--\n-- Isso \u00E9 um JOIN entre duas rela\u00E7\u00F5es, n\u00E3o um filtro constante. O planejador n\u00E3o\n-- tem por onde empurrar `b.order_id` para dentro de um agregado do outro lado:\n-- ele precisa produzir o lado direito inteiro para ent\u00E3o casar as chaves. E o\n-- lado direito \u00E9 um `GROUP BY order_id` sobre a \u00E1rvore financeira inteira\n-- (faturas \u2192 parcelas \u2192 aloca\u00E7\u00F5es \u2192 movimentos), cada n\u00EDvel pagando a RLS por\n-- linha. 8.134 linhas \u00D7 ~0,45ms de pol\u00EDtica \u00D7 tr\u00EAs n\u00EDveis \u00E9 o or\u00E7amento todo.\n--\n-- \u2500\u2500 A corre\u00E7\u00E3o \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- `LEFT JOIN LATERAL`. A correla\u00E7\u00E3o deixa de ser uma condi\u00E7\u00E3o de join e vira um\n-- PAR\u00C2METRO da subconsulta: `WHERE vb.invoice_id = b.order_id` \u00E9 avaliado com\n-- `b.order_id` j\u00E1 conhecido, e `invoice_id` \u00E9 exatamente a chave de agrupamento\n-- de `v_invoice_balances` (`GROUP BY t.order_id`, depois `o.id = tb.order_id`).\n-- Um qual sobre a chave de agrupamento desce por baixo do agregado, e o que\n-- era uma varredura de 8.134 faturas vira uma sondagem de \u00EDndice de UMA.\n--\n-- Nada muda no que a view devolve: mesmas colunas, mesma ordem, mesmos tipos \u2014\n-- `LEFT JOIN LATERAL \u2026 ON true` tem exatamente a sem\u00E2ntica do LEFT JOIN que\n-- substitui, inclusive o NULL quando n\u00E3o h\u00E1 fatura.\n--\n-- \u2500\u2500 O que esta migration N\u00C3O conserta \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- O multiplicador continua sendo a RLS por linha (~0,45ms por linha por\n-- tabela). Aqui ele deixa de doer porque o n\u00FAmero de linhas cai de 8.134 para\n-- 1, n\u00E3o porque ficou barato. Qualquer outra leitura que varra as tabelas\n-- financeiras inteiras vai reencontr\u00E1-lo \u2014 ver 016 e 019 para a forma de tirar\n-- pergunta constante de dentro do la\u00E7o.\n--\n-- MORA NA CORRENTE DA AGENDA, e n\u00E3o na do n\u00FAcleo, porque redefine\n-- `v_appointments` \u2014 view da agenda \u2014 lendo `v_invoice_balances`, view do\n-- financeiro. A corrente \u00E9 n\u00FAcleo \u2192 financeiro \u2192 \u2026 \u2192 agenda, ent\u00E3o este \u00E9 o\n-- \u00FAnico degrau em que as duas j\u00E1 existem. Nascida como 021 no n\u00FAcleo, ela\n-- falhava no `apply` com \"relation v_invoice_balances does not exist\", e o\n-- db-from-zero foi quem disse isso em voz alta.\n-- ---------------------------------------------------------------------------\n\n-- `WITH (security_invoker = true)` expl\u00EDcito: `CREATE OR REPLACE VIEW` N\u00C3O\n-- herda as op\u00E7\u00F5es da view que substitui, e sem isto ela volta a rodar com os\n-- direitos do dono \u2014 as pol\u00EDticas de quem l\u00EA deixam de valer. \u00C9 o que a\n-- asser\u00E7\u00E3o N16 da postura grada, e foi ela que pegou.\nCREATE OR REPLACE VIEW public.v_appointments WITH (security_invoker = true) AS\n SELECT b.id,\n b.tenant_id,\n b.kind,\n b.starts_at,\n b.ends_at,\n b.status,\n b.notes,\n b.order_id,\n b.location_id,\n b.metadata,\n b.created_at,\n b.updated_at,\n b.party_id AS client_id,\n COALESCE(pc.name, b.metadata ->> 'title'::text) AS client_name,\n pc.phone AS client_phone,\n pc.email AS client_email,\n pc.avatar_url AS client_avatar_url,\n b.assignee_id AS professional_id,\n ps.name AS professional_name,\n ps.avatar_url AS professional_avatar_url,\n l.name AS location_name,\n o.total AS order_total,\n o.status AS order_status,\n EXTRACT(epoch FROM COALESCE(b.ends_at, b.starts_at) - b.starts_at)::integer / 60 AS total_duration_minutes,\n o.reference_number,\n o.status AS stage,\n o.metadata ->> 'direction'::text AS direction,\n ( SELECT json_agg(json_build_object('id', oi.id, 'serviceId', oi.product_id, 'name', oi.name, 'durationMinutes', oi.duration_minutes, 'price', oi.unit_price, 'assigneeId', oi.assignee_id) ORDER BY oi.sort_order) AS json_agg\n FROM items oi\n WHERE oi.order_id = b.order_id) AS services,\n inv.status AS payment_status,\n inv.paid AS order_paid,\n inv.balance AS order_balance\n FROM appointments b\n LEFT JOIN people pc ON pc.id = b.party_id\n LEFT JOIN people ps ON ps.id = b.assignee_id\n LEFT JOIN app.units l ON l.id = b.location_id\n LEFT JOIN orders o ON o.id = b.order_id\n -- LATERAL e n\u00E3o JOIN: assim `b.order_id` \u00E9 PAR\u00C2METRO da subconsulta e o\n -- filtro desce por baixo do `GROUP BY order_id` de v_invoice_balances.\n -- Como JOIN, o agregado inteiro (8.134 faturas) era produzido por\n -- agendamento e descartado depois \u2014 21s para devolver 2 linhas.\n LEFT JOIN LATERAL (\n SELECT vb.status, vb.paid, vb.balance\n FROM v_invoice_balances vb\n WHERE vb.invoice_id = b.order_id\n LIMIT 1\n ) inv ON true;\n";
8
+ export declare const MIGRATION_007_O_CASCADE_LEVOU_A_AGENDA_JUNTO = "-- O CASCADE levou a agenda junto.\n--\n-- Sintoma: a tela de check-in (`#/agenda/checkin`) demora e n\u00E3o abre. A causa\n-- n\u00E3o est\u00E1 na agenda \u2014 est\u00E1 numa migration do FINANCEIRO, escrita tr\u00EAs dias\n-- antes.\n--\n-- As migrations 044 e 045 do plugin-financial come\u00E7am com:\n--\n-- DROP VIEW IF EXISTS public.v_financial_invoice_balances CASCADE;\n--\n-- e recriam, logo abaixo, `v_financial_invoice_balances` e `v_invoice_balances`\n-- \u2014 as duas que o autor (eu) sabia que dependiam. O CASCADE, por\u00E9m, n\u00E3o se\n-- limita ao que o autor conhece: ele derruba a \u00E1rvore inteira. Debaixo de\n-- `v_invoice_balances` moravam mais seis views, de dois plugins:\n--\n-- v_appointments (agenda \u2014 o read model do agendamento)\n-- \u251C\u2500\u2500 rep_appointments_by_period\n-- \u2502 \u2514\u2500\u2500 plg_agenda_rep_bookings\n-- \u251C\u2500\u2500 rep_confirmation_queue\n-- \u251C\u2500\u2500 rep_revenue_by_professional\n-- \u2514\u2500\u2500 rep_revenue_by_service\n-- plg_financial_rep_receivables (financeiro \u2014 restaurada na 053 de l\u00E1)\n--\n-- `v_appointments` l\u00EA `v_invoice_balances` desde a migration 006 desta corrente\n-- (o LATERAL que fez a agenda parar de apurar 8.134 faturas por agendamento).\n-- Essa depend\u00EAncia foi o conserto de um problema real e continua certa; o que\n-- ela criou, sem ningu\u00E9m notar, foi uma aresta do financeiro para a agenda \u2014\n-- e \u00E9 por ela que o CASCADE passou.\n--\n-- Nada gritou. O `DROP` n\u00E3o avisa o que levou, o `apply` das duas migrations\n-- passou verde, a lista de contas a pagar (o que eu estava consertando) ficou\n-- correta, e a agenda s\u00F3 falha quando algu\u00E9m a abre. O erro que chega ao\n-- navegador \u00E9 um 404 do PostgREST sobre uma rela\u00E7\u00E3o que o c\u00F3digo tem certeza\n-- de que existe \u2014 pista que aponta para o lugar errado.\n--\n-- \u2500\u2500 O que esta migration faz \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- Recria as cinco views da agenda, com o corpo EXATO em que estavam: o de\n-- `v_appointments` vem da migration 006 (a forma com LATERAL, n\u00E3o a do\n-- baseline), e os quatro relat\u00F3rios v\u00EAm do baseline, que n\u00E3o os alterou desde\n-- ent\u00E3o. Nenhuma coluna muda, nenhuma regra muda \u2014 o que muda \u00E9 existirem.\n--\n-- `WITH (security_invoker = true)` expl\u00EDcito em todas: uma view recriada n\u00E3o\n-- herda op\u00E7\u00E3o nenhuma, e sem isso ela passa a rodar com os direitos do dono \u2014\n-- a RLS de quem l\u00EA deixa de valer. Os GRANTs tamb\u00E9m s\u00E3o reescritos, porque\n-- morreram com as views.\n--\n-- \u2500\u2500 A li\u00E7\u00E3o, para a pr\u00F3xima \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- Antes de um `DROP \u2026 CASCADE`, pergunte ao banco quem depende:\n--\n-- SELECT DISTINCT dependente.relname\n-- FROM pg_depend d\n-- JOIN pg_rewrite r ON r.oid = d.objid\n-- JOIN pg_class dependente ON dependente.oid = r.ev_class\n-- WHERE d.refobjid = 'public.v_financial_invoice_balances'::regclass\n-- AND dependente.relname <> 'v_financial_invoice_balances';\n--\n-- A lista que voltar \u00E9 a lista que a migration tem que recriar. E depois de\n-- aplicar, o diff entre as views que o c\u00F3digo declara e as que o banco tem \u00E9 o\n-- que pega o que escapou \u2014 foi ele que encontrou estas seis.\n--\n-- Idempotente.\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- 1. v_appointments \u2014 corpo da migration 006 (LATERAL sobre v_invoice_balances)\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nCREATE OR REPLACE VIEW public.v_appointments WITH (security_invoker = true) AS\n SELECT b.id,\n b.tenant_id,\n b.kind,\n b.starts_at,\n b.ends_at,\n b.status,\n b.notes,\n b.order_id,\n b.location_id,\n b.metadata,\n b.created_at,\n b.updated_at,\n b.party_id AS client_id,\n COALESCE(pc.name, b.metadata ->> 'title'::text) AS client_name,\n pc.phone AS client_phone,\n pc.email AS client_email,\n pc.avatar_url AS client_avatar_url,\n b.assignee_id AS professional_id,\n ps.name AS professional_name,\n ps.avatar_url AS professional_avatar_url,\n l.name AS location_name,\n o.total AS order_total,\n o.status AS order_status,\n EXTRACT(epoch FROM COALESCE(b.ends_at, b.starts_at) - b.starts_at)::integer / 60 AS total_duration_minutes,\n o.reference_number,\n o.status AS stage,\n o.metadata ->> 'direction'::text AS direction,\n ( SELECT json_agg(json_build_object('id', oi.id, 'serviceId', oi.product_id, 'name', oi.name, 'durationMinutes', oi.duration_minutes, 'price', oi.unit_price, 'assigneeId', oi.assignee_id) ORDER BY oi.sort_order) AS json_agg\n FROM items oi\n WHERE oi.order_id = b.order_id) AS services,\n inv.status AS payment_status,\n inv.paid AS order_paid,\n inv.balance AS order_balance\n FROM appointments b\n LEFT JOIN people pc ON pc.id = b.party_id\n LEFT JOIN people ps ON ps.id = b.assignee_id\n LEFT JOIN app.units l ON l.id = b.location_id\n LEFT JOIN orders o ON o.id = b.order_id\n LEFT JOIN LATERAL (\n SELECT vb.status, vb.paid, vb.balance\n FROM v_invoice_balances vb\n WHERE vb.invoice_id = b.order_id\n LIMIT 1\n ) inv ON true;\n\nCOMMENT ON VIEW public.v_appointments IS\n 'O read model do agendamento: a linha do calend\u00E1rio com cliente, '\n 'profissional, unidade, itens e o estado de pagamento da Venda. L\u00EA '\n 'v_invoice_balances por LATERAL para que o filtro des\u00E7a por baixo do '\n 'agregado \u2014 como JOIN, a agenda apurava as faturas do tenant inteiro por '\n 'agendamento.';\n\nGRANT SELECT ON TABLE public.v_appointments TO authenticated, service_role;\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- 2. Os quatro relat\u00F3rios que penduram nela, e o quinto que pendura no primeiro\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nCREATE OR REPLACE VIEW public.rep_appointments_by_period WITH (security_invoker = true) AS\n SELECT tenant_id,\n id AS booking_id,\n (starts_at)::date AS date,\n starts_at,\n client_id,\n client_name,\n professional_id,\n professional_name,\n COALESCE(((services -> 0) ->> 'name'::text), ((services -> 0) ->> 'serviceName'::text)) AS service_name,\n status,\n order_total AS revenue,\n created_at,\n updated_at\n FROM public.v_appointments b;\n\nGRANT SELECT ON TABLE public.rep_appointments_by_period TO authenticated, service_role;\n\nCREATE OR REPLACE VIEW public.plg_agenda_rep_bookings WITH (security_invoker = true) AS\n SELECT tenant_id,\n booking_id,\n date,\n starts_at,\n client_id,\n client_name,\n professional_id,\n professional_name,\n service_name,\n status,\n revenue AS booked_revenue,\n CASE\n WHEN (status = ANY (ARRAY['cancelled'::text, 'no_show'::text])) THEN (0)::numeric\n ELSE COALESCE(revenue, (0)::numeric)\n END AS effective_revenue,\n CASE\n WHEN (status = ANY (ARRAY['cancelled'::text, 'no_show'::text])) THEN COALESCE(revenue, (0)::numeric)\n ELSE (0)::numeric\n END AS lost_revenue,\n CASE\n WHEN (status = 'completed'::text) THEN 1\n ELSE 0\n END AS is_completed,\n CASE\n WHEN (status = 'cancelled'::text) THEN 1\n ELSE 0\n END AS is_cancelled,\n CASE\n WHEN (status = 'no_show'::text) THEN 1\n ELSE 0\n END AS is_no_show,\n created_at,\n updated_at\n FROM public.rep_appointments_by_period b;\n\nGRANT SELECT ON TABLE public.plg_agenda_rep_bookings TO authenticated, service_role;\n\nCREATE OR REPLACE VIEW public.rep_confirmation_queue WITH (security_invoker = true) AS\n SELECT tenant_id,\n id AS booking_id,\n (starts_at)::date AS date,\n starts_at,\n client_id,\n client_name,\n professional_id,\n professional_name,\n COALESCE(((services -> 0) ->> 'name'::text), ((services -> 0) ->> 'serviceName'::text)) AS service_name,\n 'pending'::text AS confirmation_status,\n NULL::text AS confirmation_channel,\n NULL::timestamp with time zone AS confirmation_sent_at,\n NULL::timestamp with time zone AS confirmed_at,\n created_at,\n updated_at\n FROM public.v_appointments b\n WHERE ((status <> ALL (ARRAY['cancelled'::text, 'no_show'::text])) AND (starts_at >= (now() - '1 day'::interval)));\n\nGRANT SELECT ON TABLE public.rep_confirmation_queue TO authenticated, service_role;\n\nCREATE OR REPLACE VIEW public.rep_revenue_by_professional WITH (security_invoker = true) AS\n SELECT tenant_id,\n (starts_at)::date AS date,\n professional_id,\n COALESCE(professional_name, 'Sem profissional'::text) AS professional_name,\n (count(*))::integer AS appointment_count,\n (COALESCE(sum(NULLIF(order_total, (0)::numeric)), (0)::numeric))::numeric(14,2) AS total_revenue,\n (COALESCE(avg(NULLIF(order_total, (0)::numeric)), (0)::numeric))::numeric(14,2) AS avg_ticket,\n (0)::numeric(14,2) AS commission,\n max(updated_at) AS updated_at\n FROM public.v_appointments b\n WHERE (status <> ALL (ARRAY['cancelled'::text, 'no_show'::text]))\n GROUP BY tenant_id, ((starts_at)::date), professional_id, COALESCE(professional_name, 'Sem profissional'::text);\n\nGRANT SELECT ON TABLE public.rep_revenue_by_professional TO authenticated, service_role;\n\nCREATE OR REPLACE VIEW public.rep_revenue_by_service WITH (security_invoker = true) AS\n SELECT b.tenant_id,\n (b.starts_at)::date AS date,\n NULLIF((service_item.value ->> 'serviceId'::text), ''::text) AS service_id,\n COALESCE((service_item.value ->> 'name'::text), (service_item.value ->> 'serviceName'::text), 'Sem servico'::text) AS service_name,\n (count(DISTINCT b.id))::integer AS quantity,\n (COALESCE(sum((NULLIF((service_item.value ->> 'price'::text), ''::text))::numeric), (0)::numeric))::numeric(14,2) AS total_revenue,\n (COALESCE(avg((NULLIF((service_item.value ->> 'price'::text), ''::text))::numeric), (0)::numeric))::numeric(14,2) AS avg_ticket,\n max(b.updated_at) AS updated_at\n FROM (public.v_appointments b\n CROSS JOIN LATERAL jsonb_array_elements(COALESCE((b.services)::jsonb, '[]'::jsonb)) service_item(value))\n WHERE (b.status <> ALL (ARRAY['cancelled'::text, 'no_show'::text]))\n GROUP BY b.tenant_id, ((b.starts_at)::date), NULLIF((service_item.value ->> 'serviceId'::text), ''::text), COALESCE((service_item.value ->> 'name'::text), (service_item.value ->> 'serviceName'::text), 'Sem servico'::text);\n\nGRANT SELECT ON TABLE public.rep_revenue_by_service TO authenticated, service_role;\n";
9
+ export declare const MIGRATION_008_A_AGENDA_DO_V1_ATRAVESSA = "-- A agenda do V1 atravessa.\n--\n-- 354.206 agendamentos e 314.310 servi\u00E7os agendados do Espa\u00E7o Facial n\u00E3o tinham\n-- para onde ir: `appointments` e `appointment_services` n\u00E3o estavam nem na\n-- `migration.allowlist` nem na `migration.excluded_tables`. N\u00E3o era uma decis\u00E3o\n-- de deixar de fora \u2014 era a aus\u00EAncia de decis\u00E3o, e a tela de check-in abria\n-- vazia por isso.\n--\n-- \u2500\u2500 O de-para \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- V1 appointments \u2192 public.appointments\n-- V1 appointment_services \u2192 public.appointment_items\n--\n-- Tr\u00EAs refer\u00EAncias o ledger resolve, porque as tr\u00EAs j\u00E1 atravessaram:\n-- `client_id` \u2192 `public.people` (123.304 linhas), `professional_id` \u2192\n-- `public.people` (300) e `service_id` \u2192 `public.products` (122).\n--\n-- \u2500\u2500 A unidade, que o V1 n\u00E3o guarda no agendamento \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- `public.appointments` do V2 quer `unit_id` \u2014 \u00E9 por ela que a rede de 63\n-- unidades filtra a agenda. O V1 n\u00E3o tem essa coluna: ele guarda\n-- `service_location_id`, a SALA, e \u00E9 a sala que sabe a unidade\n-- (`service_locations.unit_id`, um uuid que casa com `companies.uuid`).\n--\n-- A ponte \u00E9 feita no extrator (`augment` do Step), n\u00E3o aqui, porque ela \u00E9\n-- conhecimento da ORIGEM. Medido em 19/09/2026, tenant 1:\n--\n-- agendamentos 354.206\n-- com sala que resolve a unidade 352.315 (99,5%)\n-- sem sala nenhuma 1.891\n--\n-- Os 1.891 sem sala entram com `unit_id` NULL \u2014 \u00E9 o que o V1 tem, e inventar\n-- unidade para eles seria coloc\u00E1-los na agenda de algu\u00E9m.\n--\n-- \u2500\u2500 O estado \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- O vocabul\u00E1rio do V1 tem sete palavras; o de `appointment_kinds.appointment`\n-- tem seis. Cinco s\u00E3o a mesma palavra. Sobram duas, e o mapa \u00E9 declarado no\n-- `writer_args` \u2014 dado, n\u00E3o c\u00F3digo:\n--\n-- unmarked \u2192 cancelled desmarcado \u00E9 cancelado; o V2 n\u00E3o tem a terceira\n-- palavra, e 'unmarked' n\u00E3o \u00E9 um estado, \u00E9 a\n-- aus\u00EAncia de um.\n-- waiting \u2192 confirmed a cliente chegou e espera. O V2 diz isso com\n-- `checked_in_at` (migration 003 desta corrente),\n-- que o pr\u00F3prio V1 j\u00E1 preenche \u2014 ent\u00E3o a palavra\n-- vira 'confirmed' e o REL\u00D3GIO carrega o resto.\n--\n-- O original fica em `metadata.migration.source_status`, como em toda tradu\u00E7\u00E3o\n-- de vocabul\u00E1rio deste motor.\n--\n-- \u2500\u2500 O que N\u00C3O vem \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- O agendamento do V1 n\u00E3o tem Venda. `order_id` fica NULL, e \u00E9 por isso que\n-- `v_appointments` traz `order_total`/`payment_status` vazios para o que\n-- migrou: o financeiro do V1 chega pela fatura (`invoices`), que \u00E9 outra\n-- tabela e outro caminho. Costurar as duas \u00E9 trabalho de reconcilia\u00E7\u00E3o, n\u00E3o de\n-- transporte, e faz\u00EA-lo aqui inventaria uma Venda que n\u00E3o existe.\n--\n-- Idempotente.\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- 1. O vocabul\u00E1rio fechado aprende duas palavras\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n-- A lista era LITERAL aqui, e literal \u00E9 o que quebra a corrente: a restri\u00E7\u00E3o \u00E9\n-- um vocabul\u00E1rio fechado, e reescrev\u00EA-la por extenso APAGA toda palavra que um\n-- passo anterior tenha acrescentado depois de algu\u00E9m escrever esta lista. Foi o\n-- que aconteceu com `fin_cash_session`, registrado por `financial/072`, e com\n-- `crm_pipeline`, que o spine acrescenta antes das cadeias de plugin: a linha j\u00E1\n-- existia no de-para e a restaurada n\u00E3o a admitia \u2014\n-- `check constraint \"allowlist_writer_check\" is violated by some row`, e o\n-- encadeamento do zero parava aqui.\n--\n-- Vira acr\u00E9scimo, que \u00E9 como `inventory/004` j\u00E1 fazia: l\u00EA a defini\u00E7\u00E3o vigente,\n-- costura as duas palavras novas dentro dela e devolve. O que veio antes\n-- sobrevive, e o passo seguinte herda tudo.\nDO $$\nDECLARE v_def text;\nBEGIN\n SELECT pg_get_constraintdef(oid) INTO v_def\n FROM pg_constraint WHERE conname = 'allowlist_writer_check';\n IF v_def IS NULL OR strpos(v_def, 'appointment_item') > 0 THEN RETURN; END IF;\n ALTER TABLE migration.allowlist DROP CONSTRAINT allowlist_writer_check;\n EXECUTE replace(\n 'ALTER TABLE migration.allowlist ADD CONSTRAINT allowlist_writer_check ' || v_def,\n '''person''::text',\n '''person''::text, ''appointment''::text, ''appointment_item''::text');\nEND $$;\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- 2. O writer do agendamento\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nCREATE OR REPLACE FUNCTION migration.upsert_appointment(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_map jsonb := coalesce(p_args -> 'status_map', '{}'::jsonb);\n v_kind text := coalesce(p_args ->> 'kind', 'appointment');\n v_src_status text := p_row ->> 'status';\n v_status text;\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_starts timestamptz;\n v_ends timestamptz;\n v_dur integer;\n v_party uuid;\n v_assignee uuid;\n v_unit uuid;\n v_existing uuid;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'an appointment needs an id');\n END IF;\n\n -- A hora \u00E9 o agendamento. Sem ela n\u00E3o h\u00E1 linha no calend\u00E1rio, e uma linha\n -- sem hora n\u00E3o \u00E9 um agendamento incompleto: \u00E9 outra coisa.\n v_starts := (p_row ->> 'starts_at')::timestamptz;\n IF v_starts IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value', 'reason', 'an appointment needs starts_at');\n END IF;\n\n IF NOT EXISTS (SELECT 1 FROM public.appointment_kinds k WHERE k.kind = v_kind) THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('appointment kind %L is not declared in public.appointment_kinds', v_kind));\n END IF;\n\n v_dur := nullif(p_row ->> 'duration_minutes', '')::integer;\n v_ends := v_starts + make_interval(mins => coalesce(v_dur, 0));\n\n v_status := coalesce(v_map ->> coalesce(v_src_status, ''), v_map ->> '*', v_src_status, 'scheduled');\n -- `appointment_kinds.statuses` \u00E9 text[], n\u00E3o jsonb: o vocabul\u00E1rio do tipo \u00E9\n -- uma lista de palavras, e `= ANY (array)` \u00E9 como se pergunta a ela.\n IF NOT EXISTS (SELECT 1 FROM public.appointment_kinds k\n WHERE k.kind = v_kind AND v_status = ANY (k.statuses)) THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',\n 'reason', format('status %L is not one of the statuses declared for kind %L', v_status, v_kind));\n END IF;\n v_meta := migration._deep_set(v_meta, ARRAY['migration', 'source_status'], to_jsonb(v_src_status));\n\n BEGIN v_party := (p_row ->> 'party_id')::uuid; EXCEPTION WHEN OTHERS THEN v_party := NULL; END;\n BEGIN v_assignee := (p_row ->> 'assignee_id')::uuid; EXCEPTION WHEN OTHERS THEN v_assignee := NULL; END;\n BEGIN v_unit := (p_row ->> 'unit_id')::uuid; EXCEPTION WHEN OTHERS THEN v_unit := NULL; END;\n\n SELECT tenant_id INTO v_existing FROM public.appointments WHERE id = p_id;\n IF v_existing IS NOT NULL AND v_existing <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant',\n 'reason', format('appointment %s already exists in another tenant', p_id));\n END IF;\n\n IF v_existing IS NULL THEN\n INSERT INTO public.appointments\n (id, tenant_id, kind, party_id, assignee_id, location_id, unit_id,\n starts_at, ends_at, status, notes, metadata, checked_in_at, created_at, updated_at)\n VALUES\n (p_id, p_tenant, v_kind, v_party, v_assignee, v_unit, v_unit,\n v_starts, v_ends, v_status, p_row ->> 'notes', v_meta,\n nullif(p_row ->> 'checked_in_at', '')::timestamptz,\n coalesce(nullif(p_row ->> 'created_at', '')::timestamptz, now()),\n coalesce(nullif(p_row ->> 'updated_at', '')::timestamptz, now()));\n v_created := true;\n ELSE\n UPDATE public.appointments SET\n kind = v_kind,\n party_id = coalesce(v_party, party_id),\n assignee_id = coalesce(v_assignee, assignee_id),\n -- `location_id` \u00E9 a unidade na leitura (v_appointments junta app.units\n -- por ela); `unit_id` \u00E9 a unidade na RLS. As duas apontam para a mesma\n -- coisa e \u00E9 isso que as mant\u00E9m verdadeiras juntas.\n location_id = coalesce(v_unit, location_id),\n starts_at = v_starts,\n ends_at = v_ends,\n status = CASE WHEN p_row ? 'status' THEN v_status ELSE status END,\n notes = CASE WHEN p_row ? 'notes' THEN p_row ->> 'notes' ELSE notes END,\n checked_in_at = CASE WHEN p_row ? 'checked_in_at'\n THEN nullif(p_row ->> 'checked_in_at', '')::timestamptz ELSE checked_in_at END,\n metadata = migration._merge_meta(metadata, v_meta),\n updated_at = coalesce(nullif(p_row ->> 'updated_at', '')::timestamptz, now())\n WHERE id = p_id;\n END IF;\n\n PERFORM migration._set_unit('public.appointments', p_id, v_unit);\n\n IF v_standalone THEN\n PERFORM migration._audit(p_tenant, 'public.appointments', p_id,\n jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END));\n END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id,\n 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- 3. O writer do servi\u00E7o agendado\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nCREATE OR REPLACE FUNCTION migration.upsert_appointment_item(\n p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb\n) RETURNS jsonb\nLANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''\nAS $function$\nDECLARE\n v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);\n v_booking uuid;\n v_service uuid;\n v_name text;\n v_unit uuid;\n v_existing uuid;\n v_created boolean := false;\n v_standalone boolean := migration._standalone();\nBEGIN\n IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;\n IF p_id IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state', 'reason', 'an appointment item needs an id');\n END IF;\n\n BEGIN v_booking := (p_row ->> 'booking_id')::uuid; EXCEPTION WHEN OTHERS THEN v_booking := NULL; END;\n -- Um item sem agendamento n\u00E3o \u00E9 um item solto, \u00E9 um item perdido. Recusa em\n -- vez de escrever uma linha que nenhuma tela alcan\u00E7a.\n IF v_booking IS NULL THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'fk',\n 'reason', 'an appointment item needs the appointment it belongs to');\n END IF;\n\n BEGIN v_service := (p_row ->> 'service_id')::uuid; EXCEPTION WHEN OTHERS THEN v_service := NULL; END;\n\n -- O nome \u00E9 NOT NULL no destino. A origem guarda o nome no SERVI\u00C7O, n\u00E3o no\n -- item, ent\u00E3o ele vem pela ponte do extrator; se faltar, o nome do produto\n -- que a refer\u00EAncia resolveu serve, e s\u00F3 ent\u00E3o o r\u00F3tulo gen\u00E9rico.\n v_name := coalesce(\n nullif(p_row ->> 'name', ''),\n (SELECT pr.name FROM public.products pr WHERE pr.id = v_service),\n 'Servi\u00E7o');\n\n SELECT a.unit_id INTO v_unit FROM public.appointments a WHERE a.id = v_booking;\n\n SELECT tenant_id INTO v_existing FROM public.appointment_items WHERE id = p_id;\n IF v_existing IS NOT NULL AND v_existing <> p_tenant THEN\n RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant',\n 'reason', format('appointment item %s already exists in another tenant', p_id));\n END IF;\n\n IF v_existing IS NULL THEN\n INSERT INTO public.appointment_items\n (id, tenant_id, booking_id, service_id, name, duration_minutes, price,\n sort_order, unit_id, metadata, created_at, updated_at)\n VALUES\n (p_id, p_tenant, v_booking, v_service, v_name,\n nullif(p_row ->> 'duration_minutes', '')::integer,\n coalesce(nullif(p_row ->> 'price', '')::numeric, 0),\n coalesce(nullif(p_row ->> 'sort_order', '')::integer, 0),\n v_unit, v_meta,\n coalesce(nullif(p_row ->> 'created_at', '')::timestamptz, now()),\n coalesce(nullif(p_row ->> 'updated_at', '')::timestamptz, now()));\n v_created := true;\n ELSE\n UPDATE public.appointment_items SET\n booking_id = v_booking,\n service_id = coalesce(v_service, service_id),\n name = v_name,\n duration_minutes = CASE WHEN p_row ? 'duration_minutes'\n THEN nullif(p_row ->> 'duration_minutes', '')::integer ELSE duration_minutes END,\n unit_id = coalesce(v_unit, unit_id),\n metadata = migration._merge_meta(metadata, v_meta),\n updated_at = coalesce(nullif(p_row ->> 'updated_at', '')::timestamptz, now())\n WHERE id = p_id;\n END IF;\n\n IF v_standalone THEN\n PERFORM migration._audit(p_tenant, 'public.appointment_items', p_id,\n jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END));\n END IF;\n RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id,\n 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);\nEND $function$;\n\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n-- 4. As duas linhas do de-para\n-- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- A allowlist congela a linha depois de escrita \u2014 mudar o de-para \u00E9 um arquivo\n-- de migration, n\u00E3o um UPDATE avulso, e o gatilho `trg_allowlist_frozen` \u00E9 quem\n-- cobra isso. Este \u00C9 o arquivo de migration, ent\u00E3o ele destrava; `set_local`\n-- para a destrava morrer com a transa\u00E7\u00E3o, tenha ela terminado bem ou mal.\n\nSET LOCAL migration.allowlist_unlock = 'on';\n\nSELECT migration._allow(\n 'appointments', 'public.appointments', 'agenda', 'appointment',\n jsonb_build_object(\n 'kind', 'appointment',\n 'status_map', jsonb_build_object(\n 'scheduled', 'scheduled',\n 'confirmed', 'confirmed',\n 'in_progress', 'in_progress',\n 'completed', 'completed',\n 'no_show', 'no_show',\n 'unmarked', 'cancelled',\n 'waiting', 'confirmed')),\n NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'appointment_date', 'starts_at',\n 'duration_minutes', 'duration_minutes',\n 'status', 'status',\n 'notes', 'notes',\n 'checked_in_at', 'checked_in_at',\n 'created_at', 'created_at',\n 'updated_at', 'updated_at',\n 'client_id', 'party_id@clients',\n 'professional_id', 'assignee_id@professionals',\n 'unit_company_id', 'unit_id@companies',\n 'tenant_id', '-',\n 'checkout_confirmed_by', '-',\n 'price', 'jsonb:agenda.v1_price',\n 'is_prepaid', 'jsonb:agenda.is_prepaid',\n 'prepaid_invoice_item_id', 'jsonb:agenda.v1_prepaid_invoice_item_ref',\n 'checkout_confirmed_at', 'jsonb:agenda.checkout_confirmed_at',\n 'service_location_id', 'jsonb:agenda.v1_service_location_ref',\n 'opportunity_id', 'jsonb:crm.v1_opportunity_ref',\n 'whatsapp_sent', 'jsonb:agenda.whatsapp_sent',\n 'whatsapp_confirmed', 'jsonb:agenda.whatsapp_confirmed'),\n 'O Compromisso do V1. A unidade n\u00E3o vem de uma coluna: vem da SALA '\n '(service_location_id \u2192 service_locations.unit_id \u2192 companies.uuid), e a '\n 'ponte \u00E9 feita no extrator. 99,5% resolvem; os 1.891 sem sala entram sem '\n 'unidade, que \u00E9 o que a origem tem. Sem Venda: order_id fica NULL porque o '\n 'financeiro do V1 chega pela fatura, que \u00E9 outro caminho.'\n);\n\nSELECT migration._allow(\n 'appointment_services', 'public.appointment_items', 'agenda', 'appointment_item',\n '{}'::jsonb,\n NULL::text[],\n jsonb_build_object(\n 'uuid', 'id',\n 'appointment_id', 'booking_id@appointments',\n 'service_id', 'service_id@services',\n 'service_name', 'name',\n 'duration_minutes', 'duration_minutes',\n 'price', 'price',\n 'created_at', 'created_at',\n 'updated_at', 'updated_at',\n 'tenant_id', '-',\n 'is_revision', 'jsonb:agenda.is_revision',\n 'revision_number', 'jsonb:agenda.revision_number',\n 'parent_journey_id', 'jsonb:agenda.v1_parent_journey_ref',\n 'added_during_execution', 'jsonb:agenda.added_during_execution',\n 'source_evaluation_execution_id','jsonb:agenda.v1_source_evaluation_ref'),\n 'O servi\u00E7o dentro do Compromisso. `service_name` n\u00E3o existe no V1 \u2014 vem da '\n 'ponte do extrator, porque o destino exige nome e a origem guarda o nome no '\n 'servi\u00E7o, n\u00E3o na linha. O pre\u00E7o \u00E9 o pre\u00E7o praticado NAQUELE agendamento, e '\n 'por isso vem cru e n\u00E3o da tabela de pre\u00E7os.'\n);\n\nDELETE FROM migration.excluded_tables WHERE source_table IN ('appointments', 'appointment_services');\n\nRESET migration.allowlist_unlock;\n\n\n-- \u2500\u2500 quem pode executar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- Fun\u00E7\u00E3o nova nasce execut\u00E1vel por PUBLIC, e `anon`/`authenticated` herdam\n-- dali. Estas s\u00E3o de PORTE: SECURITY DEFINER, escrevem dados de qualquer\n-- inquilino. S\u00F3 o service_role, que \u00E9 quem roda a onda.\nREVOKE ALL ON FUNCTION migration.upsert_appointment(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration.upsert_appointment(uuid, uuid, jsonb, jsonb) TO service_role;\nREVOKE ALL ON FUNCTION migration.upsert_appointment_item(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;\nGRANT EXECUTE ON FUNCTION migration.upsert_appointment_item(uuid, uuid, jsonb, jsonb) TO service_role;\n";
10
+ export declare const MIGRATION_009_O_EXECUTOR_SE_ACHA_PELO_PEDIDO = "-- O executor se acha pelo pedido.\n--\n-- `trg_financial_seed_item_participant` dispara em todo INSERT de\n-- `public.items` e pergunta uma coisa s\u00F3:\n--\n-- SELECT a.assignee_id, a.unit_id\n-- FROM public.appointments a\n-- WHERE a.order_id = NEW.order_id AND a.assignee_id IS NOT NULL\n-- ORDER BY a.created_at LIMIT 1\n--\n-- \u00C9 a pergunta certa \u2014 \"quem executou o servi\u00E7o desta linha?\" \u2014, e \u00E9 o que\n-- alimenta a comiss\u00E3o. O que faltava era o \u00EDndice que a responde:\n-- `public.appointments` tem dez \u00EDndices e **nenhum por `order_id`**.\n--\n-- \u2500\u2500 Medido \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- EXPLAIN ANALYZE de um INSERT em `public.items`, tenant Espa\u00E7o Facial,\n-- 20/09/2026, com 179.613 agendamentos na tabela:\n--\n-- Trigger items_financial_seed_participant: time=56.799\n-- ... os outros dezesseis gatilhos somados: time= 5.268\n-- Execution Time: 62.067 ms\n--\n-- **91% do custo de inserir um item.** E o n\u00FAmero cresce com a agenda: o mesmo\n-- INSERT vai custar o dobro quando os 354.208 compromissos do V1 tiverem\n-- entrado, porque a varredura \u00E9 da tabela inteira.\n--\n-- \u2500\u2500 N\u00E3o \u00E9 s\u00F3 da migra\u00E7\u00E3o \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- Vale registrar, porque o \u00EDndice se paga duas vezes: este gatilho roda toda\n-- vez que algu\u00E9m acrescenta uma linha a uma venda no app \u2014 a comanda, o\n-- carrinho, o atendimento que ganha um produto no fim. Numa rede com agenda\n-- cheia, cada item somava dezenas de milissegundos de varredura que ningu\u00E9m\n-- pediu. O porte s\u00F3 foi quem gritou primeiro: 220.462 itens a 62 ms s\u00E3o quatro\n-- horas.\n--\n-- \u2500\u2500 Por que PARCIAL \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n--\n-- O compromisso do V1 n\u00E3o tem Venda: ele chega com `order_id` NULL, e s\u00E3o\n-- centenas de milhares assim. Um \u00EDndice completo carregaria todos eles para\n-- responder sobre os poucos que t\u00EAm pedido. O parcial indexa s\u00F3 as linhas que\n-- a consulta pode encontrar \u2014 e o planejador o usa porque o predicado\n-- (`order_id = \u2026`) implica a condi\u00E7\u00E3o do \u00EDndice.\n--\n-- `CONCURRENTLY` porque o app est\u00E1 no ar: a forma normal tranca a escrita em\n-- `appointments` enquanto constr\u00F3i, e uma recep\u00E7\u00E3o marcando hor\u00E1rio n\u00E3o tem\n-- por que esperar por um \u00EDndice.\n--\n-- Idempotente.\n\n-- Sem CONCURRENTLY: \u00E9 o \u00FAnico arquivo do reposit\u00F3rio que o usava, e ele n\u00E3o\n-- roda dentro de transa\u00E7\u00E3o \u2014 o encadeamento aplica cada migra\u00E7\u00E3o numa. A\n-- tabela \u00E9 pequena o bastante para o bloqueio curto de um CREATE INDEX comum.\nCREATE INDEX IF NOT EXISTS appointments_order_idx\n ON public.appointments (order_id)\n WHERE order_id IS NOT NULL;\n\nCOMMENT ON INDEX public.appointments_order_idx IS\n 'O caminho de \"quem executou o servi\u00E7o desta linha da venda\", que '\n 'trg_financial_seed_item_participant percorre em todo INSERT de items. Sem '\n 'ele a pergunta era varredura da agenda inteira: 56,8 ms dos 62 ms de '\n 'inserir um item, medido com 179.613 compromissos.';\n";
8
11
  export declare const MIGRATIONS: Array<{
9
12
  id: string;
10
13
  sql: string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,sBAAsB,05vEAg2ClC,CAAA;AAED,eAAO,MAAM,uDAAuD,mvEAgDnE,CAAA;AAED,eAAO,MAAM,4CAA4C,w6EAqDyB,CAAA;AAElF,eAAO,MAAM,oDAAoD,61CAyBhE,CAAA;AAED,eAAO,MAAM,8CAA8C,w7EA4C1D,CAAA;AAED,eAAO,MAAM,iDAAiD,woFAiD7D,CAAA;AAED,eAAO,MAAM,oDAAoD,kxOAmHhE,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAQzD,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,sBAAsB,05vEAg2ClC,CAAA;AAED,eAAO,MAAM,uDAAuD,mvEAgDnE,CAAA;AAED,eAAO,MAAM,4CAA4C,w6EAqDyB,CAAA;AAElF,eAAO,MAAM,oDAAoD,61CAyBhE,CAAA;AAED,eAAO,MAAM,8CAA8C,w7EA4C1D,CAAA;AAED,eAAO,MAAM,iDAAiD,woFAiD7D,CAAA;AAED,eAAO,MAAM,oDAAoD,kxOAmHhE,CAAA;AAED,eAAO,MAAM,4CAA4C,mrZA2OxD,CAAA;AAED,eAAO,MAAM,sCAAsC,8tuBAoXlD,CAAA;AAED,eAAO,MAAM,4CAA4C,wkIA8DxD,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAWzD,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"CalendarView.d.ts","sourceRoot":"","sources":["../../src/views/CalendarView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA6E,MAAM,OAAO,CAAA;AA+OjG;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAYnD;AAqFD,wBAAgB,YAAY,sBAo/B3B"}
1
+ {"version":3,"file":"CalendarView.d.ts","sourceRoot":"","sources":["../../src/views/CalendarView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA6E,MAAM,OAAO,CAAA;AA+OjG;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAYnD;AAqFD,wBAAgB,YAAY,sBAi+B3B"}
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "status": "beta",
5
5
  "dependencies": []
6
6
  },
7
- "version": "0.13.4",
7
+ "version": "0.13.6",
8
8
  "description": "Fayz SDK — plugin-agenda plugin",
9
9
  "type": "module",
10
10
  "sideEffects": [
@@ -42,10 +42,10 @@
42
42
  "@tanstack/react-table": "^8.20.0",
43
43
  "@fullcalendar/resource-timegrid": "^6.1.0",
44
44
  "@fullcalendar/list": "^6.1.0",
45
- "@fayz-ai/core": "^0.22.0",
46
- "@fayz-ai/ui": "^0.22.0",
47
- "@fayz-ai/admin": "^0.22.0",
48
- "@fayz-ai/plugin-financial": "^0.14.2"
45
+ "@fayz-ai/core": "^0.23.0",
46
+ "@fayz-ai/ui": "^0.24.0",
47
+ "@fayz-ai/admin": "^0.24.0",
48
+ "@fayz-ai/plugin-financial": "^0.14.6"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@types/react": "^18.3.0",