letschatty 0.4.351__py3-none-any.whl → 0.4.353__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.

Potentially problematic release.


This version of letschatty might be problematic. Click here for more details.

Files changed (92) hide show
  1. letschatty/models/ai_microservices/__init__.py +4 -4
  2. letschatty/models/ai_microservices/expected_output.py +2 -29
  3. letschatty/models/ai_microservices/lambda_events.py +28 -155
  4. letschatty/models/ai_microservices/lambda_invokation_types.py +1 -4
  5. letschatty/models/ai_microservices/n8n_ai_agents_payload.py +1 -3
  6. letschatty/models/analytics/events/__init__.py +3 -3
  7. letschatty/models/analytics/events/chat_based_events/chat_client.py +19 -0
  8. letschatty/models/analytics/events/chat_based_events/chat_funnel.py +69 -13
  9. letschatty/models/analytics/events/company_based_events/asset_events.py +9 -2
  10. letschatty/models/analytics/events/event_type_to_classes.py +7 -3
  11. letschatty/models/analytics/events/event_types.py +11 -50
  12. letschatty/models/chat/chat.py +13 -2
  13. letschatty/models/chat/chat_with_assets.py +6 -1
  14. letschatty/models/chat/client.py +0 -2
  15. letschatty/models/chat/continuous_conversation.py +1 -1
  16. letschatty/models/company/CRM/funnel.py +365 -33
  17. letschatty/models/company/__init__.py +10 -1
  18. letschatty/models/company/assets/ai_agents_v2/ai_agents_decision_output.py +1 -1
  19. letschatty/models/company/assets/ai_agents_v2/chatty_ai_agent_in_chat.py +0 -4
  20. letschatty/models/company/assets/ai_agents_v2/chatty_ai_mode.py +2 -2
  21. letschatty/models/company/assets/ai_agents_v2/get_chat_with_prompt_response.py +0 -1
  22. letschatty/models/company/assets/ai_agents_v2/pre_qualify_config.py +1 -28
  23. letschatty/models/company/assets/automation.py +10 -19
  24. letschatty/models/company/assets/chat_assets.py +3 -2
  25. letschatty/models/company/assets/company_assets.py +2 -0
  26. letschatty/models/company/assets/sale.py +3 -3
  27. letschatty/models/company/empresa.py +4 -1
  28. letschatty/models/company/integrations/product_sync_status.py +28 -0
  29. letschatty/models/company/integrations/shopify/company_shopify_integration.py +62 -0
  30. letschatty/models/company/integrations/shopify/shopify_product_sync_status.py +18 -0
  31. letschatty/models/company/integrations/shopify/shopify_webhook_topics.py +40 -0
  32. letschatty/models/company/integrations/sync_status_enum.py +9 -0
  33. letschatty/models/company/integrations/tienda_nube/company_tienda_nube_integration.py +62 -0
  34. letschatty/models/company/integrations/tienda_nube/tienda_nube_product_sync_status.py +18 -0
  35. letschatty/models/company/integrations/tienda_nube/tienda_nube_webhook_topics.py +46 -0
  36. letschatty/models/data_base/collection_interface.py +29 -101
  37. letschatty/models/data_base/mongo_connection.py +9 -92
  38. letschatty/models/messages/chatty_messages/schema/chatty_content/content_document.py +4 -2
  39. letschatty/models/messages/chatty_messages/schema/chatty_content/content_media.py +4 -3
  40. letschatty/models/utils/custom_exceptions/custom_exceptions.py +1 -14
  41. letschatty/services/ai_agents/smart_follow_up_context_builder_v2.py +2 -5
  42. letschatty/services/chat/chat_service.py +47 -11
  43. letschatty/services/chatty_assets/__init__.py +0 -12
  44. letschatty/services/chatty_assets/asset_service.py +13 -190
  45. letschatty/services/chatty_assets/base_container.py +2 -3
  46. letschatty/services/chatty_assets/base_container_with_collection.py +26 -35
  47. letschatty/services/continuous_conversation_service/continuous_conversation_helper.py +0 -11
  48. letschatty/services/events/events_manager.py +1 -218
  49. letschatty/services/factories/analytics/events_factory.py +30 -66
  50. letschatty/services/factories/lambda_ai_orchestrartor/lambda_events_factory.py +8 -46
  51. letschatty/services/messages_helpers/get_caption_or_body_or_preview.py +4 -6
  52. letschatty/services/validators/analytics_validator.py +11 -0
  53. {letschatty-0.4.351.dist-info → letschatty-0.4.353.dist-info}/METADATA +1 -1
  54. {letschatty-0.4.351.dist-info → letschatty-0.4.353.dist-info}/RECORD +56 -83
  55. letschatty/models/analytics/events/chat_based_events/ai_agent_execution_event.py +0 -71
  56. letschatty/services/chatty_assets/assets_collections.py +0 -137
  57. letschatty/services/chatty_assets/collections/__init__.py +0 -38
  58. letschatty/services/chatty_assets/collections/ai_agent_collection.py +0 -19
  59. letschatty/services/chatty_assets/collections/ai_agent_in_chat_collection.py +0 -32
  60. letschatty/services/chatty_assets/collections/ai_component_collection.py +0 -21
  61. letschatty/services/chatty_assets/collections/chain_of_thought_collection.py +0 -30
  62. letschatty/services/chatty_assets/collections/chat_collection.py +0 -21
  63. letschatty/services/chatty_assets/collections/contact_point_collection.py +0 -21
  64. letschatty/services/chatty_assets/collections/fast_answer_collection.py +0 -21
  65. letschatty/services/chatty_assets/collections/filter_criteria_collection.py +0 -18
  66. letschatty/services/chatty_assets/collections/flow_collection.py +0 -20
  67. letschatty/services/chatty_assets/collections/product_collection.py +0 -20
  68. letschatty/services/chatty_assets/collections/sale_collection.py +0 -20
  69. letschatty/services/chatty_assets/collections/source_collection.py +0 -21
  70. letschatty/services/chatty_assets/collections/tag_collection.py +0 -19
  71. letschatty/services/chatty_assets/collections/topic_collection.py +0 -21
  72. letschatty/services/chatty_assets/collections/user_collection.py +0 -20
  73. letschatty/services/chatty_assets/example_usage.py +0 -44
  74. letschatty/services/chatty_assets/services/__init__.py +0 -37
  75. letschatty/services/chatty_assets/services/ai_agent_in_chat_service.py +0 -73
  76. letschatty/services/chatty_assets/services/ai_agent_service.py +0 -23
  77. letschatty/services/chatty_assets/services/chain_of_thought_service.py +0 -70
  78. letschatty/services/chatty_assets/services/chat_service.py +0 -25
  79. letschatty/services/chatty_assets/services/contact_point_service.py +0 -29
  80. letschatty/services/chatty_assets/services/fast_answer_service.py +0 -32
  81. letschatty/services/chatty_assets/services/filter_criteria_service.py +0 -30
  82. letschatty/services/chatty_assets/services/flow_service.py +0 -25
  83. letschatty/services/chatty_assets/services/product_service.py +0 -30
  84. letschatty/services/chatty_assets/services/sale_service.py +0 -25
  85. letschatty/services/chatty_assets/services/source_service.py +0 -28
  86. letschatty/services/chatty_assets/services/tag_service.py +0 -32
  87. letschatty/services/chatty_assets/services/topic_service.py +0 -31
  88. letschatty/services/chatty_assets/services/user_service.py +0 -32
  89. letschatty/services/events/__init__.py +0 -6
  90. letschatty/services/factories/analytics/ai_agent_event_factory.py +0 -161
  91. {letschatty-0.4.351.dist-info → letschatty-0.4.353.dist-info}/LICENSE +0 -0
  92. {letschatty-0.4.351.dist-info → letschatty-0.4.353.dist-info}/WHEEL +0 -0
