Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api): Self Service Portal APIs #8286

Open
wants to merge 11 commits into
base: develop
Choose a base branch
from
7 changes: 5 additions & 2 deletions packages/search/src/features/search/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ import {
SUPPORTED_PATIENT_IDENTIFIER_CODES,
CERTIFIED_STATUS,
REGISTERED_STATUS,
ISSUED_STATUS
ISSUED_STATUS,
DECLARED_STATUS,
REJECTED_STATUS,
VALIDATED_STATUS
} from '@opencrvs/commons/types'
import { IAdvancedSearchParam } from '@search/features/search/types'
import { transformDeprecatedParamsToSupported } from './deprecation-support'
Expand Down Expand Up @@ -76,7 +79,7 @@ export async function advancedQueryBuilder(
query_string: {
default_field: 'type',
query: isExternalSearch
? `(${REGISTERED_STATUS}) OR (${CERTIFIED_STATUS}) OR (${ISSUED_STATUS})`
? `(${REGISTERED_STATUS}) OR (${CERTIFIED_STATUS}) OR (${ISSUED_STATUS}) OR (${DECLARED_STATUS}) OR (${REJECTED_STATUS}) OR (${VALIDATED_STATUS})`
: `(${params.registrationStatuses!.join(') OR (')})`
}
})
Expand Down
10 changes: 10 additions & 0 deletions packages/webhooks/src/config/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
deleteWebhookByClientIdHandler
} from '@webhooks/features/manage/handler'
import {
approveRejectHandler,
birthRegisteredHandler,
deathRegisteredHandler,
marriageRegisteredHandler
Expand Down Expand Up @@ -105,6 +106,15 @@ export const getRoutes = () => {
tags: ['api'],
description: 'Dispatches a webhook for the marriage registration event'
}
},
{
method: 'POST',
path: '/events/{eventType}/status/{statusType}',
handler: approveRejectHandler,
config: {
tags: ['api'],
description: 'Dispatches a webhook for the event'
}
}
]
return routes
Expand Down
97 changes: 96 additions & 1 deletion packages/webhooks/src/features/event/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import Webhook, { IWebhookModel, TRIGGERS } from '@webhooks/model/webhook'
import { getQueue } from '@webhooks/queue'
import { Queue } from 'bullmq'
import fetch from 'node-fetch'
import * as ShortUIDGen from 'short-uid'
import ShortUIDGen from 'short-uid'
import { RegisteredRecord } from '@opencrvs/commons/types'

