diff --git a/CHANGELOG.md b/CHANGELOG.md index fa37d4a4..561c5b14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ 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/). +## [2.2.2] — 2026-07-21 + +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. + +### Fixed + +- **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). + ## [2.2.1] — 2026-07-20 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. diff --git a/objectstack.config.ts b/objectstack.config.ts index 558ac4ce..effcfafd 100644 --- a/objectstack.config.ts +++ b/objectstack.config.ts @@ -31,7 +31,7 @@ export default defineStack({ manifest: { id: 'app.objectstack.hotcrm', namespace: 'crm', - version: '2.2.1', + version: '2.2.2', type: 'app', name: 'HotCRM', description: 'AI-Native CRM for the ObjectStack marketplace — Accounts, Contacts, Leads, Opportunities, Cases, Knowledge, Forecasts, Campaigns, Contracts.', diff --git a/package.json b/package.json index 50dbdc7d..f4cbea8a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hotcrm", - "version": "2.2.1", + "version": "2.2.2", "description": "HotCRM - Production CRM built on ObjectStack", "private": true, "main": "./objectstack.config.ts", diff --git a/src/objects/opportunity.hook.ts b/src/objects/opportunity.hook.ts index 79c0eb72..086c0adb 100644 --- a/src/objects/opportunity.hook.ts +++ b/src/objects/opportunity.hook.ts @@ -58,14 +58,18 @@ const opportunityValidationHook: Hook = { const { event, input } = ctx; const previous = ctx.previous; - // Freeze closed opportunities FIRST, judging only the caller's actual - // edits. This must run before the derived-field recompute below injects - // expected_revenue/probability into `input` — otherwise a system write - // (e.g. the post-seed ownership backfill) on a record whose stored - // derived values are stale gets rejected for fields the caller never - // touched, and the backfill silently fails (seen as 23 boot-time - // BodyRunner errors on every fresh-DB boot). - if (event === 'beforeUpdate' && previous) { + // Freeze closed opportunities — but guard ONLY genuine USER edits. A write + // with no authenticated user (`ctx.user?.id` absent) is a system / seed / + // backfill write and must pass: the seed's `close_date: daysAgo(15)` + // re-evaluates to a new date on every reboot, so a re-seed legitimately + // changes close_date on already-closed opps. Guarding those threw 23 + // boot-time BodyRunner errors AND blocked the seed from correcting + // closed-won `probability` to 100 (#459). `ctx.user?.id` is this repo's + // system-write signal (cf. case/lead/quote hooks) and matches the + // SYSTEM_FIELDS intent above ("only user edits to business fields"). + // Still runs before the derived-field recompute below so a genuine user + // edit is judged on the caller's own fields, not injected ones. + if (event === 'beforeUpdate' && previous && ctx.user?.id) { const prevStage = previous.stage as string | undefined; const isClosed = prevStage === 'closed_won' || prevStage === 'closed_lost'; if (isClosed) { diff --git a/src/objects/quote.hook.ts b/src/objects/quote.hook.ts index 518b7dcd..9e36f0f2 100644 --- a/src/objects/quote.hook.ts +++ b/src/objects/quote.hook.ts @@ -41,7 +41,12 @@ const quoteValidation: Hook = { input.expiration_date = addDays(base, 30); } - if (event === 'beforeUpdate' && previous) { + // Guard ONLY genuine USER edits (`ctx.user?.id` present). System / seed / + // backfill writes carry no user and legitimately re-apply business fields + // (the seed's quote_date/expiration_date re-evaluate on every reboot), so + // guarding them threw boot-time BodyRunner errors (#459). Matches the + // system-write convention used across the case/lead/opportunity hooks. + if (event === 'beforeUpdate' && previous && ctx.user?.id) { const frozen = previous.status === 'accepted' || previous.status === 'expired'; if (frozen) { const allowed = new Set(['internal_notes']);