UNPKG

iworks-core-api

Version:

iwroks server api module

345 lines (294 loc) 9.79 kB
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, }; }