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
25 changes: 25 additions & 0 deletions .changeset/seed-pipeline-draggable-kanban.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
'hotcrm': patch
---

**The demo pipeline board is draggable out of the box.** In a freshly seeded database nine of the
ten open opportunities were priced at $100K or more, so every one of them was born into the
**Large Deal Approval** flow, which locks the record while the approval is pending — on the
**Sales Pipeline** kanban those cards refused every drag with `RECORD_LOCKED`, and only one open
card could be moved (#1902).

The seeded active pipeline is re-priced so that exactly **two** open deals exhibit the approval
flow, one per tier:

- **Acme Platform Upgrade** ($150,000) — Sales Manager review only.
- **Wayne Enterprise License** ($1,200,000) — Sales Manager review, then Sales Director sign-off.

The other seven open deals are now scoped under $100K (their product lines were re-scoped, and
each amount is still derived from its lines), so every active stage keeps at least one card
that moves. On a fresh database: open deals locked by a pending approval **9 → 2**, draggable
open cards **1 → 8**. The **Globex Manufacturing Suite Proposal** quote follows its deal's new
lines. The approval flow and the $100K / $500K thresholds are unchanged.

This is a change to the demo seed only, measured on a fresh database. An existing demo database is
not re-priced in place — approval requests already open there stay open until decided or
recalled; run `pnpm demo:reset` to start from the new seed.
4 changes: 2 additions & 2 deletions src/revenue/data/revenue.seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,9 +227,9 @@ export const quotes = defineSeed(Quote, {
status: 'presented',
quote_date: cel`daysAgo(7)`,
expiration_date: cel`daysFromNow(23)`,
...quoteTotals('Globex Manufacturing Suite Proposal', { discount: 5, tax: 38000, shipping_handling: 2500 }),
...quoteTotals('Globex Manufacturing Suite Proposal', { discount: 5, tax: 7220, shipping_handling: 2500 }),
payment_terms: 'net_60',
description: 'Manufacturing suite licensing with implementation services.',
description: 'Manufacturing suite licensing with plant-admin training.',
},
{
name: 'Wayne Enterprise License Quote',
Expand Down
56 changes: 34 additions & 22 deletions src/sales/data/sales.seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -662,9 +662,10 @@ export const OPPORTUNITY_LINES: Record<string, readonly LineSpec[]> = {
{ product: 'AI Agent Seat (Annual)', quantity: 35, unit_price: 1000, description: 'Agent seats for the Ops organization.' },
],
'Globex Manufacturing Suite': [
{ product: 'ObjectStack Platform', quantity: 6, unit_price: 50000, description: 'Enterprise edition across six manufacturing sites.' },
{ product: 'Implementation Services', quantity: 2, unit_price: 75000, description: 'Two-phase implementation: plant operations, then commercial.' },
{ product: 'Premium Support', quantity: 2, unit_price: 25000, description: 'Premium support for both production regions.' },
{ product: 'ObjectStack Platform', quantity: 1, unit_price: 50000, description: 'Enterprise edition — one shared tenant serving all six manufacturing sites.' },
{ product: 'Admin Training Workshop', quantity: 4, unit_price: 6000, description: 'Four workshop days, one per regional plant cluster.' },
{ product: 'Standard Support', quantity: 1, unit_price: 9000, description: 'Business-hours support for the first year.' },
{ product: 'AI Agent Seat (Annual)', quantity: 12, unit_price: 1000, description: 'Agent seats for the plant operations leads.' },
],
'Wayne Enterprise License': [
{ product: 'ObjectStack Platform', quantity: 20, unit_price: 50000, discount: 15, description: 'Enterprise-wide license, volume discount at 20 tenants.' },
Expand Down Expand Up @@ -727,38 +728,36 @@ export const OPPORTUNITY_LINES: Record<string, readonly LineSpec[]> = {
{ product: 'AI Agent Seat (Annual)', quantity: 6, unit_price: 1000, description: 'Agent seats for the compliance analysts.' },
],
'Northwind Grid Modernization': [
{ product: 'ObjectStack Platform', quantity: 3, unit_price: 50000, description: 'Enterprise edition for grid, field and customer operations.' },
{ product: 'Implementation Services', quantity: 1, unit_price: 75000, description: 'Operational-data assessment and implementation.' },
{ product: 'ObjectStack Platform', quantity: 1, unit_price: 50000, description: 'Enterprise edition for grid, field and customer operations.' },
{ product: 'Field Service Mobile', quantity: 1, unit_price: 14000, description: 'Field-service mobile for the grid maintenance crews.' },
{ product: 'Sandbox Environment (Annual)', quantity: 1, unit_price: 7500, description: 'Sandbox for the operational-data assessment.' },
{ product: 'AI Agent Seat (Annual)', quantity: 15, unit_price: 1000, description: 'Agent seats for the control-room pilot.' },
],
'Lattice Student Success Platform': [
{ product: 'ObjectStack Platform (SMB Edition)', quantity: 5, unit_price: 18000, description: 'Student-success tenants for five colleges.' },
{ product: 'Admin Training Workshop', quantity: 5, unit_price: 6000, description: 'Five workshop days for the student-success advisers.' },
{ product: 'ObjectStack Platform (SMB Edition)', quantity: 2, unit_price: 18000, description: 'Student-success tenants for the two pilot colleges.' },
{ product: 'Admin Training Workshop', quantity: 2, unit_price: 6000, description: 'Two workshop days for the student-success advisers.' },
{ product: 'AI Agent Seat (Annual)', quantity: 5, unit_price: 1000, description: 'Agent seats for the advising team.' },
],
'Vertex Analytics Expansion': [
{ product: 'ObjectStack Platform', quantity: 2, unit_price: 50000, description: 'Two tenants for the revenue organization.' },
{ product: 'Analytics Add-on', quantity: 5, unit_price: 22000, description: 'Analytics for sales, marketing, service, finance and ops.' },
{ product: 'Premium Support', quantity: 2, unit_price: 25000, description: 'Premium support for the expanded footprint.' },
{ product: 'AI Agent Seat (Annual)', quantity: 60, unit_price: 1000, description: 'Agent seats for the full revenue organization.' },
{ product: 'Analytics Add-on', quantity: 2, unit_price: 22000, description: 'Analytics for the sales and marketing teams.' },
{ product: 'Premium Support', quantity: 1, unit_price: 25000, description: 'Premium support for the expanded footprint.' },
{ product: 'AI Agent Seat (Annual)', quantity: 10, unit_price: 1000, description: 'Agent seats for the revenue operations team.' },
],
'Apex Logistics Data Hub': [
{ product: 'ObjectStack Platform', quantity: 3, unit_price: 50000, description: 'Data-hub tenants for commercial, operations and finance.' },
{ product: 'ObjectStack Platform', quantity: 1, unit_price: 50000, description: 'Data-hub tenant shared by commercial, operations and finance.' },
{ product: 'Data Migration Services', quantity: 1, unit_price: 35000, description: 'Consolidation of four fragmented commercial systems.' },
{ product: 'Implementation Services', quantity: 1, unit_price: 75000, description: 'Architecture workshop through go-live.' },
{ product: 'AI Agent Seat (Annual)', quantity: 15, unit_price: 1000, description: 'Agent seats for the commercial operations team.' },
{ product: 'AI Agent Seat (Annual)', quantity: 8, unit_price: 1000, description: 'Agent seats for the commercial operations team.' },
],
'Lattice Education Renewal': [
{ product: 'ObjectStack Platform', quantity: 3, unit_price: 50000, description: 'Renewal of the three institution tenants.' },
{ product: 'ObjectStack Platform', quantity: 1, unit_price: 50000, description: 'Renewal of the institution-wide tenant.' },
{ product: 'Analytics Add-on', quantity: 1, unit_price: 22000, description: 'Expanded analytics package for the renewal term.' },
{ product: 'Admin Training Workshop', quantity: 3, unit_price: 6000, description: 'Three workshop days for the new admissions admins.' },
{ product: 'AI Agent Seat (Annual)', quantity: 20, unit_price: 1000, description: 'Student-success automation seats.' },
{ product: 'AI Agent Seat (Annual)', quantity: 15, unit_price: 1000, description: 'Student-success automation seats.' },
],
'Vertex Enterprise Rollout': [
{ product: 'ObjectStack Platform', quantity: 10, unit_price: 50000, discount: 20, description: 'Phased enterprise rollout, ten tenants (volume discount).' },
{ product: 'Implementation Services', quantity: 1, unit_price: 75000, description: 'Phased deployment with data-residency controls.' },
{ product: 'Premium Support', quantity: 4, unit_price: 25000, description: 'Premium support across four regions.' },
{ product: 'AI Agent Seat (Annual)', quantity: 100, unit_price: 1000, description: 'Agent seats for the first rollout wave.' },
{ product: 'ObjectStack Platform', quantity: 1, unit_price: 50000, discount: 20, description: 'Phase-one tenant for the first region, with data-residency controls (multi-phase commitment discount).' },
{ product: 'Data Migration Services', quantity: 1, unit_price: 35000, description: 'Phase-one migration of the regional revenue data.' },
{ product: 'Standard Support', quantity: 1, unit_price: 9000, description: 'Business-hours support for phase one.' },
{ product: 'AI Agent Seat (Annual)', quantity: 10, unit_price: 1000, description: 'Agent seats for the first rollout wave.' },
],
// Campaign-attributed wins — see the campaign metrics note below.
'Lattice Analytics Expansion': [
Expand Down Expand Up @@ -1099,6 +1098,19 @@ analytics seats for the Ops org, (3) priority support SLA.`,
// Two cards in each active stage make the kanban immediately legible in
// a customer demo. They also tell a coherent sales story instead of
// overwhelming the board with anonymous generated rows.
//
// PRICED TO STAY DRAGGABLE (#1902). A deal at or above
// `LARGE_DEAL_AMOUNT` is born into `opportunity_approval_on_create`, whose
// approval nodes hold the record with `lockRecord: true` — on the board
// that card refuses every drag with `RECORD_LOCKED`. When nine of the ten
// open deals sat over the line, the one board a demo opens first was
// frozen. So exactly TWO open deals exhibit the approval flow, one per
// tier, and both are in the block above: `Acme Platform Upgrade`
// (manager review only, $100K–$500K) and `Wayne Enterprise License`
// (manager, then director sign-off, over `HIGH_VALUE_DEAL_AMOUNT`). Every
// deal in this block is priced under the line, so each active stage keeps
// at least one card that moves. `test/seed-consistency.test.ts` pins the
// two by name — re-pricing a deal here over $100K goes red there.
{
name: 'Northwind Grid Modernization',
crm_account: 'Northwind Energy',
Expand Down Expand Up @@ -1188,7 +1200,7 @@ analytics seats for the Ops org, (3) priority support SLA.`,
forecast_category: 'commit',
lead_source: 'referral',
stage_entry_date: celDaysAgo(3),
description: 'An enterprise rollout negotiated around phased deployment and data-residency requirements.',
description: 'Phase one of an enterprise rollout, negotiated around phased deployment and data-residency requirements.',
next_step: 'Resolve the final security addendum and confirm the procurement signature date.',
},
]
Expand Down
31 changes: 31 additions & 0 deletions test/seed-consistency.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { describe, it, expect } from 'vitest';
import { CrmSeedData } from '../objectstack.composition';
import { CASE_SLA_DEFAULT_TIER, caseSlaHours } from '../src/service/objects/_case-sla';
import { HIGH_VALUE_DEAL_AMOUNT, LARGE_DEAL_AMOUNT } from '../src/sales/objects/_thresholds';

/**
* Seed ↔ hook consistency guards (#591).
Expand Down Expand Up @@ -207,6 +208,36 @@ describe('opportunity_amount_rollup would be a no-op over the seeds', () => {
});
});

/**
* The open board stays draggable (#1902). A deal born at or above
* `LARGE_DEAL_AMOUNT` enters `opportunity_approval_on_create`, whose approval
* nodes lock the record — its kanban card refuses every drag. When nine of the
* ten open seeded deals sat over the line, a fresh demo's board was frozen.
* The ruling: exactly one deal per approval tier exhibits the flow, and every
* other open deal is priced under the line. Pinned by NAME, so a re-priced
* deal crossing the line is named back rather than counted.
*/
describe('the seeded open pipeline keeps the kanban draggable (#1902)', () => {
const settled = new Set(['closed_won', 'closed_lost']);
const open = opportunities.filter((o) => !settled.has(String(o.stage)));
const overLine = open.filter((o) => Number(o.amount) >= LARGE_DEAL_AMOUNT);

it('exactly two open deals sit at or above LARGE_DEAL_AMOUNT, one per approval tier', () => {
const tier = (o: Rec) => (Number(o.amount) > HIGH_VALUE_DEAL_AMOUNT ? 'director' : 'manager');
expect(
overLine.map((o) => `${String(o.name)} (${tier(o)})`).sort(),
).toEqual(['Acme Platform Upgrade (manager)', 'Wayne Enterprise License (director)']);
});

it('every active stage keeps a card under the line', () => {
const stages = ['prospecting', 'qualification', 'needs_analysis', 'proposal', 'negotiation'];
const lockedOnly = stages.filter(
(stage) => !open.some((o) => o.stage === stage && Number(o.amount) < LARGE_DEAL_AMOUNT),
);
expect(lockedOnly).toEqual([]);
});
});

describe('quote_total_rollup would be a no-op over the seeds', () => {
it('subtotal, discount_amount and total_price follow the rollup model', () => {
const drift: string[] = [];
Expand Down
Loading