Repository: digitalservicebund/a2j-rechtsantragstelle
Pull request #1862: feat(Upload Overview): Add Context and validation
Author: login: Spencer6497
Created: 2025-03-18T15:16:37Z | Merged: 2025-03-21T17:00:38Z

This PR introduces proper context and validation for document uploads in BerH.

- **remove belegeContext used for testing, add dokumente site to BerH abgabe xStateConfig**
- **adapt e2e test**
- **feat(Files Upload): Update App Component to match Strapi**
- **add berh dokumente context, tweak form data submission in array case**



Diff:
diff --git a/app/components/filesUpload/FileUploadInfo.tsx b/app/components/filesUpload/FileUploadInfo.tsx
index 2c2027516b..5be7af2c9b 100644
--- a/app/components/filesUpload/FileUploadInfo.tsx
+++ b/app/components/filesUpload/FileUploadInfo.tsx
@@ -1,6 +1,7 @@
 import DeleteIcon from "@digitalservicebund/icons/DeleteOutline";
 import InsertFileIcon from "@digitalservicebund/icons/InsertDriveFile";
 import classNames from "classnames";
+import { PDFFileMetadata } from "~/util/file/pdfFileSchema";
 import Button from "../Button";
 import { bytesToMegabytesString } from "./bytesToMegabytesString";
 
@@ -8,8 +9,7 @@ type FileUploadInfoProps = {
   inputName: string;
   jsAvailable: boolean;
   onFileDelete: (fieldName: string) => void;
-  fileName?: string;
-  fileSize?: number;
+  file: PDFFileMetadata;
   deleteButtonLabel?: string;
   hasError?: boolean;
 };
