From 58f51eafe3d89f203a288095be20b40f22807fbe Mon Sep 17 00:00:00 2001 From: W1 CAPITAL Date: Fri, 9 Oct 2026 17:56:26 -0300 Subject: [PATCH 1/6] fix(wa-auto): include verified movement notices for closed portfolio cases --- src/lib/wa-daily-return-policy.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/lib/wa-daily-return-policy.ts b/src/lib/wa-daily-return-policy.ts index 311401fb..718243e9 100644 --- a/src/lib/wa-daily-return-policy.ts +++ b/src/lib/wa-daily-return-policy.ts @@ -64,10 +64,13 @@ export function isStatusRequest(input:string):boolean { * A date-only last-return record cannot distinguish two events on the same day. */ export function prepareDailyReturn(row:ReturnCase,opts:{ - mode:ReturnMode;today?:string;intervalDays?:number;consentAttested?:boolean; + mode:ReturnMode;today?:string;intervalDays?:number;consentAttested?:boolean;includeClosed?:boolean; }):ReturnDecision { const meta=row.dados||{}; - if(isCasoEncerrado(row))return {ready:null,reason:'closed'}; + // Movimento posterior confirmado pode ocorrer em uma carteira marcada como encerrada. + // Apenas os avisos de movimentação da carteira optam por incluir esses casos; + // um status interno nunca é prova de encerramento definitivo no tribunal. + if(isCasoEncerrado(row) && !opts.includeClosed)return {ready:null,reason:'closed'}; const hasOptIn=['whatsapp_opt_in','consentimento_whatsapp','whatsapp_autorizado'].some(k=>affirmative(meta[k])); if(['nao_contatar','não_contatar','whatsapp_opt_out','optOut','optout','bloquear_whatsapp','naoEnviarWhatsapp'].some(k=>blockedValue(meta[k])) || ['whatsapp_opt_in','consentimento_whatsapp','whatsapp_autorizado'].some(k=>denied(meta[k]))) return {ready:null,reason:'blocked'}; From 681b0a02b17fe7995877af5cc5a9173203c4c658 Mon Sep 17 00:00:00 2001 From: W1 CAPITAL Date: Fri, 9 Oct 2026 17:56:29 -0300 Subject: [PATCH 2/6] fix(wa-auto): include verified movement notices for closed portfolio cases --- src/lib/wa-movement-builder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/wa-movement-builder.ts b/src/lib/wa-movement-builder.ts index cf0a8743..2fa601e8 100644 --- a/src/lib/wa-movement-builder.ts +++ b/src/lib/wa-movement-builder.ts @@ -9,7 +9,7 @@ export type Alert = { /** Both scanner and portfolio queues use the same last-return cutoff. */ export function prepareMovementAlert(row: SourceRow, consentAttested = false) { - const result = prepareDailyReturn(row, {mode:'single', consentAttested}); + const result = prepareDailyReturn(row, {mode:'single', consentAttested, includeClosed:true}); const notice = result.ready; if (!notice) return {alert:null, reason:result.reason}; return {reason:'ok' as const, alert:{ From 42337e11a5f4017fddb0c960e1e5e7149f724aaa Mon Sep 17 00:00:00 2001 From: W1 CAPITAL Date: Fri, 9 Oct 2026 17:56:32 -0300 Subject: [PATCH 3/6] fix(wa-auto): include verified movement notices for closed portfolio cases --- src/lib/wa-movement-campaign.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/lib/wa-movement-campaign.ts b/src/lib/wa-movement-campaign.ts index b23e1b72..c8851051 100644 --- a/src/lib/wa-movement-campaign.ts +++ b/src/lib/wa-movement-campaign.ts @@ -10,6 +10,7 @@ import { persistWhatsAppMessage } from '@/lib/whatsapp-persist'; import { prepareMovementAlert, type SourceRow, type Alert } from '@/lib/wa-movement-builder'; import {getDailyReturnSettings,upsertDailyReturnSettings} from '@/lib/wa-daily-return-service'; import {brazilToday} from '@/lib/wa-daily-return-policy'; +import {isCasoEncerrado} from '@/lib/status-encerrado'; async function requireManager() { @@ -28,7 +29,7 @@ async function collectPortfolio(empresaId: string, kind:CampaignKind='movement', const db = await getSupabaseAdmin(); const entries: Alert[] = []; const counts = { scanned:0, withoutPhone:0, withoutEvent:0, blocked:0, samePhone:0, - alreadyClosed:0, consentMissing:0, needsReview:0, alreadyNotified:0, missingReturn:0, noNewMovement:0 }; + alreadyClosed:0, closedEligible:0, consentMissing:0, needsReview:0, alreadyNotified:0, missingReturn:0, noNewMovement:0 }; let after=0; for (;;) { const {data,error}=await db.rpc('wa_notice_portfolio_rows',{p_empresa:empresaId,p_after:after,p_limit:3000}); @@ -46,7 +47,10 @@ async function collectPortfolio(empresaId: string, kind:CampaignKind='movement', else if(reason==='needs_source_review')counts.needsReview++; else if(reason==='no_new_movement')counts.noNewMovement++; else counts.withoutEvent++; - } else entries.push(alert); + } else { + entries.push(alert); + if(isCasoEncerrado(row))counts.closedEligible++; + } } if(rows.length<3000)break; after=Number(rows[rows.length-1].id); @@ -99,7 +103,7 @@ export async function createMovementCampaign(confirmed: boolean,kind:CampaignKin const {entries}=await collectPortfolio(ctx.empresa_id,kind,confirmed||settings.consentAttested); if (!entries.length) throw new Error( kind === 'publication' - ? 'Nenhum aviso elegível: confira telefone, data do último retorno e novidade em processos abertos.' + ? 'Nenhum aviso elegível: confira telefone, último retorno e nova publicação verificada na carteira.' : 'Nenhum processo com telefone válido e movimentação identificada.' ); const {data: campaign,error: createErr} = await db.from('wa_movement_campaigns') From 5430ad20c26052391e5f35c470d1cf0372954e58 Mon Sep 17 00:00:00 2001 From: W1 CAPITAL Date: Fri, 9 Oct 2026 17:56:34 -0300 Subject: [PATCH 4/6] fix(wa-auto): include verified movement notices for closed portfolio cases --- .../whatsapp/movement-campaign-control.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/whatsapp/movement-campaign-control.tsx b/src/components/whatsapp/movement-campaign-control.tsx index 2919b9cf..6f530bd2 100644 --- a/src/components/whatsapp/movement-campaign-control.tsx +++ b/src/components/whatsapp/movement-campaign-control.tsx @@ -22,7 +22,7 @@ type CampaignKind = "movement" | "publication"; type Preview = { ok:boolean;error?:string;consentAttested?:boolean;kind?:CampaignKind;counts?:{ scanned:number;withoutPhone:number;withoutEvent:number;blocked:number;samePhone:number; - alreadyQueued:number;eligible:number;alreadyClosed?:number;consentMissing?:number; + alreadyQueued:number;eligible:number;alreadyClosed?:number;closedEligible?:number;consentMissing?:number; needsReview?:number;alreadyNotified?:number;missingReturn?:number;noNewMovement?:number; }; samples?:Sample[]; }; @@ -172,8 +172,8 @@ export function MovementCampaignControl() { {kind==='publication'?'Avisar novidades pendentes da carteira':'Avisar clientes — última movimentação'} {kind==='publication' - ? 'Considera toda a carteira da empresa, de todos os responsáveis. Envia apenas para processos abertos com movimentação DataJud/DJEN posterior ao último retorno marcado. Eventos antigos e processos encerrados são excluídos.' - : 'Consulta toda a carteira da empresa. Prepara um aviso somente quando há movimentação posterior ao último retorno, excluindo encerrados, bloqueados e avisos já registrados.'} + ? 'Considera toda a carteira da empresa, de todos os responsáveis. Considera também processos marcados como encerrados. Envia somente movimentação DataJud/DJEN posterior ao último retorno, com teor oficial verificável, consentimento e sem bloqueio.' + : 'Consulta toda a carteira da empresa. Prepara um aviso por novidade posterior ao último retorno, inclusive de processos marcados como encerrados. Bloqueios, avisos repetidos e eventos sem prova verificável são excluídos.'} {loading&&!preview?
Conferindo todos os processos...
:null} @@ -197,7 +197,7 @@ export function MovementCampaignControl() { {preview.counts.blocked} bloqueados/não contatar; {preview.counts.alreadyQueued} já preparados ou enviados (não duplicar). {(
-

{preview.counts.alreadyClosed||0} já encerrados na carteira (excluídos)

+

{preview.counts.closedEligible||0} marcados como encerrados com aviso verificável elegível (incluídos na seleção)

{preview.counts.consentMissing||0} sem autorização expressa de WhatsApp (excluídos)

{preview.counts.noNewMovement||0} sem novidade após o último retorno (excluídos)

{preview.counts.missingReturn||0} sem data de último retorno para comparar

@@ -211,8 +211,8 @@ export function MovementCampaignControl() {
Nenhum comunicado está pronto neste momento. Retornos vencidos não são envios aptos. O scanner DataJud + DJEN pode conferir casos individuais; somente novidades oficiais - posteriores ao último retorno entram na fila. Casos encerrados e telefones ausentes - permanecem excluídos. + posteriores ao último retorno entram na fila. Casos sem novidade verificável e telefones ausentes + permanecem excluídos, independentemente do status interno.
)}
From bad063feaae2ffca85a7cf421d523ac553ad6b34 Mon Sep 17 00:00:00 2001 From: W1 CAPITAL Date: Fri, 9 Oct 2026 17:56:36 -0300 Subject: [PATCH 5/6] fix(wa-auto): include verified movement notices for closed portfolio cases --- src/lib/wa-daily-return-policy.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/lib/wa-daily-return-policy.test.ts b/src/lib/wa-daily-return-policy.test.ts index e2ea790e..29a33b32 100644 --- a/src/lib/wa-daily-return-policy.test.ts +++ b/src/lib/wa-daily-return-policy.test.ts @@ -1,5 +1,6 @@ import {describe,expect,it} from 'vitest'; import {isStatusRequest,normalizePhone,prepareDailyReturn,type ReturnCase} from './wa-daily-return-policy'; +import {prepareMovementAlert} from './wa-movement-builder'; const base:ReturnCase={ id:1,empresa_id:'11111111-1111-1111-1111-111111111111', cliente:'Maria de Souza',telefone:'(11) 99999-4321', @@ -43,6 +44,16 @@ describe('retorno inteligente DataJud/DJEN',()=>{ expect(prepareDailyReturn({...base,ultimo_retorno:null},{mode:'due',today:'2026-10-09'}).reason).toBe('missing_return'); expect(prepareDailyReturn({...base,status:'ENCERRADO'},{mode:'due',today:'2026-10-09'}).reason).toBe('closed'); }); + it('includes closed cases only in verified movement campaigns, not routine due returns',()=>{ + const closed={...base,status:'ENCERRADO'}; + const allowed=prepareMovementAlert(closed); + expect(allowed.reason).toBe('ok'); + expect(allowed.alert?.message).toContain('Setor Processual'); + expect(allowed.alert?.message).not.toMatch(/seu processo foi encerrado|seu processo foi extinto/i); + expect(prepareDailyReturn(closed,{mode:'due',today:'2026-10-09'}).reason).toBe('closed'); + expect(prepareMovementAlert({...closed,ultimo_retorno:'2026-10-08'}).reason).toBe('no_new_movement'); + expect(prepareMovementAlert({...closed,dados:{whatsapp_opt_in:true,whatsapp_opt_out:true}}).reason).toBe('blocked'); + }); it('requires explicit opt-in and honors opt-out',()=>{ expect(prepareDailyReturn({...base,dados:{}},{mode:'due',today:'2026-10-09'}).reason).toBe('no_consent'); expect(prepareDailyReturn({...base,dados:{whatsapp_opt_in:true,nao_contatar:true}},{mode:'due',today:'2026-10-09'}).reason).toBe('blocked'); From 09b482f5c7ab8f0102acb19fbbcbd6f2c64f968e Mon Sep 17 00:00:00 2001 From: W1 CAPITAL Date: Fri, 9 Oct 2026 17:57:54 -0300 Subject: [PATCH 6/6] test(wa-auto): assert newly allowed closed-case movements --- src/lib/wa-movement-builder.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/wa-movement-builder.test.ts b/src/lib/wa-movement-builder.test.ts index f2e410d0..3df4ca33 100644 --- a/src/lib/wa-movement-builder.test.ts +++ b/src/lib/wa-movement-builder.test.ts @@ -33,8 +33,8 @@ describe('Avisos processuais WA.Auto',()=>{ expect(prepareMovementAlert({...base,dados:{whatsapp_opt_in:false}}).reason).toBe('blocked'); expect(prepareMovementAlert({...base,dados:{consentimento_whatsapp:'não'}}).reason).toBe('blocked'); }); - it('excludes closed and already-reported events',()=>{ - expect(prepareMovementAlert({...base,status:'ENCERRADO'}).reason).toBe('closed'); + it('includes closed cases with verified updates, but excludes already-reported events',()=>{ + expect(prepareMovementAlert({...base,status:'ENCERRADO'}).reason).toBe('ok'); expect(prepareMovementAlert({...base,ultimo_retorno:'2026-10-05'}).reason).toBe('no_new_movement'); expect(prepareMovementAlert({...base,ultimo_retorno:null}).reason).toBe('missing_return'); });