@adobe/spacecat-shared-tier-client 1.3.11 → 1.3.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.releaserc.cjs CHANGED
@@ -1,8 +1,12 @@
1
1
  module.exports = {
2
2
  extends: "semantic-release-monorepo",
3
3
  plugins: [
4
- "@semantic-release/commit-analyzer",
5
- "@semantic-release/release-notes-generator",
4
+ ["@semantic-release/commit-analyzer", {
5
+ "preset": "conventionalcommits",
6
+ }],
7
+ ["@semantic-release/release-notes-generator", {
8
+ "preset": "conventionalcommits",
9
+ }],
6
10
  ["@semantic-release/changelog", {
7
11
  "changelogFile": "CHANGELOG.md",
8
12
  }],
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## [@adobe/spacecat-shared-tier-client-v1.3.13](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tier-client-v1.3.12...@adobe/spacecat-shared-tier-client-v1.3.13) (2026-03-01)
2
+
3
+ ### Bug Fixes
4
+
5
+ * **tier-client:** avoid 414 URI Too Large on orgs with many enrollments ([#1390](https://github.com/adobe/spacecat-shared/issues/1390)) ([2e9de5e](https://github.com/adobe/spacecat-shared/commit/2e9de5e685c2a25801d4d50ad7317773ee706ef0))
6
+
7
+ ## [@adobe/spacecat-shared-tier-client-v1.3.12](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tier-client-v1.3.11...@adobe/spacecat-shared-tier-client-v1.3.12) (2026-02-17)
8
+
9
+ ### Bug Fixes
10
+
11
+ * **data-access:** decouple shared packages for v2/v3 alias wrapper rollout ([#1355](https://github.com/adobe/spacecat-shared/issues/1355)) ([ba48df7](https://github.com/adobe/spacecat-shared/commit/ba48df710e0030c1cb3ef4f90661cff1b548d42f))
12
+
1
13
  # [@adobe/spacecat-shared-tier-client-v1.3.11](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tier-client-v1.3.10...@adobe/spacecat-shared-tier-client-v1.3.11) (2026-02-06)
2
14
 
3
15
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-tier-client",
3
- "version": "1.3.11",
3
+ "version": "1.3.13",
4
4
  "description": "Shared modules of the Spacecat Services - Tier Client",
5
5
  "type": "module",
6
6
  "engines": {
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@adobe/spacecat-shared-utils": "1.81.1",
39
- "@adobe/spacecat-shared-data-access": "2.88.7"
39
+ "@mysticat/data-service-types": "git+https://github.com/adobe/mysticat-data-service.git#types-ts-v1.11.1"
40
40
  },
41
41
  "devDependencies": {
42
42
  "c8": "10.1.3",
@@ -11,9 +11,9 @@
11
11
  */
12
12
 
13
13
  import { isNonEmptyObject, hasText } from '@adobe/spacecat-shared-utils';
14
- import {
15
- Entitlement as EntitlementModel,
16
- } from '@adobe/spacecat-shared-data-access';
14
+ import { MYSTICAT_ENUMS_BY_TYPE } from '@mysticat/data-service-types';
15
+
16
+ const ENTITLEMENT_TIERS = MYSTICAT_ENUMS_BY_TYPE.ENTITLEMENT_TIER;
17
17
  /**
18
18
  * TierClient provides methods to manage entitlements and site enrollments.
19
19
  */
@@ -137,8 +137,8 @@ class TierClient {
137
137
  */
138
138
  async createEntitlement(tier) {
139
139
  try {
140
- if (!Object.values(EntitlementModel.TIERS).includes(tier)) {
141
- throw new Error(`Invalid tier: ${tier}. Valid tiers: ${Object.values(EntitlementModel.TIERS).join(', ')}`);
140
+ if (!Object.values(ENTITLEMENT_TIERS).includes(tier)) {
141
+ throw new Error(`Invalid tier: ${tier}. Valid tiers: ${Object.values(ENTITLEMENT_TIERS).join(', ')}`);
142
142
  }
143
143
  const orgId = this.organization.getId();
144
144
  // Check what already exists
@@ -149,7 +149,7 @@ class TierClient {
149
149
  const currentTier = existing.entitlement.getTier();
150
150
 
151
151
  // If currentTier doesn't match with given tier and is not PAID, update it
152
- if (currentTier !== tier && currentTier !== EntitlementModel.TIERS.PAID) {
152
+ if (currentTier !== tier && currentTier !== ENTITLEMENT_TIERS.PAID) {
153
153
  existing.entitlement.setTier(tier);
154
154
  await existing.entitlement.save();
155
155
  }
@@ -239,10 +239,40 @@ class TierClient {
239
239
  return { entitlement, enrollments: [] };
240
240
  }
241
241
 
242
- // Fetch all sites using batchGetByKeys
242
+ // When a specific site is provided, skip batch fetch entirely.
243
+ // Just filter enrollments by site ID and verify org ownership with a single lookup.
244
+ if (this.site) {
245
+ const targetSiteId = this.site.getId();
246
+ const matchingEnrollments = allEnrollments.filter(
247
+ (se) => se.getSiteId() === targetSiteId,
248
+ );
249
+
250
+ if (matchingEnrollments.length === 0) {
251
+ return { entitlement, enrollments: [] };
252
+ }
253
+
254
+ const site = await this.Site.findById(targetSiteId);
255
+ if (!site || site.getOrganizationId() !== orgId) {
256
+ return { entitlement, enrollments: [] };
257
+ }
258
+
259
+ return { entitlement, enrollments: matchingEnrollments };
260
+ }
261
+
262
+ // Org-only path: fetch sites in chunks to avoid 414 URI Too Large.
263
+ // PostgREST uses GET with ?id=in.(...) which has URL length limits.
264
+ const CHUNK_SIZE = 50;
243
265
  const siteKeys = allEnrollments.map((enrollment) => ({ siteId: enrollment.getSiteId() }));
244
- const sitesResult = await this.Site.batchGetByKeys(siteKeys);
245
- const sitesMap = new Map(sitesResult.data.map((site) => [site.getId(), site]));
266
+ const sitesMap = new Map();
267
+
268
+ for (let i = 0; i < siteKeys.length; i += CHUNK_SIZE) {
269
+ const chunk = siteKeys.slice(i, i + CHUNK_SIZE);
270
+ // eslint-disable-next-line no-await-in-loop
271
+ const sitesResult = await this.Site.batchGetByKeys(chunk);
272
+ for (const site of sitesResult.data) {
273
+ sitesMap.set(site.getId(), site);
274
+ }
275
+ }
246
276
 
247
277
  // Filter enrollments where site's orgId matches the entitlement's orgId
248
278
  const validEnrollments = [];
@@ -250,27 +280,13 @@ class TierClient {
250
280
  for (const enrollment of allEnrollments) {
251
281
  const site = sitesMap.get(enrollment.getSiteId());
252
282
  if (!site) {
253
- // Site not found, log warning and skip
254
283
  this.log.warn(`Site not found for enrollment ${enrollment.getId()} with siteId ${enrollment.getSiteId()}`);
255
- } else {
256
- const siteOrgId = site.getOrganizationId();
257
- if (siteOrgId === orgId) {
258
- validEnrollments.push(enrollment);
259
- }
284
+ } else if (site.getOrganizationId() === orgId) {
285
+ validEnrollments.push(enrollment);
260
286
  }
261
287
  }
262
288
 
263
- if (this.site) {
264
- // Return site enrollments matching the entitlement and site
265
- const siteId = this.site.getId();
266
- const matchingEnrollments = validEnrollments.filter(
267
- (se) => se.getSiteId() === siteId,
268
- );
269
- return { entitlement, enrollments: matchingEnrollments };
270
- } else {
271
- // Return all valid enrollments for the entitlement
272
- return { entitlement, enrollments: validEnrollments };
273
- }
289
+ return { entitlement, enrollments: validEnrollments };
274
290
  } catch (error) {
275
291
  this.log.error(`Error getting all enrollments: ${error.message}`);
276
292
  throw error;
@@ -279,28 +295,51 @@ class TierClient {
279
295
 
280
296
  /**
281
297
  * Gets the first enrollment and its site, filtered by productCode.
282
- * - If site is provided: returns site enrollment for the entitlement matching productCode
283
- * - If org-only: returns first site enrollment for the entitlement matching productCode
298
+ * - If site is provided: finds matching enrollment and returns this.site directly
299
+ * - If org-only: iterates enrollments, fetches sites one at a time, returns first org match
284
300
  * @returns {Promise<object>} Object with entitlement, enrollment, and site.
285
301
  */
286
302
  async getFirstEnrollment() {
287
303
  try {
288
- const { entitlement, enrollments } = await this.getAllEnrollment();
304
+ const orgId = this.organization.getId();
305
+ const entitlement = await this.Entitlement
306
+ .findByOrganizationIdAndProductCode(orgId, this.productCode);
289
307
 
290
- if (!entitlement || !enrollments?.length) {
308
+ if (!entitlement) {
291
309
  return { entitlement: null, enrollment: null, site: null };
292
310
  }
293
311
 
294
- const firstEnrollment = enrollments[0];
295
- const enrollmentSiteId = firstEnrollment.getSiteId();
296
- const site = await this.Site.findById(enrollmentSiteId);
312
+ const allEnrollments = await this.SiteEnrollment.allByEntitlementId(entitlement.getId());
297
313
 
298
- if (!site) {
299
- this.log.warn(`Site not found for enrollment ${firstEnrollment.getId()} with site ID ${enrollmentSiteId}`);
300
- return { entitlement, enrollment: firstEnrollment, site: null };
314
+ if (!allEnrollments || allEnrollments.length === 0) {
315
+ return { entitlement: null, enrollment: null, site: null };
316
+ }
317
+
318
+ // When a specific site is set, find its enrollment in memory — no fetch needed.
319
+ if (this.site) {
320
+ const targetSiteId = this.site.getId();
321
+ const matchingEnrollment = allEnrollments.find(
322
+ (se) => se.getSiteId() === targetSiteId,
323
+ );
324
+ if (matchingEnrollment) {
325
+ return { entitlement, enrollment: matchingEnrollment, site: this.site };
326
+ }
327
+ return { entitlement: null, enrollment: null, site: null };
328
+ }
329
+
330
+ // Org-only: iterate enrollments, fetch site one at a time, return first org match.
331
+ // This avoids batch-fetching all sites (which causes 414 on large sets).
332
+ for (const enrollment of allEnrollments) {
333
+ // eslint-disable-next-line no-await-in-loop
334
+ const site = await this.Site.findById(enrollment.getSiteId());
335
+ if (!site) {
336
+ this.log.warn(`Site not found for enrollment ${enrollment.getId()} with siteId ${enrollment.getSiteId()}`);
337
+ } else if (site.getOrganizationId() === orgId) {
338
+ return { entitlement, enrollment, site };
339
+ }
301
340
  }
302
341
 
303
- return { entitlement, enrollment: firstEnrollment, site };
342
+ return { entitlement: null, enrollment: null, site: null };
304
343
  } catch (error) {
305
344
  this.log.error(`Error getting first enrollment: ${error.message}`);
306
345
  throw error;
@@ -314,7 +353,7 @@ class TierClient {
314
353
  async revokeEntitlement() {
315
354
  const existing = await this.checkValidEntitlement();
316
355
  if (existing.entitlement) {
317
- if (existing.entitlement.getTier() === EntitlementModel.TIERS.PAID) {
356
+ if (existing.entitlement.getTier() === ENTITLEMENT_TIERS.PAID) {
318
357
  throw new Error('Paid entitlement cannot be revoked');
319
358
  }
320
359
  await existing.entitlement.remove();
@@ -17,7 +17,6 @@ import chaiAsPromised from 'chai-as-promised';
17
17
  import sinonChai from 'sinon-chai';
18
18
  import sinon from 'sinon';
19
19
 
20
- import { Organization, Site } from '@adobe/spacecat-shared-data-access';
21
20
  import TierClient from '../src/tier-client.js';
22
21
 
23
22
  use(chaiAsPromised);
@@ -49,11 +48,7 @@ describe('TierClient', () => {
49
48
  };
50
49
 
51
50
  // Create actual Organization instance for instanceof checks
52
- const organizationInstance = Object.create(Organization.prototype);
53
- Object.assign(
54
- organizationInstance,
55
- { entityName: Organization.ENTITY_NAME, ...mockOrganization },
56
- );
51
+ const organizationInstance = { ...mockOrganization };
57
52
 
58
53
  const mockSite = {
59
54
  getId: () => siteId,
@@ -62,8 +57,7 @@ describe('TierClient', () => {
62
57
  };
63
58
 
64
59
  // Create actual Site instance for instanceof checks
65
- const siteInstance = Object.create(Site.prototype);
66
- Object.assign(siteInstance, { entityName: Site.ENTITY_NAME, ...mockSite });
60
+ const siteInstance = { ...mockSite };
67
61
 
68
62
  const mockDataAccess = {
69
63
  Entitlement: {
@@ -121,23 +115,18 @@ describe('TierClient', () => {
121
115
  });
122
116
 
123
117
  describe('Static Factory Methods', () => {
124
- const testOrganization = Object.create(Organization.prototype);
125
- Object.assign(testOrganization, { entityName: Organization.ENTITY_NAME, getId: () => orgId });
118
+ const testOrganization = { getId: () => orgId };
126
119
 
127
- const testSite = Object.create(Site.prototype);
128
- Object.assign(testSite, {
129
- entityName: Site.ENTITY_NAME,
120
+ const testSite = {
130
121
  getId: () => siteId,
131
122
  getOrganizationId: () => orgId,
132
123
  getOrganization: () => testOrganization,
133
- });
124
+ };
134
125
 
135
- const testSiteWithOrgRef = Object.create(Site.prototype);
136
- Object.assign(testSiteWithOrgRef, {
137
- entityName: Site.ENTITY_NAME,
126
+ const testSiteWithOrgRef = {
138
127
  getId: () => siteId,
139
128
  getOrganizationId: () => orgId,
140
- });
129
+ };
141
130
 
142
131
  describe('createForOrg', () => {
143
132
  it('should create TierClient for organization', () => {
@@ -775,20 +764,12 @@ describe('TierClient', () => {
775
764
  getOrganizationId: () => orgId,
776
765
  };
777
766
 
778
- const mockSiteForEnrollment2 = {
779
- getId: () => 'other-site-id',
780
- getOrganizationId: () => orgId,
781
- };
782
-
783
767
  mockDataAccess.Entitlement.findByOrganizationIdAndProductCode.resolves(mockEntitlement);
784
768
  mockDataAccess.SiteEnrollment.allByEntitlementId.resolves([
785
769
  mockSiteEnrollment,
786
770
  mockEnrollment2,
787
771
  ]);
788
- mockDataAccess.Site.batchGetByKeys.resolves({
789
- data: [mockSiteForEnrollment1, mockSiteForEnrollment2],
790
- unprocessed: [],
791
- });
772
+ mockDataAccess.Site.findById.resolves(mockSiteForEnrollment1);
792
773
 
793
774
  const result = await tierClient.getAllEnrollment();
794
775
 
@@ -796,8 +777,8 @@ describe('TierClient', () => {
796
777
  entitlement: mockEntitlement,
797
778
  enrollments: [mockSiteEnrollment],
798
779
  });
799
- expect(mockDataAccess.SiteEnrollment.allByEntitlementId)
800
- .to.have.been.calledWith('entitlement-123');
780
+ expect(mockDataAccess.Site.batchGetByKeys).to.not.have.been.called;
781
+ expect(mockDataAccess.Site.findById).to.have.been.calledWith(siteId);
801
782
  });
802
783
 
803
784
  it('should return null entitlement and empty enrollments when no entitlement exists', async () => {
@@ -831,6 +812,38 @@ describe('TierClient', () => {
831
812
  expect(mockContext.log.error).to.have.been.calledWith('Error getting all enrollments: Database error');
832
813
  });
833
814
 
815
+ it('should return empty enrollments when site has no matching enrollments', async () => {
816
+ const nonMatchingEnrollment = {
817
+ getId: () => 'enrollment-999',
818
+ getSiteId: () => 'other-site-id',
819
+ getEntitlementId: () => 'entitlement-123',
820
+ };
821
+
822
+ mockDataAccess.Entitlement.findByOrganizationIdAndProductCode.resolves(mockEntitlement);
823
+ mockDataAccess.SiteEnrollment.allByEntitlementId.resolves([nonMatchingEnrollment]);
824
+
825
+ const result = await tierClient.getAllEnrollment();
826
+
827
+ expect(result).to.deep.equal({ entitlement: mockEntitlement, enrollments: [] });
828
+ expect(mockDataAccess.Site.findById).to.not.have.been.called;
829
+ });
830
+
831
+ it('should return empty enrollments when site org does not match', async () => {
832
+ const wrongOrgSite = {
833
+ getId: () => siteId,
834
+ getOrganizationId: () => 'wrong-org-id',
835
+ };
836
+
837
+ mockDataAccess.Entitlement.findByOrganizationIdAndProductCode.resolves(mockEntitlement);
838
+ mockDataAccess.SiteEnrollment.allByEntitlementId.resolves([mockSiteEnrollment]);
839
+ mockDataAccess.Site.findById.resolves(wrongOrgSite);
840
+
841
+ const result = await tierClient.getAllEnrollment();
842
+
843
+ expect(result).to.deep.equal({ entitlement: mockEntitlement, enrollments: [] });
844
+ expect(mockDataAccess.Site.findById).to.have.been.calledWith(siteId);
845
+ });
846
+
834
847
  it('should filter out enrollments not matching site ID', async () => {
835
848
  const mockEnrollment2 = {
836
849
  getId: () => 'enrollment-456',
@@ -849,26 +862,20 @@ describe('TierClient', () => {
849
862
  getOrganizationId: () => orgId,
850
863
  };
851
864
 
852
- const mockSiteForEnrollment2 = {
853
- getId: () => 'different-site-id',
854
- getOrganizationId: () => orgId,
855
- };
856
-
857
865
  mockDataAccess.Entitlement.findByOrganizationIdAndProductCode.resolves(mockEntitlement);
858
866
  mockDataAccess.SiteEnrollment.allByEntitlementId.resolves([
859
867
  mockSiteEnrollment,
860
868
  mockEnrollment2,
861
869
  mockEnrollment3,
862
870
  ]);
863
- mockDataAccess.Site.batchGetByKeys.resolves({
864
- data: [mockSiteForEnrollment1, mockSiteForEnrollment2, mockSiteForEnrollment1],
865
- unprocessed: [],
866
- });
871
+ mockDataAccess.Site.findById.resolves(mockSiteForEnrollment1);
867
872
 
868
873
  const result = await tierClient.getAllEnrollment();
869
874
 
870
875
  expect(result.enrollments).to.have.lengthOf(2);
871
876
  expect(result.enrollments).to.deep.equal([mockSiteEnrollment, mockEnrollment3]);
877
+ expect(mockDataAccess.Site.batchGetByKeys).to.not.have.been.called;
878
+ expect(mockDataAccess.Site.findById).to.have.been.calledWith(siteId);
872
879
  });
873
880
 
874
881
  it('should filter out enrollments with mismatching orgId', async () => {
@@ -950,10 +957,6 @@ describe('TierClient', () => {
950
957
 
951
958
  mockDataAccess.Entitlement.findByOrganizationIdAndProductCode.resolves(mockEntitlement);
952
959
  mockDataAccess.SiteEnrollment.allByEntitlementId.resolves([mockSiteEnrollment]);
953
- mockDataAccess.Site.batchGetByKeys.resolves({
954
- data: [mockSiteObject],
955
- unprocessed: [],
956
- });
957
960
  mockDataAccess.Site.findById.resolves(mockSiteObject);
958
961
 
959
962
  const tierClientWithoutSite = new TierClient(
@@ -971,6 +974,7 @@ describe('TierClient', () => {
971
974
  site: mockSiteObject,
972
975
  });
973
976
  expect(mockDataAccess.Site.findById).to.have.been.calledWith(siteId);
977
+ expect(mockDataAccess.Site.batchGetByKeys).to.not.have.been.called;
974
978
  });
975
979
 
976
980
  it('should return first enrollment when multiple exist', async () => {
@@ -980,12 +984,6 @@ describe('TierClient', () => {
980
984
  getOrganizationId: () => orgId,
981
985
  };
982
986
 
983
- const mockSiteObject2 = {
984
- getId: () => 'other-site-id',
985
- getName: () => 'Other Site',
986
- getOrganizationId: () => orgId,
987
- };
988
-
989
987
  const mockEnrollment2 = {
990
988
  getId: () => 'enrollment-456',
991
989
  getSiteId: () => 'other-site-id',
@@ -997,10 +995,6 @@ describe('TierClient', () => {
997
995
  mockSiteEnrollment,
998
996
  mockEnrollment2,
999
997
  ]);
1000
- mockDataAccess.Site.batchGetByKeys.resolves({
1001
- data: [mockSiteObject, mockSiteObject2],
1002
- unprocessed: [],
1003
- });
1004
998
  mockDataAccess.Site.findById.resolves(mockSiteObject);
1005
999
 
1006
1000
  const tierClientWithoutSite = new TierClient(
@@ -1013,7 +1007,9 @@ describe('TierClient', () => {
1013
1007
  const result = await tierClientWithoutSite.getFirstEnrollment();
1014
1008
 
1015
1009
  expect(result.enrollment).to.equal(mockSiteEnrollment);
1016
- expect(mockDataAccess.Site.findById).to.have.been.calledWith(siteId);
1010
+ expect(result.site).to.equal(mockSiteObject);
1011
+ expect(mockDataAccess.Site.findById).to.have.been.calledOnceWith(siteId);
1012
+ expect(mockDataAccess.Site.batchGetByKeys).to.not.have.been.called;
1017
1013
  });
1018
1014
 
1019
1015
  it('should return nulls when no entitlement exists', async () => {
@@ -1043,19 +1039,9 @@ describe('TierClient', () => {
1043
1039
  expect(mockDataAccess.Site.findById).to.not.have.been.called;
1044
1040
  });
1045
1041
 
1046
- it('should return null site when site not found in database', async () => {
1047
- const mockSiteObject = {
1048
- getId: () => siteId,
1049
- getName: () => 'Test Site',
1050
- getOrganizationId: () => orgId,
1051
- };
1052
-
1042
+ it('should skip enrollment when site not found and return nulls', async () => {
1053
1043
  mockDataAccess.Entitlement.findByOrganizationIdAndProductCode.resolves(mockEntitlement);
1054
1044
  mockDataAccess.SiteEnrollment.allByEntitlementId.resolves([mockSiteEnrollment]);
1055
- mockDataAccess.Site.batchGetByKeys.resolves({
1056
- data: [mockSiteObject],
1057
- unprocessed: [],
1058
- });
1059
1045
  mockDataAccess.Site.findById.resolves(null);
1060
1046
 
1061
1047
  const tierClientWithoutSite = new TierClient(
@@ -1068,12 +1054,12 @@ describe('TierClient', () => {
1068
1054
  const result = await tierClientWithoutSite.getFirstEnrollment();
1069
1055
 
1070
1056
  expect(result).to.deep.equal({
1071
- entitlement: mockEntitlement,
1072
- enrollment: mockSiteEnrollment,
1057
+ entitlement: null,
1058
+ enrollment: null,
1073
1059
  site: null,
1074
1060
  });
1075
1061
  expect(mockContext.log.warn).to.have.been.calledWith(
1076
- `Site not found for enrollment ${mockSiteEnrollment.getId()} with site ID ${siteId}`,
1062
+ `Site not found for enrollment ${mockSiteEnrollment.getId()} with siteId ${siteId}`,
1077
1063
  );
1078
1064
  });
1079
1065
 
@@ -1085,35 +1071,96 @@ describe('TierClient', () => {
1085
1071
  });
1086
1072
 
1087
1073
  it('should work with site-specific client', async () => {
1088
- const mockSiteObject = {
1089
- getId: () => siteId,
1090
- getName: () => 'Test Site',
1091
- getOrganizationId: () => orgId,
1092
- };
1093
-
1094
1074
  mockDataAccess.Entitlement.findByOrganizationIdAndProductCode.resolves(mockEntitlement);
1095
1075
  mockDataAccess.SiteEnrollment.allByEntitlementId.resolves([mockSiteEnrollment]);
1096
- mockDataAccess.Site.batchGetByKeys.resolves({
1097
- data: [mockSiteObject],
1098
- unprocessed: [],
1099
- });
1100
- mockDataAccess.Site.findById.resolves(mockSiteObject);
1101
1076
 
1102
1077
  const result = await tierClient.getFirstEnrollment();
1103
1078
 
1104
1079
  expect(result).to.deep.equal({
1105
1080
  entitlement: mockEntitlement,
1106
1081
  enrollment: mockSiteEnrollment,
1107
- site: mockSiteObject,
1082
+ site: siteInstance,
1108
1083
  });
1084
+ expect(mockDataAccess.Site.findById).to.not.have.been.called;
1085
+ expect(mockDataAccess.Site.batchGetByKeys).to.not.have.been.called;
1109
1086
  });
1110
1087
 
1111
- it('should handle error when fetching site via batchGetByKeys', async () => {
1088
+ it('should handle error when fetching site via findById', async () => {
1112
1089
  mockDataAccess.Entitlement.findByOrganizationIdAndProductCode.resolves(mockEntitlement);
1113
1090
  mockDataAccess.SiteEnrollment.allByEntitlementId.resolves([mockSiteEnrollment]);
1114
- mockDataAccess.Site.batchGetByKeys.rejects(new Error('Site fetch error'));
1091
+ mockDataAccess.Site.findById.rejects(new Error('Site fetch error'));
1115
1092
 
1116
- await expect(tierClient.getFirstEnrollment()).to.be.rejectedWith('Site fetch error');
1093
+ const tierClientWithoutSite = new TierClient(
1094
+ mockContext,
1095
+ organizationInstance,
1096
+ null,
1097
+ productCode,
1098
+ );
1099
+
1100
+ await expect(tierClientWithoutSite.getFirstEnrollment()).to.be.rejectedWith('Site fetch error');
1101
+ expect(mockDataAccess.Site.batchGetByKeys).to.not.have.been.called;
1102
+ });
1103
+
1104
+ it('should return nulls when site-specific client has no matching enrollment', async () => {
1105
+ const nonMatchingEnrollment = {
1106
+ getId: () => 'enrollment-999',
1107
+ getSiteId: () => 'other-site-id',
1108
+ getEntitlementId: () => 'entitlement-123',
1109
+ };
1110
+
1111
+ mockDataAccess.Entitlement.findByOrganizationIdAndProductCode.resolves(mockEntitlement);
1112
+ mockDataAccess.SiteEnrollment.allByEntitlementId.resolves([nonMatchingEnrollment]);
1113
+
1114
+ const result = await tierClient.getFirstEnrollment();
1115
+
1116
+ expect(result).to.deep.equal({
1117
+ entitlement: null,
1118
+ enrollment: null,
1119
+ site: null,
1120
+ });
1121
+ expect(mockDataAccess.Site.findById).to.not.have.been.called;
1122
+ });
1123
+
1124
+ it('should skip enrollments with mismatching orgId and return first match', async () => {
1125
+ const wrongOrgSite = {
1126
+ getId: () => siteId,
1127
+ getOrganizationId: () => 'wrong-org-id',
1128
+ };
1129
+
1130
+ const correctSite = {
1131
+ getId: () => 'correct-site-id',
1132
+ getOrganizationId: () => orgId,
1133
+ };
1134
+
1135
+ const enrollment2 = {
1136
+ getId: () => 'enrollment-456',
1137
+ getSiteId: () => 'correct-site-id',
1138
+ getEntitlementId: () => 'entitlement-123',
1139
+ };
1140
+
1141
+ mockDataAccess.Entitlement.findByOrganizationIdAndProductCode.resolves(mockEntitlement);
1142
+ mockDataAccess.SiteEnrollment.allByEntitlementId.resolves([
1143
+ mockSiteEnrollment,
1144
+ enrollment2,
1145
+ ]);
1146
+ mockDataAccess.Site.findById.onFirstCall().resolves(wrongOrgSite);
1147
+ mockDataAccess.Site.findById.onSecondCall().resolves(correctSite);
1148
+
1149
+ const tierClientWithoutSite = new TierClient(
1150
+ mockContext,
1151
+ organizationInstance,
1152
+ null,
1153
+ productCode,
1154
+ );
1155
+
1156
+ const result = await tierClientWithoutSite.getFirstEnrollment();
1157
+
1158
+ expect(result).to.deep.equal({
1159
+ entitlement: mockEntitlement,
1160
+ enrollment: enrollment2,
1161
+ site: correctSite,
1162
+ });
1163
+ expect(mockDataAccess.Site.findById).to.have.been.calledTwice;
1117
1164
  });
1118
1165
  });
1119
1166
  });