Skip to content

Commit a688568

Browse files
committed
placeOfEvent configuration
1 parent 295c3da commit a688568

File tree

11 files changed

+451
-7
lines changed

11 files changed

+451
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ HTTP input now accepts `field('..')` references in the HTTP body definition.
1111
#### Jurisdiction
1212

1313
- Elasticsearch now stores location IDs as a full administrative hierarchy, with the leaf representing the actual event location. This enables searching events by any jurisdiction level (district, province, office, health facility etc.).
14+
- Added configurable placeOfEvent in EventConfig, allowing multiple location fields to be defined, with only one becoming the active place of event per document (based on conditionals), enabling jurisdiction-specific search by event location (e.g., birth location, child’s home address, death location).
1415

1516
## [1.9.1](https://github.com/opencrvs/opencrvs-core/compare/v1.9.0...v1.9.1)
1617

packages/client/src/v2-events/features/events/Search/SearchResult.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ const COLUMNS = {
5252
ICON_WITH_NAME_EVENT: 'iconWithNameEvent',
5353
EVENT: 'type',
5454
DATE_OF_EVENT: 'dateOfEvent',
55+
PLACE_OF_EVENT: 'placeOfEvent',
5556
SENT_FOR_REVIEW: 'sentForReview',
5657
SENT_FOR_UPDATES: 'sentForUpdates',
5758
SENT_FOR_APPROVAL: 'sentForApproval',
@@ -98,6 +99,9 @@ function changeSortedColumn(
9899
case COLUMNS.DATE_OF_EVENT:
99100
newSortedCol = COLUMNS.DATE_OF_EVENT
100101
break
102+
case COLUMNS.PLACE_OF_EVENT:
103+
newSortedCol = COLUMNS.PLACE_OF_EVENT
104+
break
101105
case COLUMNS.SENT_FOR_REVIEW:
102106
newSortedCol = COLUMNS.SENT_FOR_REVIEW
103107
break

packages/commons/src/events/EventConfig.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { findAllFields, getDeclarationFields } from './utils'
1717
import { DeclarationFormConfig } from './FormConfig'
1818

1919
import { FieldType } from './FieldType'
20-
import { FieldReference } from './FieldConfig'
20+
import { FieldReference, LOCATIONS_FIELD_TYPES } from './FieldConfig'
2121
import { FlagConfig } from './Flag'
2222

2323
/**
@@ -35,6 +35,12 @@ export const EventConfig = z
3535
dateOfEvent: FieldReference.optional().describe(
3636
'Reference to the field capturing the date of the event (e.g. date of birth). Defaults to the event creation date if unspecified.'
3737
),
38+
placeOfEvent: z
39+
.array(FieldReference)
40+
.optional()
41+
.describe(
42+
'Reference to the field capturing the place of the event (e.g. place of birth). Defaults to the meta.createdAtLocation if unspecified.'
43+
),
3844
title: TranslationConfig.describe(
3945
'Title template for the singular event, supporting variables (e.g. "{applicant.name.firstname} {applicant.name.surname}").'
4046
),
@@ -134,6 +140,33 @@ export const EventConfig = z
134140
}
135141
}
136142

143+
if (event.placeOfEvent) {
144+
const eventPlaceFieldId = getDeclarationFields(event).find(
145+
({ id }) =>
146+
Array.isArray(event.placeOfEvent) &&
147+
event.placeOfEvent.find((config) => config.$$field === id)
148+
)
149+
if (!eventPlaceFieldId) {
150+
ctx.addIssue({
151+
code: 'custom',
152+
message: `Place of event field id must match a field id in fields array.
153+
Invalid place of event field ID for event ${event.id}: ${event.placeOfEvent.map((x) => x.$$field).toString()}`,
154+
path: ['placeOfEvent']
155+
})
156+
} else if (
157+
!(LOCATIONS_FIELD_TYPES as readonly FieldType[]).includes(
158+
eventPlaceFieldId.type
159+
)
160+
) {
161+
ctx.addIssue({
162+
code: 'custom',
163+
message: `Field specified for place of event is of type: ${eventPlaceFieldId.type},
164+
but it needs to be any one of these type: ${LOCATIONS_FIELD_TYPES.join(', ')}`,
165+
path: ['placeOfEvent.fieldType']
166+
})
167+
}
168+
}
169+
137170
// Validate that all referenced action flags are configured in the event flags array.
138171
const configuredFlagIds = event.flags.map((flag) => flag.id)
139172
const actionFlagIds = event.actions.flatMap((action) =>

packages/commons/src/events/EventMetadata.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export const EventMetadata = z.object({
9595
.datetime()
9696
.describe('The timestamp when the event was first created and saved.'),
9797
dateOfEvent: ZodDate.nullish(),
98+
placeOfEvent: CreatedAtLocation,
9899
createdBy: z.string().describe('ID of the user who created the event.'),
99100
createdByUserType: z
100101
.enum(['user', 'system'])
@@ -147,6 +148,7 @@ export const EventMetadataKeysArray = [
147148
'status',
148149
'createdAt',
149150
'dateOfEvent',
151+
'placeOfEvent',
150152
'createdBy',
151153
'createdByUserType',
152154
'updatedByUserRole',
@@ -192,6 +194,11 @@ export const eventMetadataLabelMap: Record<
192194
defaultMessage: 'Date of Event',
193195
description: 'Date of Event'
194196
},
197+
'event.placeOfEvent': {
198+
id: 'event.placeOfEvent.label',
199+
defaultMessage: 'Place of Event',
200+
description: 'Place of Event'
201+
},
195202
'event.createdAtLocation': {
196203
id: 'event.createdAtLocation.label',
197204
defaultMessage: 'Location',

packages/commons/src/events/FieldConfig.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -930,3 +930,12 @@ export type FieldTypeToFieldConfig<T extends FieldType> = Extract<
930930
FieldConfigInput,
931931
{ type: T }
932932
>
933+
934+
export const LOCATIONS_FIELD_TYPES = [
935+
FieldType.ADMINISTRATIVE_AREA,
936+
FieldType.FACILITY,
937+
FieldType.OFFICE,
938+
FieldType.LOCATION,
939+
FieldType.COUNTRY,
940+
FieldType.ADDRESS
941+
] as const

packages/commons/src/events/state/index.test.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ describe('getCurrentEventState()', () => {
193193
createdAt: '2023-01-01T00:00:00.000Z',
194194
createdBy: 'user1',
195195
createdByUserType: TokenUserType.enum.user,
196-
createdAtLocation: 'location1' as UUID,
196+
createdAtLocation: getUUID(),
197197
createdBySignature: '/ocrvs/signature.png',
198198
createdByRole: 'FIELD_AGENT'
199199
}
@@ -208,7 +208,7 @@ describe('getCurrentEventState()', () => {
208208
createdAt: '2023-02-01T00:00:00.000Z',
209209
createdBy: 'user1',
210210
createdByUserType: TokenUserType.enum.user,
211-
createdAtLocation: 'location1' as UUID,
211+
createdAtLocation: getUUID(),
212212
createdBySignature: '/ocrvs/signature.png',
213213
createdByRole: 'FIELD_AGENT'
214214
}
@@ -312,7 +312,8 @@ describe('getCurrentEventState()', () => {
312312
declareRequestAction.declaration
313313
) as EventState,
314314
dateOfEvent: event.createdAt.split('T')[0],
315-
flags: [InherentFlags.PENDING_CERTIFICATION, 'validated'],
315+
placeOfEvent: createAction.createdAtLocation,
316+
flags: [InherentFlags.PENDING_CERTIFICATION],
316317
potentialDuplicates: [],
317318
legalStatuses: {
318319
[EventStatus.enum.DECLARED]: {
@@ -350,7 +351,7 @@ describe('getCurrentEventState()', () => {
350351
createdByUserType: TokenUserType.enum.user,
351352
createdBy: 'user1',
352353
createdBySignature: '/ocrvs/signature.png',
353-
createdAtLocation: 'location1' as UUID,
354+
createdAtLocation: getUUID(),
354355
createdByRole: 'FIELD_AGENT'
355356
}
356357
})
@@ -365,7 +366,7 @@ describe('getCurrentEventState()', () => {
365366
createdByUserType: TokenUserType.enum.user,
366367
createdBy: 'user1',
367368
createdBySignature: '/ocrvs/signature.png',
368-
createdAtLocation: 'location1' as UUID,
369+
createdAtLocation: getUUID(),
369370
createdByRole: 'FIELD_AGENT'
370371
}
371372
})
@@ -432,6 +433,7 @@ describe('getCurrentEventState()', () => {
432433
updatedAtLocation: registerAcceptAction.createdAtLocation,
433434
declaration: deepDropNulls(declareAcceptAction.declaration) as EventState,
434435
dateOfEvent: event.createdAt.split('T')[0],
436+
placeOfEvent: createAction.createdAtLocation,
435437
flags: [InherentFlags.PENDING_CERTIFICATION, 'validated'],
436438
potentialDuplicates: [],
437439
legalStatuses: {

packages/commons/src/events/state/index.ts

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
} from '../ActionDocument'
2121
import { EventDocument } from '../EventDocument'
2222
import { EventIndex } from '../EventIndex'
23-
import { EventStatus, ZodDate } from '../EventMetadata'
23+
import { EventMetadata, EventStatus, ZodDate } from '../EventMetadata'
2424
import { Draft } from '../Draft'
2525
import {
2626
aggregateActionDeclarations,
@@ -37,6 +37,10 @@ import {
3737
FullDocumentPath,
3838
FullDocumentUrl
3939
} from '../../documents'
40+
import { mapFieldTypeToZod } from '../FieldTypeMapping'
41+
import { FieldType } from '../FieldType'
42+
import { AddressFieldUpdateValue, AddressType } from '../CompositeFieldValue'
43+
import { isFieldVisible, LOCATIONS_FIELD_TYPES } from '..'
4044

4145
export function getStatusFromActions(actions: Array<Action>) {
4246
return actions
@@ -162,6 +166,69 @@ export function resolveDateOfEvent(
162166
return parsedDate.success ? parsedDate.data : undefined
163167
}
164168

169+
export const DEFAULT_PLACE_OF_EVENT_PROPERTY =
170+
'createdAtLocation' satisfies keyof EventMetadata
171+
172+
function getParsedUUID(id: unknown) {
173+
const parsed = UUID.safeParse(id)
174+
return parsed.success ? parsed.data : undefined
175+
}
176+
177+
export function resolvePlaceOfEvent(
178+
eventMetadata: {
179+
createdAtLocation?: UUID | null | undefined
180+
},
181+
declaration: EventState,
182+
config: EventConfig
183+
): UUID | undefined {
184+
if (!config.placeOfEvent || config.placeOfEvent.length === 0) {
185+
return getParsedUUID(eventMetadata.createdAtLocation)
186+
}
187+
const fieldConfigs = config.declaration.pages.flatMap((x) => x.fields)
188+
189+
// Find first valid location from configured fields
190+
for (const { $$field } of config.placeOfEvent) {
191+
const value = declaration[$$field]
192+
if (value === null || value === undefined) {
193+
continue
194+
}
195+
196+
const fieldConfig = fieldConfigs.find((f) => f.id === $$field)
197+
198+
if (!fieldConfig) {
199+
continue
200+
}
201+
if (
202+
!(LOCATIONS_FIELD_TYPES as readonly FieldType[]).includes(
203+
fieldConfig.type
204+
)
205+
) {
206+
continue
207+
}
208+
if (!isFieldVisible(fieldConfig, declaration, {})) {
209+
continue
210+
}
211+
212+
// Try parsing as address field first (domestic addresses use administrative area)
213+
const addressField = AddressFieldUpdateValue.safeParse(value)
214+
if (
215+
addressField.success &&
216+
addressField.data?.addressType === AddressType.DOMESTIC
217+
) {
218+
return getParsedUUID(addressField.data.administrativeArea)
219+
}
220+
221+
// Otherwise parse as standard location field
222+
const zodType = mapFieldTypeToZod(fieldConfig, ActionType.CREATE)
223+
const parsed = zodType.safeParse(value)
224+
if (parsed.success) {
225+
return getParsedUUID(parsed.data)
226+
}
227+
}
228+
// When no value can not be resolved, it defaults again to registrar office location, (ex: AddressType.DOMESTIC etc)
229+
return getParsedUUID(eventMetadata.createdAtLocation)
230+
}
231+
165232
export function extractPotentialDuplicatesFromActions(
166233
actions: Action[]
167234
): PotentialDuplicate[] {
@@ -181,6 +248,7 @@ export function extractPotentialDuplicatesFromActions(
181248

182249
/**
183250
* NOTE: This function should not run field validations. It should return the state based on the actions, without considering context (users, roles, permissions, etc).
251+
createdAtLocation: CreatedAtLocation
184252
*
185253
* If you update this function, please ensure @EventIndex type is updated accordingly.
186254
* In most cases, you won't need to add new parameters to this function. Discuss with the team before doing so.
@@ -236,6 +304,11 @@ export function getCurrentEventState(
236304
trackingId: event.trackingId,
237305
updatedByUserRole: requestActionMetadata.createdByRole,
238306
dateOfEvent: resolveDateOfEvent(event, declaration, config),
307+
placeOfEvent: resolvePlaceOfEvent(
308+
{ createdAtLocation: creationAction.createdAtLocation },
309+
declaration,
310+
config
311+
),
239312
potentialDuplicates: extractPotentialDuplicatesFromActions(sortedActions),
240313
flags: getEventFlags(event, config)
241314
})
@@ -304,6 +377,11 @@ export function applyDeclarationToEventIndex(
304377
updatedDeclaration,
305378
eventConfiguration
306379
),
380+
placeOfEvent: resolvePlaceOfEvent(
381+
eventIndex,
382+
updatedDeclaration,
383+
eventConfiguration
384+
),
307385
declaration: updatedDeclaration
308386
}
309387
}

packages/events/src/router/event/__snapshots__/event.search.test.ts.snap

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ exports[`Returns events based on the updatedAt column 1`] = `
3636
"createdByUserType": "[sanitized]",
3737
},
3838
},
39+
"placeOfEvent": "[sanitized]",
3940
"potentialDuplicates": [],
4041
"status": "DECLARED",
4142
"trackingId": "[sanitized]",
@@ -83,6 +84,7 @@ exports[`Returns events based on the updatedAt column 2`] = `
8384
"createdByUserType": "[sanitized]",
8485
},
8586
},
87+
"placeOfEvent": "[sanitized]",
8688
"potentialDuplicates": [],
8789
"status": "DECLARED",
8890
"trackingId": "[sanitized]",
@@ -226,6 +228,7 @@ exports[`Returns relevant events in right order 1`] = `
226228
"createdByUserType": "[sanitized]",
227229
},
228230
},
231+
"placeOfEvent": "[sanitized]",
229232
"potentialDuplicates": [],
230233
"status": "DECLARED",
231234
"trackingId": "[sanitized]",
@@ -288,6 +291,7 @@ exports[`Returns relevant events in right order 2`] = `
288291
"registrationNumber": "[sanitized]",
289292
},
290293
},
294+
"placeOfEvent": "[sanitized]",
291295
"potentialDuplicates": [],
292296
"status": "REGISTERED",
293297
"trackingId": "[sanitized]",
@@ -723,6 +727,7 @@ exports[`User only sees tennis club membership events within their jurisdiction
723727
"createdByUserType": "[sanitized]",
724728
},
725729
},
730+
"placeOfEvent": "[sanitized]",
726731
"potentialDuplicates": [],
727732
"status": "DECLARED",
728733
"trackingId": "[sanitized]",
@@ -768,6 +773,7 @@ exports[`User only sees tennis club membership events within their jurisdiction
768773
"createdByUserType": "[sanitized]",
769774
},
770775
},
776+
"placeOfEvent": "[sanitized]",
771777
"potentialDuplicates": [],
772778
"status": "DECLARED",
773779
"trackingId": "[sanitized]",
@@ -818,6 +824,7 @@ exports[`User only sees tennis club membership events within their jurisdiction
818824
"createdByUserType": "[sanitized]",
819825
},
820826
},
827+
"placeOfEvent": "[sanitized]",
821828
"potentialDuplicates": [],
822829
"status": "DECLARED",
823830
"trackingId": "[sanitized]",
@@ -863,6 +870,7 @@ exports[`User only sees tennis club membership events within their jurisdiction
863870
"createdByUserType": "[sanitized]",
864871
},
865872
},
873+
"placeOfEvent": "[sanitized]",
866874
"potentialDuplicates": [],
867875
"status": "DECLARED",
868876
"trackingId": "[sanitized]",
@@ -908,6 +916,7 @@ exports[`User only sees tennis club membership events within their jurisdiction
908916
"createdByUserType": "[sanitized]",
909917
},
910918
},
919+
"placeOfEvent": "[sanitized]",
911920
"potentialDuplicates": [],
912921
"status": "DECLARED",
913922
"trackingId": "[sanitized]",
@@ -957,6 +966,7 @@ exports[`User with my-jurisdiction scope can see events from other offices based
957966
"createdByUserType": "[sanitized]",
958967
},
959968
},
969+
"placeOfEvent": "[sanitized]",
960970
"potentialDuplicates": [],
961971
"status": "DECLARED",
962972
"trackingId": "[sanitized]",
@@ -994,6 +1004,7 @@ exports[`User with my-jurisdiction scope only sees events created by system user
9941004
],
9951005
"id": "[sanitized]",
9961006
"legalStatuses": {},
1007+
"placeOfEvent": "[sanitized]",
9971008
"potentialDuplicates": [],
9981009
"status": "NOTIFIED",
9991010
"trackingId": "[sanitized]",
@@ -1042,6 +1053,7 @@ exports[`User with my-jurisdiction scope only sees events from their primary off
10421053
"createdByUserType": "[sanitized]",
10431054
},
10441055
},
1056+
"placeOfEvent": "[sanitized]",
10451057
"potentialDuplicates": [],
10461058
"status": "DECLARED",
10471059
"trackingId": "[sanitized]",

0 commit comments

Comments
 (0)