Skip to content
Open
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
21 changes: 21 additions & 0 deletions src/models/bmdashboard/mockDataManifest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const mongoose = require('mongoose');

const { Schema } = mongoose;

/**
* Records the IDs inserted by a mock data seeder so its reset removes only those records.
* Used by src/scripts/seedTcsMockData.js.
*/
const mockDataManifest = new Schema({
name: { type: String, required: true, unique: true },
entries: [
{
_id: false,
collectionKey: { type: String, required: true },
ids: [{ type: Schema.Types.ObjectId }],
},
],
seededAt: { type: Date, default: Date.now },
});

module.exports = mongoose.model('mockDataManifest', mockDataManifest, 'mockDataManifests');
108 changes: 108 additions & 0 deletions src/scripts/seedTcsMockData.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* Load or reset mock data for the Total Construction Summary (BM Dashboard → Reports).
*
* Usage (from the HGNRest root):
* node src/scripts/seedTcsMockData.js # reset previous mock data, then load fresh data
* node src/scripts/seedTcsMockData.js --reset # remove the mock data only
*
* Mock data is attached to the existing BM projects (Building 1–3, Residential Test - Project,
* Commercial Test - Project) and covers the last 8 weeks. Every inserted ID is recorded in the
* "mockDataManifests" collection, so reset removes only mock records.
*
* Requires a valid .env file with the same MongoDB credentials used by the app:
* user, password, cluster, dbName, appName
*
* Seed logic lives in src/utilities/tcsMockDataSeeder.js.
*/

/* eslint-disable no-console */
require('dotenv').config();
const mongoose = require('mongoose');
const BuildingProject = require('../models/bmdashboard/buildingProject');
const UserProfile = require('../models/userProfile');
const { invTypeBase } = require('../models/bmdashboard/buildingInventoryType');
const BuildingIssue = require('../models/bmdashboard/buildingIssue');
const InjurySeverity = require('../models/bmdashboard/injujrySeverity');
const InjuryCategory = require('../models/bmdashboard/buildingInjury');
const InjuryOverTime = require('../models/bmdashboard/buildingInjuryOverTime');
const RentalChart = require('../models/bmdashboard/buildingRentalChart');
const ToolReturn = require('../models/bmdashboard/toolReturn');
const ToolsDowntime = require('../models/bmdashboard/toolsDowntime');
const ToolStoppageReason = require('../models/bmdashboard/buildingToolsStoppage');
const ToolAvailability = require('../models/bmdashboard/toolAvailability');
const Expenditure = require('../models/bmdashboard/expenditure');
const ProjectCostTracking = require('../models/bmdashboard/projectCostTracking');
const ProjectRiskProfile = require('../models/bmdashboard/projectRiskProfile');
const RiskIssue = require('../models/bmdashboard/Issues');
const TimeLog = require('../models/bmdashboard/buildingTimeLogger');
const NewLesson = require('../models/bmdashboard/buildingNewLesson');
const BuildingMaterial = require('../models/bmdashboard/buildingMaterial');
const BuildingTool = require('../models/bmdashboard/buildingTool');
const ProjectStatus = require('../models/projectStatus');
const LaborCost = require('../models/laborCost');
const Manifest = require('../models/bmdashboard/mockDataManifest');
const { seedTcsMockData, resetTcsMockData } = require('../utilities/tcsMockDataSeeder');

const models = {
buildingIssue: BuildingIssue,
injurySeverity: InjurySeverity,
injuryCategory: InjuryCategory,
injuryOverTime: InjuryOverTime,
rentalChart: RentalChart,
toolReturn: ToolReturn,
toolsDowntime: ToolsDowntime,
toolStoppageReason: ToolStoppageReason,
toolAvailability: ToolAvailability,
expenditure: Expenditure,
projectCostTracking: ProjectCostTracking,
riskIssue: RiskIssue,
projectRiskProfile: ProjectRiskProfile,
timeLog: TimeLog,
newLesson: NewLesson,
buildingMaterial: BuildingMaterial,
buildingTool: BuildingTool,
projectStatus: ProjectStatus,
laborCost: LaborCost,
};

const { user, password, cluster, dbName, appName = 'HGNRest' } = process.env;

