@fayz-ai/plugin-clinical 0.2.7 → 0.3.1

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.
@@ -20,6 +20,8 @@ export declare const MIGRATION_019_ADIAR_E_UMA_ESCOLHA = "-- Adiar \u00E9 uma es
20
20
  export declare const MIGRATION_020_COBRAR_UM_AGENDAMENTO_SEM_PEDIDO_LEVANTA_O_PEDIDO = "-- ============================================================================\n-- Cobrar um agendamento SEM pedido levanta o pedido primeiro\n--\n-- SINTOMA, visto na tela (23-09, Juliane): a aba Financeiro acende o vermelho\n-- do V1 (\"atendimento de hoje sem cobran\u00E7a levantada\"), mas a conta n\u00E3o\n-- oferece caminho nenhum \u2014 o agendamento veio da MIGRA\u00C7\u00C3O do V1 com\n-- `order_id` nulo, e a linha do servi\u00E7o mora em `appointment_items`, n\u00E3o em\n-- `items`. O V1 nunca teve esse beco: \"Gerar Recebimento\" l\u00E1 CRIA a fatura a\n-- partir dos servi\u00E7os do agendamento.\n--\n-- A tradu\u00E7\u00E3o para o V2 respeita o caminho \u00FAnico do dinheiro: a cobran\u00E7a n\u00E3o\n-- escreve fatura \u2014 ela LEVANTA O PEDIDO pela porta do n\u00FAcleo (`order_create`,\n-- a mesma que `agenda_booking_create` usa) com as linhas COM PRE\u00C7O dos\n-- `appointment_items`, amarra o pedido ao agendamento e segue o fechamento\n-- normal (`orders_raise_receivable_on_close`).\n--\n-- Linha sem pre\u00E7o n\u00E3o vira pedido: revis\u00E3o e avalia\u00E7\u00E3o gratuitas continuam\n-- sem cobran\u00E7a, e a recusa `nada_contratado` continua falando por elas.\n-- ============================================================================\n\nCREATE OR REPLACE FUNCTION public.clinical_checkout_charge(p_booking uuid)\nRETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public', 'app'\nAS $fn$\nDECLARE\n a record; v_linhas int; v_total numeric; v_mov int; v_lines jsonb; v_order uuid;\nBEGIN\n SELECT ap.id, ap.tenant_id, ap.kind, ap.status, ap.order_id,\n ap.party_id, ap.assignee_id, ap.unit_id\n INTO a\n FROM public.appointments ap WHERE ap.id = p_booking;\n IF NOT FOUND THEN RETURN jsonb_build_object('ok', false, 'reason', 'not_found'); END IF;\n IF auth.uid() IS NULL THEN RETURN jsonb_build_object('ok', false, 'reason', 'sem_sessao'); END IF;\n -- O agendamento de OUTRA conta n\u00E3o existe para esta sess\u00E3o.\n IF a.tenant_id IS DISTINCT FROM app.current_tenant_id() THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'not_found');\n END IF;\n\n IF a.order_id IS NULL THEN\n -- As linhas com pre\u00E7o do agendamento migrado. Sem nenhuma, n\u00E3o h\u00E1 o que\n -- cobrar \u2014 e isso \u00E9 um estado leg\u00EDtimo (revis\u00E3o, avalia\u00E7\u00E3o gratuita).\n SELECT jsonb_agg(jsonb_build_object(\n 'service_id', ai.service_id,\n 'name', ai.name,\n 'quantity', 1,\n 'unit_price', ai.price,\n 'sort_order', ai.sort_order,\n 'duration_minutes', ai.duration_minutes,\n 'assignee_id', coalesce(ai.assignee_id, a.assignee_id)\n ) ORDER BY ai.sort_order)\n INTO v_lines\n FROM public.appointment_items ai\n WHERE ai.booking_id = a.id AND coalesce(ai.price, 0) > 0;\n\n IF v_lines IS NULL THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'nada_contratado');\n END IF;\n\n v_order := public.order_create(\n p_kind => coalesce(a.kind, 'appointment'),\n p_lines => v_lines,\n p_party_id => a.party_id,\n p_unit_id => a.unit_id,\n p_status => 'scheduled',\n p_assignee_id => a.assignee_id,\n p_metadata => jsonb_build_object('source', 'clinical_checkout_charge', 'booking_id', a.id));\n\n UPDATE public.appointments SET order_id = v_order, updated_at = now() WHERE id = a.id;\n a.order_id := v_order;\n END IF;\n\n SELECT count(*), coalesce(sum(i.total), 0) INTO v_linhas, v_total\n FROM public.items i WHERE i.order_id = a.order_id;\n IF v_linhas = 0 THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'nada_contratado');\n END IF;\n\n -- Idempotente: o pedido j\u00E1 fechado devolve o que existe, sem levantar um\n -- segundo receb\u00EDvel para a mesma visita.\n SELECT count(*) INTO v_mov FROM public.plg_financial_movements m WHERE m.invoice_id = a.order_id;\n IF v_mov > 0 THEN\n RETURN jsonb_build_object('ok', true, 'ja_cobrado', true, 'total', v_total);\n END IF;\n\n UPDATE public.orders SET status = 'completed', updated_at = now()\n WHERE id = a.order_id AND status <> 'completed';\n\n SELECT count(*) INTO v_mov FROM public.plg_financial_movements m WHERE m.invoice_id = a.order_id;\n RETURN jsonb_build_object('ok', true, 'total', v_total, 'movimentos', v_mov);\nEND $fn$;\n\nGRANT EXECUTE ON FUNCTION public.clinical_checkout_charge(uuid) TO authenticated;\n";
