From 1505925e2c5fa735b446ead36fe1d88007a0b2c9 Mon Sep 17 00:00:00 2001 From: Barry Dwyer Date: Mon, 1 Dec 2025 15:15:55 +0200 Subject: [PATCH 01/15] Fix tests and Refactor moving files --- v1-to-v2-data-migration/helpers/types.ts | 7 +++++-- v1-to-v2-data-migration/tests/deno.json | 4 ++-- .../tests/{ => integration}/roundtrip.test.ts | 8 ++++---- .../tests/{ => unit}/transform.test.ts | 15 +++++++++------ .../tests/{ => utils}/data-generators.ts | 2 +- .../tests/{ => utils}/queries.ts | 0 .../tests/{ => utils}/utils.ts | 0 7 files changed, 21 insertions(+), 15 deletions(-) rename v1-to-v2-data-migration/tests/{ => integration}/roundtrip.test.ts (73%) rename v1-to-v2-data-migration/tests/{ => unit}/transform.test.ts (91%) rename v1-to-v2-data-migration/tests/{ => utils}/data-generators.ts (99%) rename v1-to-v2-data-migration/tests/{ => utils}/queries.ts (100%) rename v1-to-v2-data-migration/tests/{ => utils}/utils.ts (100%) diff --git a/v1-to-v2-data-migration/helpers/types.ts b/v1-to-v2-data-migration/helpers/types.ts index 4a65a4b..6cbae60 100644 --- a/v1-to-v2-data-migration/helpers/types.ts +++ b/v1-to-v2-data-migration/helpers/types.ts @@ -177,7 +177,10 @@ export interface HistoryItem { } // Resolver types -export type ResolverFunction = (data: T, eventType: 'birth' | 'death') => any +export type ResolverFunction = ( + data: T, + eventType: 'birth' | 'death' +) => any export interface ResolverMap { [fieldId: string]: ResolverFunction @@ -204,7 +207,7 @@ export interface EventRegistration { weightAtBirth?: number deathDate?: string deathDescription?: string - causeOfDeathEstablished?: "true" | "false" + causeOfDeathEstablished?: 'true' | 'false' causeOfDeathMethod?: string mannerOfDeath?: string } diff --git a/v1-to-v2-data-migration/tests/deno.json b/v1-to-v2-data-migration/tests/deno.json index 5c4c113..d8a5029 100644 --- a/v1-to-v2-data-migration/tests/deno.json +++ b/v1-to-v2-data-migration/tests/deno.json @@ -15,7 +15,7 @@ "graphql": "npm:graphql@^16.0.0" }, "tasks": { - "migration": "deno test --allow-env --allow-run --allow-net --allow-write roundtrip.test.ts", - "transform": "deno test --allow-env --allow-run --allow-net --allow-write transform.test.ts" + "migration": "deno test --allow-env --allow-run --allow-net --allow-write ./integration/roundtrip.test.ts", + "transform": "deno test --allow-env --allow-run --allow-net --allow-write ./unit/*.test.ts" } } diff --git a/v1-to-v2-data-migration/tests/roundtrip.test.ts b/v1-to-v2-data-migration/tests/integration/roundtrip.test.ts similarity index 73% rename from v1-to-v2-data-migration/tests/roundtrip.test.ts rename to v1-to-v2-data-migration/tests/integration/roundtrip.test.ts index 0f6c240..1decac2 100644 --- a/v1-to-v2-data-migration/tests/roundtrip.test.ts +++ b/v1-to-v2-data-migration/tests/integration/roundtrip.test.ts @@ -1,8 +1,8 @@ import { expect } from 'jsr:@std/expect' -import { generateBirthRegistration } from './data-generators.ts' -import { authenticate } from '../helpers/authentication.ts' -import { GATEWAY } from '../helpers/routes.ts' -import { createDeclaration, getEvent, runMigration } from './utils.ts' +import { generateBirthRegistration } from '../utils/data-generators.ts' +import { authenticate } from '../../helpers/authentication.ts' +import { GATEWAY } from '../../helpers/routes.ts' +import { createDeclaration, getEvent, runMigration } from '../utils.ts' Deno.test('Create some data to test with', async (t) => { const token = await authenticate('k.mweene', 'test') diff --git a/v1-to-v2-data-migration/tests/transform.test.ts b/v1-to-v2-data-migration/tests/unit/transform.test.ts similarity index 91% rename from v1-to-v2-data-migration/tests/transform.test.ts rename to v1-to-v2-data-migration/tests/unit/transform.test.ts index 5f6be5d..401c914 100644 --- a/v1-to-v2-data-migration/tests/transform.test.ts +++ b/v1-to-v2-data-migration/tests/unit/transform.test.ts @@ -1,10 +1,10 @@ -import { expect } from 'jsr:@std/expect' -import { transformCorrection } from '../helpers/transform.ts' +import { transformCorrection } from '../../helpers/transform.ts' import { assertEquals } from 'https://deno.land/std@0.210.0/assert/mod.ts' Deno.test('transformCorrection', async (t) => { await t.step('should transform scalar values', () => { const historyItem = { + date: '2023-10-01T12:00:00Z', output: [ { valueCode: 'informant', @@ -29,11 +29,12 @@ Deno.test('transformCorrection', async (t) => { 'informant.phoneNo': '0788787290', 'mother.nationality': 'JPN', } - assertEquals(result, expected) + assertEquals(result.output, expected) }) await t.step('should transform custom fields', () => { const historyItem = { + date: '2023-10-01T12:00:00Z', output: [ { valueCode: 'informant', @@ -62,11 +63,12 @@ Deno.test('transformCorrection', async (t) => { 'informant.nid': '0011002211', 'informant.phoneNo': '0715773955', } - assertEquals(result, expected) + assertEquals(result.output, expected) }) await t.step('should transform name values', () => { const historyItem = { + date: '2023-10-01T12:00:00Z', output: [ { valueCode: 'child', @@ -91,11 +93,12 @@ Deno.test('transformCorrection', async (t) => { 'child.name': { firstname: 'Tarzan' }, 'mother.name': { surname: 'Susan' }, } - assertEquals(result, expected) + assertEquals(result.output, expected) }) await t.step('should transform address values', () => { const historyItem = { + date: '2023-10-01T12:00:00Z', output: [ { valueCode: 'mother', @@ -162,6 +165,6 @@ Deno.test('transformCorrection', async (t) => { }, } - assertEquals(result, expected) + assertEquals(result.output, expected) }) }) diff --git a/v1-to-v2-data-migration/tests/data-generators.ts b/v1-to-v2-data-migration/tests/utils/data-generators.ts similarity index 99% rename from v1-to-v2-data-migration/tests/data-generators.ts rename to v1-to-v2-data-migration/tests/utils/data-generators.ts index 051b481..4676aed 100644 --- a/v1-to-v2-data-migration/tests/data-generators.ts +++ b/v1-to-v2-data-migration/tests/utils/data-generators.ts @@ -1,7 +1,7 @@ import { faker } from '@faker-js/faker' import { format, subDays, subYears } from 'date-fns' import { v4 as uuidv4 } from 'uuid' -import { getAllLocations } from './utils.ts' +import { getAllLocations } from '../utils.ts' import type { fhir } from 'fhir' // Types for generated data diff --git a/v1-to-v2-data-migration/tests/queries.ts b/v1-to-v2-data-migration/tests/utils/queries.ts similarity index 100% rename from v1-to-v2-data-migration/tests/queries.ts rename to v1-to-v2-data-migration/tests/utils/queries.ts diff --git a/v1-to-v2-data-migration/tests/utils.ts b/v1-to-v2-data-migration/tests/utils/utils.ts similarity index 100% rename from v1-to-v2-data-migration/tests/utils.ts rename to v1-to-v2-data-migration/tests/utils/utils.ts From 528c76edbed15d803c46cc6b7191ae75f977b7d3 Mon Sep 17 00:00:00 2001 From: Barry Dwyer Date: Mon, 1 Dec 2025 16:11:06 +0200 Subject: [PATCH 02/15] Create action mapping tests --- v1-to-v2-data-migration/helpers/types.ts | 2 +- .../tests/unit/actionMapping.test.ts | 545 ++++++++++++++++++ 2 files changed, 546 insertions(+), 1 deletion(-) create mode 100644 v1-to-v2-data-migration/tests/unit/actionMapping.test.ts diff --git a/v1-to-v2-data-migration/helpers/types.ts b/v1-to-v2-data-migration/helpers/types.ts index 6cbae60..bfe9c52 100644 --- a/v1-to-v2-data-migration/helpers/types.ts +++ b/v1-to-v2-data-migration/helpers/types.ts @@ -111,7 +111,7 @@ export interface Registration { contactEmail?: string informantsSignature?: string attachments?: Document[] - duplicates?: Array<{ compositionId: string }> + duplicates?: Array<{ compositionId: string; trackingId?: string }> } // History item types diff --git a/v1-to-v2-data-migration/tests/unit/actionMapping.test.ts b/v1-to-v2-data-migration/tests/unit/actionMapping.test.ts new file mode 100644 index 0000000..dabc2d4 --- /dev/null +++ b/v1-to-v2-data-migration/tests/unit/actionMapping.test.ts @@ -0,0 +1,545 @@ +import { transform } from '../../helpers/transform.ts' +import { assertEquals } from 'https://deno.land/std@0.210.0/assert/mod.ts' +import type { EventRegistration, HistoryItem } from '../../helpers/types.ts' + +// Builders +function buildHistoryItem(overrides: Partial = {}): HistoryItem { + return { + date: '2023-10-01T12:00:00Z', + user: { id: 'user-123', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office-456' }, + ...overrides, + } +} + +function buildEventRegistration( + overrides: Partial = {} +): EventRegistration { + return { + id: 'event-123', + registration: { + trackingId: 'TRACK123', + registrationNumber: 'REG123', + }, + history: [], + ...overrides, + } +} + +Deno.test('transform - basic action type mappings', async (t) => { + await t.step( + 'should map DECLARED regStatus to DECLARE action with annotation', + () => { + const history = [ + buildHistoryItem({ + regStatus: 'DECLARED', + comments: [{ comment: 'Declaration comment' }], + }), + ] + const registration = buildEventRegistration({ + history, + registration: { + trackingId: 'TRACK123', + informantsSignature: 'http://localhost/ocrvs/signature.png', + }, + }) + + const result = transform(registration, {}, 'birth') + + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + assertEquals(declareAction !== undefined, true) + assertEquals( + declareAction?.annotation?.['review.comment'], + 'Declaration comment' + ) + assertEquals( + declareAction?.annotation?.['review.signature']?.type, + 'image/png' + ) + } + ) + + await t.step( + 'should map REGISTERED regStatus to REGISTER action with registrationNumber', + () => { + const history = [ + buildHistoryItem({ + regStatus: 'REGISTERED', + comments: [{ comment: 'Registration comment' }], + }), + ] + const registration = buildEventRegistration({ + history, + registration: { + trackingId: 'TRACK123', + registrationNumber: 'REG123', + informantsSignature: 'http://localhost/ocrvs/signature.png', + }, + }) + + const result = transform(registration, {}, 'birth') + + const registerAction = result.actions.find((a) => a.type === 'REGISTER') + assertEquals(registerAction !== undefined, true) + assertEquals(registerAction?.registrationNumber, 'REG123') + assertEquals( + registerAction?.annotation?.['review.comment'], + 'Registration comment' + ) + } + ) + + await t.step( + 'should map WAITING_VALIDATION regStatus to REGISTER action with Requested status', + () => { + const history = [buildHistoryItem({ regStatus: 'WAITING_VALIDATION' })] + const registration = buildEventRegistration({ history }) + + const result = transform(registration, {}, 'birth') + + const registerAction = result.actions.find( + (a) => a.type === 'REGISTER' && a.status === 'Requested' + ) + assertEquals(registerAction !== undefined, true) + } + ) + + await t.step( + 'should map VALIDATED regStatus to VALIDATE action with annotation', + () => { + const history = [ + buildHistoryItem({ + regStatus: 'VALIDATED', + comments: [{ comment: 'Validation comment' }], + }), + ] + const registration = buildEventRegistration({ + history, + registration: { + trackingId: 'TRACK123', + informantsSignature: 'http://localhost/ocrvs/signature.png', + }, + }) + + const result = transform(registration, {}, 'birth') + + const validateAction = result.actions.find((a) => a.type === 'VALIDATE') + assertEquals(validateAction !== undefined, true) + assertEquals( + validateAction?.annotation?.['review.comment'], + 'Validation comment' + ) + } + ) + + await t.step( + 'should map REJECTED regStatus to REJECT action with content.reason', + () => { + const history = [ + buildHistoryItem({ + regStatus: 'REJECTED', + statusReason: { text: 'Invalid documents' }, + }), + ] + const registration = buildEventRegistration({ history }) + + const result = transform(registration, {}, 'birth') + + const rejectAction = result.actions.find((a) => a.type === 'REJECT') + assertEquals(rejectAction !== undefined, true) + assertEquals(rejectAction?.status, 'Accepted') + assertEquals(rejectAction?.content?.reason, 'Invalid documents') + } + ) + + await t.step( + 'should map ARCHIVED regStatus to ARCHIVE action with content.reason', + () => { + const history = [ + buildHistoryItem({ + regStatus: 'ARCHIVED', + statusReason: { text: 'Archived for audit' }, + }), + ] + const registration = buildEventRegistration({ history }) + + const result = transform(registration, {}, 'birth') + + const archiveAction = result.actions.find((a) => a.type === 'ARCHIVE') + assertEquals(archiveAction !== undefined, true) + assertEquals(archiveAction?.content?.reason, 'Archived for audit') + } + ) + + await t.step('should map ASSIGNED action to ASSIGN with assignedTo', () => { + const history = [ + buildHistoryItem({ + action: 'ASSIGNED', + user: { id: 'assignee-456', role: { id: 'REGISTRAR' } }, + }), + ] + const registration = buildEventRegistration({ history }) + + const result = transform(registration, {}, 'birth') + + const assignAction = result.actions.find((a) => a.type === 'ASSIGN') + assertEquals(assignAction !== undefined, true) + assertEquals(assignAction?.assignedTo, 'assignee-456') + }) + + await t.step( + 'should map FLAGGED_AS_POTENTIAL_DUPLICATE to DUPLICATE_DETECTED with content.duplicates', + () => { + const history = [ + buildHistoryItem({ + action: 'FLAGGED_AS_POTENTIAL_DUPLICATE', + }), + ] + const registration = buildEventRegistration({ + history, + registration: { + trackingId: 'TRACK123', + duplicates: [ + { compositionId: 'dup-1', trackingId: 'TRACK456' }, + { compositionId: 'dup-2', trackingId: 'TRACK789' }, + ], + }, + }) + + const result = transform(registration, {}, 'birth') + + const duplicateAction = result.actions.find( + (a) => a.type === 'DUPLICATE_DETECTED' + ) + assertEquals(duplicateAction !== undefined, true) + assertEquals(duplicateAction?.content?.duplicates?.length, 2) + assertEquals(duplicateAction?.content?.duplicates?.[0]?.id, 'dup-1') + assertEquals( + duplicateAction?.content?.duplicates?.[0]?.trackingId, + 'TRACK456' + ) + } + ) + + await t.step( + 'should map MARKED_AS_DUPLICATE action to MARK_AS_DUPLICATE', + () => { + const history = [buildHistoryItem({ action: 'MARKED_AS_DUPLICATE' })] + const registration = buildEventRegistration({ history }) + + const result = transform(registration, {}, 'birth') + + const markDupAction = result.actions.find( + (a) => a.type === 'MARK_AS_DUPLICATE' + ) + assertEquals(markDupAction !== undefined, true) + } + ) + + await t.step( + 'should map MARKED_AS_NOT_DUPLICATE action to MARK_AS_NOT_DUPLICATE', + () => { + const history = [buildHistoryItem({ action: 'MARKED_AS_NOT_DUPLICATE' })] + const registration = buildEventRegistration({ history }) + + const result = transform(registration, {}, 'birth') + + const markNotDupAction = result.actions.find( + (a) => a.type === 'MARK_AS_NOT_DUPLICATE' + ) + assertEquals(markNotDupAction !== undefined, true) + } + ) + + await t.step('should map DOWNLOADED action to READ', () => { + const history = [buildHistoryItem({ action: 'DOWNLOADED' })] + const registration = buildEventRegistration({ history }) + + const result = transform(registration, {}, 'birth') + + const readAction = result.actions.find((a) => a.type === 'READ') + assertEquals(readAction !== undefined, true) + }) + + await t.step('should map VIEWED action to READ', () => { + const history = [buildHistoryItem({ action: 'VIEWED' })] + const registration = buildEventRegistration({ history }) + + const result = transform(registration, {}, 'birth') + + const readAction = result.actions.find((a) => a.type === 'READ') + assertEquals(readAction !== undefined, true) + }) + + await t.step('should map UNASSIGNED action to UNASSIGN', () => { + const history = [buildHistoryItem({ action: 'UNASSIGNED' })] + const registration = buildEventRegistration({ history }) + + const result = transform(registration, {}, 'birth') + + const unassignAction = result.actions.find((a) => a.type === 'UNASSIGN') + assertEquals(unassignAction !== undefined, true) + }) + + await t.step('should map VERIFIED action to VALIDATE', () => { + const history = [buildHistoryItem({ action: 'VERIFIED' })] + const registration = buildEventRegistration({ history }) + + const result = transform(registration, {}, 'birth') + + const validateAction = result.actions.find((a) => a.type === 'VALIDATE') + assertEquals(validateAction !== undefined, true) + }) + + await t.step('should always create a CREATE action as first action', () => { + const history = [buildHistoryItem({ regStatus: 'DECLARED' })] + const registration = buildEventRegistration({ history }) + + const result = transform(registration, {}, 'birth') + + assertEquals(result.actions[0].type, 'CREATE') + }) + + await t.step('should handle multiple actions in sequence', () => { + const history = [ + buildHistoryItem({ regStatus: 'DECLARED', date: '2023-10-01T12:00:00Z' }), + buildHistoryItem({ + regStatus: 'REGISTERED', + date: '2023-10-02T12:00:00Z', + }), + buildHistoryItem({ action: 'VIEWED', date: '2023-10-03T12:00:00Z' }), + ] + const registration = buildEventRegistration({ history }) + + const result = transform(registration, {}, 'birth') + + assertEquals(result.actions.length, 4) // CREATE + 3 history items + assertEquals(result.actions[0].type, 'CREATE') + assertEquals(result.actions[1].type, 'DECLARE') + assertEquals(result.actions[2].type, 'REGISTER') + assertEquals(result.actions[3].type, 'READ') + }) +}) + +Deno.test( + 'transform - should map ISSUED/CERTIFIED to PRINT_CERTIFICATE with content and annotation', + () => { + const history = [ + buildHistoryItem({ + regStatus: 'DECLARED', + date: '2023-10-01T10:00:00Z', + }), + buildHistoryItem({ + regStatus: 'CERTIFIED', + date: '2023-10-01T12:00:00Z', + certificates: [{ certificateTemplateId: 'template-1' }], + }), + buildHistoryItem({ + regStatus: 'ISSUED', + date: '2023-10-01T13:00:00Z', + certificates: [ + { + certificateTemplateId: 'template-1', + hasShowedVerifiedDocument: true, + collector: { + relationship: 'MOTHER', + identifier: [{ id: '123456', type: 'NATIONAL_ID' }], + name: [{ firstNames: 'Jane', familyName: 'Doe' }], + }, + }, + ], + }), + ] + const registration = buildEventRegistration({ history }) + + const result = transform(registration, {}, 'birth') + + const printAction = result.actions.find( + (a) => a.type === 'PRINT_CERTIFICATE' + ) + assertEquals(printAction !== undefined, true) + assertEquals(printAction?.content?.templateId, 'template-1') + assertEquals(printAction?.annotation?.['collector.requesterId'], 'MOTHER') + assertEquals(printAction?.annotation?.['collector.nid'], '123456') + assertEquals(printAction?.annotation?.['collector.identity.verify'], true) + } +) + +Deno.test('transform - correction action mappings', async (t) => { + await t.step( + 'should map REQUESTED_CORRECTION action to REQUEST_CORRECTION', + () => { + const history = [ + buildHistoryItem({ + action: 'REQUESTED_CORRECTION', + output: [ + { + valueCode: 'informant', + valueId: 'registrationPhone', + value: '0788787290', + }, + ], + }), + ] + const registration = buildEventRegistration({ history }) + + const result = transform(registration, {}, 'birth') + + const requestAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) + assertEquals(requestAction !== undefined, true) + } + ) + + await t.step( + 'should map APPROVED_CORRECTION with requestId and annotation', + () => { + const history = [ + buildHistoryItem({ + action: 'APPROVED_CORRECTION', + requestId: 'req-123', + annotation: { isImmediateCorrection: true }, + }), + ] + const registration = buildEventRegistration({ history }) + + const result = transform(registration, {}, 'birth') + + const approveAction = result.actions.find( + (a) => a.type === 'APPROVE_CORRECTION' + ) + assertEquals(approveAction !== undefined, true) + assertEquals(approveAction?.status, 'Accepted') + assertEquals(approveAction?.requestId, 'req-123') + assertEquals(approveAction?.annotation?.isImmediateCorrection, true) + } + ) + + await t.step( + 'should map REJECTED_CORRECTION with requestId and content.reason', + () => { + const history = [ + buildHistoryItem({ + action: 'REJECTED_CORRECTION', + requestId: 'req-123', + reason: 'Insufficient evidence', + }), + ] + const registration = buildEventRegistration({ history }) + + const result = transform(registration, {}, 'birth') + + const rejectCorrectionAction = result.actions.find( + (a) => a.type === 'REJECT_CORRECTION' + ) + assertEquals(rejectCorrectionAction !== undefined, true) + assertEquals(rejectCorrectionAction?.status, 'Accepted') + assertEquals(rejectCorrectionAction?.requestId, 'req-123') + assertEquals( + rejectCorrectionAction?.content?.reason, + 'Insufficient evidence' + ) + } + ) +}) + +Deno.test( + 'transform - CORRECTED should create REQUEST_CORRECTION and APPROVE_CORRECTION actions', + () => { + const history = [ + buildHistoryItem({ + action: 'CORRECTED', + date: '2023-10-01T12:00:00Z', + output: [ + { + valueCode: 'informant', + valueId: 'registrationPhone', + value: '0788787290', + }, + ], + }), + ] + const registration = buildEventRegistration({ history }) + + const result = transform(registration, {}, 'birth') + + // CORRECTED should be split into REQUEST_CORRECTION and APPROVE_CORRECTION + const requestAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) + const approveAction = result.actions.find( + (a) => a.type === 'APPROVE_CORRECTION' + ) + + assertEquals(requestAction !== undefined, true) + assertEquals(approveAction !== undefined, true) + + // The approve action should have the request action's ID as requestId + if (requestAction && approveAction) { + assertEquals(approveAction.requestId, requestAction.id) + } + } +) + +Deno.test('transform - should merge ISSUED and CERTIFIED actions', () => { + const history = [ + buildHistoryItem({ + regStatus: 'CERTIFIED', + date: '2023-10-01T12:00:00Z', + certificates: [{ certificateTemplateId: 'template-1' }], + }), + buildHistoryItem({ + regStatus: 'ISSUED', + date: '2023-10-01T13:00:00Z', + certificates: [{ certificateTemplateId: 'template-1' }], + }), + ] + const registration = buildEventRegistration({ history }) + + const result = transform(registration, {}, 'birth') + + // Should create PRINT_CERTIFICATE action and filter out CERTIFIED + const printActions = result.actions.filter( + (a) => a.type === 'PRINT_CERTIFICATE' + ) + assertEquals(printActions.length, 1) +}) + +Deno.test( + 'transform - should link REQUEST_CORRECTION and APPROVE_CORRECTION actions via requestId', + () => { + const history = [ + buildHistoryItem({ + action: 'REQUESTED_CORRECTION', + date: '2023-10-01T12:00:00Z', + output: [ + { + valueCode: 'informant', + valueId: 'registrationPhone', + value: '0788787290', + }, + ], + }), + buildHistoryItem({ + action: 'APPROVED_CORRECTION', + date: '2023-10-02T12:00:00Z', + }), + ] + const registration = buildEventRegistration({ history }) + + const result = transform(registration, {}, 'birth') + + const requestAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) + const approveAction = result.actions.find( + (a) => a.type === 'APPROVE_CORRECTION' + ) + + // The preprocessing should link them via requestId + if (requestAction && approveAction) { + assertEquals(approveAction.requestId, requestAction.id) + } + } +) From 2b47314ce9589823616ac5deec64e0aa7dcbb4ce Mon Sep 17 00:00:00 2001 From: Barry Dwyer Date: Mon, 1 Dec 2025 17:00:25 +0200 Subject: [PATCH 03/15] Add birthResolver tests --- v1-to-v2-data-migration/helpers/types.ts | 2 + .../tests/unit/birthResolver.test.ts | 1200 +++++++++++++++++ 2 files changed, 1202 insertions(+) create mode 100644 v1-to-v2-data-migration/tests/unit/birthResolver.test.ts diff --git a/v1-to-v2-data-migration/helpers/types.ts b/v1-to-v2-data-migration/helpers/types.ts index bfe9c52..e3f09f1 100644 --- a/v1-to-v2-data-migration/helpers/types.ts +++ b/v1-to-v2-data-migration/helpers/types.ts @@ -40,6 +40,7 @@ export interface Document { uri: string contentType: string type: string + subject?: string } export interface ProcessedDocumentWithOptionType { @@ -56,6 +57,7 @@ export interface ProcessedDocument { // Address types export interface AddressLine { + type?: string line: string[] city?: string district?: string diff --git a/v1-to-v2-data-migration/tests/unit/birthResolver.test.ts b/v1-to-v2-data-migration/tests/unit/birthResolver.test.ts new file mode 100644 index 0000000..185a14c --- /dev/null +++ b/v1-to-v2-data-migration/tests/unit/birthResolver.test.ts @@ -0,0 +1,1200 @@ +import { transform } from '../../helpers/transform.ts' +import { assertEquals } from 'https://deno.land/std@0.210.0/assert/mod.ts' +import defaultResolvers, { + defaultBirthResolver, +} from '../../helpers/defaultResolvers.ts' +import { countryResolver } from '../../countryData/countryResolvers.ts' +import { EventRegistration } from '../../helpers/types.ts' + +// Construct birthResolver as in migrate.ipynb +const allResolvers = { ...defaultResolvers, ...countryResolver } +const birthResolver = { ...defaultBirthResolver, ...allResolvers } + +function buildEventRegistration( + overrides?: Partial +): EventRegistration { + return { + id: '123', + registration: { + trackingId: 'B123456', + registrationNumber: '2024B123456', + contactPhoneNumber: '+2600987654321', + contactEmail: 'test@example.com', + informantsSignature: 'data:image/png;base64,abc123', + }, + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + ], + ...overrides, + } +} + +Deno.test('birthResolver - child fields', async (t) => { + await t.step('should resolve child.name fields', () => { + const registration = buildEventRegistration({ + child: { + name: [ + { + firstNames: 'John', + middleName: 'Paul', + familyName: 'Smith', + }, + ], + }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['child.name'], { + firstname: 'John', + middleName: 'Paul', + surname: 'Smith', + }) + }) + + await t.step('should resolve child.gender', () => { + const registration = buildEventRegistration({ + child: { gender: 'male' }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['child.gender'], 'male') + }) + + await t.step('should resolve child.dob', () => { + const registration = buildEventRegistration({ + child: { birthDate: '2024-01-15' }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['child.dob'], '2024-01-15') + }) + + await t.step('should resolve child.placeOfBirth', () => { + const registration = buildEventRegistration({ + eventLocation: { type: 'HEALTH_FACILITY', id: 'facility1' }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['child.placeOfBirth'], + 'HEALTH_FACILITY' + ) + }) + + await t.step('should resolve child.birthLocation for HEALTH_FACILITY', () => { + const registration = buildEventRegistration({ + eventLocation: { type: 'HEALTH_FACILITY', id: 'facility1' }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['child.birthLocation'], 'facility1') + }) + + await t.step( + 'should resolve child.birthLocation.privateHome for PRIVATE_HOME', + () => { + const registration = buildEventRegistration({ + eventLocation: { + type: 'PRIVATE_HOME', + address: { + line: ['123', 'Main St', 'City', '', '', 'URBAN'], + district: 'District1', + state: 'State1', + city: 'City1', + country: 'FAR', + postalCode: '12345', + }, + }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['child.birthLocation.privateHome'] + ?.administrativeArea, + 'District1' + ) + assertEquals( + declareAction?.declaration['child.birthLocation.privateHome']?.country, + 'FAR' + ) + } + ) + + await t.step('should resolve child.birthLocation.other for OTHER', () => { + const registration = buildEventRegistration({ + eventLocation: { + type: 'OTHER', + address: { + line: ['456', 'Other St', 'Town'], + district: 'District2', + state: 'State2', + city: 'City2', + country: 'FAR', + postalCode: '54321', + }, + }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['child.birthLocation.other'] + ?.administrativeArea, + 'District2' + ) + }) + + await t.step('should resolve child.attendantAtBirth', () => { + const registration = buildEventRegistration({ + attendantAtBirth: 'PHYSICIAN', + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['child.attendantAtBirth'], + 'PHYSICIAN' + ) + }) + + await t.step('should resolve child.birthType', () => { + const registration = buildEventRegistration({ + birthType: 'TWIN', + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['child.birthType'], 'TWIN') + }) + + await t.step('should resolve child.weightAtBirth', () => { + const registration = buildEventRegistration({ + weightAtBirth: 3.5, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['child.weightAtBirth'], 3.5) + }) + + await t.step('should resolve child.reason from questionnaire', () => { + const registration = buildEventRegistration({ + questionnaire: [ + { + fieldId: 'birth.child.child-view-group.reasonForLateRegistration', + value: 'Late notification', + }, + ], + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['child.reason'], + 'Late notification' + ) + }) + + await t.step('should resolve child.nid', () => { + const registration = buildEventRegistration({ + child: { + identifier: [{ id: '1234567890', type: 'NATIONAL_ID' }], + }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['child.nid'], '1234567890') + }) +}) + +Deno.test('birthResolver - mother fields', async (t) => { + await t.step('should resolve mother.detailsNotAvailable when false', () => { + const registration = buildEventRegistration({ + mother: { detailsExist: true }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['mother.detailsNotAvailable'], + false + ) + }) + + await t.step('should resolve mother.detailsNotAvailable when true', () => { + const registration = buildEventRegistration({ + mother: { detailsExist: false }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['mother.detailsNotAvailable'], true) + }) + + await t.step('should resolve mother.reason', () => { + const registration = buildEventRegistration({ + mother: { reasonNotApplying: 'Mother unknown' }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['mother.reason'], 'Mother unknown') + }) + + await t.step('should resolve mother.name', () => { + const registration = buildEventRegistration({ + mother: { + name: [ + { + firstNames: 'Jane', + middleName: 'Marie', + familyName: 'Doe', + }, + ], + }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['mother.name'], { + firstname: 'Jane', + middleName: 'Marie', + surname: 'Doe', + }) + }) + + await t.step('should resolve mother.dob', () => { + const registration = buildEventRegistration({ + mother: { birthDate: '1990-05-15' }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['mother.dob'], '1990-05-15') + }) + + await t.step('should resolve mother.dobUnknown', () => { + const registration = buildEventRegistration({ + mother: { exactDateOfBirthUnknown: true }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['mother.dobUnknown'], true) + }) + + await t.step('should resolve mother.age with asOfDateRef', () => { + const registration = buildEventRegistration({ + mother: { ageOfIndividualInYears: 30 }, + child: { birthDate: '2024-01-15' }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['mother.age'], { + age: 30, + asOfDateRef: 'child.dob', + }) + }) + + await t.step('should resolve mother.nationality', () => { + const registration = buildEventRegistration({ + mother: { nationality: ['FAR'] }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['mother.nationality'], 'FAR') + }) + + await t.step('should resolve mother.maritalStatus', () => { + const registration = buildEventRegistration({ + mother: { maritalStatus: 'MARRIED' }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['mother.maritalStatus'], 'MARRIED') + }) + + await t.step('should resolve mother.educationalAttainment', () => { + const registration = buildEventRegistration({ + mother: { educationalAttainment: 'ISCED_4' }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['mother.educationalAttainment'], + 'ISCED_4' + ) + }) + + await t.step('should resolve mother.occupation', () => { + const registration = buildEventRegistration({ + mother: { occupation: 'Teacher' }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['mother.occupation'], 'Teacher') + }) + + await t.step('should resolve mother.previousBirths', () => { + const registration = buildEventRegistration({ + mother: { multipleBirth: 2 }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['mother.previousBirths'], 2) + }) + + await t.step('should resolve mother.address', () => { + const registration = buildEventRegistration({ + mother: { + address: [ + { + type: 'PRIMARY_ADDRESS', + line: ['10', 'Mother St', 'City', '', '', 'URBAN'], + country: 'FAR', + district: 'District1', + state: 'State1', + city: 'City1', + postalCode: '11111', + }, + ], + }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['mother.address']?.country, 'FAR') + assertEquals( + declareAction?.declaration['mother.address']?.administrativeArea, + 'District1' + ) + }) + + await t.step('should resolve mother.brn', () => { + const registration = buildEventRegistration({ + mother: { + identifier: [{ id: 'B2020123456', type: 'BIRTH_REGISTRATION_NUMBER' }], + }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['mother.brn'], 'B2020123456') + }) + + await t.step('should resolve mother.nid', () => { + const registration = buildEventRegistration({ + mother: { + identifier: [{ id: '9876543210', type: 'NATIONAL_ID' }], + }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['mother.nid'], '9876543210') + }) + + await t.step('should resolve mother.passport', () => { + const registration = buildEventRegistration({ + mother: { + identifier: [{ id: 'P123456', type: 'PASSPORT' }], + }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['mother.passport'], 'P123456') + }) + + await t.step('should resolve mother.idType from questionnaire', () => { + const registration = buildEventRegistration({ + questionnaire: [ + { + fieldId: 'birth.mother.mother-view-group.motherIdType', + value: 'NATIONAL_ID', + }, + ], + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['mother.idType'], 'NATIONAL_ID') + }) + + await t.step('should resolve mother.verified from questionnaire', () => { + const registration = buildEventRegistration({ + questionnaire: [ + { + fieldId: 'birth.mother.mother-view-group.verified', + value: 'verified', + }, + ], + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['mother.verified'], 'verified') + }) +}) + +Deno.test('birthResolver - father fields', async (t) => { + await t.step('should resolve father.detailsNotAvailable', () => { + const registration = buildEventRegistration({ + father: { detailsExist: false }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['father.detailsNotAvailable'], true) + }) + + await t.step('should resolve father.reason', () => { + const registration = buildEventRegistration({ + father: { reasonNotApplying: 'Father unknown' }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['father.reason'], 'Father unknown') + }) + + await t.step('should resolve father.name', () => { + const registration = buildEventRegistration({ + father: { + name: [ + { + firstNames: 'Michael', + familyName: 'Smith', + }, + ], + }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['father.name'], { + firstname: 'Michael', + middleName: undefined, + surname: 'Smith', + }) + }) + + await t.step('should resolve father.dob', () => { + const registration = buildEventRegistration({ + father: { birthDate: '1988-03-20' }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['father.dob'], '1988-03-20') + }) + + await t.step('should resolve father.dobUnknown', () => { + const registration = buildEventRegistration({ + father: { exactDateOfBirthUnknown: true }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['father.dobUnknown'], true) + }) + + await t.step('should resolve father.age', () => { + const registration = buildEventRegistration({ + father: { ageOfIndividualInYears: 35 }, + child: { birthDate: '2024-01-15' }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['father.age'], { + age: 35, + asOfDateRef: 'child.dob', + }) + }) + + await t.step('should resolve father.nationality', () => { + const registration = buildEventRegistration({ + father: { nationality: ['USA'] }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['father.nationality'], 'USA') + }) + + await t.step('should resolve father.maritalStatus', () => { + const registration = buildEventRegistration({ + father: { maritalStatus: 'MARRIED' }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['father.maritalStatus'], 'MARRIED') + }) + + await t.step('should resolve father.educationalAttainment', () => { + const registration = buildEventRegistration({ + father: { educationalAttainment: 'ISCED_5' }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['father.educationalAttainment'], + 'ISCED_5' + ) + }) + + await t.step('should resolve father.occupation', () => { + const registration = buildEventRegistration({ + father: { occupation: 'Engineer' }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['father.occupation'], 'Engineer') + }) + + await t.step('should resolve father.address', () => { + const registration = buildEventRegistration({ + father: { + address: [ + { + type: 'PRIMARY_ADDRESS', + line: ['20', 'Father Ave', 'Town'], + country: 'FAR', + district: 'District2', + state: 'State2', + city: 'City2', + postalCode: '22222', + }, + ], + }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['father.address']?.country, 'FAR') + assertEquals( + declareAction?.declaration['father.address']?.administrativeArea, + 'District2' + ) + }) + + await t.step( + 'should resolve father.addressSameAs when addresses match', + () => { + const sameAddress = { + type: 'PRIMARY_ADDRESS' as const, + line: ['10', 'Same St', 'City'], + country: 'FAR', + district: 'District1', + state: 'State1', + city: 'City1', + postalCode: '11111', + } + + const registration = buildEventRegistration({ + mother: { address: [sameAddress] }, + father: { address: [sameAddress] }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['father.addressSameAs'], 'YES') + } + ) + + await t.step( + 'should resolve father.addressSameAs when addresses differ', + () => { + const registration = buildEventRegistration({ + mother: { + address: [ + { + type: 'PRIMARY_ADDRESS', + line: ['10', 'Mother St'], + country: 'FAR', + district: 'District1', + }, + ], + }, + father: { + address: [ + { + type: 'PRIMARY_ADDRESS', + line: ['20', 'Father Ave'], + country: 'FAR', + district: 'District2', + }, + ], + }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['father.addressSameAs'], 'NO') + } + ) + + await t.step('should resolve father.brn', () => { + const registration = buildEventRegistration({ + father: { + identifier: [{ id: 'B2018654321', type: 'BIRTH_REGISTRATION_NUMBER' }], + }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['father.brn'], 'B2018654321') + }) + + await t.step('should resolve father.nid', () => { + const registration = buildEventRegistration({ + father: { + identifier: [{ id: '1122334455', type: 'NATIONAL_ID' }], + }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['father.nid'], '1122334455') + }) + + await t.step('should resolve father.passport', () => { + const registration = buildEventRegistration({ + father: { + identifier: [{ id: 'P654321', type: 'PASSPORT' }], + }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['father.passport'], 'P654321') + }) + + await t.step('should resolve father.idType from questionnaire', () => { + const registration = buildEventRegistration({ + questionnaire: [ + { + fieldId: 'birth.father.father-view-group.fatherIdType', + value: 'PASSPORT', + }, + ], + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['father.idType'], 'PASSPORT') + }) + + await t.step('should resolve father.verified from questionnaire', () => { + const registration = buildEventRegistration({ + questionnaire: [ + { + fieldId: 'birth.father.father-view-group.verified', + value: 'failed', + }, + ], + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['father.verified'], 'failed') + }) +}) + +Deno.test('birthResolver - informant fields', async (t) => { + await t.step('should resolve informant.dob for non-special informant', () => { + const registration = buildEventRegistration({ + informant: { + birthDate: '1995-08-10', + relationship: 'BROTHER', + }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['informant.dob'], '1995-08-10') + }) + + await t.step('should not resolve informant.dob for MOTHER', () => { + const registration = buildEventRegistration({ + informant: { + birthDate: '1995-08-10', + relationship: 'MOTHER', + }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['informant.dob'], undefined) + }) + + await t.step( + 'should resolve informant.address for non-special informant', + () => { + const registration = buildEventRegistration({ + informant: { + relationship: 'GRANDFATHER', + address: [ + { + type: 'PRIMARY_ADDRESS', + line: ['30', 'Informant Rd'], + country: 'FAR', + district: 'District3', + state: 'State3', + city: 'City3', + postalCode: '33333', + }, + ], + }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['informant.address']?.administrativeArea, + 'District3' + ) + } + ) + + await t.step( + 'should resolve informant.phoneNo with country code stripped', + () => { + const registration = buildEventRegistration({ + registration: { + trackingId: 'B123456', + contactPhoneNumber: '+260987654321', + }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['informant.phoneNo'], + '0987654321' + ) + } + ) + + await t.step('should resolve informant.email', () => { + const registration = buildEventRegistration({ + registration: { + trackingId: 'B123456', + contactEmail: 'informant@test.com', + }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['informant.email'], + 'informant@test.com' + ) + }) + + await t.step('should resolve informant.relation', () => { + const registration = buildEventRegistration({ + informant: { relationship: 'SISTER' }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['informant.relation'], 'SISTER') + }) + + await t.step('should resolve informant.other.relation', () => { + const registration = buildEventRegistration({ + informant: { otherRelationship: 'Cousin' }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['informant.other.relation'], + 'Cousin' + ) + }) + + await t.step( + 'should resolve informant.name for non-special informant', + () => { + const registration = buildEventRegistration({ + informant: { + relationship: 'LEGAL_GUARDIAN', + name: [ + { + firstNames: 'Guardian', + familyName: 'Jones', + }, + ], + }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['informant.name'], { + firstname: 'Guardian', + middleName: undefined, + surname: 'Jones', + }) + } + ) + + await t.step('should resolve informant.dobUnknown', () => { + const registration = buildEventRegistration({ + informant: { + relationship: 'GRANDFATHER', + exactDateOfBirthUnknown: true, + }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['informant.dobUnknown'], true) + }) + + await t.step('should resolve informant.age', () => { + const registration = buildEventRegistration({ + informant: { + relationship: 'OTHER', + ageOfIndividualInYears: 45, + }, + child: { birthDate: '2024-01-15' }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['informant.age'], { + age: 45, + asOfDateRef: 'child.dob', + }) + }) + + await t.step('should resolve informant.nationality', () => { + const registration = buildEventRegistration({ + informant: { + relationship: 'BROTHER', + nationality: ['GBR'], + }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['informant.nationality'], 'GBR') + }) + + await t.step('should resolve informant.brn', () => { + const registration = buildEventRegistration({ + informant: { + relationship: 'SISTER', + identifier: [{ id: 'B2015987654', type: 'BIRTH_REGISTRATION_NUMBER' }], + }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['informant.brn'], 'B2015987654') + }) + + await t.step('should resolve informant.nid', () => { + const registration = buildEventRegistration({ + informant: { + relationship: 'GRANDFATHER', + identifier: [{ id: '5566778899', type: 'NATIONAL_ID' }], + }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['informant.nid'], '5566778899') + }) + + await t.step('should resolve informant.passport', () => { + const registration = buildEventRegistration({ + informant: { + relationship: 'OTHER', + identifier: [{ id: 'P999888', type: 'PASSPORT' }], + }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['informant.passport'], 'P999888') + }) + + await t.step('should resolve informant.idType from questionnaire', () => { + const registration = buildEventRegistration({ + questionnaire: [ + { + fieldId: 'birth.informant.informant-view-group.informantIdType', + value: 'BIRTH_REGISTRATION_NUMBER', + }, + ], + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['informant.idType'], + 'BIRTH_REGISTRATION_NUMBER' + ) + }) + + await t.step('should resolve informant.verified from questionnaire', () => { + const registration = buildEventRegistration({ + questionnaire: [ + { + fieldId: 'birth.informant.informant-view-group.verified', + value: 'authenticated', + }, + ], + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['informant.verified'], + 'authenticated' + ) + }) +}) + +Deno.test('birthResolver - documents fields', async (t) => { + await t.step('should resolve documents.proofOfBirth', () => { + const registration = buildEventRegistration({ + registration: { + trackingId: 'B123456', + attachments: [ + { + uri: '/documents/birth-cert.pdf', + contentType: 'application/pdf', + type: 'BIRTH_CERTIFICATE', + subject: 'CHILD', + }, + ], + }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['documents.proofOfBirth'], { + path: '/documents/birth-cert.pdf', + originalFilename: '/documents/birth-cert.pdf', + type: 'application/pdf', + }) + }) + + await t.step('should resolve documents.proofOfMother', () => { + const registration = buildEventRegistration({ + registration: { + trackingId: 'B123456', + attachments: [ + { + uri: '/documents/mother-id.pdf', + contentType: 'application/pdf', + type: 'NATIONAL_ID', + subject: 'MOTHER', + }, + ], + }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['documents.proofOfMother'], [ + { + path: '/documents/mother-id.pdf', + originalFilename: '/documents/mother-id.pdf', + type: 'application/pdf', + option: 'NATIONAL_ID', + }, + ]) + }) + + await t.step('should resolve documents.proofOfFather', () => { + const registration = buildEventRegistration({ + registration: { + trackingId: 'B123456', + attachments: [ + { + uri: '/documents/father-id.pdf', + contentType: 'application/pdf', + type: 'PASSPORT', + subject: 'FATHER', + }, + ], + }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['documents.proofOfFather'], [ + { + path: '/documents/father-id.pdf', + originalFilename: '/documents/father-id.pdf', + type: 'application/pdf', + option: 'PASSPORT', + }, + ]) + }) + + await t.step('should resolve documents.proofOfInformant', () => { + const registration = buildEventRegistration({ + registration: { + trackingId: 'B123456', + attachments: [ + { + uri: '/documents/informant-id.pdf', + contentType: 'application/pdf', + type: 'NATIONAL_ID', + subject: 'INFORMANT_ID_PROOF', + }, + ], + }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['documents.proofOfInformant'], [ + { + path: '/documents/informant-id.pdf', + originalFilename: '/documents/informant-id.pdf', + type: 'application/pdf', + option: 'NATIONAL_ID', + }, + ]) + }) + + await t.step( + 'should resolve documents.proofOther combining OTHER and LEGAL_GUARDIAN_PROOF', + () => { + const registration = buildEventRegistration({ + registration: { + trackingId: 'B123456', + attachments: [ + { + uri: '/documents/other1.pdf', + contentType: 'application/pdf', + type: 'OTHER_DOCUMENT', + subject: 'OTHER', + }, + { + uri: '/documents/guardian.pdf', + contentType: 'application/pdf', + type: 'GUARDIAN_PROOF', + subject: 'LEGAL_GUARDIAN_PROOF', + }, + ], + }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['documents.proofOther'], [ + { + path: '/documents/other1.pdf', + originalFilename: '/documents/other1.pdf', + type: 'application/pdf', + option: 'OTHER_DOCUMENT', + }, + { + path: '/documents/guardian.pdf', + originalFilename: '/documents/guardian.pdf', + type: 'application/pdf', + option: 'GUARDIAN_PROOF', + }, + ]) + } + ) +}) From 06bec8b8f051283c707d69a8e3e799e899e5c2ca Mon Sep 17 00:00:00 2001 From: Barry Dwyer Date: Tue, 2 Dec 2025 09:21:16 +0200 Subject: [PATCH 04/15] Add death resolver tests --- .../tests/unit/deathResolver.test.ts | 1038 +++++++++++++++++ 1 file changed, 1038 insertions(+) create mode 100644 v1-to-v2-data-migration/tests/unit/deathResolver.test.ts diff --git a/v1-to-v2-data-migration/tests/unit/deathResolver.test.ts b/v1-to-v2-data-migration/tests/unit/deathResolver.test.ts new file mode 100644 index 0000000..9dbc79b --- /dev/null +++ b/v1-to-v2-data-migration/tests/unit/deathResolver.test.ts @@ -0,0 +1,1038 @@ +import { assertEquals } from 'jsr:@std/assert' +import { transform } from '../../helpers/transform.ts' +import defaultResolvers, { + defaultDeathResolver, +} from '../../helpers/defaultResolvers.ts' +import { countryResolver } from '../../countryData/countryResolvers.ts' +import type { EventRegistration } from '../../helpers/types.ts' + +// Construct deathResolver as in migrate.ipynb +const allResolvers = { ...defaultResolvers, ...countryResolver } +const deathResolver = { ...defaultDeathResolver, ...allResolvers } + +function buildEventRegistration( + overrides: Partial = {} +): EventRegistration { + return { + id: 'test-id', + deceased: { + id: 'deceased-id', + name: [{ use: 'en', firstNames: 'John', familyName: 'Doe' }], + gender: 'male', + birthDate: '1950-01-01', + identifier: [{ type: 'NATIONAL_ID', id: 'DEC123456' }], + nationality: ['FAR'], + maritalStatus: 'MARRIED', + address: [ + { + type: 'PRIMARY_ADDRESS', + line: ['123 Main St', 'Apt 4'], + district: 'District1', + state: 'State1', + country: 'FAR', + }, + ], + deceased: { + deathDate: '2024-01-01', + }, + }, + deathDate: '2024-01-01', + informant: { + id: 'informant-id', + relationship: 'SON', + name: [{ use: 'en', firstNames: 'Jane', familyName: 'Doe' }], + birthDate: '1980-01-01', + identifier: [{ type: 'NATIONAL_ID', id: 'INF789' }], + address: [ + { + type: 'PRIMARY_ADDRESS', + line: ['456 Oak Ave'], + district: 'District2', + state: 'State2', + country: 'FAR', + }, + ], + }, + spouse: { + id: 'spouse-id', + detailsExist: true, + name: [{ use: 'en', firstNames: 'Mary', familyName: 'Smith' }], + birthDate: '1952-01-01', + nationality: ['FAR'], + identifier: [{ type: 'NATIONAL_ID', id: 'SPO456' }], + address: [ + { + type: 'PRIMARY_ADDRESS', + line: ['789 Pine Rd'], + district: 'District3', + state: 'State3', + country: 'FAR', + }, + ], + }, + registration: { + trackingId: 'DW12345', + registrationNumber: 'REG123', + contactPhoneNumber: '+260987654321', + contactEmail: 'contact@example.com', + }, + history: [ + { + date: '2024-01-15T10:00:00.000Z', + regStatus: 'DECLARED', + user: { id: 'user-id', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office-id' }, + }, + ], + eventLocation: { + id: 'location-id', + type: 'HEALTH_FACILITY', + }, + causeOfDeathEstablished: 'true', + causeOfDeathMethod: 'PHYSICIAN', + mannerOfDeath: 'NATURAL_CAUSES', + questionnaire: [], + ...overrides, + } as EventRegistration +} + +Deno.test('deathResolver - deceased fields', async (t) => { + await t.step('should resolve deceased.name', () => { + const data = buildEventRegistration() + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['deceased.name'], { + firstname: 'John', + middleName: undefined, + surname: 'Doe', + }) + }) + + await t.step('should resolve deceased.gender', () => { + const data = buildEventRegistration() + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['deceased.gender'], 'male') + }) + + await t.step('should resolve deceased.dob', () => { + const data = buildEventRegistration() + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['deceased.dob'], '1950-01-01') + }) + + await t.step('should resolve deceased.dobUnknown', () => { + const data = buildEventRegistration({ + deceased: { + ...buildEventRegistration().deceased!, + exactDateOfBirthUnknown: true, + }, + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['deceased.dobUnknown'], true) + }) + + await t.step('should resolve deceased.age with asOfDateRef', () => { + const data = buildEventRegistration({ + deceased: { + ...buildEventRegistration().deceased!, + ageOfIndividualInYears: 74, + }, + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['deceased.age'], { + age: 74, + asOfDateRef: 'eventDetails.date', + }) + }) + + await t.step('should resolve deceased.nationality', () => { + const data = buildEventRegistration() + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['deceased.nationality'], 'FAR') + }) + + await t.step('should resolve deceased.idType from questionnaire', () => { + const data = buildEventRegistration({ + questionnaire: [ + { + fieldId: 'death.deceased.deceased-view-group.deceasedIdType', + value: 'NATIONAL_ID', + }, + ], + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['deceased.idType'], 'NATIONAL_ID') + }) + + await t.step('should resolve deceased.nid', () => { + const data = buildEventRegistration() + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['deceased.nid'], 'DEC123456') + }) + + await t.step('should resolve deceased.passport', () => { + const data = buildEventRegistration({ + deceased: { + ...buildEventRegistration().deceased!, + identifier: [{ type: 'PASSPORT', id: 'P123456' }], + }, + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['deceased.passport'], 'P123456') + }) + + await t.step('should resolve deceased.brn', () => { + const data = buildEventRegistration({ + deceased: { + ...buildEventRegistration().deceased!, + identifier: [{ type: 'BIRTH_REGISTRATION_NUMBER', id: 'B123456' }], + }, + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['deceased.brn'], 'B123456') + }) + + await t.step('should resolve deceased.maritalStatus', () => { + const data = buildEventRegistration() + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['deceased.maritalStatus'], + 'MARRIED' + ) + }) + + await t.step('should resolve deceased.numberOfDependants', () => { + const data = buildEventRegistration({ + questionnaire: [ + { + fieldId: 'death.deceased.deceased-view-group.numberOfDependants', + value: '3', + }, + ], + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['deceased.numberOfDependants'], 3) + }) + + await t.step('should resolve deceased.address', () => { + const data = buildEventRegistration() + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['deceased.address']?.country, 'FAR') + assertEquals( + declareAction?.declaration['deceased.address']?.administrativeArea, + 'District1' + ) + }) + + await t.step('should resolve deceased.verified from questionnaire', () => { + const data = buildEventRegistration({ + questionnaire: [ + { + fieldId: 'death.deceased.deceased-view-group.verified', + value: 'verified', + }, + ], + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['deceased.verified'], 'verified') + }) +}) + +Deno.test('deathResolver - eventDetails fields', async (t) => { + await t.step( + 'should resolve eventDetails.date from deceased.deceased.deathDate', + () => { + const data = buildEventRegistration() + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['eventDetails.date'], + '2024-01-01' + ) + } + ) + + await t.step( + 'should resolve eventDetails.date from deathDate fallback', + () => { + const data = buildEventRegistration({ + deceased: { + ...buildEventRegistration().deceased!, + deceased: undefined, + }, + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['eventDetails.date'], + '2024-01-01' + ) + } + ) + + await t.step('should resolve eventDetails.description', () => { + const data = buildEventRegistration({ + deathDescription: 'Natural causes', + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['eventDetails.description'], + 'Natural causes' + ) + }) + + await t.step('should resolve eventDetails.reasonForLateRegistration', () => { + const data = buildEventRegistration({ + questionnaire: [ + { + fieldId: + 'death.deathEvent.death-event-details.reasonForLateRegistration', + value: 'Travel delays', + }, + ], + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['eventDetails.reasonForLateRegistration'], + 'Travel delays' + ) + }) + + await t.step('should resolve eventDetails.causeOfDeathEstablished', () => { + const data = buildEventRegistration() + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['eventDetails.causeOfDeathEstablished'], + true + ) + }) + + await t.step('should resolve eventDetails.sourceCauseDeath', () => { + const data = buildEventRegistration() + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['eventDetails.sourceCauseDeath'], + 'PHYSICIAN' + ) + }) + + await t.step('should resolve eventDetails.mannerOfDeath', () => { + const data = buildEventRegistration() + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['eventDetails.mannerOfDeath'], + 'MANNER_NATURAL' + ) + }) + + await t.step('should map ACCIDENT to MANNER_ACCIDENT', () => { + const data = buildEventRegistration({ + mannerOfDeath: 'ACCIDENT', + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['eventDetails.mannerOfDeath'], + 'MANNER_ACCIDENT' + ) + }) + + await t.step('should resolve eventDetails.placeOfDeath', () => { + const data = buildEventRegistration() + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['eventDetails.placeOfDeath'], + 'HEALTH_FACILITY' + ) + }) + + await t.step( + 'should resolve eventDetails.deathLocation for HEALTH_FACILITY', + () => { + const data = buildEventRegistration() + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['eventDetails.deathLocation'], + 'location-id' + ) + } + ) + + await t.step( + 'should resolve eventDetails.deathLocationOther for OTHER', + () => { + const data = buildEventRegistration({ + eventLocation: { + id: 'other-location', + type: 'OTHER', + address: { + line: ['999 Death St'], + district: 'DistrictX', + state: 'StateX', + country: 'FAR', + }, + }, + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['eventDetails.deathLocationOther']?.country, + 'FAR' + ) + } + ) +}) + +Deno.test('deathResolver - informant fields', async (t) => { + await t.step( + 'should resolve informant.addressSameAs when addresses match', + () => { + const sharedAddress = { + type: 'PRIMARY_ADDRESS', + line: ['Same St'], + district: 'District1', + state: 'State1', + country: 'FAR', + } + const data = buildEventRegistration({ + deceased: { + ...buildEventRegistration().deceased!, + address: [sharedAddress], + }, + informant: { + ...buildEventRegistration().informant!, + address: [sharedAddress], + }, + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['informant.addressSameAs'], 'YES') + } + ) + + await t.step( + 'should resolve informant.addressSameAs when addresses differ', + () => { + const data = buildEventRegistration() + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['informant.addressSameAs'], 'NO') + } + ) + + await t.step('should resolve informant.idType from questionnaire', () => { + const data = buildEventRegistration({ + questionnaire: [ + { + fieldId: 'death.informant.informant-view-group.informantIdType', + value: 'PASSPORT', + }, + ], + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['informant.idType'], 'PASSPORT') + }) + + await t.step('should resolve informant.dob for non-special informant', () => { + const data = buildEventRegistration() + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['informant.dob'], '1980-01-01') + }) + + await t.step('should not resolve informant.dob for SPOUSE', () => { + const data = buildEventRegistration({ + informant: { + ...buildEventRegistration().informant!, + relationship: 'SPOUSE', + }, + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['informant.dob'], undefined) + }) + + await t.step( + 'should resolve informant.address for non-special informant', + () => { + const data = buildEventRegistration() + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['informant.address']?.country, + 'FAR' + ) + } + ) + + await t.step( + 'should resolve informant.phoneNo with country code stripped', + () => { + const data = buildEventRegistration() + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['informant.phoneNo'], + '0987654321' + ) + } + ) + + await t.step('should resolve informant.email', () => { + const data = buildEventRegistration() + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['informant.email'], + 'contact@example.com' + ) + }) + + await t.step('should resolve informant.relation', () => { + const data = buildEventRegistration() + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['informant.relation'], 'SON') + }) + + await t.step('should resolve informant.other.relation', () => { + const data = buildEventRegistration({ + informant: { + ...buildEventRegistration().informant!, + relationship: 'OTHER', + otherRelationship: 'Caregiver', + }, + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['informant.other.relation'], + 'Caregiver' + ) + }) + + await t.step( + 'should resolve informant.name for non-special informant', + () => { + const data = buildEventRegistration() + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['informant.name'], { + firstname: 'Jane', + middleName: undefined, + surname: 'Doe', + }) + } + ) + + await t.step('should resolve informant.dobUnknown', () => { + const data = buildEventRegistration({ + informant: { + ...buildEventRegistration().informant!, + birthDate: undefined, + exactDateOfBirthUnknown: true, + }, + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['informant.dobUnknown'], true) + }) + + await t.step('should resolve informant.age', () => { + const data = buildEventRegistration({ + informant: { + ...buildEventRegistration().informant!, + birthDate: undefined, + ageOfIndividualInYears: 44, + }, + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['informant.age'], { + age: 44, + asOfDateRef: 'eventDetails.date', + }) + }) + + await t.step('should resolve informant.nationality', () => { + const data = buildEventRegistration({ + informant: { + ...buildEventRegistration().informant!, + nationality: ['USA'], + }, + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['informant.nationality'], 'USA') + }) + + await t.step('should resolve informant.brn', () => { + const data = buildEventRegistration({ + informant: { + ...buildEventRegistration().informant!, + identifier: [{ type: 'BIRTH_REGISTRATION_NUMBER', id: 'BRN123' }], + }, + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['informant.brn'], 'BRN123') + }) + + await t.step('should resolve informant.nid', () => { + const data = buildEventRegistration() + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['informant.nid'], 'INF789') + }) + + await t.step('should resolve informant.passport', () => { + const data = buildEventRegistration({ + informant: { + ...buildEventRegistration().informant!, + identifier: [{ type: 'PASSPORT', id: 'P789' }], + }, + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['informant.passport'], 'P789') + }) + + await t.step('should resolve informant.verified from questionnaire', () => { + const data = buildEventRegistration({ + questionnaire: [ + { + fieldId: 'death.informant.informant-view-group.verified', + value: 'authenticated', + }, + ], + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['informant.verified'], + 'authenticated' + ) + }) +}) + +Deno.test('deathResolver - spouse fields', async (t) => { + await t.step('should resolve spouse.detailsNotAvailable when false', () => { + const data = buildEventRegistration() + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['spouse.detailsNotAvailable'], + false + ) + }) + + await t.step('should resolve spouse.detailsNotAvailable when true', () => { + const data = buildEventRegistration({ + spouse: { + ...buildEventRegistration().spouse!, + detailsExist: false, + }, + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['spouse.detailsNotAvailable'], true) + }) + + await t.step('should resolve spouse.reason', () => { + const data = buildEventRegistration({ + spouse: { + ...buildEventRegistration().spouse!, + reasonNotApplying: 'Divorced', + }, + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['spouse.reason'], 'Divorced') + }) + + await t.step('should resolve spouse.name', () => { + const data = buildEventRegistration() + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['spouse.name'], { + firstname: 'Mary', + middleName: undefined, + surname: 'Smith', + }) + }) + + await t.step('should resolve spouse.dob', () => { + const data = buildEventRegistration() + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['spouse.dob'], '1952-01-01') + }) + + await t.step('should resolve spouse.dobUnknown', () => { + const data = buildEventRegistration({ + spouse: { + ...buildEventRegistration().spouse!, + exactDateOfBirthUnknown: true, + }, + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['spouse.dobUnknown'], true) + }) + + await t.step('should resolve spouse.age', () => { + const data = buildEventRegistration({ + spouse: { + ...buildEventRegistration().spouse!, + ageOfIndividualInYears: 72, + }, + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['spouse.age'], { + age: 72, + asOfDateRef: 'eventDetails.date', + }) + }) + + await t.step('should resolve spouse.nationality', () => { + const data = buildEventRegistration() + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['spouse.nationality'], 'FAR') + }) + + await t.step('should resolve spouse.idType from questionnaire', () => { + const data = buildEventRegistration({ + questionnaire: [ + { + fieldId: 'death.spouse.spouse-view-group.spouseIdType', + value: 'NATIONAL_ID', + }, + ], + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['spouse.idType'], 'NATIONAL_ID') + }) + + await t.step('should resolve spouse.nid', () => { + const data = buildEventRegistration() + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['spouse.nid'], 'SPO456') + }) + + await t.step('should resolve spouse.passport', () => { + const data = buildEventRegistration({ + spouse: { + ...buildEventRegistration().spouse!, + identifier: [{ type: 'PASSPORT', id: 'P456' }], + }, + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['spouse.passport'], 'P456') + }) + + await t.step('should resolve spouse.brn', () => { + const data = buildEventRegistration({ + spouse: { + ...buildEventRegistration().spouse!, + identifier: [{ type: 'BIRTH_REGISTRATION_NUMBER', id: 'B456' }], + }, + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['spouse.brn'], 'B456') + }) + + await t.step('should resolve spouse.address', () => { + const data = buildEventRegistration() + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['spouse.address']?.country, 'FAR') + }) + + await t.step( + 'should resolve spouse.addressSameAs when addresses match', + () => { + const sharedAddress = { + type: 'PRIMARY_ADDRESS', + line: ['Same St'], + district: 'District1', + state: 'State1', + country: 'FAR', + } + const data = buildEventRegistration({ + deceased: { + ...buildEventRegistration().deceased!, + address: [sharedAddress], + }, + spouse: { + ...buildEventRegistration().spouse!, + address: [sharedAddress], + }, + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['spouse.addressSameAs'], 'YES') + } + ) + + await t.step( + 'should resolve spouse.addressSameAs when addresses differ', + () => { + const data = buildEventRegistration() + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['spouse.addressSameAs'], 'NO') + } + ) + + await t.step('should resolve spouse.verified from questionnaire', () => { + const data = buildEventRegistration({ + questionnaire: [ + { + fieldId: 'death.spouse.spouse-view-group.verified', + value: 'verified', + }, + ], + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['spouse.verified'], 'verified') + }) +}) + +Deno.test('deathResolver - documents fields', async (t) => { + await t.step('should resolve documents.proofOfDeceased', () => { + const data = buildEventRegistration({ + registration: { + trackingId: 'DW12345', + attachments: [ + { + uri: '/path/to/doc1.pdf', + contentType: 'application/pdf', + type: 'DECEASED_ID_PROOF', + subject: 'DECEASED_ID_PROOF', + }, + ], + }, + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['documents.proofOfDeceased'], [ + { + path: '/path/to/doc1.pdf', + originalFilename: '/path/to/doc1.pdf', + type: 'application/pdf', + option: 'DECEASED_ID_PROOF', + }, + ]) + }) + + await t.step('should resolve documents.proofOfDeath', () => { + const data = buildEventRegistration({ + registration: { + trackingId: 'DW12345', + attachments: [ + { + uri: '/path/to/doc2.pdf', + contentType: 'application/pdf', + type: 'DECEASED_DEATH_PROOF', + subject: 'DECEASED_DEATH_PROOF', + }, + ], + }, + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['documents.proofOfDeath'], [ + { + path: '/path/to/doc2.pdf', + originalFilename: '/path/to/doc2.pdf', + type: 'application/pdf', + option: 'DECEASED_DEATH_PROOF', + }, + ]) + }) + + await t.step('should resolve documents.proofOfCauseOfDeath', () => { + const data = buildEventRegistration({ + registration: { + trackingId: 'DW12345', + attachments: [ + { + uri: '/path/to/doc3.pdf', + contentType: 'application/pdf', + type: 'DECEASED_DEATH_CAUSE_PROOF', + subject: 'DECEASED_DEATH_CAUSE_PROOF', + }, + ], + }, + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['documents.proofOfCauseOfDeath'], [ + { + path: '/path/to/doc3.pdf', + originalFilename: '/path/to/doc3.pdf', + type: 'application/pdf', + option: 'DECEASED_DEATH_CAUSE_PROOF', + }, + ]) + }) + + await t.step('should resolve documents.proofOfInformant', () => { + const data = buildEventRegistration({ + registration: { + trackingId: 'DW12345', + attachments: [ + { + uri: '/path/to/doc4.pdf', + contentType: 'application/pdf', + type: 'INFORMANT_ID_PROOF', + subject: 'INFORMANT_ID_PROOF', + }, + ], + }, + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['documents.proofOfInformant'], [ + { + path: '/path/to/doc4.pdf', + originalFilename: '/path/to/doc4.pdf', + type: 'application/pdf', + option: 'INFORMANT_ID_PROOF', + }, + ]) + }) + + await t.step( + 'should resolve documents.proofOther combining OTHER and LEGAL_GUARDIAN_PROOF', + () => { + const data = buildEventRegistration({ + registration: { + trackingId: 'DW12345', + attachments: [ + { + uri: '/path/to/doc5.pdf', + contentType: 'application/pdf', + type: 'OTHER', + subject: 'OTHER', + }, + { + uri: '/path/to/doc6.pdf', + contentType: 'application/pdf', + type: 'LEGAL_GUARDIAN_PROOF', + subject: 'LEGAL_GUARDIAN_PROOF', + }, + ], + }, + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['documents.proofOther'], [ + { + path: '/path/to/doc5.pdf', + originalFilename: '/path/to/doc5.pdf', + type: 'application/pdf', + option: 'OTHER', + }, + { + path: '/path/to/doc6.pdf', + originalFilename: '/path/to/doc6.pdf', + type: 'application/pdf', + option: 'LEGAL_GUARDIAN_PROOF', + }, + ]) + } + ) +}) From 13504a5c34cafb84aafd7c4c7a397409a2a18431 Mon Sep 17 00:00:00 2001 From: Barry Dwyer Date: Tue, 2 Dec 2025 09:38:51 +0200 Subject: [PATCH 05/15] Refactor out test utils --- v1-to-v2-data-migration/tests/deno.lock | 3 +- .../tests/unit/actionMapping.test.ts | 29 +- .../tests/unit/birthResolver.test.ts | 174 +++++------ .../tests/unit/deathResolver.test.ts | 270 ++++++------------ .../tests/utils/data-generators.ts | 2 +- .../utils/{utils.ts => generatorUtils.ts} | 0 .../tests/utils/test-helpers.ts | 169 +++++++++++ 7 files changed, 341 insertions(+), 306 deletions(-) rename v1-to-v2-data-migration/tests/utils/{utils.ts => generatorUtils.ts} (100%) create mode 100644 v1-to-v2-data-migration/tests/utils/test-helpers.ts diff --git a/v1-to-v2-data-migration/tests/deno.lock b/v1-to-v2-data-migration/tests/deno.lock index 634ba59..ddb71a7 100644 --- a/v1-to-v2-data-migration/tests/deno.lock +++ b/v1-to-v2-data-migration/tests/deno.lock @@ -1,6 +1,7 @@ { "version": "5", "specifiers": { + "jsr:@std/assert@*": "1.0.13", "jsr:@std/assert@^1.0.13": "1.0.13", "jsr:@std/expect@*": "1.0.16", "jsr:@std/internal@^1.0.6": "1.0.9", @@ -29,7 +30,7 @@ "@std/expect@1.0.16": { "integrity": "ceeef6dda21f256a5f0f083fcc0eaca175428b523359a9b1d9b3a1df11cc7391", "dependencies": [ - "jsr:@std/assert", + "jsr:@std/assert@^1.0.13", "jsr:@std/internal@^1.0.7" ] }, diff --git a/v1-to-v2-data-migration/tests/unit/actionMapping.test.ts b/v1-to-v2-data-migration/tests/unit/actionMapping.test.ts index dabc2d4..5cea9fb 100644 --- a/v1-to-v2-data-migration/tests/unit/actionMapping.test.ts +++ b/v1-to-v2-data-migration/tests/unit/actionMapping.test.ts @@ -1,30 +1,9 @@ import { transform } from '../../helpers/transform.ts' import { assertEquals } from 'https://deno.land/std@0.210.0/assert/mod.ts' -import type { EventRegistration, HistoryItem } from '../../helpers/types.ts' - -// Builders -function buildHistoryItem(overrides: Partial = {}): HistoryItem { - return { - date: '2023-10-01T12:00:00Z', - user: { id: 'user-123', role: { id: 'FIELD_AGENT' } }, - office: { id: 'office-456' }, - ...overrides, - } -} - -function buildEventRegistration( - overrides: Partial = {} -): EventRegistration { - return { - id: 'event-123', - registration: { - trackingId: 'TRACK123', - registrationNumber: 'REG123', - }, - history: [], - ...overrides, - } -} +import { + buildHistoryItem, + buildSimpleEventRegistration as buildEventRegistration, +} from '../utils/test-helpers.ts' Deno.test('transform - basic action type mappings', async (t) => { await t.step( diff --git a/v1-to-v2-data-migration/tests/unit/birthResolver.test.ts b/v1-to-v2-data-migration/tests/unit/birthResolver.test.ts index 185a14c..ae0cafe 100644 --- a/v1-to-v2-data-migration/tests/unit/birthResolver.test.ts +++ b/v1-to-v2-data-migration/tests/unit/birthResolver.test.ts @@ -1,42 +1,16 @@ import { transform } from '../../helpers/transform.ts' import { assertEquals } from 'https://deno.land/std@0.210.0/assert/mod.ts' -import defaultResolvers, { - defaultBirthResolver, -} from '../../helpers/defaultResolvers.ts' -import { countryResolver } from '../../countryData/countryResolvers.ts' -import { EventRegistration } from '../../helpers/types.ts' +import { + buildBirthResolver, + buildBirthEventRegistration, +} from '../utils/test-helpers.ts' // Construct birthResolver as in migrate.ipynb -const allResolvers = { ...defaultResolvers, ...countryResolver } -const birthResolver = { ...defaultBirthResolver, ...allResolvers } - -function buildEventRegistration( - overrides?: Partial -): EventRegistration { - return { - id: '123', - registration: { - trackingId: 'B123456', - registrationNumber: '2024B123456', - contactPhoneNumber: '+2600987654321', - contactEmail: 'test@example.com', - informantsSignature: 'data:image/png;base64,abc123', - }, - history: [ - { - date: '2024-01-01T10:00:00Z', - regStatus: 'DECLARED', - user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, - office: { id: 'office1' }, - }, - ], - ...overrides, - } -} +const birthResolver = buildBirthResolver() Deno.test('birthResolver - child fields', async (t) => { await t.step('should resolve child.name fields', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ child: { name: [ { @@ -59,7 +33,7 @@ Deno.test('birthResolver - child fields', async (t) => { }) await t.step('should resolve child.gender', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ child: { gender: 'male' }, }) @@ -70,7 +44,7 @@ Deno.test('birthResolver - child fields', async (t) => { }) await t.step('should resolve child.dob', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ child: { birthDate: '2024-01-15' }, }) @@ -81,7 +55,7 @@ Deno.test('birthResolver - child fields', async (t) => { }) await t.step('should resolve child.placeOfBirth', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ eventLocation: { type: 'HEALTH_FACILITY', id: 'facility1' }, }) @@ -95,7 +69,7 @@ Deno.test('birthResolver - child fields', async (t) => { }) await t.step('should resolve child.birthLocation for HEALTH_FACILITY', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ eventLocation: { type: 'HEALTH_FACILITY', id: 'facility1' }, }) @@ -108,7 +82,7 @@ Deno.test('birthResolver - child fields', async (t) => { await t.step( 'should resolve child.birthLocation.privateHome for PRIVATE_HOME', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ eventLocation: { type: 'PRIVATE_HOME', address: { @@ -138,7 +112,7 @@ Deno.test('birthResolver - child fields', async (t) => { ) await t.step('should resolve child.birthLocation.other for OTHER', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ eventLocation: { type: 'OTHER', address: { @@ -163,7 +137,7 @@ Deno.test('birthResolver - child fields', async (t) => { }) await t.step('should resolve child.attendantAtBirth', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ attendantAtBirth: 'PHYSICIAN', }) @@ -177,7 +151,7 @@ Deno.test('birthResolver - child fields', async (t) => { }) await t.step('should resolve child.birthType', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ birthType: 'TWIN', }) @@ -188,7 +162,7 @@ Deno.test('birthResolver - child fields', async (t) => { }) await t.step('should resolve child.weightAtBirth', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ weightAtBirth: 3.5, }) @@ -199,7 +173,7 @@ Deno.test('birthResolver - child fields', async (t) => { }) await t.step('should resolve child.reason from questionnaire', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ questionnaire: [ { fieldId: 'birth.child.child-view-group.reasonForLateRegistration', @@ -218,7 +192,7 @@ Deno.test('birthResolver - child fields', async (t) => { }) await t.step('should resolve child.nid', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ child: { identifier: [{ id: '1234567890', type: 'NATIONAL_ID' }], }, @@ -233,7 +207,7 @@ Deno.test('birthResolver - child fields', async (t) => { Deno.test('birthResolver - mother fields', async (t) => { await t.step('should resolve mother.detailsNotAvailable when false', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ mother: { detailsExist: true }, }) @@ -247,7 +221,7 @@ Deno.test('birthResolver - mother fields', async (t) => { }) await t.step('should resolve mother.detailsNotAvailable when true', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ mother: { detailsExist: false }, }) @@ -258,7 +232,7 @@ Deno.test('birthResolver - mother fields', async (t) => { }) await t.step('should resolve mother.reason', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ mother: { reasonNotApplying: 'Mother unknown' }, }) @@ -269,7 +243,7 @@ Deno.test('birthResolver - mother fields', async (t) => { }) await t.step('should resolve mother.name', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ mother: { name: [ { @@ -292,7 +266,7 @@ Deno.test('birthResolver - mother fields', async (t) => { }) await t.step('should resolve mother.dob', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ mother: { birthDate: '1990-05-15' }, }) @@ -303,7 +277,7 @@ Deno.test('birthResolver - mother fields', async (t) => { }) await t.step('should resolve mother.dobUnknown', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ mother: { exactDateOfBirthUnknown: true }, }) @@ -314,7 +288,7 @@ Deno.test('birthResolver - mother fields', async (t) => { }) await t.step('should resolve mother.age with asOfDateRef', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ mother: { ageOfIndividualInYears: 30 }, child: { birthDate: '2024-01-15' }, }) @@ -329,7 +303,7 @@ Deno.test('birthResolver - mother fields', async (t) => { }) await t.step('should resolve mother.nationality', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ mother: { nationality: ['FAR'] }, }) @@ -340,7 +314,7 @@ Deno.test('birthResolver - mother fields', async (t) => { }) await t.step('should resolve mother.maritalStatus', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ mother: { maritalStatus: 'MARRIED' }, }) @@ -351,7 +325,7 @@ Deno.test('birthResolver - mother fields', async (t) => { }) await t.step('should resolve mother.educationalAttainment', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ mother: { educationalAttainment: 'ISCED_4' }, }) @@ -365,7 +339,7 @@ Deno.test('birthResolver - mother fields', async (t) => { }) await t.step('should resolve mother.occupation', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ mother: { occupation: 'Teacher' }, }) @@ -376,7 +350,7 @@ Deno.test('birthResolver - mother fields', async (t) => { }) await t.step('should resolve mother.previousBirths', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ mother: { multipleBirth: 2 }, }) @@ -387,7 +361,7 @@ Deno.test('birthResolver - mother fields', async (t) => { }) await t.step('should resolve mother.address', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ mother: { address: [ { @@ -414,7 +388,7 @@ Deno.test('birthResolver - mother fields', async (t) => { }) await t.step('should resolve mother.brn', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ mother: { identifier: [{ id: 'B2020123456', type: 'BIRTH_REGISTRATION_NUMBER' }], }, @@ -427,7 +401,7 @@ Deno.test('birthResolver - mother fields', async (t) => { }) await t.step('should resolve mother.nid', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ mother: { identifier: [{ id: '9876543210', type: 'NATIONAL_ID' }], }, @@ -440,7 +414,7 @@ Deno.test('birthResolver - mother fields', async (t) => { }) await t.step('should resolve mother.passport', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ mother: { identifier: [{ id: 'P123456', type: 'PASSPORT' }], }, @@ -453,7 +427,7 @@ Deno.test('birthResolver - mother fields', async (t) => { }) await t.step('should resolve mother.idType from questionnaire', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ questionnaire: [ { fieldId: 'birth.mother.mother-view-group.motherIdType', @@ -469,7 +443,7 @@ Deno.test('birthResolver - mother fields', async (t) => { }) await t.step('should resolve mother.verified from questionnaire', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ questionnaire: [ { fieldId: 'birth.mother.mother-view-group.verified', @@ -487,7 +461,7 @@ Deno.test('birthResolver - mother fields', async (t) => { Deno.test('birthResolver - father fields', async (t) => { await t.step('should resolve father.detailsNotAvailable', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ father: { detailsExist: false }, }) @@ -498,7 +472,7 @@ Deno.test('birthResolver - father fields', async (t) => { }) await t.step('should resolve father.reason', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ father: { reasonNotApplying: 'Father unknown' }, }) @@ -509,7 +483,7 @@ Deno.test('birthResolver - father fields', async (t) => { }) await t.step('should resolve father.name', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ father: { name: [ { @@ -531,7 +505,7 @@ Deno.test('birthResolver - father fields', async (t) => { }) await t.step('should resolve father.dob', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ father: { birthDate: '1988-03-20' }, }) @@ -542,7 +516,7 @@ Deno.test('birthResolver - father fields', async (t) => { }) await t.step('should resolve father.dobUnknown', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ father: { exactDateOfBirthUnknown: true }, }) @@ -553,7 +527,7 @@ Deno.test('birthResolver - father fields', async (t) => { }) await t.step('should resolve father.age', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ father: { ageOfIndividualInYears: 35 }, child: { birthDate: '2024-01-15' }, }) @@ -568,7 +542,7 @@ Deno.test('birthResolver - father fields', async (t) => { }) await t.step('should resolve father.nationality', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ father: { nationality: ['USA'] }, }) @@ -579,7 +553,7 @@ Deno.test('birthResolver - father fields', async (t) => { }) await t.step('should resolve father.maritalStatus', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ father: { maritalStatus: 'MARRIED' }, }) @@ -590,7 +564,7 @@ Deno.test('birthResolver - father fields', async (t) => { }) await t.step('should resolve father.educationalAttainment', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ father: { educationalAttainment: 'ISCED_5' }, }) @@ -604,7 +578,7 @@ Deno.test('birthResolver - father fields', async (t) => { }) await t.step('should resolve father.occupation', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ father: { occupation: 'Engineer' }, }) @@ -615,7 +589,7 @@ Deno.test('birthResolver - father fields', async (t) => { }) await t.step('should resolve father.address', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ father: { address: [ { @@ -654,7 +628,7 @@ Deno.test('birthResolver - father fields', async (t) => { postalCode: '11111', } - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ mother: { address: [sameAddress] }, father: { address: [sameAddress] }, }) @@ -669,7 +643,7 @@ Deno.test('birthResolver - father fields', async (t) => { await t.step( 'should resolve father.addressSameAs when addresses differ', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ mother: { address: [ { @@ -700,7 +674,7 @@ Deno.test('birthResolver - father fields', async (t) => { ) await t.step('should resolve father.brn', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ father: { identifier: [{ id: 'B2018654321', type: 'BIRTH_REGISTRATION_NUMBER' }], }, @@ -713,7 +687,7 @@ Deno.test('birthResolver - father fields', async (t) => { }) await t.step('should resolve father.nid', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ father: { identifier: [{ id: '1122334455', type: 'NATIONAL_ID' }], }, @@ -726,7 +700,7 @@ Deno.test('birthResolver - father fields', async (t) => { }) await t.step('should resolve father.passport', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ father: { identifier: [{ id: 'P654321', type: 'PASSPORT' }], }, @@ -739,7 +713,7 @@ Deno.test('birthResolver - father fields', async (t) => { }) await t.step('should resolve father.idType from questionnaire', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ questionnaire: [ { fieldId: 'birth.father.father-view-group.fatherIdType', @@ -755,7 +729,7 @@ Deno.test('birthResolver - father fields', async (t) => { }) await t.step('should resolve father.verified from questionnaire', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ questionnaire: [ { fieldId: 'birth.father.father-view-group.verified', @@ -773,7 +747,7 @@ Deno.test('birthResolver - father fields', async (t) => { Deno.test('birthResolver - informant fields', async (t) => { await t.step('should resolve informant.dob for non-special informant', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ informant: { birthDate: '1995-08-10', relationship: 'BROTHER', @@ -787,7 +761,7 @@ Deno.test('birthResolver - informant fields', async (t) => { }) await t.step('should not resolve informant.dob for MOTHER', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ informant: { birthDate: '1995-08-10', relationship: 'MOTHER', @@ -803,7 +777,7 @@ Deno.test('birthResolver - informant fields', async (t) => { await t.step( 'should resolve informant.address for non-special informant', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ informant: { relationship: 'GRANDFATHER', address: [ @@ -833,7 +807,7 @@ Deno.test('birthResolver - informant fields', async (t) => { await t.step( 'should resolve informant.phoneNo with country code stripped', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ registration: { trackingId: 'B123456', contactPhoneNumber: '+260987654321', @@ -851,7 +825,7 @@ Deno.test('birthResolver - informant fields', async (t) => { ) await t.step('should resolve informant.email', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ registration: { trackingId: 'B123456', contactEmail: 'informant@test.com', @@ -868,7 +842,7 @@ Deno.test('birthResolver - informant fields', async (t) => { }) await t.step('should resolve informant.relation', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ informant: { relationship: 'SISTER' }, }) @@ -879,7 +853,7 @@ Deno.test('birthResolver - informant fields', async (t) => { }) await t.step('should resolve informant.other.relation', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ informant: { otherRelationship: 'Cousin' }, }) @@ -895,7 +869,7 @@ Deno.test('birthResolver - informant fields', async (t) => { await t.step( 'should resolve informant.name for non-special informant', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ informant: { relationship: 'LEGAL_GUARDIAN', name: [ @@ -919,7 +893,7 @@ Deno.test('birthResolver - informant fields', async (t) => { ) await t.step('should resolve informant.dobUnknown', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ informant: { relationship: 'GRANDFATHER', exactDateOfBirthUnknown: true, @@ -933,7 +907,7 @@ Deno.test('birthResolver - informant fields', async (t) => { }) await t.step('should resolve informant.age', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ informant: { relationship: 'OTHER', ageOfIndividualInYears: 45, @@ -951,7 +925,7 @@ Deno.test('birthResolver - informant fields', async (t) => { }) await t.step('should resolve informant.nationality', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ informant: { relationship: 'BROTHER', nationality: ['GBR'], @@ -965,7 +939,7 @@ Deno.test('birthResolver - informant fields', async (t) => { }) await t.step('should resolve informant.brn', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ informant: { relationship: 'SISTER', identifier: [{ id: 'B2015987654', type: 'BIRTH_REGISTRATION_NUMBER' }], @@ -979,7 +953,7 @@ Deno.test('birthResolver - informant fields', async (t) => { }) await t.step('should resolve informant.nid', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ informant: { relationship: 'GRANDFATHER', identifier: [{ id: '5566778899', type: 'NATIONAL_ID' }], @@ -993,7 +967,7 @@ Deno.test('birthResolver - informant fields', async (t) => { }) await t.step('should resolve informant.passport', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ informant: { relationship: 'OTHER', identifier: [{ id: 'P999888', type: 'PASSPORT' }], @@ -1007,7 +981,7 @@ Deno.test('birthResolver - informant fields', async (t) => { }) await t.step('should resolve informant.idType from questionnaire', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ questionnaire: [ { fieldId: 'birth.informant.informant-view-group.informantIdType', @@ -1026,7 +1000,7 @@ Deno.test('birthResolver - informant fields', async (t) => { }) await t.step('should resolve informant.verified from questionnaire', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ questionnaire: [ { fieldId: 'birth.informant.informant-view-group.verified', @@ -1047,7 +1021,7 @@ Deno.test('birthResolver - informant fields', async (t) => { Deno.test('birthResolver - documents fields', async (t) => { await t.step('should resolve documents.proofOfBirth', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ registration: { trackingId: 'B123456', attachments: [ @@ -1072,7 +1046,7 @@ Deno.test('birthResolver - documents fields', async (t) => { }) await t.step('should resolve documents.proofOfMother', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ registration: { trackingId: 'B123456', attachments: [ @@ -1100,7 +1074,7 @@ Deno.test('birthResolver - documents fields', async (t) => { }) await t.step('should resolve documents.proofOfFather', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ registration: { trackingId: 'B123456', attachments: [ @@ -1128,7 +1102,7 @@ Deno.test('birthResolver - documents fields', async (t) => { }) await t.step('should resolve documents.proofOfInformant', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ registration: { trackingId: 'B123456', attachments: [ @@ -1158,7 +1132,7 @@ Deno.test('birthResolver - documents fields', async (t) => { await t.step( 'should resolve documents.proofOther combining OTHER and LEGAL_GUARDIAN_PROOF', () => { - const registration = buildEventRegistration({ + const registration = buildBirthEventRegistration({ registration: { trackingId: 'B123456', attachments: [ diff --git a/v1-to-v2-data-migration/tests/unit/deathResolver.test.ts b/v1-to-v2-data-migration/tests/unit/deathResolver.test.ts index 9dbc79b..8688d35 100644 --- a/v1-to-v2-data-migration/tests/unit/deathResolver.test.ts +++ b/v1-to-v2-data-migration/tests/unit/deathResolver.test.ts @@ -1,104 +1,16 @@ import { assertEquals } from 'jsr:@std/assert' import { transform } from '../../helpers/transform.ts' -import defaultResolvers, { - defaultDeathResolver, -} from '../../helpers/defaultResolvers.ts' -import { countryResolver } from '../../countryData/countryResolvers.ts' -import type { EventRegistration } from '../../helpers/types.ts' +import { + buildDeathResolver, + buildDeathEventRegistration, +} from '../utils/test-helpers.ts' // Construct deathResolver as in migrate.ipynb -const allResolvers = { ...defaultResolvers, ...countryResolver } -const deathResolver = { ...defaultDeathResolver, ...allResolvers } - -function buildEventRegistration( - overrides: Partial = {} -): EventRegistration { - return { - id: 'test-id', - deceased: { - id: 'deceased-id', - name: [{ use: 'en', firstNames: 'John', familyName: 'Doe' }], - gender: 'male', - birthDate: '1950-01-01', - identifier: [{ type: 'NATIONAL_ID', id: 'DEC123456' }], - nationality: ['FAR'], - maritalStatus: 'MARRIED', - address: [ - { - type: 'PRIMARY_ADDRESS', - line: ['123 Main St', 'Apt 4'], - district: 'District1', - state: 'State1', - country: 'FAR', - }, - ], - deceased: { - deathDate: '2024-01-01', - }, - }, - deathDate: '2024-01-01', - informant: { - id: 'informant-id', - relationship: 'SON', - name: [{ use: 'en', firstNames: 'Jane', familyName: 'Doe' }], - birthDate: '1980-01-01', - identifier: [{ type: 'NATIONAL_ID', id: 'INF789' }], - address: [ - { - type: 'PRIMARY_ADDRESS', - line: ['456 Oak Ave'], - district: 'District2', - state: 'State2', - country: 'FAR', - }, - ], - }, - spouse: { - id: 'spouse-id', - detailsExist: true, - name: [{ use: 'en', firstNames: 'Mary', familyName: 'Smith' }], - birthDate: '1952-01-01', - nationality: ['FAR'], - identifier: [{ type: 'NATIONAL_ID', id: 'SPO456' }], - address: [ - { - type: 'PRIMARY_ADDRESS', - line: ['789 Pine Rd'], - district: 'District3', - state: 'State3', - country: 'FAR', - }, - ], - }, - registration: { - trackingId: 'DW12345', - registrationNumber: 'REG123', - contactPhoneNumber: '+260987654321', - contactEmail: 'contact@example.com', - }, - history: [ - { - date: '2024-01-15T10:00:00.000Z', - regStatus: 'DECLARED', - user: { id: 'user-id', role: { id: 'FIELD_AGENT' } }, - office: { id: 'office-id' }, - }, - ], - eventLocation: { - id: 'location-id', - type: 'HEALTH_FACILITY', - }, - causeOfDeathEstablished: 'true', - causeOfDeathMethod: 'PHYSICIAN', - mannerOfDeath: 'NATURAL_CAUSES', - questionnaire: [], - ...overrides, - } as EventRegistration -} +const deathResolver = buildDeathResolver() Deno.test('deathResolver - deceased fields', async (t) => { await t.step('should resolve deceased.name', () => { - const data = buildEventRegistration() + const data = buildDeathEventRegistration() const result = transform(data, deathResolver, 'death') const declareAction = result.actions.find((a) => a.type === 'DECLARE') @@ -110,7 +22,7 @@ Deno.test('deathResolver - deceased fields', async (t) => { }) await t.step('should resolve deceased.gender', () => { - const data = buildEventRegistration() + const data = buildDeathEventRegistration() const result = transform(data, deathResolver, 'death') const declareAction = result.actions.find((a) => a.type === 'DECLARE') @@ -118,7 +30,7 @@ Deno.test('deathResolver - deceased fields', async (t) => { }) await t.step('should resolve deceased.dob', () => { - const data = buildEventRegistration() + const data = buildDeathEventRegistration() const result = transform(data, deathResolver, 'death') const declareAction = result.actions.find((a) => a.type === 'DECLARE') @@ -126,9 +38,9 @@ Deno.test('deathResolver - deceased fields', async (t) => { }) await t.step('should resolve deceased.dobUnknown', () => { - const data = buildEventRegistration({ + const data = buildDeathEventRegistration({ deceased: { - ...buildEventRegistration().deceased!, + ...buildDeathEventRegistration().deceased!, exactDateOfBirthUnknown: true, }, }) @@ -139,9 +51,9 @@ Deno.test('deathResolver - deceased fields', async (t) => { }) await t.step('should resolve deceased.age with asOfDateRef', () => { - const data = buildEventRegistration({ + const data = buildDeathEventRegistration({ deceased: { - ...buildEventRegistration().deceased!, + ...buildDeathEventRegistration().deceased!, ageOfIndividualInYears: 74, }, }) @@ -155,7 +67,7 @@ Deno.test('deathResolver - deceased fields', async (t) => { }) await t.step('should resolve deceased.nationality', () => { - const data = buildEventRegistration() + const data = buildDeathEventRegistration() const result = transform(data, deathResolver, 'death') const declareAction = result.actions.find((a) => a.type === 'DECLARE') @@ -163,7 +75,7 @@ Deno.test('deathResolver - deceased fields', async (t) => { }) await t.step('should resolve deceased.idType from questionnaire', () => { - const data = buildEventRegistration({ + const data = buildDeathEventRegistration({ questionnaire: [ { fieldId: 'death.deceased.deceased-view-group.deceasedIdType', @@ -178,7 +90,7 @@ Deno.test('deathResolver - deceased fields', async (t) => { }) await t.step('should resolve deceased.nid', () => { - const data = buildEventRegistration() + const data = buildDeathEventRegistration() const result = transform(data, deathResolver, 'death') const declareAction = result.actions.find((a) => a.type === 'DECLARE') @@ -186,9 +98,9 @@ Deno.test('deathResolver - deceased fields', async (t) => { }) await t.step('should resolve deceased.passport', () => { - const data = buildEventRegistration({ + const data = buildDeathEventRegistration({ deceased: { - ...buildEventRegistration().deceased!, + ...buildDeathEventRegistration().deceased!, identifier: [{ type: 'PASSPORT', id: 'P123456' }], }, }) @@ -199,9 +111,9 @@ Deno.test('deathResolver - deceased fields', async (t) => { }) await t.step('should resolve deceased.brn', () => { - const data = buildEventRegistration({ + const data = buildDeathEventRegistration({ deceased: { - ...buildEventRegistration().deceased!, + ...buildDeathEventRegistration().deceased!, identifier: [{ type: 'BIRTH_REGISTRATION_NUMBER', id: 'B123456' }], }, }) @@ -212,7 +124,7 @@ Deno.test('deathResolver - deceased fields', async (t) => { }) await t.step('should resolve deceased.maritalStatus', () => { - const data = buildEventRegistration() + const data = buildDeathEventRegistration() const result = transform(data, deathResolver, 'death') const declareAction = result.actions.find((a) => a.type === 'DECLARE') @@ -223,7 +135,7 @@ Deno.test('deathResolver - deceased fields', async (t) => { }) await t.step('should resolve deceased.numberOfDependants', () => { - const data = buildEventRegistration({ + const data = buildDeathEventRegistration({ questionnaire: [ { fieldId: 'death.deceased.deceased-view-group.numberOfDependants', @@ -238,7 +150,7 @@ Deno.test('deathResolver - deceased fields', async (t) => { }) await t.step('should resolve deceased.address', () => { - const data = buildEventRegistration() + const data = buildDeathEventRegistration() const result = transform(data, deathResolver, 'death') const declareAction = result.actions.find((a) => a.type === 'DECLARE') @@ -250,7 +162,7 @@ Deno.test('deathResolver - deceased fields', async (t) => { }) await t.step('should resolve deceased.verified from questionnaire', () => { - const data = buildEventRegistration({ + const data = buildDeathEventRegistration({ questionnaire: [ { fieldId: 'death.deceased.deceased-view-group.verified', @@ -269,7 +181,7 @@ Deno.test('deathResolver - eventDetails fields', async (t) => { await t.step( 'should resolve eventDetails.date from deceased.deceased.deathDate', () => { - const data = buildEventRegistration() + const data = buildDeathEventRegistration() const result = transform(data, deathResolver, 'death') const declareAction = result.actions.find((a) => a.type === 'DECLARE') @@ -283,9 +195,9 @@ Deno.test('deathResolver - eventDetails fields', async (t) => { await t.step( 'should resolve eventDetails.date from deathDate fallback', () => { - const data = buildEventRegistration({ + const data = buildDeathEventRegistration({ deceased: { - ...buildEventRegistration().deceased!, + ...buildDeathEventRegistration().deceased!, deceased: undefined, }, }) @@ -300,7 +212,7 @@ Deno.test('deathResolver - eventDetails fields', async (t) => { ) await t.step('should resolve eventDetails.description', () => { - const data = buildEventRegistration({ + const data = buildDeathEventRegistration({ deathDescription: 'Natural causes', }) const result = transform(data, deathResolver, 'death') @@ -313,7 +225,7 @@ Deno.test('deathResolver - eventDetails fields', async (t) => { }) await t.step('should resolve eventDetails.reasonForLateRegistration', () => { - const data = buildEventRegistration({ + const data = buildDeathEventRegistration({ questionnaire: [ { fieldId: @@ -332,7 +244,7 @@ Deno.test('deathResolver - eventDetails fields', async (t) => { }) await t.step('should resolve eventDetails.causeOfDeathEstablished', () => { - const data = buildEventRegistration() + const data = buildDeathEventRegistration() const result = transform(data, deathResolver, 'death') const declareAction = result.actions.find((a) => a.type === 'DECLARE') @@ -343,7 +255,7 @@ Deno.test('deathResolver - eventDetails fields', async (t) => { }) await t.step('should resolve eventDetails.sourceCauseDeath', () => { - const data = buildEventRegistration() + const data = buildDeathEventRegistration() const result = transform(data, deathResolver, 'death') const declareAction = result.actions.find((a) => a.type === 'DECLARE') @@ -354,7 +266,7 @@ Deno.test('deathResolver - eventDetails fields', async (t) => { }) await t.step('should resolve eventDetails.mannerOfDeath', () => { - const data = buildEventRegistration() + const data = buildDeathEventRegistration() const result = transform(data, deathResolver, 'death') const declareAction = result.actions.find((a) => a.type === 'DECLARE') @@ -365,7 +277,7 @@ Deno.test('deathResolver - eventDetails fields', async (t) => { }) await t.step('should map ACCIDENT to MANNER_ACCIDENT', () => { - const data = buildEventRegistration({ + const data = buildDeathEventRegistration({ mannerOfDeath: 'ACCIDENT', }) const result = transform(data, deathResolver, 'death') @@ -378,7 +290,7 @@ Deno.test('deathResolver - eventDetails fields', async (t) => { }) await t.step('should resolve eventDetails.placeOfDeath', () => { - const data = buildEventRegistration() + const data = buildDeathEventRegistration() const result = transform(data, deathResolver, 'death') const declareAction = result.actions.find((a) => a.type === 'DECLARE') @@ -391,7 +303,7 @@ Deno.test('deathResolver - eventDetails fields', async (t) => { await t.step( 'should resolve eventDetails.deathLocation for HEALTH_FACILITY', () => { - const data = buildEventRegistration() + const data = buildDeathEventRegistration() const result = transform(data, deathResolver, 'death') const declareAction = result.actions.find((a) => a.type === 'DECLARE') @@ -405,7 +317,7 @@ Deno.test('deathResolver - eventDetails fields', async (t) => { await t.step( 'should resolve eventDetails.deathLocationOther for OTHER', () => { - const data = buildEventRegistration({ + const data = buildDeathEventRegistration({ eventLocation: { id: 'other-location', type: 'OTHER', @@ -439,13 +351,13 @@ Deno.test('deathResolver - informant fields', async (t) => { state: 'State1', country: 'FAR', } - const data = buildEventRegistration({ + const data = buildDeathEventRegistration({ deceased: { - ...buildEventRegistration().deceased!, + ...buildDeathEventRegistration().deceased!, address: [sharedAddress], }, informant: { - ...buildEventRegistration().informant!, + ...buildDeathEventRegistration().informant!, address: [sharedAddress], }, }) @@ -459,7 +371,7 @@ Deno.test('deathResolver - informant fields', async (t) => { await t.step( 'should resolve informant.addressSameAs when addresses differ', () => { - const data = buildEventRegistration() + const data = buildDeathEventRegistration() const result = transform(data, deathResolver, 'death') const declareAction = result.actions.find((a) => a.type === 'DECLARE') @@ -468,7 +380,7 @@ Deno.test('deathResolver - informant fields', async (t) => { ) await t.step('should resolve informant.idType from questionnaire', () => { - const data = buildEventRegistration({ + const data = buildDeathEventRegistration({ questionnaire: [ { fieldId: 'death.informant.informant-view-group.informantIdType', @@ -483,7 +395,7 @@ Deno.test('deathResolver - informant fields', async (t) => { }) await t.step('should resolve informant.dob for non-special informant', () => { - const data = buildEventRegistration() + const data = buildDeathEventRegistration() const result = transform(data, deathResolver, 'death') const declareAction = result.actions.find((a) => a.type === 'DECLARE') @@ -491,9 +403,9 @@ Deno.test('deathResolver - informant fields', async (t) => { }) await t.step('should not resolve informant.dob for SPOUSE', () => { - const data = buildEventRegistration({ + const data = buildDeathEventRegistration({ informant: { - ...buildEventRegistration().informant!, + ...buildDeathEventRegistration().informant!, relationship: 'SPOUSE', }, }) @@ -506,7 +418,7 @@ Deno.test('deathResolver - informant fields', async (t) => { await t.step( 'should resolve informant.address for non-special informant', () => { - const data = buildEventRegistration() + const data = buildDeathEventRegistration() const result = transform(data, deathResolver, 'death') const declareAction = result.actions.find((a) => a.type === 'DECLARE') @@ -520,7 +432,7 @@ Deno.test('deathResolver - informant fields', async (t) => { await t.step( 'should resolve informant.phoneNo with country code stripped', () => { - const data = buildEventRegistration() + const data = buildDeathEventRegistration() const result = transform(data, deathResolver, 'death') const declareAction = result.actions.find((a) => a.type === 'DECLARE') @@ -532,7 +444,7 @@ Deno.test('deathResolver - informant fields', async (t) => { ) await t.step('should resolve informant.email', () => { - const data = buildEventRegistration() + const data = buildDeathEventRegistration() const result = transform(data, deathResolver, 'death') const declareAction = result.actions.find((a) => a.type === 'DECLARE') @@ -543,7 +455,7 @@ Deno.test('deathResolver - informant fields', async (t) => { }) await t.step('should resolve informant.relation', () => { - const data = buildEventRegistration() + const data = buildDeathEventRegistration() const result = transform(data, deathResolver, 'death') const declareAction = result.actions.find((a) => a.type === 'DECLARE') @@ -551,9 +463,9 @@ Deno.test('deathResolver - informant fields', async (t) => { }) await t.step('should resolve informant.other.relation', () => { - const data = buildEventRegistration({ + const data = buildDeathEventRegistration({ informant: { - ...buildEventRegistration().informant!, + ...buildDeathEventRegistration().informant!, relationship: 'OTHER', otherRelationship: 'Caregiver', }, @@ -570,7 +482,7 @@ Deno.test('deathResolver - informant fields', async (t) => { await t.step( 'should resolve informant.name for non-special informant', () => { - const data = buildEventRegistration() + const data = buildDeathEventRegistration() const result = transform(data, deathResolver, 'death') const declareAction = result.actions.find((a) => a.type === 'DECLARE') @@ -583,9 +495,9 @@ Deno.test('deathResolver - informant fields', async (t) => { ) await t.step('should resolve informant.dobUnknown', () => { - const data = buildEventRegistration({ + const data = buildDeathEventRegistration({ informant: { - ...buildEventRegistration().informant!, + ...buildDeathEventRegistration().informant!, birthDate: undefined, exactDateOfBirthUnknown: true, }, @@ -597,9 +509,9 @@ Deno.test('deathResolver - informant fields', async (t) => { }) await t.step('should resolve informant.age', () => { - const data = buildEventRegistration({ + const data = buildDeathEventRegistration({ informant: { - ...buildEventRegistration().informant!, + ...buildDeathEventRegistration().informant!, birthDate: undefined, ageOfIndividualInYears: 44, }, @@ -614,9 +526,9 @@ Deno.test('deathResolver - informant fields', async (t) => { }) await t.step('should resolve informant.nationality', () => { - const data = buildEventRegistration({ + const data = buildDeathEventRegistration({ informant: { - ...buildEventRegistration().informant!, + ...buildDeathEventRegistration().informant!, nationality: ['USA'], }, }) @@ -627,9 +539,9 @@ Deno.test('deathResolver - informant fields', async (t) => { }) await t.step('should resolve informant.brn', () => { - const data = buildEventRegistration({ + const data = buildDeathEventRegistration({ informant: { - ...buildEventRegistration().informant!, + ...buildDeathEventRegistration().informant!, identifier: [{ type: 'BIRTH_REGISTRATION_NUMBER', id: 'BRN123' }], }, }) @@ -640,7 +552,7 @@ Deno.test('deathResolver - informant fields', async (t) => { }) await t.step('should resolve informant.nid', () => { - const data = buildEventRegistration() + const data = buildDeathEventRegistration() const result = transform(data, deathResolver, 'death') const declareAction = result.actions.find((a) => a.type === 'DECLARE') @@ -648,9 +560,9 @@ Deno.test('deathResolver - informant fields', async (t) => { }) await t.step('should resolve informant.passport', () => { - const data = buildEventRegistration({ + const data = buildDeathEventRegistration({ informant: { - ...buildEventRegistration().informant!, + ...buildDeathEventRegistration().informant!, identifier: [{ type: 'PASSPORT', id: 'P789' }], }, }) @@ -661,7 +573,7 @@ Deno.test('deathResolver - informant fields', async (t) => { }) await t.step('should resolve informant.verified from questionnaire', () => { - const data = buildEventRegistration({ + const data = buildDeathEventRegistration({ questionnaire: [ { fieldId: 'death.informant.informant-view-group.verified', @@ -681,7 +593,7 @@ Deno.test('deathResolver - informant fields', async (t) => { Deno.test('deathResolver - spouse fields', async (t) => { await t.step('should resolve spouse.detailsNotAvailable when false', () => { - const data = buildEventRegistration() + const data = buildDeathEventRegistration() const result = transform(data, deathResolver, 'death') const declareAction = result.actions.find((a) => a.type === 'DECLARE') @@ -692,9 +604,9 @@ Deno.test('deathResolver - spouse fields', async (t) => { }) await t.step('should resolve spouse.detailsNotAvailable when true', () => { - const data = buildEventRegistration({ + const data = buildDeathEventRegistration({ spouse: { - ...buildEventRegistration().spouse!, + ...buildDeathEventRegistration().spouse!, detailsExist: false, }, }) @@ -705,9 +617,9 @@ Deno.test('deathResolver - spouse fields', async (t) => { }) await t.step('should resolve spouse.reason', () => { - const data = buildEventRegistration({ + const data = buildDeathEventRegistration({ spouse: { - ...buildEventRegistration().spouse!, + ...buildDeathEventRegistration().spouse!, reasonNotApplying: 'Divorced', }, }) @@ -718,7 +630,7 @@ Deno.test('deathResolver - spouse fields', async (t) => { }) await t.step('should resolve spouse.name', () => { - const data = buildEventRegistration() + const data = buildDeathEventRegistration() const result = transform(data, deathResolver, 'death') const declareAction = result.actions.find((a) => a.type === 'DECLARE') @@ -730,7 +642,7 @@ Deno.test('deathResolver - spouse fields', async (t) => { }) await t.step('should resolve spouse.dob', () => { - const data = buildEventRegistration() + const data = buildDeathEventRegistration() const result = transform(data, deathResolver, 'death') const declareAction = result.actions.find((a) => a.type === 'DECLARE') @@ -738,9 +650,9 @@ Deno.test('deathResolver - spouse fields', async (t) => { }) await t.step('should resolve spouse.dobUnknown', () => { - const data = buildEventRegistration({ + const data = buildDeathEventRegistration({ spouse: { - ...buildEventRegistration().spouse!, + ...buildDeathEventRegistration().spouse!, exactDateOfBirthUnknown: true, }, }) @@ -751,9 +663,9 @@ Deno.test('deathResolver - spouse fields', async (t) => { }) await t.step('should resolve spouse.age', () => { - const data = buildEventRegistration({ + const data = buildDeathEventRegistration({ spouse: { - ...buildEventRegistration().spouse!, + ...buildDeathEventRegistration().spouse!, ageOfIndividualInYears: 72, }, }) @@ -767,7 +679,7 @@ Deno.test('deathResolver - spouse fields', async (t) => { }) await t.step('should resolve spouse.nationality', () => { - const data = buildEventRegistration() + const data = buildDeathEventRegistration() const result = transform(data, deathResolver, 'death') const declareAction = result.actions.find((a) => a.type === 'DECLARE') @@ -775,7 +687,7 @@ Deno.test('deathResolver - spouse fields', async (t) => { }) await t.step('should resolve spouse.idType from questionnaire', () => { - const data = buildEventRegistration({ + const data = buildDeathEventRegistration({ questionnaire: [ { fieldId: 'death.spouse.spouse-view-group.spouseIdType', @@ -790,7 +702,7 @@ Deno.test('deathResolver - spouse fields', async (t) => { }) await t.step('should resolve spouse.nid', () => { - const data = buildEventRegistration() + const data = buildDeathEventRegistration() const result = transform(data, deathResolver, 'death') const declareAction = result.actions.find((a) => a.type === 'DECLARE') @@ -798,9 +710,9 @@ Deno.test('deathResolver - spouse fields', async (t) => { }) await t.step('should resolve spouse.passport', () => { - const data = buildEventRegistration({ + const data = buildDeathEventRegistration({ spouse: { - ...buildEventRegistration().spouse!, + ...buildDeathEventRegistration().spouse!, identifier: [{ type: 'PASSPORT', id: 'P456' }], }, }) @@ -811,9 +723,9 @@ Deno.test('deathResolver - spouse fields', async (t) => { }) await t.step('should resolve spouse.brn', () => { - const data = buildEventRegistration({ + const data = buildDeathEventRegistration({ spouse: { - ...buildEventRegistration().spouse!, + ...buildDeathEventRegistration().spouse!, identifier: [{ type: 'BIRTH_REGISTRATION_NUMBER', id: 'B456' }], }, }) @@ -824,7 +736,7 @@ Deno.test('deathResolver - spouse fields', async (t) => { }) await t.step('should resolve spouse.address', () => { - const data = buildEventRegistration() + const data = buildDeathEventRegistration() const result = transform(data, deathResolver, 'death') const declareAction = result.actions.find((a) => a.type === 'DECLARE') @@ -841,13 +753,13 @@ Deno.test('deathResolver - spouse fields', async (t) => { state: 'State1', country: 'FAR', } - const data = buildEventRegistration({ + const data = buildDeathEventRegistration({ deceased: { - ...buildEventRegistration().deceased!, + ...buildDeathEventRegistration().deceased!, address: [sharedAddress], }, spouse: { - ...buildEventRegistration().spouse!, + ...buildDeathEventRegistration().spouse!, address: [sharedAddress], }, }) @@ -861,7 +773,7 @@ Deno.test('deathResolver - spouse fields', async (t) => { await t.step( 'should resolve spouse.addressSameAs when addresses differ', () => { - const data = buildEventRegistration() + const data = buildDeathEventRegistration() const result = transform(data, deathResolver, 'death') const declareAction = result.actions.find((a) => a.type === 'DECLARE') @@ -870,7 +782,7 @@ Deno.test('deathResolver - spouse fields', async (t) => { ) await t.step('should resolve spouse.verified from questionnaire', () => { - const data = buildEventRegistration({ + const data = buildDeathEventRegistration({ questionnaire: [ { fieldId: 'death.spouse.spouse-view-group.verified', @@ -887,7 +799,7 @@ Deno.test('deathResolver - spouse fields', async (t) => { Deno.test('deathResolver - documents fields', async (t) => { await t.step('should resolve documents.proofOfDeceased', () => { - const data = buildEventRegistration({ + const data = buildDeathEventRegistration({ registration: { trackingId: 'DW12345', attachments: [ @@ -914,7 +826,7 @@ Deno.test('deathResolver - documents fields', async (t) => { }) await t.step('should resolve documents.proofOfDeath', () => { - const data = buildEventRegistration({ + const data = buildDeathEventRegistration({ registration: { trackingId: 'DW12345', attachments: [ @@ -941,7 +853,7 @@ Deno.test('deathResolver - documents fields', async (t) => { }) await t.step('should resolve documents.proofOfCauseOfDeath', () => { - const data = buildEventRegistration({ + const data = buildDeathEventRegistration({ registration: { trackingId: 'DW12345', attachments: [ @@ -968,7 +880,7 @@ Deno.test('deathResolver - documents fields', async (t) => { }) await t.step('should resolve documents.proofOfInformant', () => { - const data = buildEventRegistration({ + const data = buildDeathEventRegistration({ registration: { trackingId: 'DW12345', attachments: [ @@ -997,7 +909,7 @@ Deno.test('deathResolver - documents fields', async (t) => { await t.step( 'should resolve documents.proofOther combining OTHER and LEGAL_GUARDIAN_PROOF', () => { - const data = buildEventRegistration({ + const data = buildDeathEventRegistration({ registration: { trackingId: 'DW12345', attachments: [ diff --git a/v1-to-v2-data-migration/tests/utils/data-generators.ts b/v1-to-v2-data-migration/tests/utils/data-generators.ts index 4676aed..9d30d12 100644 --- a/v1-to-v2-data-migration/tests/utils/data-generators.ts +++ b/v1-to-v2-data-migration/tests/utils/data-generators.ts @@ -1,7 +1,7 @@ import { faker } from '@faker-js/faker' import { format, subDays, subYears } from 'date-fns' import { v4 as uuidv4 } from 'uuid' -import { getAllLocations } from '../utils.ts' +import { getAllLocations } from './generatorUtils.ts' import type { fhir } from 'fhir' // Types for generated data diff --git a/v1-to-v2-data-migration/tests/utils/utils.ts b/v1-to-v2-data-migration/tests/utils/generatorUtils.ts similarity index 100% rename from v1-to-v2-data-migration/tests/utils/utils.ts rename to v1-to-v2-data-migration/tests/utils/generatorUtils.ts diff --git a/v1-to-v2-data-migration/tests/utils/test-helpers.ts b/v1-to-v2-data-migration/tests/utils/test-helpers.ts new file mode 100644 index 0000000..01063e8 --- /dev/null +++ b/v1-to-v2-data-migration/tests/utils/test-helpers.ts @@ -0,0 +1,169 @@ +import defaultResolvers, { + defaultBirthResolver, + defaultDeathResolver, +} from '../../helpers/defaultResolvers.ts' +import { countryResolver } from '../../countryData/countryResolvers.ts' +import type { EventRegistration, HistoryItem } from '../../helpers/types.ts' + +/** + * Build a birth resolver with all default and country resolvers + */ +export function buildBirthResolver() { + const allResolvers = { ...defaultResolvers, ...countryResolver } + return { ...defaultBirthResolver, ...allResolvers } +} + +/** + * Build a death resolver with all default and country resolvers + */ +export function buildDeathResolver() { + const allResolvers = { ...defaultResolvers, ...countryResolver } + return { ...defaultDeathResolver, ...allResolvers } +} + +/** + * Build a basic EventRegistration for birth tests with sensible defaults + */ +export function buildBirthEventRegistration( + overrides?: Partial +): EventRegistration { + return { + id: '123', + registration: { + trackingId: 'B123456', + registrationNumber: '2024B123456', + contactPhoneNumber: '+2600987654321', + contactEmail: 'test@example.com', + informantsSignature: 'data:image/png;base64,abc123', + }, + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + ], + ...overrides, + } +} + +/** + * Build a basic EventRegistration for death tests with sensible defaults + */ +export function buildDeathEventRegistration( + overrides: Partial = {} +): EventRegistration { + return { + id: 'test-id', + deceased: { + id: 'deceased-id', + name: [{ use: 'en', firstNames: 'John', familyName: 'Doe' }], + gender: 'male', + birthDate: '1950-01-01', + identifier: [{ type: 'NATIONAL_ID', id: 'DEC123456' }], + nationality: ['FAR'], + maritalStatus: 'MARRIED', + address: [ + { + type: 'PRIMARY_ADDRESS', + line: ['123 Main St', 'Apt 4'], + district: 'District1', + state: 'State1', + country: 'FAR', + }, + ], + deceased: { + deathDate: '2024-01-01', + }, + }, + deathDate: '2024-01-01', + informant: { + id: 'informant-id', + relationship: 'SON', + name: [{ use: 'en', firstNames: 'Jane', familyName: 'Doe' }], + birthDate: '1980-01-01', + identifier: [{ type: 'NATIONAL_ID', id: 'INF789' }], + address: [ + { + type: 'PRIMARY_ADDRESS', + line: ['456 Oak Ave'], + district: 'District2', + state: 'State2', + country: 'FAR', + }, + ], + }, + spouse: { + id: 'spouse-id', + detailsExist: true, + name: [{ use: 'en', firstNames: 'Mary', familyName: 'Smith' }], + birthDate: '1952-01-01', + nationality: ['FAR'], + identifier: [{ type: 'NATIONAL_ID', id: 'SPO456' }], + address: [ + { + type: 'PRIMARY_ADDRESS', + line: ['789 Pine Rd'], + district: 'District3', + state: 'State3', + country: 'FAR', + }, + ], + }, + registration: { + trackingId: 'DW12345', + registrationNumber: 'REG123', + contactPhoneNumber: '+260987654321', + contactEmail: 'contact@example.com', + }, + history: [ + { + date: '2024-01-15T10:00:00.000Z', + regStatus: 'DECLARED', + user: { id: 'user-id', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office-id' }, + }, + ], + eventLocation: { + id: 'location-id', + type: 'HEALTH_FACILITY', + }, + causeOfDeathEstablished: 'true', + causeOfDeathMethod: 'PHYSICIAN', + mannerOfDeath: 'NATURAL_CAUSES', + questionnaire: [], + ...overrides, + } as EventRegistration +} + +/** + * Build a basic HistoryItem for action mapping tests with sensible defaults + */ +export function buildHistoryItem( + overrides: Partial = {} +): HistoryItem { + return { + date: '2023-10-01T12:00:00Z', + user: { id: 'user-123', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office-456' }, + ...overrides, + } +} + +/** + * Build a simple EventRegistration for action mapping tests + */ +export function buildSimpleEventRegistration( + overrides: Partial = {} +): EventRegistration { + return { + id: 'event-123', + registration: { + trackingId: 'TRACK123', + registrationNumber: 'REG123', + }, + history: [], + ...overrides, + } +} From c5476ee500d5ef6cf4042a11dcc8c90c9accfb5c Mon Sep 17 00:00:00 2001 From: Barry Dwyer Date: Tue, 2 Dec 2025 10:42:56 +0200 Subject: [PATCH 06/15] Add correction tests --- .../helpers/defaultMappings.ts | 5 +- .../tests/unit/corrections.test.ts | 1853 +++++++++++++++++ 2 files changed, 1855 insertions(+), 3 deletions(-) create mode 100644 v1-to-v2-data-migration/tests/unit/corrections.test.ts diff --git a/v1-to-v2-data-migration/helpers/defaultMappings.ts b/v1-to-v2-data-migration/helpers/defaultMappings.ts index 836507c..ba2f09d 100644 --- a/v1-to-v2-data-migration/helpers/defaultMappings.ts +++ b/v1-to-v2-data-migration/helpers/defaultMappings.ts @@ -89,7 +89,7 @@ export const DEFAULT_FIELD_MAPPINGS = { 'death.spouse.reasonNotApplying': 'spouse.reason', 'death.spouse.spouseBirthDate': 'spouse.dob', 'death.spouse.exactDateOfBirthUnknown': 'spouse.dobUnknown', - 'death.spouse.ageOfIndividualInYears': 'spouse.age', + // age mapping handled by AGE_MAPPINGS to transform to object with asOfDateRef 'death.spouse.nationality': 'spouse.nationality', 'death.spouse.spouseNationalId': 'spouse.nid', 'death.spouse.spousePassport': 'spouse.passport', @@ -122,7 +122,7 @@ export const CUSTOM_FIELD_MAPPINGS = { 'death.spouse.spouse-view-group.verified': 'spouse.verified', } -export const VERIFIED_MAPPINGS : Record< +export const VERIFIED_MAPPINGS: Record< string, (data: string) => Record > = { @@ -187,4 +187,3 @@ export const AGE_MAPPINGS: Record< }, }), } - diff --git a/v1-to-v2-data-migration/tests/unit/corrections.test.ts b/v1-to-v2-data-migration/tests/unit/corrections.test.ts new file mode 100644 index 0000000..4b10c16 --- /dev/null +++ b/v1-to-v2-data-migration/tests/unit/corrections.test.ts @@ -0,0 +1,1853 @@ +import { transform } from '../../helpers/transform.ts' +import { + buildBirthResolver, + buildDeathResolver, + buildBirthEventRegistration, + buildDeathEventRegistration, +} from '../utils/test-helpers.ts' +import { assertEquals } from 'https://deno.land/std@0.210.0/assert/mod.ts' +import type { EventRegistration, HistoryItem } from '../../helpers/types.ts' + +Deno.test('Corrections - Birth', async (t) => { + const birthResolver = buildBirthResolver() + + await t.step( + 'should transform scalar field corrections with input and output', + () => { + const registration = buildBirthEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'informant', + valueId: 'registrationPhone', + value: '0788888888', + }, + ], + output: [ + { + valueCode: 'informant', + valueId: 'registrationPhone', + value: '0799999999', + }, + ], + }, + ], + }) + + const result = transform(registration, birthResolver, 'birth') + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) + + assertEquals(correctionAction !== undefined, true) + assertEquals(correctionAction?.declaration, { + 'informant.phoneNo': '0799999999', + }) + assertEquals( + correctionAction?.annotation?.['informant.phoneNo'], + '0788888888' + ) + } + ) + + await t.step('should transform custom field corrections', () => { + const registration = buildBirthEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'informant', + valueId: 'informantIdType', + value: 'PASSPORT', + }, + { + valueCode: 'informant', + valueId: 'informantPassport', + value: 'OLD123', + }, + ], + output: [ + { + valueCode: 'informant', + valueId: 'informantIdType', + value: 'NATIONAL_ID', + }, + { + valueCode: 'informant', + valueId: 'informantNationalId', + value: 'NEW456', + }, + ], + }, + ], + }) + + const result = transform(registration, birthResolver, 'birth') + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) + + assertEquals(correctionAction?.declaration, { + 'informant.idType': 'NATIONAL_ID', + 'informant.nid': 'NEW456', + }) + assertEquals(correctionAction?.annotation?.['informant.idType'], 'PASSPORT') + assertEquals(correctionAction?.annotation?.['informant.passport'], 'OLD123') + }) + + await t.step('should transform name field corrections', () => { + const registration = buildBirthEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'child', + valueId: 'firstNamesEng', + value: 'OldFirstName', + }, + { + valueCode: 'child', + valueId: 'familyNameEng', + value: 'OldLastName', + }, + ], + output: [ + { + valueCode: 'child', + valueId: 'firstNamesEng', + value: 'NewFirstName', + }, + { + valueCode: 'child', + valueId: 'familyNameEng', + value: 'NewLastName', + }, + ], + }, + ], + }) + + const result = transform(registration, birthResolver, 'birth') + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) + + assertEquals(correctionAction?.declaration, { + 'child.name': { + firstname: 'NewFirstName', + surname: 'NewLastName', + }, + }) + assertEquals(correctionAction?.annotation?.['child.name'], { + firstname: 'OldFirstName', + surname: 'OldLastName', + }) + }) + + await t.step('should transform address field corrections', () => { + const registration = buildBirthEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'mother', + valueId: 'internationalStatePrimaryMother', + value: 'OldState', + }, + { + valueCode: 'mother', + valueId: 'internationalCityPrimaryMother', + value: 'OldCity', + }, + ], + output: [ + { + valueCode: 'mother', + valueId: 'internationalStatePrimaryMother', + value: 'NewState', + }, + { + valueCode: 'mother', + valueId: 'internationalCityPrimaryMother', + value: 'NewCity', + }, + { + valueCode: 'mother', + valueId: 'internationalAddressLine1PrimaryMother', + value: '123 Main St', + }, + ], + }, + ], + }) + + const result = transform(registration, birthResolver, 'birth') + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) + + assertEquals( + correctionAction?.declaration?.['mother.address']?.streetLevelDetails, + { + state: 'NewState', + cityOrTown: 'NewCity', + addressLine1: '123 Main St', + } + ) + assertEquals( + correctionAction?.annotation?.['mother.address']?.streetLevelDetails + ?.state, + 'OldState' + ) + assertEquals( + correctionAction?.annotation?.['mother.address']?.streetLevelDetails + ?.cityOrTown, + 'OldCity' + ) + }) + + await t.step('should transform multiple person field corrections', () => { + const registration = buildBirthEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'mother', + valueId: 'nationality', + value: 'USA', + }, + { + valueCode: 'father', + valueId: 'nationality', + value: 'CAN', + }, + ], + output: [ + { + valueCode: 'mother', + valueId: 'nationality', + value: 'GBR', + }, + { + valueCode: 'father', + valueId: 'nationality', + value: 'FRA', + }, + ], + }, + ], + }) + + const result = transform(registration, birthResolver, 'birth') + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) + + assertEquals(correctionAction?.declaration, { + 'mother.nationality': 'GBR', + 'father.nationality': 'FRA', + }) + assertEquals(correctionAction?.annotation?.['mother.nationality'], 'USA') + assertEquals(correctionAction?.annotation?.['father.nationality'], 'CAN') + }) + + await t.step('should transform age field corrections', () => { + const registration = buildBirthEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'mother', + valueId: 'ageOfIndividualInYears', + value: '25', + }, + ], + output: [ + { + valueCode: 'mother', + valueId: 'ageOfIndividualInYears', + value: '26', + }, + ], + }, + ], + }) + + const result = transform(registration, birthResolver, 'birth') + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) + + assertEquals(correctionAction?.declaration?.['mother.age'], { + age: '26', + asOfDateRef: 'child.dob', + }) + assertEquals(correctionAction?.annotation?.['mother.age'], { + age: '25', + asOfDateRef: 'child.dob', + }) + }) + + await t.step('should transform child birth details corrections', () => { + const registration = buildBirthEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'child', + valueId: 'childBirthDate', + value: '2024-01-01', + }, + { + valueCode: 'child', + valueId: 'placeOfBirth', + value: 'HEALTH_FACILITY', + }, + { + valueCode: 'child', + valueId: 'attendantAtBirth', + value: 'PHYSICIAN', + }, + { + valueCode: 'child', + valueId: 'birthType', + value: 'SINGLE', + }, + { + valueCode: 'child', + valueId: 'weightAtBirth', + value: '3.5', + }, + ], + output: [ + { + valueCode: 'child', + valueId: 'childBirthDate', + value: '2024-01-05', + }, + { + valueCode: 'child', + valueId: 'placeOfBirth', + value: 'PRIVATE_HOME', + }, + { + valueCode: 'child', + valueId: 'attendantAtBirth', + value: 'NURSE', + }, + { + valueCode: 'child', + valueId: 'birthType', + value: 'TWIN', + }, + { + valueCode: 'child', + valueId: 'weightAtBirth', + value: '3.8', + }, + ], + }, + ], + }) + + const result = transform(registration, birthResolver, 'birth') + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) + + assertEquals(correctionAction?.declaration, { + 'child.dob': '2024-01-05', + 'child.placeOfBirth': 'PRIVATE_HOME', + 'child.attendantAtBirth': 'NURSE', + 'child.birthType': 'TWIN', + 'child.weightAtBirth': '3.8', + }) + assertEquals(correctionAction?.annotation?.['child.dob'], '2024-01-01') + assertEquals( + correctionAction?.annotation?.['child.placeOfBirth'], + 'HEALTH_FACILITY' + ) + assertEquals( + correctionAction?.annotation?.['child.attendantAtBirth'], + 'PHYSICIAN' + ) + assertEquals(correctionAction?.annotation?.['child.birthType'], 'SINGLE') + assertEquals(correctionAction?.annotation?.['child.weightAtBirth'], '3.5') + }) + + await t.step('should transform mother detailed corrections', () => { + const registration = buildBirthEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'mother', + valueId: 'motherBirthDate', + value: '1990-01-01', + }, + { + valueCode: 'mother', + valueId: 'exactDateOfBirthUnknown', + value: 'false', + }, + { + valueCode: 'mother', + valueId: 'maritalStatus', + value: 'MARRIED', + }, + { + valueCode: 'mother', + valueId: 'educationalAttainment', + value: 'UPPER_SECONDARY_ISCED_3', + }, + { + valueCode: 'mother', + valueId: 'occupation', + value: 'Teacher', + }, + { + valueCode: 'mother', + valueId: 'multipleBirth', + value: '2', + }, + ], + output: [ + { + valueCode: 'mother', + valueId: 'motherBirthDate', + value: '1990-05-15', + }, + { + valueCode: 'mother', + valueId: 'exactDateOfBirthUnknown', + value: 'true', + }, + { + valueCode: 'mother', + valueId: 'maritalStatus', + value: 'SINGLE', + }, + { + valueCode: 'mother', + valueId: 'educationalAttainment', + value: 'POST_SECONDARY_ISCED_4', + }, + { + valueCode: 'mother', + valueId: 'occupation', + value: 'Doctor', + }, + { + valueCode: 'mother', + valueId: 'multipleBirth', + value: '3', + }, + ], + }, + ], + }) + + const result = transform(registration, birthResolver, 'birth') + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) + + assertEquals(correctionAction?.declaration, { + 'mother.dob': '1990-05-15', + 'mother.dobUnknown': 'true', + 'mother.maritalStatus': 'SINGLE', + 'mother.educationalAttainment': 'POST_SECONDARY_ISCED_4', + 'mother.occupation': 'Doctor', + 'mother.previousBirths': '3', + }) + assertEquals(correctionAction?.annotation?.['mother.dob'], '1990-01-01') + assertEquals(correctionAction?.annotation?.['mother.dobUnknown'], 'false') + assertEquals( + correctionAction?.annotation?.['mother.maritalStatus'], + 'MARRIED' + ) + assertEquals( + correctionAction?.annotation?.['mother.educationalAttainment'], + 'UPPER_SECONDARY_ISCED_3' + ) + assertEquals(correctionAction?.annotation?.['mother.occupation'], 'Teacher') + assertEquals(correctionAction?.annotation?.['mother.previousBirths'], '2') + }) + + await t.step('should transform father identification corrections', () => { + const registration = buildBirthEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'father', + valueId: 'fatherNationalId', + value: 'OLD123', + }, + { + valueCode: 'father', + valueId: 'fatherBirthRegistrationNumber', + value: 'BRN-OLD', + }, + { + valueCode: 'father', + valueId: 'fatherBirthDate', + value: '1985-01-01', + }, + { + valueCode: 'father', + valueId: 'maritalStatus', + value: 'MARRIED', + }, + { + valueCode: 'father', + valueId: 'educationalAttainment', + value: 'PRIMARY_ISCED_1', + }, + { + valueCode: 'father', + valueId: 'occupation', + value: 'Engineer', + }, + ], + output: [ + { + valueCode: 'father', + valueId: 'fatherNationalId', + value: 'NEW456', + }, + { + valueCode: 'father', + valueId: 'fatherBirthRegistrationNumber', + value: 'BRN-NEW', + }, + { + valueCode: 'father', + valueId: 'fatherBirthDate', + value: '1985-06-15', + }, + { + valueCode: 'father', + valueId: 'maritalStatus', + value: 'WIDOWED', + }, + { + valueCode: 'father', + valueId: 'educationalAttainment', + value: 'LOWER_SECONDARY_ISCED_2', + }, + { + valueCode: 'father', + valueId: 'occupation', + value: 'Architect', + }, + ], + }, + ], + }) + + const result = transform(registration, birthResolver, 'birth') + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) + + assertEquals(correctionAction?.declaration, { + 'father.nid': 'NEW456', + 'father.brn': 'BRN-NEW', + 'father.dob': '1985-06-15', + 'father.maritalStatus': 'WIDOWED', + 'father.educationalAttainment': 'LOWER_SECONDARY_ISCED_2', + 'father.occupation': 'Architect', + }) + assertEquals(correctionAction?.annotation?.['father.nid'], 'OLD123') + assertEquals(correctionAction?.annotation?.['father.brn'], 'BRN-OLD') + assertEquals(correctionAction?.annotation?.['father.dob'], '1985-01-01') + assertEquals( + correctionAction?.annotation?.['father.maritalStatus'], + 'MARRIED' + ) + assertEquals( + correctionAction?.annotation?.['father.educationalAttainment'], + 'PRIMARY_ISCED_1' + ) + assertEquals( + correctionAction?.annotation?.['father.occupation'], + 'Engineer' + ) + }) + + await t.step('should transform informant details corrections', () => { + const registration = buildBirthEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'informant', + valueId: 'informantBirthDate', + value: '1980-01-01', + }, + { + valueCode: 'informant', + valueId: 'exactDateOfBirthUnknown', + value: 'false', + }, + { + valueCode: 'informant', + valueId: 'informantType', + value: 'FATHER', + }, + { + valueCode: 'informant', + valueId: 'otherInformantType', + value: '', + }, + { + valueCode: 'informant', + valueId: 'registrationEmail', + value: 'old@example.com', + }, + { + valueCode: 'informant', + valueId: 'nationality', + value: 'USA', + }, + ], + output: [ + { + valueCode: 'informant', + valueId: 'informantBirthDate', + value: '1980-05-15', + }, + { + valueCode: 'informant', + valueId: 'exactDateOfBirthUnknown', + value: 'true', + }, + { + valueCode: 'informant', + valueId: 'informantType', + value: 'OTHER', + }, + { + valueCode: 'informant', + valueId: 'otherInformantType', + value: 'LEGAL_GUARDIAN', + }, + { + valueCode: 'informant', + valueId: 'registrationEmail', + value: 'new@example.com', + }, + { + valueCode: 'informant', + valueId: 'nationality', + value: 'GBR', + }, + ], + }, + ], + }) + + const result = transform(registration, birthResolver, 'birth') + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) + + assertEquals(correctionAction?.declaration, { + 'informant.dob': '1980-05-15', + 'informant.dobUnknown': 'true', + 'informant.relation': 'OTHER', + 'informant.other.relation': 'LEGAL_GUARDIAN', + 'informant.email': 'new@example.com', + 'informant.nationality': 'GBR', + }) + assertEquals(correctionAction?.annotation?.['informant.dob'], '1980-01-01') + assertEquals( + correctionAction?.annotation?.['informant.dobUnknown'], + 'false' + ) + assertEquals(correctionAction?.annotation?.['informant.relation'], 'FATHER') + assertEquals(correctionAction?.annotation?.['informant.other.relation'], '') + assertEquals( + correctionAction?.annotation?.['informant.email'], + 'old@example.com' + ) + assertEquals(correctionAction?.annotation?.['informant.nationality'], 'USA') + }) + + await t.step('should transform verified field corrections', () => { + const registration = buildBirthEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'mother', + valueId: 'verified', + value: 'false', + }, + { + valueCode: 'father', + valueId: 'verified', + value: 'false', + }, + { + valueCode: 'informant', + valueId: 'verified', + value: 'true', + }, + ], + output: [ + { + valueCode: 'mother', + valueId: 'verified', + value: 'true', + }, + { + valueCode: 'father', + valueId: 'verified', + value: 'true', + }, + { + valueCode: 'informant', + valueId: 'verified', + value: 'false', + }, + ], + }, + ], + }) + + const result = transform(registration, birthResolver, 'birth') + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) + + assertEquals(correctionAction?.declaration, { + 'mother.verified': 'true', + 'father.verified': 'true', + 'informant.verified': 'false', + }) + assertEquals(correctionAction?.annotation?.['mother.verified'], 'false') + assertEquals(correctionAction?.annotation?.['father.verified'], 'false') + assertEquals(correctionAction?.annotation?.['informant.verified'], 'true') + }) + + await t.step('should handle corrections without input (output-only)', () => { + const registration = buildBirthEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + output: [ + { + valueCode: 'child', + valueId: 'gender', + value: 'male', + }, + ], + }, + ], + }) + + const result = transform(registration, birthResolver, 'birth') + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) + + assertEquals(correctionAction?.declaration, { + 'child.gender': 'male', + }) + }) +}) + +Deno.test('Corrections - Death', async (t) => { + const deathResolver = buildDeathResolver() + + await t.step('should transform deceased field corrections', () => { + const registration = buildDeathEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'deceased', + valueId: 'gender', + value: 'male', + }, + { + valueCode: 'deceased', + valueId: 'nationality', + value: 'USA', + }, + ], + output: [ + { + valueCode: 'deceased', + valueId: 'gender', + value: 'female', + }, + { + valueCode: 'deceased', + valueId: 'nationality', + value: 'GBR', + }, + ], + }, + ], + }) + + const result = transform(registration, deathResolver, 'death') + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) + + assertEquals(correctionAction?.declaration, { + 'deceased.gender': 'female', + 'deceased.nationality': 'GBR', + }) + assertEquals(correctionAction?.annotation?.['deceased.gender'], 'male') + assertEquals(correctionAction?.annotation?.['deceased.nationality'], 'USA') + }) + + await t.step('should transform deceased detailed field corrections', () => { + const registration = buildDeathEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'deceased', + valueId: 'deceasedBirthDate', + value: '1950-01-01', + }, + { + valueCode: 'deceased', + valueId: 'exactDateOfBirthUnknown', + value: 'false', + }, + { + valueCode: 'deceased', + valueId: 'maritalStatus', + value: 'MARRIED', + }, + { + valueCode: 'deceased', + valueId: 'deceasedBirthRegistrationNumber', + value: 'BRN-OLD', + }, + ], + output: [ + { + valueCode: 'deceased', + valueId: 'deceasedBirthDate', + value: '1950-06-15', + }, + { + valueCode: 'deceased', + valueId: 'exactDateOfBirthUnknown', + value: 'true', + }, + { + valueCode: 'deceased', + valueId: 'maritalStatus', + value: 'WIDOWED', + }, + { + valueCode: 'deceased', + valueId: 'deceasedBirthRegistrationNumber', + value: 'BRN-NEW', + }, + ], + }, + ], + }) + + const result = transform(registration, deathResolver, 'death') + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) + + assertEquals(correctionAction?.declaration, { + 'deceased.dob': '1950-06-15', + 'deceased.dobUnknown': 'true', + 'deceased.maritalStatus': 'WIDOWED', + 'deceased.brn': 'BRN-NEW', + }) + assertEquals(correctionAction?.annotation?.['deceased.dob'], '1950-01-01') + assertEquals(correctionAction?.annotation?.['deceased.dobUnknown'], 'false') + assertEquals( + correctionAction?.annotation?.['deceased.maritalStatus'], + 'MARRIED' + ) + assertEquals(correctionAction?.annotation?.['deceased.brn'], 'BRN-OLD') + }) + + await t.step('should transform death event details corrections', () => { + const registration = buildDeathEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'deathEvent', + valueId: 'deathDate', + value: '2024-01-01', + }, + { + valueCode: 'deathEvent', + valueId: 'mannerOfDeath', + value: 'NATURAL_CAUSES', + }, + ], + output: [ + { + valueCode: 'deathEvent', + valueId: 'deathDate', + value: '2024-01-05', + }, + { + valueCode: 'deathEvent', + valueId: 'mannerOfDeath', + value: 'ACCIDENT', + }, + ], + }, + ], + }) + + const result = transform(registration, deathResolver, 'death') + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) + + assertEquals(correctionAction?.declaration, { + 'eventDetails.date': '2024-01-05', + 'eventDetails.mannerOfDeath': 'ACCIDENT', + }) + assertEquals( + correctionAction?.annotation?.['eventDetails.date'], + '2024-01-01' + ) + assertEquals( + correctionAction?.annotation?.['eventDetails.mannerOfDeath'], + 'NATURAL_CAUSES' + ) + }) + + await t.step('should transform death event comprehensive corrections', () => { + const registration = buildDeathEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'deathEvent', + valueId: 'placeOfDeath', + value: 'HEALTH_FACILITY', + }, + { + valueCode: 'deathEvent', + valueId: 'causeOfDeathEstablished', + value: 'true', + }, + { + valueCode: 'deathEvent', + valueId: 'causeOfDeathMethod', + value: 'PHYSICIAN', + }, + { + valueCode: 'deathEvent', + valueId: 'deathDescription', + value: 'Old description', + }, + ], + output: [ + { + valueCode: 'deathEvent', + valueId: 'placeOfDeath', + value: 'OTHER', + }, + { + valueCode: 'deathEvent', + valueId: 'causeOfDeathEstablished', + value: 'false', + }, + { + valueCode: 'deathEvent', + valueId: 'causeOfDeathMethod', + value: 'VERBAL_AUTOPSY', + }, + { + valueCode: 'deathEvent', + valueId: 'deathDescription', + value: 'New description', + }, + ], + }, + ], + }) + + const result = transform(registration, deathResolver, 'death') + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) + + assertEquals(correctionAction?.declaration, { + 'eventDetails.placeOfDeath': 'OTHER', + 'eventDetails.causeOfDeathEstablished': 'false', + 'eventDetails.sourceCauseDeath': 'VERBAL_AUTOPSY', + 'eventDetails.description': 'New description', + }) + assertEquals( + correctionAction?.annotation?.['eventDetails.placeOfDeath'], + 'HEALTH_FACILITY' + ) + assertEquals( + correctionAction?.annotation?.['eventDetails.causeOfDeathEstablished'], + 'true' + ) + assertEquals( + correctionAction?.annotation?.['eventDetails.sourceCauseDeath'], + 'PHYSICIAN' + ) + assertEquals( + correctionAction?.annotation?.['eventDetails.description'], + 'Old description' + ) + }) + + await t.step('should transform spouse field corrections', () => { + const registration = buildDeathEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'spouse', + valueId: 'firstNamesEng', + value: 'OldName', + }, + { + valueCode: 'spouse', + valueId: 'nationality', + value: 'USA', + }, + ], + output: [ + { + valueCode: 'spouse', + valueId: 'firstNamesEng', + value: 'NewName', + }, + { + valueCode: 'spouse', + valueId: 'nationality', + value: 'GBR', + }, + ], + }, + ], + }) + + const result = transform(registration, deathResolver, 'death') + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) + + assertEquals(correctionAction?.declaration?.['spouse.name'], { + firstname: 'NewName', + }) + assertEquals(correctionAction?.declaration?.['spouse.nationality'], 'GBR') + assertEquals(correctionAction?.annotation?.['spouse.name'], { + firstname: 'OldName', + }) + assertEquals(correctionAction?.annotation?.['spouse.nationality'], 'USA') + }) + + await t.step('should transform spouse detailed corrections', () => { + const registration = buildDeathEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'spouse', + valueId: 'spouseBirthDate', + value: '1952-01-01', + }, + { + valueCode: 'spouse', + valueId: 'exactDateOfBirthUnknown', + value: 'false', + }, + { + valueCode: 'spouse', + valueId: 'spouseNationalId', + value: 'OLD-NID', + }, + { + valueCode: 'spouse', + valueId: 'spouseBirthRegistrationNumber', + value: 'OLD-BRN', + }, + { + valueCode: 'spouse', + valueId: 'ageOfIndividualInYears', + value: '70', + }, + ], + output: [ + { + valueCode: 'spouse', + valueId: 'spouseBirthDate', + value: '1952-06-15', + }, + { + valueCode: 'spouse', + valueId: 'exactDateOfBirthUnknown', + value: 'true', + }, + { + valueCode: 'spouse', + valueId: 'spouseNationalId', + value: 'NEW-NID', + }, + { + valueCode: 'spouse', + valueId: 'spouseBirthRegistrationNumber', + value: 'NEW-BRN', + }, + { + valueCode: 'spouse', + valueId: 'ageOfIndividualInYears', + value: '71', + }, + ], + }, + ], + }) + + const result = transform(registration, deathResolver, 'death') + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) + + // Note: AGE_MAPPINGS transforms age into an object, not a string + assertEquals(correctionAction?.declaration?.['spouse.dob'], '1952-06-15') + assertEquals(correctionAction?.declaration?.['spouse.dobUnknown'], 'true') + assertEquals(correctionAction?.declaration?.['spouse.nid'], 'NEW-NID') + assertEquals(correctionAction?.declaration?.['spouse.brn'], 'NEW-BRN') + // AGE_MAPPINGS transforms age values + assertEquals(correctionAction?.declaration?.['spouse.age']?.age, '71') + assertEquals( + correctionAction?.declaration?.['spouse.age']?.asOfDateRef, + 'eventDetails.date' + ) + + assertEquals(correctionAction?.annotation?.['spouse.dob'], '1952-01-01') + assertEquals(correctionAction?.annotation?.['spouse.dobUnknown'], 'false') + assertEquals(correctionAction?.annotation?.['spouse.nid'], 'OLD-NID') + assertEquals(correctionAction?.annotation?.['spouse.brn'], 'OLD-BRN') + // AGE_MAPPINGS transforms age into an object for both input and output + assertEquals(correctionAction?.annotation?.['spouse.age']?.age, '70') + assertEquals( + correctionAction?.annotation?.['spouse.age']?.asOfDateRef, + 'eventDetails.date' + ) + }) + + await t.step('should transform death location address corrections', () => { + const registration = buildDeathEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'deathEvent', + valueId: 'internationalStatePlaceofdeath', + value: 'OldState', + }, + { + valueCode: 'deathEvent', + valueId: 'internationalCityPlaceofdeath', + value: 'OldCity', + }, + ], + output: [ + { + valueCode: 'deathEvent', + valueId: 'internationalStatePlaceofdeath', + value: 'NewState', + }, + { + valueCode: 'deathEvent', + valueId: 'internationalCityPlaceofdeath', + value: 'NewCity', + }, + ], + }, + ], + }) + + const result = transform(registration, deathResolver, 'death') + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) + + assertEquals( + correctionAction?.declaration?.['eventDetails.deathLocationOther'] + ?.streetLevelDetails?.state, + 'NewState' + ) + assertEquals( + correctionAction?.declaration?.['eventDetails.deathLocationOther'] + ?.streetLevelDetails?.cityOrTown, + 'NewCity' + ) + assertEquals( + correctionAction?.annotation?.['eventDetails.deathLocationOther'] + ?.streetLevelDetails?.state, + 'OldState' + ) + assertEquals( + correctionAction?.annotation?.['eventDetails.deathLocationOther'] + ?.streetLevelDetails?.cityOrTown, + 'OldCity' + ) + }) + + await t.step('should transform informant corrections in death', () => { + const registration = buildDeathEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'informant', + valueId: 'informantType', + value: 'SON', + }, + { + valueCode: 'informant', + valueId: 'registrationPhone', + value: '0788888888', + }, + ], + output: [ + { + valueCode: 'informant', + valueId: 'informantType', + value: 'DAUGHTER', + }, + { + valueCode: 'informant', + valueId: 'registrationPhone', + value: '0799999999', + }, + ], + }, + ], + }) + + const result = transform(registration, deathResolver, 'death') + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) + + assertEquals(correctionAction?.declaration, { + 'informant.relation': 'DAUGHTER', + 'informant.phoneNo': '0799999999', + }) + assertEquals(correctionAction?.annotation?.['informant.relation'], 'SON') + assertEquals( + correctionAction?.annotation?.['informant.phoneNo'], + '0788888888' + ) + }) + + await t.step( + 'should transform informant address corrections in death', + () => { + const registration = buildDeathEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'informant', + valueId: 'internationalStatePrimaryInformant', + value: 'OldState', + }, + { + valueCode: 'informant', + valueId: 'internationalDistrictPrimaryInformant', + value: 'OldDistrict', + }, + { + valueCode: 'informant', + valueId: 'internationalCityPrimaryInformant', + value: 'OldCity', + }, + { + valueCode: 'informant', + valueId: 'internationalPostalCodePrimaryInformant', + value: '12345', + }, + ], + output: [ + { + valueCode: 'informant', + valueId: 'internationalStatePrimaryInformant', + value: 'NewState', + }, + { + valueCode: 'informant', + valueId: 'internationalDistrictPrimaryInformant', + value: 'NewDistrict', + }, + { + valueCode: 'informant', + valueId: 'internationalCityPrimaryInformant', + value: 'NewCity', + }, + { + valueCode: 'informant', + valueId: 'internationalPostalCodePrimaryInformant', + value: '54321', + }, + ], + }, + ], + }) + + const result = transform(registration, deathResolver, 'death') + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) + + // Check individual fields to avoid default field interference + assertEquals( + correctionAction?.declaration?.['informant.address']?.streetLevelDetails + ?.state, + 'NewState' + ) + assertEquals( + correctionAction?.declaration?.['informant.address']?.streetLevelDetails + ?.district2, + 'NewDistrict' + ) + assertEquals( + correctionAction?.declaration?.['informant.address']?.streetLevelDetails + ?.cityOrTown, + 'NewCity' + ) + assertEquals( + correctionAction?.declaration?.['informant.address']?.streetLevelDetails + ?.postcodeOrZip, + '54321' + ) + assertEquals( + correctionAction?.annotation?.['informant.address']?.streetLevelDetails + ?.state, + 'OldState' + ) + assertEquals( + correctionAction?.annotation?.['informant.address']?.streetLevelDetails + ?.district2, + 'OldDistrict' + ) + assertEquals( + correctionAction?.annotation?.['informant.address']?.streetLevelDetails + ?.cityOrTown, + 'OldCity' + ) + assertEquals( + correctionAction?.annotation?.['informant.address']?.streetLevelDetails + ?.postcodeOrZip, + '12345' + ) + } + ) + + await t.step('should transform custom field corrections in death', () => { + const registration = buildDeathEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'deceased', + valueId: 'deceasedIdType', + value: 'PASSPORT', + }, + { + valueCode: 'deceased', + valueId: 'deceasedPassport', + value: 'P123456', + }, + ], + output: [ + { + valueCode: 'deceased', + valueId: 'deceasedIdType', + value: 'NATIONAL_ID', + }, + { + valueCode: 'deceased', + valueId: 'deceasedNationalId', + value: 'N789012', + }, + ], + }, + ], + }) + + const result = transform(registration, deathResolver, 'death') + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) + + assertEquals(correctionAction?.declaration, { + 'deceased.idType': 'NATIONAL_ID', + 'deceased.nid': 'N789012', + }) + assertEquals(correctionAction?.annotation?.['deceased.idType'], 'PASSPORT') + assertEquals(correctionAction?.annotation?.['deceased.passport'], 'P123456') + }) + + await t.step('should transform verified fields in death', () => { + const registration = buildDeathEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'deceased', + valueId: 'verified', + value: 'false', + }, + { + valueCode: 'informant', + valueId: 'verified', + value: 'true', + }, + { + valueCode: 'spouse', + valueId: 'verified', + value: 'false', + }, + ], + output: [ + { + valueCode: 'deceased', + valueId: 'verified', + value: 'true', + }, + { + valueCode: 'informant', + valueId: 'verified', + value: 'false', + }, + { + valueCode: 'spouse', + valueId: 'verified', + value: 'true', + }, + ], + }, + ], + }) + + const result = transform(registration, deathResolver, 'death') + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) + + assertEquals(correctionAction?.declaration, { + 'deceased.verified': 'true', + 'informant.verified': 'false', + 'spouse.verified': 'true', + }) + assertEquals(correctionAction?.annotation?.['deceased.verified'], 'false') + assertEquals(correctionAction?.annotation?.['informant.verified'], 'true') + assertEquals(correctionAction?.annotation?.['spouse.verified'], 'false') + }) +}) + +Deno.test('Corrections - Edge Cases', async (t) => { + const birthResolver = buildBirthResolver() + + await t.step('should handle empty input and output arrays', () => { + const registration = buildBirthEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [], + output: [], + }, + ], + }) + + const result = transform(registration, birthResolver, 'birth') + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) + + assertEquals(correctionAction?.declaration, {}) + }) + + await t.step( + 'should handle mixed field type corrections in one request', + () => { + const registration = buildBirthEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'child', + valueId: 'gender', + value: 'male', + }, + { + valueCode: 'mother', + valueId: 'firstNamesEng', + value: 'OldName', + }, + { + valueCode: 'informant', + valueId: 'registrationPhone', + value: '0788888888', + }, + ], + output: [ + { + valueCode: 'child', + valueId: 'gender', + value: 'female', + }, + { + valueCode: 'mother', + valueId: 'firstNamesEng', + value: 'NewName', + }, + { + valueCode: 'informant', + valueId: 'registrationPhone', + value: '0799999999', + }, + ], + }, + ], + }) + + const result = transform(registration, birthResolver, 'birth') + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) + + assertEquals(correctionAction?.declaration, { + 'child.gender': 'female', + 'mother.name': { firstname: 'NewName' }, + 'informant.phoneNo': '0799999999', + }) + assertEquals(correctionAction?.annotation?.['child.gender'], 'male') + assertEquals(correctionAction?.annotation?.['mother.name'], { + firstname: 'OldName', + }) + assertEquals( + correctionAction?.annotation?.['informant.phoneNo'], + '0788888888' + ) + } + ) + + await t.step( + 'should preserve addressType when correcting address fields', + () => { + const registration = buildBirthEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'father', + valueId: 'internationalStatePrimaryFather', + value: 'OldState', + }, + ], + output: [ + { + valueCode: 'father', + valueId: 'internationalStatePrimaryFather', + value: 'NewState', + }, + ], + }, + ], + }) + + const result = transform(registration, birthResolver, 'birth') + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) + + // Should have addressType set based on country/international fields + assertEquals( + correctionAction?.declaration?.['father.address']?.addressType !== + undefined, + true + ) + assertEquals( + correctionAction?.declaration?.['father.address']?.streetLevelDetails + ?.state, + 'NewState' + ) + } + ) +}) From 1a28f668eceb51b628130211aae66df78aa91417 Mon Sep 17 00:00:00 2001 From: Barry Dwyer Date: Tue, 2 Dec 2025 11:17:34 +0200 Subject: [PATCH 07/15] Add tests to reverse engineer declaration from correction inputs --- v1-to-v2-data-migration/helpers/transform.ts | 112 ++- .../tests/unit/postProcess.test.ts | 946 ++++++++++++++++++ 2 files changed, 1046 insertions(+), 12 deletions(-) create mode 100644 v1-to-v2-data-migration/tests/unit/postProcess.test.ts diff --git a/v1-to-v2-data-migration/helpers/transform.ts b/v1-to-v2-data-migration/helpers/transform.ts index a3500fa..8d85de2 100644 --- a/v1-to-v2-data-migration/helpers/transform.ts +++ b/v1-to-v2-data-migration/helpers/transform.ts @@ -462,13 +462,51 @@ export function transform( return postProcess(documents) } +/** + * Deep merge two objects, with priority given to source values + */ +function deepMerge(target: any, source: any): any { + const result = { ...target } + + for (const key in source) { + if (source.hasOwnProperty(key)) { + const sourceValue = source[key] + const targetValue = target[key] + + // If both are plain objects, merge recursively + if ( + sourceValue && + typeof sourceValue === 'object' && + !Array.isArray(sourceValue) && + targetValue && + typeof targetValue === 'object' && + !Array.isArray(targetValue) + ) { + result[key] = deepMerge(targetValue, sourceValue) + } else { + // Otherwise, use source value + result[key] = sourceValue + } + } + } + + return result +} + function postProcess(documents: TransformedDocument): TransformedDocument { const correctionResolverKeys = Object.keys(correctionResolver) + // Track which fields have been processed to handle sequential corrections correctly + const processedFields = new Set() + for (let i = 0; i < documents.actions.length; i++) { const action = documents.actions[i] - if (action.type === 'REQUEST_CORRECTION' && action.annotation) { + if ( + action.type === 'REQUEST_CORRECTION' && + action.annotation && + action.declaration + ) { // Filter out correctionResolver fields from the annotation const filteredAnnotation = Object.fromEntries( Object.entries(action.annotation).filter( @@ -476,17 +514,67 @@ function postProcess(documents: TransformedDocument): TransformedDocument { ) ) - // Find the first action before this one with a non-empty declaration - for (let j = i - 1; j >= 0; j--) { - const previousAction = documents.actions[j] - - if ( - previousAction.declaration && - typeof previousAction.declaration === 'object' && - Object.keys(previousAction.declaration).length > 0 - ) { - previousAction.declaration = filteredAnnotation - break + // Only extract fields that are in the correction's declaration (i.e., fields that were actually corrected) + // This prevents us from copying the entire base declaration + const correctedFieldKeys = new Set(Object.keys(action.declaration)) + const actualCorrectionFields: Record = {} + + for (const [key, value] of Object.entries(filteredAnnotation)) { + if (correctedFieldKeys.has(key)) { + actualCorrectionFields[key] = value + } + } + + // Separate fields into new and already-corrected + const newFields: Record = {} + const correctedFields: Record = {} + + for (const [key, value] of Object.entries(actualCorrectionFields)) { + if (processedFields.has(key)) { + correctedFields[key] = value + } else { + newFields[key] = value + processedFields.add(key) + } + } + + // For new fields, find the base action (REGISTER/DECLARE) and update it + if (Object.keys(newFields).length > 0) { + for (let j = i - 1; j >= 0; j--) { + const previousAction = documents.actions[j] + + // Skip other correction actions - we want to update the base action + if (previousAction.type === 'REQUEST_CORRECTION') { + continue + } + + if ( + previousAction.declaration && + typeof previousAction.declaration === 'object' && + Object.keys(previousAction.declaration).length > 0 + ) { + previousAction.declaration = deepMerge( + previousAction.declaration, + newFields + ) + break + } + } + } + + // For already-corrected fields, find the previous correction action and update it + if (Object.keys(correctedFields).length > 0) { + for (let j = i - 1; j >= 0; j--) { + const previousAction = documents.actions[j] + + // Look for the previous correction action + if (previousAction.type === 'REQUEST_CORRECTION') { + previousAction.declaration = deepMerge( + previousAction.declaration, + correctedFields + ) + break + } } } } diff --git a/v1-to-v2-data-migration/tests/unit/postProcess.test.ts b/v1-to-v2-data-migration/tests/unit/postProcess.test.ts new file mode 100644 index 0000000..197a073 --- /dev/null +++ b/v1-to-v2-data-migration/tests/unit/postProcess.test.ts @@ -0,0 +1,946 @@ +import { transform } from '../../helpers/transform.ts' +import { + buildBirthResolver, + buildBirthEventRegistration, + buildDeathEventRegistration, + buildDeathResolver, +} from '../utils/test-helpers.ts' +import { assertEquals } from 'https://deno.land/std@0.210.0/assert/mod.ts' +import type { Action } from '../../helpers/types.ts' + +Deno.test('PostProcess - Single Correction', async (t) => { + const birthResolver = buildBirthResolver() + + await t.step( + 'should update previous action declaration with correction input for single correction', + () => { + const registration = buildBirthEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + regStatus: 'REGISTERED', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-03T14:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user3', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'child', + valueId: 'gender', + value: 'male', + }, + { + valueCode: 'informant', + valueId: 'registrationPhone', + value: '0788888888', + }, + ], + output: [ + { + valueCode: 'child', + valueId: 'gender', + value: 'female', + }, + { + valueCode: 'informant', + valueId: 'registrationPhone', + value: '0799999999', + }, + ], + }, + ], + }) + + const result = transform(registration, birthResolver, 'birth') + + // Find the actions + const registerAction = result.actions.find( + (a) => a.type === 'REGISTER' + ) as Action + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) as Action + + // The correction action should have the new values in declaration + assertEquals(correctionAction?.declaration?.['child.gender'], 'female') + assertEquals( + correctionAction?.declaration?.['informant.phoneNo'], + '0799999999' + ) + + // The correction action should have the old values in annotation + assertEquals(correctionAction?.annotation?.['child.gender'], 'male') + assertEquals( + correctionAction?.annotation?.['informant.phoneNo'], + '0788888888' + ) + + // The REGISTER action (previous action with declaration) should now have the old values + assertEquals(registerAction?.declaration?.['child.gender'], 'male') + assertEquals( + registerAction?.declaration?.['informant.phoneNo'], + '0788888888' + ) + } + ) + + await t.step( + 'should handle correction when previous action is DECLARE instead of REGISTER', + () => { + const registration = buildBirthEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T14:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'mother', + valueId: 'nationality', + value: 'USA', + }, + ], + output: [ + { + valueCode: 'mother', + valueId: 'nationality', + value: 'GBR', + }, + ], + }, + ], + }) + + const result = transform(registration, birthResolver, 'birth') + + const declareAction = result.actions.find( + (a) => a.type === 'DECLARE' + ) as Action + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) as Action + + // Correction has new value + assertEquals(correctionAction?.declaration?.['mother.nationality'], 'GBR') + assertEquals(correctionAction?.annotation?.['mother.nationality'], 'USA') + + // DECLARE action should now have old value + assertEquals(declareAction?.declaration?.['mother.nationality'], 'USA') + } + ) + + await t.step( + 'should handle correction with complex nested fields (names)', + () => { + const registration = buildBirthEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + regStatus: 'REGISTERED', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-03T14:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user3', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'child', + valueId: 'firstNamesEng', + value: 'OldFirstName', + }, + { + valueCode: 'child', + valueId: 'familyNameEng', + value: 'OldLastName', + }, + ], + output: [ + { + valueCode: 'child', + valueId: 'firstNamesEng', + value: 'NewFirstName', + }, + { + valueCode: 'child', + valueId: 'familyNameEng', + value: 'NewLastName', + }, + ], + }, + ], + }) + + const result = transform(registration, birthResolver, 'birth') + + const registerAction = result.actions.find( + (a) => a.type === 'REGISTER' + ) as Action + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) as Action + + // Correction has new name + assertEquals(correctionAction?.declaration?.['child.name'], { + firstname: 'NewFirstName', + surname: 'NewLastName', + }) + assertEquals(correctionAction?.annotation?.['child.name'], { + firstname: 'OldFirstName', + surname: 'OldLastName', + }) + + // REGISTER action should now have old name + assertEquals(registerAction?.declaration?.['child.name'], { + firstname: 'OldFirstName', + surname: 'OldLastName', + }) + } + ) + + await t.step('should handle correction with address fields', () => { + const registration = buildBirthEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + regStatus: 'REGISTERED', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-03T14:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user3', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'mother', + valueId: 'internationalStatePrimaryMother', + value: 'OldState', + }, + { + valueCode: 'mother', + valueId: 'internationalCityPrimaryMother', + value: 'OldCity', + }, + ], + output: [ + { + valueCode: 'mother', + valueId: 'internationalStatePrimaryMother', + value: 'NewState', + }, + { + valueCode: 'mother', + valueId: 'internationalCityPrimaryMother', + value: 'NewCity', + }, + ], + }, + ], + }) + + const result = transform(registration, birthResolver, 'birth') + + const registerAction = result.actions.find( + (a) => a.type === 'REGISTER' + ) as Action + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) as Action + + // Correction has new address + assertEquals( + correctionAction?.declaration?.['mother.address']?.streetLevelDetails + ?.state, + 'NewState' + ) + assertEquals( + correctionAction?.declaration?.['mother.address']?.streetLevelDetails + ?.cityOrTown, + 'NewCity' + ) + + // Annotation has old address + assertEquals( + correctionAction?.annotation?.['mother.address']?.streetLevelDetails + ?.state, + 'OldState' + ) + assertEquals( + correctionAction?.annotation?.['mother.address']?.streetLevelDetails + ?.cityOrTown, + 'OldCity' + ) + + // REGISTER action should now have old address + assertEquals( + registerAction?.declaration?.['mother.address']?.streetLevelDetails + ?.state, + 'OldState' + ) + assertEquals( + registerAction?.declaration?.['mother.address']?.streetLevelDetails + ?.cityOrTown, + 'OldCity' + ) + }) +}) + +Deno.test('PostProcess - Multiple Corrections', async (t) => { + const birthResolver = buildBirthResolver() + + await t.step( + 'should handle multiple sequential corrections correctly', + () => { + const registration = buildBirthEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + regStatus: 'REGISTERED', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-03T14:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user3', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'child', + valueId: 'gender', + value: 'male', + }, + ], + output: [ + { + valueCode: 'child', + valueId: 'gender', + value: 'female', + }, + ], + }, + { + date: '2024-01-04T16:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user4', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'child', + valueId: 'gender', + value: 'female', + }, + { + valueCode: 'mother', + valueId: 'nationality', + value: 'USA', + }, + ], + output: [ + { + valueCode: 'child', + valueId: 'gender', + value: 'male', + }, + { + valueCode: 'mother', + valueId: 'nationality', + value: 'GBR', + }, + ], + }, + ], + }) + + const result = transform(registration, birthResolver, 'birth') + + // Find all actions + const registerAction = result.actions.find( + (a) => a.type === 'REGISTER' + ) as Action + const corrections = result.actions.filter( + (a) => a.type === 'REQUEST_CORRECTION' + ) as Action[] + + assertEquals(corrections.length, 2, 'Should have 2 correction actions') + + const firstCorrection = corrections[0] + const secondCorrection = corrections[1] + + // First correction: male → female + assertEquals(firstCorrection?.declaration?.['child.gender'], 'female') + assertEquals(firstCorrection?.annotation?.['child.gender'], 'male') + + // Second correction: female → male, USA → GBR + assertEquals(secondCorrection?.declaration?.['child.gender'], 'male') + assertEquals(secondCorrection?.declaration?.['mother.nationality'], 'GBR') + assertEquals(secondCorrection?.annotation?.['child.gender'], 'female') + assertEquals(secondCorrection?.annotation?.['mother.nationality'], 'USA') + + // REGISTER action should have the ORIGINAL value (male) from before first correction + assertEquals(registerAction?.declaration?.['child.gender'], 'male') + + // The first correction should now show the state after register (male) was changed + // So first correction declaration should still show female, and annotation should show male + // But this is already tested above + + // The second correction should reference the first correction's output as its input + // This is already captured in the secondCorrection checks above + } + ) + + await t.step( + 'should handle corrections of different fields across multiple corrections', + () => { + const registration = buildBirthEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + regStatus: 'REGISTERED', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-03T14:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user3', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'child', + valueId: 'gender', + value: 'male', + }, + ], + output: [ + { + valueCode: 'child', + valueId: 'gender', + value: 'female', + }, + ], + }, + { + date: '2024-01-04T16:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user4', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'mother', + valueId: 'nationality', + value: 'USA', + }, + ], + output: [ + { + valueCode: 'mother', + valueId: 'nationality', + value: 'GBR', + }, + ], + }, + ], + }) + + const result = transform(registration, birthResolver, 'birth') + + const registerAction = result.actions.find( + (a) => a.type === 'REGISTER' + ) as Action + const corrections = result.actions.filter( + (a) => a.type === 'REQUEST_CORRECTION' + ) as Action[] + + assertEquals(corrections.length, 2) + + // First correction (child.gender) + assertEquals(corrections[0]?.declaration?.['child.gender'], 'female') + assertEquals(corrections[0]?.annotation?.['child.gender'], 'male') + + // Second correction (mother.nationality) - different field + assertEquals(corrections[1]?.declaration?.['mother.nationality'], 'GBR') + assertEquals(corrections[1]?.annotation?.['mother.nationality'], 'USA') + + // REGISTER should have BOTH original values + assertEquals(registerAction?.declaration?.['child.gender'], 'male') + assertEquals(registerAction?.declaration?.['mother.nationality'], 'USA') + } + ) +}) + +Deno.test('PostProcess - Corrections With Actions In Between', async (t) => { + const birthResolver = buildBirthResolver() + + await t.step( + 'should handle correction with other actions before it (skip empty declarations)', + () => { + const registration = buildBirthEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + regStatus: 'REGISTERED', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-03T13:00:00Z', + action: 'ASSIGNED', + user: { id: 'user3', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-04T14:00:00Z', + action: 'VIEWED', + user: { id: 'user4', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-05T15:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user5', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'child', + valueId: 'gender', + value: 'male', + }, + ], + output: [ + { + valueCode: 'child', + valueId: 'gender', + value: 'female', + }, + ], + }, + ], + }) + + const result = transform(registration, birthResolver, 'birth') + + const registerAction = result.actions.find( + (a) => a.type === 'REGISTER' + ) as Action + const assignAction = result.actions.find( + (a) => a.type === 'ASSIGN' + ) as Action + const readAction = result.actions.find((a) => a.type === 'READ') as Action + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) as Action + + // Correction has new value + assertEquals(correctionAction?.declaration?.['child.gender'], 'female') + assertEquals(correctionAction?.annotation?.['child.gender'], 'male') + + // ASSIGN and READ actions should have empty declarations + assertEquals(Object.keys(assignAction?.declaration || {}).length, 0) + assertEquals(Object.keys(readAction?.declaration || {}).length, 0) + + // REGISTER action should have the old value (skipping empty declaration actions) + assertEquals(registerAction?.declaration?.['child.gender'], 'male') + } + ) + + await t.step( + 'should reverse engineer through multiple corrections with actions in between', + () => { + const registration = buildBirthEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + regStatus: 'REGISTERED', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-03T14:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user3', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'child', + valueId: 'gender', + value: 'male', + }, + ], + output: [ + { + valueCode: 'child', + valueId: 'gender', + value: 'female', + }, + ], + }, + { + date: '2024-01-04T15:00:00Z', + action: 'VIEWED', + user: { id: 'user4', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-05T16:00:00Z', + action: 'ASSIGNED', + user: { id: 'user5', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-06T17:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user6', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'child', + valueId: 'gender', + value: 'female', + }, + { + valueCode: 'mother', + valueId: 'nationality', + value: 'USA', + }, + ], + output: [ + { + valueCode: 'child', + valueId: 'gender', + value: 'male', + }, + { + valueCode: 'mother', + valueId: 'nationality', + value: 'GBR', + }, + ], + }, + ], + }) + + const result = transform(registration, birthResolver, 'birth') + + const registerAction = result.actions.find( + (a) => a.type === 'REGISTER' + ) as Action + const corrections = result.actions.filter( + (a) => a.type === 'REQUEST_CORRECTION' + ) as Action[] + + assertEquals(corrections.length, 2) + + // First correction: male → female + assertEquals(corrections[0]?.declaration?.['child.gender'], 'female') + assertEquals(corrections[0]?.annotation?.['child.gender'], 'male') + + // Second correction: female → male, USA → GBR + assertEquals(corrections[1]?.declaration?.['child.gender'], 'male') + assertEquals(corrections[1]?.declaration?.['mother.nationality'], 'GBR') + assertEquals(corrections[1]?.annotation?.['child.gender'], 'female') + assertEquals(corrections[1]?.annotation?.['mother.nationality'], 'USA') + + // The first correction should be reverse-engineered to show the state before it + // which should trace back to REGISTER (skipping VIEWED and ASSIGNED) + // So first correction should update the REGISTER action to show original male + assertEquals(registerAction?.declaration?.['child.gender'], 'male') + + // The REGISTER should also have the original mother.nationality + assertEquals(registerAction?.declaration?.['mother.nationality'], 'USA') + + // The second correction should update the first correction's declaration + // to show the state that existed before second correction (which is after first) + // First correction's declaration already shows female, which is correct + // Second correction's annotation shows female, which references first correction's output + } + ) + + await t.step( + 'should handle validation action between register and correction', + () => { + const registration = buildBirthEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + regStatus: 'REGISTERED', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-03T13:00:00Z', + regStatus: 'VALIDATED', + user: { id: 'user3', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-04T14:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user4', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'informant', + valueId: 'registrationPhone', + value: '0788888888', + }, + ], + output: [ + { + valueCode: 'informant', + valueId: 'registrationPhone', + value: '0799999999', + }, + ], + }, + ], + }) + + const result = transform(registration, birthResolver, 'birth') + + const validateAction = result.actions.find( + (a) => a.type === 'VALIDATE' + ) as Action + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) as Action + + // Correction has new phone + assertEquals( + correctionAction?.declaration?.['informant.phoneNo'], + '0799999999' + ) + assertEquals( + correctionAction?.annotation?.['informant.phoneNo'], + '0788888888' + ) + + // VALIDATE action (most recent before correction with non-empty declaration) + // should have the old phone number + assertEquals( + validateAction?.declaration?.['informant.phoneNo'], + '0788888888' + ) + } + ) +}) + +Deno.test('PostProcess - Death Event Corrections', async (t) => { + const deathResolver = buildDeathResolver() + + await t.step('should handle single correction for death event', () => { + const registration = buildDeathEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + regStatus: 'REGISTERED', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-03T14:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user3', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'deceased', + valueId: 'gender', + value: 'male', + }, + ], + output: [ + { + valueCode: 'deceased', + valueId: 'gender', + value: 'female', + }, + ], + }, + ], + }) + + const result = transform(registration, deathResolver, 'death') + + const registerAction = result.actions.find( + (a) => a.type === 'REGISTER' + ) as Action + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) as Action + + // Correction has new value + assertEquals(correctionAction?.declaration?.['deceased.gender'], 'female') + assertEquals(correctionAction?.annotation?.['deceased.gender'], 'male') + + // REGISTER action should have old value + assertEquals(registerAction?.declaration?.['deceased.gender'], 'male') + }) + + await t.step( + 'should handle multiple corrections for death event with different fields', + () => { + const registration = buildDeathEventRegistration({ + deceased: { + nationality: ['USA'], + }, + mannerOfDeath: 'NATURAL_CAUSES', + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + regStatus: 'REGISTERED', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-03T14:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user3', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'deceased', + valueId: 'nationality', + value: 'USA', + }, + ], + output: [ + { + valueCode: 'deceased', + valueId: 'nationality', + value: 'GBR', + }, + ], + }, + { + date: '2024-01-04T16:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user4', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'deathEvent', + valueId: 'mannerOfDeath', + value: 'NATURAL_CAUSES', + }, + ], + output: [ + { + valueCode: 'deathEvent', + valueId: 'mannerOfDeath', + value: 'ACCIDENT', + }, + ], + }, + ], + }) + + const result = transform(registration, deathResolver, 'death') + + const registerAction = result.actions.find( + (a) => a.type === 'REGISTER' + ) as Action + const corrections = result.actions.filter( + (a) => a.type === 'REQUEST_CORRECTION' + ) as Action[] + + assertEquals(corrections.length, 2) + + // First correction + assertEquals(corrections[0]?.declaration?.['deceased.nationality'], 'GBR') + assertEquals(corrections[0]?.annotation?.['deceased.nationality'], 'USA') + + // Second correction + assertEquals( + corrections[1]?.declaration?.['eventDetails.mannerOfDeath'], + 'ACCIDENT' + ) + assertEquals( + corrections[1]?.annotation?.['eventDetails.mannerOfDeath'], + 'NATURAL_CAUSES' + ) + + // REGISTER should have both original values + assertEquals(registerAction?.declaration?.['deceased.nationality'], 'USA') + assertEquals( + registerAction?.declaration?.['eventDetails.mannerOfDeath'], + 'NATURAL_CAUSES' + ) + } + ) +}) From a3a561365451154146a38cb21ccbabb82996089b Mon Sep 17 00:00:00 2001 From: Barry Dwyer Date: Tue, 2 Dec 2025 11:17:58 +0200 Subject: [PATCH 08/15] Add github action to run unit tests --- .github/workflows/test-migration.yml | 57 ++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 .github/workflows/test-migration.yml diff --git a/.github/workflows/test-migration.yml b/.github/workflows/test-migration.yml new file mode 100644 index 0000000..c801d8e --- /dev/null +++ b/.github/workflows/test-migration.yml @@ -0,0 +1,57 @@ +name: Test Data Migration + +on: + pull_request: + paths: + - 'v1-to-v2-data-migration/**' + - '.github/workflows/test-migration.yml' + +jobs: + test: + name: Run Migration Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Deno + uses: denoland/setup-deno@v1 + with: + deno-version: v1.x + + - name: Cache Deno dependencies + uses: actions/cache@v4 + with: + path: | + ~/.deno + ~/.cache/deno + key: ${{ runner.os }}-deno-${{ hashFiles('v1-to-v2-data-migration/tests/deno.lock') }} + restore-keys: | + ${{ runner.os }}-deno- + + - name: Run unit tests + working-directory: v1-to-v2-data-migration/tests + run: | + echo "Running unit tests..." + deno test unit/ --allow-all --coverage=coverage + + - name: Generate coverage report + working-directory: v1-to-v2-data-migration/tests + run: | + deno coverage coverage --lcov --output=coverage.lcov + + - name: Test Summary + if: always() + run: | + echo "## Test Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "✅ Unit tests completed" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Tests Included" >> $GITHUB_STEP_SUMMARY + echo "- Birth resolver tests" >> $GITHUB_STEP_SUMMARY + echo "- Death resolver tests" >> $GITHUB_STEP_SUMMARY + echo "- Action mapping tests" >> $GITHUB_STEP_SUMMARY + echo "- Corrections tests" >> $GITHUB_STEP_SUMMARY + echo "- Transform function tests" >> $GITHUB_STEP_SUMMARY + echo "- Post-process tests" >> $GITHUB_STEP_SUMMARY From 71b2bb5efdd3b801b4f18aef637c789d47d2114f Mon Sep 17 00:00:00 2001 From: Barry Dwyer Date: Tue, 2 Dec 2025 11:29:19 +0200 Subject: [PATCH 09/15] Update deno version in github action --- .github/workflows/test-migration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-migration.yml b/.github/workflows/test-migration.yml index c801d8e..befcda7 100644 --- a/.github/workflows/test-migration.yml +++ b/.github/workflows/test-migration.yml @@ -18,7 +18,7 @@ jobs: - name: Setup Deno uses: denoland/setup-deno@v1 with: - deno-version: v1.x + deno-version: v2.x - name: Cache Deno dependencies uses: actions/cache@v4 From 80d920434c431c4d957531f94cd5599b8c9bba85 Mon Sep 17 00:00:00 2001 From: Barry Dwyer Date: Wed, 3 Dec 2025 14:12:33 +0200 Subject: [PATCH 10/15] Move country specific tests into folders and run separately --- .github/workflows/test-migration.yml | 31 +- .../countryData/addressResolver.ts | 4 +- .../tests/FAR/address.test.ts | 350 ++++++++++++++++++ .../tests/FAR/corrections.test.ts | 320 ++++++++++++++++ v1-to-v2-data-migration/tests/deno.json | 1 + .../tests/integration/roundtrip.test.ts | 2 +- v1-to-v2-data-migration/tests/run-tests.ts | 60 +++ .../tests/unit/actionMapping.test.ts | 2 +- .../tests/unit/birthResolver.test.ts | 206 +---------- .../tests/unit/corrections.test.ts | 310 +--------------- .../tests/unit/deathResolver.test.ts | 145 +------- .../tests/unit/postProcess.test.ts | 96 +---- .../{data-generators.ts => dataGenerators.ts} | 0 .../tests/utils/phoneBuilder.ts | 38 ++ .../utils/{test-helpers.ts => testHelpers.ts} | 25 +- 15 files changed, 825 insertions(+), 765 deletions(-) create mode 100644 v1-to-v2-data-migration/tests/FAR/address.test.ts create mode 100644 v1-to-v2-data-migration/tests/FAR/corrections.test.ts create mode 100644 v1-to-v2-data-migration/tests/run-tests.ts rename v1-to-v2-data-migration/tests/utils/{data-generators.ts => dataGenerators.ts} (100%) create mode 100644 v1-to-v2-data-migration/tests/utils/phoneBuilder.ts rename v1-to-v2-data-migration/tests/utils/{test-helpers.ts => testHelpers.ts} (87%) diff --git a/.github/workflows/test-migration.yml b/.github/workflows/test-migration.yml index befcda7..ce20dd4 100644 --- a/.github/workflows/test-migration.yml +++ b/.github/workflows/test-migration.yml @@ -30,28 +30,37 @@ jobs: restore-keys: | ${{ runner.os }}-deno- - - name: Run unit tests + - name: Detect country and run tests working-directory: v1-to-v2-data-migration/tests run: | - echo "Running unit tests..." - deno test unit/ --allow-all --coverage=coverage - - - name: Generate coverage report - working-directory: v1-to-v2-data-migration/tests - run: | - deno coverage coverage --lcov --output=coverage.lcov + echo "🧪 Running migration tests with automatic country detection..." + deno task test - name: Test Summary if: always() + working-directory: v1-to-v2-data-migration/tests run: | - echo "## Test Results" >> $GITHUB_STEP_SUMMARY + # Extract country code from addressResolver.ts + COUNTRY=$(deno eval "import { COUNTRY_CODE } from '../countryData/addressResolver.ts'; console.log(COUNTRY_CODE)") + + echo "## 🧪 Migration Test Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Branch:** \`${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY + echo "**Country:** \`$COUNTRY\`" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "✅ Unit tests completed" >> $GITHUB_STEP_SUMMARY + echo "### 📦 Test Suites Executed" >> $GITHUB_STEP_SUMMARY + echo "- ✅ Common unit tests (\`unit/\`)" >> $GITHUB_STEP_SUMMARY + echo "- ✅ Country-specific tests (\`$COUNTRY/\`)" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "### Tests Included" >> $GITHUB_STEP_SUMMARY + echo "### 🎯 Tests Included" >> $GITHUB_STEP_SUMMARY + echo "**Common Tests:**" >> $GITHUB_STEP_SUMMARY echo "- Birth resolver tests" >> $GITHUB_STEP_SUMMARY echo "- Death resolver tests" >> $GITHUB_STEP_SUMMARY echo "- Action mapping tests" >> $GITHUB_STEP_SUMMARY echo "- Corrections tests" >> $GITHUB_STEP_SUMMARY echo "- Transform function tests" >> $GITHUB_STEP_SUMMARY echo "- Post-process tests" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Country-Specific Tests:**" >> $GITHUB_STEP_SUMMARY + echo "- Address resolution tests" >> $GITHUB_STEP_SUMMARY + echo "- Address corrections tests" >> $GITHUB_STEP_SUMMARY diff --git a/v1-to-v2-data-migration/countryData/addressResolver.ts b/v1-to-v2-data-migration/countryData/addressResolver.ts index 0d3d41c..d0e60ec 100644 --- a/v1-to-v2-data-migration/countryData/addressResolver.ts +++ b/v1-to-v2-data-migration/countryData/addressResolver.ts @@ -38,7 +38,9 @@ export function resolveAddress( if (!address) { return null } - const lines = address.line.filter(Boolean).filter((line) => !['URBAN', 'RURAL'].includes(line)) + const lines = address.line + .filter(Boolean) + .filter((line) => !['URBAN', 'RURAL'].includes(line)) const international = address.country !== COUNTRY_CODE if (international) { return { diff --git a/v1-to-v2-data-migration/tests/FAR/address.test.ts b/v1-to-v2-data-migration/tests/FAR/address.test.ts new file mode 100644 index 0000000..a1731db --- /dev/null +++ b/v1-to-v2-data-migration/tests/FAR/address.test.ts @@ -0,0 +1,350 @@ +import { transform } from '../../helpers/transform.ts' +import { assertEquals } from 'https://deno.land/std@0.210.0/assert/mod.ts' +import { + buildBirthResolver, + buildBirthEventRegistration, + buildDeathResolver, + buildDeathEventRegistration, +} from '../utils/testHelpers.ts' + +// Construct resolvers as in migrate.ipynb +const birthResolver = buildBirthResolver() +const deathResolver = buildDeathResolver() + +Deno.test('FAR address tests - birth events', async (t) => { + await t.step( + 'should resolve child.birthLocation.privateHome for PRIVATE_HOME', + () => { + const registration = buildBirthEventRegistration({ + eventLocation: { + type: 'PRIVATE_HOME', + address: { + line: ['123', 'Main St', 'City', '', '', 'URBAN'], + district: 'District1', + state: 'State1', + city: 'City1', + country: 'FAR', + postalCode: '12345', + }, + }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['child.birthLocation.privateHome'] + ?.administrativeArea, + 'District1' + ) + assertEquals( + declareAction?.declaration['child.birthLocation.privateHome']?.country, + 'FAR' + ) + } + ) + + await t.step('should resolve child.birthLocation.other for OTHER', () => { + const registration = buildBirthEventRegistration({ + eventLocation: { + type: 'OTHER', + address: { + line: ['456', 'Other St', 'Town'], + district: 'District2', + state: 'State2', + city: 'City2', + country: 'FAR', + postalCode: '54321', + }, + }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['child.birthLocation.other'] + ?.administrativeArea, + 'District2' + ) + }) + + await t.step('should resolve mother.address', () => { + const registration = buildBirthEventRegistration({ + mother: { + address: [ + { + type: 'PRIMARY_ADDRESS', + line: ['10', 'Mother St', 'City', '', '', 'URBAN'], + country: 'FAR', + district: 'District1', + state: 'State1', + city: 'City1', + postalCode: '11111', + }, + ], + }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['mother.address']?.country, 'FAR') + assertEquals( + declareAction?.declaration['mother.address']?.administrativeArea, + 'District1' + ) + }) + + await t.step('should resolve father.address', () => { + const registration = buildBirthEventRegistration({ + father: { + address: [ + { + type: 'PRIMARY_ADDRESS', + line: ['20', 'Father Ave', 'Town'], + country: 'FAR', + district: 'District2', + state: 'State2', + city: 'City2', + postalCode: '22222', + }, + ], + }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['father.address']?.country, 'FAR') + assertEquals( + declareAction?.declaration['father.address']?.administrativeArea, + 'District2' + ) + }) + + await t.step( + 'should resolve father.addressSameAs when addresses match', + () => { + const sameAddress = { + type: 'PRIMARY_ADDRESS' as const, + line: ['10', 'Same St', 'City'], + country: 'FAR', + district: 'District1', + state: 'State1', + city: 'City1', + postalCode: '11111', + } + + const registration = buildBirthEventRegistration({ + mother: { address: [sameAddress] }, + father: { address: [sameAddress] }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['father.addressSameAs'], 'YES') + } + ) + + await t.step( + 'should resolve father.addressSameAs when addresses differ', + () => { + const registration = buildBirthEventRegistration({ + mother: { + address: [ + { + type: 'PRIMARY_ADDRESS', + line: ['10', 'Mother St'], + country: 'FAR', + district: 'District1', + }, + ], + }, + father: { + address: [ + { + type: 'PRIMARY_ADDRESS', + line: ['20', 'Father Ave'], + country: 'FAR', + district: 'District2', + }, + ], + }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['father.addressSameAs'], 'NO') + } + ) + + await t.step( + 'should resolve informant.address for non-special informant', + () => { + const registration = buildBirthEventRegistration({ + informant: { + relationship: 'GRANDFATHER', + address: [ + { + type: 'PRIMARY_ADDRESS', + line: ['30', 'Informant Rd'], + country: 'FAR', + district: 'District3', + state: 'State3', + city: 'City3', + postalCode: '33333', + }, + ], + }, + }) + + const result = transform(registration, birthResolver, 'birth') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['informant.address']?.administrativeArea, + 'District3' + ) + } + ) +}) + +Deno.test('FAR address tests - death events', async (t) => { + await t.step('should resolve deceased.address', () => { + const data = buildDeathEventRegistration() + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['deceased.address']?.country, 'FAR') + assertEquals( + declareAction?.declaration['deceased.address']?.administrativeArea, + 'District1' + ) + }) + + await t.step( + 'should resolve eventDetails.deathLocationOther for OTHER', + () => { + const data = buildDeathEventRegistration({ + eventLocation: { + id: 'other-location', + type: 'OTHER', + address: { + line: ['999 Death St'], + district: 'DistrictX', + state: 'StateX', + country: 'FAR', + }, + }, + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['eventDetails.deathLocationOther']?.country, + 'FAR' + ) + } + ) + + await t.step( + 'should resolve informant.addressSameAs when addresses match', + () => { + const sharedAddress = { + type: 'PRIMARY_ADDRESS', + line: ['Same St'], + district: 'District1', + state: 'State1', + country: 'FAR', + } + const data = buildDeathEventRegistration({ + deceased: { + ...buildDeathEventRegistration().deceased!, + address: [sharedAddress], + }, + informant: { + ...buildDeathEventRegistration().informant!, + address: [sharedAddress], + }, + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['informant.addressSameAs'], 'YES') + } + ) + + await t.step( + 'should resolve informant.addressSameAs when addresses differ', + () => { + const data = buildDeathEventRegistration() + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['informant.addressSameAs'], 'NO') + } + ) + + await t.step( + 'should resolve informant.address for non-special informant', + () => { + const data = buildDeathEventRegistration() + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['informant.address']?.country, + 'FAR' + ) + } + ) + + await t.step('should resolve spouse.address', () => { + const data = buildDeathEventRegistration() + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['spouse.address']?.country, 'FAR') + }) + + await t.step( + 'should resolve spouse.addressSameAs when addresses match', + () => { + const sharedAddress = { + type: 'PRIMARY_ADDRESS', + line: ['Same St'], + district: 'District1', + state: 'State1', + country: 'FAR', + } + const data = buildDeathEventRegistration({ + deceased: { + ...buildDeathEventRegistration().deceased!, + address: [sharedAddress], + }, + spouse: { + ...buildDeathEventRegistration().spouse!, + address: [sharedAddress], + }, + }) + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['spouse.addressSameAs'], 'YES') + } + ) + + await t.step( + 'should resolve spouse.addressSameAs when addresses differ', + () => { + const data = buildDeathEventRegistration() + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['spouse.addressSameAs'], 'NO') + } + ) +}) diff --git a/v1-to-v2-data-migration/tests/FAR/corrections.test.ts b/v1-to-v2-data-migration/tests/FAR/corrections.test.ts new file mode 100644 index 0000000..0c5f1f5 --- /dev/null +++ b/v1-to-v2-data-migration/tests/FAR/corrections.test.ts @@ -0,0 +1,320 @@ +import { transform } from '../../helpers/transform.ts' +import { + buildBirthResolver, + buildDeathResolver, + buildBirthEventRegistration, + buildDeathEventRegistration, +} from '../utils/testHelpers.ts' +import { assertEquals } from 'https://deno.land/std@0.210.0/assert/mod.ts' + +Deno.test('FAR Corrections - Address Fields', async (t) => { + const birthResolver = buildBirthResolver() + const deathResolver = buildDeathResolver() + + await t.step('should transform address field corrections in birth', () => { + const registration = buildBirthEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'mother', + valueId: 'internationalStatePrimaryMother', + value: 'OldState', + }, + { + valueCode: 'mother', + valueId: 'internationalCityPrimaryMother', + value: 'OldCity', + }, + ], + output: [ + { + valueCode: 'mother', + valueId: 'internationalStatePrimaryMother', + value: 'NewState', + }, + { + valueCode: 'mother', + valueId: 'internationalCityPrimaryMother', + value: 'NewCity', + }, + { + valueCode: 'mother', + valueId: 'internationalAddressLine1PrimaryMother', + value: '123 Main St', + }, + ], + }, + ], + }) + + const result = transform(registration, birthResolver, 'birth') + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) + + assertEquals( + correctionAction?.declaration?.['mother.address']?.streetLevelDetails, + { + state: 'NewState', + cityOrTown: 'NewCity', + addressLine1: '123 Main St', + } + ) + assertEquals( + correctionAction?.annotation?.['mother.address']?.streetLevelDetails + ?.state, + 'OldState' + ) + assertEquals( + correctionAction?.annotation?.['mother.address']?.streetLevelDetails + ?.cityOrTown, + 'OldCity' + ) + }) + + await t.step('should transform death location address corrections', () => { + const registration = buildDeathEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'deathEvent', + valueId: 'internationalStatePlaceofdeath', + value: 'OldState', + }, + { + valueCode: 'deathEvent', + valueId: 'internationalCityPlaceofdeath', + value: 'OldCity', + }, + ], + output: [ + { + valueCode: 'deathEvent', + valueId: 'internationalStatePlaceofdeath', + value: 'NewState', + }, + { + valueCode: 'deathEvent', + valueId: 'internationalCityPlaceofdeath', + value: 'NewCity', + }, + ], + }, + ], + }) + + const result = transform(registration, deathResolver, 'death') + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) + + assertEquals( + correctionAction?.declaration?.['eventDetails.deathLocationOther'] + ?.streetLevelDetails?.state, + 'NewState' + ) + assertEquals( + correctionAction?.declaration?.['eventDetails.deathLocationOther'] + ?.streetLevelDetails?.cityOrTown, + 'NewCity' + ) + assertEquals( + correctionAction?.annotation?.['eventDetails.deathLocationOther'] + ?.streetLevelDetails?.state, + 'OldState' + ) + assertEquals( + correctionAction?.annotation?.['eventDetails.deathLocationOther'] + ?.streetLevelDetails?.cityOrTown, + 'OldCity' + ) + }) + + await t.step( + 'should transform informant address corrections in death', + () => { + const registration = buildDeathEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'informant', + valueId: 'internationalStatePrimaryInformant', + value: 'OldState', + }, + { + valueCode: 'informant', + valueId: 'internationalDistrictPrimaryInformant', + value: 'OldDistrict', + }, + { + valueCode: 'informant', + valueId: 'internationalCityPrimaryInformant', + value: 'OldCity', + }, + { + valueCode: 'informant', + valueId: 'internationalPostalCodePrimaryInformant', + value: '12345', + }, + ], + output: [ + { + valueCode: 'informant', + valueId: 'internationalStatePrimaryInformant', + value: 'NewState', + }, + { + valueCode: 'informant', + valueId: 'internationalDistrictPrimaryInformant', + value: 'NewDistrict', + }, + { + valueCode: 'informant', + valueId: 'internationalCityPrimaryInformant', + value: 'NewCity', + }, + { + valueCode: 'informant', + valueId: 'internationalPostalCodePrimaryInformant', + value: '54321', + }, + ], + }, + ], + }) + + const result = transform(registration, deathResolver, 'death') + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) + + // Check individual fields to avoid default field interference + assertEquals( + correctionAction?.declaration?.['informant.address']?.streetLevelDetails + ?.state, + 'NewState' + ) + assertEquals( + correctionAction?.declaration?.['informant.address']?.streetLevelDetails + ?.district2, + 'NewDistrict' + ) + assertEquals( + correctionAction?.declaration?.['informant.address']?.streetLevelDetails + ?.cityOrTown, + 'NewCity' + ) + assertEquals( + correctionAction?.declaration?.['informant.address']?.streetLevelDetails + ?.postcodeOrZip, + '54321' + ) + assertEquals( + correctionAction?.annotation?.['informant.address']?.streetLevelDetails + ?.state, + 'OldState' + ) + assertEquals( + correctionAction?.annotation?.['informant.address']?.streetLevelDetails + ?.district2, + 'OldDistrict' + ) + assertEquals( + correctionAction?.annotation?.['informant.address']?.streetLevelDetails + ?.cityOrTown, + 'OldCity' + ) + assertEquals( + correctionAction?.annotation?.['informant.address']?.streetLevelDetails + ?.postcodeOrZip, + '12345' + ) + } + ) + + await t.step( + 'should preserve addressType when correcting address fields', + () => { + const registration = buildBirthEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'father', + valueId: 'internationalStatePrimaryFather', + value: 'OldState', + }, + ], + output: [ + { + valueCode: 'father', + valueId: 'internationalStatePrimaryFather', + value: 'NewState', + }, + ], + }, + ], + }) + + const result = transform(registration, birthResolver, 'birth') + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) + + // Should have addressType set based on country/international fields + assertEquals( + correctionAction?.declaration?.['father.address']?.addressType !== + undefined, + true + ) + assertEquals( + correctionAction?.declaration?.['father.address']?.streetLevelDetails + ?.state, + 'NewState' + ) + } + ) +}) diff --git a/v1-to-v2-data-migration/tests/deno.json b/v1-to-v2-data-migration/tests/deno.json index d8a5029..cd03b6f 100644 --- a/v1-to-v2-data-migration/tests/deno.json +++ b/v1-to-v2-data-migration/tests/deno.json @@ -15,6 +15,7 @@ "graphql": "npm:graphql@^16.0.0" }, "tasks": { + "test": "deno run --allow-read --allow-env --allow-run run-tests.ts", "migration": "deno test --allow-env --allow-run --allow-net --allow-write ./integration/roundtrip.test.ts", "transform": "deno test --allow-env --allow-run --allow-net --allow-write ./unit/*.test.ts" } diff --git a/v1-to-v2-data-migration/tests/integration/roundtrip.test.ts b/v1-to-v2-data-migration/tests/integration/roundtrip.test.ts index 1decac2..0c1373e 100644 --- a/v1-to-v2-data-migration/tests/integration/roundtrip.test.ts +++ b/v1-to-v2-data-migration/tests/integration/roundtrip.test.ts @@ -1,5 +1,5 @@ import { expect } from 'jsr:@std/expect' -import { generateBirthRegistration } from '../utils/data-generators.ts' +import { generateBirthRegistration } from '../utils/dataGenerators.ts' import { authenticate } from '../../helpers/authentication.ts' import { GATEWAY } from '../../helpers/routes.ts' import { createDeclaration, getEvent, runMigration } from '../utils.ts' diff --git a/v1-to-v2-data-migration/tests/run-tests.ts b/v1-to-v2-data-migration/tests/run-tests.ts new file mode 100644 index 0000000..1c2f3ce --- /dev/null +++ b/v1-to-v2-data-migration/tests/run-tests.ts @@ -0,0 +1,60 @@ +/** + * Test Runner Script + * + * Automatically detects the country code and runs: + * 1. All unit tests (common tests) + * 2. Country-specific tests for the current branch + */ + +import { COUNTRY_CODE } from '../countryData/addressResolver.ts' + +const TESTS_DIR = new URL('.', import.meta.url).pathname + +async function runTests() { + console.log('🧪 OpenCRVS Data Migration Test Runner\n') + console.log(`📍 Detected Country: ${COUNTRY_CODE}`) + console.log('━'.repeat(50)) + + // Build the test command + const testPaths = [ + 'unit/', // Always run common unit tests + `${COUNTRY_CODE}/`, // Run country-specific tests + ] + + console.log('\n📦 Test Suites:') + console.log(` ✓ Common tests (unit/)`) + console.log(` ✓ Country-specific tests (${COUNTRY_CODE}/)`) + console.log('━'.repeat(50) + '\n') + + // Create Deno test command + const cmd = new Deno.Command('deno', { + args: [ + 'test', + ...testPaths, + '--allow-read', + '--allow-env', + '--allow-net', + '--allow-sys', + ], + cwd: TESTS_DIR, + stdout: 'inherit', + stderr: 'inherit', + }) + + const { code } = await cmd.output() + + if (code === 0) { + console.log('\n' + '━'.repeat(50)) + console.log('✅ All tests passed!') + console.log('━'.repeat(50)) + } else { + console.log('\n' + '━'.repeat(50)) + console.log('❌ Some tests failed') + console.log('━'.repeat(50)) + Deno.exit(code) + } +} + +if (import.meta.main) { + await runTests() +} diff --git a/v1-to-v2-data-migration/tests/unit/actionMapping.test.ts b/v1-to-v2-data-migration/tests/unit/actionMapping.test.ts index 5cea9fb..eaaa2f0 100644 --- a/v1-to-v2-data-migration/tests/unit/actionMapping.test.ts +++ b/v1-to-v2-data-migration/tests/unit/actionMapping.test.ts @@ -3,7 +3,7 @@ import { assertEquals } from 'https://deno.land/std@0.210.0/assert/mod.ts' import { buildHistoryItem, buildSimpleEventRegistration as buildEventRegistration, -} from '../utils/test-helpers.ts' +} from '../utils/testHelpers.ts' Deno.test('transform - basic action type mappings', async (t) => { await t.step( diff --git a/v1-to-v2-data-migration/tests/unit/birthResolver.test.ts b/v1-to-v2-data-migration/tests/unit/birthResolver.test.ts index ae0cafe..913cd49 100644 --- a/v1-to-v2-data-migration/tests/unit/birthResolver.test.ts +++ b/v1-to-v2-data-migration/tests/unit/birthResolver.test.ts @@ -3,7 +3,8 @@ import { assertEquals } from 'https://deno.land/std@0.210.0/assert/mod.ts' import { buildBirthResolver, buildBirthEventRegistration, -} from '../utils/test-helpers.ts' +} from '../utils/testHelpers.ts' +import { buildPhoneNumber, stripCountryCode } from '../utils/phoneBuilder.ts' // Construct birthResolver as in migrate.ipynb const birthResolver = buildBirthResolver() @@ -79,63 +80,6 @@ Deno.test('birthResolver - child fields', async (t) => { assertEquals(declareAction?.declaration['child.birthLocation'], 'facility1') }) - await t.step( - 'should resolve child.birthLocation.privateHome for PRIVATE_HOME', - () => { - const registration = buildBirthEventRegistration({ - eventLocation: { - type: 'PRIVATE_HOME', - address: { - line: ['123', 'Main St', 'City', '', '', 'URBAN'], - district: 'District1', - state: 'State1', - city: 'City1', - country: 'FAR', - postalCode: '12345', - }, - }, - }) - - const result = transform(registration, birthResolver, 'birth') - const declareAction = result.actions.find((a) => a.type === 'DECLARE') - - assertEquals( - declareAction?.declaration['child.birthLocation.privateHome'] - ?.administrativeArea, - 'District1' - ) - assertEquals( - declareAction?.declaration['child.birthLocation.privateHome']?.country, - 'FAR' - ) - } - ) - - await t.step('should resolve child.birthLocation.other for OTHER', () => { - const registration = buildBirthEventRegistration({ - eventLocation: { - type: 'OTHER', - address: { - line: ['456', 'Other St', 'Town'], - district: 'District2', - state: 'State2', - city: 'City2', - country: 'FAR', - postalCode: '54321', - }, - }, - }) - - const result = transform(registration, birthResolver, 'birth') - const declareAction = result.actions.find((a) => a.type === 'DECLARE') - - assertEquals( - declareAction?.declaration['child.birthLocation.other'] - ?.administrativeArea, - 'District2' - ) - }) - await t.step('should resolve child.attendantAtBirth', () => { const registration = buildBirthEventRegistration({ attendantAtBirth: 'PHYSICIAN', @@ -360,33 +304,6 @@ Deno.test('birthResolver - mother fields', async (t) => { assertEquals(declareAction?.declaration['mother.previousBirths'], 2) }) - await t.step('should resolve mother.address', () => { - const registration = buildBirthEventRegistration({ - mother: { - address: [ - { - type: 'PRIMARY_ADDRESS', - line: ['10', 'Mother St', 'City', '', '', 'URBAN'], - country: 'FAR', - district: 'District1', - state: 'State1', - city: 'City1', - postalCode: '11111', - }, - ], - }, - }) - - const result = transform(registration, birthResolver, 'birth') - const declareAction = result.actions.find((a) => a.type === 'DECLARE') - - assertEquals(declareAction?.declaration['mother.address']?.country, 'FAR') - assertEquals( - declareAction?.declaration['mother.address']?.administrativeArea, - 'District1' - ) - }) - await t.step('should resolve mother.brn', () => { const registration = buildBirthEventRegistration({ mother: { @@ -588,91 +505,6 @@ Deno.test('birthResolver - father fields', async (t) => { assertEquals(declareAction?.declaration['father.occupation'], 'Engineer') }) - await t.step('should resolve father.address', () => { - const registration = buildBirthEventRegistration({ - father: { - address: [ - { - type: 'PRIMARY_ADDRESS', - line: ['20', 'Father Ave', 'Town'], - country: 'FAR', - district: 'District2', - state: 'State2', - city: 'City2', - postalCode: '22222', - }, - ], - }, - }) - - const result = transform(registration, birthResolver, 'birth') - const declareAction = result.actions.find((a) => a.type === 'DECLARE') - - assertEquals(declareAction?.declaration['father.address']?.country, 'FAR') - assertEquals( - declareAction?.declaration['father.address']?.administrativeArea, - 'District2' - ) - }) - - await t.step( - 'should resolve father.addressSameAs when addresses match', - () => { - const sameAddress = { - type: 'PRIMARY_ADDRESS' as const, - line: ['10', 'Same St', 'City'], - country: 'FAR', - district: 'District1', - state: 'State1', - city: 'City1', - postalCode: '11111', - } - - const registration = buildBirthEventRegistration({ - mother: { address: [sameAddress] }, - father: { address: [sameAddress] }, - }) - - const result = transform(registration, birthResolver, 'birth') - const declareAction = result.actions.find((a) => a.type === 'DECLARE') - - assertEquals(declareAction?.declaration['father.addressSameAs'], 'YES') - } - ) - - await t.step( - 'should resolve father.addressSameAs when addresses differ', - () => { - const registration = buildBirthEventRegistration({ - mother: { - address: [ - { - type: 'PRIMARY_ADDRESS', - line: ['10', 'Mother St'], - country: 'FAR', - district: 'District1', - }, - ], - }, - father: { - address: [ - { - type: 'PRIMARY_ADDRESS', - line: ['20', 'Father Ave'], - country: 'FAR', - district: 'District2', - }, - ], - }, - }) - - const result = transform(registration, birthResolver, 'birth') - const declareAction = result.actions.find((a) => a.type === 'DECLARE') - - assertEquals(declareAction?.declaration['father.addressSameAs'], 'NO') - } - ) - await t.step('should resolve father.brn', () => { const registration = buildBirthEventRegistration({ father: { @@ -774,49 +606,21 @@ Deno.test('birthResolver - informant fields', async (t) => { assertEquals(declareAction?.declaration['informant.dob'], undefined) }) - await t.step( - 'should resolve informant.address for non-special informant', - () => { - const registration = buildBirthEventRegistration({ - informant: { - relationship: 'GRANDFATHER', - address: [ - { - type: 'PRIMARY_ADDRESS', - line: ['30', 'Informant Rd'], - country: 'FAR', - district: 'District3', - state: 'State3', - city: 'City3', - postalCode: '33333', - }, - ], - }, - }) - - const result = transform(registration, birthResolver, 'birth') - const declareAction = result.actions.find((a) => a.type === 'DECLARE') - - assertEquals( - declareAction?.declaration['informant.address']?.administrativeArea, - 'District3' - ) - } - ) - await t.step( 'should resolve informant.phoneNo with country code stripped', () => { + const fullPhoneNumber = buildPhoneNumber('0987654321') const registration = buildBirthEventRegistration({ registration: { trackingId: 'B123456', - contactPhoneNumber: '+260987654321', + contactPhoneNumber: fullPhoneNumber, }, }) const result = transform(registration, birthResolver, 'birth') const declareAction = result.actions.find((a) => a.type === 'DECLARE') + // Should strip country code and return local number with leading zero assertEquals( declareAction?.declaration['informant.phoneNo'], '0987654321' diff --git a/v1-to-v2-data-migration/tests/unit/corrections.test.ts b/v1-to-v2-data-migration/tests/unit/corrections.test.ts index 4b10c16..b551f96 100644 --- a/v1-to-v2-data-migration/tests/unit/corrections.test.ts +++ b/v1-to-v2-data-migration/tests/unit/corrections.test.ts @@ -4,9 +4,8 @@ import { buildDeathResolver, buildBirthEventRegistration, buildDeathEventRegistration, -} from '../utils/test-helpers.ts' +} from '../utils/testHelpers.ts' import { assertEquals } from 'https://deno.land/std@0.210.0/assert/mod.ts' -import type { EventRegistration, HistoryItem } from '../../helpers/types.ts' Deno.test('Corrections - Birth', async (t) => { const birthResolver = buildBirthResolver() @@ -175,78 +174,6 @@ Deno.test('Corrections - Birth', async (t) => { }) }) - await t.step('should transform address field corrections', () => { - const registration = buildBirthEventRegistration({ - history: [ - { - date: '2024-01-01T10:00:00Z', - regStatus: 'DECLARED', - user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, - office: { id: 'office1' }, - }, - { - date: '2024-01-02T12:00:00Z', - action: 'REQUESTED_CORRECTION', - user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, - office: { id: 'office1' }, - input: [ - { - valueCode: 'mother', - valueId: 'internationalStatePrimaryMother', - value: 'OldState', - }, - { - valueCode: 'mother', - valueId: 'internationalCityPrimaryMother', - value: 'OldCity', - }, - ], - output: [ - { - valueCode: 'mother', - valueId: 'internationalStatePrimaryMother', - value: 'NewState', - }, - { - valueCode: 'mother', - valueId: 'internationalCityPrimaryMother', - value: 'NewCity', - }, - { - valueCode: 'mother', - valueId: 'internationalAddressLine1PrimaryMother', - value: '123 Main St', - }, - ], - }, - ], - }) - - const result = transform(registration, birthResolver, 'birth') - const correctionAction = result.actions.find( - (a) => a.type === 'REQUEST_CORRECTION' - ) - - assertEquals( - correctionAction?.declaration?.['mother.address']?.streetLevelDetails, - { - state: 'NewState', - cityOrTown: 'NewCity', - addressLine1: '123 Main St', - } - ) - assertEquals( - correctionAction?.annotation?.['mother.address']?.streetLevelDetails - ?.state, - 'OldState' - ) - assertEquals( - correctionAction?.annotation?.['mother.address']?.streetLevelDetails - ?.cityOrTown, - 'OldCity' - ) - }) - await t.step('should transform multiple person field corrections', () => { const registration = buildBirthEventRegistration({ history: [ @@ -1329,75 +1256,6 @@ Deno.test('Corrections - Death', async (t) => { ) }) - await t.step('should transform death location address corrections', () => { - const registration = buildDeathEventRegistration({ - history: [ - { - date: '2024-01-01T10:00:00Z', - regStatus: 'DECLARED', - user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, - office: { id: 'office1' }, - }, - { - date: '2024-01-02T12:00:00Z', - action: 'REQUESTED_CORRECTION', - user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, - office: { id: 'office1' }, - input: [ - { - valueCode: 'deathEvent', - valueId: 'internationalStatePlaceofdeath', - value: 'OldState', - }, - { - valueCode: 'deathEvent', - valueId: 'internationalCityPlaceofdeath', - value: 'OldCity', - }, - ], - output: [ - { - valueCode: 'deathEvent', - valueId: 'internationalStatePlaceofdeath', - value: 'NewState', - }, - { - valueCode: 'deathEvent', - valueId: 'internationalCityPlaceofdeath', - value: 'NewCity', - }, - ], - }, - ], - }) - - const result = transform(registration, deathResolver, 'death') - const correctionAction = result.actions.find( - (a) => a.type === 'REQUEST_CORRECTION' - ) - - assertEquals( - correctionAction?.declaration?.['eventDetails.deathLocationOther'] - ?.streetLevelDetails?.state, - 'NewState' - ) - assertEquals( - correctionAction?.declaration?.['eventDetails.deathLocationOther'] - ?.streetLevelDetails?.cityOrTown, - 'NewCity' - ) - assertEquals( - correctionAction?.annotation?.['eventDetails.deathLocationOther'] - ?.streetLevelDetails?.state, - 'OldState' - ) - assertEquals( - correctionAction?.annotation?.['eventDetails.deathLocationOther'] - ?.streetLevelDetails?.cityOrTown, - 'OldCity' - ) - }) - await t.step('should transform informant corrections in death', () => { const registration = buildDeathEventRegistration({ history: [ @@ -1456,119 +1314,6 @@ Deno.test('Corrections - Death', async (t) => { ) }) - await t.step( - 'should transform informant address corrections in death', - () => { - const registration = buildDeathEventRegistration({ - history: [ - { - date: '2024-01-01T10:00:00Z', - regStatus: 'DECLARED', - user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, - office: { id: 'office1' }, - }, - { - date: '2024-01-02T12:00:00Z', - action: 'REQUESTED_CORRECTION', - user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, - office: { id: 'office1' }, - input: [ - { - valueCode: 'informant', - valueId: 'internationalStatePrimaryInformant', - value: 'OldState', - }, - { - valueCode: 'informant', - valueId: 'internationalDistrictPrimaryInformant', - value: 'OldDistrict', - }, - { - valueCode: 'informant', - valueId: 'internationalCityPrimaryInformant', - value: 'OldCity', - }, - { - valueCode: 'informant', - valueId: 'internationalPostalCodePrimaryInformant', - value: '12345', - }, - ], - output: [ - { - valueCode: 'informant', - valueId: 'internationalStatePrimaryInformant', - value: 'NewState', - }, - { - valueCode: 'informant', - valueId: 'internationalDistrictPrimaryInformant', - value: 'NewDistrict', - }, - { - valueCode: 'informant', - valueId: 'internationalCityPrimaryInformant', - value: 'NewCity', - }, - { - valueCode: 'informant', - valueId: 'internationalPostalCodePrimaryInformant', - value: '54321', - }, - ], - }, - ], - }) - - const result = transform(registration, deathResolver, 'death') - const correctionAction = result.actions.find( - (a) => a.type === 'REQUEST_CORRECTION' - ) - - // Check individual fields to avoid default field interference - assertEquals( - correctionAction?.declaration?.['informant.address']?.streetLevelDetails - ?.state, - 'NewState' - ) - assertEquals( - correctionAction?.declaration?.['informant.address']?.streetLevelDetails - ?.district2, - 'NewDistrict' - ) - assertEquals( - correctionAction?.declaration?.['informant.address']?.streetLevelDetails - ?.cityOrTown, - 'NewCity' - ) - assertEquals( - correctionAction?.declaration?.['informant.address']?.streetLevelDetails - ?.postcodeOrZip, - '54321' - ) - assertEquals( - correctionAction?.annotation?.['informant.address']?.streetLevelDetails - ?.state, - 'OldState' - ) - assertEquals( - correctionAction?.annotation?.['informant.address']?.streetLevelDetails - ?.district2, - 'OldDistrict' - ) - assertEquals( - correctionAction?.annotation?.['informant.address']?.streetLevelDetails - ?.cityOrTown, - 'OldCity' - ) - assertEquals( - correctionAction?.annotation?.['informant.address']?.streetLevelDetails - ?.postcodeOrZip, - '12345' - ) - } - ) - await t.step('should transform custom field corrections in death', () => { const registration = buildDeathEventRegistration({ history: [ @@ -1797,57 +1542,4 @@ Deno.test('Corrections - Edge Cases', async (t) => { ) } ) - - await t.step( - 'should preserve addressType when correcting address fields', - () => { - const registration = buildBirthEventRegistration({ - history: [ - { - date: '2024-01-01T10:00:00Z', - regStatus: 'DECLARED', - user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, - office: { id: 'office1' }, - }, - { - date: '2024-01-02T12:00:00Z', - action: 'REQUESTED_CORRECTION', - user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, - office: { id: 'office1' }, - input: [ - { - valueCode: 'father', - valueId: 'internationalStatePrimaryFather', - value: 'OldState', - }, - ], - output: [ - { - valueCode: 'father', - valueId: 'internationalStatePrimaryFather', - value: 'NewState', - }, - ], - }, - ], - }) - - const result = transform(registration, birthResolver, 'birth') - const correctionAction = result.actions.find( - (a) => a.type === 'REQUEST_CORRECTION' - ) - - // Should have addressType set based on country/international fields - assertEquals( - correctionAction?.declaration?.['father.address']?.addressType !== - undefined, - true - ) - assertEquals( - correctionAction?.declaration?.['father.address']?.streetLevelDetails - ?.state, - 'NewState' - ) - } - ) }) diff --git a/v1-to-v2-data-migration/tests/unit/deathResolver.test.ts b/v1-to-v2-data-migration/tests/unit/deathResolver.test.ts index 8688d35..5f700cf 100644 --- a/v1-to-v2-data-migration/tests/unit/deathResolver.test.ts +++ b/v1-to-v2-data-migration/tests/unit/deathResolver.test.ts @@ -3,7 +3,8 @@ import { transform } from '../../helpers/transform.ts' import { buildDeathResolver, buildDeathEventRegistration, -} from '../utils/test-helpers.ts' +} from '../utils/testHelpers.ts' +import { COUNTRY_CODE } from '../../countryData/addressResolver.ts' // Construct deathResolver as in migrate.ipynb const deathResolver = buildDeathResolver() @@ -71,7 +72,10 @@ Deno.test('deathResolver - deceased fields', async (t) => { const result = transform(data, deathResolver, 'death') const declareAction = result.actions.find((a) => a.type === 'DECLARE') - assertEquals(declareAction?.declaration['deceased.nationality'], 'FAR') + assertEquals( + declareAction?.declaration['deceased.nationality'], + COUNTRY_CODE + ) }) await t.step('should resolve deceased.idType from questionnaire', () => { @@ -149,18 +153,6 @@ Deno.test('deathResolver - deceased fields', async (t) => { assertEquals(declareAction?.declaration['deceased.numberOfDependants'], 3) }) - await t.step('should resolve deceased.address', () => { - const data = buildDeathEventRegistration() - const result = transform(data, deathResolver, 'death') - const declareAction = result.actions.find((a) => a.type === 'DECLARE') - - assertEquals(declareAction?.declaration['deceased.address']?.country, 'FAR') - assertEquals( - declareAction?.declaration['deceased.address']?.administrativeArea, - 'District1' - ) - }) - await t.step('should resolve deceased.verified from questionnaire', () => { const data = buildDeathEventRegistration({ questionnaire: [ @@ -313,72 +305,9 @@ Deno.test('deathResolver - eventDetails fields', async (t) => { ) } ) - - await t.step( - 'should resolve eventDetails.deathLocationOther for OTHER', - () => { - const data = buildDeathEventRegistration({ - eventLocation: { - id: 'other-location', - type: 'OTHER', - address: { - line: ['999 Death St'], - district: 'DistrictX', - state: 'StateX', - country: 'FAR', - }, - }, - }) - const result = transform(data, deathResolver, 'death') - const declareAction = result.actions.find((a) => a.type === 'DECLARE') - - assertEquals( - declareAction?.declaration['eventDetails.deathLocationOther']?.country, - 'FAR' - ) - } - ) }) Deno.test('deathResolver - informant fields', async (t) => { - await t.step( - 'should resolve informant.addressSameAs when addresses match', - () => { - const sharedAddress = { - type: 'PRIMARY_ADDRESS', - line: ['Same St'], - district: 'District1', - state: 'State1', - country: 'FAR', - } - const data = buildDeathEventRegistration({ - deceased: { - ...buildDeathEventRegistration().deceased!, - address: [sharedAddress], - }, - informant: { - ...buildDeathEventRegistration().informant!, - address: [sharedAddress], - }, - }) - const result = transform(data, deathResolver, 'death') - const declareAction = result.actions.find((a) => a.type === 'DECLARE') - - assertEquals(declareAction?.declaration['informant.addressSameAs'], 'YES') - } - ) - - await t.step( - 'should resolve informant.addressSameAs when addresses differ', - () => { - const data = buildDeathEventRegistration() - const result = transform(data, deathResolver, 'death') - const declareAction = result.actions.find((a) => a.type === 'DECLARE') - - assertEquals(declareAction?.declaration['informant.addressSameAs'], 'NO') - } - ) - await t.step('should resolve informant.idType from questionnaire', () => { const data = buildDeathEventRegistration({ questionnaire: [ @@ -415,20 +344,6 @@ Deno.test('deathResolver - informant fields', async (t) => { assertEquals(declareAction?.declaration['informant.dob'], undefined) }) - await t.step( - 'should resolve informant.address for non-special informant', - () => { - const data = buildDeathEventRegistration() - const result = transform(data, deathResolver, 'death') - const declareAction = result.actions.find((a) => a.type === 'DECLARE') - - assertEquals( - declareAction?.declaration['informant.address']?.country, - 'FAR' - ) - } - ) - await t.step( 'should resolve informant.phoneNo with country code stripped', () => { @@ -683,7 +598,7 @@ Deno.test('deathResolver - spouse fields', async (t) => { const result = transform(data, deathResolver, 'death') const declareAction = result.actions.find((a) => a.type === 'DECLARE') - assertEquals(declareAction?.declaration['spouse.nationality'], 'FAR') + assertEquals(declareAction?.declaration['spouse.nationality'], COUNTRY_CODE) }) await t.step('should resolve spouse.idType from questionnaire', () => { @@ -735,52 +650,6 @@ Deno.test('deathResolver - spouse fields', async (t) => { assertEquals(declareAction?.declaration['spouse.brn'], 'B456') }) - await t.step('should resolve spouse.address', () => { - const data = buildDeathEventRegistration() - const result = transform(data, deathResolver, 'death') - const declareAction = result.actions.find((a) => a.type === 'DECLARE') - - assertEquals(declareAction?.declaration['spouse.address']?.country, 'FAR') - }) - - await t.step( - 'should resolve spouse.addressSameAs when addresses match', - () => { - const sharedAddress = { - type: 'PRIMARY_ADDRESS', - line: ['Same St'], - district: 'District1', - state: 'State1', - country: 'FAR', - } - const data = buildDeathEventRegistration({ - deceased: { - ...buildDeathEventRegistration().deceased!, - address: [sharedAddress], - }, - spouse: { - ...buildDeathEventRegistration().spouse!, - address: [sharedAddress], - }, - }) - const result = transform(data, deathResolver, 'death') - const declareAction = result.actions.find((a) => a.type === 'DECLARE') - - assertEquals(declareAction?.declaration['spouse.addressSameAs'], 'YES') - } - ) - - await t.step( - 'should resolve spouse.addressSameAs when addresses differ', - () => { - const data = buildDeathEventRegistration() - const result = transform(data, deathResolver, 'death') - const declareAction = result.actions.find((a) => a.type === 'DECLARE') - - assertEquals(declareAction?.declaration['spouse.addressSameAs'], 'NO') - } - ) - await t.step('should resolve spouse.verified from questionnaire', () => { const data = buildDeathEventRegistration({ questionnaire: [ diff --git a/v1-to-v2-data-migration/tests/unit/postProcess.test.ts b/v1-to-v2-data-migration/tests/unit/postProcess.test.ts index 197a073..3185cae 100644 --- a/v1-to-v2-data-migration/tests/unit/postProcess.test.ts +++ b/v1-to-v2-data-migration/tests/unit/postProcess.test.ts @@ -4,7 +4,7 @@ import { buildBirthEventRegistration, buildDeathEventRegistration, buildDeathResolver, -} from '../utils/test-helpers.ts' +} from '../utils/testHelpers.ts' import { assertEquals } from 'https://deno.land/std@0.210.0/assert/mod.ts' import type { Action } from '../../helpers/types.ts' @@ -222,100 +222,6 @@ Deno.test('PostProcess - Single Correction', async (t) => { }) } ) - - await t.step('should handle correction with address fields', () => { - const registration = buildBirthEventRegistration({ - history: [ - { - date: '2024-01-01T10:00:00Z', - regStatus: 'DECLARED', - user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, - office: { id: 'office1' }, - }, - { - date: '2024-01-02T12:00:00Z', - regStatus: 'REGISTERED', - user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, - office: { id: 'office1' }, - }, - { - date: '2024-01-03T14:00:00Z', - action: 'REQUESTED_CORRECTION', - user: { id: 'user3', role: { id: 'REGISTRATION_AGENT' } }, - office: { id: 'office1' }, - input: [ - { - valueCode: 'mother', - valueId: 'internationalStatePrimaryMother', - value: 'OldState', - }, - { - valueCode: 'mother', - valueId: 'internationalCityPrimaryMother', - value: 'OldCity', - }, - ], - output: [ - { - valueCode: 'mother', - valueId: 'internationalStatePrimaryMother', - value: 'NewState', - }, - { - valueCode: 'mother', - valueId: 'internationalCityPrimaryMother', - value: 'NewCity', - }, - ], - }, - ], - }) - - const result = transform(registration, birthResolver, 'birth') - - const registerAction = result.actions.find( - (a) => a.type === 'REGISTER' - ) as Action - const correctionAction = result.actions.find( - (a) => a.type === 'REQUEST_CORRECTION' - ) as Action - - // Correction has new address - assertEquals( - correctionAction?.declaration?.['mother.address']?.streetLevelDetails - ?.state, - 'NewState' - ) - assertEquals( - correctionAction?.declaration?.['mother.address']?.streetLevelDetails - ?.cityOrTown, - 'NewCity' - ) - - // Annotation has old address - assertEquals( - correctionAction?.annotation?.['mother.address']?.streetLevelDetails - ?.state, - 'OldState' - ) - assertEquals( - correctionAction?.annotation?.['mother.address']?.streetLevelDetails - ?.cityOrTown, - 'OldCity' - ) - - // REGISTER action should now have old address - assertEquals( - registerAction?.declaration?.['mother.address']?.streetLevelDetails - ?.state, - 'OldState' - ) - assertEquals( - registerAction?.declaration?.['mother.address']?.streetLevelDetails - ?.cityOrTown, - 'OldCity' - ) - }) }) Deno.test('PostProcess - Multiple Corrections', async (t) => { diff --git a/v1-to-v2-data-migration/tests/utils/data-generators.ts b/v1-to-v2-data-migration/tests/utils/dataGenerators.ts similarity index 100% rename from v1-to-v2-data-migration/tests/utils/data-generators.ts rename to v1-to-v2-data-migration/tests/utils/dataGenerators.ts diff --git a/v1-to-v2-data-migration/tests/utils/phoneBuilder.ts b/v1-to-v2-data-migration/tests/utils/phoneBuilder.ts new file mode 100644 index 0000000..516628f --- /dev/null +++ b/v1-to-v2-data-migration/tests/utils/phoneBuilder.ts @@ -0,0 +1,38 @@ +import { COUNTRY_PHONE_CODE } from '../../countryData/addressResolver.ts' + +/** + * Builds a phone number with the country code prefix + * @param localNumber - The local phone number without country code (e.g., '0987654321') + * @returns Full phone number with country code (e.g., '+252987654321') + */ +export function buildPhoneNumber(localNumber: string): string { + // Remove leading zero if present, as international format doesn't use it + const numberWithoutZero = localNumber.startsWith('0') + ? localNumber.substring(1) + : localNumber + + return `${COUNTRY_PHONE_CODE}${numberWithoutZero}` +} + +/** + * Strips the country code from a phone number + * @param fullNumber - Phone number with country code (e.g., '+252987654321') + * @returns Local phone number with leading zero (e.g., '0987654321') + */ +export function stripCountryCode(fullNumber: string): string { + if (!fullNumber.startsWith(COUNTRY_PHONE_CODE)) { + return fullNumber + } + + const localPart = fullNumber.substring(COUNTRY_PHONE_CODE.length) + // Add leading zero if not present + return localPart.startsWith('0') ? localPart : `0${localPart}` +} + +/** + * Gets the current country phone code + * @returns Country phone code (e.g., '+252') + */ +export function getCountryPhoneCode(): string { + return COUNTRY_PHONE_CODE +} diff --git a/v1-to-v2-data-migration/tests/utils/test-helpers.ts b/v1-to-v2-data-migration/tests/utils/testHelpers.ts similarity index 87% rename from v1-to-v2-data-migration/tests/utils/test-helpers.ts rename to v1-to-v2-data-migration/tests/utils/testHelpers.ts index 01063e8..d3a6aeb 100644 --- a/v1-to-v2-data-migration/tests/utils/test-helpers.ts +++ b/v1-to-v2-data-migration/tests/utils/testHelpers.ts @@ -4,6 +4,8 @@ import defaultResolvers, { } from '../../helpers/defaultResolvers.ts' import { countryResolver } from '../../countryData/countryResolvers.ts' import type { EventRegistration, HistoryItem } from '../../helpers/types.ts' +import { buildPhoneNumber } from './phoneBuilder.ts' +import { COUNTRY_CODE } from '../../countryData/addressResolver.ts' /** * Build a birth resolver with all default and country resolvers @@ -23,6 +25,7 @@ export function buildDeathResolver() { /** * Build a basic EventRegistration for birth tests with sensible defaults + * Includes custom fields that country resolvers might expect */ export function buildBirthEventRegistration( overrides?: Partial @@ -32,7 +35,7 @@ export function buildBirthEventRegistration( registration: { trackingId: 'B123456', registrationNumber: '2024B123456', - contactPhoneNumber: '+2600987654321', + contactPhoneNumber: buildPhoneNumber('0987654321'), contactEmail: 'test@example.com', informantsSignature: 'data:image/png;base64,abc123', }, @@ -44,6 +47,7 @@ export function buildBirthEventRegistration( office: { id: 'office1' }, }, ], + questionnaire: [], ...overrides, } } @@ -62,7 +66,7 @@ export function buildDeathEventRegistration( gender: 'male', birthDate: '1950-01-01', identifier: [{ type: 'NATIONAL_ID', id: 'DEC123456' }], - nationality: ['FAR'], + nationality: [COUNTRY_CODE], maritalStatus: 'MARRIED', address: [ { @@ -70,7 +74,7 @@ export function buildDeathEventRegistration( line: ['123 Main St', 'Apt 4'], district: 'District1', state: 'State1', - country: 'FAR', + country: COUNTRY_CODE, }, ], deceased: { @@ -90,7 +94,7 @@ export function buildDeathEventRegistration( line: ['456 Oak Ave'], district: 'District2', state: 'State2', - country: 'FAR', + country: COUNTRY_CODE, }, ], }, @@ -99,7 +103,7 @@ export function buildDeathEventRegistration( detailsExist: true, name: [{ use: 'en', firstNames: 'Mary', familyName: 'Smith' }], birthDate: '1952-01-01', - nationality: ['FAR'], + nationality: [COUNTRY_CODE], identifier: [{ type: 'NATIONAL_ID', id: 'SPO456' }], address: [ { @@ -107,14 +111,14 @@ export function buildDeathEventRegistration( line: ['789 Pine Rd'], district: 'District3', state: 'State3', - country: 'FAR', + country: COUNTRY_CODE, }, ], }, registration: { trackingId: 'DW12345', registrationNumber: 'REG123', - contactPhoneNumber: '+260987654321', + contactPhoneNumber: buildPhoneNumber('0987654321'), contactEmail: 'contact@example.com', }, history: [ @@ -132,7 +136,12 @@ export function buildDeathEventRegistration( causeOfDeathEstablished: 'true', causeOfDeathMethod: 'PHYSICIAN', mannerOfDeath: 'NATURAL_CAUSES', - questionnaire: [], + questionnaire: [ + { + fieldId: 'death.deceased.deceased-view-group.birthRegNo', + value: 'B123456', + }, + ], ...overrides, } as EventRegistration } From 9d9ae6e02882886cc437deb8bc94ce97ee85c3d6 Mon Sep 17 00:00:00 2001 From: Barry Dwyer Date: Wed, 3 Dec 2025 14:22:15 +0200 Subject: [PATCH 11/15] Remove old test file --- .../tests/unit/transform.test.ts | 170 ------------------ 1 file changed, 170 deletions(-) delete mode 100644 v1-to-v2-data-migration/tests/unit/transform.test.ts diff --git a/v1-to-v2-data-migration/tests/unit/transform.test.ts b/v1-to-v2-data-migration/tests/unit/transform.test.ts deleted file mode 100644 index 401c914..0000000 --- a/v1-to-v2-data-migration/tests/unit/transform.test.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { transformCorrection } from '../../helpers/transform.ts' -import { assertEquals } from 'https://deno.land/std@0.210.0/assert/mod.ts' - -Deno.test('transformCorrection', async (t) => { - await t.step('should transform scalar values', () => { - const historyItem = { - date: '2023-10-01T12:00:00Z', - output: [ - { - valueCode: 'informant', - valueId: 'registrationPhone', - value: '0788787290', - __typename: 'InputOutput', - }, - { - valueCode: 'mother', - valueId: 'nationality', - value: 'JPN', - __typename: 'InputOutput', - }, - ], - } - - const event = 'birth' as const - - const result = transformCorrection(historyItem, event, {}) - - const expected = { - 'informant.phoneNo': '0788787290', - 'mother.nationality': 'JPN', - } - assertEquals(result.output, expected) - }) - - await t.step('should transform custom fields', () => { - const historyItem = { - date: '2023-10-01T12:00:00Z', - output: [ - { - valueCode: 'informant', - valueId: 'informantIdType', - value: 'NATIONAL_ID', - }, - { - valueCode: 'informant', - valueId: 'informantNationalId', - value: '0011002211', - }, - { - valueCode: 'informant', - valueId: 'registrationPhone', - value: '0715773955', - }, - ], - } - - const event = 'birth' as const - - const result = transformCorrection(historyItem, event, {}) - - const expected = { - 'informant.idType': 'NATIONAL_ID', - 'informant.nid': '0011002211', - 'informant.phoneNo': '0715773955', - } - assertEquals(result.output, expected) - }) - - await t.step('should transform name values', () => { - const historyItem = { - date: '2023-10-01T12:00:00Z', - output: [ - { - valueCode: 'child', - valueId: 'firstNamesEng', - value: 'Tarzan', - __typename: 'InputOutput', - }, - { - valueCode: 'mother', - valueId: 'familyNameEng', - value: 'Susan', - __typename: 'InputOutput', - }, - ], - } - - const event = 'birth' as const - - const result = transformCorrection(historyItem, event, {}) - - const expected = { - 'child.name': { firstname: 'Tarzan' }, - 'mother.name': { surname: 'Susan' }, - } - assertEquals(result.output, expected) - }) - - await t.step('should transform address values', () => { - const historyItem = { - date: '2023-10-01T12:00:00Z', - output: [ - { - valueCode: 'mother', - valueId: 'internationalStatePrimaryMother', - value: 'SSSTSSTTT', - }, - { - valueCode: 'mother', - valueId: 'internationalDistrictPrimaryMother', - value: 'DDDDDDDD', - }, - { - valueCode: 'mother', - valueId: 'internationalCityPrimaryMother', - value: 'CCCCCCCCC', - }, - { - valueCode: 'mother', - valueId: 'internationalAddressLine1PrimaryMother', - value: '111', - }, - { - valueCode: 'mother', - valueId: 'internationalAddressLine2PrimaryMother', - value: '2222', - }, - { - valueCode: 'mother', - valueId: 'internationalAddressLine3PrimaryMother', - value: '3333', - }, - { - valueCode: 'mother', - valueId: 'internationalPostalCodePrimaryMother', - value: '400111', - }, - ], - } - - const declaration = { - 'mother.address': { - addressType: 'INTERNATIONAL', - country: 'KEN', - }, - } - - const event = 'birth' as const - - const result = transformCorrection(historyItem, event, declaration) - - const expected = { - 'mother.address': { - addressType: 'INTERNATIONAL', - country: 'KEN', - streetLevelDetails: { - state: 'SSSTSSTTT', - district2: 'DDDDDDDD', - cityOrTown: 'CCCCCCCCC', - addressLine1: '111', - addressLine2: '2222', - addressLine3: '3333', - postcodeOrZip: '400111', - }, - }, - } - - assertEquals(result.output, expected) - }) -}) From 39d1f04b2c0defd0e608a50e391afd7796d2ffe4 Mon Sep 17 00:00:00 2001 From: Barry Dwyer Date: Wed, 3 Dec 2025 15:27:04 +0200 Subject: [PATCH 12/15] Made special informants configurable --- .../countryData/countryResolvers.ts | 5 +++ .../helpers/resolverUtils.ts | 33 ++++++++++++------- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/v1-to-v2-data-migration/countryData/countryResolvers.ts b/v1-to-v2-data-migration/countryData/countryResolvers.ts index 22e1d1b..a804799 100644 --- a/v1-to-v2-data-migration/countryData/countryResolvers.ts +++ b/v1-to-v2-data-migration/countryData/countryResolvers.ts @@ -1 +1,6 @@ export const countryResolver = {} + +// The V1 response will populate both the informant and special informant fields +// so we need to check if the informant is a special informant to avoid duplication +export const birthSpecialInformants = ['MOTHER', 'FATHER'] +export const deathSpecialInformants = ['SPOUSE'] diff --git a/v1-to-v2-data-migration/helpers/resolverUtils.ts b/v1-to-v2-data-migration/helpers/resolverUtils.ts index c888cf2..d225c2d 100644 --- a/v1-to-v2-data-migration/helpers/resolverUtils.ts +++ b/v1-to-v2-data-migration/helpers/resolverUtils.ts @@ -1,4 +1,14 @@ -import { Identifier, Document, ProcessedDocumentWithOptionType, PersonWithIdentifiers, ProcessedDocument } from './types.ts' +import { + birthSpecialInformants, + deathSpecialInformants, +} from '../countryData/countryResolvers.ts' +import { + Identifier, + Document, + ProcessedDocumentWithOptionType, + PersonWithIdentifiers, + ProcessedDocument, +} from './types.ts' export const getIdentifier = ( data: { identifier?: Identifier[] } | undefined, @@ -16,7 +26,7 @@ export const getDocument = ( return { path: doc.uri, originalFilename: doc.uri.replace('/ocrvs/', ''), - type: doc.contentType + type: doc.contentType, } })[0] if (!document) { @@ -50,20 +60,21 @@ export function getCustomField(data: any, id: string): any { )?.value } - /** * Special informants have their own special sections like `mother.`, `father.` or `spouse.`. */ -export const isSpecialInformant = (informant: PersonWithIdentifiers | undefined, eventType: 'birth' | 'death') => { - if (!informant) return false +export const isSpecialInformant = ( + informant: PersonWithIdentifiers | undefined, + eventType: 'birth' | 'death' +) => { + if (!informant?.relationship) return false - if(eventType === 'birth') { - return informant.relationship === 'MOTHER' - || informant.relationship === 'FATHER' + if (eventType === 'birth') { + return birthSpecialInformants.includes(informant.relationship) } - if(eventType === 'death') { - return informant.relationship === 'SPOUSE' + if (eventType === 'death') { + return deathSpecialInformants.includes(informant.relationship) } return false -} \ No newline at end of file +} From e029ffd8b133d6742b38419b987f0f27f574526a Mon Sep 17 00:00:00 2001 From: Barry Dwyer Date: Thu, 4 Dec 2025 21:15:15 +0200 Subject: [PATCH 13/15] Add test and logic for reverse engineering declaration --- v1-to-v2-data-migration/helpers/transform.ts | 132 ++++++++---------- .../tests/unit/birthResolver.test.ts | 2 +- .../tests/unit/postProcess.test.ts | 103 ++++++++++++++ 3 files changed, 166 insertions(+), 71 deletions(-) diff --git a/v1-to-v2-data-migration/helpers/transform.ts b/v1-to-v2-data-migration/helpers/transform.ts index 8d85de2..88f14a2 100644 --- a/v1-to-v2-data-migration/helpers/transform.ts +++ b/v1-to-v2-data-migration/helpers/transform.ts @@ -496,87 +496,79 @@ function deepMerge(target: any, source: any): any { function postProcess(documents: TransformedDocument): TransformedDocument { const correctionResolverKeys = Object.keys(correctionResolver) - // Track which fields have been processed to handle sequential corrections correctly - const processedFields = new Set() - - for (let i = 0; i < documents.actions.length; i++) { - const action = documents.actions[i] + // Step 1: Build a map of current declaration state at each action + // We start from the final declaration and work backwards + let currentDeclaration: Record = {} + // Find the final declaration by merging all DECLARE/REGISTER/VALIDATE declarations + for (const action of documents.actions) { if ( - action.type === 'REQUEST_CORRECTION' && - action.annotation && - action.declaration + (action.type === 'DECLARE' || + action.type === 'REGISTER' || + action.type === 'VALIDATE') && + action.declaration && + Object.keys(action.declaration).length > 0 ) { - // Filter out correctionResolver fields from the annotation - const filteredAnnotation = Object.fromEntries( - Object.entries(action.annotation).filter( - ([key]) => !correctionResolverKeys.includes(key) - ) - ) - - // Only extract fields that are in the correction's declaration (i.e., fields that were actually corrected) - // This prevents us from copying the entire base declaration - const correctedFieldKeys = new Set(Object.keys(action.declaration)) - const actualCorrectionFields: Record = {} + currentDeclaration = deepMerge(currentDeclaration, action.declaration) + } + } - for (const [key, value] of Object.entries(filteredAnnotation)) { - if (correctedFieldKeys.has(key)) { - actualCorrectionFields[key] = value - } - } + // Step 2: Process corrections in reverse order to reverse-engineer the original state + const corrections: Array<{ index: number; action: Action }> = [] - // Separate fields into new and already-corrected - const newFields: Record = {} - const correctedFields: Record = {} + for (let i = 0; i < documents.actions.length; i++) { + if (documents.actions[i].type === 'REQUEST_CORRECTION') { + corrections.push({ index: i, action: documents.actions[i] }) + } + } - for (const [key, value] of Object.entries(actualCorrectionFields)) { - if (processedFields.has(key)) { - correctedFields[key] = value - } else { - newFields[key] = value - processedFields.add(key) - } - } + // Process corrections from newest to oldest to reverse engineer + for (let i = corrections.length - 1; i >= 0; i--) { + const { action } = corrections[i] - // For new fields, find the base action (REGISTER/DECLARE) and update it - if (Object.keys(newFields).length > 0) { - for (let j = i - 1; j >= 0; j--) { - const previousAction = documents.actions[j] + if (!action.annotation || !action.declaration) { + continue + } - // Skip other correction actions - we want to update the base action - if (previousAction.type === 'REQUEST_CORRECTION') { - continue - } + // Filter out correctionResolver metadata fields from annotation + const filteredAnnotation = Object.fromEntries( + Object.entries(action.annotation).filter( + ([key]) => !correctionResolverKeys.includes(key) + ) + ) - if ( - previousAction.declaration && - typeof previousAction.declaration === 'object' && - Object.keys(previousAction.declaration).length > 0 - ) { - previousAction.declaration = deepMerge( - previousAction.declaration, - newFields - ) - break - } - } + // First, reverse the correction: for each field in the correction's declaration, + // replace the current state with the input value (from filteredAnnotation) + for (const key of Object.keys(action.declaration)) { + if (filteredAnnotation.hasOwnProperty(key)) { + currentDeclaration[key] = filteredAnnotation[key] } + } - // For already-corrected fields, find the previous correction action and update it - if (Object.keys(correctedFields).length > 0) { - for (let j = i - 1; j >= 0; j--) { - const previousAction = documents.actions[j] - - // Look for the previous correction action - if (previousAction.type === 'REQUEST_CORRECTION') { - previousAction.declaration = deepMerge( - previousAction.declaration, - correctedFields - ) - break - } - } - } + // Now set the annotation to the reversed state (which is the state BEFORE this correction) + const newAnnotation = { + ...currentDeclaration, + ...Object.fromEntries( + Object.entries(action.annotation).filter(([key]) => + correctionResolverKeys.includes(key) + ) + ), + } + + action.annotation = newAnnotation + } + + // Step 3: Update the base DECLARE/REGISTER/VALIDATE actions with the reverse-engineered state + // Find the first action with a non-empty declaration + for (const action of documents.actions) { + if ( + (action.type === 'DECLARE' || + action.type === 'REGISTER' || + action.type === 'VALIDATE') && + action.declaration && + Object.keys(action.declaration).length > 0 + ) { + action.declaration = { ...currentDeclaration } } } diff --git a/v1-to-v2-data-migration/tests/unit/birthResolver.test.ts b/v1-to-v2-data-migration/tests/unit/birthResolver.test.ts index 913cd49..8da46d2 100644 --- a/v1-to-v2-data-migration/tests/unit/birthResolver.test.ts +++ b/v1-to-v2-data-migration/tests/unit/birthResolver.test.ts @@ -4,7 +4,7 @@ import { buildBirthResolver, buildBirthEventRegistration, } from '../utils/testHelpers.ts' -import { buildPhoneNumber, stripCountryCode } from '../utils/phoneBuilder.ts' +import { buildPhoneNumber } from '../utils/phoneBuilder.ts' // Construct birthResolver as in migrate.ipynb const birthResolver = buildBirthResolver() diff --git a/v1-to-v2-data-migration/tests/unit/postProcess.test.ts b/v1-to-v2-data-migration/tests/unit/postProcess.test.ts index 3185cae..c0614cb 100644 --- a/v1-to-v2-data-migration/tests/unit/postProcess.test.ts +++ b/v1-to-v2-data-migration/tests/unit/postProcess.test.ts @@ -850,3 +850,106 @@ Deno.test('PostProcess - Death Event Corrections', async (t) => { } ) }) + +Deno.test('PostProcess - Multiple Corrections', async (t) => { + const birthResolver = buildBirthResolver() + + await t.step( + 'should set annotation to previous declaration and reverse enngineer the original declaration', + () => { + const registration = buildBirthEventRegistration({ + child: { + gender: 'female', + }, + mother: { + nationality: ['GBR'], + }, + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + regStatus: 'REGISTERED', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-03T14:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user3', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'child', + valueId: 'gender', + value: 'male', + }, + ], + output: [ + { + valueCode: 'child', + valueId: 'gender', + value: 'female', + }, + ], + }, + { + date: '2024-01-04T16:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user4', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [ + { + valueCode: 'mother', + valueId: 'nationality', + value: 'USA', + }, + ], + output: [ + { + valueCode: 'mother', + valueId: 'nationality', + value: 'GBR', + }, + ], + }, + ], + }) + + const result = transform(registration, birthResolver, 'birth') + console.log(JSON.stringify(result, null, 2)) + + const registerAction = result.actions.find( + (a) => a.type === 'REGISTER' + ) as Action + const declareAction = result.actions.find( + (a) => a.type === 'DECLARE' + ) as Action + const corrections = result.actions.filter( + (a) => a.type === 'REQUEST_CORRECTION' + ) as Action[] + + assertEquals(corrections.length, 2) + + // First correction (child.gender) + assertEquals(corrections[0]?.declaration?.['child.gender'], 'female') + assertEquals(corrections[0]?.annotation?.['child.gender'], 'male') + assertEquals(corrections[0]?.annotation?.['mother.nationality'], 'USA') + + // Second correction (mother.nationality) - different field + assertEquals(corrections[1]?.declaration?.['mother.nationality'], 'GBR') + assertEquals(corrections[1]?.annotation?.['mother.nationality'], 'USA') + assertEquals(corrections[1]?.annotation?.['child.gender'], 'female') + + // REGISTER and DECLARE should have BOTH reverse engineered values + assertEquals(registerAction?.declaration?.['child.gender'], 'male') + assertEquals(declareAction?.declaration?.['child.gender'], 'male') + assertEquals(registerAction?.declaration?.['mother.nationality'], 'USA') + assertEquals(declareAction?.declaration?.['mother.nationality'], 'USA') + } + ) +}) From cf473e67c8f2347815c7d8aa7b8ddbaaea5f161b Mon Sep 17 00:00:00 2001 From: Barry Dwyer Date: Fri, 5 Dec 2025 09:00:50 +0200 Subject: [PATCH 14/15] Change tests to fit with actual implementation --- .../tests/unit/corrections.test.ts | 27 +++++++++++-------- .../tests/unit/postProcess.test.ts | 1 - 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/v1-to-v2-data-migration/tests/unit/corrections.test.ts b/v1-to-v2-data-migration/tests/unit/corrections.test.ts index b551f96..4ee378d 100644 --- a/v1-to-v2-data-migration/tests/unit/corrections.test.ts +++ b/v1-to-v2-data-migration/tests/unit/corrections.test.ts @@ -82,8 +82,8 @@ Deno.test('Corrections - Birth', async (t) => { }, { valueCode: 'informant', - valueId: 'informantPassport', - value: 'OLD123', + valueId: 'informantNationalId', + value: '', }, ], output: [ @@ -107,12 +107,14 @@ Deno.test('Corrections - Birth', async (t) => { (a) => a.type === 'REQUEST_CORRECTION' ) + console.log(JSON.stringify(correctionAction, null, 2)) + assertEquals(correctionAction?.declaration, { 'informant.idType': 'NATIONAL_ID', 'informant.nid': 'NEW456', }) assertEquals(correctionAction?.annotation?.['informant.idType'], 'PASSPORT') - assertEquals(correctionAction?.annotation?.['informant.passport'], 'OLD123') + assertEquals(correctionAction?.annotation?.['informant.nid'], '') }) await t.step('should transform name field corrections', () => { @@ -1335,9 +1337,9 @@ Deno.test('Corrections - Death', async (t) => { value: 'PASSPORT', }, { - valueCode: 'deceased', - valueId: 'deceasedPassport', - value: 'P123456', + valueCode: 'deathEvent', + valueId: 'reasonForLateRegistration', + value: 'Out of country', }, ], output: [ @@ -1347,9 +1349,9 @@ Deno.test('Corrections - Death', async (t) => { value: 'NATIONAL_ID', }, { - valueCode: 'deceased', - valueId: 'deceasedNationalId', - value: 'N789012', + valueCode: 'deathEvent', + valueId: 'reasonForLateRegistration', + value: 'Missing in action', }, ], }, @@ -1363,10 +1365,13 @@ Deno.test('Corrections - Death', async (t) => { assertEquals(correctionAction?.declaration, { 'deceased.idType': 'NATIONAL_ID', - 'deceased.nid': 'N789012', + 'eventDetails.reasonForLateRegistration': 'Missing in action', }) assertEquals(correctionAction?.annotation?.['deceased.idType'], 'PASSPORT') - assertEquals(correctionAction?.annotation?.['deceased.passport'], 'P123456') + assertEquals( + correctionAction?.annotation?.['eventDetails.reasonForLateRegistration'], + 'Out of country' + ) }) await t.step('should transform verified fields in death', () => { diff --git a/v1-to-v2-data-migration/tests/unit/postProcess.test.ts b/v1-to-v2-data-migration/tests/unit/postProcess.test.ts index c0614cb..20fd9df 100644 --- a/v1-to-v2-data-migration/tests/unit/postProcess.test.ts +++ b/v1-to-v2-data-migration/tests/unit/postProcess.test.ts @@ -921,7 +921,6 @@ Deno.test('PostProcess - Multiple Corrections', async (t) => { }) const result = transform(registration, birthResolver, 'birth') - console.log(JSON.stringify(result, null, 2)) const registerAction = result.actions.find( (a) => a.type === 'REGISTER' From 6a0d7118870272be5821ed22f04e8da312b22ff4 Mon Sep 17 00:00:00 2001 From: Barry Dwyer Date: Fri, 5 Dec 2025 09:24:17 +0200 Subject: [PATCH 15/15] Fix an issue with correction dates being incorrectly formatted in v1 --- v1-to-v2-data-migration/helpers/dateUtils.ts | 31 +++++ v1-to-v2-data-migration/helpers/transform.ts | 7 +- .../tests/unit/corrections.test.ts | 125 +++++++++++++++++- 3 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 v1-to-v2-data-migration/helpers/dateUtils.ts diff --git a/v1-to-v2-data-migration/helpers/dateUtils.ts b/v1-to-v2-data-migration/helpers/dateUtils.ts new file mode 100644 index 0000000..c152bd3 --- /dev/null +++ b/v1-to-v2-data-migration/helpers/dateUtils.ts @@ -0,0 +1,31 @@ +/** + * @example + * normalizeDateString('2025-3-3') // returns '2025-03-03' + */ +export function normalizeDateString( + dateStr: string | undefined +): string | undefined { + if (!dateStr) { + return dateStr + } + + const datePattern = /^(\d{4})-(\d{1,2})-(\d{1,2})$/ + const match = dateStr.match(datePattern) + + if (!match) { + return dateStr + } + + const [, year, month, day] = match + + const paddedMonth = month.padStart(2, '0') + const paddedDay = day.padStart(2, '0') + + return `${year}-${paddedMonth}-${paddedDay}` +} + +export function isDateField(fieldId: string): boolean { + const dateFieldPatterns = ['BirthDate', 'birthDate', 'Date', 'deathDate'] + + return dateFieldPatterns.some((pattern) => fieldId.includes(pattern)) +} diff --git a/v1-to-v2-data-migration/helpers/transform.ts b/v1-to-v2-data-migration/helpers/transform.ts index 88f14a2..9c38483 100644 --- a/v1-to-v2-data-migration/helpers/transform.ts +++ b/v1-to-v2-data-migration/helpers/transform.ts @@ -6,6 +6,7 @@ import { AGE_MAPPINGS, VERIFIED_MAPPINGS, } from './defaultMappings.ts' +import { normalizeDateString, isDateField } from './dateUtils.ts' import { COUNTRY_FIELD_MAPPINGS } from '../countryData/countryMappings.ts' import { NAME_MAPPINGS } from '../countryData/nameMappings.ts' import { ADDRESS_MAPPINGS } from '../countryData/addressMappings.ts' @@ -124,7 +125,11 @@ export function transformCorrection( const v1OutputDeclaration = historyItem.output?.reduce((acc: Record, curr: any) => { - acc[`${event}.${curr.valueCode}.${curr.valueId}`] = curr.value + // Normalize date strings in output to ensure proper zero-padding + const value = isDateField(curr.valueId) + ? normalizeDateString(curr.value) + : curr.value + acc[`${event}.${curr.valueCode}.${curr.valueId}`] = value return acc }, {}) || {} diff --git a/v1-to-v2-data-migration/tests/unit/corrections.test.ts b/v1-to-v2-data-migration/tests/unit/corrections.test.ts index 4ee378d..ae9b6c3 100644 --- a/v1-to-v2-data-migration/tests/unit/corrections.test.ts +++ b/v1-to-v2-data-migration/tests/unit/corrections.test.ts @@ -107,8 +107,6 @@ Deno.test('Corrections - Birth', async (t) => { (a) => a.type === 'REQUEST_CORRECTION' ) - console.log(JSON.stringify(correctionAction, null, 2)) - assertEquals(correctionAction?.declaration, { 'informant.idType': 'NATIONAL_ID', 'informant.nid': 'NEW456', @@ -1548,3 +1546,126 @@ Deno.test('Corrections - Edge Cases', async (t) => { } ) }) + +Deno.test('Corrections - with incorrectly formatted dates', async (t) => { + const birthResolver = buildBirthResolver() + + await t.step( + 'for birth should zero pad months and days with one character', + () => { + const registration = buildBirthEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [], + output: [ + { + valueCode: 'child', + valueId: 'childBirthDate', + value: '2025-3-3', + }, + { + valueCode: 'mother', + valueId: 'motherBirthDate', + value: '1971-1-1', + }, + { + valueCode: 'father', + valueId: 'fatherBirthDate', + value: '1970-6-6', + }, + { + valueCode: 'informant', + valueId: 'informantBirthDate', + value: '1966-6-6', + }, + ], + }, + ], + }) + + const result = transform(registration, birthResolver, 'birth') + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) + + assertEquals(correctionAction?.declaration, { + 'child.dob': '2025-03-03', + 'mother.dob': '1971-01-01', + 'father.dob': '1970-06-06', + 'informant.dob': '1966-06-06', + }) + } + ) + + await t.step( + 'for death should zero pad months and days with one character', + () => { + const registration = buildDeathEventRegistration({ + history: [ + { + date: '2024-01-01T10:00:00Z', + regStatus: 'DECLARED', + user: { id: 'user1', role: { id: 'FIELD_AGENT' } }, + office: { id: 'office1' }, + }, + { + date: '2024-01-02T12:00:00Z', + action: 'REQUESTED_CORRECTION', + user: { id: 'user2', role: { id: 'REGISTRATION_AGENT' } }, + office: { id: 'office1' }, + input: [], + output: [ + { + valueCode: 'deceased', + valueId: 'deceasedBirthDate', + value: '2025-3-3', + }, + { + valueCode: 'deathEvent', + valueId: 'deathDate', + value: '1971-1-1', + }, + { + valueCode: 'spouse', + valueId: 'spouseBirthDate', + value: '1970-6-6', + }, + { + valueCode: 'informant', + valueId: 'informantBirthDate', + value: '1966-6-6', + }, + { + valueCode: 'informant', + valueId: 'exactDateOfBirthUnknown', + value: 'false', + }, + ], + }, + ], + }) + + const result = transform(registration, birthResolver, 'death') + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) + assertEquals(correctionAction?.declaration, { + 'deceased.dob': '2025-03-03', + 'eventDetails.date': '1971-01-01', + 'spouse.dob': '1970-06-06', + 'informant.dob': '1966-06-06', + 'informant.dobUnknown': 'false', + }) + } + ) +})