diff --git a/app/src/components/blocks/_admin/reviewDetailsAssessmentStage/ReviewDetailsAssessmentStage.test.tsx b/app/src/components/blocks/_admin/reviewDetailsAssessmentStage/ReviewDetailsAssessmentStage.test.tsx index 405a686023..ca605ad2ff 100644 --- a/app/src/components/blocks/_admin/reviewDetailsAssessmentStage/ReviewDetailsAssessmentStage.test.tsx +++ b/app/src/components/blocks/_admin/reviewDetailsAssessmentStage/ReviewDetailsAssessmentStage.test.tsx @@ -12,6 +12,7 @@ import { import { ReviewDetails } from '../../../../types/generic/reviews'; import * as getReviewsModule from '../../../../helpers/requests/getReviews'; import { DOCUMENT_TYPE } from '../../../../helpers/utils/documentType'; +import { buildPatientDetails } from '../../../../helpers/test/testBuilders'; const mockedUseNavigate = vi.fn(); const mockSetPatientDetails = vi.fn(); @@ -375,7 +376,7 @@ describe('ReviewDetailsAssessmentPage', () => { }); it('displays patient demographics', async () => { - mockUsePatientDetailsContext.mockReturnValue([null, mockSetPatientDetails]); + mockUsePatientDetailsContext.mockReturnValue([buildPatientDetails(), mockSetPatientDetails]); render( ({ @@ -134,7 +135,9 @@ describe('ReviewDetailsFileSelectStage', () => { expect(screen.queryByText('existing.pdf')).not.toBeInTheDocument(); }); - it('renders patient demograhics', () => { + it('renders patient demographics', () => { + mockUsePatientDetailsContext.mockReturnValue([buildPatientDetails(), mockSetPatientDetails]); + const documents: ReviewUploadDocument[] = [ makeReviewDoc('file1.pdf', DOCUMENT_UPLOAD_STATE.UNSELECTED, UploadDocumentType.REVIEW), makeReviewDoc( @@ -144,7 +147,7 @@ describe('ReviewDetailsFileSelectStage', () => { ), ]; - renderApp({ reviewData: mockReviewData, uploadDocuments: documents }); + renderApp({ reviewData: mockReviewData, uploadDocuments: documents, mockPatientContext: false }); expect(screen.getByTestId('patient-summary')).toBeInTheDocument(); expect(screen.getByTestId('patient-summary-full-name')).toBeInTheDocument(); diff --git a/app/src/components/blocks/_admin/reviewsDetailsStage/ReviewsDetailsStage.tsx b/app/src/components/blocks/_admin/reviewsDetailsStage/ReviewsDetailsStage.tsx index 2a04b7d1a4..78001edb14 100644 --- a/app/src/components/blocks/_admin/reviewsDetailsStage/ReviewsDetailsStage.tsx +++ b/app/src/components/blocks/_admin/reviewsDetailsStage/ReviewsDetailsStage.tsx @@ -302,7 +302,7 @@ const ReviewsDetailsStage = ({ )} } isFullScreen={session.isFullscreen || false} diff --git a/app/src/components/blocks/_admin/reviewsPage/ReviewsPage.test.tsx b/app/src/components/blocks/_admin/reviewsPage/ReviewsPage.test.tsx index 8b79f86385..bdccd3e552 100644 --- a/app/src/components/blocks/_admin/reviewsPage/ReviewsPage.test.tsx +++ b/app/src/components/blocks/_admin/reviewsPage/ReviewsPage.test.tsx @@ -209,21 +209,21 @@ describe('ReviewsPage', () => { if (snomed === '16521000000101') { return { content: { - reviewList: 'Lloyd George', + reviewDocumentTitle: 'Scanned paper notes', }, - displayName: 'Lloyd George Record', + displayName: 'scanned paper notes', }; } else if (snomed === '717391000000106') { return { content: { - reviewList: 'Electronic Health Record', + reviewDocumentTitle: 'Electronic health record', }, - displayName: 'Electronic Health Record', + displayName: 'electronic health record', }; } return { content: { - reviewList: 'Unknown Type', + reviewDocumentTitle: 'Unknown Type', }, displayName: 'Unknown Type', }; @@ -289,8 +289,8 @@ describe('ReviewsPage', () => { }); expect(screen.getByText('900 000 0002')).toBeInTheDocument(); - expect(screen.getByText('Lloyd George')).toBeInTheDocument(); - expect(screen.getByText('Electronic Health Record')).toBeInTheDocument(); + expect(screen.getByText('Scanned paper notes')).toBeInTheDocument(); + expect(screen.getByText('Electronic health record')).toBeInTheDocument(); expect(screen.getByText('Y12345')).toBeInTheDocument(); expect(screen.getByText('Y67890')).toBeInTheDocument(); }); @@ -334,10 +334,10 @@ describe('ReviewsPage', () => { renderComponent(); await waitFor(() => { - expect(screen.getByText('Lloyd George')).toBeInTheDocument(); + expect(screen.getByText('Scanned paper notes')).toBeInTheDocument(); }); - expect(screen.getByText('Electronic Health Record')).toBeInTheDocument(); + expect(screen.getByText('Electronic health record')).toBeInTheDocument(); }); it('displays "Unknown Type" for unrecognized SNOMED codes', async () => { diff --git a/app/src/components/blocks/_admin/reviewsPage/ReviewsPage.tsx b/app/src/components/blocks/_admin/reviewsPage/ReviewsPage.tsx index a037f99f9a..3d6f9bddbd 100644 --- a/app/src/components/blocks/_admin/reviewsPage/ReviewsPage.tsx +++ b/app/src/components/blocks/_admin/reviewsPage/ReviewsPage.tsx @@ -219,7 +219,7 @@ export const ReviewsPage = ({ setReviewData }: ReviewsPageProps): React.JSX.Elem let recordType: string = ''; try { recordType = getConfigForDocType(dto.documentSnomedCodeType).content - .reviewList as string; + .reviewDocumentTitle as string; } catch {} return { diff --git a/app/src/components/blocks/_delete/deleteSubmitStage/DeleteSubmitStage.test.tsx b/app/src/components/blocks/_delete/deleteSubmitStage/DeleteSubmitStage.test.tsx index 058d6174c9..0ca123f419 100644 --- a/app/src/components/blocks/_delete/deleteSubmitStage/DeleteSubmitStage.test.tsx +++ b/app/src/components/blocks/_delete/deleteSubmitStage/DeleteSubmitStage.test.tsx @@ -167,6 +167,26 @@ describe('DeleteSubmitStage', () => { }); }); + it('renders DeletionCompleteStage when the Yes is selected and Continue clicked, when user role is PCSE and feature flag is disabled', async () => { + mockedUseRole.mockReturnValue(REPOSITORY_ROLE.PCSE); + + mockedAxios.delete.mockReturnValue(Promise.resolve({ status: 200, data: 'Success' })); + + renderComponent(DOCUMENT_TYPE.LLOYD_GEORGE, history); + + expect(screen.getByRole('radio', { name: 'Yes' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Continue' })).toBeInTheDocument(); + + await userEvent.click(screen.getByRole('radio', { name: 'Yes' })); + await userEvent.click(screen.getByRole('button', { name: 'Continue' })); + + await waitFor(() => { + expect(mockedUseNavigate).toHaveBeenCalledWith( + routeChildren.DOCUMENT_DELETE_COMPLETE, + ); + }); + }); + it('renders DeletionCompleteStage when the Yes is selected and Continue clicked, when user role is GP_ADMIN and feature flag is enabled', async () => { mockedUseRole.mockReturnValue(REPOSITORY_ROLE.GP_ADMIN); mockuseConfig.mockReturnValue({ diff --git a/app/src/components/blocks/_delete/deleteSubmitStage/DeleteSubmitStage.tsx b/app/src/components/blocks/_delete/deleteSubmitStage/DeleteSubmitStage.tsx index 1b34f0b91d..d2c87fd7ec 100644 --- a/app/src/components/blocks/_delete/deleteSubmitStage/DeleteSubmitStage.tsx +++ b/app/src/components/blocks/_delete/deleteSubmitStage/DeleteSubmitStage.tsx @@ -84,7 +84,7 @@ export const DeleteSubmitStageIndexView = ({ resetDocState(); setDeletionStage(SUBMISSION_STATE.SUCCEEDED); navigate( - config.featureFlags.uploadDocumentIteration3Enabled + config.featureFlags.uploadDocumentIteration3Enabled || role === REPOSITORY_ROLE.PCSE ? routeChildren.DOCUMENT_DELETE_COMPLETE : routeChildren.LLOYD_GEORGE_DELETE_COMPLETE, ); diff --git a/app/src/components/blocks/_documentUpload/documentUploadCompleteStage/DocumentUploadCompleteStage.test.tsx b/app/src/components/blocks/_documentUpload/documentUploadCompleteStage/DocumentUploadCompleteStage.test.tsx index f0168385c7..708e94bc4e 100644 --- a/app/src/components/blocks/_documentUpload/documentUploadCompleteStage/DocumentUploadCompleteStage.test.tsx +++ b/app/src/components/blocks/_documentUpload/documentUploadCompleteStage/DocumentUploadCompleteStage.test.tsx @@ -71,6 +71,15 @@ describe('DocumentUploadCompleteStage', () => { const expectedDob = getFormattedDate(new Date(patientDetails.birthDate)); expect(screen.getByTestId('dob').textContent).toEqual('Date of birth: ' + expectedDob); + + expect( + screen.queryByText( + 'You are not the data controller', + { + exact: false, + } + ), + ).not.toBeInTheDocument(); }); it('should navigate to search when clicking the search link', async () => { @@ -153,6 +162,25 @@ describe('DocumentUploadCompleteStage', () => { expect(screen.getByText(documents[1].file.name)).toBeInTheDocument(); }); + it('should render non-data controller message when user is not data controller', async () => { + vi.mocked(usePatient).mockReturnValueOnce( + buildPatientDetails({ + canManageRecord: false, + }), + ); + + renderApp(documents); + + expect( + screen.getByText( + 'You are not the data controller', + { + exact: false, + } + ), + ).toBeInTheDocument(); + }); + const renderApp = (documents: UploadDocument[]) => { render( diff --git a/app/src/components/blocks/_documentUpload/documentUploadCompleteStage/DocumentUploadCompleteStage.tsx b/app/src/components/blocks/_documentUpload/documentUploadCompleteStage/DocumentUploadCompleteStage.tsx index ae8de73e2b..d44d78a0ef 100644 --- a/app/src/components/blocks/_documentUpload/documentUploadCompleteStage/DocumentUploadCompleteStage.tsx +++ b/app/src/components/blocks/_documentUpload/documentUploadCompleteStage/DocumentUploadCompleteStage.tsx @@ -126,6 +126,13 @@ const DocumentUploadCompleteStage = ({ documents, documentConfig }: Props): Reac

)} + {patientDetails.canManageRecord === false && ( +

+ You are not the data controller for this patient so you cannot view the files + you have uploaded in this service. +

+ )} +

If you think you've made a mistake, contact the Patient Record Management team at{' '} england.prmteam@nhs.net. diff --git a/app/src/components/blocks/_documentUpload/documentUploadConfirmStage/DocumentUploadConfirmStage.test.tsx b/app/src/components/blocks/_documentUpload/documentUploadConfirmStage/DocumentUploadConfirmStage.test.tsx index 1db4f6ed7a..cfe7e6e535 100644 --- a/app/src/components/blocks/_documentUpload/documentUploadConfirmStage/DocumentUploadConfirmStage.test.tsx +++ b/app/src/components/blocks/_documentUpload/documentUploadConfirmStage/DocumentUploadConfirmStage.test.tsx @@ -2,7 +2,7 @@ import { render, waitFor, screen, RenderResult } from '@testing-library/react'; import DocumentUploadConfirmStage from './DocumentUploadConfirmStage'; import { formatNhsNumber } from '../../../../helpers/utils/formatNhsNumber'; import { getFormattedDate } from '../../../../helpers/utils/formatDate'; -import { buildDocumentConfig, buildPatientDetails } from '../../../../helpers/test/testBuilders'; +import { buildDocument, buildDocumentConfig, buildPatientDetails } from '../../../../helpers/test/testBuilders'; import usePatient from '../../../../helpers/hooks/usePatient'; import { DOCUMENT_UPLOAD_STATE, @@ -11,7 +11,6 @@ import { import * as ReactRouter from 'react-router-dom'; import { MemoryHistory, createMemoryHistory } from 'history'; import userEvent from '@testing-library/user-event'; -import { routeChildren, routes } from '../../../../types/generic/routes'; import { getFormattedPatientFullName } from '../../../../helpers/utils/formatPatientFullName'; import { DOCUMENT_TYPE } from '../../../../helpers/utils/documentType'; import { getJourney } from '../../../../helpers/utils/urlManipulations'; @@ -34,22 +33,6 @@ vi.mock('react-router-dom', async () => { const mockedUseNavigate = vi.fn(); -vi.mock('./components/DocumentList', async () => { - const actual = await vi.importActual('./components/DocumentList'); - return { - ...actual, - default: ({ documents }: { documents: UploadDocument[] }): React.JSX.Element => { - return ( -

- Document List with - {documents.length} - documents -
- ); - }, - }; -}); - vi.mock('../documentUploadLloydGeorgePreview/DocumentUploadLloydGeorgePreview', () => ({ default: ({ documents, @@ -80,9 +63,10 @@ let history = createMemoryHistory({ initialIndex: 0, }); -let docConfig = buildDocumentConfig(); const mockConfirmFiles = vi.fn(); +let mockDocuments: UploadDocument[] = []; + describe('DocumentUploadConfirmStage', () => { beforeEach(() => { vi.mocked(usePatient).mockReturnValue(patientDetails); @@ -92,10 +76,11 @@ describe('DocumentUploadConfirmStage', () => { }); afterEach(() => { vi.clearAllMocks(); + mockDocuments = []; }); it('renders', async () => { - renderApp(history, 1); + renderApp(history); await waitFor(async () => { expect(screen.getByText('Check files are for the correct patient')).toBeInTheDocument(); @@ -103,7 +88,7 @@ describe('DocumentUploadConfirmStage', () => { }); it('should call confirmFiles when confirm button is clicked', async () => { - renderApp(history, 1); + renderApp(history); await userEvent.click(await screen.findByTestId('confirm-button')); @@ -113,17 +98,19 @@ describe('DocumentUploadConfirmStage', () => { }); it.each([ - { fileCount: 3, expectedPreviewCount: 3, stitched: true }, - { fileCount: 1, expectedPreviewCount: 1, stitched: false }, + { fileCount: 3, expectedPreviewCount: 3, docType: DOCUMENT_TYPE.LLOYD_GEORGE }, + { fileCount: 1, expectedPreviewCount: 1, docType: DOCUMENT_TYPE.EHR }, ])( 'should render correct number files in the preview %s', - async ({ fileCount, expectedPreviewCount, stitched }) => { - docConfig = buildDocumentConfig({ - snomedCode: DOCUMENT_TYPE.EHR, - stitched, - }); - - renderApp(history, fileCount); + async ({ fileCount, expectedPreviewCount, docType }) => { + for (let i = 1; i <= fileCount; i++) { + mockDocuments.push(buildDocument( + new File(['file'], `file 1.pdf`, { type: 'application/pdf' }), + DOCUMENT_UPLOAD_STATE.SELECTED, + docType, + )); + } + renderApp(history); await waitFor(async () => { expect(screen.getByTestId('lloyd-george-preview-count').textContent).toBe( @@ -133,9 +120,68 @@ describe('DocumentUploadConfirmStage', () => { }, ); + it('should hide preview when the previewed document is removed', async () => { + mockDocuments.push(buildDocument( + new File(['file'], `file 1.pdf`, { type: 'application/pdf' }), + DOCUMENT_UPLOAD_STATE.SELECTED, + DOCUMENT_TYPE.EHR_ATTACHMENTS, + )); + mockDocuments.push(buildDocument( + new File(['file'], `file 2.pdf`, { type: 'application/pdf' }), + DOCUMENT_UPLOAD_STATE.SELECTED, + DOCUMENT_TYPE.EHR_ATTACHMENTS, + )); + renderApp(history); + + const firstDocumentViewButton = await screen.findByTestId(`preview-${mockDocuments[0].id}-button`); + expect(firstDocumentViewButton).toBeInTheDocument(); + + await userEvent.click(firstDocumentViewButton); + + await waitFor(() => { + expect(screen.getByTestId('lloyd-george-preview')).toBeInTheDocument(); + }); + + const removeButton = screen.getByTestId(`remove-${mockDocuments[0].id}-button`); + expect(removeButton).toBeInTheDocument(); + + await userEvent.click(removeButton); + + await waitFor(() => { + expect(screen.queryByTestId('lloyd-george-preview')).not.toBeInTheDocument(); + }); + }); + + it('should show preview when 1 pdf remains after removing a document', async () => { + mockDocuments.push(buildDocument( + new File(['file'], `file 1.pdf`, { type: 'application/pdf' }), + DOCUMENT_UPLOAD_STATE.SELECTED, + DOCUMENT_TYPE.EHR_ATTACHMENTS, + )); + mockDocuments.push(buildDocument( + new File(['file'], `file 2.txt`, { type: 'text/plain' }), + DOCUMENT_UPLOAD_STATE.SELECTED, + DOCUMENT_TYPE.EHR_ATTACHMENTS, + )); + renderApp(history); + + await waitFor(() => { + expect(screen.queryByTestId('lloyd-george-preview')).not.toBeInTheDocument(); + }); + + const removeButton = screen.getByTestId(`remove-${mockDocuments[0].id}-button`); + expect(removeButton).toBeInTheDocument(); + + await userEvent.click(removeButton); + + await waitFor(() => { + expect(screen.queryByTestId('lloyd-george-preview')).not.toBeInTheDocument(); + }); + }); + describe('Navigation', () => { it('should navigate to previous screen when go back is clicked', async () => { - renderApp(history, 1); + renderApp(history); userEvent.click(await screen.findByTestId('go-back-link')); @@ -145,7 +191,7 @@ describe('DocumentUploadConfirmStage', () => { }); it('renders patient summary fields is inset', async () => { - renderApp(history, 1); + renderApp(history); const insetText = screen .getByText('Make sure that all files uploaded are for this patient only:') @@ -179,7 +225,7 @@ describe('DocumentUploadConfirmStage', () => { }); it('should still render all page elements correctly', async () => { - renderApp(history, 1); + renderApp(history); await waitFor(async () => { expect( @@ -191,22 +237,19 @@ describe('DocumentUploadConfirmStage', () => { }); }); - const renderApp = (history: MemoryHistory, docsLength: number): RenderResult => { - const documents: UploadDocument[] = []; - for (let i = 1; i <= docsLength; i++) { - documents.push({ - attempts: 0, - id: `${i}`, - docType: DOCUMENT_TYPE.LLOYD_GEORGE, - file: new File(['file'], `file ${i}.pdf`, { type: 'application/pdf' }), - state: DOCUMENT_UPLOAD_STATE.SELECTED, - }); + const renderApp = (history: MemoryHistory): RenderResult => { + if (mockDocuments.length === 0) { + mockDocuments.push(buildDocument( + new File(['file'], `file 1.pdf`, { type: 'application/pdf' }), + DOCUMENT_UPLOAD_STATE.SELECTED, + DOCUMENT_TYPE.LLOYD_GEORGE, + )); } return render( {}} /> diff --git a/app/src/components/blocks/_documentUpload/documentUploadConfirmStage/DocumentUploadConfirmStage.tsx b/app/src/components/blocks/_documentUpload/documentUploadConfirmStage/DocumentUploadConfirmStage.tsx index 0631223df8..afb8ae35ff 100644 --- a/app/src/components/blocks/_documentUpload/documentUploadConfirmStage/DocumentUploadConfirmStage.tsx +++ b/app/src/components/blocks/_documentUpload/documentUploadConfirmStage/DocumentUploadConfirmStage.tsx @@ -114,8 +114,10 @@ const DocumentUploadConfirmStage = ({ setCurrentPreviewDocument(document); // timeout to wait for first render before scrolling setTimeout(() => { - documentPreviewRef.current?.scrollIntoView({ behavior: 'smooth' }); - }, 0); + if (typeof documentPreviewRef?.current?.scrollIntoView === 'function') { + documentPreviewRef?.current?.scrollIntoView({ behavior: 'smooth' }); + } + }, 2); }; const removeDocument = (docToRemove: UploadDocument): void => { @@ -123,7 +125,10 @@ const DocumentUploadConfirmStage = ({ groupDocumentsByType(updatedDocs); - if (updatedDocs.length === 1) { + if (currentPreviewDocument?.id === docToRemove.id) { + setCurrentPreviewDocument(undefined); + } + else if (updatedDocs.length === 1 && updatedDocs[0].file.type === 'application/pdf') { setCurrentPreviewDocument(updatedDocs[0]); } @@ -156,7 +161,7 @@ const DocumentUploadConfirmStage = ({ const documentPreview = (): React.JSX.Element => { if (!currentPreviewDocument) { - return <>; + return
; } const config = getConfigForDocType(currentPreviewDocument.docType); @@ -170,8 +175,7 @@ const DocumentUploadConfirmStage = ({ const showCurrentlyViewingText = hasUnstitchedDocType && documents.length > 0 && - !!groupedDocuments && - Object.keys(groupedDocuments).length > 1; + !!groupedDocuments; return (
diff --git a/app/src/components/generic/patientSummary/PatientSummary.test.tsx b/app/src/components/generic/patientSummary/PatientSummary.test.tsx index 867998cc87..5692c53951 100644 --- a/app/src/components/generic/patientSummary/PatientSummary.test.tsx +++ b/app/src/components/generic/patientSummary/PatientSummary.test.tsx @@ -16,6 +16,12 @@ describe('PatientSummary', () => { vi.clearAllMocks(); }); + it('renders nothing when patient details are null', () => { + mockedUsePatient.mockReturnValue(null); + const { container } = render(); + expect(container).toBeEmptyDOMElement(); + }); + it('renders provided patient information', () => { const mockDetails = buildPatientDetails({ familyName: 'Jones', @@ -244,21 +250,6 @@ describe('PatientSummary', () => { expect(screen.getByText('First name')).toBeInTheDocument(); }); - it('handles null patient details gracefully', () => { - mockedUsePatient.mockReturnValue(null); - render( - - - - - - , - ); - - expect(screen.getByTestId('patient-summary')).toBeInTheDocument(); - // Components should render but with empty/default values - }); - it('handles empty NHS number', () => { const mockDetails = buildPatientDetails({ nhsNumber: '', diff --git a/app/src/components/generic/patientSummary/PatientSummary.tsx b/app/src/components/generic/patientSummary/PatientSummary.tsx index 82b41726a6..bbabdaf235 100644 --- a/app/src/components/generic/patientSummary/PatientSummary.tsx +++ b/app/src/components/generic/patientSummary/PatientSummary.tsx @@ -133,8 +133,13 @@ const PatientSummary = ({ if (reviewPatientDetails) { patientDetails = reviewPatientDetails; } + const patientDetailsContextValue = useMemo(() => ({ patientDetails }), [patientDetails]); + if (!patientDetails) { + return <>; + } + if (oneLine) { const nameLengthLimit = 30; const givenName = patientDetails?.givenName.join(' ') || ''; @@ -168,11 +173,11 @@ const PatientSummary = ({ data-testid="patient-summary-small-nhs-number" className="nhsuk-u-padding-right-9" > - NHS number: {formatNhsNumber(patientDetails!.nhsNumber)} + NHS number: {formatNhsNumber(patientDetails.nhsNumber)} Date of birth:{' '} - {getFormattedDate(new Date(patientDetails!.birthDate))} + {getFormattedDate(new Date(patientDetails.birthDate))}

diff --git a/app/src/config/documentTypesConfig.json b/app/src/config/documentTypesConfig.json index 6b27a05e78..4c1b639cae 100644 --- a/app/src/config/documentTypesConfig.json +++ b/app/src/config/documentTypesConfig.json @@ -28,5 +28,15 @@ "upload_title": "", "upload_description": "" } + }, + { + "name": "Letters and Documents", + "snomed_code": "162931000000103", + "config_name": "lettersAndDocumentsConfig", + "canUploadIndependently": true, + "content": { + "upload_title": "Other documents", + "upload_description": "Upload other letters and documents that have arrived for this patient after they have left your practice. For example, letters, test results and referrals." + } } ] \ No newline at end of file diff --git a/app/src/config/electronicHealthRecordAttachmentsConfig.json b/app/src/config/electronicHealthRecordAttachmentsConfig.json index bd038d2c63..368beeaa26 100644 --- a/app/src/config/electronicHealthRecordAttachmentsConfig.json +++ b/app/src/config/electronicHealthRecordAttachmentsConfig.json @@ -32,7 +32,7 @@ "beforeYouUploadTitle": "Before you upload", "previewUploadTitle": "Preview electronic health record attachment", "uploadFilesExtraParagraph": "", - "reviewList": "EHR Attachments", + "reviewDocumentTitle": "EHR Attachments", "skipDocumentLinkText": "Continue without uploading any EHR attachments" } } \ No newline at end of file diff --git a/app/src/config/electronicHealthRecordConfig.json b/app/src/config/electronicHealthRecordConfig.json index 8fd70b0703..483061a47d 100644 --- a/app/src/config/electronicHealthRecordConfig.json +++ b/app/src/config/electronicHealthRecordConfig.json @@ -35,7 +35,7 @@ "beforeYouUploadTitle": "Before you upload", "previewUploadTitle": "Preview this electronic health record", "uploadFilesExtraParagraph": "", - "reviewList": "Electronic Health Record", + "reviewDocumentTitle": "Electronic health record", "skipDocumentLinkText": "Continue without uploading EHR notes" } } \ No newline at end of file diff --git a/app/src/config/lettersAndDocumentsConfig.json b/app/src/config/lettersAndDocumentsConfig.json new file mode 100644 index 0000000000..0f00538476 --- /dev/null +++ b/app/src/config/lettersAndDocumentsConfig.json @@ -0,0 +1,36 @@ +{ + "snomedCode": "162931000000103", + "displayName": "other docs and letters", + "filenameOverride": "", + "canBeUpdated": false, + "associatedSnomed": "", + "multifileUpload": true, + "multifileZipped": false, + "multifileReview": false, + "canBeDiscarded": true, + "stitched": false, + "stitchedFilenamePrefix": "", + "singleDocumentOnly": false, + "acceptedFileTypes": [], + "content": { + "viewDocumentTitle": "Letters and documents", + "addFilesSelectTitle": "", + "uploadFilesSelectTitle": "Choose documents to upload", + "uploadFilesBulletPoints": [ + "There is no maximum number of size of files you can upload", + "You can upload files in any format except .zip and .exe files", + "If there is a problem with your files during upload, you'll need to resolve these before continuing" + ], + "chooseFilesMessage": "Choose files to upload", + "chooseFilesButtonLabel": "Choose files", + "chooseFilesWarningText": "", + "skipDocumentLinkText": "", + "confirmFilesTitle": "Check files are for the correct patient", + "confirmFilesTableTitle": "Files to upload", + "confirmFilesTableParagraph": "", + "beforeYouUploadTitle": "Before you upload", + "previewUploadTitle": "Preview your PDF files", + "uploadFilesExtraParagraph": "", + "reviewDocumentTitle": "Letters and documents" + } +} \ No newline at end of file diff --git a/app/src/config/lloydGeorgeConfig.json b/app/src/config/lloydGeorgeConfig.json index de6e325866..124e7f184f 100644 --- a/app/src/config/lloydGeorgeConfig.json +++ b/app/src/config/lloydGeorgeConfig.json @@ -31,8 +31,8 @@ "confirmFilesTableTitle": "Scanned paper notes to upload", "confirmFilesTableParagraph": "", "beforeYouUploadTitle": "Before you upload", - "previewUploadTitle": "Preview this scanned paper notes record", + "previewUploadTitle": "Preview these scanned paper notes", "uploadFilesExtraParagraph": "You can add a note to the patient's electronic health record to say their Lloyd George record is stored in this service. Use SNOMED code 16521000000101.", - "reviewList": "Scanned Paper Notes" + "reviewDocumentTitle": "Scanned paper notes" } } \ No newline at end of file diff --git a/app/src/config/rejectedFileTypes.json b/app/src/config/rejectedFileTypes.json index ba34252e99..5bd427ed80 100644 --- a/app/src/config/rejectedFileTypes.json +++ b/app/src/config/rejectedFileTypes.json @@ -1,7 +1,9 @@ [ + "7Z", "ACTION", "APK", "APP", + "B6Z", "BAT", "BIN", "CAB", @@ -10,18 +12,24 @@ "COMMAND", "CPL", "CSH", + "DLL", + "DMG", "EX_", "EXE", "GADGET", + "GZ", "INF1", "INS", "INX", "IPA", "ISU", + "ISO", + "JAR", "JOB", "JSE", "KSH", "LNK", + "LZ", "MSC", "MSI", "MSP", @@ -32,13 +40,20 @@ "PIF", "PRG", "PS1", + "RAR", "REG", "RGS", "RUN", + "S7Z", "SCR", "SCT", "SHB", "SHS", + "TAR", + "TBZ2", + "TGZ", + "TLZ", + "TX7", "U3P", "VB", "VBE", @@ -47,5 +62,11 @@ "WORKFLOW", "WS", "WSF", - "WSH" + "WSH", + "X7", + "Z", + "ZIP", + "ZIPX", + "ZST", + "ZZ" ] \ No newline at end of file diff --git a/app/src/helpers/utils/documentType.test.ts b/app/src/helpers/utils/documentType.test.ts index a325796207..50d1ca4c5f 100644 --- a/app/src/helpers/utils/documentType.test.ts +++ b/app/src/helpers/utils/documentType.test.ts @@ -50,6 +50,13 @@ describe('documentType', () => { expect(config.multifileUpload).toBe(true); }); + it('should return config for LETTERS_AND_DOCS', () => { + const config = getConfigForDocType(DOCUMENT_TYPE.LETTERS_AND_DOCS); + expect(config.snomedCode).toBe(DOCUMENT_TYPE.LETTERS_AND_DOCS); + expect(config.displayName).toBe('other docs and letters'); + expect(config.multifileUpload).toBe(true); + }); + it('should throw error for unknown document type', () => { expect(() => getConfigForDocType('unknown' as DOCUMENT_TYPE)).toThrow( 'No config found for document type: unknown', diff --git a/app/src/helpers/utils/documentType.ts b/app/src/helpers/utils/documentType.ts index 9b1b068e20..f34d075b8e 100644 --- a/app/src/helpers/utils/documentType.ts +++ b/app/src/helpers/utils/documentType.ts @@ -1,6 +1,7 @@ import lloydGeorgeConfig from '../../config/lloydGeorgeConfig.json'; import electronicHealthRecordConfig from '../../config/electronicHealthRecordConfig.json'; import electronicHealthRecordAttachmentsConfig from '../../config/electronicHealthRecordAttachmentsConfig.json'; +import lettersAndDocumentsConfig from '../../config/lettersAndDocumentsConfig.json'; export enum DOCUMENT_TYPE { LLOYD_GEORGE = '16521000000101', @@ -11,7 +12,7 @@ export enum DOCUMENT_TYPE { } export type ContentKey = - | 'reviewList' + | 'reviewDocumentTitle' | 'viewDocumentTitle' | 'addFilesSelectTitle' | 'uploadFilesSelectTitle' @@ -83,6 +84,8 @@ export const getConfigForDocType = (docType: DOCUMENT_TYPE): DOCUMENT_TYPE_CONFI return electronicHealthRecordConfig as DOCUMENT_TYPE_CONFIG; case DOCUMENT_TYPE.EHR_ATTACHMENTS: return electronicHealthRecordAttachmentsConfig as DOCUMENT_TYPE_CONFIG; + case DOCUMENT_TYPE.LETTERS_AND_DOCS: + return lettersAndDocumentsConfig as DOCUMENT_TYPE_CONFIG; default: throw new Error(`No config found for document type: ${docType}`); } diff --git a/app/src/pages/documentSearchResultsPage/DocumentSearchResultsPage.tsx b/app/src/pages/documentSearchResultsPage/DocumentSearchResultsPage.tsx index 648d5f990f..f1df6535ca 100644 --- a/app/src/pages/documentSearchResultsPage/DocumentSearchResultsPage.tsx +++ b/app/src/pages/documentSearchResultsPage/DocumentSearchResultsPage.tsx @@ -87,6 +87,16 @@ const DocumentSearchResultsPage = (): React.JSX.Element => { fileName: 'EHR Attachments.zip', contentType: 'application/zip', }), + buildSearchResult({ + documentSnomedCodeType: DOCUMENT_TYPE.LETTERS_AND_DOCS, + fileName: 'Later letter.pdf', + contentType: 'application/pdf', + }), + buildSearchResult({ + documentSnomedCodeType: DOCUMENT_TYPE.LETTERS_AND_DOCS, + fileName: 'Later letter 2.docx', + contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }), ]); setSubmissionState(SUBMISSION_STATE.SUCCEEDED); } else { diff --git a/lambdas/enums/metadata_field_names.py b/lambdas/enums/metadata_field_names.py index f11a8a9181..75439c3e86 100755 --- a/lambdas/enums/metadata_field_names.py +++ b/lambdas/enums/metadata_field_names.py @@ -19,6 +19,7 @@ class DocumentReferenceMetadataFields(Enum): DOC_STATUS = "DocStatus" CUSTODIAN = "Custodian" DOCUMENT_SCAN_CREATION = "DocumentScanCreation" + DOCUMENT_SNOMED_CODE_TYPE = "DocumentSnomedCodeType" @staticmethod def list() -> list[str]: diff --git a/lambdas/enums/snomed_codes.py b/lambdas/enums/snomed_codes.py index e40f2fdf4b..4af5726027 100644 --- a/lambdas/enums/snomed_codes.py +++ b/lambdas/enums/snomed_codes.py @@ -21,6 +21,9 @@ class SnomedCodes(Enum): EHR_ATTACHMENTS = SnomedCode( code="24511000000107", display_name="Electronic Health Record Attachments" ) + LETTERS_AND_DOCUMENTS = SnomedCode( + code="162931000000103", display_name="Care record elements" + ) # Temporary snomed code used. PATIENT_DATA = SnomedCode( code="717391000000106", display_name="Confidential patient data" diff --git a/lambdas/enums/supported_document_types.py b/lambdas/enums/supported_document_types.py index db12e1f411..1e05bcde07 100644 --- a/lambdas/enums/supported_document_types.py +++ b/lambdas/enums/supported_document_types.py @@ -12,6 +12,7 @@ class SupportedDocumentTypes(StrEnum): LG = SnomedCodes.LLOYD_GEORGE.value.code EHR = SnomedCodes.EHR.value.code EHR_ATTACHMENTS = SnomedCodes.EHR_ATTACHMENTS.value.code + LETTERS_AND_DOCUMENTS = SnomedCodes.LETTERS_AND_DOCUMENTS.value.code @staticmethod def list(): @@ -39,6 +40,9 @@ def get_dynamodb_table_name(self) -> str: SupportedDocumentTypes.EHR_ATTACHMENTS: os.getenv( "LLOYD_GEORGE_DYNAMODB_NAME" ), + SupportedDocumentTypes.LETTERS_AND_DOCUMENTS: os.getenv( + "LLOYD_GEORGE_BUCKET_NAME" + ), } return document_type_to_table_name[self] @@ -50,5 +54,8 @@ def get_s3_bucket_name(self) -> str: SupportedDocumentTypes.EHR_ATTACHMENTS: os.getenv( "LLOYD_GEORGE_BUCKET_NAME" ), + SupportedDocumentTypes.LETTERS_AND_DOCUMENTS: os.getenv( + "LLOYD_GEORGE_BUCKET_NAME" + ), } return lookup_dict[self] diff --git a/lambdas/enums/upload_forbidden_file_extensions.py b/lambdas/enums/upload_forbidden_file_extensions.py index 9684f89e8c..6b87ee7cdd 100644 --- a/lambdas/enums/upload_forbidden_file_extensions.py +++ b/lambdas/enums/upload_forbidden_file_extensions.py @@ -7,6 +7,7 @@ class ForbiddenFileType(StrEnum): ACTION = "ACTION" APK = "APK" APP = "APP" + B6Z = "B6Z" BAT = "BAT" BIN = "BIN" CAB = "CAB" @@ -15,18 +16,24 @@ class ForbiddenFileType(StrEnum): COMMAND = "COMMAND" CPL = "CPL" CSH = "CSH" - EX_ = "EX_" + DLL = "DLL" + DMG = "DMG" EXE = "EXE" + EX_ = "EX_" GADGET = "GADGET" + GZ = "GZ" INF1 = "INF1" INS = "INS" INX = "INX" IPA = "IPA" + ISO = "ISO" ISU = "ISU" + JAR = "JAR" JOB = "JOB" JSE = "JSE" KSH = "KSH" LNK = "LNK" + LZ = "LZ" MSC = "MSC" MSI = "MSI" MSP = "MSP" @@ -41,10 +48,16 @@ class ForbiddenFileType(StrEnum): REG = "REG" RGS = "RGS" RUN = "RUN" + S7Z = "S7Z" SCR = "SCR" SCT = "SCT" SHB = "SHB" SHS = "SHS" + TAR = "TAR" + TBZ2 = "TBZ2" + TGZ = "TGZ" + TLZ = "TLZ" + TX7 = "TX7" U3P = "U3P" VB = "VB" VBE = "VBE" @@ -54,6 +67,11 @@ class ForbiddenFileType(StrEnum): WS = "WS" WSF = "WSF" WSH = "WSH" + X7 = "X7" + Z = "Z" + ZIPX = "ZIPX" + ZST = "ZST" + ZZ = "ZZ" _7Z = "7Z" diff --git a/lambdas/models/upload_file_config.py b/lambdas/models/upload_file_config.py index 2abb362fd6..357cff4b57 100644 --- a/lambdas/models/upload_file_config.py +++ b/lambdas/models/upload_file_config.py @@ -19,6 +19,7 @@ class DocumentConfig(BaseModel): multifile_zipped: bool multifile_review: bool can_be_discarded: bool + single_file_only: bool stitched: bool accepted_file_types: List[str] content: List[Any] diff --git a/lambdas/services/create_document_reference_service.py b/lambdas/services/create_document_reference_service.py index 73cd7013ce..28d45813f0 100644 --- a/lambdas/services/create_document_reference_service.py +++ b/lambdas/services/create_document_reference_service.py @@ -15,8 +15,9 @@ ) from utils import upload_file_configs from utils.audit_logging_setup import LoggingService -from utils.common_query_filters import NotDeleted +from utils.common_query_filters import get_document_type_filter from utils.constants.ssm import UPLOAD_PILOT_ODS_ALLOWED_LIST +from utils.dynamo_query_filter_builder import DynamoQueryFilterBuilder from utils.exceptions import ( ConfigNotFoundException, InvalidNhsNumberException, @@ -60,14 +61,6 @@ def create_document_reference_request( url_responses = {} upload_request_documents = self.parse_documents_list(documents_list) - if any( - document.doc_type == SupportedDocumentTypes.LG - for document in upload_request_documents - ): - self.check_existing_lloyd_george_records_and_remove_failed_upload( - nhs_number - ) - try: user_ods_code = extract_ods_code_from_request_context() @@ -80,12 +73,21 @@ def create_document_reference_request( for validated_doc in upload_request_documents: snomed_code = validated_doc.doc_type + config = upload_file_configs.get_config_by_snomed_code( + snomed_code + ) + + if config.single_file_only: + self.check_existing_records_and_remove_failed_upload( + nhs_number, + snomed_code + ) document_reference = self.create_document_reference( nhs_number, user_ods_code, validated_doc, snomed_code ) - self.validate_document_file_type(validated_doc, snomed_code) + self.validate_document_file_type(validated_doc, config) upload_document_names.append(validated_doc.file_name) @@ -115,6 +117,7 @@ def create_document_reference_request( InvalidNhsNumberException, LGInvalidFilesException, PdsTooManyRequestsException, + ConfigNotFoundException, ) as e: logger.error( f"{LambdaError.DocRefInvalidFiles.to_str()} :{str(e)}", @@ -122,12 +125,8 @@ def create_document_reference_request( ) raise DocumentRefException(400, LambdaError.DocRefInvalidFiles) - def validate_document_file_type(self, validated_doc, snomed_code): - accepted_file_types = self.get_accepted_file_types( - snomed_code, upload_file_configs - ) - - if not is_file_type_allowed(validated_doc.file_name, accepted_file_types): + def validate_document_file_type(self, validated_doc, document_config): + if not is_file_type_allowed(validated_doc.file_name, document_config.accepted_file_types): raise LGInvalidFilesException( f"Unsupported file type for file: {validated_doc.file_name}" ) @@ -159,18 +158,6 @@ def validate_patient_user_ods_codes_match(self, user_ods_code, patient_ods_code) ) raise DocumentRefException(401, LambdaError.DocRefUnauthorizedOdsCode) - def get_accepted_file_types(self, snomed_code, upload_file_configs): - try: - return upload_file_configs.get_config_by_snomed_code( - snomed_code - ).accepted_file_types - except ConfigNotFoundException: - logger.error( - f"{LambdaError.DocRefInvalidType.to_str()}", - {"Result": UPLOAD_REFERENCE_FAILED_MESSAGE}, - ) - raise DocumentRefException(400, LambdaError.DocRefInvalidType) - def build_doc_ref_info( self, validated_doc, nhs_number, snomed_code, user_ods_code ) -> DocumentReferenceInfo: @@ -247,16 +234,19 @@ def create_document_reference( ) return document_reference - def check_existing_lloyd_george_records_and_remove_failed_upload( + def check_existing_records_and_remove_failed_upload( self, nhs_number: str, + doc_type: str, ) -> None: logger.info("Looking for previous records for this patient...") + query_filter = get_document_type_filter(DynamoQueryFilterBuilder(), doc_type) + previous_records = self.post_fhir_doc_ref_service.document_service.fetch_available_document_references_by_type( nhs_number=nhs_number, - doc_type=SupportedDocumentTypes.LG, - query_filter=NotDeleted, + doc_type=SupportedDocumentTypes(doc_type), + query_filter=query_filter, ) if not previous_records: logger.info( diff --git a/lambdas/tests/unit/enums/test_metadata_field_names.py b/lambdas/tests/unit/enums/test_metadata_field_names.py index 382854c89e..b4c1708130 100755 --- a/lambdas/tests/unit/enums/test_metadata_field_names.py +++ b/lambdas/tests/unit/enums/test_metadata_field_names.py @@ -8,7 +8,7 @@ def test_can_get_one_field_name(): def test_returns_all_as_list(): subject = DocumentReferenceMetadataFields.list() - assert len(subject) == 16 + assert len(subject) == 17 assert DocumentReferenceMetadataFields.ID.value in subject assert DocumentReferenceMetadataFields.CONTENT_TYPE.value in subject assert DocumentReferenceMetadataFields.CREATED.value in subject @@ -25,3 +25,4 @@ def test_returns_all_as_list(): assert DocumentReferenceMetadataFields.DOCUMENT_SCAN_CREATION.value in subject assert DocumentReferenceMetadataFields.CUSTODIAN.value in subject assert DocumentReferenceMetadataFields.FILE_SIZE.value in subject + assert DocumentReferenceMetadataFields.DOCUMENT_SNOMED_CODE_TYPE.value in subject \ No newline at end of file diff --git a/lambdas/tests/unit/helpers/data/test_documents.py b/lambdas/tests/unit/helpers/data/test_documents.py index 4f7d98fa55..6035c1e474 100644 --- a/lambdas/tests/unit/helpers/data/test_documents.py +++ b/lambdas/tests/unit/helpers/data/test_documents.py @@ -56,15 +56,18 @@ def create_test_lloyd_george_doc_store_refs( refs[0].s3_file_key = f"{TEST_NHS_NUMBER}/test-key-1" refs[0].file_location = f"s3://{MOCK_LG_BUCKET}/{TEST_NHS_NUMBER}/test-key-1" refs[0].s3_bucket_name = MOCK_LG_BUCKET + refs[0].document_snomed_code_type = SnomedCodes.LLOYD_GEORGE.value.code refs[1].file_name = filename_2 refs[1].s3_file_key = f"{TEST_NHS_NUMBER}/test-key-2" refs[1].file_location = f"s3://{MOCK_LG_BUCKET}/{TEST_NHS_NUMBER}/test-key-2" refs[1].s3_bucket_name = MOCK_LG_BUCKET + refs[1].document_snomed_code_type = SnomedCodes.LLOYD_GEORGE.value.code refs[2].file_name = filename_3 refs[2].s3_file_key = f"{TEST_NHS_NUMBER}/test-key-3" refs[2].file_location = f"s3://{MOCK_LG_BUCKET}/{TEST_NHS_NUMBER}/test-key-3" refs[2].s3_bucket_name = MOCK_LG_BUCKET - + refs[2].document_snomed_code_type = SnomedCodes.LLOYD_GEORGE.value.code + if override: refs = [doc_ref.model_copy(update=override) for doc_ref in refs] return refs diff --git a/lambdas/tests/unit/services/test_create_document_reference_service.py b/lambdas/tests/unit/services/test_create_document_reference_service.py index 21abadf58c..8a1332e037 100644 --- a/lambdas/tests/unit/services/test_create_document_reference_service.py +++ b/lambdas/tests/unit/services/test_create_document_reference_service.py @@ -2,7 +2,9 @@ from datetime import datetime import pytest +from enums.dynamo_filter import AttributeOperator from enums.lambda_error import LambdaError +from enums.metadata_field_names import DocumentReferenceMetadataFields from enums.snomed_codes import SnomedCodes from freezegun import freeze_time from models.document_reference import DocumentReference, UploadRequestDocument @@ -19,7 +21,9 @@ from tests.unit.helpers.data.test_documents import ( create_test_lloyd_george_doc_store_refs, ) +from utils.common_query_filters import NotDeleted from utils.constants.ssm import UPLOAD_PILOT_ODS_ALLOWED_LIST +from utils.dynamo_query_filter_builder import DynamoQueryFilterBuilder from utils.exceptions import PatientNotFoundException from utils.lambda_exceptions import DocumentRefException from utils.lloyd_george_validator import LGInvalidFilesException @@ -120,12 +124,12 @@ def mock_remove_records(mock_create_doc_ref_service, mocker): @pytest.fixture() -def mock_check_existing_lloyd_george_records_and_remove_failed_upload( +def mock_check_existing_records_and_remove_failed_upload( mock_create_doc_ref_service, mocker ): yield mocker.patch.object( mock_create_doc_ref_service, - "check_existing_lloyd_george_records_and_remove_failed_upload", + "check_existing_records_and_remove_failed_upload", ) @@ -197,7 +201,7 @@ def test_create_document_reference_request_with_lg_list_happy_path( mock_process_fhir_document_reference, mock_getting_patient_info_from_pds, mock_get_allowed_list_of_ods_codes_for_upload_pilot, - mock_check_existing_lloyd_george_records_and_remove_failed_upload, + mock_check_existing_records_and_remove_failed_upload, mock_fetch_available_document_references_by_type, mock_check_for_duplicate_files, ): @@ -216,8 +220,8 @@ def test_create_document_reference_request_with_lg_list_happy_path( } assert url_references == expected_response - mock_check_existing_lloyd_george_records_and_remove_failed_upload.assert_called_with( - TEST_NHS_NUMBER + mock_check_existing_records_and_remove_failed_upload.assert_called_with( + TEST_NHS_NUMBER, LG_FILE_LIST[0]["docType"] ) mock_check_for_duplicate_files.assert_called_once() @@ -231,7 +235,7 @@ def test_create_document_reference_request_raise_error_when_invalid_lg( mock_check_for_duplicate_files, mock_getting_patient_info_from_pds, mock_get_allowed_list_of_ods_codes_for_upload_pilot, - mock_check_existing_lloyd_george_records_and_remove_failed_upload, + mock_check_existing_records_and_remove_failed_upload, ): document_references = [] side_effects = [] @@ -329,7 +333,7 @@ def test_cdr_non_pdf_file_raises_exception( mock_check_for_duplicate_files, mock_getting_patient_info_from_pds, mock_get_allowed_list_of_ods_codes_for_upload_pilot, - mock_check_existing_lloyd_george_records_and_remove_failed_upload, + mock_check_existing_records_and_remove_failed_upload, ): mock_check_for_duplicate_files.side_effect = LGInvalidFilesException mock_get_allowed_list_of_ods_codes_for_upload_pilot.return_value = [ @@ -399,7 +403,7 @@ def test_create_document_reference_request_lg_upload_throw_lambda_error_if_got_a assert e.value == DocumentRefException(422, LambdaError.DocRefRecordAlreadyInPlace) -def test_check_existing_lloyd_george_records_remove_previous_failed_upload_and_continue( +def test_check_existing_records_remove_previous_failed_upload_and_continue( mock_fhir_doc_ref_base_service, mock_create_doc_ref_service, mock_fetch_available_document_references_by_type, @@ -415,8 +419,8 @@ def test_check_existing_lloyd_george_records_remove_previous_failed_upload_and_c mock_create_doc_ref_service.stop_if_all_records_uploaded = mocker.MagicMock() mock_create_doc_ref_service.stop_if_upload_is_in_process = mocker.MagicMock() - mock_create_doc_ref_service.check_existing_lloyd_george_records_and_remove_failed_upload( - TEST_NHS_NUMBER + mock_create_doc_ref_service.check_existing_records_and_remove_failed_upload( + TEST_NHS_NUMBER, mock_doc_refs_of_failed_upload[0].document_snomed_code_type ) mock_remove_records.assert_called_with( MOCK_LG_TABLE_NAME, mock_doc_refs_of_failed_upload @@ -505,7 +509,7 @@ def test_prepare_doc_object_lg_happy_path( ) -def test_check_existing_lloyd_george_records_does_nothing_if_no_record_exist( +def test_check_existing_records_does_nothing_if_no_record_exist( mock_fhir_doc_ref_base_service, mock_create_doc_ref_service, mock_fetch_available_document_references_by_type, @@ -515,8 +519,9 @@ def test_check_existing_lloyd_george_records_does_nothing_if_no_record_exist( mock_fetch_available_document_references_by_type.return_value = [] assert ( - mock_create_doc_ref_service.check_existing_lloyd_george_records_and_remove_failed_upload( - TEST_NHS_NUMBER + mock_create_doc_ref_service.check_existing_records_and_remove_failed_upload( + TEST_NHS_NUMBER, + SupportedDocumentTypes.LG ) is None ) @@ -524,7 +529,7 @@ def test_check_existing_lloyd_george_records_does_nothing_if_no_record_exist( @freeze_time("2023-10-30T10:25:00") -def test_check_existing_lloyd_george_records_throw_error_if_upload_in_progress( +def test_check_existing_records_throw_error_if_upload_in_progress( mock_fhir_doc_ref_base_service, mock_create_doc_ref_service, mock_fetch_available_document_references_by_type, @@ -542,8 +547,9 @@ def test_check_existing_lloyd_george_records_throw_error_if_upload_in_progress( ) with pytest.raises(Exception) as e: - mock_create_doc_ref_service.check_existing_lloyd_george_records_and_remove_failed_upload( - TEST_NHS_NUMBER + mock_create_doc_ref_service.check_existing_records_and_remove_failed_upload( + TEST_NHS_NUMBER, + SupportedDocumentTypes.LG ) ex = e.value assert isinstance(ex, DocumentRefException) @@ -553,7 +559,7 @@ def test_check_existing_lloyd_george_records_throw_error_if_upload_in_progress( mock_remove_records.assert_not_called() -def test_check_existing_lloyd_george_records_throw_error_if_got_a_full_set_of_uploaded_record( +def test_check_existing_records_throw_error_if_got_a_full_set_of_uploaded_record( mock_fhir_doc_ref_base_service, mock_create_doc_ref_service, mock_fetch_available_document_references_by_type, @@ -564,8 +570,9 @@ def test_check_existing_lloyd_george_records_throw_error_if_got_a_full_set_of_up ) with pytest.raises(Exception) as e: - mock_create_doc_ref_service.check_existing_lloyd_george_records_and_remove_failed_upload( - TEST_NHS_NUMBER + mock_create_doc_ref_service.check_existing_records_and_remove_failed_upload( + TEST_NHS_NUMBER, + SupportedDocumentTypes.LG ) ex = e.value @@ -648,7 +655,7 @@ def test_patient_ods_does_not_match_user_ods_and_raises_exception( mock_fhir_doc_ref_base_service, mock_create_doc_ref_service, mock_create_document_reference, - mock_check_existing_lloyd_george_records_and_remove_failed_upload, + mock_check_existing_records_and_remove_failed_upload, ): with pytest.raises(DocumentRefException) as exc_info: @@ -667,10 +674,10 @@ def test_patient_ods_does_not_match_user_ods_and_raises_exception( ) -def test_unable_to_find_config_reiases_exception( +def test_unable_to_find_config_raises_exception( mock_fhir_doc_ref_base_service, mock_create_doc_ref_service, - mock_check_existing_lloyd_george_records_and_remove_failed_upload, + mock_check_existing_records_and_remove_failed_upload, mock_getting_patient_info_from_pds, mock_get_allowed_list_of_ods_codes_for_upload_pilot, mock_process_fhir_document_reference, @@ -689,7 +696,36 @@ def test_unable_to_find_config_reiases_exception( assert exception.status_code == 400 assert ( exception.message - == "Failed to parse document upload request data due to invalid document type" + == "Invalid files or id" ) mock_process_fhir_document_reference.assert_not_called() + +def test_check_existing_records_fetches_previous_records_for_doc_type( + mock_fhir_doc_ref_base_service, + mock_create_doc_ref_service, + mock_fetch_available_document_references_by_type, + mock_remove_records, + mocker +): + doc_type = SupportedDocumentTypes.LG + + expected_query_filter = NotDeleted & DynamoQueryFilterBuilder().add_condition( + DocumentReferenceMetadataFields.DOCUMENT_SNOMED_CODE_TYPE, + AttributeOperator.EQUAL, + doc_type + ).build() + mocker.patch( + "services.create_document_reference_service.get_document_type_filter" + ).return_value = expected_query_filter + + mock_create_doc_ref_service.check_existing_records_and_remove_failed_upload( + TEST_NHS_NUMBER, + doc_type + ) + + mock_fetch_available_document_references_by_type.assert_called_with( + nhs_number=TEST_NHS_NUMBER, + doc_type=doc_type, + query_filter=expected_query_filter + ) \ No newline at end of file diff --git a/lambdas/utils/dynamo_utils.py b/lambdas/utils/dynamo_utils.py index 336de29101..8cc40be3fb 100644 --- a/lambdas/utils/dynamo_utils.py +++ b/lambdas/utils/dynamo_utils.py @@ -348,6 +348,7 @@ def __init__(self): SnomedCodes.LLOYD_GEORGE.value.code: self.lg_dynamo_table, SnomedCodes.EHR.value.code: self.lg_dynamo_table, SnomedCodes.EHR_ATTACHMENTS.value.code: self.lg_dynamo_table, + SnomedCodes.LETTERS_AND_DOCUMENTS.value.code: self.lg_dynamo_table, SnomedCodes.PATIENT_DATA.value.code: self.core_dynamo_table, } diff --git a/lambdas/utils/upload_file_configs.py b/lambdas/utils/upload_file_configs.py index 35ae33d18c..14809812ae 100644 --- a/lambdas/utils/upload_file_configs.py +++ b/lambdas/utils/upload_file_configs.py @@ -13,6 +13,7 @@ multifile_zipped=False, multifile_review=True, can_be_discarded=True, + single_file_only=True, stitched=True, accepted_file_types=["PDF"], content=[], @@ -27,6 +28,7 @@ multifile_zipped=False, multifile_review=True, can_be_discarded=True, + single_file_only=False, stitched=True, accepted_file_types=["PDF"], content=[], @@ -41,15 +43,32 @@ multifile_zipped=False, multifile_review=True, can_be_discarded=True, + single_file_only=False, stitched=True, accepted_file_types=["ZIP"], content=[], ) +LETTERS_AND_DOCUMENTS = DocumentConfig( + snomed_code=SnomedCodes.LETTERS_AND_DOCUMENTS.value.code, + display_name="Letters and Documents", + can_be_updated=False, + associated_snomed="", + multifile_upload=True, + multifile_zipped=False, + multifile_review=False, + can_be_discarded=True, + single_file_only=False, + stitched=False, + accepted_file_types=[], + content=[], +) + ALL_CONFIGS = [ LLOYD_GEORGE, ELECTRONIC_HEALTH_RECORD, ATTACHMENTS, + LETTERS_AND_DOCUMENTS, ] CONFIG_BY_SNOMED: Dict[str, DocumentConfig] = {