21
21
  export declare const MIGRATION_021_AGENDAMENTO_NASCIDO_NO_V2_TAMBEM_TEM_REGUA = "-- ============================================================================\n-- O AGENDAMENTO NASCIDO NO V2 TAMB\u00C9M TEM R\u00C9GUA\n--\n-- O Iniciar atendimento percorre `appointment_items` \u2014 a linha do agendamento\n-- do V1, onde as 314 mil linhas migradas moram. S\u00F3 que o Novo Agendamento do\n-- V2 grava as linhas pelo caminho do PEDIDO (`agenda_booking_create` \u2192\n-- `orders` + `items`), e nunca escreve `appointment_items`. Resultado, visto\n-- na tela em 23/09: o bot\u00E3o vira \"Em atendimento\", e a r\u00E9gua nasce VAZIA \u2014\n-- zero sess\u00F5es, zero exig\u00EAncias, bolas nenhuma. Todo agendamento criado pela\n-- pr\u00F3pria UI do V2 ca\u00EDa nisso; s\u00F3 os migrados do V1 funcionavam.\n--\n-- O conserto MATERIALIZA a linha do agendamento na hora de come\u00E7ar: se o\n-- agendamento tem pedido e ainda n\u00E3o tem `appointment_items`, elas nascem das\n-- linhas do pedido \u2014 e o resto do fluxo (sess\u00F5es, FK de `booking_item_id`,\n-- r\u00E9gua, produtos por servi\u00E7o) continua lendo o modelo que sempre leu.\n-- Idempotente: s\u00F3 insere quando n\u00E3o h\u00E1 linha nenhuma para o agendamento.\n-- ============================================================================\n\nCREATE OR REPLACE FUNCTION public.clinical_attendance_start(p_booking uuid)\nRETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public', 'app'\nAS $fn$\nDECLARE\n a record; v_person uuid; v_created int := 0; v_adotadas int := 0;\n v_item record; v_refresh jsonb; v_antiga uuid;\nBEGIN\n SELECT ap.id, ap.tenant_id, ap.unit_id, ap.party_id, ap.assignee_id, ap.status,\n ap.starts_at, ap.order_id\n INTO a FROM public.appointments ap WHERE ap.id = p_booking;\n IF NOT FOUND THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'not_found');\n END IF;\n\n v_person := public.current_person_id();\n IF v_person IS NULL THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'sem_ficha_de_profissional');\n END IF;\n IF a.assignee_id IS DISTINCT FROM v_person THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'nao_e_seu_atendimento');\n END IF;\n IF a.starts_at::date <> (now() AT TIME ZONE 'America/Sao_Paulo')::date THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'nao_e_de_hoje');\n END IF;\n IF a.status IN ('completed', 'cancelled', 'no_show') THEN\n RETURN jsonb_build_object('ok', false, 'reason', 'ja_encerrado', 'status', a.status);\n END IF;\n\n -- Agendamento nascido no V2: as linhas vivem no pedido. Materializa a linha\n -- do agendamento \u2014 uma vez s\u00F3 \u2014 para o resto do fluxo ter onde se pendurar.\n IF a.order_id IS NOT NULL\n AND NOT EXISTS (SELECT 1 FROM public.appointment_items ai WHERE ai.booking_id = a.id) THEN\n INSERT INTO public.appointment_items (\n tenant_id, unit_id, booking_id, service_id, name, price,\n duration_minutes, assignee_id, sort_order)\n -- `coalesce(product_id, service_id)`: o `order_create` grava o servi\u00E7o em\n -- `service_id`; a edi\u00E7\u00E3o pela agenda grava em `product_id` (spine 190).\n -- As duas gera\u00E7\u00F5es de linha existem no banco.\n SELECT a.tenant_id, a.unit_id, a.id, coalesce(it.product_id, it.service_id),\n it.name, it.unit_price,\n it.duration_minutes, coalesce(it.assignee_id, a.assignee_id),\n coalesce(it.sort_order, 0)\n FROM public.items it\n WHERE it.order_id = a.order_id;\n END IF;\n\n FOR v_item IN\n SELECT ai.id, ai.service_id, ai.assignee_id\n FROM public.appointment_items ai\n WHERE ai.booking_id = p_booking\n LOOP\n -- J\u00E1 existe sess\u00E3o para este item? (dois toques, duas pessoas ao mesmo tempo)\n CONTINUE WHEN EXISTS (\n SELECT 1 FROM public.plg_clinical_sessions cs\n WHERE cs.booking_id = p_booking AND cs.booking_item_id = v_item.id);\n\n -- O QUE FOI COMPRADO ANTES E AINDA N\u00C3O SAIU: adota em vez de criar.\n SELECT cs.id INTO v_antiga\n FROM public.plg_clinical_sessions cs\n WHERE cs.tenant_id = a.tenant_id\n AND cs.party_id = a.party_id\n AND cs.service_id = v_item.service_id\n AND cs.booking_id IS NULL\n AND cs.status IN ('contracted', 'quoted', 'pending')\n ORDER BY cs.created_at\n LIMIT 1;\n\n IF v_antiga IS NOT NULL THEN\n UPDATE public.plg_clinical_sessions\n SET booking_id = p_booking,\n booking_item_id = v_item.id,\n unit_id = coalesce(a.unit_id, unit_id),\n assignee_id = coalesce(v_item.assignee_id, a.assignee_id),\n performed_by_id = v_person,\n status = 'in_service',\n scheduled_for = a.starts_at,\n started_at = coalesce(started_at, now()),\n updated_at = now()\n WHERE id = v_antiga;\n v_adotadas := v_adotadas + 1;\n ELSE\n INSERT INTO public.plg_clinical_sessions (\n tenant_id, unit_id, status, booking_id, booking_item_id, party_id,\n assignee_id, performed_by_id, service_id, scheduled_for, started_at)\n VALUES (a.tenant_id, a.unit_id, 'in_service', a.id, v_item.id, a.party_id,\n coalesce(v_item.assignee_id, a.assignee_id), v_person, v_item.service_id,\n a.starts_at, now());\n v_created := v_created + 1;\n END IF;\n END LOOP;\n\n UPDATE public.appointments\n SET status = 'in_progress', updated_at = now()\n WHERE id = p_booking AND status <> 'in_progress';\n\n v_refresh := public.clinical_requirements_refresh(p_booking);\n\n RETURN jsonb_build_object(\n 'ok', true,\n 'sessoes_criadas', v_created,\n 'sessoes_adotadas', v_adotadas,\n 'sessoes', (SELECT count(*) FROM public.plg_clinical_sessions WHERE booking_id = p_booking),\n 'regua', v_refresh);\nEND $fn$;\n\nGRANT EXECUTE ON FUNCTION public.clinical_attendance_start(uuid) TO authenticated;\n";