@@ -14,27 +14,18 @@ class Automation(BaseModel):
14
14
  chatty_ai_agent_config: Optional[ChattyAIConfigForAutomation] = Field(default=None)
15
15
  area: Optional[Area] = Field(default=None)
16
16
  agent_id: Optional[StrObjectId] = Field(default=None)
17
- chain_of_thought : Optional[str] = Field(default=None)
17
+ chain_of_thought: Optional[str] = Field(default=None)
18
+ # Funnel transition automations
19
+ target_funnel_id: Optional[StrObjectId] = Field(
20
+ default=None,
21
+ description="Target funnel to move the chat to (for cross-funnel transitions)"
22
+ )
23
+ target_stage_id: Optional[StrObjectId] = Field(
24
+ default=None,
25
+ description="Target stage within the target funnel"
26
+ )
18
27
  # client_info: Optional[ClientInfo] = Field(default=None) me gustaría que levante el mail y/o otros atributos
19
28
 
20
- @property
21
- def has_automation(self) -> bool:
22
- """
23
- Check if there's an actual automation defined (tags, products, or flow).
24
-
25
- Returns:
26
- bool: True if there's at least one tag, product, or flow defined
27
- """
28
- return (
29
- len(self.tags) > 0 or
30
- len(self.products) > 0 or
31
- len(self.flow) > 0 or
32
- self.quality_score is not None or
33
- self.chatty_ai_agent_config is not None or
34
- self.area is not None or
35
- self.highlight_description is not None
36
- )
37
-
38
29
  @model_validator(mode='after')
