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.
- letschatty/models/ai_microservices/__init__.py +4 -4
- letschatty/models/ai_microservices/expected_output.py +2 -29
- letschatty/models/ai_microservices/lambda_events.py +28 -155
- letschatty/models/ai_microservices/lambda_invokation_types.py +1 -4
- letschatty/models/ai_microservices/n8n_ai_agents_payload.py +1 -3
- letschatty/models/analytics/events/__init__.py +3 -3
- letschatty/models/analytics/events/chat_based_events/chat_client.py +19 -0
- letschatty/models/analytics/events/chat_based_events/chat_funnel.py +69 -13
- letschatty/models/analytics/events/company_based_events/asset_events.py +9 -2
- letschatty/models/analytics/events/event_type_to_classes.py +7 -3
- letschatty/models/analytics/events/event_types.py +11 -50
- letschatty/models/chat/chat.py +13 -2
- letschatty/models/chat/chat_with_assets.py +6 -1
- letschatty/models/chat/client.py +0 -2
- letschatty/models/chat/continuous_conversation.py +1 -1
- letschatty/models/company/CRM/funnel.py +365 -33
- letschatty/models/company/__init__.py +10 -1
- letschatty/models/company/assets/ai_agents_v2/ai_agents_decision_output.py +1 -1
- letschatty/models/company/assets/ai_agents_v2/chatty_ai_agent_in_chat.py +0 -4
- letschatty/models/company/assets/ai_agents_v2/chatty_ai_mode.py +2 -2
- letschatty/models/company/assets/ai_agents_v2/get_chat_with_prompt_response.py +0 -1
- letschatty/models/company/assets/ai_agents_v2/pre_qualify_config.py +1 -28
- letschatty/models/company/assets/automation.py +10 -19
- letschatty/models/company/assets/chat_assets.py +3 -2
- letschatty/models/company/assets/company_assets.py +2 -0
- letschatty/models/company/assets/sale.py +3 -3
- letschatty/models/company/empresa.py +4 -1
- letschatty/models/company/integrations/product_sync_status.py +28 -0
- letschatty/models/company/integrations/shopify/company_shopify_integration.py +62 -0
- letschatty/models/company/integrations/shopify/shopify_product_sync_status.py +18 -0
- letschatty/models/company/integrations/shopify/shopify_webhook_topics.py +40 -0
- letschatty/models/company/integrations/sync_status_enum.py +9 -0
- letschatty/models/company/integrations/tienda_nube/company_tienda_nube_integration.py +62 -0
- letschatty/models/company/integrations/tienda_nube/tienda_nube_product_sync_status.py +18 -0
- letschatty/models/company/integrations/tienda_nube/tienda_nube_webhook_topics.py +46 -0
- letschatty/models/data_base/collection_interface.py +29 -101
- letschatty/models/data_base/mongo_connection.py +9 -92
- letschatty/models/messages/chatty_messages/schema/chatty_content/content_document.py +4 -2
- letschatty/models/messages/chatty_messages/schema/chatty_content/content_media.py +4 -3
- letschatty/models/utils/custom_exceptions/custom_exceptions.py +1 -14
- letschatty/services/ai_agents/smart_follow_up_context_builder_v2.py +2 -5
- letschatty/services/chat/chat_service.py +47 -11
- letschatty/services/chatty_assets/__init__.py +0 -12
- letschatty/services/chatty_assets/asset_service.py +13 -190
- letschatty/services/chatty_assets/base_container.py +2 -3
- letschatty/services/chatty_assets/base_container_with_collection.py +26 -35
- letschatty/services/continuous_conversation_service/continuous_conversation_helper.py +0 -11
- letschatty/services/events/events_manager.py +1 -218
- letschatty/services/factories/analytics/events_factory.py +30 -66
- letschatty/services/factories/lambda_ai_orchestrartor/lambda_events_factory.py +8 -46
- letschatty/services/messages_helpers/get_caption_or_body_or_preview.py +4 -6
- letschatty/services/validators/analytics_validator.py +11 -0
- {letschatty-0.4.351.dist-info → letschatty-0.4.353.dist-info}/METADATA +1 -1
- {letschatty-0.4.351.dist-info → letschatty-0.4.353.dist-info}/RECORD +56 -83
- letschatty/models/analytics/events/chat_based_events/ai_agent_execution_event.py +0 -71
- letschatty/services/chatty_assets/assets_collections.py +0 -137
- letschatty/services/chatty_assets/collections/__init__.py +0 -38
- letschatty/services/chatty_assets/collections/ai_agent_collection.py +0 -19
- letschatty/services/chatty_assets/collections/ai_agent_in_chat_collection.py +0 -32
- letschatty/services/chatty_assets/collections/ai_component_collection.py +0 -21
- letschatty/services/chatty_assets/collections/chain_of_thought_collection.py +0 -30
- letschatty/services/chatty_assets/collections/chat_collection.py +0 -21
- letschatty/services/chatty_assets/collections/contact_point_collection.py +0 -21
- letschatty/services/chatty_assets/collections/fast_answer_collection.py +0 -21
- letschatty/services/chatty_assets/collections/filter_criteria_collection.py +0 -18
- letschatty/services/chatty_assets/collections/flow_collection.py +0 -20
- letschatty/services/chatty_assets/collections/product_collection.py +0 -20
- letschatty/services/chatty_assets/collections/sale_collection.py +0 -20
- letschatty/services/chatty_assets/collections/source_collection.py +0 -21
- letschatty/services/chatty_assets/collections/tag_collection.py +0 -19
- letschatty/services/chatty_assets/collections/topic_collection.py +0 -21
- letschatty/services/chatty_assets/collections/user_collection.py +0 -20
- letschatty/services/chatty_assets/example_usage.py +0 -44
- letschatty/services/chatty_assets/services/__init__.py +0 -37
- letschatty/services/chatty_assets/services/ai_agent_in_chat_service.py +0 -73
- letschatty/services/chatty_assets/services/ai_agent_service.py +0 -23
- letschatty/services/chatty_assets/services/chain_of_thought_service.py +0 -70
- letschatty/services/chatty_assets/services/chat_service.py +0 -25
- letschatty/services/chatty_assets/services/contact_point_service.py +0 -29
- letschatty/services/chatty_assets/services/fast_answer_service.py +0 -32
- letschatty/services/chatty_assets/services/filter_criteria_service.py +0 -30
- letschatty/services/chatty_assets/services/flow_service.py +0 -25
- letschatty/services/chatty_assets/services/product_service.py +0 -30
- letschatty/services/chatty_assets/services/sale_service.py +0 -25
- letschatty/services/chatty_assets/services/source_service.py +0 -28
- letschatty/services/chatty_assets/services/tag_service.py +0 -32
- letschatty/services/chatty_assets/services/topic_service.py +0 -31
- letschatty/services/chatty_assets/services/user_service.py +0 -32
- letschatty/services/events/__init__.py +0 -6
- letschatty/services/factories/analytics/ai_agent_event_factory.py +0 -161
- {letschatty-0.4.351.dist-info → letschatty-0.4.353.dist-info}/LICENSE +0 -0
- {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
|
|
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(
|
|
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,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,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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
99
|
-
"
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
108
|
-
"
|
|
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
|
-
|
|
114
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
|