export interface IAuthHeader {
Expand Down Expand Up @@ -230,6 +230,101 @@ export async function marriageRegisteredHandler(
return h.response().code(200)
}

export async function approveRejectHandler(
request: Hapi.Request,
h: Hapi.ResponseToolkit
) {
const { eventType, statusType } = request.params
const bundle = request.payload as { trackingId: string }

let currentTrigger: TRIGGERS

if (eventType === 'BIRTH' && statusType === 'approved') {
currentTrigger = TRIGGERS.BIRTH_APPROVED
} else if (eventType === 'BIRTH' && statusType === 'rejected') {
currentTrigger = TRIGGERS.BIRTH_REJECTED
} else if (eventType === 'DEATH' && statusType === 'approved') {
currentTrigger = TRIGGERS.DEATH_APPROVED
} else if (eventType === 'DEATH' && statusType === 'rejected') {
currentTrigger = TRIGGERS.DEATH_REJECTED
} else {
throw new Error('Invalid eventType or statusType')
}
Copy link
Member

Choose a reason for hiding this comment

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

Is this filtering needed? We're making it possible for event type to be whatever value in the future so if these variables could be whatever, we'd support the new approach right away

Copy link
Member

Choose a reason for hiding this comment

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

If you want to keep this filtering, then do it on the route definition level, so instead of one catch-all

'/events/{eventType}/status/{statusType}'

you would do

/events/birth/status/approved'
/events/birth/status/rejected'

and so on.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, we can remove this. However, what will serve as the trigger point for the webhook?
Currently, it's set up like BIRTH_REGISTERED, DEATH_REGISTERED and so on.
How can we filter the webhook to ensure it only triggers for the desired events?

Copy link
Member

@rikukissa rikukissa Jan 8, 2025

Choose a reason for hiding this comment

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

Is there a specific constraint that prevents us from calling /events/{eventType}/status/{statusType} with arbitrary event type and status type e.g. '/events/divorce/status/my-custom-status'? The hub.topic that gets sent could directly be divorce/my-custom-status in this case without us needing the hard coded triggers at all

export enum TRIGGERS {
  BIRTH_REGISTERED,
  DEATH_REGISTERED,
  BIRTH_CERTIFIED,
  DEATH_CERTIFIED,
  BIRTH_CORRECTED,
  DEATH_CORRECTED
}

This approach would support Events v2 right away.

cc @euanmillar

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Will try this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@rikukissa @euanmillar
When doing this, the webhook subscribe POST API also needs to be changed.
Currently, the hub.topic should be in TRIGGERS
I have removed that validation in this API also.
Changes are pushed.


let webhookQueue: Queue

try {
webhookQueue = getQueue()
} catch (error) {
logger.error(`Can't get webhook queue: ${error}`)
return internal(error)
}
tharakadadigama20 marked this conversation as resolved.
Show resolved Hide resolved

try {
tharakadadigama20 marked this conversation as resolved.
Show resolved Hide resolved
const webhooks: IWebhookModel[] | null = await Webhook.find()
if (!webhooks) {
throw internal('Failed to find webhooks')
}
logger.info(`Subscribed webhooks: ${JSON.stringify(webhooks)}`)
if (webhooks) {
tharakadadigama20 marked this conversation as resolved.
Show resolved Hide resolved
for (const webhookToNotify of webhooks) {
logger.info(
`Queueing webhook ${webhookToNotify.trigger} ${TRIGGERS[currentTrigger]}`
)
if (webhookToNotify.trigger === TRIGGERS[currentTrigger]) {
const payload = {
timestamp: new Date().toISOString(),
id: webhookToNotify.webhookId,
event: {
hub: {
topic: TRIGGERS[currentTrigger]
},
context: {
trackingId: bundle?.trackingId,
status: statusType
}
}
}
logger.info(
`Dispatching webhook: ${JSON.stringify({
timestamp: payload.timestamp,
id: payload.id,
event: { hub: { topic: payload.event.hub.topic } },
context: ['<<redacted>>']
})}`
)
const hmac = createRequestSignature(
'sha256',
webhookToNotify.sha_secret,
JSON.stringify(payload)
)
webhookQueue.add(
`${webhookToNotify.webhookId}_${TRIGGERS[currentTrigger]}`,
{
payload,
url: webhookToNotify.address,
hmac
},
{
jobId: `WEBHOOK_${new ShortUIDGen().randomUUID().toUpperCase()}_${
webhookToNotify.webhookId
}`,
attempts: 3
}
)
}
}
} else {
logger.info(`No webhooks subscribed to approve & reject trigger`)
}
} catch (error) {
logger.error(`Webhooks/approveRejectHandler: error: ${error}`)
return internal(error)
}

return h.response().code(200)
}

const fetchSystemPermissions = async (
{ createdBy: { client_id, type } }: IWebhookModel,
authHeader: IAuthHeader,
Expand Down
6 changes: 5 additions & 1 deletion packages/webhooks/src/model/webhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ export enum TRIGGERS {
BIRTH_CERTIFIED,
DEATH_CERTIFIED,
BIRTH_CORRECTED,
DEATH_CORRECTED
DEATH_CORRECTED,
BIRTH_APPROVED,
DEATH_APPROVED,
BIRTH_REJECTED,
DEATH_REJECTED
}
export interface IClient {
client_id: string
Expand Down
9 changes: 9 additions & 0 deletions packages/workflow/src/records/handler/correction/approve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { createRoute } from '@workflow/states'
import { getToken } from '@workflow/utils/auth-utils'
import { validateRequest } from '@workflow/utils/index'
import { findActiveCorrectionRequest, sendNotification } from './utils'
import { invokeWebhooks } from '@workflow/records/webhooks'

export const approveCorrectionRoute = createRoute({
method: 'POST',
Expand Down Expand Up @@ -148,6 +149,14 @@ export const approveCorrectionRoute = createRoute({
}
)

await invokeWebhooks({
Copy link
Collaborator

Choose a reason for hiding this comment

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

@tharakadadigama20 this webhook for correction isnt dispatching for me

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I have updated it. Please check now.

bundle: record,
token,
event: getEventType(record),
isNotRegistred: true,
tharakadadigama20 marked this conversation as resolved.
Show resolved Hide resolved
statusType: 'approved'
})

return recordWithUpdatedValues
}
})
9 changes: 9 additions & 0 deletions packages/workflow/src/records/handler/correction/reject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { getToken } from '@workflow/utils/auth-utils'
import { validateRequest } from '@workflow/utils/index'
import { findActiveCorrectionRequest, sendNotification } from './utils'
import { getEventType } from '@workflow/features/registration/utils'
import { invokeWebhooks } from '@workflow/records/webhooks'

export const rejectCorrectionRoute = createRoute({
method: 'POST',
Expand Down Expand Up @@ -124,6 +125,14 @@ export const rejectCorrectionRoute = createRoute({
}
)

await invokeWebhooks({
bundle: record,
token: getToken(request),
event: getEventType(record),
isNotRegistred: true,
tharakadadigama20 marked this conversation as resolved.
Show resolved Hide resolved
statusType: 'rejected'
})

return recordWithCorrectionRejectedTask
}
})
10 changes: 10 additions & 0 deletions packages/workflow/src/records/handler/reject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { toRejected } from '@workflow/records/state-transitions'
import { indexBundle } from '@workflow/records/search'
import { auditEvent } from '@workflow/records/audit'
import { sendNotification } from '@workflow/records/notification'
import { getEventType } from '@workflow/features/registration/utils'
import { invokeWebhooks } from '@workflow/records/webhooks'

const requestSchema = z.object({
comment: z.string(),
Expand Down Expand Up @@ -48,6 +50,14 @@ export const rejectRoute = createRoute({
await auditEvent('sent-for-updates', rejectedRecord, token)
await sendNotification('sent-for-updates', rejectedRecord, token)

await invokeWebhooks({
bundle: record,
token,
event: getEventType(record),
isNotRegistred: true,
tharakadadigama20 marked this conversation as resolved.
Show resolved Hide resolved
statusType: 'rejected'
})

return rejectedRecord
}
})
36 changes: 31 additions & 5 deletions packages/workflow/src/records/webhooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,15 @@
* Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS.
*/
import fetch from 'node-fetch'
import { EVENT_TYPE, RegisteredRecord } from '@opencrvs/commons/types'
import {
CorrectionRequestedRecord,
EVENT_TYPE,
getTrackingId,
InProgressRecord,
ReadyForReviewRecord,
RegisteredRecord,
ValidatedRecord
} from '@opencrvs/commons/types'
import { WEBHOOKS_URL } from '@workflow/constants'

const WEBHOOK_URLS = {
Expand All @@ -24,15 +32,33 @@ const WEBHOOK_URLS = {
export const invokeWebhooks = async ({
bundle,
token,
event
event,
isNotRegistred,
statusType
}: {
bundle: RegisteredRecord
bundle:
| RegisteredRecord
| CorrectionRequestedRecord
| InProgressRecord
| ReadyForReviewRecord
| ValidatedRecord
token: string
event: EVENT_TYPE
isNotRegistred?: boolean
statusType?: 'rejected' | 'approved'
}) => {
const request = await fetch(WEBHOOK_URLS[event], {
const trackingId = getTrackingId(bundle)

const url = isNotRegistred
? new URL(`/events/${event}/status/${statusType}`, WEBHOOKS_URL)
: WEBHOOK_URLS[event]
const body = isNotRegistred
? `{"trackingId": "${trackingId}"}`
: JSON.stringify(bundle)

const request = await fetch(url, {
method: 'POST',
body: JSON.stringify(bundle),
body: body,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
Expand Down
Loading