39
30
  def check_agent_id(self):
40
31
  if self.area == Area.WITH_AGENT and not self.agent_id:
@@ -10,7 +10,7 @@ from datetime import datetime
10
10
  from zoneinfo import ZoneInfo
11
11
  from bson import ObjectId
12
12
  import json
13
- from typing import Dict, Any, Optional
13
+ from typing import Dict, Any, Optional, List
14
14
  from letschatty.models.utils.types.serializer_type import SerializerType
15
15
  from letschatty.models.company.assets.ai_agents_v2.chatty_ai_mode import ChattyAIMode
16
16
 
@@ -67,7 +67,8 @@ class AssignedAssetToChat(BaseModel):
67
67
 
68
68
 
69
69
  class SaleAssignedToChat(AssignedAssetToChat):
70
- product_id: StrObjectId = Field(frozen=True)
70
+ product_id: Optional[StrObjectId] = Field(default=None)
71
+ product_ids: List[StrObjectId] = Field(default_factory=list)
71
72
 
72
73
  class ContactPointAssignedToChat(AssignedAssetToChat):
73
74
  source_id: StrObjectId = Field(frozen=True)
@@ -5,6 +5,8 @@ class CompanyAssetType(StrEnum):
5
5
  USERS = "users"
6
6
  BUSINESS_AREAS = "business_areas"
7
7
  FUNNELS = "funnels"
8
+ FUNNEL_STAGES = "funnel_stages"
9
+ FUNNEL_MEMBERS = "funnel_members"
8
10
  PRODUCTS = "products"
9
11
  SALES = "sales"
10
12
  TAGS = "tags"
@@ -5,7 +5,7 @@ from ...utils.types.identifier import StrObjectId
5
5
 
6
6
  class Sale(CompanyAssetModel):
7
7
  chat_id: StrObjectId
8
- product_id: StrObjectId
8
+ product_id: Optional[StrObjectId] = Field(default=None)
9
9
  quantity: int
10
10
  total_amount: float
11
11
  currency: str
@@ -16,7 +16,7 @@ class Sale(CompanyAssetModel):
16
16
  creator_id: StrObjectId
17
17
 
18
18
  class SaleRequest(BaseModel):
19
- product_id: StrObjectId
19
+ product_id: Optional[StrObjectId] = Field(default=None)
20
20
  quantity: int
21
21
  creator_id: StrObjectId
22
22
  total_amount: float
@@ -36,4 +36,4 @@ class SaleRequest(BaseModel):
36
36
  "paid_amount": 100,
37
37
  "installments": 1,
38
38
  "details": {}
39
- }
39
+ }
@@ -1,3 +1,5 @@
1
+ from letschatty.models.company.integrations.shopify.company_shopify_integration import ShopifyIntegration
2
+ from letschatty.models.company.integrations.tienda_nube.company_tienda_nube_integration import TiendaNubeIntegration
1
3
  from pydantic import Field, ConfigDict, field_validator, SecretStr, model_validator
2
4
  from typing import Optional, List, Dict
3
5
 
@@ -34,7 +36,8 @@ class EmpresaModel(ChattyAssetModel):
34
36
  continuous_conversation_template_name: Optional[str] = Field(default = None, description="The name of the continuous conversation template")
35
37
  default_follow_up_strategy_id: Optional[StrObjectId] = Field(default = None, description="The id of the default follow up strategy")
36
38
  messaging_settings: MessagingSettings = Field(default = MessagingSettings(), description="The messaging settings for the company")
37
-
39
+ shopify_integration: ShopifyIntegration = Field(default = ShopifyIntegration(), description="The Shopify integration for the company")
40
+ tienda_nube_integration: TiendaNubeIntegration = Field(default = TiendaNubeIntegration(), description="The Tienda Nube integration for the company")
38
41
 
