partner
This commit is contained in:
73
src/features/nearle-admin/partnerList.test.ts
Normal file
73
src/features/nearle-admin/partnerList.test.ts
Normal file
@@ -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> = {}): 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],
|
||||||
|
);
|
||||||
|
});
|
||||||
51
src/features/nearle-admin/partnerList.ts
Normal file
51
src/features/nearle-admin/partnerList.ts
Normal file
@@ -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<number, Partner>();
|
||||||
|
|
||||||
|
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()];
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ import { customersApi, type CustomerQuery } from '@/api/customers';
|
|||||||
import { telemetryApi, type RiderSnapshot } from '@/api/telemetry';
|
import { telemetryApi, type RiderSnapshot } from '@/api/telemetry';
|
||||||
import { uploadsApi } from '@/api/uploads';
|
import { uploadsApi } from '@/api/uploads';
|
||||||
import { APP_BROWSE_CATEGORY } from '@/features/catalogue/tenantCategories';
|
import { APP_BROWSE_CATEGORY } from '@/features/catalogue/tenantCategories';
|
||||||
|
import { dedupePartners } from '@/features/nearle-admin/partnerList';
|
||||||
import { queryKeys } from './keys';
|
import { queryKeys } from './keys';
|
||||||
import { LIVE_REFETCH_MS } from './interval';
|
import { LIVE_REFETCH_MS } from './interval';
|
||||||
|
|
||||||
@@ -144,8 +145,12 @@ export function useImportedRefs(tenantid: number | undefined) {
|
|||||||
* Every delivery partner across every region.
|
* Every delivery partner across every region.
|
||||||
*
|
*
|
||||||
* `getpartners` takes one region at a time and refuses 0, so the whole list is
|
* `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.
|
* 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() {
|
export function useAllPartners() {
|
||||||
const regions = useAppRegions();
|
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 {
|
return {
|
||||||
data: results.flatMap((result) => result.data ?? []),
|
data: dedupePartners(results.map((result) => result.data)),
|
||||||
isLoading: regions.isLoading || results.some((result) => result.isLoading),
|
isLoading: regions.isLoading || results.some((result) => result.isLoading),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user