From 2cd048e6f09a69a395d87dc3e667b7c4a3a11303 Mon Sep 17 00:00:00 2001 From: abhishek Date: Thu, 17 Sep 2026 11:03:57 +0530 Subject: [PATCH] partner --- src/features/nearle-admin/partnerList.test.ts | 73 +++++++++++++++++++ src/features/nearle-admin/partnerList.ts | 51 +++++++++++++ src/queries/hooks.ts | 11 ++- 3 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 src/features/nearle-admin/partnerList.test.ts create mode 100644 src/features/nearle-admin/partnerList.ts diff --git a/src/features/nearle-admin/partnerList.test.ts b/src/features/nearle-admin/partnerList.test.ts new file mode 100644 index 0000000..73f84d0 --- /dev/null +++ b/src/features/nearle-admin/partnerList.test.ts @@ -0,0 +1,73 @@ +/** + * Joining up the per-region partner lists. + * + * `getpartners` answers one region at a time, and since it began scoping on + * `partnerlocations` a partner who covers two cities is returned by both of + * those answers. Concatenating them put Xpress-Cbe-Main on the Rider partners + * page twice — two identical rows, each with its own Edit and Riders buttons + * and no way to tell which was which. + * + * The overlap is the read being right, so these are about the caller joining + * the answers up correctly rather than about the backend sending less. + */ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import type { Partner } from '@/api/deliveries'; +import { dedupePartners } from './partnerList'; + +const partner = (partnerid: number, over: Partial = {}): Partner => + ({ partnerid, partnername: `Partner ${partnerid}`, ...over }) as Partner; + +test('a partner working two regions is listed once', () => { + // The measured case: partner 44 has link rows for regions 1 and 2. + const rows = dedupePartners([ + [partner(44, { partnername: 'Xpress-Cbe-Main' })], + [partner(44, { partnername: 'Xpress-Cbe-Main' })], + [], + ]); + assert.equal(rows.length, 1); + assert.equal(rows[0]?.partnerid, 44); +}); + +test('partners in different regions are all kept', () => { + const rows = dedupePartners([[partner(44)], [partner(60)], [partner(61)]]); + assert.deepEqual( + rows.map((r) => r.partnerid), + [44, 60, 61], + ); +}); + +test('order is stable, so the list does not reshuffle as regions arrive', () => { + // The three requests resolve at different speeds and re-render as they land. + // A list that reorders under the cursor loses whoever was about to be clicked. + const rows = dedupePartners([[partner(7), partner(3)], [partner(9)], [partner(3)]]); + assert.deepEqual( + rows.map((r) => r.partnerid), + [7, 3, 9], + ); +}); + +test('a region that has not answered yet is not an empty list', () => { + // `useQueries` gives `undefined` until a query resolves, and one region still + // loading must not blank the partners the others already returned. + const rows = dedupePartners([[partner(44)], undefined, []]); + assert.equal(rows.length, 1); +}); + +test('nothing anywhere is an empty list, not a crash', () => { + assert.deepEqual(dedupePartners([]), []); + assert.deepEqual(dedupePartners([undefined, undefined]), []); +}); + +test('a partner with no id is dropped rather than collapsed under one key', () => { + // Two of them would otherwise become a single row keyed 0 that is neither, + // and no row on this page can be opened, counted or edited without an id. + const rows = dedupePartners([ + [partner(44), { partnername: 'No id at all' } as Partner], + [{ partnerid: 0, partnername: 'Zero id' } as Partner], + ]); + assert.deepEqual( + rows.map((r) => r.partnerid), + [44], + ); +}); diff --git a/src/features/nearle-admin/partnerList.ts b/src/features/nearle-admin/partnerList.ts new file mode 100644 index 0000000..48563c9 --- /dev/null +++ b/src/features/nearle-admin/partnerList.ts @@ -0,0 +1,51 @@ +import type { Partner } from '@/api/deliveries'; + +/** + * One row per partner, from answers that are fetched one region at a time. + * + * ── Why the answers overlap ───────────────────────────────────────────────── + * + * `getpartners` takes a single region and refuses 0, so the whole list is the + * regions fanned out and concatenated. That was safe while the backend filtered + * on `partnerinfo.applocationid` — a partner's HOME region, one value, so a + * partner was in exactly one answer. + * + * It now filters on `partnerlocations`, the table recording every region a + * partner covers. That is the fix for two things at once: a partner working two + * cities was invisible to a query for the second, and a row in that table is + * what separates partners registered through this console from the ones another + * product left in the shared `partnerinfo`. + * + * The cost is that the answers legitimately overlap. Xpress-Cbe-Main covers + * regions 1 and 2, so it comes back from both and was listed on the Rider + * partners page twice, identically, with no way to tell which was which. + * + * ── Deduped here rather than in the query ─────────────────────────────────── + * + * Asking "who works in Madurai" and "who works in Coimbatore" SHOULD both name + * a partner who works in both; the overlap is the read being correct, not a + * fault to push back to the backend. So the joining-up belongs to the caller + * that does the fanning out. + * + * First answer wins. Every copy is the same `partnerinfo` record — the query + * selects no per-region columns — so there is nothing to merge and no reason to + * prefer the row that happened to arrive first. + * + * Order is preserved so the list does not reshuffle between renders as regions + * resolve at different speeds. + */ +export function dedupePartners(pages: readonly (readonly Partner[] | undefined)[]): Partner[] { + const byPartner = new Map(); + + for (const page of pages) { + for (const partner of page ?? []) { + // A partner with no id cannot be addressed, counted or opened, and two of + // them would collapse into one row that is neither. Dropped rather than + // kept under a key of 0. + if (!partner?.partnerid) continue; + if (!byPartner.has(partner.partnerid)) byPartner.set(partner.partnerid, partner); + } + } + + return [...byPartner.values()]; +} diff --git a/src/queries/hooks.ts b/src/queries/hooks.ts index 71c3fa6..059820f 100644 --- a/src/queries/hooks.ts +++ b/src/queries/hooks.ts @@ -21,6 +21,7 @@ import { customersApi, type CustomerQuery } from '@/api/customers'; import { telemetryApi, type RiderSnapshot } from '@/api/telemetry'; import { uploadsApi } from '@/api/uploads'; import { APP_BROWSE_CATEGORY } from '@/features/catalogue/tenantCategories'; +import { dedupePartners } from '@/features/nearle-admin/partnerList'; import { queryKeys } from './keys'; import { LIVE_REFETCH_MS } from './interval'; @@ -144,8 +145,12 @@ export function useImportedRefs(tenantid: number | undefined) { * Every delivery partner across every region. * * `getpartners` takes one region at a time and refuses 0, so the whole list is - * the regions fanned out and flattened. Three regions today — Coimbatore, + * the regions fanned out and joined up. Three regions today — Coimbatore, * Madurai, Nagercoil — so this is three requests, not a page of them. + * + * Joined up, not concatenated: the backend scopes on `partnerlocations`, so a + * partner covering two cities is returned by both of those requests and is + * still one partner. See `dedupePartners`. */ export function useAllPartners() { const regions = useAppRegions(); @@ -159,8 +164,10 @@ export function useAllPartners() { })), }); + // Deduped, because the per-region answers legitimately overlap now that the + // backend scopes on `partnerlocations` — see `dedupePartners`. return { - data: results.flatMap((result) => result.data ?? []), + data: dedupePartners(results.map((result) => result.data)), isLoading: regions.isLoading || results.some((result) => result.isLoading), }; }