22
22
  export declare const MIGRATION_022_A_VISTA_DO_QUE_EXECUTAR_RESPEITA_A_CERCA = "-- ============================================================================\n-- THE \"TO EXECUTE\" VIEW READS WITH THE CALLER'S PRIVILEGES\n--\n-- `v_clinical_to_execute` was created in 014 without `security_invoker`, so it\n-- ran with the privileges of the role that OWNS it, and a view owned by a\n-- privileged role does not apply the row policies of the tables beneath it.\n-- `plg_clinical_sessions`, `items` and `plg_financial_movements` are all\n-- tenant-scoped with RLS; read through this view, they were not.\n--\n-- Not theoretical: the view answers, per party, how much a patient has already\n-- PAID for procedures not yet performed. Without the fence that is every\n-- clinic's prepaid balance, to anyone holding any session.\n--\n-- \u2500\u2500 why nobody saw it \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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-- N16 of the posture suite grades exactly this (\"every public view over a\n-- tenant_id table is security_invoker\"), and it stayed green throughout:\n-- plugin-clinical was never listed in `packages/db/chain.json`, so its objects\n-- were never built on the bench and never graded. The plugin reached a live\n-- pool on 2026-09-22 and no board at all. Putting it in the chain is what made\n-- the suite fail, on the first run, in the expected place.\n--\n-- \u2500\u2500 why a new file, and not an edit to 014 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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-- 014 is in `public._migrations` on a live pool with checksum 6f04df24cdb2,\n-- and the ledger treats a changed checksum on an applied file as a HARD STOP\n-- (cli/src/lib/ledger.ts). Editing it in place would not fix the view \u2014 it\n-- would stop `fayz db apply` on every cluster that already ran it.\n--\n-- The body below is 014's, copied unchanged. `CREATE OR REPLACE VIEW` refuses\n-- a different column list, and the only thing that may differ here is who the\n-- row filter runs as.\n-- ============================================================================\n\nCREATE OR REPLACE VIEW public.v_clinical_to_execute WITH (security_invoker = true) AS\nWITH sessao AS (\n SELECT\n cs.tenant_id,\n cs.party_id,\n cs.order_item_id,\n cs.status,\n i.order_id,\n coalesce(i.total, 0) AS item_total\n FROM public.plg_clinical_sessions cs\n JOIN public.items i ON i.id = cs.order_item_id\n WHERE cs.party_id IS NOT NULL\n AND cs.status IN ('contracted', 'quoted', 'in_service', 'scheduled', 'pending')\n),\n-- Quanto do PEDIDO j\u00E1 foi recebido, em propor\u00E7\u00E3o: o pagamento \u00E9 por parcela e\n-- n\u00E3o por item, ent\u00E3o a fatia paga de cada item \u00E9 a fatia paga do pedido.\npago AS (\n SELECT m.invoice_id AS order_id,\n sum(coalesce(m.paid_amount, 0)) AS pago,\n nullif(sum(coalesce(m.amount, 0)), 0) AS total\n FROM public.plg_financial_movements m\n WHERE m.direction = 'credit'\n GROUP BY m.invoice_id\n)\nSELECT\n s.tenant_id,\n s.party_id,\n sum(s.item_total * coalesce(p.pago / p.total, 0))::numeric(14,2) AS paid_to_execute,\n sum(s.item_total * (1 - coalesce(p.pago / p.total, 0)))::numeric(14,2) AS open_to_execute,\n count(*)::int AS sessions_to_execute\nFROM sessao s\nLEFT JOIN pago p ON p.order_id = s.order_id\nGROUP BY s.tenant_id, s.party_id;\n";
