amsdal_crm 0.1.9__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,46 +1 @@
1
1
  """CRM Models."""
2
-
3
- # Import File first for Attachment's forward reference
4
- from amsdal.models.core.file import File # noqa: F401
5
-
6
- from amsdal_crm.models.account import Account
7
- from amsdal_crm.models.activity import Activity
8
- from amsdal_crm.models.activity import ActivityRelatedTo
9
- from amsdal_crm.models.activity import ActivityType
10
- from amsdal_crm.models.activity import Call
11
- from amsdal_crm.models.activity import EmailActivity
12
- from amsdal_crm.models.activity import Event
13
- from amsdal_crm.models.activity import Note
14
- from amsdal_crm.models.activity import Task
15
- from amsdal_crm.models.attachment import Attachment
16
- from amsdal_crm.models.contact import Contact
17
- from amsdal_crm.models.custom_field_definition import CustomFieldDefinition
18
- from amsdal_crm.models.deal import Deal
19
- from amsdal_crm.models.pipeline import Pipeline
20
- from amsdal_crm.models.stage import Stage
21
- from amsdal_crm.models.workflow_rule import WorkflowRule
22
-
23
- __all__ = [
24
- 'Account',
25
- 'Activity',
26
- 'ActivityRelatedTo',
27
- 'ActivityType',
28
- 'Attachment',
29
- 'Call',
30
- 'Contact',
31
- 'CustomFieldDefinition',
32
- 'Deal',
33
- 'EmailActivity',
34
- 'Event',
35
- 'Note',
36
- 'Pipeline',
37
- 'Stage',
38
- 'Task',
39
- 'WorkflowRule',
40
- ]
41
-
42
- # Rebuild models to resolve forward references
43
- Contact.model_rebuild()
44
- Deal.model_rebuild()
45
- Stage.model_rebuild()
46
- Attachment.model_rebuild()
@@ -26,8 +26,7 @@ class ActivityType(str, Enum):
26
26
  class ActivityRelatedTo(str, Enum):
27
27
  """What type of record this activity is related to."""
28
28
 
29
- CONTACT = 'Contact'
30
- ACCOUNT = 'Account'
29
+ ENTITY = 'Entity'
31
30
  DEAL = 'Deal'
32
31
 
33
32
 
@@ -41,7 +40,6 @@ class Activity(TimestampMixin, Model):
41
40
  __module_type__: ClassVar[ModuleType] = ModuleType.CONTRIB
42
41
  __indexes__: ClassVar[list[IndexInfo]] = [
43
42
  IndexInfo(name='idx_activity_related_to', field='related_to_id'),
44
- IndexInfo(name='idx_activity_owner', field='owner_email'),
45
43
  IndexInfo(name='idx_activity_created_at', field='created_at'),
46
44
  IndexInfo(name='idx_activity_due_date', field='due_date'),
47
45
  ]
@@ -54,12 +52,10 @@ class Activity(TimestampMixin, Model):
54
52
  description: str | None = Field(default=None, title='Description')
55
53
 
56
54
  # Polymorphic relationship (generic FK pattern)
57
- related_to_type: ActivityRelatedTo = Field(title='Related To Type')
58
- related_to_id: str = Field(title='Related To ID')
59
-
60
- # Owner
61
- owner_email: str = Field(title='Owner Email')
55
+ related_to_type: ActivityRelatedTo | None = Field(title='Related To Type')
56
+ related_to_id: str | None = Field(title='Related To ID')
62
57
 
58
+ assigned_to: User | None = Field(default=None, title='Assigned To')
63
59
  # Timing
64
60
  due_date: datetime | None = Field(default=None, title='Due Date')
65
61
  completed_at: datetime | None = Field(default=None, title='Completed At')
@@ -83,7 +79,7 @@ class Activity(TimestampMixin, Model):
83
79
  True if user has permission, False otherwise