async function run() {
if (!user || !password || !cluster || !dbName) {
throw new Error('Missing required env vars: user, password, cluster, dbName');
}

const uri = `mongodb+srv://${user}:${encodeURIComponent(password)}@${cluster}/${dbName}?retryWrites=true&w=majority&appName=${appName}`;
await mongoose.connect(uri, { useNewUrlParser: true, useUnifiedTopology: true });

if (process.argv.includes('--reset')) {
const { removed } = await resetTcsMockData({ models, Manifest });
console.log(`Removed ${removed} mock record(s)`);
} else {
const summary = await seedTcsMockData({
models,
BuildingProject,
UserProfile,
InventoryType: invTypeBase,
Manifest,
createId: () => new mongoose.Types.ObjectId(),
});
console.log(`Removed ${summary.removed} previous mock record(s)`);
if (summary.missingUser) console.warn('No active Administrator found; nothing inserted.');
Object.entries(summary.inserted).forEach(([key, count]) =>
console.log(` - ${key}: ${count} record(s)`),
);
console.log(`Projects: ${summary.seededProjects.join(', ') || 'none'}`);
if (summary.missingProjects.length > 0) {
console.warn('BM projects not found (skipped):', summary.missingProjects.join(', '));
}
}

await mongoose.disconnect();
}

run().catch(async (err) => {
console.error(err);
await mongoose.disconnect().catch(() => {});

Check warning on line 106 in src/scripts/seedTcsMockData.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Avoid nesting promises.

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HGNRest&issues=AaGtthcYCH4VcpNEs-st&open=AaGtthcYCH4VcpNEs-st&pullRequest=2467
process.exitCode = 1;
});
223 changes: 223 additions & 0 deletions src/utilities/__tests__/tcsMockDataSeeder.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
const {
MOCK_PROJECT_NAMES,
MANIFEST_NAME,
MOCK_COLLECTIONS,
createRandom,
splitInventoryTypes,
buildMockData,
seedTcsMockData,
resetTcsMockData,
} = require('../tcsMockDataSeeder');

const NOW = new Date('2026-11-05T12:00:00.000Z');
const DAY_MS = 24 * 60 * 60 * 1000;
const PROJECTS = [
{ _id: 'p1', name: 'Building 1' },
{ _id: 'p2', name: 'Building 2' },
];
const USER = { _id: 'u1' };

const leanResult = (value) => ({ lean: jest.fn().mockResolvedValue(value) });

const makeModels = () =>
Object.fromEntries(
MOCK_COLLECTIONS.map(([key]) => [
key,
{
insertMany: jest.fn((docs) =>
Promise.resolve(docs.map((doc, i) => ({ ...doc, _id: `${key}-${i}` }))),
),
deleteMany: jest.fn().mockResolvedValue({ deletedCount: 2 }),
},
]),
);

const makeDeps = ({ projects = PROJECTS, user = USER, manifest = null } = {}) => ({
models: makeModels(),
BuildingProject: { find: jest.fn(() => leanResult(projects)) },
UserProfile: { findOne: jest.fn(() => leanResult(user)) },
InventoryType: {
find: jest.fn(() =>
leanResult([
{ _id: 'm1', category: 'Material' },
{ _id: 't1', category: 'Tool' },
]),
),
},
Manifest: {
findOne: jest.fn(() => leanResult(manifest)),
deleteOne: jest.fn().mockResolvedValue({}),
create: jest.fn().mockResolvedValue({}),
},
now: NOW,
});

