The 8→9 stage refresh from earlier today only updated constants.ts and the DB —
20 component/service files still hardcoded the old enum, leaving labels blank,
filter dropdowns wrong, kanban columns mismatched, and the analytics funnel
silently dropping new-stage rows. The platform also never advanced
pipelineStage on EOI lifecycle events: documents.service.ts wrote eoiStatus
but left the user-visible stage stuck.
This commit closes both gaps:
1. Single source of truth in src/lib/constants.ts — adds STAGE_LABELS,
STAGE_BADGE, STAGE_DOT, STAGE_WEIGHTS, STAGE_TRANSITIONS plus
stageLabel / stageBadgeClass / stageDotClass / safeStage /
canTransitionStage helpers. components/clients/pipeline-constants.ts
becomes a re-export shim so existing imports keep working.
2. 18 stale-enum surfaces migrated — interest list (table, card, filters,
form, stage picker), pipeline board, client card, berth interests tab,
portal client interests page, dashboard pipeline / funnel / revenue-
forecast charts, settings pipeline_weights default, dashboard.service
weights, analytics.service funnel stages, alert-rules stale-interest
filter, interest-scoring stage rank.
3. Documents tab wired into interest detail — replaced the placeholder in
interest-tabs.tsx with InterestDocumentsTab + InterestFilesTab so the
EOI launcher is back where salespeople work.
4. Auto-advance — new advanceStageIfBehind() in interests.service.ts
(forward-only, no-op if interest is already past the target). Called
from documents.service.ts on send (→ eoi_sent), Documenso completed
webhook (→ eoi_signed), and manual signed-EOI upload (→ eoi_signed).
5. Transition guard — canTransitionStage() blocks egregious skips
(e.g. completed → open, open → contract_signed). Enforced in
changeInterestStage before the DB write.
Tests updated to reflect the 9-stage model. tsc clean, vitest 832/832,
ESLint clean on every file touched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
97 lines
2.7 KiB
TypeScript
97 lines
2.7 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { PIPELINE_STAGES, BERTH_STATUSES, NOTIFICATION_TYPES } from '@/lib/constants';
|
|
|
|
describe('PIPELINE_STAGES', () => {
|
|
it('has exactly 9 entries', () => {
|
|
expect(PIPELINE_STAGES).toHaveLength(9);
|
|
});
|
|
|
|
it('starts with "open"', () => {
|
|
expect(PIPELINE_STAGES[0]).toBe('open');
|
|
});
|
|
|
|
it('ends with "completed"', () => {
|
|
expect(PIPELINE_STAGES[PIPELINE_STAGES.length - 1]).toBe('completed');
|
|
});
|
|
|
|
it('contains all expected stages in order', () => {
|
|
expect(PIPELINE_STAGES).toEqual([
|
|
'open',
|
|
'details_sent',
|
|
'in_communication',
|
|
'eoi_sent',
|
|
'eoi_signed',
|
|
'deposit_10pct',
|
|
'contract_sent',
|
|
'contract_signed',
|
|
'completed',
|
|
]);
|
|
});
|
|
|
|
it('is a readonly tuple — type-level immutability via `as const`', () => {
|
|
const arr = PIPELINE_STAGES as unknown as string[];
|
|
expect(arr).toHaveLength(9);
|
|
});
|
|
|
|
it('has no duplicate entries', () => {
|
|
const unique = new Set(PIPELINE_STAGES);
|
|
expect(unique.size).toBe(PIPELINE_STAGES.length);
|
|
});
|
|
});
|
|
|
|
describe('BERTH_STATUSES', () => {
|
|
it('has exactly 3 entries', () => {
|
|
expect(BERTH_STATUSES).toHaveLength(3);
|
|
});
|
|
|
|
it('contains "available"', () => {
|
|
expect(BERTH_STATUSES).toContain('available');
|
|
});
|
|
|
|
it('contains "under_offer"', () => {
|
|
expect(BERTH_STATUSES).toContain('under_offer');
|
|
});
|
|
|
|
it('contains "sold"', () => {
|
|
expect(BERTH_STATUSES).toContain('sold');
|
|
});
|
|
|
|
it('has no duplicate entries', () => {
|
|
const unique = new Set(BERTH_STATUSES);
|
|
expect(unique.size).toBe(BERTH_STATUSES.length);
|
|
});
|
|
});
|
|
|
|
describe('NOTIFICATION_TYPES', () => {
|
|
it('contains "interest_stage_changed"', () => {
|
|
expect(NOTIFICATION_TYPES).toContain('interest_stage_changed');
|
|
});
|
|
|
|
it('contains "mention"', () => {
|
|
expect(NOTIFICATION_TYPES).toContain('mention');
|
|
});
|
|
|
|
it('contains "email_received"', () => {
|
|
expect(NOTIFICATION_TYPES).toContain('email_received');
|
|
});
|
|
|
|
it('has no duplicate entries', () => {
|
|
const unique = new Set(NOTIFICATION_TYPES);
|
|
expect(unique.size).toBe(NOTIFICATION_TYPES.length);
|
|
});
|
|
|
|
it('contains expected notification categories (interest, document, reminder, financial, email, system)', () => {
|
|
const types = new Set(NOTIFICATION_TYPES);
|
|
// Interest
|
|
expect(types.has('interest_stage_changed')).toBe(true);
|
|
expect(types.has('interest_created')).toBe(true);
|
|
// Document
|
|
expect(types.has('document_sent')).toBe(true);
|
|
expect(types.has('document_signed')).toBe(true);
|
|
// Financial
|
|
expect(types.has('invoice_paid')).toBe(true);
|
|
// System
|
|
expect(types.has('system_alert')).toBe(true);
|
|
});
|
|
});
|