diff --git a/app/package-lock.json b/app/package-lock.json index 34fa13af3..b5f90e719 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -40,6 +40,7 @@ "@babel/core": "7.21.4", "@babel/preset-env": "7.21.4", "@babel/preset-typescript": "7.21.4", + "@faker-js/faker": "^8.0.2", "@graphql-codegen/cli": "3.2.2", "@graphql-codegen/typed-document-node": "3.0.2", "@graphql-codegen/typescript": "3.0.2", @@ -58,6 +59,7 @@ "@tailwindcss/typography": "^0.5.0", "@testing-library/jest-dom": "^5.16.1", "@testing-library/svelte": "^3.0.3", + "@testing-library/user-event": "^14.4.3", "@types/cookie": "^0.5.0", "@types/jsonwebtoken": "9.0.1", "@types/nodemailer": "^6.4.4", @@ -2707,6 +2709,22 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@faker-js/faker": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.0.2.tgz", + "integrity": "sha512-Uo3pGspElQW91PCvKSIAXoEgAUlRnH29sX2/p89kg7sP1m2PzCufHINd0FhTXQf6DYGiUlVncdSPa2F9wxed2A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0", + "npm": ">=6.14.13" + } + }, "node_modules/@fal-works/esbuild-plugin-global-externals": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@fal-works/esbuild-plugin-global-externals/-/esbuild-plugin-global-externals-2.1.2.tgz", @@ -6736,6 +6754,19 @@ "svelte": "3.x" } }, + "node_modules/@testing-library/user-event": { + "version": "14.4.3", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.4.3.tgz", + "integrity": "sha512-kCUc5MEwaEMakkO5x7aoD+DLi02ehmEM2QCGWvNqAS1dV/fAvORWEjnjsEIvml59M7Y5kCkWN6fCCyPOe8OL6Q==", + "dev": true, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -26294,6 +26325,12 @@ "integrity": "sha512-x5vzdtOOGgFVDCUs81QRB2+liax8rFg3+7hqM+QhBG0/G3F1ZsoYl97UrqgHgQ9KKT7G6c4V+aTUCgu/n22v1A==", "dev": true }, + "@faker-js/faker": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.0.2.tgz", + "integrity": "sha512-Uo3pGspElQW91PCvKSIAXoEgAUlRnH29sX2/p89kg7sP1m2PzCufHINd0FhTXQf6DYGiUlVncdSPa2F9wxed2A==", + "dev": true + }, "@fal-works/esbuild-plugin-global-externals": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@fal-works/esbuild-plugin-global-externals/-/esbuild-plugin-global-externals-2.1.2.tgz", @@ -29292,6 +29329,13 @@ "@testing-library/dom": "^8.1.0" } }, + "@testing-library/user-event": { + "version": "14.4.3", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.4.3.tgz", + "integrity": "sha512-kCUc5MEwaEMakkO5x7aoD+DLi02ehmEM2QCGWvNqAS1dV/fAvORWEjnjsEIvml59M7Y5kCkWN6fCCyPOe8OL6Q==", + "dev": true, + "requires": {} + }, "@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", diff --git a/app/package.json b/app/package.json index 15cbe7857..948e4c311 100644 --- a/app/package.json +++ b/app/package.json @@ -52,6 +52,7 @@ "@babel/core": "7.21.4", "@babel/preset-env": "7.21.4", "@babel/preset-typescript": "7.21.4", + "@faker-js/faker": "^8.0.2", "@graphql-codegen/cli": "3.2.2", "@graphql-codegen/typed-document-node": "3.0.2", "@graphql-codegen/typescript": "3.0.2", @@ -70,6 +71,7 @@ "@tailwindcss/typography": "^0.5.0", "@testing-library/jest-dom": "^5.16.1", "@testing-library/svelte": "^3.0.3", + "@testing-library/user-event": "^14.4.3", "@types/cookie": "^0.5.0", "@types/jsonwebtoken": "9.0.1", "@types/nodemailer": "^6.4.4", diff --git a/app/src/lib/helpers.ts b/app/src/lib/helpers.ts index d9d36bdb2..eaffc7c24 100644 --- a/app/src/lib/helpers.ts +++ b/app/src/lib/helpers.ts @@ -22,6 +22,11 @@ export const pluck = function pluck( return result; }; +export function trimToNull(arg0: string): string { + const trimmed = arg0.trim(); + return trimmed === '' ? null : trimmed; +} + export function filterFalsyProps(obj: Record): Record { const result = {}; for (const [k, v] of Object.entries(obj)) { diff --git a/app/src/lib/ui/ProBeneficiaryUpdate/ProBeneficiaryUpdateFields.svelte b/app/src/lib/ui/ProBeneficiaryUpdate/ProBeneficiaryUpdateFields.svelte index 44d03fef8..0ebfef2df 100644 --- a/app/src/lib/ui/ProBeneficiaryUpdate/ProBeneficiaryUpdateFields.svelte +++ b/app/src/lib/ui/ProBeneficiaryUpdate/ProBeneficiaryUpdateFields.svelte @@ -2,6 +2,7 @@ export type Field = | 'firstname' | 'lastname' + | 'nir' | 'dateOfBirth' | 'rightRsa' | 'rightAre' @@ -16,14 +17,9 @@ import { Checkbox, Radio } from '../forms'; export let disabledFields: Field[]; - // We can by default edit any field - - function isFieldDisabled(fieldName: Field) { - return disabledFields.includes(fieldName); - } function titleForField(fieldName: Field) { - return isFieldDisabled(fieldName) + return disabledFields.includes(fieldName) ? 'Ce champ n‘est pas modifiable. Si toutefois vous devez y apporter une modification, merci de nous contacter par chat.' : ''; } @@ -34,7 +30,7 @@ placeholder="Jean-Baptiste" name="firstname" required - disabled={isFieldDisabled('firstname')} + disabled={disabledFields.includes('firstname')} title={titleForField('firstname')} /> @@ -43,7 +39,7 @@ placeholder="Poquelin" name="lastname" required - disabled={isFieldDisabled('lastname')} + disabled={disabledFields.includes('lastname')} title={titleForField('lastname')} /> @@ -55,10 +51,20 @@ inputHint="Format JJ/MM/AAAA" name="dateOfBirth" required - disabled={isFieldDisabled('dateOfBirth')} + disabled={disabledFields.includes('dateOfBirth')} title={titleForField('dateOfBirth')} /> + + @@ -78,19 +84,23 @@ legend="Revenu de solidarité active (RSA)" name="rightRsa" options={rsaRightKeys.options} - disabled={isFieldDisabled('rightRsa')} + disabled={disabledFields.includes('rightRsa')} />
Autres aides
- +
- +
- +
diff --git a/app/src/lib/ui/ProBeneficiaryUpdate/beneficiary.schema.ts b/app/src/lib/ui/ProBeneficiaryUpdate/beneficiary.schema.ts index a89858071..69f3c8255 100644 --- a/app/src/lib/ui/ProBeneficiaryUpdate/beneficiary.schema.ts +++ b/app/src/lib/ui/ProBeneficiaryUpdate/beneficiary.schema.ts @@ -3,6 +3,7 @@ import { nullifyEmptyString, validateCodePostal, validateDateInput, + validateNir, validatePhoneNumber, } from '$lib/validation'; import * as yup from 'yup'; @@ -17,6 +18,17 @@ const beneficiaryAccountSchemaObject = { .test('is-date-valid', 'Le format de la date est incorrect', validateDateInput) .required(), + nir: yup + .string() + .trim() + .test( + 'is-nir-valid', + 'Le NIR doit être composé de 13 chiffres', + (value) => !value || validateNir(value) + ) + .transform(nullifyEmptyString) + .nullable(), + mobileNumber: yup .string() .trim() @@ -58,6 +70,7 @@ export const beneficiaryAccountPartialSchema = yup.object().shape({ firstname: beneficiaryAccountSchemaObject.firstname.optional(), lastname: beneficiaryAccountSchemaObject.lastname.optional(), dateOfBirth: beneficiaryAccountSchemaObject.dateOfBirth.optional(), + nir: beneficiaryAccountSchemaObject.nir.optional(), }); export type BeneficiaryAccountInput = yup.InferType; diff --git a/app/src/lib/ui/ProNotebookPersonalInfo/ProNotebookPersonalInfoUpdate.svelte b/app/src/lib/ui/ProNotebookPersonalInfo/ProNotebookPersonalInfoUpdate.svelte index e24ecfc2d..5d05e1eb8 100644 --- a/app/src/lib/ui/ProNotebookPersonalInfo/ProNotebookPersonalInfoUpdate.svelte +++ b/app/src/lib/ui/ProNotebookPersonalInfo/ProNotebookPersonalInfoUpdate.svelte @@ -1,83 +1,28 @@ -
-

Informations personnelles

-
- - - -
- - -
- -
+ diff --git a/app/src/lib/ui/ProNotebookPersonalInfo/ProNotebookPersonalInfoUpdateView.svelte b/app/src/lib/ui/ProNotebookPersonalInfo/ProNotebookPersonalInfoUpdateView.svelte new file mode 100644 index 000000000..00ba1079a --- /dev/null +++ b/app/src/lib/ui/ProNotebookPersonalInfo/ProNotebookPersonalInfoUpdateView.svelte @@ -0,0 +1,106 @@ + + + + +
+

Informations personnelles

+
+ + + +
+ + +
+ +
diff --git a/app/src/lib/ui/ProNotebookPersonalInfo/ProNotebookPersonalInfoUpdateView.test.ts b/app/src/lib/ui/ProNotebookPersonalInfo/ProNotebookPersonalInfoUpdateView.test.ts new file mode 100644 index 000000000..742233d7d --- /dev/null +++ b/app/src/lib/ui/ProNotebookPersonalInfo/ProNotebookPersonalInfoUpdateView.test.ts @@ -0,0 +1,97 @@ +import '@testing-library/jest-dom'; + +import { RoleEnum } from '$lib/graphql/_gen/typed-document-nodes'; +import { faker } from '@faker-js/faker'; +import { fireEvent, render, screen, waitFor } from '@testing-library/svelte'; +import userEvent from '@testing-library/user-event'; +import { v4 as uuidv4 } from 'uuid'; +import { expect, vi } from 'vitest'; +import ProNotebookPersonalInfoUpdateView from './ProNotebookPersonalInfoUpdateView.svelte'; + +const user = userEvent.setup(); +describe('Mise à jour des données personelles', () => { + describe('Permissions', () => { + it('Un professionel ne peut pas éditer,prénom, nom, date de naissance et NIR', () => { + setup({ role: RoleEnum.Professional }); + + expect(screen.getByRole('textbox', { name: /Prénom/ })).toBeDisabled(); + expect(screen.getByRole('textbox', { name: /Nom/ })).toBeDisabled(); + expect(screen.getByLabelText(/Date de naissance/)).toBeDisabled(); + expect(screen.getByRole('textbox', { name: /NIR/ })).toBeDisabled(); + }); + }); + describe('NIR', () => { + describe(`Met à jour le NIR`, async () => { + it.each(['123452A890123', '1234567890123'])('%s', async (nir) => { + const { onSubmit } = setup(); + + await changeNir(nir); + await submit(); + + await waitFor(() => + expect(onSubmit.mock.lastCall[1]).toEqual(expect.objectContaining({ nir: nir })) + ); + }); + }); + + describe('Rejette les valeurs incorrectes', () => { + it.each([ + ['Je ne suis pas un NIR', 'alphanumérique'], + ['123456789012', 'pas assez de caractères'], + ['12345678901234', 'trop de caractères'], + ])('%s est invalide car : %s', async (nir, _) => { + setup(); + + await changeNir(nir); + await submit(); + + await waitFor(() => + expect(screen.getByText('Le NIR doit être composé de 13 chiffres')).toBeInTheDocument() + ); + }); + }); + it('transforme un NIR blanc en undefined', async () => { + const { onSubmit } = setup(); + + await changeNir(' '); + await submit(); + + await waitFor(() => + expect(onSubmit.mock.lastCall[1]).toEqual(expect.objectContaining({ nir: null })) + ); + }); + }); +}); + +const setup = (params: { role: RoleEnum } = { role: RoleEnum.Manager }) => { + const onSubmit = vi.fn(); + const onCancel = vi.fn(); + render(ProNotebookPersonalInfoUpdateView, { + beneficiary: { + id: uuidv4(), + firstname: faker.person.firstName(), + lastname: faker.person.lastName(), + dateOfBirth: faker.date.past().toISOString(), + nir: faker.number.int({ min: 1000000000000, max: 9999999999999 }) + '', + rightAre: false, + rightAss: false, + rightBonus: false, + }, + role: params.role, + onSubmit, + onCancel, + }); + // throw Error(printDateFr(faker.date.past())); + return { onSubmit, onCancel }; +}; +async function changeNir(nir: string) { + const nirField = screen.getByRole('textbox', { name: 'NIR' }); + await user.clear(nirField); + await user.type(nirField, nir); + expect(nirField).toHaveValue(nir); + return nir; +} + +async function submit() { + await fireEvent.click(screen.getByRole('button', { name: 'Enregistrer' })); +} diff --git a/app/src/lib/ui/ProNotebookPersonalInfo/ProNotebookPersonalInfoView.svelte b/app/src/lib/ui/ProNotebookPersonalInfo/ProNotebookPersonalInfoView.svelte index 37d0a107a..8eff0348c 100644 --- a/app/src/lib/ui/ProNotebookPersonalInfo/ProNotebookPersonalInfoView.svelte +++ b/app/src/lib/ui/ProNotebookPersonalInfo/ProNotebookPersonalInfoView.svelte @@ -24,6 +24,7 @@ | 'lastname' | 'mobileNumber' | 'email' + | 'nir' | 'dateOfBirth' | 'address1' | 'address2' @@ -81,6 +82,7 @@
Né le {formatDateLocale(beneficiary.dateOfBirth)}
+
NIR: {beneficiary.nir ?? 'Inconnu'}

Informations personnelles

diff --git a/app/src/lib/validation.ts b/app/src/lib/validation.ts index 3e300cb49..24a67f465 100644 --- a/app/src/lib/validation.ts +++ b/app/src/lib/validation.ts @@ -19,6 +19,10 @@ export function validatePhoneNumber(value: string): boolean { return /^(?:(?:\+|00)33|0)\s*[1-9](?:[\s.-]*\d{2}){4}$/.test(value.trim()); } +export function validateNir(value: string): boolean { + return /^[\d\w]{13}$/.test(value.trim()); +} + export function validateCodePostal(value: string): boolean { // le code postal est composé de 5 chiffres mais // ne peut pas commencer par 00 sinon il est pas valide diff --git a/app/src/routes/(auth)/beneficiaire/_getNotebookByBeneficiaryId.gql b/app/src/routes/(auth)/beneficiaire/_getNotebookByBeneficiaryId.gql index 232afe9d8..9c8f3c05b 100644 --- a/app/src/routes/(auth)/beneficiaire/_getNotebookByBeneficiaryId.gql +++ b/app/src/routes/(auth)/beneficiaire/_getNotebookByBeneficiaryId.gql @@ -73,6 +73,7 @@ fragment notebookFragment on notebook { cafNumber city dateOfBirth + nir email firstname id diff --git a/app/src/routes/(auth)/pro/carnet/_getNotebook.gql b/app/src/routes/(auth)/pro/carnet/_getNotebook.gql index 76757f7cf..ce25a6586 100644 --- a/app/src/routes/(auth)/pro/carnet/_getNotebook.gql +++ b/app/src/routes/(auth)/pro/carnet/_getNotebook.gql @@ -13,6 +13,7 @@ query GetNotebook($id: uuid!, $withOrientationRequests: Boolean = true) { city dateOfBirth email + nir firstname id lastname diff --git a/hasura/metadata/actions.graphql b/hasura/metadata/actions.graphql index 89e24f542..83af0323c 100644 --- a/hasura/metadata/actions.graphql +++ b/hasura/metadata/actions.graphql @@ -31,12 +31,6 @@ type Query { ): PoleEmploiDossierIndividu } -type Mutation { - update_notebook_from_pole_emploi( - notebookId: uuid! - ): UpdateNotebookFromPoleEmploiOutput -} - type Mutation { remove_notebook_focus( id: uuid! @@ -50,6 +44,12 @@ type Mutation { ): UpdateNotebookFocusLink } +type Mutation { + update_notebook_from_pole_emploi( + notebookId: uuid! + ): UpdateNotebookFromPoleEmploiOutput +} + type Mutation { update_notebook_target_status( status: String! diff --git a/hasura/metadata/actions.yaml b/hasura/metadata/actions.yaml index 01a852181..1aeaae5bc 100644 --- a/hasura/metadata/actions.yaml +++ b/hasura/metadata/actions.yaml @@ -60,22 +60,22 @@ actions: - role: orientation_manager - role: professional comment: Récupération du diagnostic individu pole-emploi - - name: update_notebook_from_pole_emploi + - name: remove_notebook_focus definition: kind: synchronous - handler: '{{BACKEND_API_ACTION_URL}}/v1/notebooks/update-notebook-from-pole-emploi' + handler: '{{BACKEND_API_ACTION_URL}}/v1/notebook_focus/delete' headers: - name: secret-token value_from_env: ACTION_SECRET permissions: - - role: admin_structure - role: professional - role: orientation_manager - - role: manager - - name: remove_notebook_focus + - role: admin_cdb + comment: Remove notebook focus + - name: update_notebook_focus_link definition: kind: synchronous - handler: '{{BACKEND_API_ACTION_URL}}/v1/notebook_focus/delete' + handler: '{{BACKEND_API_ACTION_URL}}/v1/notebook_focus/update' headers: - name: secret-token value_from_env: ACTION_SECRET @@ -83,19 +83,19 @@ actions: - role: professional - role: orientation_manager - role: admin_cdb - comment: Remove notebook focus - - name: update_notebook_focus_link + comment: Update a notebook focus link + - name: update_notebook_from_pole_emploi definition: kind: synchronous - handler: '{{BACKEND_API_ACTION_URL}}/v1/notebook_focus/update' + handler: '{{BACKEND_API_ACTION_URL}}/v1/notebooks/update-notebook-from-pole-emploi' headers: - name: secret-token value_from_env: ACTION_SECRET permissions: + - role: admin_structure - role: professional - role: orientation_manager - - role: admin_cdb - comment: Update a notebook focus link + - role: manager - name: update_notebook_target_status definition: kind: synchronous diff --git a/hasura/metadata/databases/carnet_de_bord/tables/public_beneficiary.yaml b/hasura/metadata/databases/carnet_de_bord/tables/public_beneficiary.yaml index 0a5bd0dd2..fdab4e883 100644 --- a/hasura/metadata/databases/carnet_de_bord/tables/public_beneficiary.yaml +++ b/hasura/metadata/databases/carnet_de_bord/tables/public_beneficiary.yaml @@ -130,11 +130,12 @@ insert_permissions: - date_of_birth - deployment_id - email + - external_id - firstname - id - - external_id - lastname - mobile_number + - nir - pe_number - place_of_birth - postal_code @@ -184,12 +185,13 @@ select_permissions: - date_of_birth - deployment_id - email + - external_id - firstname - id - - external_id - is_homeless - lastname - mobile_number + - nir - pe_number - pe_unique_import_id - place_of_birth @@ -221,6 +223,7 @@ select_permissions: - is_homeless - lastname - mobile_number + - nir - pe_number - place_of_birth - postal_code @@ -266,6 +269,7 @@ select_permissions: - is_homeless - lastname - mobile_number + - nir - pe_number - pe_unique_import_id - postal_code @@ -297,6 +301,7 @@ select_permissions: - is_homeless - lastname - mobile_number + - nir - pe_number - place_of_birth - postal_code @@ -329,6 +334,7 @@ select_permissions: - is_homeless - lastname - mobile_number + - nir - pe_number - place_of_birth - postal_code @@ -361,6 +367,7 @@ select_permissions: - is_homeless - lastname - mobile_number + - nir - pe_number - place_of_birth - postal_code @@ -392,6 +399,7 @@ update_permissions: - id - lastname - mobile_number + - nir - pe_number - place_of_birth - postal_code @@ -458,6 +466,7 @@ update_permissions: - is_homeless - lastname - mobile_number + - nir - pe_number - place_of_birth - postal_code