@nextsparkjs/theme-crm 0.1.0-beta.19 → 0.1.0-beta.20
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/package.json +2 -2
- package/tests/cypress/e2e/api/activities/activities-crud.cy.ts +686 -0
- package/tests/cypress/e2e/api/campaigns/campaigns-crud.cy.ts +592 -0
- package/tests/cypress/e2e/api/companies/companies-crud.cy.ts +682 -0
- package/tests/cypress/e2e/api/contacts/contacts-crud.cy.ts +668 -0
- package/tests/cypress/e2e/api/leads/leads-crud.cy.ts +648 -0
- package/tests/cypress/e2e/api/notes/notes-crud.cy.ts +424 -0
- package/tests/cypress/e2e/api/opportunities/opportunities-crud.cy.ts +865 -0
- package/tests/cypress/e2e/api/pipelines/pipelines-crud.cy.ts +545 -0
- package/tests/cypress/e2e/api/products/products-crud.cy.ts +447 -0
- package/tests/cypress/e2e/ui/activities/activities-admin.cy.ts +268 -0
- package/tests/cypress/e2e/ui/activities/activities-member.cy.ts +257 -0
- package/tests/cypress/e2e/ui/activities/activities-owner.cy.ts +268 -0
- package/tests/cypress/e2e/ui/companies/companies-admin.cy.ts +188 -0
- package/tests/cypress/e2e/ui/companies/companies-member.cy.ts +166 -0
- package/tests/cypress/e2e/ui/companies/companies-owner.cy.ts +189 -0
- package/tests/cypress/e2e/ui/contacts/contacts-admin.cy.ts +252 -0
- package/tests/cypress/e2e/ui/contacts/contacts-member.cy.ts +224 -0
- package/tests/cypress/e2e/ui/contacts/contacts-owner.cy.ts +236 -0
- package/tests/cypress/e2e/ui/leads/leads-admin.cy.ts +286 -0
- package/tests/cypress/e2e/ui/leads/leads-member.cy.ts +193 -0
- package/tests/cypress/e2e/ui/leads/leads-owner.cy.ts +210 -0
- package/tests/cypress/e2e/ui/opportunities/opportunities-admin.cy.ts +197 -0
- package/tests/cypress/e2e/ui/opportunities/opportunities-member.cy.ts +229 -0
- package/tests/cypress/e2e/ui/opportunities/opportunities-owner.cy.ts +196 -0
- package/tests/cypress/e2e/ui/pipelines/pipelines-admin.cy.ts +320 -0
- package/tests/cypress/e2e/ui/pipelines/pipelines-member.cy.ts +262 -0
- package/tests/cypress/e2e/ui/pipelines/pipelines-owner.cy.ts +282 -0
- package/tests/cypress/fixtures/blocks.json +9 -0
- package/tests/cypress/fixtures/entities.json +240 -0
- package/tests/cypress/src/components/CRMDataTable.js +223 -0
- package/tests/cypress/src/components/CRMMobileNav.js +138 -0
- package/tests/cypress/src/components/CRMSidebar.js +145 -0
- package/tests/cypress/src/components/CRMTopBar.js +194 -0
- package/tests/cypress/src/components/DealCard.js +197 -0
- package/tests/cypress/src/components/EntityDetail.ts +290 -0
- package/tests/cypress/src/components/EntityForm.ts +357 -0
- package/tests/cypress/src/components/EntityList.ts +360 -0
- package/tests/cypress/src/components/PipelineKanban.js +204 -0
- package/tests/cypress/src/components/StageColumn.js +196 -0
- package/tests/cypress/src/components/index.js +13 -0
- package/tests/cypress/src/components/index.ts +22 -0
- package/tests/cypress/src/controllers/ActivityAPIController.ts +113 -0
- package/tests/cypress/src/controllers/BaseAPIController.ts +307 -0
- package/tests/cypress/src/controllers/CampaignAPIController.ts +114 -0
- package/tests/cypress/src/controllers/CompanyAPIController.ts +112 -0
- package/tests/cypress/src/controllers/ContactAPIController.ts +104 -0
- package/tests/cypress/src/controllers/LeadAPIController.ts +96 -0
- package/tests/cypress/src/controllers/NoteAPIController.ts +130 -0
- package/tests/cypress/src/controllers/OpportunityAPIController.ts +134 -0
- package/tests/cypress/src/controllers/PipelineAPIController.ts +116 -0
- package/tests/cypress/src/controllers/ProductAPIController.ts +113 -0
- package/tests/cypress/src/controllers/index.ts +35 -0
- package/tests/cypress/src/entities/ActivitiesPOM.ts +130 -0
- package/tests/cypress/src/entities/CompaniesPOM.ts +117 -0
- package/tests/cypress/src/entities/ContactsPOM.ts +117 -0
- package/tests/cypress/src/entities/LeadsPOM.ts +129 -0
- package/tests/cypress/src/entities/OpportunitiesPOM.ts +178 -0
- package/tests/cypress/src/entities/PipelinesPOM.ts +341 -0
- package/tests/cypress/src/entities/index.ts +31 -0
- package/tests/cypress/src/forms/OpportunityForm.js +316 -0
- package/tests/cypress/src/forms/PipelineForm.js +243 -0
- package/tests/cypress/src/forms/index.js +8 -0
- package/tests/cypress/src/index.js +22 -0
- package/tests/cypress/src/index.ts +68 -0
- package/tests/cypress/src/selectors.ts +50 -0
- package/tests/cypress/src/session-helpers.ts +94 -0
- package/tests/cypress/support/e2e.ts +89 -0
- package/tests/cypress.config.ts +165 -0
- package/tests/tsconfig.json +15 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CampaignAPIController - TypeScript controller for Campaigns API
|
|
3
|
+
*
|
|
4
|
+
* Handles CRUD operations for /api/v1/campaigns endpoints
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { BaseAPIController, APIRequestOptions, APIResponse } from './BaseAPIController'
|
|
8
|
+
|
|
9
|
+
export interface CampaignData {
|
|
10
|
+
name?: string
|
|
11
|
+
type?: string
|
|
12
|
+
status?: string
|
|
13
|
+
startDate?: string
|
|
14
|
+
endDate?: string
|
|
15
|
+
budget?: number
|
|
16
|
+
actualCost?: number
|
|
17
|
+
expectedRevenue?: number
|
|
18
|
+
description?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface CampaignGetAllOptions extends APIRequestOptions {
|
|
22
|
+
type?: string
|
|
23
|
+
status?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class CampaignAPIController extends BaseAPIController {
|
|
27
|
+
protected entitySlug = 'campaigns'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* GET all campaigns with filtering options
|
|
31
|
+
*/
|
|
32
|
+
getAll(options: CampaignGetAllOptions = {}): Cypress.Chainable<APIResponse> {
|
|
33
|
+
return super.getAll(options)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Activate campaign
|
|
38
|
+
*/
|
|
39
|
+
activate(id: string, options: APIRequestOptions = {}): Cypress.Chainable<APIResponse> {
|
|
40
|
+
return this.update(id, { status: 'active' }, options)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Complete campaign
|
|
45
|
+
*/
|
|
46
|
+
complete(id: string, options: APIRequestOptions = {}): Cypress.Chainable<APIResponse> {
|
|
47
|
+
return this.update(id, { status: 'completed' }, options)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Generate random campaign data for testing
|
|
52
|
+
*/
|
|
53
|
+
generateRandomData(overrides: Partial<CampaignData> = {}): CampaignData {
|
|
54
|
+
const timestamp = Date.now()
|
|
55
|
+
const randomId = Math.random().toString(36).substring(2, 8)
|
|
56
|
+
|
|
57
|
+
const types = ['email', 'social_media', 'webinar', 'event', 'content', 'ppc', 'seo']
|
|
58
|
+
const statuses = ['planned', 'active', 'paused', 'completed', 'cancelled']
|
|
59
|
+
const campaignNames = [
|
|
60
|
+
'Q1 Product Launch',
|
|
61
|
+
'Summer Sale',
|
|
62
|
+
'Holiday Promotion',
|
|
63
|
+
'Brand Awareness',
|
|
64
|
+
'Lead Generation',
|
|
65
|
+
'Customer Retention'
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
// Generate dates
|
|
69
|
+
const startDate = new Date()
|
|
70
|
+
startDate.setDate(startDate.getDate() + Math.floor(Math.random() * 30))
|
|
71
|
+
const endDate = new Date(startDate)
|
|
72
|
+
endDate.setDate(endDate.getDate() + Math.floor(Math.random() * 90) + 30)
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
name: `${campaignNames[Math.floor(Math.random() * campaignNames.length)]} ${randomId}`,
|
|
76
|
+
type: types[Math.floor(Math.random() * types.length)],
|
|
77
|
+
status: statuses[Math.floor(Math.random() * statuses.length)],
|
|
78
|
+
startDate: startDate.toISOString().split('T')[0],
|
|
79
|
+
endDate: endDate.toISOString().split('T')[0],
|
|
80
|
+
budget: Math.floor(Math.random() * 100000) + 10000,
|
|
81
|
+
actualCost: Math.floor(Math.random() * 50000) + 5000,
|
|
82
|
+
expectedRevenue: Math.floor(Math.random() * 500000) + 50000,
|
|
83
|
+
description: `Test campaign created at ${new Date(timestamp).toISOString()}`,
|
|
84
|
+
...overrides
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Validate campaign object structure
|
|
90
|
+
*/
|
|
91
|
+
validateObject(campaign: Record<string, unknown>, allowMetas = false): void {
|
|
92
|
+
this.validateSystemFields(campaign)
|
|
93
|
+
|
|
94
|
+
expect(campaign).to.have.property('name')
|
|
95
|
+
expect(campaign.name).to.be.a('string')
|
|
96
|
+
|
|
97
|
+
this.validateOptionalStringFields(campaign, [
|
|
98
|
+
'type', 'status', 'startDate', 'endDate', 'description'
|
|
99
|
+
])
|
|
100
|
+
|
|
101
|
+
const numericFields = ['budget', 'actualCost', 'expectedRevenue']
|
|
102
|
+
numericFields.forEach(field => {
|
|
103
|
+
if (campaign[field] !== null && campaign[field] !== undefined) {
|
|
104
|
+
expect(Number(campaign[field])).to.be.a('number')
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
if (allowMetas && Object.prototype.hasOwnProperty.call(campaign, 'metas')) {
|
|
109
|
+
expect(campaign.metas).to.be.an('object')
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export default CampaignAPIController
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CompanyAPIController - TypeScript controller for Companies API
|
|
3
|
+
*
|
|
4
|
+
* Handles CRUD operations for /api/v1/companies endpoints
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { BaseAPIController, APIRequestOptions, APIResponse } from './BaseAPIController'
|
|
8
|
+
|
|
9
|
+
export interface CompanyData {
|
|
10
|
+
name?: string
|
|
11
|
+
legalName?: string
|
|
12
|
+
website?: string
|
|
13
|
+
email?: string
|
|
14
|
+
industry?: string
|
|
15
|
+
size?: string
|
|
16
|
+
type?: string
|
|
17
|
+
phone?: string
|
|
18
|
+
address?: string
|
|
19
|
+
city?: string
|
|
20
|
+
state?: string
|
|
21
|
+
country?: string
|
|
22
|
+
postalCode?: string
|
|
23
|
+
description?: string
|
|
24
|
+
rating?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CompanyGetAllOptions extends APIRequestOptions {
|
|
28
|
+
industry?: string
|
|
29
|
+
size?: string
|
|
30
|
+
type?: string
|
|
31
|
+
rating?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class CompanyAPIController extends BaseAPIController {
|
|
35
|
+
protected entitySlug = 'companies'
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* GET all companies with filtering options
|
|
39
|
+
*/
|
|
40
|
+
getAll(options: CompanyGetAllOptions = {}): Cypress.Chainable<APIResponse> {
|
|
41
|
+
return super.getAll(options)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Update company rating
|
|
46
|
+
*/
|
|
47
|
+
updateRating(id: string, rating: string, options: APIRequestOptions = {}): Cypress.Chainable<APIResponse> {
|
|
48
|
+
return this.update(id, { rating }, options)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Generate random company data for testing
|
|
53
|
+
*/
|
|
54
|
+
generateRandomData(overrides: Partial<CompanyData> = {}): CompanyData {
|
|
55
|
+
const timestamp = Date.now()
|
|
56
|
+
const randomId = Math.random().toString(36).substring(2, 8)
|
|
57
|
+
|
|
58
|
+
const industries = ['Technology', 'Healthcare', 'Finance', 'Manufacturing', 'Retail', 'Education']
|
|
59
|
+
const sizes = ['1-10', '11-50', '51-200', '201-500', '500+']
|
|
60
|
+
const types = ['prospect', 'customer', 'partner', 'vendor', 'competitor']
|
|
61
|
+
const ratings = ['hot', 'warm', 'cold']
|
|
62
|
+
const cities = ['New York', 'San Francisco', 'Los Angeles', 'Chicago', 'Boston', 'Seattle']
|
|
63
|
+
const countries = ['USA', 'UK', 'Canada', 'Germany', 'France', 'Australia']
|
|
64
|
+
|
|
65
|
+
const companyNames = [
|
|
66
|
+
'Tech Solutions',
|
|
67
|
+
'Global Industries',
|
|
68
|
+
'Innovate Corp',
|
|
69
|
+
'Digital Dynamics',
|
|
70
|
+
'Enterprise Systems',
|
|
71
|
+
'Cloud Networks'
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
name: `${companyNames[Math.floor(Math.random() * companyNames.length)]} ${randomId}`,
|
|
76
|
+
legalName: `${companyNames[Math.floor(Math.random() * companyNames.length)]} LLC`,
|
|
77
|
+
website: `https://www.company-${randomId}.com`,
|
|
78
|
+
email: `contact@company-${randomId}.com`,
|
|
79
|
+
industry: industries[Math.floor(Math.random() * industries.length)],
|
|
80
|
+
size: sizes[Math.floor(Math.random() * sizes.length)],
|
|
81
|
+
type: types[Math.floor(Math.random() * types.length)],
|
|
82
|
+
phone: `+1-555-${Math.floor(Math.random() * 9000) + 1000}`,
|
|
83
|
+
address: `${Math.floor(Math.random() * 9000) + 100} Business Ave`,
|
|
84
|
+
city: cities[Math.floor(Math.random() * cities.length)],
|
|
85
|
+
country: countries[Math.floor(Math.random() * countries.length)],
|
|
86
|
+
description: `Test company created at ${new Date(timestamp).toISOString()}`,
|
|
87
|
+
rating: ratings[Math.floor(Math.random() * ratings.length)],
|
|
88
|
+
...overrides
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Validate company object structure
|
|
94
|
+
*/
|
|
95
|
+
validateObject(company: Record<string, unknown>, allowMetas = false): void {
|
|
96
|
+
this.validateSystemFields(company)
|
|
97
|
+
|
|
98
|
+
expect(company).to.have.property('name')
|
|
99
|
+
expect(company.name).to.be.a('string')
|
|
100
|
+
|
|
101
|
+
this.validateOptionalStringFields(company, [
|
|
102
|
+
'legalName', 'website', 'email', 'industry', 'size', 'type',
|
|
103
|
+
'phone', 'address', 'city', 'state', 'country', 'postalCode', 'description', 'rating'
|
|
104
|
+
])
|
|
105
|
+
|
|
106
|
+
if (allowMetas && Object.prototype.hasOwnProperty.call(company, 'metas')) {
|
|
107
|
+
expect(company.metas).to.be.an('object')
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export default CompanyAPIController
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ContactAPIController - TypeScript controller for Contacts API
|
|
3
|
+
*
|
|
4
|
+
* Handles CRUD operations for /api/v1/contacts endpoints
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { BaseAPIController, APIRequestOptions, APIResponse } from './BaseAPIController'
|
|
8
|
+
|
|
9
|
+
export interface ContactData {
|
|
10
|
+
firstName?: string
|
|
11
|
+
lastName?: string
|
|
12
|
+
email?: string
|
|
13
|
+
phone?: string
|
|
14
|
+
mobile?: string
|
|
15
|
+
companyId?: string
|
|
16
|
+
position?: string
|
|
17
|
+
department?: string
|
|
18
|
+
isPrimary?: boolean
|
|
19
|
+
birthDate?: string
|
|
20
|
+
linkedin?: string
|
|
21
|
+
twitter?: string
|
|
22
|
+
preferredChannel?: string
|
|
23
|
+
timezone?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ContactGetAllOptions extends APIRequestOptions {
|
|
27
|
+
companyId?: string
|
|
28
|
+
position?: string
|
|
29
|
+
department?: string
|
|
30
|
+
isPrimary?: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class ContactAPIController extends BaseAPIController {
|
|
34
|
+
protected entitySlug = 'contacts'
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* GET all contacts with filtering options
|
|
38
|
+
*/
|
|
39
|
+
getAll(options: ContactGetAllOptions = {}): Cypress.Chainable<APIResponse> {
|
|
40
|
+
return super.getAll(options)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Set contact as primary for company
|
|
45
|
+
*/
|
|
46
|
+
setAsPrimary(id: string, options: APIRequestOptions = {}): Cypress.Chainable<APIResponse> {
|
|
47
|
+
return this.update(id, { isPrimary: true }, options)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Generate random contact data for testing
|
|
52
|
+
*/
|
|
53
|
+
generateRandomData(overrides: Partial<ContactData> = {}): ContactData {
|
|
54
|
+
const timestamp = Date.now()
|
|
55
|
+
const randomId = Math.random().toString(36).substring(2, 8)
|
|
56
|
+
|
|
57
|
+
const firstNames = ['John', 'Jane', 'Michael', 'Sarah', 'David', 'Emily', 'Robert', 'Maria']
|
|
58
|
+
const lastNames = ['Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis']
|
|
59
|
+
const positions = ['CEO', 'CTO', 'CFO', 'VP Sales', 'VP Marketing', 'Director', 'Manager', 'Engineer']
|
|
60
|
+
const departments = ['Executive', 'Sales', 'Marketing', 'Engineering', 'Finance', 'Operations', 'HR']
|
|
61
|
+
const channels = ['email', 'phone', 'linkedin', 'twitter']
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
firstName: firstNames[Math.floor(Math.random() * firstNames.length)],
|
|
65
|
+
lastName: `${lastNames[Math.floor(Math.random() * lastNames.length)]}_${randomId}`,
|
|
66
|
+
email: `contact_${timestamp}_${randomId}@test.com`,
|
|
67
|
+
phone: `+1-555-${Math.floor(Math.random() * 9000) + 1000}`,
|
|
68
|
+
mobile: `+1-555-${Math.floor(Math.random() * 9000) + 1000}`,
|
|
69
|
+
position: positions[Math.floor(Math.random() * positions.length)],
|
|
70
|
+
department: departments[Math.floor(Math.random() * departments.length)],
|
|
71
|
+
preferredChannel: channels[Math.floor(Math.random() * channels.length)],
|
|
72
|
+
isPrimary: false,
|
|
73
|
+
...overrides
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Validate contact object structure
|
|
79
|
+
*/
|
|
80
|
+
validateObject(contact: Record<string, unknown>, allowMetas = false): void {
|
|
81
|
+
this.validateSystemFields(contact)
|
|
82
|
+
|
|
83
|
+
expect(contact).to.have.property('firstName')
|
|
84
|
+
expect(contact.firstName).to.be.a('string')
|
|
85
|
+
|
|
86
|
+
expect(contact).to.have.property('email')
|
|
87
|
+
expect(contact.email).to.be.a('string')
|
|
88
|
+
|
|
89
|
+
this.validateOptionalStringFields(contact, [
|
|
90
|
+
'lastName', 'phone', 'mobile', 'companyId', 'position',
|
|
91
|
+
'department', 'birthDate', 'linkedin', 'twitter', 'preferredChannel', 'timezone'
|
|
92
|
+
])
|
|
93
|
+
|
|
94
|
+
if (contact.isPrimary !== null && contact.isPrimary !== undefined) {
|
|
95
|
+
expect(contact.isPrimary).to.be.a('boolean')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (allowMetas && Object.prototype.hasOwnProperty.call(contact, 'metas')) {
|
|
99
|
+
expect(contact.metas).to.be.an('object')
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export default ContactAPIController
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LeadAPIController - TypeScript controller for Leads API
|
|
3
|
+
*
|
|
4
|
+
* Handles CRUD operations for /api/v1/leads endpoints
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { BaseAPIController, APIRequestOptions, APIResponse } from './BaseAPIController'
|
|
8
|
+
|
|
9
|
+
export interface LeadData {
|
|
10
|
+
firstName?: string
|
|
11
|
+
lastName?: string
|
|
12
|
+
email?: string
|
|
13
|
+
phone?: string
|
|
14
|
+
company?: string
|
|
15
|
+
source?: string
|
|
16
|
+
status?: string
|
|
17
|
+
rating?: string
|
|
18
|
+
notes?: string
|
|
19
|
+
assignedTo?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface LeadGetAllOptions extends APIRequestOptions {
|
|
23
|
+
status?: string
|
|
24
|
+
source?: string
|
|
25
|
+
rating?: string
|
|
26
|
+
assignedTo?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class LeadAPIController extends BaseAPIController {
|
|
30
|
+
protected entitySlug = 'leads'
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* GET all leads with filtering options
|
|
34
|
+
*/
|
|
35
|
+
getAll(options: LeadGetAllOptions = {}): Cypress.Chainable<APIResponse> {
|
|
36
|
+
return super.getAll(options)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Convert lead to contact
|
|
41
|
+
*/
|
|
42
|
+
convertToContact(id: string, options: APIRequestOptions = {}): Cypress.Chainable<APIResponse> {
|
|
43
|
+
return this.update(id, { status: 'converted' }, options)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Generate random lead data for testing
|
|
48
|
+
*/
|
|
49
|
+
generateRandomData(overrides: Partial<LeadData> = {}): LeadData {
|
|
50
|
+
const timestamp = Date.now()
|
|
51
|
+
const randomId = Math.random().toString(36).substring(2, 8)
|
|
52
|
+
|
|
53
|
+
const sources = ['website', 'referral', 'social_media', 'trade_show', 'cold_call', 'advertisement']
|
|
54
|
+
const statuses = ['new', 'contacted', 'qualified', 'unqualified', 'converted']
|
|
55
|
+
const ratings = ['hot', 'warm', 'cold']
|
|
56
|
+
|
|
57
|
+
const firstNames = ['John', 'Jane', 'Michael', 'Sarah', 'David', 'Emily']
|
|
58
|
+
const lastNames = ['Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia']
|
|
59
|
+
const companies = ['Acme Corp', 'Tech Solutions', 'Global Industries', 'Innovate LLC', 'StartUp Inc']
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
firstName: firstNames[Math.floor(Math.random() * firstNames.length)],
|
|
63
|
+
lastName: `${lastNames[Math.floor(Math.random() * lastNames.length)]}_${randomId}`,
|
|
64
|
+
email: `lead_${timestamp}_${randomId}@test.com`,
|
|
65
|
+
phone: `+1-555-${Math.floor(Math.random() * 9000) + 1000}`,
|
|
66
|
+
company: companies[Math.floor(Math.random() * companies.length)],
|
|
67
|
+
source: sources[Math.floor(Math.random() * sources.length)],
|
|
68
|
+
status: statuses[Math.floor(Math.random() * statuses.length)],
|
|
69
|
+
rating: ratings[Math.floor(Math.random() * ratings.length)],
|
|
70
|
+
...overrides
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Validate lead object structure
|
|
76
|
+
*/
|
|
77
|
+
validateObject(lead: Record<string, unknown>, allowMetas = false): void {
|
|
78
|
+
this.validateSystemFields(lead)
|
|
79
|
+
|
|
80
|
+
expect(lead).to.have.property('firstName')
|
|
81
|
+
expect(lead.firstName).to.be.a('string')
|
|
82
|
+
|
|
83
|
+
expect(lead).to.have.property('email')
|
|
84
|
+
expect(lead.email).to.be.a('string')
|
|
85
|
+
|
|
86
|
+
this.validateOptionalStringFields(lead, [
|
|
87
|
+
'lastName', 'phone', 'company', 'source', 'status', 'rating', 'notes', 'assignedTo'
|
|
88
|
+
])
|
|
89
|
+
|
|
90
|
+
if (allowMetas && Object.prototype.hasOwnProperty.call(lead, 'metas')) {
|
|
91
|
+
expect(lead.metas).to.be.an('object')
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export default LeadAPIController
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NoteAPIController - TypeScript controller for Notes API
|
|
3
|
+
*
|
|
4
|
+
* Handles CRUD operations for /api/v1/notes endpoints
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { BaseAPIController, APIRequestOptions, APIResponse } from './BaseAPIController'
|
|
8
|
+
|
|
9
|
+
export interface NoteData {
|
|
10
|
+
title?: string
|
|
11
|
+
content?: string
|
|
12
|
+
type?: string
|
|
13
|
+
isPinned?: boolean
|
|
14
|
+
isPrivate?: boolean
|
|
15
|
+
contactId?: string
|
|
16
|
+
companyId?: string
|
|
17
|
+
opportunityId?: string
|
|
18
|
+
entityType?: string
|
|
19
|
+
entityId?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface NoteGetAllOptions extends APIRequestOptions {
|
|
23
|
+
type?: string
|
|
24
|
+
isPinned?: boolean
|
|
25
|
+
isPrivate?: boolean
|
|
26
|
+
contactId?: string
|
|
27
|
+
companyId?: string
|
|
28
|
+
opportunityId?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class NoteAPIController extends BaseAPIController {
|
|
32
|
+
protected entitySlug = 'notes'
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* GET all notes with filtering options
|
|
36
|
+
*/
|
|
37
|
+
getAll(options: NoteGetAllOptions = {}): Cypress.Chainable<APIResponse> {
|
|
38
|
+
return super.getAll(options)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Pin a note
|
|
43
|
+
*/
|
|
44
|
+
pin(id: string, options: APIRequestOptions = {}): Cypress.Chainable<APIResponse> {
|
|
45
|
+
return this.update(id, { isPinned: true }, options)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Unpin a note
|
|
50
|
+
*/
|
|
51
|
+
unpin(id: string, options: APIRequestOptions = {}): Cypress.Chainable<APIResponse> {
|
|
52
|
+
return this.update(id, { isPinned: false }, options)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Make note private
|
|
57
|
+
*/
|
|
58
|
+
makePrivate(id: string, options: APIRequestOptions = {}): Cypress.Chainable<APIResponse> {
|
|
59
|
+
return this.update(id, { isPrivate: true }, options)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Make note public
|
|
64
|
+
*/
|
|
65
|
+
makePublic(id: string, options: APIRequestOptions = {}): Cypress.Chainable<APIResponse> {
|
|
66
|
+
return this.update(id, { isPrivate: false }, options)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Generate random note data for testing
|
|
71
|
+
*/
|
|
72
|
+
generateRandomData(overrides: Partial<NoteData> = {}): NoteData {
|
|
73
|
+
const timestamp = Date.now()
|
|
74
|
+
const randomId = Math.random().toString(36).substring(2, 8)
|
|
75
|
+
|
|
76
|
+
const types = ['general', 'meeting', 'call', 'email', 'task', 'followup', 'feedback', 'reminder']
|
|
77
|
+
const titles = [
|
|
78
|
+
'Meeting Notes',
|
|
79
|
+
'Follow-up Required',
|
|
80
|
+
'Important Information',
|
|
81
|
+
'Customer Feedback',
|
|
82
|
+
'Action Items',
|
|
83
|
+
'Project Update'
|
|
84
|
+
]
|
|
85
|
+
const contents = [
|
|
86
|
+
'Discussed product roadmap and upcoming features.',
|
|
87
|
+
'Customer expressed interest in enterprise plan.',
|
|
88
|
+
'Follow up needed regarding pricing discussion.',
|
|
89
|
+
'Technical requirements gathered during call.',
|
|
90
|
+
'Positive feedback on demo presentation.',
|
|
91
|
+
'Next steps defined for implementation.'
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
title: `${titles[Math.floor(Math.random() * titles.length)]} ${randomId}`,
|
|
96
|
+
content: `${contents[Math.floor(Math.random() * contents.length)]} Created at ${new Date(timestamp).toISOString()}`,
|
|
97
|
+
type: types[Math.floor(Math.random() * types.length)],
|
|
98
|
+
isPinned: Math.random() > 0.8,
|
|
99
|
+
isPrivate: Math.random() > 0.9,
|
|
100
|
+
...overrides
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Validate note object structure
|
|
106
|
+
*/
|
|
107
|
+
validateObject(note: Record<string, unknown>, allowMetas = false): void {
|
|
108
|
+
this.validateSystemFields(note)
|
|
109
|
+
|
|
110
|
+
expect(note).to.have.property('content')
|
|
111
|
+
expect(note.content).to.be.a('string')
|
|
112
|
+
|
|
113
|
+
this.validateOptionalStringFields(note, [
|
|
114
|
+
'title', 'type', 'contactId', 'companyId', 'opportunityId', 'entityType', 'entityId'
|
|
115
|
+
])
|
|
116
|
+
|
|
117
|
+
const booleanFields = ['isPinned', 'isPrivate']
|
|
118
|
+
booleanFields.forEach(field => {
|
|
119
|
+
if (note[field] !== null && note[field] !== undefined) {
|
|
120
|
+
expect(note[field]).to.be.a('boolean')
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
if (allowMetas && Object.prototype.hasOwnProperty.call(note, 'metas')) {
|
|
125
|
+
expect(note.metas).to.be.an('object')
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export default NoteAPIController
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpportunityAPIController - TypeScript controller for Opportunities API
|
|
3
|
+
*
|
|
4
|
+
* Handles CRUD operations for /api/v1/opportunities endpoints
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { BaseAPIController, APIRequestOptions, APIResponse } from './BaseAPIController'
|
|
8
|
+
|
|
9
|
+
export interface OpportunityData {
|
|
10
|
+
name?: string
|
|
11
|
+
companyId?: string
|
|
12
|
+
contactId?: string
|
|
13
|
+
pipelineId?: string
|
|
14
|
+
stageId?: string
|
|
15
|
+
amount?: number
|
|
16
|
+
currency?: string
|
|
17
|
+
closeDate?: string
|
|
18
|
+
probability?: number
|
|
19
|
+
type?: string
|
|
20
|
+
source?: string
|
|
21
|
+
competitor?: string
|
|
22
|
+
status?: string
|
|
23
|
+
lostReason?: string
|
|
24
|
+
assignedTo?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface OpportunityGetAllOptions extends APIRequestOptions {
|
|
28
|
+
pipelineId?: string
|
|
29
|
+
stageId?: string
|
|
30
|
+
companyId?: string
|
|
31
|
+
status?: string
|
|
32
|
+
type?: string
|
|
33
|
+
source?: string
|
|
34
|
+
assignedTo?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class OpportunityAPIController extends BaseAPIController {
|
|
38
|
+
protected entitySlug = 'opportunities'
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* GET all opportunities with filtering options
|
|
42
|
+
*/
|
|
43
|
+
getAll(options: OpportunityGetAllOptions = {}): Cypress.Chainable<APIResponse> {
|
|
44
|
+
return super.getAll(options)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Move opportunity to a different stage
|
|
49
|
+
*/
|
|
50
|
+
moveToStage(id: string, stageId: string, options: APIRequestOptions = {}): Cypress.Chainable<APIResponse> {
|
|
51
|
+
return this.update(id, { stageId }, options)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Close opportunity as won
|
|
56
|
+
*/
|
|
57
|
+
closeAsWon(id: string, options: APIRequestOptions = {}): Cypress.Chainable<APIResponse> {
|
|
58
|
+
return this.update(id, { status: 'won', probability: 100 }, options)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Close opportunity as lost
|
|
63
|
+
*/
|
|
64
|
+
closeAsLost(id: string, lostReason: string, options: APIRequestOptions = {}): Cypress.Chainable<APIResponse> {
|
|
65
|
+
return this.update(id, { status: 'lost', probability: 0, lostReason }, options)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Generate random opportunity data for testing
|
|
70
|
+
*/
|
|
71
|
+
generateRandomData(overrides: Partial<OpportunityData> = {}): OpportunityData {
|
|
72
|
+
const timestamp = Date.now()
|
|
73
|
+
const randomId = Math.random().toString(36).substring(2, 8)
|
|
74
|
+
|
|
75
|
+
const types = ['new_business', 'upgrade', 'renewal', 'existing_business']
|
|
76
|
+
const sources = ['web', 'referral', 'social_media', 'trade_show', 'cold_call', 'inbound']
|
|
77
|
+
const statuses = ['open', 'won', 'lost']
|
|
78
|
+
const currencies = ['USD', 'EUR', 'GBP']
|
|
79
|
+
|
|
80
|
+
const opportunityNames = [
|
|
81
|
+
'Enterprise License Deal',
|
|
82
|
+
'Professional Package',
|
|
83
|
+
'Annual Renewal',
|
|
84
|
+
'Premium Upgrade',
|
|
85
|
+
'Custom Implementation',
|
|
86
|
+
'Multi-Year Contract'
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
// Generate future close date (30-180 days from now)
|
|
90
|
+
const daysFromNow = Math.floor(Math.random() * 150) + 30
|
|
91
|
+
const closeDate = new Date()
|
|
92
|
+
closeDate.setDate(closeDate.getDate() + daysFromNow)
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
name: `${opportunityNames[Math.floor(Math.random() * opportunityNames.length)]} ${randomId}`,
|
|
96
|
+
amount: Math.floor(Math.random() * 200000) + 10000,
|
|
97
|
+
currency: currencies[Math.floor(Math.random() * currencies.length)],
|
|
98
|
+
closeDate: closeDate.toISOString().split('T')[0],
|
|
99
|
+
probability: Math.floor(Math.random() * 80) + 10,
|
|
100
|
+
type: types[Math.floor(Math.random() * types.length)],
|
|
101
|
+
source: sources[Math.floor(Math.random() * sources.length)],
|
|
102
|
+
status: statuses[0], // Default to 'open'
|
|
103
|
+
...overrides
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Validate opportunity object structure
|
|
109
|
+
*/
|
|
110
|
+
validateObject(opportunity: Record<string, unknown>, allowMetas = false): void {
|
|
111
|
+
this.validateSystemFields(opportunity)
|
|
112
|
+
|
|
113
|
+
expect(opportunity).to.have.property('name')
|
|
114
|
+
expect(opportunity.name).to.be.a('string')
|
|
115
|
+
|
|
116
|
+
this.validateOptionalStringFields(opportunity, [
|
|
117
|
+
'companyId', 'contactId', 'pipelineId', 'stageId', 'currency',
|
|
118
|
+
'closeDate', 'type', 'source', 'competitor', 'status', 'lostReason', 'assignedTo'
|
|
119
|
+
])
|
|
120
|
+
|
|
121
|
+
const numericFields = ['amount', 'probability']
|
|
122
|
+
numericFields.forEach(field => {
|
|
123
|
+
if (opportunity[field] !== null && opportunity[field] !== undefined) {
|
|
124
|
+
expect(Number(opportunity[field])).to.be.a('number')
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
if (allowMetas && Object.prototype.hasOwnProperty.call(opportunity, 'metas')) {
|
|
129
|
+
expect(opportunity.metas).to.be.an('object')
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export default OpportunityAPIController
|