Přeskočit na obsah

Duplicitní poznámky — vícero entry points zapisuje stejný obsah

import { Aside } from ‘@astrojs/starlight/components’;

Uživatel v UI u klienta vidí v sekci „Poznámky” dvě skoro identické záznamy:

[Zapier/Meta Ads – Lead #001110]
Typ služby: Renovace střechy
Region: Pardubický
Popis: Klient pozadáno na…
[Lead #001110 – konverze CONTACTED]
Typ služby: Renovace střechy
Region: Pardubický
Popis: Klient pozadáno na…

Liší se jen hlavička. Stejný popis, stejný klient, jen 2× zapsaný. Stěžování od klienta: „poznámky se duplikují, je toho moc”.

Dva nezávislé entry pointy zapisují note pro stejný kontext:

1) Webhook (Zapier / Meta Ads):

/api/webhooks/zapier-leads/route.ts
const lead = await prisma.lead.create(...);
if (fullDescription) {
await prisma.note.create({
data: {
clientId: client.id,
content: `[Zapier/Meta Ads – Lead #${leadNumber}]\n${fullDescription}`,
noteType: "lead_import",
},
});
}

2) Konverzní flow (PUT lead status):

// /api/leads/[id]/route.ts při status NEW → CONTACTED
const fullLead = await prisma.lead.findUnique(...);
if (fullLead?.description) {
await prisma.note.create({
data: {
clientId: lead.client.id,
content: `[Lead #${fullLead.leadNumber} – konverze ${newStatus}]\n${fullLead.description}`,
noteType: "lead_conversion",
},
});
}

Oba endpointy vytvářejí poznámku se stejným popisem leadu, jen s jiným hlavičkovým „štítkem”. Žádný z nich nekontroluje, že druhý už něco vytvořil. Při rapid status change (NEW → CONTACTED → INTERESTED v krátkém čase) se to ještě násobí — pro každý přechod vznikne nová lead_conversion poznámka.

Vznik problému je organický: tým přidal webhook s noty (CRM-XX), o měsíc později jiný tým přidal konverzní flow s noty (CRM-YY). Oba devs viděli „svůj” code path, neviděli druhý — duplikace prošla code review.

1) Idempotentní create — check before insert:

// V konverzním flow (i ve webhooku — symetricky):
const leadKey = fullLead.leadNumber || fullLead.id;
const existing = await prisma.note.findFirst({
where: {
clientId: lead.client.id,
OR: [
{ content: { startsWith: `[Lead #${leadKey}` } },
{ content: { startsWith: `[Zapier/Meta Ads – Lead #${leadKey}` } },
],
},
select: { id: true },
});
if (!existing) {
await prisma.note.create({ data: { clientId, content, noteType } });
}

Klíč deduplikace = prefix obsahu obsahující business ID (lead number / order number / entity ID). Není to perfektní (regex match na string), ale je deterministický a triviálně rychlý.

2) Cleanup existujících duplicit:

BEGIN;
-- Pokud klient má lead_import + lead_conversion pro stejný lead, lead_import vyhrává (víc detailů)
DELETE FROM "Note" c
WHERE c."noteType" = 'lead_conversion'
AND EXISTS (
SELECT 1 FROM "Note" i
WHERE i."clientId" = c."clientId" AND i."noteType" = 'lead_import'
);
-- Mezi více lead_conversion pro stejného klienta nech nejnovější
WITH ranked AS (
SELECT id, ROW_NUMBER() OVER (PARTITION BY "clientId" ORDER BY "createdAt" DESC) AS rn
FROM "Note" WHERE "noteType" = 'lead_conversion'
)
DELETE FROM "Note" WHERE id IN (SELECT id FROM ranked WHERE rn > 1);
COMMIT;

3) Lepší dlouhodobé řešení — DB constraint:

Místo runtime kontroly přidat unique constraint na úrovni DB:

CREATE UNIQUE INDEX note_lead_unique_idx
ON "Note" ("clientId", (substring(content from '\[(Lead|Zapier/Meta Ads . Lead) #([^ \]]+)')))
WHERE "noteType" IN ('lead_import', 'lead_conversion');

DB pak odmítne duplicitu, takže ji nikdo nezapomene.

Co zkontrolovat ve svém kódu

Sekce “Co zkontrolovat ve svém kódu”
Terminál
# Najít všechna místa, kde se vytváří entita stejného typu
grep -rn 'prisma\.note\.create\|prisma\.activity\.create\|prisma\.emailLog\.create' src/
# Pro každé místo si položit otázku:
# "Existuje druhý entry point, který může vytvořit záznam pro stejný kontext?"
# (webhook + UI button + cron + interní service call)

Multi-source create je typický pro:

  • Email log — IMAP sync + ručně poslaný e-mail z CRM + automatický shoot z webhooku
  • Activity log — interní log + audit log + business action log
  • Note/comment — manuální + import + konverze + cron summary
  • [[soft-delete-preserve-history]] — souvisí s tím, jak různé entry pointy mají respektovat business semantiku.
  • Idempotence je obecně téma — webhook retries, double-click prevention, race conditions.
Přidal aiarchitekt.cz · 1. 6. 2026 2:00
Provozuje aiarchitekt.cz