@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.
- package/bin/create-dynamics-app.js +1 -1
- package/dist/index.js +140 -15
- package/dist/index.js.map +1 -1
- package/dist/utils/consultingHelpers.d.ts +13 -0
- package/dist/utils/consultingHelpers.d.ts.map +1 -0
- package/dist/utils/consultingHelpers.js +569 -0
- package/dist/utils/consultingHelpers.js.map +1 -0
- package/dist/utils/copyTemplate.d.ts.map +1 -1
- package/dist/utils/copyTemplate.js.map +1 -1
- package/dist/utils/initGit.d.ts.map +1 -1
- package/dist/utils/initGit.js.map +1 -1
- package/dist/utils/installDependencies.d.ts.map +1 -1
- package/dist/utils/installDependencies.js +3 -2
- package/dist/utils/installDependencies.js.map +1 -1
- package/dist/utils/updatePackageJson.d.ts +1 -1
- package/dist/utils/updatePackageJson.d.ts.map +1 -1
- package/dist/utils/updatePackageJson.js +11 -1
- package/dist/utils/updatePackageJson.js.map +1 -1
- package/package.json +1 -1
- package/templates/dynamics-365-starter/INTEGRATION_TEST_RESULTS.md +302 -0
- package/templates/dynamics-365-starter/PHASE_4_COMPLETION_SUMMARY.md +305 -0
- package/templates/dynamics-365-starter/README.md +566 -137
- package/templates/dynamics-365-starter/deployment/QUICKSTART-MAC.md +507 -0
- package/templates/dynamics-365-starter/deployment/QUICKSTART-WINDOWS.md +372 -0
- package/templates/dynamics-365-starter/deployment/README.md +484 -0
- package/templates/dynamics-365-starter/deployment/pipelines/README.md +375 -0
- package/templates/dynamics-365-starter/deployment/pipelines/azure-pipelines.yml +330 -0
- package/templates/dynamics-365-starter/deployment/pipelines/github-actions.yml +422 -0
- package/templates/dynamics-365-starter/deployment/pipelines/jenkins.groovy +636 -0
- package/templates/dynamics-365-starter/deployment/scripts/deploy.ps1 +417 -0
- package/templates/dynamics-365-starter/deployment/scripts/deploy.sh +582 -0
- package/templates/dynamics-365-starter/deployment/scripts/team-onboarding.ps1 +486 -0
- package/templates/dynamics-365-starter/deployment/scripts/team-onboarding.sh +567 -0
- package/templates/dynamics-365-starter/deployment/scripts/validate-setup.ps1 +703 -0
- package/templates/dynamics-365-starter/deployment/scripts/validate-setup.sh +671 -0
- package/templates/dynamics-365-starter/docs/ARCHITECTURE_OVERVIEW.md +506 -0
- package/templates/dynamics-365-starter/docs/BEST_PRACTICES.md +723 -0
- package/templates/dynamics-365-starter/docs/MIGRATION_GUIDE.md +447 -0
- package/templates/dynamics-365-starter/docs/team-standards/README.md +273 -0
- package/templates/dynamics-365-starter/docs/team-standards/client-onboarding.md +577 -0
- package/templates/dynamics-365-starter/docs/team-standards/code-review-checklist.md +359 -0
- package/templates/dynamics-365-starter/docs/team-standards/coding-standards.md +700 -0
- package/templates/dynamics-365-starter/docs/team-standards/cross-platform-team-guide.md +736 -0
- package/templates/dynamics-365-starter/docs/team-standards/development-workflows.md +727 -0
- package/templates/dynamics-365-starter/docs/troubleshooting/common-errors.md +758 -0
- package/templates/dynamics-365-starter/docs/troubleshooting/platform-specific-issues.md +878 -0
- package/templates/dynamics-365-starter/package.json +22 -1
- package/templates/dynamics-365-starter/public/index.html +8 -11
- package/templates/dynamics-365-starter/scripts/custom-build.js +255 -0
- package/templates/dynamics-365-starter/src/client-project-template/README.md +234 -0
- package/templates/dynamics-365-starter/src/client-project-template/config/client.template.json +114 -0
- package/templates/dynamics-365-starter/src/client-project-template/config/environments/template.json +186 -0
- package/templates/dynamics-365-starter/src/client-project-template/scripts/client-setup.js +667 -0
- package/templates/dynamics-365-starter/src/components/AccountForm.css +71 -0
- package/templates/dynamics-365-starter/src/components/AccountForm.tsx +541 -0
- package/templates/dynamics-365-starter/src/components/AccountManagement.css +86 -0
- package/templates/dynamics-365-starter/src/components/AccountManagement.tsx +370 -0
- package/templates/dynamics-365-starter/src/components/ContactForm.tsx +149 -63
- package/templates/dynamics-365-starter/src/components/ContactManagement.tsx +153 -63
- package/templates/dynamics-365-starter/src/components/Logging/LogDialog.tsx +291 -0
- package/templates/dynamics-365-starter/src/components/Logging/LoggingContext.tsx +166 -0
- package/templates/dynamics-365-starter/src/components/Logging/LoggingDebugPanel.css +192 -0
- package/templates/dynamics-365-starter/src/components/Logging/LoggingDebugPanel.tsx +177 -0
- package/templates/dynamics-365-starter/src/components/Logging/LoggingProvider.tsx +3 -0
- package/templates/dynamics-365-starter/src/components/Logging/logger.ts +193 -0
- package/templates/dynamics-365-starter/src/constants/account.ts +410 -0
- package/templates/dynamics-365-starter/src/constants/contact.ts +362 -0
- package/templates/dynamics-365-starter/src/examples/README.md +52 -0
- package/templates/dynamics-365-starter/src/examples/component-examples/opportunity-management.tsx +625 -0
- package/templates/dynamics-365-starter/src/examples/entity-examples/opportunity-model.ts +545 -0
- package/templates/dynamics-365-starter/src/examples/integration-examples/custom-pcf-wrapper.tsx +722 -0
- package/templates/dynamics-365-starter/src/examples/workflow-examples/sales-workflow.ts +662 -0
- package/templates/dynamics-365-starter/src/index.tsx +107 -19
- package/templates/dynamics-365-starter/src/models/Account.ts +480 -0
- package/templates/dynamics-365-starter/src/models/BaseEntity.ts +204 -0
- package/templates/dynamics-365-starter/src/models/Contact.ts +580 -0
- package/templates/dynamics-365-starter/src/page-templates/EntityDashboard.tsx +519 -0
- package/templates/dynamics-365-starter/src/page-templates/EntityDetailPage.tsx +456 -0
- package/templates/dynamics-365-starter/src/page-templates/EntityListPage.tsx +406 -0
- package/templates/dynamics-365-starter/src/page-templates/RelatedEntitiesPage.tsx +578 -0
- package/templates/dynamics-365-starter/src/page-templates/SearchPage.tsx +629 -0
- package/templates/dynamics-365-starter/src/pcf/ContactControlWrapper.tsx +75 -22
- package/templates/dynamics-365-starter/src/pcf/MultiEntityControlWrapper.tsx +205 -0
- package/templates/dynamics-365-starter/src/providers/DynamicsProvider.tsx +297 -80
- package/templates/dynamics-365-starter/src/services/MockApiService.ts +260 -0
- package/templates/dynamics-365-starter/src/services/ServiceFactory.ts +65 -0
- package/templates/dynamics-365-starter/src/services/XrmApiService.ts +213 -0
- package/templates/dynamics-365-starter/src/styles/index.css +74 -7
- package/templates/dynamics-365-starter/tools/entity-generator/index.js +168 -0
- package/templates/dynamics-365-starter/tools/entity-generator/templates/constants.template.ts +124 -0
- package/templates/dynamics-365-starter/tools/entity-generator/templates/form.template.css +283 -0
- package/templates/dynamics-365-starter/tools/entity-generator/templates/form.template.tsx +275 -0
- package/templates/dynamics-365-starter/tools/entity-generator/templates/management.template.css +204 -0
- package/templates/dynamics-365-starter/tools/entity-generator/templates/management.template.tsx +413 -0
- package/templates/dynamics-365-starter/tools/entity-generator/templates/model.template.ts +250 -0
- package/templates/dynamics-365-starter/tools/metadata-sync/d365-client.js +410 -0
- package/templates/dynamics-365-starter/tools/metadata-sync/index.js +512 -0
- package/templates/dynamics-365-starter/tools/metadata-sync/type-generator.js +675 -0
- package/templates/dynamics-365-starter/tsconfig.json +11 -8
- package/templates/dynamics-365-starter/webpack.config.js +8 -9
- package/templates/power-pages-starter/README.md +7 -1
- package/templates/power-pages-starter/public/index.html +8 -11
- package/templates/power-pages-starter/src/components/ContactForm.tsx +60 -41
- package/templates/power-pages-starter/src/index.tsx +3 -3
- package/templates/power-pages-starter/src/providers/PowerPagesProvider.tsx +46 -23
- package/templates/power-pages-starter/tsconfig.json +3 -9
- package/templates/power-pages-starter/webpack.config.js +8 -3
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example: Complete Sales Workflow Implementation
|
|
3
|
+
*
|
|
4
|
+
* This example demonstrates how to create a comprehensive business workflow
|
|
5
|
+
* that spans multiple entities (Account → Contact → Opportunity) using the
|
|
6
|
+
* enhanced template's entity models and patterns.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* const workflow = new SalesWorkflow(apiService);
|
|
11
|
+
*
|
|
12
|
+
* // Execute complete workflow
|
|
13
|
+
* const result = await workflow.executeCompleteWorkflow({
|
|
14
|
+
* accountName: 'ACME Corporation',
|
|
15
|
+
* contactName: 'John Smith',
|
|
16
|
+
* contactEmail: 'john.smith@acme.com',
|
|
17
|
+
* opportunityName: 'Q1 Software License',
|
|
18
|
+
* opportunityValue: 75000
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* console.log('Workflow completed:', result);
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { IApiService } from '@khester/dynamics-ui-api-client';
|
|
26
|
+
import { Account } from '../../models/Account';
|
|
27
|
+
import { Contact } from '../../models/Contact';
|
|
28
|
+
import { Logger } from '../../components/Logging/logger';
|
|
29
|
+
import {
|
|
30
|
+
Opportunity,
|
|
31
|
+
SalesStageCode_OptionSet,
|
|
32
|
+
OpportunityStateCode_OptionSet,
|
|
33
|
+
} from '../entity-examples/opportunity-model';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Interface for workflow input data
|
|
37
|
+
*/
|
|
38
|
+
export interface SalesWorkflowInput {
|
|
39
|
+
/** Account information */
|
|
40
|
+
accountName: string;
|
|
41
|
+
accountWebsite?: string;
|
|
42
|
+
accountPhone?: string;
|
|
43
|
+
accountRevenue?: number;
|
|
44
|
+
industryCode?: number;
|
|
45
|
+
|
|
46
|
+
/** Contact information */
|
|
47
|
+
contactFirstName?: string;
|
|
48
|
+
contactLastName?: string;
|
|
49
|
+
contactEmail: string;
|
|
50
|
+
contactPhone?: string;
|
|
51
|
+
|
|
52
|
+
/** Opportunity information */
|
|
53
|
+
opportunityName: string;
|
|
54
|
+
opportunityValue?: number;
|
|
55
|
+
closeProbability?: number;
|
|
56
|
+
estimatedCloseDate?: Date;
|
|
57
|
+
description?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Interface for workflow result
|
|
62
|
+
*/
|
|
63
|
+
export interface SalesWorkflowResult {
|
|
64
|
+
/** Created account */
|
|
65
|
+
account: Account;
|
|
66
|
+
/** Created contact */
|
|
67
|
+
contact: Contact;
|
|
68
|
+
/** Created opportunity */
|
|
69
|
+
opportunity: Opportunity;
|
|
70
|
+
/** Workflow execution summary */
|
|
71
|
+
summary: {
|
|
72
|
+
executionTime: number;
|
|
73
|
+
stepsCompleted: string[];
|
|
74
|
+
warnings: string[];
|
|
75
|
+
success: boolean;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Sales Workflow Class
|
|
81
|
+
*
|
|
82
|
+
* Implements a complete sales workflow that demonstrates:
|
|
83
|
+
* - Multi-entity operations with proper relationships
|
|
84
|
+
* - Transaction-like behavior with rollback on failure
|
|
85
|
+
* - Comprehensive error handling and logging
|
|
86
|
+
* - Performance monitoring and optimization
|
|
87
|
+
* - Business rule validation and enforcement
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```typescript
|
|
91
|
+
* // Initialize workflow
|
|
92
|
+
* const salesWorkflow = new SalesWorkflow(apiService);
|
|
93
|
+
*
|
|
94
|
+
* // Execute with validation
|
|
95
|
+
* try {
|
|
96
|
+
* const result = await salesWorkflow.executeCompleteWorkflow(workflowData);
|
|
97
|
+
* console.log('Sales workflow completed successfully');
|
|
98
|
+
* } catch (error) {
|
|
99
|
+
* console.error('Workflow failed:', error.message);
|
|
100
|
+
* }
|
|
101
|
+
*
|
|
102
|
+
* // Execute individual steps
|
|
103
|
+
* const account = await salesWorkflow.createOrFindAccount(accountData);
|
|
104
|
+
* const contact = await salesWorkflow.createContactForAccount(contactData, account.accountid);
|
|
105
|
+
* const opportunity = await salesWorkflow.createOpportunity(opportunityData, account.accountid, contact.contactid);
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
export class SalesWorkflow {
|
|
109
|
+
private apiService: IApiService;
|
|
110
|
+
private loggerContext = 'SalesWorkflow';
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Constructor
|
|
114
|
+
* @param apiService - The API service instance for Dynamics 365 operations
|
|
115
|
+
*/
|
|
116
|
+
constructor(apiService: IApiService) {
|
|
117
|
+
this.apiService = apiService;
|
|
118
|
+
Logger.log('SalesWorkflow initialized', this.loggerContext);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Execute the complete sales workflow
|
|
123
|
+
*
|
|
124
|
+
* This method orchestrates the entire process:
|
|
125
|
+
* 1. Validate input data
|
|
126
|
+
* 2. Create or find existing account
|
|
127
|
+
* 3. Create contact associated with account
|
|
128
|
+
* 4. Create opportunity with proper relationships
|
|
129
|
+
* 5. Apply business rules and validation
|
|
130
|
+
*
|
|
131
|
+
* @param input - Workflow input data
|
|
132
|
+
* @returns Promise resolving to workflow result
|
|
133
|
+
* @throws Error if workflow fails at any step
|
|
134
|
+
*/
|
|
135
|
+
public async executeCompleteWorkflow(
|
|
136
|
+
input: SalesWorkflowInput
|
|
137
|
+
): Promise<SalesWorkflowResult> {
|
|
138
|
+
const startTime = performance.now();
|
|
139
|
+
const stepsCompleted: string[] = [];
|
|
140
|
+
const warnings: string[] = [];
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
Logger.userAction(
|
|
144
|
+
'Sales workflow execution started',
|
|
145
|
+
{
|
|
146
|
+
opportunityName: input.opportunityName,
|
|
147
|
+
accountName: input.accountName,
|
|
148
|
+
},
|
|
149
|
+
this.loggerContext
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// Step 1: Validate input
|
|
153
|
+
this.validateWorkflowInput(input);
|
|
154
|
+
stepsCompleted.push('Input validation');
|
|
155
|
+
|
|
156
|
+
// Step 2: Create or find account
|
|
157
|
+
Logger.log('Step 2: Creating or finding account', this.loggerContext);
|
|
158
|
+
const account = await this.createOrFindAccount({
|
|
159
|
+
name: input.accountName,
|
|
160
|
+
websiteurl: input.accountWebsite,
|
|
161
|
+
telephone1: input.accountPhone,
|
|
162
|
+
revenue: input.accountRevenue,
|
|
163
|
+
industrycode: input.industryCode,
|
|
164
|
+
});
|
|
165
|
+
stepsCompleted.push('Account creation/retrieval');
|
|
166
|
+
|
|
167
|
+
// Step 3: Create contact
|
|
168
|
+
Logger.log('Step 3: Creating contact', this.loggerContext);
|
|
169
|
+
const contact = await this.createContactForAccount(
|
|
170
|
+
{
|
|
171
|
+
firstname: input.contactFirstName,
|
|
172
|
+
lastname: input.contactLastName,
|
|
173
|
+
emailaddress1: input.contactEmail,
|
|
174
|
+
telephone1: input.contactPhone,
|
|
175
|
+
},
|
|
176
|
+
account.accountid!
|
|
177
|
+
);
|
|
178
|
+
stepsCompleted.push('Contact creation');
|
|
179
|
+
|
|
180
|
+
// Step 4: Create opportunity
|
|
181
|
+
Logger.log('Step 4: Creating opportunity', this.loggerContext);
|
|
182
|
+
const opportunity = await this.createOpportunity(
|
|
183
|
+
{
|
|
184
|
+
name: input.opportunityName,
|
|
185
|
+
estimatedvalue: input.opportunityValue,
|
|
186
|
+
closeprobability: input.closeProbability,
|
|
187
|
+
estimatedclosedate: input.estimatedCloseDate?.toISOString(),
|
|
188
|
+
description: input.description,
|
|
189
|
+
},
|
|
190
|
+
account.accountid!,
|
|
191
|
+
contact.contactid!
|
|
192
|
+
);
|
|
193
|
+
stepsCompleted.push('Opportunity creation');
|
|
194
|
+
|
|
195
|
+
// Step 5: Apply business rules
|
|
196
|
+
Logger.log('Step 5: Applying business rules', this.loggerContext);
|
|
197
|
+
const businessRuleWarnings = await this.applyBusinessRules(
|
|
198
|
+
account,
|
|
199
|
+
contact,
|
|
200
|
+
opportunity
|
|
201
|
+
);
|
|
202
|
+
warnings.push(...businessRuleWarnings);
|
|
203
|
+
stepsCompleted.push('Business rules validation');
|
|
204
|
+
|
|
205
|
+
const executionTime = performance.now() - startTime;
|
|
206
|
+
Logger.timing('Sales workflow execution', startTime, this.loggerContext);
|
|
207
|
+
|
|
208
|
+
const result: SalesWorkflowResult = {
|
|
209
|
+
account,
|
|
210
|
+
contact,
|
|
211
|
+
opportunity,
|
|
212
|
+
summary: {
|
|
213
|
+
executionTime,
|
|
214
|
+
stepsCompleted,
|
|
215
|
+
warnings,
|
|
216
|
+
success: true,
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
Logger.userAction(
|
|
221
|
+
'Sales workflow completed successfully',
|
|
222
|
+
{
|
|
223
|
+
executionTime: `${executionTime.toFixed(2)}ms`,
|
|
224
|
+
stepsCompleted: stepsCompleted.length,
|
|
225
|
+
warnings: warnings.length,
|
|
226
|
+
},
|
|
227
|
+
this.loggerContext
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
return result;
|
|
231
|
+
} catch (error) {
|
|
232
|
+
const executionTime = performance.now() - startTime;
|
|
233
|
+
|
|
234
|
+
Logger.error('Sales workflow failed', this.loggerContext, {
|
|
235
|
+
error,
|
|
236
|
+
stepsCompleted,
|
|
237
|
+
executionTime: `${executionTime.toFixed(2)}ms`,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// In a production system, you might want to implement rollback logic here
|
|
241
|
+
throw new Error(
|
|
242
|
+
`Sales workflow failed at step: ${stepsCompleted[stepsCompleted.length - 1] || 'initialization'}. ${error}`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Create or find an existing account
|
|
249
|
+
*
|
|
250
|
+
* Implements intelligent account creation that:
|
|
251
|
+
* - Searches for existing accounts by name
|
|
252
|
+
* - Creates new account if none found
|
|
253
|
+
* - Applies duplicate detection logic
|
|
254
|
+
*
|
|
255
|
+
* @param accountData - Account data
|
|
256
|
+
* @returns Promise resolving to account (existing or new)
|
|
257
|
+
*/
|
|
258
|
+
public async createOrFindAccount(accountData: {
|
|
259
|
+
name: string;
|
|
260
|
+
websiteurl?: string;
|
|
261
|
+
telephone1?: string;
|
|
262
|
+
revenue?: number;
|
|
263
|
+
industrycode?: number;
|
|
264
|
+
}): Promise<Account> {
|
|
265
|
+
try {
|
|
266
|
+
// Search for existing account by name
|
|
267
|
+
Logger.log(
|
|
268
|
+
`Searching for existing account: ${accountData.name}`,
|
|
269
|
+
this.loggerContext
|
|
270
|
+
);
|
|
271
|
+
const existingAccounts = await Account.retrieveByName(
|
|
272
|
+
this.apiService,
|
|
273
|
+
accountData.name
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
if (existingAccounts.length > 0) {
|
|
277
|
+
const existingAccount = existingAccounts[0];
|
|
278
|
+
Logger.log(
|
|
279
|
+
`Found existing account: ${existingAccount.name} (${existingAccount.accountid})`,
|
|
280
|
+
this.loggerContext
|
|
281
|
+
);
|
|
282
|
+
return existingAccount;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Create new account
|
|
286
|
+
Logger.log(
|
|
287
|
+
`Creating new account: ${accountData.name}`,
|
|
288
|
+
this.loggerContext
|
|
289
|
+
);
|
|
290
|
+
const newAccount = new Account(accountData);
|
|
291
|
+
const createdAccount = await Account.create(this.apiService, newAccount);
|
|
292
|
+
|
|
293
|
+
Logger.log(
|
|
294
|
+
`Successfully created account: ${createdAccount.name} (${createdAccount.accountid})`,
|
|
295
|
+
this.loggerContext
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
return createdAccount;
|
|
299
|
+
} catch (error) {
|
|
300
|
+
Logger.error(
|
|
301
|
+
`Failed to create or find account: ${accountData.name}`,
|
|
302
|
+
this.loggerContext,
|
|
303
|
+
error
|
|
304
|
+
);
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Create a contact associated with an account
|
|
311
|
+
*
|
|
312
|
+
* @param contactData - Contact data
|
|
313
|
+
* @param accountId - Account ID to associate with
|
|
314
|
+
* @returns Promise resolving to created contact
|
|
315
|
+
*/
|
|
316
|
+
public async createContactForAccount(
|
|
317
|
+
contactData: {
|
|
318
|
+
firstname?: string;
|
|
319
|
+
lastname?: string;
|
|
320
|
+
emailaddress1: string;
|
|
321
|
+
telephone1?: string;
|
|
322
|
+
},
|
|
323
|
+
accountId: string
|
|
324
|
+
): Promise<Contact> {
|
|
325
|
+
try {
|
|
326
|
+
// Check for existing contact with same email
|
|
327
|
+
Logger.log(
|
|
328
|
+
`Checking for existing contact with email: ${contactData.emailaddress1}`,
|
|
329
|
+
this.loggerContext
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
const existingContacts = await Contact.retrieveByEmail(
|
|
333
|
+
this.apiService,
|
|
334
|
+
contactData.emailaddress1
|
|
335
|
+
);
|
|
336
|
+
if (existingContacts.length > 0) {
|
|
337
|
+
const warning = `Contact with email ${contactData.emailaddress1} already exists`;
|
|
338
|
+
Logger.warn(warning, this.loggerContext);
|
|
339
|
+
// Return existing contact but you might want to handle this differently
|
|
340
|
+
return existingContacts[0];
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Create new contact with account relationship
|
|
344
|
+
const newContact = new Contact({
|
|
345
|
+
...contactData,
|
|
346
|
+
parentcustomerid: accountId, // Associate with account
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
const createdContact = await Contact.create(this.apiService, newContact);
|
|
350
|
+
|
|
351
|
+
Logger.log(
|
|
352
|
+
`Successfully created contact: ${createdContact.firstname} ${createdContact.lastname} (${createdContact.contactid})`,
|
|
353
|
+
this.loggerContext
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
return createdContact;
|
|
357
|
+
} catch (error) {
|
|
358
|
+
Logger.error(
|
|
359
|
+
`Failed to create contact for account ${accountId}`,
|
|
360
|
+
this.loggerContext,
|
|
361
|
+
error
|
|
362
|
+
);
|
|
363
|
+
throw error;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Create an opportunity with proper relationships
|
|
369
|
+
*
|
|
370
|
+
* @param opportunityData - Opportunity data
|
|
371
|
+
* @param accountId - Related account ID
|
|
372
|
+
* @param contactId - Related contact ID
|
|
373
|
+
* @returns Promise resolving to created opportunity
|
|
374
|
+
*/
|
|
375
|
+
public async createOpportunity(
|
|
376
|
+
opportunityData: {
|
|
377
|
+
name: string;
|
|
378
|
+
estimatedvalue?: number;
|
|
379
|
+
closeprobability?: number;
|
|
380
|
+
estimatedclosedate?: string;
|
|
381
|
+
description?: string;
|
|
382
|
+
},
|
|
383
|
+
accountId: string,
|
|
384
|
+
contactId: string
|
|
385
|
+
): Promise<Opportunity> {
|
|
386
|
+
try {
|
|
387
|
+
// Create opportunity with relationships and default values
|
|
388
|
+
const newOpportunity = new Opportunity({
|
|
389
|
+
...opportunityData,
|
|
390
|
+
parentaccountid: accountId,
|
|
391
|
+
parentcontactid: contactId,
|
|
392
|
+
salesstagecode: SalesStageCode_OptionSet.Qualify, // Start in Qualify stage
|
|
393
|
+
statecode: OpportunityStateCode_OptionSet.Open, // Active opportunity
|
|
394
|
+
closeprobability: opportunityData.closeprobability || 25, // Default probability for new opportunities
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
const createdOpportunity = await Opportunity.create(
|
|
398
|
+
this.apiService,
|
|
399
|
+
newOpportunity
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
Logger.log(
|
|
403
|
+
`Successfully created opportunity: ${createdOpportunity.name} (${createdOpportunity.opportunityid})`,
|
|
404
|
+
this.loggerContext
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
return createdOpportunity;
|
|
408
|
+
} catch (error) {
|
|
409
|
+
Logger.error(
|
|
410
|
+
`Failed to create opportunity for account ${accountId} and contact ${contactId}`,
|
|
411
|
+
this.loggerContext,
|
|
412
|
+
error
|
|
413
|
+
);
|
|
414
|
+
throw error;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Apply business rules and validation
|
|
420
|
+
*
|
|
421
|
+
* Implements custom business logic such as:
|
|
422
|
+
* - Credit limit checks
|
|
423
|
+
* - Opportunity value validation
|
|
424
|
+
* - Relationship consistency checks
|
|
425
|
+
*
|
|
426
|
+
* @param account - Created account
|
|
427
|
+
* @param contact - Created contact
|
|
428
|
+
* @param opportunity - Created opportunity
|
|
429
|
+
* @returns Array of warning messages
|
|
430
|
+
*/
|
|
431
|
+
private async applyBusinessRules(
|
|
432
|
+
account: Account,
|
|
433
|
+
contact: Contact,
|
|
434
|
+
opportunity: Opportunity
|
|
435
|
+
): Promise<string[]> {
|
|
436
|
+
const warnings: string[] = [];
|
|
437
|
+
|
|
438
|
+
try {
|
|
439
|
+
// Business Rule 1: High-value opportunity warning
|
|
440
|
+
if (opportunity.estimatedvalue && opportunity.estimatedvalue > 100000) {
|
|
441
|
+
const warning = `High-value opportunity detected: $${opportunity.estimatedvalue.toLocaleString()}`;
|
|
442
|
+
warnings.push(warning);
|
|
443
|
+
Logger.log(warning, this.loggerContext);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Business Rule 2: Account revenue consistency
|
|
447
|
+
if (
|
|
448
|
+
account.revenue &&
|
|
449
|
+
opportunity.estimatedvalue &&
|
|
450
|
+
opportunity.estimatedvalue > account.revenue
|
|
451
|
+
) {
|
|
452
|
+
const warning = 'Opportunity value exceeds account annual revenue';
|
|
453
|
+
warnings.push(warning);
|
|
454
|
+
Logger.log(warning, this.loggerContext);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Business Rule 3: Contact email domain validation
|
|
458
|
+
if (contact.emailaddress1 && account.websiteurl) {
|
|
459
|
+
const emailDomain = contact.emailaddress1.split('@')[1];
|
|
460
|
+
const accountDomain = account.websiteurl
|
|
461
|
+
.replace(/^https?:\/\//, '')
|
|
462
|
+
.replace(/^www\./, '');
|
|
463
|
+
|
|
464
|
+
if (!emailDomain.includes(accountDomain.split('.')[0])) {
|
|
465
|
+
const warning = 'Contact email domain does not match account website';
|
|
466
|
+
warnings.push(warning);
|
|
467
|
+
Logger.log(warning, this.loggerContext);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Business Rule 4: Opportunity close date validation
|
|
472
|
+
if (opportunity.estimatedclosedate) {
|
|
473
|
+
const closeDate = new Date(opportunity.estimatedclosedate);
|
|
474
|
+
const threeMonthsFromNow = new Date();
|
|
475
|
+
threeMonthsFromNow.setMonth(threeMonthsFromNow.getMonth() + 3);
|
|
476
|
+
|
|
477
|
+
if (closeDate > threeMonthsFromNow) {
|
|
478
|
+
const warning =
|
|
479
|
+
'Opportunity close date is more than 3 months in the future';
|
|
480
|
+
warnings.push(warning);
|
|
481
|
+
Logger.log(warning, this.loggerContext);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
Logger.log(
|
|
486
|
+
`Business rules applied. Generated ${warnings.length} warnings.`,
|
|
487
|
+
this.loggerContext
|
|
488
|
+
);
|
|
489
|
+
return warnings;
|
|
490
|
+
} catch (error) {
|
|
491
|
+
Logger.error('Error applying business rules', this.loggerContext, error);
|
|
492
|
+
warnings.push('Error occurred while applying business rules');
|
|
493
|
+
return warnings;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Validate workflow input data
|
|
499
|
+
*
|
|
500
|
+
* @param input - Input data to validate
|
|
501
|
+
* @throws Error if validation fails
|
|
502
|
+
*/
|
|
503
|
+
private validateWorkflowInput(input: SalesWorkflowInput): void {
|
|
504
|
+
const errors: string[] = [];
|
|
505
|
+
|
|
506
|
+
// Required fields validation
|
|
507
|
+
if (!input.accountName?.trim()) {
|
|
508
|
+
errors.push('Account name is required');
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (!input.contactEmail?.trim()) {
|
|
512
|
+
errors.push('Contact email is required');
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (!input.opportunityName?.trim()) {
|
|
516
|
+
errors.push('Opportunity name is required');
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Email format validation
|
|
520
|
+
if (
|
|
521
|
+
input.contactEmail &&
|
|
522
|
+
!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input.contactEmail)
|
|
523
|
+
) {
|
|
524
|
+
errors.push('Invalid contact email format');
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Opportunity value validation
|
|
528
|
+
if (input.opportunityValue !== undefined && input.opportunityValue < 0) {
|
|
529
|
+
errors.push('Opportunity value cannot be negative');
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Close probability validation
|
|
533
|
+
if (
|
|
534
|
+
input.closeProbability !== undefined &&
|
|
535
|
+
(input.closeProbability < 0 || input.closeProbability > 100)
|
|
536
|
+
) {
|
|
537
|
+
errors.push('Close probability must be between 0 and 100');
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Close date validation
|
|
541
|
+
if (input.estimatedCloseDate && input.estimatedCloseDate < new Date()) {
|
|
542
|
+
errors.push('Estimated close date cannot be in the past');
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (errors.length > 0) {
|
|
546
|
+
const errorMessage = `Workflow validation failed: ${errors.join(', ')}`;
|
|
547
|
+
Logger.validation('SalesWorkflow', errors, this.loggerContext);
|
|
548
|
+
throw new Error(errorMessage);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
Logger.log('Workflow input validation passed', this.loggerContext);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Get workflow execution statistics
|
|
556
|
+
*
|
|
557
|
+
* @returns Promise resolving to workflow statistics
|
|
558
|
+
*/
|
|
559
|
+
public async getWorkflowStatistics(): Promise<{
|
|
560
|
+
totalAccounts: number;
|
|
561
|
+
totalContacts: number;
|
|
562
|
+
totalOpportunities: number;
|
|
563
|
+
averageOpportunityValue: number;
|
|
564
|
+
}> {
|
|
565
|
+
try {
|
|
566
|
+
const [accounts, contacts, opportunities] = await Promise.all([
|
|
567
|
+
Account.retrieveActiveAccounts(this.apiService),
|
|
568
|
+
Contact.retrieveActiveContacts(this.apiService),
|
|
569
|
+
Opportunity.retrieveActiveOpportunities(this.apiService),
|
|
570
|
+
]);
|
|
571
|
+
|
|
572
|
+
const totalOpportunityValue = opportunities.reduce(
|
|
573
|
+
(sum, opp) => sum + (opp.estimatedvalue || 0),
|
|
574
|
+
0
|
|
575
|
+
);
|
|
576
|
+
const averageOpportunityValue =
|
|
577
|
+
opportunities.length > 0
|
|
578
|
+
? totalOpportunityValue / opportunities.length
|
|
579
|
+
: 0;
|
|
580
|
+
|
|
581
|
+
return {
|
|
582
|
+
totalAccounts: accounts.length,
|
|
583
|
+
totalContacts: contacts.length,
|
|
584
|
+
totalOpportunities: opportunities.length,
|
|
585
|
+
averageOpportunityValue,
|
|
586
|
+
};
|
|
587
|
+
} catch (error) {
|
|
588
|
+
Logger.error(
|
|
589
|
+
'Failed to get workflow statistics',
|
|
590
|
+
this.loggerContext,
|
|
591
|
+
error
|
|
592
|
+
);
|
|
593
|
+
throw error;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Example usage scenarios
|
|
600
|
+
*/
|
|
601
|
+
export const workflowExamples = {
|
|
602
|
+
/**
|
|
603
|
+
* Basic workflow execution
|
|
604
|
+
*/
|
|
605
|
+
basicWorkflow: async (apiService: IApiService) => {
|
|
606
|
+
const workflow = new SalesWorkflow(apiService);
|
|
607
|
+
|
|
608
|
+
const result = await workflow.executeCompleteWorkflow({
|
|
609
|
+
accountName: 'Contoso Ltd',
|
|
610
|
+
accountWebsite: 'https://contoso.com',
|
|
611
|
+
accountRevenue: 5000000,
|
|
612
|
+
contactFirstName: 'Jane',
|
|
613
|
+
contactLastName: 'Doe',
|
|
614
|
+
contactEmail: 'jane.doe@contoso.com',
|
|
615
|
+
contactPhone: '+1-555-0123',
|
|
616
|
+
opportunityName: 'Q1 Enterprise License',
|
|
617
|
+
opportunityValue: 75000,
|
|
618
|
+
closeProbability: 70,
|
|
619
|
+
estimatedCloseDate: new Date('2024-03-31'),
|
|
620
|
+
description: 'Enterprise software licensing opportunity for Q1',
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
return result;
|
|
624
|
+
},
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* High-value opportunity workflow
|
|
628
|
+
*/
|
|
629
|
+
highValueWorkflow: async (apiService: IApiService) => {
|
|
630
|
+
const workflow = new SalesWorkflow(apiService);
|
|
631
|
+
|
|
632
|
+
const result = await workflow.executeCompleteWorkflow({
|
|
633
|
+
accountName: 'Fortune 500 Corp',
|
|
634
|
+
accountRevenue: 10000000,
|
|
635
|
+
contactEmail: 'cto@fortune500corp.com',
|
|
636
|
+
opportunityName: 'Digital Transformation Initiative',
|
|
637
|
+
opportunityValue: 500000,
|
|
638
|
+
closeProbability: 60,
|
|
639
|
+
description: 'Large-scale digital transformation project',
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
return result;
|
|
643
|
+
},
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Workflow with existing account
|
|
647
|
+
*/
|
|
648
|
+
existingAccountWorkflow: async (apiService: IApiService) => {
|
|
649
|
+
const workflow = new SalesWorkflow(apiService);
|
|
650
|
+
|
|
651
|
+
// This will find the existing account instead of creating a new one
|
|
652
|
+
const result = await workflow.executeCompleteWorkflow({
|
|
653
|
+
accountName: 'Microsoft Corporation', // Assuming this exists
|
|
654
|
+
contactEmail: 'partner@microsoft.com',
|
|
655
|
+
opportunityName: 'Partnership Opportunity',
|
|
656
|
+
opportunityValue: 250000,
|
|
657
|
+
closeProbability: 80,
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
return result;
|
|
661
|
+
},
|
|
662
|
+
};
|