@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.
Files changed (70) hide show
  1. package/package.json +2 -2
  2. package/tests/cypress/e2e/api/activities/activities-crud.cy.ts +686 -0
  3. package/tests/cypress/e2e/api/campaigns/campaigns-crud.cy.ts +592 -0
  4. package/tests/cypress/e2e/api/companies/companies-crud.cy.ts +682 -0
  5. package/tests/cypress/e2e/api/contacts/contacts-crud.cy.ts +668 -0
  6. package/tests/cypress/e2e/api/leads/leads-crud.cy.ts +648 -0
  7. package/tests/cypress/e2e/api/notes/notes-crud.cy.ts +424 -0
  8. package/tests/cypress/e2e/api/opportunities/opportunities-crud.cy.ts +865 -0
  9. package/tests/cypress/e2e/api/pipelines/pipelines-crud.cy.ts +545 -0
  10. package/tests/cypress/e2e/api/products/products-crud.cy.ts +447 -0
  11. package/tests/cypress/e2e/ui/activities/activities-admin.cy.ts +268 -0
  12. package/tests/cypress/e2e/ui/activities/activities-member.cy.ts +257 -0
  13. package/tests/cypress/e2e/ui/activities/activities-owner.cy.ts +268 -0
  14. package/tests/cypress/e2e/ui/companies/companies-admin.cy.ts +188 -0
  15. package/tests/cypress/e2e/ui/companies/companies-member.cy.ts +166 -0
  16. package/tests/cypress/e2e/ui/companies/companies-owner.cy.ts +189 -0
  17. package/tests/cypress/e2e/ui/contacts/contacts-admin.cy.ts +252 -0
  18. package/tests/cypress/e2e/ui/contacts/contacts-member.cy.ts +224 -0
  19. package/tests/cypress/e2e/ui/contacts/contacts-owner.cy.ts +236 -0
  20. package/tests/cypress/e2e/ui/leads/leads-admin.cy.ts +286 -0
  21. package/tests/cypress/e2e/ui/leads/leads-member.cy.ts +193 -0
  22. package/tests/cypress/e2e/ui/leads/leads-owner.cy.ts +210 -0
  23. package/tests/cypress/e2e/ui/opportunities/opportunities-admin.cy.ts +197 -0
  24. package/tests/cypress/e2e/ui/opportunities/opportunities-member.cy.ts +229 -0
  25. package/tests/cypress/e2e/ui/opportunities/opportunities-owner.cy.ts +196 -0
  26. package/tests/cypress/e2e/ui/pipelines/pipelines-admin.cy.ts +320 -0
  27. package/tests/cypress/e2e/ui/pipelines/pipelines-member.cy.ts +262 -0
  28. package/tests/cypress/e2e/ui/pipelines/pipelines-owner.cy.ts +282 -0
  29. package/tests/cypress/fixtures/blocks.json +9 -0
  30. package/tests/cypress/fixtures/entities.json +240 -0
  31. package/tests/cypress/src/components/CRMDataTable.js +223 -0
  32. package/tests/cypress/src/components/CRMMobileNav.js +138 -0
  33. package/tests/cypress/src/components/CRMSidebar.js +145 -0
  34. package/tests/cypress/src/components/CRMTopBar.js +194 -0
  35. package/tests/cypress/src/components/DealCard.js +197 -0
  36. package/tests/cypress/src/components/EntityDetail.ts +290 -0
  37. package/tests/cypress/src/components/EntityForm.ts +357 -0
  38. package/tests/cypress/src/components/EntityList.ts +360 -0
  39. package/tests/cypress/src/components/PipelineKanban.js +204 -0
  40. package/tests/cypress/src/components/StageColumn.js +196 -0
  41. package/tests/cypress/src/components/index.js +13 -0
  42. package/tests/cypress/src/components/index.ts +22 -0
  43. package/tests/cypress/src/controllers/ActivityAPIController.ts +113 -0
  44. package/tests/cypress/src/controllers/BaseAPIController.ts +307 -0
  45. package/tests/cypress/src/controllers/CampaignAPIController.ts +114 -0
  46. package/tests/cypress/src/controllers/CompanyAPIController.ts +112 -0
  47. package/tests/cypress/src/controllers/ContactAPIController.ts +104 -0
  48. package/tests/cypress/src/controllers/LeadAPIController.ts +96 -0
  49. package/tests/cypress/src/controllers/NoteAPIController.ts +130 -0
  50. package/tests/cypress/src/controllers/OpportunityAPIController.ts +134 -0
  51. package/tests/cypress/src/controllers/PipelineAPIController.ts +116 -0
  52. package/tests/cypress/src/controllers/ProductAPIController.ts +113 -0
  53. package/tests/cypress/src/controllers/index.ts +35 -0
  54. package/tests/cypress/src/entities/ActivitiesPOM.ts +130 -0
  55. package/tests/cypress/src/entities/CompaniesPOM.ts +117 -0
  56. package/tests/cypress/src/entities/ContactsPOM.ts +117 -0
  57. package/tests/cypress/src/entities/LeadsPOM.ts +129 -0
  58. package/tests/cypress/src/entities/OpportunitiesPOM.ts +178 -0
  59. package/tests/cypress/src/entities/PipelinesPOM.ts +341 -0
  60. package/tests/cypress/src/entities/index.ts +31 -0
  61. package/tests/cypress/src/forms/OpportunityForm.js +316 -0
  62. package/tests/cypress/src/forms/PipelineForm.js +243 -0
  63. package/tests/cypress/src/forms/index.js +8 -0
  64. package/tests/cypress/src/index.js +22 -0
  65. package/tests/cypress/src/index.ts +68 -0
  66. package/tests/cypress/src/selectors.ts +50 -0
  67. package/tests/cypress/src/session-helpers.ts +94 -0
  68. package/tests/cypress/support/e2e.ts +89 -0
  69. package/tests/cypress.config.ts +165 -0
  70. 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