84
80
  """
85
81
  # Owner has all permissions
86
- if self.owner_email == user.email:
82
+ if self.assigned_to and self.assigned_to.email == user.email:
87
83
  return True
88
84
 
89
85
  # Check admin permissions
@@ -27,7 +27,7 @@ class Attachment(Model):
27
27
  file: File = Field(title='File')
28
28
 
29
29
  # Polymorphic relationship
30
- related_to_type: Literal['Contact', 'Account', 'Deal', 'Activity'] = Field(title='Related To Type')
30
+ related_to_type: Literal['Entity', 'Deal', 'Activity'] = Field(title='Related To Type')
31
31
  related_to_id: str = Field(title='Related To ID')
32
32
 
33
33
  # Metadata
@@ -22,7 +22,9 @@ class CustomFieldDefinition(Model):
22
22
  UniqueConstraint(name='unq_custom_field_entity_name', fields=['entity_type', 'field_name'])
23
23
  ]
24
24
 
25
- entity_type: Literal['Contact', 'Account', 'Deal'] = Field(title='Entity Type')
25
+ entity_type: Literal[
26
+ 'Entity', 'EntityRelationship', 'Deal', 'EntityIdentifier', 'EntityContactPoint', 'EntityAddress'
27
+ ] = Field(title='Entity Type')
26
28
  field_name: str = Field(title='Field Name')
27
29
  field_label: str = Field(title='Field Label')
28
30
  field_type: Literal['text', 'number', 'date', 'choice'] = Field(title='Field Type')
amsdal_crm/models/deal.py CHANGED
@@ -3,7 +3,7 @@
3
3
  import datetime as _dt
4
4
  from typing import Any
5
5
  from typing import ClassVar
6
- from typing import Optional
6
+ from typing import Literal
7
7
 
8
8
  from amsdal.contrib.auth.models.user import User
9
9
  from amsdal.models.mixins import TimestampMixin
@@ -29,7 +29,6 @@ class Deal(TimestampMixin, Model):
29
29
 
30
30
  __module_type__: ClassVar[ModuleType] = ModuleType.CONTRIB
31
31
  __indexes__: ClassVar[list[IndexInfo]] = [
32
- IndexInfo(name='idx_deal_owner_email', field='owner_email'),
33
32
  IndexInfo(name='idx_deal_close_date', field='expected_close_date'),
34
33
  IndexInfo(name='idx_deal_created_at', field='created_at'),
35
34
  ]
@@ -40,18 +39,16 @@ class Deal(TimestampMixin, Model):
40
39
  currency: str = Field(default='USD', title='Currency')
41
40
 
42
41
  # Relationships
43
- account: Optional['Account'] = Field(default=None, title='Account')
44
- contact: Optional['Contact'] = Field(default=None, title='Primary Contact')
42
+ entity: 'Entity' = Field(title='Entity')
45
43
  stage: 'Stage' = Field(title='Stage')
46
- owner_email: str = Field(title='Owner Email')
44
+ assigned_to: User | None = Field(default=None, title='Assigned To')
47
45
 
48
46
  # Dates
49
47
  expected_close_date: _dt.datetime | None = Field(default=None, title='Expected Close Date')
50
48
  closed_date: _dt.datetime | None = Field(default=None, title='Closed Date')
51
49
 
52
50
  # Status tracking
53
- is_closed: bool = Field(default=False, title='Is Closed')
54
- is_won: bool = Field(default=False, title='Is Won')
51
+ status: Literal['open', 'closed_won', 'closed_lost'] = Field(default='open', title='Status')
55
52
 
56
53
  # Custom fields (JSON)
57
54
  custom_fields: dict[str, Any] | None = Field(default=None, title='Custom Fields')
@@ -78,8 +75,9 @@ class Deal(TimestampMixin, Model):
78
75
  Returns:
79
76
  True if user has permission, False otherwise
80
77
  """
78
+
81
79
  # Owner has all permissions
82
- if self.owner_email == user.email:
80
+ if self.assigned_to and self.assigned_to.email == user.email:
83
81
  return True
84
82
 
85
83
  # Check admin permissions
@@ -124,10 +122,16 @@ class Deal(TimestampMixin, Model):
124
122
  from amsdal_models.classes.helpers.reference_loader import ReferenceLoader
125
123
 
126
124
  stage = ReferenceLoader(self.stage).load_reference() if isinstance(self.stage, Reference) else self.stage
127
- self.is_closed = stage.is_closed_won or stage.is_closed_lost
128
- self.is_won = stage.is_closed_won
129
125
 
