@open-mercato/enterprise 0.4.5-develop-2e9903a57a → 0.4.5-develop-eeccf7adf4

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.
Files changed (32) hide show
  1. package/package.json +4 -4
  2. package/dist/modules/record_locks/__integration__/TC-LOCK-001.spec.js +0 -73
  3. package/dist/modules/record_locks/__integration__/TC-LOCK-001.spec.js.map +0 -7
  4. package/dist/modules/record_locks/__integration__/TC-LOCK-002.spec.js +0 -114
  5. package/dist/modules/record_locks/__integration__/TC-LOCK-002.spec.js.map +0 -7
  6. package/dist/modules/record_locks/__integration__/TC-LOCK-003.spec.js +0 -119
  7. package/dist/modules/record_locks/__integration__/TC-LOCK-003.spec.js.map +0 -7
  8. package/dist/modules/record_locks/__integration__/TC-LOCK-004.spec.js +0 -119
  9. package/dist/modules/record_locks/__integration__/TC-LOCK-004.spec.js.map +0 -7
  10. package/dist/modules/record_locks/__integration__/TC-LOCK-005.spec.js +0 -90
  11. package/dist/modules/record_locks/__integration__/TC-LOCK-005.spec.js.map +0 -7
  12. package/dist/modules/record_locks/__integration__/TC-LOCK-006.spec.js +0 -90
  13. package/dist/modules/record_locks/__integration__/TC-LOCK-006.spec.js.map +0 -7
  14. package/dist/modules/record_locks/__integration__/TC-LOCK-007.spec.js +0 -211
  15. package/dist/modules/record_locks/__integration__/TC-LOCK-007.spec.js.map +0 -7
  16. package/dist/modules/record_locks/__integration__/helpers/recordLocks.js +0 -219
  17. package/dist/modules/record_locks/__integration__/helpers/recordLocks.js.map +0 -7
  18. package/src/modules/record_locks/__integration__/TC-LOCK-001.spec.ts +0 -84
  19. package/src/modules/record_locks/__integration__/TC-LOCK-002.spec.ts +0 -129
  20. package/src/modules/record_locks/__integration__/TC-LOCK-003.spec.ts +0 -136
  21. package/src/modules/record_locks/__integration__/TC-LOCK-004.spec.ts +0 -136
  22. package/src/modules/record_locks/__integration__/TC-LOCK-005.spec.ts +0 -106
  23. package/src/modules/record_locks/__integration__/TC-LOCK-006.spec.ts +0 -113
  24. package/src/modules/record_locks/__integration__/TC-LOCK-007.spec.ts +0 -251
  25. package/src/modules/record_locks/__integration__/helpers/recordLocks.ts +0 -366
  26. package/src/modules/record_locks/__tests__/config.test.ts +0 -21
  27. package/src/modules/record_locks/__tests__/crudMutationGuardService.test.ts +0 -106
  28. package/src/modules/record_locks/__tests__/recordLockService.test.ts +0 -1226
  29. package/src/modules/record_locks/__tests__/recordLockWidgetHeaders.test.ts +0 -127
  30. package/src/modules/record_locks/api/__tests__/acquire.route.test.ts +0 -175
  31. package/src/modules/record_locks/api/__tests__/release.route.test.ts +0 -135
  32. package/src/modules/record_locks/api/__tests__/settings.route.test.ts +0 -85
