Skip to content

Commit a8f25a2

Browse files
author
vikasrohit
authored
Merge pull request #306 from topcoder-platform/feature/project-estimation-model
Added ProjectEstimation model
2 parents c321f50 + 495f65e commit a8f25a2

File tree

5 files changed

+270
-1
lines changed

5 files changed

+270
-1
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
--
2+
-- CREATE NEW TABLE:
3+
-- project_estimations
4+
--
5+
6+
CREATE TABLE project_estimations
7+
(
8+
id bigint NOT NULL,
9+
"buildingBlockKey" character varying(255) NOT NULL,
10+
conditions character varying(512) NOT NULL,
11+
price double precision NOT NULL,
12+
"minTime" integer NOT NULL,
13+
"maxTime" integer NOT NULL,
14+
metadata json NOT NULL DEFAULT '{}'::json,
15+
"projectId" bigint NOT NULL,
16+
"deletedAt" timestamp with time zone,
17+
"createdAt" timestamp with time zone,
18+
"updatedAt" timestamp with time zone,
19+
"deletedBy" bigint,
20+
"createdBy" integer NOT NULL,
21+
"updatedBy" integer NOT NULL,
22+
CONSTRAINT project_estimations_pkey PRIMARY KEY (id)
23+
);
24+
25+
CREATE SEQUENCE project_estimations_id_seq
26+
START WITH 1
27+
INCREMENT BY 1
28+
NO MINVALUE
29+
NO MAXVALUE
30+
CACHE 1;
31+
32+
ALTER SEQUENCE project_estimations_id_seq OWNED BY project_estimations.id;
33+
34+
ALTER TABLE project_estimations
35+
ALTER COLUMN id SET DEFAULT nextval('project_estimations_id_seq');

src/models/projectEstimation.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
module.exports = function defineProjectHistory(sequelize, DataTypes) {
2+
const ProjectEstimation = sequelize.define(
3+
'ProjectEstimation',
4+
{
5+
id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
6+
buildingBlockKey: { type: DataTypes.STRING, allowNull: false },
7+
conditions: { type: DataTypes.STRING, allowNull: false },
8+
price: { type: DataTypes.DOUBLE, allowNull: false },
9+
minTime: { type: DataTypes.INTEGER, allowNull: false },
10+
maxTime: { type: DataTypes.INTEGER, allowNull: false },
11+
metadata: { type: DataTypes.JSON, allowNull: false, defaultValue: {} },
12+
projectId: { type: DataTypes.BIGINT, allowNull: false },
13+
14+
deletedAt: { type: DataTypes.DATE, allowNull: true },
15+
createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
16+
updatedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
17+
deletedBy: DataTypes.BIGINT,
18+
createdBy: { type: DataTypes.INTEGER, allowNull: false },
19+
updatedBy: { type: DataTypes.INTEGER, allowNull: false },
20+
},
21+
{
22+
tableName: 'project_estimations',
23+
paranoid: true,
24+
timestamps: true,
25+
updatedAt: 'updatedAt',
26+
createdAt: 'createdAt',
27+
indexes: [],
28+
},
29+
);
30+
31+
return ProjectEstimation;
32+
};