130
- if self.is_closed and not self.closed_date:
126
+ if stage.status == 'open':
127
+ self.status = 'open'
128
+ if stage.status == 'closed_won':
129
+ self.status = 'closed_won'
130
+
131
+ if stage.status == 'closed_lost':
132
+ self.status = 'closed_lost'
133
+
134
+ if self.status in ('closed_won', 'closed_lost') and not self.closed_date:
131
135
  self.closed_date = _dt.datetime.now(_dt.UTC)
132
136
 
133
137
  # Call parent to handle timestamps
@@ -148,10 +152,15 @@ class Deal(TimestampMixin, Model):
148
152
  # Load stage if it's a reference and sync closed status
149
153
 
150
154
  stage = await self.stage
151
- self.is_closed = stage.is_closed_won or stage.is_closed_lost
152
- self.is_won = stage.is_closed_won
155
+ if stage.status == 'open':
156
+ self.status = 'open'
157
+ if stage.status == 'closed_won':
158
+ self.status = 'closed_won'
153
159
 
154
- if self.is_closed and not self.closed_date:
160
+ if stage.status == 'closed_lost':
161
+ self.status = 'closed_lost'
162
+
163
+ if self.status in ('closed_won', 'closed_lost') and not self.closed_date:
155
164
  self.closed_date = _dt.datetime.now(_dt.UTC)
156
165
 
157
166
  # Call parent to handle timestamps
@@ -167,11 +176,10 @@ class Deal(TimestampMixin, Model):
167
176
  """Async hook called after updating deal."""
168
177
  from amsdal_crm.services.workflow_service import WorkflowService
169
178
 
170
- WorkflowService.execute_rules('Deal', 'update', self)
179
+ await WorkflowService.aexecute_rules('Deal', 'update', self)
171
180
 
172
181
 
173
- from amsdal_crm.models.account import Account
174
- from amsdal_crm.models.contact import Contact
182
+ from amsdal_crm.models.entity import Entity
175
183
  from amsdal_crm.models.stage import Stage
176
184
 
177
185
  Deal.model_rebuild()
