From 6f71e5aee9da582edcc1f289304e4296c9dd5fa7 Mon Sep 17 00:00:00 2001 From: maaz519 Date: Wed, 27 May 2026 23:56:14 +0530 Subject: [PATCH] feat(worker): handleStarReaction returns ApprovalResult with indexDoc --- apps/worker/src/core/approval.test.ts | 175 +++++++++++--------------- apps/worker/src/core/approval.ts | 27 +++- 2 files changed, 92 insertions(+), 110 deletions(-) diff --git a/apps/worker/src/core/approval.test.ts b/apps/worker/src/core/approval.test.ts index 1799852..3a05883 100644 --- a/apps/worker/src/core/approval.test.ts +++ b/apps/worker/src/core/approval.test.ts @@ -14,143 +14,111 @@ function makeReaction(overrides: Partial = {}): NormalizedRe const adminJids = ['919876543210@s.whatsapp.net']; +function makeMessage(overrides: object = {}) { + return { + id: 'msg_1', + status: 'PENDING', + approval: null, + content: 'hello world', + senderName: 'Alice', + sourceGroupId: 'grp_1', + tags: ['#important'], + platform: 'whatsapp', + sourceGroup: { name: 'UP Parivar Dallas', syncRoutesFrom: [] }, + ...overrides, + }; +} + +function makePrisma(messageOverrides: object = {}, txCount = 1) { + return { + message: { findUnique: jest.fn().mockResolvedValue(makeMessage(messageOverrides)) }, + $transaction: jest.fn().mockImplementation(async (fn: any) => + fn({ + message: { updateMany: jest.fn().mockResolvedValue({ count: txCount }) }, + approval: { create: jest.fn().mockResolvedValue({}) }, + }), + ), + } as any; +} + describe('handleStarReaction', () => { it('returns null for non-star emoji', async () => { - const result = await handleStarReaction(makeReaction({ emoji: '👍' }), adminJids, {} as any); - expect(result).toBeNull(); + expect(await handleStarReaction(makeReaction({ emoji: '👍' }), adminJids, {} as any)).toBeNull(); }); it('returns null when reactor is not an admin', async () => { - const result = await handleStarReaction( - makeReaction({ reactorJid: 'stranger@s.whatsapp.net' }), - adminJids, - {} as any, - ); - expect(result).toBeNull(); + expect( + await handleStarReaction(makeReaction({ reactorJid: 'stranger@s.whatsapp.net' }), adminJids, {} as any), + ).toBeNull(); }); it('returns null when message not found', async () => { const prisma = { message: { findUnique: jest.fn().mockResolvedValue(null) } } as any; - const result = await handleStarReaction(makeReaction(), adminJids, prisma); - expect(result).toBeNull(); + expect(await handleStarReaction(makeReaction(), adminJids, prisma)).toBeNull(); expect(prisma.message.findUnique).toHaveBeenCalledWith({ - where: { - platform_platformMsgId: { platform: 'whatsapp', platformMsgId: 'TARGET_MSG_123' }, - }, + where: { platform_platformMsgId: { platform: 'whatsapp', platformMsgId: 'TARGET_MSG_123' } }, include: { approval: true, sourceGroup: { - include: { - syncRoutesFrom: { where: { isActive: true }, include: { targetGroup: true } }, - }, + include: { syncRoutesFrom: { where: { isActive: true }, include: { targetGroup: true } } }, }, }, }); }); it('returns null when message status is not PENDING', async () => { + const prisma = { + message: { findUnique: jest.fn().mockResolvedValue(makeMessage({ status: 'REJECTED' })) }, + } as any; + expect(await handleStarReaction(makeReaction(), adminJids, prisma)).toBeNull(); + }); + + it('returns null when approval record already exists', async () => { const prisma = { message: { - findUnique: jest.fn().mockResolvedValue({ - id: 'msg_1', - status: 'REJECTED', - approval: null, - sourceGroup: { name: 'Test Group', syncRoutesFrom: [] }, - }), + findUnique: jest.fn().mockResolvedValue(makeMessage({ status: 'APPROVED', approval: { id: 'appr_1' } })), }, } as any; expect(await handleStarReaction(makeReaction(), adminJids, prisma)).toBeNull(); }); - it('returns null when message is already approved (approval record exists)', async () => { - const prisma = { - message: { - findUnique: jest.fn().mockResolvedValue({ - id: 'msg_1', - status: 'APPROVED', - approval: { id: 'appr_1' }, - sourceGroup: { name: 'Test Group', syncRoutesFrom: [] }, - }), - }, - } as any; - expect(await handleStarReaction(makeReaction(), adminJids, prisma)).toBeNull(); - }); - - it('returns null on double-approval race (updateMany returns count=0)', async () => { - const prisma = { - message: { - findUnique: jest.fn().mockResolvedValue({ - id: 'msg_1', - status: 'PENDING', - approval: null, - content: 'hello', - senderName: 'Alice', - sourceGroup: { name: 'UP Parivar Dallas', syncRoutesFrom: [] }, - }), - }, - $transaction: jest.fn().mockImplementation(async (fn: any) => fn({ - message: { updateMany: jest.fn().mockResolvedValue({ count: 0 }) }, - approval: { create: jest.fn().mockResolvedValue({}) }, - })), - } as any; - - const result = await handleStarReaction(makeReaction(), adminJids, prisma); + it('returns null on double-approval race (updateMany count=0)', async () => { + const result = await handleStarReaction(makeReaction(), adminJids, makePrisma({}, 0)); expect(result).toBeNull(); - expect(prisma.$transaction).toHaveBeenCalled(); }); - it('approves message and returns empty array when no sync routes', async () => { - const prisma = { - message: { - findUnique: jest.fn().mockResolvedValue({ - id: 'msg_1', - status: 'PENDING', - approval: null, - content: 'hello', - senderName: 'Alice', - sourceGroup: { name: 'UP Parivar Dallas', syncRoutesFrom: [] }, - }), - }, - 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.$transaction).toHaveBeenCalled(); + it('returns ApprovalResult with empty forwardJobs and valid indexDoc when no sync routes', async () => { + const result = await handleStarReaction(makeReaction(), adminJids, makePrisma()); + expect(result).not.toBeNull(); + expect(result!.forwardJobs).toEqual([]); + expect(result!.indexDoc).toMatchObject({ + messageId: 'msg_1', + content: 'hello world', + senderName: 'Alice', + sourceGroupId: 'grp_1', + sourceGroupName: 'UP Parivar Dallas', + tags: ['#important'], + platform: 'whatsapp', + }); + expect(result!.indexDoc.approvedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/); }); it('returns ForwardJobData for each active sync route', async () => { - const prisma = { - message: { - findUnique: jest.fn().mockResolvedValue({ - id: 'msg_1', - status: 'PENDING', - approval: null, - content: 'important announcement', - senderName: 'Bob', - sourceGroup: { - name: 'Source Group', - syncRoutesFrom: [ - { targetGroup: { platformId: '999@g.us', accountId: 'acc_2' } }, - { targetGroup: { platformId: '888@g.us', accountId: null } }, - ], - }, - }), + const prisma = makePrisma({ + content: 'important announcement', + senderName: 'Bob', + sourceGroup: { + name: 'Source Group', + syncRoutesFrom: [ + { targetGroup: { platformId: '999@g.us', accountId: 'acc_2' } }, + { targetGroup: { platformId: '888@g.us', accountId: null } }, + ], }, - 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).toHaveLength(2); - expect(result![0]).toMatchObject({ + expect(result!.forwardJobs).toHaveLength(2); + expect(result!.forwardJobs[0]).toMatchObject({ messageId: 'msg_1', content: 'important announcement', sourceGroupName: 'Source Group', @@ -158,8 +126,7 @@ describe('handleStarReaction', () => { toGroupJid: '999@g.us', fromAccountId: 'acc_2', }); - // falls back to reaction.accountId when targetGroup.accountId is null - expect(result![1]).toMatchObject({ + expect(result!.forwardJobs[1]).toMatchObject({ toGroupJid: '888@g.us', fromAccountId: 'acc_1', }); diff --git a/apps/worker/src/core/approval.ts b/apps/worker/src/core/approval.ts index 6502433..5f9c059 100644 --- a/apps/worker/src/core/approval.ts +++ b/apps/worker/src/core/approval.ts @@ -1,10 +1,15 @@ -import { NormalizedReaction, ForwardJobData } from '@tower/types'; +import { NormalizedReaction, ForwardJobData, IndexJobData } from '@tower/types'; + +export interface ApprovalResult { + forwardJobs: ForwardJobData[]; + indexDoc: IndexJobData; +} export async function handleStarReaction( reaction: NormalizedReaction, adminJids: string[], prisma: any, -): Promise { +): Promise { if (reaction.emoji !== '⭐') return null; if (!adminJids.includes(reaction.reactorJid)) return null; @@ -36,7 +41,7 @@ export async function handleStarReaction( where: { id: message.id, status: 'PENDING' }, data: { status: 'APPROVED' }, }); - if (updated.count === 0) return; // another admin approved first — idempotent + if (updated.count === 0) return; approved = true; await tx.approval.create({ data: { @@ -49,7 +54,7 @@ export async function handleStarReaction( if (!approved) return null; - const jobs: ForwardJobData[] = message.sourceGroup.syncRoutesFrom + const forwardJobs: ForwardJobData[] = message.sourceGroup.syncRoutesFrom .filter((route: any) => route.targetGroup != null) .map((route: any) => ({ messageId: message.id, @@ -57,9 +62,19 @@ export async function handleStarReaction( 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; + const indexDoc: IndexJobData = { + messageId: message.id, + content: message.content, + senderName: message.senderName ?? null, + sourceGroupId: message.sourceGroupId, + sourceGroupName: message.sourceGroup.name, + tags: message.tags, + platform: message.platform, + approvedAt: new Date().toISOString(), + }; + + return { forwardJobs, indexDoc }; }