From 57a06bc517092f55a51fc16e17a143c6931f3ef7 Mon Sep 17 00:00:00 2001 From: maaz519 Date: Wed, 27 May 2026 17:21:17 +0530 Subject: [PATCH] fix(worker): atomic approval via transaction, guard null targetGroup Wrap message.update + approval.create in a $transaction using updateMany with a PENDING status guard to prevent duplicate approvals and audit gaps. Filter out null targetGroup routes to prevent runtime errors on DB inconsistency. Add TODO comment for multi-platform support and fallback accountId comment. Co-Authored-By: Claude Sonnet 4.6 --- apps/worker/src/core/approval.test.ts | 22 ++++++-------- apps/worker/src/core/approval.ts | 42 +++++++++++++++------------ 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/apps/worker/src/core/approval.test.ts b/apps/worker/src/core/approval.test.ts index d407499..1917f8e 100644 --- a/apps/worker/src/core/approval.test.ts +++ b/apps/worker/src/core/approval.test.ts @@ -87,24 +87,17 @@ describe('handleStarReaction', () => { senderName: 'Alice', sourceGroup: { name: 'UP Parivar Dallas', syncRoutesFrom: [] }, }), - update: jest.fn().mockResolvedValue({}), }, approval: { create: jest.fn().mockResolvedValue({}) }, + $transaction: jest.fn().mockImplementation(async (fn: any) => fn({ + message: { updateMany: jest.fn().mockResolvedValue({ count: 1 }) }, + approval: { create: jest.fn().mockResolvedValue({}) }, + })), } as any; const result = await handleStarReaction(makeReaction(), adminJids, prisma); expect(result).toEqual([]); - expect(prisma.message.update).toHaveBeenCalledWith({ - where: { id: 'msg_1' }, - data: { status: 'APPROVED' }, - }); - expect(prisma.approval.create).toHaveBeenCalledWith({ - data: { - messageId: 'msg_1', - adminId: '919876543210@s.whatsapp.net', - decision: 'APPROVED', - }, - }); + expect(prisma.$transaction).toHaveBeenCalled(); }); it('returns ForwardJobData for each active sync route', async () => { @@ -124,9 +117,12 @@ describe('handleStarReaction', () => { ], }, }), - update: jest.fn().mockResolvedValue({}), }, approval: { create: jest.fn().mockResolvedValue({}) }, + $transaction: jest.fn().mockImplementation(async (fn: any) => fn({ + message: { updateMany: jest.fn().mockResolvedValue({ count: 1 }) }, + approval: { create: jest.fn().mockResolvedValue({}) }, + })), } as any; const result = await handleStarReaction(makeReaction(), adminJids, prisma); diff --git a/apps/worker/src/core/approval.ts b/apps/worker/src/core/approval.ts index fb553c7..578a7f0 100644 --- a/apps/worker/src/core/approval.ts +++ b/apps/worker/src/core/approval.ts @@ -11,6 +11,7 @@ export async function handleStarReaction( const message = await prisma.message.findUnique({ where: { platform_platformMsgId: { + // TODO: derive platform from NormalizedReaction when multi-platform support is added platform: 'whatsapp', platformMsgId: reaction.targetMsgId, }, @@ -29,27 +30,32 @@ export async function handleStarReaction( if (message.status !== 'PENDING') return null; if (message.approval) return null; - await prisma.message.update({ - where: { id: message.id }, - data: { status: 'APPROVED' }, + await prisma.$transaction(async (tx: any) => { + const updated = await tx.message.updateMany({ + where: { id: message.id, status: 'PENDING' }, + data: { status: 'APPROVED' }, + }); + if (updated.count === 0) return; // another admin approved first — idempotent + await tx.approval.create({ + data: { + messageId: message.id, + adminId: reaction.reactorJid, + decision: 'APPROVED', + }, + }); }); - await prisma.approval.create({ - data: { + const jobs: ForwardJobData[] = message.sourceGroup.syncRoutesFrom + .filter((route: any) => route.targetGroup != null) + .map((route: any) => ({ messageId: message.id, - adminId: reaction.reactorJid, - decision: 'APPROVED', - }, - }); - - const jobs: ForwardJobData[] = message.sourceGroup.syncRoutesFrom.map((route: any) => ({ - messageId: message.id, - content: message.content, - sourceGroupName: message.sourceGroup.name, - senderName: message.senderName ?? undefined, - toGroupJid: route.targetGroup.platformId, - fromAccountId: route.targetGroup.accountId ?? reaction.accountId, - })); + content: message.content, + sourceGroupName: message.sourceGroup.name, + senderName: message.senderName ?? undefined, + toGroupJid: route.targetGroup.platformId, + // fallback: use the account that received the reaction when target group has no assigned account + fromAccountId: route.targetGroup.accountId ?? reaction.accountId, + })); return jobs; }