iworks-core-api
Version:
iwroks server api module
345 lines (294 loc) • 9.79 kB
text/typescript
import { transaction } from 'objection';
import * as uuid from 'node-uuid';
import { RenovationProject, RenovationProjectTpl } from 'iworks-db-model';
import {
ICreateProject,
IDeleteProject,
IProject,
IJob,
ProjectStatus,
ProjectPhaseStatus,
IProjectPhase,
IUpdateProject,
} from '../model/renovationProject';
import {
ProjectStageStatus, IProjectStage, IJobInstance, IJobInstanceMaterial,
} from '../model/projectStage';
import {
ProjectTplStatus,
} from '../model/renovationProjectTpl';
import {
JobPriceType,
} from '../model/jobTpl';
import {
evenRound,
jobPriceCalcStrategies,
} from '../helpers/price';
// Create a new project based on Create parameters and Project template
const getProjectData = (
{
stateProperty,
customer,
jobs,
managerId,
ownerId,
customerId,
props,
}: ICreateProject,
renovationProjectTpl: any)
: Partial<IProject> => {
if (!jobs || jobs.length === 0) {
throw new Error('You should provide at least one job for the project');
}
const date = (new Date()).toISOString();
const created = date;
const updated = date;
const statePropertyWithDates = Object.assign(
{
created,
updated,
id: uuid.v4(),
},
stateProperty,
);
const newOrExistCutomer = customerId && !customer ? { customerId } : {
customer: Object.assign(
{
created,
updated,
id: uuid.v4(),
},
customer,
),
};
const jobMap: Map<string, IJob> = jobs.reduce(
(curMap, job) => {
curMap.set(job.id, job);
return curMap;
},
new Map<string, IJob>(),
);
const projectId = uuid.v4();
return {
...newOrExistCutomer,
created,
updated,
managerId,
ownerId,
props,
id: projectId,
stateProperty: statePropertyWithDates,
status: ProjectStatus.created,
renovationProjectTplId: renovationProjectTpl.id,
projectPhases: renovationProjectTpl.phases
.map((phase: any) => {
const projectPhase = {
created,
updated,
phaseId: phase.id,
status: ProjectPhaseStatus.created,
projectStages: phase.stages
.map((stage: any) => {
const projStage = {
created,
updated,
stageId: stage.id,
ownerId: managerId,
status: ProjectStageStatus.created,
jobInstances: stage.jobTplStages
.filter((jobTplStage: any) => jobTplStage.jobTpl && jobMap.has(jobTplStage.jobTpl.id))
.map((jobTplStage: any) => {
const jobTpl = jobTplStage.jobTpl;
const job = jobMap.get(jobTpl.id);
if (!job) {
return {
jobTplId: jobTpl.id,
vol: 0,
};
}
const price = evenRound(jobPriceCalcStrategies.has(jobTpl.jobPriceType)
? jobPriceCalcStrategies.get(jobTpl.jobPriceType)(job.vol, jobTpl)
: 0.0, 2);
return {
price,
jobTplId: jobTpl.id,
vol: job.vol,
providerLinked: job.providerLinked,
jobInstanceMaterials: jobTpl.jobTplMaterials.map((jobTplMaterial: any) => ({
consumption: jobTplMaterial.consumption,
price: jobTplMaterial.material.price,
materialId: jobTplMaterial.material.id,
})),
};
}),
};
if (projStage.jobInstances.length === 0) {
return null;
}
return projStage;
})
.filter((projStage: any) => !!projStage),
};
if (projectPhase.projectStages.length === 0) {
return null;
}
return projectPhase;
})
.filter((projPhase: any) => !!projPhase),
};
};
export function createRenovationProject(createProjectDto: ICreateProject)
: Promise<Partial<IProject>> {
const knex = RenovationProjectTpl.knex();
return transaction(knex, async (trx) => {
const renovationProjectTpls = await RenovationProjectTpl
.query(trx)
.where('id', createProjectDto.renovationProjectTplId)
.eager('phases.stages.jobTplStages.jobTpl.jobTplMaterials(onlyDefault).material', {
onlyDefault: (builder: any) => {
builder.where('default', true);
},
});
if (renovationProjectTpls.length === 0) {
throw new Error(`Can't find project template with
id ${createProjectDto.renovationProjectTplId}`);
}
const renovationProjectTpl = renovationProjectTpls[0];
if (renovationProjectTpl.status === ProjectTplStatus.archived) {
throw new Error('Can`t create project for an archived template');
}
const projectData = getProjectData(createProjectDto, renovationProjectTpl);
const projWithPrice: Partial<IProject> = await recalculateProjectPrice(projectData, trx);
return RenovationProject
.query(trx)
.insertGraph({
...projectData,
...projWithPrice,
});
});
}
export function updateRenovationProject(updateProjectDto: IUpdateProject)
: Promise<Partial<IProject>> {
const knex = RenovationProject.knex();
const date = (new Date()).toISOString();
return transaction(knex, async (trx) => {
const project = await RenovationProject
.query(trx)
.eager('projectPhases.projectStages.jobInstances.jobInstanceMaterials')
.findById(updateProjectDto.id);
if (!project) {
throw new Error(`Project with id ${updateProjectDto.id} does not exist`);
}
const projWithPrice: Partial<IProject> = await recalculateProjectPrice({
...project,
...updateProjectDto,
}, trx);
return RenovationProject
.query(trx)
.upsertGraph({
...updateProjectDto,
...projWithPrice,
updated: date,
});
});
}
export function deleteRenovationProject(delProjectDto : IDeleteProject) {
const knex = RenovationProject.knex();
return transaction(knex, async (trx) => {
const currProject = await RenovationProject
.query(trx)
.findById(delProjectDto.id);
if (!currProject) {
return false;
}
if (currProject.status !== ProjectStatus.created) {
throw new Error(`Can't delete project with id ${delProjectDto.id},
it is in status ${currProject.status}`);
}
const numDeleted = await RenovationProject
.query(trx)
.where('id', delProjectDto.id)
.delete();
return numDeleted > 0;
});
}
const jobInstancePriceReducer = (
curStagePrice: number,
jobInstance: Partial<IJobInstance>,
jobsMulty: number,
materialsDiscount: number,
): number => {
// Calculate materials price
const materialPrice: number = jobInstance.jobInstanceMaterials
.reduce(
(curMaterialPrice: number, jobInstanceMaterial: Partial<IJobInstanceMaterial>) => {
const nextMaterialPrice: number =
evenRound(
curMaterialPrice + jobInstanceMaterial.price * jobInstanceMaterial.consumption * jobInstance.vol,
2,
);
return nextMaterialPrice;
},
0.0,
);
// Uppend job price
const nextStagePrice: number = evenRound(
curStagePrice + jobInstance.price * jobsMulty + materialPrice * (1 - materialsDiscount / 100),
2,
);
return nextStagePrice;
};
export async function recalculateProjectPrice(project: string | Partial<IProject>, trx: any): Promise<Partial<IProject>> {
const projectId = typeof project === 'string'
? project
: project.id;
let currProject: Partial<IProject>;
if (typeof project === 'string') {
currProject = await RenovationProject
.query(trx)
.eager('projectPhases.projectStages.jobInstances.jobInstanceMaterials')
.findById(projectId);
} else {
currProject = project;
}
const date = (new Date()).toISOString();
const materialsDiscount: number = currProject.props.fees.materialsDiscount || 0;
const jobsMulty: number = currProject.props.fees.jobPriceMultiplicator || 1;
const price: number = currProject.projectPhases
.reduce(
(curProjPrice: number, projPhase: Partial<IProjectPhase>) => {
// Add current phase price
const nextProjPrice: number = evenRound(
curProjPrice + projPhase.projectStages
.reduce(
(curPhasePrice: number, projStage: Partial<IProjectStage>) => {
// Add current stage price
const nextPhasePrice: number = evenRound(
curPhasePrice + projStage.jobInstances
.reduce(
(curStagePrice: number, jobInstance: Partial<IJobInstance>) => {
return jobInstancePriceReducer(curStagePrice, jobInstance, jobsMulty, materialsDiscount);
},
0.0,
),
2,
);
return nextPhasePrice;
},
0.0,
),
2,
);
return nextProjPrice;
},
0.0,
);
const agentFee: number = currProject.props.fees.agentFee || 0;
const tnFee: number = currProject.props.fees.tnFee || 0;
const pmFee: number = currProject.props.fees.pmFee || 0;
const totalPrice: number = evenRound((1 + (tnFee + pmFee + agentFee) / 100) * price, 2);
return {
price,
totalPrice,
};
}