Skip to content

Added ProjectEstimation model #306

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 27, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions migrations/20190526_project_estimation.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
--
-- CREATE NEW TABLE:
-- project_estimations
--

CREATE TABLE project_estimations
(
id bigint NOT NULL,
"buildingBlockKey" character varying(255) NOT NULL,
conditions character varying(512) NOT NULL,
price double precision NOT NULL,
"minTime" integer NOT NULL,
"maxTime" integer NOT NULL,
metadata json NOT NULL DEFAULT '{}'::json,
"projectId" bigint NOT NULL,
"deletedAt" timestamp with time zone,
"createdAt" timestamp with time zone,
"updatedAt" timestamp with time zone,
"deletedBy" bigint,
"createdBy" integer NOT NULL,
"updatedBy" integer NOT NULL,
CONSTRAINT project_estimations_pkey PRIMARY KEY (id)
);

CREATE SEQUENCE project_estimations_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;

ALTER SEQUENCE project_estimations_id_seq OWNED BY project_estimations.id;

ALTER TABLE project_estimations
ALTER COLUMN id SET DEFAULT nextval('project_estimations_id_seq');
32 changes: 32 additions & 0 deletions src/models/projectEstimation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
module.exports = function defineProjectHistory(sequelize, DataTypes) {
const ProjectEstimation = sequelize.define(
'ProjectEstimation',
{
id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
buildingBlockKey: { type: DataTypes.STRING, allowNull: false },
conditions: { type: DataTypes.STRING, allowNull: false },
price: { type: DataTypes.DOUBLE, allowNull: false },
minTime: { type: DataTypes.INTEGER, allowNull: false },
maxTime: { type: DataTypes.INTEGER, allowNull: false },
metadata: { type: DataTypes.JSON, allowNull: false, defaultValue: {} },
projectId: { type: DataTypes.BIGINT, allowNull: false },

deletedAt: { type: DataTypes.DATE, allowNull: true },
createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
updatedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
deletedBy: DataTypes.BIGINT,
createdBy: { type: DataTypes.INTEGER, allowNull: false },
updatedBy: { type: DataTypes.INTEGER, allowNull: false },
},
{
tableName: 'project_estimations',
paranoid: true,
timestamps: true,
updatedAt: 'updatedAt',
createdAt: 'createdAt',
indexes: [],
},
);

return ProjectEstimation;
};
24 changes: 23 additions & 1 deletion src/routes/projects/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ const createProjectValdiations = {
})).allow(null),
templateId: Joi.number().integer().positive(),
version: Joi.string(),
estimation: Joi.array().items(Joi.object().keys({
conditions: Joi.string().required(),
price: Joi.number().required(),
minTime: Joi.number().integer().required(),
maxTime: Joi.number().integer().required(),
buildingBlockKey: Joi.string().required(),
metadata: Joi.object().optional(),
})).optional(),
}).required(),
},
};
Expand All @@ -81,6 +89,17 @@ function createProjectAndPhases(req, project, projectTemplate, productTemplates)
model: models.ProjectMember,
as: 'members',
}],
}).then((newProject) => {
if (project.estimation && (project.estimation.length > 0)) {
req.log.debug('creating project estimation');
const estimations = project.estimation.map(estimation => Object.assign({
projectId: newProject.id,
createdBy: req.authUser.userId,
updatedBy: req.authUser.userId,
}, estimation));
return models.ProjectEstimation.bulkCreate(estimations).then(() => Promise.resolve(newProject));
}
return Promise.resolve(newProject);
}).then((newProject) => {
result.newProject = newProject;

Expand Down Expand Up @@ -212,7 +231,10 @@ module.exports = [
utm: null,
});
traverse(project).forEach(function (x) { // eslint-disable-line func-names
if (this.isLeaf && typeof x === 'string') this.update(req.sanitize(x));
// keep the raw '&&' string in conditions string in estimation
const isEstimationCondition =
(this.path.length === 3) && (this.path[0] === 'estimation') && (this.key === 'conditions');
if (this.isLeaf && typeof x === 'string' && (!isEstimationCondition)) this.update(req.sanitize(x));
});
// override values
_.assign(project, {
Expand Down
154 changes: 154 additions & 0 deletions src/routes/projects/create.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,23 @@ describe('Project create', () => {
.expect(422, done);
});

it('should return 422 with wrong format estimation field', (done) => {
const invalidBody = _.cloneDeep(body);
invalidBody.param.estimation = [
{

},
];
request(server)
.post('/v4/projects')
.set({
Authorization: `Bearer ${testUtil.jwts.member}`,
})
.send(invalidBody)
.expect('Content-Type', /json/)
.expect(422, done);
});

it('should return 201 if error to create direct project', (done) => {
const validBody = _.cloneDeep(body);
validBody.param.templateId = 3;
Expand Down Expand Up @@ -469,6 +486,143 @@ describe('Project create', () => {
});
});

it('should return 201 if valid user and data (with estimation)', (done) => {
const validBody = _.cloneDeep(body);
validBody.param.estimation = [
{
conditions: '( HAS_DESIGN_DELIVERABLE && HAS_ZEPLIN_APP_ADDON && CA_NEEDED)',
price: 6,
minTime: 2,
maxTime: 2,
metadata: {
deliverable: 'design',
},
buildingBlockKey: 'ZEPLIN_APP_ADDON_CA',
},
{
conditions: '( HAS_DESIGN_DELIVERABLE && COMPREHENSIVE_DESIGN && TWO_TARGET_DEVICES'
+ ' && SCREENS_COUNT_SMALL && CA_NEEDED )',
price: 95,
minTime: 14,
maxTime: 14,
metadata: {
deliverable: 'design',
},
buildingBlockKey: 'SMALL_COMP_DESIGN_TWO_DEVICE_CA',
},
{
conditions: '( HAS_DEV_DELIVERABLE && (ONLY_ONE_OS_MOBILE || ONLY_ONE_OS_DESKTOP'
+ ' || ONLY_ONE_OS_PROGRESSIVE) && SCREENS_COUNT_SMALL && CA_NEEDED)',
price: 50,
minTime: 35,
maxTime: 35,
metadata: {
deliverable: 'dev-qa',
},
buildingBlockKey: 'SMALL_DEV_ONE_OS_CA',
},
{
conditions: '( HAS_DEV_DELIVERABLE && HAS_SSO_INTEGRATION_ADDON && CA_NEEDED)',
price: 80,
minTime: 5,
maxTime: 5,
metadata: {
deliverable: 'dev-qa',
},
buildingBlockKey: 'HAS_SSO_INTEGRATION_ADDON_CA',
},
{
conditions: '( HAS_DEV_DELIVERABLE && HAS_CHECKMARX_SCANNING_ADDON && CA_NEEDED)',
price: 4,
minTime: 10,
maxTime: 10,
metadata: {
deliverable: 'dev-qa',
},
buildingBlockKey: 'HAS_CHECKMARX_SCANNING_ADDON_CA',
},
{
conditions: '( HAS_DEV_DELIVERABLE && HAS_UNIT_TESTING_ADDON && CA_NEEDED)',
price: 90,
minTime: 12,
maxTime: 12,
metadata: {
deliverable: 'dev-qa',
},
buildingBlockKey: 'HAS_UNIT_TESTING_ADDON_CA',
},
];
validBody.param.templateId = 3;
const mockHttpClient = _.merge(testUtil.mockHttpClient, {
post: () => Promise.resolve({
status: 200,
data: {
id: 'requesterId',
version: 'v3',
result: {
success: true,
status: 200,
content: {
projectId: 128,
},
},
},
}),
});
sandbox.stub(util, 'getHttpClient', () => mockHttpClient);
request(server)
.post('/v4/projects')
.set({
Authorization: `Bearer ${testUtil.jwts.member}`,
})
.send(validBody)
.expect('Content-Type', /json/)
.expect(201)
.end((err, res) => {
if (err) {
done(err);
} else {
const resJson = res.body.result.content;
should.exist(resJson);
should.exist(resJson.billingAccountId);
should.exist(resJson.name);
resJson.directProjectId.should.be.eql(128);
resJson.status.should.be.eql('draft');
resJson.type.should.be.eql(body.param.type);
resJson.version.should.be.eql('v3');
resJson.members.should.have.lengthOf(1);
resJson.members[0].role.should.be.eql('customer');
resJson.members[0].userId.should.be.eql(40051331);
resJson.members[0].projectId.should.be.eql(resJson.id);
resJson.members[0].isPrimary.should.be.truthy;
resJson.bookmarks.should.have.lengthOf(1);
resJson.bookmarks[0].title.should.be.eql('title1');
resJson.bookmarks[0].address.should.be.eql('http://www.address.com');
// Check that activity fields are set
resJson.lastActivityUserId.should.be.eql('40051331');
resJson.lastActivityAt.should.be.not.null;
server.services.pubsub.publish.calledWith('project.draft-created').should.be.true;

// Check new ProjectEstimation records are created.
models.ProjectEstimation.findAll({
where: {
projectId: resJson.id,
},
}).then((projectEstimations) => {
projectEstimations.length.should.be.eql(6);
projectEstimations[0].conditions.should.be.eql(
'( HAS_DESIGN_DELIVERABLE && HAS_ZEPLIN_APP_ADDON && CA_NEEDED)');
projectEstimations[0].price.should.be.eql(6);
projectEstimations[0].minTime.should.be.eql(2);
projectEstimations[0].maxTime.should.be.eql(2);
projectEstimations[0].metadata.deliverable.should.be.eql('design');
projectEstimations[0].buildingBlockKey.should.be.eql('ZEPLIN_APP_ADDON_CA');
done();
});
}
});
});

xit('should return 201 if valid user and data (using Bearer userId_<userId>)', (done) => {
const mockHttpClient = _.merge(testUtil.mockHttpClient, {
post: () => Promise.resolve({
Expand Down
26 changes: 26 additions & 0 deletions swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3161,6 +3161,32 @@ definitions:
data:
type: string
description: 300 Char length text blob for customer provided data
estimation:
type: array
items:
type: object
required:
- conditions
- price
- maxTime
- minTime
- buildingBlockKey
properties:
conditions:
type: string
price:
type: number
format: float
maxTime:
type: number
format: integer
minTime:
type: integer
format: integer
metadata:
type: object
buildingBlockKey:
type: string
type:
type: string
description: project type
Expand Down