store-member-counts.test.mjs 3.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
  1. import test from 'node:test';
  2. import assert from 'node:assert/strict';
  3. import { readFileSync } from 'node:fs';
  4. import { runInNewContext } from 'node:vm';
  5. const source = readFileSync(new URL('../deploy-admin-functions.mjs', import.meta.url), 'utf8');
  6. const gateway = source.slice(source.indexOf('async function queryUsers(context,input)'));
  7. function functionSource(name) {
  8. const start = gateway.search(new RegExp(`^(?:async )?function ${name}\\(`, 'm'));
  9. assert.ok(start >= 0, `Missing ${name}`);
  10. const rest = gateway.slice(start);
  11. const end = rest.slice(1).search(/^(?:async )?function /m);
  12. return end < 0 ? rest : rest.slice(0, end + 1);
  13. }
  14. for (const live of [false, true]) {
  15. test(`门店在学正常人数复用会员筛选,按归属汇总且不受门店分页影响(旧门店数据:${live})`, async () => {
  16. const member = (userId, parentUserId, learningStatus, status) => ({ userId, parentUserId, learningStatus, status });
  17. const members = [
  18. member(101, 1, 'studying', 'active'), member(102, 1, 'studying', 'active'),
  19. member(103, 1, 'studying', 'disabled'), member(104, 1, 'completed', 'active'),
  20. member(105, 1, 'trial', 'active'), member(106, 1, 'not_started', 'active'),
  21. member(201, 2, 'studying', 'active'), member(202, 2, 'completed', 'active'),
  22. member(401, 4, 'studying', 'active'), member(901, 0, 'studying', 'active'),
  23. ];
  24. const stores = [1, 2, 3].map((id) => ({ objectId: `store-${id}`, legacyUserId: id, username: `门店${id}` }));
  25. const context = {
  26. number: (value, fallback = 0) => Number(value) || fallback,
  27. text: (value) => String(value ?? ''),
  28. companyIdOf: () => 'company', roleOf: () => 'ops-manager',
  29. legacyOf: (row) => row.legacyUserData || {}, mobileOf: () => '', maskMobile: () => '',
  30. registeredAtOf: () => '', firstDateIso: () => '', dateIso: () => '', businessDate: () => '2026-09-25',
  31. Psql: { query: async (sql) => sql.startsWith('SELECT u.*') ? stores : [] },
  32. coachRows: async () => [], appointmentSnapshot: async () => ({ appointments: [], live: {} }),
  33. optionalLegacyMemberSnapshot: async () => ({ available: live, coachIds: [], items: live ? [{ userId: 4, groupId: 2, username: '旧门店4' }] : [] }),
  34. queryUsers: async () => members, operationalSyncIndicator: async () => ({}),
  35. searchRows: (rows) => rows,
  36. };
  37. runInNewContext(['pageInput', 'pageRows', 'filterMembers', 'storeNameOf', 'storeMobileOf', 'storeRows'].map(functionSource).join('\n') + '\nthis.listStores = storeRows;', context);
  38. const result = await context.listStores({}, { page: 1, pageSize: 20 });
  39. const byId = new Map(result.items.map((row) => [row.userId, row]));
  40. assert.equal(byId.get(1).memberCount, 6);
  41. assert.equal(byId.get(1).activeStudyingMemberCount, 2);
  42. assert.equal(byId.get(2).activeStudyingMemberCount, 1);
  43. assert.equal(byId.get(3).activeStudyingMemberCount, 0);
  44. if (live) assert.equal(byId.get(4).activeStudyingMemberCount, 1);
  45. for (const row of result.items) {
  46. const expected = context.filterMembers(members, { filters: { parentUserId: row.userId, status: 'active', learningStatus: 'studying' } }).length;
  47. assert.equal(row.activeStudyingMemberCount, expected);
  48. }
  49. const paged = await context.listStores({}, { page: 2, pageSize: 1 });
  50. assert.equal(paged.items.length, 1);
  51. assert.equal(paged.items[0].activeStudyingMemberCount, byId.get(paged.items[0].userId).activeStudyingMemberCount);
  52. });
  53. }