39
42
  model_config = ConfigDict(
40
43
  validate_by_name=True,
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from typing import ClassVar, Optional
5
+
6
+ from pydantic import Field
7
+
8
+ from letschatty.models.company.integrations.sync_status_enum import SyncStatusEnum
9
+ from letschatty.models.base_models import CompanyAssetModel
10
+
11
+
12
+ class ProductSyncStatus(CompanyAssetModel):
13
+ """Generic product sync status for any e-commerce integration."""
14
+
15
+ COLLECTION: ClassVar[str] = "product_sync_statuses"
16
+
17
+ integration_type: str = Field(
18
+ description="Integration type (shopify, tiendanube, etc.)"
19
+ )
20
+ status: SyncStatusEnum = Field(description="Current sync status")
21
+
22
+ products_created: int = Field(default=0)
23
+ products_updated: int = Field(default=0)
24
+
25
+ name: str = Field(default="")
26
+
27
+ finished_at: Optional[datetime] = Field(default=None)
28
+
@@ -0,0 +1,62 @@
1
+ from typing import List, Optional
2
+ from datetime import datetime
3
+ from zoneinfo import ZoneInfo
4
+ from pydantic import BaseModel, Field
5
+
6
+ class ShopifyWebhookSubscription(BaseModel):
7
+ """Represents a single webhook subscription"""
8
+ topic: str = Field(description="Webhook topic (e.g., 'products/create')")
9
+ webhook_id: Optional[str] = Field(default=None, description="Shopify webhook ID")
10
+ subscribed_at: datetime = Field(default_factory=lambda: datetime.now(tz=ZoneInfo("UTC")))
11
+ is_active: bool = Field(default=True, description="Whether subscription is active")
12
+
13
+ class ShopifyIntegration(BaseModel):
14
+ """Shopify integration for the company"""
15
+ shopify_store_url: str = Field(default="")
16
+ oauth_state: str = Field(default="")
17
+ oauth_state_at: datetime = Field(default_factory=lambda: datetime.now(tz=ZoneInfo("UTC")))
18
+ access_token: Optional[str] = Field(default=None)
19
+ connected_at: Optional[datetime] = Field(default=None)
20
+ scope: Optional[str] = Field(default=None)
21
+
22
+ # Webhook subscriptions
23
+ webhook_subscriptions: List[ShopifyWebhookSubscription] = Field(
24
+ default_factory=list,
25
+ description="List of active webhook subscriptions"
26
+ )
27
+
28
+ # Scheduled sync settings
29
+ product_sync_enabled: bool = Field(
30
+ default=False,
31
+ description="Whether scheduled product sync is enabled"
32
+ )
33
+ product_sync_interval_hours: int = Field(
34
+ default=24,
35
+ description="Interval in hours for scheduled product sync"
36
+ )
37
+ last_product_sync_at: Optional[datetime] = Field(
38
+ default=None,
39
+ description="Timestamp of last product sync"
40
+ )
41
+
42
+ @property
43
+ def is_connected(self) -> bool:
44
+ """Check if the integration is fully connected"""
45
+ return bool(self.access_token and self.shopify_store_url)
46
+
47
+ def get_subscribed_topics(self) -> List[str]:
48
+ """Get list of currently subscribed webhook topics"""
49
+ return [sub.topic for sub in self.webhook_subscriptions if sub.is_active]
50
+
51
+ def reset(self) -> None:
52
+ """Reset integration to disconnected state"""
53
+ self.shopify_store_url = ""
54
+ self.oauth_state = ""
55
+ self.oauth_state_at = datetime.now(tz=ZoneInfo("UTC"))
56
+ self.access_token = None
57
+ self.connected_at = None
58
+ self.scope = None
59
+ self.webhook_subscriptions = []
60
+ self.product_sync_enabled = False
61
+ self.product_sync_interval_hours = 24
62
+ self.last_product_sync_at = None
@@ -0,0 +1,18 @@
1
+ from pydantic import Field
2
+
3
+ from letschatty.models.company.integrations.product_sync_status import ProductSyncStatus
4
+ from letschatty.models.company.integrations.sync_status_enum import SyncStatusEnum
5
+
6
+
7
+ # Backwards-compatible alias (Shopify-specific name, generic enum)
8
+ ShopifyProductSyncStatusEnum = SyncStatusEnum
9
+
10
+
11
+ class ShopifyProductSyncStatus(ProductSyncStatus):
12
+ """Shopify-flavored wrapper for the generic ProductSyncStatus."""
13
+
14
+ integration_type: str = Field(
15
+ default="shopify",
16
+ frozen=True,
17
+ description="Integration type for this sync status"
18
+ )
@@ -0,0 +1,40 @@
1
+ from enum import Enum
2
+
3
+ class ShopifyWebhookTopic(str, Enum):
4
+ """Shopify webhook topics for products and orders"""
5
+
6
+ # Product webhooks
7
+ PRODUCTS_CREATE = "products/create"
8
+ PRODUCTS_UPDATE = "products/update"
9
+ PRODUCTS_DELETE = "products/delete"
10
+
11
+ # Order webhooks
12
+ ORDERS_CREATE = "orders/create"
13
+ ORDERS_UPDATE = "orders/updated"
14
+ ORDERS_DELETE = "orders/delete"
15
+ ORDERS_FULFILLED = "orders/fulfilled"
16
+ ORDERS_PARTIALLY_FULFILLED = "orders/partially_fulfilled"
17
+ ORDERS_PAID = "orders/paid"
18
+ ORDERS_CANCELLED = "orders/cancelled"
19
+
20
+ @classmethod
21
+ def get_product_topics(cls) -> list[str]:
22
+ """Get all product-related webhook topics"""
23
+ return [
24
+ cls.PRODUCTS_CREATE.value,
25
+ cls.PRODUCTS_UPDATE.value,
26
+ cls.PRODUCTS_DELETE.value,
27
+ ]
28
+
29
+ @classmethod
30
+ def get_order_topics(cls) -> list[str]:
31
+ """Get all order-related webhook topics"""
32
+ return [
33
+ cls.ORDERS_CREATE.value,
34
+ cls.ORDERS_UPDATE.value,
35
+ cls.ORDERS_DELETE.value,
36
+ cls.ORDERS_FULFILLED.value,
37
+ cls.ORDERS_PARTIALLY_FULFILLED.value,
38
+ cls.ORDERS_PAID.value,
39
+ cls.ORDERS_CANCELLED.value,
40
+ ]
@@ -0,0 +1,9 @@
1
+ from enum import Enum
2
+
3
+
4
+ class SyncStatusEnum(str, Enum):
5
+ STARTED = "STARTED"
6
+ RUNNING = "RUNNING"
7
+ FINISHED = "FINISHED"
8
+ FAILED = "FAILED"
9
+
@@ -0,0 +1,62 @@
1
+ from typing import List, Optional
2
+ from datetime import datetime
3
+ from zoneinfo import ZoneInfo
4
+ from pydantic import BaseModel, Field
5
+
6
+ class TiendaNubeWebhookSubscription(BaseModel):
7
+ """Represents a single webhook subscription"""
8
+ topic: str = Field(description="Webhook topic (e.g., 'product/created')")
9
+ webhook_id: Optional[str] = Field(default=None, description="Tienda Nube webhook ID")
10
+ subscribed_at: datetime = Field(default_factory=lambda: datetime.now(tz=ZoneInfo("UTC")))
11
+ is_active: bool = Field(default=True, description="Whether subscription is active")
12
+
13
+ class TiendaNubeIntegration(BaseModel):
14
+ """Tienda Nube integration for the company"""
15
+ store_id: str = Field(default="")
16
+ oauth_state: str = Field(default="")
17
+ oauth_state_at: datetime = Field(default_factory=lambda: datetime.now(tz=ZoneInfo("UTC")))
18
+ access_token: Optional[str] = Field(default=None)
19
+ connected_at: Optional[datetime] = Field(default=None)
20
+ scope: Optional[str] = Field(default=None)
21
+
22
+ # Webhook subscriptions
23
+ webhook_subscriptions: List[TiendaNubeWebhookSubscription] = Field(
24
+ default_factory=list,
25
+ description="List of active webhook subscriptions"
26
+ )
27
+
28
+ # Scheduled sync settings
29
+ product_sync_enabled: bool = Field(
30
+ default=False,
31
+ description="Whether scheduled product sync is enabled"
32
+ )
33
+ product_sync_interval_hours: int = Field(
34
+ default=24,
35
+ description="Interval in hours for scheduled product sync"
36
+ )
37
+ last_product_sync_at: Optional[datetime] = Field(
38
+ default=None,
39
+ description="Timestamp of last product sync"
40
+ )
41
+
42
+ @property
43
+ def is_connected(self) -> bool:
44
+ """Check if the integration is fully connected"""
45
+ return bool(self.access_token and self.store_id)
46
+
47
+ def get_subscribed_topics(self) -> List[str]:
48
+ """Get list of currently subscribed webhook topics"""
49
+ return [sub.topic for sub in self.webhook_subscriptions if sub.is_active]
50
+
51
+ def reset(self) -> None:
52
+ """Reset integration to disconnected state"""
53
+ self.store_id = ""
54
+ self.oauth_state = ""
55
+ self.oauth_state_at = datetime.now(tz=ZoneInfo("UTC"))
56
+ self.access_token = None
57
+ self.connected_at = None
58
+ self.scope = None
59
+ self.webhook_subscriptions = []
60
+ self.product_sync_enabled = False
61
+ self.product_sync_interval_hours = 24
62
+ self.last_product_sync_at = None
@@ -0,0 +1,18 @@
1
+ from pydantic import Field
2
+
3
+ from letschatty.models.company.integrations.product_sync_status import ProductSyncStatus
4
+ from letschatty.models.company.integrations.sync_status_enum import SyncStatusEnum
5
+
6
+
7
+ # Backwards-compatible alias (Tienda Nube-specific name, generic enum)
8
+ TiendaNubeProductSyncStatusEnum = SyncStatusEnum
9
+
10
+
11
+ class TiendaNubeProductSyncStatus(ProductSyncStatus):
12
+ """Tienda Nube-flavored wrapper for the generic ProductSyncStatus."""
13
+
14
+ integration_type: str = Field(
15
+ default="tiendanube",
16
+ frozen=True,
17
+ description="Integration type for this sync status"
18
+ )
@@ -0,0 +1,46 @@
1
+ from enum import Enum
2
+
3
+ class TiendaNubeWebhookTopic(str, Enum):
4
+ """Tienda Nube webhook topics for products and orders"""
5
+
6
+ # Product webhooks
7
+ PRODUCT_CREATED = "product/created"
8
+ PRODUCT_UPDATED = "product/updated"
9
+ PRODUCT_DELETED = "product/deleted"
10
+
11
+ # Order webhooks
12
+ ORDER_CREATED = "order/created"
13
+ ORDER_UPDATED = "order/updated"
14
+ ORDER_PAID = "order/paid"
15
+ ORDER_PACKED = "order/packed"
16
+ ORDER_FULFILLED = "order/fulfilled"
17
+ ORDER_CANCELLED = "order/cancelled"
18
+ ORDER_EDITED = "order/edited"
19
+ ORDER_PENDING = "order/pending"
20
+ ORDER_VOIDED = "order/voided"
21
+ ORDER_UNPACKED = "order/unpacked"
22
+
23
+ @classmethod
24
+ def get_product_topics(cls) -> list[str]:
25
+ """Get all product-related webhook topics"""
26
+ return [
27
+ cls.PRODUCT_CREATED.value,
28
+ cls.PRODUCT_UPDATED.value,
29
+ cls.PRODUCT_DELETED.value,
30
+ ]
31
+
32
+ @classmethod
33
+ def get_order_topics(cls) -> list[str]:
34
+ """Get all order-related webhook topics"""
35
+ return [
36
+ cls.ORDER_CREATED.value,
37
+ cls.ORDER_UPDATED.value,
38
+ cls.ORDER_PAID.value,
39
+ cls.ORDER_PACKED.value,
40
+ cls.ORDER_FULFILLED.value,
41
+ cls.ORDER_CANCELLED.value,
42
+ cls.ORDER_EDITED.value,
43
+ cls.ORDER_PENDING.value,
44
+ cls.ORDER_VOIDED.value,
45
+ cls.ORDER_UNPACKED.value,
46
+ ]
@@ -4,7 +4,6 @@ from typing import TYPE_CHECKING, Dict, List, Generic, TypeVar, Type, Optional,
4
4
  from bson.objectid import ObjectId
5
5
  from pymongo.collection import Collection
6
6
  from pymongo.database import Database
7
- from motor.motor_asyncio import AsyncIOMotorDatabase, AsyncIOMotorCollection
8
7
 
9
8
  from letschatty.models.chat.chat import Chat
10
9
  from ...models.base_models.chatty_asset_model import ChattyAssetModel, CompanyAssetModel, ChattyAssetPreview
@@ -27,116 +26,58 @@ P = TypeVar('P', bound=ChattyAssetPreview)
27
26
  class ChattyAssetCollectionInterface(Generic[T, P], ABC):
28
27
  def __init__(self, database: str, collection: str, connection: MongoConnection, type: Type[T], preview_type: Optional[Type[P]] = None):
29
28
  logger.info(f"Initializing collection {collection} in database {database}")
30
- # Sync database and collection (existing)
31
29
  self.db: Database = connection.client[database]
32
30
  self.collection: Collection = connection.client[database][collection]
33
-
34
- # NEW: Async database and collection
35
- # Store connection reference to ensure we use current event loop
36
- self._connection = connection
37
- self._database_name = database
38
- self._collection_name = collection
39
- self._async_db: Optional[AsyncIOMotorDatabase] = None
40
- self._async_collection: Optional[AsyncIOMotorCollection] = None
41
-
42
31
  self.type = type
43
32
  self.preview_type = preview_type
44
-
45
- @property
46
- def async_db(self) -> AsyncIOMotorDatabase:
47
- """Get async database, ensuring it uses the current event loop"""
48
- # Always ensure connection's async client is using current loop (for Lambda compatibility)
49
- self._connection._ensure_async_client_loop()
50
- # Recreate database reference to ensure it uses the current client
51
- self._async_db = self._connection.async_client[self._database_name]
52
- return self._async_db
53
-
54
- @property
55
- def async_collection(self) -> AsyncIOMotorCollection:
56
- """Get async collection, ensuring it uses the current event loop"""
57
- # Always ensure connection's async client is using current loop (for Lambda compatibility)
58
- self._connection._ensure_async_client_loop()
59
- # Recreate collection reference to ensure it uses the current client
60
- self._async_collection = self._connection.async_client[self._database_name][self._collection_name]
61
- return self._async_collection
62
33
  @abstractmethod
63
34
  def create_instance(self, data: dict) -> T:
64
35
  """Factory method to create instance from data"""
65
36
  pass
66
37
 
67
- # All methods are now async-only for better performance
68
- async def insert(self, asset: T) -> StrObjectId:
69
- """Async insert operation"""
38
+ def insert(self, asset: T) -> StrObjectId:
70
39
  if not isinstance(asset, self.type):
71
40
  raise ValueError(f"Asset must be of type {self.type.__name__}")
72
41
  document = asset.model_dump_json(serializer=SerializerType.DATABASE)
73
42
  logger.debug(f"Inserting document: {document}")
74
- result = await self.async_collection.insert_one(document)
43
+ result = self.collection.insert_one(document)
75
44
  if not result.inserted_id:
76
45
  raise Exception("Failed to insert document")
77
46
  logger.debug(f"Inserted document with id {result.inserted_id}")
78
47
  return result.inserted_id
79
48
 
80
- async def update(self, asset: T) -> StrObjectId:
81
- """Async update operation"""
49
+ def update(self, asset: T) -> StrObjectId:
82
50
  logger.debug(f"Updating document with id {asset.id}")
83
51
  if not isinstance(asset, self.type):
84
52
  raise ValueError(f"Asset must be of type {self.type.__name__}")
85
53
  asset.update_now()
86
54
  document = asset.model_dump_json(serializer=SerializerType.DATABASE)
87
- document.pop('_id', None)
88
- result = await self.async_collection.update_one(
89
- {"_id": ObjectId(asset.id)},
90
- {"$set": document}
91
- )
55
+ document.pop('_id', None) # Still needed
56
+ result = self.collection.update_one({"_id": ObjectId(asset.id)}, {"$set": document})
92
57
  if result.matched_count == 0:
93
58
  raise NotFoundError(f"No document found with id {asset.id}")
94
59
  if result.modified_count == 0:
95
60
  logger.debug(f"No changes were made to the document with id {asset.id} probably because the values were the same")
96
61
  return asset.id
97
62
 
98
- async def get_by_id(self, doc_id: str) -> T:
99
- """Get by ID operation"""
100
- logger.debug(f"Getting document with id {doc_id} from collection {self.async_collection.name}")
101
- doc = await self.async_collection.find_one({"_id": ObjectId(doc_id)})
63
+ def get_by_id(self, doc_id: str) -> T:
64
+ logger.debug(f"Getting document with id {doc_id} from collection {self.collection.name} and db {self.db.name}")
65
+ doc = self.collection.find_one({"_id": ObjectId(doc_id)})
66
+
102
67
  if doc:
103
68
  return self.create_instance(doc)
104
69
  else:
105
- raise NotFoundError(f"No document found with id {doc_id} in collection")
70
+ raise NotFoundError(f"No document found with id {doc_id} in db collection {self.collection.name} and db {self.db.name}")
106
71
 
107
- async def get_docs(self, company_id: Optional[StrObjectId], query={}, limit=0) -> List[T]:
108
- """Get multiple documents operation"""
109
- logger.debug(f"Getting documents from collection with company_id {company_id} and query {query}")
72
+ def get_docs(self, company_id:Optional[StrObjectId], query = {}, limit = 0) -> List[T]:
73
+ logger.debug(f"Getting documents from collection {self.collection.name} with company_id {company_id} and query {query}")
110
74
  if company_id:
111
- query = query.copy()
75
+ query = query.copy() # Create a copy to avoid modifying the original
112
76
  query["company_id"] = company_id
113
- cursor = self.async_collection.find(filter=query)
114
- if limit:
115
- cursor = cursor.limit(limit)
116
- docs = await cursor.to_list(length=limit if limit > 0 else None)
117
- logger.debug(f"Found {len(docs)} documents")
77
+ docs = list(self.collection.find(filter=query).limit(limit))
78
+ logger.debug(f"Found {len(docs)} documents in collection {self.collection.name}")
118
79
  return [self.create_instance(doc) for doc in docs]
119
80
 
120
- async def delete(self, doc_id: str, deletion_type: DeletionType = DeletionType.LOGICAL) -> StrObjectId:
121
- """Delete operation"""
122
- logger.debug(f"Deleting document with id {doc_id} - deletion type: {deletion_type}")
123
- if deletion_type == DeletionType.LOGICAL:
124
- result = await self.async_collection.update_one(
125
- {"_id": ObjectId(doc_id)},
126
- {"$set": {"deleted_at": datetime.now(ZoneInfo("UTC")), "updated_at": datetime.now(ZoneInfo("UTC"))}}
127
- )
128
- if result.modified_count == 0:
129
- raise NotFoundError(f"No document found with id {doc_id}")
130
- return doc_id
131
- elif deletion_type == DeletionType.PHYSICAL:
132
- result = await self.async_collection.delete_one({"_id": ObjectId(doc_id)})
133
- if result.deleted_count == 0:
134
- raise NotFoundError(f"No document found with id {doc_id}")
135
- return doc_id
136
- else:
137
- raise ValueError(f"Invalid deletion type: {deletion_type}")
138
-
139
- # Additional methods - keeping these sync as they're less critical
140
81
  def get_preview_docs(self, projection = {}, all=True) -> List[P]:
141
82
  """We get the previews of all the documents in the collection for all companies"""
142
83
  if not self.preview_type:
@@ -157,31 +98,18 @@ class ChattyAssetCollectionInterface(Generic[T, P], ABC):
157
98
  docs = self.collection.find(query)
158
99
  return [self.create_instance(doc) for doc in docs]
159
100
 
160
- async def get_by_ids(self, ids: List[StrObjectId]) -> List[T]:
161
- """
162
- Get multiple assets by their IDs in a single query.
163
-
164
- Args:
165
- ids: List of asset IDs
166
-
167
- Returns:
168
- List of assets objects
169
- """
170
- if not ids:
171
- return []
172
-
173
- # Convert string IDs to ObjectIds
174
- object_ids = [ObjectId(id) for id in ids]
175
-
176
- # Query for all filter criteria with matching IDs
177
- query = {
178
- "_id": {"$in": object_ids},
179
- "deleted_at": None
180
- }
181
-
182
- # Use the sync collection directly (inherited from ChattyAssetCollectionInterface)
183
- docs = await self.async_collection.find(query).to_list(length=None)
184
-
185
- # Create FilterCriteria instances
186
- return [self.create_instance(doc) for doc in docs]
101
+ def delete(self, doc_id: str, deletion_type : DeletionType = DeletionType.LOGICAL) -> StrObjectId:
102
+ logger.debug(f"Deleting document with id {doc_id} - deletion type: {deletion_type}")
103
+ if deletion_type == DeletionType.LOGICAL:
104
+ result = self.collection.update_one({"_id": ObjectId(doc_id)}, {"$set": {"deleted_at": datetime.now(ZoneInfo("UTC")), "updated_at": datetime.now(ZoneInfo("UTC"))}})
105
+ if result.modified_count == 0:
106
+ raise NotFoundError(f"No document found with id {doc_id}")
107
+ return doc_id
108
+ elif deletion_type == DeletionType.PHYSICAL:
109
+ result = self.collection.delete_one({"_id": ObjectId(doc_id)})
110
+ if result.deleted_count == 0:
111
+ raise NotFoundError(f"No document found with id {doc_id}")
112
+ return doc_id
113
+ else:
114
+ raise ValueError(f"Invalid deletion type: {deletion_type}")
187
115