Skip to content

Conversation

@Nil20
Copy link
Contributor

@Nil20 Nil20 commented Oct 24, 2025

Description

Country config PR: opencrvs/opencrvs-countryconfig#1138
Farajaland PR: opencrvs/opencrvs-farajaland#1811

Clearly describe what has been changed. Include relevant context or background.
Explain how the issue was fixed (if applicable) and the root cause.

Link this pull request to the GitHub issue (and optionally name the branch ocrvs-<issue #>)

Checklist

  • I have linked the correct Github issue under "Development"
  • I have tested the changes locally, and written appropriate tests
  • I have tested beyond the happy path (e.g. edge cases, failure paths)
  • I have updated the changelog with this change (if applicable)
  • I have updated the GitHub issue status accordingly

@Nil20 Nil20 added the Skip e2e Helps run lint/unit test only for faster feedback. Remove before merge. label Oct 24, 2025
@github-actions

This comment has been minimized.

@Nil20 Nil20 marked this pull request as draft October 24, 2025 08:23
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Greptile Overview

Important Files Changed

File Analysis

Filename Score Overview
packages/commons/src/events/CompositeFieldValue.ts 0/5 Refactored address field schemas to use discriminated unions and added nested data structure for QueryParamReader; contains critical syntax error on line 136
packages/commons/src/events/FieldValue.ts 4/5 Refactored DataFieldValue from record to nested object structure, removed StreetLevelDetailsValue from union
packages/commons/src/events/FieldConfig.ts 5/5 Changed DefaultAddressFieldValue to extend DomesticAddressFieldValue instead of AddressFieldValue

3 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Copy link
Contributor

@makelicious makelicious left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks very good, thank you 🙏 . Some type related comments. Let's review it once we are finished

return omitHiddenFields(
annotationFields,
{ ...declaration, ...annotation },
{ ...declaration, ...annotation } as EventState,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check if we really need to cast this? If we do, ActionUpdate is the truthful one. At least stripping nulls from annotation (or the combination) should make it EventState

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{ ...declaration, ...annotation }, declaration and annotation together can be treated as ActionUpdate since annotation is ActionUpdate and it's values are nullish values(FieldUpdateValue) which can not be EventState(FieldValue)

streetLevelDetails: {
province: 'a45b982a-5c7b-4bd9-8fd8-a42d0994054c',
district: '5ef450bc-712d-48ad-93f3-8da0fa453baa',
urbanOrRural: 'RURAL' as const,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think urbanOrRural is not used anymore. In the best case, this should throw an error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const StreetLevelDetailsValue = z
  .record(z.string(), z.string())
  .optional()

streetLevelDetails accepts any objects with a string as a key and a value, right? On that basis, i dont think it should throw errors

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't we validate them against whatever countryconfig specifies?

// src/form/street-address-configuration.ts

export const defaultStreetAddressConfiguration = [
  {
    id: 'town',
    required: false,
    parent: field('country'),
    label: {
      id: 'field.address.town.label',
      defaultMessage: 'Town',
      description: 'This is the label for the field'
    },
    conditionals: [
      {
        type: ConditionalType.SHOW,
        conditional: and(
          isDomesticAddress(),
          not(field('district').isUndefined())
        )
      }
    ],
    type: FieldType.TEXT
  },
  {
    id: 'residentialArea',
    required: false,
    parent: field('country'),
    label: {
      id: 'field.address.residentialArea.label',
      defaultMessage: 'Residential Area',
      description: 'This is the label for the field'
    },
    conditionals: [
      {
        type: ConditionalType.SHOW,
        conditional: and(
          isDomesticAddress(),
          not(field('district').isUndefined())
        )
      }
    ],
    type: FieldType.TEXT
  },
  {
    id: 'street',
    required: false,
    parent: field('country'),
    label: {
      id: 'field.address.street.label',
      defaultMessage: 'Street',
      description: 'This is the label for the field'
    },
    conditionals: [
      {
        type: ConditionalType.SHOW,
        conditional: and(
          isDomesticAddress(),
          not(field('district').isUndefined())
        )
      }
    ],
    type: FieldType.TEXT
  },
  {
    id: 'number',
    required: false,
    parent: field('country'),
    label: {
      id: 'field.address.number.label',
      defaultMessage: 'Number',
      description: 'This is the label for the field'
    },
    conditionals: [
      {
        type: ConditionalType.SHOW,
        conditional: and(
          isDomesticAddress(),
          not(field('district').isUndefined())
        )
      }
    ],
    type: FieldType.TEXT
  },
  {
    id: 'zipCode',
    required: false,
    parent: field('country'),
    label: {
      id: 'field.address.postcodeOrZip.label',
      defaultMessage: 'Postcode / Zip',
      description: 'This is the label for the field'
    },
    conditionals: [
      {
        type: ConditionalType.SHOW,
        conditional: and(
          isDomesticAddress(),
          not(field('district').isUndefined())
        )
      }
    ],
    type: FieldType.TEXT
  },
  {
    id: 'state',
    conditionals: [
      {
        type: ConditionalType.SHOW,
        conditional: isInternationalAddress()
      }
    ],
    parent: field('country'),
    required: {
      message: {
        id: 'field.address.state.label.required',
        description: 'State required message',
        defaultMessage: 'State is required'
      }
    },
    label: {
      id: 'field.address.state.label',
      defaultMessage: 'State',
      description: 'This is the label for the field'
    },
    type: FieldType.TEXT
  },
  {
    id: 'district2',
    parent: field('country'),
    conditionals: [
      {
        type: ConditionalType.SHOW,
        conditional: isInternationalAddress()
      }
    ],
    required: {
      message: {
        id: 'field.address.district2.label.required',
        description: 'District2 required message',
        defaultMessage: 'District is required'
      }
    },
    label: {
      id: 'field.address.district2.label',
      defaultMessage: 'District',
      description: 'This is the label for the field'
    },
    type: FieldType.TEXT
  },
  {
    id: 'cityOrTown',
    parent: field('country'),
    conditionals: [
      {
        type: ConditionalType.SHOW,
        conditional: isInternationalAddress()
      }
    ],
    required: false,
    label: {
      id: 'field.address.cityOrTown.label',
      defaultMessage: 'City / Town',
      description: 'This is the label for the field'
    },
    type: FieldType.TEXT
  },
  {
    id: 'addressLine1',
    parent: field('country'),
    conditionals: [
      {
        type: ConditionalType.SHOW,
        conditional: isInternationalAddress()
      }
    ],
    required: false,
    label: {
      id: 'field.address.addressLine1.label',
      defaultMessage: 'Address Line 1',
      description: 'This is the label for the field'
    },
    type: FieldType.TEXT
  },
  {
    id: 'addressLine2',
    parent: field('country'),
    conditionals: [
      {
        type: ConditionalType.SHOW,
        conditional: isInternationalAddress()
      }
    ],
    required: false,
    label: {
      id: 'field.address.addressLine2.label',
      defaultMessage: 'Address Line 2',
      description: 'This is the label for the field'
    },
    type: FieldType.TEXT
  },
  {
    id: 'addressLine3',
    parent: field('country'),
    conditionals: [
      {
        type: ConditionalType.SHOW,
        conditional: isInternationalAddress()
      }
    ],
    required: false,
    label: {
      id: 'field.address.addressLine3.label',
      defaultMessage: 'Address Line 3',
      description: 'This is the label for the field'
    },
    type: FieldType.TEXT
  },
  {
    id: 'postcodeOrZip',
    parent: field('country'),
    conditionals: [
      {
        type: ConditionalType.SHOW,
        conditional: isInternationalAddress()
      }
    ],
    required: false,
    label: {
      id: 'field.address.postcodeOrZip.label',
      defaultMessage: 'Postcode / Zip',
      description: 'This is the label for the field'
    },
    type: FieldType.TEXT
  }
]

@rikukissa @pankaj-pant

@Nil20 Nil20 removed the Skip e2e Helps run lint/unit test only for faster feedback. Remove before merge. label Oct 29, 2025
@Nil20 Nil20 marked this pull request as ready for review October 29, 2025 10:52
@Nil20 Nil20 requested a review from makelicious October 29, 2025 10:52
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Greptile Overview

Important Files Changed

File Analysis

Filename Score Overview
packages/commons/src/events/FieldValue.ts 5/5 Restructured FieldValue and FieldUpdateValue types to use explicit type annotations with z.ZodType, removed StreetLevelDetails as top-level types, and changed DataFieldValue to wrap data in an object structure
packages/commons/src/events/CompositeFieldValue.ts 5/5 Refactored AddressFieldValue to use discriminated unions for DOMESTIC vs INTERNATIONAL, and changed QueryParamReaderFieldValue to wrap params in an object structure
packages/client/src/v2-events/features/events/registered-fields/QueryParamReader.tsx 5/5 Updated to wrap params in params object property to match new QueryParamReaderFieldValue structure
packages/client/src/v2-events/features/events/registered-fields/Data.tsx 5/5 Updated to wrap data values in data object property and access via value.data[id] to match new DataFieldValue structure
packages/client/src/v2-events/features/events/registered-fields/Address.tsx 5/5 Added type guards checking addressType === AddressType.DOMESTIC before accessing administrativeArea to handle discriminated union properly
packages/commons/src/events/utils.ts 5/5 Added generic type parameters and updated return types to properly handle EventState vs ActionUpdate, added satisfies clause for type safety
packages/events/src/service/indexing/utils.ts 5/5 Updated to access administrativeArea only after checking addressType === DOMESTIC to properly handle discriminated union

23 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

Copy link
Contributor

@makelicious makelicious left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well done on the improvements 🏅 . Could you check the changes again: https://www.chromatic.com/review?appId=67862a3f4457d14c317e5cca&number=10866&type=linked&activeElementId=comparison-test-key-68b70a692b622cd342f08dd5-1200px&view=changes

If I read this right, we lose data on the address review. Is it due bad mock data? If so, could it be updated

data: z.record(z.string(), z.string())
})

const ReadDataValue = z.object({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this still needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be. With this, FieldUpdateValue would not compile any values with this shape z.record(z.string(), z.string())

@Nil20 Nil20 linked an issue Oct 31, 2025 that may be closed by this pull request
@Nil20 Nil20 requested a review from makelicious November 10, 2025 12:55
'child.details': {
nested: 'value'
}
'child.details': 'value'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests were overwritten because typescript were complaining of these unknown keys e.g. - dob, nested being passed as keys of FieldValue. When a key was added to QueryParamReaderFieldValue, rather than being z.record(z.string(), z.string()), these were thrown.

Comment on lines +47 to +56
administrativeArea: '27160bbd-32d1-4625-812f-860226bfb92a',
streetLevelDetails: {
town: 'Example Town',
residentialArea: 'Example Residential Area',
street: 'Example Street',
number: '55',
zipCode: '123456',
state: 'Example State',
district2: 'Example District 2'
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we changing the data intentionally?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! It's tightened by type here with EventState

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to revisit this as the the current mock values probably aren't aligned with the original intention of the test

'child.details': {
dob: new Date('2125-01-01').toISOString().split('T')[0]
}
'child.details': new Date('2125-01-01').toISOString().split('T')[0]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test is validating the support for $now variable even in nested levels, so it should be passing with the previous value. Although as .object has been deprecated, what we could do it use .get('dob') instead

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

used .get('data')

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep the nested tests intact

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah! with known keys from FieldValues

// @NOTE: This happens to map to a valid location in events test environment. Updating it will break tests.
// @TODO: Find a way to give out context aware mock values in the future.
administrativeArea: '27160bbd-32d1-4625-812f-860226bfb92a' as UUID
administrativeArea: '27160bbd-32d1-4625-812f-860226bfb92a' as UUID,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can remove the todo comments now?

Comment on lines 47 to 54
it('allows optional middlename', () => {
const schema = getDynamicNameValue(NameField)
expect(() =>
schema.parse({
firstname: 'Jane',
surname: 'Doe'
})
).not.toThrow()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see a middlename being passed tho

Comment on lines 64 to 90
return z.object({
country: z.string(),
addressType: z.enum([AddressType.DOMESTIC, AddressType.INTERNATIONAL]),
administrativeArea: z
.string()
.uuid()
.optional() /* Leaf level admin structure */,
streetLevelDetails: StreetLevelDetailsUpdateValue.refine((arg) => {
const submittedKeys = Object.keys(arg ?? {})
const configIds =
(streetAddressConfig && streetAddressConfig.map((a) => a.id)) ?? []
const invalidKeys = submittedKeys.filter(
(key) => !configIds.includes(key)
)
if (invalidKeys.length) {
// eslint-disable-next-line no-console
console.log(
'Invalid streetLevelDetails: unknown keys',
JSON.stringify(invalidKeys, null, 2)
)
return false
}

return true
})
})
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's check if we can use .shape to reuse the one defined in CompositeValue.ts

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can a country be nullish for FieldUpdateValue?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

most likely not

@Nil20 Nil20 requested a review from Zangetsu101 November 12, 2025 09:44
@ocrvs-bot
Copy link
Contributor

Your environment is deployed to https://ocrvs-10634.e2e-k8s.opencrvs.dev

@Nil20 Nil20 merged commit 611cc0c into develop Nov 13, 2025
123 of 124 checks passed
@Nil20 Nil20 deleted the ocrvs-10634 branch November 13, 2025 07:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Address field allows malformed input

5 participants