{"record":{"id":"5e8de0507b21711d","repo":"koala73/worldmonitor","slug":"invalid-company-patch","errorCode":"INVALID_COMPANY_PATCH","errorMessage":"INVALID_COMPANY_PATCH","messagePattern":"INVALID_COMPANY_PATCH","errorType":"validation","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/companyMonitoring/companies.ts","lineNumber":211,"sourceCode":"  handler: async (ctx, args) => {\n    const account = await requireActiveAccount(ctx, args.ownerUserId);\n    const company = await ctx.db\n      .query(\"companyMonitoringCompanies\")\n      .withIndex(\"by_account_companyId\", (q) =>\n        q.eq(\"ownerAccountId\", account.logicalAccountId).eq(\"companyId\", args.companyId),\n      )\n      .unique();\n    if (!company || company.lifecycle === \"removed\" || !company.name || !company.domicileCountry) {\n      throw new ConvexError(\"NOT_FOUND\");\n    }\n    const patch = args.patch;\n    const addClaimInputs = patch.addClaims ?? [];\n    const removeClaimInputs = patch.removeClaimIds ?? [];\n    if (\n      addClaimInputs.length > COMPANY_MONITORING_LIMITS.maxClaimsPerCompany ||\n      removeClaimInputs.length > COMPANY_MONITORING_LIMITS.maxClaimsPerCompany\n    ) {\n      throw new ConvexError(\"INVALID_COMPANY_PATCH\");\n    }\n\n    const hasName = Object.prototype.hasOwnProperty.call(patch, \"name\");\n    const hasDomicile = Object.prototype.hasOwnProperty.call(patch, \"domicileCountry\");\n    const hasCustomerReference = Object.prototype.hasOwnProperty.call(patch, \"customerReference\");\n    const normalizedFields = normalizeMonitoredCompanyInput({\n      name: hasName ? patch.name! : company.name,\n      domicileCountry: hasDomicile ? patch.domicileCountry! : company.domicileCountry,\n      customerReference: hasCustomerReference\n        ? patch.customerReference\n        : company.customerReference,\n    });\n    if (hasCustomerReference && normalizedFields.customerReference) {\n      const conflict = await findNoopByCustomerReference(\n        ctx,\n        account.logicalAccountId,\n        normalizedFields.customerReference,\n      );","sourceCodeStart":193,"sourceCodeEnd":229,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/convex/companyMonitoring/companies.ts#L193-L229","documentation":"Thrown by the company update mutation (convex/companyMonitoring/companies.ts:211) when patch.addClaims or patch.removeClaimIds exceeds COMPANY_MONITORING_LIMITS.maxClaimsPerCompany. Each update request may add or remove at most that many claims in one call.","triggerScenarios":"Submitting a bulk claim edit (e.g. import 200 claims at once) in a single patch; a select-all-then-remove UI action generating a huge removeClaimIds array; concatenating multiple edits into one patch to save round trips.","commonSituations":"Spreadsheet/CSV importers mapping every row to addClaims; UIs with no client-side cap on multi-select; limit value lowered in a deploy while older clients still send big batches.","solutions":["Split the edit into chunks of at most maxClaimsPerCompany entries per mutation call","Read COMPANY_MONITORING_LIMITS from the shared contract package and enforce the same cap in the UI (disable submit beyond it)","For large imports, use the dedicated import path (clientImportId metadata) instead of patch","Show the remaining claim budget to the user before they assemble the patch"],"exampleFix":"// before\nawait api.companyMonitoring.updateCompany({\n  ownerUserId, companyId,\n  patch: { addClaims: allImportedClaims }, // 200+ claims -> INVALID_COMPANY_PATCH\n});\n\n// after\nconst CHUNK = COMPANY_MONITORING_LIMITS.maxClaimsPerCompany;\nfor (let i = 0; i < allImportedClaims.length; i += CHUNK) {\n  await api.companyMonitoring.updateCompany({\n    ownerUserId, companyId,\n    patch: { addClaims: allImportedClaims.slice(i, i + CHUNK) },\n  });\n}","handlingStrategy":"validation","validationCode":"const CHUNK = COMPANY_MONITORING_LIMITS.maxClaimsPerCompany; // from the shared contract\nfunction chunkClaims<T>(items: T[]): T[][] {\n  const out: T[][] = [];\n  for (let i = 0; i < items.length; i += CHUNK) out.push(items.slice(i, i + CHUNK));\n  return out;\n}\nif (patch.addClaims.length > CHUNK || (patch.removeClaimIds ?? []).length > CHUNK) {\n  for (const addChunk of chunkClaims(patch.addClaims)) {\n    await api.companyMonitoring.updateCompany({ ownerUserId, companyId, patch: { addClaims: addChunk } });\n  }\n} else {\n  await api.companyMonitoring.updateCompany({ ownerUserId, companyId, patch });\n}","typeGuard":null,"tryCatchPattern":"try {\n  await api.companyMonitoring.updateCompany({ ownerUserId, companyId, patch });\n} catch (err) {\n  if (err instanceof ConvexError && err.data === \"INVALID_COMPANY_PATCH\") {\n    const CHUNK = COMPANY_MONITORING_LIMITS.maxClaimsPerCompany;\n    for (let i = 0; i < patch.addClaims.length; i += CHUNK) {\n      await api.companyMonitoring.updateCompany({ ownerUserId, companyId, patch: { addClaims: patch.addClaims.slice(i, i + CHUNK) } });\n    }\n    return;\n  }\n  throw err;\n}","preventionTips":["Enforce maxClaimsPerCompany in the UI (cap multi-select and import previews)","Import via the dedicated import path (clientImportId) for large claim sets, not patch","Import COMPANY_MONITORING_LIMITS from the shared contract so client and server caps match","Show the claim budget remaining before the user assembles the patch"],"tags":["convex","limits","claims","batching","validation"],"backgroundTag":"payload-limit-exceeded","analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}