@fayz-ai/plugin-clinical 0.2.6 → 0.3.0

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.
package/dist/index.js CHANGED
@@ -3434,6 +3434,285 @@ FROM sessao s
3434
3434
  LEFT JOIN pago p ON p.order_id = s.order_id
3435
3435
  GROUP BY s.tenant_id, s.party_id;
3436
3436
  `;
3437
+ var MIGRATION_023_A_REVISAO_SE_ESCOLHE_AO_AGENDAR = `-- ============================================================================
3438
+ -- 023 \u2014 The revision is chosen when it is booked, not only during the visit.
3439
+ -- ============================================================================
3440
+ -- V1 asks "De qual servi\xE7o \xE9 esta revis\xE3o?" the moment the reception picks the
3441
+ -- Revis\xE3o service in Novo Agendamento (FAY-1613). 015 answered it only from an
3442
+ -- existing booking. This lists the same candidates for a PERSON, with the ordinal
3443
+ -- the next revision of each will take (V1 shows "1\xAA Revis\xE3o").
3444
+ --
3445
+ -- Also closes a gap in 015: \`clinical_revision_candidates(p_booking)\` is a
3446
+ -- definer function that read any booking id it was given, so a member of one
3447
+ -- tenant could list another tenant's patient history by guessing an id. Both
3448
+ -- now answer only inside the caller's tenant.
3449
+ --
3450
+ -- Idempotent.
3451
+
3452
+ CREATE OR REPLACE FUNCTION public.clinical_revision_candidates_for_person(p_person uuid)
3453
+ RETURNS TABLE(session_id uuid, service_id uuid, service_name text, executed_on timestamp with time zone,
3454
+ deadline_days integer, days_since integer, within_deadline boolean, next_revision_number integer)
3455
+ LANGUAGE sql
3456
+ STABLE SECURITY DEFINER
3457
+ SET search_path TO 'public', 'app'
3458
+ AS $function$
3459
+ SELECT
3460
+ cs.id,
3461
+ cs.service_id,
3462
+ p.name,
3463
+ coalesce(cs.ended_at, cs.scheduled_for) AS executed_on,
3464
+ rev.deadline_days,
3465
+ (current_date - coalesce(cs.ended_at, cs.scheduled_for)::date)::int AS days_since,
3466
+ CASE WHEN rev.deadline_days IS NULL THEN NULL
3467
+ ELSE (current_date - coalesce(cs.ended_at, cs.scheduled_for)::date) <= rev.deadline_days
3468
+ END AS within_deadline,
3469
+ coalesce((SELECT max(r.revision_number) FROM public.plg_clinical_sessions r
3470
+ WHERE r.tenant_id = cs.tenant_id AND r.parent_session_id = cs.id), 0) + 1
3471
+ FROM public.plg_clinical_sessions cs
3472
+ LEFT JOIN public.products p ON p.id = cs.service_id
3473
+ LEFT JOIN LATERAL (
3474
+ SELECT r.deadline_days FROM public.plg_clinical_service_revisions r
3475
+ WHERE r.tenant_id = cs.tenant_id AND r.service_id = cs.service_id
3476
+ ORDER BY r.revision_number LIMIT 1
3477
+ ) rev ON true
3478
+ WHERE cs.tenant_id = app.current_tenant_id()
3479
+ AND cs.party_id = p_person
3480
+ AND cs.status = 'executed'
3481
+ AND cs.parent_session_id IS NULL
3482
+ ORDER BY coalesce(cs.ended_at, cs.scheduled_for) DESC NULLS LAST
3483
+ LIMIT 50;
3484
+ $function$;
3485
+
3486
+ REVOKE ALL ON FUNCTION public.clinical_revision_candidates_for_person(uuid) FROM PUBLIC, anon;
3487
+ GRANT EXECUTE ON FUNCTION public.clinical_revision_candidates_for_person(uuid) TO authenticated;
3488
+
3489
+ CREATE OR REPLACE FUNCTION public.clinical_revision_candidates(p_booking uuid)
3490
+ RETURNS TABLE(session_id uuid, service_id uuid, service_name text, executed_on timestamp with time zone,
3491
+ deadline_days integer, days_since integer, within_deadline boolean)
3492
+ LANGUAGE sql
3493
+ STABLE SECURITY DEFINER
3494
+ SET search_path TO 'public', 'app'
3495
+ AS $function$
3496
+ WITH alvo AS (
3497
+ SELECT ap.tenant_id, ap.party_id FROM public.appointments ap
3498
+ WHERE ap.id = p_booking AND ap.tenant_id = app.current_tenant_id()
3499
+ )
3500
+ SELECT
3501
+ cs.id,
3502
+ cs.service_id,
3503
+ p.name,
3504
+ coalesce(cs.ended_at, cs.scheduled_for) AS executed_on,
3505
+ rev.deadline_days,
3506
+ (current_date - coalesce(cs.ended_at, cs.scheduled_for)::date)::int AS days_since,
3507
+ CASE WHEN rev.deadline_days IS NULL THEN NULL
3508
+ ELSE (current_date - coalesce(cs.ended_at, cs.scheduled_for)::date) <= rev.deadline_days
3509
+ END AS within_deadline
3510
+ FROM public.plg_clinical_sessions cs
3511
+ JOIN alvo a ON a.tenant_id = cs.tenant_id AND a.party_id = cs.party_id
3512
+ LEFT JOIN public.products p ON p.id = cs.service_id
3513
+ LEFT JOIN LATERAL (
3514
+ SELECT r.deadline_days FROM public.plg_clinical_service_revisions r
3515
+ WHERE r.tenant_id = cs.tenant_id AND r.service_id = cs.service_id
3516
+ ORDER BY r.revision_number LIMIT 1
3517
+ ) rev ON true
3518
+ WHERE cs.status = 'executed'
3519
+ AND cs.booking_id IS DISTINCT FROM p_booking
3520
+ ORDER BY coalesce(cs.ended_at, cs.scheduled_for) DESC NULLS LAST
3521
+ LIMIT 50;
3522
+ $function$;
3523
+ `;
3524
+ var MIGRATION_024_A_AVALIACAO_DESTA_VISITA_VALE_PARA_O_QUE_ELA_CONTRATOU = `-- The evaluation done in this attendance covers what was contracted in it.
3525
+ --
3526
+ -- FAY-1613 (Espa\xE7o Facial, etapa 3): the professional fills the "Avalia\xE7\xE3o" form, indicates, the
3527
+ -- client contracts \u2014 and every contracted session came out with its own "Avalia\xE7\xE3o" step pending,
3528
+ -- because nothing in V2 points a contracted session at the evaluation that authorised it
3529
+ -- (\`source_evaluation_id\` is only set by the V1 import). V1 shows "Avalia\xE7\xE3o conclu\xEDda \u2014 o
3530
+ -- atendimento segue para a execu\xE7\xE3o" at that point.
3531
+ --
3532
+ -- Same body as live, plus step 5c. Idempotent (CREATE OR REPLACE).
3533
+
3534
+ CREATE OR REPLACE FUNCTION public.clinical_requirements_refresh(p_booking uuid, p_final boolean DEFAULT false)
3535
+ RETURNS jsonb
3536
+ LANGUAGE plpgsql
3537
+ SECURITY DEFINER
3538
+ SET search_path TO 'public'
3539
+ AS $function$
3540
+ DECLARE
3541
+ a record; v_mode text; v_now timestamptz := now(); v_eval jsonb;
3542
+ n_mat int := 0; n_reopen int := 0;
3543
+ -- \`v_sessao\`, n\xE3o \`r\`: plpgsql resolve o nome da VARI\xC1VEL antes do apelido da
3544
+ -- tabela, ent\xE3o um \`record r\` declarado aqui sequestra todo \`r.coluna\` dos
3545
+ -- UPDATEs abaixo \u2014 e o erro que sai \xE9 "record r has no field booking_id".
3546
+ v_sessao record;
3547
+ BEGIN
3548
+ SELECT ap.id, ap.tenant_id, ap.party_id, ap.status INTO a
3549
+ FROM public.appointments ap WHERE ap.id = p_booking;
3550
+ IF NOT FOUND THEN RETURN jsonb_build_object('ok', false, 'reason', 'not_found'); END IF;
3551
+
3552
+ v_mode := public.clinical_requirements_mode(a.tenant_id);
3553
+ IF v_mode = 'off' THEN RETURN jsonb_build_object('ok', true, 'mode', v_mode, 'skipped', true); END IF;
3554
+
3555
+ p_final := coalesce(p_final, false) AND a.status = 'completed';
3556
+
3557
+ BEGIN
3558
+ PERFORM pg_advisory_xact_lock(hashtextextended('clinical_req:' || p_booking::text, 0));
3559
+ IF a.status = 'completed' AND NOT p_final THEN
3560
+ RETURN jsonb_build_object('ok', true, 'mode', v_mode, 'frozen', true);
3561
+ END IF;
3562
+
3563
+ -- 1) Materializa o que falta (sess\xE3o nova, ficha t\xE9cnica criada depois).
3564
+ FOR v_sessao IN SELECT cs.id FROM public.plg_clinical_sessions cs WHERE cs.booking_id = p_booking
3565
+ LOOP
3566
+ n_mat := n_mat + public.clinical_requirements_materialize(v_sessao.id);
3567
+ END LOOP;
3568
+
3569
+ v_eval := coalesce((SELECT t.settings -> 'evaluation_flow' FROM public.tenants t WHERE t.id = a.tenant_id), '{}'::jsonb);
3570
+ IF jsonb_typeof(v_eval) <> 'object' THEN v_eval := '{}'::jsonb; END IF;
3571
+
3572
+ -- 1b) Regra que saiu da configura\xE7\xE3o para de bloquear (o upsert reabre se voltar).
3573
+ UPDATE public.plg_clinical_requirements r
3574
+ SET status = 'not_applicable', candidate_type = NULL, candidate_id = NULL, updated_at = v_now
3575
+ WHERE r.booking_id = p_booking AND r.status = 'pending' AND r.frozen_at IS NULL
3576
+ AND r.metadata -> 'migration' IS NULL
3577
+ AND ((r.kind = 'revision_link' AND NOT coalesce((v_eval ->> 'revision_link_required')::boolean, false))
3578
+ OR (r.kind = 'evaluation'
3579
+ AND r.rule_sources = '["evaluation_flow.require_evaluation_for_kind_service"]'::jsonb
3580
+ AND NOT coalesce((v_eval ->> 'require_evaluation_for_kind_service')::boolean, false)));
3581
+
3582
+ -- 2) Reabre o que perdeu a fonte. Linha congelada n\xE3o se mexe.
3583
+ UPDATE public.plg_clinical_requirements r
3584
+ SET status = 'pending', subject_type = NULL, subject_id = NULL, resolved_via = NULL,
3585
+ resolved_at = NULL, valid_until = NULL, updated_at = v_now
3586
+ WHERE r.booking_id = p_booking AND r.status = 'resolved' AND r.frozen_at IS NULL
3587
+ AND ((r.subject_type = 'documents' AND NOT EXISTS (SELECT 1 FROM public.documents d WHERE d.id = r.subject_id))
3588
+ OR (r.subject_type = 'clinical_sessions' AND NOT EXISTS (SELECT 1 FROM public.plg_clinical_sessions x WHERE x.id = r.subject_id))
3589
+ OR (r.subject_type = 'items' AND NOT EXISTS (SELECT 1 FROM public.items oi WHERE oi.id = r.subject_id)));
3590
+ GET DIAGNOSTICS n_reopen = ROW_COUNT;
3591
+
3592
+ -- 3) Formul\xE1rio universal: a \xFAltima resposta do cliente, em qualquer
3593
+ -- atendimento, ainda dentro da validade do modelo.
3594
+ UPDATE public.plg_clinical_requirements r
3595
+ SET status = 'resolved', subject_type = 'documents', subject_id = x.id,
3596
+ resolved_via = CASE WHEN x.subject_id = p_booking THEN 'attendance' ELSE 'auto_client' END,
3597
+ resolved_at = v_now,
3598
+ valid_until = CASE WHEN x.validity_days IS NULL THEN NULL
3599
+ ELSE x.eff + make_interval(days => x.validity_days) END,
3600
+ updated_at = v_now
3601
+ FROM (SELECT DISTINCT ON (fd.template_id) d.id, fd.template_id, d.subject_id,
3602
+ coalesce(d.updated_at, d.created_at) AS eff, ft.validity_days
3603
+ FROM public.plg_forms_documents fd
3604
+ JOIN public.documents d ON d.id = fd.document_id
3605
+ JOIN public.plg_forms_templates ft ON ft.id = fd.template_id
3606
+ WHERE d.person_id = a.party_id AND d.tenant_id = a.tenant_id
3607
+ ORDER BY fd.template_id, coalesce(d.updated_at, d.created_at) DESC) x
3608
+ WHERE r.booking_id = p_booking AND r.status = 'pending' AND r.kind = 'form'
3609
+ AND r.scope = 'attendance' AND r.ref_id = x.template_id
3610
+ AND r.rule_sources @> '["forms_templates.required_on_attendance"]'::jsonb
3611
+ AND (x.validity_days IS NULL OR x.eff + make_interval(days => x.validity_days) > v_now);
3612
+
3613
+ -- 4) Demais formul\xE1rios: qualquer resposta DESTE atendimento com o modelo.
3614
+ UPDATE public.plg_clinical_requirements r
3615
+ SET status = 'resolved', subject_type = 'documents', subject_id = x.id,
3616
+ resolved_via = 'attendance', resolved_at = v_now, updated_at = v_now
3617
+ FROM (SELECT DISTINCT ON (fd.template_id, coalesce(fd.product_id, '00000000-0000-0000-0000-000000000000'::uuid))
3618
+ d.id, fd.template_id, fd.product_id
3619
+ FROM public.plg_forms_documents fd
3620
+ JOIN public.documents d ON d.id = fd.document_id
3621
+ WHERE d.subject_type = 'appointment' AND d.subject_id = p_booking
3622
+ ORDER BY fd.template_id, coalesce(fd.product_id, '00000000-0000-0000-0000-000000000000'::uuid),
3623
+ coalesce(d.updated_at, d.created_at) DESC) x
3624
+ WHERE r.booking_id = p_booking AND r.status = 'pending' AND r.kind = 'form'
3625
+ AND r.ref_id = x.template_id
3626
+ AND NOT (r.rule_sources @> '["forms_templates.required_on_attendance"]'::jsonb)
3627
+ AND (x.product_id IS NULL OR r.scope = 'attendance' OR x.product_id = r.service_id);
3628
+
3629
+ -- 5) Avalia\xE7\xE3o vinculada: a sess\xE3o aponta a avalia\xE7\xE3o que a autorizou.
3630
+ UPDATE public.plg_clinical_requirements r
3631
+ SET status = 'resolved', subject_type = 'clinical_sessions', subject_id = x.eval_id,
3632
+ resolved_via = CASE WHEN x.same_booking THEN 'attendance' ELSE 'auto_client' END,
3633
+ resolved_at = v_now, updated_at = v_now
3634
+ FROM (SELECT r2.id AS req_id, e.id AS eval_id, (e.booking_id = p_booking) AS same_booking
3635
+ FROM public.plg_clinical_requirements r2
3636
+ JOIN public.plg_clinical_sessions cs ON cs.id = r2.session_id
3637
+ JOIN public.plg_clinical_sessions e ON e.id = cs.source_evaluation_id
3638
+ WHERE r2.booking_id = p_booking AND r2.kind = 'evaluation' AND r2.frozen_at IS NULL
3639
+ AND (r2.status = 'pending' OR (r2.status = 'resolved' AND r2.resolved_via = 'legacy_grace'))) x
3640
+ WHERE r.id = x.req_id;
3641
+
3642
+ -- 5b) Gra\xE7a legada: o cliente j\xE1 tem atendimento conclu\xEDdo com servi\xE7o de
3643
+ -- avalia\xE7\xE3o. \xC9 a mesma concess\xE3o do V1, e vai marcada como tal.
3644
+ UPDATE public.plg_clinical_requirements r
3645
+ SET status = 'resolved', subject_type = 'appointments', subject_id = x.booking_id,
3646
+ resolved_via = 'legacy_grace', resolved_at = v_now, updated_at = v_now
3647
+ FROM (SELECT ap.id AS booking_id
3648
+ FROM public.appointments ap
3649
+ JOIN public.appointment_items ai ON ai.booking_id = ap.id
3650
+ JOIN public.products p ON p.id = ai.service_id AND p.service_kind = 'evaluation'
3651
+ WHERE ap.party_id = a.party_id AND ap.tenant_id = a.tenant_id AND ap.status = 'completed'
3652
+ ORDER BY ap.starts_at DESC NULLS LAST LIMIT 1) x
3653
+ WHERE r.booking_id = p_booking AND r.status = 'pending' AND r.kind = 'evaluation';
3654
+
3655
+ -- 5c) Avalia\xE7\xE3o feita NESTE atendimento. Contratar depois de avaliar cria sess\xF5es novas, e cada
3656
+ -- uma nascia com a pr\xF3pria bola "Avalia\xE7\xE3o" pendente \u2014 o profissional acabou de avaliar.
3657
+ -- Indica\xE7\xE3o registrada aqui (o formul\xE1rio de avalia\xE7\xE3o) ou sess\xE3o de avalia\xE7\xE3o deste
3658
+ -- atendimento resolvem as sess\xF5es contratadas na mesma visita.
3659
+ UPDATE public.plg_clinical_requirements r
3660
+ SET status = 'resolved', subject_type = 'appointments', subject_id = p_booking,
3661
+ resolved_via = 'attendance', resolved_at = v_now, updated_at = v_now
3662
+ WHERE r.booking_id = p_booking AND r.status = 'pending' AND r.kind = 'evaluation'
3663
+ AND r.frozen_at IS NULL
3664
+ AND (EXISTS (SELECT 1 FROM public.plg_crm_deal_interest_appointments x WHERE x.appointment_id = p_booking)
3665
+ OR EXISTS (SELECT 1 FROM public.plg_clinical_sessions e
3666
+ JOIN public.products p ON p.id = e.service_id AND p.service_kind = 'evaluation'
3667
+ WHERE e.booking_id = p_booking));
3668
+
3669
+ -- 6) Revis\xE3o vinculada ao servi\xE7o original.
3670
+ UPDATE public.plg_clinical_requirements r
3671
+ SET status = 'resolved', subject_type = 'clinical_sessions', subject_id = cs.parent_session_id,
3672
+ resolved_via = 'attendance', resolved_at = v_now, updated_at = v_now
3673
+ FROM public.plg_clinical_sessions cs
3674
+ WHERE cs.id = r.session_id AND r.booking_id = p_booking
3675
+ AND r.status = 'pending' AND r.kind = 'revision_link'
3676
+ AND cs.parent_session_id IS NOT NULL
3677
+ AND EXISTS (SELECT 1 FROM public.plg_clinical_sessions p WHERE p.id = cs.parent_session_id);
3678
+
3679
+ -- 7) Produtos: a sess\xE3o declara que foram conferidos.
3680
+ UPDATE public.plg_clinical_requirements r
3681
+ SET status = 'resolved', subject_type = 'clinical_sessions', subject_id = cs.id,
3682
+ resolved_via = 'attendance', resolved_at = v_now, updated_at = v_now
3683
+ FROM public.plg_clinical_sessions cs
3684
+ WHERE cs.id = r.session_id AND r.booking_id = p_booking
3685
+ AND r.status = 'pending' AND r.kind = 'products'
3686
+ AND coalesce(cs.products_initialized, false);
3687
+
3688
+ -- 8) Item de pedido vinculado. A linha do pedido no V2 \xE9 \`public.items\` \u2014
3689
+ -- n\xE3o existe \`order_items\`; o pedido \xE9 o documento e o item \xE9 dele.
3690
+ UPDATE public.plg_clinical_requirements r
3691
+ SET status = 'resolved', subject_type = 'items', subject_id = oi.id,
3692
+ resolved_via = 'attendance', resolved_at = v_now, updated_at = v_now
3693
+ FROM public.plg_clinical_sessions cs
3694
+ JOIN public.items oi ON oi.id = cs.order_item_id
3695
+ WHERE cs.id = r.session_id AND r.booking_id = p_booking
3696
+ AND r.status = 'pending' AND r.kind = 'invoice_item';
3697
+
3698
+ -- 9) Congelamento: s\xF3 na \xFAltima passada, com o atendimento conclu\xEDdo.
3699
+ IF p_final THEN
3700
+ UPDATE public.plg_clinical_requirements
3701
+ SET frozen_at = coalesce(frozen_at, v_now), updated_at = v_now
3702
+ WHERE booking_id = p_booking;
3703
+ END IF;
3704
+
3705
+ RETURN jsonb_build_object(
3706
+ 'ok', true, 'mode', v_mode, 'materialized', n_mat, 'reopened', n_reopen,
3707
+ 'pending', (SELECT count(*) FROM public.plg_clinical_requirements WHERE booking_id = p_booking AND status = 'pending'),
3708
+ 'resolved', (SELECT count(*) FROM public.plg_clinical_requirements WHERE booking_id = p_booking AND status = 'resolved'),
3709
+ 'frozen', p_final);
3710
+ EXCEPTION WHEN others THEN
3711
+ RAISE WARNING 'clinical_requirements_refresh(%): %', p_booking, sqlerrm;
3712
+ RETURN jsonb_build_object('ok', false, 'error', sqlerrm);
3713
+ END;
3714
+ END $function$;
3715
+ `;
3437
3716
  var MIGRATIONS = [
3438
3717
  { id: "001_o_atendimento_clinico_tem_onde_morar", sql: MIGRATION_001_O_ATENDIMENTO_CLINICO_TEM_ONDE_MORAR },
3439
3718
  { id: "002_o_prontuario_do_v1_atravessa", sql: MIGRATION_002_O_PRONTUARIO_DO_V1_ATRAVESSA },
@@ -3456,7 +3735,9 @@ var MIGRATIONS = [
3456
3735
  { id: "019_adiar_e_uma_escolha", sql: MIGRATION_019_ADIAR_E_UMA_ESCOLHA },
3457
3736
  { id: "020_cobrar_um_agendamento_sem_pedido_levanta_o_pedido", sql: MIGRATION_020_COBRAR_UM_AGENDAMENTO_SEM_PEDIDO_LEVANTA_O_PEDIDO },
3458
3737
  { id: "021_agendamento_nascido_no_v2_tambem_tem_regua", sql: MIGRATION_021_AGENDAMENTO_NASCIDO_NO_V2_TAMBEM_TEM_REGUA },
3459
- { id: "022_a_vista_do_que_executar_respeita_a_cerca", sql: MIGRATION_022_A_VISTA_DO_QUE_EXECUTAR_RESPEITA_A_CERCA }
3738
+ { id: "022_a_vista_do_que_executar_respeita_a_cerca", sql: MIGRATION_022_A_VISTA_DO_QUE_EXECUTAR_RESPEITA_A_CERCA },
3739
+ { id: "023_a_revisao_se_escolhe_ao_agendar", sql: MIGRATION_023_A_REVISAO_SE_ESCOLHE_AO_AGENDAR },
3740
+ { id: "024_a_avaliacao_desta_visita_vale_para_o_que_ela_contratou", sql: MIGRATION_024_A_AVALIACAO_DESTA_VISITA_VALE_PARA_O_QUE_ELA_CONTRATOU }
3460
3741
  ];
3461
3742
 
3462
3743
  // src/index.ts