# Instructions

- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.

# Test info

- Name: counters.spec.js >> Counters — E2E supervisor flow >> Phase 4.6 — official trigger fans out + dispatch_notify → mail_outbox + supervisor RPCs
- Location: tests/counters.spec.js:761:3

# Error details

```
Error: expect(received).toBe(expected) // Object.is equality

Expected: true
Received: undefined
```

# Test source

```ts
  694 |       headers: { Authorization: hubAuth, 'Content-Type': 'application/json' },
  695 |       data: JSON.stringify({ sub_id: 'RC_1779435019915_0SZC', ttl_s: 300 }),
  696 |     });
  697 |     expect(signed.status()).toBe(200);
  698 |     const body = await signed.json();
  699 |     expect(body.ok).toBe(true);
  700 |     expect(body.urls.html).toContain('?t=');
  701 | 
  702 |     // 3. Hit the signed URL — file should stream
  703 |     const file = await ctx.get('https://dev.srv1111289.hstgr.cloud' + body.urls.html);
  704 |     expect(file.status()).toBe(200);
  705 |     expect((await file.text()).toLowerCase()).toContain('<html');
  706 | 
  707 |     // 4. Tampered token → 401
  708 |     const bad = body.urls.html.slice(0, -3) + 'AAA';
  709 |     const tampered = await ctx.get('https://dev.srv1111289.hstgr.cloud' + bad);
  710 |     expect(tampered.status()).toBe(401);
  711 | 
  712 |     // 5. Token for sub_id A cannot read sub_id B
  713 |     const otherFile = await ctx.get(
  714 |       `https://dev.srv1111289.hstgr.cloud/recce-view-proxy/RC_OTHER_AAAA.html?t=${body.token}`
  715 |     );
  716 |     expect(otherFile.status()).toBe(403);
  717 |     await ctx.dispose();
  718 |   });
  719 | 
  720 |   test('dedup panel surfaces for similar-name same-pincode entry', async ({ page }) => {
  721 |     // Seed one counter via API
  722 |     const ctx = await request.newContext();
  723 |     const stamp = Date.now().toString(36).slice(-6);
  724 |     const seedId = `cntr-pwseed-${stamp}`;
  725 |     const seedName = `Seed Counter ${stamp}`;
  726 |     await ctx.post(`${DB}/counter`, {
  727 |       headers: {
  728 |         'Accept-Profile':  'counters',
  729 |         'Content-Profile': 'counters',
  730 |         'Content-Type':    'application/json',
  731 |         Prefer:            'return=minimal',
  732 |       },
  733 |       data: JSON.stringify({
  734 |         counter_id:    seedId,
  735 |         display_name:  seedName,
  736 |         address_line1: '1 Seed St',
  737 |         city:          'Mohali',
  738 |         pincode:       '160055',
  739 |         created_by:    'playwright-seed',
  740 |       }),
  741 |     });
  742 | 
  743 |     await injectSession(page);
  744 |     await page.goto(BASE);
  745 |     await page.waitForTimeout(600);
  746 | 
  747 |     await page.click('#btn-new');
  748 |     await page.waitForTimeout(150);
  749 |     // Same pincode + near-identical name → dedup should fire
  750 |     await page.fill('#f-display_name', seedName + ' X');
  751 |     await page.fill('#f-pincode',      '160055');
  752 |     await page.waitForTimeout(500);   // dedup is debounced 250ms
  753 |     await expect(page.locator('#dedup')).toBeVisible();
  754 |     await expect(page.locator('#dedup-list')).toContainText(seedName);
  755 | 
  756 |     // Cleanup
  757 |     await dbDelete(ctx, `/counter?counter_id=eq.${seedId}`);
  758 |     await ctx.dispose();
  759 |   });
  760 | 
  761 |   test('Phase 4.6 — official trigger fans out + dispatch_notify → mail_outbox + supervisor RPCs', async () => {
  762 |     const ctx = await request.newContext();
  763 |     const HC = { 'Accept-Profile': 'counters', 'Content-Profile': 'counters', 'Content-Type': 'application/json' };
  764 |     const HC_PMIN = { ...HC, Prefer: 'return=minimal' };
  765 |     const HR = { 'Accept-Profile': 'recce',    'Content-Profile': 'recce',    'Content-Type': 'application/json' };
  766 |     const HR_PMIN = { ...HR, Prefer: 'return=minimal' };
  767 |     const stamp     = Date.now().toString(36).slice(-6);
  768 |     const brand     = `P46_${stamp.toUpperCase()}`;
  769 |     const clientId  = `cli-p46-${stamp}`;
  770 |     const cuEmail   = `p46-${stamp}@spec.test`;
  771 |     const counterId = `cntr-p46-${stamp}`;
  772 |     const subId     = `sub-p46-${stamp}`;
  773 | 
  774 |     // Seed counter (FK target)
  775 |     await ctx.post(`${DB}/counter`, { headers: HC_PMIN, data: JSON.stringify({
  776 |       counter_id: counterId, display_name: `P46 ${stamp}`, address_line1: '1 P46',
  777 |       pincode: '160001', created_by: 'pw-p46',
  778 |     })});
  779 | 
  780 |     // Seed client tagged with our test brand
  781 |     await ctx.post(`${DB}/client`, { headers: HC_PMIN, data: JSON.stringify({
  782 |       client_id: clientId, name: `P46 Brand ${stamp}`, brand_codes: [brand],
  783 |       created_by: 'pw-p46',
  784 |     })});
  785 | 
  786 |     // Invite → creates active client_user
  787 |     const inv = await (await ctx.post(`${DB}/rpc/invite_client_user`, { headers: HC,
  788 |       data: JSON.stringify({
  789 |         p_email: cuEmail, p_client_id: clientId, p_role: 'SM',
  790 |         p_scope_kind: 'all', p_scope_values: [], p_display_name: 'P46',
  791 |         p_invited_by: 'harish',
  792 |       }),
  793 |     })).json();
> 794 |     expect(inv.ok).toBe(true);
      |                    ^ Error: expect(received).toBe(expected) // Object.is equality
  795 |     const cuId     = inv.client_user_id;
  796 |     const inviteId = inv.invite_id;
  797 | 
  798 |     // Trigger: INSERT recce submission with is_official_for_client=true
  799 |     // Fans out one notify_queue row per matching active client_user, plus
  800 |     // one 'supervisor' row already marked 'dispatched'.
  801 |     await ctx.post(`${DB}/submissions`, { headers: HR_PMIN, data: JSON.stringify({
  802 |       sub_id: subId, store_name: `P46 Store`, brand, visit_date: '2026-06-06',
  803 |       counter_id: counterId, submitted_by: 'harish',
  804 |       is_official_for_client: true, official_picked_by: 'harish',
  805 |     })});
  806 | 
  807 |     const rows = await (await ctx.get(
  808 |       `${DB}/notify_queue?payload->>sub_id=eq.${subId}&order=notify_id.asc`,
  809 |       { headers: { 'Accept-Profile': 'counters' }},
  810 |     )).json();
  811 |     const cuRow  = rows.find(r => r.recipient_type === 'client_user');
  812 |     const supRow = rows.find(r => r.recipient_type === 'supervisor');
  813 |     expect(cuRow).toBeTruthy();
  814 |     expect(cuRow.status).toBe('pending');
  815 |     expect(cuRow.recipient_email).toBe(cuEmail);
  816 |     expect(cuRow.event_type).toBe('recce_marked_official');
  817 |     expect(supRow).toBeTruthy();
  818 |     expect(supRow.status).toBe('dispatched');
  819 |     expect(supRow.recipient_email).toBeNull();
  820 | 
  821 |     // dispatch_notify on the pending client_user row → mail_outbox row + status flip
  822 |     const disp = await (await ctx.post(`${DB}/rpc/dispatch_notify`, { headers: HC,
  823 |       data: JSON.stringify({
  824 |         p_notify_id: cuRow.notify_id, p_subject: 'P46 spec subject',
  825 |         p_body_text: 'spec body',     p_body_html: '<p>spec body</p>',
  826 |       }),
  827 |     })).json();
  828 |     expect(disp.ok).toBe(true);
  829 |     expect(disp.message_id).toBeTruthy();
  830 | 
  831 |     const after = await (await ctx.get(`${DB}/notify_queue?notify_id=eq.${cuRow.notify_id}`,
  832 |       { headers: { 'Accept-Profile': 'counters' }})).json();
  833 |     expect(after[0].status).toBe('dispatched');
  834 | 
  835 |     const mailRow = await (await ctx.get(
  836 |       `${DB}/mail_outbox?reference=eq.notify-${cuRow.notify_id}`,
  837 |       { headers: { 'Accept-Profile': 'counters' }})).json();
  838 |     expect(mailRow).toHaveLength(1);
  839 |     expect(mailRow[0].to_email).toBe(cuEmail);
  840 |     expect(mailRow[0].subject).toBe('P46 spec subject');
  841 | 
  842 |     // Re-dispatching the same row → 'not_pending'
  843 |     const re = await (await ctx.post(`${DB}/rpc/dispatch_notify`, { headers: HC,
  844 |       data: JSON.stringify({
  845 |         p_notify_id: cuRow.notify_id, p_subject: 'x', p_body_text: 'x', p_body_html: 'x',
  846 |       }),
  847 |     })).json();
  848 |     expect(re).toEqual({ ok: false, error: 'not_pending' });
  849 | 
  850 |     // admin_list_notifications — supervisor sees the supervisor row
  851 |     const list = await (await ctx.post(`${DB}/rpc/admin_list_notifications`, { headers: HC,
  852 |       data: JSON.stringify({ p_supervisor_id: 'harish', p_limit: 100 }),
  853 |     })).json();
  854 |     expect(list.find(n => Number(n.notify_id) === Number(supRow.notify_id))).toBeTruthy();
  855 | 
  856 |     // Non-supervisor → empty (NOT IN supervisor allow-list)
  857 |     const denied = await (await ctx.post(`${DB}/rpc/admin_list_notifications`, { headers: HC,
  858 |       data: JSON.stringify({ p_supervisor_id: 'mukesh', p_limit: 100 }),
  859 |     })).json();
  860 |     expect(Array.isArray(denied)).toBe(true);
  861 |     expect(denied).toHaveLength(0);
  862 | 
  863 |     // admin_mark_notification_seen on supervisor row → status flips to 'suppressed'
  864 |     const mark = await (await ctx.post(`${DB}/rpc/admin_mark_notification_seen`, { headers: HC,
  865 |       data: JSON.stringify({ p_supervisor_id: 'harish', p_notify_id: supRow.notify_id }),
  866 |     })).json();
  867 |     expect(mark.ok).toBe(true);
  868 |     const supAfter = await (await ctx.get(`${DB}/notify_queue?notify_id=eq.${supRow.notify_id}`,
  869 |       { headers: { 'Accept-Profile': 'counters' }})).json();
  870 |     expect(supAfter[0].status).toBe('suppressed');
  871 | 
  872 |     // Non-supervisor mark_seen → forbidden
  873 |     const fz = await (await ctx.post(`${DB}/rpc/admin_mark_notification_seen`, { headers: HC,
  874 |       data: JSON.stringify({ p_supervisor_id: 'mukesh', p_notify_id: supRow.notify_id }),
  875 |     })).json();
  876 |     expect(fz).toEqual({ ok: false, error: 'forbidden' });
  877 | 
  878 |     // client_response trigger: client_status change creates a supervisor-row
  879 |     await ctx.patch(`${DB}/submissions?sub_id=eq.${subId}`, { headers: HR_PMIN,
  880 |       data: JSON.stringify({ client_status: 'approved', client_approved_by: 'p46-client' }),
  881 |     });
  882 |     const respRows = await (await ctx.get(
  883 |       `${DB}/notify_queue?payload->>sub_id=eq.${subId}&event_type=eq.client_response`,
  884 |       { headers: { 'Accept-Profile': 'counters' }})).json();
  885 |     expect(respRows).toHaveLength(1);
  886 |     expect(respRows[0].status).toBe('dispatched');
  887 |     expect(respRows[0].recipient_type).toBe('supervisor');
  888 | 
  889 |     // Cleanup
  890 |     const HCd = { headers: { ...HC, Prefer: 'return=minimal' } };
  891 |     const HRd = { headers: { ...HR, Prefer: 'return=minimal' } };
  892 |     await ctx.delete(`${DB}/submissions?sub_id=eq.${subId}`, HRd);
  893 |     await ctx.delete(`${DB}/notify_queue?payload->>sub_id=eq.${subId}`, HCd);
  894 |     await ctx.delete(`${DB}/notify_queue?recipient_email=eq.${cuEmail}`, HCd);
```