src/routes/projects/create.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ const createProjectValdiations = {
5757
})).allow(null),
5858
templateId: Joi.number().integer().positive(),
5959
version: Joi.string(),
60+
estimation: Joi.array().items(Joi.object().keys({
61+
conditions: Joi.string().required(),
62+
price: Joi.number().required(),
63+
minTime: Joi.number().integer().required(),
64+
maxTime: Joi.number().integer().required(),
65+
buildingBlockKey: Joi.string().required(),
66+
metadata: Joi.object().optional(),
67+
})).optional(),
6068
}).required(),
6169
},
6270
};
@@ -81,6 +89,17 @@ function createProjectAndPhases(req, project, projectTemplate, productTemplates)
8189
model: models.ProjectMember,
8290
as: 'members',
8391
}],
92+
}).then((newProject) => {
93+
if (project.estimation && (project.estimation.length > 0)) {
94+
req.log.debug('creating project estimation');
95+
const estimations = project.estimation.map(estimation => Object.assign({
96+
projectId: newProject.id,
97+
createdBy: req.authUser.userId,
98+
updatedBy: req.authUser.userId,
99+
}, estimation));
100+
return models.ProjectEstimation.bulkCreate(estimations).then(() => Promise.resolve(newProject));
101+
}
102+
return Promise.resolve(newProject);
84103
}).then((newProject) => {
85104
result.newProject = newProject;
86105

@@ -212,7 +231,10 @@ module.exports = [
212231
utm: null,
213232
});
214233
traverse(project).forEach(function (x) { // eslint-disable-line func-names
215-
if (this.isLeaf && typeof x === 'string') this.update(req.sanitize(x));
234+
// keep the raw '&&' string in conditions string in estimation
235+
const isEstimationCondition =
236+
(this.path.length === 3) && (this.path[0] === 'estimation') && (this.key === 'conditions');
237+
if (this.isLeaf && typeof x === 'string' && (!isEstimationCondition)) this.update(req.sanitize(x));
216238
});
217239
// override values
218240
_.assign(project, {

src/routes/projects/create.spec.js

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,23 @@ describe('Project create', () => {
262262
.expect(422, done);
263263
});
264264

265+
it('should return 422 with wrong format estimation field', (done) => {
266+
const invalidBody = _.cloneDeep(body);
267+
invalidBody.param.estimation = [
268+
{
269+
270+
},
271+
];
272+
request(server)
273+
.post('/v4/projects')
274+
.set({
275+
Authorization: `Bearer ${testUtil.jwts.member}`,
276+
})
277+
.send(invalidBody)
278+
.expect('Content-Type', /json/)
279+
.expect(422, done);
280+
});
281+
265282
it('should return 201 if error to create direct project', (done) => {
266283
const validBody = _.cloneDeep(body);
267284
validBody.param.templateId = 3;
@@ -469,6 +486,143 @@ describe('Project create', () => {
469486
});
470487
});
471488

489+
it('should return 201 if valid user and data (with estimation)', (done) => {
490+
const validBody = _.cloneDeep(body);
491+
validBody.param.estimation = [
492+
{
493+
conditions: '( HAS_DESIGN_DELIVERABLE && HAS_ZEPLIN_APP_ADDON && CA_NEEDED)',
494+
price: 6,
495+
minTime: 2,
496+
maxTime: 2,
497+
metadata: {
498+
deliverable: 'design',
499+
},
500+
buildingBlockKey: 'ZEPLIN_APP_ADDON_CA',
501+
},
502+
{
503+
conditions: '( HAS_DESIGN_DELIVERABLE && COMPREHENSIVE_DESIGN && TWO_TARGET_DEVICES'
504+
+ ' && SCREENS_COUNT_SMALL && CA_NEEDED )',
505+
price: 95,
506+
minTime: 14,
507+
maxTime: 14,
508+
metadata: {
509+
deliverable: 'design',
510+
},
511+
buildingBlockKey: 'SMALL_COMP_DESIGN_TWO_DEVICE_CA',
512+
},
513+
{
514+
conditions: '( HAS_DEV_DELIVERABLE && (ONLY_ONE_OS_MOBILE || ONLY_ONE_OS_DESKTOP'
515+
+ ' || ONLY_ONE_OS_PROGRESSIVE) && SCREENS_COUNT_SMALL && CA_NEEDED)',
516+
price: 50,
517+
minTime: 35,
518+
maxTime: 35,
519+
metadata: {
520+
deliverable: 'dev-qa',
521+
},
522+
buildingBlockKey: 'SMALL_DEV_ONE_OS_CA',
523+
},
524+
{
525+
conditions: '( HAS_DEV_DELIVERABLE && HAS_SSO_INTEGRATION_ADDON && CA_NEEDED)',
526+
price: 80,
527+
minTime: 5,
528+
maxTime: 5,
529+
metadata: {
530+
deliverable: 'dev-qa',
531+
},
532+
buildingBlockKey: 'HAS_SSO_INTEGRATION_ADDON_CA',
533+
},
534+
{
535+
conditions: '( HAS_DEV_DELIVERABLE && HAS_CHECKMARX_SCANNING_ADDON && CA_NEEDED)',
536+
price: 4,
537+
minTime: 10,
538+
maxTime: 10,
539+
metadata: {
540+
deliverable: 'dev-qa',
541+
},
542+
buildingBlockKey: 'HAS_CHECKMARX_SCANNING_ADDON_CA',
543+
},
544+
{
545+
conditions: '( HAS_DEV_DELIVERABLE && HAS_UNIT_TESTING_ADDON && CA_NEEDED)',
546+
price: 90,
547+
minTime: 12,
548+
maxTime: 12,
549+
metadata: {
550+
deliverable: 'dev-qa',
551+
},
552+
buildingBlockKey: 'HAS_UNIT_TESTING_ADDON_CA',
553+
},
554+
];
555+
validBody.param.templateId = 3;
556+
const mockHttpClient = _.merge(testUtil.mockHttpClient, {
557+
post: () => Promise.resolve({
558+
status: 200,
559+
data: {
560+
id: 'requesterId',
561+
version: 'v3',
562+
result: {
563+
success: true,
564+
status: 200,
565+
content: {
566+
projectId: 128,
567+
},
568+
},
569+
},
570+
}),
571+
});
572+
sandbox.stub(util, 'getHttpClient', () => mockHttpClient);
573+
request(server)
574+
.post('/v4/projects')
575+
.set({
576+
Authorization: `Bearer ${testUtil.jwts.member}`,
577+
})
578+
.send(validBody)
579+
.expect('Content-Type', /json/)
580+
.expect(201)
581+
.end((err, res) => {
582+
if (err) {
583+
done(err);
584+
} else {
585+
const resJson = res.body.result.content;
586+
should.exist(resJson);
587+
should.exist(resJson.billingAccountId);
588+
should.exist(resJson.name);
589+
resJson.directProjectId.should.be.eql(128);
590+
resJson.status.should.be.eql('draft');
591+
resJson.type.should.be.eql(body.param.type);
592+
resJson.version.should.be.eql('v3');
593+
resJson.members.should.have.lengthOf(1);
594+
resJson.members[0].role.should.be.eql('customer');
595+
resJson.members[0].userId.should.be.eql(40051331);
596+
resJson.members[0].projectId.should.be.eql(resJson.id);
597+
resJson.members[0].isPrimary.should.be.truthy;
598+
resJson.bookmarks.should.have.lengthOf(1);
599+
resJson.bookmarks[0].title.should.be.eql('title1');
600+
resJson.bookmarks[0].address.should.be.eql('http://www.address.com');
601+
// Check that activity fields are set
602+
resJson.lastActivityUserId.should.be.eql('40051331');
603+
resJson.lastActivityAt.should.be.not.null;
604+
server.services.pubsub.publish.calledWith('project.draft-created').should.be.true;
605+
606+
// Check new ProjectEstimation records are created.
607+
models.ProjectEstimation.findAll({
608+
where: {
609+
projectId: resJson.id,
610+
},
611+
}).then((projectEstimations) => {
612+
projectEstimations.length.should.be.eql(6);
613+
projectEstimations[0].conditions.should.be.eql(
614+
'( HAS_DESIGN_DELIVERABLE && HAS_ZEPLIN_APP_ADDON && CA_NEEDED)');
615+
projectEstimations[0].price.should.be.eql(6);
616+
projectEstimations[0].minTime.should.be.eql(2);
617+
projectEstimations[0].maxTime.should.be.eql(2);
618+
projectEstimations[0].metadata.deliverable.should.be.eql('design');
619+
projectEstimations[0].buildingBlockKey.should.be.eql('ZEPLIN_APP_ADDON_CA');
620+
done();
621+
});
622+
}
623+
});
624+
});
625+
472626
xit('should return 201 if valid user and data (using Bearer userId_<userId>)', (done) => {
473627
const mockHttpClient = _.merge(testUtil.mockHttpClient, {
474628
post: () => Promise.resolve({

swagger.yaml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3161,6 +3161,32 @@ definitions:
31613161
data:
31623162
type: string
31633163
description: 300 Char length text blob for customer provided data
3164+
estimation:
3165+
type: array
3166+
items:
3167+
type: object
3168+
required:
3169+
- conditions
3170+
- price
3171+
- maxTime
3172+
- minTime
3173+
- buildingBlockKey
3174+
properties:
3175+
conditions:
3176+
type: string
3177+
price:
3178+
type: number
3179+
format: float
3180+
maxTime:
3181+
type: number
3182+
format: integer
3183+
minTime:
3184+
type: integer
3185+
format: integer
3186+
metadata:
3187+
type: object
3188+
buildingBlockKey:
3189+
type: string
31643190
type:
31653191
type: string
31663192
description: project type

0 commit comments

Comments
 (0)