Skip to content

Commit 168e5cf

Browse files
authored
fix: idempotency logic (#529)
1 parent 590e742 commit 168e5cf

File tree

4 files changed

+97
-51
lines changed

4 files changed

+97
-51
lines changed

platforms/blabsy-w3ds-auth-api/src/controllers/WebhookController.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,15 @@ export class WebhookController {
158158
if (tableName === "users") {
159159
docRef = collection.doc(data.ename);
160160
} else if (tableName === "chats") {
161+
// Check if this is a DM with DM ID in description (already processed)
162+
// This prevents duplicate chat creation when webhooks arrive from eCurrency
163+
const description = data.description || mappedData.description;
164+
if (description && typeof description === 'string' && description.startsWith('DM ID:')) {
165+
console.log("⏭️ Skipping DM creation - this DM has DM ID in description (already processed)");
166+
adapter.addToLockedIds(globalId);
167+
return; // Exit early, don't create new chat
168+
}
169+
161170
// Check for existing DM (2 participants, no name) before creating
162171
const participants = mappedData.participants || [];
163172
const isDM = participants.length === 2 && !mappedData.name;

platforms/eCurrency-api/src/services/LedgerService.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -130,21 +130,22 @@ export class LedgerService {
130130

131131
// Send transaction notifications for all transfer types
132132
// (USER-to-USER, USER-to-GROUP, GROUP-to-USER, GROUP-to-GROUP)
133-
try {
133+
// Fire-and-forget: don't block the transfer response
134+
setImmediate(() => {
134135
const notificationService = new TransactionNotificationService();
135-
await notificationService.sendTransactionNotifications(
136+
notificationService.sendTransactionNotifications(
136137
amount,
137138
currency,
138139
fromAccountId,
139140
fromAccountType,
140141
toAccountId,
141142
toAccountType,
142143
description
143-
);
144-
} catch (error) {
145-
// Don't fail the transfer if notification fails
146-
console.error("Error sending transaction notifications:", error);
147-
}
144+
).catch(error => {
145+
// Don't fail the transfer if notification fails
146+
console.error("Error sending transaction notifications:", error);
147+
});
148+
});
148149

149150
return { debit, credit };
150151
}

platforms/eCurrency-api/src/web3adapter/watchers/subscriber.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,12 @@ export class PostgresSubscriber implements EntitySubscriberInterface {
351351
*/
352352
private async sendGroupWebhook(data: any): Promise<void> {
353353
try {
354+
// Skip DMs with DM ID in description (already processed, prevents duplicate webhooks)
355+
if (data.description && typeof data.description === 'string' && data.description.startsWith('DM ID:')) {
356+
console.log(`🔍 Skipping DM webhook - has DM ID in description: ${data.description}`);
357+
return;
358+
}
359+
354360
let globalId = await this.adapter.mappingDb.getGlobalId(data.id);
355361
globalId = globalId ?? "";
356362

platforms/pictique-api/src/controllers/WebhookController.ts

Lines changed: 74 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,13 @@ export class WebhookController {
215215
}
216216
}
217217
} else if (mapping.tableName === "chats") {
218+
// Check if this is a DM with DM ID in description (already processed)
219+
// This prevents duplicate chat creation when webhooks arrive from eCurrency
220+
if (local.data.description && typeof local.data.description === 'string' && local.data.description.startsWith('DM ID:')) {
221+
console.log("⏭️ Skipping DM creation - this DM has DM ID in description (already processed)");
222+
return res.status(200).send("Skipped DM ID group");
223+
}
224+
218225
let participants: User[] = [];
219226
if (
220227
local.data.participants &&
@@ -265,30 +272,20 @@ export class WebhookController {
265272
this.adapter.addToLockedIds(localId);
266273
await this.chatService.chatRepository.save(chat);
267274
} else {
268-
// Check for existing DM (2 participants, no name) before creating
269-
const participantIds = participants.map((p) => p.id);
270-
const isDM = participantIds.length === 2 && !local.data.name;
271-
272-
let chat;
273-
if (isDM) {
274-
const existingChat = await this.chatService.findChatByParticipants(participantIds);
275+
let chat: Chat | null = null;
276+
277+
// First, check if a mapping already exists for this globalId
278+
// This prevents duplicates when webhooks arrive simultaneously from multiple platforms
279+
const existingLocalId = await this.adapter.mappingDb.getLocalId(globalId);
280+
if (existingLocalId) {
281+
const existingChat = await this.chatService.findById(existingLocalId);
275282
if (existingChat) {
276-
// Use existing chat and store mapping
283+
console.log(`⚠️ Chat mapping already exists for globalId ${globalId}, using existing chat: ${existingChat.id}`);
277284
chat = existingChat;
285+
chat.name = local.data.name as string;
286+
chat.participants = participants;
278287
chat.admins = admins;
279-
await this.chatService.chatRepository.save(chat);
280-
this.adapter.addToLockedIds(chat.id);
281-
await this.adapter.mappingDb.storeMapping({
282-
localId: chat.id,
283-
globalId: req.body.id,
284-
});
285-
} else {
286-
// Create new chat
287-
chat = await this.chatService.createChat(
288-
local.data.name as string,
289-
participantIds
290-
);
291-
chat.admins = admins;
288+
chat.ename = local.data.ename as string;
292289
await this.chatService.chatRepository.save(chat);
293290
this.adapter.addToLockedIds(chat.id);
294291
await this.adapter.mappingDb.storeMapping({
@@ -297,17 +294,15 @@ export class WebhookController {
297294
});
298295
}
299296
} else {
300-
// Group chat - check for eCurrency Chat duplicates by name
301-
const chatName = local.data.name as string;
302-
if (chatName && (chatName.startsWith("eCurrency Chat") || chatName.includes("eCurrency Chat"))) {
303-
// Check if chat with this name already exists
304-
const existingChat = await this.chatService.chatRepository.findOne({
305-
where: { name: chatName },
306-
relations: ["participants", "admins"]
307-
});
308-
297+
// Check for existing DM (2 participants, no name) before creating
298+
const participantIds = participants.map((p) => p.id);
299+
const isDM = participantIds.length === 2 && !local.data.name;
300+
301+
let chat;
302+
if (isDM) {
303+
const existingChat = await this.chatService.findChatByParticipants(participantIds);
309304
if (existingChat) {
310-
console.log(`⚠️ eCurrency Chat with name "${chatName}" already exists, using existing: ${existingChat.id}`);
305+
// Use existing chat and store mapping
311306
chat = existingChat;
312307
chat.admins = admins;
313308
await this.chatService.chatRepository.save(chat);
@@ -319,7 +314,7 @@ export class WebhookController {
319314
} else {
320315
// Create new chat
321316
chat = await this.chatService.createChat(
322-
chatName,
317+
local.data.name as string,
323318
participantIds
324319
);
325320
chat.admins = admins;
@@ -331,18 +326,53 @@ export class WebhookController {
331326
});
332327
}
333328
} else {
334-
// Regular group chat - always create new
335-
chat = await this.chatService.createChat(
336-
chatName,
337-
participantIds
338-
);
339-
chat.admins = admins;
340-
await this.chatService.chatRepository.save(chat);
341-
this.adapter.addToLockedIds(chat.id);
342-
await this.adapter.mappingDb.storeMapping({
343-
localId: chat.id,
344-
globalId: req.body.id,
345-
});
329+
// Group chat - check for eCurrency Chat duplicates by name
330+
const chatName = local.data.name as string;
331+
if (chatName && (chatName.startsWith("eCurrency Chat") || chatName.includes("eCurrency Chat"))) {
332+
// Check if chat with this name already exists
333+
const existingChat = await this.chatService.chatRepository.findOne({
334+
where: { name: chatName },
335+
relations: ["participants", "admins"]
336+
});
337+
338+
if (existingChat) {
339+
console.log(`⚠️ eCurrency Chat with name "${chatName}" already exists, using existing: ${existingChat.id}`);
340+
chat = existingChat;
341+
chat.admins = admins;
342+
await this.chatService.chatRepository.save(chat);
343+
this.adapter.addToLockedIds(chat.id);
344+
await this.adapter.mappingDb.storeMapping({
345+
localId: chat.id,
346+
globalId: req.body.id,
347+
});
348+
} else {
349+
// Create new chat
350+
chat = await this.chatService.createChat(
351+
chatName,
352+
participantIds
353+
);
354+
chat.admins = admins;
355+
await this.chatService.chatRepository.save(chat);
356+
this.adapter.addToLockedIds(chat.id);
357+
await this.adapter.mappingDb.storeMapping({
358+
localId: chat.id,
359+
globalId: req.body.id,
360+
});
361+
}
362+
} else {
363+
// Regular group chat - always create new
364+
chat = await this.chatService.createChat(
365+
chatName,
366+
participantIds
367+
);
368+
chat.admins = admins;
369+
await this.chatService.chatRepository.save(chat);
370+
this.adapter.addToLockedIds(chat.id);
371+
await this.adapter.mappingDb.storeMapping({
372+
localId: chat.id,
373+
globalId: req.body.id,
374+
});
375+
}
346376
}
347377
}
348378
}

0 commit comments

Comments
 (0)