diff --git a/.github/workflows/test-migration.yml b/.github/workflows/test-migration.yml new file mode 100644 index 0000000..ce20dd4 --- /dev/null +++ b/.github/workflows/test-migration.yml @@ -0,0 +1,66 @@ +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: v2.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: Detect country and run tests + working-directory: v1-to-v2-data-migration/tests + run: | + 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: | + # 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 "### ๐Ÿ“ฆ 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 "**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/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/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/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/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 +} diff --git a/v1-to-v2-data-migration/helpers/transform.ts b/v1-to-v2-data-migration/helpers/transform.ts index a3500fa..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 }, {}) || {} @@ -462,34 +467,114 @@ 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) + // 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 === 'DECLARE' || + action.type === 'REGISTER' || + action.type === 'VALIDATE') && + action.declaration && + Object.keys(action.declaration).length > 0 + ) { + currentDeclaration = deepMerge(currentDeclaration, action.declaration) + } + } + + // Step 2: Process corrections in reverse order to reverse-engineer the original state + const corrections: Array<{ index: number; action: Action }> = [] + for (let i = 0; i < documents.actions.length; i++) { - const action = documents.actions[i] + if (documents.actions[i].type === 'REQUEST_CORRECTION') { + corrections.push({ index: i, action: documents.actions[i] }) + } + } - if (action.type === 'REQUEST_CORRECTION' && action.annotation) { - // Filter out correctionResolver fields from the annotation - const filteredAnnotation = Object.fromEntries( - Object.entries(action.annotation).filter( - ([key]) => !correctionResolverKeys.includes(key) - ) + // Process corrections from newest to oldest to reverse engineer + for (let i = corrections.length - 1; i >= 0; i--) { + const { action } = corrections[i] + + if (!action.annotation || !action.declaration) { + continue + } + + // Filter out correctionResolver metadata fields from annotation + const filteredAnnotation = Object.fromEntries( + Object.entries(action.annotation).filter( + ([key]) => !correctionResolverKeys.includes(key) ) + ) - // 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 - } + // 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] } } + + // 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 } + } } return documents diff --git a/v1-to-v2-data-migration/helpers/types.ts b/v1-to-v2-data-migration/helpers/types.ts index 4a65a4b..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 @@ -111,7 +113,7 @@ export interface Registration { contactEmail?: string informantsSignature?: string attachments?: Document[] - duplicates?: Array<{ compositionId: string }> + duplicates?: Array<{ compositionId: string; trackingId?: string }> } // History item types @@ -177,7 +179,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 +209,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/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 5c4c113..cd03b6f 100644 --- a/v1-to-v2-data-migration/tests/deno.json +++ b/v1-to-v2-data-migration/tests/deno.json @@ -15,7 +15,8 @@ "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" + "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/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/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..0c1373e 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/dataGenerators.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/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/transform.test.ts b/v1-to-v2-data-migration/tests/transform.test.ts deleted file mode 100644 index 5f6be5d..0000000 --- a/v1-to-v2-data-migration/tests/transform.test.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { expect } from 'jsr:@std/expect' -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 = { - 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, expected) - }) - - await t.step('should transform custom fields', () => { - const historyItem = { - 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, expected) - }) - - await t.step('should transform name values', () => { - const historyItem = { - 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, expected) - }) - - await t.step('should transform address values', () => { - const historyItem = { - 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, expected) - }) -}) 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..eaaa2f0 --- /dev/null +++ b/v1-to-v2-data-migration/tests/unit/actionMapping.test.ts @@ -0,0 +1,524 @@ +import { transform } from '../../helpers/transform.ts' +import { assertEquals } from 'https://deno.land/std@0.210.0/assert/mod.ts' +import { + buildHistoryItem, + buildSimpleEventRegistration as buildEventRegistration, +} from '../utils/testHelpers.ts' + +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) + } + } +) 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..8da46d2 --- /dev/null +++ b/v1-to-v2-data-migration/tests/unit/birthResolver.test.ts @@ -0,0 +1,978 @@ +import { transform } from '../../helpers/transform.ts' +import { assertEquals } from 'https://deno.land/std@0.210.0/assert/mod.ts' +import { + buildBirthResolver, + buildBirthEventRegistration, +} from '../utils/testHelpers.ts' +import { buildPhoneNumber } from '../utils/phoneBuilder.ts' + +// Construct birthResolver as in migrate.ipynb +const birthResolver = buildBirthResolver() + +Deno.test('birthResolver - child fields', async (t) => { + await t.step('should resolve child.name fields', () => { + const registration = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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.attendantAtBirth', () => { + const registration = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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.brn', () => { + const registration = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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.brn', () => { + const registration = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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.phoneNo with country code stripped', + () => { + const fullPhoneNumber = buildPhoneNumber('0987654321') + const registration = buildBirthEventRegistration({ + registration: { + trackingId: 'B123456', + 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' + ) + } + ) + + await t.step('should resolve informant.email', () => { + const registration = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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 = buildBirthEventRegistration({ + 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', + }, + ]) + } + ) +}) 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..ae9b6c3 --- /dev/null +++ b/v1-to-v2-data-migration/tests/unit/corrections.test.ts @@ -0,0 +1,1671 @@ +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('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: 'informantNationalId', + value: '', + }, + ], + 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.nid'], '') + }) + + 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 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 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 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: 'deathEvent', + valueId: 'reasonForLateRegistration', + value: 'Out of country', + }, + ], + output: [ + { + valueCode: 'deceased', + valueId: 'deceasedIdType', + value: 'NATIONAL_ID', + }, + { + valueCode: 'deathEvent', + valueId: 'reasonForLateRegistration', + value: 'Missing in action', + }, + ], + }, + ], + }) + + const result = transform(registration, deathResolver, 'death') + const correctionAction = result.actions.find( + (a) => a.type === 'REQUEST_CORRECTION' + ) + + assertEquals(correctionAction?.declaration, { + 'deceased.idType': 'NATIONAL_ID', + 'eventDetails.reasonForLateRegistration': 'Missing in action', + }) + assertEquals(correctionAction?.annotation?.['deceased.idType'], 'PASSPORT') + assertEquals( + correctionAction?.annotation?.['eventDetails.reasonForLateRegistration'], + 'Out of country' + ) + }) + + 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' + ) + } + ) +}) + +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', + }) + } + ) +}) 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..5f700cf --- /dev/null +++ b/v1-to-v2-data-migration/tests/unit/deathResolver.test.ts @@ -0,0 +1,819 @@ +import { assertEquals } from 'jsr:@std/assert' +import { transform } from '../../helpers/transform.ts' +import { + buildDeathResolver, + buildDeathEventRegistration, +} from '../utils/testHelpers.ts' +import { COUNTRY_CODE } from '../../countryData/addressResolver.ts' + +// Construct deathResolver as in migrate.ipynb +const deathResolver = buildDeathResolver() + +Deno.test('deathResolver - deceased fields', async (t) => { + await t.step('should resolve deceased.name', () => { + const data = buildDeathEventRegistration() + 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 = buildDeathEventRegistration() + 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 = buildDeathEventRegistration() + 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 = buildDeathEventRegistration({ + deceased: { + ...buildDeathEventRegistration().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 = buildDeathEventRegistration({ + deceased: { + ...buildDeathEventRegistration().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 = buildDeathEventRegistration() + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['deceased.nationality'], + COUNTRY_CODE + ) + }) + + await t.step('should resolve deceased.idType from questionnaire', () => { + const data = buildDeathEventRegistration({ + 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 = buildDeathEventRegistration() + 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 = buildDeathEventRegistration({ + deceased: { + ...buildDeathEventRegistration().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 = buildDeathEventRegistration({ + deceased: { + ...buildDeathEventRegistration().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 = buildDeathEventRegistration() + 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 = buildDeathEventRegistration({ + 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.verified from questionnaire', () => { + const data = buildDeathEventRegistration({ + 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 = buildDeathEventRegistration() + 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 = buildDeathEventRegistration({ + deceased: { + ...buildDeathEventRegistration().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 = buildDeathEventRegistration({ + 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 = buildDeathEventRegistration({ + 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 = buildDeathEventRegistration() + 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 = buildDeathEventRegistration() + 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 = buildDeathEventRegistration() + 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 = buildDeathEventRegistration({ + 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 = buildDeathEventRegistration() + 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 = buildDeathEventRegistration() + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals( + declareAction?.declaration['eventDetails.deathLocation'], + 'location-id' + ) + } + ) +}) + +Deno.test('deathResolver - informant fields', async (t) => { + await t.step('should resolve informant.idType from questionnaire', () => { + const data = buildDeathEventRegistration({ + 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 = buildDeathEventRegistration() + 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 = buildDeathEventRegistration({ + informant: { + ...buildDeathEventRegistration().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.phoneNo with country code stripped', + () => { + const data = buildDeathEventRegistration() + 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 = buildDeathEventRegistration() + 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 = buildDeathEventRegistration() + 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 = buildDeathEventRegistration({ + informant: { + ...buildDeathEventRegistration().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 = buildDeathEventRegistration() + 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 = buildDeathEventRegistration({ + informant: { + ...buildDeathEventRegistration().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 = buildDeathEventRegistration({ + informant: { + ...buildDeathEventRegistration().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 = buildDeathEventRegistration({ + informant: { + ...buildDeathEventRegistration().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 = buildDeathEventRegistration({ + informant: { + ...buildDeathEventRegistration().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 = buildDeathEventRegistration() + 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 = buildDeathEventRegistration({ + informant: { + ...buildDeathEventRegistration().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 = buildDeathEventRegistration({ + 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 = buildDeathEventRegistration() + 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 = buildDeathEventRegistration({ + spouse: { + ...buildDeathEventRegistration().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 = buildDeathEventRegistration({ + spouse: { + ...buildDeathEventRegistration().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 = buildDeathEventRegistration() + 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 = buildDeathEventRegistration() + 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 = buildDeathEventRegistration({ + spouse: { + ...buildDeathEventRegistration().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 = buildDeathEventRegistration({ + spouse: { + ...buildDeathEventRegistration().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 = buildDeathEventRegistration() + const result = transform(data, deathResolver, 'death') + const declareAction = result.actions.find((a) => a.type === 'DECLARE') + + assertEquals(declareAction?.declaration['spouse.nationality'], COUNTRY_CODE) + }) + + await t.step('should resolve spouse.idType from questionnaire', () => { + const data = buildDeathEventRegistration({ + 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 = buildDeathEventRegistration() + 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 = buildDeathEventRegistration({ + spouse: { + ...buildDeathEventRegistration().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 = buildDeathEventRegistration({ + spouse: { + ...buildDeathEventRegistration().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.verified from questionnaire', () => { + const data = buildDeathEventRegistration({ + 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 = buildDeathEventRegistration({ + 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 = buildDeathEventRegistration({ + 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 = buildDeathEventRegistration({ + 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 = buildDeathEventRegistration({ + 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 = buildDeathEventRegistration({ + 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', + }, + ]) + } + ) +}) 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..20fd9df --- /dev/null +++ b/v1-to-v2-data-migration/tests/unit/postProcess.test.ts @@ -0,0 +1,954 @@ +import { transform } from '../../helpers/transform.ts' +import { + buildBirthResolver, + buildBirthEventRegistration, + buildDeathEventRegistration, + buildDeathResolver, +} from '../utils/testHelpers.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', + }) + } + ) +}) + +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' + ) + } + ) +}) + +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') + + 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') + } + ) +}) diff --git a/v1-to-v2-data-migration/tests/data-generators.ts b/v1-to-v2-data-migration/tests/utils/dataGenerators.ts similarity index 99% rename from v1-to-v2-data-migration/tests/data-generators.ts rename to v1-to-v2-data-migration/tests/utils/dataGenerators.ts index 051b481..9d30d12 100644 --- a/v1-to-v2-data-migration/tests/data-generators.ts +++ b/v1-to-v2-data-migration/tests/utils/dataGenerators.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.ts b/v1-to-v2-data-migration/tests/utils/generatorUtils.ts similarity index 100% rename from v1-to-v2-data-migration/tests/utils.ts rename to v1-to-v2-data-migration/tests/utils/generatorUtils.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/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/testHelpers.ts b/v1-to-v2-data-migration/tests/utils/testHelpers.ts new file mode 100644 index 0000000..d3a6aeb --- /dev/null +++ b/v1-to-v2-data-migration/tests/utils/testHelpers.ts @@ -0,0 +1,178 @@ +import defaultResolvers, { + defaultBirthResolver, + defaultDeathResolver, +} 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 + */ +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 + * Includes custom fields that country resolvers might expect + */ +export function buildBirthEventRegistration( + overrides?: Partial +): EventRegistration { + return { + id: '123', + registration: { + trackingId: 'B123456', + registrationNumber: '2024B123456', + contactPhoneNumber: buildPhoneNumber('0987654321'), + 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' }, + }, + ], + questionnaire: [], + ...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: [COUNTRY_CODE], + maritalStatus: 'MARRIED', + address: [ + { + type: 'PRIMARY_ADDRESS', + line: ['123 Main St', 'Apt 4'], + district: 'District1', + state: 'State1', + country: COUNTRY_CODE, + }, + ], + 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: COUNTRY_CODE, + }, + ], + }, + spouse: { + id: 'spouse-id', + detailsExist: true, + name: [{ use: 'en', firstNames: 'Mary', familyName: 'Smith' }], + birthDate: '1952-01-01', + nationality: [COUNTRY_CODE], + identifier: [{ type: 'NATIONAL_ID', id: 'SPO456' }], + address: [ + { + type: 'PRIMARY_ADDRESS', + line: ['789 Pine Rd'], + district: 'District3', + state: 'State3', + country: COUNTRY_CODE, + }, + ], + }, + registration: { + trackingId: 'DW12345', + registrationNumber: 'REG123', + contactPhoneNumber: buildPhoneNumber('0987654321'), + 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: [ + { + fieldId: 'death.deceased.deceased-view-group.birthRegNo', + value: 'B123456', + }, + ], + ...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, + } +}