@@ -0,0 +1,318 @@
1
+ """Account Model."""
2
+
3
+ from typing import Any
4
+ from typing import ClassVar
5
+ from typing import Literal
6
+
7
+ from amsdal.contrib.auth.models.user import User
8
+ from amsdal.models.mixins import TimestampMixin
9
+ from amsdal_models.classes.data_models.constraints import UniqueConstraint
10
+ from amsdal_models.classes.data_models.indexes import IndexInfo
11
+ from amsdal_models.classes.model import Model
12
+ from amsdal_utils.models.enums import ModuleType
13
+ from pydantic.fields import Field
14
+
15
+
16
+ class Entity(TimestampMixin, Model):
17
+ """Entity (Person/Organization/Trust) model.
18
+
19
+ Represents a company or organization in the CRM system.
20
+ Owned by individual users with permission controls.
21
+ """
22
+
23
+ __module_type__: ClassVar[ModuleType] = ModuleType.CONTRIB
24
+ __constraints__: ClassVar[list[UniqueConstraint]] = [UniqueConstraint(name='unq_entity_name', fields=['name'])]
25
+ __indexes__: ClassVar[list[IndexInfo]] = [
26
+ IndexInfo(name='idx_entity_created_at', field='created_at'),
27
+ ]
28
+
29
+ # Core fields
30
+ name: str = Field(title='Entity Name')
31
+ legal_name: str | None = Field(default=None, title='Legal Name')
32
+ status: Literal['Active', 'Inactive'] = Field(default='Active', title='Status')
33
+ note: str | None = Field(default=None, title='Note')
34
+
35
+ assigned_to: User | None = Field(default=None, title='Assigned To')
36
+
37
+ # Custom fields (JSON)
38
+ custom_fields: dict[str, Any] | None = Field(default=None, title='Custom Fields')
39
+
40
+ @property
41
+ def display_name(self) -> str:
42
+ """Return display name for the account."""
43
+ return self.name
44
+
45
+ def has_object_permission(self, user: 'User', action: str) -> bool:
46
+ """Check if user has permission to perform action on this account.
47
+
48
+ Args:
49
+ user: The user attempting the action
50
+ action: The action being attempted (read, create, update, delete)
51
+
52
+ Returns:
53
+ True if user has permission, False otherwise
54
+ """
55
+ if self.assigned_to and self.assigned_to.email == user.email:
56
+ return True
57
+
58
+ # Check admin permissions
59
+ if user.permissions:
60
+ for permission in user.permissions:
61
+ if permission.model == '*' and permission.action in ('*', action):
62
+ return True
63
+ if permission.model == 'Entity' and permission.action in ('*', action):
64
+ return True
65
+
66
+ return False
67
+
68
+ def pre_create(self) -> None:
69
+ """Hook called before creating account."""
70
+ if self.custom_fields:
71
+ from amsdal_crm.services.custom_field_service import CustomFieldService
72
+
73
+ self.custom_fields = CustomFieldService.validate_custom_fields('Entity', self.custom_fields)
74
+ super().pre_create()
75
+
76
+ async def apre_create(self) -> None:
77
+ """Async hook called before creating account."""
78
+ if self.custom_fields:
79
+ from amsdal_crm.services.custom_field_service import CustomFieldService
80
+
81
+ self.custom_fields = await CustomFieldService.avalidate_custom_fields('Entity', self.custom_fields)
82
+ await super().apre_create()
83
+
84
+ def pre_update(self) -> None:
85
+ """Hook called before updating account."""
86
+ # Validate custom fields first
87
+ if self.custom_fields:
88
+ from amsdal_crm.services.custom_field_service import CustomFieldService
89
+
90
+ self.custom_fields = CustomFieldService.validate_custom_fields('Entity', self.custom_fields)
91
+
92
+ # Call parent to handle timestamps
93
+ super().pre_update()
94
+
95
+ async def apre_update(self) -> None:
96
+ """Async hook called before updating account."""
97
+ # Validate custom fields first
98
+ if self.custom_fields:
99
+ from amsdal_crm.services.custom_field_service import CustomFieldService
100
+
101
+ self.custom_fields = await CustomFieldService.avalidate_custom_fields('Entity', self.custom_fields)
102
+
103
+ # Call parent to handle timestamps
104
+ await super().apre_update()
105
+
106
+ def post_update(self) -> None:
107
+ """Hook called after updating account."""
108
+ from amsdal_crm.services.workflow_service import WorkflowService
109
+
110
+ WorkflowService.execute_rules('Entity', 'update', self)
111
+
112
+ async def apost_update(self) -> None:
113
+ """Async hook called after updating account."""
114
+ from amsdal_crm.services.workflow_service import WorkflowService
115
+
116
+ await WorkflowService.aexecute_rules('Entity', 'update', self)
117
+
118
+
119
+ class EntityRelationship(TimestampMixin, Model):
120
+ from_entity: Entity = Field(title='From Entity')
121
+ to_entity: Entity = Field(title='To Entity')
122
+ start_date: str | None = Field(default=None, title='Start Date')
123
+ end_date: str | None = Field(default=None, title='End Date')
124
+ relationship_group_name: str | None = Field(default=None, title='Relationship Group Name')
125
+
126
+ def pre_create(self) -> None:
127
+ """Hook called before creating account."""
128
+ if self.custom_fields:
129
+ from amsdal_crm.services.custom_field_service import CustomFieldService
130
+
131
+ self.custom_fields = CustomFieldService.validate_custom_fields('EntityRelationship', self.custom_fields)
132
+ super().pre_create()
133
+
134
+ async def apre_create(self) -> None:
135
+ """Async hook called before creating account."""
136
+ if self.custom_fields:
137
+ from amsdal_crm.services.custom_field_service import CustomFieldService
138
+
139
+ self.custom_fields = await CustomFieldService.avalidate_custom_fields(
140
+ 'EntityRelationship', self.custom_fields
141
+ )
142
+ await super().apre_create()
143
+
144
+ def pre_update(self) -> None:
145
+ """Hook called before updating account."""
146
+ # Validate custom fields first
147
+ if self.custom_fields:
148
+ from amsdal_crm.services.custom_field_service import CustomFieldService
149
+
150
+ self.custom_fields = CustomFieldService.validate_custom_fields('EntityRelationship', self.custom_fields)
151
+
152
+ # Call parent to handle timestamps
153
+ super().pre_update()
154
+
155
+ async def apre_update(self) -> None:
156
+ """Async hook called before updating account."""
157
+ # Validate custom fields first
158
+ if self.custom_fields:
159
+ from amsdal_crm.services.custom_field_service import CustomFieldService
160
+
161
+ self.custom_fields = await CustomFieldService.avalidate_custom_fields(
162
+ 'EntityRelationship', self.custom_fields
163
+ )
164
+
165
+ # Call parent to handle timestamps
166
+ await super().apre_update()
167
+
168
+
169
+ class EntityIdentifier(TimestampMixin, Model):
170
+ entity: Entity = Field(title='Entity')
171
+ value: str = Field(title='Identifier Value')
172
+ country: str | None = Field(default=None, title='Country')
173
+
174
+ # TODO: validate one per entity
175
+ is_primary: bool = Field(default=False, title='Is Primary')
176
+
177
+ def pre_create(self) -> None:
178
+ """Hook called before creating account."""
179
+ if self.custom_fields:
180
+ from amsdal_crm.services.custom_field_service import CustomFieldService
181
+
182
+ self.custom_fields = CustomFieldService.validate_custom_fields('EntityIdentifier', self.custom_fields)
183
+ super().pre_create()
184
+
185
+ async def apre_create(self) -> None:
186
+ """Async hook called before creating account."""
187
+ if self.custom_fields:
188
+ from amsdal_crm.services.custom_field_service import CustomFieldService
189
+
190
+ self.custom_fields = await CustomFieldService.avalidate_custom_fields(
191
+ 'EntityIdentifier', self.custom_fields
192
+ )
193
+ await super().apre_create()
194
+
195
+ def pre_update(self) -> None:
196
+ """Hook called before updating account."""
197
+ # Validate custom fields first
198
+ if self.custom_fields:
199
+ from amsdal_crm.services.custom_field_service import CustomFieldService
200
+
201
+ self.custom_fields = CustomFieldService.validate_custom_fields('EntityIdentifier', self.custom_fields)
202
+
203
+ # Call parent to handle timestamps
204
+ super().pre_update()
205
+
206
+ async def apre_update(self) -> None:
207
+ """Async hook called before updating account."""
208
+ # Validate custom fields first
209
+ if self.custom_fields:
210
+ from amsdal_crm.services.custom_field_service import CustomFieldService
211
+
212
+ self.custom_fields = await CustomFieldService.avalidate_custom_fields(
213
+ 'EntityIdentifier', self.custom_fields
214
+ )
215
+
216
+ # Call parent to handle timestamps
217
+ await super().apre_update()
218
+
219
+
220
+ class EntityContactPoint(TimestampMixin, Model):
221
+ entity: Entity = Field(title='Entity')
222
+ value: str = Field(title='Contact Point Value')
223
+
224
+ # TODO: validate one per entity
225
+ is_primary: bool = Field(default=False, title='Is Primary')
226
+ can_contact: bool = Field(default=True, title='Can Contact')
227
+
228
+ def pre_create(self) -> None:
229
+ """Hook called before creating account."""
230
+ if self.custom_fields:
231
+ from amsdal_crm.services.custom_field_service import CustomFieldService
232
+
233
+ self.custom_fields = CustomFieldService.validate_custom_fields('EntityContactPoint', self.custom_fields)
234
+ super().pre_create()
235
+
236
+ async def apre_create(self) -> None:
237
+ """Async hook called before creating account."""
238
+ if self.custom_fields:
239
+ from amsdal_crm.services.custom_field_service import CustomFieldService
240
+
241
+ self.custom_fields = await CustomFieldService.avalidate_custom_fields(
242
+ 'EntityContactPoint', self.custom_fields
243
+ )
244
+ await super().apre_create()
245
+
246
+ def pre_update(self) -> None:
247
+ """Hook called before updating account."""
248
+ # Validate custom fields first
249
+ if self.custom_fields:
250
+ from amsdal_crm.services.custom_field_service import CustomFieldService
251
+
252
+ self.custom_fields = CustomFieldService.validate_custom_fields('EntityContactPoint', self.custom_fields)
253
+
254
+ # Call parent to handle timestamps
255
+ super().pre_update()
256
+
257
+ async def apre_update(self) -> None:
258
+ """Async hook called before updating account."""
259
+ # Validate custom fields first
260
+ if self.custom_fields:
261
+ from amsdal_crm.services.custom_field_service import CustomFieldService
262
+
263
+ self.custom_fields = await CustomFieldService.avalidate_custom_fields(
264
+ 'EntityContactPoint', self.custom_fields
265
+ )
266
+
267
+ # Call parent to handle timestamps
268
+ await super().apre_update()
269
+
270
+
271
+ class EntityAddress(TimestampMixin, Model):
272
+ line1: str | None = Field(title='Address Line 1')
273
+ line2: str | None = Field(default=None, title='Address Line 2')
274
+ city: str | None = Field(title='City')
275
+ region: str | None = Field(default=None, title='Region/State')
276
+ postal_code: str | None = Field(default=None, title='Postal Code')
277
+ country: str | None = Field(title='Country')
278
+
279
+ # TODO: validate one per entity
280
+ is_primary: bool = Field(default=False, title='Is Primary')
281
+
282
+ def pre_create(self) -> None:
283
+ """Hook called before creating account."""
284
+ if self.custom_fields:
285
+ from amsdal_crm.services.custom_field_service import CustomFieldService
286
+
287
+ self.custom_fields = CustomFieldService.validate_custom_fields('EntityAddress', self.custom_fields)
288
+ super().pre_create()
289
+
290
+ async def apre_create(self) -> None:
291
+ """Async hook called before creating account."""
292
+ if self.custom_fields:
293
+ from amsdal_crm.services.custom_field_service import CustomFieldService
294
+
295
+ self.custom_fields = await CustomFieldService.avalidate_custom_fields('EntityAddress', self.custom_fields)
296
+ await super().apre_create()
297
+
298
+ def pre_update(self) -> None:
299
+ """Hook called before updating account."""
300
+ # Validate custom fields first
301
+ if self.custom_fields:
302
+ from amsdal_crm.services.custom_field_service import CustomFieldService
303
+
304
+ self.custom_fields = CustomFieldService.validate_custom_fields('EntityAddress', self.custom_fields)
305
+
306
+ # Call parent to handle timestamps
307
+ super().pre_update()
308
+
309
+ async def apre_update(self) -> None:
310
+ """Async hook called before updating account."""
311
+ # Validate custom fields first
312
+ if self.custom_fields:
313
+ from amsdal_crm.services.custom_field_service import CustomFieldService
314
+
315
+ self.custom_fields = await CustomFieldService.avalidate_custom_fields('EntityAddress', self.custom_fields)
316
+
317
+ # Call parent to handle timestamps
318
+ await super().apre_update()
@@ -1,6 +1,7 @@
1
1
  """Stage Model."""
