{"record":{"id":"73029eb5c633e628","repo":"koala73/worldmonitor","slug":"customer-reference-conflict","errorCode":"CUSTOMER_REFERENCE_CONFLICT","errorMessage":"CUSTOMER_REFERENCE_CONFLICT","messagePattern":"CUSTOMER_REFERENCE_CONFLICT","errorType":"exception","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/companyMonitoring/companies.ts","lineNumber":231,"sourceCode":"\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      );\n      if (conflict && conflict.companyId !== company.companyId) {\n        throw new ConvexError(\"CUSTOMER_REFERENCE_CONFLICT\");\n      }\n    }\n\n    const currentClaims = await ctx.db\n      .query(\"companyMonitoringClaims\")\n      .withIndex(\"by_account_company\", (q) =>\n        q.eq(\"ownerAccountId\", account.logicalAccountId).eq(\"companyId\", company.companyId),\n      )\n      .collect();\n    const currentById = new Map(currentClaims.map((claim) => [claim.claimId, claim]));\n    const removeIds = new Set(\n      removeClaimInputs.map((claimId) => assertLogicalId(\"claim\", String(claimId))),\n    );\n    for (const claimId of removeIds) {\n      if (!currentById.has(claimId)) throw new ConvexError(\"CLAIM_NOT_FOUND\");\n    }\n    if (hasCustomerReference && normalizedFields.customerReference !== company.customerReference) {\n      for (const claim of currentClaims) {","sourceCodeStart":213,"sourceCodeEnd":249,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/convex/companyMonitoring/companies.ts#L213-L249","documentation":"Thrown by the company update mutation (convex/companyMonitoring/companies.ts:231) when the patch sets a customerReference that is already used by a different active or paused company on the same account (findNoopByCustomerReference). Customer references must be unique per account across non-removed companies.","triggerScenarios":"Editing company A's customerReference to the value already carried by company B; merging duplicates by relabeling instead of removing; pasting a reference from another company's record; re-importing a reference that still belongs to a paused company.","commonSituations":"ERP/CRM syncs where external refs get reassigned but the old company keeps its copy; import files with duplicated reference columns; paused companies still holding the reference (pause does not free it — only removal does).","solutions":["Clear or change the customerReference on the other company first (or remove that company), then retry the patch","Validate uniqueness client-side before submitting: search the account's companies for the reference","Make import tooling detect duplicate references in the source file before any mutation runs","Remember lifecycle: active and paused both conflict; only removed releases the reference"],"exampleFix":"// before\nawait api.companyMonitoring.updateCompany({\n  ownerUserId, companyId,\n  patch: { customerReference: \"ACME-001\" }, // already on another company\n});\n\n// after\nconst existing = await api.companyMonitoring.listCompanies({ ownerUserId });\nconst clash = existing.find((c) => c.customerReference === \"ACME-001\" && c.companyId !== companyId);\nif (clash) throw new UserFacingError(`Reference already used by ${clash.name}`);\nawait api.companyMonitoring.updateCompany({ ownerUserId, companyId, patch: { customerReference: \"ACME-001\" } });","handlingStrategy":"validation","validationCode":"const companies = await api.companyMonitoring.listCompanies({ ownerUserId });\nconst clash = companies.find(\n  (c) => c.customerReference === nextReference && c.companyId !== companyId && (c.lifecycle === \"active\" || c.lifecycle === \"paused\"),\n);\nif (clash) {\n  throw new UserFacingError(`Reference already used by \"${clash.name}\" (${clash.lifecycle}).`);\n}\nawait api.companyMonitoring.updateCompany({ ownerUserId, companyId, patch: { customerReference: nextReference } });","typeGuard":"function isCustomerReferenceFree(companies: { companyId: string; customerReference?: string; lifecycle: string }[], nextReference: string, companyId: string): boolean {\n  return !companies.some(\n    (c) => c.customerReference === nextReference && c.companyId !== companyId && (c.lifecycle === \"active\" || c.lifecycle === \"paused\"),\n  );\n}","tryCatchPattern":"try {\n  await api.companyMonitoring.updateCompany({ ownerUserId, companyId, patch: { customerReference: nextReference } });\n} catch (err) {\n  if (err instanceof ConvexError && err.data === \"CUSTOMER_REFERENCE_CONFLICT\") {\n    ui.fieldError(\"customerReference\", \"Already used by another active or paused company.\");\n    return;\n  }\n  throw err;\n}","preventionTips":["Validate reference uniqueness client-side against the current company list before submit","Deduplicate reference columns in import files before running mutations","Remember paused companies still hold their reference; only removal frees it","When relabeling during a merge, clear the reference on the old company first"],"tags":["convex","uniqueness","conflict","customer-reference","mutation"],"backgroundTag":"unique-constraint-violation","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"}