@@ -1,90 +0,0 @@
1
- import { expect, test } from "@playwright/test";
2
- import { getAuthToken } from "@open-mercato/core/modules/core/__integration__/helpers/api";
3
- import { createCompanyFixture } from "@open-mercato/core/modules/core/__integration__/helpers/crmFixtures";
4
- import {
5
- acquireRecordLock,
6
- buildScopeCookieFromToken,
7
- cleanupCompany,
8
- getRecordLockSettings,
9
- releaseRecordLock,
10
- saveRecordLockSettings
11
- } from "./helpers/recordLocks.js";
12
- test.describe("TC-LOCK-006: Lock payload exposes participant ring with redacted email only", () => {
13
- test.describe.configure({ timeout: 9e4 });
14
- test("should return participant queue data with masked email when another user views the same record", async ({ request }) => {
15
- const superadminToken = await getAuthToken(request, "superadmin");
16
- const adminToken = await getAuthToken(request, "admin");
17
- const superadminScopeCookie = buildScopeCookieFromToken(superadminToken);
18
- const superadminScopeHeaders = superadminScopeCookie ? { cookie: superadminScopeCookie } : void 0;
19
- const ownerIp = "198.51.100.24";
20
- let previousSettings = null;
21
- let companyId = null;
22
- let ownerLockToken = null;
23
- try {
24
- previousSettings = await getRecordLockSettings(request, superadminToken);
25
- await saveRecordLockSettings(request, superadminToken, {
26
- ...previousSettings,
27
- enabled: true,
28
- strategy: "optimistic",
29
- enabledResources: ["customers.company"],
30
- allowForceUnlock: true
31
- });
32
- companyId = await createCompanyFixture(request, adminToken, `QA TC-LOCK-006 Company ${Date.now()}`);
33
- const ownerAcquire = await acquireRecordLock(
34
- request,
35
- superadminToken,
36
- "customers.company",
37
- companyId,
38
- {
39
- ...superadminScopeHeaders ?? {},
40
- "x-forwarded-for": `${ownerIp}, 10.0.0.1`
41
- }
42
- );
43
- expect(ownerAcquire.status).toBe(200);
44
- ownerLockToken = ownerAcquire.body?.lock?.token ?? null;
45
- expect(ownerLockToken).toBeTruthy();
46
- const viewerAcquire = await acquireRecordLock(
47
- request,
48
- adminToken,
49
- "customers.company",
50
- companyId
51
- );
52
- expect(viewerAcquire.status).toBe(200);
53
- expect(viewerAcquire.body?.acquired).toBe(true);
54
- const lock = viewerAcquire.body?.lock ?? null;
55
- expect(lock).toBeTruthy();
56
- expect(lock?.activeParticipantCount).toBeGreaterThanOrEqual(2);
57
- expect(lock?.lockedByIp ?? null).toBeNull();
58
- expect(lock?.lockedByName ?? null).toBeNull();
59
- expect(lock?.lockedByEmail ?? null).toMatch(/^[a-z0-9]{1,2}\*\*@[a-z0-9]{1,4}\*\*\.[a-z0-9.]+$/);
60
- const viewerId = viewerAcquire.body?.currentUserId ?? null;
61
- expect(viewerId).toBeTruthy();
62
- const otherParticipants = (lock?.participants ?? []).filter((entry) => entry.userId !== viewerId);
63
- const ownerParticipant = otherParticipants.find((entry) => entry.userId);
64
- expect(ownerParticipant?.lockedByIp).toBeUndefined();
65
- expect(ownerParticipant?.lockedByName).toBeUndefined();
66
- expect(ownerParticipant?.lockedByEmail ?? null).toMatch(/^[a-z0-9]{1,2}\*\*@[a-z0-9]{1,4}\*\*\.[a-z0-9.]+$/);
67
- expect(otherParticipants.length).toBeGreaterThanOrEqual(1);
68
- } finally {
69
- if (ownerLockToken && companyId) {
70
- await releaseRecordLock(
71
- request,
72
- superadminToken,
73
- "customers.company",
74
- companyId,
75
- ownerLockToken,
76
- "cancelled",
77
- void 0,
78
- superadminScopeHeaders
79
- ).catch(() => {
80
- });
81
- }
82
- await cleanupCompany(request, adminToken, companyId);
83
- if (previousSettings) {
84
- await saveRecordLockSettings(request, superadminToken, previousSettings).catch(() => {
85
- });
86
- }
87
- }
88
- });
89
- });
90
- //# sourceMappingURL=TC-LOCK-006.spec.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../../src/modules/record_locks/__integration__/TC-LOCK-006.spec.ts"],
4
- "sourcesContent": ["import { expect, test } from '@playwright/test';\nimport { getAuthToken } from '@open-mercato/core/modules/core/__integration__/helpers/api';\nimport { createCompanyFixture } from '@open-mercato/core/modules/core/__integration__/helpers/crmFixtures';\nimport {\n acquireRecordLock,\n buildScopeCookieFromToken,\n cleanupCompany,\n getRecordLockSettings,\n releaseRecordLock,\n saveRecordLockSettings,\n type RecordLockSettings,\n} from './helpers/recordLocks';\n\n/**\n * TC-LOCK-006: Lock payload exposes participant ring with redacted email only\n */\ntest.describe('TC-LOCK-006: Lock payload exposes participant ring with redacted email only', () => {\n test.describe.configure({ timeout: 90_000 });\n\n test('should return participant queue data with masked email when another user views the same record', async ({ request }) => {\n const superadminToken = await getAuthToken(request, 'superadmin');\n const adminToken = await getAuthToken(request, 'admin');\n const superadminScopeCookie = buildScopeCookieFromToken(superadminToken);\n const superadminScopeHeaders = superadminScopeCookie ? { cookie: superadminScopeCookie } : undefined;\n const ownerIp = '198.51.100.24';\n\n let previousSettings: RecordLockSettings | null = null;\n let companyId: string | null = null;\n let ownerLockToken: string | null = null;\n\n try {\n previousSettings = await getRecordLockSettings(request, superadminToken);\n await saveRecordLockSettings(request, superadminToken, {\n ...previousSettings,\n enabled: true,\n strategy: 'optimistic',\n enabledResources: ['customers.company'],\n allowForceUnlock: true,\n });\n\n companyId = await createCompanyFixture(request, adminToken, `QA TC-LOCK-006 Company ${Date.now()}`);\n\n const ownerAcquire = await acquireRecordLock(\n request,\n superadminToken,\n 'customers.company',\n companyId,\n {\n ...(superadminScopeHeaders ?? {}),\n 'x-forwarded-for': `${ownerIp}, 10.0.0.1`,\n },\n );\n expect(ownerAcquire.status).toBe(200);\n ownerLockToken = (ownerAcquire.body?.lock as { token?: string | null } | undefined)?.token ?? null;\n expect(ownerLockToken).toBeTruthy();\n\n const viewerAcquire = await acquireRecordLock(\n request,\n adminToken,\n 'customers.company',\n companyId,\n );\n expect(viewerAcquire.status).toBe(200);\n expect(viewerAcquire.body?.acquired).toBe(true);\n\n const lock = (viewerAcquire.body?.lock as {\n lockedByUserId?: string;\n lockedByName?: string | null;\n lockedByEmail?: string | null;\n lockedByIp?: string | null;\n activeParticipantCount?: number;\n participants?: Array<{\n userId?: string;\n lockedByName?: string | null;\n lockedByEmail?: string | null;\n lockedByIp?: string | null;\n }>;\n } | null | undefined) ?? null;\n\n expect(lock).toBeTruthy();\n expect(lock?.activeParticipantCount).toBeGreaterThanOrEqual(2);\n expect(lock?.lockedByIp ?? null).toBeNull();\n expect(lock?.lockedByName ?? null).toBeNull();\n expect(lock?.lockedByEmail ?? null).toMatch(/^[a-z0-9]{1,2}\\*\\*@[a-z0-9]{1,4}\\*\\*\\.[a-z0-9.]+$/);\n const viewerId =\n (viewerAcquire.body as { currentUserId?: string | null } | null)?.currentUserId ?? null;\n expect(viewerId).toBeTruthy();\n const otherParticipants = (lock?.participants ?? []).filter((entry) => entry.userId !== viewerId);\n const ownerParticipant = otherParticipants.find((entry) => entry.userId);\n expect(ownerParticipant?.lockedByIp).toBeUndefined();\n expect(ownerParticipant?.lockedByName).toBeUndefined();\n expect(ownerParticipant?.lockedByEmail ?? null).toMatch(/^[a-z0-9]{1,2}\\*\\*@[a-z0-9]{1,4}\\*\\*\\.[a-z0-9.]+$/);\n expect(otherParticipants.length).toBeGreaterThanOrEqual(1);\n } finally {\n if (ownerLockToken && companyId) {\n await releaseRecordLock(\n request,\n superadminToken,\n 'customers.company',\n companyId,\n ownerLockToken,\n 'cancelled',\n undefined,\n superadminScopeHeaders,\n ).catch(() => {});\n }\n await cleanupCompany(request, adminToken, companyId);\n if (previousSettings) {\n await saveRecordLockSettings(request, superadminToken, previousSettings).catch(() => {});\n }\n }\n });\n});\n"],
5
- "mappings": "AAAA,SAAS,QAAQ,YAAY;AAC7B,SAAS,oBAAoB;AAC7B,SAAS,4BAA4B;AACrC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAKP,KAAK,SAAS,+EAA+E,MAAM;AACjG,OAAK,SAAS,UAAU,EAAE,SAAS,IAAO,CAAC;AAE3C,OAAK,kGAAkG,OAAO,EAAE,QAAQ,MAAM;AAC5H,UAAM,kBAAkB,MAAM,aAAa,SAAS,YAAY;AAChE,UAAM,aAAa,MAAM,aAAa,SAAS,OAAO;AACtD,UAAM,wBAAwB,0BAA0B,eAAe;AACvE,UAAM,yBAAyB,wBAAwB,EAAE,QAAQ,sBAAsB,IAAI;AAC3F,UAAM,UAAU;AAEhB,QAAI,mBAA8C;AAClD,QAAI,YAA2B;AAC/B,QAAI,iBAAgC;AAEpC,QAAI;AACF,yBAAmB,MAAM,sBAAsB,SAAS,eAAe;AACvE,YAAM,uBAAuB,SAAS,iBAAiB;AAAA,QACrD,GAAG;AAAA,QACH,SAAS;AAAA,QACT,UAAU;AAAA,QACV,kBAAkB,CAAC,mBAAmB;AAAA,QACtC,kBAAkB;AAAA,MACpB,CAAC;AAED,kBAAY,MAAM,qBAAqB,SAAS,YAAY,0BAA0B,KAAK,IAAI,CAAC,EAAE;AAElG,YAAM,eAAe,MAAM;AAAA,QACzB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,UACE,GAAI,0BAA0B,CAAC;AAAA,UAC/B,mBAAmB,GAAG,OAAO;AAAA,QAC/B;AAAA,MACF;AACA,aAAO,aAAa,MAAM,EAAE,KAAK,GAAG;AACpC,uBAAkB,aAAa,MAAM,MAAgD,SAAS;AAC9F,aAAO,cAAc,EAAE,WAAW;AAElC,YAAM,gBAAgB,MAAM;AAAA,QAC1B;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AACA,aAAO,cAAc,MAAM,EAAE,KAAK,GAAG;AACrC,aAAO,cAAc,MAAM,QAAQ,EAAE,KAAK,IAAI;AAE9C,YAAM,OAAQ,cAAc,MAAM,QAYT;AAEzB,aAAO,IAAI,EAAE,WAAW;AACxB,aAAO,MAAM,sBAAsB,EAAE,uBAAuB,CAAC;AAC7D,aAAO,MAAM,cAAc,IAAI,EAAE,SAAS;AAC1C,aAAO,MAAM,gBAAgB,IAAI,EAAE,SAAS;AAC5C,aAAO,MAAM,iBAAiB,IAAI,EAAE,QAAQ,mDAAmD;AAC/F,YAAM,WACH,cAAc,MAAmD,iBAAiB;AACrF,aAAO,QAAQ,EAAE,WAAW;AAC5B,YAAM,qBAAqB,MAAM,gBAAgB,CAAC,GAAG,OAAO,CAAC,UAAU,MAAM,WAAW,QAAQ;AAChG,YAAM,mBAAmB,kBAAkB,KAAK,CAAC,UAAU,MAAM,MAAM;AACvE,aAAO,kBAAkB,UAAU,EAAE,cAAc;AACnD,aAAO,kBAAkB,YAAY,EAAE,cAAc;AACrD,aAAO,kBAAkB,iBAAiB,IAAI,EAAE,QAAQ,mDAAmD;AAC3G,aAAO,kBAAkB,MAAM,EAAE,uBAAuB,CAAC;AAAA,IAC3D,UAAE;AACA,UAAI,kBAAkB,WAAW;AAC/B,cAAM;AAAA,UACJ;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QACF,EAAE,MAAM,MAAM;AAAA,QAAC,CAAC;AAAA,MAClB;AACA,YAAM,eAAe,SAAS,YAAY,SAAS;AACnD,UAAI,kBAAkB;AACpB,cAAM,uBAAuB,SAAS,iBAAiB,gBAAgB,EAAE,MAAM,MAAM;AAAA,QAAC,CAAC;AAAA,MACzF;AAAA,IACF;AAAA,EACF,CAAC;AACH,CAAC;",
6
- "names": []
7
- }
@@ -1,211 +0,0 @@
1
- import { expect, test } from "@playwright/test";
2
- import { getAuthToken } from "@open-mercato/core/modules/core/__integration__/helpers/api";
3
- import { createCompanyFixture } from "@open-mercato/core/modules/core/__integration__/helpers/crmFixtures";
4
- import {
5
- acquireRecordLock,
6
- buildScopeCookieFromToken,
7
- cleanupCompany,
8
- getCompanyDisplayName,
9
- getRecordLockSettings,
10
- releaseRecordLock,
11
- saveRecordLockSettings,
12
- updateCompany,
13
- waitForNotification
14
- } from "./helpers/recordLocks.js";
15
- async function createConflictScenario(request, superadminToken, adminToken, companyId, superadminScopeHeaders) {
16
- const acquire = await acquireRecordLock(
17
- request,
18
- superadminToken,
19
- "customers.company",
20
- companyId,
21
- superadminScopeHeaders
22
- );
23
- expect(acquire.status).toBe(200);
24
- const ownerLockToken = acquire.body?.lock?.token ?? null;
25
- const baseLogId = acquire.body?.latestActionLogId ?? null;
26
- expect(ownerLockToken).toBeTruthy();
27
- expect(baseLogId).toBeTruthy();
28
- const incomingName = `QA TC-LOCK-007 Incoming ${Date.now()}`;
29
- const incomingUpdate = await updateCompany(request, adminToken, companyId, incomingName);
30
- expect(incomingUpdate.status).toBe(200);
31
- const conflictAttempt = await updateCompany(
32
- request,
33
- superadminToken,
34
- companyId,
35
- `QA TC-LOCK-007 Mine ${Date.now()}`,
36
- {
37
- token: ownerLockToken,
38
- baseLogId,
39
- resolution: "normal"
40
- },
41
- superadminScopeHeaders
42
- );
43
- expect(conflictAttempt.status).toBe(409);
44
- expect(conflictAttempt.body?.code).toBe("record_lock_conflict");
45
- const conflictId = conflictAttempt.body?.conflict?.id ?? null;
46
- expect(conflictId).toBeTruthy();
47
- const notification = await waitForNotification(
48
- request,
49
- superadminToken,
50
- "record_locks.conflict.detected",
51
- (item) => item.sourceEntityId === conflictId
52
- );
53
- return {
54
- conflictId,
55
- notification,
56
- ownerLockToken,
57
- baseLogId,
58
- incomingName
59
- };
60
- }
61
- test.describe("TC-LOCK-007: Conflict notification changed fields and apply/reject actions", () => {
62
- test.describe.configure({ timeout: 9e4 });
63
- test("should include changed incoming fields and execute accept_incoming action from notification", async ({ request }) => {
64
- const superadminToken = await getAuthToken(request, "superadmin");
65
- const adminToken = await getAuthToken(request, "admin");
66
- const superadminScopeCookie = buildScopeCookieFromToken(superadminToken);
67
- const superadminScopeHeaders = superadminScopeCookie ? { cookie: superadminScopeCookie } : void 0;
68
- let previousSettings = null;
69
- let companyId = null;
70
- let ownerLockToken = null;
71
- try {
72
- previousSettings = await getRecordLockSettings(request, superadminToken);
73
- await saveRecordLockSettings(request, superadminToken, {
74
- ...previousSettings,
75
- enabled: true,
76
- strategy: "optimistic",
77
- enabledResources: ["customers.company"],
78
- notifyOnConflict: true
79
- });
80
- companyId = await createCompanyFixture(request, adminToken, `QA TC-LOCK-007 Company A ${Date.now()}`);
81
- const conflict = await createConflictScenario(
82
- request,
83
- superadminToken,
84
- adminToken,
85
- companyId,
86
- superadminScopeHeaders
87
- );
88
- ownerLockToken = conflict.ownerLockToken;
89
- expect(conflict.notification.bodyVariables?.changedFields?.toLowerCase()).toContain("display");
90
- const actionIds = (conflict.notification.actions ?? []).map((item) => item.id);
91
- expect(actionIds).toEqual([]);
92
- const releaseResult = await releaseRecordLock(
93
- request,
94
- superadminToken,
95
- "customers.company",
96
- companyId,
97
- conflict.ownerLockToken,
98
- "conflict_resolved",
99
- {
100
- conflictId: conflict.conflictId,
101
- resolution: "accept_incoming"
102
- },
103
- superadminScopeHeaders
104
- );
105
- expect(releaseResult.status).toBe(200);
106
- expect(releaseResult.body?.ok).toBe(true);
107
- expect(releaseResult.body?.conflictResolved).toBe(true);
108
- const finalName = await getCompanyDisplayName(request, adminToken, companyId);
109
- expect(finalName).toBe(conflict.incomingName);
110
- await waitForNotification(
111
- request,
112
- adminToken,
113
- "record_locks.conflict.resolved",
114
- (item) => item.sourceEntityId === conflict.conflictId && item.bodyVariables?.resolution === "accept_incoming"
115
- );
116
- ownerLockToken = null;
117
- } finally {
118
- if (ownerLockToken && companyId) {
119
- await releaseRecordLock(
120
- request,
121
- superadminToken,
122
- "customers.company",
123
- companyId,
124
- ownerLockToken,
125
- "cancelled",
126
- void 0,
127
- superadminScopeHeaders
128
- ).catch(() => {
129
- });
130
- }
131
- await cleanupCompany(request, adminToken, companyId);
132
- if (previousSettings) {
133
- await saveRecordLockSettings(request, superadminToken, previousSettings).catch(() => {
134
- });
135
- }
136
- }
137
- });
138
- test("should execute accept_mine action from notification and emit resolved notification", async ({ request }) => {
139
- const superadminToken = await getAuthToken(request, "superadmin");
140
- const adminToken = await getAuthToken(request, "admin");
141
- const superadminScopeCookie = buildScopeCookieFromToken(superadminToken);
142
- const superadminScopeHeaders = superadminScopeCookie ? { cookie: superadminScopeCookie } : void 0;
143
- let previousSettings = null;
144
- let companyId = null;
145
- let ownerLockToken = null;
146
- try {
147
- previousSettings = await getRecordLockSettings(request, superadminToken);
148
- await saveRecordLockSettings(request, superadminToken, {
149
- ...previousSettings,
150
- enabled: true,
151
- strategy: "optimistic",
152
- enabledResources: ["customers.company"],
153
- notifyOnConflict: true
154
- });
155
- companyId = await createCompanyFixture(request, adminToken, `QA TC-LOCK-007 Company B ${Date.now()}`);
156
- const conflict = await createConflictScenario(
157
- request,
158
- superadminToken,
159
- adminToken,
160
- companyId,
161
- superadminScopeHeaders
162
- );
163
- ownerLockToken = conflict.ownerLockToken;
164
- const keepMineName = `QA TC-LOCK-007 Keep Mine ${Date.now()}`;
165
- const updateResult = await updateCompany(
166
- request,
167
- superadminToken,
168
- companyId,
169
- keepMineName,
170
- {
171
- token: conflict.ownerLockToken,
172
- baseLogId: conflict.baseLogId,
173
- resolution: "accept_mine",
174
- conflictId: conflict.conflictId
175
- },
176
- superadminScopeHeaders
177
- );
178
- expect(updateResult.status).toBe(200);
179
- expect(updateResult.body?.ok).toBe(true);
180
- const finalName = await getCompanyDisplayName(request, adminToken, companyId);
181
- expect(finalName).toBe(keepMineName);
182
- await waitForNotification(
183
- request,
184
- adminToken,
185
- "record_locks.conflict.resolved",
186
- (item) => item.sourceEntityId === conflict.conflictId && item.bodyVariables?.resolution === "accept_mine"
187
- );
188
- ownerLockToken = null;
189
- } finally {
190
- if (ownerLockToken && companyId) {
191
- await releaseRecordLock(
192
- request,
193
- superadminToken,
194
- "customers.company",
195
- companyId,
196
- ownerLockToken,
197
- "cancelled",
198
- void 0,
199
- superadminScopeHeaders
200
- ).catch(() => {
201
- });
202
- }
203
- await cleanupCompany(request, adminToken, companyId);
204
- if (previousSettings) {
205
- await saveRecordLockSettings(request, superadminToken, previousSettings).catch(() => {
206
- });
207
- }
208
- }
209
- });
210
- });
211
- //# sourceMappingURL=TC-LOCK-007.spec.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../../src/modules/record_locks/__integration__/TC-LOCK-007.spec.ts"],
4
- "sourcesContent": ["import { expect, test, type APIRequestContext } from '@playwright/test';\nimport { getAuthToken } from '@open-mercato/core/modules/core/__integration__/helpers/api';\nimport { createCompanyFixture } from '@open-mercato/core/modules/core/__integration__/helpers/crmFixtures';\nimport {\n acquireRecordLock,\n buildScopeCookieFromToken,\n cleanupCompany,\n getCompanyDisplayName,\n getRecordLockSettings,\n releaseRecordLock,\n saveRecordLockSettings,\n updateCompany,\n waitForNotification,\n type RecordLockSettings,\n type NotificationItem,\n} from './helpers/recordLocks';\n\ntype ConflictContext = {\n conflictId: string;\n notification: NotificationItem;\n ownerLockToken: string;\n baseLogId: string;\n incomingName: string;\n};\n\nasync function createConflictScenario(\n request: APIRequestContext,\n superadminToken: string,\n adminToken: string,\n companyId: string,\n superadminScopeHeaders?: Record<string, string>,\n): Promise<ConflictContext> {\n const acquire = await acquireRecordLock(\n request,\n superadminToken,\n 'customers.company',\n companyId,\n superadminScopeHeaders,\n );\n expect(acquire.status).toBe(200);\n\n const ownerLockToken = (acquire.body?.lock as { token?: string | null } | undefined)?.token ?? null;\n const baseLogId =\n (acquire.body as { latestActionLogId?: string | null } | null)?.latestActionLogId ?? null;\n expect(ownerLockToken).toBeTruthy();\n expect(baseLogId).toBeTruthy();\n\n const incomingName = `QA TC-LOCK-007 Incoming ${Date.now()}`;\n const incomingUpdate = await updateCompany(request, adminToken, companyId, incomingName);\n expect(incomingUpdate.status).toBe(200);\n\n const conflictAttempt = await updateCompany(\n request,\n superadminToken,\n companyId,\n `QA TC-LOCK-007 Mine ${Date.now()}`,\n {\n token: ownerLockToken,\n baseLogId,\n resolution: 'normal',\n },\n superadminScopeHeaders,\n );\n expect(conflictAttempt.status).toBe(409);\n expect(conflictAttempt.body?.code).toBe('record_lock_conflict');\n\n const conflictId = (conflictAttempt.body?.conflict as { id?: string } | undefined)?.id ?? null;\n expect(conflictId).toBeTruthy();\n\n const notification = await waitForNotification(\n request,\n superadminToken,\n 'record_locks.conflict.detected',\n (item) => item.sourceEntityId === conflictId,\n );\n\n return {\n conflictId: conflictId as string,\n notification,\n ownerLockToken: ownerLockToken as string,\n baseLogId: baseLogId as string,\n incomingName,\n };\n}\n\n/**\n * TC-LOCK-007: Conflict notification changed fields and apply/reject actions\n */\ntest.describe('TC-LOCK-007: Conflict notification changed fields and apply/reject actions', () => {\n test.describe.configure({ timeout: 90_000 });\n\n test('should include changed incoming fields and execute accept_incoming action from notification', async ({ request }) => {\n const superadminToken = await getAuthToken(request, 'superadmin');\n const adminToken = await getAuthToken(request, 'admin');\n const superadminScopeCookie = buildScopeCookieFromToken(superadminToken);\n const superadminScopeHeaders = superadminScopeCookie ? { cookie: superadminScopeCookie } : undefined;\n\n let previousSettings: RecordLockSettings | null = null;\n let companyId: string | null = null;\n let ownerLockToken: string | null = null;\n\n try {\n previousSettings = await getRecordLockSettings(request, superadminToken);\n await saveRecordLockSettings(request, superadminToken, {\n ...previousSettings,\n enabled: true,\n strategy: 'optimistic',\n enabledResources: ['customers.company'],\n notifyOnConflict: true,\n });\n\n companyId = await createCompanyFixture(request, adminToken, `QA TC-LOCK-007 Company A ${Date.now()}`);\n\n const conflict = await createConflictScenario(\n request,\n superadminToken,\n adminToken,\n companyId,\n superadminScopeHeaders,\n );\n ownerLockToken = conflict.ownerLockToken;\n\n expect(conflict.notification.bodyVariables?.changedFields?.toLowerCase()).toContain('display');\n const actionIds = (conflict.notification.actions ?? []).map((item) => item.id);\n expect(actionIds).toEqual([]);\n\n const releaseResult = await releaseRecordLock(\n request,\n superadminToken,\n 'customers.company',\n companyId,\n conflict.ownerLockToken,\n 'conflict_resolved',\n {\n conflictId: conflict.conflictId,\n resolution: 'accept_incoming',\n },\n superadminScopeHeaders,\n );\n expect(releaseResult.status).toBe(200);\n expect(releaseResult.body?.ok).toBe(true);\n expect((releaseResult.body as { conflictResolved?: boolean } | null)?.conflictResolved).toBe(true);\n\n const finalName = await getCompanyDisplayName(request, adminToken, companyId);\n expect(finalName).toBe(conflict.incomingName);\n\n await waitForNotification(\n request,\n adminToken,\n 'record_locks.conflict.resolved',\n (item) => item.sourceEntityId === conflict.conflictId && item.bodyVariables?.resolution === 'accept_incoming',\n );\n ownerLockToken = null;\n } finally {\n if (ownerLockToken && companyId) {\n await releaseRecordLock(\n request,\n superadminToken,\n 'customers.company',\n companyId,\n ownerLockToken,\n 'cancelled',\n undefined,\n superadminScopeHeaders,\n ).catch(() => {});\n }\n await cleanupCompany(request, adminToken, companyId);\n if (previousSettings) {\n await saveRecordLockSettings(request, superadminToken, previousSettings).catch(() => {});\n }\n }\n });\n\n test('should execute accept_mine action from notification and emit resolved notification', async ({ request }) => {\n const superadminToken = await getAuthToken(request, 'superadmin');\n const adminToken = await getAuthToken(request, 'admin');\n const superadminScopeCookie = buildScopeCookieFromToken(superadminToken);\n const superadminScopeHeaders = superadminScopeCookie ? { cookie: superadminScopeCookie } : undefined;\n\n let previousSettings: RecordLockSettings | null = null;\n let companyId: string | null = null;\n let ownerLockToken: string | null = null;\n\n try {\n previousSettings = await getRecordLockSettings(request, superadminToken);\n await saveRecordLockSettings(request, superadminToken, {\n ...previousSettings,\n enabled: true,\n strategy: 'optimistic',\n enabledResources: ['customers.company'],\n notifyOnConflict: true,\n });\n\n companyId = await createCompanyFixture(request, adminToken, `QA TC-LOCK-007 Company B ${Date.now()}`);\n\n const conflict = await createConflictScenario(\n request,\n superadminToken,\n adminToken,\n companyId,\n superadminScopeHeaders,\n );\n ownerLockToken = conflict.ownerLockToken;\n const keepMineName = `QA TC-LOCK-007 Keep Mine ${Date.now()}`;\n\n const updateResult = await updateCompany(\n request,\n superadminToken,\n companyId,\n keepMineName,\n {\n token: conflict.ownerLockToken,\n baseLogId: conflict.baseLogId,\n resolution: 'accept_mine',\n conflictId: conflict.conflictId,\n },\n superadminScopeHeaders,\n );\n expect(updateResult.status).toBe(200);\n expect(updateResult.body?.ok).toBe(true);\n\n const finalName = await getCompanyDisplayName(request, adminToken, companyId);\n expect(finalName).toBe(keepMineName);\n\n await waitForNotification(\n request,\n adminToken,\n 'record_locks.conflict.resolved',\n (item) => item.sourceEntityId === conflict.conflictId && item.bodyVariables?.resolution === 'accept_mine',\n );\n ownerLockToken = null;\n } finally {\n if (ownerLockToken && companyId) {\n await releaseRecordLock(\n request,\n superadminToken,\n 'customers.company',\n companyId,\n ownerLockToken,\n 'cancelled',\n undefined,\n superadminScopeHeaders,\n ).catch(() => {});\n }\n await cleanupCompany(request, adminToken, companyId);\n if (previousSettings) {\n await saveRecordLockSettings(request, superadminToken, previousSettings).catch(() => {});\n }\n }\n });\n});\n"],
5
- "mappings": "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",
6
- "names": []
7
- }
@@ -1,219 +0,0 @@
1
- import { expect } from "@playwright/test";
2
- import { apiRequest } from "@open-mercato/core/modules/core/__integration__/helpers/api";
3
- import { deleteEntityIfExists } from "@open-mercato/core/modules/core/__integration__/helpers/crmFixtures";
4
- const BASE_URL = process.env.BASE_URL || "http://localhost:3000";
5
- function sleep(ms) {
6
- return new Promise((resolve) => {
7
- setTimeout(resolve, ms);
8
- });
9
- }
10
- async function readJsonSafe(response) {
11
- const raw = await response.text();
12
- if (!raw) return null;
13
- try {
14
- return JSON.parse(raw);
15
- } catch {
16
- return null;
17
- }
18
- }
19
- async function requestJson(request, method, path, token, data, extraHeaders) {
20
- const headers = {
21
- Authorization: `Bearer ${token}`,
22
- "Content-Type": "application/json",
23
- ...extraHeaders ?? {}
24
- };
25
- const response = await request.fetch(`${BASE_URL}${path}`, {
26
- method,
27
- headers,
28
- data: data === void 0 ? void 0 : data
29
- });
30
- const body = await readJsonSafe(response);
31
- return {
32
- response,
33
- status: response.status(),
34
- body
35
- };
36
- }
37
- async function getRecordLockSettings(request, token) {
38
- const result = await requestJson(
39
- request,
40
- "GET",
41
- "/api/record_locks/settings",
42
- token
43
- );
44
- expect(result.status).toBe(200);
45
- expect(result.body?.settings).toBeTruthy();
46
- return result.body?.settings;
47
- }
48
- async function saveRecordLockSettings(request, token, settings) {
49
- const result = await requestJson(
50
- request,
51
- "POST",
52
- "/api/record_locks/settings",
53
- token,
54
- settings
55
- );
56
- expect(result.status).toBe(200);
57
- expect(result.body?.settings).toBeTruthy();
58
- return result.body?.settings;
59
- }
60
- async function acquireRecordLock(request, token, resourceKind, resourceId, extraHeaders) {
61
- return requestJson(
62
- request,
63
- "POST",
64
- "/api/record_locks/acquire",
65
- token,
66
- { resourceKind, resourceId },
67
- extraHeaders
68
- );
69
- }
70
- async function releaseRecordLock(request, token, resourceKind, resourceId, lockToken, reason = "cancelled", options, extraHeaders) {
71
- return requestJson(
72
- request,
73
- "POST",
74
- "/api/record_locks/release",
75
- token,
76
- {
77
- resourceKind,
78
- resourceId,
79
- token: lockToken,
80
- reason,
81
- ...options?.conflictId ? { conflictId: options.conflictId } : {},
82
- ...options?.resolution ? { resolution: options.resolution } : {}
83
- },
84
- extraHeaders
85
- );
86
- }
87
- async function forceReleaseRecordLock(request, token, resourceKind, resourceId, reason, extraHeaders) {
88
- return requestJson(
89
- request,
90
- "POST",
91
- "/api/record_locks/force-release",
92
- token,
93
- {
94
- resourceKind,
95
- resourceId,
96
- ...reason ? { reason } : {}
97
- },
98
- extraHeaders
99
- );
100
- }
101
- async function updateCompany(request, token, companyId, displayName, lockHeaders, extraHeaders) {
102
- const requestHeaders = { ...extraHeaders ?? {} };
103
- if (lockHeaders) {
104
- requestHeaders["x-om-record-lock-kind"] = "customers.company";
105
- requestHeaders["x-om-record-lock-resource-id"] = companyId;
106
- if (lockHeaders.token) {
107
- requestHeaders["x-om-record-lock-token"] = lockHeaders.token;
108
- }
109
- if (lockHeaders.baseLogId) {
110
- requestHeaders["x-om-record-lock-base-log-id"] = lockHeaders.baseLogId;
111
- }
112
- if (lockHeaders.resolution) {
113
- requestHeaders["x-om-record-lock-resolution"] = lockHeaders.resolution;
114
- }
115
- if (lockHeaders.conflictId) {
116
- requestHeaders["x-om-record-lock-conflict-id"] = lockHeaders.conflictId;
117
- }
118
- }
119
- return requestJson(
120
- request,
121
- "PUT",
122
- "/api/customers/companies",
123
- token,
124
- {
125
- id: companyId,
126
- displayName
127
- },
128
- requestHeaders
129
- );
130
- }
131
- function buildScopeCookieFromToken(token) {
132
- const parts = token.split(".");
133
- if (parts.length < 2) return null;
134
- try {
135
- const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
136
- const tenantId = typeof payload.tenantId === "string" && payload.tenantId.trim().length > 0 ? payload.tenantId.trim() : null;
137
- const orgId = typeof payload.orgId === "string" && payload.orgId.trim().length > 0 ? payload.orgId.trim() : null;
138
- const cookies = [];
139
- if (tenantId) cookies.push(`om_selected_tenant=${encodeURIComponent(tenantId)}`);
140
- if (orgId) cookies.push(`om_selected_org=${encodeURIComponent(orgId)}`);
141
- return cookies.length ? cookies.join("; ") : null;
142
- } catch {
143
- return null;
144
- }
145
- }
146
- async function getCompanyDisplayName(request, token, companyId) {
147
- const response = await apiRequest(
148
- request,
149
- "GET",
150
- `/api/customers/companies?id=${encodeURIComponent(companyId)}&pageSize=5`,
151
- { token }
152
- );
153
- expect(response.ok(), `Failed to read company ${companyId}: ${response.status()}`).toBeTruthy();
154
- const payload = await readJsonSafe(response);
155
- const rows = Array.isArray(payload?.items) ? payload.items : [];
156
- const row = rows.find((item) => typeof item.id === "string" && item.id === companyId) ?? rows[0] ?? null;
157
- if (!row) return null;
158
- const snake = row.display_name;
159
- if (typeof snake === "string") return snake;
160
- const camel = row.displayName;
161
- if (typeof camel === "string") return camel;
162
- return null;
163
- }
164
- async function listNotificationsByType(request, token, type) {
165
- const response = await apiRequest(
166
- request,
167
- "GET",
168
- `/api/notifications?type=${encodeURIComponent(type)}&pageSize=100`,
169
- { token }
170
- );
171
- expect(response.ok(), `Failed to list notifications: ${response.status()}`).toBeTruthy();
172
- const payload = await readJsonSafe(response);
173
- const items = Array.isArray(payload?.items) ? payload.items : [];
174
- return items.filter((entry) => {
175
- if (!entry || typeof entry !== "object") return false;
176
- const candidate = entry;
177
- return typeof candidate.id === "string" && typeof candidate.type === "string";
178
- });
179
- }
180
- async function waitForNotification(request, token, type, predicate, timeoutMs = 15e3, pollMs = 250) {
181
- const startedAt = Date.now();
182
- while (Date.now() - startedAt <= timeoutMs) {
183
- const items = await listNotificationsByType(request, token, type);
184
- const found = items.find(predicate);
185
- if (found) return found;
186
- await sleep(pollMs);
187
- }
188
- throw new Error(`Notification ${type} not found within ${timeoutMs}ms`);
189
- }
190
- async function executeNotificationAction(request, token, notificationId, actionId, payload) {
191
- return requestJson(
192
- request,
193
- "POST",
194
- `/api/notifications/${encodeURIComponent(notificationId)}/action`,
195
- token,
196
- {
197
- actionId,
198
- ...payload ? { payload } : {}
199
- }
200
- );
201
- }
202
- async function cleanupCompany(request, token, companyId) {
203
- await deleteEntityIfExists(request, token, "/api/customers/companies", companyId);
204
- }
205
- export {
206
- acquireRecordLock,
207
- buildScopeCookieFromToken,
208
- cleanupCompany,
209
- executeNotificationAction,
210
- forceReleaseRecordLock,
211
- getCompanyDisplayName,
212
- getRecordLockSettings,
213
- listNotificationsByType,
214
- releaseRecordLock,
215
- saveRecordLockSettings,
216
- updateCompany,
217
- waitForNotification
218
- };
219
- //# sourceMappingURL=recordLocks.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../../../src/modules/record_locks/__integration__/helpers/recordLocks.ts"],
4
- "sourcesContent": ["import { expect, type APIRequestContext, type APIResponse } from '@playwright/test';\nimport { apiRequest } from '@open-mercato/core/modules/core/__integration__/helpers/api';\nimport { deleteEntityIfExists } from '@open-mercato/core/modules/core/__integration__/helpers/crmFixtures';\n\nconst BASE_URL = process.env.BASE_URL || 'http://localhost:3000';\n\nexport type RecordLockSettings = {\n enabled: boolean;\n strategy: 'optimistic' | 'pessimistic';\n timeoutSeconds: number;\n heartbeatSeconds: number;\n enabledResources: string[];\n allowForceUnlock: boolean;\n allowIncomingOverride?: boolean;\n notifyOnConflict: boolean;\n};\n\nexport type RecordLockMutationResolution = 'normal' | 'accept_mine' | 'merged';\n\nexport type RecordLockMutationHeaders = {\n token?: string | null;\n baseLogId?: string | null;\n resolution?: RecordLockMutationResolution;\n conflictId?: string | null;\n};\n\nexport type NotificationItem = {\n id: string;\n type: string;\n status?: 'unread' | 'read' | 'actioned' | 'dismissed';\n actions?: Array<{\n id: string;\n label?: string;\n labelKey?: string;\n }>;\n sourceEntityId?: string | null;\n bodyVariables?: Record<string, string>;\n};\n\ntype ApiCallResult<TBody> = {\n response: APIResponse;\n status: number;\n body: TBody | null;\n};\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n}\n\nasync function readJsonSafe(response: APIResponse): Promise<unknown> {\n const raw = await response.text();\n if (!raw) return null;\n try {\n return JSON.parse(raw) as unknown;\n } catch {\n return null;\n }\n}\n\nasync function requestJson<TBody>(\n request: APIRequestContext,\n method: string,\n path: string,\n token: string,\n data?: unknown,\n extraHeaders?: Record<string, string>,\n): Promise<ApiCallResult<TBody>> {\n const headers: Record<string, string> = {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n ...(extraHeaders ?? {}),\n };\n\n const response = await request.fetch(`${BASE_URL}${path}`, {\n method,\n headers,\n data: data === undefined ? undefined : data,\n });\n\n const body = (await readJsonSafe(response)) as TBody | null;\n\n return {\n response,\n status: response.status(),\n body,\n };\n}\n\nexport async function getRecordLockSettings(\n request: APIRequestContext,\n token: string,\n): Promise<RecordLockSettings> {\n const result = await requestJson<{ settings?: RecordLockSettings }>(\n request,\n 'GET',\n '/api/record_locks/settings',\n token,\n );\n\n expect(result.status).toBe(200);\n expect(result.body?.settings).toBeTruthy();\n\n return result.body?.settings as RecordLockSettings;\n}\n\nexport async function saveRecordLockSettings(\n request: APIRequestContext,\n token: string,\n settings: RecordLockSettings,\n): Promise<RecordLockSettings> {\n const result = await requestJson<{ settings?: RecordLockSettings }>(\n request,\n 'POST',\n '/api/record_locks/settings',\n token,\n settings,\n );\n\n expect(result.status).toBe(200);\n expect(result.body?.settings).toBeTruthy();\n\n return result.body?.settings as RecordLockSettings;\n}\n\nexport async function acquireRecordLock(\n request: APIRequestContext,\n token: string,\n resourceKind: string,\n resourceId: string,\n extraHeaders?: Record<string, string>,\n): Promise<ApiCallResult<Record<string, unknown>>> {\n return requestJson<Record<string, unknown>>(\n request,\n 'POST',\n '/api/record_locks/acquire',\n token,\n { resourceKind, resourceId },\n extraHeaders,\n );\n}\n\nexport async function releaseRecordLock(\n request: APIRequestContext,\n token: string,\n resourceKind: string,\n resourceId: string,\n lockToken: string,\n reason: 'saved' | 'cancelled' | 'unmount' | 'conflict_resolved' = 'cancelled',\n options?: {\n conflictId?: string | null;\n resolution?: 'accept_incoming';\n },\n extraHeaders?: Record<string, string>,\n): Promise<ApiCallResult<Record<string, unknown>>> {\n return requestJson<Record<string, unknown>>(\n request,\n 'POST',\n '/api/record_locks/release',\n token,\n {\n resourceKind,\n resourceId,\n token: lockToken,\n reason,\n ...(options?.conflictId ? { conflictId: options.conflictId } : {}),\n ...(options?.resolution ? { resolution: options.resolution } : {}),\n },\n extraHeaders,\n );\n}\n\nexport async function forceReleaseRecordLock(\n request: APIRequestContext,\n token: string,\n resourceKind: string,\n resourceId: string,\n reason?: string,\n extraHeaders?: Record<string, string>,\n): Promise<ApiCallResult<Record<string, unknown>>> {\n return requestJson<Record<string, unknown>>(\n request,\n 'POST',\n '/api/record_locks/force-release',\n token,\n {\n resourceKind,\n resourceId,\n ...(reason ? { reason } : {}),\n },\n extraHeaders,\n );\n}\n\nexport async function updateCompany(\n request: APIRequestContext,\n token: string,\n companyId: string,\n displayName: string,\n lockHeaders?: RecordLockMutationHeaders,\n extraHeaders?: Record<string, string>,\n): Promise<ApiCallResult<Record<string, unknown>>> {\n const requestHeaders: Record<string, string> = { ...(extraHeaders ?? {}) };\n\n if (lockHeaders) {\n requestHeaders['x-om-record-lock-kind'] = 'customers.company';\n requestHeaders['x-om-record-lock-resource-id'] = companyId;\n\n if (lockHeaders.token) {\n requestHeaders['x-om-record-lock-token'] = lockHeaders.token;\n }\n\n if (lockHeaders.baseLogId) {\n requestHeaders['x-om-record-lock-base-log-id'] = lockHeaders.baseLogId;\n }\n\n if (lockHeaders.resolution) {\n requestHeaders['x-om-record-lock-resolution'] = lockHeaders.resolution;\n }\n\n if (lockHeaders.conflictId) {\n requestHeaders['x-om-record-lock-conflict-id'] = lockHeaders.conflictId;\n }\n }\n\n return requestJson<Record<string, unknown>>(\n request,\n 'PUT',\n '/api/customers/companies',\n token,\n {\n id: companyId,\n displayName,\n },\n requestHeaders,\n );\n}\n\ntype JwtScopePayload = {\n tenantId?: string | null;\n orgId?: string | null;\n};\n\nexport function buildScopeCookieFromToken(token: string): string | null {\n const parts = token.split('.');\n if (parts.length < 2) return null;\n\n try {\n const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8')) as JwtScopePayload;\n const tenantId = typeof payload.tenantId === 'string' && payload.tenantId.trim().length > 0\n ? payload.tenantId.trim()\n : null;\n const orgId = typeof payload.orgId === 'string' && payload.orgId.trim().length > 0\n ? payload.orgId.trim()\n : null;\n\n const cookies: string[] = [];\n if (tenantId) cookies.push(`om_selected_tenant=${encodeURIComponent(tenantId)}`);\n if (orgId) cookies.push(`om_selected_org=${encodeURIComponent(orgId)}`);\n\n return cookies.length ? cookies.join('; ') : null;\n } catch {\n return null;\n }\n}\n\nexport async function getCompanyDisplayName(\n request: APIRequestContext,\n token: string,\n companyId: string,\n): Promise<string | null> {\n const response = await apiRequest(\n request,\n 'GET',\n `/api/customers/companies?id=${encodeURIComponent(companyId)}&pageSize=5`,\n { token },\n );\n\n expect(response.ok(), `Failed to read company ${companyId}: ${response.status()}`).toBeTruthy();\n\n const payload = (await readJsonSafe(response)) as { items?: Array<Record<string, unknown>> } | null;\n const rows = Array.isArray(payload?.items) ? payload.items : [];\n const row = rows.find((item) => typeof item.id === 'string' && item.id === companyId) ?? rows[0] ?? null;\n\n if (!row) return null;\n\n const snake = row.display_name;\n if (typeof snake === 'string') return snake;\n\n const camel = row.displayName;\n if (typeof camel === 'string') return camel;\n\n return null;\n}\n\nexport async function listNotificationsByType(\n request: APIRequestContext,\n token: string,\n type: string,\n): Promise<NotificationItem[]> {\n const response = await apiRequest(\n request,\n 'GET',\n `/api/notifications?type=${encodeURIComponent(type)}&pageSize=100`,\n { token },\n );\n\n expect(response.ok(), `Failed to list notifications: ${response.status()}`).toBeTruthy();\n\n const payload = (await readJsonSafe(response)) as { items?: unknown[] } | null;\n const items = Array.isArray(payload?.items) ? payload.items : [];\n\n return items.filter((entry): entry is NotificationItem => {\n if (!entry || typeof entry !== 'object') return false;\n const candidate = entry as Record<string, unknown>;\n return typeof candidate.id === 'string' && typeof candidate.type === 'string';\n });\n}\n\nexport async function waitForNotification(\n request: APIRequestContext,\n token: string,\n type: string,\n predicate: (item: NotificationItem) => boolean,\n timeoutMs = 15_000,\n pollMs = 250,\n): Promise<NotificationItem> {\n const startedAt = Date.now();\n\n while (Date.now() - startedAt <= timeoutMs) {\n const items = await listNotificationsByType(request, token, type);\n const found = items.find(predicate);\n if (found) return found;\n await sleep(pollMs);\n }\n\n throw new Error(`Notification ${type} not found within ${timeoutMs}ms`);\n}\n\nexport async function executeNotificationAction(\n request: APIRequestContext,\n token: string,\n notificationId: string,\n actionId: string,\n payload?: Record<string, unknown>,\n): Promise<ApiCallResult<{ ok?: boolean; result?: unknown; href?: string }>> {\n return requestJson<{ ok?: boolean; result?: unknown; href?: string }>(\n request,\n 'POST',\n `/api/notifications/${encodeURIComponent(notificationId)}/action`,\n token,\n {\n actionId,\n ...(payload ? { payload } : {}),\n },\n );\n}\n\nexport async function cleanupCompany(\n request: APIRequestContext,\n token: string | null,\n companyId: string | null,\n): Promise<void> {\n await deleteEntityIfExists(request, token, '/api/customers/companies', companyId);\n}\n"],
5
- "mappings": "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",
6
- "names": []
7
- }