@khester/create-dynamics-app 1.0.8 → 1.1.0

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 (107) hide show
  1. package/bin/create-dynamics-app.js +1 -1
  2. package/dist/index.js +140 -15
  3. package/dist/index.js.map +1 -1
  4. package/dist/utils/consultingHelpers.d.ts +13 -0
  5. package/dist/utils/consultingHelpers.d.ts.map +1 -0
  6. package/dist/utils/consultingHelpers.js +569 -0
  7. package/dist/utils/consultingHelpers.js.map +1 -0
  8. package/dist/utils/copyTemplate.d.ts.map +1 -1
  9. package/dist/utils/copyTemplate.js.map +1 -1
  10. package/dist/utils/initGit.d.ts.map +1 -1
  11. package/dist/utils/initGit.js.map +1 -1
  12. package/dist/utils/installDependencies.d.ts.map +1 -1
  13. package/dist/utils/installDependencies.js +3 -2
  14. package/dist/utils/installDependencies.js.map +1 -1
  15. package/dist/utils/updatePackageJson.d.ts +1 -1
  16. package/dist/utils/updatePackageJson.d.ts.map +1 -1
  17. package/dist/utils/updatePackageJson.js +11 -1
  18. package/dist/utils/updatePackageJson.js.map +1 -1
  19. package/package.json +1 -1
  20. package/templates/dynamics-365-starter/INTEGRATION_TEST_RESULTS.md +302 -0
  21. package/templates/dynamics-365-starter/PHASE_4_COMPLETION_SUMMARY.md +305 -0
  22. package/templates/dynamics-365-starter/README.md +566 -137
  23. package/templates/dynamics-365-starter/deployment/QUICKSTART-MAC.md +507 -0
  24. package/templates/dynamics-365-starter/deployment/QUICKSTART-WINDOWS.md +372 -0
  25. package/templates/dynamics-365-starter/deployment/README.md +484 -0
  26. package/templates/dynamics-365-starter/deployment/pipelines/README.md +375 -0
  27. package/templates/dynamics-365-starter/deployment/pipelines/azure-pipelines.yml +330 -0
  28. package/templates/dynamics-365-starter/deployment/pipelines/github-actions.yml +422 -0
  29. package/templates/dynamics-365-starter/deployment/pipelines/jenkins.groovy +636 -0
  30. package/templates/dynamics-365-starter/deployment/scripts/deploy.ps1 +417 -0
  31. package/templates/dynamics-365-starter/deployment/scripts/deploy.sh +582 -0
  32. package/templates/dynamics-365-starter/deployment/scripts/team-onboarding.ps1 +486 -0
  33. package/templates/dynamics-365-starter/deployment/scripts/team-onboarding.sh +567 -0
  34. package/templates/dynamics-365-starter/deployment/scripts/validate-setup.ps1 +703 -0
  35. package/templates/dynamics-365-starter/deployment/scripts/validate-setup.sh +671 -0
  36. package/templates/dynamics-365-starter/docs/ARCHITECTURE_OVERVIEW.md +506 -0
  37. package/templates/dynamics-365-starter/docs/BEST_PRACTICES.md +723 -0
  38. package/templates/dynamics-365-starter/docs/MIGRATION_GUIDE.md +447 -0
  39. package/templates/dynamics-365-starter/docs/team-standards/README.md +273 -0
  40. package/templates/dynamics-365-starter/docs/team-standards/client-onboarding.md +577 -0
  41. package/templates/dynamics-365-starter/docs/team-standards/code-review-checklist.md +359 -0
  42. package/templates/dynamics-365-starter/docs/team-standards/coding-standards.md +700 -0
  43. package/templates/dynamics-365-starter/docs/team-standards/cross-platform-team-guide.md +736 -0
  44. package/templates/dynamics-365-starter/docs/team-standards/development-workflows.md +727 -0
  45. package/templates/dynamics-365-starter/docs/troubleshooting/common-errors.md +758 -0
  46. package/templates/dynamics-365-starter/docs/troubleshooting/platform-specific-issues.md +878 -0
  47. package/templates/dynamics-365-starter/package.json +22 -1
  48. package/templates/dynamics-365-starter/public/index.html +8 -11
  49. package/templates/dynamics-365-starter/scripts/custom-build.js +255 -0
  50. package/templates/dynamics-365-starter/src/client-project-template/README.md +234 -0
  51. package/templates/dynamics-365-starter/src/client-project-template/config/client.template.json +114 -0
  52. package/templates/dynamics-365-starter/src/client-project-template/config/environments/template.json +186 -0
  53. package/templates/dynamics-365-starter/src/client-project-template/scripts/client-setup.js +667 -0
  54. package/templates/dynamics-365-starter/src/components/AccountForm.css +71 -0
  55. package/templates/dynamics-365-starter/src/components/AccountForm.tsx +541 -0
  56. package/templates/dynamics-365-starter/src/components/AccountManagement.css +86 -0
  57. package/templates/dynamics-365-starter/src/components/AccountManagement.tsx +370 -0
  58. package/templates/dynamics-365-starter/src/components/ContactForm.tsx +149 -63
  59. package/templates/dynamics-365-starter/src/components/ContactManagement.tsx +153 -63
  60. package/templates/dynamics-365-starter/src/components/Logging/LogDialog.tsx +291 -0
  61. package/templates/dynamics-365-starter/src/components/Logging/LoggingContext.tsx +166 -0
  62. package/templates/dynamics-365-starter/src/components/Logging/LoggingDebugPanel.css +192 -0
  63. package/templates/dynamics-365-starter/src/components/Logging/LoggingDebugPanel.tsx +177 -0
  64. package/templates/dynamics-365-starter/src/components/Logging/LoggingProvider.tsx +3 -0
  65. package/templates/dynamics-365-starter/src/components/Logging/logger.ts +193 -0
  66. package/templates/dynamics-365-starter/src/constants/account.ts +410 -0
  67. package/templates/dynamics-365-starter/src/constants/contact.ts +362 -0
  68. package/templates/dynamics-365-starter/src/examples/README.md +52 -0
  69. package/templates/dynamics-365-starter/src/examples/component-examples/opportunity-management.tsx +625 -0
  70. package/templates/dynamics-365-starter/src/examples/entity-examples/opportunity-model.ts +545 -0
  71. package/templates/dynamics-365-starter/src/examples/integration-examples/custom-pcf-wrapper.tsx +722 -0
  72. package/templates/dynamics-365-starter/src/examples/workflow-examples/sales-workflow.ts +662 -0
  73. package/templates/dynamics-365-starter/src/index.tsx +107 -19
  74. package/templates/dynamics-365-starter/src/models/Account.ts +480 -0
  75. package/templates/dynamics-365-starter/src/models/BaseEntity.ts +204 -0
  76. package/templates/dynamics-365-starter/src/models/Contact.ts +580 -0
  77. package/templates/dynamics-365-starter/src/page-templates/EntityDashboard.tsx +519 -0
  78. package/templates/dynamics-365-starter/src/page-templates/EntityDetailPage.tsx +456 -0
  79. package/templates/dynamics-365-starter/src/page-templates/EntityListPage.tsx +406 -0
  80. package/templates/dynamics-365-starter/src/page-templates/RelatedEntitiesPage.tsx +578 -0
  81. package/templates/dynamics-365-starter/src/page-templates/SearchPage.tsx +629 -0
  82. package/templates/dynamics-365-starter/src/pcf/ContactControlWrapper.tsx +75 -22
  83. package/templates/dynamics-365-starter/src/pcf/MultiEntityControlWrapper.tsx +205 -0
  84. package/templates/dynamics-365-starter/src/providers/DynamicsProvider.tsx +297 -80
  85. package/templates/dynamics-365-starter/src/services/MockApiService.ts +260 -0
  86. package/templates/dynamics-365-starter/src/services/ServiceFactory.ts +65 -0
  87. package/templates/dynamics-365-starter/src/services/XrmApiService.ts +213 -0
  88. package/templates/dynamics-365-starter/src/styles/index.css +74 -7
  89. package/templates/dynamics-365-starter/tools/entity-generator/index.js +168 -0
  90. package/templates/dynamics-365-starter/tools/entity-generator/templates/constants.template.ts +124 -0
  91. package/templates/dynamics-365-starter/tools/entity-generator/templates/form.template.css +283 -0
  92. package/templates/dynamics-365-starter/tools/entity-generator/templates/form.template.tsx +275 -0
  93. package/templates/dynamics-365-starter/tools/entity-generator/templates/management.template.css +204 -0
  94. package/templates/dynamics-365-starter/tools/entity-generator/templates/management.template.tsx +413 -0
  95. package/templates/dynamics-365-starter/tools/entity-generator/templates/model.template.ts +250 -0
  96. package/templates/dynamics-365-starter/tools/metadata-sync/d365-client.js +410 -0
  97. package/templates/dynamics-365-starter/tools/metadata-sync/index.js +512 -0
  98. package/templates/dynamics-365-starter/tools/metadata-sync/type-generator.js +675 -0
  99. package/templates/dynamics-365-starter/tsconfig.json +11 -8
  100. package/templates/dynamics-365-starter/webpack.config.js +8 -9
  101. package/templates/power-pages-starter/README.md +7 -1
  102. package/templates/power-pages-starter/public/index.html +8 -11
  103. package/templates/power-pages-starter/src/components/ContactForm.tsx +60 -41
  104. package/templates/power-pages-starter/src/index.tsx +3 -3
  105. package/templates/power-pages-starter/src/providers/PowerPagesProvider.tsx +46 -23
  106. package/templates/power-pages-starter/tsconfig.json +3 -9
  107. package/templates/power-pages-starter/webpack.config.js +8 -3