23
+ export declare const MIGRATION_023_A_REVISAO_SE_ESCOLHE_AO_AGENDAR = "-- ============================================================================\n-- 023 \u2014 The revision is chosen when it is booked, not only during the visit.\n-- ============================================================================\n-- V1 asks \"De qual servi\u00E7o \u00E9 esta revis\u00E3o?\" the moment the reception picks the\n-- Revis\u00E3o service in Novo Agendamento (FAY-1613). 015 answered it only from an\n-- existing booking. This lists the same candidates for a PERSON, with the ordinal\n-- the next revision of each will take (V1 shows \"1\u00AA Revis\u00E3o\").\n--\n-- Also closes a gap in 015: `clinical_revision_candidates(p_booking)` is a\n-- definer function that read any booking id it was given, so a member of one\n-- tenant could list another tenant's patient history by guessing an id. Both\n-- now answer only inside the caller's tenant.\n--\n-- Idempotent.\n\nCREATE OR REPLACE FUNCTION public.clinical_revision_candidates_for_person(p_person uuid)\nRETURNS TABLE(session_id uuid, service_id uuid, service_name text, executed_on timestamp with time zone,\n deadline_days integer, days_since integer, within_deadline boolean, next_revision_number integer)\nLANGUAGE sql\nSTABLE SECURITY DEFINER\nSET search_path TO 'public', 'app'\nAS $function$\n SELECT\n cs.id,\n cs.service_id,\n p.name,\n coalesce(cs.ended_at, cs.scheduled_for) AS executed_on,\n rev.deadline_days,\n (current_date - coalesce(cs.ended_at, cs.scheduled_for)::date)::int AS days_since,\n CASE WHEN rev.deadline_days IS NULL THEN NULL\n ELSE (current_date - coalesce(cs.ended_at, cs.scheduled_for)::date) <= rev.deadline_days\n END AS within_deadline,\n coalesce((SELECT max(r.revision_number) FROM public.plg_clinical_sessions r\n WHERE r.tenant_id = cs.tenant_id AND r.parent_session_id = cs.id), 0) + 1\n FROM public.plg_clinical_sessions cs\n LEFT JOIN public.products p ON p.id = cs.service_id\n LEFT JOIN LATERAL (\n SELECT r.deadline_days FROM public.plg_clinical_service_revisions r\n WHERE r.tenant_id = cs.tenant_id AND r.service_id = cs.service_id\n ORDER BY r.revision_number LIMIT 1\n ) rev ON true\n WHERE cs.tenant_id = app.current_tenant_id()\n AND cs.party_id = p_person\n AND cs.status = 'executed'\n AND cs.parent_session_id IS NULL\n ORDER BY coalesce(cs.ended_at, cs.scheduled_for) DESC NULLS LAST\n LIMIT 50;\n$function$;\n\nREVOKE ALL ON FUNCTION public.clinical_revision_candidates_for_person(uuid) FROM PUBLIC, anon;\nGRANT EXECUTE ON FUNCTION public.clinical_revision_candidates_for_person(uuid) TO authenticated;\n\nCREATE OR REPLACE FUNCTION public.clinical_revision_candidates(p_booking uuid)\nRETURNS TABLE(session_id uuid, service_id uuid, service_name text, executed_on timestamp with time zone,\n deadline_days integer, days_since integer, within_deadline boolean)\nLANGUAGE sql\nSTABLE SECURITY DEFINER\nSET search_path TO 'public', 'app'\nAS $function$\n WITH alvo AS (\n SELECT ap.tenant_id, ap.party_id FROM public.appointments ap\n WHERE ap.id = p_booking AND ap.tenant_id = app.current_tenant_id()\n )\n SELECT\n cs.id,\n cs.service_id,\n p.name,\n coalesce(cs.ended_at, cs.scheduled_for) AS executed_on,\n rev.deadline_days,\n (current_date - coalesce(cs.ended_at, cs.scheduled_for)::date)::int AS days_since,\n CASE WHEN rev.deadline_days IS NULL THEN NULL\n ELSE (current_date - coalesce(cs.ended_at, cs.scheduled_for)::date) <= rev.deadline_days\n END AS within_deadline\n FROM public.plg_clinical_sessions cs\n JOIN alvo a ON a.tenant_id = cs.tenant_id AND a.party_id = cs.party_id\n LEFT JOIN public.products p ON p.id = cs.service_id\n LEFT JOIN LATERAL (\n SELECT r.deadline_days FROM public.plg_clinical_service_revisions r\n WHERE r.tenant_id = cs.tenant_id AND r.service_id = cs.service_id\n ORDER BY r.revision_number LIMIT 1\n ) rev ON true\n WHERE cs.status = 'executed'\n AND cs.booking_id IS DISTINCT FROM p_booking\n ORDER BY coalesce(cs.ended_at, cs.scheduled_for) DESC NULLS LAST\n LIMIT 50;\n$function$;\n";
24
+ export declare const MIGRATION_024_A_AVALIACAO_DESTA_VISITA_VALE_PARA_O_QUE_ELA_CONTRATOU = "-- The evaluation done in this attendance covers what was contracted in it.\n--\n-- FAY-1613 (Espa\u00E7o Facial, etapa 3): the professional fills the \"Avalia\u00E7\u00E3o\" form, indicates, the\n-- client contracts \u2014 and every contracted session came out with its own \"Avalia\u00E7\u00E3o\" step pending,\n-- because nothing in V2 points a contracted session at the evaluation that authorised it\n-- (`source_evaluation_id` is only set by the V1 import). V1 shows \"Avalia\u00E7\u00E3o conclu\u00EDda \u2014 o\n-- atendimento segue para a execu\u00E7\u00E3o\" at that point.\n--\n-- Same body as live, plus step 5c. Idempotent (CREATE OR REPLACE).\n\nCREATE OR REPLACE FUNCTION public.clinical_requirements_refresh(p_booking uuid, p_final boolean DEFAULT false)\n RETURNS jsonb\n LANGUAGE plpgsql\n SECURITY DEFINER\n SET search_path TO 'public'\nAS $function$\nDECLARE\n a record; v_mode text; v_now timestamptz := now(); v_eval jsonb;\n n_mat int := 0; n_reopen int := 0;\n -- `v_sessao`, n\u00E3o `r`: plpgsql resolve o nome da VARI\u00C1VEL antes do apelido da\n -- tabela, ent\u00E3o um `record r` declarado aqui sequestra todo `r.coluna` dos\n -- UPDATEs abaixo \u2014 e o erro que sai \u00E9 \"record r has no field booking_id\".\n v_sessao record;\nBEGIN\n SELECT ap.id, ap.tenant_id, ap.party_id, ap.status INTO a\n FROM public.appointments ap WHERE ap.id = p_booking;\n IF NOT FOUND THEN RETURN jsonb_build_object('ok', false, 'reason', 'not_found'); END IF;\n\n v_mode := public.clinical_requirements_mode(a.tenant_id);\n IF v_mode = 'off' THEN RETURN jsonb_build_object('ok', true, 'mode', v_mode, 'skipped', true); END IF;\n\n p_final := coalesce(p_final, false) AND a.status = 'completed';\n\n BEGIN\n PERFORM pg_advisory_xact_lock(hashtextextended('clinical_req:' || p_booking::text, 0));\n IF a.status = 'completed' AND NOT p_final THEN\n RETURN jsonb_build_object('ok', true, 'mode', v_mode, 'frozen', true);\n END IF;\n\n -- 1) Materializa o que falta (sess\u00E3o nova, ficha t\u00E9cnica criada depois).\n FOR v_sessao IN SELECT cs.id FROM public.plg_clinical_sessions cs WHERE cs.booking_id = p_booking\n LOOP\n n_mat := n_mat + public.clinical_requirements_materialize(v_sessao.id);\n END LOOP;\n\n v_eval := coalesce((SELECT t.settings -> 'evaluation_flow' FROM public.tenants t WHERE t.id = a.tenant_id), '{}'::jsonb);\n IF jsonb_typeof(v_eval) <> 'object' THEN v_eval := '{}'::jsonb; END IF;\n\n -- 1b) Regra que saiu da configura\u00E7\u00E3o para de bloquear (o upsert reabre se voltar).\n UPDATE public.plg_clinical_requirements r\n SET status = 'not_applicable', candidate_type = NULL, candidate_id = NULL, updated_at = v_now\n WHERE r.booking_id = p_booking AND r.status = 'pending' AND r.frozen_at IS NULL\n AND r.metadata -> 'migration' IS NULL\n AND ((r.kind = 'revision_link' AND NOT coalesce((v_eval ->> 'revision_link_required')::boolean, false))\n OR (r.kind = 'evaluation'\n AND r.rule_sources = '[\"evaluation_flow.require_evaluation_for_kind_service\"]'::jsonb\n AND NOT coalesce((v_eval ->> 'require_evaluation_for_kind_service')::boolean, false)));\n\n -- 2) Reabre o que perdeu a fonte. Linha congelada n\u00E3o se mexe.\n UPDATE public.plg_clinical_requirements r\n SET status = 'pending', subject_type = NULL, subject_id = NULL, resolved_via = NULL,\n resolved_at = NULL, valid_until = NULL, updated_at = v_now\n WHERE r.booking_id = p_booking AND r.status = 'resolved' AND r.frozen_at IS NULL\n AND ((r.subject_type = 'documents' AND NOT EXISTS (SELECT 1 FROM public.documents d WHERE d.id = r.subject_id))\n OR (r.subject_type = 'clinical_sessions' AND NOT EXISTS (SELECT 1 FROM public.plg_clinical_sessions x WHERE x.id = r.subject_id))\n OR (r.subject_type = 'items' AND NOT EXISTS (SELECT 1 FROM public.items oi WHERE oi.id = r.subject_id)));\n GET DIAGNOSTICS n_reopen = ROW_COUNT;\n\n -- 3) Formul\u00E1rio universal: a \u00FAltima resposta do cliente, em qualquer\n -- atendimento, ainda dentro da validade do modelo.\n UPDATE public.plg_clinical_requirements r\n SET status = 'resolved', subject_type = 'documents', subject_id = x.id,\n resolved_via = CASE WHEN x.subject_id = p_booking THEN 'attendance' ELSE 'auto_client' END,\n resolved_at = v_now,\n valid_until = CASE WHEN x.validity_days IS NULL THEN NULL\n ELSE x.eff + make_interval(days => x.validity_days) END,\n updated_at = v_now\n FROM (SELECT DISTINCT ON (fd.template_id) d.id, fd.template_id, d.subject_id,\n coalesce(d.updated_at, d.created_at) AS eff, ft.validity_days\n FROM public.plg_forms_documents fd\n JOIN public.documents d ON d.id = fd.document_id\n JOIN public.plg_forms_templates ft ON ft.id = fd.template_id\n WHERE d.person_id = a.party_id AND d.tenant_id = a.tenant_id\n ORDER BY fd.template_id, coalesce(d.updated_at, d.created_at) DESC) x\n WHERE r.booking_id = p_booking AND r.status = 'pending' AND r.kind = 'form'\n AND r.scope = 'attendance' AND r.ref_id = x.template_id\n AND r.rule_sources @> '[\"forms_templates.required_on_attendance\"]'::jsonb\n AND (x.validity_days IS NULL OR x.eff + make_interval(days => x.validity_days) > v_now);\n\n -- 4) Demais formul\u00E1rios: qualquer resposta DESTE atendimento com o modelo.\n UPDATE public.plg_clinical_requirements r\n SET status = 'resolved', subject_type = 'documents', subject_id = x.id,\n resolved_via = 'attendance', resolved_at = v_now, updated_at = v_now\n FROM (SELECT DISTINCT ON (fd.template_id, coalesce(fd.product_id, '00000000-0000-0000-0000-000000000000'::uuid))\n d.id, fd.template_id, fd.product_id\n FROM public.plg_forms_documents fd\n JOIN public.documents d ON d.id = fd.document_id\n WHERE d.subject_type = 'appointment' AND d.subject_id = p_booking\n ORDER BY fd.template_id, coalesce(fd.product_id, '00000000-0000-0000-0000-000000000000'::uuid),\n coalesce(d.updated_at, d.created_at) DESC) x\n WHERE r.booking_id = p_booking AND r.status = 'pending' AND r.kind = 'form'\n AND r.ref_id = x.template_id\n AND NOT (r.rule_sources @> '[\"forms_templates.required_on_attendance\"]'::jsonb)\n AND (x.product_id IS NULL OR r.scope = 'attendance' OR x.product_id = r.service_id);\n\n -- 5) Avalia\u00E7\u00E3o vinculada: a sess\u00E3o aponta a avalia\u00E7\u00E3o que a autorizou.\n UPDATE public.plg_clinical_requirements r\n SET status = 'resolved', subject_type = 'clinical_sessions', subject_id = x.eval_id,\n resolved_via = CASE WHEN x.same_booking THEN 'attendance' ELSE 'auto_client' END,\n resolved_at = v_now, updated_at = v_now\n FROM (SELECT r2.id AS req_id, e.id AS eval_id, (e.booking_id = p_booking) AS same_booking\n FROM public.plg_clinical_requirements r2\n JOIN public.plg_clinical_sessions cs ON cs.id = r2.session_id\n JOIN public.plg_clinical_sessions e ON e.id = cs.source_evaluation_id\n WHERE r2.booking_id = p_booking AND r2.kind = 'evaluation' AND r2.frozen_at IS NULL\n AND (r2.status = 'pending' OR (r2.status = 'resolved' AND r2.resolved_via = 'legacy_grace'))) x\n WHERE r.id = x.req_id;\n\n -- 5b) Gra\u00E7a legada: o cliente j\u00E1 tem atendimento conclu\u00EDdo com servi\u00E7o de\n -- avalia\u00E7\u00E3o. \u00C9 a mesma concess\u00E3o do V1, e vai marcada como tal.\n UPDATE public.plg_clinical_requirements r\n SET status = 'resolved', subject_type = 'appointments', subject_id = x.booking_id,\n resolved_via = 'legacy_grace', resolved_at = v_now, updated_at = v_now\n FROM (SELECT ap.id AS booking_id\n FROM public.appointments ap\n JOIN public.appointment_items ai ON ai.booking_id = ap.id\n JOIN public.products p ON p.id = ai.service_id AND p.service_kind = 'evaluation'\n WHERE ap.party_id = a.party_id AND ap.tenant_id = a.tenant_id AND ap.status = 'completed'\n ORDER BY ap.starts_at DESC NULLS LAST LIMIT 1) x\n WHERE r.booking_id = p_booking AND r.status = 'pending' AND r.kind = 'evaluation';\n\n -- 5c) Avalia\u00E7\u00E3o feita NESTE atendimento. Contratar depois de avaliar cria sess\u00F5es novas, e cada\n -- uma nascia com a pr\u00F3pria bola \"Avalia\u00E7\u00E3o\" pendente \u2014 o profissional acabou de avaliar.\n -- Indica\u00E7\u00E3o registrada aqui (o formul\u00E1rio de avalia\u00E7\u00E3o) ou sess\u00E3o de avalia\u00E7\u00E3o deste\n -- atendimento resolvem as sess\u00F5es contratadas na mesma visita.\n UPDATE public.plg_clinical_requirements r\n SET status = 'resolved', subject_type = 'appointments', subject_id = p_booking,\n resolved_via = 'attendance', resolved_at = v_now, updated_at = v_now\n WHERE r.booking_id = p_booking AND r.status = 'pending' AND r.kind = 'evaluation'\n AND r.frozen_at IS NULL\n AND (EXISTS (SELECT 1 FROM public.plg_crm_deal_interest_appointments x WHERE x.appointment_id = p_booking)\n OR EXISTS (SELECT 1 FROM public.plg_clinical_sessions e\n JOIN public.products p ON p.id = e.service_id AND p.service_kind = 'evaluation'\n WHERE e.booking_id = p_booking));\n\n -- 6) Revis\u00E3o vinculada ao servi\u00E7o original.\n UPDATE public.plg_clinical_requirements r\n SET status = 'resolved', subject_type = 'clinical_sessions', subject_id = cs.parent_session_id,\n resolved_via = 'attendance', resolved_at = v_now, updated_at = v_now\n FROM public.plg_clinical_sessions cs\n WHERE cs.id = r.session_id AND r.booking_id = p_booking\n AND r.status = 'pending' AND r.kind = 'revision_link'\n AND cs.parent_session_id IS NOT NULL\n AND EXISTS (SELECT 1 FROM public.plg_clinical_sessions p WHERE p.id = cs.parent_session_id);\n\n -- 7) Produtos: a sess\u00E3o declara que foram conferidos.\n UPDATE public.plg_clinical_requirements r\n SET status = 'resolved', subject_type = 'clinical_sessions', subject_id = cs.id,\n resolved_via = 'attendance', resolved_at = v_now, updated_at = v_now\n FROM public.plg_clinical_sessions cs\n WHERE cs.id = r.session_id AND r.booking_id = p_booking\n AND r.status = 'pending' AND r.kind = 'products'\n AND coalesce(cs.products_initialized, false);\n\n -- 8) Item de pedido vinculado. A linha do pedido no V2 \u00E9 `public.items` \u2014\n -- n\u00E3o existe `order_items`; o pedido \u00E9 o documento e o item \u00E9 dele.\n UPDATE public.plg_clinical_requirements r\n SET status = 'resolved', subject_type = 'items', subject_id = oi.id,\n resolved_via = 'attendance', resolved_at = v_now, updated_at = v_now\n FROM public.plg_clinical_sessions cs\n JOIN public.items oi ON oi.id = cs.order_item_id\n WHERE cs.id = r.session_id AND r.booking_id = p_booking\n AND r.status = 'pending' AND r.kind = 'invoice_item';\n\n -- 9) Congelamento: s\u00F3 na \u00FAltima passada, com o atendimento conclu\u00EDdo.\n IF p_final THEN\n UPDATE public.plg_clinical_requirements\n SET frozen_at = coalesce(frozen_at, v_now), updated_at = v_now\n WHERE booking_id = p_booking;\n END IF;\n\n RETURN jsonb_build_object(\n 'ok', true, 'mode', v_mode, 'materialized', n_mat, 'reopened', n_reopen,\n 'pending', (SELECT count(*) FROM public.plg_clinical_requirements WHERE booking_id = p_booking AND status = 'pending'),\n 'resolved', (SELECT count(*) FROM public.plg_clinical_requirements WHERE booking_id = p_booking AND status = 'resolved'),\n 'frozen', p_final);\n EXCEPTION WHEN others THEN\n RAISE WARNING 'clinical_requirements_refresh(%): %', p_booking, sqlerrm;\n RETURN jsonb_build_object('ok', false, 'error', sqlerrm);\n END;\nEND $function$;\n";
23
25
  export declare const MIGRATIONS: Array<{
24
26
  id: string;
25
27
  sql: string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,kDAAkD,6yuCAiiB9D,CAAA;AAED,eAAO,MAAM,0CAA0C,g64CAwvBtD,CAAA;AAED,eAAO,MAAM,+DAA+D,s8HAsE3E,CAAA;AAED,eAAO,MAAM,yCAAyC,ilzBAqarD,CAAA;AAED,eAAO,MAAM,iDAAiD,woIAkF7D,CAAA;AAED,eAAO,MAAM,mCAAmC,86KAyG/C,CAAA;AAED,eAAO,MAAM,uCAAuC,20MAuHnD,CAAA;AAED,eAAO,MAAM,oCAAoC,6qGA+DhD,CAAA;AAED,eAAO,MAAM,qCAAqC,uwFAsDjD,CAAA;AAED,eAAO,MAAM,0CAA0C,olLAmGtD,CAAA;AAED,eAAO,MAAM,kDAAkD,miHA0E9D,CAAA;AAED,eAAO,MAAM,8CAA8C,8/HA2E1D,CAAA;AAED,eAAO,MAAM,+BAA+B,s7FAwD3C,CAAA;AAED,eAAO,MAAM,sDAAsD,i1FA2DlE,CAAA;AAED,eAAO,MAAM,2CAA2C,ukIAyFvD,CAAA;AAED,eAAO,MAAM,2DAA2D,g7IAoFvE,CAAA;AAED,eAAO,MAAM,oDAAoD,wlJAwGhE,CAAA;AAED,eAAO,MAAM,8CAA8C,s6NAqJ1D,CAAA;AAED,eAAO,MAAM,iCAAiC,2zMAuH7C,CAAA;AAED,eAAO,MAAM,+DAA+D,w6IA2F3E,CAAA;AAED,eAAO,MAAM,wDAAwD,0wLA4HpE,CAAA;AAED,eAAO,MAAM,sDAAsD,8mHAmElE,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAuBzD,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,kDAAkD,6yuCAiiB9D,CAAA;AAED,eAAO,MAAM,0CAA0C,g64CAwvBtD,CAAA;AAED,eAAO,MAAM,+DAA+D,s8HAsE3E,CAAA;AAED,eAAO,MAAM,yCAAyC,ilzBAqarD,CAAA;AAED,eAAO,MAAM,iDAAiD,woIAkF7D,CAAA;AAED,eAAO,MAAM,mCAAmC,86KAyG/C,CAAA;AAED,eAAO,MAAM,uCAAuC,20MAuHnD,CAAA;AAED,eAAO,MAAM,oCAAoC,6qGA+DhD,CAAA;AAED,eAAO,MAAM,qCAAqC,uwFAsDjD,CAAA;AAED,eAAO,MAAM,0CAA0C,olLAmGtD,CAAA;AAED,eAAO,MAAM,kDAAkD,miHA0E9D,CAAA;AAED,eAAO,MAAM,8CAA8C,8/HA2E1D,CAAA;AAED,eAAO,MAAM,+BAA+B,s7FAwD3C,CAAA;AAED,eAAO,MAAM,sDAAsD,i1FA2DlE,CAAA;AAED,eAAO,MAAM,2CAA2C,ukIAyFvD,CAAA;AAED,eAAO,MAAM,2DAA2D,g7IAoFvE,CAAA;AAED,eAAO,MAAM,oDAAoD,wlJAwGhE,CAAA;AAED,eAAO,MAAM,8CAA8C,s6NAqJ1D,CAAA;AAED,eAAO,MAAM,iCAAiC,2zMAuH7C,CAAA;AAED,eAAO,MAAM,+DAA+D,w6IA2F3E,CAAA;AAED,eAAO,MAAM,wDAAwD,0wLA4HpE,CAAA;AAED,eAAO,MAAM,sDAAsD,8mHAmElE,CAAA;AAED,eAAO,MAAM,6CAA6C,+iIAsFzD,CAAA;AAED,eAAO,MAAM,oEAAoE,q6XA+LhF,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAyBzD,CAAA"}
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "agenda"
7
7
  ]
8
8
  },
9
- "version": "0.2.7",
9
+ "version": "0.3.1",
10
10
  "description": "[preview] Fayz SDK — o atendimento clínico: anamnese com validade, avaliação, execução por sessão e consumo de produto",
11
11
  "type": "module",
12
12
  "sideEffects": false,
@@ -27,7 +27,7 @@
27
27
  "react-dom": "^18.0.0 || ^19.0.0"
28
28
  },
29
29
  "dependencies": {
30
- "@fayz-ai/core": "^0.38.0"
30
+ "@fayz-ai/core": "^0.40.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/react": "^18.3.0",