ee-ts-util
Version:
typescript utilities and functions
136 lines • 7.87 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const common_1 = require("./common");
const usersInfoAdapter_1 = require("./usersInfoAdapter");
const ramda_1 = require("ramda");
const projectAdapter_1 = require("./projectAdapter");
const Option_1 = require("fp-ts/lib/Option");
const assignmentsAdapter_1 = require("./assignmentsAdapter");
const log4js = require("log4js");
var AssignmentType;
(function (AssignmentType) {
AssignmentType["PROJECT"] = "Project";
AssignmentType["LEAVE_TYPE"] = "LeaveType";
})(AssignmentType || (AssignmentType = {}));
exports.buildFetchTimeEntryAdapter = (baseUrl, token) => exports.buildFetchTimeEntryAdapterWithResultsPerPage(baseUrl, token, 50);
// TODO: this adapter is no more either the TimeEntries adapter or a ts-util adapter
// we need to refactor this code into an adapter at the backend layer.
// TimeEntries Adapter should just fetch time entries and all this orchestration should be done at the backend level.
// Making it work now, but this will need refactoring work in it on the future.
// author: @jpinho
exports.buildFetchTimeEntryAdapterWithResultsPerPage = (baseUrl, token, resultsPerPage) => (from, to) => __awaiter(this, void 0, void 0, function* () {
try {
const timeEntries = yield common_1.fetchPageData(baseUrl, `/api/v1/time_entries?fields=approvals&from=${from}&to=${to}&per_page=${resultsPerPage}`, token, [], exports.extractDto);
const getUsersInfoAdapter = usersInfoAdapter_1.buildGetUsersInfoAdapterWithResultsPerPage(baseUrl, token, resultsPerPage);
const usersInfo = yield getUsersInfoAdapter(ramda_1.uniq(timeEntries.map((te) => te.userId)));
const uniqueAssignableIds = ramda_1.uniq(timeEntries
.filter((te) => te.assignableType !== "LeaveType")
.map((te) => te.assignableId));
const fetchProjectInfo = projectAdapter_1.buildFetchProjectInfoAdapter(baseUrl, token);
const projects = yield Promise.all(uniqueAssignableIds.map((projectId) => __awaiter(this, void 0, void 0, function* () { return yield fetchProjectInfo(projectId); })));
const fetchAssignmentsForAProjectAdapter = assignmentsAdapter_1.buildFetchAssignmentsAdapter(baseUrl, token);
const assignmentsMap = new Map();
yield Promise.all(uniqueAssignableIds.map((assignableId) => __awaiter(this, void 0, void 0, function* () {
const assignmentsDtos = yield fetchAssignmentsForAProjectAdapter(assignableId);
assignmentsMap.set(assignableId, assignmentsDtos);
return;
})));
const genuineTimeentries = timeEntries.filter((t) => t.hours > 0);
return populateTE(genuineTimeentries, usersInfo, assignmentsMap, projects);
}
catch (error) {
return Promise.reject(error);
}
});
const populateTE = (timeEntries, usersInfo, assignmentsMap, projects) => timeEntries.map((te) => {
const userInfo = usersInfo.find((u) => u.userId === te.userId);
if (!userInfo) {
throw new Error(`Could not find userId ${te.userId}`);
}
te.firstName = userInfo.firstName;
te.lastName = userInfo.lastName;
te.email = userInfo.email;
if (te.assignableType !== AssignmentType.LEAVE_TYPE) {
const projectInfo = projects.find((project) => project.id === te.assignableId) || projectAdapter_1.UNDEFINED_PROJECT;
te.projectName = projectInfo.name;
te.assignableName = projectInfo.clientName;
te.billable = projectInfo.billable;
te.parentId = projectInfo.parentId;
te.projectOrPhaseStartDate = projectInfo.startDate;
te.projectOrPhaseEndDate = projectInfo.endDate;
populateResourceStartAndEndDate(assignmentsMap, te);
}
else {
te.assignableName = te.assignableType;
te.billable = false;
}
return te;
});
const populateResourceStartAndEndDate = (assignmentsMap, te) => {
const log = log4js.getLogger();
const assignmentsDtos = assignmentsMap.get(te.assignableId);
if (assignmentsDtos) {
const assignmentsDtoForUser = assignmentsDtos.filter((dto) => dto.user_id === te.userId);
if (assignmentsDtoForUser.length === 0) {
const errorMsg = `No matching assignments for a user ${te.userId} for project ${te.assignableId}.` +
`This might have happened because, consultant is removed from project.` +
`Best terminate a resource assigning a end date on project than removing from it. `;
log.warn(errorMsg);
te.resourceStartDateOnProjectOrPhase = te.projectOrPhaseStartDate;
te.resourceEndDateOnProjectOrPhase = te.projectOrPhaseEndDate;
}
else {
te.resourceStartDateOnProjectOrPhase = assignmentsDtoForUser[0].starts_at;
te.resourceEndDateOnProjectOrPhase = assignmentsDtoForUser[0].ends_at;
}
}
else {
throw new Error(`Assignment not found for ${te.assignableId}`);
}
};
exports.extractDto = (element) => ({
hours: element.hours,
day: new Date(element.date),
userId: element.user_id,
assignableId: element.assignable_id,
assignableType: element.assignable_type,
approved: exports.toApprovedOrNot(element.approvals.data),
status: element.approvals.data,
hourlyBillRate: element.bill_rate,
createdAt: element.created_at,
updatedAt: element.updated_at,
});
// TODO RF : 27/12/2017 : We have a small dillema here (here comes the story):
// 1) toApprovedOrNot is clearly an internal function, thus it shouldn't have a unit test.
// 1.a) another sign that a unit test for toApprovedOrNot is a smell: it deals with external data
// (10KFT, that we don't control).
// 2) the other option is to have integration tests covering all the toApprovedOrNot paths, but that's not good either.
// 2.a) integration tests are not meant to cover different paths, but to be sure a component "talks" correctly with
// the external world.
// 2.b) these tests would be very fragile and complex to maintain, since they would depend on very specific
// combination of data in a staging environment.
// Conclusion 1: This seems to confirm what our team members are already feeling: that we have too much logic
// happening in ts-util adaptors.
// If we leave ts-util to convert a external domain to EE domain, and implement business logic in the
// backend, we solve this problem.
// Conclusion 2: Until we refactor the adaptors in ts-util and move domain related logic to the backend,
// we have to choose the "less" bad option.
// So we are exposing toApprovedOrNot and implementing unit tests for it.
exports.toApprovedOrNot = (maybeApprovals) => fromEmpty(maybeApprovals).fold(() => false, (approvals) => {
const approved = true;
return approvals.reduce((acc, current) => acc && isApproved(current), approved);
});
const isApproved = (a) => a.status === "approved";
// This is DUPLICATED in projectAdapter
const fromEmpty = (maybeArray) => {
return Option_1.fromNullable(maybeArray).fold(() => Option_1.none, (array) => array.length === 0 ? Option_1.none : Option_1.some(array));
};
//# sourceMappingURL=timeEntryAdapter.js.map