Skip to content

fix(case): stop requiring an account on the case form, so a web case can be claimed - #2116

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-2060-item-4-claim-web-case
Oct 15, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-2060-item-4-claim-web-case

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #2060 (item 4, finding B-03). Items 5 and 7 to 18 of the card, and item 1's delete half, are not touched here; the card stays open.

Clause-②: no

What was wrong (measured on main 421bd1d8, @objectstack/* 18.0.0-next.1)

A service agent could not save a web case as In Progress. The case form (src/service/views/case.view.ts:432 on base) marked Account required. A web-to-case submission has no account, and crm_case.crm_account is optional on the object.

Measured on a live server (port 4164, fresh DB, pnpm demo:staff). Each case was submitted anonymously at /_console/f/support (POST /api/v1/forms/support/submit answered 201):

  • Routed path (the usual one since fix(intake): route web leads and cases, flag web duplicates (runAs: 'system') #2103). With service.agent holding the Service Agent position, the web case (CASE-00039) was routed to that agent: owner_id = the agent, crm_account: null, status: new. The agent opened it, chose More actions → Edit, set Status to In Progress and clicked Update. The toast read "Please check the highlighted fields: Account", the field read "Account is required", and no PATCH was sent.
  • Unowned path (the claim in content/docs/service/cases.mdx, "Claiming a case out of triage"). With the pool empty at submission, the case (CASE-00040) stayed unowned. After the pool was staffed and case_unassigned_triage_sharing was evaluated, the agent could read it. The same Edit → In Progress → Update was refused the same way, with no PATCH sent.
  • Server control. On another unowned web case (CASE-00041), the agent's PATCH {status: 'in_progress'} answered 200, and the case became the agent's (owner_id = agent, crm_account: null). The only thing that refused the claim was the form's own required flag.

#2103 and #2115 do not change this. The agent now usually meets a web case as its routed owner rather than unowned in triage, and both paths stopped at the same field.

Provenance (H2). git log -S "field: 'account', required: true" leads to 344c5f57 (2026-05-22, "Reset hotcrm to framework/app-crm baseline"). It came in with the imported baseline, and 75730205 (the crm_ prefix) and 36b27dd8 (#1910, the package move) only carried it along. No ruling or card asked for it.

Conditional requiredness (H4). In the installed @objectstack/spec 18.0.0-next.1, the form field schema (FormFieldBaseSchema, dist/ui/index.mjs) has only a boolean required and a client-only visibleWhen. It has no requiredWhen. The only server-enforced conditional requirement is the object field's requiredWhen, and the claim keeps crm_case.crm_account off limits. So the route is plain removal, as #2099 did for the lead owner.

What changed

  • src/service/views/case.view.ts: the form's crm_account entry is now the bare field name, without required: true. It stays on the form, with a comment saying why.
  • test/case-create-form-narrowing.test.ts: one pin in the suite that already owns this form's field contract. crm_account is on the form and not required.
  • .changeset/2060-item-4-claim-web-case.md.

The object field, the web-to-case form and the docs are unchanged. No sentence in content/docs/service/cases*.mdx becomes false. The claim section now describes what happens.

Downstream of a case with no account (H3)

This form is also the create dialog, so an internal user can now save a case with no account. Every reader of the account already handles a missing one, because web cases have never had one:

  • case_sla_defaults (case.hook.ts) reads the account's tier only when an id is present, and otherwise uses the smb column.
  • case_status_side_effects opens the account-owner escalation task only && accountId. cases.mdx already says "only for cases that have an account".
  • case_resolution_account_activity returns early with no account.
  • case_escalation_reassign and the escalation flow do not read the account.
  • No case report or the case_metrics dataset groups by account.

Measured after the change, on the same server:

  • Claim, unowned (console): CASE-00044, Edit → In Progress → Update. The toast read "Case updated successfully" and the PATCH answered 200. Re-read: owner_id = agent, status: in_progress, crm_account: null. The case left the triage list.
  • Claim, routed (console): CASE-00039 gave the same result ("Case updated successfully", PATCH 200, in_progress).
  • Create, no account (console, as service.agent): New → subject, description, Medium → Create. "Case created successfully", POST 201, crm_account: null, a 48 h SLA (the smb cell).
  • Create, with an account: "Case created successfully", POST 201, crm_account set, a 36 h SLA (that account's tier).
  • Escalation, no account (API, as the agent): 200. The case went to the service manager and escalated_from was stamped. No follow-up task was created.
  • Resolution, no account (API): 200.

With no account picked, the form's Contact picker reads "Select Account first" (crm_contact.dependsOn: ['crm_account']). That is the existing cascade, and this PR does not change it.

Tests

  • pnpm exec vitest run test/case-create-form-narrowing.test.ts: 17 passed (17).
  • Ablation. This used ablation-replace in wrap mode under the verify lock, with a trap restore. It put { field: 'crm_account', required: true } back: anchor 1 → 0, blob 8aa274c3 → c28b8bcc. The new pin failed with "the case form demands an account again: expected true to be false", 1 failed and 16 passed. It turned red, as predicted. The restore left the blob equal to HEAD (8aa274c3), and git diff HEAD was empty. The test imports the view from src, so dist was not involved.

Gates (all at 41a967fa, exit codes captured before any pipe)

  • pnpm validate: 0, "✓ Validation passed", "Data: 18 Objects 358 Fields 5 Picklists"
  • pnpm typecheck: 0. tsc --listFiles includes the edited test.
  • pnpm lint: 0, "18 suggestion(s)", none naming a file or key of this PR
  • pnpm lint:i18n-gate: 0, "✓ i18n lint gate: 0 i18n/missing-* issues"
  • pnpm hygiene: 0, "✓ source hygiene clean"
  • pnpm hygiene:tokens: 0, "✓ source token ratchet clean"
  • pnpm build: 0, "✓ Build complete", the same 8 author-time warnings as base
  • pnpm test: 0, "Test Files 181 passed (181)", "Tests 3687 passed | 1 skipped (3688)"

validate, lint, build and test ran under os-verify-lock. Playwright e2e is CI's.

Acceptance notes (not filed)

  • A platform finding, reported to the seat and not routed around here. On every boot, plugin-security's claimSeedOwnershipOnSettle / claimSeedOwnership re-owns every row whose owner_id is null to the platform admin, not only seeded rows. CASE-00045 was an unowned web case that the agent could read in triage (200). After a plain objectstack start restart it was owned by admin@objectos.ai, and the agent got 404. CASE-00040, -42 and -43 went the same way on a dev auto-restart. This empties Unassigned — triage whenever the server restarts.
  • The public support form shows Priority as required ("Required: Priority", no option preselected), although crm_case.priority declares low as default: true and case.view.ts says an unset one is filled by that default. A guest has to pick one. Carrier: Browser pass #2058 — HotCRM-side defects (18 items: accepted quote still takes lines, half-converted leads, agents losing their cases, unreachable Sales Home, …) — list card, split per item at dispatch #2060 item 7 (form required flags that block a default).
  • The triage grant is not re-materialised when someone takes the Service Agent position later. In this measurement it needed POST /api/v1/sharing/rules/ID/evaluate. scripts/demo-staff.ts already documents that staffing needs this step. Observation only.

Generated by Claude Code

…can be claimed

A web-to-case submission carries no account, and crm_case.crm_account is
optional on the object, but the case form marked it required. That form
is also the edit form an agent claims a case through (a move to In
Progress), so the console refused the claim client-side with "Please
check the highlighted fields: Account" while the server accepted the same
save. The form keeps Account, without the required flag, and the
narrowing suite pins that.

Claude-Session: https://claude.ai/code/session_01P8mZuzuyZAHSrgQKmv6ASv
Co-authored-by: Claude <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
hotcrm Ignored Ignored Oct 15, 2026 3:28am UTC

Request Review

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

复核人:repo:hotcrm 执行席(#1353)· 判决 ACCEPT · 钉 head 41a967facddbc89888fad6b2cd57e02ee4f0aa06 · 2026-10-15T03:41Z

形态:Part of #2060(第 4 项,B-03),无关闭关键词,Clause-②: no;3 文件,+26/−1 = 27 行(派发预算约 60);路径面无 governed 面 → 非 Tier H,席位落地。origin/main 421bd1d8 是 head 的祖先。提交 trailer 为无模型名的 Co-authored-by: Claude + Claude-Session。

前提实测:派发令 H1/H2/H4 成立,走「直接去掉」路线。diff 逐行读过:

  • case.view.ts:案例表单的 crm_account 由 { field: 'crm_account', required: true } 改为纯 'crm_account'——字段仍在表单上,只是不再必填;注释写明理由(对象上本就可选、web 提交不带客户)。
  • H2:必填标记来自导入基线 344c5f57(2026-05-22 重置为 framework/app-crm 基线),之后两次改名/搬迁原样带过,没有任何裁决或卡片要求过它。
  • H4:spec 18.0.0-next.1 的 FormFieldBaseSchema 只有布尔 required,没有 requiredWhen,故无条件必填可选,只能去掉。
  • H3(下游读者都能处理无客户):席位复读 _case-sla.ts——无客户落到 CASE_SLA_DEFAULT_TIER = 'smb' 列;dev 读过升级任务与解决加分两处均跳过无客户案例,无报表/数据集按客户分组。
  • 测试:在 case-create-form-narrowing.test.ts 加一条 it,断言 crm_account 在表单上且非必填。

dev 读数:基线上客服对一条无客户的 web 案例点「编辑 → 进行中 → 更新」被前端拦下(「Account is required」,未发 PATCH);而同一客服用 API PATCH {status: in_progress} 返回 200 并自认领——表单的必填标记是唯一的拦截者。分支上:认领无主 web 案例(PATCH 200,owner = 客服,离开分诊队列)、已路由案例保存、内部新建无客户(201,48h smb SLA)与有客户(201,36h)、无客户升级(200,交经理、无任务)与解决(200)均通过。切除(加回 required: true)红 1 例,恢复后 blob 与 HEAD 一致。

门禁 check-runs(钉 head):Build and Test (22.x) ✓ · Quality Checks ✓ · Check Changeset ✓ · Playwright ✓ · link-check ✓ · CodeQL ✓ · Analyze Code (javascript) ✓ · Label Pull Request ✓ · Vercel Preview Comments ✓ — 9/9 success(Typecheck and Build 只对 content/docs/** / apps/docs/** 触发,本 PR 未触及,不出现属正常),mergeable_state clean,base 421bd1d8 = 当前 origin/main(未移动,无需在合并树上复读 token ratchet)。

接受的偏差:① 派发令写「仅当已有兄弟测试钉住必填性才加测试」,实际没有,dev 仍加了一条 pin——落在认领面点名的文件里、不是新门禁,席位接受。② dev 为测到无主路径,在本地库里删除/重建过 service.agent 的岗位行并手动评估共享规则,只限本地实例。

范围外去向:

落地条款:席位经中继 pr_ready + automerge_enable,以 objectstack-fleet[bot] 入合并队列;合并后 #2060 退回 pm:queue(Part of),按串行口径派下一项(第 5 项,p2 队列的最后一项)。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd CI plumbing and the verification pipeline metadata Declarative metadata — schema, security posture, UI surfaces

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants