@htlkg/data 0.0.20 → 0.0.21
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/dist/hooks/index.d.ts +88 -2
- package/dist/hooks/index.js +63 -3
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +441 -3
- package/dist/index.js.map +1 -1
- package/dist/mutations/index.d.ts +338 -2
- package/dist/mutations/index.js +286 -0
- package/dist/mutations/index.js.map +1 -1
- package/dist/queries/index.d.ts +110 -2
- package/dist/queries/index.js +110 -1
- package/dist/queries/index.js.map +1 -1
- package/package.json +13 -12
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useContacts.test.ts +159 -0
- package/src/hooks/useContacts.ts +176 -0
- package/src/mutations/contacts.test.ts +604 -0
- package/src/mutations/contacts.ts +554 -0
- package/src/mutations/index.ts +18 -0
- package/src/queries/contacts.test.ts +505 -0
- package/src/queries/contacts.ts +237 -0
- package/src/queries/index.ts +10 -0
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contact Mutation Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for contact CRUD operations including Zod validation,
|
|
5
|
+
* soft delete, restore, and merge functionality.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
9
|
+
import type { Contact } from "@htlkg/core/types";
|
|
10
|
+
import {
|
|
11
|
+
createContact,
|
|
12
|
+
updateContact,
|
|
13
|
+
softDeleteContact,
|
|
14
|
+
restoreContact,
|
|
15
|
+
deleteContact,
|
|
16
|
+
mergeContacts,
|
|
17
|
+
ContactValidationError,
|
|
18
|
+
} from "./contacts";
|
|
19
|
+
|
|
20
|
+
// Mock the systemSettings query functions
|
|
21
|
+
vi.mock("../queries/systemSettings", () => ({
|
|
22
|
+
checkRestoreEligibility: vi.fn((deletedAt: string | null, retentionDays: number) => {
|
|
23
|
+
if (!deletedAt) {
|
|
24
|
+
return { canRestore: false, daysRemaining: 0, daysExpired: 0 };
|
|
25
|
+
}
|
|
26
|
+
const deletedDate = new Date(deletedAt);
|
|
27
|
+
const now = new Date();
|
|
28
|
+
const diffMs = now.getTime() - deletedDate.getTime();
|
|
29
|
+
const diffDays = diffMs / (1000 * 60 * 60 * 24);
|
|
30
|
+
|
|
31
|
+
if (diffDays > retentionDays) {
|
|
32
|
+
return {
|
|
33
|
+
canRestore: false,
|
|
34
|
+
daysRemaining: 0,
|
|
35
|
+
daysExpired: Math.floor(diffDays - retentionDays),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
canRestore: true,
|
|
41
|
+
daysRemaining: Math.ceil(retentionDays - diffDays),
|
|
42
|
+
daysExpired: 0,
|
|
43
|
+
};
|
|
44
|
+
}),
|
|
45
|
+
DEFAULT_SOFT_DELETE_RETENTION_DAYS: 30,
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
// Mock the contacts query functions
|
|
49
|
+
vi.mock("../queries/contacts", () => ({
|
|
50
|
+
getContact: vi.fn(),
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
import { getContact as mockGetContact } from "../queries/contacts";
|
|
54
|
+
|
|
55
|
+
describe("Contact Mutations", () => {
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
vi.clearAllMocks();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("createContact", () => {
|
|
61
|
+
const validInput = {
|
|
62
|
+
brandId: "brand-123",
|
|
63
|
+
email: "john.doe@example.com",
|
|
64
|
+
firstName: "John",
|
|
65
|
+
lastName: "Doe",
|
|
66
|
+
gdprConsent: true,
|
|
67
|
+
gdprConsentDate: "2024-01-15T10:00:00Z",
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
it("should create a contact with valid input", async () => {
|
|
71
|
+
const mockCreate = vi.fn().mockResolvedValue({
|
|
72
|
+
data: { id: "contact-001", ...validInput },
|
|
73
|
+
errors: null,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const mockClient = {
|
|
77
|
+
models: {
|
|
78
|
+
Contact: { create: mockCreate },
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const result = await createContact(mockClient, validInput);
|
|
83
|
+
|
|
84
|
+
expect(result).toBeTruthy();
|
|
85
|
+
expect(result?.id).toBe("contact-001");
|
|
86
|
+
expect(mockCreate).toHaveBeenCalledTimes(1);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should throw ContactValidationError for missing required fields", async () => {
|
|
90
|
+
const mockClient = {
|
|
91
|
+
models: {
|
|
92
|
+
Contact: { create: vi.fn() },
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const invalidInput = {
|
|
97
|
+
brandId: "brand-123",
|
|
98
|
+
email: "john.doe@example.com",
|
|
99
|
+
// Missing firstName, lastName, gdprConsent
|
|
100
|
+
} as any;
|
|
101
|
+
|
|
102
|
+
await expect(createContact(mockClient, invalidInput)).rejects.toThrow(
|
|
103
|
+
ContactValidationError
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should throw ContactValidationError for invalid email", async () => {
|
|
108
|
+
const mockClient = {
|
|
109
|
+
models: {
|
|
110
|
+
Contact: { create: vi.fn() },
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const invalidInput = {
|
|
115
|
+
...validInput,
|
|
116
|
+
email: "invalid-email",
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
await expect(createContact(mockClient, invalidInput)).rejects.toThrow(
|
|
120
|
+
ContactValidationError
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("should throw ContactValidationError for empty brandId", async () => {
|
|
125
|
+
const mockClient = {
|
|
126
|
+
models: {
|
|
127
|
+
Contact: { create: vi.fn() },
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const invalidInput = {
|
|
132
|
+
...validInput,
|
|
133
|
+
brandId: "",
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
await expect(createContact(mockClient, invalidInput)).rejects.toThrow(
|
|
137
|
+
ContactValidationError
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("should return null when GraphQL returns errors", async () => {
|
|
142
|
+
const mockCreate = vi.fn().mockResolvedValue({
|
|
143
|
+
data: null,
|
|
144
|
+
errors: [{ message: "Database error" }],
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const mockClient = {
|
|
148
|
+
models: {
|
|
149
|
+
Contact: { create: mockCreate },
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const result = await createContact(mockClient, validInput);
|
|
154
|
+
|
|
155
|
+
expect(result).toBeNull();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("should accept optional fields", async () => {
|
|
159
|
+
const mockCreate = vi.fn().mockResolvedValue({
|
|
160
|
+
data: { id: "contact-001" },
|
|
161
|
+
errors: null,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const mockClient = {
|
|
165
|
+
models: {
|
|
166
|
+
Contact: { create: mockCreate },
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const inputWithOptionals = {
|
|
171
|
+
...validInput,
|
|
172
|
+
phone: "+1234567890",
|
|
173
|
+
locale: "en-US",
|
|
174
|
+
marketingOptIn: true,
|
|
175
|
+
preferences: { theme: "dark" },
|
|
176
|
+
tags: ["vip", "returning"],
|
|
177
|
+
totalVisits: 5,
|
|
178
|
+
lastVisitDate: "2024-06-01",
|
|
179
|
+
firstVisitDate: "2023-01-15",
|
|
180
|
+
legacyId: "legacy-123",
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
await createContact(mockClient, inputWithOptionals);
|
|
184
|
+
|
|
185
|
+
expect(mockCreate).toHaveBeenCalledWith(inputWithOptionals);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe("updateContact", () => {
|
|
190
|
+
it("should update a contact with valid input", async () => {
|
|
191
|
+
const mockUpdate = vi.fn().mockResolvedValue({
|
|
192
|
+
data: { id: "contact-001", firstName: "Jane" },
|
|
193
|
+
errors: null,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const mockClient = {
|
|
197
|
+
models: {
|
|
198
|
+
Contact: { update: mockUpdate },
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const result = await updateContact(mockClient, {
|
|
203
|
+
id: "contact-001",
|
|
204
|
+
firstName: "Jane",
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
expect(result).toBeTruthy();
|
|
208
|
+
expect(mockUpdate).toHaveBeenCalledWith({ id: "contact-001", firstName: "Jane" });
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("should throw ContactValidationError for missing id", async () => {
|
|
212
|
+
const mockClient = {
|
|
213
|
+
models: {
|
|
214
|
+
Contact: { update: vi.fn() },
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
await expect(
|
|
219
|
+
updateContact(mockClient, { id: "" } as any)
|
|
220
|
+
).rejects.toThrow(ContactValidationError);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("should throw ContactValidationError for invalid email in update", async () => {
|
|
224
|
+
const mockClient = {
|
|
225
|
+
models: {
|
|
226
|
+
Contact: { update: vi.fn() },
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
await expect(
|
|
231
|
+
updateContact(mockClient, {
|
|
232
|
+
id: "contact-001",
|
|
233
|
+
email: "invalid-email",
|
|
234
|
+
})
|
|
235
|
+
).rejects.toThrow(ContactValidationError);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("should return null when GraphQL returns errors", async () => {
|
|
239
|
+
const mockUpdate = vi.fn().mockResolvedValue({
|
|
240
|
+
data: null,
|
|
241
|
+
errors: [{ message: "Update failed" }],
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const mockClient = {
|
|
245
|
+
models: {
|
|
246
|
+
Contact: { update: mockUpdate },
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const result = await updateContact(mockClient, {
|
|
251
|
+
id: "contact-001",
|
|
252
|
+
firstName: "Jane",
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
expect(result).toBeNull();
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe("softDeleteContact", () => {
|
|
260
|
+
it("should set deletedAt and deletedBy", async () => {
|
|
261
|
+
const mockUpdate = vi.fn().mockResolvedValue({
|
|
262
|
+
data: { id: "contact-001" },
|
|
263
|
+
errors: null,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const mockClient = {
|
|
267
|
+
models: {
|
|
268
|
+
Contact: { update: mockUpdate },
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const result = await softDeleteContact(
|
|
273
|
+
mockClient,
|
|
274
|
+
"contact-001",
|
|
275
|
+
"admin@example.com"
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
expect(result).toBe(true);
|
|
279
|
+
const callArgs = mockUpdate.mock.calls[0][0];
|
|
280
|
+
expect(callArgs.id).toBe("contact-001");
|
|
281
|
+
expect(callArgs.deletedAt).toBeDefined();
|
|
282
|
+
expect(callArgs.deletedBy).toBe("admin@example.com");
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("should return false on GraphQL error", async () => {
|
|
286
|
+
const mockUpdate = vi.fn().mockResolvedValue({
|
|
287
|
+
data: null,
|
|
288
|
+
errors: [{ message: "Error" }],
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const mockClient = {
|
|
292
|
+
models: {
|
|
293
|
+
Contact: { update: mockUpdate },
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const result = await softDeleteContact(
|
|
298
|
+
mockClient,
|
|
299
|
+
"contact-001",
|
|
300
|
+
"admin@example.com"
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
expect(result).toBe(false);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
describe("restoreContact", () => {
|
|
308
|
+
it("should restore a recently deleted contact", async () => {
|
|
309
|
+
const recentDeletedAt = new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(); // 5 days ago
|
|
310
|
+
|
|
311
|
+
(mockGetContact as any).mockResolvedValue({
|
|
312
|
+
id: "contact-001",
|
|
313
|
+
deletedAt: recentDeletedAt,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
const mockUpdate = vi.fn().mockResolvedValue({
|
|
317
|
+
data: { id: "contact-001" },
|
|
318
|
+
errors: null,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const mockClient = {
|
|
322
|
+
models: {
|
|
323
|
+
Contact: { get: vi.fn(), update: mockUpdate },
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const result = await restoreContact(mockClient, "contact-001");
|
|
328
|
+
|
|
329
|
+
expect(result.success).toBe(true);
|
|
330
|
+
expect(mockUpdate).toHaveBeenCalledWith({
|
|
331
|
+
id: "contact-001",
|
|
332
|
+
deletedAt: null,
|
|
333
|
+
deletedBy: null,
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("should reject restoration after retention period expires", async () => {
|
|
338
|
+
const oldDeletedAt = new Date(Date.now() - 60 * 24 * 60 * 60 * 1000).toISOString(); // 60 days ago
|
|
339
|
+
|
|
340
|
+
(mockGetContact as any).mockResolvedValue({
|
|
341
|
+
id: "contact-001",
|
|
342
|
+
deletedAt: oldDeletedAt,
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const mockClient = {
|
|
346
|
+
models: {
|
|
347
|
+
Contact: { get: vi.fn(), update: vi.fn() },
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const result = await restoreContact(mockClient, "contact-001", 30);
|
|
352
|
+
|
|
353
|
+
expect(result.success).toBe(false);
|
|
354
|
+
expect(result.error).toContain("Retention period");
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("should return error when contact not found", async () => {
|
|
358
|
+
(mockGetContact as any).mockResolvedValue(null);
|
|
359
|
+
|
|
360
|
+
const mockClient = {
|
|
361
|
+
models: {
|
|
362
|
+
Contact: { get: vi.fn(), update: vi.fn() },
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const result = await restoreContact(mockClient, "nonexistent");
|
|
367
|
+
|
|
368
|
+
expect(result.success).toBe(false);
|
|
369
|
+
expect(result.error).toBe("Contact not found");
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
describe("deleteContact", () => {
|
|
374
|
+
it("should permanently delete a contact", async () => {
|
|
375
|
+
const mockDelete = vi.fn().mockResolvedValue({
|
|
376
|
+
data: { id: "contact-001" },
|
|
377
|
+
errors: null,
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
const mockClient = {
|
|
381
|
+
models: {
|
|
382
|
+
Contact: { delete: mockDelete },
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const result = await deleteContact(mockClient, "contact-001");
|
|
387
|
+
|
|
388
|
+
expect(result).toBe(true);
|
|
389
|
+
expect(mockDelete).toHaveBeenCalledWith({ id: "contact-001" });
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("should return false on GraphQL error", async () => {
|
|
393
|
+
const mockDelete = vi.fn().mockResolvedValue({
|
|
394
|
+
data: null,
|
|
395
|
+
errors: [{ message: "Cannot delete" }],
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const mockClient = {
|
|
399
|
+
models: {
|
|
400
|
+
Contact: { delete: mockDelete },
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const result = await deleteContact(mockClient, "contact-001");
|
|
405
|
+
|
|
406
|
+
expect(result).toBe(false);
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
describe("mergeContacts", () => {
|
|
411
|
+
const primaryContact: Contact = {
|
|
412
|
+
id: "contact-primary",
|
|
413
|
+
brandId: "brand-123",
|
|
414
|
+
email: "primary@example.com",
|
|
415
|
+
firstName: "John",
|
|
416
|
+
lastName: "Doe",
|
|
417
|
+
gdprConsent: true,
|
|
418
|
+
totalVisits: 5,
|
|
419
|
+
firstVisitDate: "2023-06-01",
|
|
420
|
+
lastVisitDate: "2024-01-15",
|
|
421
|
+
tags: ["vip"],
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const duplicateContact: Contact = {
|
|
425
|
+
id: "contact-dup-1",
|
|
426
|
+
brandId: "brand-123",
|
|
427
|
+
email: "duplicate@example.com",
|
|
428
|
+
firstName: "John",
|
|
429
|
+
lastName: "Doe",
|
|
430
|
+
gdprConsent: true,
|
|
431
|
+
totalVisits: 3,
|
|
432
|
+
firstVisitDate: "2023-01-15",
|
|
433
|
+
lastVisitDate: "2024-06-01",
|
|
434
|
+
tags: ["returning"],
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
it("should merge contacts and aggregate data", async () => {
|
|
438
|
+
// Mock getContact to return contacts
|
|
439
|
+
(mockGetContact as any)
|
|
440
|
+
.mockResolvedValueOnce(primaryContact)
|
|
441
|
+
.mockResolvedValueOnce(duplicateContact);
|
|
442
|
+
|
|
443
|
+
const mockUpdate = vi.fn().mockResolvedValue({
|
|
444
|
+
data: { id: "contact-primary" },
|
|
445
|
+
errors: null,
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
const mockClient = {
|
|
449
|
+
models: {
|
|
450
|
+
Contact: { update: mockUpdate },
|
|
451
|
+
},
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
const result = await mergeContacts(
|
|
455
|
+
mockClient,
|
|
456
|
+
{
|
|
457
|
+
primaryId: "contact-primary",
|
|
458
|
+
duplicateIds: ["contact-dup-1"],
|
|
459
|
+
},
|
|
460
|
+
"admin@example.com"
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
expect(result.success).toBe(true);
|
|
464
|
+
expect(result.deletedIds).toContain("contact-dup-1");
|
|
465
|
+
|
|
466
|
+
// Verify aggregated data
|
|
467
|
+
const updateCall = mockUpdate.mock.calls[0][0];
|
|
468
|
+
expect(updateCall.totalVisits).toBe(8); // 5 + 3
|
|
469
|
+
expect(updateCall.firstVisitDate).toBe("2023-01-15"); // Earlier date
|
|
470
|
+
expect(updateCall.lastVisitDate).toBe("2024-06-01"); // Later date
|
|
471
|
+
expect(updateCall.tags).toContain("vip");
|
|
472
|
+
expect(updateCall.tags).toContain("returning");
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it("should throw ContactValidationError for missing primaryId", async () => {
|
|
476
|
+
const mockClient = {
|
|
477
|
+
models: {
|
|
478
|
+
Contact: { update: vi.fn() },
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
await expect(
|
|
483
|
+
mergeContacts(
|
|
484
|
+
mockClient,
|
|
485
|
+
{ primaryId: "", duplicateIds: ["contact-dup-1"] },
|
|
486
|
+
"admin@example.com"
|
|
487
|
+
)
|
|
488
|
+
).rejects.toThrow(ContactValidationError);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it("should throw ContactValidationError for empty duplicateIds", async () => {
|
|
492
|
+
const mockClient = {
|
|
493
|
+
models: {
|
|
494
|
+
Contact: { update: vi.fn() },
|
|
495
|
+
},
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
await expect(
|
|
499
|
+
mergeContacts(
|
|
500
|
+
mockClient,
|
|
501
|
+
{ primaryId: "contact-primary", duplicateIds: [] },
|
|
502
|
+
"admin@example.com"
|
|
503
|
+
)
|
|
504
|
+
).rejects.toThrow(ContactValidationError);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it("should return error when primary contact not found", async () => {
|
|
508
|
+
(mockGetContact as any).mockResolvedValueOnce(null);
|
|
509
|
+
|
|
510
|
+
const mockClient = {
|
|
511
|
+
models: {
|
|
512
|
+
Contact: { update: vi.fn() },
|
|
513
|
+
},
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
const result = await mergeContacts(
|
|
517
|
+
mockClient,
|
|
518
|
+
{
|
|
519
|
+
primaryId: "nonexistent",
|
|
520
|
+
duplicateIds: ["contact-dup-1"],
|
|
521
|
+
},
|
|
522
|
+
"admin@example.com"
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
expect(result.success).toBe(false);
|
|
526
|
+
expect(result.error).toContain("Primary contact not found");
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it("should return error when duplicate contact not found", async () => {
|
|
530
|
+
(mockGetContact as any)
|
|
531
|
+
.mockResolvedValueOnce(primaryContact)
|
|
532
|
+
.mockResolvedValueOnce(null);
|
|
533
|
+
|
|
534
|
+
const mockClient = {
|
|
535
|
+
models: {
|
|
536
|
+
Contact: { update: vi.fn() },
|
|
537
|
+
},
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
const result = await mergeContacts(
|
|
541
|
+
mockClient,
|
|
542
|
+
{
|
|
543
|
+
primaryId: "contact-primary",
|
|
544
|
+
duplicateIds: ["nonexistent"],
|
|
545
|
+
},
|
|
546
|
+
"admin@example.com"
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
expect(result.success).toBe(false);
|
|
550
|
+
expect(result.error).toContain("Duplicate contact not found");
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it("should handle multiple duplicates", async () => {
|
|
554
|
+
const duplicate2: Contact = {
|
|
555
|
+
id: "contact-dup-2",
|
|
556
|
+
brandId: "brand-123",
|
|
557
|
+
email: "duplicate2@example.com",
|
|
558
|
+
firstName: "John",
|
|
559
|
+
lastName: "Doe",
|
|
560
|
+
gdprConsent: true,
|
|
561
|
+
totalVisits: 2,
|
|
562
|
+
firstVisitDate: "2024-01-01",
|
|
563
|
+
lastVisitDate: "2024-07-01",
|
|
564
|
+
tags: ["newsletter"],
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
(mockGetContact as any)
|
|
568
|
+
.mockResolvedValueOnce(primaryContact)
|
|
569
|
+
.mockResolvedValueOnce(duplicateContact)
|
|
570
|
+
.mockResolvedValueOnce(duplicate2);
|
|
571
|
+
|
|
572
|
+
const mockUpdate = vi.fn().mockResolvedValue({
|
|
573
|
+
data: { id: "contact-primary" },
|
|
574
|
+
errors: null,
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
const mockClient = {
|
|
578
|
+
models: {
|
|
579
|
+
Contact: { update: mockUpdate },
|
|
580
|
+
},
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
const result = await mergeContacts(
|
|
584
|
+
mockClient,
|
|
585
|
+
{
|
|
586
|
+
primaryId: "contact-primary",
|
|
587
|
+
duplicateIds: ["contact-dup-1", "contact-dup-2"],
|
|
588
|
+
},
|
|
589
|
+
"admin@example.com"
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
expect(result.success).toBe(true);
|
|
593
|
+
|
|
594
|
+
// Verify aggregated data from all contacts
|
|
595
|
+
const updateCall = mockUpdate.mock.calls[0][0];
|
|
596
|
+
expect(updateCall.totalVisits).toBe(10); // 5 + 3 + 2
|
|
597
|
+
expect(updateCall.firstVisitDate).toBe("2023-01-15"); // Earliest
|
|
598
|
+
expect(updateCall.lastVisitDate).toBe("2024-07-01"); // Latest
|
|
599
|
+
expect(updateCall.tags).toContain("vip");
|
|
600
|
+
expect(updateCall.tags).toContain("returning");
|
|
601
|
+
expect(updateCall.tags).toContain("newsletter");
|
|
602
|
+
});
|
|
603
|
+
});
|
|
604
|
+
});
|