@@ -0,0 +1,580 @@
1
+ import { IApiService } from '@khester/dynamics-ui-api-client';
2
+ import { BaseEntity } from './BaseEntity';
3
+ import { ContactConstants } from '../constants/contact';
4
+
5
+ /**
6
+ * Interface for Contact entity
7
+ */
8
+ export interface IContact {
9
+ contactid?: string;
10
+ firstname?: string;
11
+ lastname?: string;
12
+ fullname?: string;
13
+ middlename?: string;
14
+ salutation?: string;
15
+ suffix?: string;
16
+ jobtitle?: string;
17
+ emailaddress1?: string;
18
+ emailaddress2?: string;
19
+ emailaddress3?: string;
20
+ telephone1?: string; // Business Phone
21
+ telephone2?: string; // Home Phone
22
+ mobilephone?: string;
23
+ fax?: string;
24
+ pager?: string;
25
+ parentcustomerid?: string;
26
+ parentcustomerid_name?: string;
27
+ department?: string;
28
+ managerid?: string;
29
+ assistantname?: string;
30
+ assistantphone?: string;
31
+ birthdate?: string;
32
+ anniversary?: string;
33
+ gendercode?: number;
34
+ familystatuscode?: number;
35
+ haschildrencode?: boolean;
36
+ childrensnames?: string;
37
+ description?: string;
38
+ donotbulkemail?: boolean;
39
+ donotemail?: boolean;
40
+ donotfax?: boolean;
41
+ donotphone?: boolean;
42
+ donotpostalmail?: boolean;
43
+ donotsendmm?: boolean;
44
+ preferredcontactmethodcode?: number;
45
+ preferredappointmentdaycode?: number;
46
+ preferredappointmenttimecode?: number;
47
+ address1_addresstypecode?: string;
48
+ address1_city?: string;
49
+ address1_country?: string;
50
+ address1_county?: string;
51
+ address1_fax?: string;
52
+ address1_latitude?: number;
53
+ address1_line1?: string;
54
+ address1_line2?: string;
55
+ address1_line3?: string;
56
+ address1_longitude?: number;
57
+ address1_name?: string;
58
+ address1_postalcode?: string;
59
+ address1_postofficebox?: string;
60
+ address1_primarycontactname?: string;
61
+ address1_shippingmethodcode?: number;
62
+ address1_stateorprovince?: string;
63
+ address1_telephone1?: string;
64
+ address1_telephone2?: string;
65
+ address1_telephone3?: string;
66
+ address1_upszone?: string;
67
+ address1_utcoffset?: number;
68
+ creditlimit?: number;
69
+ creditonhold?: boolean;
70
+ paymenttermscode?: number;
71
+ customersizecode?: number;
72
+ leadsourcecode?: number;
73
+ websiteurl?: string;
74
+ governmentid?: string;
75
+ yomifirstname?: string;
76
+ yomilastname?: string;
77
+ yomimiddlename?: string;
78
+ yomifullname?: string;
79
+ statecode?: number;
80
+ statuscode?: number;
81
+ createdon?: string;
82
+ modifiedon?: string;
83
+ createdby?: string;
84
+ modifiedby?: string;
85
+ ownerid?: string;
86
+ transactioncurrencyid?: string;
87
+ }
88
+
89
+ /**
90
+ * Contact entity model class
91
+ */
92
+ export class Contact extends BaseEntity implements IContact {
93
+ contactid?: string;
94
+ firstname?: string;
95
+ lastname?: string;
96
+ fullname?: string;
97
+ middlename?: string;
98
+ salutation?: string;
99
+ suffix?: string;
100
+ jobtitle?: string;
101
+ emailaddress1?: string;
102
+ emailaddress2?: string;
103
+ emailaddress3?: string;
104
+ telephone1?: string;
105
+ telephone2?: string;
106
+ mobilephone?: string;
107
+ fax?: string;
108
+ pager?: string;
109
+ parentcustomerid?: string;
110
+ parentcustomerid_name?: string;
111
+ department?: string;
112
+ managerid?: string;
113
+ assistantname?: string;
114
+ assistantphone?: string;
115
+ birthdate?: string;
116
+ anniversary?: string;
117
+ gendercode?: number;
118
+ familystatuscode?: number;
119
+ haschildrencode?: boolean;
120
+ childrensnames?: string;
121
+ description?: string;
122
+ donotbulkemail?: boolean;
123
+ donotemail?: boolean;
124
+ donotfax?: boolean;
125
+ donotphone?: boolean;
126
+ donotpostalmail?: boolean;
127
+ donotsendmm?: boolean;
128
+ preferredcontactmethodcode?: number;
129
+ preferredappointmentdaycode?: number;
130
+ preferredappointmenttimecode?: number;
131
+ address1_addresstypecode?: string;
132
+ address1_city?: string;
133
+ address1_country?: string;
134
+ address1_county?: string;
135
+ address1_fax?: string;
136
+ address1_latitude?: number;
137
+ address1_line1?: string;
138
+ address1_line2?: string;
139
+ address1_line3?: string;
140
+ address1_longitude?: number;
141
+ address1_name?: string;
142
+ address1_postalcode?: string;
143
+ address1_postofficebox?: string;
144
+ address1_primarycontactname?: string;
145
+ address1_shippingmethodcode?: number;
146
+ address1_stateorprovince?: string;
147
+ address1_telephone1?: string;
148
+ address1_telephone2?: string;
149
+ address1_telephone3?: string;
150
+ address1_upszone?: string;
151
+ address1_utcoffset?: number;
152
+ creditlimit?: number;
153
+ creditonhold?: boolean;
154
+ paymenttermscode?: number;
155
+ customersizecode?: number;
156
+ leadsourcecode?: number;
157
+ websiteurl?: string;
158
+ governmentid?: string;
159
+ yomifirstname?: string;
160
+ yomilastname?: string;
161
+ yomimiddlename?: string;
162
+ yomifullname?: string;
163
+ statecode?: number;
164
+ statuscode?: number;
165
+ createdon?: string;
166
+ modifiedon?: string;
167
+ createdby?: string;
168
+ modifiedby?: string;
169
+ ownerid?: string;
170
+ transactioncurrencyid?: string;
171
+
172
+ /**
173
+ * Constructor for Contact entity
174
+ * @param contact The contact data
175
+ */
176
+ constructor(contact: IContact) {
177
+ super();
178
+ this.contactid = contact.contactid;
179
+ this.firstname = contact.firstname;
180
+ this.lastname = contact.lastname;
181
+ this.fullname = contact.fullname;
182
+ this.middlename = contact.middlename;
183
+ this.salutation = contact.salutation;
184
+ this.suffix = contact.suffix;
185
+ this.jobtitle = contact.jobtitle;
186
+ this.emailaddress1 = contact.emailaddress1;
187
+ this.emailaddress2 = contact.emailaddress2;
188
+ this.emailaddress3 = contact.emailaddress3;
189
+ this.telephone1 = contact.telephone1;
190
+ this.telephone2 = contact.telephone2;
191
+ this.mobilephone = contact.mobilephone;
192
+ this.fax = contact.fax;
193
+ this.pager = contact.pager;
194
+ this.parentcustomerid = contact.parentcustomerid;
195
+ this.parentcustomerid_name = contact.parentcustomerid_name;
196
+ this.department = contact.department;
197
+ this.managerid = contact.managerid;
198
+ this.assistantname = contact.assistantname;
199
+ this.assistantphone = contact.assistantphone;
200
+ this.birthdate = contact.birthdate;
201
+ this.anniversary = contact.anniversary;
202
+ this.gendercode = contact.gendercode;
203
+ this.familystatuscode = contact.familystatuscode;
204
+ this.haschildrencode = contact.haschildrencode;
205
+ this.childrensnames = contact.childrensnames;
206
+ this.description = contact.description;
207
+ this.donotbulkemail = contact.donotbulkemail;
208
+ this.donotemail = contact.donotemail;
209
+ this.donotfax = contact.donotfax;
210
+ this.donotphone = contact.donotphone;
211
+ this.donotpostalmail = contact.donotpostalmail;
212
+ this.donotsendmm = contact.donotsendmm;
213
+ this.preferredcontactmethodcode = contact.preferredcontactmethodcode;
214
+ this.preferredappointmentdaycode = contact.preferredappointmentdaycode;
215
+ this.preferredappointmenttimecode = contact.preferredappointmenttimecode;
216
+ this.address1_addresstypecode = contact.address1_addresstypecode;
217
+ this.address1_city = contact.address1_city;
218
+ this.address1_country = contact.address1_country;
219
+ this.address1_county = contact.address1_county;
220
+ this.address1_fax = contact.address1_fax;
221
+ this.address1_latitude = contact.address1_latitude;
222
+ this.address1_line1 = contact.address1_line1;
223
+ this.address1_line2 = contact.address1_line2;
224
+ this.address1_line3 = contact.address1_line3;
225
+ this.address1_longitude = contact.address1_longitude;
226
+ this.address1_name = contact.address1_name;
227
+ this.address1_postalcode = contact.address1_postalcode;
228
+ this.address1_postofficebox = contact.address1_postofficebox;
229
+ this.address1_primarycontactname = contact.address1_primarycontactname;
230
+ this.address1_shippingmethodcode = contact.address1_shippingmethodcode;
231
+ this.address1_stateorprovince = contact.address1_stateorprovince;
232
+ this.address1_telephone1 = contact.address1_telephone1;
233
+ this.address1_telephone2 = contact.address1_telephone2;
234
+ this.address1_telephone3 = contact.address1_telephone3;
235
+ this.address1_upszone = contact.address1_upszone;
236
+ this.address1_utcoffset = contact.address1_utcoffset;
237
+ this.creditlimit = contact.creditlimit;
238
+ this.creditonhold = contact.creditonhold;
239
+ this.paymenttermscode = contact.paymenttermscode;
240
+ this.customersizecode = contact.customersizecode;
241
+ this.leadsourcecode = contact.leadsourcecode;
242
+ this.websiteurl = contact.websiteurl;
243
+ this.governmentid = contact.governmentid;
244
+ this.yomifirstname = contact.yomifirstname;
245
+ this.yomilastname = contact.yomilastname;
246
+ this.yomimiddlename = contact.yomimiddlename;
247
+ this.yomifullname = contact.yomifullname;
248
+ this.statecode = contact.statecode;
249
+ this.statuscode = contact.statuscode;
250
+ this.createdon = contact.createdon;
251
+ this.modifiedon = contact.modifiedon;
252
+ this.createdby = contact.createdby;
253
+ this.modifiedby = contact.modifiedby;
254
+ this.ownerid = contact.ownerid;
255
+ this.transactioncurrencyid = contact.transactioncurrencyid;
256
+ }
257
+
258
+ /**
259
+ * Validates the contact instance
260
+ * @returns True if valid, throws error if invalid
261
+ */
262
+ validate(): boolean {
263
+ // At least one of firstname or lastname is required
264
+ if (
265
+ (!this.firstname || this.firstname.trim().length === 0) &&
266
+ (!this.lastname || this.lastname.trim().length === 0)
267
+ ) {
268
+ throw new Error('Contact must have either first name or last name');
269
+ }
270
+
271
+ if (this.firstname && this.firstname.length > 50) {
272
+ throw new Error('First name cannot exceed 50 characters');
273
+ }
274
+
275
+ if (this.lastname && this.lastname.length > 50) {
276
+ throw new Error('Last name cannot exceed 50 characters');
277
+ }
278
+
279
+ if (this.middlename && this.middlename.length > 100) {
280
+ throw new Error('Middle name cannot exceed 100 characters');
281
+ }
282
+
283
+ if (this.jobtitle && this.jobtitle.length > 100) {
284
+ throw new Error('Job title cannot exceed 100 characters');
285
+ }
286
+
287
+ if (this.emailaddress1 && !this.isValidEmail(this.emailaddress1)) {
288
+ throw new Error('Invalid primary email address format');
289
+ }
290
+
291
+ if (this.emailaddress2 && !this.isValidEmail(this.emailaddress2)) {
292
+ throw new Error('Invalid secondary email address format');
293
+ }
294
+
295
+ if (this.emailaddress3 && !this.isValidEmail(this.emailaddress3)) {
296
+ throw new Error('Invalid tertiary email address format');
297
+ }
298
+
299
+ if (this.websiteurl && !this.isValidUrl(this.websiteurl)) {
300
+ throw new Error('Invalid website URL format');
301
+ }
302
+
303
+ if (this.birthdate && !this.isValidDate(this.birthdate)) {
304
+ throw new Error('Invalid birth date format');
305
+ }
306
+
307
+ if (this.anniversary && !this.isValidDate(this.anniversary)) {
308
+ throw new Error('Invalid anniversary date format');
309
+ }
310
+
311
+ if (this.creditlimit !== undefined && this.creditlimit < 0) {
312
+ throw new Error('Credit limit cannot be negative');
313
+ }
314
+
315
+ return true;
316
+ }
317
+
318
+ /**
319
+ * Creates a new contact record
320
+ * @param apiService The API service instance
321
+ * @param contact The contact to create
322
+ * @returns Promise resolving to the created contact
323
+ */
324
+ public static async create(
325
+ apiService: IApiService,
326
+ contact: Contact
327
+ ): Promise<Contact> {
328
+ const loggerContext = 'Contact.create';
329
+ console.log(`${loggerContext}: Creating new contact`, {
330
+ firstname: contact.firstname,
331
+ lastname: contact.lastname,
332
+ });
333
+
334
+ return await this.createEntity<Contact>(
335
+ apiService,
336
+ contact,
337
+ ContactConstants.EntityCollectionName,
338
+ loggerContext
339
+ );
340
+ }
341
+
342
+ /**
343
+ * Updates an existing contact record
344
+ * @param apiService The API service instance
345
+ * @param contact The contact to update
346
+ * @returns Promise resolving to the updated contact
347
+ */
348
+ public static async update(
349
+ apiService: IApiService,
350
+ contact: Contact
351
+ ): Promise<Contact> {
352
+ const loggerContext = 'Contact.update';
353
+
354
+ if (!contact.contactid) {
355
+ throw new Error('Contact ID is required for update');
356
+ }
357
+
358
+ console.log(`${loggerContext}: Updating contact`, {
359
+ contactid: contact.contactid,
360
+ firstname: contact.firstname,
361
+ lastname: contact.lastname,
362
+ });
363
+
364
+ return await this.updateEntity<Contact>(
365
+ apiService,
366
+ contact.contactid,
367
+ contact,
368
+ ContactConstants.EntityCollectionName,
369
+ ContactConstants.PrimaryKey,
370
+ loggerContext
371
+ );
372
+ }
373
+
374
+ /**
375
+ * Deletes a contact record
376
+ * @param apiService The API service instance
377
+ * @param contactId The ID of the contact to delete
378
+ * @returns Promise resolving when deletion is complete
379
+ */
380
+ public static async delete(
381
+ apiService: IApiService,
382
+ contactId: string
383
+ ): Promise<void> {
384
+ const loggerContext = 'Contact.delete';
385
+ console.log(`${loggerContext}: Deleting contact`, { contactid: contactId });
386
+
387
+ return await this.deleteEntity(
388
+ apiService,
389
+ contactId,
390
+ ContactConstants.EntityCollectionName,
391
+ loggerContext
392
+ );
393
+ }
394
+
395
+ /**
396
+ * Retrieves contacts based on a filter
397
+ * @param apiService The API service instance
398
+ * @param filter Optional FetchXML filter string
399
+ * @returns Promise resolving to an array of contacts
400
+ */
401
+ public static async retrieveByFilter(
402
+ apiService: IApiService,
403
+ filter?: string
404
+ ): Promise<Contact[]> {
405
+ const loggerContext = 'Contact.retrieveByFilter';
406
+ console.log(`${loggerContext}: Retrieving contacts with filter`, {
407
+ filter,
408
+ });
409
+
410
+ const attributes = [
411
+ ContactConstants.PrimaryKey,
412
+ ContactConstants.FirstName,
413
+ ContactConstants.LastName,
414
+ ContactConstants.PrimaryName,
415
+ ContactConstants.JobTitle,
416
+ ContactConstants.EMailAddress1,
417
+ ContactConstants.BusinessPhone,
418
+ ContactConstants.MobilePhone,
419
+ ContactConstants.CompanyName,
420
+ ContactConstants.Department,
421
+ ContactConstants.Address1_City,
422
+ ContactConstants.Address1_StateOrProvince,
423
+ ContactConstants.Address1_Country,
424
+ ContactConstants.StateCode,
425
+ ContactConstants.StatusCode,
426
+ ContactConstants.CreatedOn,
427
+ ContactConstants.ModifiedOn,
428
+ ];
429
+
430
+ const fetchXml = this.buildFetchXml(
431
+ ContactConstants.EntityName,
432
+ attributes,
433
+ filter,
434
+ { attribute: ContactConstants.LastName }
435
+ );
436
+
437
+ return await this.retrieveEntitiesByFilter<Contact>(
438
+ apiService,
439
+ ContactConstants.EntityCollectionName,
440
+ fetchXml,
441
+ Contact,
442
+ loggerContext
443
+ );
444
+ }
445
+
446
+ /**
447
+ * Retrieves a single contact by ID
448
+ * @param apiService The API service instance
449
+ * @param contactId The contact ID
450
+ * @returns Promise resolving to the contact or null if not found
451
+ */
452
+ public static async retrieveById(
453
+ apiService: IApiService,
454
+ contactId: string
455
+ ): Promise<Contact | null> {
456
+ const loggerContext = 'Contact.retrieveById';
457
+ console.log(`${loggerContext}: Retrieving contact by ID`, {
458
+ contactid: contactId,
459
+ });
460
+
461
+ const filter = `<filter type="and">
462
+ <condition attribute="${ContactConstants.PrimaryKey}" operator="eq" value="${this.escapeXml(contactId)}" />
463
+ </filter>`;
464
+
465
+ const contacts = await this.retrieveByFilter(apiService, filter);
466
+ return contacts.length > 0 ? contacts[0] : null;
467
+ }
468
+
469
+ /**
470
+ * Retrieves contacts by name (partial match on first or last name)
471
+ * @param apiService The API service instance
472
+ * @param name The name to search for
473
+ * @returns Promise resolving to an array of matching contacts
474
+ */
475
+ public static async retrieveByName(
476
+ apiService: IApiService,
477
+ name: string
478
+ ): Promise<Contact[]> {
479
+ const loggerContext = 'Contact.retrieveByName';
480
+ console.log(`${loggerContext}: Retrieving contacts by name`, { name });
481
+
482
+ const filter = `<filter type="or">
483
+ <condition attribute="${ContactConstants.FirstName}" operator="like" value="%${this.escapeXml(name)}%" />
484
+ <condition attribute="${ContactConstants.LastName}" operator="like" value="%${this.escapeXml(name)}%" />
485
+ <condition attribute="${ContactConstants.PrimaryName}" operator="like" value="%${this.escapeXml(name)}%" />
486
+ </filter>`;
487
+
488
+ return await this.retrieveByFilter(apiService, filter);
489
+ }
490
+
491
+ /**
492
+ * Retrieves contacts by email address
493
+ * @param apiService The API service instance
494
+ * @param email The email address to search for
495
+ * @returns Promise resolving to an array of matching contacts
496
+ */
497
+ public static async retrieveByEmail(
498
+ apiService: IApiService,
499
+ email: string
500
+ ): Promise<Contact[]> {
501
+ const loggerContext = 'Contact.retrieveByEmail';
502
+ console.log(`${loggerContext}: Retrieving contacts by email`, { email });
503
+
504
+ const filter = `<filter type="or">
505
+ <condition attribute="${ContactConstants.EMailAddress1}" operator="eq" value="${this.escapeXml(email)}" />
506
+ <condition attribute="${ContactConstants.EMailAddress2}" operator="eq" value="${this.escapeXml(email)}" />
507
+ <condition attribute="${ContactConstants.EMailAddress3}" operator="eq" value="${this.escapeXml(email)}" />
508
+ </filter>`;
509
+
510
+ return await this.retrieveByFilter(apiService, filter);
511
+ }
512
+
513
+ /**
514
+ * Retrieves contacts by account (parent customer)
515
+ * @param apiService The API service instance
516
+ * @param accountId The account ID
517
+ * @returns Promise resolving to an array of contacts for the account
518
+ */
519
+ public static async retrieveByAccount(
520
+ apiService: IApiService,
521
+ accountId: string
522
+ ): Promise<Contact[]> {
523
+ const loggerContext = 'Contact.retrieveByAccount';
524
+ console.log(`${loggerContext}: Retrieving contacts by account`, {
525
+ accountId,
526
+ });
527
+
528
+ const filter = `<filter type="and">
529
+ <condition attribute="${ContactConstants.ParentCustomerId}" operator="eq" value="${this.escapeXml(accountId)}" />
530
+ </filter>`;
531
+
532
+ return await this.retrieveByFilter(apiService, filter);
533
+ }
534
+
535
+ /**
536
+ * Retrieves active contacts only
537
+ * @param apiService The API service instance
538
+ * @returns Promise resolving to an array of active contacts
539
+ */
540
+ public static async retrieveActiveContacts(
541
+ apiService: IApiService
542
+ ): Promise<Contact[]> {
543
+ const loggerContext = 'Contact.retrieveActiveContacts';
544
+ console.log(`${loggerContext}: Retrieving active contacts`);
545
+
546
+ const filter = `<filter type="and">
547
+ <condition attribute="${ContactConstants.StateCode}" operator="eq" value="0" />
548
+ </filter>`;
549
+
550
+ return await this.retrieveByFilter(apiService, filter);
551
+ }
552
+
553
+ /**
554
+ * Helper method to validate email format
555
+ */
556
+ private isValidEmail(email: string): boolean {
557
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
558
+ return emailRegex.test(email);
559
+ }
560
+
561
+ /**
562
+ * Helper method to validate URL format
563
+ */
564
+ private isValidUrl(url: string): boolean {
565
+ try {
566
+ new URL(url);
567
+ return true;
568
+ } catch {
569
+ return false;
570
+ }
571
+ }
572
+
573
+ /**
574
+ * Helper method to validate date format
575
+ */
576
+ private isValidDate(dateString: string): boolean {
577
+ const date = new Date(dateString);
578
+ return !isNaN(date.getTime());
579
+ }
580
+ }