2
2
 
3
3
  from typing import ClassVar
4
+ from typing import Literal
4
5
 
5
6
  from amsdal_models.classes.data_models.indexes import IndexInfo
6
7
  from amsdal_models.classes.model import Model
@@ -33,10 +34,11 @@ class Stage(Model):
33
34
 
34
35
  pipeline: 'Pipeline' = Field(title='Pipeline')
35
36
  name: str = Field(title='Stage Name')
37
+ description: str | None = Field(default=None, title='Description')
36
38
  order: int = Field(title='Order')
37
39
  probability: float = Field(default=0.0, title='Win Probability (%)', ge=0, le=100)
38
- is_closed_won: bool = Field(default=False, title='Is Closed Won')
39
- is_closed_lost: bool = Field(default=False, title='Is Closed Lost')
40
+
41
+ status: Literal['open', 'closed_won', 'closed_lost'] = Field(default='open', title='Status')
40
42
 
41
43
  @property
42
44
  def display_name(self) -> str:
@@ -19,7 +19,7 @@ class WorkflowRule(Model):
19
19
  __module_type__: ClassVar[ModuleType] = ModuleType.CONTRIB
20
20
 
21
21
  name: str = Field(title='Rule Name')
22
- entity_type: Literal['Contact', 'Account', 'Deal', 'Activity'] = Field(title='Entity Type')
22
+ entity_type: Literal['Entity', 'Deal', 'Activity'] = Field(title='Entity Type')
23
23
 
