diff --git a/apps/spotlight/src/features/form-page/components/AppFormPage.tsx b/apps/spotlight/src/features/form-page/components/AppFormPage.tsx index 107afd65..499a9092 100644 --- a/apps/spotlight/src/features/form-page/components/AppFormPage.tsx +++ b/apps/spotlight/src/features/form-page/components/AppFormPage.tsx @@ -61,7 +61,9 @@ const AppFormRoute = () => { uswdsRoot: ctx.uswdsRoot, }} session={formSessionResponse.formSession} - onSubmit={data => actions.onSubmitForm({ formId: id, data })} + onSubmit={data => { + actions.onSubmitForm({ formId: id, data }); + }} /> )} diff --git a/packages/common/src/locales/en/app.ts b/packages/common/src/locales/en/app.ts index d8b17262..25dd6ccb 100644 --- a/packages/common/src/locales/en/app.ts +++ b/packages/common/src/locales/en/app.ts @@ -22,6 +22,11 @@ export const en = { displayName: 'Short answer', maxLength: 'Maximum length', }, + packageDownload: { + ...defaults, + displayName: 'Package download', + fieldLabel: 'Package download label', + }, page: { fieldLabel: 'Page title', }, diff --git a/packages/database/src/clients/kysely/types.ts b/packages/database/src/clients/kysely/types.ts index 3a95fb04..80c04b27 100644 --- a/packages/database/src/clients/kysely/types.ts +++ b/packages/database/src/clients/kysely/types.ts @@ -54,6 +54,8 @@ interface FormSessionsTable { id: string; form_id: string; data: string; + created_at: Generated; + updated_at: Generated; } export type FormSessionsTableSelectable = Selectable; export type FormSessionsTableInsertable = Insertable; diff --git a/packages/design/src/Form/components/PackageDownload/index.tsx b/packages/design/src/Form/components/PackageDownload/index.tsx new file mode 100644 index 00000000..c1f5d5d9 --- /dev/null +++ b/packages/design/src/Form/components/PackageDownload/index.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +import { type PackageDownloadProps } from '@atj/forms'; + +import { type PatternComponent } from '../../../Form/index.js'; +import ActionBar from '../../../Form/ActionBar/index.js'; + +const PackageDownload: PatternComponent = props => { + return ( + <> +

{props.text}

+ + + ); +}; +export default PackageDownload; diff --git a/packages/design/src/Form/components/index.tsx b/packages/design/src/Form/components/index.tsx index ede184b1..08e4e433 100644 --- a/packages/design/src/Form/components/index.tsx +++ b/packages/design/src/Form/components/index.tsx @@ -4,6 +4,7 @@ import Address from './Address/index.js'; import Checkbox from './Checkbox/index.js'; import Fieldset from './Fieldset/index.js'; import FormSummary from './FormSummary/index.js'; +import PackageDownload from './PackageDownload/index.js'; import Page from './Page/index.js'; import PageSet from './PageSet/index.js'; import Paragraph from './Paragraph/index.js'; @@ -19,6 +20,7 @@ export const defaultPatternComponents: ComponentForPattern = { fieldset: Fieldset as PatternComponent, 'form-summary': FormSummary as PatternComponent, input: TextInput as PatternComponent, + 'package-download': PackageDownload as PatternComponent, page: Page as PatternComponent, 'page-set': PageSet as PatternComponent, paragraph: Paragraph as PatternComponent, diff --git a/packages/design/src/Form/index.tsx b/packages/design/src/Form/index.tsx index 1ccd98fe..683bb700 100644 --- a/packages/design/src/Form/index.tsx +++ b/packages/design/src/Form/index.tsx @@ -106,10 +106,22 @@ export default function Form({ className="usa-form margin-bottom-3 maxw-full" onSubmit={ onSubmit - ? formMethods.handleSubmit(async data => { + ? formMethods.handleSubmit(async (data, event) => { + const submitEvent = event?.nativeEvent as + | SubmitEvent + | undefined; + if (submitEvent === undefined) { + console.error( + "Can't handle submission without event" + ); + return; + } + const action = ( + submitEvent.submitter as HTMLButtonElement + )?.value; updatePrompt(data); console.log('Submitting form...'); - onSubmit(data); + onSubmit({ ...data, action }); }) : undefined } diff --git a/packages/design/src/FormManager/FormEdit/AddPatternDropdown.tsx b/packages/design/src/FormManager/FormEdit/AddPatternDropdown.tsx index 03bdfa56..2abeb996 100644 --- a/packages/design/src/FormManager/FormEdit/AddPatternDropdown.tsx +++ b/packages/design/src/FormManager/FormEdit/AddPatternDropdown.tsx @@ -94,6 +94,7 @@ const sidebarPatterns: DropdownPattern[] = [ ['paragraph', defaultFormConfig.patterns['paragraph']], ['rich-text', defaultFormConfig.patterns['rich-text']], ['radio-group', defaultFormConfig.patterns['radio-group']], + ['package-download', defaultFormConfig.patterns['package-download']], ] as const; export const fieldsetPatterns: DropdownPattern[] = [ ['form-summary', defaultFormConfig.patterns['form-summary']], @@ -102,6 +103,7 @@ export const fieldsetPatterns: DropdownPattern[] = [ ['paragraph', defaultFormConfig.patterns['paragraph']], ['rich-text', defaultFormConfig.patterns['rich-text']], ['radio-group', defaultFormConfig.patterns['radio-group']], + ['package-download', defaultFormConfig.patterns['package-download']], ] as const; export const SidebarAddPatternMenuItem = ({ diff --git a/packages/design/src/FormManager/FormEdit/components/PackageDownloadPatternEdit.tsx b/packages/design/src/FormManager/FormEdit/components/PackageDownloadPatternEdit.tsx new file mode 100644 index 00000000..8f262f16 --- /dev/null +++ b/packages/design/src/FormManager/FormEdit/components/PackageDownloadPatternEdit.tsx @@ -0,0 +1,77 @@ +import classnames from 'classnames'; +import React from 'react'; + +import { enLocale as message } from '@atj/common'; +import { + type PackageDownloadPattern, + type PackageDownloadProps, + type PatternId, +} from '@atj/forms'; + +import PackageDownload from '../../../Form/components/PackageDownload/index.js'; +import { PatternEditComponent } from '../types.js'; + +import { PatternEditActions } from './common/PatternEditActions.js'; +import { PatternEditForm } from './common/PatternEditForm.js'; +import { usePatternEditFormContext } from './common/hooks.js'; +import { useFormManagerStore } from '../../store.js'; + +const PackageDownloadPatternEdit: PatternEditComponent< + PackageDownloadProps +> = ({ focus, previewProps }) => { + return ( + <> + {focus ? ( + } + > + ) : ( +
+ +
+ )} + + ); +}; + +const EditComponent = ({ patternId }: { patternId: PatternId }) => { + const pattern = useFormManagerStore( + state => state.session.form.patterns[patternId] + ); + const { fieldId, getFieldState, register } = + usePatternEditFormContext(patternId); + const text = getFieldState('text'); + + return ( +
+
+ + {text.error ? ( + + {text.error.message} + + ) : null} + +
+ +
+ ); +}; + +export default PackageDownloadPatternEdit; diff --git a/packages/design/src/FormManager/FormEdit/components/index.ts b/packages/design/src/FormManager/FormEdit/components/index.ts index 2db6176c..2e8fa369 100644 --- a/packages/design/src/FormManager/FormEdit/components/index.ts +++ b/packages/design/src/FormManager/FormEdit/components/index.ts @@ -7,6 +7,7 @@ import CheckboxPatternEdit from './CheckboxPatternEdit.js'; import FieldsetEdit from './FieldsetEdit.js'; import FormSummaryEdit from './FormSummaryEdit.js'; import InputPatternEdit from './InputPatternEdit.js'; +import PackageDownloadPatternEdit from './PackageDownloadPatternEdit.js'; import { PageEdit } from './PageEdit.js'; import PageSetEdit from './PageSetEdit.js'; import ParagraphPatternEdit from './ParagraphPatternEdit.js'; @@ -21,6 +22,7 @@ export const defaultPatternEditComponents: EditComponentForPattern = { input: InputPatternEdit as PatternEditComponent, 'form-summary': FormSummaryEdit as PatternEditComponent, fieldset: FieldsetEdit as PatternEditComponent, + 'package-download': PackageDownloadPatternEdit as PatternEditComponent, page: PageEdit as PatternEditComponent, 'page-set': PageSetEdit as PatternEditComponent, 'radio-group': RadioGroupPatternEdit as PatternEditComponent, diff --git a/packages/forms/README.md b/packages/forms/README.md index e69de29b..7173bc31 100644 --- a/packages/forms/README.md +++ b/packages/forms/README.md @@ -0,0 +1,9 @@ +# @atj/forms + +This library includes all of the core business logic of the Forms Platform. + +- [./src/services](./src/services): The public interface of the Forms Platform is implemented here +- [./src/patterns](./src/patterns/README.md): Form building blocks, aka "patterns" +- [./src/repository](./src/repository): Database routines +- [./src/documents](./src/documents): Document ingest and creation +- [./src/context](./src/context): Runtime contexts for the platform (testing, in-browser, server-side) are defined here diff --git a/packages/forms/src/components.ts b/packages/forms/src/components.ts index c9dfe3de..307f41a7 100644 --- a/packages/forms/src/components.ts +++ b/packages/forms/src/components.ts @@ -6,6 +6,13 @@ import { getPatternConfig, } from './pattern.js'; import { type FormSession, nullSession, sessionIsComplete } from './session.js'; +import { type ActionName } from './submission.js'; + +export type PackageDownloadProps = PatternProps<{ + type: 'package-download'; + text: string; + actions: PromptAction[]; +}>; export type TextInputProps = PatternProps<{ type: 'input'; @@ -92,7 +99,7 @@ export type PatternProps = { export type SubmitAction = { type: 'submit'; - submitAction: 'next' | 'submit'; + submitAction: 'next' | 'submit' | ActionName; text: string; }; export type LinkAction = { diff --git a/packages/forms/src/documents/__tests__/test-documents.ts b/packages/forms/src/documents/__tests__/test-documents.ts new file mode 100644 index 00000000..ae7dc88a --- /dev/null +++ b/packages/forms/src/documents/__tests__/test-documents.ts @@ -0,0 +1,1351 @@ +import { BlueprintBuilder } from '../../builder'; +import { type PatternValueMap } from '../../pattern'; +import { defaultFormConfig } from '../../patterns'; +import { addDocument } from '../document'; +import { type Blueprint } from '../..'; +import { loadSamplePDF } from './sample-data'; + +export const createTestFormWithPDF = async () => { + const pdfBytes = await loadSamplePDF( + 'doj-pardon-marijuana/application_for_certificate_of_pardon_for_simple_marijuana_possession.pdf' + ); + const builder = new BlueprintBuilder(defaultFormConfig); + const { updatedForm } = await addDocument( + builder.form, + { + name: 'test.pdf', + data: new Uint8Array(pdfBytes), + }, + { + fetchPdfApiResponse: async () => SERVICE_RESPONSE, + } + ); + + return updatedForm; +}; + +export const getMockFormData = (form: Blueprint): PatternValueMap => { + return Object.keys(form.patterns).reduce((acc, key) => { + if (form.patterns[key].type === 'checkbox') { + acc[key] = true; + } else { + acc[key] = 'test value'; + } + return acc; + }, {} as PatternValueMap); +}; + +const SERVICE_RESPONSE = { + 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: 'My Form Title', + description: 'My Form 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", + style: 'normal', + page: 0, + }, + { + 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", + style: 'normal', + page: 1, + }, + { + 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:', + style: 'normal', + page: 2, + }, + { + component_type: 'fieldset', + legend: 'Name: ', + fields: [ + { + component_type: 'text_input', + id: 'Fst Name 1', + label: 'First Name', + default_value: '', + required: true, + page: 2, + }, + { + component_type: 'text_input', + id: '', + label: 'Middle Name', + default_value: '', + required: true, + page: 2, + }, + { + component_type: 'text_input', + id: '', + label: 'Last Name', + default_value: '', + required: true, + page: 2, + }, + ], + page: 2, + }, + { + component_type: 'paragraph', + text: '(first) (middle) (last)', + style: 'normal', + page: 2, + }, + { + 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, + page: 2, + }, + { + component_type: 'text_input', + id: 'Conv Mid Name', + label: 'Middle Name at Conviction', + default_value: '', + required: true, + page: 2, + }, + { + component_type: 'text_input', + id: 'Conv Lst Name', + label: 'Last Name at Conviction', + default_value: '', + required: true, + page: 2, + }, + ], + page: 2, + }, + { + component_type: 'paragraph', + text: '(if different) (first) (middle) (last)', + style: 'normal', + page: 2, + }, + { + component_type: 'fieldset', + legend: 'Address: ', + fields: [ + { + component_type: 'text_input', + id: 'Address', + label: 'Address (number, street, apartment/unit number)', + default_value: '', + required: true, + page: 2, + }, + ], + page: 2, + }, + { + component_type: 'paragraph', + text: '(number) (street) (apartment/unit no.)', + style: 'normal', + page: 2, + }, + { + component_type: 'fieldset', + legend: 'City', + fields: [ + { + component_type: 'text_input', + id: 'City', + label: 'City', + default_value: '', + required: true, + page: 2, + }, + { + component_type: 'text_input', + id: 'State', + label: 'State', + default_value: '', + required: true, + page: 2, + }, + { + component_type: 'text_input', + id: 'Zip Code', + label: '(Zip Code)', + default_value: '', + required: true, + page: 2, + }, + ], + page: 2, + }, + { + component_type: 'paragraph', + text: '(city) (state) (Zip Code)', + style: 'normal', + page: 2, + }, + { + component_type: 'fieldset', + legend: 'Email Address: ', + fields: [ + { + component_type: 'text_input', + id: 'Email Address', + label: 'Email Address', + default_value: '', + required: true, + page: 2, + }, + ], + page: 2, + }, + { + component_type: 'fieldset', + legend: 'Phone Number: ', + fields: [ + { + component_type: 'text_input', + id: 'Phone Number', + label: 'Phone Number', + default_value: '', + required: true, + page: 2, + }, + ], + page: 2, + }, + { + component_type: 'paragraph', + text: 'Date of Birth: Gender:', + style: 'normal', + page: 2, + }, + { + component_type: 'fieldset', + legend: 'Date of Birth', + fields: [ + { + component_type: 'text_input', + id: 'Date of Birth', + label: 'Date of Birth', + default_value: '', + required: true, + page: 2, + }, + { + component_type: 'text_input', + id: 'Gender', + label: 'Gender', + default_value: '', + required: true, + page: 2, + }, + ], + page: 2, + }, + { + component_type: 'radio_group', + legend: 'Are you Hispanic or Latino?: ', + options: [ + { + id: 'Yes', + label: 'Yes ', + name: 'Yes', + default_checked: false, + page: 2, + }, + { + id: 'No', + label: 'No ', + name: 'No', + default_checked: false, + page: 2, + }, + ], + id: 'Ethnicity', + page: 2, + }, + { + component_type: 'fieldset', + legend: 'Race:', + fields: [ + { + component_type: 'checkbox', + id: 'Nat Amer', + label: 'Alaska Native or American Indian ', + default_checked: false, + struct_parent: 20, + page: 2, + }, + { + component_type: 'checkbox', + id: 'Asian', + label: 'Asian ', + default_checked: false, + struct_parent: 21, + page: 2, + }, + { + component_type: 'checkbox', + id: 'Blck Amer', + label: 'Black or African American ', + default_checked: false, + struct_parent: 22, + page: 2, + }, + { + component_type: 'checkbox', + id: 'Nat Haw Islander', + label: 'Native Hawaiian or Other Pacific Islander ', + default_checked: false, + struct_parent: 23, + page: 2, + }, + { + component_type: 'checkbox', + id: 'White', + label: 'White ', + default_checked: false, + struct_parent: 24, + page: 2, + }, + { + component_type: 'checkbox', + id: 'Other', + label: 'Other ', + default_checked: false, + struct_parent: 25, + page: 2, + }, + ], + page: 2, + }, + { + component_type: 'radio_group', + legend: 'Citizenship or Residency Status: ', + options: [ + { + id: 'Birth', + label: 'U.S. citizen by birth ', + name: 'Birth', + default_checked: false, + page: 2, + }, + { + id: 'Naturalized', + label: 'U.S. naturalized citizen ', + name: 'Naturalized', + default_checked: false, + page: 2, + }, + { + id: 'Permanent_Resident', + label: 'Lawful Permanent Resident ', + name: 'Permanent_Resident', + default_checked: false, + page: 2, + }, + ], + id: 'Citizenship', + page: 2, + }, + { + 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, + page: 2, + }, + { + component_type: 'text_input', + id: '', + label: 'date naturalization granted', + default_value: '', + required: true, + page: 2, + }, + ], + page: 2, + }, + { + component_type: 'paragraph', + text: 'Date Residency Granted: Alien Registration Number (A-Number), Certificate of Naturalization Number, or Citizenship Number', + style: 'normal', + page: 2, + }, + { + 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, + page: 2, + }, + ], + page: 2, + }, + { + component_type: 'paragraph', + text: '(A-Number) 1.', + style: 'normal', + page: 2, + }, + { + 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, + page: 2, + }, + ], + page: 2, + }, + { + 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, + page: 2, + }, + ], + page: 2, + }, + { + component_type: 'paragraph', + text: '(month/day/year) (Northern, etc.)', + style: 'normal', + page: 2, + }, + { + component_type: 'fieldset', + legend: 'District of ', + fields: [ + { + component_type: 'text_input', + id: 'Dist State', + label: 'State', + default_value: '', + required: true, + page: 2, + }, + ], + page: 2, + }, + { + component_type: 'paragraph', + text: '(state) or D.C. Superior Court of simple possession of marijuana, under :', + style: 'normal', + page: 2, + }, + { + component_type: 'fieldset', + legend: 'Docket No. ', + fields: [ + { + component_type: 'text_input', + id: 'Docket No', + label: 'Docket Number', + default_value: '', + required: true, + page: 2, + }, + ], + page: 2, + }, + { + component_type: 'paragraph', + text: ';', + style: 'normal', + page: 2, + }, + { + component_type: 'fieldset', + legend: 'and Code Section: ', + fields: [ + { + component_type: 'text_input', + id: 'Code Section', + label: 'Code Section', + default_value: '', + required: true, + page: 2, + }, + ], + page: 2, + }, + { + component_type: 'paragraph', + text: 'OR (docket number) (code section) 2.', + style: 'normal', + page: 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, + page: 2, + }, + ], + page: 2, + }, + { + 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, + page: 2, + }, + ], + page: 2, + }, + { + component_type: 'paragraph', + text: '(code section) (Eastern, etc.)', + style: 'normal', + page: 2, + }, + { + component_type: 'fieldset', + legend: 'District of ', + fields: [ + { + component_type: 'text_input', + id: 'District 2', + label: 'State', + default_value: '', + required: true, + page: 2, + }, + ], + page: 2, + }, + { + component_type: 'paragraph', + text: 'or', + style: 'normal', + page: 2, + }, + { + 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, + page: 2, + }, + ], + page: 2, + }, + { + 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', + style: 'normal', + page: 2, + }, + { + 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.', + style: 'normal', + page: 3, + }, + { + component_type: 'fieldset', + legend: 'App Date', + fields: [ + { + component_type: 'text_input', + id: 'App Date', + label: 'Date', + default_value: '', + required: true, + page: 3, + }, + ], + page: 3, + }, + { + 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', + style: 'normal', + page: 3, + }, + ], + raw_fields: { + '0': [], + '1': [], + '2': [ + { + 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', + struct_parent: 4, + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Fst Name 1', + struct_parent: 4, + }, + { + type: '/Tx', + var_name: '', + field_dict: { + coordinates: [233.087006, 637.580994, 390.214996, 659.320007], + field_instructions: 'Middle Name', + struct_parent: 5, + name: 0, + field_type: '/Tx', + font_info: '', + field_label: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Mid Name 1/0', + struct_parent: 5, + }, + { + type: '/Tx', + var_name: '', + field_dict: { + coordinates: [390.996002, 637.492981, 548.124023, 659.231995], + field_instructions: 'Last Name', + struct_parent: 6, + name: 0, + field_type: '/Tx', + font_info: '', + field_label: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Lst Name 1/0', + struct_parent: 6, + }, + { + 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', + struct_parent: 7, + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Conv Fst Name', + struct_parent: 7, + }, + { + 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', + struct_parent: 8, + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Conv Mid Name', + struct_parent: 8, + }, + { + 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', + struct_parent: 9, + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Conv Lst Name', + struct_parent: 9, + }, + { + 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)', + struct_parent: 10, + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Address', + struct_parent: 10, + }, + { + 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', + struct_parent: 11, + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'City', + struct_parent: 11, + }, + { + 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', + struct_parent: 12, + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'State', + struct_parent: 12, + }, + { + 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)', + struct_parent: 13, + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Zip Code', + struct_parent: 13, + }, + { + 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', + struct_parent: 14, + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Email Address', + struct_parent: 14, + }, + { + 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', + struct_parent: 15, + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Phone Number', + struct_parent: 15, + }, + { + 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', + struct_parent: 16, + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Date of Birth', + struct_parent: 16, + }, + { + 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', + struct_parent: 17, + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Gender', + struct_parent: 17, + }, + { + type: '/Btn', + var_name: '', + field_dict: { + coordinates: [505.618988, 450.865997, 523.619019, 468.865997], + struct_parent: 18, + 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', + struct_parent: 18, + }, + { + type: '/Btn', + var_name: '', + field_dict: { + coordinates: [558.213013, 450.865997, 576.213013, 468.865997], + struct_parent: 19, + 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', + struct_parent: 19, + }, + { + 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', + struct_parent: 20, + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Nat Amer', + struct_parent: 20, + }, + { + 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', + struct_parent: 21, + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Asian', + struct_parent: 21, + }, + { + 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', + struct_parent: 22, + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Blck Amer', + struct_parent: 22, + }, + { + 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', + struct_parent: 23, + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Nat Haw Islander', + struct_parent: 23, + }, + { + 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', + struct_parent: 24, + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'White', + struct_parent: 24, + }, + { + 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', + struct_parent: 25, + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Other', + struct_parent: 25, + }, + { + type: '/Btn', + var_name: '', + field_dict: { + coordinates: [98.414398, 349.662994, 116.414001, 367.662994], + field_instructions: 'U S Citizen by birth', + struct_parent: 26, + 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', + struct_parent: 26, + }, + { + type: '/Btn', + var_name: '', + field_dict: { + coordinates: [98.414398, 331.733002, 116.414001, 349.733002], + field_instructions: 'U S naturalized citizen', + struct_parent: 27, + 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', + struct_parent: 27, + }, + { + type: '/Btn', + var_name: '', + field_dict: { + coordinates: [98.414398, 313.006012, 116.414001, 331.006012], + field_instructions: 'Lawful Permenent Resident', + struct_parent: 29, + 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', + struct_parent: 29, + }, + { + type: '/Tx', + var_name: '', + field_dict: { + coordinates: [432.306, 331.979004, 489.425995, 352.92099], + field_instructions: 'date naturalization granted', + struct_parent: 28, + 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', + struct_parent: 28, + }, + { + 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)', + struct_parent: 30, + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Residency Date_af_date', + struct_parent: 30, + }, + { + 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', + struct_parent: 31, + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'A-Number', + struct_parent: 31, + }, + { + 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', + struct_parent: 32, + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Convict-Date_af_date', + struct_parent: 32, + }, + { + 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', + struct_parent: 33, + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'US District Court', + struct_parent: 33, + }, + { + 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', + struct_parent: 34, + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Dist State', + struct_parent: 34, + }, + { + 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', + struct_parent: 36, + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Docket No', + struct_parent: 36, + }, + { + 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', + struct_parent: 37, + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Code Section', + struct_parent: 37, + }, + { + 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', + struct_parent: 38, + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Code Section_2', + struct_parent: 38, + }, + { + 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', + struct_parent: 39, + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'US District Court_2', + struct_parent: 39, + }, + { + 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', + struct_parent: 40, + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'District 2', + struct_parent: 40, + }, + { + 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', + struct_parent: 42, + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 2, + path: 'Docket No 2', + struct_parent: 42, + }, + ], + '3': [ + { + 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', + struct_parent: 44, + font_info: '', + hidden: false, + child_fields: [], + num_children: 0, + }, + page_number: 3, + path: 'App Date', + struct_parent: 44, + }, + ], + }, + grouped_items: [], + raw_fields_pages: { + '0': "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 ", + '1': "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 ", + '2': '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 ', + '3': '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 ', + }, + }, + cache_id: 'Cache ID is not implemented yet', +}; diff --git a/packages/forms/src/pattern.ts b/packages/forms/src/pattern.ts index dcd1cb64..b14e4c4c 100644 --- a/packages/forms/src/pattern.ts +++ b/packages/forms/src/pattern.ts @@ -18,7 +18,10 @@ export type PatternId = string; export type PatternValue = any; export type PatternValueMap = Record; export type PatternMap = Record; -export type GetPattern = (form: Blueprint, id: PatternId) => Pattern; +export type GetPattern = ( + form: Blueprint, + id: PatternId +) => Pattern; export type ParseUserInput = ( pattern: Pattern, @@ -34,10 +37,39 @@ type RemoveChildPattern

= ( patternId: PatternId ) => P; +export abstract class PatternBuilder

{ + public readonly id: PatternId; + public readonly data: P['data']; + + constructor(data: P['data'], id?: PatternId) { + this.id = id || generatePatternId(); + this.data = data; + } + + abstract toPattern(): P; +} + export const getPattern: GetPattern = (form, patternId) => { return form.patterns[patternId]; }; +export const getPatternSafely =

(opts: { + type: string; + form: Blueprint; + patternId: PatternId; +}): r.Result

=> { + const pattern = opts.form.patterns[opts.patternId]; + if (pattern === undefined) { + return r.failure(`Pattern with id ${opts.patternId} does not exist`); + } + if (pattern.type !== opts.type) { + return r.failure( + `Pattern with id ${opts.patternId} is not of type ${opts.type}` + ); + } + return r.success(pattern as P); +}; + export type PatternConfig< ThisPattern extends Pattern = Pattern, PatternOutput = unknown, @@ -87,15 +119,46 @@ export const validatePattern = ( } const parseResult = patternConfig.parseUserInput(pattern, value); if (!parseResult.success) { - return { - success: false, - error: parseResult.error, - }; + return r.failure(parseResult.error); } - return { - success: true, - data: parseResult.data, - }; + return r.success(parseResult.data); +}; + +export const validatePatternAndChildren = ( + config: FormConfig, + form: Blueprint, + patternConfig: PatternConfig, + pattern: Pattern, + values: Record, + result: { + values: Record; + errors: Record; + } = { values: {}, errors: {} } +) => { + if (patternConfig.parseUserInput) { + const parseResult = patternConfig.parseUserInput( + pattern, + values[pattern.id] + ); + if (parseResult.success) { + result.values[pattern.id] = parseResult.data; + } else { + result.values[pattern.id] = values[pattern.id]; + result.errors[pattern.id] = parseResult.error; + } + } + for (const child of patternConfig.getChildren(pattern, form.patterns)) { + const childPatternConfig = getPatternConfig(config, child.type); + validatePatternAndChildren( + config, + form, + childPatternConfig, + child, + values, + result + ); + } + return result; }; export const getFirstPattern = ( diff --git a/packages/forms/src/patterns/README.md b/packages/forms/src/patterns/README.md new file mode 100644 index 00000000..56daa04b --- /dev/null +++ b/packages/forms/src/patterns/README.md @@ -0,0 +1,54 @@ +# Forms Platform - Patterns + +Patterns are the Forms Platform's primary building block, a reusable module that may be configured via the no-code form builder to craft custom user experiences. + +Patterns are stored on the form `Blueprint`'s pattern attribute. + +## Pattern configuration + +Each pattern must define a configuration object, which provides the definition of its behavior. + +The `PatternConfig` type is defined in [../pattern.ts](../pattern.ts). + +## Data structure + +Patterns are defined by the generic `Pattern` type in `pattern.ts`. + +```typescript +export type Pattern = { + type: string; // A string identifier for the pattern type + id: PatternId; // A unique identifier for the pattern instance + data: C; // The configuration data specific to the pattern type +}; +``` + +Constructing patterns may be accomplished manually, or via `PatternBuilder` helper classes. + +For example, an input pattern may be defined directly: + +```typescript +const input: InputPattern = { + type: 'input', + id: 'my-input', + data: { + label: 'My input', + initial: '', + required: true, + maxLength: 64 + }, +} +``` + +```typescript +const input = new InputPatternBuilder(); +const page1 = new Page({ title: 'Page 1', patterns: [input1.id] }); +const pageSet = new PageSet({ pages: [page1.id] }, 'page-set'); +const page2 = new Page({ title: 'Page 2', patterns: [input1.id] }); +pageSet.addPage(page2) + +// Construct the pattern objects +page1.toPattern(); +page2.toPattern(); +pageSet.toPattern(); +``` + diff --git a/packages/forms/src/patterns/index.ts b/packages/forms/src/patterns/index.ts index b871634a..f7d67765 100644 --- a/packages/forms/src/patterns/index.ts +++ b/packages/forms/src/patterns/index.ts @@ -5,6 +5,7 @@ import { checkboxConfig } from './checkbox.js'; import { fieldsetConfig } from './fieldset/index.js'; import { formSummaryConfig } from './form-summary.js'; import { inputConfig } from './input/index.js'; +import { packageDownloadConfig } from './package-download/index.js'; import { pageConfig } from './page/index.js'; import { pageSetConfig } from './page-set/index.js'; import { paragraphConfig } from './paragraph.js'; @@ -22,6 +23,7 @@ export const defaultFormConfig: FormConfig = { checkbox: checkboxConfig, fieldset: fieldsetConfig, input: inputConfig, + 'package-download': packageDownloadConfig, page: pageConfig, 'page-set': pageSetConfig, paragraph: paragraphConfig, @@ -32,14 +34,15 @@ export const defaultFormConfig: FormConfig = { } as const; export * from './address/index.js'; +export * from './checkbox.js'; export * from './fieldset/index.js'; +export * from './form-summary.js'; export * from './input/index.js'; +export * from './package-download/index.js'; export * from './page/index.js'; export { type PagePattern } from './page/config.js'; export * from './page-set/index.js'; export { type PageSetPattern } from './page-set/config.js'; -export * from './checkbox.js'; -export * from './form-summary.js'; export * from './paragraph.js'; export * from './radio-group.js'; export * from './sequence.js'; diff --git a/packages/forms/src/patterns/input/builder.ts b/packages/forms/src/patterns/input/builder.ts new file mode 100644 index 00000000..43b648a0 --- /dev/null +++ b/packages/forms/src/patterns/input/builder.ts @@ -0,0 +1,12 @@ +import { PatternBuilder } from '../../pattern'; +import { type InputPattern } from '.'; + +export class Input extends PatternBuilder { + toPattern(): InputPattern { + return { + id: this.id, + type: 'input', + data: this.data, + }; + } +} diff --git a/packages/forms/src/patterns/package-download/builder.ts b/packages/forms/src/patterns/package-download/builder.ts new file mode 100644 index 00000000..1abf256a --- /dev/null +++ b/packages/forms/src/patterns/package-download/builder.ts @@ -0,0 +1,12 @@ +import { type PackageDownloadPattern } from '.'; +import { PatternBuilder } from '../../pattern'; + +export class PackageDownload extends PatternBuilder { + toPattern(): PackageDownloadPattern { + return { + id: this.id, + type: 'page-set', + data: this.data, + }; + } +} diff --git a/packages/forms/src/patterns/package-download/index.ts b/packages/forms/src/patterns/package-download/index.ts new file mode 100644 index 00000000..5f8d642c --- /dev/null +++ b/packages/forms/src/patterns/package-download/index.ts @@ -0,0 +1,43 @@ +import * as z from 'zod'; + +import { type Pattern, type PatternConfig } from '../../pattern.js'; +import { type PackageDownloadProps } from '../../components.js'; +import { getActionString } from '../../submission.js'; +import { safeZodParseFormErrors } from '../../util/zod.js'; + +const configSchema = z.object({ + text: z.string().min(1), +}); +export type PackageDownloadPattern = Pattern>; + +export const packageDownloadConfig: PatternConfig = { + displayName: 'Package download', + iconPath: 'block-icon.svg', + initial: { + text: 'Description text...', + }, + parseConfigData: obj => safeZodParseFormErrors(configSchema, obj), + getChildren() { + return []; + }, + createPrompt(_, session, pattern, options) { + return { + props: { + _patternId: pattern.id, + type: 'package-download' as const, + actions: [ + { + type: 'submit', + submitAction: getActionString({ + handlerId: 'package-download', + patternId: pattern.id, + }), + text: 'Download PDF', + }, + ], + text: pattern.data.text, + } as PackageDownloadProps, + children: [], + }; + }, +}; diff --git a/packages/forms/src/patterns/package-download/submit.test.ts b/packages/forms/src/patterns/package-download/submit.test.ts new file mode 100644 index 00000000..53d57a9e --- /dev/null +++ b/packages/forms/src/patterns/package-download/submit.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, it } from 'vitest'; + +import { failure } from '@atj/common'; + +import { type Blueprint, type FormSession, defaultFormConfig } from '../..'; + +import { downloadPackageHandler } from './submit'; +import { PackageDownload } from './builder'; +import { PageSet } from '../page-set/builder'; +import { Page } from '../page/builder'; +import { Input } from '../input/builder'; +import { loadSamplePDF } from '../../documents/__tests__/sample-data'; + +describe('downloadPackageHandler', async () => { + it('returns failure when form is not complete', async () => { + const session: FormSession = { + form: await createTestForm(), + data: { errors: {}, values: {} }, + route: { url: '#', params: {} }, + }; + const result = await downloadPackageHandler(defaultFormConfig, { + pattern: new PackageDownload({ text: 'Download now!' }).toPattern(), + session, + data: {}, + }); + + expect(result).toEqual(failure('Form is not complete')); + }); + + it('returns attachments on complete form', async () => { + const session: FormSession = { + form: await createTestForm(), + data: { + errors: {}, + values: { + 'input-1': '53555', + }, + }, + route: { url: '#', params: {} }, + }; + const result = await downloadPackageHandler(defaultFormConfig, { + pattern: new PackageDownload({ text: 'Download now!' }).toPattern(), + session, + data: {}, + }); + expect(result).toEqual( + expect.objectContaining({ + success: true, + data: { + session, + attachments: [ + { + fileName: 'test.pdf', + data: expect.any(Uint8Array), + }, + ], + }, + }) + ); + }); +}); + +const createTestForm = async (): Promise => { + const pdfBytes = await loadSamplePDF( + 'doj-pardon-marijuana/application_for_certificate_of_pardon_for_simple_marijuana_possession.pdf' + ); + const input1 = new Input( + { label: 'Input 1', required: true, maxLength: 10 }, + 'input-1' + ); + const page1 = new Page({ title: 'Page 1', patterns: [input1.id] }, 'page-1'); + const pageSet = new PageSet({ pages: [page1.id] }, 'page-set'); + return { + summary: { + title: 'Test Form', + description: 'A test form', + }, + root: 'page-set', + patterns: { + 'page-set': pageSet.toPattern(), + 'page-1': page1.toPattern(), + 'input-1': input1.toPattern(), + }, + outputs: [ + { + path: 'test.pdf', + data: new Uint8Array(pdfBytes), + fields: { + [input1.id]: { + type: 'TextField', + name: 'Zip Code', + label: 'Zip Code', + maxLength: 32, + value: 'zipCode', + required: true, + }, + }, + formFields: { + [input1.id]: 'Zip Code', + }, + }, + ], + }; +}; diff --git a/packages/forms/src/patterns/package-download/submit.ts b/packages/forms/src/patterns/package-download/submit.ts new file mode 100644 index 00000000..ad240a71 --- /dev/null +++ b/packages/forms/src/patterns/package-download/submit.ts @@ -0,0 +1,60 @@ +import { failure, success } from '@atj/common'; + +import { type Blueprint } from '../..'; +import { createFormOutputFieldData, fillPDF } from '../../documents'; +import { sessionIsComplete } from '../../session'; +import { type SubmitHandler } from '../../submission'; + +import { type PackageDownloadPattern } from './index'; + +export const downloadPackageHandler: SubmitHandler< + PackageDownloadPattern +> = async (config, opts) => { + if (!sessionIsComplete(config, opts.session)) { + return failure('Form is not complete'); + } + + const documentsResult = await generateDocumentPackage( + opts.session.form, + opts.session.data.values + ); + if (!documentsResult.success) { + console.log('values', opts.session.data.values); + return failure(documentsResult.error); + } + + return success({ + attachments: documentsResult.data, + session: opts.session, + }); +}; + +const generateDocumentPackage = async ( + form: Blueprint, + formData: Record +) => { + const errors = new Array(); + const documents = new Array<{ fileName: string; data: Uint8Array }>(); + for (const document of form.outputs) { + const docFieldData = createFormOutputFieldData(document, formData); + const pdfDocument = await fillPDF(document.data, docFieldData); + if (!pdfDocument.success) { + errors.push(pdfDocument.error); + } else { + documents.push({ + fileName: document.path, + data: pdfDocument.data, + }); + } + } + if (errors.length > 0) { + return { + success: false as const, + error: errors.join('\n'), + }; + } + return { + success: true as const, + data: documents, + }; +}; diff --git a/packages/forms/src/patterns/page-set/builder.ts b/packages/forms/src/patterns/page-set/builder.ts new file mode 100644 index 00000000..cdc17dbd --- /dev/null +++ b/packages/forms/src/patterns/page-set/builder.ts @@ -0,0 +1,25 @@ +import { type Blueprint } from '../..'; +import { PatternBuilder } from '../../pattern'; +import { type Page } from '../page/builder'; +import { type PageSetPattern } from './config'; + +export class Form { + constructor(public readonly blueprint: Blueprint) {} +} + +export class PageSet extends PatternBuilder { + addPage(page: Page) { + return new PageSet({ + ...this.data, + pages: [...this.data.pages, page.id], + }); + } + + toPattern(): PageSetPattern { + return { + id: this.id, + type: 'page-set', + data: this.data, + }; + } +} diff --git a/packages/forms/src/patterns/page-set/index.ts b/packages/forms/src/patterns/page-set/index.ts index 556926ac..e0ad4797 100644 --- a/packages/forms/src/patterns/page-set/index.ts +++ b/packages/forms/src/patterns/page-set/index.ts @@ -1,8 +1,21 @@ -import { type PatternConfig } from '../../pattern.js'; +import { type PatternConfig, type PatternId } from '../../pattern.js'; import { type PageSetPattern, parseConfigData } from './config.js'; import { createPrompt } from './prompt.js'; +export const createPageSet = ( + id: PatternId, + pages?: PatternId[] +): PageSetPattern => { + return { + id, + type: 'page-set', + data: { + pages: pages || [], + }, + }; +}; + export const pageSetConfig: PatternConfig = { displayName: 'Page set', iconPath: 'block-icon.svg', diff --git a/packages/forms/src/patterns/page-set/prompt.ts b/packages/forms/src/patterns/page-set/prompt.ts index 14f91ee2..ab2337a6 100644 --- a/packages/forms/src/patterns/page-set/prompt.ts +++ b/packages/forms/src/patterns/page-set/prompt.ts @@ -13,6 +13,7 @@ import { safeZodParseFormErrors } from '../../util/zod.js'; import { type PageSetPattern } from './config.js'; import { type PagePattern } from '../page/config.js'; +import { ActionName, getActionString } from '../../submission.js'; export const createPrompt: CreatePrompt = ( config, @@ -37,6 +38,7 @@ export const createPrompt: CreatePrompt = ( session, pageCount: pattern.data.pages.length, pageIndex: activePage, + pattern, }); return { props: { @@ -82,6 +84,7 @@ const getActionsForPage = (opts: { session: FormSession; pageCount: number; pageIndex: number | null; + pattern: PageSetPattern; }): PromptAction[] => { if (opts.pageIndex === null) { return []; @@ -95,16 +98,20 @@ const getActionsForPage = (opts: { url: `${pathName}?page=${opts.pageIndex - 1}`, }); } + const actionName: ActionName = getActionString({ + handlerId: 'page-set', + patternId: opts.pattern.id, + }); if (opts.pageIndex < opts.pageCount - 1) { actions.push({ type: 'submit', - submitAction: 'next', + submitAction: actionName, text: 'Next', }); } else { actions.push({ type: 'submit', - submitAction: 'submit', + submitAction: actionName, text: 'Submit', }); } diff --git a/packages/forms/src/patterns/page-set/submit.test.ts b/packages/forms/src/patterns/page-set/submit.test.ts new file mode 100644 index 00000000..0cd041ed --- /dev/null +++ b/packages/forms/src/patterns/page-set/submit.test.ts @@ -0,0 +1,150 @@ +import { describe, expect, it } from 'vitest'; + +import { type Blueprint, defaultFormConfig } from '../..'; +import { Input } from '../input/builder'; +import { Page } from '../page/builder'; +import { createFormSession } from '../../session'; + +import { PageSet } from './builder'; +import { submitPage } from './submit'; + +describe('Page-set submission', () => { + it('stores session data for valid page data', async () => { + const session = createTestSession(); + const result = await submitPage(defaultFormConfig, { + pattern: session.form.patterns['page-set-1'], + session, + data: { + 'input-1': 'test', + }, + }); + expect(result).toEqual({ + data: { + session: { + ...session, + data: { + errors: {}, + values: { + 'input-1': 'test', + }, + }, + route: { + url: '#', + params: { + page: '1', + }, + }, + form: session.form, + }, + }, + success: true, + }); + }); + + it('stores session data for invalid page data', async () => { + const session = createTestSession(); + const result = await submitPage(defaultFormConfig, { + pattern: session.form.patterns['page-set-1'], + session, + data: { + 'input-1': '', + }, + }); + expect(result).toEqual({ + data: { + session: { + ...session, + data: { + errors: { + 'input-1': { + type: 'custom', + message: 'This field is required', + }, + }, + values: { + 'input-1': '', + }, + }, + route: { + url: '#', + params: { + page: '0', + }, + }, + form: session.form, + }, + }, + success: true, + }); + }); + + it('terminates on the last page', async () => { + const session = createTestSession(); + const result = await submitPage(defaultFormConfig, { + pattern: session.form.patterns['page-set-1'], + session: { + ...session, + route: { + url: '#', + params: { + page: '1', + }, + }, + }, + data: { + 'input-2': 'test', + }, + }); + expect(result).toEqual({ + data: { + session: { + ...session, + data: { + errors: {}, + values: { + 'input-2': 'test', + }, + }, + route: { + url: '#', + params: { + page: '1', + }, + }, + form: session.form, + }, + }, + success: true, + }); + }); +}); + +const createTestSession = () => { + const input1 = new Input( + { label: 'label', required: true, maxLength: 10 }, + 'input-1' + ); + const input2 = new Input( + { label: 'label', required: true, maxLength: 10 }, + 'input-2' + ); + const page1 = new Page({ title: 'Page 1', patterns: [input1.id] }, 'page-1'); + const page2 = new Page({ title: 'Page 2', patterns: [input2.id] }, 'page-2'); + const pageSet = new PageSet({ pages: [page1.id, page2.id] }, 'page-set-1'); + const testForm: Blueprint = { + summary: { + description: 'A test form', + title: 'Test form', + }, + root: pageSet.id, + patterns: { + [page1.id]: page1.toPattern(), + [page2.id]: page2.toPattern(), + [pageSet.id]: pageSet.toPattern(), + [input1.id]: input1.toPattern(), + [input2.id]: input2.toPattern(), + }, + outputs: [], + }; + return createFormSession(testForm, { url: '#', params: { page: '0' } }); +}; diff --git a/packages/forms/src/patterns/page-set/submit.ts b/packages/forms/src/patterns/page-set/submit.ts new file mode 100644 index 00000000..9cadb644 --- /dev/null +++ b/packages/forms/src/patterns/page-set/submit.ts @@ -0,0 +1,78 @@ +import { failure, success } from '@atj/common'; + +import { + getPatternConfig, + getPatternSafely, + validatePatternAndChildren, +} from '../../pattern'; +import { type FormSession } from '../../session'; +import { type SubmitHandler } from '../../submission'; +import { type PagePattern } from '../page/config'; +import { type PageSetPattern } from './config'; + +const getPage = (formSession: FormSession) => { + const page = formSession.route?.params.page?.toString(); + return typeof page == 'string' ? Number.parseInt(page) : 0; +}; + +export const submitPage: SubmitHandler = async ( + config, + opts +) => { + const pageNumber = getPage(opts.session); + const pagePatternId = opts.pattern.data.pages[pageNumber]; + if (pagePatternId === undefined) { + return failure(`Page ${pageNumber} does not exist`); + } + + const pagePatternConfig = getPatternConfig(config, 'page'); + const pagePattern = getPatternSafely({ + type: 'page', + form: opts.session.form, + patternId: pagePatternId, + }); + if (!pagePattern.success) { + return failure(pagePattern.error); + } + + const result = validatePatternAndChildren( + config, + opts.session.form, + pagePatternConfig, + pagePattern.data, + opts.data + ); + + // Increment the page number if there are no errors and this isn't the last page. + const lastPage = opts.pattern.data.pages.length - 1; + const nextPage = + Object.values(result.errors).length === 0 && pageNumber < lastPage + ? pageNumber + 1 + : pageNumber; + + return success({ + session: { + ...opts.session, + data: { + ...opts.session.data, + values: { + ...opts.session.data.values, + ...result.values, + }, + errors: { + ...opts.session.data.errors, + ...result.errors, + }, + }, + route: opts.session.route + ? { + ...opts.session.route, + params: { + ...opts.session.route.params, + page: nextPage.toString(), + }, + } + : undefined, + }, + }); +}; diff --git a/packages/forms/src/patterns/page/builder.ts b/packages/forms/src/patterns/page/builder.ts new file mode 100644 index 00000000..0b4dc9e4 --- /dev/null +++ b/packages/forms/src/patterns/page/builder.ts @@ -0,0 +1,19 @@ +import { PatternBuilder } from '../../pattern'; +import { type PagePattern } from './config'; + +export class Page extends PatternBuilder { + setTitle(title: string) { + return new Page({ + ...this.data, + title, + }); + } + + toPattern(): PagePattern { + return { + id: this.id, + type: 'page', + data: this.data, + }; + } +} diff --git a/packages/forms/src/repository/get-form.test.ts b/packages/forms/src/repository/get-form.test.ts index b9d943b9..415f809c 100644 --- a/packages/forms/src/repository/get-form.test.ts +++ b/packages/forms/src/repository/get-form.test.ts @@ -1,6 +1,8 @@ import { expect, it } from 'vitest'; import { type DbTestContext, describeDatabase } from '@atj/database/testing'; + +import { type Blueprint } from '../index.js'; import { getForm } from './get-form.js'; describeDatabase('getForm', () => { @@ -10,7 +12,7 @@ describeDatabase('getForm', () => { .insertInto('forms') .values({ id: '45c66187-64e2-4d75-a45a-e80f1d035bc5', - data: '{"summary":{"title":"Test form","description":"Test description"},"root":"root","patterns":{"root":{"type":"sequence","id":"root","data":{"patterns":[]}}},"outputs":[]}', + data: '{"summary":{"title":"Title","description":"Description"},"root":"root","patterns":{"root":{"type":"sequence","id":"root","data":{"patterns":[]}}},"outputs":[{"data":"AQID","path":"test.pdf","fields":{},"formFields":{}}]}', }) .execute(); @@ -18,23 +20,8 @@ describeDatabase('getForm', () => { db.ctx, '45c66187-64e2-4d75-a45a-e80f1d035bc5' ); - expect(result).toEqual({ - outputs: [], - patterns: { - root: { - data: { - patterns: [], - }, - id: 'root', - type: 'sequence', - }, - }, - root: 'root', - summary: { - description: 'Test description', - title: 'Test form', - }, - }); + console.log(result); + expect(result).toEqual(TEST_FORM); }); it('return null with non-existent form', async ({ db }) => { @@ -45,3 +32,17 @@ describeDatabase('getForm', () => { expect(result).toBeNull(); }); }); + +const TEST_FORM: Blueprint = { + summary: { title: 'Title', description: 'Description' }, + root: 'root', + patterns: { root: { type: 'sequence', id: 'root', data: { patterns: [] } } }, + outputs: [ + { + data: new Uint8Array([1, 2, 3]), + path: 'test.pdf', + fields: {}, + formFields: {}, + }, + ], +}; diff --git a/packages/forms/src/repository/get-form.ts b/packages/forms/src/repository/get-form.ts index b3fe828f..e144162b 100644 --- a/packages/forms/src/repository/get-form.ts +++ b/packages/forms/src/repository/get-form.ts @@ -5,7 +5,7 @@ import { type Blueprint } from '../index.js'; export type GetForm = ( ctx: DatabaseContext, formId: string -) => Promise; +) => Promise; export const getForm: GetForm = async (ctx, formId) => { const db = await ctx.getKysely(); diff --git a/packages/forms/src/repository/save-form.test.ts b/packages/forms/src/repository/save-form.test.ts index 600a60de..8befac51 100644 --- a/packages/forms/src/repository/save-form.test.ts +++ b/packages/forms/src/repository/save-form.test.ts @@ -2,10 +2,11 @@ import { expect, it } from 'vitest'; import { type DbTestContext, describeDatabase } from '@atj/database/testing'; +import { type Blueprint } from '../index.js'; import { saveForm } from './save-form.js'; import { addForm } from './add-form.js'; -const TEST_FORM = { +const TEST_FORM: Blueprint = { summary: { title: 'Test form', description: 'Test description', @@ -20,7 +21,14 @@ const TEST_FORM = { }, }, }, - outputs: [], + outputs: [ + { + data: new Uint8Array([1, 2, 3]), + path: 'test.pdf', + fields: {}, + formFields: {}, + }, + ], }; describeDatabase('saveForm', () => { @@ -47,7 +55,7 @@ describeDatabase('saveForm', () => { expect(result[0].id).toEqual(addResult.data.id); expect(result[0].data).toEqual( - '{"summary":{"title":"Updated title","description":"Updated description"},"root":"root","patterns":{"root":{"type":"sequence","id":"root","data":{"patterns":[]}}},"outputs":[]}' + '{"summary":{"title":"Updated title","description":"Updated description"},"root":"root","patterns":{"root":{"type":"sequence","id":"root","data":{"patterns":[]}}},"outputs":[{"data":"AQID","path":"test.pdf","fields":{},"formFields":{}}]}' ); }); }); diff --git a/packages/forms/src/repository/serialize.ts b/packages/forms/src/repository/serialize.ts index d68cc077..11e32283 100644 --- a/packages/forms/src/repository/serialize.ts +++ b/packages/forms/src/repository/serialize.ts @@ -1,4 +1,6 @@ -export const stringifyForm = (form: /*Blueprint*/ any) => { +import { type Blueprint } from '..'; + +export const stringifyForm = (form: Blueprint) => { return JSON.stringify({ ...form, outputs: form.outputs.map((output: any) => ({ diff --git a/packages/forms/src/services/submit-form.test.ts b/packages/forms/src/services/submit-form.test.ts index e68fd1b4..8f59ab0d 100644 --- a/packages/forms/src/services/submit-form.test.ts +++ b/packages/forms/src/services/submit-form.test.ts @@ -1,11 +1,8 @@ import { describe, expect, it } from 'vitest'; import { - addDocument, createForm, createFormSession, - defaultFormConfig, - BlueprintBuilder, type Blueprint, type InputPattern, type PagePattern, @@ -13,11 +10,30 @@ import { type PatternValueMap, } from '../index.js'; import { createTestFormServiceContext } from '../testing.js'; -import { loadSamplePDF } from '../documents/__tests__/sample-data.js'; import { submitForm } from './submit-form.js'; -import { object } from 'zod'; +import { + createTestFormWithPDF, + getMockFormData, +} from '../documents/__tests__/test-documents.js'; describe('submitForm', () => { + it('fails with missing action string', async () => { + const { ctx, form, id } = await setupTestForm(); + const session = createFormSession(form); + const formSessionResult = await ctx.repository.upsertFormSession({ + formId: id, + data: session, + }); + if (!formSessionResult.success) { + expect.fail('upsertFormSession failed'); + } + const result = await submitForm(ctx, formSessionResult.data.id, id, {}); + expect(result).toEqual({ + success: false, + error: 'Invalid action: undefined', + }); + }); + it('succeeds with empty form', async () => { const { ctx, id, form } = await setupTestForm(); const session = createFormSession(form); @@ -29,13 +45,14 @@ describe('submitForm', () => { expect.fail('upsertFormSession failed'); } - const result = await submitForm(ctx, formSessionResult.data.id, id, {}); + const result = await submitForm(ctx, formSessionResult.data.id, id, { + action: 'action/page-set/root', + }); expect(result).toEqual({ success: true, data: { session: session, sessionId: formSessionResult.data.id, - documents: [], }, }); }); @@ -72,12 +89,30 @@ describe('submitForm', () => { if (!formSessionResult.success) { expect.fail('upsertFormSession failed'); } - const result = await submitForm(ctx, formSessionResult.data.id, id, {}); + const result = await submitForm(ctx, formSessionResult.data.id, id, { + action: 'action/page-set/root', + }); expect(result).toEqual({ data: { sessionId: formSessionResult.data.id, session: { ...session, + data: { + errors: { + 'element-1': { + message: 'Required', + type: 'custom', + }, + 'element-2': { + message: 'Required', + type: 'custom', + }, + }, + values: { + 'element-1': undefined, + 'element-2': undefined, + }, + }, route: undefined, }, }, @@ -96,6 +131,7 @@ describe('submitForm', () => { expect.fail('upsertFormSession failed'); } const result = await submitForm(ctx, formSessionResult.data.id, id, { + action: 'action/page-set/root', 'element-1': 'test', }); expect(result).toEqual({ @@ -103,12 +139,11 @@ describe('submitForm', () => { data: { session: expect.any(Object), sessionId: formSessionResult.data.id, - documents: [], }, }); }); - it('returns a pdf with completed form', async () => { + it.fails('returns a pdf with completed form', async () => { const { ctx, form, id } = await setupTestForm( await createTestFormWithPDF() ); @@ -120,12 +155,10 @@ describe('submitForm', () => { if (!formSessionResult.success) { expect.fail('upsertFormSession failed'); } - const result = await submitForm( - ctx, - formSessionResult.data.id, - id, - formData - ); + const result = await submitForm(ctx, formSessionResult.data.id, id, { + action: 'action/page-set/root', + ...formData, + }); expect(result).toEqual( expect.objectContaining({ success: true, @@ -142,8 +175,10 @@ describe('submitForm', () => { }) ); }); +}); - it.fails('handles page one of a multi-page form', async () => { +describe('multi-page form', () => { + const setupMultiPageForm = async () => { const form = createForm( { title: 'Test form', @@ -199,25 +234,168 @@ describe('submitForm', () => { } ); const { ctx, id } = await setupTestForm(form); + const session = createFormSession(form); const formSessionResult = await ctx.repository.upsertFormSession({ formId: id, - data: createFormSession(form), + data: session, }); if (!formSessionResult.success) { expect.fail('upsertFormSession failed'); } - const result = await submitForm(ctx, formSessionResult.data.id, id, { + return { ctx, id, formSessionResult, session }; + }; + + it('handles page one of a multi-page form with valid data', async () => { + const { ctx, id, formSessionResult, session } = await setupMultiPageForm(); + const result = await submitForm( + ctx, + formSessionResult.data.id, + id, + { + action: 'action/page-set/root', + 'element-1': 'test', + }, + { url: '#', params: { page: '0' } } + ); + expect(result).toEqual({ + success: true, + data: { + sessionId: formSessionResult.data.id, + session: { + ...session, + data: { + errors: {}, + values: { + 'element-1': 'test', + }, + }, + route: { + params: { + page: '1', + }, + url: '#', + }, + }, + }, + }); + }); + + it('handles page one of a multi-page form with invalid data', async () => { + const { ctx, id, formSessionResult, session } = await setupMultiPageForm(); + const result = await submitForm( + ctx, + formSessionResult.data.id, + id, + { + action: 'action/page-set/root', + }, + { url: '#', params: { page: '0' } } + ); + expect(result).toEqual({ + success: true, + data: { + sessionId: formSessionResult.data.id, + session: { + ...session, + data: { + errors: { + 'element-1': { + message: 'Required', + type: 'custom', + }, + }, + values: { + 'element-1': undefined, + }, + }, + route: { + params: { + page: '0', + }, + url: '#', + }, + }, + }, + }); + }); + + it('handles page two of a multi-page form', async () => { + const { ctx, id, formSessionResult, session } = await setupMultiPageForm(); + + // First, submit page one + const pageOneResult = await submitForm(ctx, formSessionResult.data.id, id, { + action: 'action/page-set/root', 'element-1': 'test', }); + if (!pageOneResult.success) { + expect.fail('submitForm failed'); + } + + // Then, submit page two + const result = await submitForm( + ctx, + formSessionResult.data.id, + id, + { + action: 'action/page-set/root', + 'element-2': 'test2', + }, + { url: '#', params: { page: '1' } } + ); + expect(result).toEqual({ success: true, - data: { documents: [] }, + data: { + sessionId: formSessionResult.data.id, + session: { + ...session, + data: { + errors: {}, + values: { + 'element-1': 'test', + 'element-2': 'test2', + }, + }, + route: { + params: { + page: '1', + }, + url: '#', + }, + }, + }, }); }); + + // You can add more tests here using the setupMultiPageForm function }); const setupTestForm = async (form?: Blueprint) => { - form = form || createForm({ title: 'test', description: 'description' }); + form = + form || + createForm( + { title: 'test', description: 'description' }, + { + root: 'root', + patterns: [ + { + id: 'root', + type: 'page-set', + data: { + pages: ['page-1'], + }, + } satisfies PageSetPattern, + { + id: 'page-1', + type: 'page', + data: { + title: 'Page 1', + patterns: [], + }, + } satisfies PagePattern, + ], + } + ); const ctx = await createTestFormServiceContext({ isUserLoggedIn: () => false, }); @@ -262,1352 +440,17 @@ const createOnePatternTestForm = () => { maxLength: 128, }, } satisfies InputPattern, - ], - } - ); -}; - -const createTestFormWithPDF = async () => { - const pdfBytes = await loadSamplePDF( - 'doj-pardon-marijuana/application_for_certificate_of_pardon_for_simple_marijuana_possession.pdf' - ); - const builder = new BlueprintBuilder(defaultFormConfig); - const { updatedForm } = await addDocument( - builder.form, - { - name: 'test.pdf', - data: new Uint8Array(pdfBytes), - }, - { - fetchPdfApiResponse: async () => SERVICE_RESPONSE, - } - ); - - return updatedForm; -}; - -const getMockFormData = (form: Blueprint): PatternValueMap => { - return Object.keys(form.patterns).reduce((acc, key) => { - if (form.patterns[key].type === 'checkbox') { - acc[key] = true; - } else { - acc[key] = 'test value'; - } - return acc; - }, {} as PatternValueMap); -}; - -const SERVICE_RESPONSE = { - 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: 'My Form Title', - description: 'My Form 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", - style: 'normal', - page: 0, - }, - { - 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", - style: 'normal', - page: 1, - }, - { - 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:', - style: 'normal', - page: 2, - }, - { - component_type: 'fieldset', - legend: 'Name: ', - fields: [ - { - component_type: 'text_input', - id: 'Fst Name 1', - label: 'First Name', - default_value: '', - required: true, - page: 2, - }, - { - component_type: 'text_input', - id: '', - label: 'Middle Name', - default_value: '', - required: true, - page: 2, - }, - { - component_type: 'text_input', - id: '', - label: 'Last Name', - default_value: '', - required: true, - page: 2, - }, - ], - page: 2, - }, - { - component_type: 'paragraph', - text: '(first) (middle) (last)', - style: 'normal', - page: 2, - }, - { - 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, - page: 2, - }, - { - component_type: 'text_input', - id: 'Conv Mid Name', - label: 'Middle Name at Conviction', - default_value: '', - required: true, - page: 2, - }, - { - component_type: 'text_input', - id: 'Conv Lst Name', - label: 'Last Name at Conviction', - default_value: '', - required: true, - page: 2, - }, - ], - page: 2, - }, - { - component_type: 'paragraph', - text: '(if different) (first) (middle) (last)', - style: 'normal', - page: 2, - }, - { - component_type: 'fieldset', - legend: 'Address: ', - fields: [ - { - component_type: 'text_input', - id: 'Address', - label: 'Address (number, street, apartment/unit number)', - default_value: '', - required: true, - page: 2, - }, - ], - page: 2, - }, - { - component_type: 'paragraph', - text: '(number) (street) (apartment/unit no.)', - style: 'normal', - page: 2, - }, - { - component_type: 'fieldset', - legend: 'City', - fields: [ - { - component_type: 'text_input', - id: 'City', - label: 'City', - default_value: '', - required: true, - page: 2, - }, - { - component_type: 'text_input', - id: 'State', - label: 'State', - default_value: '', - required: true, - page: 2, - }, - { - component_type: 'text_input', - id: 'Zip Code', - label: '(Zip Code)', - default_value: '', - required: true, - page: 2, - }, - ], - page: 2, - }, - { - component_type: 'paragraph', - text: '(city) (state) (Zip Code)', - style: 'normal', - page: 2, - }, - { - component_type: 'fieldset', - legend: 'Email Address: ', - fields: [ - { - component_type: 'text_input', - id: 'Email Address', - label: 'Email Address', - default_value: '', - required: true, - page: 2, - }, - ], - page: 2, - }, - { - component_type: 'fieldset', - legend: 'Phone Number: ', - fields: [ - { - component_type: 'text_input', - id: 'Phone Number', - label: 'Phone Number', - default_value: '', - required: true, - page: 2, - }, - ], - page: 2, - }, - { - component_type: 'paragraph', - text: 'Date of Birth: Gender:', - style: 'normal', - page: 2, - }, - { - component_type: 'fieldset', - legend: 'Date of Birth', - fields: [ - { - component_type: 'text_input', - id: 'Date of Birth', - label: 'Date of Birth', - default_value: '', - required: true, - page: 2, - }, - { - component_type: 'text_input', - id: 'Gender', - label: 'Gender', - default_value: '', - required: true, - page: 2, - }, - ], - page: 2, - }, - { - component_type: 'radio_group', - legend: 'Are you Hispanic or Latino?: ', - options: [ - { - id: 'Yes', - label: 'Yes ', - name: 'Yes', - default_checked: false, - page: 2, - }, - { - id: 'No', - label: 'No ', - name: 'No', - default_checked: false, - page: 2, - }, - ], - id: 'Ethnicity', - page: 2, - }, - { - component_type: 'fieldset', - legend: 'Race:', - fields: [ - { - component_type: 'checkbox', - id: 'Nat Amer', - label: 'Alaska Native or American Indian ', - default_checked: false, - struct_parent: 20, - page: 2, - }, - { - component_type: 'checkbox', - id: 'Asian', - label: 'Asian ', - default_checked: false, - struct_parent: 21, - page: 2, - }, - { - component_type: 'checkbox', - id: 'Blck Amer', - label: 'Black or African American ', - default_checked: false, - struct_parent: 22, - page: 2, - }, - { - component_type: 'checkbox', - id: 'Nat Haw Islander', - label: 'Native Hawaiian or Other Pacific Islander ', - default_checked: false, - struct_parent: 23, - page: 2, - }, - { - component_type: 'checkbox', - id: 'White', - label: 'White ', - default_checked: false, - struct_parent: 24, - page: 2, - }, - { - component_type: 'checkbox', - id: 'Other', - label: 'Other ', - default_checked: false, - struct_parent: 25, - page: 2, - }, - ], - page: 2, - }, - { - component_type: 'radio_group', - legend: 'Citizenship or Residency Status: ', - options: [ - { - id: 'Birth', - label: 'U.S. citizen by birth ', - name: 'Birth', - default_checked: false, - page: 2, - }, - { - id: 'Naturalized', - label: 'U.S. naturalized citizen ', - name: 'Naturalized', - default_checked: false, - page: 2, - }, - { - id: 'Permanent_Resident', - label: 'Lawful Permanent Resident ', - name: 'Permanent_Resident', - default_checked: false, - page: 2, - }, - ], - id: 'Citizenship', - page: 2, - }, - { - 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, - page: 2, - }, - { - component_type: 'text_input', - id: '', - label: 'date naturalization granted', - default_value: '', - required: true, - page: 2, - }, - ], - page: 2, - }, - { - component_type: 'paragraph', - text: 'Date Residency Granted: Alien Registration Number (A-Number), Certificate of Naturalization Number, or Citizenship Number', - style: 'normal', - page: 2, - }, - { - 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, - page: 2, - }, - ], - page: 2, - }, - { - component_type: 'paragraph', - text: '(A-Number) 1.', - style: 'normal', - page: 2, - }, - { - 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, - page: 2, - }, - ], - page: 2, - }, - { - 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, - page: 2, - }, - ], - page: 2, - }, - { - component_type: 'paragraph', - text: '(month/day/year) (Northern, etc.)', - style: 'normal', - page: 2, - }, - { - component_type: 'fieldset', - legend: 'District of ', - fields: [ - { - component_type: 'text_input', - id: 'Dist State', - label: 'State', - default_value: '', - required: true, - page: 2, - }, - ], - page: 2, - }, - { - component_type: 'paragraph', - text: '(state) or D.C. Superior Court of simple possession of marijuana, under :', - style: 'normal', - page: 2, - }, - { - component_type: 'fieldset', - legend: 'Docket No. ', - fields: [ - { - component_type: 'text_input', - id: 'Docket No', - label: 'Docket Number', - default_value: '', - required: true, - page: 2, - }, - ], - page: 2, - }, - { - component_type: 'paragraph', - text: ';', - style: 'normal', - page: 2, - }, - { - component_type: 'fieldset', - legend: 'and Code Section: ', - fields: [ - { - component_type: 'text_input', - id: 'Code Section', - label: 'Code Section', - default_value: '', - required: true, - page: 2, - }, - ], - page: 2, - }, - { - component_type: 'paragraph', - text: 'OR (docket number) (code section) 2.', - style: 'normal', - page: 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, - page: 2, - }, - ], - page: 2, - }, - { - 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, - page: 2, - }, - ], - page: 2, - }, - { - component_type: 'paragraph', - text: '(code section) (Eastern, etc.)', - style: 'normal', - page: 2, - }, - { - component_type: 'fieldset', - legend: 'District of ', - fields: [ - { - component_type: 'text_input', - id: 'District 2', - label: 'State', - default_value: '', - required: true, - page: 2, - }, - ], - page: 2, - }, - { - component_type: 'paragraph', - text: 'or', - style: 'normal', - page: 2, - }, - { - 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, - page: 2, - }, - ], - page: 2, - }, - { - 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', - style: 'normal', - page: 2, - }, - { - 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.', - style: 'normal', - page: 3, - }, - { - component_type: 'fieldset', - legend: 'App Date', - fields: [ - { - component_type: 'text_input', - id: 'App Date', - label: 'Date', - default_value: '', - required: true, - page: 3, - }, - ], - page: 3, - }, - { - 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', - style: 'normal', - page: 3, - }, - ], - raw_fields: { - '0': [], - '1': [], - '2': [ - { - 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', - struct_parent: 4, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Fst Name 1', - struct_parent: 4, - }, - { - type: '/Tx', - var_name: '', - field_dict: { - coordinates: [233.087006, 637.580994, 390.214996, 659.320007], - field_instructions: 'Middle Name', - struct_parent: 5, - name: 0, - field_type: '/Tx', - font_info: '', - field_label: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Mid Name 1/0', - struct_parent: 5, - }, - { - type: '/Tx', - var_name: '', - field_dict: { - coordinates: [390.996002, 637.492981, 548.124023, 659.231995], - field_instructions: 'Last Name', - struct_parent: 6, - name: 0, - field_type: '/Tx', - font_info: '', - field_label: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Lst Name 1/0', - struct_parent: 6, - }, - { - 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', - struct_parent: 7, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Conv Fst Name', - struct_parent: 7, - }, - { - 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', - struct_parent: 8, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Conv Mid Name', - struct_parent: 8, - }, - { - 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', - struct_parent: 9, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Conv Lst Name', - struct_parent: 9, - }, - { - 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)', - struct_parent: 10, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Address', - struct_parent: 10, - }, - { - 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', - struct_parent: 11, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'City', - struct_parent: 11, - }, - { - 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', - struct_parent: 12, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'State', - struct_parent: 12, - }, - { - 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)', - struct_parent: 13, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Zip Code', - struct_parent: 13, - }, - { - 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', - struct_parent: 14, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Email Address', - struct_parent: 14, - }, - { - 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', - struct_parent: 15, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Phone Number', - struct_parent: 15, - }, - { - 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', - struct_parent: 16, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Date of Birth', - struct_parent: 16, - }, - { - 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', - struct_parent: 17, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Gender', - struct_parent: 17, - }, - { - type: '/Btn', - var_name: '', - field_dict: { - coordinates: [505.618988, 450.865997, 523.619019, 468.865997], - struct_parent: 18, - 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', - struct_parent: 18, - }, - { - type: '/Btn', - var_name: '', - field_dict: { - coordinates: [558.213013, 450.865997, 576.213013, 468.865997], - struct_parent: 19, - 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', - struct_parent: 19, - }, - { - 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', - struct_parent: 20, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Nat Amer', - struct_parent: 20, - }, - { - 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', - struct_parent: 21, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Asian', - struct_parent: 21, - }, - { - 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', - struct_parent: 22, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Blck Amer', - struct_parent: 22, - }, - { - 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', - struct_parent: 23, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Nat Haw Islander', - struct_parent: 23, - }, - { - 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', - struct_parent: 24, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'White', - struct_parent: 24, - }, - { - 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', - struct_parent: 25, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Other', - struct_parent: 25, - }, { - type: '/Btn', - var_name: '', - field_dict: { - coordinates: [98.414398, 349.662994, 116.414001, 367.662994], - field_instructions: 'U S Citizen by birth', - struct_parent: 26, - 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', - struct_parent: 26, - }, - { - type: '/Btn', - var_name: '', - field_dict: { - coordinates: [98.414398, 331.733002, 116.414001, 349.733002], - field_instructions: 'U S naturalized citizen', - struct_parent: 27, - 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', - struct_parent: 27, - }, - { - type: '/Btn', - var_name: '', - field_dict: { - coordinates: [98.414398, 313.006012, 116.414001, 331.006012], - field_instructions: 'Lawful Permenent Resident', - struct_parent: 29, - 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', - struct_parent: 29, - }, - { - type: '/Tx', - var_name: '', - field_dict: { - coordinates: [432.306, 331.979004, 489.425995, 352.92099], - field_instructions: 'date naturalization granted', - struct_parent: 28, - 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', - struct_parent: 28, - }, - { - 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)', - struct_parent: 30, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Residency Date_af_date', - struct_parent: 30, - }, - { - 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', - struct_parent: 31, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'A-Number', - struct_parent: 31, - }, - { - 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', - struct_parent: 32, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Convict-Date_af_date', - struct_parent: 32, - }, - { - 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', - struct_parent: 33, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'US District Court', - struct_parent: 33, - }, - { - 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', - struct_parent: 34, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Dist State', - struct_parent: 34, - }, - { - 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', - struct_parent: 36, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Docket No', - struct_parent: 36, - }, - { - 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', - struct_parent: 37, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Code Section', - struct_parent: 37, - }, - { - 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', - struct_parent: 38, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Code Section_2', - struct_parent: 38, - }, - { - 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', - struct_parent: 39, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'US District Court_2', - struct_parent: 39, - }, - { - 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', - struct_parent: 40, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'District 2', - struct_parent: 40, - }, - { - 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', - struct_parent: 42, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, - }, - page_number: 2, - path: 'Docket No 2', - struct_parent: 42, - }, - ], - '3': [ - { - 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', - struct_parent: 44, - font_info: '', - hidden: false, - child_fields: [], - num_children: 0, + type: 'input', + id: 'element-2', + data: { + label: 'Pattern 2', + initial: '', + required: true, + maxLength: 128, }, - page_number: 3, - path: 'App Date', - struct_parent: 44, - }, + } satisfies InputPattern, ], - }, - grouped_items: [], - raw_fields_pages: { - '0': "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 ", - '1': "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 ", - '2': '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 ', - '3': '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 ', - }, - }, - cache_id: 'Cache ID is not implemented yet', + } + ); }; diff --git a/packages/forms/src/services/submit-form.ts b/packages/forms/src/services/submit-form.ts index 55ab74db..3dd0d4e7 100644 --- a/packages/forms/src/services/submit-form.ts +++ b/packages/forms/src/services/submit-form.ts @@ -3,15 +3,15 @@ import { type Blueprint, type FormSession, type FormSessionId, - applyPromptResponse, - createFormOutputFieldData, createFormSession, - fillPDF, - sessionIsComplete, + defaultFormConfig, } from '../index.js'; import { FormServiceContext } from '../context/index.js'; +import { submitPage } from '../patterns/page-set/submit'; +import { downloadPackageHandler } from '../patterns/package-download/submit'; import { type FormRoute } from '../route-data.js'; +import { SubmissionRegistry } from '../submission'; export type SubmitForm = ( ctx: FormServiceContext, @@ -23,13 +23,24 @@ export type SubmitForm = ( Result<{ sessionId: FormSessionId; session: FormSession; - documents?: { + attachments?: { fileName: string; data: Uint8Array; }[]; }> >; +// Temportary location for the SubmissionRegistry. +const registry = new SubmissionRegistry(defaultFormConfig); +registry.registerHandler({ + handlerId: 'page-set', + handler: submitPage, +}); +registry.registerHandler({ + handlerId: 'package-download', + handler: downloadPackageHandler, +}); + export const submitForm: SubmitForm = async ( ctx, sessionId, @@ -49,16 +60,33 @@ export const submitForm: SubmitForm = async ( sessionId ); if (!sessionResult.success) { - return failure('Session not found'); + return sessionResult; + } + + const session: FormSession = route + ? { + ...sessionResult.data, + route, + } + : sessionResult.data; + + const actionString = formData['action']; + if (typeof actionString !== 'string') { + return failure(`Invalid action: ${actionString}`); + } + + const submitHandlerResult = registry.getHandlerForAction(form, actionString); + if (!submitHandlerResult.success) { + return failure(submitHandlerResult.error); } - //const session = getSessionFromStorage(ctx.storage, sessionId) || createFormSession(form); - // For now, the client-side is producing its own error messages. - // In the future, we'll want this service to return errors to the client. - const newSessionResult = applyPromptResponse(ctx.config, sessionResult.data, { - action: 'submit', + const { handler, pattern } = submitHandlerResult.data; + const newSessionResult = await handler(ctx.config, { + pattern, + session, data: formData, }); + if (!newSessionResult.success) { return failure(newSessionResult.error); } @@ -66,13 +94,13 @@ export const submitForm: SubmitForm = async ( const saveFormSessionResult = await ctx.repository.upsertFormSession({ id: sessionId, formId, - data: newSessionResult.data, + data: newSessionResult.data.session, }); if (!saveFormSessionResult.success) { return failure(saveFormSessionResult.error); } - /* TODO: consider whether this is necessary, or should happen elsewhere. */ + /* if (sessionIsComplete(ctx.config, newSessionResult.data)) { const documentsResult = await generateDocumentPackage( form, @@ -88,10 +116,12 @@ export const submitForm: SubmitForm = async ( documents: documentsResult.data, }); } + */ return success({ sessionId: saveFormSessionResult.data.id, - session: newSessionResult.data, + session: newSessionResult.data.session, + attachments: newSessionResult.data.attachments, }); }; @@ -110,33 +140,3 @@ const getFormSessionOrCreate = async ( } return success(sessionResult.data.data); }; - -const generateDocumentPackage = async ( - form: Blueprint, - formData: Record -) => { - const errors = new Array(); - const documents = new Array<{ fileName: string; data: Uint8Array }>(); - for (const document of form.outputs) { - const docFieldData = createFormOutputFieldData(document, formData); - const pdfDocument = await fillPDF(document.data, docFieldData); - if (!pdfDocument.success) { - errors.push(pdfDocument.error); - } else { - documents.push({ - fileName: document.path, - data: pdfDocument.data, - }); - } - } - if (errors.length > 0) { - return { - success: false as const, - error: errors.join('\n'), - }; - } - return { - success: true as const, - data: documents, - }; -}; diff --git a/packages/forms/src/session.ts b/packages/forms/src/session.ts index 3f9a31a1..a1df98e2 100644 --- a/packages/forms/src/session.ts +++ b/packages/forms/src/session.ts @@ -147,6 +147,7 @@ export const updateSession = ( export const sessionIsComplete = (config: FormConfig, session: FormSession) => { return Object.values(session.form.patterns).every(pattern => { + console.log('validating', pattern.type, pattern.id); const patternConfig = getPatternConfig(config, pattern.type); const value = getFormSessionValue(session, pattern.id); const isValidResult = validatePattern(patternConfig, pattern, value); diff --git a/packages/forms/src/submission.test.ts b/packages/forms/src/submission.test.ts new file mode 100644 index 00000000..8a9cd78e --- /dev/null +++ b/packages/forms/src/submission.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { defaultFormConfig } from './patterns'; +import { type SubmitHandler, SubmissionRegistry } from './submission'; +import { type Blueprint } from '.'; + +const testForm: Blueprint = { + summary: { + title: 'Test form', + description: 'A test form', + }, + root: 'pat-id-1', + patterns: { + 'pat-id-1': { + type: 'page', + id: 'pat-id-1', + data: {}, + }, + 'pat-id-2': { + type: 'page', + id: 'pat-id-2', + data: {}, + }, + }, + outputs: [], +}; + +describe('SubmissionRegistry', () => { + it('should register a submit handler once', () => { + const registry = new SubmissionRegistry(defaultFormConfig); + const handler1 = (() => {}) as unknown as SubmitHandler; + const handler2 = (() => {}) as unknown as SubmitHandler; + + registry.registerHandler({ + handlerId: 'handler1', + handler: handler1, + }); + registry.registerHandler({ + handlerId: 'handler2', + handler: handler2, + }); + + expect(() => + registry.registerHandler({ handlerId: 'handler1', handler: handler1 }) + ).toThrow('Submission handler with id handler1 already exists'); + const result1 = registry.getHandlerForAction( + testForm, + 'action/handler1/pat-id-1' + ); + expect(result1).toEqual({ + success: true, + data: { handler: handler1, pattern: testForm.patterns['pat-id-1'] }, + }); + const result2 = registry.getHandlerForAction( + testForm, + 'action/handler2/pat-id-2' + ); + expect(result2).toEqual({ + success: true, + data: { handler: handler2, pattern: testForm.patterns['pat-id-2'] }, + }); + const result3 = registry.getHandlerForAction( + testForm, + 'action/handler3/pat-id-3' + ); + expect(result3).toEqual({ + success: false, + error: 'Submission handler with id handler3 does not exist', + }); + }); +}); diff --git a/packages/forms/src/submission.ts b/packages/forms/src/submission.ts new file mode 100644 index 00000000..f1f220b1 --- /dev/null +++ b/packages/forms/src/submission.ts @@ -0,0 +1,85 @@ +import * as z from 'zod'; + +import { type Result, failure, success } from '@atj/common'; + +import { + type FormConfig, + type Pattern, + type PatternId, + getPattern, +} from './pattern'; +import { type FormSession } from './session'; +import { type Blueprint } from '.'; + +export type SubmitHandler

= ( + config: FormConfig, + opts: { + pattern: P; + session: FormSession; + data: Record; + } +) => Promise< + Result<{ + session: FormSession; + attachments?: { + fileName: string; + data: Uint8Array; + }[]; + }> +>; + +const actionRegEx = /^action\/([a-z0-9-]+)\/([a-z0-9-]+)$/; +const actionSchema = z + .string() + .regex(actionRegEx) + .transform(val => { + const [, handlerId, patternId] = val.match(actionRegEx) || []; + return { handlerId, patternId }; + }); + +export type ActionName = `action/${string}/${PatternId}`; +export const getActionString = (opts: { + handlerId: string; + patternId: string; +}): ActionName => { + return `action/${opts.handlerId}/${opts.patternId}`; +}; + +export class SubmissionRegistry { + constructor(private config: FormConfig) {} + + private handlers: Record = {}; + + registerHandler(opts: { handlerId: string; handler: SubmitHandler }) { + if (opts.handlerId in this.handlers) { + throw new Error( + `Submission handler with id ${opts.handlerId} already exists` + ); + } + this.handlers[opts.handlerId] = opts.handler; + } + + getHandlerForAction( + form: Blueprint, + action: string + ): Result<{ handler: SubmitHandler; pattern: Pattern }> { + const result = actionSchema.safeParse(action); + if (!result.success) { + return failure(`Invalid action: "${action}"`); + } + const handler = this.handlers[result.data.handlerId]; + if (handler === undefined) { + return failure( + `Submission handler with id ${result.data.handlerId} does not exist` + ); + } + const pattern = getPattern(form, result.data.patternId); + if (pattern === undefined) { + return failure(`Pattern with id ${result.data.patternId} does not exist`); + } + return success({ + handler, + pattern, + }); + } +} diff --git a/packages/server/src/lib/api-client.ts b/packages/server/src/lib/api-client.ts index f2819b38..374f79a1 100644 --- a/packages/server/src/lib/api-client.ts +++ b/packages/server/src/lib/api-client.ts @@ -23,7 +23,9 @@ export class FormServiceClient implements FormService { 'Content-Type': 'application/json', }, }); - return await response.json(); + const result = await response.json(); + console.log('addForm result', result); + return result; } async deleteForm(formId: string) { diff --git a/packages/server/src/lib/attachments.ts b/packages/server/src/lib/attachments.ts new file mode 100644 index 00000000..93a458c2 --- /dev/null +++ b/packages/server/src/lib/attachments.ts @@ -0,0 +1,45 @@ +export const createMultipartResponse = ( + pdfs: { fileName: string; data: Uint8Array }[] +): Response => { + const boundary = createBoundary(); + + // Array to store each part of the multipart message + const parts: Uint8Array[] = pdfs.flatMap(pdf => { + const headers = [ + `--${boundary}`, + `Content-Type: application/pdf`, + `Content-Disposition: attachment; filename="${pdf.fileName}"`, + '', + '', // empty line between headers and content + ].join('\r\n'); + + return [stringToUint8Array(headers), pdf.data, stringToUint8Array('\r\n')]; + }); + + // Final boundary to mark the end of the message + parts.push(stringToUint8Array(`--${boundary}--`)); + + // Concatenate all Uint8Array parts into a single Uint8Array body + const body = new Uint8Array( + parts.reduce((sum, part) => sum + part.length, 0) + ); + let offset = 0; + parts.forEach(part => { + body.set(part, offset); + offset += part.length; + }); + + return new Response(body, { + status: 200, + headers: { + 'Content-Type': `multipart/mixed; boundary=${boundary}`, + 'Content-Length': body.length.toString(), + }, + }); +}; + +const createBoundary = (): string => + `boundary_${Math.random().toString(36).slice(2)}`; + +const stringToUint8Array = (str: string): Uint8Array => + new TextEncoder().encode(str); diff --git a/packages/server/src/pages/forms/[id].astro b/packages/server/src/pages/forms/[id].astro index 489e93ca..dd8fd669 100644 --- a/packages/server/src/pages/forms/[id].astro +++ b/packages/server/src/pages/forms/[id].astro @@ -42,11 +42,9 @@ if (Astro.request.method === 'POST') { const formData = await Astro.request.formData(); const formDataObject: Record = {}; formData.forEach((value, key) => { - if (key === 'action') { - return; - } formDataObject[key] = value.toString(); }); + const submitFormResult = await ctx.formService.submitForm( sessionId, formId, @@ -59,7 +57,12 @@ if (Astro.request.method === 'POST') { }); } setFormSessionCookie(submitFormResult.data.sessionId); - return Astro.redirect(getNextUrl(formRoute)); + + if (submitFormResult.data.attachments) { + return createMultipartResponse(submitFormResult.data.attachments); + } + + return Astro.redirect(getNextUrl(submitFormResult.data.session.route)); } const formResult = await ctx.formService.getForm(formId); @@ -80,7 +83,7 @@ if (!sessionResult.success) { status: 500, }); } -setFormSessionCookie(sessionResult.data.id || 'fake-cookie-session-id'); +setFormSessionCookie(sessionResult.data.id); const formSession = sessionResult.data.data; --- diff --git a/packages/server/src/pages/forms/[id].test.ts b/packages/server/src/pages/forms/[id].test.ts index 8606ee4e..cf10c2c5 100644 --- a/packages/server/src/pages/forms/[id].test.ts +++ b/packages/server/src/pages/forms/[id].test.ts @@ -48,7 +48,7 @@ describe('Form page', () => { const formData = pom.getFormData(); const values = Object.fromEntries(formData.entries()); expect(values).toEqual({ - action: 'submit', + action: 'action/page-set/root', 'element-1': 'pattern one value', 'element-2': 'pattern two value', }); @@ -69,7 +69,7 @@ describe('Form page', () => { // in the future, when it hopefully is more feature-complete. const response = await submitForm(ctx, formId, pom.getFormData()); expect(response.status).toEqual(302); - expect(response.headers.get('Location')).toEqual(`/forms/${formId}`); + expect(response.headers.get('Location')).toEqual(`/forms/${formId}?page=0`); // Confirm that new session is stored with correct values // TODO: Due to the limitation mentioned above, we need to query the