diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index ec732943e5c..468ccd92f66 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -78,7 +78,6 @@ services: - NODE_ENV=development - FHIR_URL=http://hearth:3447/fhir - AUTH_URL=http://auth:4040 - - CONFIRM_REGISTRATION_URL=http://workflow:5050/confirm/registration - CHECK_INVALID_TOKEN=true ports: - '3040:3040' diff --git a/packages/commons/src/authentication.ts b/packages/commons/src/authentication.ts index 384a263473e..585273e9e37 100644 --- a/packages/commons/src/authentication.ts +++ b/packages/commons/src/authentication.ts @@ -26,7 +26,8 @@ export const userScopes = { bypassRateLimit: 'bypassratelimit', teams: 'teams', config: 'config', - confirmRegistration: 'record.confirm-registration' + confirmRegistration: 'record.confirm-registration', + rejectRegistration: 'record.reject-registration' } as const export const userRoleScopes = { diff --git a/packages/gateway/src/features/registration/root-resolvers.ts b/packages/gateway/src/features/registration/root-resolvers.ts index bad69765683..d241a6289dc 100644 --- a/packages/gateway/src/features/registration/root-resolvers.ts +++ b/packages/gateway/src/features/registration/root-resolvers.ts @@ -58,7 +58,9 @@ import { duplicateRegistration, viewDeclaration, verifyRegistration, - markNotADuplicate + markNotADuplicate, + rejectRegistration, + confirmRegistration } from '@gateway/workflow/index' import { getRecordById } from '@gateway/records' @@ -636,20 +638,46 @@ export const resolvers: GQLResolver = { return taskEntry.resource.id }, - async confirmRegistration(_, { id }, { headers: authHeader }) { + async confirmRegistration(_, { id, details }, { headers: authHeader }) { if (!inScope(authHeader, ['record.confirm-registration'])) { - throw new Error( - 'User does not have a "record.confirm-registration" scope' - ) + throw new Error('User does not have a Confirm Registration scope') + } + + if (!hasRecordAccess(authHeader, id)) { + throw new Error('User does not have access to the record') + } + + try { + const taskEntry = await confirmRegistration(id, authHeader, { + error: details.error, + registrationNumber: details.registrationNumber, + identifiers: details.identifiers + }) + + return taskEntry.resource.id + } catch (error) { + throw new Error(`Failed to confirm registration: ${error.message}`) + } + }, + async rejectRegistration(_, { id, details }, { headers: authHeader }) { + if (!inScope(authHeader, ['record.reject-registration'])) { + throw new Error('User does not have a Reject Registration" scope') } if (!hasRecordAccess(authHeader, id)) { throw new Error('User does not have access to the record') } - // @TODO this is a no-op, only to test the token exchange actually works - // An upcoming pull request will implement this and a `rejectRegistration` mutations - return id + try { + const taskEntry = await rejectRegistration(id, authHeader, { + comment: details.comment || 'No comment provided', + reason: details.reason + }) + + return taskEntry.resource.id + } catch (error) { + throw new Error(`Error in rejectRegistration: ${error.message}`) + } } } } diff --git a/packages/gateway/src/features/registration/schema.graphql b/packages/gateway/src/features/registration/schema.graphql index eab611f0358..a5992899db7 100644 --- a/packages/gateway/src/features/registration/schema.graphql +++ b/packages/gateway/src/features/registration/schema.graphql @@ -567,6 +567,22 @@ input CorrectionRejectionInput { timeLoggedMS: Int! } +input IdentifierInput { + type: String! + value: String! +} + +input ConfirmRegistrationInput { + registrationNumber: String! + error: String + identifiers: [IdentifierInput!] +} + +input RejectRegistrationInput { + reason: String! + comment: String +} + type Mutation { # Generic correction handlers for all event types # Applying a correction request is made on a event level as payload is dependant on event type @@ -643,5 +659,6 @@ type Mutation { comment: String duplicateTrackingId: String ): ID! - confirmRegistration(id: ID!): ID! + confirmRegistration(id: ID!, details: ConfirmRegistrationInput!): ID! + rejectRegistration(id: ID!, details: RejectRegistrationInput!): ID! } diff --git a/packages/gateway/src/graphql/schema.d.ts b/packages/gateway/src/graphql/schema.d.ts index 31096478dd8..6d9a27bf80b 100644 --- a/packages/gateway/src/graphql/schema.d.ts +++ b/packages/gateway/src/graphql/schema.d.ts @@ -86,6 +86,7 @@ export interface GQLMutation { markMarriageAsIssued: string markEventAsDuplicate: string confirmRegistration: string + rejectRegistration: string createOrUpdateUser: GQLUser activateUser?: string changePassword?: string @@ -576,6 +577,17 @@ export interface GQLReinstated { registrationStatus?: GQLRegStatus } +export interface GQLConfirmRegistrationInput { + registrationNumber: string + error?: string + identifiers?: Array +} + +export interface GQLRejectRegistrationInput { + reason: string + comment?: string +} + export interface GQLUserInput { id?: string name: Array @@ -1169,6 +1181,11 @@ export const enum GQLRegStatus { ISSUED = 'ISSUED' } +export interface GQLIdentifierInput { + type: string + value: string +} + export interface GQLHumanNameInput { use?: string firstNames?: string @@ -2452,6 +2469,7 @@ export interface GQLMutationTypeResolver { markMarriageAsIssued?: MutationToMarkMarriageAsIssuedResolver markEventAsDuplicate?: MutationToMarkEventAsDuplicateResolver confirmRegistration?: MutationToConfirmRegistrationResolver + rejectRegistration?: MutationToRejectRegistrationResolver createOrUpdateUser?: MutationToCreateOrUpdateUserResolver activateUser?: MutationToActivateUserResolver changePassword?: MutationToChangePasswordResolver @@ -2986,6 +3004,7 @@ export interface MutationToMarkEventAsDuplicateResolver< export interface MutationToConfirmRegistrationArgs { id: string + details: GQLConfirmRegistrationInput } export interface MutationToConfirmRegistrationResolver< TParent = any, @@ -2999,6 +3018,22 @@ export interface MutationToConfirmRegistrationResolver< ): TResult } +export interface MutationToRejectRegistrationArgs { + id: string + details: GQLRejectRegistrationInput +} +export interface MutationToRejectRegistrationResolver< + TParent = any, + TResult = any +> { + ( + parent: TParent, + args: MutationToRejectRegistrationArgs, + context: Context, + info: GraphQLResolveInfo + ): TResult +} + export interface MutationToCreateOrUpdateUserArgs { user: GQLUserInput } diff --git a/packages/gateway/src/graphql/schema.graphql b/packages/gateway/src/graphql/schema.graphql index c254f048bd2..ab6ae7b7952 100644 --- a/packages/gateway/src/graphql/schema.graphql +++ b/packages/gateway/src/graphql/schema.graphql @@ -216,7 +216,8 @@ type Mutation { comment: String duplicateTrackingId: String ): ID! - confirmRegistration(id: ID!): ID! + confirmRegistration(id: ID!, details: ConfirmRegistrationInput!): ID! + rejectRegistration(id: ID!, details: RejectRegistrationInput!): ID! createOrUpdateUser(user: UserInput!): User! activateUser( userId: String! @@ -697,6 +698,17 @@ type Reinstated { registrationStatus: RegStatus } +input ConfirmRegistrationInput { + registrationNumber: String! + error: String + identifiers: [IdentifierInput!] +} + +input RejectRegistrationInput { + reason: String! + comment: String +} + input UserInput { id: ID name: [HumanNameInput!]! @@ -1266,6 +1278,11 @@ enum RegStatus { ISSUED } +input IdentifierInput { + type: String! + value: String! +} + input HumanNameInput { use: String firstNames: String diff --git a/packages/gateway/src/workflow/index.ts b/packages/gateway/src/workflow/index.ts index f56faf6e0ea..fe44e60e818 100644 --- a/packages/gateway/src/workflow/index.ts +++ b/packages/gateway/src/workflow/index.ts @@ -36,6 +36,11 @@ import { GQLPaymentInput } from '@gateway/graphql/schema' +export type IdentifierInput = { + type: string + value: string +} + const createRequest = async ( method: 'POST' | 'GET' | 'PUT' | 'DELETE', path: string, @@ -235,6 +240,47 @@ export async function markNotADuplicate(id: string, authHeader: IAuthHeader) { return getComposition(response) } +export async function confirmRegistration( + id: string, + authHeader: IAuthHeader, + details: { + error: string | undefined + registrationNumber: string + identifiers?: IdentifierInput[] + } +) { + const res: ReadyForReviewRecord = await createRequest( + 'POST', + `/records/${id}/confirm`, + authHeader, + details + ) + + const taskEntry = res.entry.find((e) => e.resource.resourceType === 'Task') + if (!taskEntry) { + throw new Error('No task entry found in the confirmation response') + } + + return taskEntry +} + +export async function rejectRegistration( + recordId: string, + authHeader: IAuthHeader, + details: { comment: string; reason: string } +) { + const res: RejectedRecord = await createRequest( + 'POST', + `/records/${recordId}/reject`, + authHeader, + details + ) + + const taskEntry = res.entry.find((e) => e.resource.resourceType === 'Task')! + + return taskEntry +} + export async function archiveRegistration( id: string, authHeader: IAuthHeader, diff --git a/packages/workflow/src/config/routes.ts b/packages/workflow/src/config/routes.ts index f018f25b396..f7a649ded9e 100644 --- a/packages/workflow/src/config/routes.ts +++ b/packages/workflow/src/config/routes.ts @@ -61,7 +61,7 @@ export const getRoutes = () => { }, { method: 'POST', - path: '/confirm/registration', + path: '/records/{id}/confirm', handler: markEventAsRegisteredCallbackHandler, config: { tags: ['api'], diff --git a/packages/workflow/src/features/registration/handler.ts b/packages/workflow/src/features/registration/handler.ts index 7c51a68e2b0..c9e9ec555b5 100644 --- a/packages/workflow/src/features/registration/handler.ts +++ b/packages/workflow/src/features/registration/handler.ts @@ -26,8 +26,7 @@ export interface EventRegistrationPayload { trackingId: string registrationNumber: string error: string - compositionId: string - childIdentifiers?: { + identifiers?: { type: SupportedPatientIdentifierCode value: string }[] @@ -38,7 +37,8 @@ export async function markEventAsRegisteredCallbackHandler( h: Hapi.ResponseToolkit ) { const token = getToken(request) - const { registrationNumber, error, childIdentifiers, compositionId } = + const compositionId = request.params.id + const { registrationNumber, error, identifiers } = request.payload as EventRegistrationPayload if (error) { @@ -60,7 +60,7 @@ export async function markEventAsRegisteredCallbackHandler( savedRecord, registrationNumber, token, - childIdentifiers + identifiers ) const event = getEventType(bundle) diff --git a/packages/workflow/src/records/handler/register.test.ts b/packages/workflow/src/records/handler/register.test.ts index de4dd9a7116..84a4948e2ad 100644 --- a/packages/workflow/src/records/handler/register.test.ts +++ b/packages/workflow/src/records/handler/register.test.ts @@ -159,10 +159,9 @@ describe('Register record endpoint', () => { const response = await server.server.inject({ method: 'POST', - url: '/confirm/registration', + url: '/records/7c3af302-08c9-41af-8701-92de9a71a3e4/confirm', payload: { - registrationNumber: '1234', - compositionId: '7c3af302-08c9-41af-8701-92de9a71a3e4' + registrationNumber: '1234' }, headers: { Authorization: `Bearer ${token}` diff --git a/packages/workflow/src/records/state-transitions.ts b/packages/workflow/src/records/state-transitions.ts index 9775d82c72e..588ab595a5b 100644 --- a/packages/workflow/src/records/state-transitions.ts +++ b/packages/workflow/src/records/state-transitions.ts @@ -474,7 +474,7 @@ export async function toRegistered( record: WaitingForValidationRecord, registrationNumber: EventRegistrationPayload['registrationNumber'], token: string, - childIdentifiers?: EventRegistrationPayload['childIdentifiers'] + identifiers?: EventRegistrationPayload['identifiers'] ): Promise { const previousTask = getTaskFromSavedBundle(record) const registeredTaskWithoutPractitionerExtensions = @@ -507,10 +507,10 @@ export async function toRegistered( value: registrationNumber as RegistrationNumber }) - if (event === EVENT_TYPE.BIRTH && childIdentifiers) { + if (event === EVENT_TYPE.BIRTH && identifiers) { // For birth event patients[0] is child and it should // already be initialized with the RN identifier - childIdentifiers.forEach((childIdentifier) => { + identifiers.forEach((childIdentifier) => { const previousIdentifier = patientsWithRegNumber[0].identifier!.find( ({ type }) => type?.coding?.[0].code === childIdentifier.type ) diff --git a/sequence-diagrams/backend/create-birth-registration-registrar.md b/sequence-diagrams/backend/create-birth-registration-registrar.md index f2644994c5b..e0e6e3ad21f 100644 --- a/sequence-diagrams/backend/create-birth-registration-registrar.md +++ b/sequence-diagrams/backend/create-birth-registration-registrar.md @@ -68,7 +68,7 @@ sequenceDiagram Workflow--)Metrics: POST bundle to /events/{event}/waiting-external-validation Workflow--)Country-Config: POST record to /event-registration - Country-Config->>Workflow: POST /confirm/registration + Country-Config->>Workflow: POST /records/{id}/confirm Workflow->>Search: Get record by id Workflow->>User management: Fetch user/system information