@@ -18,8 +18,7 @@ export const FileUploadInfo = ({
   inputName,
   jsAvailable,
   onFileDelete,
-  fileName,
-  fileSize,
+  file,
   deleteButtonLabel,
   hasError,
 }: FileUploadInfoProps) => {
@@ -36,12 +35,13 @@ export const FileUploadInfo = ({
       <div className="max-w-full flex justify-between items-center">
         <InsertFileIcon className="shrink-0 fill-gray-900" aria-hidden="true" />
         <p className="ds-body-01-reg text-black mr-8 ml-10 truncate">
-          {fileName}
+          {file.filename}
         </p>
         <p className="ds-body-01-reg text-gray-900">
-          {bytesToMegabytesString(fileSize ?? 0)}
+          {bytesToMegabytesString(file.fileSize ?? 0)}
         </p>
       </div>
+      {!hasError && <HiddenFileInputs inputName={inputName} file={file} />}
       <Button
         iconLeft={<DeleteIcon className="" aria-hidden="true" />}
         look="ghost"
@@ -55,3 +55,41 @@ export const FileUploadInfo = ({
     </div>
   );
 };
+
+export const HiddenFileInputs = ({
+  inputName,
+  file,
+}: {
+  inputName: string;
+  file: PDFFileMetadata;
+}) => {
+  return (
+    <>
+      <input
+        type="hidden"
+        name={`${inputName}.filename`}
+        value={file.filename}
+      />
+      <input
+        type="hidden"
+        name={`${inputName}.savedFileKey`}
+        value={file.savedFileKey}
+      />
+      <input
+        type="hidden"
+        name={`${inputName}.fileType`}
+        value={file.fileType}
+      />
+      <input
+        type="hidden"
+        name={`${inputName}.fileSize`}
+        value={file.fileSize}
+      />
+      <input
+        type="hidden"
+        name={`${inputName}.createdOn`}
+        value={file.createdOn}
+      />
+    </>
+  );
+};
diff --git a/app/components/filesUpload/FilesUpload.tsx b/app/components/filesUpload/FilesUpload.tsx
index fd7b3785c4..9555e3b2e2 100644
--- a/app/components/filesUpload/FilesUpload.tsx
+++ b/app/components/filesUpload/FilesUpload.tsx
@@ -31,7 +31,7 @@ const FilesUpload = ({
   const response = useActionData<
     ValidationErrorResponseData | Context | undefined
   >();
-  const { defaultValue } = useField(name, { formId });
+  const { defaultValue, error } = useField(name, { formId });
   const items: Array<PDFFileMetadata | undefined> =
     (response?.repopulateFields as Context | undefined)?.[name] ??
     (response as Context | undefined)?.[name] ??
@@ -46,15 +46,23 @@ const FilesUpload = ({
 
   useEffect(() => setJsAvailable(true), []);
 
-  const groupError: string | undefined = scopedErrors[name];
-
   const classes = classNames("w-full bg-white p-16", {
-    "bg-red-200 border border-red-900": !!groupError,
+    "bg-red-200 border border-red-900": !!error,
   });
 
   const showAddMoreButton =
-    (items.length < fileUploadLimit || items.length === 0) &&
-    Object.entries(scopedErrors).length === 0;
+    items.length === 0 ||
+    (items.length < fileUploadLimit &&
+      Object.entries(scopedErrors).length === 0);
+
+  /**
+   * if the non-JS component doesn't have a value, or has an error displayed, normally nothing is submitted in the FormData.
+   * We need to send at least an empty array, to display an array-level error that it's empty
+   */
+  const shouldSubmitEmptyArray =
+    !jsAvailable &&
+    (items.length === 0 ||
+      (items.length === 1 && Object.entries(scopedErrors).length > 0));
 
   return (
     title !== "" && (
@@ -83,9 +91,11 @@ const FilesUpload = ({
             )}
           </div>
         </div>
+        {shouldSubmitEmptyArray && (
+          <input type="hidden" name={"arrayPostfix"} value={name} />
+        )}
         <InputError id={errorId}>
-          {errorMessages?.find((err) => err.code === groupError)?.text ??
-            groupError}
+          {errorMessages?.find((err) => err.code === error)?.text ?? error}
         </InputError>
         {inlineNotices?.map((inlineNotice) => (
           <InlineNotice key={inlineNotice.title} {...inlineNotice} />
diff --git a/app/components/filesUpload/__test__/FileUploadInfo.test.tsx b/app/components/filesUpload/__test__/FileUploadInfo.test.tsx
index e32f30c058..a914f607b6 100644
--- a/app/components/filesUpload/__test__/FileUploadInfo.test.tsx
+++ b/app/components/filesUpload/__test__/FileUploadInfo.test.tsx
@@ -1,10 +1,18 @@
 import { fireEvent, render } from "@testing-library/react";
 import { FileUploadInfo } from "~/components/filesUpload/FileUploadInfo";
+import { PDFFileMetadata } from "~/util/file/pdfFileSchema";
 
 const deleteButtonLabel = "Delete";
 const fileName = "testfile1.pdf";
 const inputName = "belege[0]";
 const fileSize = 3145728;
+const mockFile: PDFFileMetadata = {
+  filename: fileName,
+  fileSize: fileSize,
+  fileType: "application/pdf",
+  createdOn: new Date().toString(),
+};
+
 describe("FileUploadInfo", () => {
   it("renders an icon, file name and size", () => {
     const { getByText, getByTestId } = render(
@@ -12,8 +20,7 @@ describe("FileUploadInfo", () => {
         jsAvailable={true}
         onFileDelete={vi.fn()}
         inputName={inputName}
-        fileName={fileName}
-        fileSize={fileSize}
+        file={mockFile}
         deleteButtonLabel={deleteButtonLabel}
       />,
     );
@@ -36,8 +43,7 @@ describe("FileUploadInfo", () => {
         jsAvailable={true}
         onFileDelete={vi.fn()}
         inputName={inputName}
-        fileName={fileName}
-        fileSize={fileSize}
+        file={mockFile}
         deleteButtonLabel={deleteButtonLabel}
         hasError={true}
       />,
@@ -54,8 +60,7 @@ describe("FileUploadInfo", () => {
           jsAvailable={true}
           onFileDelete={onFileDelete}
           inputName={inputName}
-          fileName={fileName}
-          fileSize={fileSize}
+          file={mockFile}
           deleteButtonLabel={deleteButtonLabel}
         />,
       );
@@ -73,8 +78,7 @@ describe("FileUploadInfo", () => {
           jsAvailable={false}
           onFileDelete={onFileDelete}
           inputName={inputName}
-          fileName={fileName}
-          fileSize={fileSize}
+          file={mockFile}
           deleteButtonLabel={deleteButtonLabel}
         />,
       );
diff --git a/app/components/filesUpload/__test__/FilesUpload.test.tsx b/app/components/filesUpload/__test__/FilesUpload.test.tsx
index 231025a737..1a869b855a 100644
--- a/app/components/filesUpload/__test__/FilesUpload.test.tsx
+++ b/app/components/filesUpload/__test__/FilesUpload.test.tsx
@@ -36,9 +36,11 @@ vi.mock("~/services/translations/translationsContext", () => ({
 }));
 
 let defaultValue: PDFFileMetadata[] = [];
+let error: string | undefined;
 vi.mock("remix-validated-form", () => ({
   useField: () => ({
     defaultValue,
+    error,
   }),
 }));
 
@@ -103,11 +105,7 @@ describe("FilesUpload", () => {
   });
 
   it("should display a top-level error", () => {
-    actionResponse = {
-      fieldErrors: {
-        [fieldName]: minimumFileError,
-      },
-    };
+    error = minimumFileError;
     const { getByText, queryByText } = renderFilesUpload();
     expect(getByText(minimumFileError)).toBeInTheDocument();
     expect(queryByText(addAnotherLabel)).not.toBeInTheDocument();
diff --git a/app/components/inputs/FileInput.tsx b/app/components/inputs/FileInput.tsx
index 52903caa8f..4a1d0841d7 100644
--- a/app/components/inputs/FileInput.tsx
+++ b/app/components/inputs/FileInput.tsx
@@ -46,15 +46,14 @@ export const FileInput = ({
           inputName={name}
           onFileDelete={onFileDelete}
           jsAvailable={jsAvailable}
-          fileName={selectedFile.filename}
-          fileSize={selectedFile.fileSize}
+          file={selectedFile}
           deleteButtonLabel={translations?.delete}
           hasError={!!error}
         />
       ) : (
         <label htmlFor={name} className={"flex flex-col md:flex-row"}>
           <input
-            name={name}
+            name={jsAvailable ? undefined : name}
             onChange={(event) => onFileUpload(name, event.target.files?.[0])}
             type="file"
             accept=".pdf"
diff --git a/app/domains/beratungshilfe/formular/abgabe/dokumente/context.ts b/app/domains/beratungshilfe/formular/abgabe/dokumente/context.ts
index d08488bf13..2d2de754eb 100644
--- a/app/domains/beratungshilfe/formular/abgabe/dokumente/context.ts
+++ b/app/domains/beratungshilfe/formular/abgabe/dokumente/context.ts
@@ -10,7 +10,7 @@ export const dokumenteContext = {
     .array(pdfFileMetaDataSchema)
     .nonempty()
     .max(fileUploadLimit, fileUploadErrorMap.fileLimitReached())
-    .optional(),
+    .optional(), // remove after e2e tests have been added
 };
 
 const _contextObject = z.object(dokumenteContext).partial();
diff --git a/app/domains/beratungshilfe/formular/index.ts b/app/domains/beratungshilfe/formular/index.ts
index af9aacd20f..51dbed9d69 100644
--- a/app/domains/beratungshilfe/formular/index.ts
+++ b/app/domains/beratungshilfe/formular/index.ts
@@ -17,6 +17,7 @@ import {
   ausgabenStrings,
   weiteresEinkommenStrings,
   eigentumZusammenfassungShowTotalWorthWarnings,
+  getDokumenteStrings,
 } from "./stringReplacements";
 import { beratungshilfeXstateConfig } from "./xstateConfig";
 import type { AbgabeContext } from "../../shared/formular/abgabe/context";
@@ -48,6 +49,7 @@ export const beratungshilfeFormular = {
     ...getMissingInformationStrings(context),
     ...ausgabenStrings(context),
     ...geldAnlagenStrings(context),
+    ...getDokumenteStrings(),
     ...weiteresEinkommenStrings(context),
   }),
 } satisfies Flow;
diff --git a/app/domains/beratungshilfe/formular/stringReplacements.ts b/app/domains/beratungshilfe/formular/stringReplacements.ts
index 7e425271f3..318e6319f0 100644
--- a/app/domains/beratungshilfe/formular/stringReplacements.ts
+++ b/app/domains/beratungshilfe/formular/stringReplacements.ts
@@ -77,6 +77,12 @@ export const ausgabenStrings = (context: BeratungshilfeFormularContext) => {
   };
 };
 
+export const getDokumenteStrings = () => {
+  return {
+    hasRechtsschutzversicherung: true,
+  };
+};
+
 export const getAnwaltStrings = (context: BeratungshilfeFormularContext) => {
   return { hasNoAnwalt: context.anwaltskanzlei !== "yes" };
 };
diff --git a/app/services/cms/components/StrapiFilesUpload.ts b/app/services/cms/components/StrapiFilesUpload.ts
index 754490ae20..7c3dd9be2b 100644
--- a/app/services/cms/components/StrapiFilesUpload.ts
+++ b/app/services/cms/components/StrapiFilesUpload.ts
@@ -9,7 +9,7 @@ const StrapiFilesUploadSchema = z
   .object({
     name: z.string(),
     title: z.string(),
-    description: z.string().optional(),
+    description: z.string().nullable(),
     inlineNotice: z.array(StrapiInlineNoticeSchema).optional(),
     errors: StrapiErrorRelationSchema,
   })
diff --git a/app/services/externalDataStorage/__test__/userFileS3Helpers.test.ts b/app/services/externalDataStorage/__test__/userFileS3Helpers.test.ts
index f0c11869c7..bf9e84a803 100644
--- a/app/services/externalDataStorage/__test__/userFileS3Helpers.test.ts
+++ b/app/services/externalDataStorage/__test__/userFileS3Helpers.test.ts
@@ -105,8 +105,9 @@ describe("userFileS3Helpers", () => {
         S3_DATA_STORAGE_BUCKET_NAME: "test-bucket",
       };
       setupFileMocks(mockSessionId, mockConfig);
-
-      await uploadUserFileToS3(mockCookie, mockFlowId, undefined);
+      await expect(
+        uploadUserFileToS3(mockCookie, mockFlowId, undefined),
+      ).rejects.toThrow();
 
       expect(sendSentryMessage).toHaveBeenCalledWith(
         `Error storing user uploaded file to S3 bucket: ${mockError.message}`,
diff --git a/app/services/externalDataStorage/userFileS3Helpers.ts b/app/services/externalDataStorage/userFileS3Helpers.ts
index 425adb4629..5a3928d2f5 100644
--- a/app/services/externalDataStorage/userFileS3Helpers.ts
+++ b/app/services/externalDataStorage/userFileS3Helpers.ts
@@ -42,6 +42,7 @@ export async function uploadUserFileToS3(
       `Error storing user uploaded file to S3 bucket: ${errorDescription}`,
       "error",
     );
+    throw error;
   }
 }
 
@@ -67,5 +68,6 @@ export async function deleteUserFileFromS3(
       `Error storing user uploaded file to S3 bucket: ${errorDescription}`,
       "error",
     );
+    throw error;
   }
 }
diff --git a/app/services/flow/server/fileUploadHelpers.server.ts b/app/services/flow/server/fileUploadHelpers.server.ts
index a24a49de42..447959990b 100644
--- a/app/services/flow/server/fileUploadHelpers.server.ts
+++ b/app/services/flow/server/fileUploadHelpers.server.ts
@@ -3,6 +3,8 @@ import {
   unstable_parseMultipartFormData,
 } from "@remix-run/node";
 import { withZod } from "@remix-validated-form/with-zod";
+// import get from "lodash/get";
+import pickBy from "lodash/pickBy";
 import {
   ErrorResult,
   SuccessResult,
@@ -36,9 +38,14 @@ export async function uploadUserFile(
   const { fieldName, inputIndex } = splitFieldName(inputName);
   const file = await parseFileFromFormData(request, inputName);
   const fileMeta = convertFileToMetadata(file);
-  const scopedContext = Object.fromEntries(
-    Object.entries(getContext(flowId)).filter(([key]) => key === fieldName),
-  );
+  /**
+   * Need to scope the context, otherwise we validate against the entire context,
+   * of which we only have partial data at this point
+   */
+  const scopedContext = pickBy(
+    getContext(flowId),
+    (_val, key) => key === fieldName,
+  ) as Record<string, ZodTypeAny>;
   const validationResult = await validateUploadedFile(
     inputName,
     fileMeta,
diff --git a/app/services/validation/stepValidator/buildStepValidator.ts b/app/services/validation/stepValidator/buildStepValidator.ts
index ecda9ee407..4b631fdd84 100644
--- a/app/services/validation/stepValidator/buildStepValidator.ts
+++ b/app/services/validation/stepValidator/buildStepValidator.ts
@@ -22,6 +22,9 @@ export function buildStepValidator(
         throw Error(`No schema found for ${arrayFieldName as string}`);
       }
       fieldValidators[fieldName] = objectSchemas[arrayFieldName];
+    } else if (new RegExp(/\[\d+\]/).test(fieldName)) {
+      const arrayName = fieldName.split("[")[0];
+      fieldValidators[arrayName] = schemas[arrayName];
     } else {
       const stepOrFieldName = fieldName.split(".")[0];
       if (!isKeyOfObject(stepOrFieldName, schemas)) {
diff --git a/app/util/file/pdfFileSchema.ts b/app/util/file/pdfFileSchema.ts
index 588f3bcb6a..9ae7b96bb2 100644
--- a/app/util/file/pdfFileSchema.ts
+++ b/app/util/file/pdfFileSchema.ts
@@ -18,9 +18,15 @@ export const pdfFileMetaDataSchema = z.object({
     .string()
     .regex(/application\/pdf/, fileUploadErrorMap.wrongFileType),
   fileSize: z
-    .number()
-    .max(TEN_MB_IN_BYTES, fileUploadErrorMap.fileSizeTooBig)
-    .min(1, fileUploadErrorMap.fileRequired),
+    // Need to have string here as html input types (our hidden inputs) always submit strings
+    .string()
+    .transform((str) => parseInt(str))
+    .or(
+      z
+        .number()
+        .max(TEN_MB_IN_BYTES, fileUploadErrorMap.fileSizeTooBig)
+        .min(1, fileUploadErrorMap.fileRequired),
+    ),
   createdOn: z.string(),
 });
 
diff --git a/app/util/filterFormData.ts b/app/util/filterFormData.ts
index b606885ba2..143a919361 100644
--- a/app/util/filterFormData.ts
+++ b/app/util/filterFormData.ts
@@ -1,7 +1,12 @@
 export const filterFormData = (formData: FormData) =>
   // Note: fromEntries() reduces same-named form fields to the last one
   Object.fromEntries(
-    Array.from(formData.entries()).filter(
-      ([key, val]) => !key.startsWith("_") && typeof val === "string",
-    ),
+    Array.from(formData.entries())
+      .filter(([key, val]) => !key.startsWith("_") && typeof val === "string")
+      .map(([key, val]) => {
+        if (key === "arrayPostfix") {
+          return [val, []];
+        }
+        return [key, val];
+      }),
   );


Reviews:
user: login: frederike-ramin
body: OMG, did you do it without multi-field validation?! 😍 This solution 👌 - I love it. Just love it. With the hidden fields, it feels super nice, completely hacked the HTML world here, but with our system it actually works super well 🙏 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T17:51:18Z
user: login: Spencer6497
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T15:37:39Z
user: login: Spencer6497
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T15:44:59Z
user: login: Spencer6497
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T16:09:10Z
user: login: Spencer6497
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T16:27:51Z
user: login: frederike-ramin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T12:50:35Z

Review comments:
diff_hunk: @@ -46,15 +46,14 @@ export const FileInput = ({
           inputName={name}
           onFileDelete={onFileDelete}
           jsAvailable={jsAvailable}
-          fileName={selectedFile.filename}
-          fileSize={selectedFile.fileSize}
+          file={selectedFile}
           deleteButtonLabel={translations?.delete}
           hasError={!!error}
         />
       ) : (
         <label htmlFor={name} className={"flex flex-col md:flex-row"}>
           <input
-            name={name}
+            name={jsAvailable ? undefined : name}
path: app/components/inputs/FileInput.tsx
user: login: frederike-ramin
body: Why don't we want the name if JS is available? I gueass we don't need it, but does it "hurt"?
created_at: 2025-03-20T16:39:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,16 @@
+import { z } from "zod";
+import {
+  pdfFileMetaDataSchema,
+  fileUploadLimit,
+  fileUploadErrorMap,
+} from "~/util/file/pdfFileSchema";
+
+export const dokumenteContext = {
+  rechtsschutzversicherungBeweis: z
+    .array(pdfFileMetaDataSchema)
+    .nonempty()
+    .max(fileUploadLimit, fileUploadErrorMap.fileLimitReached()),
+};
path: app/domains/beratungshilfe/formular/abgabe/dokumente/context.ts
user: login: frederike-ramin
body: This (and the "hasRechtsschutzversicherung" string) are mocked data to show something, right? Can we mark it as such to not cause confusion later?
created_at: 2025-03-20T16:45:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,7 +1,12 @@
 export const filterFormData = (formData: FormData) =>
   // Note: fromEntries() reduces same-named form fields to the last one
   Object.fromEntries(
-    Array.from(formData.entries()).filter(
-      ([key, val]) => !key.startsWith("_") && typeof val === "string",
-    ),
+    Array.from(formData.entries())
+      .filter(([key, val]) => !key.startsWith("_") && typeof val === "string")
+      .map(([key, val]) => {
+        if (key === "arrayPostfix") {
+          return [val, []];
+        }
path: app/util/filterFormData.ts
user: login: frederike-ramin
body: I'm wondering if it would be possible to have the hidden input with the key `val` already and and the value be `"[]"`? That then probably causes all kinds of other problems, mh? Because we can't differentiate them from a user typing in `"[]"` anymore?
created_at: 2025-03-20T16:51:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -18,9 +18,15 @@ export const pdfFileMetaDataSchema = z.object({
     .string()
     .regex(/application\/pdf/, fileUploadErrorMap.wrongFileType),
   fileSize: z
-    .number()
-    .max(TEN_MB_IN_BYTES, fileUploadErrorMap.fileSizeTooBig)
-    .min(1, fileUploadErrorMap.fileRequired),
+    // Need to have string here as html input types (our hidden inputs) always submit strings
+    .string()
+    .transform((str) => parseInt(str))
+    .or(
+      z
+        .number()
+        .max(TEN_MB_IN_BYTES, fileUploadErrorMap.fileSizeTooBig)
+        .min(1, fileUploadErrorMap.fileRequired),
+    ),
path: app/util/file/pdfFileSchema.ts
user: login: frederike-ramin
body: It apparently works, but I'm wondering how: Having `or` here should make it possible when sending a string to not validate the size. But with or without JS, the validation works correctly. Is it because the hidden inputs you mean here are only submitted when the whole page is submitted and are thus already validated?
created_at: 2025-03-20T17:50:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -46,15 +46,14 @@ export const FileInput = ({
           inputName={name}
           onFileDelete={onFileDelete}
           jsAvailable={jsAvailable}
-          fileName={selectedFile.filename}
-          fileSize={selectedFile.fileSize}
+          file={selectedFile}
           deleteButtonLabel={translations?.delete}
           hasError={!!error}
         />
       ) : (
         <label htmlFor={name} className={"flex flex-col md:flex-row"}>
           <input
-            name={name}
+            name={jsAvailable ? undefined : name}
path: app/components/inputs/FileInput.tsx
user: login: Spencer6497
body: this input would be the `Weitere Datei Hinzufügen` button, which, in the JS case, when it has a name gets parsed against the schema like an actual file input (and fails because it's really a dummy input)
created_at: 2025-03-21T15:37:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,16 @@
+import { z } from "zod";
+import {
+  pdfFileMetaDataSchema,
+  fileUploadLimit,
+  fileUploadErrorMap,
+} from "~/util/file/pdfFileSchema";
+
+export const dokumenteContext = {
+  rechtsschutzversicherungBeweis: z
+    .array(pdfFileMetaDataSchema)
+    .nonempty()
+    .max(fileUploadLimit, fileUploadErrorMap.fileLimitReached()),
+};
path: app/domains/beratungshilfe/formular/abgabe/dokumente/context.ts
user: login: Spencer6497
body: i think these will actually eventually be used; they were one of the first Belege on the list so I used them as a proof-of-concept
created_at: 2025-03-21T15:44:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,7 +1,12 @@
 export const filterFormData = (formData: FormData) =>
   // Note: fromEntries() reduces same-named form fields to the last one
   Object.fromEntries(
-    Array.from(formData.entries()).filter(
-      ([key, val]) => !key.startsWith("_") && typeof val === "string",
-    ),
+    Array.from(formData.entries())
+      .filter(([key, val]) => !key.startsWith("_") && typeof val === "string")
+      .map(([key, val]) => {
+        if (key === "arrayPostfix") {
+          return [val, []];
+        }
path: app/util/filterFormData.ts
user: login: Spencer6497
body: yeah exactly that, we would still have to do some form of postprocessing and it would make that trickier :/
created_at: 2025-03-21T16:09:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -18,9 +18,15 @@ export const pdfFileMetaDataSchema = z.object({
     .string()
     .regex(/application\/pdf/, fileUploadErrorMap.wrongFileType),
   fileSize: z
-    .number()
-    .max(TEN_MB_IN_BYTES, fileUploadErrorMap.fileSizeTooBig)
-    .min(1, fileUploadErrorMap.fileRequired),
+    // Need to have string here as html input types (our hidden inputs) always submit strings
+    .string()
+    .transform((str) => parseInt(str))
+    .or(
+      z
+        .number()
+        .max(TEN_MB_IN_BYTES, fileUploadErrorMap.fileSizeTooBig)
+        .min(1, fileUploadErrorMap.fileRequired),
+    ),
path: app/util/file/pdfFileSchema.ts
user: login: Spencer6497
body: yes that's correct!
created_at: 2025-03-21T16:27:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,16 @@
+import { z } from "zod";
+import {
+  pdfFileMetaDataSchema,
+  fileUploadLimit,
+  fileUploadErrorMap,
+} from "~/util/file/pdfFileSchema";
+
+export const dokumenteContext = {
+  rechtsschutzversicherungBeweis: z
+    .array(pdfFileMetaDataSchema)
+    .nonempty()
+    .max(fileUploadLimit, fileUploadErrorMap.fileLimitReached()),
+};
path: app/domains/beratungshilfe/formular/abgabe/dokumente/context.ts
user: login: frederike-ramin
body: Ah okay, then the only this that's mocked is the `hasRechtsschutzversicherung: true,` value atm. 👌 
created_at: 2025-03-24T12:50:35Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-03-21T16:46:51Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=digitalservicebund_a2j-rechtsantragstelle&pullRequest=1862) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=digitalservicebund_a2j-rechtsantragstelle&pullRequest=1862&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=digitalservicebund_a2j-rechtsantragstelle&pullRequest=1862&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=digitalservicebund_a2j-rechtsantragstelle&pullRequest=1862&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [76.6% Coverage on New Code](https://sonarcloud.io/component_measures?id=digitalservicebund_a2j-rechtsantragstelle&pullRequest=1862&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=digitalservicebund_a2j-rechtsantragstelle&pullRequest=1862&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=digitalservicebund_a2j-rechtsantragstelle&pullRequest=1862)


author_association: NONE