Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ describe('SurveyObjectParsers Integration', () => {
await populatePersonsForHousehold(
surveyObjectsWithErrors,
surveyObjectsWithErrors.household!,
undefined,
correctedResponse,
surveyObjectsRegistry
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,35 @@
* License text available at https://opensource.org/licenses/MIT
*/

import _omit from 'lodash/omit';

import { SurveyObjectsWithErrors } from 'evolution-common/lib/services/baseObjects/types';
import { CorrectedResponse } from 'evolution-common/lib/services/questionnaire/types';
import { Person } from 'evolution-common/lib/services/baseObjects/Person';
import { isOk } from 'evolution-common/lib/types/Result.type';
import { Household } from 'evolution-common/lib/services/baseObjects/Household';
import { Home } from 'evolution-common/lib/services/baseObjects/Home';
import { Optional } from 'evolution-common/lib/types/Optional.type';
import projectConfig from '../../config/projectConfig';
import { SurveyObjectsRegistry } from 'evolution-common/lib/services/baseObjects/SurveyObjectsRegistry';
import { compareSequenceThenUuid } from 'evolution-common/lib/services/baseObjects/sequenceUtils';
import { populateJourneysForPerson } from './JourneyFactory';
import { ExtendedPersonAttributes } from 'evolution-common/lib/services/baseObjects/Person';

/**
* Generate persons
* Populate members for a household from the household's persons attributes
* @param {SurveyObjectsWithErrors} surveyObjectsWithErrors - Container for created objects with errors
* @param {Household} household - The household to add the members to
* @param {Home} home - The home object for geography assignment, needed by nested journeys
* @param {CorrectedResponse} correctedResponse - corrected response
* @param {SurveyObjectsRegistry} surveyObjectsRegistry - SurveyObjectsRegistry
* @returns {Promise<void>}
*/
export async function populatePersonsForHousehold(
surveyObjectsWithErrors: SurveyObjectsWithErrors,
household: Household,
home: Optional<Home>,
correctedResponse: CorrectedResponse,
surveyObjectsRegistry: SurveyObjectsRegistry
): Promise<void> {
Expand Down Expand Up @@ -56,13 +64,30 @@ export async function populatePersonsForHousehold(
? projectConfig.surveyObjectParsers.person(originalCorrectedPersonAttributes, correctedResponse)
: originalCorrectedPersonAttributes;

const personResult = Person.create(personAttributes as { [key: string]: unknown }, surveyObjectsRegistry);
// Omit journeys as they will be populated separately in the next step (populateJourneysForPerson)
const personResult = Person.create(
Comment thread
tahini marked this conversation as resolved.
_omit(personAttributes as { [key: string]: unknown }, ['journeys']) as ExtendedPersonAttributes,
surveyObjectsRegistry
);

if (isOk(personResult)) {
// Assign color to person
personResult.result.assignColor(personIndex);
household.members.push(personResult.result);
personIndex++;

// Create journeys for this person (includes visited places, trips, and segments)
await populateJourneysForPerson(
surveyObjectsWithErrors,
personResult.result,
personAttributes as ExtendedPersonAttributes,
home,
correctedResponse,
surveyObjectsRegistry
);

// Setup work and school places after all visited places are created
personResult.result.setupWorkAndSchoolPlaces();
} else {
console.log(
` ==== Person ${personUuid} creation failed with errors count: ${personResult.errors?.length || 0} ====`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ import { isOk } from 'evolution-common/lib/types/Result.type';
import { SurveyObjectsWithErrors } from 'evolution-common/lib/services/baseObjects/types';
import { CorrectedResponse } from 'evolution-common/lib/services/questionnaire/types';
import { populatePersonsForHousehold } from './PersonFactory';
import { populateJourneysForPerson } from './JourneyFactory';
import { ExtendedPersonAttributes } from 'evolution-common/lib/services/baseObjects/Person';
import projectConfig from '../../config/projectConfig';
import { Home } from 'evolution-common/lib/services/baseObjects/Home';
import { ExtendedHouseholdAttributes, Household } from 'evolution-common/lib/services/baseObjects/Household';
Expand Down Expand Up @@ -177,37 +175,15 @@ export class SurveyObjectsFactory {

// Continue with persons, journeys, etc. if household and home were created
if (household && householdAttributes) {
// For now, we'll keep the existing factory functions for persons/journeys
// These can be refactored later in the same way
// populatePersonsForHousehold also creates journeys (and their visited places,
// trips, and segments) for each successfully created person
await populatePersonsForHousehold(
surveyObjectsWithErrors,
household,
home,
correctedResponse,
surveyObjectsRegistry
);

const personsAttributes = householdAttributes.persons || {};
const persons = household.members || [];

// Loop through each person
for (let i = 0, count = persons.length; i < count; i++) {
const person = persons[i];
const personUuid = person._uuid!;
const personAttributes = personsAttributes[personUuid] as ExtendedPersonAttributes;

// Generate all journeys for this person (includes visited places, trips, and segments)
await populateJourneysForPerson(
surveyObjectsWithErrors,
person,
personAttributes,
home,
correctedResponse,
surveyObjectsRegistry
);

// Setup work and school places after all visited places are created
person.setupWorkAndSchoolPlaces();
}
}

return surveyObjectsWithErrors;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import { v4 as uuidV4 } from 'uuid';
import { populatePersonsForHousehold } from '../PersonFactory';
import { populateJourneysForPerson } from '../JourneyFactory';
import { SurveyObjectsWithErrors } from 'evolution-common/lib/services/baseObjects/types';
import { Household } from 'evolution-common/lib/services/baseObjects/Household';
import { Person } from 'evolution-common/lib/services/baseObjects/Person';
Expand All @@ -20,7 +21,11 @@ jest.mock('evolution-common/lib/services/baseObjects/Person', () => ({
create: jest.fn()
}
}));
jest.mock('../JourneyFactory');
const MockedPerson = Person as jest.MockedClass<typeof Person>;
const mockedPopulateJourneysForPerson = populateJourneysForPerson as jest.MockedFunction<
typeof populateJourneysForPerson
>;

describe('PersonFactory', () => {
let surveyObjectsRegistry: SurveyObjectsRegistry;
Expand Down Expand Up @@ -78,6 +83,7 @@ describe('PersonFactory', () => {

// Clear all mocks
jest.clearAllMocks();
mockedPopulateJourneysForPerson.mockResolvedValue();
});

describe('populatePersonsForHousehold', () => {
Expand All @@ -86,20 +92,22 @@ describe('PersonFactory', () => {
const mockPerson1 = {
_uuid: 'person-1',
age: 30,
assignColor: jest.fn()
assignColor: jest.fn(),
setupWorkAndSchoolPlaces: jest.fn()
} as unknown as Person;

const mockPerson2 = {
_uuid: 'person-2',
age: 25,
assignColor: jest.fn()
assignColor: jest.fn(),
setupWorkAndSchoolPlaces: jest.fn()
} as unknown as Person;

(MockedPerson.create as jest.Mock)
.mockReturnValueOnce(createOk(mockPerson1))
.mockReturnValueOnce(createOk(mockPerson2));

await populatePersonsForHousehold(surveyObjectsWithErrors, household, correctedResponse, surveyObjectsRegistry);
await populatePersonsForHousehold(surveyObjectsWithErrors, household, undefined, correctedResponse, surveyObjectsRegistry);

// Verify Person.create was called with correct attributes
expect(MockedPerson.create).toHaveBeenCalledTimes(2);
Expand Down Expand Up @@ -135,17 +143,124 @@ describe('PersonFactory', () => {
expect(surveyObjectsWithErrors.errorsByObject.personsByUuid).toEqual({});
});

it('should omit journeys from Person.create attributes', async () => {
correctedResponse.household!.persons = {
'person-1': {
_uuid: 'person-1',
_sequence: 1,
age: 30,
journeys: {
'journey-1': { _uuid: 'journey-1', _sequence: 1 }
}
}
};

(MockedPerson.create as jest.Mock).mockReturnValue(createOk({
_uuid: 'person-1',
assignColor: jest.fn(),
setupWorkAndSchoolPlaces: jest.fn()
} as unknown as Person));

await populatePersonsForHousehold(surveyObjectsWithErrors, household, undefined, correctedResponse, surveyObjectsRegistry);

expect(MockedPerson.create).toHaveBeenCalledWith(
expect.objectContaining({
_uuid: 'person-1',
_sequence: 1,
age: 30
}),
surveyObjectsRegistry
);
expect(MockedPerson.create).toHaveBeenCalledWith(
expect.not.objectContaining({
journeys: expect.anything()
}),
surveyObjectsRegistry
);
expect(household.members).toHaveLength(1);
});

it('should populate journeys and setup work/school places for each successfully created person', async () => {
const home = { _uuid: 'home-uuid' } as any;
const mockPerson1 = {
_uuid: 'person-1',
assignColor: jest.fn(),
setupWorkAndSchoolPlaces: jest.fn()
} as unknown as Person;
const mockPerson2 = {
_uuid: 'person-2',
assignColor: jest.fn(),
setupWorkAndSchoolPlaces: jest.fn()
} as unknown as Person;

(MockedPerson.create as jest.Mock)
.mockReturnValueOnce(createOk(mockPerson1))
.mockReturnValueOnce(createOk(mockPerson2));

await populatePersonsForHousehold(surveyObjectsWithErrors, household, home, correctedResponse, surveyObjectsRegistry);

expect(mockedPopulateJourneysForPerson).toHaveBeenCalledTimes(2);
expect(mockedPopulateJourneysForPerson).toHaveBeenNthCalledWith(
1,
surveyObjectsWithErrors,
mockPerson1,
expect.objectContaining({ _uuid: 'person-1' }),
home,
correctedResponse,
surveyObjectsRegistry
);
expect(mockedPopulateJourneysForPerson).toHaveBeenNthCalledWith(
2,
surveyObjectsWithErrors,
mockPerson2,
expect.objectContaining({ _uuid: 'person-2' }),
home,
correctedResponse,
surveyObjectsRegistry
);

expect(mockPerson1.setupWorkAndSchoolPlaces).toHaveBeenCalledTimes(1);
expect(mockPerson2.setupWorkAndSchoolPlaces).toHaveBeenCalledTimes(1);
});

it('should not populate journeys or setup work/school places for persons that failed to be created', async () => {
const errors = [new Error('Invalid age')];
const mockPerson2 = {
_uuid: 'person-2',
assignColor: jest.fn(),
setupWorkAndSchoolPlaces: jest.fn()
} as unknown as Person;

(MockedPerson.create as jest.Mock)
.mockReturnValueOnce(createErrors(errors))
.mockReturnValueOnce(createOk(mockPerson2));

await populatePersonsForHousehold(surveyObjectsWithErrors, household, undefined, correctedResponse, surveyObjectsRegistry);

expect(mockedPopulateJourneysForPerson).toHaveBeenCalledTimes(1);
expect(mockedPopulateJourneysForPerson).toHaveBeenCalledWith(
surveyObjectsWithErrors,
mockPerson2,
expect.objectContaining({ _uuid: 'person-2' }),
undefined,
correctedResponse,
surveyObjectsRegistry
);
expect(mockPerson2.setupWorkAndSchoolPlaces).toHaveBeenCalledTimes(1);
});

it('should handle person creation errors', async () => {
const errors = [new Error('Invalid age')];

(MockedPerson.create as jest.Mock)
.mockReturnValueOnce(createErrors(errors))
.mockReturnValueOnce(createOk({
_uuid: 'person-2',
assignColor: jest.fn()
assignColor: jest.fn(),
setupWorkAndSchoolPlaces: jest.fn()
} as unknown as Person));

await populatePersonsForHousehold(surveyObjectsWithErrors, household, correctedResponse, surveyObjectsRegistry);
await populatePersonsForHousehold(surveyObjectsWithErrors, household, undefined, correctedResponse, surveyObjectsRegistry);

// Verify error was stored
expect(surveyObjectsWithErrors.errorsByObject.personsByUuid['person-1']).toEqual(errors);
Expand All @@ -171,10 +286,11 @@ describe('PersonFactory', () => {

(MockedPerson.create as jest.Mock).mockReturnValue(createOk({
_uuid: 'person-1',
assignColor: jest.fn()
assignColor: jest.fn(),
setupWorkAndSchoolPlaces: jest.fn()
} as unknown as Person));

await populatePersonsForHousehold(surveyObjectsWithErrors, household, correctedResponse, surveyObjectsRegistry);
await populatePersonsForHousehold(surveyObjectsWithErrors, household, undefined, correctedResponse, surveyObjectsRegistry);

// Should only create one person (skip undefined)
expect(MockedPerson.create).toHaveBeenCalledTimes(1);
Expand All @@ -184,15 +300,15 @@ describe('PersonFactory', () => {
it('should handle missing household', async () => {
surveyObjectsWithErrors.household = undefined;

await populatePersonsForHousehold(surveyObjectsWithErrors, household, correctedResponse, surveyObjectsRegistry);
await populatePersonsForHousehold(surveyObjectsWithErrors, household, undefined, correctedResponse, surveyObjectsRegistry);

expect(MockedPerson.create).not.toHaveBeenCalled();
});

it('should handle missing persons attributes', async () => {
correctedResponse.household!.persons = undefined;

await populatePersonsForHousehold(surveyObjectsWithErrors, household, correctedResponse, surveyObjectsRegistry);
await populatePersonsForHousehold(surveyObjectsWithErrors, household, undefined, correctedResponse, surveyObjectsRegistry);

expect(MockedPerson.create).not.toHaveBeenCalled();
expect(household.members).toHaveLength(0);
Expand All @@ -219,11 +335,11 @@ describe('PersonFactory', () => {
};

(MockedPerson.create as jest.Mock)
.mockReturnValueOnce(createOk({ _uuid: 'person-1', assignColor: jest.fn() } as unknown as Person))
.mockReturnValueOnce(createOk({ _uuid: 'person-2', assignColor: jest.fn() } as unknown as Person))
.mockReturnValueOnce(createOk({ _uuid: 'person-3', assignColor: jest.fn() } as unknown as Person));
.mockReturnValueOnce(createOk({ _uuid: 'person-1', assignColor: jest.fn(), setupWorkAndSchoolPlaces: jest.fn() } as unknown as Person))
.mockReturnValueOnce(createOk({ _uuid: 'person-2', assignColor: jest.fn(), setupWorkAndSchoolPlaces: jest.fn() } as unknown as Person))
.mockReturnValueOnce(createOk({ _uuid: 'person-3', assignColor: jest.fn(), setupWorkAndSchoolPlaces: jest.fn() } as unknown as Person));

await populatePersonsForHousehold(surveyObjectsWithErrors, household, correctedResponse, surveyObjectsRegistry);
await populatePersonsForHousehold(surveyObjectsWithErrors, household, undefined, correctedResponse, surveyObjectsRegistry);

// Verify persons were created in sequence order (1, 2, 3)
expect(MockedPerson.create).toHaveBeenNthCalledWith(1, expect.objectContaining({ _sequence: 1 }), surveyObjectsRegistry);
Expand Down
Loading
Loading