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 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 17:21:17 +05:30
parent a07f393373
commit 57a06bc517
2 changed files with 33 additions and 31 deletions
+9 -13
View File
@@ -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);
+24 -18
View File
@@ -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;
}