@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,700 @@
1
+ # Coding Standards
2
+
3
+ ## Table of Contents
4
+
5
+ 1. [General Principles](#general-principles)
6
+ 2. [TypeScript Standards](#typescript-standards)
7
+ 3. [React Component Standards](#react-component-standards)
8
+ 4. [Entity Model Standards](#entity-model-standards)
9
+ 5. [CSS and Styling Standards](#css-and-styling-standards)
10
+ 6. [API and Service Standards](#api-and-service-standards)
11
+ 7. [Error Handling Standards](#error-handling-standards)
12
+ 8. [Testing Standards](#testing-standards)
13
+ 9. [Documentation Standards](#documentation-standards)
14
+ 10. [File Organization](#file-organization)
15
+
16
+ ## General Principles
17
+
18
+ ### Code Quality
19
+
20
+ - **Readability**: Code should be self-documenting
21
+ - **Consistency**: Follow established patterns throughout the codebase
22
+ - **Simplicity**: Choose simple solutions over complex ones
23
+ - **Maintainability**: Write code that's easy to modify and extend
24
+ - **Performance**: Consider performance implications of coding decisions
25
+
26
+ ### Best Practices
27
+
28
+ - Use meaningful variable and function names
29
+ - Keep functions small and focused (single responsibility)
30
+ - Avoid deep nesting (max 3 levels)
31
+ - Use early returns to reduce complexity
32
+ - Prefer composition over inheritance
33
+ - Follow DRY (Don't Repeat Yourself) principle
34
+
35
+ ## TypeScript Standards
36
+
37
+ ### Type Definitions
38
+
39
+ ```typescript
40
+ // ✅ Good: Explicit interface definitions
41
+ interface IAccount {
42
+ id: string;
43
+ name: string;
44
+ revenue?: number;
45
+ createdOn: Date;
46
+ }
47
+
48
+ // ❌ Bad: Using 'any' type
49
+ function processData(data: any): any {
50
+ return data.something;
51
+ }
52
+
53
+ // ✅ Good: Proper type guards
54
+ function isAccount(obj: unknown): obj is IAccount {
55
+ return typeof obj === 'object' && obj !== null && 'id' in obj && 'name' in obj;
56
+ }
57
+ ```
58
+
59
+ ### Naming Conventions
60
+
61
+ ```typescript
62
+ // Interfaces: PascalCase with 'I' prefix
63
+ interface IApiService {}
64
+
65
+ // Classes: PascalCase
66
+ class AccountService {}
67
+
68
+ // Functions and variables: camelCase
69
+ const fetchAccountData = async () => {};
70
+
71
+ // Constants: SCREAMING_SNAKE_CASE
72
+ const MAX_RETRY_ATTEMPTS = 3;
73
+
74
+ // Enums: PascalCase
75
+ enum EntityState {
76
+ Active = 0,
77
+ Inactive = 1,
78
+ }
79
+ ```
80
+
81
+ ### Generic Types
82
+
83
+ ```typescript
84
+ // ✅ Good: Descriptive generic constraints
85
+ interface IRepository<T extends BaseEntity> {
86
+ create(entity: T): Promise<T>;
87
+ update(id: string, entity: Partial<T>): Promise<T>;
88
+ delete(id: string): Promise<void>;
89
+ }
90
+
91
+ // ✅ Good: Default generic parameters
92
+ interface IApiResponse<T = unknown> {
93
+ data: T;
94
+ success: boolean;
95
+ message?: string;
96
+ }
97
+ ```
98
+
99
+ ## React Component Standards
100
+
101
+ ### Component Structure
102
+
103
+ ```typescript
104
+ // ✅ Good: Functional component with proper typing
105
+ interface AccountFormProps {
106
+ account?: Account;
107
+ onSave: (account: Account) => Promise<void>;
108
+ onCancel: () => void;
109
+ readOnly?: boolean;
110
+ }
111
+
112
+ export const AccountForm: React.FC<AccountFormProps> = ({
113
+ account,
114
+ onSave,
115
+ onCancel,
116
+ readOnly = false
117
+ }) => {
118
+ // State declarations
119
+ const [formData, setFormData] = useState<Partial<IAccount>>({});
120
+ const [loading, setLoading] = useState(false);
121
+ const [errors, setErrors] = useState<string[]>([]);
122
+
123
+ // Effects
124
+ useEffect(() => {
125
+ if (account) {
126
+ setFormData(account);
127
+ }
128
+ }, [account]);
129
+
130
+ // Event handlers
131
+ const handleSubmit = async (event: React.FormEvent) => {
132
+ event.preventDefault();
133
+ // Implementation
134
+ };
135
+
136
+ // Render
137
+ return (
138
+ <form onSubmit={handleSubmit}>
139
+ {/* JSX content */}
140
+ </form>
141
+ );
142
+ };
143
+ ```
144
+
145
+ ### Hooks Usage
146
+
147
+ ```typescript
148
+ // ✅ Good: Custom hooks for reusable logic
149
+ const useAccountData = (accountId: string) => {
150
+ const [account, setAccount] = useState<Account | null>(null);
151
+ const [loading, setLoading] = useState(true);
152
+ const [error, setError] = useState<string | null>(null);
153
+
154
+ useEffect(() => {
155
+ const loadAccount = async () => {
156
+ try {
157
+ setLoading(true);
158
+ const data = await AccountService.getById(accountId);
159
+ setAccount(data);
160
+ } catch (err) {
161
+ setError(err instanceof Error ? err.message : 'Unknown error');
162
+ } finally {
163
+ setLoading(false);
164
+ }
165
+ };
166
+
167
+ loadAccount();
168
+ }, [accountId]);
169
+
170
+ return { account, loading, error };
171
+ };
172
+ ```
173
+
174
+ ### Event Handlers
175
+
176
+ ```typescript
177
+ // ✅ Good: Properly typed event handlers
178
+ const handleInputChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
179
+ const { name, value } = event.target;
180
+ setFormData((prev) => ({ ...prev, [name]: value }));
181
+ }, []);
182
+
183
+ const handleDropdownChange = useCallback(
184
+ (_: React.FormEvent<HTMLDivElement>, option?: IDropdownOption) => {
185
+ if (option) {
186
+ setSelectedValue(option.key as string);
187
+ }
188
+ },
189
+ []
190
+ );
191
+ ```
192
+
193
+ ## Entity Model Standards
194
+
195
+ ### Entity Class Structure
196
+
197
+ ```typescript
198
+ // ✅ Good: Well-structured entity class
199
+ export class Account extends BaseEntity implements IAccount {
200
+ // Properties with proper typing
201
+ public id: string = '';
202
+ public name: string = '';
203
+ public revenue?: number;
204
+ public createdOn?: Date;
205
+ public modifiedOn?: Date;
206
+
207
+ constructor(data?: Partial<IAccount>) {
208
+ super();
209
+ if (data) {
210
+ Object.assign(this, data);
211
+ }
212
+ }
213
+
214
+ // Static factory methods
215
+ public static async create(apiService: IApiService, account: Account): Promise<Account> {
216
+ try {
217
+ logger.logInfo(`Creating account: ${account.name}`);
218
+
219
+ const validation = account.validate();
220
+ if (!validation.isValid) {
221
+ throw new Error(`Validation failed: ${validation.errors.join(', ')}`);
222
+ }
223
+
224
+ const result = await apiService.createRecord(
225
+ AccountConstants.EntityLogicalName,
226
+ account.toCreateData()
227
+ );
228
+
229
+ return new Account({
230
+ ...account,
231
+ id: result.id,
232
+ });
233
+ } catch (error) {
234
+ logger.logError('Error creating account:', error);
235
+ throw error;
236
+ }
237
+ }
238
+
239
+ // Instance methods
240
+ public validate(): ValidationResult {
241
+ const errors: string[] = [];
242
+
243
+ if (!this.name?.trim()) {
244
+ errors.push('Name is required');
245
+ }
246
+
247
+ if (this.revenue !== undefined && this.revenue < 0) {
248
+ errors.push('Revenue cannot be negative');
249
+ }
250
+
251
+ return {
252
+ isValid: errors.length === 0,
253
+ errors,
254
+ };
255
+ }
256
+
257
+ private toCreateData(): Partial<IAccount> {
258
+ return {
259
+ name: this.name,
260
+ revenue: this.revenue,
261
+ };
262
+ }
263
+ }
264
+ ```
265
+
266
+ ### Constants Structure
267
+
268
+ ```typescript
269
+ // ✅ Good: Comprehensive constants class
270
+ export class AccountConstants {
271
+ // Entity metadata
272
+ public static readonly EntityLogicalName = 'account';
273
+ public static readonly EntityCollectionName = 'accounts';
274
+ public static readonly PrimaryIdAttribute = 'accountid';
275
+ public static readonly PrimaryNameAttribute = 'name';
276
+
277
+ // Field names with metadata comments
278
+ /** Type: String, RequiredLevel: ApplicationRequired, MaxLength: 160 */
279
+ public static readonly Name = 'name';
280
+
281
+ /** Type: Money, RequiredLevel: None */
282
+ public static readonly Revenue = 'revenue';
283
+
284
+ /** Type: DateTime, ReadOnly: true */
285
+ public static readonly CreatedOn = 'createdon';
286
+
287
+ // Option set values
288
+ public static readonly StateCodeOptions = {
289
+ Active: 0,
290
+ Inactive: 1,
291
+ } as const;
292
+
293
+ // Display names for UI
294
+ public static readonly FieldDisplayNames = {
295
+ [this.Name]: 'Account Name',
296
+ [this.Revenue]: 'Annual Revenue',
297
+ [this.CreatedOn]: 'Created On',
298
+ } as const;
299
+
300
+ // Required fields array
301
+ public static readonly RequiredFields = [this.Name] as const;
302
+ }
303
+ ```
304
+
305
+ ## CSS and Styling Standards
306
+
307
+ ### CSS Modules
308
+
309
+ ```css
310
+ /* ✅ Good: BEM-like naming with CSS modules */
311
+ .accountForm {
312
+ padding: 20px;
313
+ max-width: 600px;
314
+ }
315
+
316
+ .accountForm__field {
317
+ margin-bottom: 16px;
318
+ }
319
+
320
+ .accountForm__field--required {
321
+ position: relative;
322
+ }
323
+
324
+ .accountForm__field--required::after {
325
+ content: '*';
326
+ color: #d13438;
327
+ margin-left: 4px;
328
+ }
329
+
330
+ .accountForm__actions {
331
+ display: flex;
332
+ justify-content: flex-end;
333
+ gap: 12px;
334
+ margin-top: 24px;
335
+ }
336
+
337
+ /* Responsive design */
338
+ @media (max-width: 768px) {
339
+ .accountForm {
340
+ padding: 16px;
341
+ max-width: 100%;
342
+ }
343
+
344
+ .accountForm__actions {
345
+ flex-direction: column;
346
+ }
347
+ }
348
+ ```
349
+
350
+ ### Styling Conventions
351
+
352
+ - Use CSS Modules for component-specific styles
353
+ - Follow BEM-like naming convention
354
+ - Use semantic class names
355
+ - Implement responsive design principles
356
+ - Use CSS custom properties for theming
357
+ - Avoid inline styles except for dynamic values
358
+
359
+ ## API and Service Standards
360
+
361
+ ### Service Interface
362
+
363
+ ```typescript
364
+ // ✅ Good: Well-defined service interface
365
+ interface IApiService {
366
+ createRecord(entityName: string, data: Record<string, unknown>): Promise<ApiResponse>;
367
+ updateRecord(entityName: string, id: string, data: Record<string, unknown>): Promise<void>;
368
+ deleteRecord(entityName: string, id: string): Promise<void>;
369
+ retrieveRecord(entityName: string, id: string, fields?: string[]): Promise<ApiResponse>;
370
+ retrieveMultipleRecords(entityName: string, fetchXml: string): Promise<ApiResponse>;
371
+ }
372
+ ```
373
+
374
+ ### Service Implementation
375
+
376
+ ```typescript
377
+ // ✅ Good: Proper error handling and logging
378
+ export class XrmApiService implements IApiService {
379
+ private readonly baseUrl: string;
380
+ private readonly timeout: number;
381
+
382
+ constructor(config: ApiConfig) {
383
+ this.baseUrl = config.baseUrl;
384
+ this.timeout = config.timeout || 30000;
385
+ }
386
+
387
+ public async createRecord(
388
+ entityName: string,
389
+ data: Record<string, unknown>
390
+ ): Promise<ApiResponse> {
391
+ try {
392
+ logger.logInfo(`Creating ${entityName} record`);
393
+
394
+ const response = await this.makeRequest('POST', `${entityName}`, data);
395
+
396
+ logger.logInfo(`Successfully created ${entityName} record`);
397
+ return response;
398
+ } catch (error) {
399
+ logger.logError(`Error creating ${entityName} record:`, error);
400
+ throw this.handleApiError(error);
401
+ }
402
+ }
403
+
404
+ private async makeRequest(
405
+ method: string,
406
+ endpoint: string,
407
+ data?: Record<string, unknown>
408
+ ): Promise<ApiResponse> {
409
+ const url = `${this.baseUrl}/${endpoint}`;
410
+ const options: RequestInit = {
411
+ method,
412
+ headers: {
413
+ 'Content-Type': 'application/json',
414
+ Accept: 'application/json',
415
+ },
416
+ signal: AbortSignal.timeout(this.timeout),
417
+ };
418
+
419
+ if (data) {
420
+ options.body = JSON.stringify(data);
421
+ }
422
+
423
+ const response = await fetch(url, options);
424
+
425
+ if (!response.ok) {
426
+ throw new ApiError(`HTTP ${response.status}: ${response.statusText}`);
427
+ }
428
+
429
+ return response.json();
430
+ }
431
+
432
+ private handleApiError(error: unknown): Error {
433
+ if (error instanceof ApiError) {
434
+ return error;
435
+ }
436
+
437
+ if (error instanceof Error) {
438
+ return new ApiError(`API request failed: ${error.message}`);
439
+ }
440
+
441
+ return new ApiError('Unknown API error occurred');
442
+ }
443
+ }
444
+ ```
445
+
446
+ ## Error Handling Standards
447
+
448
+ ### Error Types
449
+
450
+ ```typescript
451
+ // ✅ Good: Custom error classes
452
+ export class ValidationError extends Error {
453
+ constructor(
454
+ message: string,
455
+ public readonly field: string,
456
+ public readonly value: unknown
457
+ ) {
458
+ super(message);
459
+ this.name = 'ValidationError';
460
+ }
461
+ }
462
+
463
+ export class ApiError extends Error {
464
+ constructor(
465
+ message: string,
466
+ public readonly statusCode?: number,
467
+ public readonly response?: unknown
468
+ ) {
469
+ super(message);
470
+ this.name = 'ApiError';
471
+ }
472
+ }
473
+ ```
474
+
475
+ ### Error Handling Patterns
476
+
477
+ ```typescript
478
+ // ✅ Good: Comprehensive error handling
479
+ const saveAccount = async (account: Account): Promise<void> => {
480
+ try {
481
+ // Validate input
482
+ const validation = account.validate();
483
+ if (!validation.isValid) {
484
+ throw new ValidationError(validation.errors.join(', '), 'account', account);
485
+ }
486
+
487
+ // Attempt save
488
+ await account.save(apiService);
489
+
490
+ // Log success
491
+ logger.logInfo(`Account saved successfully: ${account.name}`);
492
+ } catch (error) {
493
+ // Log error with context
494
+ logger.logError('Failed to save account:', {
495
+ error,
496
+ accountName: account.name,
497
+ accountId: account.id,
498
+ });
499
+
500
+ // Re-throw with user-friendly message
501
+ if (error instanceof ValidationError) {
502
+ throw new Error(`Validation failed: ${error.message}`);
503
+ } else if (error instanceof ApiError) {
504
+ throw new Error('Unable to save account. Please try again.');
505
+ } else {
506
+ throw new Error('An unexpected error occurred.');
507
+ }
508
+ }
509
+ };
510
+ ```
511
+
512
+ ## Testing Standards
513
+
514
+ ### Unit Test Structure
515
+
516
+ ```typescript
517
+ // ✅ Good: Comprehensive unit tests
518
+ describe('Account', () => {
519
+ let mockApiService: jest.Mocked<IApiService>;
520
+
521
+ beforeEach(() => {
522
+ mockApiService = {
523
+ createRecord: jest.fn(),
524
+ updateRecord: jest.fn(),
525
+ deleteRecord: jest.fn(),
526
+ retrieveRecord: jest.fn(),
527
+ retrieveMultipleRecords: jest.fn(),
528
+ };
529
+ });
530
+
531
+ describe('validation', () => {
532
+ it('should require name field', () => {
533
+ // Arrange
534
+ const account = new Account({ name: '' });
535
+
536
+ // Act
537
+ const result = account.validate();
538
+
539
+ // Assert
540
+ expect(result.isValid).toBe(false);
541
+ expect(result.errors).toContain('Name is required');
542
+ });
543
+
544
+ it('should not allow negative revenue', () => {
545
+ // Arrange
546
+ const account = new Account({
547
+ name: 'Test Account',
548
+ revenue: -1000,
549
+ });
550
+
551
+ // Act
552
+ const result = account.validate();
553
+
554
+ // Assert
555
+ expect(result.isValid).toBe(false);
556
+ expect(result.errors).toContain('Revenue cannot be negative');
557
+ });
558
+ });
559
+
560
+ describe('create', () => {
561
+ it('should create account successfully', async () => {
562
+ // Arrange
563
+ const accountData = { name: 'Test Account', revenue: 100000 };
564
+ const account = new Account(accountData);
565
+
566
+ mockApiService.createRecord.mockResolvedValue({
567
+ data: { id: 'test-id', ...accountData },
568
+ success: true,
569
+ });
570
+
571
+ // Act
572
+ const result = await Account.create(mockApiService, account);
573
+
574
+ // Assert
575
+ expect(mockApiService.createRecord).toHaveBeenCalledWith(
576
+ AccountConstants.EntityLogicalName,
577
+ expect.objectContaining(accountData)
578
+ );
579
+ expect(result.id).toBe('test-id');
580
+ expect(result.name).toBe('Test Account');
581
+ });
582
+ });
583
+ });
584
+ ```
585
+
586
+ ## Documentation Standards
587
+
588
+ ### JSDoc Comments
589
+
590
+ ````typescript
591
+ /**
592
+ * Represents an Account entity in Dynamics 365
593
+ *
594
+ * @example
595
+ * ```typescript
596
+ * const account = new Account({ name: 'ACME Corp', revenue: 100000 });
597
+ * await Account.create(apiService, account);
598
+ * ```
599
+ */
600
+ export class Account extends BaseEntity {
601
+ /**
602
+ * Creates a new account in Dynamics 365
603
+ *
604
+ * @param apiService - The API service instance
605
+ * @param account - The account data to create
606
+ * @returns Promise resolving to the created account
607
+ * @throws {ValidationError} When account data is invalid
608
+ * @throws {ApiError} When API request fails
609
+ */
610
+ public static async create(apiService: IApiService, account: Account): Promise<Account> {
611
+ // Implementation
612
+ }
613
+
614
+ /**
615
+ * Validates the account data
616
+ *
617
+ * @returns Validation result with isValid flag and error messages
618
+ */
619
+ public validate(): ValidationResult {
620
+ // Implementation
621
+ }
622
+ }
623
+ ````
624
+
625
+ ## File Organization
626
+
627
+ ### Directory Structure
628
+
629
+ ```
630
+ src/
631
+ ├── components/ # React components
632
+ │ ├── AccountForm/
633
+ │ │ ├── AccountForm.tsx
634
+ │ │ ├── AccountForm.css
635
+ │ │ ├── AccountForm.test.tsx
636
+ │ │ └── index.ts
637
+ │ └── shared/ # Shared components
638
+ ├── models/ # Entity models
639
+ │ ├── BaseEntity.ts
640
+ │ ├── Account.ts
641
+ │ └── Contact.ts
642
+ ├── constants/ # Entity constants
643
+ │ ├── account.ts
644
+ │ └── contact.ts
645
+ ├── services/ # API services
646
+ │ ├── IApiService.ts
647
+ │ ├── XrmApiService.ts
648
+ │ └── MockApiService.ts
649
+ ├── utils/ # Utility functions
650
+ ├── types/ # Type definitions
651
+ ├── hooks/ # Custom React hooks
652
+ └── providers/ # React context providers
653
+ ```
654
+
655
+ ### Import Order
656
+
657
+ ```typescript
658
+ // ✅ Good: Organized imports
659
+ // 1. Node modules
660
+ import React, { useState, useEffect } from 'react';
661
+ import { IColumn, DetailsList } from '@fluentui/react';
662
+
663
+ // 2. Internal modules (absolute paths)
664
+ import { Account } from '../models/Account';
665
+ import { AccountConstants } from '../constants/account';
666
+ import { useApiService } from '../providers/DynamicsProvider';
667
+
668
+ // 3. Relative imports
669
+ import './AccountList.css';
670
+ ```
671
+
672
+ ### Naming Conventions
673
+
674
+ - **Files**: PascalCase for components, camelCase for utilities
675
+ - **Directories**: kebab-case or camelCase consistently
676
+ - **Test files**: Same name as source file with `.test.ts` suffix
677
+ - **Type files**: Same name as source file with `.types.ts` suffix
678
+
679
+ ## Code Review Requirements
680
+
681
+ ### Before Submitting
682
+
683
+ - [ ] All TypeScript errors resolved
684
+ - [ ] All ESLint warnings addressed
685
+ - [ ] Unit tests added/updated
686
+ - [ ] Documentation updated
687
+ - [ ] Manual testing completed
688
+
689
+ ### Review Checklist
690
+
691
+ - [ ] Code follows established patterns
692
+ - [ ] Proper error handling implemented
693
+ - [ ] Performance considerations addressed
694
+ - [ ] Security best practices followed
695
+ - [ ] Accessibility requirements met
696
+
697
+ ---
698
+
699
+ These standards ensure consistency, maintainability, and quality across all Dynamics 365 consulting
700
+ projects.