Das Ereignis, das zweimal ankommt: Warum Firmspace Zahlung, Nachricht und Anruf nur einmal zählt
Ein Zahlungsanbieter schickt einen Webhook. Das CRM bucht die Zahlung, antwortet, und der Anbieter — der die Antwort nicht schnell genug erhalten hat — schickt dieselbe Meldung noch einmal. Ein Messenger wiederholt eine Zustellung nach einem Timeout; ein Telefonsystem meldet dasselbe Gesprächsergebnis zweimal. Das ist normales Verhalten im Netz, keine kaputte Integration. Gefährlich ist, was ein CRM mit der zweiten Kopie macht.
Firmspace antwortet mit einer einfachen Regel: Jedes Ereignis muss eine Identität von seiner Quelle tragen, und das CRM prüft diese Identität, bevor es etwas ändert. Geld, Nachrichten und Anrufe nutzen verschiedene Tabellen und verschiedene Schlüssel, aber das Prinzip ist dasselbe. Dasselbe Ereignis kann viele Male ankommen; seine Wirkung darf nur einmal eintreten. Diese Identität ist auch die Voraussetzung dafür, dass ein Assistent oder eine Automatisierungsregel auf das Ereignis reagieren darf: die Maschine handelt einmal, nicht zweimal. Die zweite Zustellung wird bestätigt und dann ignoriert.
Nehmen wir zuerst eine Kartenzahlung. Die Webhook-Route prüft die Signatur des Anbieters, bevor sie den Betrag liest; eine ungültige Signatur wird abgewiesen. Zwischenstände wie created oder processing ändern nichts. Nur der Status paid geht weiter. Der Idempotenzschlüssel ist ein gekürzter SHA-256-Fingerabdruck des rohen Meldungstextes. Anbieter, die wiederholen, senden meist dieselben Bytes, also erzeugt dieselbe Meldung denselben Schlüssel.
Dieser Schlüssel geht an registerPayment, die einzige Funktion, die in Firmspace Geld bucht. Der manuelle Bezahlt-Knopf, der Assistent, der Acquiring-Webhook und der Bankabgleich enden alle dort. In einer Datenbanktransaktion sperrt die Funktion die Rechnungszeile, liest sie und sucht nach einem Zahlungsereignis mit derselben Organisation, Quelle und externen Id. Existiert das Ereignis, lautet die Antwort duplicate, und der angewandte Betrag ist null. Es wird nichts geschrieben. Existiert es nicht, wendet die Funktion den Betrag an, legt das Ereignis an und aktualisiert die Rechnung.
Die Datenbank erzwingt dieselbe Regel mit einem eindeutigen Schlüssel über Organisation, Quelle und externe Id. Zwei identische Meldungen, die im selben Moment ankommen, können nicht beide ein Ereignis anlegen: eine Transaktion gewinnt, und die zweite sieht entweder nach der Rechnungssperre das bestehende Ereignis oder läuft in den eindeutigen Schlüssel; beide Ausgänge gelten als Duplikat, nicht als Ausfall. Die Sperre der Rechnungszeile schließt das verbleibende Fenster, sodass zwei verschiedene Zahlungen nicht denselben Stand bereits bezahlt lesen und einander überschreiben.
Teilzahlungen sind der Punkt, an dem das am meisten zählt. Ein Kunde zahlt eine Anzahlung; der Anbieter wiederholt die Meldung; ohne Schlüssel würde die zweite Zustellung die Anzahlung erneut addieren, und die Rechnung sähe bezahlter aus, als sie ist. Mit dem Schlüssel liefert die zweite Zustellung dieselbe Rechnung und einen angewandten Betrag von null. Der Saldo bewegt sich nicht. Wird gar kein Betrag mitgeschickt, wendet die Funktion nur den noch offenen Rest an, nicht das volle Brutto ein zweites Mal.
Der Antwortcode gehört zum Mechanismus. Ein Duplikat liefert HTTP 200 mit duplicate true; eine bereits geschlossene Rechnung liefert ebenfalls Erfolg, weil der Anbieter aufhören soll zu wiederholen. Ein Serverfehler würde das Gegenteil bewirken: er würde weitere Versuche einladen. Die Duplikat-Antwort ist deshalb kein technisches Detail. Sie ist die Art, wie das CRM dem Absender sagt, dass das Ereignis bereits gespeichert ist.
Bankauszüge folgen derselben Disziplin. Eine monobank-Zeile erhält eine externe Id aus der Id ihrer Zeile im Auszug; eine CSV-Zeile trägt die Id aus ihrer Datei. Vor dem Einfügen lädt der Import die für dieses Konto bereits bekannten externen Ids und filtert die eingehenden Zeilen. Ein wiederholter Import desselben Auszugs erzeugt keine zweite Bewegung. Die Zuordnung einer Bankzeile zu einer Rechnung nutzt die Id der Banktransaktion als Zahlungsschlüssel; dieselbe Zeile erneut zuzuordnen fügt kein Geld hinzu, und ein Wechsel der Zuordnung macht zuerst die alte Zahlung rückgängig und wendet dann die neue an.
Nachrichten haben ihre eigene Version der Regel. Eine eingehende Nachricht eines Anbieters trägt eine Nachrichten-Id. Die Kanalschicht prüft das Paar aus Integration und Nachrichten-Id, bevor sie einen Anhang herunterlädt — eine Wiederholung soll dieselbe Datei nicht zweimal speichern — und prüft kurz vor dem Einfügen der Nachricht noch einmal. Ein Duplikat liefert die bestehende Unterhaltung und keine neue Nachricht, sodass Benachrichtigung, Lead-Filter und Aktivitätseintrag im Kontakt nicht wiederholt werden.
Anrufe nutzen dieselbe Kanalschicht. Der Browser oder der SIP-Anbieter sendet eine Call-Id, eine Richtung, einen Teilnehmer, einen Status und eine Dauer. Die Schicht macht daraus den Schlüssel call:CALL_ID:DIRECTION. Ein wiederholter Aufruf liefert Erfolg mit duplicate true und HTTP 200 statt 201. Der Anrufeintrag, der Eintrag in der Aktivität des Kontakts, das Ereignis für einen verpassten Anruf und die Benachrichtigung existieren einmal. Das ist wichtig, weil ein verpasster Anruf eine Automatisierungsregel starten oder eine Aufgabe erzeugen kann; zwei Einträge würden zwei Aufgaben bedeuten.
Auch die Folgen sind geschützt. Eine Benachrichtigung kann einen Schlüssel tragen; existiert bereits eine Benachrichtigung mit derselben Organisation und demselben Schlüssel, wird die zweite nicht angelegt. Team-Benachrichtigungen hängen den Empfänger an den Schlüssel, sodass jeder Kollege trotzdem genau eine Kopie erhält. Wenn ein Aktivitätseintrag einen Schlüssel trägt, prüft das Aktivitätsprotokoll in einer Chance, einem Kontakt oder einer Firma diesen Schlüssel, bevor es den Eintrag anhängt; der Zahlungseintrag etwa trägt einen Schlüssel aus Rechnung, Quelle und externer Id. Eine wiederholte Zahlung fügt der Kundengeschichte daher keine zweite Zeile Zahlung erhalten hinzu.
Es gibt ehrliche Grenzen. Die Kanalprüfung ist ein Lesen gefolgt von einem Einfügen, kein eindeutiger Datenbank-Schlüssel auf der Nachrichten-Id; ein schmales Rennen bleibt möglich, obwohl der Code zweimal prüft. Auch der Bankimport filtert bekannte externe Ids im Anwendungscode, statt sich auf einen eindeutigen Schlüssel zu verlassen. Und kein Schlüssel kann entscheiden, dass zwei verschiedene Anbieter-Ids dieselbe reale Überweisung beschreiben. Ändert ein Anbieter die Id bei jeder Wiederholung, oder wird eine Zahlung zuerst vom Kartenprozessor und später von der Bank gemeldet, kann nur ein Mensch beim Abgleich entscheiden, ob es dasselbe Geld ist.
Was bewusst nicht zusammengelegt wird, ist ebenso wichtig. Zwei Zahlungen mit verschiedenen Anbieter-Ids sind zwei Zahlungen, auch wenn Betrag und Kunde übereinstimmen. Zwei Nachrichten mit verschiedenen Ids sind zwei Nachrichten. Ein Kunde, der zweimal hallo schreibt, hat zweimal geschrieben; ein Anbieter, der eine Nachricht wiederholt, nicht. Die Regel ist Identität, nicht Ähnlichkeit. Das Zusammenlegen ähnlicher Ereignisse würde echtes Wiederholungsgeschäft verstecken und das Hauptbuch in die andere Richtung belügen.
Der Knopf Rechnung duplizieren gehört in eine andere Kategorie. Er legt bewusst einen neuen Entwurf mit neuer Nummer an; das ist eine Kopie, keine idempotente Wiederholung. Auch eine Gutschrift ist ein neues Dokument mit eigenen Regeln. Die Ereignisregel gilt für dasselbe Ereignis, das erneut ankommt, nicht für einen Menschen, der ein weiteres Dokument verlangt.
Deshalb geht das Thema über die Verkabelung hinaus. Die Schreibaktionen des Assistenten nutzen dieselben Werkzeuge und beim Geld dieselbe Zahlungsfunktion. Automatisierungsregeln hören auf Ereignisse wie invoice paid, message received oder call missed. Wird ein Duplikat einmal gespeichert, läuft die Regel einmal; wäre es zweimal gespeichert, könnte ein KI-Schritt zwei Antworten senden, zwei Aufgaben anlegen oder dieselbe Zahlung zweimal buchen. Wiederholungen sind nur sicher, wenn die Wirkung eindeutig ist.
Ein CRM kann das Netz nicht davon abhalten, sich zu wiederholen. Es kann die Wiederholung harmlos machen. Die Designentscheidung in Firmspace ist, jedem Ereignis einen Namen von seiner Quelle zu geben, diesen Namen vor dem Handeln zu prüfen, die Datenbank dort, wo es um Geld geht, die Eindeutigkeit erzwingen zu lassen und einem Duplikat mit Erfolg statt mit einem Fehler zu antworten. Der Inhaber sieht eine Zahlung, eine Nachricht, einen Anruf — und einen Satz von Folgen.