From ffdcb859fb7df29c1106e094b7e1fee21110632e Mon Sep 17 00:00:00 2001 From: Daniel Naab Date: Wed, 29 May 2024 14:34:52 -0500 Subject: [PATCH] Pattern constructors (#142) * Fix route parsing in form filler view. * Create new pattern constructor that validates the config data, and wire up to the external PDF parsing library. Context: we haven't been validating input from the PDF parsing process, and there were hard to debug issues. A remaining issue that this identified is the presence of spaces in some option IDs. These, and other errors, are currently being logged to the console, and the corresponding pattern is ignored (not added to the form blueprint). Follow-up work should include fixes this issue, and also presentation of these validation errors to the user. * Split api call and api response processing * Fix test * Add mock-response.ts --- .../RadioGroupPatternEdit.stories.tsx | 2 +- .../FormManager/FormInspect/FormInspect.tsx | 11 + .../src/FormManager/FormInspect/index.ts | 1 + packages/design/src/FormManager/index.tsx | 25 + packages/design/src/FormManager/routes.ts | 5 + packages/forms/src/builder/index.ts | 10 +- .../src/documents/__tests__/document.test.ts | 50 + .../documents/__tests__/suggestions.test.ts | 9 - packages/forms/src/documents/document.ts | 15 +- packages/forms/src/documents/index.ts | 1 - .../forms/src/documents/pdf/mock-response.ts | 1127 +++++++++++++++++ .../forms/src/documents/pdf/parsing-api.ts | 220 ++-- packages/forms/src/documents/suggestions.ts | 16 - packages/forms/src/pattern.ts | 31 +- packages/forms/src/patterns/index.ts | 2 +- packages/forms/src/patterns/radio-group.ts | 5 +- 16 files changed, 1404 insertions(+), 126 deletions(-) create mode 100644 packages/design/src/FormManager/FormInspect/FormInspect.tsx create mode 100644 packages/design/src/FormManager/FormInspect/index.ts create mode 100644 packages/forms/src/documents/__tests__/document.test.ts delete mode 100644 packages/forms/src/documents/__tests__/suggestions.test.ts create mode 100644 packages/forms/src/documents/pdf/mock-response.ts delete mode 100644 packages/forms/src/documents/suggestions.ts diff --git a/packages/design/src/FormManager/FormEdit/components/RadioGroupPatternEdit.stories.tsx b/packages/design/src/FormManager/FormEdit/components/RadioGroupPatternEdit.stories.tsx index 957417ec..b4a892a8 100644 --- a/packages/design/src/FormManager/FormEdit/components/RadioGroupPatternEdit.stories.tsx +++ b/packages/design/src/FormManager/FormEdit/components/RadioGroupPatternEdit.stories.tsx @@ -117,7 +117,7 @@ export const Error: StoryObj = { optionId.blur(); await expect( - await canvas.findByText('Invalid option ID') + await canvas.findByText('Option ID may not contain spaces') ).toBeInTheDocument(); await userEvent.clear(optionLabel); diff --git a/packages/design/src/FormManager/FormInspect/FormInspect.tsx b/packages/design/src/FormManager/FormInspect/FormInspect.tsx new file mode 100644 index 00000000..a80d3b91 --- /dev/null +++ b/packages/design/src/FormManager/FormInspect/FormInspect.tsx @@ -0,0 +1,11 @@ +import { useFormManagerStore } from '../store'; +import React from 'react'; + +export const FormInspect = () => { + const form = useFormManagerStore(state => state.session.form); + return ( +
+      {JSON.stringify(form, null, 2)}
+    
+ ); +}; diff --git a/packages/design/src/FormManager/FormInspect/index.ts b/packages/design/src/FormManager/FormInspect/index.ts new file mode 100644 index 00000000..0419710a --- /dev/null +++ b/packages/design/src/FormManager/FormInspect/index.ts @@ -0,0 +1 @@ +export { FormInspect } from './FormInspect'; diff --git a/packages/design/src/FormManager/index.tsx b/packages/design/src/FormManager/index.tsx index ec99f128..c325beb1 100644 --- a/packages/design/src/FormManager/index.tsx +++ b/packages/design/src/FormManager/index.tsx @@ -16,6 +16,7 @@ import FormDelete from './FormDelete'; import { FormDocumentImport } from './FormDocumentImport'; import FormEdit from './FormEdit'; import { type EditComponentForPattern } from './FormEdit/types'; +import { FormInspect } from './FormInspect'; import FormList from './FormList'; import { FormManagerLayout } from './FormManagerLayout'; import { NavPage } from './FormManagerLayout/TopNavigation'; @@ -52,6 +53,30 @@ export default function FormManager({ context }: FormManagerProps) { ); }} /> + { + const { formId } = useParams(); + if (formId === undefined) { + return
formId is undefined
; + } + const formResult = context.formService.getForm(formId); + if (!formResult.success) { + return
Error loading form preview
; + } + return ( + + + + + + ); + }} + /> { diff --git a/packages/design/src/FormManager/routes.ts b/packages/design/src/FormManager/routes.ts index df65d2aa..7fa2b200 100644 --- a/packages/design/src/FormManager/routes.ts +++ b/packages/design/src/FormManager/routes.ts @@ -8,6 +8,11 @@ export const MyForms: Route<[]> = { getUrl: () => `#`, }; +export const Inspect: Route = { + path: '/:formId/inspect', + getUrl: (formId: string) => `#/${formId}/inspect`, +}; + export const Preview: Route = { path: '/:formId/preview', getUrl: (formId: string) => `#/${formId}/preview`, diff --git a/packages/forms/src/builder/index.ts b/packages/forms/src/builder/index.ts index c20f9213..80eb7c51 100644 --- a/packages/forms/src/builder/index.ts +++ b/packages/forms/src/builder/index.ts @@ -10,9 +10,8 @@ import { addDocument, addPageToPageSet, addPatternToPage, - createPattern, + createDefaultPattern, getPattern, - nullBlueprint, removePatternFromBlueprint, updateFormSummary, updatePatternFromFormData, @@ -44,14 +43,17 @@ export class BlueprintBuilder { } addPage() { - const newPage = createPattern(this.config, 'page'); + const newPage = createDefaultPattern(this.config, 'page'); this.bp = addPageToPageSet(this.form, newPage); return newPage; } addPatternToFirstPage(patternType: string) { - const pattern = createPattern(this.config, patternType); + const pattern = createDefaultPattern(this.config, patternType); const root = this.form.patterns[this.form.root] as PageSetPattern; + if (root.type !== 'page-set') { + throw new Error('expected root to be a page-set'); + } const firstPagePatternId = root.data.pages[0]; this.bp = addPatternToPage(this.form, firstPagePatternId, pattern); return pattern; diff --git a/packages/forms/src/documents/__tests__/document.test.ts b/packages/forms/src/documents/__tests__/document.test.ts new file mode 100644 index 00000000..0e2f04e4 --- /dev/null +++ b/packages/forms/src/documents/__tests__/document.test.ts @@ -0,0 +1,50 @@ +/** + * @vitest-environment jsdom + */ +import { describe, expect, it } from 'vitest'; + +import { getPattern } from '../..'; +import { BlueprintBuilder } from '../../builder'; +import { defaultFormConfig } from '../../patterns'; +import { type PageSetPattern } from '../../patterns/page-set/config'; +import { type PagePattern } from '../../patterns/page/config'; + +import { addDocument } from '../document'; +import { loadSamplePDF } from './sample-data'; + +describe('addDocument document processing', () => { + it('creates expected blueprint', async () => { + const builder = new BlueprintBuilder(defaultFormConfig); + const pdfBytes = await loadSamplePDF( + 'doj-pardon-marijuana/application_for_certificate_of_pardon_for_simple_marijuana_possession.pdf' + ); + const { updatedForm, errors } = await addDocument( + builder.form, + { + name: 'test.pdf', + data: new Uint8Array(pdfBytes), + }, + { + fetchPdfApiResponse: async () => { + const { mockResponse } = await import('../pdf/mock-response'); + return mockResponse; + }, + } + ); + const rootPattern = getPattern( + updatedForm, + updatedForm.root + ); + + console.error(JSON.stringify(errors, null, 2)); // Fix these + expect(rootPattern).toEqual(expect.objectContaining({ type: 'page-set' })); + expect(rootPattern.data.pages.length).toEqual(1); + const pagePattern = getPattern( + updatedForm, + rootPattern.data.pages[0] + ); + + // As a sanity check, just confirm that there is content on the first page. + expect(pagePattern.data.patterns.length).toBeGreaterThan(1); + }); +}); diff --git a/packages/forms/src/documents/__tests__/suggestions.test.ts b/packages/forms/src/documents/__tests__/suggestions.test.ts deleted file mode 100644 index 18222b9c..00000000 --- a/packages/forms/src/documents/__tests__/suggestions.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { describe, it } from 'vitest'; - -import { loadSampleFields } from './sample-data'; - -describe('PDF form field extraction', () => { - it('extracts data from California UD-105 form', async () => { - const fields = loadSampleFields('ca-unlawful-detainer/ud-105.fields.json'); - }); -}); diff --git a/packages/forms/src/documents/document.ts b/packages/forms/src/documents/document.ts index 4e0e9e42..351bbb1a 100644 --- a/packages/forms/src/documents/document.ts +++ b/packages/forms/src/documents/document.ts @@ -9,7 +9,11 @@ import { import { InputPattern } from '../patterns/input'; import { SequencePattern } from '../patterns/sequence'; import { PDFDocument, getDocumentFieldData } from './pdf'; -import { getSuggestedPatterns } from './suggestions'; +import { + type FetchPdfApiResponse, + processApiResponse, + fetchPdfApiResponse, +} from './pdf/parsing-api'; import { DocumentFieldMap } from './types'; export type DocumentTemplate = PDFDocument; @@ -19,10 +23,14 @@ export const addDocument = async ( fileDetails: { name: string; data: Uint8Array; - } + }, + context: { + fetchPdfApiResponse: FetchPdfApiResponse; + } = { fetchPdfApiResponse } ) => { const fields = await getDocumentFieldData(fileDetails.data); - const parsedPdf = await getSuggestedPatterns(fileDetails.data); + const json = await context.fetchPdfApiResponse(fileDetails.data); + const parsedPdf = await processApiResponse(json); if (parsedPdf) { form = updateFormSummary(form, { @@ -43,6 +51,7 @@ export const addDocument = async ( return { newFields: fields, updatedForm, + errors: parsedPdf.errors, }; } else { const formWithFields = addDocumentFieldsToForm(form, fields); diff --git a/packages/forms/src/documents/index.ts b/packages/forms/src/documents/index.ts index 1ad9ff63..fb806b46 100644 --- a/packages/forms/src/documents/index.ts +++ b/packages/forms/src/documents/index.ts @@ -1,6 +1,5 @@ export * from './document'; export * from './pdf'; -export * from './suggestions'; export * from './types'; export const SAMPLE_DOCUMENTS = [ diff --git a/packages/forms/src/documents/pdf/mock-response.ts b/packages/forms/src/documents/pdf/mock-response.ts new file mode 100644 index 00000000..64257d9f --- /dev/null +++ b/packages/forms/src/documents/pdf/mock-response.ts @@ -0,0 +1,1127 @@ +export const mockResponse = { + message: 'PDF parsed successfully', + parsed_pdf: { + raw_text: + 'OMB Control No: 1123-0014 Expires 03/31/2027 \nAPPLICATION FOR CERTIFICATE OF PARDON FOR THE OFFENSES OF \nSIMPLE POSSESSION, ATTEMPTED SIMPLE POSSESSION, OR USE OF \nMARIJUANA \nOn October 6, 2022, President Biden issued a presidential proclamation that pardoned many federal and D.C. \noffenses for simple marijuana possession. On December 22, 2023, President Biden issued another proclamation that \nexpanded the relief provided by the original proclamation by pardoning the federal offenses of simple possession, \nattempted possession, and use of marijuana. \nHow a pardon can help you \nA pardon is an expression of the President\u2019s forgiveness. It does not mean you are innocent or expunge your \nconviction. But it does remove civil disabilities\u2014such as restrictions on the right to vote, to hold office, or to sit \non a jury\u2014that are imposed because of the pardoned conviction. It may also be helpful in obtaining licenses, \nbonding, or employment. Learn more about the pardon. \nYou qualify for the pardon if: \n\u2022 On or before December 22, 2023, you were charged with or convicted of simple possession, attempted \npossession, or use of marijuana under the federal code, the District of Columbia code, or the Code of \nFederal Regulations \n\u2022 You were a U.S. citizen or lawfully present in the United States at the time of the offense \n\u2022 You were a U.S. citizen or lawful permanent resident on December 22, 2023 \nRequest a certificate to show proof of the pardon \nA Certificate of Pardon is proof that you were pardoned under the proclamation. The certificate is the only \ndocumentation you will receive of the pardon. Use the application below to start your request. \nWhat you\'ll need for the request \nAbout you \nYou can submit a request for yourself or someone else can submit on your behalf. You must provide \npersonal details, like name or citizenship status and either a mailing address, an email address or both to \ncontact you. We strongly recommend including an email address, if available, as we may not be able to \nrespond as quickly if you do not provide it. You can also use the mailing address or email address of \nanother person, if you do not have your own. \nAbout the charge or conviction \nYou must state whether it was a charge or conviction, the court district where it happened, and the date \n(month, day, year). If possible, you should also: \n\u2022 enter information about your case (docket or case number and the code section that was \ncharged) \n\u2022 upload your documents \no charging documents, like the indictment, complaint, criminal information, ticket or \ncitation; or \no conviction documents, like the judgment of conviction, the court docket sheet showing \nthe sentence and date it was imposed, or if you did not go to court, the receipt showing \npayment of fine \nIf you were charged by a ticket or citation and paid a fine instead of appearing in court, you should also provide the \ndate of conviction or the date the fine was paid. \nWithout this information, we can\'t guarantee that we\'ll be able to determine if you qualify for the pardon under \nthe proclamation. \n \nPage 1 of 4 \nUnited States Department of Justice Office of the Pardon Attorney Washington, D.C. 20530 January 2024 OMB Control No: 1123-0014 Expires 03/31/2027 \nAPPLICATION FOR CERTIFICATE OF PARDON FOR THE OFFENSES OF \nSIMPLE POSSESSION, ATTEMPTED SIMPLE POSSESSION, OR USE OF \nMARIJUANA \nInstructions: \nAn online version of this application is available at: Presidential Proclamation on Marijuana Possession \n(justice.gov). You can also complete and return this application with the required documents to \nUSPardon.Attorney@usdoj.gov or U.S. Department of Justice, Office of the Pardon Attorney, 950 Pennsylvania \nAvenue NW, Washington, DC 20530. \nPublic Burden Statement: \nThis collection meets the requirements of 44 U.S.C. \u00a7 3507, as amended by the Paperwork Reduction Act of 1995. \nWe estimate that it will take 120 minutes to read the instructions, gather the relevant materials, and answer \nquestions on the form. Send comments regarding the burden estimate or any other aspect of this collection of \ninformation, including suggestions for reducing this burden, to Office of the Pardon Attorney, U.S. Department of \nJustice, Attn: OMB Number 1123-0014, RFK Building, 950 Pennsylvania Avenue, N.W., Washington DC 20530. \nThe OMB Clearance number, 1123-0014, is currently valid. \nPrivacy Act Statement: \nThe Office of the Pardon Attorney has authority to collect this information under the U.S. Constitution, Article \nII, Section 2 (the pardon clause); Orders of the Attorney General Nos. 1798-93, 58 Fed. Reg. 53658 and 53659 \n(1993), 2317-2000, 65 Fed. Reg. 48381 (2000), and 2323-2000, 65 Fed. Reg. 58223 and 58224 (2000), codified in \n28 C.F.R. \u00a7\u00a7 1.1 et seq. (the rules governing petitions for executive clemency); and Order of the Attorney General \nNo. 1012-83, 48 Fed. Reg. 22290 (1983), as codified in 28 C.F.R. \u00a7\u00a7 0.35 and 0.36 (the authority of the Office of \nthe Pardon Attorney). The principal purpose for collecting this information is to enable the Office of the Pardon \nAttorney to issue an individual certificate of pardon to you. The routine uses which may be made of this \ninformation include provision of data to the President and his staff, other governmental entities, and the public. \nThe full list of routine uses for this correspondence can be found in the System of Records Notice titled, \u201cPrivacy \nAct of 1974; System of Records,\u201d published in Federal Register, September 15, 2011, Vol. 76, No. 179, at pages \n57078 through 57080; as amended by \u201cPrivacy Act of 1974; System of Records,\u201d published in the Federal \nRegister, May 25, 2017, Vol. 82, No. 100, at page 24161, and at the U.S. Department of Justice, Office of Privacy \nand Civil Liberties\' website at: https://www.justice.gov/opcl/doj-systems-records#OPA. \nBy signing the attached form, you consent to allowing the Office of the Pardon Attorney to obtain information \nregarding your citizenship and/or immigration status from the courts, from other government agencies, from other \ncomponents within the Department of Justice, and from the Department of Homeland Security, U.S. Citizenship \nand Immigration Services (DHS-USCIS), Systematic Alien Verification for Entitlements (SAVE) program. The \ninformation received from these sources will be used for the sole purposes of determining an applicant\'s \nqualification for a Certificate of Pardon under the December 22 proclamation and for record-keeping of those \ndeterminations. Further, please be aware that if the Office of the Pardon Attorney is unable to verify your \ncitizenship or immigration status based on the information provided below, we may contact you to obtain \nadditional verification information. Learn more about the DHS-USCIS\'s SAVE program and its ordinary uses. \nYour disclosure of information to the Office of the Pardon Attorney on this form is voluntary. If you do not \ncomplete all or some of the information fields in this form, however, the Office of the Pardon Attorney may not be \nable to effectively respond. Information regarding gender, race, or ethnicity is not required and will not affect the \nprocessing of the application. \nNote: Submit a separate form for each conviction or charge for which you are seeking a certificate of pardon. \nApplication Form on page 3. \nPage 2 of 4 \nUnited States Department of Justice Office of the Pardon Attorney Washington, D.C. 20530 January 2024 OMB Control No: 1123-0014 Expires 03/31/2027 \nAPPLICATION FOR CERTIFICATE OF PARDON FOR THE OFFENSES OF \nSIMPLE POSSESSION, ATTEMPTED SIMPLE POSSESSION, OR USE OF \nMARIJUANA \nComplete the following: \nName: \n(first) (middle) (last) \nName at Conviction: \n(if different) (first) (middle) (last) \nAddress: \n(number) (street) (apartment/unit no.) \n\n(city) (state) (Zip Code) \nEmail Address: Phone Number: \nDate of Birth: Gender: Are you Hispanic or Latino?: Yes No \nRace: Alaska Native or American Indian Asian Black or African American \nNative Hawaiian or Other Pacific Islander White Other \nCitizenship or Residency Status: \nU.S. citizen by birth \nU.S. naturalized citizen Date Naturalization Granted: \nLawful Permanent Resident Date Residency Granted: \nAlien Registration Number (A-Number), Certificate of Naturalization Number, or Citizenship Number \n(if applicant is a lawful permanent resident or naturalized citizen): \n(A-Number) \n1. Applicant was convicted on: in the U.S. District Court for the \n(month/day/year) (Northern, etc.) \nDistrict of (state) or D.C. Superior Court of simple possession of marijuana, under \nDocket No. : and Code Section: ; OR \n(docket number) (code section) \n2. Applicant was charged with Code Section: in the U.S. District Court for the \n(code section) (Eastern, etc.) \nDistrict of or D.C. Superior Court under Docket No: \n(state) (docket number) \n \nUnited States Department of Justice Office of the Pardon Attorney Page 3 of 4 Washington, D.C. 20530 January 2024 OMB Control No: 1123-0014 Expires 03/31/2027 \nAPPLICATION FOR CERTIFICATE OF PARDON FOR THE OFFENSES OF \nSIMPLE POSSESSION, ATTEMPTED SIMPLE POSSESSION, OR USE OF \nMARIJUANA \nWith knowledge of the penalties for false statements to Federal Agencies, as provided by 18 \nU.S.C. \u00a7 1001, and with knowledge that this statement is submitted by me to affect action by \nthe U.S. Department of Justice, I certify that: \n1. The applicant was either a U.S. citizen or lawfully present in the United States at the time of the \noffense. \n2. The applicant was a U.S. citizen or lawful permanent resident on December 22, 2023. \n3. The above statements, and accompanying documents, are true and complete to the \n best of my knowledge, information, and belief. \n4. I acknowledge that any certificate issued in reliance on the above information will be \nvoided, if the information is subsequently determined to be false. \n\n(date) (signature) \nPage 4 of 4 \nUnited States Department of Justice Office of the Pardon Attorney Washington, D.C. 20530 January 2024 ', + form_summary: { + component_type: 'form_summary', + title: '', + description: '', + }, + elements: [ + { + component_type: 'paragraph', + text: "OMB Control No: 1123-0014 Expires 03/31/2027 APPLICATION FOR CERTIFICATE OF PARDON FOR THE OFFENSES OF SIMPLE POSSESSION, ATTEMPTED SIMPLE POSSESSION, OR USE OF MARIJUANA On October 6, 2022, President Biden issued a presidential proclamation that pardoned many federal and D.C. offenses for simple marijuana possession. On December 22, 2023, President Biden issued another proclamation that expanded the relief provided by the original proclamation by pardoning the federal offenses of simple possession, attempted possession, and use of marijuana. How a pardon can help you A pardon is an expression of the President\u2019s forgiveness. It does not mean you are innocent or expunge your conviction. But it does remove civil disabilities\u2014such as restrictions on the right to vote, to hold office, or to sit on a jury\u2014that are imposed because of the pardoned conviction. It may also be helpful in obtaining licenses, bonding, or employment. Learn more about the pardon. You qualify for the pardon if: \u2022 On or before December 22, 2023, you were charged with or convicted of simple possession, attempted possession, or use of marijuana under the federal code, the District of Columbia code, or the Code of Federal Regulations \u2022 You were a U.S. citizen or lawfully present in the United States at the time of the offense \u2022 You were a U.S. citizen or lawful permanent resident on December 22, 2023 Request a certificate to show proof of the pardon A Certificate of Pardon is proof that you were pardoned under the proclamation. The certificate is the only documentation you will receive of the pardon. Use the application below to start your request. What you'll need for the request About you You can submit a request for yourself or someone else can submit on your behalf. You must provide personal details, like name or citizenship status and either a mailing address, an email address or both to contact you. We strongly recommend including an email address, if available, as we may not be able to respond as quickly if you do not provide it. You can also use the mailing address or email address of another person, if you do not have your own. About the charge or conviction You must state whether it was a charge or conviction, the court district where it happened, and the date (month, day, year). If possible, you should also: \u2022 enter information about your case (docket or case number and the code section that was charged) \u2022 upload your documents o charging documents, like the indictment, complaint, criminal information, ticket or citation; or o conviction documents, like the judgment of conviction, the court docket sheet showing the sentence and date it was imposed, or if you did not go to court, the receipt showing payment of fine If you were charged by a ticket or citation and paid a fine instead of appearing in court, you should also provide the date of conviction or the date the fine was paid. Without this information, we can't guarantee that we'll be able to determine if you qualify for the pardon under the proclamation. Page 1 of 4 United States Department of Justice Office of the Pardon Attorney Washington, D.C. 20530 January 2024", + }, + { + component_type: 'paragraph', + text: "OMB Control No: 1123-0014 Expires 03/31/2027 APPLICATION FOR CERTIFICATE OF PARDON FOR THE OFFENSES OF SIMPLE POSSESSION, ATTEMPTED SIMPLE POSSESSION, OR USE OF MARIJUANA Instructions: An online version of this application is available at: Presidential Proclamation on Marijuana Possession (justice.gov). You can also complete and return this application with the required documents to USPardon.Attorney@usdoj.gov or U.S. Department of Justice, Office of the Pardon Attorney, 950 Pennsylvania Avenue NW, Washington, DC 20530. Public Burden Statement: This collection meets the requirements of 44 U.S.C. \u00a7 3507, as amended by the Paperwork Reduction Act of 1995. We estimate that it will take 120 minutes to read the instructions, gather the relevant materials, and answer questions on the form. Send comments regarding the burden estimate or any other aspect of this collection of information, including suggestions for reducing this burden, to Office of the Pardon Attorney, U.S. Department of Justice, Attn: OMB Number 1123-0014, RFK Building, 950 Pennsylvania Avenue, N.W., Washington DC 20530. The OMB Clearance number, 1123-0014, is currently valid. Privacy Act Statement: The Office of the Pardon Attorney has authority to collect this information under the U.S. Constitution, Article II, Section 2 (the pardon clause); Orders of the Attorney General Nos. 1798-93, 58 Fed. Reg. 53658 and 53659 (1993), 2317-2000, 65 Fed. Reg. 48381 (2000), and 2323-2000, 65 Fed. Reg. 58223 and 58224 (2000), codified in 28 C.F.R. \u00a7\u00a7 1.1 et seq. (the rules governing petitions for executive clemency); and Order of the Attorney General No. 1012-83, 48 Fed. Reg. 22290 (1983), as codified in 28 C.F.R. \u00a7\u00a7 0.35 and 0.36 (the authority of the Office of the Pardon Attorney). The principal purpose for collecting this information is to enable the Office of the Pardon Attorney to issue an individual certificate of pardon to you. The routine uses which may be made of this information include provision of data to the President and his staff, other governmental entities, and the public. The full list of routine uses for this correspondence can be found in the System of Records Notice titled, \u201cPrivacy Act of 1974; System of Records,\u201d published in Federal Register, September 15, 2011, Vol. 76, No. 179, at pages 57078 through 57080; as amended by \u201cPrivacy Act of 1974; System of Records,\u201d published in the Federal Register, May 25, 2017, Vol. 82, No. 100, at page 24161, and at the U.S. Department of Justice, Office of Privacy and Civil Liberties' website at: https://www.justice.gov/opcl/doj-systems-records#OPA. By signing the attached form, you consent to allowing the Office of the Pardon Attorney to obtain information regarding your citizenship and/or immigration status from the courts, from other government agencies, from other components within the Department of Justice, and from the Department of Homeland Security, U.S. Citizenship and Immigration Services (DHS-USCIS), Systematic Alien Verification for Entitlements (SAVE) program. The information received from these sources will be used for the sole purposes of determining an applicant's qualification for a Certificate of Pardon under the December 22 proclamation and for record-keeping of those determinations. Further, please be aware that if the Office of the Pardon Attorney is unable to verify your citizenship or immigration status based on the information provided below, we may contact you to obtain additional verification information. Learn more about the DHS-USCIS's SAVE program and its ordinary uses. Your disclosure of information to the Office of the Pardon Attorney on this form is voluntary. If you do not complete all or some of the information fields in this form, however, the Office of the Pardon Attorney may not be able to effectively respond. Information regarding gender, race, or ethnicity is not required and will not affect the processing of the application. Note: Submit a separate form for each conviction or charge for which you are seeking a certificate of pardon. Application Form on page 3. Page 2 of 4 United States Department of Justice Office of the Pardon Attorney Washington, D.C. 20530 January 2024", + }, + { + component_type: 'paragraph', + text: 'OMB Control No: 1123-0014 Expires 03/31/2027 APPLICATION FOR CERTIFICATE OF PARDON FOR THE OFFENSES OF SIMPLE POSSESSION, ATTEMPTED SIMPLE POSSESSION, OR USE OF MARIJUANA Complete the following:', + }, + { + component_type: 'fieldset', + legend: 'Name: ', + fields: [ + { + component_type: 'text_input', + id: 'Fst Name 1', + label: 'First Name', + default_value: '', + required: true, + }, + { + component_type: 'text_input', + id: '', + label: 'Middle Name', + default_value: '', + required: true, + }, + { + component_type: 'text_input', + id: '', + label: 'Last Name', + default_value: '', + required: true, + }, + ], + }, + { + component_type: 'paragraph', + text: '(first) (middle) (last)', + }, + { + component_type: 'fieldset', + legend: 'Name at Conviction: ', + fields: [ + { + component_type: 'text_input', + id: 'Conv Fst Name', + label: 'First Name at Conviction', + default_value: '', + required: true, + }, + { + component_type: 'text_input', + id: 'Conv Mid Name', + label: 'Middle Name at Conviction', + default_value: '', + required: true, + }, + { + component_type: 'text_input', + id: 'Conv Lst Name', + label: 'Last Name at Conviction', + default_value: '', + required: true, + }, + ], + }, + { + component_type: 'paragraph', + text: '(if different) (first) (middle) (last)', + }, + { + component_type: 'fieldset', + legend: 'Address: ', + fields: [ + { + component_type: 'text_input', + id: 'Address', + label: 'Address (number, street, apartment/unit number)', + default_value: '', + required: true, + }, + ], + }, + { + component_type: 'paragraph', + text: '(number) (street) (apartment/unit no.)', + }, + { + component_type: 'fieldset', + legend: 'City', + fields: [ + { + component_type: 'text_input', + id: 'City', + label: 'City', + default_value: '', + required: true, + }, + { + component_type: 'text_input', + id: 'State', + label: 'State', + default_value: '', + required: true, + }, + { + component_type: 'text_input', + id: 'Zip Code', + label: '(Zip Code)', + default_value: '', + required: true, + }, + ], + }, + { + component_type: 'paragraph', + text: '(city) (state) (Zip Code)', + }, + { + component_type: 'fieldset', + legend: 'Email Address: ', + fields: [ + { + component_type: 'text_input', + id: 'Email Address', + label: 'Email Address', + default_value: '', + required: true, + }, + ], + }, + { + component_type: 'fieldset', + legend: 'Phone Number: ', + fields: [ + { + component_type: 'text_input', + id: 'Phone Number', + label: 'Phone Number', + default_value: '', + required: true, + }, + ], + }, + { + component_type: 'paragraph', + text: 'Date of Birth: Gender:', + }, + { + component_type: 'fieldset', + legend: 'Date of Birth', + fields: [ + { + component_type: 'text_input', + id: 'Date of Birth', + label: 'Date of Birth', + default_value: '', + required: true, + }, + { + component_type: 'text_input', + id: 'Gender', + label: 'Gender', + default_value: '', + required: true, + }, + ], + }, + { + component_type: 'paragraph', + text: 'Yes', + }, + { + component_type: 'radio_group', + legend: 'Are you Hispanic or Latino?: ', + options: [ + { + id: 'Ethnicity/Yes', + label: 'Yes ', + name: 'Ethnicity/Yes', + default_checked: false, + }, + { + id: 'Ethnicity/No', + label: 'No ', + name: 'Ethnicity/No', + default_checked: false, + }, + ], + id: 'Ethnicity', + }, + { + component_type: 'paragraph', + text: 'Race:', + }, + { + component_type: 'checkbox', + id: 'Nat Amer', + label: 'Alaska Native or American Indian ', + default_checked: false, + }, + { + component_type: 'checkbox', + id: 'Asian', + label: 'Asian ', + default_checked: false, + }, + { + component_type: 'checkbox', + id: 'Blck Amer', + label: 'Black or African American ', + default_checked: false, + }, + { + component_type: 'checkbox', + id: 'Nat Haw Islander', + label: 'Native Hawaiian or Other Pacific Islander ', + default_checked: false, + }, + { + component_type: 'checkbox', + id: 'White', + label: 'White ', + default_checked: false, + }, + { + component_type: 'checkbox', + id: 'Other', + label: 'Other ', + default_checked: false, + }, + { + component_type: 'radio_group', + legend: 'Citizenship or Residency Status: ', + options: [ + { + id: 'Citizenship/Birth', + label: 'U.S. citizen by birth ', + name: 'Citizenship/Birth', + default_checked: false, + }, + { + id: 'Citizenship/Naturalized', + label: 'U.S. naturalized citizen ', + name: 'Citizenship/Naturalized', + default_checked: false, + }, + { + id: 'Citizenship/Permanent Resident', + label: 'Lawful Permanent Resident ', + name: 'Citizenship/Permanent Resident', + default_checked: false, + }, + ], + id: 'Citizenship', + }, + { + component_type: 'paragraph', + text: 'Date Naturalization Granted:', + }, + { + component_type: 'fieldset', + legend: 'U.S. naturalized citizen ', + fields: [ + { + component_type: 'text_input', + id: 'Residency Date_af_date', + label: 'Date Residency Granted (mm/dd/yyyy)', + default_value: '', + required: true, + }, + { + component_type: 'text_input', + id: '', + label: 'date naturalization granted', + default_value: '', + required: true, + }, + ], + }, + { + component_type: 'paragraph', + text: 'Date Residency Granted: Alien Registration Number (A-Number), Certificate of Naturalization Number, or Citizenship Number', + }, + { + component_type: 'fieldset', + legend: + '(if applicant is a lawful permanent resident or naturalized citizen): ', + fields: [ + { + component_type: 'text_input', + id: 'A-Number', + label: 'Alien Registration, Naturalization, or Citizenship Number', + default_value: '', + required: true, + }, + ], + }, + { + component_type: 'paragraph', + text: '(A-Number) 1.', + }, + { + component_type: 'fieldset', + legend: ' Applicant was convicted on: ', + fields: [ + { + component_type: 'text_input', + id: 'Convict-Date_af_date', + label: 'Convict Date', + default_value: '', + required: true, + }, + ], + }, + { + component_type: 'fieldset', + legend: 'in the U.S. District Court for the ', + fields: [ + { + component_type: 'text_input', + id: 'US District Court', + label: 'US District Court', + default_value: '', + required: true, + }, + ], + }, + { + component_type: 'paragraph', + text: '(month/day/year) (Northern, etc.)', + }, + { + component_type: 'fieldset', + legend: 'District of ', + fields: [ + { + component_type: 'text_input', + id: 'Dist State', + label: 'State', + default_value: '', + required: true, + }, + ], + }, + { + component_type: 'paragraph', + text: '(state) or D.C. Superior Court of simple possession of marijuana, under :', + }, + { + component_type: 'fieldset', + legend: 'Docket No. ', + fields: [ + { + component_type: 'text_input', + id: 'Docket No', + label: 'Docket Number', + default_value: '', + required: true, + }, + ], + }, + { + component_type: 'paragraph', + text: ';', + }, + { + component_type: 'fieldset', + legend: 'and Code Section: ', + fields: [ + { + component_type: 'text_input', + id: 'Code Section', + label: 'Code Section', + default_value: '', + required: true, + }, + ], + }, + { + component_type: 'paragraph', + text: 'OR (docket number) (code section) 2.', + }, + { + component_type: 'fieldset', + legend: ' Applicant was charged with Code Section: ', + fields: [ + { + component_type: 'text_input', + id: 'Code Section_2', + label: 'Code Section', + default_value: '', + required: true, + }, + ], + }, + { + component_type: 'fieldset', + legend: 'in the U.S. District Court for the ', + fields: [ + { + component_type: 'text_input', + id: 'US District Court_2', + label: 'U.S. District Court', + default_value: '', + required: true, + }, + ], + }, + { + component_type: 'paragraph', + text: '(code section) (Eastern, etc.)', + }, + { + component_type: 'fieldset', + legend: 'District of ', + fields: [ + { + component_type: 'text_input', + id: 'District 2', + label: 'State', + default_value: '', + required: true, + }, + ], + }, + { + component_type: 'paragraph', + text: 'or', + }, + { + component_type: 'fieldset', + legend: 'D.C. Superior Court under Docket No: ', + fields: [ + { + component_type: 'text_input', + id: 'Docket No 2', + label: 'Docket No 2', + default_value: '', + required: true, + }, + ], + }, + { + component_type: 'paragraph', + text: '(state) (docket number) United States Department of Justice Office of the Pardon Attorney Page 3 of 4 Washington, D.C. 20530 January 2024', + }, + { + component_type: 'paragraph', + text: 'OMB Control No: 1123-0014 Expires 03/31/2027 APPLICATION FOR CERTIFICATE OF PARDON FOR THE OFFENSES OF SIMPLE POSSESSION, ATTEMPTED SIMPLE POSSESSION, OR USE OF MARIJUANA With knowledge of the penalties for false statements to Federal Agencies, as provided by 18 U.S.C. \u00a7 1001, and with knowledge that this statement is submitted by me to affect action by the U.S. Department of Justice, I certify that: 1. The applicant was either a U.S. citizen or lawfully present in the United States at the time of the offense. 2. The applicant was a U.S. citizen or lawful permanent resident on December 22, 2023. 3. The above statements, and accompanying documents, are true and complete to the best of my knowledge, information, and belief. 4. I acknowledge that any certificate issued in reliance on the above information will be voided, if the information is subsequently determined to be false.', + }, + { + component_type: 'fieldset', + legend: 'App Date', + fields: [ + { + component_type: 'text_input', + id: 'App Date', + label: 'Date', + default_value: '', + required: true, + }, + ], + }, + { + component_type: 'paragraph', + text: '(date) (signature) Page 4 of 4 United States Department of Justice Office of the Pardon Attorney Washington, D.C. 20530 January 2024', + }, + ], + raw_fields: [ + { + type: '/Tx', + var_name: 'Fst Name 1', + field_dict: { + field_type: '/Tx', + coordinates: [97.0, 636.960022, 233.279999, 659.640015], + field_label: 'Fst Name 1', + field_instructions: 'First Name', + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Fst Name 1', + }, + { + type: '/Tx', + var_name: '', + field_dict: { + coordinates: [233.087006, 637.580994, 390.214996, 659.320007], + field_instructions: 'Middle Name', + name: 0, + field_type: '/Tx', + font_info: '', + field_label: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Mid Name 1/0', + }, + { + type: '/Tx', + var_name: '', + field_dict: { + coordinates: [390.996002, 637.492981, 548.124023, 659.231995], + field_instructions: 'Last Name', + name: 0, + field_type: '/Tx', + font_info: '', + field_label: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Lst Name 1/0', + }, + { + type: '/Tx', + var_name: 'Conv Fst Name', + field_dict: { + field_type: '/Tx', + coordinates: [153.740005, 598.085022, 283.246002, 620.765015], + field_label: 'Conv Fst Name', + field_instructions: 'First Name at Conviction', + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Conv Fst Name', + }, + { + type: '/Tx', + var_name: 'Conv Mid Name', + field_dict: { + field_type: '/Tx', + coordinates: [282.497986, 598.164001, 410.80899, 620.843994], + field_label: 'Conv Mid Name', + field_instructions: 'Middle Name at Conviction', + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Conv Mid Name', + }, + { + type: '/Tx', + var_name: 'Conv Lst Name', + field_dict: { + field_type: '/Tx', + coordinates: [410.212006, 597.677002, 536.132019, 620.357971], + field_label: 'Conv Lst Name', + field_instructions: 'Last Name at Conviction', + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Conv Lst Name', + }, + { + type: '/Tx', + var_name: 'Address', + field_dict: { + field_type: '/Tx', + coordinates: [102.839996, 563.880005, 547.080017, 586.559998], + field_label: 'Address', + field_instructions: 'Address (number, street, apartment/unit number)', + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Address', + }, + { + type: '/Tx', + var_name: 'City', + field_dict: { + field_type: '/Tx', + coordinates: [64.500504, 531.0, 269.519989, 551.880005], + field_label: 'City', + field_instructions: 'City', + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'City', + }, + { + type: '/Tx', + var_name: 'State', + field_dict: { + field_type: '/Tx', + coordinates: [273.959991, 531.0, 440.519989, 551.880005], + field_label: 'State', + field_instructions: 'State', + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'State', + }, + { + type: '/Tx', + var_name: 'Zip Code', + field_dict: { + field_type: '/Tx', + coordinates: [444.959991, 531.0, 552.719971, 551.880005], + field_label: 'Zip Code', + field_instructions: '(Zip Code)', + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Zip Code', + }, + { + type: '/Tx', + var_name: 'Email Address', + field_dict: { + field_type: '/Tx', + coordinates: [131.863998, 489.600006, 290.743988, 512.280029], + field_label: 'Email Address', + field_instructions: 'Email Address', + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Email Address', + }, + { + type: '/Tx', + var_name: 'Phone Number', + field_dict: { + field_type: '/Tx', + coordinates: [385.679993, 489.600006, 549.599976, 512.280029], + field_label: 'Phone Number', + field_instructions: 'Phone Number', + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Phone Number', + }, + { + type: '/Tx', + var_name: 'Date of Birth', + field_dict: { + field_type: '/Tx', + coordinates: [126.480003, 451.679993, 197.880005, 474.359985], + field_label: 'Date of Birth', + field_instructions: 'Date of Birth', + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Date of Birth', + }, + { + type: '/Tx', + var_name: 'Gender', + field_dict: { + field_type: '/Tx', + coordinates: [241.559998, 451.679993, 313.079987, 474.359985], + field_label: 'Gender', + field_instructions: 'Gender', + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Gender', + }, + { + type: '/Btn', + var_name: '', + field_dict: { + coordinates: [505.618988, 450.865997, 523.619019, 468.865997], + name: 'Yes', + field_type: '/Btn', + field_instructions: '', + font_info: '', + field_label: '', + flags: { + '15': 'NoToggleToOff: (Radio buttons only) If set, exactly one radio button must be selected at all times; clicking the currently selected button has no effect. If clear, clicking the selected button deselects it, leaving no button selected.', + '16': 'Radio: If set, the field is a set of radio buttons; if clear, the field is a check box. This flag is meaningful only if the Pushbutton flag is clear.', + }, + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Ethnicity/Yes', + }, + { + type: '/Btn', + var_name: '', + field_dict: { + coordinates: [558.213013, 450.865997, 576.213013, 468.865997], + name: 'No', + field_type: '/Btn', + field_instructions: '', + font_info: '', + field_label: '', + flags: { + '15': 'NoToggleToOff: (Radio buttons only) If set, exactly one radio button must be selected at all times; clicking the currently selected button has no effect. If clear, clicking the selected button deselects it, leaving no button selected.', + '16': 'Radio: If set, the field is a set of radio buttons; if clear, the field is a check box. This flag is meaningful only if the Pushbutton flag is clear.', + }, + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Ethnicity/No', + }, + { + type: '/Btn', + var_name: 'Nat Amer', + field_dict: { + field_type: '/Btn', + coordinates: [280.10199, 426.162994, 298.10199, 444.162994], + field_label: 'Nat Amer', + field_instructions: 'Alaska Native or American Indian', + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Nat Amer', + }, + { + type: '/Btn', + var_name: 'Asian', + field_dict: { + field_type: '/Btn', + coordinates: [366.563995, 426.162994, 384.563995, 444.162994], + field_label: 'Asian', + field_instructions: 'Asian', + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Asian', + }, + { + type: '/Btn', + var_name: 'Blck Amer', + field_dict: { + field_type: '/Btn', + coordinates: [531.517029, 426.162994, 549.517029, 444.162994], + field_label: 'Blck Amer', + field_instructions: 'Black or African American', + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Blck Amer', + }, + { + type: '/Btn', + var_name: 'Nat Haw Islander', + field_dict: { + field_type: '/Btn', + coordinates: [309.587006, 401.061005, 327.587006, 419.061005], + field_label: 'Nat Haw Islander', + field_instructions: 'Native Hawaiian or Other Pacific Islander', + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Nat Haw Islander', + }, + { + type: '/Btn', + var_name: 'White', + field_dict: { + field_type: '/Btn', + coordinates: [438.681, 401.061005, 456.681, 419.061005], + field_label: 'White', + field_instructions: 'White', + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'White', + }, + { + type: '/Btn', + var_name: 'Other', + field_dict: { + field_type: '/Btn', + coordinates: [508.806, 401.061005, 526.80603, 419.061005], + field_label: 'Other', + field_instructions: 'Other', + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Other', + }, + { + type: '/Btn', + var_name: '', + field_dict: { + coordinates: [98.414398, 349.662994, 116.414001, 367.662994], + field_instructions: 'U S Citizen by birth', + name: 'Birth', + field_type: '/Btn', + font_info: '', + field_label: '', + flags: { + '15': 'NoToggleToOff: (Radio buttons only) If set, exactly one radio button must be selected at all times; clicking the currently selected button has no effect. If clear, clicking the selected button deselects it, leaving no button selected.', + '16': 'Radio: If set, the field is a set of radio buttons; if clear, the field is a check box. This flag is meaningful only if the Pushbutton flag is clear.', + }, + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Citizenship/Birth', + }, + { + type: '/Btn', + var_name: '', + field_dict: { + coordinates: [98.414398, 331.733002, 116.414001, 349.733002], + field_instructions: 'U S naturalized citizen', + name: 'Naturalized', + field_type: '/Btn', + font_info: '', + field_label: '', + flags: { + '15': 'NoToggleToOff: (Radio buttons only) If set, exactly one radio button must be selected at all times; clicking the currently selected button has no effect. If clear, clicking the selected button deselects it, leaving no button selected.', + '16': 'Radio: If set, the field is a set of radio buttons; if clear, the field is a check box. This flag is meaningful only if the Pushbutton flag is clear.', + }, + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Citizenship/Naturalized', + }, + { + type: '/Btn', + var_name: '', + field_dict: { + coordinates: [98.414398, 313.006012, 116.414001, 331.006012], + field_instructions: 'Lawful Permenent Resident', + name: 'Permanent Resident', + field_type: '/Btn', + font_info: '', + field_label: '', + flags: { + '15': 'NoToggleToOff: (Radio buttons only) If set, exactly one radio button must be selected at all times; clicking the currently selected button has no effect. If clear, clicking the selected button deselects it, leaving no button selected.', + '16': 'Radio: If set, the field is a set of radio buttons; if clear, the field is a check box. This flag is meaningful only if the Pushbutton flag is clear.', + }, + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Citizenship/Permanent Resident', + }, + { + type: '/Tx', + var_name: '', + field_dict: { + coordinates: [432.306, 331.979004, 489.425995, 352.92099], + field_instructions: 'date naturalization granted', + name: 0, + field_type: '/Tx', + font_info: '', + field_label: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Naturalization Date_af_date/0', + }, + { + type: '/Tx', + var_name: 'Residency Date_af_date', + field_dict: { + field_type: '/Tx', + coordinates: [414.304993, 329.523987, 471.424988, 308.582001], + field_label: 'Residency Date_af_date', + field_instructions: 'Date Residency Granted (mm/dd/yyyy)', + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Residency Date_af_date', + }, + { + type: '/Tx', + var_name: 'A-Number', + field_dict: { + field_type: '/Tx', + coordinates: [296.279999, 257.76001, 507.959991, 280.440002], + field_label: 'A-Number', + field_instructions: + 'Alien Registration, Naturalization, or Citizenship Number', + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'A-Number', + }, + { + type: '/Tx', + var_name: 'Convict-Date_af_date', + field_dict: { + field_type: '/Tx', + coordinates: [203.602005, 218.822006, 301.363007, 245.341995], + field_label: 'Convict-Date_af_date', + field_instructions: 'Convict Date', + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Convict-Date_af_date', + }, + { + type: '/Tx', + var_name: 'US District Court', + field_dict: { + field_type: '/Tx', + coordinates: [451.200012, 219.0, 522.719971, 241.679993], + field_label: 'US District Court', + field_instructions: 'US District Court', + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'US District Court', + }, + { + type: '/Tx', + var_name: 'Dist State', + field_dict: { + field_type: '/Tx', + coordinates: [105.720001, 187.919998, 177.240005, 210.600006], + field_label: 'Dist State', + field_instructions: 'State', + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Dist State', + }, + { + type: '/Tx', + var_name: 'Docket No', + field_dict: { + field_type: '/Tx', + coordinates: [114.015999, 153.479996, 262.575989, 176.160004], + field_label: 'Docket No', + field_instructions: 'Docket Number', + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Docket No', + }, + { + type: '/Tx', + var_name: 'Code Section', + field_dict: { + field_type: '/Tx', + coordinates: [349.320007, 153.479996, 448.320007, 176.160004], + field_label: 'Code Section', + field_instructions: 'Code Section', + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Code Section', + }, + { + type: '/Tx', + var_name: 'Code Section_2', + field_dict: { + field_type: '/Tx', + coordinates: [266.640015, 121.440002, 316.200012, 144.119995], + field_label: 'Code Section_2', + field_instructions: 'Code Section', + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Code Section_2', + }, + { + type: '/Tx', + var_name: 'US District Court_2', + field_dict: { + field_type: '/Tx', + coordinates: [464.040009, 121.32, 542.039978, 144.0], + field_label: 'US District Court_2', + field_instructions: 'U.S. District Court', + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'US District Court_2', + }, + { + type: '/Tx', + var_name: 'District 2', + field_dict: { + field_type: '/Tx', + coordinates: [105.720001, 86.760002, 188.160004, 109.440002], + field_label: 'District 2', + field_instructions: 'State', + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'District 2', + }, + { + type: '/Tx', + var_name: 'Docket No 2', + field_dict: { + field_type: '/Tx', + coordinates: [403.920013, 86.760002, 525.0, 109.440002], + field_label: 'Docket No 2', + field_instructions: 'Docket No 2', + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Docket No 2', + }, + { + type: '/Tx', + var_name: 'App Date', + field_dict: { + field_type: '/Tx', + coordinates: [75.120003, 396.720001, 219.479996, 425.519989], + field_label: 'App Date', + field_instructions: 'Date', + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 3, + path: 'App Date', + }, + ], + grouped_items: [], + }, + cache_id: 'Cache ID is not implemented yet', +}; diff --git a/packages/forms/src/documents/pdf/parsing-api.ts b/packages/forms/src/documents/pdf/parsing-api.ts index 4d0e4f13..3bf3ee66 100644 --- a/packages/forms/src/documents/pdf/parsing-api.ts +++ b/packages/forms/src/documents/pdf/parsing-api.ts @@ -1,6 +1,14 @@ import * as z from 'zod'; -import { generatePatternId, type PatternId, type PatternMap } from '../..'; +import { + type FormConfig, + type FormErrors, + type Pattern, + type PatternId, + type PatternMap, + createPattern, + defaultFormConfig, +} from '../..'; import { type FieldsetPattern } from '../../patterns/fieldset'; import { type InputPattern } from '../../patterns/input'; @@ -125,19 +133,28 @@ type ExtractedObject = z.infer; export type ParsedPdf = { patterns: PatternMap; + errors: { + type: Pattern['type']; + data: Pattern['data']; + errors: FormErrors; + }[]; outputs: DocumentFieldMap; // to populate FormOutput root: PatternId; title: string; description: string; }; -export const callExternalParser = async ( +export type FetchPdfApiResponse = ( rawData: Uint8Array, - endpointUrl: string = 'https://10x-atj-doc-automation-staging.app.cloud.gov/api/v1/parse' -): Promise => { - const base64 = await uint8ArrayToBase64(rawData); + url?: string +) => Promise; - const response = await fetch(endpointUrl, { +export const fetchPdfApiResponse: FetchPdfApiResponse = async ( + rawData: Uint8Array, + url: string = 'https://10x-atj-doc-automation-staging.app.cloud.gov/api/v1/parse' +) => { + const base64 = await uint8ArrayToBase64(rawData); + const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -146,18 +163,18 @@ export const callExternalParser = async ( pdf: base64, }), }); - if (!response.ok) { throw new Error('Network response was not ok'); } + return await response.json(); +}; - const json = await response.json(); +export const processApiResponse = async (json: any): Promise => { const extracted: ExtractedObject = ExtractedObject.parse(json.parsed_pdf); - const rootSequence: PatternId[] = []; - const parsedPdf: ParsedPdf = { patterns: {}, + errors: [], outputs: {}, root: 'root', title: extracted.form_summary.title || 'Default Form Title', @@ -165,54 +182,61 @@ export const callExternalParser = async ( extracted.form_summary.description || 'Default Form Description', }; - const formSummaryId = generatePatternId(); - - parsedPdf.patterns[formSummaryId] = { - type: 'form-summary', - id: formSummaryId, - data: { + const summary = processPatternData( + defaultFormConfig, + parsedPdf, + 'form-summary', + { title: extracted.form_summary.title || 'Default Form Title', description: extracted.form_summary.description || 'Default Form Description', - }, - } satisfies FormSummary; - rootSequence.push(formSummaryId); + } + ); + if (summary) { + rootSequence.push(summary.id); + } for (const element of extracted.elements) { - const randomId = generatePatternId(); const fieldsetPatterns: PatternId[] = []; // Add paragraph elements if (element.component_type === 'paragraph') { - parsedPdf.patterns[randomId] = { - type: 'paragraph', - id: randomId, - data: { + const paragraph = processPatternData( + defaultFormConfig, + parsedPdf, + 'paragraph', + { text: element.text, - }, - } satisfies ParagraphPattern; - rootSequence.push(randomId); + } + ); + if (paragraph) { + rootSequence.push(paragraph.id); + } continue; } if (element.component_type === 'checkbox') { - parsedPdf.patterns[element.id] = { - type: 'checkbox', - id: element.id, - data: { + const checkbox = processPatternData( + defaultFormConfig, + parsedPdf, + 'checkbox', + { label: element.label, defaultChecked: element.default_checked, - }, - } satisfies CheckboxPattern; - rootSequence.push(element.id); + } + ); + if (checkbox) { + rootSequence.push(checkbox.id); + } continue; } if (element.component_type === 'radio_group') { - parsedPdf.patterns[element.id] = { - type: 'radio-group', - id: element.id, - data: { + const radioGroup = processPatternData( + defaultFormConfig, + parsedPdf, + 'radio-group', + { label: element.legend, options: element.options.map(option => ({ id: option.id, @@ -220,38 +244,39 @@ export const callExternalParser = async ( name: option.name, defaultChecked: option.default_checked, })), - }, - } satisfies RadioGroupPattern; - rootSequence.push(element.id); + } + ); + if (radioGroup) { + rootSequence.push(radioGroup.id); + } continue; } if (element.component_type === 'fieldset') { for (const input of element.fields) { if (input.component_type === 'text_input') { - // const id = stringToBase64(input.id); - - parsedPdf.patterns[input.id] = { - type: 'input', - id: input.id, - data: { + const inputPattern = processPatternData( + defaultFormConfig, + parsedPdf, + 'input', + { label: input.label, required: false, initial: '', maxLength: 128, - }, - } satisfies InputPattern; - - fieldsetPatterns.push(input.id); - - parsedPdf.outputs[input.id] = { - type: 'TextField', - name: input.id, - label: input.label, - value: '', - maxLength: 1024, - required: input.required, - }; + } + ); + if (inputPattern) { + fieldsetPatterns.push(inputPattern.id); + parsedPdf.outputs[inputPattern.id] = { + type: 'TextField', + name: input.id, + label: input.label, + value: '', + maxLength: 1024, + required: input.required, + }; + } } // TODO: Look for checkbox or other element types } @@ -259,37 +284,70 @@ export const callExternalParser = async ( // Add fieldset to parsedPdf.patterns and rootSequence if (element.component_type === 'fieldset' && fieldsetPatterns.length > 0) { - parsedPdf.patterns[randomId] = { - id: randomId, - type: 'fieldset', - data: { + const fieldset = processPatternData( + defaultFormConfig, + parsedPdf, + 'fieldset', + { legend: element.legend, patterns: fieldsetPatterns, - }, - } satisfies FieldsetPattern; - rootSequence.push(randomId); + } + ); + if (fieldset) { + rootSequence.push(fieldset.id); + } } } // Create a pattern for the single, first page. - const pagePattern = { - id: 'single-page-sequence', - type: 'page', - data: { + const pagePattern = processPatternData( + defaultFormConfig, + parsedPdf, + 'page', + { title: 'Untitled Page', patterns: rootSequence, - }, - } satisfies PagePattern; - parsedPdf.patterns[pagePattern.id] = pagePattern; + } + ); + + const pages: PatternId[] = []; + if (pagePattern) { + parsedPdf.patterns[pagePattern.id] = pagePattern; + pages.push(pagePattern.id); + } // Assign the page to the root page set. - parsedPdf.patterns['root'] = { - id: 'root', - type: 'page-set', - data: { - pages: [pagePattern.id], + const rootPattern = processPatternData( + defaultFormConfig, + parsedPdf, + 'page-set', + { + pages, }, - } satisfies PageSetPattern; - + 'root' + ); + if (rootPattern) { + parsedPdf.patterns['root'] = rootPattern; + } return parsedPdf; }; + +const processPatternData = ( + config: FormConfig, + parsedPdf: ParsedPdf, + patternType: T['type'], + patternData: T['data'], + patternId?: PatternId +) => { + const result = createPattern(config, patternType, patternData, patternId); + if (!result.success) { + parsedPdf.errors.push({ + type: patternType, + data: patternData, + errors: result.error, + }); + return; + } + parsedPdf.patterns[result.data.id] = result.data; + return result.data; +}; diff --git a/packages/forms/src/documents/suggestions.ts b/packages/forms/src/documents/suggestions.ts deleted file mode 100644 index 8a80d6f0..00000000 --- a/packages/forms/src/documents/suggestions.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { callExternalParser, type ParsedPdf } from './pdf/parsing-api'; - -export type SuggestedForm = { - id: string; - tag: 'input' | 'textarea'; - name: string; - label: string; - value?: string; - type?: 'text'; -}[]; - -export const getSuggestedPatterns = async ( - rawData: Uint8Array -): Promise => { - return await callExternalParser(rawData); -}; diff --git a/packages/forms/src/pattern.ts b/packages/forms/src/pattern.ts index b305ee27..5fd1b1cf 100644 --- a/packages/forms/src/pattern.ts +++ b/packages/forms/src/pattern.ts @@ -1,4 +1,4 @@ -import { type Result } from '@atj/common'; +import * as r from '@atj/common'; import { type FormErrors, type Blueprint, updatePattern, FormError } from '..'; import { type CreatePrompt } from './components'; @@ -18,11 +18,11 @@ export type GetPattern = (form: Blueprint, id: PatternId) => Pattern; export type ParseUserInput = ( pattern: Pattern, obj: unknown -) => Result; +) => r.Result; export type ParsePatternConfigData = ( patternData: unknown -) => Result; +) => r.Result; type RemoveChildPattern

= ( pattern: P, @@ -72,7 +72,7 @@ export const validatePattern = ( patternConfig: PatternConfig, pattern: Pattern, value: any -): Result => { +): r.Result => { if (!patternConfig.parseUserInput) { return { success: true, @@ -113,7 +113,7 @@ export const updatePatternFromFormData = ( form: Blueprint, pattern: Pattern, formData: PatternMap -): Result => { +): r.Result => { const elementConfig = getPatternConfig(config, pattern.type); const result = elementConfig.parseConfigData(formData[pattern.id]); if (!result.success) { @@ -131,7 +131,7 @@ export const updatePatternFromFormData = ( export const generatePatternId = () => crypto.randomUUID(); -export const createPattern = ( +export const createDefaultPattern = ( config: FormConfig, patternType: string ): Pattern => { @@ -142,6 +142,25 @@ export const createPattern = ( }; }; +export const createPattern = ( + config: FormConfig, + patternType: keyof FormConfig['patterns'], + configData: T['data'], + patternId?: PatternId +): r.Result => { + const result = config.patterns[patternType].parseConfigData( + configData || config.patterns[patternType].initial + ); + if (!result.success) { + return r.failure(result.error); + } + return r.success({ + id: patternId || generatePatternId(), + type: patternType, + data: result.data, + } as T); +}; + export const removeChildPattern = ( config: FormConfig, pattern: Pattern, diff --git a/packages/forms/src/patterns/index.ts b/packages/forms/src/patterns/index.ts index f6829327..60accbbc 100644 --- a/packages/forms/src/patterns/index.ts +++ b/packages/forms/src/patterns/index.ts @@ -27,4 +27,4 @@ export const defaultFormConfig: FormConfig = { 'radio-group': radioGroupConfig, sequence: sequenceConfig, }, -}; +} as const; diff --git a/packages/forms/src/patterns/radio-group.ts b/packages/forms/src/patterns/radio-group.ts index 64b554d1..966800e4 100644 --- a/packages/forms/src/patterns/radio-group.ts +++ b/packages/forms/src/patterns/radio-group.ts @@ -12,7 +12,7 @@ const configSchema = z.object({ label: z.string().min(1), options: z .object({ - id: z.string().regex(/^[^\s]+$/, 'Invalid option ID'), + id: z.string().regex(/^[^\s]+$/, 'Option ID may not contain spaces'), label: z.string().min(1), }) .array(), @@ -37,9 +37,6 @@ export const radioGroupConfig: PatternConfig = }, parseConfigData: obj => { const result = safeZodParseFormErrors(configSchema, obj); - if (!result.success) { - console.error(result.error); - } return result; }, getChildren() {