Skip to content

Commit 32d63e7

Browse files
os-zhuangclaude
andauthored
fix(hooks): freeze-guards skip system/seed writes — fixes 23 boot errors + closed-won probability (2.2.2) (#462)
Closes #459. The opportunity_lifecycle and quote freeze-guards ran on every write to a closed/accepted record. The seed re-applies rows on each reboot, and its `close_date: daysAgo(15)` / quote dates re-evaluate to a *new* date every boot, so the re-seed genuinely changes a guarded field on an already-closed record — which the guard rejected. Result: 23 `BodyRunner` errors per boot, and the seed could never set closed-won `probability` to 100 (it fell back to the field default 10). Both guards now fire ONLY for genuine user edits (`ctx.user?.id` present). System / seed / backfill writes (no user) pass — matching this repo's system-write convention (case/lead hooks) and the guards' own stated intent. A user editing a closed opportunity / accepted quote via the UI is still blocked. Verified: reboot on the existing (persisted) DB — the exact 23-error scenario — now logs 0 BodyRunner errors, and all closed_won opps report probability 100 (open stages keep their stage-derived values). verify (validate/build/test 17/17) green. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent f3f4899 commit 32d63e7

5 files changed

Lines changed: 28 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
All notable changes to HotCRM are documented in this file. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); HotCRM follows [Semantic Versioning](https://semver.org/).
44

5+
## [2.2.2] — 2026-07-21
6+
7+
Patch before the ObjectStack 16 marketplace release. Fixes [#459](https://github.com/objectstack-ai/hotcrm/issues/459) — the highest-severity issue from the v2.2.1 QA dogfood.
8+
9+
### Fixed
10+
11+
- **Opportunity/quote freeze-guards no longer reject system/seed writes → 23 boot errors gone + closed-won probability corrected.** The `opportunity_lifecycle` and quote freeze-guards ran on **every** write to a closed/accepted record, so the seed re-applying rows on reboot (its `close_date: daysAgo(15)` / `quote_date` re-evaluate to a *new* date each boot) was rejected — logging 23 `BodyRunner` errors per boot and blocking the seed from setting closed-won `probability` to 100 (it fell back to the field default `10`). Both guards now fire **only for genuine user edits** (`ctx.user?.id` present); system/seed/backfill writes (no user) pass — matching this repo's system-write convention (`case`/`lead` hooks) and the guards' own stated intent. A user editing a closed opportunity or accepted quote through the UI is still blocked. Fixes [opportunity.hook.ts](src/objects/opportunity.hook.ts) + [quote.hook.ts](src/objects/quote.hook.ts).
12+
513
## [2.2.1] — 2026-07-20
614

715
Follow-up patch to 2.2.0. The dashboard-filter fix in 2.2.0 only covered the built-in `dateRange` picker; the **`globalFilters[]`** have the same propagation behaviour (ObjectStack 15 / framework#2501 injects every dashboard filter into each widget's query) and were still crashing widgets on objects that lack the filtered field. Browser-verified by actually selecting the filter values, not just loading the dashboards.

objectstack.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export default defineStack({
3131
manifest: {
3232
id: 'app.objectstack.hotcrm',
3333
namespace: 'crm',
34-
version: '2.2.1',
34+
version: '2.2.2',
3535
type: 'app',
3636
name: 'HotCRM',
3737
description: 'AI-Native CRM for the ObjectStack marketplace — Accounts, Contacts, Leads, Opportunities, Cases, Knowledge, Forecasts, Campaigns, Contracts.',

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "hotcrm",
3-
"version": "2.2.1",
3+
"version": "2.2.2",
44
"description": "HotCRM - Production CRM built on ObjectStack",
55
"private": true,
66
"main": "./objectstack.config.ts",

src/objects/opportunity.hook.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,18 @@ const opportunityValidationHook: Hook = {
5858
const { event, input } = ctx;
5959
const previous = ctx.previous;
6060

61-
// Freeze closed opportunities FIRST, judging only the caller's actual
62-
// edits. This must run before the derived-field recompute below injects
63-
// expected_revenue/probability into `input` — otherwise a system write
64-
// (e.g. the post-seed ownership backfill) on a record whose stored
65-
// derived values are stale gets rejected for fields the caller never
66-
// touched, and the backfill silently fails (seen as 23 boot-time
67-
// BodyRunner errors on every fresh-DB boot).
68-
if (event === 'beforeUpdate' && previous) {
61+
// Freeze closed opportunities — but guard ONLY genuine USER edits. A write
62+
// with no authenticated user (`ctx.user?.id` absent) is a system / seed /
63+
// backfill write and must pass: the seed's `close_date: daysAgo(15)`
64+
// re-evaluates to a new date on every reboot, so a re-seed legitimately
65+
// changes close_date on already-closed opps. Guarding those threw 23
66+
// boot-time BodyRunner errors AND blocked the seed from correcting
67+
// closed-won `probability` to 100 (#459). `ctx.user?.id` is this repo's
68+
// system-write signal (cf. case/lead/quote hooks) and matches the
69+
// SYSTEM_FIELDS intent above ("only user edits to business fields").
70+
// Still runs before the derived-field recompute below so a genuine user
71+
// edit is judged on the caller's own fields, not injected ones.
72+
if (event === 'beforeUpdate' && previous && ctx.user?.id) {
6973
const prevStage = previous.stage as string | undefined;
7074
const isClosed = prevStage === 'closed_won' || prevStage === 'closed_lost';
7175
if (isClosed) {

src/objects/quote.hook.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,12 @@ const quoteValidation: Hook = {
4141
input.expiration_date = addDays(base, 30);
4242
}
4343

44-
if (event === 'beforeUpdate' && previous) {
44+
// Guard ONLY genuine USER edits (`ctx.user?.id` present). System / seed /
45+
// backfill writes carry no user and legitimately re-apply business fields
46+
// (the seed's quote_date/expiration_date re-evaluate on every reboot), so
47+
// guarding them threw boot-time BodyRunner errors (#459). Matches the
48+
// system-write convention used across the case/lead/opportunity hooks.
49+
if (event === 'beforeUpdate' && previous && ctx.user?.id) {
4550
const frozen = previous.status === 'accepted' || previous.status === 'expired';
4651
if (frozen) {
4752
const allowed = new Set(['internal_notes']);

0 commit comments

Comments
 (0)