Duplicitní poznámky — vícero entry points zapisuje stejný obsah
import { Aside } from ‘@astrojs/starlight/components’;
Symptom
Sekce “Symptom”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řechyRegion: PardubickýPopis: Klient pozadáno na…
[Lead #001110 – konverze CONTACTED]Typ služby: Renovace střechyRegion: 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”.
Root cause
Sekce “Root cause”Dva nezávislé entry pointy zapisují note pro stejný kontext:
1) Webhook (Zapier / Meta Ads):
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 → CONTACTEDconst 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.
Fix
Sekce “Fix”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" cWHERE 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”# Najít všechna místa, kde se vytváří entita stejného typugrep -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
Související
Sekce “Související”- [[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.