diff --git a/.github/DISCUSSION_TEMPLATE/Installation.yml b/.github/DISCUSSION_TEMPLATE/Installation.yml new file mode 100644 index 0000000000..c746353674 --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/Installation.yml @@ -0,0 +1,57 @@ +title: 'Installation' +body: + - type: markdown + attributes: + value: | + ## Explain the issue & help you require + + ## What git branch are you on in Core? + + ## What git branch are you on in Countryconfig? + + If we are unable to help you here we can offer dedicated support. + Email us at team@opencrvs.org and please answer the following questions: + + Please tell us your full name and how you wish to be referred to. + Please tell us the name of the organisation or company you work for. + Are you working with a government on an active country implementation? If so, ... + Which country’s implementation are you working on? + Tell us about your project, brief, the client and timeline if you can? + - type: input + id: core-version + attributes: + label: OpenCRVS Core version + description: e.g 1.3.2 + validations: + required: true + - type: input + id: countryconfig-version + attributes: + label: OpenCRVS Countryconfig version + description: e.g 1.3.2 + validations: + required: true + - type: input + id: os + attributes: + label: Your operating system and version + description: e.g Ubuntu 22.04 + validations: + required: true + - type: input + id: node + attributes: + label: Your node version + description: e.g v16.20.0 + validations: + required: true + - type: checkboxes + attributes: + label: Which of these describes your project best? + options: + - label: Testing OpenCRVS for business development, educational purposes or for personal interest. + - label: A research project to evaluate, configure and test OpenCRVS as a possible solution for a government, an active brief or proposal. + - label: Actively installing and configuring OpenCRVS Core for production use in a country. + - label: Customising OpenCRVS Core code in production in a country + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/---bug.md b/.github/ISSUE_TEMPLATE/---bug.md index d4a8584dd9..32d38e3840 100644 --- a/.github/ISSUE_TEMPLATE/---bug.md +++ b/.github/ISSUE_TEMPLATE/---bug.md @@ -1,8 +1,8 @@ --- -name: "\U0001F479 Bug" +name: 'Bug' about: Used to submit bugs and things that appear broken. title: '' -labels: "\U0001F479Bug" +labels: 'Bug' assignees: '' --- diff --git a/.github/ISSUE_TEMPLATE/---feature.md b/.github/ISSUE_TEMPLATE/---feature.md index 667f5142c2..5457c3afa7 100644 --- a/.github/ISSUE_TEMPLATE/---feature.md +++ b/.github/ISSUE_TEMPLATE/---feature.md @@ -1,10 +1,9 @@ --- -name: "\U0001F680 Feature" +name: 'Feature' about: Used to suggest something that you wish OpenCRVS could do title: '' -labels: "☕️ Discussion" +labels: 'Feature' assignees: '' - --- ## 🚀 Feature diff --git a/.github/ISSUE_TEMPLATE/epic.md b/.github/ISSUE_TEMPLATE/epic.md deleted file mode 100644 index 24fc4f2ec3..0000000000 --- a/.github/ISSUE_TEMPLATE/epic.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -name: Epic -about: 'For describing a big chunk of work with one common objective. ' -title: '' -labels: "⭐️ Epic" -assignees: '' - ---- - -### **Problem statement** - -(Describe the problem you're trying to solve by doing this work) - -### **Proposed work** - -(High-level overview of what we're building and why we think it will solve the problem). - -### **Success criteria** - -(The criteria that must be met in order to consider this piece of work a success) - -### **User stories** - -(How the product should work for various user types) - -- - -### **Scope** - -(Current requirements) - -### **Future work** - -(Future requirements) diff --git a/.github/ISSUE_TEMPLATE/user-story.md b/.github/ISSUE_TEMPLATE/user-story.md deleted file mode 100644 index 4a10187f6d..0000000000 --- a/.github/ISSUE_TEMPLATE/user-story.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -name: User story -about: For describing a small piece of functionality so users can accomplish a specific goal -title: '' -labels: '' -assignees: '' ---- - -### Description - -... - -### Acceptance criteria - -GIVEN -WHEN -THEN - -### Design - -(Link to Figma) - -### Dev tasks - -- [ ] Task 1 diff --git a/docker-compose.yml b/docker-compose.yml index 681e161027..95433f0940 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -71,7 +71,7 @@ services: - NODE_ENV=development - NOTIFICATION_SERVICE_URL=http://notification:2020/ - USER_MANAGEMENT_URL=http://user-mgnt:3030/ - - RESOURCE_SERVICE_URL=http://countryconfig:3040/ + - COUNTRY_CONFIG_URL=http://countryconfig:3040 - HEARTH_URL=http://hearth:3447/fhir - OPENHIM_URL=http://openhim-core:5001 - APPLICATION_CONFIG_URL=http://config:2021/ diff --git a/license-config.json b/license-config.json index 9118e846c7..78564d0514 100644 --- a/license-config.json +++ b/license-config.json @@ -2,6 +2,7 @@ "ignore": [ "**/*.@(editorconfig|md|log|lock|patch|prettierrc|gitignore|eslintignore|stylelintrc|csv|gz|geojson|woff2|woff|xml|yarnrc|yarn-integrity|ttf|map|pdf|snap|dockerignore|jsonc|idea|env|info|key|pub|cjs|sql)", ".git", + ".github/DISCUSSION_TEMPLATE/Installation.yml", ".idea", "**/patches", ".secrets", diff --git a/package.json b/package.json index 17d06775b9..dfe3052537 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "description": "OpenCRVS core workspace", "license": "MPL-2.0", - "version": "1.3.1", + "version": "1.3.2", "private": true, "workspaces": [ "packages/*" diff --git a/packages/auth/package.json b/packages/auth/package.json index e43ec90e54..3352a5c704 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/auth", - "version": "1.3.1", + "version": "1.3.2", "description": "OpenCRVS authentication service", "license": "MPL-2.0", "private": true, diff --git a/packages/auth/src/features/authenticate/service.ts b/packages/auth/src/features/authenticate/service.ts index ada8f17bfd..e20fb42f28 100644 --- a/packages/auth/src/features/authenticate/service.ts +++ b/packages/auth/src/features/authenticate/service.ts @@ -172,7 +172,7 @@ export async function generateAndSendVerificationCode( mobile?: string, email?: string ) { - const isDemoUser = scope.indexOf('demo') > -1 + const isDemoUser = scope.indexOf('demo') > -1 || QA_ENV logger.info( `isDemoUser, ${JSON.stringify({ diff --git a/packages/client/package.json b/packages/client/package.json index 53a884925d..a46972f4fe 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/client", - "version": "1.3.1", + "version": "1.3.2", "description": "OpenCRVS client application", "license": "MPL-2.0", "private": true, diff --git a/packages/client/src/components/form/ReviewActionComponent.tsx b/packages/client/src/components/form/ReviewActionComponent.tsx index f952e6584e..3eb0236941 100644 --- a/packages/client/src/components/form/ReviewActionComponent.tsx +++ b/packages/client/src/components/form/ReviewActionComponent.tsx @@ -25,6 +25,7 @@ interface IReviewActionProps extends React.HTMLAttributes { id?: string draftDeclaration?: boolean completeDeclaration: boolean + hasErrorsOnFields?: boolean totalFileSizeExceeded: boolean declarationToBeValidated?: boolean declarationToBeRegistered?: boolean @@ -103,7 +104,12 @@ enum ACTION { DECLARATION_TO_BE_REGISTERED = 'DECLARATION_TO_BE_REGISTERED' } -const ACTION_TO_CONTENT_MAP: { [key: string]: any } = { +const ACTION_TO_CONTENT_MAP_SKELETON: ( + deliveryMethod: string, + hasErrorsOnFields: boolean +) => { + [key: string]: any +} = (deliveryMethod, hasErrorsOnFields) => ({ [String(ACTION.DECLARATION_TO_BE_DECLARED)]: { draftStatus: { true: { @@ -137,7 +143,9 @@ const ACTION_TO_CONTENT_MAP: { [key: string]: any } = { payload: { completeDeclaration: false } }, description: { - message: messages.reviewActionDescriptionIncomplete, + message: !hasErrorsOnFields + ? messages.reviewActionDescriptionIncomplete + : messages.reviewActionDescriptionForErrors, payload: { deliveryMethod: window.config.INFORMANT_NOTIFICATION_DELIVERY_METHOD @@ -283,7 +291,7 @@ const ACTION_TO_CONTENT_MAP: { [key: string]: any } = { } } } -} +}) interface IReviewActionState { showSubmitModal: boolean @@ -310,8 +318,13 @@ class ReviewActionComponent extends React.Component< submitDeclarationAction, draftDeclaration, rejectDeclarationAction, - intl + intl, + hasErrorsOnFields } = this.props + const ACTION_TO_CONTENT_MAP = ACTION_TO_CONTENT_MAP_SKELETON( + window.config.INFORMANT_NOTIFICATION_DELIVERY_METHOD, + !!hasErrorsOnFields + ) const background = !completeDeclaration ? 'error' @@ -377,7 +390,7 @@ class ReviewActionComponent extends React.Component< size="large" id="submit_form" onClick={this.toggleSubmitModalOpen} - disabled={totalFileSizeExceeded} + disabled={hasErrorsOnFields || totalFileSizeExceeded} > {intl.formatMessage( diff --git a/packages/client/src/forms/validation.ts b/packages/client/src/forms/validation.ts index d22075c1c8..ea9d06ba2f 100644 --- a/packages/client/src/forms/validation.ts +++ b/packages/client/src/forms/validation.ts @@ -40,7 +40,8 @@ const getValidationErrors = { values: IFormSectionData, offlineCountryConfig?: IOfflineData, drafts?: IFormData, - requiredErrorMessage?: MessageDescriptor + requiredErrorMessage?: MessageDescriptor, + checkValidationErrorsOnly?: boolean ) { const value = field.nestedFields && values[field.name] @@ -66,7 +67,7 @@ const getValidationErrors = { validators.push(...getFieldValidation(field as IDynamicFormField, values)) - if (field.required) { + if (field.required && !checkValidationErrorsOnly) { validators.push(required(requiredErrorMessage)) } else if (field.validateEmpty) { } else if (!value && value !== 0) { @@ -127,7 +128,8 @@ export function getValidationErrorsForForm( values: IFormSectionData, resource?: IOfflineData, drafts?: IFormData, - requiredErrorMessage?: MessageDescriptor + requiredErrorMessage?: MessageDescriptor, + checkValidationErrorsOnly?: boolean ) { return fields.reduce( (errorsForAllFields: Errors, field) => @@ -141,7 +143,8 @@ export function getValidationErrorsForForm( values, resource, drafts, - requiredErrorMessage + requiredErrorMessage, + checkValidationErrorsOnly ) }, {} diff --git a/packages/client/src/i18n/messages/views/review.ts b/packages/client/src/i18n/messages/views/review.ts index b2a74c4e3b..3dfeec732b 100644 --- a/packages/client/src/i18n/messages/views/review.ts +++ b/packages/client/src/i18n/messages/views/review.ts @@ -191,11 +191,18 @@ const messagesToDefine = { }, reviewActionDescriptionIncomplete: { defaultMessage: - 'By sending this incomplete declaration, there will be a digital record made.\n\nTell the informant that they will receive an {deliveryMethod} with a tracking ID. They will need this to complete the declaration at a registration office within 30 days. The informant will need to provide all mandatory information before the {eventType, select, birth {birth declaration} death {death declaration}} can be registered.', + 'The informant will receive an {deliveryMethod} with a tracking ID that they can use to provide the additional mandatory information required for registration.', description: 'Description for review action component when incomplete declaration', id: 'review.actions.description.confirmInComplete' }, + reviewActionDescriptionForErrors: { + defaultMessage: + 'Please ensure all fields are either empty or have a valid value in them.', + description: + 'Description for review action component when declaration has errors on fields', + id: 'review.actions.description.hasError' + }, reviewActionTitle: { defaultMessage: 'Declaration {completeDeclaration, select, true {complete} false {incomplete}}', diff --git a/packages/client/src/i18n/reducer.ts b/packages/client/src/i18n/reducer.ts index b7d3640ad2..8eae65c301 100644 --- a/packages/client/src/i18n/reducer.ts +++ b/packages/client/src/i18n/reducer.ts @@ -146,6 +146,9 @@ const getNextMessages = ( language: string, languages: ILanguageState ): IntlMessages => { + if (!languages[language]) { + return languages[getDefaultLanguage()].messages + } return languages[language].messages } diff --git a/packages/client/src/tests/util.tsx b/packages/client/src/tests/util.tsx index 535a25da42..70e7afb839 100644 --- a/packages/client/src/tests/util.tsx +++ b/packages/client/src/tests/util.tsx @@ -810,7 +810,7 @@ export const mockDeathDeclarationData = { registrationEmail: 'sesrthsthsr@sdfsgt.com', informantIdType: 'NATIONAL_ID', iDType: 'NATIONAL_ID', - informantID: '1230000000000', + informantID: '123456789', informantFirstNames: '', informantFamilyName: 'ইসলাম', informantFirstNamesEng: 'Islam', diff --git a/packages/client/src/utils/constants.ts b/packages/client/src/utils/constants.ts index 6c0c70f6e9..c50a36540f 100644 --- a/packages/client/src/utils/constants.ts +++ b/packages/client/src/utils/constants.ts @@ -84,5 +84,5 @@ export const DESKTOP_TIME_OUT_MILLISECONDS = 900000 export const INFORMANT_MINIMUM_AGE = 16 /** Current application version used in the left navigation. It's saved to localStorage to determine if a user logged into a newer version of the app for the first time */ -export const APPLICATION_VERSION = 'v1.3.1' +export const APPLICATION_VERSION = 'v1.3.2' export const IS_PROD_ENVIRONMENT = import.meta.env.PROD diff --git a/packages/client/src/utils/persistence/persistenceMiddleware.ts b/packages/client/src/utils/persistence/persistenceMiddleware.ts index b878fae86f..6662275915 100644 --- a/packages/client/src/utils/persistence/persistenceMiddleware.ts +++ b/packages/client/src/utils/persistence/persistenceMiddleware.ts @@ -24,10 +24,9 @@ import { NATL_ADMIN_ROLES } from '@client/utils/constants' import { client } from '@client/utils/apolloClient' -import { READY } from '@client/offline/actions' import startOfMonth from 'date-fns/startOfMonth' import subMonths from 'date-fns/subMonths' -import { QueryOptions } from '@apollo/client' +import { QueryOptions } from '@apollo/client/core' const isUserOfNationalScope = (userDetails: UserDetails) => [...NATIONAL_REGISTRAR_ROLES, ...NATL_ADMIN_ROLES].includes( @@ -109,20 +108,5 @@ export const persistenceMiddleware: Middleware<{}, IStoreState> = client.query(query) } } - } else if (action.type === READY) { - const { locations } = getState().offline.offlineData - const userDetails = getState().profile.userDetails - if (!isFieldAgent(userDetails!)) { - const stateIds = Object.values(locations!) - .filter((location) => location.partOf === 'Location/0') - .map((location) => location.id) - - for (const stateId of stateIds) { - const queriesToPrefetch = getQueriesToPrefetch(stateId, false) - for (const query of queriesToPrefetch) { - client.query(query) - } - } - } } } diff --git a/packages/client/src/views/PrintCertificate/VerifyCollector.tsx b/packages/client/src/views/PrintCertificate/VerifyCollector.tsx index 9485d6d586..892cb9f496 100644 --- a/packages/client/src/views/PrintCertificate/VerifyCollector.tsx +++ b/packages/client/src/views/PrintCertificate/VerifyCollector.tsx @@ -55,7 +55,7 @@ interface IMatchParams { interface IStateProps { registerForm: IForm - declaration: IPrintableDeclaration + declaration?: IPrintableDeclaration offlineCountryConfiguration: IOfflineData } interface IDispatchProps { @@ -75,13 +75,14 @@ type IFullProps = IStateProps & IDispatchProps & IOwnProps class VerifyCollectorComponent extends React.Component { handleVerification = (hasShowedVerifiedDocument: boolean) => { const isIssueUrl = window.location.href.includes('issue') - const event = this.props.declaration.event - const eventDate = getEventDate(this.props.declaration.data, event) - const registeredDate = getRegisteredDate(this.props.declaration.data) + + const event = this.props.declaration!.event + const eventDate = getEventDate(this.props.declaration!.data, event) + const registeredDate = getRegisteredDate(this.props.declaration!.data) const { offlineCountryConfiguration } = this.props - const declaration = { ...this.props.declaration } - if (declaration.data.registration.certificates.length) { + const declaration = { ...this.props.declaration! } + if (declaration?.data?.registration.certificates.length) { declaration.data.registration.certificates[0].hasShowedVerifiedDocument = hasShowedVerifiedDocument } @@ -125,18 +126,19 @@ class VerifyCollectorComponent extends React.Component { getGenericCollectorInfo = (collector: string): ICollectorInfo => { const { intl, declaration, registerForm } = this.props - const info = declaration.data[collector] + + const info = declaration!.data[collector] const eventRegistrationInput = draftToGqlTransformer( registerForm, - declaration.data + declaration!.data ) const informantType = eventRegistrationInput.registration.informantType.toLowerCase() const fields = verifyIDOnDeclarationCertificateCollectorDefinition[ - declaration.event + declaration!.event ][collector] as IVerifyIDCertificateCollectorField const iD = @@ -252,6 +254,21 @@ const mapStateToProps = ( const declaration = state.declarationsState.declarations.find( (draft) => draft.id === registrationId ) as IPrintableDeclaration + + /** + * ISSUE : The user clicks on the Back button after unasigning the declaration (in the case of printing) + * SOLUTION : This condition enables the redirection to be activated when the declaration is not present in the State. + */ + if (!declaration) { + return { + registerForm: { + sections: [] + }, + declaration: undefined, + offlineCountryConfiguration: getOfflineData(state) + } + } + const registerForm = getEventRegisterForm(state, declaration.event) return { diff --git a/packages/client/src/views/PrintCertificate/collectorForm/CollectorForm.tsx b/packages/client/src/views/PrintCertificate/collectorForm/CollectorForm.tsx index e9464143fb..8dff82481b 100644 --- a/packages/client/src/views/PrintCertificate/collectorForm/CollectorForm.tsx +++ b/packages/client/src/views/PrintCertificate/collectorForm/CollectorForm.tsx @@ -19,8 +19,7 @@ import { storeDeclaration, writeDeclaration, IPrintableDeclaration, - ICertificate, - IDeclaration + ICertificate } from '@client/declarations' import { FormFieldGenerator } from '@client/components/form' import { @@ -82,16 +81,22 @@ const ErrorWrapper = styled.div` margin-bottom: 16px; ` -interface IBaseProps { +type PropsWhenDeclarationIsFound = { registerForm: IForm event: Event pageRoute: string declarationId: string - declaration: IPrintableDeclaration | undefined + declaration: IPrintableDeclaration formSection: IFormSection formGroup: IFormSectionGroup offlineCountryConfiguration: IOfflineData - theme: ITheme +} +type PropsWhenDeclarationIsNotFound = { + declaration: undefined +} + +interface IBaseProps { + theme?: ITheme goBack: typeof goBack goToHomeTab: typeof goToHomeTab storeDeclaration: typeof storeDeclaration @@ -103,7 +108,9 @@ interface IBaseProps { goToPrintCertificatePayment: typeof goToPrintCertificatePayment } -type IProps = IBaseProps & IntlShapeProps +type IProps = + | (IBaseProps & PropsWhenDeclarationIsFound & IntlShapeProps) + | (IBaseProps & PropsWhenDeclarationIsNotFound & IntlShapeProps) function getNextSectionIds( formSection: IFormSection, @@ -297,12 +304,14 @@ class CollectorFormComponent extends React.Component { declaration: IPrintableDeclaration, event: Event ) => { + const { offlineCountryConfiguration } = this + .props as PropsWhenDeclarationIsFound if ( isFreeOfCost( event, getEventDate(declaration.data, event), getRegisteredDate(declaration.data), - this.props.offlineCountryConfiguration + offlineCountryConfiguration ) ) { this.props.goToReviewCertificate(declarationId, event) @@ -324,23 +333,10 @@ class CollectorFormComponent extends React.Component { } render() { - const { - intl, - event, - declarationId, - declaration, - formSection, - formGroup, - goBack - } = this.props - const { showError, showModalForNoSignedAffidavit } = this.state + const props = this.props + const { declaration } = props - const nextSectionGroup = getNextSectionIds( - formSection, - formGroup, - declaration - ) const declarationToBeCertified = declaration if (!declarationToBeCertified) { return ( @@ -352,6 +348,14 @@ class CollectorFormComponent extends React.Component { /> ) } + + const { intl, event, declarationId, formSection, formGroup, goBack } = props + + const nextSectionGroup = getNextSectionIds( + formSection, + formGroup, + declaration + ) return ( <> -) => { +): PropsWhenDeclarationIsFound | PropsWhenDeclarationIsNotFound => { const { registrationId, eventType, groupId } = props.match.params const event = getEvent(eventType) @@ -468,12 +472,14 @@ const mapStateToProps = ( (declaration) => declaration.id === registrationId ) as IPrintableDeclaration | undefined + if (!declaration) { + return { declaration: undefined } + } + const userDetails = getUserDetails(state) const userOfficeId = userDetails?.primaryOffice?.id const registeringOfficeId = getRegisteringOfficeId(declaration) - const certFormSection = getCertificateCollectorFormSection( - declaration as IDeclaration - ) + const certFormSection = getCertificateCollectorFormSection(declaration) const isAllowPrintInAdvance = event === Event.Birth @@ -541,4 +547,4 @@ export const CollectorForm = connect(mapStateToProps, { goToVerifyCollector, goToReviewCertificate, goToPrintCertificatePayment -})(injectIntl(withTheme(CollectorFormComponent))) +})(injectIntl<'intl', IProps>(withTheme(CollectorFormComponent))) diff --git a/packages/client/src/views/RegisterForm/review/ReviewSection.tsx b/packages/client/src/views/RegisterForm/review/ReviewSection.tsx index 34a8ff0a93..677a88e589 100644 --- a/packages/client/src/views/RegisterForm/review/ReviewSection.tsx +++ b/packages/client/src/views/RegisterForm/review/ReviewSection.tsx @@ -579,7 +579,8 @@ const renderValue = ( export const getErrorsOnFieldsBySection = ( formSections: IFormSection[], offlineCountryConfig: IOfflineData, - draft: IDeclaration + draft: IDeclaration, + checkValidationErrorsOnly?: boolean ): IErrorsBySection => { return formSections.reduce((sections, section: IFormSection) => { const fields: IFormField[] = getSectionFields( @@ -592,7 +593,9 @@ export const getErrorsOnFieldsBySection = ( fields, draft.data[section.id] || {}, offlineCountryConfig, - draft.data + draft.data, + undefined, + checkValidationErrorsOnly ) return { @@ -1653,6 +1656,12 @@ class ReviewSectionComp extends React.Component { offlineCountryConfiguration, declaration ) + const badInputErrors = getErrorsOnFieldsBySection( + formSections, + offlineCountryConfiguration, + declaration, + true + ) const isSignatureMissing = () => { if (isCorrection(declaration)) { @@ -1678,6 +1687,10 @@ class ReviewSectionComp extends React.Component { flatten(Object.values(errorsOnFields).map(Object.values)).filter( (errors) => errors.errors.length > 0 ).length === 0 && !isSignatureMissing() + const hasValidationErrors = + flatten(Object.values(badInputErrors).map(Object.values)).filter( + (errors) => errors.errors.length > 0 + ).length > 0 const textAreaProps = { id: 'additional_comments', @@ -1977,6 +1990,7 @@ class ReviewSectionComp extends React.Component { declaration={declaration} submitDeclarationAction={submitClickEvent} rejectDeclarationAction={rejectDeclarationClickEvent} + hasErrorsOnFields={hasValidationErrors} /> ) : ( diff --git a/packages/commons/package.json b/packages/commons/package.json index 7d959e898e..39133f0c0b 100644 --- a/packages/commons/package.json +++ b/packages/commons/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/commons", - "version": "1.3.1", + "version": "1.3.2", "description": "OpenCRVS common modules and utils", "license": "MPL-2.0", "main": "./build/dist/index.js", diff --git a/packages/components/package.json b/packages/components/package.json index 1573beab5e..2e5e4679f5 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,7 +1,7 @@ { "name": "@opencrvs/components", "main": "lib/index", - "version": "1.3.1", + "version": "1.3.2", "description": "OpenCRVS UI Component library", "license": "MPL-2.0", "private": true, diff --git a/packages/config/package.json b/packages/config/package.json index 6b21ca28cf..b0e6817e24 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/config", - "version": "1.3.1", + "version": "1.3.2", "description": "OpenCRVS public configuration microservice", "license": "MPL-2.0", "scripts": { diff --git a/packages/dashboards/package.json b/packages/dashboards/package.json index 20f4566758..c10eb8cf23 100644 --- a/packages/dashboards/package.json +++ b/packages/dashboards/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/dashboards", - "version": "1.3.1", + "version": "1.3.2", "description": "OpenCRVS performance dashboards", "main": "index.js", "scripts": { diff --git a/packages/data-seeder/package.json b/packages/data-seeder/package.json index ea93ae7e65..06a1bc8403 100644 --- a/packages/data-seeder/package.json +++ b/packages/data-seeder/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/data-seeder", - "version": "1.3.1", + "version": "1.3.2", "description": "OpenCRVS data-seeder microservice", "homepage": "https://github.com/opencrvs/opencrvs-core#readme", "license": "MPL-2.0", diff --git a/packages/documents/package.json b/packages/documents/package.json index 113426380c..1295ef136f 100644 --- a/packages/documents/package.json +++ b/packages/documents/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/documents", - "version": "1.3.1", + "version": "1.3.2", "description": "OpenCRVS Documents service", "license": "MPL-2.0", "private": true, diff --git a/packages/gateway/package.json b/packages/gateway/package.json index 06a73aabd8..b9e87b7fd5 100644 --- a/packages/gateway/package.json +++ b/packages/gateway/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/gateway", - "version": "1.3.1", + "version": "1.3.2", "description": "OpenCRVS API Gateway with GraphQL", "license": "MPL-2.0", "scripts": { diff --git a/packages/gateway/src/features/registration/fhir-builders.ts b/packages/gateway/src/features/registration/fhir-builders.ts index 11c666abbb..1da79be2ea 100644 --- a/packages/gateway/src/features/registration/fhir-builders.ts +++ b/packages/gateway/src/features/registration/fhir-builders.ts @@ -104,6 +104,7 @@ import { GQLDeathRegistrationInput, GQLMarriageRegistrationInput } from '@gateway/graphql/schema' +import { subYears, format } from 'date-fns' export enum SignatureExtensionPostfix { INFORMANT = 'informants-signature', @@ -752,9 +753,14 @@ function createAgeOfIndividualInYearsBuilder( }) } + const age = parseInt(fieldValue.toString(), 10) + if (resource.deceasedDateTime) { + const birthDate = subYears(new Date(resource.deceasedDateTime), age) + resource.birthDate = format(birthDate, 'yyyy-MM-dd') + return + } // for storing an assumed birthdate when exact DOB is not known - const birthYear = - new Date().getFullYear() - parseInt(fieldValue.toString(), 10) + const birthYear = new Date().getFullYear() - age const firstDayOfBirthYear = new Date(birthYear, 0, 1) resource.birthDate = `${firstDayOfBirthYear.getFullYear()}-${String( firstDayOfBirthYear.getMonth() + 1 @@ -1909,6 +1915,24 @@ export const builders: IFieldBuilders = { DECEASED_TITLE, fhirBundle ) + /* + * setting birthDate from both here + * & age builder as it depends on which + * one gets called second + */ + const age = person.extension?.find( + ({ url }) => + url === + `${OPENCRVS_SPECIFICATION_URL}extension/age-of-individual-in-years` + )?.valueString + + if (age) { + const birthDate = subYears( + new Date(fieldValue as string), + parseInt(age, 10) + ) + person.birthDate = format(birthDate, 'yyyy-MM-dd') + } person.deceasedDateTime = fieldValue as string } }, diff --git a/packages/login/package.json b/packages/login/package.json index 14d02a2e85..87e3e716a6 100644 --- a/packages/login/package.json +++ b/packages/login/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/login", - "version": "1.3.1", + "version": "1.3.2", "description": "OpenCRVS login client application", "license": "MPL-2.0", "private": true, diff --git a/packages/login/src/i18n/reducer.ts b/packages/login/src/i18n/reducer.ts index 1a8bdea6a7..0bedc672e5 100644 --- a/packages/login/src/i18n/reducer.ts +++ b/packages/login/src/i18n/reducer.ts @@ -89,6 +89,9 @@ const getNextMessages = ( language: string, languages: ILanguageState ): IntlMessages => { + if (!languages[language]) { + return languages[getDefaultLanguage()].messages + } return languages[language].messages } diff --git a/packages/metrics/package.json b/packages/metrics/package.json index 58dfa7c9d1..0e675ec28e 100644 --- a/packages/metrics/package.json +++ b/packages/metrics/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/metrics", - "version": "1.3.1", + "version": "1.3.2", "description": "OpenCRVS metrics service", "license": "MPL-2.0", "private": true, diff --git a/packages/migration/package.json b/packages/migration/package.json index 70890e6442..1e763743b0 100644 --- a/packages/migration/package.json +++ b/packages/migration/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/migration", - "version": "1.3.1", + "version": "1.3.2", "description": "OpenCRVS migration microservice", "homepage": "https://github.com/opencrvs/opencrvs-core#readme", "type": "module", @@ -29,6 +29,7 @@ "@elastic/elasticsearch": "7.17.13", "bcryptjs": "^2.4.3", "file-type": "^16.5.3", + "date-fns": "^2.28.0", "influx": "^5.0.7", "is-svg": "^4.3.2", "lodash-es": "^4.17.21", diff --git a/packages/migration/src/migrations/hearth/20231123125224-deceased-birth-date.ts b/packages/migration/src/migrations/hearth/20231123125224-deceased-birth-date.ts new file mode 100644 index 0000000000..a714d8a5be --- /dev/null +++ b/packages/migration/src/migrations/hearth/20231123125224-deceased-birth-date.ts @@ -0,0 +1,95 @@ +/* + * 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 { subYears, format } from 'date-fns' +import { Db, MongoClient } from 'mongodb' +import { updateComposition } from '../../utils/elasticsearch-helper.js' +import { reportProgress } from '../../utils/progressTracker.js' + +export const up = async (db: Db, client: MongoClient) => { + const session = client.startSession() + try { + const ageExtension = + 'http://opencrvs.org/specs/extension/age-of-individual-in-years' + + const filter = { + deceasedDateTime: { $ne: null }, + 'extension.url': ageExtension + } + + await session.withTransaction(async () => { + const totalPatients = await db + .collection('Patient') + .countDocuments(filter) + const patientIterator = db + .collection('Patient') + .find(filter) + .project<{ + id: string + deceasedDateTime: string + extension: Array<{ url: string; valueString: string }> + }>({ + id: 1, + deceasedDateTime: 1, + extension: { + $filter: { + input: '$extension', + as: 'extension', + cond: { + $eq: ['$$extension.url', ageExtension] + } + } + } + }) + let processed = 0 + for await (const patient of patientIterator) { + const age = parseInt(patient.extension[0].valueString, 10) + const birthDate = subYears(new Date(patient.deceasedDateTime), age) + db.collection('Patient').updateOne( + { id: patient.id }, + { $set: { birthDate: format(birthDate, 'yyyy-MM-dd') } } + ) + processed += 1 + + reportProgress( + 'Deceased birth date migration', + processed, + totalPatients + ) + + const composition = await db + .collection('Composition') + .findOne<{ id: string }>( + { + 'section.entry.reference': `Patient/${patient.id}` + }, + { projection: { id: 1 } } + ) + if (!composition) { + console.log( + `Composition corresponding to the patient "${patient.id}" not found. Skipping search data migration` + ) + continue + } + await updateComposition(composition.id, { + deceasedDoB: format(birthDate, 'yyyy-MM-dd') + }) + } + }) + } finally { + await session.endSession() + } +} + +export const down = async (db: Db, client: MongoClient) => { + // Add migration logic for reverting changes made by the up() function + // This code will be executed when rolling back the migration + // It should reverse the changes made in the up() function +} diff --git a/packages/migration/src/utils/progressTracker.ts b/packages/migration/src/utils/progressTracker.ts new file mode 100644 index 0000000000..0722fac320 --- /dev/null +++ b/packages/migration/src/utils/progressTracker.ts @@ -0,0 +1,18 @@ +/* + * 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. + */ + +export function reportProgress(message: string, step: number, total: number) { + // log every 0.1% progress + const logEvery = Math.max(10, Math.round(total / 1000)) + if (step % logEvery === 0 || step === total) { + console.log(`${message}: ${((100 * step) / total).toFixed(2)}%`) + } +} diff --git a/packages/notification/package.json b/packages/notification/package.json index 5a566262cc..afe79fd85d 100644 --- a/packages/notification/package.json +++ b/packages/notification/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/notification", - "version": "1.3.1", + "version": "1.3.2", "description": "OpenCRVS notification service", "license": "MPL-2.0", "private": true, diff --git a/packages/search/package.json b/packages/search/package.json index 5dd7b67f0c..c538ef3024 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/search", - "version": "1.3.1", + "version": "1.3.2", "description": "OpenCRVS search service", "license": "MPL-2.0", "private": true, diff --git a/packages/user-mgnt/package.json b/packages/user-mgnt/package.json index 898aacae55..250bbef125 100644 --- a/packages/user-mgnt/package.json +++ b/packages/user-mgnt/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/user-mgnt", - "version": "1.3.1", + "version": "1.3.2", "description": "OpenCRVS user management service", "license": "MPL-2.0", "private": true, diff --git a/packages/webhooks/package.json b/packages/webhooks/package.json index db6aaa30ad..6d3bfe7dc7 100644 --- a/packages/webhooks/package.json +++ b/packages/webhooks/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/webhooks", - "version": "1.3.1", + "version": "1.3.2", "description": "OpenCRVS webhooks service", "license": "MPL-2.0", "private": true, diff --git a/packages/workflow/package.json b/packages/workflow/package.json index caf141ba45..48b0bcbf1f 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -1,6 +1,6 @@ { "name": "@opencrvs/workflow", - "version": "1.3.1", + "version": "1.3.2", "description": "OpenCRVS workflow service", "license": "MPL-2.0", "private": true, diff --git a/packages/workflow/src/constants.ts b/packages/workflow/src/constants.ts index 05eef70439..269625b64f 100644 --- a/packages/workflow/src/constants.ts +++ b/packages/workflow/src/constants.ts @@ -20,8 +20,8 @@ export const NOTIFICATION_SERVICE_URL = export const MOSIP_TOKEN_SEEDER_URL = process.env.MOSIP_TOKEN_SEEDER_URL || 'http://localhost:8085' -export const RESOURCE_SERVICE_URL = - process.env.RESOURCE_SERVICE_URL || `http://localhost:3040/` +export const COUNTRY_CONFIG_URL = + process.env.COUNTRY_CONFIG_URL || `http://localhost:3040/` export const CERT_PUBLIC_KEY_PATH = (process.env.CERT_PUBLIC_KEY_PATH as string) || '../../.secrets/public-key.pem' diff --git a/packages/workflow/src/features/registration/fhir/fhir-bundle-modifier.test.ts b/packages/workflow/src/features/registration/fhir/fhir-bundle-modifier.test.ts index d3e69e9bf6..8bd951cf58 100644 --- a/packages/workflow/src/features/registration/fhir/fhir-bundle-modifier.test.ts +++ b/packages/workflow/src/features/registration/fhir/fhir-bundle-modifier.test.ts @@ -46,8 +46,9 @@ const fetch = fetchAny as any describe('Verify fhir bundle modifier functions', () => { describe('setTrackingId', () => { - it('Successfully modified the provided fhirBundle with birth trackingid', () => { - const fhirBundle = setTrackingId(testFhirBundle) + it('Successfully modified the provided fhirBundle with birth trackingid', async () => { + fetch.mockResponses(['B123456']) + const fhirBundle = await setTrackingId(testFhirBundle, '1234') if ( fhirBundle && fhirBundle.entry && @@ -75,8 +76,9 @@ describe('Verify fhir bundle modifier functions', () => { } }) - it('Successfully modified the provided fhirBundle with death trackingid', () => { - const fhirBundle = setTrackingId(testDeathFhirBundle) + it('Successfully modified the provided fhirBundle with death trackingid', async () => { + fetch.mockResponses(['D123456']) + const fhirBundle = await setTrackingId(testDeathFhirBundle, '1234') if ( fhirBundle && fhirBundle.entry && @@ -103,8 +105,9 @@ describe('Verify fhir bundle modifier functions', () => { } }) - it('Successfully modified the provided fhirBundle with marriage trackingid', () => { - const fhirBundle = setTrackingId(testMarriageFhirBundle) + it('Successfully modified the provided fhirBundle with marriage trackingid', async () => { + fetch.mockResponses(['M123456']) + const fhirBundle = await setTrackingId(testMarriageFhirBundle, '1234') if ( fhirBundle && fhirBundle.entry && @@ -131,31 +134,35 @@ describe('Verify fhir bundle modifier functions', () => { } }) - it('Throws error if invalid fhir bundle is provided', () => { + it('Throws error if invalid fhir bundle is provided', async () => { const invalidData = { ...testFhirBundle, entry: [] } - expect(() => setTrackingId(invalidData)).toThrowError( + await expect(setTrackingId(invalidData, '1234')).rejects.toThrowError( 'Invalid FHIR bundle found' ) }) - it('Will push the composite resource identifier if it is missing on fhirDoc', () => { - const fhirBundle = setTrackingId({ - ...testFhirBundle, - entry: [ - { - resource: { - code: { - coding: [ - { - system: 'http://opencrvs.org/specs/types', - code: 'BIRTH' - } - ] + it('Will push the composite resource identifier if it is missing on fhirDoc', async () => { + fetch.mockResponses(['B123456']) + const fhirBundle = await setTrackingId( + { + ...testFhirBundle, + entry: [ + { + resource: { + code: { + coding: [ + { + system: 'http://opencrvs.org/specs/types', + code: 'BIRTH' + } + ] + } } } - } - ] - }) + ] + }, + '1234' + ) if ( fhirBundle && diff --git a/packages/workflow/src/features/registration/fhir/fhir-bundle-modifier.ts b/packages/workflow/src/features/registration/fhir/fhir-bundle-modifier.ts index 8b57b2f58d..8379e4c1ee 100644 --- a/packages/workflow/src/features/registration/fhir/fhir-bundle-modifier.ts +++ b/packages/workflow/src/features/registration/fhir/fhir-bundle-modifier.ts @@ -45,10 +45,7 @@ import { } from '@workflow/features/user/utils' import { logger } from '@workflow/logger' import * as Hapi from '@hapi/hapi' -import { - APPLICATION_CONFIG_URL, - RESOURCE_SERVICE_URL -} from '@workflow/constants' +import { APPLICATION_CONFIG_URL, COUNTRY_CONFIG_URL } from '@workflow/constants' import { getToken, getTokenPayload, @@ -76,7 +73,7 @@ export async function modifyRegistrationBundle( throw new Error('Invalid FHIR bundle found for declaration') } /* setting unique trackingid here */ - fhirBundle = setTrackingId(fhirBundle) + fhirBundle = await setTrackingId(fhirBundle, token) const taskResource = selectOrCreateTaskRefResource(fhirBundle) as fhir.Task const eventType = getEventType(fhirBundle) @@ -165,14 +162,17 @@ export async function invokeRegistrationValidation( token: string ): Promise<{ bundle: fhir.Bundle; regValidationError?: boolean }> { try { - const res = await fetch(`${RESOURCE_SERVICE_URL}event-registration`, { - method: 'POST', - body: JSON.stringify(bundle), - headers: { - 'Content-Type': 'application/json', - ...headers + const res = await fetch( + new URL('event-registration', COUNTRY_CONFIG_URL).toString(), + { + method: 'POST', + body: JSON.stringify(bundle), + headers: { + 'Content-Type': 'application/json', + ...headers + } } - }) + ) if (!res.ok) { const errorData = await res.json() throw `System error: ${res.statusText} ${res.status} ${errorData.msg}` @@ -388,9 +388,16 @@ export async function touchBundle( return bundle } -export function setTrackingId(fhirBundle: fhir.Bundle): fhir.Bundle { +export async function setTrackingId( + fhirBundle: fhir.Bundle, + token: string +): Promise { const eventType = getEventType(fhirBundle) - const trackingId = generateTrackingIdForEvents(eventType) + const trackingId = await generateTrackingIdForEvents( + eventType, + fhirBundle, + token + ) const trackingIdFhirName = `${eventType.toLowerCase()}-tracking-id` if ( diff --git a/packages/workflow/src/features/registration/fhir/fhir-utils.test.ts b/packages/workflow/src/features/registration/fhir/fhir-utils.test.ts index c338581968..2496d7da4f 100644 --- a/packages/workflow/src/features/registration/fhir/fhir-utils.test.ts +++ b/packages/workflow/src/features/registration/fhir/fhir-utils.test.ts @@ -128,8 +128,9 @@ describe('Verify getCRVSOfficeName', () => { }) describe('Verify getTrackingId', () => { - it('Returned tracking id properly for birth', () => { - const trackingid = getTrackingId(setTrackingId(testFhirBundle)) + it('Returned tracking id properly for birth', async () => { + fetch.mockResponseOnce(null, { status: 404 }) + const trackingid = getTrackingId(await setTrackingId(testFhirBundle, '123')) if (trackingid) { expect(trackingid).toMatch(/^B/) expect(trackingid.length).toBe(7) @@ -138,8 +139,11 @@ describe('Verify getTrackingId', () => { } }) - it('Returned tracking id properly for death', () => { - const trackingid = getTrackingId(setTrackingId(testDeathFhirBundle)) + it('Returned tracking id properly for death', async () => { + fetch.mockResponseOnce(null, { status: 404 }) + const trackingid = getTrackingId( + await setTrackingId(testDeathFhirBundle, '123') + ) if (trackingid) { expect(trackingid).toMatch(/^D/) expect(trackingid.length).toBe(7) @@ -148,8 +152,11 @@ describe('Verify getTrackingId', () => { } }) - it('Returned tracking id properly for marriage', () => { - const trackingid = getTrackingId(setTrackingId(testMarriageFhirBundle)) + it('Returned tracking id properly for marriage', async () => { + fetch.mockResponseOnce(null, { status: 404 }) + const trackingid = getTrackingId( + await setTrackingId(testMarriageFhirBundle, '123') + ) if (trackingid) { expect(trackingid).toMatch(/^M/) expect(trackingid.length).toBe(7) diff --git a/packages/workflow/src/features/registration/handler.test.ts b/packages/workflow/src/features/registration/handler.test.ts index e08c4b8eba..ba6d281811 100644 --- a/packages/workflow/src/features/registration/handler.test.ts +++ b/packages/workflow/src/features/registration/handler.test.ts @@ -145,6 +145,7 @@ describe('Verify handler', () => { describe('createRegistrationHandler', () => { beforeEach(() => { fetch.mockResponses( + [null, { status: 404 }], [userMock, { status: 200 }], [fieldAgentPractitionerMock, { status: 200 }], [fieldAgentPractitionerRoleMock, { status: 200 }], @@ -547,43 +548,6 @@ describe('Verify handler', () => { expect(res.statusCode).toBe(500) }) - it('generates a new tracking id and repeats the request if a 409 is received from hearth', async () => { - fetch.mockResponses( - ['', { status: 409 }], - ['', { status: 409 }], - [ - JSON.stringify({ - resourceType: 'Bundle', - entry: [ - { - response: { location: 'Patient/12423/_history/1' } - } - ] - }) - ] - ) - - const token = jwt.sign( - { scope: ['declare'] }, - readFileSync('../auth/test/cert.key'), - { - algorithm: 'RS256', - issuer: 'opencrvs:auth-service', - audience: 'opencrvs:workflow-user' - } - ) - - const res = await server.server.inject({ - method: 'POST', - url: '/fhir', - payload: testFhirBundle, - headers: { - Authorization: `Bearer ${token}` - } - }) - expect(res.statusCode).toBe(200) - }) - it('fails after trying to generate a new trackingID and sending to Hearth 5 times', async () => { fetch.mockResponses( ['', { status: 409 }], diff --git a/packages/workflow/src/features/registration/handler.ts b/packages/workflow/src/features/registration/handler.ts index 87074b40c3..6f3b87c291 100644 --- a/packages/workflow/src/features/registration/handler.ts +++ b/packages/workflow/src/features/registration/handler.ts @@ -14,7 +14,6 @@ import { markBundleAsValidated, markEventAsRegistered, modifyRegistrationBundle, - setTrackingId, markBundleAsWaitingValidation, updatePatientIdentifierWithRN, touchBundle, @@ -74,10 +73,7 @@ interface IEventRegistrationCallbackPayload { }[] } -async function sendBundleToHearth( - payload: fhir.Bundle, - count = 1 -): Promise { +async function sendBundleToHearth(payload: fhir.Bundle): Promise { const res = await fetch(HEARTH_URL, { method: 'POST', body: JSON.stringify(payload), @@ -86,11 +82,6 @@ async function sendBundleToHearth( } }) if (!res.ok) { - if (res.status === 409 && count < 5) { - setTrackingId(payload) - return await sendBundleToHearth(payload, count + 1) - } - throw new Error( `FHIR post to /fhir failed with [${res.status}] body: ${await res.text()}` ) diff --git a/packages/workflow/src/features/registration/utils.test.ts b/packages/workflow/src/features/registration/utils.test.ts index db1c01403b..3a92a71f71 100644 --- a/packages/workflow/src/features/registration/utils.test.ts +++ b/packages/workflow/src/features/registration/utils.test.ts @@ -37,14 +37,24 @@ describe('Verify utility functions', () => { }) it('Generates proper birth tracking id successfully', async () => { - const trackingId = generateTrackingIdForEvents(EVENT_TYPE.BIRTH) + fetch.mockResponseOnce(null, { status: 404 }) + const trackingId = await generateTrackingIdForEvents( + EVENT_TYPE.BIRTH, + {} as fhir.Bundle, + '123' + ) expect(trackingId).toBeDefined() expect(trackingId.length).toBe(7) expect(trackingId).toMatch(/^B/) }) it('Generates proper death tracking id successfully', async () => { - const trackingId = generateTrackingIdForEvents(EVENT_TYPE.DEATH) + fetch.mockResponseOnce(null, { status: 404 }) + const trackingId = await generateTrackingIdForEvents( + EVENT_TYPE.DEATH, + {} as fhir.Bundle, + '123' + ) expect(trackingId).toBeDefined() expect(trackingId.length).toBe(7) @@ -52,7 +62,12 @@ describe('Verify utility functions', () => { }) it('Generates proper marriage tracking id successfully', async () => { - const trackingId = generateTrackingIdForEvents(EVENT_TYPE.MARRIAGE) + fetch.mockResponseOnce(null, { status: 404 }) + const trackingId = await generateTrackingIdForEvents( + EVENT_TYPE.MARRIAGE, + {} as fhir.Bundle, + '123' + ) expect(trackingId).toBeDefined() expect(trackingId.length).toBe(7) @@ -67,7 +82,8 @@ describe('Verify utility functions', () => { }) it('send in-progress birth declaration notification successfully', async () => { - const fhirBundle = setTrackingId(testFhirBundle) + fetch.mockResponseOnce(null, { status: 404 }) + const fhirBundle = await setTrackingId(testFhirBundle, '123') fetch.mockResponse(officeMock) expect( sendEventNotification( @@ -81,7 +97,8 @@ describe('Verify utility functions', () => { ).resolves.not.toThrow() }) it('send Birth declaration notification successfully', async () => { - const fhirBundle = setTrackingId(testFhirBundle) + fetch.mockResponseOnce(null, { status: 404 }) + const fhirBundle = await setTrackingId(testFhirBundle, '123') fetch.mockResponse(officeMock) expect( sendEventNotification( @@ -115,7 +132,8 @@ describe('Verify utility functions', () => { ) }) it('send mark birth registration notification successfully', async () => { - const fhirBundle = setTrackingId(testFhirBundle) + fetch.mockResponseOnce(null, { status: 404 }) + const fhirBundle = await setTrackingId(testFhirBundle, '123') fetch.mockResponse(officeMock) //@ts-ignore fhirBundle.entry[1].resource.identifier.push({ @@ -152,7 +170,8 @@ describe('Verify utility functions', () => { ) }) it('send Birth rejection notification successfully', async () => { - const fhirBundle = setTrackingId(testFhirBundle) + fetch.mockResponseOnce(null, { status: 404 }) + const fhirBundle = await setTrackingId(testFhirBundle, '123') fetch.mockResponse(officeMock) expect( sendEventNotification( @@ -166,7 +185,8 @@ describe('Verify utility functions', () => { ).toBeDefined() }) it('send in-progress death declaration notification successfully', async () => { - const fhirBundle = setTrackingId(testFhirBundleWithIdsForDeath) + fetch.mockResponseOnce(null, { status: 404 }) + const fhirBundle = await setTrackingId(testFhirBundleWithIdsForDeath, '123') fetch.mockResponse(officeMock) expect( sendEventNotification( @@ -180,7 +200,8 @@ describe('Verify utility functions', () => { ).toBeDefined() }) it('send Death declaration notification successfully', async () => { - const fhirBundle = setTrackingId(testFhirBundleWithIdsForDeath) + fetch.mockResponseOnce(null, { status: 404 }) + const fhirBundle = await setTrackingId(testFhirBundleWithIdsForDeath, '123') fetch.mockResponse(officeMock) expect( sendEventNotification( @@ -212,7 +233,8 @@ describe('Verify utility functions', () => { ) }) it('send mark death registration notification successfully', async () => { - const fhirBundle = setTrackingId(testFhirBundleWithIdsForDeath) + fetch.mockResponseOnce(null, { status: 404 }) + const fhirBundle = await setTrackingId(testFhirBundleWithIdsForDeath, '123') //@ts-ignore fhirBundle.entry[1].resource.identifier.push({ system: 'http://opencrvs.org/specs/id/death-registration-number', @@ -250,7 +272,8 @@ describe('Verify utility functions', () => { ) }) it('send Death rejection notification successfully', async () => { - const fhirBundle = setTrackingId(testFhirBundleWithIdsForDeath) + fetch.mockResponseOnce(null, { status: 404 }) + const fhirBundle = await setTrackingId(testFhirBundleWithIdsForDeath, '123') fetch.mockResponses([officeMock, { status: 200 }]) fetch.mockResponses([deathTaskMock, { status: 200 }]) expect( @@ -265,7 +288,8 @@ describe('Verify utility functions', () => { ).toBeDefined() }) it('send Death declaration notification successfully', async () => { - const fhirBundle = setTrackingId(testFhirBundleWithIdsForDeath) + fetch.mockResponseOnce(null, { status: 404 }) + const fhirBundle = await setTrackingId(testFhirBundleWithIdsForDeath, '123') fetch.mockResponses([officeMock, { status: 200 }]) fetch.mockResponses([deathTaskMock, { status: 200 }]) fetch.mockResponses([deathTaskMock, { status: 200 }]) @@ -283,6 +307,9 @@ describe('Verify utility functions', () => { }) describe('getMosipUINToken functions', () => { + beforeAll(() => { + fetch.mockClear() + }) it('Calls mosip token seeder function and returns success', async () => { fetch.mockResponse(mosipSuccessMock) const mosipResponse = await getMosipUINToken(mosipDeceasedPatientMock) diff --git a/packages/workflow/src/features/registration/utils.ts b/packages/workflow/src/features/registration/utils.ts index 41bc7514a4..03431d74f6 100644 --- a/packages/workflow/src/features/registration/utils.ts +++ b/packages/workflow/src/features/registration/utils.ts @@ -12,7 +12,8 @@ import * as ShortUIDGen from 'short-uid' import { NOTIFICATION_SERVICE_URL, MOSIP_TOKEN_SEEDER_URL, - HEARTH_URL + HEARTH_URL, + COUNTRY_CONFIG_URL } from '@workflow/constants' import fetch from 'node-fetch' import { logger } from '@workflow/logger' @@ -64,10 +65,40 @@ export enum FHIR_RESOURCE_TYPE { PATIENT = 'Patient' } -export function generateTrackingIdForEvents(eventType: EVENT_TYPE): string { - // using first letter of eventType for prefix - // TODO: for divorce, need to think about prefix as Death & Divorce prefix is same 'D' - return generateTrackingId(eventType.charAt(0)) +export async function generateTrackingIdForEvents( + eventType: EVENT_TYPE, + bundle: fhir.Bundle, + token: string +): Promise { + const trackingIdFromCountryConfig = await getTrackingIdFromCountryConfig( + bundle, + token + ) + if (trackingIdFromCountryConfig) { + return trackingIdFromCountryConfig + } else { + // using first letter of eventType for prefix + // TODO: for divorce, need to think about prefix as Death & Divorce prefix is same 'D' + return generateTrackingId(eventType.charAt(0)) + } +} + +export async function getTrackingIdFromCountryConfig( + bundle: fhir.Bundle, + token: string +): Promise { + return fetch(new URL('/tracking-id', COUNTRY_CONFIG_URL).toString(), { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-type': 'application/json' + }, + body: JSON.stringify(bundle) + }).then((res) => { + if (res.ok) return res.text() + else if (res.status === 404) return null + else throw new Error(res.statusText) + }) } function generateTrackingId(prefix: string): string {