24
24
  # Trigger
25
25
  trigger_event: Literal['create', 'update', 'delete'] = Field(title='Trigger Event')
@@ -50,7 +50,6 @@ class DealService:
50
50
  description=note or f'Deal stage changed from {old_stage_name} to {new_stage.name}',
51
51
  related_to_type=ActivityRelatedTo.DEAL,
52
52
  related_to_id=deal._object_id,
53
- owner_email=user_email,
54
53
  due_date=None,
55
54
  completed_at=None,
56
55
  is_completed=False,
@@ -66,9 +65,9 @@ class DealService:
66
65
  user_email=user_email,
67
66
  )
68
67
 
69
- if new_stage.is_closed_won:
68
+ if new_stage.status == 'closed_won':
70
69
  LifecycleProducer.publish(CRMLifecycleEvent.ON_DEAL_WON, deal=deal, user_email=user_email) # type: ignore[arg-type]
71
- elif new_stage.is_closed_lost:
70
+ elif new_stage.status == 'closed_lost':
72
71
  LifecycleProducer.publish(CRMLifecycleEvent.ON_DEAL_LOST, deal=deal, user_email=user_email) # type: ignore[arg-type]
73
72
 
74
73
  return deal
@@ -107,7 +106,6 @@ class DealService:
107
106
  description=note or f'Deal stage changed from {old_stage_name} to {new_stage.name}',
