@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,554 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contact Mutation Functions
|
|
3
|
+
*
|
|
4
|
+
* Provides type-safe mutation functions for creating, updating, and deleting contacts.
|
|
5
|
+
* Includes Zod validation for input data before mutations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import type { Contact } from "@htlkg/core/types";
|
|
10
|
+
import {
|
|
11
|
+
checkRestoreEligibility,
|
|
12
|
+
DEFAULT_SOFT_DELETE_RETENTION_DAYS,
|
|
13
|
+
} from "../queries/systemSettings";
|
|
14
|
+
import { getContact } from "../queries/contacts";
|
|
15
|
+
import type { CreateAuditFields, UpdateWithSoftDeleteFields } from "./common";
|
|
16
|
+
|
|
17
|
+
// ============================================
|
|
18
|
+
// Added Zod Validation Schemas
|
|
19
|
+
// ============================================
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Zod schema for creating a contact
|
|
23
|
+
*/
|
|
24
|
+
export const createContactSchema = z.object({
|
|
25
|
+
brandId: z.string().min(1, "Brand ID is required"),
|
|
26
|
+
email: z.string().email("Invalid email address"),
|
|
27
|
+
phone: z.string().optional(),
|
|
28
|
+
firstName: z.string().min(1, "First name is required"),
|
|
29
|
+
lastName: z.string().min(1, "Last name is required"),
|
|
30
|
+
locale: z.string().optional(),
|
|
31
|
+
gdprConsent: z.boolean(),
|
|
32
|
+
gdprConsentDate: z.string().optional(),
|
|
33
|
+
marketingOptIn: z.boolean().optional(),
|
|
34
|
+
preferences: z.record(z.any()).optional(),
|
|
35
|
+
tags: z.array(z.string()).optional(),
|
|
36
|
+
totalVisits: z.number().int().min(0).optional(),
|
|
37
|
+
lastVisitDate: z.string().optional(),
|
|
38
|
+
firstVisitDate: z.string().optional(),
|
|
39
|
+
legacyId: z.string().optional(),
|
|
40
|
+
// Audit fields
|
|
41
|
+
createdAt: z.string().optional(),
|
|
42
|
+
createdBy: z.string().optional(),
|
|
43
|
+
updatedAt: z.string().optional(),
|
|
44
|
+
updatedBy: z.string().optional(),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Zod schema for updating a contact
|
|
49
|
+
*/
|
|
50
|
+
export const updateContactSchema = z.object({
|
|
51
|
+
id: z.string().min(1, "Contact ID is required"),
|
|
52
|
+
brandId: z.string().min(1).optional(),
|
|
53
|
+
email: z.string().email("Invalid email address").optional(),
|
|
54
|
+
phone: z.string().optional(),
|
|
55
|
+
firstName: z.string().min(1).optional(),
|
|
56
|
+
lastName: z.string().min(1).optional(),
|
|
57
|
+
locale: z.string().optional(),
|
|
58
|
+
gdprConsent: z.boolean().optional(),
|
|
59
|
+
gdprConsentDate: z.string().optional(),
|
|
60
|
+
marketingOptIn: z.boolean().optional(),
|
|
61
|
+
preferences: z.record(z.any()).optional(),
|
|
62
|
+
tags: z.array(z.string()).optional(),
|
|
63
|
+
totalVisits: z.number().int().min(0).optional(),
|
|
64
|
+
lastVisitDate: z.string().optional(),
|
|
65
|
+
firstVisitDate: z.string().optional(),
|
|
66
|
+
legacyId: z.string().optional(),
|
|
67
|
+
// Audit fields
|
|
68
|
+
updatedAt: z.string().optional(),
|
|
69
|
+
updatedBy: z.string().optional(),
|
|
70
|
+
deletedAt: z.string().nullable().optional(),
|
|
71
|
+
deletedBy: z.string().nullable().optional(),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Zod schema for merging contacts
|
|
76
|
+
*/
|
|
77
|
+
export const mergeContactsSchema = z.object({
|
|
78
|
+
primaryId: z.string().min(1, "Primary contact ID is required"),
|
|
79
|
+
duplicateIds: z.array(z.string().min(1)).min(1, "At least one duplicate ID is required"),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ============================================
|
|
83
|
+
// Validation Error Class
|
|
84
|
+
// ============================================
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Validation error class for contact operations
|
|
88
|
+
*/
|
|
89
|
+
export class ContactValidationError extends Error {
|
|
90
|
+
public readonly issues: z.ZodIssue[];
|
|
91
|
+
|
|
92
|
+
constructor(message: string, issues: z.ZodIssue[] = []) {
|
|
93
|
+
super(message);
|
|
94
|
+
this.name = "ContactValidationError";
|
|
95
|
+
this.issues = issues;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ============================================
|
|
100
|
+
// Input Types
|
|
101
|
+
// ============================================
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Input type for creating a contact
|
|
105
|
+
*/
|
|
106
|
+
export interface CreateContactInput extends CreateAuditFields {
|
|
107
|
+
brandId: string;
|
|
108
|
+
email: string;
|
|
109
|
+
phone?: string;
|
|
110
|
+
firstName: string;
|
|
111
|
+
lastName: string;
|
|
112
|
+
locale?: string;
|
|
113
|
+
gdprConsent: boolean;
|
|
114
|
+
gdprConsentDate?: string;
|
|
115
|
+
marketingOptIn?: boolean;
|
|
116
|
+
preferences?: Record<string, any>;
|
|
117
|
+
tags?: string[];
|
|
118
|
+
totalVisits?: number;
|
|
119
|
+
lastVisitDate?: string;
|
|
120
|
+
firstVisitDate?: string;
|
|
121
|
+
legacyId?: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Input type for updating a contact
|
|
126
|
+
*/
|
|
127
|
+
export interface UpdateContactInput extends UpdateWithSoftDeleteFields {
|
|
128
|
+
id: string;
|
|
129
|
+
brandId?: string;
|
|
130
|
+
email?: string;
|
|
131
|
+
phone?: string;
|
|
132
|
+
firstName?: string;
|
|
133
|
+
lastName?: string;
|
|
134
|
+
locale?: string;
|
|
135
|
+
gdprConsent?: boolean;
|
|
136
|
+
gdprConsentDate?: string;
|
|
137
|
+
marketingOptIn?: boolean;
|
|
138
|
+
preferences?: Record<string, any>;
|
|
139
|
+
tags?: string[];
|
|
140
|
+
totalVisits?: number;
|
|
141
|
+
lastVisitDate?: string;
|
|
142
|
+
firstVisitDate?: string;
|
|
143
|
+
legacyId?: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Input type for merging contacts
|
|
148
|
+
*/
|
|
149
|
+
export interface MergeContactsInput {
|
|
150
|
+
primaryId: string;
|
|
151
|
+
duplicateIds: string[];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Result type for merge operation
|
|
156
|
+
*/
|
|
157
|
+
export interface MergeContactsResult {
|
|
158
|
+
success: boolean;
|
|
159
|
+
mergedContact?: Contact;
|
|
160
|
+
deletedIds?: string[];
|
|
161
|
+
error?: string;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ============================================
|
|
165
|
+
// Mutation Functions
|
|
166
|
+
// ============================================
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Create a new contact with Zod validation
|
|
170
|
+
*
|
|
171
|
+
* @throws {ContactValidationError} if input validation fails
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* ```typescript
|
|
175
|
+
* import { createContact } from '@htlkg/data/mutations';
|
|
176
|
+
* import { generateClient } from '@htlkg/data/client';
|
|
177
|
+
*
|
|
178
|
+
* const client = generateClient<Schema>();
|
|
179
|
+
* const contact = await createContact(client, {
|
|
180
|
+
* brandId: 'brand-123',
|
|
181
|
+
* email: 'guest@example.com',
|
|
182
|
+
* firstName: 'John',
|
|
183
|
+
* lastName: 'Doe',
|
|
184
|
+
* gdprConsent: true,
|
|
185
|
+
* gdprConsentDate: new Date().toISOString()
|
|
186
|
+
* });
|
|
187
|
+
* ```
|
|
188
|
+
*/
|
|
189
|
+
export async function createContact<TClient = any>(
|
|
190
|
+
client: TClient,
|
|
191
|
+
input: CreateContactInput,
|
|
192
|
+
): Promise<Contact | null> {
|
|
193
|
+
try {
|
|
194
|
+
// Validate input with Zod
|
|
195
|
+
const validationResult = createContactSchema.safeParse(input);
|
|
196
|
+
|
|
197
|
+
if (!validationResult.success) {
|
|
198
|
+
const errorMessage = validationResult.error.issues
|
|
199
|
+
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
|
200
|
+
.join(", ");
|
|
201
|
+
throw new ContactValidationError(
|
|
202
|
+
`Validation failed: ${errorMessage}`,
|
|
203
|
+
validationResult.error.issues
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const { data, errors } = await (client as any).models.Contact.create(input);
|
|
208
|
+
|
|
209
|
+
if (errors) {
|
|
210
|
+
console.error("[createContact] GraphQL errors:", errors);
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return data as Contact;
|
|
215
|
+
} catch (error) {
|
|
216
|
+
if (error instanceof ContactValidationError) {
|
|
217
|
+
console.error("[createContact] Validation error:", error.message);
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
console.error("[createContact] Error creating contact:", error);
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Update an existing contact with Zod validation
|
|
227
|
+
*
|
|
228
|
+
* @throws {ContactValidationError} if input validation fails
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* ```typescript
|
|
232
|
+
* import { updateContact } from '@htlkg/data/mutations';
|
|
233
|
+
* import { generateClient } from '@htlkg/data/client';
|
|
234
|
+
*
|
|
235
|
+
* const client = generateClient<Schema>();
|
|
236
|
+
* const contact = await updateContact(client, {
|
|
237
|
+
* id: 'contact-123',
|
|
238
|
+
* firstName: 'Jane',
|
|
239
|
+
* marketingOptIn: true
|
|
240
|
+
* });
|
|
241
|
+
* ```
|
|
242
|
+
*/
|
|
243
|
+
export async function updateContact<TClient = any>(
|
|
244
|
+
client: TClient,
|
|
245
|
+
input: UpdateContactInput,
|
|
246
|
+
): Promise<Contact | null> {
|
|
247
|
+
try {
|
|
248
|
+
// Validate input with Zod
|
|
249
|
+
const validationResult = updateContactSchema.safeParse(input);
|
|
250
|
+
|
|
251
|
+
if (!validationResult.success) {
|
|
252
|
+
const errorMessage = validationResult.error.issues
|
|
253
|
+
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
|
254
|
+
.join(", ");
|
|
255
|
+
throw new ContactValidationError(
|
|
256
|
+
`Validation failed: ${errorMessage}`,
|
|
257
|
+
validationResult.error.issues
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const { data, errors } = await (client as any).models.Contact.update(input);
|
|
262
|
+
|
|
263
|
+
if (errors) {
|
|
264
|
+
console.error("[updateContact] GraphQL errors:", errors);
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return data as Contact;
|
|
269
|
+
} catch (error) {
|
|
270
|
+
if (error instanceof ContactValidationError) {
|
|
271
|
+
console.error("[updateContact] Validation error:", error.message);
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
console.error("[updateContact] Error updating contact:", error);
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Soft delete a contact (sets deletedAt/deletedBy instead of removing)
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* ```typescript
|
|
284
|
+
* import { softDeleteContact } from '@htlkg/data/mutations';
|
|
285
|
+
* import { generateClient } from '@htlkg/data/client';
|
|
286
|
+
*
|
|
287
|
+
* const client = generateClient<Schema>();
|
|
288
|
+
* await softDeleteContact(client, 'contact-123', 'admin@example.com');
|
|
289
|
+
* ```
|
|
290
|
+
*/
|
|
291
|
+
export async function softDeleteContact<TClient = any>(
|
|
292
|
+
client: TClient,
|
|
293
|
+
id: string,
|
|
294
|
+
deletedBy: string,
|
|
295
|
+
): Promise<boolean> {
|
|
296
|
+
try {
|
|
297
|
+
const { errors } = await (client as any).models.Contact.update({
|
|
298
|
+
id,
|
|
299
|
+
deletedAt: new Date().toISOString(),
|
|
300
|
+
deletedBy,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
if (errors) {
|
|
304
|
+
console.error("[softDeleteContact] GraphQL errors:", errors);
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return true;
|
|
309
|
+
} catch (error) {
|
|
310
|
+
console.error("[softDeleteContact] Error soft-deleting contact:", error);
|
|
311
|
+
throw error;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Restore a soft-deleted contact
|
|
317
|
+
* Checks retention period before allowing restoration
|
|
318
|
+
*
|
|
319
|
+
* @example
|
|
320
|
+
* ```typescript
|
|
321
|
+
* import { restoreContact } from '@htlkg/data/mutations';
|
|
322
|
+
* import { generateClient } from '@htlkg/data/client';
|
|
323
|
+
*
|
|
324
|
+
* const client = generateClient<Schema>();
|
|
325
|
+
* await restoreContact(client, 'contact-123');
|
|
326
|
+
* // Or with custom retention days:
|
|
327
|
+
* await restoreContact(client, 'contact-123', 60);
|
|
328
|
+
* ```
|
|
329
|
+
*/
|
|
330
|
+
export async function restoreContact<TClient = any>(
|
|
331
|
+
client: TClient,
|
|
332
|
+
id: string,
|
|
333
|
+
retentionDays: number = DEFAULT_SOFT_DELETE_RETENTION_DAYS,
|
|
334
|
+
): Promise<{ success: boolean; error?: string }> {
|
|
335
|
+
try {
|
|
336
|
+
// First, get the contact to check its deletedAt timestamp
|
|
337
|
+
const contact = await getContact(client, id);
|
|
338
|
+
|
|
339
|
+
if (!contact) {
|
|
340
|
+
console.error("[restoreContact] Contact not found");
|
|
341
|
+
return { success: false, error: "Contact not found" };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Check if restoration is allowed based on retention period
|
|
345
|
+
const eligibility = checkRestoreEligibility(
|
|
346
|
+
(contact as any).deletedAt,
|
|
347
|
+
retentionDays
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
if (!eligibility.canRestore) {
|
|
351
|
+
const errorMsg = `Cannot restore contact. Retention period of ${retentionDays} days has expired. Item was deleted ${eligibility.daysExpired} days ago.`;
|
|
352
|
+
console.error("[restoreContact]", errorMsg);
|
|
353
|
+
return { success: false, error: errorMsg };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const { errors } = await (client as any).models.Contact.update({
|
|
357
|
+
id,
|
|
358
|
+
deletedAt: null,
|
|
359
|
+
deletedBy: null,
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
if (errors) {
|
|
363
|
+
console.error("[restoreContact] GraphQL errors:", errors);
|
|
364
|
+
return { success: false, error: "Failed to restore contact" };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return { success: true };
|
|
368
|
+
} catch (error) {
|
|
369
|
+
console.error("[restoreContact] Error restoring contact:", error);
|
|
370
|
+
throw error;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Hard delete a contact (permanently removes from database)
|
|
376
|
+
* Use with caution - prefer softDeleteContact for recoverable deletion
|
|
377
|
+
*
|
|
378
|
+
* @example
|
|
379
|
+
* ```typescript
|
|
380
|
+
* import { deleteContact } from '@htlkg/data/mutations';
|
|
381
|
+
* import { generateClient } from '@htlkg/data/client';
|
|
382
|
+
*
|
|
383
|
+
* const client = generateClient<Schema>();
|
|
384
|
+
* await deleteContact(client, 'contact-123');
|
|
385
|
+
* ```
|
|
386
|
+
*/
|
|
387
|
+
export async function deleteContact<TClient = any>(
|
|
388
|
+
client: TClient,
|
|
389
|
+
id: string,
|
|
390
|
+
): Promise<boolean> {
|
|
391
|
+
try {
|
|
392
|
+
const { errors } = await (client as any).models.Contact.delete({ id });
|
|
393
|
+
|
|
394
|
+
if (errors) {
|
|
395
|
+
console.error("[deleteContact] GraphQL errors:", errors);
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return true;
|
|
400
|
+
} catch (error) {
|
|
401
|
+
console.error("[deleteContact] Error deleting contact:", error);
|
|
402
|
+
throw error;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Merge duplicate contacts into a primary contact
|
|
408
|
+
*
|
|
409
|
+
* This function:
|
|
410
|
+
* 1. Validates all contact IDs exist
|
|
411
|
+
* 2. Aggregates data from duplicates into the primary contact
|
|
412
|
+
* 3. Soft deletes the duplicate contacts
|
|
413
|
+
* 4. Returns the updated primary contact
|
|
414
|
+
*
|
|
415
|
+
* Merge strategy:
|
|
416
|
+
* - totalVisits: Sum of all contacts
|
|
417
|
+
* - firstVisitDate: Earliest date across all contacts
|
|
418
|
+
* - lastVisitDate: Latest date across all contacts
|
|
419
|
+
* - tags: Merged unique tags from all contacts
|
|
420
|
+
* - preferences: Primary contact preferences take precedence
|
|
421
|
+
* - Other fields: Primary contact values are preserved
|
|
422
|
+
*
|
|
423
|
+
* @throws {ContactValidationError} if input validation fails
|
|
424
|
+
*
|
|
425
|
+
* @example
|
|
426
|
+
* ```typescript
|
|
427
|
+
* import { mergeContacts } from '@htlkg/data/mutations';
|
|
428
|
+
* import { generateClient } from '@htlkg/data/client';
|
|
429
|
+
*
|
|
430
|
+
* const client = generateClient<Schema>();
|
|
431
|
+
* const result = await mergeContacts(client, {
|
|
432
|
+
* primaryId: 'contact-primary',
|
|
433
|
+
* duplicateIds: ['contact-dup-1', 'contact-dup-2']
|
|
434
|
+
* }, 'admin@example.com');
|
|
435
|
+
* ```
|
|
436
|
+
*/
|
|
437
|
+
export async function mergeContacts<TClient = any>(
|
|
438
|
+
client: TClient,
|
|
439
|
+
input: MergeContactsInput,
|
|
440
|
+
mergedBy: string,
|
|
441
|
+
): Promise<MergeContactsResult> {
|
|
442
|
+
try {
|
|
443
|
+
// Validate input with Zod
|
|
444
|
+
const validationResult = mergeContactsSchema.safeParse(input);
|
|
445
|
+
|
|
446
|
+
if (!validationResult.success) {
|
|
447
|
+
const errorMessage = validationResult.error.issues
|
|
448
|
+
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
|
449
|
+
.join(", ");
|
|
450
|
+
throw new ContactValidationError(
|
|
451
|
+
`Validation failed: ${errorMessage}`,
|
|
452
|
+
validationResult.error.issues
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const { primaryId, duplicateIds } = input;
|
|
457
|
+
|
|
458
|
+
// Fetch primary contact
|
|
459
|
+
const primaryContact = await getContact(client, primaryId);
|
|
460
|
+
if (!primaryContact) {
|
|
461
|
+
return {
|
|
462
|
+
success: false,
|
|
463
|
+
error: `Primary contact not found: ${primaryId}`,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Fetch all duplicate contacts
|
|
468
|
+
const duplicateContacts: Contact[] = [];
|
|
469
|
+
for (const duplicateId of duplicateIds) {
|
|
470
|
+
const duplicate = await getContact(client, duplicateId);
|
|
471
|
+
if (!duplicate) {
|
|
472
|
+
return {
|
|
473
|
+
success: false,
|
|
474
|
+
error: `Duplicate contact not found: ${duplicateId}`,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
duplicateContacts.push(duplicate);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Aggregate data from duplicates
|
|
481
|
+
let totalVisits = primaryContact.totalVisits || 0;
|
|
482
|
+
let firstVisitDate = primaryContact.firstVisitDate;
|
|
483
|
+
let lastVisitDate = primaryContact.lastVisitDate;
|
|
484
|
+
const allTags = new Set<string>(primaryContact.tags || []);
|
|
485
|
+
|
|
486
|
+
for (const duplicate of duplicateContacts) {
|
|
487
|
+
// Sum total visits
|
|
488
|
+
totalVisits += duplicate.totalVisits || 0;
|
|
489
|
+
|
|
490
|
+
// Find earliest first visit date
|
|
491
|
+
if (duplicate.firstVisitDate) {
|
|
492
|
+
if (!firstVisitDate || duplicate.firstVisitDate < firstVisitDate) {
|
|
493
|
+
firstVisitDate = duplicate.firstVisitDate;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Find latest last visit date
|
|
498
|
+
if (duplicate.lastVisitDate) {
|
|
499
|
+
if (!lastVisitDate || duplicate.lastVisitDate > lastVisitDate) {
|
|
500
|
+
lastVisitDate = duplicate.lastVisitDate;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Merge tags
|
|
505
|
+
if (duplicate.tags) {
|
|
506
|
+
duplicate.tags.forEach((tag) => allTags.add(tag));
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Update primary contact with aggregated data
|
|
511
|
+
const updateInput: UpdateContactInput = {
|
|
512
|
+
id: primaryId,
|
|
513
|
+
totalVisits,
|
|
514
|
+
firstVisitDate,
|
|
515
|
+
lastVisitDate,
|
|
516
|
+
tags: Array.from(allTags),
|
|
517
|
+
updatedAt: new Date().toISOString(),
|
|
518
|
+
updatedBy: mergedBy,
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
const updatedPrimary = await updateContact(client, updateInput);
|
|
522
|
+
|
|
523
|
+
if (!updatedPrimary) {
|
|
524
|
+
return {
|
|
525
|
+
success: false,
|
|
526
|
+
error: "Failed to update primary contact with merged data",
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Soft delete all duplicate contacts
|
|
531
|
+
const deletedIds: string[] = [];
|
|
532
|
+
for (const duplicateId of duplicateIds) {
|
|
533
|
+
const deleted = await softDeleteContact(client, duplicateId, mergedBy);
|
|
534
|
+
if (deleted) {
|
|
535
|
+
deletedIds.push(duplicateId);
|
|
536
|
+
} else {
|
|
537
|
+
console.error(`[mergeContacts] Failed to soft-delete duplicate: ${duplicateId}`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return {
|
|
542
|
+
success: true,
|
|
543
|
+
mergedContact: updatedPrimary,
|
|
544
|
+
deletedIds,
|
|
545
|
+
};
|
|
546
|
+
} catch (error) {
|
|
547
|
+
if (error instanceof ContactValidationError) {
|
|
548
|
+
console.error("[mergeContacts] Validation error:", error.message);
|
|
549
|
+
throw error;
|
|
550
|
+
}
|
|
551
|
+
console.error("[mergeContacts] Error merging contacts:", error);
|
|
552
|
+
throw error;
|
|
553
|
+
}
|
|
554
|
+
}
|
package/src/mutations/index.ts
CHANGED
|
@@ -75,3 +75,21 @@ export {
|
|
|
75
75
|
type UpdateReservationInput,
|
|
76
76
|
type ReservationStatus,
|
|
77
77
|
} from "./reservations";
|
|
78
|
+
|
|
79
|
+
// Contact mutations
|
|
80
|
+
export {
|
|
81
|
+
createContact,
|
|
82
|
+
updateContact,
|
|
83
|
+
deleteContact,
|
|
84
|
+
softDeleteContact,
|
|
85
|
+
restoreContact,
|
|
86
|
+
mergeContacts,
|
|
87
|
+
ContactValidationError,
|
|
88
|
+
createContactSchema,
|
|
89
|
+
updateContactSchema,
|
|
90
|
+
mergeContactsSchema,
|
|
91
|
+
type CreateContactInput,
|
|
92
|
+
type UpdateContactInput,
|
|
93
|
+
type MergeContactsInput,
|
|
94
|
+
type MergeContactsResult,
|
|
95
|
+
} from "./contacts";
|