diff --git a/packages/client/src/i18n/messages/views/integrations.ts b/packages/client/src/i18n/messages/views/integrations.ts index 0b2d698dfd0..2c988d83d44 100644 --- a/packages/client/src/i18n/messages/views/integrations.ts +++ b/packages/client/src/i18n/messages/views/integrations.ts @@ -127,6 +127,12 @@ const messagesToDefine = { description: 'Label for event notification' }, + selfServicePortal: { + id: 'integrations.type.selfServicePortal', + defaultMessage: 'Self Service Portal', + description: 'Label for Self Service Portal' + }, + childDetails: { id: 'integrations.childDetails', defaultMessage: `Child's details`, diff --git a/packages/client/src/tests/schema.graphql b/packages/client/src/tests/schema.graphql index b466d5d5d48..3b0dc071e37 100644 --- a/packages/client/src/tests/schema.graphql +++ b/packages/client/src/tests/schema.graphql @@ -1725,6 +1725,7 @@ enum SystemType { HEALTH RECORD_SEARCH WEBHOOK + SELF_SERVICE_PORTAL } enum TelecomSystem { diff --git a/packages/client/src/utils/gateway.ts b/packages/client/src/utils/gateway.ts index 29efdc0c2fc..dd0fddf39dd 100644 --- a/packages/client/src/utils/gateway.ts +++ b/packages/client/src/utils/gateway.ts @@ -2142,7 +2142,8 @@ export enum SystemType { Health = 'HEALTH', NationalId = 'NATIONAL_ID', RecordSearch = 'RECORD_SEARCH', - Webhook = 'WEBHOOK' + Webhook = 'WEBHOOK', + SelfServicePortal = 'SELF_SERVICE_PORTAL' } export enum TelecomSystem { diff --git a/packages/client/src/v2-events/components/forms/FormFieldGenerator.tsx b/packages/client/src/v2-events/components/forms/FormFieldGenerator.tsx index 23b50c344e4..68497822389 100644 --- a/packages/client/src/v2-events/components/forms/FormFieldGenerator.tsx +++ b/packages/client/src/v2-events/components/forms/FormFieldGenerator.tsx @@ -63,6 +63,7 @@ import { Select } from '@client/v2-events/features/events/registered-fields/Sele import { SelectCountry } from '@client/v2-events/features/events/registered-fields/SelectCountry' import { SubHeader } from '@opencrvs/components' import { formatISO } from 'date-fns' +import { Divider } from '@opencrvs/components' const fadeIn = keyframes` from { opacity: 0; } @@ -117,7 +118,9 @@ const GeneratedInputField = React.memo( const inputFieldProps = { id: fieldDefinition.id, - label: intl.formatMessage(fieldDefinition.label), + label: fieldDefinition.hideLabel + ? undefined + : intl.formatMessage(fieldDefinition.label), // helperText: fieldDefinition.helperText, // tooltip: fieldDefinition.tooltip, // description: fieldDefinition.description, @@ -189,7 +192,17 @@ const GeneratedInputField = React.memo( if (fieldDefinition.type === TEXT) { return ( - + + return ( + + + + ) } if (fieldDefinition.type === 'SELECT') { return ( - setFieldValue(fieldDefinition.id, val)} + /> + ) } if (fieldDefinition.type === 'COUNTRY') { return ( - + + + ) } if (fieldDefinition.type === 'CHECKBOX') { @@ -244,37 +265,46 @@ const GeneratedInputField = React.memo( } if (fieldDefinition.type === 'RADIO_GROUP') { return ( - + + + ) } if (fieldDefinition.type === 'LOCATION') { if (fieldDefinition.options.type === 'HEALTH_FACILITY') return ( - + + + ) + return ( + + - ) - return ( - + ) } + if (fieldDefinition.type === 'DIVIDER') { + return + } throw new Error(`Unsupported field ${fieldDefinition}`) } ) @@ -424,7 +454,9 @@ class FormSectionComponent extends React.Component { const language = this.props.intl.locale - const errors = this.props.errors as unknown as Errors + const errors = makeFormFieldIdsFormikCompatible( + this.props.errors as unknown as Errors + ) const fields = fieldsWithDotIds.map((field) => ({ ...field, @@ -476,11 +508,7 @@ class FormSectionComponent extends React.Component { error={isFieldDisabled ? '' : error} fields={fields} formData={formData} - touched={ - makeFormikFieldIdsOpenCRVSCompatible(touched)[ - field.id - ] || false - } + touched={touched[field.id] || false} values={values} onUploadingStateChanged={ this.props.onUploadingStateChanged diff --git a/packages/client/src/v2-events/components/forms/utils.ts b/packages/client/src/v2-events/components/forms/utils.ts index 604c8f3354e..25c54ebc260 100644 --- a/packages/client/src/v2-events/components/forms/utils.ts +++ b/packages/client/src/v2-events/components/forms/utils.ts @@ -21,7 +21,8 @@ import { validate, DateFieldValue, TextFieldValue, - RadioGroupFieldValue + RadioGroupFieldValue, + FileFieldValue } from '@opencrvs/commons/client' import { CheckboxFieldValue, @@ -124,7 +125,8 @@ const initialValueMapping: Record = { [FieldType.COUNTRY]: null, [FieldType.LOCATION]: null, [FieldType.SELECT]: null, - [FieldType.PAGE_HEADER]: null + [FieldType.PAGE_HEADER]: null, + [FieldType.DIVIDER]: null } export function getInitialValues(fields: FieldConfig[]) { diff --git a/packages/client/src/v2-events/components/forms/validation.ts b/packages/client/src/v2-events/components/forms/validation.ts index 5dedd5f22fa..9a481fd04d5 100644 --- a/packages/client/src/v2-events/components/forms/validation.ts +++ b/packages/client/src/v2-events/components/forms/validation.ts @@ -67,9 +67,32 @@ function getValidationErrors( const validators = field.validation ? field.validation : [] - // if (field.required && !checkValidationErrorsOnly) { - // validators.push(required(requiredErrorMessage)) - // } else if (isFieldButton(field)) { + if (field.required && !checkValidationErrorsOnly) { + validators.push({ + message: { + defaultMessage: 'Required for registration', + description: 'This is the error message for required fields', + id: 'error.required' + }, + validator: { + type: 'object', + properties: { + $form: { + type: 'object', + properties: { + [field.id]: { + type: 'string', + minLength: 1 + } + }, + required: [field.id] + } + }, + required: ['$form'] + } + }) + } + // else if (isFieldButton(field)) { // const { trigger } = field.options // validators.push(httpErrorResponseValidator(trigger)) // } else if (field.validateEmpty) { diff --git a/packages/client/src/v2-events/features/events/registered-fields/Select.tsx b/packages/client/src/v2-events/features/events/registered-fields/Select.tsx index cb85fd0d684..bb350df4d7e 100644 --- a/packages/client/src/v2-events/features/events/registered-fields/Select.tsx +++ b/packages/client/src/v2-events/features/events/registered-fields/Select.tsx @@ -16,7 +16,6 @@ import { SelectOption } from '@opencrvs/commons/client' import { Select as SelectComponent } from '@opencrvs/components' -import { InputField } from '@client/components/form/InputField' export function Select({ onChange, @@ -36,14 +35,11 @@ export function Select({ })) return ( - - - + ) } diff --git a/packages/client/src/v2-events/features/workqueues/EventOverview/EventOverview.tsx b/packages/client/src/v2-events/features/workqueues/EventOverview/EventOverview.tsx index f0e00dccde6..83c60199d1f 100644 --- a/packages/client/src/v2-events/features/workqueues/EventOverview/EventOverview.tsx +++ b/packages/client/src/v2-events/features/workqueues/EventOverview/EventOverview.tsx @@ -104,7 +104,7 @@ function EventOverview({ : '' return ( } + icon={() => } size={ContentSize.LARGE} title={title || fallbackTitle} titleColor={event.id ? 'copy' : 'grey600'} diff --git a/packages/client/src/v2-events/features/workqueues/EventOverview/EventOverviewContext.tsx b/packages/client/src/v2-events/features/workqueues/EventOverview/EventOverviewContext.tsx index 186ce4cc253..4facbd6fef9 100644 --- a/packages/client/src/v2-events/features/workqueues/EventOverview/EventOverviewContext.tsx +++ b/packages/client/src/v2-events/features/workqueues/EventOverview/EventOverviewContext.tsx @@ -28,7 +28,7 @@ const MISSING_USER = { family: 'user' } ], - systemRole: '-' + role: '-' } /** diff --git a/packages/client/src/v2-events/features/workqueues/EventOverview/components/EventHistory/EventHistory.tsx b/packages/client/src/v2-events/features/workqueues/EventOverview/components/EventHistory/EventHistory.tsx index 67a9114e702..e023db8884d 100644 --- a/packages/client/src/v2-events/features/workqueues/EventOverview/components/EventHistory/EventHistory.tsx +++ b/packages/client/src/v2-events/features/workqueues/EventOverview/components/EventHistory/EventHistory.tsx @@ -11,7 +11,7 @@ import React from 'react' import { format } from 'date-fns' import styled from 'styled-components' -import { useIntl } from 'react-intl' +import { defineMessages, useIntl } from 'react-intl' import { useNavigate } from 'react-router-dom' import { stringify } from 'query-string' import { Link } from '@opencrvs/components' @@ -28,7 +28,6 @@ import { formatUrl } from '@client/navigation' import { useEventOverviewContext } from '@client/v2-events/features/workqueues/EventOverview/EventOverviewContext' import { EventHistoryModal } from './EventHistoryModal' import { UserAvatar } from './UserAvatar' -import { messages } from './messages' /** * Based on packages/client/src/views/RecordAudit/History.tsx @@ -40,6 +39,25 @@ const TableDiv = styled.div` const DEFAULT_HISTORY_RECORD_PAGE_SIZE = 10 +const messages = defineMessages({ + 'event.history.timeFormat': { + defaultMessage: 'MMMM dd, yyyy · hh.mm a', + id: 'event.history.timeFormat', + description: 'Time format for timestamps in event history' + }, + 'events.history.status': { + id: `events.history.status`, + defaultMessage: + '{status, select, CREATE {Draft} VALIDATE {Validated} DRAFT {Draft} DECLARE {Declared} REGISTER {Registered} other {Unknown}}' + }, + 'event.history.role': { + id: 'event.history.role', + defaultMessage: + '{role, select, LOCAL_REGISTRAR{Local Registrar} other{Unknown}}', + description: 'Role of the user in the event history' + } +}) + /** * Renders the event history table. Used for audit trail. */ @@ -72,7 +90,9 @@ export function EventHistory({ history }: { history: ActionDocument[] }) { onHistoryRowClick(item, user) }} > - {item.type} + {intl.formatMessage(messages['events.history.status'], { + status: item.type + })} ), user: ( @@ -95,7 +115,9 @@ export function EventHistory({ history }: { history: ActionDocument[] }) { /> ), - role: user.systemRole, + role: intl.formatMessage(messages['event.history.role'], { + role: user.role + }), location: ( (x: T, message: string) { if (x === undefined || x === null) { throw new Error(message) @@ -207,8 +216,8 @@ function Workqueue({ ...fieldsWithPopulatedValues, ...event, event: intl.formatMessage(eventConfig.label), - createdAt: intl.formatDate(new Date(event.createdAt)), - modifiedAt: intl.formatDate(new Date(event.modifiedAt)), + createdAt: formattedDuration(new Date(event.createdAt)), + modifiedAt: formattedDuration(new Date(event.modifiedAt)), status: intl.formatMessage( { diff --git a/packages/client/src/v2-events/hooks/useTransformer.ts b/packages/client/src/v2-events/hooks/useTransformer.ts index ce558c1b06c..def68af9b71 100644 --- a/packages/client/src/v2-events/hooks/useTransformer.ts +++ b/packages/client/src/v2-events/hooks/useTransformer.ts @@ -11,7 +11,11 @@ import { useIntl } from 'react-intl' import { useSelector } from 'react-redux' -import { ActionFormData, findPageFields } from '@opencrvs/commons/client' +import { + ActionFormData, + findPageFields, + FieldType +} from '@opencrvs/commons/client' import { fieldValueToString } from '@client/v2-events/components/forms/utils' import { useEventConfiguration } from '@client/v2-events/features/events/useEventConfiguration' // eslint-disable-next-line no-restricted-imports @@ -36,6 +40,10 @@ export const useTransformer = (eventType: string) => { throw new Error(`Field not found for ${key}`) } + if (fieldConfig.type === FieldType.FILE) { + continue + } + stringifiedValues[key] = fieldValueToString({ fieldConfig, value, diff --git a/packages/client/src/v2-events/layouts/form/FormHeader.tsx b/packages/client/src/v2-events/layouts/form/FormHeader.tsx index 8c180429e8b..6a75baec310 100644 --- a/packages/client/src/v2-events/layouts/form/FormHeader.tsx +++ b/packages/client/src/v2-events/layouts/form/FormHeader.tsx @@ -112,9 +112,7 @@ export function FormHeader({ {modal} } - desktopTitle={intl.formatMessage(messages.newVitalEventRegistration, { - event: intl.formatMessage(label) - })} + desktopTitle={intl.formatMessage(label)} mobileLeft={} mobileRight={ <> @@ -147,9 +145,7 @@ export function FormHeader({ {modal} } - mobileTitle={intl.formatMessage(messages.newVitalEventRegistration, { - event: intl.formatMessage(label) - })} + mobileTitle={intl.formatMessage(label)} /> ) } diff --git a/packages/client/src/v2-events/messages/index.ts b/packages/client/src/v2-events/messages/index.ts index 0c9249addb7..d1f4a8d95de 100644 --- a/packages/client/src/v2-events/messages/index.ts +++ b/packages/client/src/v2-events/messages/index.ts @@ -10,7 +10,6 @@ */ export * from './constants' -export * from './registrarHome' export * from './workqueue' export * from './navigation' export * from './errors' diff --git a/packages/client/src/v2-events/messages/registrarHome.ts b/packages/client/src/v2-events/messages/registrarHome.ts deleted file mode 100644 index 6126ee1b3a9..00000000000 --- a/packages/client/src/v2-events/messages/registrarHome.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * OpenCRVS is also distributed under the terms of the Civil Registration - * & Healthcare Disclaimer located at http://opencrvs.org/license. - * - * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. - */ -import { defineMessages, MessageDescriptor } from 'react-intl' - -interface IOfficeHomeMessages - extends Record { - empty: MessageDescriptor -} - -const messagesToDefine: IOfficeHomeMessages = { - empty: { - defaultMessage: 'Empty message', - description: 'Label for workqueue tooltip', - id: 'regHome.issued' - } -} - -export const messages: IOfficeHomeMessages = defineMessages(messagesToDefine) diff --git a/packages/client/src/views/SysAdmin/Config/Systems/Systems.tsx b/packages/client/src/views/SysAdmin/Config/Systems/Systems.tsx index 81c82d039ac..5719e2ac078 100644 --- a/packages/client/src/views/SysAdmin/Config/Systems/Systems.tsx +++ b/packages/client/src/views/SysAdmin/Config/Systems/Systems.tsx @@ -215,7 +215,10 @@ export function SystemList() { HEALTH: intl.formatMessage(integrationMessages.eventNotification), RECORD_SEARCH: intl.formatMessage(integrationMessages.recordSearch), NATIONAL_ID: intl.formatMessage(integrationMessages.nationalId), - WEBHOOK: intl.formatMessage(integrationMessages.webhook) + WEBHOOK: intl.formatMessage(integrationMessages.webhook), + SELF_SERVICE_PORTAL: intl.formatMessage( + integrationMessages.selfServicePortal + ) } const systemToLabel = (system: System) => { @@ -532,6 +535,12 @@ export function SystemList() { { label: intl.formatMessage(integrationMessages.webhook), value: SystemType.Webhook + }, + { + label: intl.formatMessage( + integrationMessages.selfServicePortal + ), + value: SystemType.SelfServicePortal } ]} id={'permissions-selectors'} diff --git a/packages/commons/src/authentication.ts b/packages/commons/src/authentication.ts index 45003118427..cb4de20bf52 100644 --- a/packages/commons/src/authentication.ts +++ b/packages/commons/src/authentication.ts @@ -21,7 +21,8 @@ export { scopes, Scope, SCOPES } from './scopes' export const SYSTEM_INTEGRATION_SCOPES = { recordsearch: SCOPES.RECORDSEARCH, webhook: SCOPES.WEBHOOK, - nationalId: SCOPES.NATIONALID + nationalId: SCOPES.NATIONALID, + selfServicePortal: SCOPES.SELF_SERVICE_PORTAL } as const export const DEFAULT_ROLES_DEFINITION = [ @@ -174,7 +175,8 @@ export const DEFAULT_SYSTEM_INTEGRATION_ROLE_SCOPES = { HEALTH: [SCOPES.NOTIFICATION_API], NATIONAL_ID: [SCOPES.NATIONALID], RECORD_SEARCH: [SCOPES.RECORDSEARCH], - WEBHOOK: [SCOPES.WEBHOOK] + WEBHOOK: [SCOPES.WEBHOOK], + SELF_SERVICE_PORTAL: [SCOPES.SELF_SERVICE_PORTAL] } satisfies Record /* diff --git a/packages/commons/src/events/ActionDocument.ts b/packages/commons/src/events/ActionDocument.ts index 67f811d4736..361e13271ed 100644 --- a/packages/commons/src/events/ActionDocument.ts +++ b/packages/commons/src/events/ActionDocument.ts @@ -112,7 +112,7 @@ export type ActionDocument = z.infer export const ResolvedUser = z.object({ id: z.string(), - systemRole: z.string(), + role: z.string(), name: z.array( z.object({ use: z.string(), diff --git a/packages/commons/src/events/FieldConfig.ts b/packages/commons/src/events/FieldConfig.ts index 0059221615b..5e7c010bd83 100644 --- a/packages/commons/src/events/FieldConfig.ts +++ b/packages/commons/src/events/FieldConfig.ts @@ -82,7 +82,8 @@ const BaseField = z.object({ .default([]) .optional(), dependsOn: z.array(FieldId).default([]).optional(), - label: TranslationConfig + label: TranslationConfig, + hideLabel: z.boolean().default(false).optional() }) export type BaseField = z.infer @@ -99,7 +100,8 @@ export const FieldType = { CHECKBOX: 'CHECKBOX', SELECT: 'SELECT', COUNTRY: 'COUNTRY', - LOCATION: 'LOCATION' + LOCATION: 'LOCATION', + DIVIDER: 'DIVIDER' } as const export const fieldTypes = Object.values(FieldType) @@ -119,12 +121,18 @@ export interface FieldValueByType { [FieldType.SELECT]: SelectFieldValue } +const Divider = BaseField.extend({ + type: z.literal(FieldType.DIVIDER) +}) + const TextField = BaseField.extend({ type: z.literal(FieldType.TEXT), options: z .object({ maxLength: z.number().optional().describe('Maximum length of the text'), - type: z.enum(['text', 'email', 'password', 'number']).optional() + type: z.enum(['text', 'email', 'password', 'number']).optional(), + prefix: TranslationConfig.optional(), + postfix: TranslationConfig.optional() }) .default({ type: 'text' }) .optional() @@ -239,6 +247,7 @@ export type AllFields = | typeof File | typeof Country | typeof Location + | typeof Divider export const FieldConfig = z.discriminatedUnion('type', [ TextField, @@ -251,7 +260,8 @@ export const FieldConfig = z.discriminatedUnion('type', [ Checkbox, File, Country, - Location + Location, + Divider ]) as unknown as z.ZodDiscriminatedUnion<'type', AllFields[]> export type SelectField = z.infer diff --git a/packages/commons/src/scopes.ts b/packages/commons/src/scopes.ts index 5164d8a1250..be753605b1a 100644 --- a/packages/commons/src/scopes.ts +++ b/packages/commons/src/scopes.ts @@ -30,6 +30,7 @@ export const SCOPES = { NATIONALID: 'nationalId', NOTIFICATION_API: 'notification-api', RECORDSEARCH: 'recordsearch', + SELF_SERVICE_PORTAL: 'self-service-portal', // declare RECORD_DECLARE_BIRTH: 'record.declare-birth', diff --git a/packages/components/src/FormWizard/FormWizard.tsx b/packages/components/src/FormWizard/FormWizard.tsx index 9df4606a3c5..c59160bc659 100644 --- a/packages/components/src/FormWizard/FormWizard.tsx +++ b/packages/components/src/FormWizard/FormWizard.tsx @@ -52,7 +52,7 @@ export const FormWizard = ({ )} - + {children} diff --git a/packages/events/src/router/user/user.list.test.ts b/packages/events/src/router/user/user.list.test.ts index cc22ff808d6..301647af071 100644 --- a/packages/events/src/router/user/user.list.test.ts +++ b/packages/events/src/router/user/user.list.test.ts @@ -39,7 +39,7 @@ test('Returns user in correct format', async () => { { id: user.id, name: user.name, - systemRole: user.systemRole + role: user.role } ]) }) diff --git a/packages/events/src/service/deduplication/deduplication.test.ts b/packages/events/src/service/deduplication/deduplication.test.ts index 410ef96263a..c6741022816 100644 --- a/packages/events/src/service/deduplication/deduplication.test.ts +++ b/packages/events/src/service/deduplication/deduplication.test.ts @@ -10,9 +10,10 @@ */ import { getOrCreateClient } from '@events/storage/elasticsearch' -import { DeduplicationConfig, getUUID } from '@opencrvs/commons' +import { DeduplicationConfig, EventIndex, getUUID } from '@opencrvs/commons' import { searchForDuplicates } from './deduplication' import { getEventIndexName } from '@events/storage/__mocks__/elasticsearch' +import { encodeEventIndex } from '@events/service/indexing/indexing' const LEGACY_BIRTH_DEDUPLICATION_RULES = { id: 'Legacy birth deduplication check', @@ -155,11 +156,11 @@ export async function findDuplicates( index: getEventIndexName(), id: getUUID(), body: { - doc: { + doc: encodeEventIndex({ id: getUUID(), transactionId: getUUID(), data: existingComposition - }, + } as unknown as EventIndex), doc_as_upsert: true }, refresh: 'wait_for' diff --git a/packages/events/src/service/deduplication/deduplication.ts b/packages/events/src/service/deduplication/deduplication.ts index 1e7d5b9af4c..f8347e4902f 100644 --- a/packages/events/src/service/deduplication/deduplication.ts +++ b/packages/events/src/service/deduplication/deduplication.ts @@ -22,19 +22,25 @@ import { ClauseOutput } from '@opencrvs/commons/events' import { subDays, addDays } from 'date-fns' +import { + decodeEventIndex, + EncodedEventIndex, + encodeEventIndex, + encodeFieldId +} from '@events/service/indexing/indexing' function dataReference(fieldName: string) { return `data.${fieldName}` } function generateElasticsearchQuery( - eventIndex: EventIndex, + eventIndex: EncodedEventIndex, configuration: ClauseOutput ): elasticsearch.estypes.QueryDslQueryContainer | null { const matcherFieldWithoutData = configuration.type !== 'and' && configuration.type !== 'or' && - !eventIndex.data[configuration.fieldId] + !eventIndex.data[encodeFieldId(configuration.fieldId)] if (matcherFieldWithoutData) { return null @@ -66,47 +72,56 @@ function generateElasticsearchQuery( ) } } - case 'fuzzy': + case 'fuzzy': { + const encodedFieldId = encodeFieldId(configuration.fieldId) return { match: { - ['data.' + configuration.fieldId]: { - query: eventIndex.data[configuration.fieldId], + ['data.' + encodedFieldId]: { + query: eventIndex.data[encodedFieldId], fuzziness: configuration.options.fuzziness, boost: configuration.options.boost } } } - case 'strict': + } + case 'strict': { + const encodedFieldId = encodeFieldId(configuration.fieldId) return { match_phrase: { - [dataReference(configuration.fieldId)]: - eventIndex.data[configuration.fieldId] || '' + [dataReference(encodedFieldId)]: eventIndex.data[encodedFieldId] || '' } } - case 'dateRange': + } + case 'dateRange': { + const encodedFieldId = encodeFieldId(configuration.fieldId) + const origin = encodeFieldId(configuration.options.origin) return { range: { - [dataReference(configuration.fieldId)]: { + [dataReference(encodedFieldId)]: { // @TODO: Improve types for origin field to be sure it returns a string when accessing data gte: subDays( - new Date(eventIndex.data[configuration.options.origin] as string), + new Date(eventIndex.data[origin] as string), configuration.options.days ).toISOString(), lte: addDays( - new Date(eventIndex.data[configuration.options.origin] as string), + new Date(eventIndex.data[origin] as string), configuration.options.days ).toISOString() } } } - case 'dateDistance': + } + case 'dateDistance': { + const encodedFieldId = encodeFieldId(configuration.fieldId) + const origin = encodeFieldId(configuration.options.origin) return { distance_feature: { - field: dataReference(configuration.fieldId), + field: dataReference(encodedFieldId), pivot: `${configuration.options.days}d`, - origin: eventIndex.data[configuration.options.origin] + origin: eventIndex.data[origin] } } + } } } @@ -117,14 +132,17 @@ export async function searchForDuplicates( const esClient = getOrCreateClient() const query = Clause.parse(configuration.query) - const esQuery = generateElasticsearchQuery(eventIndex, query) + const esQuery = generateElasticsearchQuery( + encodeEventIndex(eventIndex), + query + ) if (!esQuery) { return [] } - const result = await esClient.search({ - index: getEventIndexName('TENNIS_CLUB_MEMBERSHIP'), + const result = await esClient.search({ + index: getEventIndexName(eventIndex.type), query: { bool: { should: [esQuery], @@ -137,6 +155,6 @@ export async function searchForDuplicates( .filter((hit) => hit._source) .map((hit) => ({ score: hit._score || 0, - event: hit._source + event: hit._source && decodeEventIndex(hit._source) })) } diff --git a/packages/events/src/service/indexing/indexing.ts b/packages/events/src/service/indexing/indexing.ts index 3f883c017e2..ffbf3f5a464 100644 --- a/packages/events/src/service/indexing/indexing.ts +++ b/packages/events/src/service/indexing/indexing.ts @@ -28,7 +28,34 @@ import { Transform } from 'stream' import { z } from 'zod' function eventToEventIndex(event: EventDocument): EventIndex { - return getCurrentEventState(event) + return encodeEventIndex(getCurrentEventState(event)) +} + +export type EncodedEventIndex = EventIndex +export function encodeEventIndex(event: EventIndex): EncodedEventIndex { + return { + ...event, + data: Object.entries(event.data).reduce( + (acc, [key, value]) => ({ + ...acc, + [encodeFieldId(key)]: value + }), + {} + ) + } +} + +export function decodeEventIndex(event: EncodedEventIndex): EventIndex { + return { + ...event, + data: Object.entries(event.data).reduce( + (acc, [key, value]) => ({ + ...acc, + [decodeFieldId(key)]: value + }), + {} + ) + } } /* @@ -86,12 +113,15 @@ export async function createIndex( function getElasticsearchMappingForType(field: FieldConfig) { switch (field.type) { case 'DATE': - return { type: 'date' } + // @TODO: This should be changed back to 'date' + // When we have proper validation of custom fields. + return { type: 'text' } case 'TEXT': case 'PARAGRAPH': - case 'PAGE_HEADER': case 'BULLET_LIST': + case 'PAGE_HEADER': return { type: 'text' } + case 'DIVIDER': case 'RADIO_GROUP': case 'SELECT': case 'COUNTRY': @@ -117,11 +147,21 @@ function assertNever(): never { throw new Error('Should never happen') } +const SEPARATOR = '____' + +export function encodeFieldId(fieldId: string) { + return fieldId.replaceAll('.', SEPARATOR) +} + +function decodeFieldId(fieldId: string) { + return fieldId.replaceAll(SEPARATOR, '.') +} + function formFieldsToDataMapping(fields: FieldConfig[]) { return fields.reduce((acc, field) => { return { ...acc, - [field.id]: getElasticsearchMappingForType(field) + [encodeFieldId(field.id)]: getElasticsearchMappingForType(field) } }, {}) } @@ -203,11 +243,18 @@ export async function getIndexedEvents() { return [] } - const response = await esClient.search({ + const response = await esClient.search({ index: getEventAliasName(), size: 10000, request_cache: false }) - return z.array(EventIndex).parse(response.hits.hits.map((hit) => hit._source)) + const events = z.array(EventIndex).parse( + response.hits.hits + .map((hit) => hit._source) + .filter((event): event is EncodedEventIndex => event !== undefined) + .map((event) => decodeEventIndex(event)) + ) + + return events } diff --git a/packages/events/src/service/users/users.ts b/packages/events/src/service/users/users.ts index 8659466a71e..dba3574aece 100644 --- a/packages/events/src/service/users/users.ts +++ b/packages/events/src/service/users/users.ts @@ -24,7 +24,7 @@ export const getUsersById = async (ids: string[]) => { .collection<{ _id: ObjectId name: ResolvedUser['name'] - systemRole: string + role: string }>('users') .find({ _id: { @@ -38,6 +38,6 @@ export const getUsersById = async (ids: string[]) => { return results.map((user) => ({ id: user._id.toString(), name: user.name, - systemRole: user.systemRole + role: user.role })) } diff --git a/packages/events/src/tests/generators.ts b/packages/events/src/tests/generators.ts index 9612f5c3aa7..bf05d740137 100644 --- a/packages/events/src/tests/generators.ts +++ b/packages/events/src/tests/generators.ts @@ -30,13 +30,13 @@ interface Name { export interface CreatedUser { id: string primaryOfficeId: string - systemRole: string + role: string name: Array } interface CreateUser { primaryOfficeId: string - systemRole?: string + role?: string name?: Array } /** @@ -127,7 +127,7 @@ export function payloadGenerator() { const user = { create: (input: CreateUser) => ({ - systemRole: input.systemRole ?? 'REGISTRATION_AGENT', + role: input.role ?? 'REGISTRATION_AGENT', name: input.name ?? [{ use: 'en', family: 'Doe', given: ['John'] }], primaryOfficeId: input.primaryOfficeId }) @@ -168,7 +168,7 @@ export function seeder() { return { primaryOfficeId: user.primaryOfficeId, name: user.name, - systemRole: user.systemRole, + role: user.role, id: createdUser.insertedId.toString() } } diff --git a/packages/events/tsconfig.json b/packages/events/tsconfig.json index fdabbd8f49b..f5e4c7bb98b 100644 --- a/packages/events/tsconfig.json +++ b/packages/events/tsconfig.json @@ -11,7 +11,7 @@ "skipLibCheck": true, "moduleResolution": "node16", "rootDir": ".", - "lib": ["esnext.asynciterable", "es6", "es2019"], + "lib": ["esnext.asynciterable", "es6", "es2019", "es2021"], "forceConsistentCasingInFileNames": true, "noImplicitReturns": true, "noImplicitThis": true, diff --git a/packages/gateway/src/features/registration/root-resolvers.ts b/packages/gateway/src/features/registration/root-resolvers.ts index 18d51cd915f..7e202cea767 100644 --- a/packages/gateway/src/features/registration/root-resolvers.ts +++ b/packages/gateway/src/features/registration/root-resolvers.ts @@ -513,7 +513,10 @@ export const resolvers: GQLResolver = { }, markBirthAsCertified: requireAssignment( async (_, { id, details }, { headers: authHeader }) => { - if (!hasScope(authHeader, SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES)) { + if ( + !hasScope(authHeader, SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES) && + !hasScope(authHeader, SCOPES.SELF_SERVICE_PORTAL) + ) { throw new Error('User does not have enough scope') } return markEventAsCertified(id, details, authHeader, EVENT_TYPE.BIRTH) @@ -521,14 +524,20 @@ export const resolvers: GQLResolver = { ), // @todo: add new query for certify and issue later where require assignment wrapper will be used markBirthAsIssued: (_, { id, details }, { headers: authHeader }) => { - if (!hasScope(authHeader, SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES)) { + if ( + !hasScope(authHeader, SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES) && + !hasScope(authHeader, SCOPES.SELF_SERVICE_PORTAL) + ) { throw new Error('User does not have enough scope') } return markEventAsIssued(id, details, authHeader, EVENT_TYPE.BIRTH) }, markDeathAsCertified: requireAssignment( (_, { id, details }, { headers: authHeader }) => { - if (!hasScope(authHeader, SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES)) { + if ( + !hasScope(authHeader, SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES) && + !hasScope(authHeader, SCOPES.SELF_SERVICE_PORTAL) + ) { throw new Error('User does not have enough scope') } return markEventAsCertified(id, details, authHeader, EVENT_TYPE.DEATH) @@ -536,7 +545,10 @@ export const resolvers: GQLResolver = { ), // @todo: add new query for certify and issue later where require assignment wrapper will be used markDeathAsIssued: (_, { id, details }, { headers: authHeader }) => { - if (!hasScope(authHeader, SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES)) { + if ( + !hasScope(authHeader, SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES) && + !hasScope(authHeader, SCOPES.SELF_SERVICE_PORTAL) + ) { throw new Error('User does not have enough scope') } return markEventAsIssued(id, details, authHeader, EVENT_TYPE.DEATH) diff --git a/packages/gateway/src/features/registration/utils.ts b/packages/gateway/src/features/registration/utils.ts index 9c33de2b9dc..49417a14b2b 100644 --- a/packages/gateway/src/features/registration/utils.ts +++ b/packages/gateway/src/features/registration/utils.ts @@ -40,6 +40,9 @@ export async function setCollectorForPrintInAdvance( authHeader: IAuthHeader ) { const tokenPayload = getTokenPayload(authHeader.Authorization.split(' ')[1]) + if (tokenPayload.scope.indexOf('self-service-portal') > -1) { + return details + } const userId = tokenPayload.sub const userDetails = await getUser({ userId }, authHeader) const name = userDetails.name.map((nameItem) => ({ diff --git a/packages/gateway/src/features/systems/schema.graphql b/packages/gateway/src/features/systems/schema.graphql index e1893cb2393..b05f2e08ca9 100644 --- a/packages/gateway/src/features/systems/schema.graphql +++ b/packages/gateway/src/features/systems/schema.graphql @@ -25,6 +25,7 @@ enum SystemType { HEALTH RECORD_SEARCH WEBHOOK + SELF_SERVICE_PORTAL } enum IntegratingSystemType { diff --git a/packages/gateway/src/graphql/config.ts b/packages/gateway/src/graphql/config.ts index f3f51b95a79..f9b9e1f018e 100644 --- a/packages/gateway/src/graphql/config.ts +++ b/packages/gateway/src/graphql/config.ts @@ -199,7 +199,9 @@ export function authSchemaTransformer(schema: GraphQLSchema) { try { const userId = credentials.sub let user: IUserModelData | ISystemModelData - const isSystemUser = credentials.scope.indexOf('recordsearch') > -1 + const isSystemUser = + credentials.scope.indexOf('recordsearch') > -1 || + credentials.scope.indexOf('self-service-portal') > -1 if (isSystemUser) { user = await getSystem( { systemId: userId }, diff --git a/packages/gateway/src/graphql/schema.d.ts b/packages/gateway/src/graphql/schema.d.ts index 1e91d5eb4e4..f06a8ec6711 100644 --- a/packages/gateway/src/graphql/schema.d.ts +++ b/packages/gateway/src/graphql/schema.d.ts @@ -979,7 +979,8 @@ export const enum GQLSystemType { NATIONAL_ID = 'NATIONAL_ID', HEALTH = 'HEALTH', RECORD_SEARCH = 'RECORD_SEARCH', - WEBHOOK = 'WEBHOOK' + WEBHOOK = 'WEBHOOK', + SELF_SERVICE_PORTAL = 'SELF_SERVICE_PORTAL' } export const enum GQLIntegratingSystemType { diff --git a/packages/gateway/src/graphql/schema.graphql b/packages/gateway/src/graphql/schema.graphql index 6cad976ed93..ecbc8b9b474 100644 --- a/packages/gateway/src/graphql/schema.graphql +++ b/packages/gateway/src/graphql/schema.graphql @@ -1073,6 +1073,7 @@ enum SystemType { HEALTH RECORD_SEARCH WEBHOOK + SELF_SERVICE_PORTAL } enum IntegratingSystemType { diff --git a/packages/gateway/tsconfig.json b/packages/gateway/tsconfig.json index 7c94cd211e4..03881691ae4 100644 --- a/packages/gateway/tsconfig.json +++ b/packages/gateway/tsconfig.json @@ -12,7 +12,7 @@ "sourceMap": true, "moduleResolution": "node16", "rootDir": ".", - "lib": ["esnext.asynciterable", "es6", "es2019", "DOM.Iterable"], + "lib": ["esnext.asynciterable", "es6", "es2019", "DOM.Iterable", "es2021"], "forceConsistentCasingInFileNames": true, "noImplicitReturns": true, "noImplicitThis": true, diff --git a/packages/metrics/src/features/audit/handler.ts b/packages/metrics/src/features/audit/handler.ts index beee98dccd7..c0c2f2779ac 100644 --- a/packages/metrics/src/features/audit/handler.ts +++ b/packages/metrics/src/features/audit/handler.ts @@ -81,6 +81,30 @@ export async function getUser( return await res.json() } +export async function getSystem( + systemId: string, + authHeader: { Authorization: string } +) { + const res = await fetch(`${USER_MANAGEMENT_URL}/getSystem`, { + method: 'POST', + body: JSON.stringify({ systemId }), + headers: { + 'Content-Type': 'application/json', + ...authHeader + } + }) + + if (!res.ok) { + throw new Error( + `Unable to retrieve user mobile number. Error: ${ + res.status + } status received. ${await res.text()}` + ) + } + + return await res.json() +} + export async function getUserAuditsHandler(request: Hapi.Request) { const practitionerId = request.query[PRACTITIONER_ID] const skip = request.query['skip'] || 0 diff --git a/packages/metrics/src/features/registration/handler.ts b/packages/metrics/src/features/registration/handler.ts index 4f72c2b07af..864ac2fe209 100644 --- a/packages/metrics/src/features/registration/handler.ts +++ b/packages/metrics/src/features/registration/handler.ts @@ -118,7 +118,6 @@ export async function sentNotificationForReviewHandler( h: Hapi.ResponseToolkit ) { const points = [] - await createUserAuditPointFromFHIR('DECLARED', request) try { diff --git a/packages/metrics/src/features/registration/pointGenerator.ts b/packages/metrics/src/features/registration/pointGenerator.ts index d7d9a0dc919..c6fda604f7b 100644 --- a/packages/metrics/src/features/registration/pointGenerator.ts +++ b/packages/metrics/src/features/registration/pointGenerator.ts @@ -68,7 +68,7 @@ import { OPENCRVS_SPECIFICATION_URL } from '@metrics/features/metrics/constants' import { fetchTaskHistory } from '@metrics/api' import { EVENT_TYPE } from '@metrics/features/metrics/utils' import { getTokenPayload } from '@metrics/utils/authUtils' -import { getUser } from '@metrics/features/audit/handler' +import { getSystem, getUser } from '@metrics/features/audit/handler' export const generateInCompleteFieldPoints = async ( payload: fhir.Bundle, @@ -507,9 +507,20 @@ export async function generateDeclarationStartedPoint( const task = getTask(payload) const tokenPayload = getTokenPayload(authHeader.Authorization) - const userId = tokenPayload.sub - const user = await getUser(userId, authHeader) - + const isNotificationAPIUser = + tokenPayload.scope.indexOf('notification-api') > -1 + const isSelfServicePortalAPIUser = + tokenPayload.scope.indexOf('self-service-portal') > -1 + let user + if (isNotificationAPIUser || isSelfServicePortalAPIUser) { + user = await getSystem(tokenPayload.sub, { + Authorization: `Bearer ${authHeader.Authorization}` + }) + } else { + user = await getUser(tokenPayload.sub, { + Authorization: `Bearer ${authHeader.Authorization}` + }) + } if (!composition) { throw new Error('composition not found') } diff --git a/packages/search/src/config/routes.ts b/packages/search/src/config/routes.ts index ab17ae3c767..0d4de81b08c 100644 --- a/packages/search/src/config/routes.ts +++ b/packages/search/src/config/routes.ts @@ -251,7 +251,8 @@ export const getRoutes = () => { SCOPES.SEARCH_MARRIAGE, SCOPES.SEARCH_BIRTH_MY_JURISDICTION, SCOPES.SEARCH_DEATH_MY_JURISDICTION, - SCOPES.SEARCH_MARRIAGE_MY_JURISDICTION + SCOPES.SEARCH_MARRIAGE_MY_JURISDICTION, + SCOPES.SELF_SERVICE_PORTAL ] }, description: 'Handles searching from declarations' @@ -270,7 +271,8 @@ export const getRoutes = () => { SCOPES.SEARCH_MARRIAGE, SCOPES.SEARCH_BIRTH_MY_JURISDICTION, SCOPES.SEARCH_DEATH_MY_JURISDICTION, - SCOPES.SEARCH_MARRIAGE_MY_JURISDICTION + SCOPES.SEARCH_MARRIAGE_MY_JURISDICTION, + SCOPES.SELF_SERVICE_PORTAL ] }, description: 'Handle searching from death declarations' diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index 67306aedfcd..ed53a5be02f 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/toolkit", - "version": "0.0.30-rr", + "version": "0.0.32-jr", "description": "OpenCRVS toolkit for building country configurations", "license": "MPL-2.0", "exports": { @@ -15,12 +15,14 @@ "build:all": "lerna run build --include-dependencies --scope @opencrvs/toolkit && ./build.sh" }, "dependencies": { + "uuid": "^9.0.0", "ajv": "^8.17.1", "superjson": "1.9.0-0", "@trpc/client": "^11.0.0-rc.648", "@trpc/server": "^11.0.0-rc.532" }, "devDependencies": { + "@types/uuid": "^9.0.3", "esbuild": "^0.24.0", "typescript": "^5.6.3", "@opencrvs/events": "^1.7.0" diff --git a/packages/user-mgnt/src/model/system.ts b/packages/user-mgnt/src/model/system.ts index a2d1bafdce3..b439aa60c68 100644 --- a/packages/user-mgnt/src/model/system.ts +++ b/packages/user-mgnt/src/model/system.ts @@ -78,7 +78,13 @@ const systemSchema = new Schema({ creationDate: { type: Number, default: Date.now }, type: { type: String, - enum: [types.HEALTH, types.NATIONAL_ID, types.RECORD_SEARCH, types.WEBHOOK] + enum: [ + types.HEALTH, + types.NATIONAL_ID, + types.RECORD_SEARCH, + types.WEBHOOK, + types.SELF_SERVICE_PORTAL + ] }, integratingSystemType: { type: String, diff --git a/packages/user-mgnt/src/utils/system.ts b/packages/user-mgnt/src/utils/system.ts index ef3a21084c4..94c1c81774a 100644 --- a/packages/user-mgnt/src/utils/system.ts +++ b/packages/user-mgnt/src/utils/system.ts @@ -16,7 +16,8 @@ export const types = { NATIONAL_ID: 'NATIONAL_ID', HEALTH: 'HEALTH', RECORD_SEARCH: 'RECORD_SEARCH', - WEBHOOK: 'WEBHOOK' + WEBHOOK: 'WEBHOOK', + SELF_SERVICE_PORTAL: 'SELF_SERVICE_PORTAL' } export const integratingSystemTypes = { diff --git a/packages/webhooks/src/features/manage/handler.test.ts b/packages/webhooks/src/features/manage/handler.test.ts index 573f9454058..edc6ed58722 100644 --- a/packages/webhooks/src/features/manage/handler.test.ts +++ b/packages/webhooks/src/features/manage/handler.test.ts @@ -150,7 +150,7 @@ describe('subscribeWebhooksHandler handler', () => { expect(res.statusCode).toBe(400) }) - it('return an error if a topic is unsupported', async () => { + it('return an error if a topic is undefined', async () => { fetch.mockResponses( [JSON.stringify(mockActiveSystem), { status: 200 }], [JSON.stringify({ challenge: '123' }), { status: 200 }] @@ -167,14 +167,14 @@ describe('subscribeWebhooksHandler handler', () => { callback: 'https://www.your-great-domain.com/webhooks', mode: 'subscribe', secret: '123', - topic: 'XXX' + topic: undefined } }, headers: { Authorization: `Bearer ${token}` } }) - expect(res.result.hub.reason).toEqual('Unsupported topic: XXX') + expect(res.result.hub.reason).toEqual('hub.topic is required') expect(res.statusCode).toBe(400) }) diff --git a/packages/webhooks/src/features/manage/handler.ts b/packages/webhooks/src/features/manage/handler.ts index 4ca526f0f8d..d0166f2e8ef 100644 --- a/packages/webhooks/src/features/manage/handler.ts +++ b/packages/webhooks/src/features/manage/handler.ts @@ -40,17 +40,16 @@ export async function subscribeWebhooksHandler( h: Hapi.ResponseToolkit ) { const { hub } = request.payload as ISubscribePayload - // if (!(hub.topic in TRIGGERS)) { - // return h - // .response({ - // hub: { - // mode: 'denied', - // topic: hub.topic, - // reason: `Unsupported topic: ${hub.topic}` - // } - // }) - // .code(400) - // } + if (hub.topic === undefined) { + return h + .response({ + hub: { + mode: 'denied', + reason: 'hub.topic is required' + } + }) + .code(400) + } const token: ITokenPayload = getTokenPayload( request.headers.authorization.split(' ')[1] ) diff --git a/packages/workflow/src/config/routes.ts b/packages/workflow/src/config/routes.ts index 85bdb67ac91..1a7e091266e 100644 --- a/packages/workflow/src/config/routes.ts +++ b/packages/workflow/src/config/routes.ts @@ -95,7 +95,8 @@ export const getRoutes = () => { scope: [ SCOPES.RECORD_DECLARE_BIRTH, SCOPES.RECORD_DECLARE_DEATH, - SCOPES.RECORD_DECLARE_MARRIAGE + SCOPES.RECORD_DECLARE_MARRIAGE, + SCOPES.SELF_SERVICE_PORTAL ] }, tags: ['api'], diff --git a/packages/workflow/src/features/user/utils.ts b/packages/workflow/src/features/user/utils.ts index 758646d39f0..cea0b743e15 100644 --- a/packages/workflow/src/features/user/utils.ts +++ b/packages/workflow/src/features/user/utils.ts @@ -137,9 +137,14 @@ export async function getLoggedInPractitionerResource( const isNotificationAPIUser = tokenPayload.scope.indexOf('notification-api') > -1 const isRecordSearchAPIUser = tokenPayload.scope.indexOf('recordsearch') > -1 - + const isSelfServicePortalAPIUser = + tokenPayload.scope.indexOf('self-service-portal') > -1 let userResponse - if (isNotificationAPIUser || isRecordSearchAPIUser) { + if ( + isNotificationAPIUser || + isRecordSearchAPIUser || + isSelfServicePortalAPIUser + ) { userResponse = await getSystem(tokenPayload.sub, { Authorization: `Bearer ${token}` }) diff --git a/packages/workflow/src/records/fhir.ts b/packages/workflow/src/records/fhir.ts index a1ad32c7595..ed176c9b4fb 100644 --- a/packages/workflow/src/records/fhir.ts +++ b/packages/workflow/src/records/fhir.ts @@ -136,14 +136,14 @@ export async function withPractitionerDetails( type }) }) - return [ - newTask, - { - type: 'document', - resourceType: 'Bundle', - entry: [] - } - ] + // return [ + // newTask, + // { + // type: 'document', + // resourceType: 'Bundle', + // entry: [] + // } + // ] } const user = userOrSystem const practitioner = await getLoggedInPractitionerResource(token) diff --git a/packages/workflow/src/records/handler/archive.test.ts b/packages/workflow/src/records/handler/archive.test.ts index 676a2261d11..080504699e5 100644 --- a/packages/workflow/src/records/handler/archive.test.ts +++ b/packages/workflow/src/records/handler/archive.test.ts @@ -22,6 +22,10 @@ import { } from '@opencrvs/commons/types' import { READY_FOR_REVIEW_BIRTH_RECORD } from '@test/mocks/records/readyForReview' import { SCOPES } from '@opencrvs/commons/authentication' +import { invokeWebhooks } from '@workflow/records/webhooks' +import { getEventType } from '@workflow/features/registration/utils' + +jest.mock('@workflow/records/webhooks') describe('archive record endpoint', () => { let server: Awaited> @@ -85,6 +89,14 @@ describe('archive record endpoint', () => { } }) + expect(invokeWebhooks).toHaveBeenCalledWith({ + bundle: READY_FOR_REVIEW_BIRTH_RECORD, + token, + event: getEventType(READY_FOR_REVIEW_BIRTH_RECORD), + isNotRegistered: true, + statusType: 'archived' + }) + const task = getTaskFromSavedBundle(JSON.parse(res.payload) as ValidRecord) const businessStatus = getStatusFromTask(task) diff --git a/packages/workflow/src/records/handler/certify.test.ts b/packages/workflow/src/records/handler/certify.test.ts index b2030b912f3..1e10b9560b8 100644 --- a/packages/workflow/src/records/handler/certify.test.ts +++ b/packages/workflow/src/records/handler/certify.test.ts @@ -22,6 +22,10 @@ import { } from '@opencrvs/commons/types' import { REGISTERED_BIRTH_RECORD } from '@test/mocks/records/register' import { SCOPES } from '@opencrvs/commons/authentication' +import { invokeWebhooks } from '@workflow/records/webhooks' +import { getEventType } from '@workflow/features/registration/utils' + +jest.mock('@workflow/records/webhooks') describe('Certify record endpoint', () => { let server: Awaited> @@ -241,6 +245,14 @@ describe('Certify record endpoint', () => { } }) + expect(invokeWebhooks).toHaveBeenCalledWith({ + bundle: REGISTERED_BIRTH_RECORD, + token, + event: getEventType(REGISTERED_BIRTH_RECORD), + isNotRegistered: true, + statusType: 'certified' + }) + const certifiedRecord = JSON.parse(response.payload) as CertifiedRecord const task = getTaskFromSavedBundle(certifiedRecord) const businessStatus = getStatusFromTask(task) diff --git a/packages/workflow/src/records/handler/certify.ts b/packages/workflow/src/records/handler/certify.ts index 7ecd6325936..0e1110bb47c 100644 --- a/packages/workflow/src/records/handler/certify.ts +++ b/packages/workflow/src/records/handler/certify.ts @@ -28,7 +28,10 @@ export const certifyRoute = createRoute({ allowedStartStates: ['REGISTERED'], action: 'CERTIFY', includeHistoryResources: true, - allowedScopes: [SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES], + allowedScopes: [ + SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES, + SCOPES.SELF_SERVICE_PORTAL + ], handler: async (request, record): Promise => { const token = getToken(request) const { certificate: certificateDetailsWithRawAttachments, event } = diff --git a/packages/workflow/src/records/handler/create.ts b/packages/workflow/src/records/handler/create.ts index 7d7bf71832d..1b5f59cfdfa 100644 --- a/packages/workflow/src/records/handler/create.ts +++ b/packages/workflow/src/records/handler/create.ts @@ -282,7 +282,6 @@ export default async function createRecordHandler( requestSchema, request.payload ) - const existingDeclarationIds = recordDetails.registration?.draftId && (await findExistingDeclarationIds(recordDetails.registration.draftId)) @@ -335,6 +334,7 @@ export default async function createRecordHandler( record = await toWaitingForExternalValidationState(record, token) await auditEvent('waiting-external-validation', record, token) } + const eventAction = getEventAction(record) await indexBundle(record, token) diff --git a/packages/workflow/src/records/handler/issue.test.ts b/packages/workflow/src/records/handler/issue.test.ts index 887988ed362..915f7077655 100644 --- a/packages/workflow/src/records/handler/issue.test.ts +++ b/packages/workflow/src/records/handler/issue.test.ts @@ -22,6 +22,10 @@ import { } from '@opencrvs/commons/types' import { CERTIFIED_BIRTH_RECORD } from '@test/mocks/records/certify' import { SCOPES } from '@opencrvs/commons/authentication' +import { invokeWebhooks } from '@workflow/records/webhooks' +import { getEventType } from '@workflow/features/registration/utils' + +jest.mock('@workflow/records/webhooks') describe('Issue record endpoint', () => { let server: Awaited> @@ -127,6 +131,14 @@ describe('Issue record endpoint', () => { } }) + expect(invokeWebhooks).toHaveBeenCalledWith({ + bundle: CERTIFIED_BIRTH_RECORD, + token, + event: getEventType(CERTIFIED_BIRTH_RECORD), + isNotRegistered: true, + statusType: 'issued' + }) + const issuedRecord = JSON.parse(response.payload) as IssuedRecord const task = getTaskFromSavedBundle(issuedRecord) const businessStatus = getStatusFromTask(task) diff --git a/packages/workflow/src/records/handler/issue.ts b/packages/workflow/src/records/handler/issue.ts index 8b4ac71ab3b..d74c0f2886a 100644 --- a/packages/workflow/src/records/handler/issue.ts +++ b/packages/workflow/src/records/handler/issue.ts @@ -28,7 +28,10 @@ export const issueRoute = createRoute({ allowedStartStates: ['CERTIFIED'], action: 'ISSUE', includeHistoryResources: true, - allowedScopes: [SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES], + allowedScopes: [ + SCOPES.RECORD_PRINT_ISSUE_CERTIFIED_COPIES, + SCOPES.SELF_SERVICE_PORTAL + ], handler: async (request, record): Promise => { const token = getToken(request) const { certificate: certificateDetailsWithRawAttachments, event } = diff --git a/packages/workflow/src/records/handler/reject.test.ts b/packages/workflow/src/records/handler/reject.test.ts index 750041a90c8..2bafbc137eb 100644 --- a/packages/workflow/src/records/handler/reject.test.ts +++ b/packages/workflow/src/records/handler/reject.test.ts @@ -23,6 +23,10 @@ import { } from '@opencrvs/commons/types' import { READY_FOR_REVIEW_BIRTH_RECORD } from '@test/mocks/records/readyForReview' import { SCOPES } from '@opencrvs/commons/authentication' +import { invokeWebhooks } from '@workflow/records/webhooks' +import { getEventType } from '@workflow/features/registration/utils' + +jest.mock('@workflow/records/webhooks') function getReasonFromTask(task: SavedTask) { return task.statusReason?.text @@ -95,6 +99,14 @@ describe('Reject record endpoint', () => { } }) + expect(invokeWebhooks).toHaveBeenCalledWith({ + bundle: READY_FOR_REVIEW_BIRTH_RECORD, + token, + event: getEventType(READY_FOR_REVIEW_BIRTH_RECORD), + isNotRegistered: true, + statusType: 'rejected' + }) + const task = getTaskFromSavedBundle( JSON.parse(response.payload) as ValidRecord ) diff --git a/packages/workflow/src/records/handler/update-field.ts b/packages/workflow/src/records/handler/update-field.ts index 7ccd6a5c01b..c4d74b1bcb5 100644 --- a/packages/workflow/src/records/handler/update-field.ts +++ b/packages/workflow/src/records/handler/update-field.ts @@ -61,12 +61,7 @@ export async function updateField( const updatedRecord = { ...savedRecord, - entry: [ - ...savedRecord.entry.filter( - ({ resource }) => !isQuestionnaireResponse(resource) - ), - updatedQuestionnaireResponseResource - ] + entry: [updatedQuestionnaireResponseResource] } await sendBundleToHearth(updatedRecord) diff --git a/packages/workflow/src/records/handler/validate.test.ts b/packages/workflow/src/records/handler/validate.test.ts index 5563fc3e3bf..069156cf8b1 100644 --- a/packages/workflow/src/records/handler/validate.test.ts +++ b/packages/workflow/src/records/handler/validate.test.ts @@ -22,6 +22,10 @@ import { ValidRecord } from '@opencrvs/commons/types' import { SCOPES } from '@opencrvs/commons/authentication' +import { getEventType } from '@workflow/features/registration/utils' +import { invokeWebhooks } from '@workflow/records/webhooks' + +jest.mock('@workflow/records/webhooks') describe('Validate record endpoint', () => { let server: Awaited> @@ -88,6 +92,15 @@ describe('Validate record endpoint', () => { const task = getTaskFromSavedBundle( JSON.parse(response.payload) as ValidRecord ) + + expect(invokeWebhooks).toHaveBeenCalledWith({ + bundle: READY_FOR_REVIEW_BIRTH_RECORD, + token, + event: getEventType(READY_FOR_REVIEW_BIRTH_RECORD), + isNotRegistered: true, + statusType: 'validated' + }) + const businessStatus = getStatusFromTask(task) expect(response.statusCode).toBe(200) diff --git a/packages/workflow/src/records/user.ts b/packages/workflow/src/records/user.ts index 33c257d60ce..1ecbab5abe6 100644 --- a/packages/workflow/src/records/user.ts +++ b/packages/workflow/src/records/user.ts @@ -77,8 +77,15 @@ export async function getUserOrSystem( const tokenPayload = getTokenPayload(token) const isNotificationAPIUser = tokenPayload.scope.includes('notification-api') const isRecordSearchAPIUser = tokenPayload.scope.includes('recordsearch') + const isSelfServicePortalAPIUser = tokenPayload.scope.includes( + 'self-service-portal' + ) - if (isNotificationAPIUser || isRecordSearchAPIUser) { + if ( + isNotificationAPIUser || + isRecordSearchAPIUser || + isSelfServicePortalAPIUser + ) { return await getSystem(tokenPayload.sub, { Authorization: `Bearer ${token}` })