-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathopportunity.object.ts
More file actions
127 lines (123 loc) · 4.58 KB
/
Copy pathopportunity.object.ts
File metadata and controls
127 lines (123 loc) · 4.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
import { ObjectSchema, Field } from '@objectstack/spec/data';
import { cel, P } from '@objectstack/spec';
export const Opportunity = ObjectSchema.create({
name: 'crm_opportunity',
label: 'Opportunity',
pluralLabel: 'Opportunities',
icon: 'trending-up',
description: 'A potential sale tied to an account.',
fields: {
name: Field.text({
label: 'Opportunity Name',
required: true,
searchable: true,
maxLength: 200,
}),
account: Field.lookup('crm_account', {
label: 'Account',
required: true,
}),
stage: Field.select({
label: 'Stage',
required: true,
options: [
{ label: 'Prospecting', value: 'prospecting', default: true, color: '#94A3B8' },
{ label: 'Qualification', value: 'qualification', color: '#3B82F6' },
{ label: 'Proposal', value: 'proposal', color: '#F59E0B' },
{ label: 'Closed Won', value: 'closed_won', color: '#10B981' },
{ label: 'Closed Lost', value: 'closed_lost', color: '#EF4444' },
],
}),
amount: Field.currency({
label: 'Amount',
scale: 2,
min: 0,
}),
probability: Field.percent({
label: 'Probability',
defaultValue: 50,
min: 0,
max: 100,
}),
expected_revenue: Field.formula({
label: 'Expected Revenue',
expression: cel`(record.amount == null ? 0 : record.amount) * (record.probability == null ? 0 : record.probability) / 100`,
}),
close_date: Field.date({
label: 'Close Date',
}),
// Exercises the newly-registered `daysBetween` stdlib function — the canonical
// "days remaining" formula (negative once the close date has passed).
days_to_close: Field.formula({
label: 'Days to Close',
expression: cel`record.close_date == null ? 0 : daysBetween(today(), record.close_date)`,
}),
discount_percent: Field.percent({
label: 'Discount %',
defaultValue: 0,
min: 0,
max: 100,
}),
// Mirror target for the Discount Approval flow's approval nodes
// (ADR-0019). The approval runtime writes the request status here; it is
// readonly to users so only the flow drives it.
approval_status: Field.select({
label: 'Approval Status',
readonly: true,
options: [
{ label: 'Pending', value: 'pending', color: '#F59E0B' },
{ label: 'Approved', value: 'approved', color: '#10B981' },
{ label: 'Rejected', value: 'rejected', color: '#EF4444' },
{ label: 'Recalled', value: 'recalled', color: '#94A3B8' },
],
}),
renewal_of: Field.lookup('crm_opportunity', {
label: 'Renewal Of',
}),
// Roll-up of the opportunity's product line items — recomputed server-side
// as lines are inserted/updated/deleted (child FK auto-detected:
// crm_opportunity_line_item.opportunity). The line-item grid also shows a
// live running total during entry (inlineAmountField: 'amount').
line_total: Field.summary({
label: 'Products Total',
summaryOperations: { object: 'crm_opportunity_line_item', field: 'amount', function: 'sum' },
}),
},
validations: [
{
type: 'script' as const,
name: 'discount_cap',
label: 'Discount Cap 40%',
description: 'Discounts over 40% require special approval.',
condition: P`record.discount_percent != null && record.discount_percent > 40`,
message: 'Discount cannot exceed 40% without an approved exception.',
severity: 'error' as const,
},
{
type: 'cross_field' as const,
name: 'opp_close_date_not_past',
label: 'Close Date Must Be Future',
description: 'Prevent back-dating the close_date of an OPEN opportunity. Closed (won/lost) deals legitimately carry a historical close date, so they are exempt.',
fields: ['close_date'],
condition: P`has(record.close_date) && record.close_date < now() && record.stage != "closed_won" && record.stage != "closed_lost"`,
message: 'Close Date must be today or a future date.',
events: ['insert'],
},
{
type: 'state_machine' as const,
name: 'opp_stage_transitions',
label: 'Opportunity Stage Flow',
description: 'Opportunities should progress through stages in order.',
field: 'stage',
message: 'Invalid stage transition.',
transitions: {
prospecting: ['qualification', 'closed_lost'],
qualification:['proposal', 'closed_lost'],
proposal: ['closed_won', 'closed_lost'],
closed_won: [],
closed_lost: ['prospecting'],
},
},
],
});