mirror of
https://github.com/melgarafael/DeskcommCRM.git
synced 2026-10-02 01:28:34 +08:00
feat(followup): enhance inbound message handling and tests
- Updated `inboundEhDestaPergunta` to utilize `enrollment.updated_at` for better message filtering. - Modified `ultimoInboundDoContato` to return both message text and sent timestamp. - Adjusted `aplicarTextoAosEnrollmentsEmEspera` to apply messages conditionally based on the new timestamp logic. - Refactored tests to cover new behavior, ensuring proper handling of inbound messages without reusing old text. - Improved test descriptions for clarity and added checks for message application logic.
This commit is contained in:
@@ -29,10 +29,21 @@ describe("inboundEhDestaPergunta", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("aplicarTextoNosFollowups — segunda passada", () => {
|
||||
it("reaplica waiting_reply depois de avançar active (corrida cap_nome)", () => {
|
||||
const fonte = readFileSync(join(process.cwd(), "lib/followup/aplicar-inbound.ts"), "utf8");
|
||||
const chamadas = fonte.match(/await aplicarTextoAosEnrollmentsEmEspera\(/g) ?? [];
|
||||
expect(chamadas.length).toBe(2);
|
||||
describe("aplicarTextoNosFollowups — uma mensagem, uma pergunta", () => {
|
||||
const fonte = () => readFileSync(join(process.cwd(), "lib/followup/aplicar-inbound.ts"), "utf8");
|
||||
|
||||
it("filtra waiting_reply com inboundEhDestaPergunta (não reaproveita texto velho)", () => {
|
||||
const src = fonte();
|
||||
expect(src).toMatch(/inboundEhDestaPergunta\(enviadaEm, enrollment\.updated_at\)/);
|
||||
expect(src).toMatch(/if \(!enviadaEm \|\| !inboundEhDestaPergunta/);
|
||||
});
|
||||
|
||||
it("não tem 2ª passada cega fora do loop (regressão: confirm + endereço no mesmo request)", () => {
|
||||
const src = fonte();
|
||||
// Uma chamada só, dentro do for — a 2ª passada solta reaplicava o texto
|
||||
// que enfileirou a confirmação de nome como se fosse o SIM.
|
||||
const chamadas = src.match(/await aplicarTextoAosEnrollmentsEmEspera\(/g) ?? [];
|
||||
expect(chamadas.length).toBe(1);
|
||||
expect(src).toMatch(/for \(let i = 0; i < 6; i\+\+\) \{[\s\S]*aplicarTextoAosEnrollmentsEmEspera/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -49,19 +49,24 @@ function tickDepsDe(admin: SupabaseClient): TickDeps {
|
||||
/**
|
||||
* Uma mensagem do lead vale para UM `match_reply`. Reaplicar o mesmo texto
|
||||
* no passo seguinte (endereço, motivo…) pula a pergunta.
|
||||
*
|
||||
* `esperaDesde` = `enrollment.updated_at` no momento em que o nó estacionou
|
||||
* em `waiting_reply` (confirmação de nome, pergunta de endereço, etc.).
|
||||
*/
|
||||
export function inboundEhDestaPergunta(enviadaEm: string, esperaDesde: string): boolean {
|
||||
return enviadaEm >= esperaDesde;
|
||||
}
|
||||
|
||||
type InboundResolvido = { texto: string; enviadaEm: string | null };
|
||||
|
||||
async function ultimoInboundDoContato(
|
||||
admin: SupabaseClient,
|
||||
orgId: string,
|
||||
contactIds: string[],
|
||||
): Promise<string> {
|
||||
): Promise<InboundResolvido> {
|
||||
const { data, error } = await admin
|
||||
.from("messages")
|
||||
.select("body")
|
||||
.select("body, sent_at")
|
||||
.eq("organization_id", orgId)
|
||||
.in("contact_id", contactIds)
|
||||
.eq("direction", "inbound")
|
||||
@@ -69,7 +74,9 @@ async function ultimoInboundDoContato(
|
||||
.limit(1)
|
||||
.maybeSingle();
|
||||
if (error) throw new Error(error.message);
|
||||
return typeof data?.body === "string" ? data.body.trim() : "";
|
||||
const texto = typeof data?.body === "string" ? data.body.trim() : "";
|
||||
const enviadaEm = typeof data?.sent_at === "string" ? data.sent_at : null;
|
||||
return { texto, enviadaEm };
|
||||
}
|
||||
|
||||
async function aplicarTextoAosEnrollmentsEmEspera(
|
||||
@@ -77,8 +84,9 @@ async function aplicarTextoAosEnrollmentsEmEspera(
|
||||
orgId: string,
|
||||
contactIds: string[],
|
||||
texto: string,
|
||||
enviadaEm: string | null,
|
||||
deps: TickDeps,
|
||||
): Promise<void> {
|
||||
): Promise<number> {
|
||||
const { data, error } = await admin
|
||||
.from("followup_enrollments")
|
||||
.select("*")
|
||||
@@ -86,9 +94,16 @@ async function aplicarTextoAosEnrollmentsEmEspera(
|
||||
.in("contact_id", contactIds)
|
||||
.eq("status", "waiting_reply");
|
||||
if (error) throw new Error(error.message);
|
||||
let aplicados = 0;
|
||||
for (const row of data ?? []) {
|
||||
await aplicarRespostaInbound(deps, row as EnrollmentRow, texto);
|
||||
const enrollment = row as EnrollmentRow;
|
||||
// Sem sent_at não dá pra saber se a mensagem é desta pergunta — fail-closed
|
||||
// (igual ao relógio): não avança com texto velho.
|
||||
if (!enviadaEm || !inboundEhDestaPergunta(enviadaEm, enrollment.updated_at)) continue;
|
||||
await aplicarRespostaInbound(deps, enrollment, texto);
|
||||
aplicados++;
|
||||
}
|
||||
return aplicados;
|
||||
}
|
||||
|
||||
export async function aplicarTextoNosFollowups(
|
||||
@@ -96,12 +111,24 @@ export async function aplicarTextoNosFollowups(
|
||||
sinal: SinalDeInboundFollowup,
|
||||
): Promise<void> {
|
||||
const contactIds = await idsDoContatoEGemeos(admin, sinal.organizationId, sinal.contactId);
|
||||
const texto = (sinal.texto?.trim() || (await ultimoInboundDoContato(admin, sinal.organizationId, contactIds))).trim();
|
||||
const ultimo = await ultimoInboundDoContato(admin, sinal.organizationId, contactIds);
|
||||
const texto = (sinal.texto?.trim() || ultimo.texto).trim();
|
||||
if (!texto) return;
|
||||
const enviadaEm = ultimo.enviadaEm;
|
||||
const deps = tickDepsDe(admin);
|
||||
// 1ª passada: resposta chegou com o enrollment já em waiting_reply.
|
||||
await aplicarTextoAosEnrollmentsEmEspera(admin, sinal.organizationId, contactIds, texto, deps);
|
||||
|
||||
// Apply dentro do loop (não numa 2ª passada cega): a mensagem que ENFILEIROU
|
||||
// a confirmação de nome não pode responder a essa confirmação no mesmo request.
|
||||
// `inboundEhDestaPergunta` exige sent_at >= updated_at do waiting_reply.
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const aplicados = await aplicarTextoAosEnrollmentsEmEspera(
|
||||
admin,
|
||||
sinal.organizationId,
|
||||
contactIds,
|
||||
texto,
|
||||
enviadaEm,
|
||||
deps,
|
||||
);
|
||||
const agora = new Date().toISOString();
|
||||
const { data: vivos, error: vivosErr } = await admin
|
||||
.from("followup_enrollments")
|
||||
@@ -116,9 +143,6 @@ export async function aplicarTextoNosFollowups(
|
||||
await avancarEnrollmentAtivo(deps, row as EnrollmentRow);
|
||||
}
|
||||
const enviados = await enviarTextoFixoPendente(admin, contactIds);
|
||||
if (!(vivos?.length) && !enviados) break;
|
||||
if (!aplicados && !(vivos?.length) && !enviados) break;
|
||||
}
|
||||
// 2ª passada: o SIM pode ter chegado enquanto o nó ainda era `active` (ex.:
|
||||
// cap_nome enfileirando a pergunta de confirmação neste mesmo request).
|
||||
await aplicarTextoAosEnrollmentsEmEspera(admin, sinal.organizationId, contactIds, texto, deps);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user