@tellescope/sdk 1.236.1 → 1.236.2
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/lib/cjs/enduser.d.ts +20 -0
- package/lib/cjs/enduser.d.ts.map +1 -1
- package/lib/cjs/sdk.d.ts +42 -0
- package/lib/cjs/sdk.d.ts.map +1 -1
- package/lib/cjs/sdk.js +1 -0
- package/lib/cjs/sdk.js.map +1 -1
- package/lib/cjs/tests/api_tests/auto_merge_form_submission.test.d.ts +9 -0
- package/lib/cjs/tests/api_tests/auto_merge_form_submission.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/auto_merge_form_submission.test.js +1399 -0
- package/lib/cjs/tests/api_tests/auto_merge_form_submission.test.js.map +1 -0
- package/lib/cjs/tests/tests.d.ts.map +1 -1
- package/lib/cjs/tests/tests.js +111 -106
- package/lib/cjs/tests/tests.js.map +1 -1
- package/lib/esm/enduser.d.ts +20 -0
- package/lib/esm/enduser.d.ts.map +1 -1
- package/lib/esm/sdk.d.ts +42 -0
- package/lib/esm/sdk.d.ts.map +1 -1
- package/lib/esm/sdk.js +1 -0
- package/lib/esm/sdk.js.map +1 -1
- package/lib/esm/tests/api_tests/auto_merge_form_submission.test.d.ts +9 -0
- package/lib/esm/tests/api_tests/auto_merge_form_submission.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/auto_merge_form_submission.test.js +1372 -0
- package/lib/esm/tests/api_tests/auto_merge_form_submission.test.js.map +1 -0
- package/lib/esm/tests/tests.d.ts.map +1 -1
- package/lib/esm/tests/tests.js +111 -106
- package/lib/esm/tests/tests.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/src/sdk.ts +2 -1
- package/src/tests/api_tests/auto_merge_form_submission.test.ts +876 -0
- package/src/tests/tests.ts +4 -1
- package/test_generated.pdf +0 -0
|
@@ -0,0 +1,876 @@
|
|
|
1
|
+
require('source-map-support').install();
|
|
2
|
+
|
|
3
|
+
import * as buffer from 'buffer'
|
|
4
|
+
import { Session, EnduserSession } from "../../sdk"
|
|
5
|
+
import {
|
|
6
|
+
async_test,
|
|
7
|
+
log_header,
|
|
8
|
+
} from "@tellescope/testing"
|
|
9
|
+
import { setup_tests } from "../setup"
|
|
10
|
+
import { Form, FormField } from "@tellescope/types-client"
|
|
11
|
+
|
|
12
|
+
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Helper: Create a form with auto-merge enabled and intake fields
|
|
16
|
+
*/
|
|
17
|
+
const createAutoMergeForm = async (sdk: Session, autoMergeOnSubmission = true) => {
|
|
18
|
+
const form = await sdk.api.forms.createOne({
|
|
19
|
+
title: 'Auto Merge Test Form',
|
|
20
|
+
allowPublicURL: true,
|
|
21
|
+
autoMergeOnSubmission,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
// Add intake fields - must create sequentially due to previousFields dependencies
|
|
25
|
+
const fnameField = await sdk.api.form_fields.createOne({
|
|
26
|
+
formId: form.id,
|
|
27
|
+
title: 'First Name',
|
|
28
|
+
type: 'string',
|
|
29
|
+
intakeField: 'fname',
|
|
30
|
+
previousFields: [{ type: 'root', info: {} }]
|
|
31
|
+
})
|
|
32
|
+
const lnameField = await sdk.api.form_fields.createOne({
|
|
33
|
+
formId: form.id,
|
|
34
|
+
title: 'Last Name',
|
|
35
|
+
type: 'string',
|
|
36
|
+
intakeField: 'lname',
|
|
37
|
+
previousFields: [{ type: 'after', info: { fieldId: fnameField.id } }]
|
|
38
|
+
})
|
|
39
|
+
const emailField = await sdk.api.form_fields.createOne({
|
|
40
|
+
formId: form.id,
|
|
41
|
+
title: 'Email',
|
|
42
|
+
type: 'email',
|
|
43
|
+
intakeField: 'email',
|
|
44
|
+
previousFields: [{ type: 'after', info: { fieldId: lnameField.id } }]
|
|
45
|
+
})
|
|
46
|
+
const phoneField = await sdk.api.form_fields.createOne({
|
|
47
|
+
formId: form.id,
|
|
48
|
+
title: 'Phone',
|
|
49
|
+
type: 'phone',
|
|
50
|
+
intakeField: 'phone',
|
|
51
|
+
previousFields: [{ type: 'after', info: { fieldId: emailField.id } }]
|
|
52
|
+
})
|
|
53
|
+
const dobField = await sdk.api.form_fields.createOne({
|
|
54
|
+
formId: form.id,
|
|
55
|
+
title: 'Date of Birth',
|
|
56
|
+
type: 'dateString',
|
|
57
|
+
intakeField: 'dateOfBirth',
|
|
58
|
+
previousFields: [{ type: 'after', info: { fieldId: phoneField.id } }]
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const fields = [fnameField, lnameField, emailField, phoneField, dobField]
|
|
62
|
+
return { form, fields }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Helper: Submit a public form with skipMatch and get the created enduser ID
|
|
67
|
+
*/
|
|
68
|
+
const submitPublicFormWithSkipMatch = async (
|
|
69
|
+
form: Form,
|
|
70
|
+
fields: FormField[],
|
|
71
|
+
values: { fname?: string, lname?: string, email?: string, phone?: string, dateOfBirth?: string }
|
|
72
|
+
) => {
|
|
73
|
+
const enduserSDK = new EnduserSession({ host, businessId: form.businessId })
|
|
74
|
+
|
|
75
|
+
const { authToken, accessCode, enduserId } = await enduserSDK.api.form_responses.session_for_public_form({
|
|
76
|
+
formId: form.id,
|
|
77
|
+
businessId: form.businessId,
|
|
78
|
+
skipMatch: true,
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const authedSDK = new EnduserSession({ host, businessId: form.businessId, authToken })
|
|
82
|
+
|
|
83
|
+
const responses = []
|
|
84
|
+
const fnameField = fields.find(f => f.intakeField === 'fname')
|
|
85
|
+
const lnameField = fields.find(f => f.intakeField === 'lname')
|
|
86
|
+
const emailField = fields.find(f => f.intakeField === 'email')
|
|
87
|
+
const phoneField = fields.find(f => f.intakeField === 'phone')
|
|
88
|
+
const dobField = fields.find(f => f.intakeField === 'dateOfBirth')
|
|
89
|
+
|
|
90
|
+
if (values.fname && fnameField) {
|
|
91
|
+
responses.push({ fieldId: fnameField.id, fieldTitle: fnameField.title, answer: { type: 'string' as const, value: values.fname } })
|
|
92
|
+
}
|
|
93
|
+
if (values.lname && lnameField) {
|
|
94
|
+
responses.push({ fieldId: lnameField.id, fieldTitle: lnameField.title, answer: { type: 'string' as const, value: values.lname } })
|
|
95
|
+
}
|
|
96
|
+
if (values.email && emailField) {
|
|
97
|
+
responses.push({ fieldId: emailField.id, fieldTitle: emailField.title, answer: { type: 'email' as const, value: values.email } })
|
|
98
|
+
}
|
|
99
|
+
if (values.phone && phoneField) {
|
|
100
|
+
responses.push({ fieldId: phoneField.id, fieldTitle: phoneField.title, answer: { type: 'phone' as const, value: values.phone } })
|
|
101
|
+
}
|
|
102
|
+
if (values.dateOfBirth && dobField) {
|
|
103
|
+
responses.push({ fieldId: dobField.id, fieldTitle: dobField.title, answer: { type: 'dateString' as const, value: values.dateOfBirth } })
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
await authedSDK.api.form_responses.submit_form_response({
|
|
107
|
+
accessCode,
|
|
108
|
+
responses,
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
return { enduserId, accessCode, authedSDK }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Helper: Check if enduser has been deleted (immediate check, no polling)
|
|
116
|
+
* Since auto-merge is now synchronous, we don't need to poll
|
|
117
|
+
*/
|
|
118
|
+
const isEnduserDeleted = async (sdk: Session, enduserId: string): Promise<boolean> => {
|
|
119
|
+
try {
|
|
120
|
+
await sdk.api.endusers.getOne(enduserId)
|
|
121
|
+
return false // Still exists
|
|
122
|
+
} catch {
|
|
123
|
+
return true // Deleted
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Main test function that can be called independently or as part of the test suite
|
|
129
|
+
*/
|
|
130
|
+
export const auto_merge_form_submission_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sdkNonAdmin: Session }) => {
|
|
131
|
+
log_header("Auto-Merge Form Submission Tests")
|
|
132
|
+
|
|
133
|
+
// Test 1: Happy Path - Merge by Email Match
|
|
134
|
+
await async_test(
|
|
135
|
+
"Auto-merge: Merge occurs when matching by email",
|
|
136
|
+
async () => {
|
|
137
|
+
const testEmail = `automerge.email.${Date.now()}@test.com`
|
|
138
|
+
const destination = await sdk.api.endusers.createOne({
|
|
139
|
+
fname: 'John',
|
|
140
|
+
lname: 'Doe',
|
|
141
|
+
email: testEmail
|
|
142
|
+
})
|
|
143
|
+
const { form, fields } = await createAutoMergeForm(sdk, true)
|
|
144
|
+
|
|
145
|
+
const { enduserId: sourceId } = await submitPublicFormWithSkipMatch(form, fields, {
|
|
146
|
+
fname: 'John',
|
|
147
|
+
lname: 'Doe',
|
|
148
|
+
email: testEmail,
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
// Merge is synchronous - source should be deleted immediately after submission
|
|
152
|
+
const sourceDeleted = await isEnduserDeleted(sdk, sourceId)
|
|
153
|
+
const updatedDestination = await sdk.api.endusers.getOne(destination.id)
|
|
154
|
+
const formResponses = await sdk.api.form_responses.getSome({ filter: { enduserId: destination.id } })
|
|
155
|
+
|
|
156
|
+
// Cleanup
|
|
157
|
+
await sdk.api.forms.deleteOne(form.id)
|
|
158
|
+
await sdk.api.endusers.deleteOne(destination.id)
|
|
159
|
+
|
|
160
|
+
return sourceDeleted
|
|
161
|
+
&& updatedDestination.mergedIds?.includes(sourceId)
|
|
162
|
+
&& formResponses.length === 1
|
|
163
|
+
},
|
|
164
|
+
{ expectedResult: true }
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
// Test 2: Happy Path - Merge by Phone Match
|
|
168
|
+
await async_test(
|
|
169
|
+
"Auto-merge: Merge occurs when matching by phone",
|
|
170
|
+
async () => {
|
|
171
|
+
const testPhone = '+15555551234'
|
|
172
|
+
const destination = await sdk.api.endusers.createOne({
|
|
173
|
+
fname: 'Jane',
|
|
174
|
+
lname: 'Smith',
|
|
175
|
+
phone: testPhone
|
|
176
|
+
})
|
|
177
|
+
const { form, fields } = await createAutoMergeForm(sdk, true)
|
|
178
|
+
|
|
179
|
+
const { enduserId: sourceId } = await submitPublicFormWithSkipMatch(form, fields, {
|
|
180
|
+
fname: 'Jane',
|
|
181
|
+
lname: 'Smith',
|
|
182
|
+
phone: testPhone,
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
// Merge is synchronous - source should be deleted immediately after submission
|
|
186
|
+
const sourceDeleted = await isEnduserDeleted(sdk, sourceId)
|
|
187
|
+
const updatedDestination = await sdk.api.endusers.getOne(destination.id)
|
|
188
|
+
|
|
189
|
+
// Cleanup
|
|
190
|
+
await sdk.api.forms.deleteOne(form.id)
|
|
191
|
+
await sdk.api.endusers.deleteOne(destination.id)
|
|
192
|
+
|
|
193
|
+
return sourceDeleted && updatedDestination.mergedIds?.includes(sourceId)
|
|
194
|
+
},
|
|
195
|
+
{ expectedResult: true }
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
// Test 3: Happy Path - Merge by DateOfBirth Match
|
|
199
|
+
await async_test(
|
|
200
|
+
"Auto-merge: Merge occurs when matching by dateOfBirth",
|
|
201
|
+
async () => {
|
|
202
|
+
const testDOB = '1990-05-15'
|
|
203
|
+
const destination = await sdk.api.endusers.createOne({
|
|
204
|
+
fname: 'Bob',
|
|
205
|
+
lname: 'Johnson',
|
|
206
|
+
dateOfBirth: testDOB
|
|
207
|
+
})
|
|
208
|
+
const { form, fields } = await createAutoMergeForm(sdk, true)
|
|
209
|
+
|
|
210
|
+
const { enduserId: sourceId } = await submitPublicFormWithSkipMatch(form, fields, {
|
|
211
|
+
fname: 'Bob',
|
|
212
|
+
lname: 'Johnson',
|
|
213
|
+
dateOfBirth: testDOB,
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
// Merge is synchronous - source should be deleted immediately after submission
|
|
217
|
+
const sourceDeleted = await isEnduserDeleted(sdk, sourceId)
|
|
218
|
+
const updatedDestination = await sdk.api.endusers.getOne(destination.id)
|
|
219
|
+
|
|
220
|
+
// Cleanup
|
|
221
|
+
await sdk.api.forms.deleteOne(form.id)
|
|
222
|
+
await sdk.api.endusers.deleteOne(destination.id)
|
|
223
|
+
|
|
224
|
+
return sourceDeleted && updatedDestination.mergedIds?.includes(sourceId)
|
|
225
|
+
},
|
|
226
|
+
{ expectedResult: true }
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
// Test 4: No Merge - Multiple Matches
|
|
230
|
+
await async_test(
|
|
231
|
+
"Auto-merge: No merge when multiple matches found",
|
|
232
|
+
async () => {
|
|
233
|
+
// Use dateOfBirth for matching since email/phone have uniqueness constraints
|
|
234
|
+
const testDOB = '1975-01-15'
|
|
235
|
+
const destination1 = await sdk.api.endusers.createOne({
|
|
236
|
+
fname: 'Multi',
|
|
237
|
+
lname: 'Match',
|
|
238
|
+
dateOfBirth: testDOB
|
|
239
|
+
})
|
|
240
|
+
const destination2 = await sdk.api.endusers.createOne({
|
|
241
|
+
fname: 'Multi',
|
|
242
|
+
lname: 'Match',
|
|
243
|
+
dateOfBirth: testDOB
|
|
244
|
+
})
|
|
245
|
+
const { form, fields } = await createAutoMergeForm(sdk, true)
|
|
246
|
+
|
|
247
|
+
const { enduserId: sourceId } = await submitPublicFormWithSkipMatch(form, fields, {
|
|
248
|
+
fname: 'Multi',
|
|
249
|
+
lname: 'Match',
|
|
250
|
+
dateOfBirth: testDOB,
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
// Merge is synchronous - no need to wait, source should still exist
|
|
254
|
+
const sourceDeleted = await isEnduserDeleted(sdk, sourceId)
|
|
255
|
+
const dest1 = await sdk.api.endusers.getOne(destination1.id)
|
|
256
|
+
const dest2 = await sdk.api.endusers.getOne(destination2.id)
|
|
257
|
+
|
|
258
|
+
// Cleanup
|
|
259
|
+
await sdk.api.forms.deleteOne(form.id)
|
|
260
|
+
await sdk.api.endusers.deleteOne(destination1.id)
|
|
261
|
+
await sdk.api.endusers.deleteOne(destination2.id)
|
|
262
|
+
if (!sourceDeleted) await sdk.api.endusers.deleteOne(sourceId)
|
|
263
|
+
|
|
264
|
+
return !sourceDeleted // Source should NOT be deleted
|
|
265
|
+
&& !dest1.mergedIds?.includes(sourceId)
|
|
266
|
+
&& !dest2.mergedIds?.includes(sourceId)
|
|
267
|
+
},
|
|
268
|
+
{ expectedResult: true }
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
// Test 5: No Merge - autoMergeOnSubmission Disabled
|
|
272
|
+
await async_test(
|
|
273
|
+
"Auto-merge: No merge when autoMergeOnSubmission is disabled",
|
|
274
|
+
async () => {
|
|
275
|
+
const testEmail = `automerge.disabled.${Date.now()}@test.com`
|
|
276
|
+
const destination = await sdk.api.endusers.createOne({
|
|
277
|
+
fname: 'Disabled',
|
|
278
|
+
lname: 'Test',
|
|
279
|
+
email: testEmail
|
|
280
|
+
})
|
|
281
|
+
const { form, fields } = await createAutoMergeForm(sdk, false) // Disabled
|
|
282
|
+
|
|
283
|
+
const { enduserId: sourceId } = await submitPublicFormWithSkipMatch(form, fields, {
|
|
284
|
+
fname: 'Disabled',
|
|
285
|
+
lname: 'Test',
|
|
286
|
+
email: testEmail,
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
// Merge is synchronous - no need to wait, source should still exist
|
|
290
|
+
const sourceDeleted = await isEnduserDeleted(sdk, sourceId)
|
|
291
|
+
|
|
292
|
+
// Cleanup
|
|
293
|
+
await sdk.api.forms.deleteOne(form.id)
|
|
294
|
+
await sdk.api.endusers.deleteOne(destination.id)
|
|
295
|
+
if (!sourceDeleted) await sdk.api.endusers.deleteOne(sourceId)
|
|
296
|
+
|
|
297
|
+
return !sourceDeleted // Source should NOT be deleted
|
|
298
|
+
},
|
|
299
|
+
{ expectedResult: true }
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
// Test 6: No Merge - No Matching Enduser
|
|
303
|
+
await async_test(
|
|
304
|
+
"Auto-merge: No merge when no matching enduser exists",
|
|
305
|
+
async () => {
|
|
306
|
+
const { form, fields } = await createAutoMergeForm(sdk, true)
|
|
307
|
+
|
|
308
|
+
const { enduserId: sourceId } = await submitPublicFormWithSkipMatch(form, fields, {
|
|
309
|
+
fname: 'NoMatch',
|
|
310
|
+
lname: 'Person',
|
|
311
|
+
email: `nomatch.${Date.now()}@test.com`,
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
// Merge is synchronous - no need to wait, source should still exist (no match found)
|
|
315
|
+
const sourceDeleted = await isEnduserDeleted(sdk, sourceId)
|
|
316
|
+
|
|
317
|
+
// Cleanup
|
|
318
|
+
await sdk.api.forms.deleteOne(form.id)
|
|
319
|
+
if (!sourceDeleted) await sdk.api.endusers.deleteOne(sourceId)
|
|
320
|
+
|
|
321
|
+
return !sourceDeleted // Source should NOT be deleted
|
|
322
|
+
},
|
|
323
|
+
{ expectedResult: true }
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
// Test 7: Case Sensitive Matching - No Merge When Case Differs
|
|
327
|
+
await async_test(
|
|
328
|
+
"Auto-merge: No merge when case differs (case-sensitive matching)",
|
|
329
|
+
async () => {
|
|
330
|
+
const testEmail = `automerge.case.${Date.now()}@test.com`
|
|
331
|
+
const destination = await sdk.api.endusers.createOne({
|
|
332
|
+
fname: 'Case',
|
|
333
|
+
lname: 'Test',
|
|
334
|
+
email: testEmail
|
|
335
|
+
})
|
|
336
|
+
const { form, fields } = await createAutoMergeForm(sdk, true)
|
|
337
|
+
|
|
338
|
+
// Submit with different case - should NOT match due to case-sensitive matching
|
|
339
|
+
const { enduserId: sourceId } = await submitPublicFormWithSkipMatch(form, fields, {
|
|
340
|
+
fname: 'CASE', // Different case
|
|
341
|
+
lname: 'TEST', // Different case
|
|
342
|
+
email: testEmail.toUpperCase(), // Different case
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
// Merge is case-sensitive - source should still exist (no match found)
|
|
346
|
+
const sourceDeleted = await isEnduserDeleted(sdk, sourceId)
|
|
347
|
+
const updatedDestination = await sdk.api.endusers.getOne(destination.id)
|
|
348
|
+
|
|
349
|
+
// Cleanup
|
|
350
|
+
await sdk.api.forms.deleteOne(form.id)
|
|
351
|
+
await sdk.api.endusers.deleteOne(destination.id)
|
|
352
|
+
if (!sourceDeleted) await sdk.api.endusers.deleteOne(sourceId)
|
|
353
|
+
|
|
354
|
+
return !sourceDeleted // Source should NOT be deleted (no merge)
|
|
355
|
+
&& !updatedDestination.mergedIds?.includes(sourceId)
|
|
356
|
+
},
|
|
357
|
+
{ expectedResult: true }
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
// Test 8: eligibleForAutoMerge Flag Verification
|
|
361
|
+
await async_test(
|
|
362
|
+
"Auto-merge: eligibleForAutoMerge flag is set correctly",
|
|
363
|
+
async () => {
|
|
364
|
+
// Form with autoMergeOnSubmission: true
|
|
365
|
+
const { form: formEnabled, fields: fieldsEnabled } = await createAutoMergeForm(sdk, true)
|
|
366
|
+
const enduserSDKEnabled = new EnduserSession({ host, businessId: formEnabled.businessId })
|
|
367
|
+
const { enduserId: enabledEnduserId } = await enduserSDKEnabled.api.form_responses.session_for_public_form({
|
|
368
|
+
formId: formEnabled.id,
|
|
369
|
+
businessId: formEnabled.businessId,
|
|
370
|
+
skipMatch: true,
|
|
371
|
+
})
|
|
372
|
+
const enabledEnduser = await sdk.api.endusers.getOne(enabledEnduserId)
|
|
373
|
+
|
|
374
|
+
// Form with autoMergeOnSubmission: false
|
|
375
|
+
const { form: formDisabled, fields: fieldsDisabled } = await createAutoMergeForm(sdk, false)
|
|
376
|
+
const enduserSDKDisabled = new EnduserSession({ host, businessId: formDisabled.businessId })
|
|
377
|
+
const { enduserId: disabledEnduserId } = await enduserSDKDisabled.api.form_responses.session_for_public_form({
|
|
378
|
+
formId: formDisabled.id,
|
|
379
|
+
businessId: formDisabled.businessId,
|
|
380
|
+
skipMatch: true,
|
|
381
|
+
})
|
|
382
|
+
const disabledEnduser = await sdk.api.endusers.getOne(disabledEnduserId)
|
|
383
|
+
|
|
384
|
+
// Cleanup
|
|
385
|
+
await sdk.api.forms.deleteOne(formEnabled.id)
|
|
386
|
+
await sdk.api.forms.deleteOne(formDisabled.id)
|
|
387
|
+
await sdk.api.endusers.deleteOne(enabledEnduserId)
|
|
388
|
+
await sdk.api.endusers.deleteOne(disabledEnduserId)
|
|
389
|
+
|
|
390
|
+
return enabledEnduser.eligibleForAutoMerge === true
|
|
391
|
+
&& disabledEnduser.eligibleForAutoMerge !== true
|
|
392
|
+
},
|
|
393
|
+
{ expectedResult: true }
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
// Test 9: Files Transfer on Merge
|
|
397
|
+
await async_test(
|
|
398
|
+
"Auto-merge: Files are transferred to destination enduser",
|
|
399
|
+
async () => {
|
|
400
|
+
const testEmail = `automerge.files.${Date.now()}@test.com`
|
|
401
|
+
const destination = await sdk.api.endusers.createOne({
|
|
402
|
+
fname: 'Files',
|
|
403
|
+
lname: 'Test',
|
|
404
|
+
email: testEmail
|
|
405
|
+
})
|
|
406
|
+
const { form, fields } = await createAutoMergeForm(sdk, true)
|
|
407
|
+
|
|
408
|
+
// Create public session to get source enduser
|
|
409
|
+
const enduserSDK = new EnduserSession({ host, businessId: form.businessId })
|
|
410
|
+
const { authToken, accessCode, enduserId: sourceId } = await enduserSDK.api.form_responses.session_for_public_form({
|
|
411
|
+
formId: form.id,
|
|
412
|
+
businessId: form.businessId,
|
|
413
|
+
skipMatch: true,
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
// Create a file for the source enduser using prepare_file_upload + UPLOAD
|
|
417
|
+
const buff = buffer.Buffer.from('test file data for auto-merge')
|
|
418
|
+
const { presignedUpload, file } = await sdk.api.files.prepare_file_upload({
|
|
419
|
+
name: 'test-file.txt',
|
|
420
|
+
type: 'text/plain',
|
|
421
|
+
size: buff.byteLength,
|
|
422
|
+
enduserId: sourceId,
|
|
423
|
+
})
|
|
424
|
+
await sdk.UPLOAD(presignedUpload as any, buff)
|
|
425
|
+
|
|
426
|
+
// Now submit the form to trigger merge
|
|
427
|
+
const authedSDK = new EnduserSession({ host, businessId: form.businessId, authToken })
|
|
428
|
+
const fnameField = fields.find(f => f.intakeField === 'fname')!
|
|
429
|
+
const lnameField = fields.find(f => f.intakeField === 'lname')!
|
|
430
|
+
const emailField = fields.find(f => f.intakeField === 'email')!
|
|
431
|
+
|
|
432
|
+
await authedSDK.api.form_responses.submit_form_response({
|
|
433
|
+
accessCode,
|
|
434
|
+
responses: [
|
|
435
|
+
{ fieldId: fnameField.id, fieldTitle: fnameField.title, answer: { type: 'string', value: 'Files' } },
|
|
436
|
+
{ fieldId: lnameField.id, fieldTitle: lnameField.title, answer: { type: 'string', value: 'Test' } },
|
|
437
|
+
{ fieldId: emailField.id, fieldTitle: emailField.title, answer: { type: 'email', value: testEmail } },
|
|
438
|
+
],
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
// Merge is synchronous - source should be deleted immediately after submission
|
|
442
|
+
const sourceDeleted = await isEnduserDeleted(sdk, sourceId)
|
|
443
|
+
const updatedFile = await sdk.api.files.getOne(file.id)
|
|
444
|
+
|
|
445
|
+
// Cleanup
|
|
446
|
+
await sdk.api.forms.deleteOne(form.id)
|
|
447
|
+
await sdk.api.files.deleteOne(file.id)
|
|
448
|
+
await sdk.api.endusers.deleteOne(destination.id)
|
|
449
|
+
|
|
450
|
+
return sourceDeleted && updatedFile.enduserId === destination.id
|
|
451
|
+
},
|
|
452
|
+
{ expectedResult: true }
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
// Test 10: Calendar Events Transfer on Merge
|
|
456
|
+
await async_test(
|
|
457
|
+
"Auto-merge: Calendar events are transferred to destination enduser",
|
|
458
|
+
async () => {
|
|
459
|
+
const testEmail = `automerge.events.${Date.now()}@test.com`
|
|
460
|
+
const destination = await sdk.api.endusers.createOne({
|
|
461
|
+
fname: 'Events',
|
|
462
|
+
lname: 'Test',
|
|
463
|
+
email: testEmail
|
|
464
|
+
})
|
|
465
|
+
const { form, fields } = await createAutoMergeForm(sdk, true)
|
|
466
|
+
|
|
467
|
+
// Create public session to get source enduser
|
|
468
|
+
const enduserSDK = new EnduserSession({ host, businessId: form.businessId })
|
|
469
|
+
const { authToken, accessCode, enduserId: sourceId } = await enduserSDK.api.form_responses.session_for_public_form({
|
|
470
|
+
formId: form.id,
|
|
471
|
+
businessId: form.businessId,
|
|
472
|
+
skipMatch: true,
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
// Create a calendar event with source enduser as attendee
|
|
476
|
+
const event = await sdk.api.calendar_events.createOne({
|
|
477
|
+
title: 'Test Event',
|
|
478
|
+
startTimeInMS: Date.now() + 86400000, // Tomorrow
|
|
479
|
+
durationInMinutes: 30,
|
|
480
|
+
attendees: [{ id: sourceId, type: 'enduser' }],
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
// Now submit the form to trigger merge
|
|
484
|
+
const authedSDK = new EnduserSession({ host, businessId: form.businessId, authToken })
|
|
485
|
+
const fnameField = fields.find(f => f.intakeField === 'fname')!
|
|
486
|
+
const lnameField = fields.find(f => f.intakeField === 'lname')!
|
|
487
|
+
const emailField = fields.find(f => f.intakeField === 'email')!
|
|
488
|
+
|
|
489
|
+
await authedSDK.api.form_responses.submit_form_response({
|
|
490
|
+
accessCode,
|
|
491
|
+
responses: [
|
|
492
|
+
{ fieldId: fnameField.id, fieldTitle: fnameField.title, answer: { type: 'string', value: 'Events' } },
|
|
493
|
+
{ fieldId: lnameField.id, fieldTitle: lnameField.title, answer: { type: 'string', value: 'Test' } },
|
|
494
|
+
{ fieldId: emailField.id, fieldTitle: emailField.title, answer: { type: 'email', value: testEmail } },
|
|
495
|
+
],
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
// Merge is synchronous - source should be deleted immediately after submission
|
|
499
|
+
const sourceDeleted = await isEnduserDeleted(sdk, sourceId)
|
|
500
|
+
const updatedEvent = await sdk.api.calendar_events.getOne(event.id)
|
|
501
|
+
|
|
502
|
+
// Cleanup
|
|
503
|
+
await sdk.api.forms.deleteOne(form.id)
|
|
504
|
+
await sdk.api.calendar_events.deleteOne(event.id)
|
|
505
|
+
await sdk.api.endusers.deleteOne(destination.id)
|
|
506
|
+
|
|
507
|
+
return sourceDeleted && updatedEvent.attendees?.some(a => a.id === destination.id)
|
|
508
|
+
},
|
|
509
|
+
{ expectedResult: true }
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
// Test 11: Form response enduserId is updated to destination (placeholder is updated before submission completes)
|
|
513
|
+
await async_test(
|
|
514
|
+
"Auto-merge: Form response enduserId is updated to destination",
|
|
515
|
+
async () => {
|
|
516
|
+
const testEmail = `automerge.directfr.${Date.now()}@test.com`
|
|
517
|
+
const destination = await sdk.api.endusers.createOne({
|
|
518
|
+
fname: 'Direct',
|
|
519
|
+
lname: 'Response',
|
|
520
|
+
email: testEmail
|
|
521
|
+
})
|
|
522
|
+
const { form, fields } = await createAutoMergeForm(sdk, true)
|
|
523
|
+
|
|
524
|
+
const { enduserId: sourceId, accessCode } = await submitPublicFormWithSkipMatch(form, fields, {
|
|
525
|
+
fname: 'Direct',
|
|
526
|
+
lname: 'Response',
|
|
527
|
+
email: testEmail,
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
// Merge is synchronous - verify form response was created with destination ID
|
|
531
|
+
const sourceDeleted = await isEnduserDeleted(sdk, sourceId)
|
|
532
|
+
|
|
533
|
+
// Get form responses by accessCode to find the one we created
|
|
534
|
+
const formResponses = await sdk.api.form_responses.getSome({ filter: { accessCode } })
|
|
535
|
+
const createdFormResponse = formResponses[0]
|
|
536
|
+
|
|
537
|
+
// Cleanup
|
|
538
|
+
await sdk.api.forms.deleteOne(form.id)
|
|
539
|
+
await sdk.api.endusers.deleteOne(destination.id)
|
|
540
|
+
|
|
541
|
+
// The form response should have been created directly with destination enduser ID
|
|
542
|
+
// (not transferred after creation)
|
|
543
|
+
return sourceDeleted
|
|
544
|
+
&& createdFormResponse !== undefined
|
|
545
|
+
&& createdFormResponse.enduserId === destination.id
|
|
546
|
+
},
|
|
547
|
+
{ expectedResult: true }
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
// Test 12: Intake fields update destination enduser directly
|
|
551
|
+
await async_test(
|
|
552
|
+
"Auto-merge: Intake fields update destination enduser directly",
|
|
553
|
+
async () => {
|
|
554
|
+
const testEmail = `automerge.intake.${Date.now()}@test.com`
|
|
555
|
+
const destination = await sdk.api.endusers.createOne({
|
|
556
|
+
fname: 'Intake',
|
|
557
|
+
lname: 'Test',
|
|
558
|
+
email: testEmail,
|
|
559
|
+
// No phone or DOB set initially
|
|
560
|
+
})
|
|
561
|
+
const { form, fields } = await createAutoMergeForm(sdk, true)
|
|
562
|
+
|
|
563
|
+
const newPhone = '+15555559876'
|
|
564
|
+
const newDOB = '1985-03-20'
|
|
565
|
+
|
|
566
|
+
const { enduserId: sourceId } = await submitPublicFormWithSkipMatch(form, fields, {
|
|
567
|
+
fname: 'Intake',
|
|
568
|
+
lname: 'Test',
|
|
569
|
+
email: testEmail,
|
|
570
|
+
phone: newPhone,
|
|
571
|
+
dateOfBirth: newDOB,
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
// Merge is synchronous - verify intake fields updated destination directly
|
|
575
|
+
const sourceDeleted = await isEnduserDeleted(sdk, sourceId)
|
|
576
|
+
const updatedDestination = await sdk.api.endusers.getOne(destination.id)
|
|
577
|
+
|
|
578
|
+
// Cleanup
|
|
579
|
+
await sdk.api.forms.deleteOne(form.id)
|
|
580
|
+
await sdk.api.endusers.deleteOne(destination.id)
|
|
581
|
+
|
|
582
|
+
// Intake fields should have been applied to the destination enduser
|
|
583
|
+
return sourceDeleted
|
|
584
|
+
&& updatedDestination.phone === newPhone
|
|
585
|
+
&& updatedDestination.dateOfBirth === newDOB
|
|
586
|
+
},
|
|
587
|
+
{ expectedResult: true }
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
// Test 13: eligibleForAutoMerge is unset after submission (no merge case)
|
|
591
|
+
await async_test(
|
|
592
|
+
"Auto-merge: eligibleForAutoMerge is unset after submission when no merge occurs",
|
|
593
|
+
async () => {
|
|
594
|
+
const { form, fields } = await createAutoMergeForm(sdk, true)
|
|
595
|
+
|
|
596
|
+
// Submit form - no match exists, so no merge will happen
|
|
597
|
+
const { enduserId: sourceId } = await submitPublicFormWithSkipMatch(form, fields, {
|
|
598
|
+
fname: 'Unset',
|
|
599
|
+
lname: 'Flag',
|
|
600
|
+
email: `unset.flag.${Date.now()}@test.com`,
|
|
601
|
+
})
|
|
602
|
+
|
|
603
|
+
// Verify enduser still exists and eligibleForAutoMerge is unset
|
|
604
|
+
const updatedEnduser = await sdk.api.endusers.getOne(sourceId)
|
|
605
|
+
|
|
606
|
+
// Cleanup
|
|
607
|
+
await sdk.api.forms.deleteOne(form.id)
|
|
608
|
+
await sdk.api.endusers.deleteOne(sourceId)
|
|
609
|
+
|
|
610
|
+
// eligibleForAutoMerge should be unset (undefined/falsy) after submission
|
|
611
|
+
return updatedEnduser.eligibleForAutoMerge !== true
|
|
612
|
+
},
|
|
613
|
+
{ expectedResult: true }
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
// ============================================
|
|
617
|
+
// BACKWARDS COMPATIBILITY & EDGE CASE TESTS
|
|
618
|
+
// ============================================
|
|
619
|
+
|
|
620
|
+
// Test 14: No merge when source enduser has multiple form responses
|
|
621
|
+
await async_test(
|
|
622
|
+
"Auto-merge: No merge when source enduser already has multiple form responses",
|
|
623
|
+
async () => {
|
|
624
|
+
const testEmail = `automerge.multiresponse.${Date.now()}@test.com`
|
|
625
|
+
const destination = await sdk.api.endusers.createOne({
|
|
626
|
+
fname: 'Multi',
|
|
627
|
+
lname: 'Response',
|
|
628
|
+
email: testEmail
|
|
629
|
+
})
|
|
630
|
+
const { form: form1, fields: fields1 } = await createAutoMergeForm(sdk, true)
|
|
631
|
+
const { form: form2, fields: fields2 } = await createAutoMergeForm(sdk, true)
|
|
632
|
+
|
|
633
|
+
// Create first public session and submit (this creates first form response)
|
|
634
|
+
const enduserSDK1 = new EnduserSession({ host, businessId: form1.businessId })
|
|
635
|
+
const session1 = await enduserSDK1.api.form_responses.session_for_public_form({
|
|
636
|
+
formId: form1.id,
|
|
637
|
+
businessId: form1.businessId,
|
|
638
|
+
skipMatch: true,
|
|
639
|
+
})
|
|
640
|
+
const sourceId = session1.enduserId
|
|
641
|
+
|
|
642
|
+
// Submit first form with non-matching data (no merge should happen)
|
|
643
|
+
const authedSDK1 = new EnduserSession({ host, businessId: form1.businessId, authToken: session1.authToken })
|
|
644
|
+
const fnameField1 = fields1.find(f => f.intakeField === 'fname')!
|
|
645
|
+
const lnameField1 = fields1.find(f => f.intakeField === 'lname')!
|
|
646
|
+
const emailField1 = fields1.find(f => f.intakeField === 'email')!
|
|
647
|
+
await authedSDK1.api.form_responses.submit_form_response({
|
|
648
|
+
accessCode: session1.accessCode,
|
|
649
|
+
responses: [
|
|
650
|
+
{ fieldId: fnameField1.id, fieldTitle: fnameField1.title, answer: { type: 'string', value: 'Different' } },
|
|
651
|
+
{ fieldId: lnameField1.id, fieldTitle: lnameField1.title, answer: { type: 'string', value: 'Person' } },
|
|
652
|
+
{ fieldId: emailField1.id, fieldTitle: emailField1.title, answer: { type: 'email', value: `different.${Date.now()}@test.com` } },
|
|
653
|
+
],
|
|
654
|
+
})
|
|
655
|
+
|
|
656
|
+
// Now create a second form response for the SAME source enduser via admin SDK
|
|
657
|
+
await sdk.api.form_responses.createOne({
|
|
658
|
+
formId: form2.id,
|
|
659
|
+
formTitle: 'Auto Merge Test Form',
|
|
660
|
+
enduserId: sourceId,
|
|
661
|
+
})
|
|
662
|
+
|
|
663
|
+
// Re-set eligibleForAutoMerge manually to simulate another attempt
|
|
664
|
+
await sdk.api.endusers.updateOne(sourceId, { eligibleForAutoMerge: true })
|
|
665
|
+
|
|
666
|
+
// Create another public session that would match - but source now has 2+ form responses
|
|
667
|
+
const enduserSDK2 = new EnduserSession({ host, businessId: form2.businessId })
|
|
668
|
+
const session2 = await enduserSDK2.api.form_responses.session_for_public_form({
|
|
669
|
+
formId: form2.id,
|
|
670
|
+
businessId: form2.businessId,
|
|
671
|
+
skipMatch: true,
|
|
672
|
+
})
|
|
673
|
+
const sourceId2 = session2.enduserId
|
|
674
|
+
|
|
675
|
+
// Manually add another form response to sourceId2 to trigger the >1 check
|
|
676
|
+
await sdk.api.form_responses.createOne({
|
|
677
|
+
formId: form1.id,
|
|
678
|
+
formTitle: 'Auto Merge Test Form',
|
|
679
|
+
enduserId: sourceId2,
|
|
680
|
+
})
|
|
681
|
+
|
|
682
|
+
// Submit with matching data - should NOT merge because source has >1 form responses
|
|
683
|
+
const authedSDK2 = new EnduserSession({ host, businessId: form2.businessId, authToken: session2.authToken })
|
|
684
|
+
const fnameField2 = fields2.find(f => f.intakeField === 'fname')!
|
|
685
|
+
const lnameField2 = fields2.find(f => f.intakeField === 'lname')!
|
|
686
|
+
const emailField2 = fields2.find(f => f.intakeField === 'email')!
|
|
687
|
+
await authedSDK2.api.form_responses.submit_form_response({
|
|
688
|
+
accessCode: session2.accessCode,
|
|
689
|
+
responses: [
|
|
690
|
+
{ fieldId: fnameField2.id, fieldTitle: fnameField2.title, answer: { type: 'string', value: 'Multi' } },
|
|
691
|
+
{ fieldId: lnameField2.id, fieldTitle: lnameField2.title, answer: { type: 'string', value: 'Response' } },
|
|
692
|
+
{ fieldId: emailField2.id, fieldTitle: emailField2.title, answer: { type: 'email', value: testEmail } },
|
|
693
|
+
],
|
|
694
|
+
})
|
|
695
|
+
|
|
696
|
+
// Source should NOT be deleted because it had multiple form responses
|
|
697
|
+
const sourceStillExists = !(await isEnduserDeleted(sdk, sourceId2))
|
|
698
|
+
|
|
699
|
+
// Cleanup
|
|
700
|
+
await sdk.api.forms.deleteOne(form1.id)
|
|
701
|
+
await sdk.api.forms.deleteOne(form2.id)
|
|
702
|
+
await sdk.api.endusers.deleteOne(destination.id)
|
|
703
|
+
await sdk.api.endusers.deleteOne(sourceId)
|
|
704
|
+
if (sourceStillExists) await sdk.api.endusers.deleteOne(sourceId2)
|
|
705
|
+
|
|
706
|
+
return sourceStillExists
|
|
707
|
+
},
|
|
708
|
+
{ expectedResult: true }
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
// Test 15: Backwards compat - skipMatch=false does NOT set eligibleForAutoMerge
|
|
712
|
+
await async_test(
|
|
713
|
+
"Backwards compat: skipMatch=false does not set eligibleForAutoMerge even with autoMergeOnSubmission=true",
|
|
714
|
+
async () => {
|
|
715
|
+
const { form, fields } = await createAutoMergeForm(sdk, true)
|
|
716
|
+
const testPhone = `+1555555${Date.now().toString().slice(-4)}`
|
|
717
|
+
|
|
718
|
+
// Create public session WITHOUT skipMatch (normal flow - requires phone)
|
|
719
|
+
const enduserSDK = new EnduserSession({ host, businessId: form.businessId })
|
|
720
|
+
const { enduserId } = await enduserSDK.api.form_responses.session_for_public_form({
|
|
721
|
+
formId: form.id,
|
|
722
|
+
businessId: form.businessId,
|
|
723
|
+
phone: testPhone, // Phone is required when skipMatch is not set
|
|
724
|
+
// skipMatch is NOT set (defaults to false)
|
|
725
|
+
})
|
|
726
|
+
|
|
727
|
+
const enduser = await sdk.api.endusers.getOne(enduserId)
|
|
728
|
+
|
|
729
|
+
// Cleanup
|
|
730
|
+
await sdk.api.forms.deleteOne(form.id)
|
|
731
|
+
await sdk.api.endusers.deleteOne(enduserId)
|
|
732
|
+
|
|
733
|
+
// eligibleForAutoMerge should NOT be set when skipMatch is false
|
|
734
|
+
return enduser.eligibleForAutoMerge !== true
|
|
735
|
+
},
|
|
736
|
+
{ expectedResult: true }
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
// Test 16: Backwards compat - Private form submission doesn't trigger auto-merge
|
|
740
|
+
await async_test(
|
|
741
|
+
"Backwards compat: Private form submission does not trigger auto-merge",
|
|
742
|
+
async () => {
|
|
743
|
+
const testEmail = `backcompat.private.${Date.now()}@test.com`
|
|
744
|
+
const destination = await sdk.api.endusers.createOne({
|
|
745
|
+
fname: 'Private',
|
|
746
|
+
lname: 'Test',
|
|
747
|
+
email: testEmail
|
|
748
|
+
})
|
|
749
|
+
|
|
750
|
+
// Create source enduser with eligibleForAutoMerge manually set
|
|
751
|
+
const source = await sdk.api.endusers.createOne({
|
|
752
|
+
fname: 'Private',
|
|
753
|
+
lname: 'Test',
|
|
754
|
+
email: testEmail + '.source',
|
|
755
|
+
eligibleForAutoMerge: true, // Manually set to test that private submission ignores it
|
|
756
|
+
})
|
|
757
|
+
|
|
758
|
+
const { form, fields } = await createAutoMergeForm(sdk, true)
|
|
759
|
+
|
|
760
|
+
// Create form response via admin SDK (private/non-public submission)
|
|
761
|
+
const fnameField = fields.find(f => f.intakeField === 'fname')!
|
|
762
|
+
const lnameField = fields.find(f => f.intakeField === 'lname')!
|
|
763
|
+
const emailField = fields.find(f => f.intakeField === 'email')!
|
|
764
|
+
|
|
765
|
+
await sdk.api.form_responses.createOne({
|
|
766
|
+
formId: form.id,
|
|
767
|
+
formTitle: form.title,
|
|
768
|
+
enduserId: source.id,
|
|
769
|
+
responses: [
|
|
770
|
+
{ fieldId: fnameField.id, fieldTitle: fnameField.title, answer: { type: 'string', value: 'Private' } },
|
|
771
|
+
{ fieldId: lnameField.id, fieldTitle: lnameField.title, answer: { type: 'string', value: 'Test' } },
|
|
772
|
+
{ fieldId: emailField.id, fieldTitle: emailField.title, answer: { type: 'email', value: testEmail } },
|
|
773
|
+
],
|
|
774
|
+
})
|
|
775
|
+
|
|
776
|
+
// Source should NOT be deleted because this was a private submission (not publicSubmit)
|
|
777
|
+
const sourceStillExists = !(await isEnduserDeleted(sdk, source.id))
|
|
778
|
+
|
|
779
|
+
// Cleanup
|
|
780
|
+
await sdk.api.forms.deleteOne(form.id)
|
|
781
|
+
await sdk.api.endusers.deleteOne(destination.id)
|
|
782
|
+
if (sourceStillExists) await sdk.api.endusers.deleteOne(source.id)
|
|
783
|
+
|
|
784
|
+
return sourceStillExists
|
|
785
|
+
},
|
|
786
|
+
{ expectedResult: true }
|
|
787
|
+
)
|
|
788
|
+
|
|
789
|
+
// Test 17: OR logic - matches on email even when phone differs
|
|
790
|
+
await async_test(
|
|
791
|
+
"Auto-merge: Merge occurs when email matches even if phone differs (OR logic)",
|
|
792
|
+
async () => {
|
|
793
|
+
const testEmail = `automerge.orlogic.${Date.now()}@test.com`
|
|
794
|
+
const destination = await sdk.api.endusers.createOne({
|
|
795
|
+
fname: 'OrLogic',
|
|
796
|
+
lname: 'Test',
|
|
797
|
+
email: testEmail,
|
|
798
|
+
phone: '+15555550001', // Different phone
|
|
799
|
+
})
|
|
800
|
+
const { form, fields } = await createAutoMergeForm(sdk, true)
|
|
801
|
+
|
|
802
|
+
// Submit with matching email but DIFFERENT phone
|
|
803
|
+
const { enduserId: sourceId } = await submitPublicFormWithSkipMatch(form, fields, {
|
|
804
|
+
fname: 'OrLogic',
|
|
805
|
+
lname: 'Test',
|
|
806
|
+
email: testEmail,
|
|
807
|
+
phone: '+15555550002', // Different phone than destination
|
|
808
|
+
})
|
|
809
|
+
|
|
810
|
+
// Should merge because email matches (OR logic, not AND)
|
|
811
|
+
const sourceDeleted = await isEnduserDeleted(sdk, sourceId)
|
|
812
|
+
const updatedDestination = await sdk.api.endusers.getOne(destination.id)
|
|
813
|
+
|
|
814
|
+
// Cleanup
|
|
815
|
+
await sdk.api.forms.deleteOne(form.id)
|
|
816
|
+
await sdk.api.endusers.deleteOne(destination.id)
|
|
817
|
+
|
|
818
|
+
return sourceDeleted && updatedDestination.mergedIds?.includes(sourceId)
|
|
819
|
+
},
|
|
820
|
+
{ expectedResult: true }
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
// Test 18: Partial name mismatch - fname matches but lname differs
|
|
824
|
+
await async_test(
|
|
825
|
+
"Auto-merge: No merge when fname matches but lname differs",
|
|
826
|
+
async () => {
|
|
827
|
+
const testEmail = `automerge.partial.${Date.now()}@test.com`
|
|
828
|
+
const destination = await sdk.api.endusers.createOne({
|
|
829
|
+
fname: 'Partial',
|
|
830
|
+
lname: 'Match',
|
|
831
|
+
email: testEmail
|
|
832
|
+
})
|
|
833
|
+
const { form, fields } = await createAutoMergeForm(sdk, true)
|
|
834
|
+
|
|
835
|
+
// Submit with same fname but DIFFERENT lname
|
|
836
|
+
const { enduserId: sourceId } = await submitPublicFormWithSkipMatch(form, fields, {
|
|
837
|
+
fname: 'Partial', // Same
|
|
838
|
+
lname: 'Different', // Different!
|
|
839
|
+
email: testEmail, // Same email
|
|
840
|
+
})
|
|
841
|
+
|
|
842
|
+
// Should NOT merge because lname differs
|
|
843
|
+
const sourceDeleted = await isEnduserDeleted(sdk, sourceId)
|
|
844
|
+
|
|
845
|
+
// Cleanup
|
|
846
|
+
await sdk.api.forms.deleteOne(form.id)
|
|
847
|
+
await sdk.api.endusers.deleteOne(destination.id)
|
|
848
|
+
if (!sourceDeleted) await sdk.api.endusers.deleteOne(sourceId)
|
|
849
|
+
|
|
850
|
+
return !sourceDeleted // Source should NOT be deleted
|
|
851
|
+
},
|
|
852
|
+
{ expectedResult: true }
|
|
853
|
+
)
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// Allow running this test file independently
|
|
857
|
+
if (require.main === module) {
|
|
858
|
+
console.log(`Using API URL: ${host}`)
|
|
859
|
+
const sdk = new Session({ host })
|
|
860
|
+
const sdkNonAdmin = new Session({ host })
|
|
861
|
+
|
|
862
|
+
const runTests = async () => {
|
|
863
|
+
await setup_tests(sdk, sdkNonAdmin)
|
|
864
|
+
await auto_merge_form_submission_tests({ sdk, sdkNonAdmin })
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
runTests()
|
|
868
|
+
.then(() => {
|
|
869
|
+
console.log("Auto-merge form submission test suite completed successfully")
|
|
870
|
+
process.exit(0)
|
|
871
|
+
})
|
|
872
|
+
.catch((error) => {
|
|
873
|
+
console.error("Auto-merge form submission test suite failed:", error)
|
|
874
|
+
process.exit(1)
|
|
875
|
+
})
|
|
876
|
+
}
|