108
107
  related_to_type=ActivityRelatedTo.DEAL,
109
108
  related_to_id=deal._object_id,
110
- owner_email=user_email,
111
109
  due_date=None,
112
110
  completed_at=None,
113
111
  is_completed=False,
@@ -123,9 +121,9 @@ class DealService:
123
121
  user_email=user_email,
124
122
  )
125
123
 
126
- if new_stage.is_closed_won:
124
+ if new_stage.status == 'closed_won':
127
125
  await LifecycleProducer.publish_async(CRMLifecycleEvent.ON_DEAL_WON, deal=deal, user_email=user_email) # type: ignore[arg-type]
128
- elif new_stage.is_closed_lost:
126
+ elif new_stage.status == 'closed_lost':
129
127
  await LifecycleProducer.publish_async(CRMLifecycleEvent.ON_DEAL_LOST, deal=deal, user_email=user_email) # type: ignore[arg-type]
130
128
 
131
129
  return deal
@@ -22,7 +22,6 @@ class EmailService:
22
22
  cc_addresses: list[str] | None,
23
23
  related_to_type: ActivityRelatedTo,
24
24
  related_to_id: str,
25
- owner_email: str,
26
25
  *,
27
26
  is_outbound: bool = True,
28
27
  ) -> EmailActivity:
@@ -38,9 +37,8 @@ class EmailService:
38
37
  from_address: Sender email address
39
38
  to_addresses: List of recipient email addresses
40
39
  cc_addresses: List of CC email addresses
41
- related_to_type: Type of related record (Contact, Account, Deal)
40
+ related_to_type: Type of related record (Entity, Deal)
42
41
  related_to_id: ID of related record
43
- owner_email: Email of user who owns this activity
44
42
  is_outbound: True if sent from CRM, False if received
45
43
 
46
44
  Returns:
@@ -55,7 +53,6 @@ class EmailService:
55
53
  cc_addresses=cc_addresses,
56
54
  related_to_type=related_to_type,
57
55
  related_to_id=related_to_id,
58
- owner_email=owner_email,
59
56
  is_outbound=is_outbound,
60
57
  description=f'Email: {subject}',
61
58
  due_date=None,
@@ -77,7 +74,6 @@ class EmailService:
77
74
  cc_addresses: list[str] | None,
78
75
  related_to_type: ActivityRelatedTo,
79
76
  related_to_id: str,
80
- owner_email: str,
81
77
  *,
82
78
  is_outbound: bool = True,
83
79
  ) -> EmailActivity:
@@ -89,9 +85,8 @@ class EmailService:
89
85
  from_address: Sender email address
90
86
  to_addresses: List of recipient email addresses
91
87
  cc_addresses: List of CC email addresses
92
- related_to_type: Type of related record (Contact, Account, Deal)
88
+ related_to_type: Type of related record (Entity, Deal)
93
89
  related_to_id: ID of related record
94
- owner_email: Email of user who owns this activity
95
90
  is_outbound: True if sent from CRM, False if received
96
91
 
97
92
  Returns:
@@ -106,7 +101,6 @@ class EmailService:
106
101
  cc_addresses=cc_addresses,
107
102
  related_to_type=related_to_type,
108
103
  related_to_id=related_to_id,
109
- owner_email=owner_email,
110
104
  is_outbound=is_outbound,
111
105
  description=f'Email: {subject}',
112
106
  due_date=None,