Skip to content

Commit e9bcdf4

Browse files
committed
placeOfEvent configuration
1 parent 1401fe4 commit e9bcdf4

File tree

10 files changed

+438
-6
lines changed

10 files changed

+438
-6
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
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
@@ -96,6 +96,7 @@ export const EventMetadata = z.object({
9696
.datetime()
9797
.describe('The timestamp when the event was first created and saved.'),
9898
dateOfEvent: ZodDate.nullish(),
99+
placeOfEvent: CreatedAtLocation,
99100
createdBy: z.string().describe('ID of the user who created the event.'),
100101
createdByUserType: z
101102
.enum(['user', 'system'])
@@ -148,6 +149,7 @@ export const EventMetadataKeysArray = [
148149
'status',
149150
'createdAt',
150151
'dateOfEvent',
152+
'placeOfEvent',
151153
'createdBy',
152154
'createdByUserType',
153155
'updatedByUserRole',
@@ -193,6 +195,11 @@ export const eventMetadataLabelMap: Record<
193195
defaultMessage: 'Date of Event',
194196
description: 'Date of Event'
195197
},
198+
'event.placeOfEvent': {
199+
id: 'event.placeOfEvent.label',
200+
defaultMessage: 'Place of Event',
201+
description: 'Place of Event'
202+
},
196203
'event.createdAtLocation': {
197204
id: 'event.createdAtLocation.label',
198205
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: 6 additions & 4 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,6 +312,7 @@ describe('getCurrentEventState()', () => {
312312
declareRequestAction.declaration
313313
) as EventState,
314314
dateOfEvent: event.createdAt.split('T')[0],
315+
placeOfEvent: createAction.createdAtLocation,
315316
flags: [InherentFlags.PENDING_CERTIFICATION],
316317
potentialDuplicates: [],
317318
legalStatuses: {
@@ -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],
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
@@ -163,6 +167,69 @@ export function resolveDateOfEvent(
163167
return parsedDate.success ? parsedDate.data : undefined
164168
}
165169

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

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

0 commit comments

Comments
 (0)