Repository: opencrvs/opencrvs-core
Pull request #8826: Tracking ID saved for events
Author: login: cibelius
Created: 2025-03-03T07:14:05Z | Merged: 2025-03-03T13:12:45Z

* Generate a `trackingId` as part of the event create-action
* Index the tracking id as a `keyword` to elasticsearch
* Add test for checking for generated tracking ID

This PR does not contain displaying of the tracking ID on the summary page, since its not in the scope of the issue.


Diff:
diff --git a/packages/client/src/v2-events/components/forms/inputs/FileInput/fixtures.ts b/packages/client/src/v2-events/components/forms/inputs/FileInput/fixtures.ts
index bebf0e20fe4..ede02cb6a19 100644
--- a/packages/client/src/v2-events/components/forms/inputs/FileInput/fixtures.ts
+++ b/packages/client/src/v2-events/components/forms/inputs/FileInput/fixtures.ts
@@ -13,6 +13,7 @@ import { EventConfig, EventDocument } from '@opencrvs/commons/client'
 export const birthDocument: EventDocument = {
   type: 'birth',
   id: 'e1e9ff08-ae5d-490a-b76a-74722269ff4a',
+  trackingId: 'TEST12',
   createdAt: '2025-02-06T06:15:39.922Z',
   updatedAt: '2025-02-06T06:15:39.922Z',
   actions: [
diff --git a/packages/client/src/v2-events/features/events/actions/declare/Review.stories.tsx b/packages/client/src/v2-events/features/events/actions/declare/Review.stories.tsx
index ad04ffdba4c..2e757e649fc 100644
--- a/packages/client/src/v2-events/features/events/actions/declare/Review.stories.tsx
+++ b/packages/client/src/v2-events/features/events/actions/declare/Review.stories.tsx
@@ -51,6 +51,7 @@ const tRPCMsw = createTRPCMsw<AppRouter>({
 const eventDocument = {
   type: 'TENNIS_CLUB_MEMBERSHIP',
   id: eventId,
+  trackingId: 'TEST12',
   createdAt: '2025-01-23T05:30:02.615Z',
   updatedAt: '2025-01-23T05:35:27.689Z',
   actions: [
diff --git a/packages/client/src/v2-events/features/events/fixtures.ts b/packages/client/src/v2-events/features/events/fixtures.ts
index b8af3c28330..ed8ce340027 100644
--- a/packages/client/src/v2-events/features/events/fixtures.ts
+++ b/packages/client/src/v2-events/features/events/fixtures.ts
@@ -14,6 +14,7 @@ import { EventDocument, EventIndex } from '@opencrvs/commons/client'
 export const tennisClubMembershipEventIndex: EventIndex = {
   id: uuid(),
   type: 'TENNIS_CLUB_MEMBERSHIP',
+  trackingId: 'TEST12',
   status: 'CREATED',
   createdAt: '2023-03-01T00:00:00.000Z',
   createdBy: uuid(),
@@ -31,6 +32,7 @@ export const tennisClubMembershipEventIndex: EventIndex = {
 export const tennisClueMembershipEventDocument: EventDocument = {
   type: 'TENNIS_CLUB_MEMBERSHIP',
   id: 'c5d9d901-00bf-4631-89dc-89ca5060cb52',
+  trackingId: 'TEST12',
   createdAt: '2025-01-23T05:30:02.615Z',
   updatedAt: '2025-01-23T05:35:27.689Z',
   actions: [
diff --git a/packages/client/src/v2-events/features/events/useEvents/procedures/create.ts b/packages/client/src/v2-events/features/events/useEvents/procedures/create.ts
index 23f3b727bae..5d4285702b9 100644
--- a/packages/client/src/v2-events/features/events/useEvents/procedures/create.ts
+++ b/packages/client/src/v2-events/features/events/useEvents/procedures/create.ts
@@ -58,6 +58,7 @@ setMutationDefaults(utils.event.create, {
     const optimisticEvent = {
       id: newEvent.transactionId,
       type: newEvent.type,
+      trackingId: '', // Tracking ID is generated on the server side, so optimistic event can use an empty string as a placeholder
       transactionId: newEvent.transactionId,
       createdAt: new Date().toISOString(),
       updatedAt: new Date().toISOString(),
diff --git a/packages/client/src/v2-events/features/events/useEvents/useEvents.test.tsx b/packages/client/src/v2-events/features/events/useEvents/useEvents.test.tsx
index 90b5dc0ce8d..6ba4e50ce14 100644
--- a/packages/client/src/v2-events/features/events/useEvents/useEvents.test.tsx
+++ b/packages/client/src/v2-events/features/events/useEvents/useEvents.test.tsx
@@ -57,6 +57,7 @@ const createHandler = trpcHandler(async ({ request }) => {
   return HttpResponse.json({
     type: 'TENNIS_CLUB_MEMBERSHIP',
     id: '_REAL_UUID_',
+    trackingId: 'TEST12',
     createdAt: new Date('2024-12-05T18:37:31.295Z').toISOString(),
     updatedAt: new Date('2024-12-05T18:37:31.295Z').toISOString(),
     actions: [
diff --git a/packages/commons/src/conditionals/conditionals.test.ts b/packages/commons/src/conditionals/conditionals.test.ts
index 788b09c3a9b..dca83af3bb6 100644
--- a/packages/commons/src/conditionals/conditionals.test.ts
+++ b/packages/commons/src/conditionals/conditionals.test.ts
@@ -235,6 +235,7 @@ describe('"event" conditionals', () => {
       $event: {
         id: '123',
         type: 'birth',
+        trackingId: 'TEST12',
         createdAt: now,
         updatedAt: now,
         actions: [
diff --git a/packages/commons/src/events/EventDocument.ts b/packages/commons/src/events/EventDocument.ts
index 40f81a2f570..cfa21701c0f 100644
--- a/packages/commons/src/events/EventDocument.ts
+++ b/packages/commons/src/events/EventDocument.ts
@@ -17,7 +17,8 @@ export const EventDocument = z.object({
   type: z.string(),
   createdAt: z.string().datetime(),
   updatedAt: z.string().datetime(),
-  actions: z.array(ActionDocument)
+  actions: z.array(ActionDocument),
+  trackingId: z.string()
 })
 
 export type EventDocument = z.infer<typeof EventDocument>
diff --git a/packages/commons/src/events/EventMetadata.ts b/packages/commons/src/events/EventMetadata.ts
index 458a2676960..fe3c3c80fff 100644
--- a/packages/commons/src/events/EventMetadata.ts
+++ b/packages/commons/src/events/EventMetadata.ts
@@ -42,7 +42,8 @@ export const EventMetadata = z.object({
   createdAtLocation: z.string(),
   modifiedAt: z.string().datetime(),
   assignedTo: z.string().nullable(),
-  updatedBy: z.string()
+  updatedBy: z.string(),
+  trackingId: z.string()
 })
 
 export type EventMetadata = z.infer<typeof EventMetadata>
@@ -102,5 +103,10 @@ export const eventMetadataLabelMap: Record<
     id: 'event.updatedBy.label',
     defaultMessage: 'Updated By',
     description: 'Updated By'
+  },
+  'event.trackingId': {
+    id: 'event.trackingId.label',
+    defaultMessage: 'Tracking ID',
+    description: 'Tracking ID'
   }
 }
diff --git a/packages/commons/src/events/state/index.test.ts b/packages/commons/src/events/state/index.test.ts
index a0512d82071..a8e84752ba1 100644
--- a/packages/commons/src/events/state/index.test.ts
+++ b/packages/commons/src/events/state/index.test.ts
@@ -16,6 +16,7 @@ describe('correction requests', () => {
     const state = getCurrentEventState({
       type: 'TENNIS_CLUB_MEMBERSHIP',
       id: 'f743a5d5-19d4-44eb-9b0f-301a2d823bcf',
+      trackingId: 'TEST12',
       createdAt: '2025-01-23T02:21:38.343Z',
       updatedAt: '2025-01-23T02:21:42.230Z',
       actions: [
@@ -68,6 +69,7 @@ describe('correction requests', () => {
     const state = getCurrentEventState({
       type: 'TENNIS_CLUB_MEMBERSHIP',
       id: 'f743a5d5-19d4-44eb-9b0f-301a2d823bcf',
+      trackingId: 'TEST12',
       createdAt: '2025-01-23T02:21:38.343Z',
       updatedAt: '2025-01-23T02:21:42.230Z',
       actions: [
diff --git a/packages/commons/src/events/state/index.ts b/packages/commons/src/events/state/index.ts
index a14e63d6732..ce0e48f3f1f 100644
--- a/packages/commons/src/events/state/index.ts
+++ b/packages/commons/src/events/state/index.ts
@@ -117,6 +117,7 @@ export function getCurrentEventState(event: EventDocument): EventIndex {
     modifiedAt: latestAction.createdAt,
     assignedTo: getAssignedUserFromActions(event.actions),
     updatedBy: latestAction.createdBy,
-    data: getData(event.actions)
+    data: getData(event.actions),
+    trackingId: event.trackingId
   }
 }
diff --git a/packages/events/src/router/event/event.create.test.ts b/packages/events/src/router/event/event.create.test.ts
index b58262a3ba1..a6676cae1da 100644
--- a/packages/events/src/router/event/event.create.test.ts
+++ b/packages/events/src/router/event/event.create.test.ts
@@ -41,6 +41,18 @@ test('event can be created and fetched', async () => {
   expect(fetchedEvent).toEqual(event)
 })
 
+test('created event should have be assigned a random 6 character tracking ID', async () => {
+  const { user, generator } = await setupTestCase()
+  const client = createTestClient(user)
+  const event = await client.event.create(generator.event.create())
+
+  // trackingId should be 6 characters long and include only uppercase letters and numbers
+  expect(event.trackingId).toMatch(/^[A-Z0-9]{6}$/)
+
+  const fetchedEvent = await client.event.get(event.id)
+  expect(fetchedEvent.trackingId).toMatch(/^[A-Z0-9]{6}$/)
+})
+
 test('creating an event is an idempotent operation', async () => {
   const { user, generator, eventsDb } = await setupTestCase()
   const client = createTestClient(user)
diff --git a/packages/events/src/service/deduplication/deduplication.test.ts b/packages/events/src/service/deduplication/deduplication.test.ts
index c6741022816..73eb843ad51 100644
--- a/packages/events/src/service/deduplication/deduplication.test.ts
+++ b/packages/events/src/service/deduplication/deduplication.test.ts
@@ -178,7 +178,8 @@ export async function findDuplicates(
       createdAtLocation: 'test',
       assignedTo: 'test',
       modifiedAt: '2025-01-01',
-      updatedBy: 'test'
+      updatedBy: 'test',
+      trackingId: 'TEST12'
     },
     DeduplicationConfig.parse(LEGACY_BIRTH_DEDUPLICATION_RULES)
   )
@@ -254,7 +255,8 @@ describe('deduplication tests', () => {
         createdAtLocation: 'test',
         assignedTo: 'test',
         modifiedAt: '2025-01-01',
-        updatedBy: 'test'
+        updatedBy: 'test',
+        trackingId: 'TEST12'
       },
       DeduplicationConfig.parse(LEGACY_BIRTH_DEDUPLICATION_RULES)
     )
diff --git a/packages/events/src/service/events/events.ts b/packages/events/src/service/events/events.ts
index f60c48612eb..274e7338a13 100644
--- a/packages/events/src/service/events/events.ts
+++ b/packages/events/src/service/events/events.ts
@@ -120,6 +120,20 @@ async function deleteEventAttachments(token: string, event: EventDocument) {
   }
 }
 
+const TRACKING_ID_LENGTH = 6
+const TRACKING_ID_CHARACTERS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+
+function generateTrackingId(): string {
+  let result = ''
+  for (let i = 0; i < TRACKING_ID_LENGTH; i++) {
+    const randomIndex = Math.floor(
+      Math.random() * TRACKING_ID_CHARACTERS.length
+    )
+    result += TRACKING_ID_CHARACTERS[randomIndex]
+  }
+  return result
+}
+
 type EventDocumentWithTransActionId = EventDocument & { transactionId: string }
 export async function createEvent({
   eventInput,
@@ -143,6 +157,7 @@ export async function createEvent({
 
   const now = new Date().toISOString()
   const id = getUUID()
+  const trackingId = generateTrackingId()
 
   await collection.insertOne({
     ...eventInput,
@@ -150,6 +165,7 @@ export async function createEvent({
     transactionId,
     createdAt: now,
     updatedAt: now,
+    trackingId,
     actions: [
       {
         type: ActionType.CREATE,
diff --git a/packages/events/src/service/indexing/indexing.ts b/packages/events/src/service/indexing/indexing.ts
index d8c6f8991fe..753b2fab64c 100644
--- a/packages/events/src/service/indexing/indexing.ts
+++ b/packages/events/src/service/indexing/indexing.ts
@@ -99,7 +99,8 @@ export async function createIndex(
           data: {
             type: 'object',
             properties: formFieldsToDataMapping(formFields)
-          }
+          },
+          trackingId: { type: 'keyword' }
         } satisfies EventIndexMapping
       }
     }


Reviews:
user: login: makelicious
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T09:41:31Z
user: login: rikukissa
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-03T09:57:10Z

Review comments:
diff_hunk: @@ -120,6 +120,16 @@ async function deleteEventAttachments(token: string, event: EventDocument) {
   }
 }
 
+function generateTrackingId(): string {
+  const characters = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+  let result = ''
+  for (let i = 0; i < 6; i++) {
path: packages/events/src/service/events/events.ts
user: login: makelicious
body: 6 could use a constant
created_at: 2025-03-03T09:36:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,6 +58,7 @@ setMutationDefaults(utils.event.create, {
     const optimisticEvent = {
       id: newEvent.transactionId,
       type: newEvent.type,
+      trackingId: '',
path: packages/client/src/v2-events/features/events/useEvents/procedures/create.ts
user: login: makelicious
body: Could we add a comment here regarding the tracking id being generated on backend, or some kind of todo? How should we handle this?
created_at: 2025-03-03T09:40:49Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-03T07:14:21Z
body: Oops! Looks like you forgot to update the changelog. When updating CHANGELOG.md, please consider the following:
  - Changelog is read by country implementors who might not always be familiar with all technical details of OpenCRVS. Keep language high-level, user friendly and avoid technical references to internals.
  - Answer "What's new?", "Why was the change made?" and "Why should I care?" for each change.
  - If it's a breaking change, include a migration guide answering "What do I need to do to upgrade?".
author_association: NONE
user: login: ocrvs-bot
created_at: 2025-03-03T11:55:42Z
body: Your environment is deployed to https://ocrvs-8638.opencrvs.dev
author_association: CONTRIBUTOR