@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,865 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opportunities API - CRUD Tests
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive test suite for Opportunity API endpoints.
|
|
5
|
+
* Tests GET, POST, PATCH, DELETE operations.
|
|
6
|
+
*
|
|
7
|
+
* Entity characteristics:
|
|
8
|
+
* - Required fields: name, companyId, pipelineId, stageId, amount, currency, closeDate
|
|
9
|
+
* - Optional fields: contactId, probability, type, source, competitor, status, lostReason, assignedTo
|
|
10
|
+
* - Access: shared within team (all team members see all opportunities)
|
|
11
|
+
* - Team context: required (x-team-id header)
|
|
12
|
+
* - Special: Stage moves, win/loss tracking, weighted value calculation
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/// <reference types="cypress" />
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
OpportunityAPIController,
|
|
19
|
+
CompanyAPIController,
|
|
20
|
+
PipelineAPIController
|
|
21
|
+
} from '../../../src/controllers'
|
|
22
|
+
|
|
23
|
+
describe('Opportunities API - CRUD Operations', () => {
|
|
24
|
+
// Test constants
|
|
25
|
+
const SUPERADMIN_API_KEY = 'test_api_key_for_testing_purposes_only_not_a_real_secret_key_abc123'
|
|
26
|
+
const TEAM_ID = 'team-tmt-001'
|
|
27
|
+
const BASE_URL = Cypress.config('baseUrl') || 'http://localhost:5173'
|
|
28
|
+
|
|
29
|
+
// Controller instances
|
|
30
|
+
let opportunityAPI: InstanceType<typeof OpportunityAPIController>
|
|
31
|
+
let companyAPI: InstanceType<typeof CompanyAPIController>
|
|
32
|
+
let pipelineAPI: InstanceType<typeof PipelineAPIController>
|
|
33
|
+
|
|
34
|
+
// Track created resources for cleanup
|
|
35
|
+
let createdOpportunities: any[] = []
|
|
36
|
+
let createdCompanies: any[] = []
|
|
37
|
+
let createdPipelines: any[] = []
|
|
38
|
+
|
|
39
|
+
// Test fixtures (created once in before hook)
|
|
40
|
+
let testCompany: any
|
|
41
|
+
let testPipeline: any
|
|
42
|
+
let testStageId: string
|
|
43
|
+
|
|
44
|
+
before(() => {
|
|
45
|
+
// Initialize controllers with superadmin credentials
|
|
46
|
+
opportunityAPI = new OpportunityAPIController(BASE_URL, SUPERADMIN_API_KEY, TEAM_ID)
|
|
47
|
+
companyAPI = new CompanyAPIController(BASE_URL, SUPERADMIN_API_KEY, TEAM_ID)
|
|
48
|
+
pipelineAPI = new PipelineAPIController(BASE_URL, SUPERADMIN_API_KEY, TEAM_ID)
|
|
49
|
+
|
|
50
|
+
// Create test company (shared across tests) - with retry for 500 errors
|
|
51
|
+
cy.wrap(null).then(() => {
|
|
52
|
+
return companyAPI.createTestRecord({
|
|
53
|
+
name: `Test Company for Opportunities ${Date.now()}`,
|
|
54
|
+
type: 'prospect',
|
|
55
|
+
size: '11-50'
|
|
56
|
+
}, { withRetry: true }).then((company: any) => {
|
|
57
|
+
testCompany = company
|
|
58
|
+
createdCompanies.push(testCompany)
|
|
59
|
+
cy.log(`Created test company: ${testCompany.id}`)
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// Create test pipeline with stages (shared across tests)
|
|
64
|
+
cy.wrap(null).then(() => {
|
|
65
|
+
return pipelineAPI.createTestRecord({
|
|
66
|
+
name: 'Test Pipeline for Opportunities',
|
|
67
|
+
stages: [
|
|
68
|
+
{ name: 'Qualification', probability: 10, order: 1 },
|
|
69
|
+
{ name: 'Proposal', probability: 50, order: 2 },
|
|
70
|
+
{ name: 'Negotiation', probability: 75, order: 3 },
|
|
71
|
+
{ name: 'Closed Won', probability: 100, order: 4 }
|
|
72
|
+
]
|
|
73
|
+
}, { withRetry: true }).then((pipeline: any) => {
|
|
74
|
+
testPipeline = pipeline
|
|
75
|
+
createdPipelines.push(pipeline)
|
|
76
|
+
// Get first stage ID for testing
|
|
77
|
+
if (pipeline.stages && pipeline.stages.length > 0) {
|
|
78
|
+
testStageId = pipeline.stages[0].id || `stage-${Date.now()}`
|
|
79
|
+
}
|
|
80
|
+
cy.log(`Created test pipeline: ${testPipeline.id} with ${pipeline.stages?.length || 0} stages`)
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
afterEach(() => {
|
|
86
|
+
// Cleanup created opportunities after each test
|
|
87
|
+
createdOpportunities.forEach((opportunity) => {
|
|
88
|
+
if (opportunity?.id) {
|
|
89
|
+
opportunityAPI.delete(opportunity.id)
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
createdOpportunities = []
|
|
93
|
+
// Small delay to allow database connections to be released
|
|
94
|
+
cy.wait(200)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
after(() => {
|
|
98
|
+
// Cleanup shared test fixtures
|
|
99
|
+
if (testCompany?.id) {
|
|
100
|
+
companyAPI.delete(testCompany.id)
|
|
101
|
+
}
|
|
102
|
+
if (testPipeline?.id) {
|
|
103
|
+
pipelineAPI.delete(testPipeline.id)
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
// ============================================
|
|
108
|
+
// GET /api/v1/opportunities - List Opportunities
|
|
109
|
+
// ============================================
|
|
110
|
+
describe('GET /api/v1/opportunities - List Opportunities', () => {
|
|
111
|
+
it('OPPO_API_001: Should list opportunities with valid API key', () => {
|
|
112
|
+
opportunityAPI.getAll().then((response: any) => {
|
|
113
|
+
opportunityAPI.validateSuccessResponse(response, 200)
|
|
114
|
+
expect(response.body.data).to.be.an('array')
|
|
115
|
+
expect(response.body.info).to.have.property('page')
|
|
116
|
+
expect(response.body.info).to.have.property('limit')
|
|
117
|
+
expect(response.body.info).to.have.property('total')
|
|
118
|
+
expect(response.body.info).to.have.property('totalPages')
|
|
119
|
+
|
|
120
|
+
cy.log(`Found ${response.body.data.length} opportunities`)
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('OPPO_API_002: Should list opportunities with pagination', () => {
|
|
125
|
+
opportunityAPI.getAll({ page: 1, limit: 5 }).then((response: any) => {
|
|
126
|
+
opportunityAPI.validateSuccessResponse(response, 200)
|
|
127
|
+
expect(response.body.info.page).to.eq(1)
|
|
128
|
+
expect(response.body.info.limit).to.eq(5)
|
|
129
|
+
expect(response.body.data.length).to.be.at.most(5)
|
|
130
|
+
|
|
131
|
+
cy.log(`Page 1 with limit 5: ${response.body.data.length} opportunities`)
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('OPPO_API_003: Should filter opportunities by pipelineId', () => {
|
|
136
|
+
// First create an opportunity with the test pipeline
|
|
137
|
+
const opportunityData = opportunityAPI.generateRandomData({
|
|
138
|
+
companyId: testCompany.id,
|
|
139
|
+
pipelineId: testPipeline.id,
|
|
140
|
+
stageId: testStageId
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
opportunityAPI.create(opportunityData).then((createResponse: any) => {
|
|
144
|
+
expect(createResponse.status).to.eq(201)
|
|
145
|
+
createdOpportunities.push(createResponse.body.data)
|
|
146
|
+
|
|
147
|
+
// Now filter by that pipeline
|
|
148
|
+
opportunityAPI.getAll({ pipelineId: testPipeline.id }).then((response: any) => {
|
|
149
|
+
opportunityAPI.validateSuccessResponse(response, 200)
|
|
150
|
+
expect(response.body.data).to.be.an('array')
|
|
151
|
+
|
|
152
|
+
// All returned opportunities should have the specified pipelineId
|
|
153
|
+
response.body.data.forEach((opportunity: any) => {
|
|
154
|
+
expect(opportunity.pipelineId).to.eq(testPipeline.id)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
cy.log(`Found ${response.body.data.length} opportunities for pipeline '${testPipeline.id}'`)
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('OPPO_API_004: Should filter opportunities by stageId', () => {
|
|
163
|
+
// Create an opportunity with specific stage
|
|
164
|
+
const opportunityData = opportunityAPI.generateRandomData({
|
|
165
|
+
companyId: testCompany.id,
|
|
166
|
+
pipelineId: testPipeline.id,
|
|
167
|
+
stageId: testStageId
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
opportunityAPI.create(opportunityData).then((createResponse: any) => {
|
|
171
|
+
expect(createResponse.status).to.eq(201)
|
|
172
|
+
createdOpportunities.push(createResponse.body.data)
|
|
173
|
+
|
|
174
|
+
// Filter by that stage
|
|
175
|
+
opportunityAPI.getAll({ stageId: testStageId }).then((response: any) => {
|
|
176
|
+
opportunityAPI.validateSuccessResponse(response, 200)
|
|
177
|
+
expect(response.body.data).to.be.an('array')
|
|
178
|
+
|
|
179
|
+
// All returned opportunities should have the specified stageId
|
|
180
|
+
response.body.data.forEach((opportunity: any) => {
|
|
181
|
+
expect(opportunity.stageId).to.eq(testStageId)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
cy.log(`Found ${response.body.data.length} opportunities in stage '${testStageId}'`)
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('OPPO_API_005: Should filter opportunities by status', () => {
|
|
190
|
+
// Create an opportunity with specific status
|
|
191
|
+
const opportunityData = opportunityAPI.generateRandomData({
|
|
192
|
+
companyId: testCompany.id,
|
|
193
|
+
pipelineId: testPipeline.id,
|
|
194
|
+
stageId: testStageId,
|
|
195
|
+
status: 'open'
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
opportunityAPI.create(opportunityData).then((createResponse: any) => {
|
|
199
|
+
expect(createResponse.status).to.eq(201)
|
|
200
|
+
createdOpportunities.push(createResponse.body.data)
|
|
201
|
+
|
|
202
|
+
// Filter by that status
|
|
203
|
+
opportunityAPI.getAll({ status: 'open' }).then((response: any) => {
|
|
204
|
+
opportunityAPI.validateSuccessResponse(response, 200)
|
|
205
|
+
expect(response.body.data).to.be.an('array')
|
|
206
|
+
|
|
207
|
+
// All returned opportunities should have the specified status
|
|
208
|
+
response.body.data.forEach((opportunity: any) => {
|
|
209
|
+
expect(opportunity.status).to.eq('open')
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
cy.log(`Found ${response.body.data.length} opportunities with status 'open'`)
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('OPPO_API_006: Should search opportunities by name', () => {
|
|
218
|
+
// Create an opportunity with unique searchable term
|
|
219
|
+
const uniqueTerm = `SearchOppo${Date.now()}`
|
|
220
|
+
const opportunityData = opportunityAPI.generateRandomData({
|
|
221
|
+
name: `${uniqueTerm} Deal`,
|
|
222
|
+
companyId: testCompany.id,
|
|
223
|
+
pipelineId: testPipeline.id,
|
|
224
|
+
stageId: testStageId
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
opportunityAPI.create(opportunityData).then((createResponse: any) => {
|
|
228
|
+
expect(createResponse.status).to.eq(201)
|
|
229
|
+
createdOpportunities.push(createResponse.body.data)
|
|
230
|
+
|
|
231
|
+
// Search for the unique term
|
|
232
|
+
opportunityAPI.getAll({ search: uniqueTerm }).then((response: any) => {
|
|
233
|
+
opportunityAPI.validateSuccessResponse(response, 200)
|
|
234
|
+
expect(response.body.data).to.be.an('array')
|
|
235
|
+
expect(response.body.data.length).to.be.greaterThan(0)
|
|
236
|
+
|
|
237
|
+
// Verify the found opportunity contains our search term
|
|
238
|
+
const foundOpportunity = response.body.data.find(
|
|
239
|
+
(o: any) => o.id === createResponse.body.data.id
|
|
240
|
+
)
|
|
241
|
+
expect(foundOpportunity).to.exist
|
|
242
|
+
expect(foundOpportunity.name).to.include(uniqueTerm)
|
|
243
|
+
|
|
244
|
+
cy.log(`Search found ${response.body.data.length} opportunities matching '${uniqueTerm}'`)
|
|
245
|
+
})
|
|
246
|
+
})
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('OPPO_API_007: Should reject request without API key', () => {
|
|
250
|
+
const noAuthAPI = new OpportunityAPIController(BASE_URL, null, TEAM_ID)
|
|
251
|
+
|
|
252
|
+
noAuthAPI.getAll().then((response: any) => {
|
|
253
|
+
expect(response.status).to.eq(401)
|
|
254
|
+
expect(response.body).to.have.property('success', false)
|
|
255
|
+
|
|
256
|
+
cy.log('Request without API key rejected with 401')
|
|
257
|
+
})
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it('OPPO_API_008: Should reject request without x-team-id', () => {
|
|
261
|
+
const noTeamAPI = new OpportunityAPIController(BASE_URL, SUPERADMIN_API_KEY, null)
|
|
262
|
+
|
|
263
|
+
noTeamAPI.getAll().then((response: any) => {
|
|
264
|
+
expect(response.status).to.eq(400)
|
|
265
|
+
expect(response.body).to.have.property('success', false)
|
|
266
|
+
expect(response.body).to.have.property('code', 'TEAM_CONTEXT_REQUIRED')
|
|
267
|
+
|
|
268
|
+
cy.log('Request without x-team-id rejected with TEAM_CONTEXT_REQUIRED')
|
|
269
|
+
})
|
|
270
|
+
})
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
// ============================================
|
|
274
|
+
// POST /api/v1/opportunities - Create Opportunity
|
|
275
|
+
// ============================================
|
|
276
|
+
describe('POST /api/v1/opportunities - Create Opportunity', () => {
|
|
277
|
+
it('OPPO_API_010: Should create opportunity with valid data', () => {
|
|
278
|
+
const opportunityData = opportunityAPI.generateRandomData({
|
|
279
|
+
name: 'Enterprise Deal Q4',
|
|
280
|
+
companyId: testCompany.id,
|
|
281
|
+
pipelineId: testPipeline.id,
|
|
282
|
+
stageId: testStageId,
|
|
283
|
+
amount: 150000,
|
|
284
|
+
currency: 'USD',
|
|
285
|
+
closeDate: '2025-12-31',
|
|
286
|
+
probability: 50,
|
|
287
|
+
type: 'new_business',
|
|
288
|
+
source: 'web',
|
|
289
|
+
status: 'open'
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
opportunityAPI.create(opportunityData).then((response: any) => {
|
|
293
|
+
opportunityAPI.validateSuccessResponse(response, 201)
|
|
294
|
+
createdOpportunities.push(response.body.data)
|
|
295
|
+
|
|
296
|
+
const opportunity = response.body.data
|
|
297
|
+
opportunityAPI.validateObject(opportunity)
|
|
298
|
+
|
|
299
|
+
// Verify provided data
|
|
300
|
+
expect(opportunity.name).to.eq(opportunityData.name)
|
|
301
|
+
expect(opportunity.companyId).to.eq(opportunityData.companyId)
|
|
302
|
+
expect(opportunity.pipelineId).to.eq(opportunityData.pipelineId)
|
|
303
|
+
expect(opportunity.stageId).to.eq(opportunityData.stageId)
|
|
304
|
+
expect(Number(opportunity.amount)).to.eq(opportunityData.amount)
|
|
305
|
+
expect(opportunity.currency).to.eq(opportunityData.currency)
|
|
306
|
+
expect(opportunity.closeDate).to.include(opportunityData.closeDate.split('T')[0])
|
|
307
|
+
expect(Number(opportunity.probability)).to.eq(opportunityData.probability)
|
|
308
|
+
expect(opportunity.type).to.eq(opportunityData.type)
|
|
309
|
+
expect(opportunity.source).to.eq(opportunityData.source)
|
|
310
|
+
expect(opportunity.status).to.eq(opportunityData.status)
|
|
311
|
+
|
|
312
|
+
cy.log(`Created opportunity: ${opportunity.name} (ID: ${opportunity.id})`)
|
|
313
|
+
})
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
it('OPPO_API_011: Should create opportunity with minimal required fields', () => {
|
|
317
|
+
const minimalData = {
|
|
318
|
+
name: `Minimal Opportunity ${Date.now()}`,
|
|
319
|
+
companyId: testCompany.id,
|
|
320
|
+
pipelineId: testPipeline.id,
|
|
321
|
+
stageId: testStageId,
|
|
322
|
+
amount: 50000,
|
|
323
|
+
currency: 'USD',
|
|
324
|
+
closeDate: '2025-12-15'
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
opportunityAPI.create(minimalData).then((response: any) => {
|
|
328
|
+
opportunityAPI.validateSuccessResponse(response, 201)
|
|
329
|
+
createdOpportunities.push(response.body.data)
|
|
330
|
+
|
|
331
|
+
const opportunity = response.body.data
|
|
332
|
+
opportunityAPI.validateObject(opportunity)
|
|
333
|
+
|
|
334
|
+
// Verify required fields
|
|
335
|
+
expect(opportunity.name).to.eq(minimalData.name)
|
|
336
|
+
expect(opportunity.companyId).to.eq(minimalData.companyId)
|
|
337
|
+
expect(opportunity.pipelineId).to.eq(minimalData.pipelineId)
|
|
338
|
+
expect(opportunity.stageId).to.eq(minimalData.stageId)
|
|
339
|
+
expect(Number(opportunity.amount)).to.eq(minimalData.amount)
|
|
340
|
+
expect(opportunity.currency).to.eq(minimalData.currency)
|
|
341
|
+
expect(opportunity.closeDate).to.include(minimalData.closeDate.split('T')[0])
|
|
342
|
+
|
|
343
|
+
cy.log(`Created opportunity with minimal data: ${opportunity.id}`)
|
|
344
|
+
})
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
it('OPPO_API_012: Should create opportunity with all optional fields', () => {
|
|
348
|
+
const opportunityData = opportunityAPI.generateRandomData({
|
|
349
|
+
name: `Complete Opportunity ${Date.now()}`,
|
|
350
|
+
companyId: testCompany.id,
|
|
351
|
+
pipelineId: testPipeline.id,
|
|
352
|
+
stageId: testStageId,
|
|
353
|
+
amount: 200000,
|
|
354
|
+
currency: 'EUR',
|
|
355
|
+
closeDate: '2025-10-30',
|
|
356
|
+
probability: 75,
|
|
357
|
+
type: 'upgrade',
|
|
358
|
+
source: 'referral',
|
|
359
|
+
competitor: 'Competitor XYZ',
|
|
360
|
+
status: 'open'
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
opportunityAPI.create(opportunityData).then((response: any) => {
|
|
364
|
+
opportunityAPI.validateSuccessResponse(response, 201)
|
|
365
|
+
createdOpportunities.push(response.body.data)
|
|
366
|
+
|
|
367
|
+
const opportunity = response.body.data
|
|
368
|
+
|
|
369
|
+
// Verify all fields
|
|
370
|
+
expect(opportunity.name).to.eq(opportunityData.name)
|
|
371
|
+
expect(opportunity.companyId).to.eq(opportunityData.companyId)
|
|
372
|
+
expect(opportunity.pipelineId).to.eq(opportunityData.pipelineId)
|
|
373
|
+
expect(opportunity.stageId).to.eq(opportunityData.stageId)
|
|
374
|
+
expect(Number(opportunity.amount)).to.eq(opportunityData.amount)
|
|
375
|
+
expect(opportunity.currency).to.eq(opportunityData.currency)
|
|
376
|
+
expect(opportunity.closeDate).to.include(opportunityData.closeDate.split('T')[0])
|
|
377
|
+
expect(Number(opportunity.probability)).to.eq(opportunityData.probability)
|
|
378
|
+
expect(opportunity.type).to.eq(opportunityData.type)
|
|
379
|
+
expect(opportunity.source).to.eq(opportunityData.source)
|
|
380
|
+
expect(opportunity.competitor).to.eq(opportunityData.competitor)
|
|
381
|
+
expect(opportunity.status).to.eq(opportunityData.status)
|
|
382
|
+
|
|
383
|
+
cy.log(`Created opportunity with all fields: ${opportunity.id}`)
|
|
384
|
+
})
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
it('OPPO_API_013: Should reject creation without name', () => {
|
|
388
|
+
const invalidData = {
|
|
389
|
+
companyId: testCompany.id,
|
|
390
|
+
pipelineId: testPipeline.id,
|
|
391
|
+
stageId: testStageId,
|
|
392
|
+
amount: 50000,
|
|
393
|
+
currency: 'USD',
|
|
394
|
+
closeDate: '2025-12-31'
|
|
395
|
+
// Missing: name
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
opportunityAPI.create(invalidData).then((response: any) => {
|
|
399
|
+
opportunityAPI.validateErrorResponse(response, 400, 'VALIDATION_ERROR')
|
|
400
|
+
|
|
401
|
+
cy.log('Creation without name rejected with VALIDATION_ERROR')
|
|
402
|
+
})
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
it('OPPO_API_014: Should allow creation without companyId (nullable relation)', () => {
|
|
406
|
+
// Note: companyId is nullable in the database even though entity marks it required
|
|
407
|
+
const minimalData = {
|
|
408
|
+
name: 'Test Opportunity',
|
|
409
|
+
pipelineId: testPipeline.id,
|
|
410
|
+
stageId: testStageId,
|
|
411
|
+
amount: 50000,
|
|
412
|
+
currency: 'USD',
|
|
413
|
+
closeDate: '2025-12-31'
|
|
414
|
+
// Missing: companyId - API may allow null
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
opportunityAPI.create(minimalData).then((response: any) => {
|
|
418
|
+
if (response.status === 201) {
|
|
419
|
+
createdOpportunities.push(response.body.data)
|
|
420
|
+
cy.log('Creation without companyId allowed - nullable relation')
|
|
421
|
+
} else {
|
|
422
|
+
expect(response.status).to.eq(400)
|
|
423
|
+
cy.log('Creation without companyId rejected')
|
|
424
|
+
}
|
|
425
|
+
})
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
it('OPPO_API_015: Should allow creation without pipelineId (nullable relation)', () => {
|
|
429
|
+
// Note: pipelineId is nullable in the database even though entity marks it required
|
|
430
|
+
const minimalData = {
|
|
431
|
+
name: 'Test Opportunity',
|
|
432
|
+
companyId: testCompany.id,
|
|
433
|
+
stageId: testStageId,
|
|
434
|
+
amount: 50000,
|
|
435
|
+
currency: 'USD',
|
|
436
|
+
closeDate: '2025-12-31'
|
|
437
|
+
// Missing: pipelineId - API may allow null
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
opportunityAPI.create(minimalData).then((response: any) => {
|
|
441
|
+
if (response.status === 201) {
|
|
442
|
+
createdOpportunities.push(response.body.data)
|
|
443
|
+
cy.log('Creation without pipelineId allowed - nullable relation')
|
|
444
|
+
} else {
|
|
445
|
+
expect(response.status).to.eq(400)
|
|
446
|
+
cy.log('Creation without pipelineId rejected')
|
|
447
|
+
}
|
|
448
|
+
})
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
it('OPPO_API_016: Should reject creation without stageId', () => {
|
|
452
|
+
const invalidData = {
|
|
453
|
+
name: 'Test Opportunity',
|
|
454
|
+
companyId: testCompany.id,
|
|
455
|
+
pipelineId: testPipeline.id,
|
|
456
|
+
amount: 50000,
|
|
457
|
+
currency: 'USD',
|
|
458
|
+
closeDate: '2025-12-31'
|
|
459
|
+
// Missing: stageId
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
opportunityAPI.create(invalidData).then((response: any) => {
|
|
463
|
+
opportunityAPI.validateErrorResponse(response, 400, 'VALIDATION_ERROR')
|
|
464
|
+
|
|
465
|
+
cy.log('Creation without stageId rejected with VALIDATION_ERROR')
|
|
466
|
+
})
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
it('OPPO_API_017: Should reject creation without x-team-id', () => {
|
|
470
|
+
const noTeamAPI = new OpportunityAPIController(BASE_URL, SUPERADMIN_API_KEY, null)
|
|
471
|
+
const opportunityData = opportunityAPI.generateRandomData({
|
|
472
|
+
companyId: testCompany.id,
|
|
473
|
+
pipelineId: testPipeline.id,
|
|
474
|
+
stageId: testStageId
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
noTeamAPI.create(opportunityData).then((response: any) => {
|
|
478
|
+
expect(response.status).to.eq(400)
|
|
479
|
+
expect(response.body).to.have.property('success', false)
|
|
480
|
+
expect(response.body).to.have.property('code', 'TEAM_CONTEXT_REQUIRED')
|
|
481
|
+
|
|
482
|
+
cy.log('Creation without x-team-id rejected with TEAM_CONTEXT_REQUIRED')
|
|
483
|
+
})
|
|
484
|
+
})
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
// ============================================
|
|
488
|
+
// GET /api/v1/opportunities/{id} - Get Opportunity by ID
|
|
489
|
+
// ============================================
|
|
490
|
+
describe('GET /api/v1/opportunities/{id} - Get Opportunity by ID', () => {
|
|
491
|
+
it('OPPO_API_020: Should get opportunity by valid ID', () => {
|
|
492
|
+
// First create an opportunity
|
|
493
|
+
const opportunityData = opportunityAPI.generateRandomData({
|
|
494
|
+
companyId: testCompany.id,
|
|
495
|
+
pipelineId: testPipeline.id,
|
|
496
|
+
stageId: testStageId
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
opportunityAPI.create(opportunityData).then((createResponse: any) => {
|
|
500
|
+
expect(createResponse.status).to.eq(201)
|
|
501
|
+
createdOpportunities.push(createResponse.body.data)
|
|
502
|
+
|
|
503
|
+
const opportunityId = createResponse.body.data.id
|
|
504
|
+
|
|
505
|
+
// Get the opportunity by ID
|
|
506
|
+
opportunityAPI.getById(opportunityId).then((response: any) => {
|
|
507
|
+
opportunityAPI.validateSuccessResponse(response, 200)
|
|
508
|
+
|
|
509
|
+
const opportunity = response.body.data
|
|
510
|
+
opportunityAPI.validateObject(opportunity)
|
|
511
|
+
expect(opportunity.id).to.eq(opportunityId)
|
|
512
|
+
expect(opportunity.name).to.eq(opportunityData.name)
|
|
513
|
+
|
|
514
|
+
cy.log(`Retrieved opportunity: ${opportunity.name}`)
|
|
515
|
+
})
|
|
516
|
+
})
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
it('OPPO_API_021: Should return 404 for non-existent opportunity', () => {
|
|
520
|
+
const fakeId = 'non-existent-opportunity-id-12345'
|
|
521
|
+
|
|
522
|
+
opportunityAPI.getById(fakeId).then((response: any) => {
|
|
523
|
+
expect(response.status).to.eq(404)
|
|
524
|
+
expect(response.body).to.have.property('success', false)
|
|
525
|
+
|
|
526
|
+
cy.log('Non-existent opportunity returns 404')
|
|
527
|
+
})
|
|
528
|
+
})
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
// ============================================
|
|
532
|
+
// PATCH /api/v1/opportunities/{id} - Update Opportunity
|
|
533
|
+
// ============================================
|
|
534
|
+
describe('PATCH /api/v1/opportunities/{id} - Update Opportunity', () => {
|
|
535
|
+
it('OPPO_API_030: Should update opportunity with multiple fields', () => {
|
|
536
|
+
// First create an opportunity
|
|
537
|
+
opportunityAPI.createTestRecord({
|
|
538
|
+
companyId: testCompany.id,
|
|
539
|
+
pipelineId: testPipeline.id,
|
|
540
|
+
stageId: testStageId
|
|
541
|
+
}, { withRetry: true }).then((testOpportunity: any) => {
|
|
542
|
+
createdOpportunities.push(testOpportunity)
|
|
543
|
+
|
|
544
|
+
const updateData = {
|
|
545
|
+
name: 'Updated Opportunity Name',
|
|
546
|
+
amount: 250000,
|
|
547
|
+
probability: 80,
|
|
548
|
+
type: 'existing_business',
|
|
549
|
+
source: 'social_media'
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
opportunityAPI.update(testOpportunity.id, updateData).then((response: any) => {
|
|
553
|
+
opportunityAPI.validateSuccessResponse(response, 200)
|
|
554
|
+
|
|
555
|
+
const opportunity = response.body.data
|
|
556
|
+
expect(opportunity.name).to.eq(updateData.name)
|
|
557
|
+
expect(Number(opportunity.amount)).to.eq(updateData.amount)
|
|
558
|
+
expect(Number(opportunity.probability)).to.eq(updateData.probability)
|
|
559
|
+
expect(opportunity.type).to.eq(updateData.type)
|
|
560
|
+
expect(opportunity.source).to.eq(updateData.source)
|
|
561
|
+
// Original values should be preserved
|
|
562
|
+
expect(opportunity.companyId).to.eq(testOpportunity.companyId)
|
|
563
|
+
expect(opportunity.pipelineId).to.eq(testOpportunity.pipelineId)
|
|
564
|
+
|
|
565
|
+
cy.log(`Updated opportunity: ${opportunity.name}`)
|
|
566
|
+
})
|
|
567
|
+
})
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
it('OPPO_API_031: Should update stageId (move stage)', () => {
|
|
571
|
+
// Create opportunity in first stage
|
|
572
|
+
opportunityAPI.createTestRecord({
|
|
573
|
+
companyId: testCompany.id,
|
|
574
|
+
pipelineId: testPipeline.id,
|
|
575
|
+
stageId: testStageId
|
|
576
|
+
}, { withRetry: true }).then((testOpportunity: any) => {
|
|
577
|
+
createdOpportunities.push(testOpportunity)
|
|
578
|
+
|
|
579
|
+
// Get second stage ID
|
|
580
|
+
const secondStageId = testPipeline.stages[1]?.id || testStageId
|
|
581
|
+
|
|
582
|
+
// Move to second stage
|
|
583
|
+
opportunityAPI.update(testOpportunity.id, { stageId: secondStageId }).then((response: any) => {
|
|
584
|
+
opportunityAPI.validateSuccessResponse(response, 200)
|
|
585
|
+
expect(response.body.data.stageId).to.eq(secondStageId)
|
|
586
|
+
|
|
587
|
+
cy.log(`Moved opportunity to stage: ${secondStageId}`)
|
|
588
|
+
})
|
|
589
|
+
})
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
it('OPPO_API_032: Should update amount and probability', () => {
|
|
593
|
+
opportunityAPI.createTestRecord({
|
|
594
|
+
companyId: testCompany.id,
|
|
595
|
+
pipelineId: testPipeline.id,
|
|
596
|
+
stageId: testStageId
|
|
597
|
+
}, { withRetry: true }).then((testOpportunity: any) => {
|
|
598
|
+
createdOpportunities.push(testOpportunity)
|
|
599
|
+
|
|
600
|
+
const updateData = {
|
|
601
|
+
amount: 300000,
|
|
602
|
+
probability: 90
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
opportunityAPI.update(testOpportunity.id, updateData).then((response: any) => {
|
|
606
|
+
opportunityAPI.validateSuccessResponse(response, 200)
|
|
607
|
+
expect(Number(response.body.data.amount)).to.eq(updateData.amount)
|
|
608
|
+
expect(Number(response.body.data.probability)).to.eq(updateData.probability)
|
|
609
|
+
|
|
610
|
+
// Weighted value = amount * (probability / 100)
|
|
611
|
+
const expectedWeightedValue = updateData.amount * (updateData.probability / 100)
|
|
612
|
+
cy.log(`Updated amount: ${updateData.amount}, probability: ${updateData.probability}%`)
|
|
613
|
+
cy.log(`Expected weighted value: ${expectedWeightedValue}`)
|
|
614
|
+
})
|
|
615
|
+
})
|
|
616
|
+
})
|
|
617
|
+
|
|
618
|
+
it('OPPO_API_033: Should update status to won', () => {
|
|
619
|
+
opportunityAPI.createTestRecord({
|
|
620
|
+
companyId: testCompany.id,
|
|
621
|
+
pipelineId: testPipeline.id,
|
|
622
|
+
stageId: testStageId,
|
|
623
|
+
status: 'open'
|
|
624
|
+
}, { withRetry: true }).then((testOpportunity: any) => {
|
|
625
|
+
createdOpportunities.push(testOpportunity)
|
|
626
|
+
|
|
627
|
+
const updateData = {
|
|
628
|
+
status: 'won',
|
|
629
|
+
probability: 100
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
opportunityAPI.update(testOpportunity.id, updateData).then((response: any) => {
|
|
633
|
+
opportunityAPI.validateSuccessResponse(response, 200)
|
|
634
|
+
expect(response.body.data.status).to.eq('won')
|
|
635
|
+
expect(Number(response.body.data.probability)).to.eq(100)
|
|
636
|
+
|
|
637
|
+
cy.log(`Opportunity marked as WON`)
|
|
638
|
+
})
|
|
639
|
+
})
|
|
640
|
+
})
|
|
641
|
+
|
|
642
|
+
it('OPPO_API_034: Should update status to lost with lostReason', () => {
|
|
643
|
+
opportunityAPI.createTestRecord({
|
|
644
|
+
companyId: testCompany.id,
|
|
645
|
+
pipelineId: testPipeline.id,
|
|
646
|
+
stageId: testStageId,
|
|
647
|
+
status: 'open'
|
|
648
|
+
}, { withRetry: true }).then((testOpportunity: any) => {
|
|
649
|
+
createdOpportunities.push(testOpportunity)
|
|
650
|
+
|
|
651
|
+
const updateData = {
|
|
652
|
+
status: 'lost',
|
|
653
|
+
probability: 0,
|
|
654
|
+
lostReason: 'Price too high'
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
opportunityAPI.update(testOpportunity.id, updateData).then((response: any) => {
|
|
658
|
+
opportunityAPI.validateSuccessResponse(response, 200)
|
|
659
|
+
expect(response.body.data.status).to.eq('lost')
|
|
660
|
+
expect(Number(response.body.data.probability)).to.eq(0)
|
|
661
|
+
expect(response.body.data.lostReason).to.eq(updateData.lostReason)
|
|
662
|
+
|
|
663
|
+
cy.log(`Opportunity marked as LOST: ${updateData.lostReason}`)
|
|
664
|
+
})
|
|
665
|
+
})
|
|
666
|
+
})
|
|
667
|
+
|
|
668
|
+
it('OPPO_API_035: Should return 404 for non-existent opportunity', () => {
|
|
669
|
+
const fakeId = 'non-existent-opportunity-id-12345'
|
|
670
|
+
|
|
671
|
+
opportunityAPI.update(fakeId, { name: 'New Name' }).then((response: any) => {
|
|
672
|
+
expect(response.status).to.eq(404)
|
|
673
|
+
expect(response.body).to.have.property('success', false)
|
|
674
|
+
|
|
675
|
+
cy.log('Update non-existent opportunity returns 404')
|
|
676
|
+
})
|
|
677
|
+
})
|
|
678
|
+
|
|
679
|
+
it('OPPO_API_036: Should reject empty update body', () => {
|
|
680
|
+
opportunityAPI.createTestRecord({
|
|
681
|
+
companyId: testCompany.id,
|
|
682
|
+
pipelineId: testPipeline.id,
|
|
683
|
+
stageId: testStageId
|
|
684
|
+
}, { withRetry: true }).then((testOpportunity: any) => {
|
|
685
|
+
createdOpportunities.push(testOpportunity)
|
|
686
|
+
|
|
687
|
+
opportunityAPI.update(testOpportunity.id, {}).then((response: any) => {
|
|
688
|
+
expect(response.status).to.eq(400)
|
|
689
|
+
expect(response.body).to.have.property('success', false)
|
|
690
|
+
// Error code can be NO_FIELDS or HTTP_400 depending on validation layer
|
|
691
|
+
expect(response.body.code).to.be.oneOf(['NO_FIELDS', 'HTTP_400'])
|
|
692
|
+
|
|
693
|
+
cy.log('Empty update body rejected')
|
|
694
|
+
})
|
|
695
|
+
})
|
|
696
|
+
})
|
|
697
|
+
})
|
|
698
|
+
|
|
699
|
+
// ============================================
|
|
700
|
+
// DELETE /api/v1/opportunities/{id} - Delete Opportunity
|
|
701
|
+
// ============================================
|
|
702
|
+
describe('DELETE /api/v1/opportunities/{id} - Delete Opportunity', () => {
|
|
703
|
+
it('OPPO_API_040: Should delete opportunity by valid ID', () => {
|
|
704
|
+
// Create an opportunity to delete
|
|
705
|
+
const opportunityData = opportunityAPI.generateRandomData({
|
|
706
|
+
companyId: testCompany.id,
|
|
707
|
+
pipelineId: testPipeline.id,
|
|
708
|
+
stageId: testStageId
|
|
709
|
+
})
|
|
710
|
+
|
|
711
|
+
opportunityAPI.create(opportunityData).then((createResponse: any) => {
|
|
712
|
+
expect(createResponse.status).to.eq(201)
|
|
713
|
+
const opportunityId = createResponse.body.data.id
|
|
714
|
+
|
|
715
|
+
// Delete the opportunity
|
|
716
|
+
opportunityAPI.delete(opportunityId).then((response: any) => {
|
|
717
|
+
opportunityAPI.validateSuccessResponse(response, 200)
|
|
718
|
+
expect(response.body.data).to.have.property('success', true)
|
|
719
|
+
expect(response.body.data).to.have.property('id', opportunityId)
|
|
720
|
+
|
|
721
|
+
cy.log(`Deleted opportunity: ${opportunityId}`)
|
|
722
|
+
})
|
|
723
|
+
})
|
|
724
|
+
})
|
|
725
|
+
|
|
726
|
+
it('OPPO_API_041: Should return 404 for non-existent opportunity', () => {
|
|
727
|
+
const fakeId = 'non-existent-opportunity-id-12345'
|
|
728
|
+
|
|
729
|
+
opportunityAPI.delete(fakeId).then((response: any) => {
|
|
730
|
+
expect(response.status).to.eq(404)
|
|
731
|
+
expect(response.body).to.have.property('success', false)
|
|
732
|
+
|
|
733
|
+
cy.log('Delete non-existent opportunity returns 404')
|
|
734
|
+
})
|
|
735
|
+
})
|
|
736
|
+
|
|
737
|
+
it('OPPO_API_042: Should verify deletion persists', () => {
|
|
738
|
+
// Create an opportunity
|
|
739
|
+
const opportunityData = opportunityAPI.generateRandomData({
|
|
740
|
+
companyId: testCompany.id,
|
|
741
|
+
pipelineId: testPipeline.id,
|
|
742
|
+
stageId: testStageId
|
|
743
|
+
})
|
|
744
|
+
|
|
745
|
+
opportunityAPI.create(opportunityData).then((createResponse: any) => {
|
|
746
|
+
expect(createResponse.status).to.eq(201)
|
|
747
|
+
const opportunityId = createResponse.body.data.id
|
|
748
|
+
|
|
749
|
+
// Delete it
|
|
750
|
+
opportunityAPI.delete(opportunityId).then((deleteResponse: any) => {
|
|
751
|
+
expect(deleteResponse.status).to.eq(200)
|
|
752
|
+
|
|
753
|
+
// Verify it's gone
|
|
754
|
+
opportunityAPI.getById(opportunityId).then((getResponse: any) => {
|
|
755
|
+
expect(getResponse.status).to.eq(404)
|
|
756
|
+
expect(getResponse.body).to.have.property('success', false)
|
|
757
|
+
|
|
758
|
+
cy.log('Deletion verified - opportunity no longer exists')
|
|
759
|
+
})
|
|
760
|
+
})
|
|
761
|
+
})
|
|
762
|
+
})
|
|
763
|
+
})
|
|
764
|
+
|
|
765
|
+
// ============================================
|
|
766
|
+
// Integration - Complete CRUD Lifecycle
|
|
767
|
+
// ============================================
|
|
768
|
+
describe('Integration - Complete CRUD Lifecycle', () => {
|
|
769
|
+
it('OPPO_API_100: Should complete full lifecycle: Create -> Read -> Update -> Stage Move -> Win/Loss -> Delete', () => {
|
|
770
|
+
// 1. CREATE
|
|
771
|
+
const opportunityData = opportunityAPI.generateRandomData({
|
|
772
|
+
name: 'Lifecycle Test Opportunity',
|
|
773
|
+
companyId: testCompany.id,
|
|
774
|
+
pipelineId: testPipeline.id,
|
|
775
|
+
stageId: testStageId,
|
|
776
|
+
amount: 100000,
|
|
777
|
+
currency: 'USD',
|
|
778
|
+
closeDate: '2025-12-31',
|
|
779
|
+
probability: 25,
|
|
780
|
+
type: 'new_business',
|
|
781
|
+
source: 'web',
|
|
782
|
+
status: 'open'
|
|
783
|
+
})
|
|
784
|
+
|
|
785
|
+
opportunityAPI.create(opportunityData).then((createResponse: any) => {
|
|
786
|
+
opportunityAPI.validateSuccessResponse(createResponse, 201)
|
|
787
|
+
const opportunityId = createResponse.body.data.id
|
|
788
|
+
|
|
789
|
+
cy.log(`1. Created opportunity: ${opportunityId}`)
|
|
790
|
+
|
|
791
|
+
// 2. READ
|
|
792
|
+
opportunityAPI.getById(opportunityId).then((readResponse: any) => {
|
|
793
|
+
opportunityAPI.validateSuccessResponse(readResponse, 200)
|
|
794
|
+
expect(readResponse.body.data.name).to.eq(opportunityData.name)
|
|
795
|
+
expect(Number(readResponse.body.data.amount)).to.eq(opportunityData.amount)
|
|
796
|
+
|
|
797
|
+
cy.log(`2. Read opportunity: ${readResponse.body.data.name}`)
|
|
798
|
+
|
|
799
|
+
// 3. UPDATE BASIC FIELDS
|
|
800
|
+
const updateData = {
|
|
801
|
+
name: 'Updated Lifecycle Opportunity',
|
|
802
|
+
amount: 150000,
|
|
803
|
+
probability: 50,
|
|
804
|
+
type: 'upgrade'
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
opportunityAPI.update(opportunityId, updateData).then((updateResponse: any) => {
|
|
808
|
+
opportunityAPI.validateSuccessResponse(updateResponse, 200)
|
|
809
|
+
expect(updateResponse.body.data.name).to.eq(updateData.name)
|
|
810
|
+
expect(Number(updateResponse.body.data.amount)).to.eq(updateData.amount)
|
|
811
|
+
expect(Number(updateResponse.body.data.probability)).to.eq(updateData.probability)
|
|
812
|
+
|
|
813
|
+
cy.log(`3. Updated opportunity: ${updateResponse.body.data.name}`)
|
|
814
|
+
|
|
815
|
+
// 4. MOVE STAGE (to Proposal stage - index 1)
|
|
816
|
+
const proposalStageId = testPipeline.stages[1]?.id || testStageId
|
|
817
|
+
|
|
818
|
+
opportunityAPI.update(opportunityId, { stageId: proposalStageId }).then((stageResponse: any) => {
|
|
819
|
+
opportunityAPI.validateSuccessResponse(stageResponse, 200)
|
|
820
|
+
expect(stageResponse.body.data.stageId).to.eq(proposalStageId)
|
|
821
|
+
|
|
822
|
+
cy.log(`4. Moved to Proposal stage: ${proposalStageId}`)
|
|
823
|
+
|
|
824
|
+
// 5. INCREASE PROBABILITY (Negotiation stage)
|
|
825
|
+
opportunityAPI.update(opportunityId, {
|
|
826
|
+
probability: 75,
|
|
827
|
+
amount: 175000
|
|
828
|
+
}).then((probResponse: any) => {
|
|
829
|
+
opportunityAPI.validateSuccessResponse(probResponse, 200)
|
|
830
|
+
expect(Number(probResponse.body.data.probability)).to.eq(75)
|
|
831
|
+
|
|
832
|
+
cy.log(`5. Increased probability to 75% and amount to 175000`)
|
|
833
|
+
|
|
834
|
+
// 6. CLOSE AS WON
|
|
835
|
+
opportunityAPI.closeAsWon(opportunityId).then((wonResponse: any) => {
|
|
836
|
+
opportunityAPI.validateSuccessResponse(wonResponse, 200)
|
|
837
|
+
expect(wonResponse.body.data.status).to.eq('won')
|
|
838
|
+
expect(Number(wonResponse.body.data.probability)).to.eq(100)
|
|
839
|
+
|
|
840
|
+
cy.log(`6. Closed as WON`)
|
|
841
|
+
|
|
842
|
+
// 7. DELETE
|
|
843
|
+
opportunityAPI.delete(opportunityId).then((deleteResponse: any) => {
|
|
844
|
+
opportunityAPI.validateSuccessResponse(deleteResponse, 200)
|
|
845
|
+
expect(deleteResponse.body.data).to.have.property('success', true)
|
|
846
|
+
|
|
847
|
+
cy.log(`7. Deleted opportunity: ${opportunityId}`)
|
|
848
|
+
|
|
849
|
+
// 8. VERIFY DELETION
|
|
850
|
+
opportunityAPI.getById(opportunityId).then((verifyResponse: any) => {
|
|
851
|
+
expect(verifyResponse.status).to.eq(404)
|
|
852
|
+
|
|
853
|
+
cy.log('8. Verified deletion - opportunity no longer exists')
|
|
854
|
+
cy.log('Full CRUD lifecycle with stage moves and win tracking completed successfully!')
|
|
855
|
+
})
|
|
856
|
+
})
|
|
857
|
+
})
|
|
858
|
+
})
|
|
859
|
+
})
|
|
860
|
+
})
|
|
861
|
+
})
|
|
862
|
+
})
|
|
863
|
+
})
|
|
864
|
+
})
|
|
865
|
+
})
|