Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion objectstack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
20 changes: 12 additions & 8 deletions src/objects/opportunity.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
7 changes: 6 additions & 1 deletion src/objects/quote.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
Expand Down
Loading