describe('tcsMockDataSeeder', () => {
describe('createRandom', () => {
it('returns the same sequence for the same seed', () => {
const a = createRandom(7);
const b = createRandom(7);
expect([a.next(), a.next(), a.next()]).toEqual([b.next(), b.next(), b.next()]);
});

it('keeps int() within the given range', () => {
const rand = createRandom(3);
for (let i = 0; i < 50; i += 1) {
const value = rand.int(2, 5);
expect(value).toBeGreaterThanOrEqual(2);
expect(value).toBeLessThanOrEqual(5);
}
});
});

describe('splitInventoryTypes', () => {
it('separates material and tool types by category or discriminator', () => {
const result = splitInventoryTypes([
{ _id: 'm1', category: 'Material' },
{ _id: 't1', __t: 'tool_type' },
{ _id: 'x1', category: 'Consumable' },
]);
expect(result.materialTypes.map((t) => t._id)).toEqual(['m1']);
expect(result.toolTypes.map((t) => t._id)).toEqual(['t1']);
});
});

describe('buildMockData', () => {
const data = buildMockData({
projects: PROJECTS,
userId: 'u1',
now: NOW,
rand: createRandom(1),
materialTypes: [{ _id: 'm1' }],
toolTypes: [{ _id: 't1' }],
});

it('builds records for every collection', () => {
MOCK_COLLECTIONS.forEach(([key]) => {
expect(data[key].length).toBeGreaterThan(0);
});
});

it('attaches issues to the given projects with valid status and severity', () => {
data.buildingIssue.forEach((issue) => {
expect(['p1', 'p2']).toContain(issue.projectId);
expect(['open', 'closed']).toContain(issue.status);
expect(['Low', 'Medium', 'High', 'Critical']).toContain(issue.severity);
expect(issue.createdBy).toBe('u1');
});
});

it('keeps dated records within the last 8 weeks', () => {
const earliest = NOW.getTime() - 8 * 7 * DAY_MS - DAY_MS;
['buildingIssue', 'injurySeverity', 'rentalChart', 'projectCostTracking'].forEach((key) => {
data[key].forEach((doc) => {
const time = (doc.issueDate || doc.date).getTime();
expect(time).toBeGreaterThanOrEqual(earliest);
expect(time).toBeLessThanOrEqual(NOW.getTime());
});
});
});

it('includes material usage in the last week', () => {
const weekAgo = NOW.getTime() - 7 * DAY_MS;
const recent = data.buildingMaterial.flatMap((m) =>
m.updateRecord.filter((r) => r.date.getTime() >= weekAgo),
);
expect(recent.length).toBeGreaterThan(0);
});

it('links each risk profile to an Issues document carrying the project name', () => {
let next = 0;
const linked = buildMockData({
projects: PROJECTS,
userId: 'u1',
now: NOW,
rand: createRandom(1),
materialTypes: [],
toolTypes: [],
createId: () => {
next += 1;
return `issue-${next}`;
},
});
linked.projectRiskProfile.forEach((profile, i) => {
const issueDoc = linked.riskIssue[i];
expect(profile.projectId).toBe(issueDoc._id);
expect(profile.issues).toEqual([issueDoc._id]);
expect(issueDoc.projectName).toBe(PROJECTS[i].name);
});
});

it('creates project status and labor cost rows by project name', () => {
expect(data.projectStatus.map((row) => row.name)).toEqual(['Building 1', 'Building 2']);
data.laborCost.forEach((row) => {
expect(['Building 1', 'Building 2']).toContain(row.project_name);
});
});

it('respects lesson field limits', () => {
data.newLesson.forEach((lesson) => {
expect(lesson.title.length).toBeLessThanOrEqual(20);
lesson.tags.forEach((tag) => expect(tag.length).toBeLessThanOrEqual(10));
});
});
});

describe('resetTcsMockData', () => {
it('does nothing when there is no manifest', async () => {
const deps = makeDeps();
const result = await resetTcsMockData(deps);
expect(result).toEqual({ removed: 0 });
expect(deps.Manifest.deleteOne).not.toHaveBeenCalled();
});

it('deletes only the IDs listed in the manifest', async () => {
const deps = makeDeps({
manifest: { entries: [{ collectionKey: 'buildingIssue', ids: ['a', 'b'] }] },
});
const result = await resetTcsMockData(deps);
expect(deps.models.buildingIssue.deleteMany).toHaveBeenCalledWith({
_id: { $in: ['a', 'b'] },
});
expect(deps.models.injurySeverity.deleteMany).not.toHaveBeenCalled();
expect(deps.Manifest.deleteOne).toHaveBeenCalledWith({ name: MANIFEST_NAME });
expect(result.removed).toBe(2);
});
});

describe('seedTcsMockData', () => {
it('inserts mock data for found projects and records the IDs in the manifest', async () => {
const deps = makeDeps();
const summary = await seedTcsMockData(deps);

expect(deps.BuildingProject.find).toHaveBeenCalledWith(
{ name: { $in: MOCK_PROJECT_NAMES } },
'_id name',
);
MOCK_COLLECTIONS.forEach(([key]) => {
expect(deps.models[key].insertMany).toHaveBeenCalled();
});
const manifest = deps.Manifest.create.mock.calls[0][0];
expect(manifest.name).toBe(MANIFEST_NAME);
expect(manifest.entries).toHaveLength(MOCK_COLLECTIONS.length);
expect(summary.seededProjects).toEqual(['Building 1', 'Building 2']);
expect(summary.missingProjects).toEqual(
MOCK_PROJECT_NAMES.filter((name) => !['Building 1', 'Building 2'].includes(name)),
);
});

it('inserts nothing when no projects are found', async () => {
const deps = makeDeps({ projects: [] });
const summary = await seedTcsMockData(deps);
expect(summary.inserted).toEqual({});
expect(deps.Manifest.create).not.toHaveBeenCalled();
});

it('inserts nothing when no administrator user is found', async () => {
const deps = makeDeps({ user: null });
const summary = await seedTcsMockData(deps);
expect(summary.missingUser).toBe(true);
expect(deps.Manifest.create).not.toHaveBeenCalled();
});
});
});
Loading
Loading