fusesell 1.2.1__py3-none-any.whl → 1.2.3__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 fusesell might be problematic. Click here for more details.
- {fusesell-1.2.1.dist-info → fusesell-1.2.3.dist-info}/METADATA +1 -1
- {fusesell-1.2.1.dist-info → fusesell-1.2.3.dist-info}/RECORD +13 -9
- fusesell_local/__init__.py +1 -1
- fusesell_local/cli.py +39 -12
- fusesell_local/tests/conftest.py +11 -0
- fusesell_local/tests/test_data_manager_products.py +140 -0
- fusesell_local/tests/test_data_manager_sales_process.py +115 -0
- fusesell_local/tests/test_data_manager_teams.py +133 -0
- fusesell_local/utils/data_manager.py +324 -219
- {fusesell-1.2.1.dist-info → fusesell-1.2.3.dist-info}/WHEEL +0 -0
- {fusesell-1.2.1.dist-info → fusesell-1.2.3.dist-info}/entry_points.txt +0 -0
- {fusesell-1.2.1.dist-info → fusesell-1.2.3.dist-info}/licenses/LICENSE +0 -0
- {fusesell-1.2.1.dist-info → fusesell-1.2.3.dist-info}/top_level.txt +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
fusesell.py,sha256=t5PjkhWEJGINp4k517u0EX0ge7lzuHOUHHro-BE1mGk,596
|
|
2
|
-
fusesell-1.2.
|
|
3
|
-
fusesell_local/__init__.py,sha256=
|
|
2
|
+
fusesell-1.2.3.dist-info/licenses/LICENSE,sha256=GDz1ZoC4lB0kwjERpzqc_OdA_awYVso2aBnUH-ErW_w,1070
|
|
3
|
+
fusesell_local/__init__.py,sha256=fEvogPM4NPOiL4oLU9oK0HWECKyTvAGB4rSUr1XI-So,966
|
|
4
4
|
fusesell_local/api.py,sha256=AcPune5YJdgi7nsMeusCUqc49z5UiycsQb6n3yiV_No,10839
|
|
5
|
-
fusesell_local/cli.py,sha256=
|
|
5
|
+
fusesell_local/cli.py,sha256=MYnVxuEf5KTR4VcO3sc-VtP9NkWlSixJsYfOWST2Ds0,65859
|
|
6
6
|
fusesell_local/pipeline.py,sha256=KO5oAIHZ3L_uAZWOszauJyv0QWlsQMIDNGRuwQSxNmQ,39531
|
|
7
7
|
fusesell_local/config/__init__.py,sha256=0ErO7QiSDqKn-LHcjIRdLZzh5QaRTkRsIlwfgpkkDz8,209
|
|
8
8
|
fusesell_local/config/prompts.py,sha256=5O3Y2v3GCi9d9FEyR6Ekc1UXVq2TcZp3Rrspvx4bkac,10164
|
|
@@ -14,18 +14,22 @@ fusesell_local/stages/data_preparation.py,sha256=XWLg9b1w2NrMxLcrWDqB95mRmLQmVIM
|
|
|
14
14
|
fusesell_local/stages/follow_up.py,sha256=2CSen5SHJ5k6KMHXpqoRBb3n3IrcMRdDnuyTsOeuRTA,74625
|
|
15
15
|
fusesell_local/stages/initial_outreach.py,sha256=jox2caveSwI3xIfjn8FGYprkjTbW8YhDNvCzz9wNcBE,107503
|
|
16
16
|
fusesell_local/stages/lead_scoring.py,sha256=ir3l849eMGrGLf0OYUcmA1F3FwyYhAplS4niU3R2GRY,60658
|
|
17
|
+
fusesell_local/tests/conftest.py,sha256=TWUtlP6cNPVOYkTPz-j9BzS_KnXdPWy8D-ObPLHvXYs,366
|
|
17
18
|
fusesell_local/tests/test_api.py,sha256=763rUVb5pAuAQOovug6Ka0T9eGK8-WVOC_J08M7TETo,1827
|
|
18
19
|
fusesell_local/tests/test_cli.py,sha256=iNgU8nDlVrcQM5MpBUTIJ5q3oh2-jgX77hJeaqBxToM,1007
|
|
20
|
+
fusesell_local/tests/test_data_manager_products.py,sha256=g8EUSxTqdg18VifzhuOtDDywiMYzwOWFADny5Vntc28,4691
|
|
21
|
+
fusesell_local/tests/test_data_manager_sales_process.py,sha256=NbwxQ9oBKCZfrkRQYxzHHQ08F7epqPUsyeGz_vm3kf8,4447
|
|
22
|
+
fusesell_local/tests/test_data_manager_teams.py,sha256=kjk4V4r9ja4EVREIiQMxkuZd470SSwRHJAvpHln9KO4,4578
|
|
19
23
|
fusesell_local/utils/__init__.py,sha256=TVemlo0wpckhNUxP3a1Tky3ekswy8JdIHaXBlkKXKBQ,330
|
|
20
24
|
fusesell_local/utils/birthday_email_manager.py,sha256=NKLoUyzPedyhewZPma21SOoU8p9wPquehloer7TRA9U,20478
|
|
21
|
-
fusesell_local/utils/data_manager.py,sha256=
|
|
25
|
+
fusesell_local/utils/data_manager.py,sha256=w2Ed0HAdkTg23r_Fz_v8j3H_jcXbgD_Xk_lfeySi9bk,178447
|
|
22
26
|
fusesell_local/utils/event_scheduler.py,sha256=rjtWwtYQoJP0YwoN1-43t6K9GpLfqRq3c7Fv4papvbI,25725
|
|
23
27
|
fusesell_local/utils/llm_client.py,sha256=FVc25UlGt6hro7h5Iw7PHSXY3E3_67Xc-SUbHuMSRs0,10437
|
|
24
28
|
fusesell_local/utils/logger.py,sha256=sWlV8Tjyz_Z8J4zXKOnNalh8_iD6ytfrwPZpD-wcEOs,6259
|
|
25
29
|
fusesell_local/utils/timezone_detector.py,sha256=0cAE4c8ZXqCA8AvxRKm6PrFKmAmsbq3HOHR6w-mW3KQ,39997
|
|
26
30
|
fusesell_local/utils/validators.py,sha256=Z1VzeoxFsnuzlIA_ZaMWoy-0Cgyqupd47kIdljlMDbs,15438
|
|
27
|
-
fusesell-1.2.
|
|
28
|
-
fusesell-1.2.
|
|
29
|
-
fusesell-1.2.
|
|
30
|
-
fusesell-1.2.
|
|
31
|
-
fusesell-1.2.
|
|
31
|
+
fusesell-1.2.3.dist-info/METADATA,sha256=Izbft0CEbjY1O9NQebg5b6FxgMJpd8p24N8FDWE1e-o,34976
|
|
32
|
+
fusesell-1.2.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
33
|
+
fusesell-1.2.3.dist-info/entry_points.txt,sha256=Vqek7tbiX7iF4rQkCRBZvT5WrB0HUduqKTsI2ZjhsXo,53
|
|
34
|
+
fusesell-1.2.3.dist-info/top_level.txt,sha256=VP9y1K6DEq6gNq2UgLd7ChujxViF6OzeAVCK7IUBXPA,24
|
|
35
|
+
fusesell-1.2.3.dist-info/RECORD,,
|
fusesell_local/__init__.py
CHANGED
fusesell_local/cli.py
CHANGED
|
@@ -410,11 +410,32 @@ Examples:
|
|
|
410
410
|
update_parser.add_argument(
|
|
411
411
|
'--subcategory', help='New product subcategory')
|
|
412
412
|
|
|
413
|
-
# Product list
|
|
414
|
-
list_parser = product_subparsers.add_parser(
|
|
415
|
-
'list', help='List products')
|
|
416
|
-
list_parser.add_argument(
|
|
417
|
-
'--org-id', required=True, help='Organization ID')
|
|
413
|
+
# Product list
|
|
414
|
+
list_parser = product_subparsers.add_parser(
|
|
415
|
+
'list', help='List products')
|
|
416
|
+
list_parser.add_argument(
|
|
417
|
+
'--org-id', required=True, help='Organization ID')
|
|
418
|
+
list_parser.add_argument(
|
|
419
|
+
'--status',
|
|
420
|
+
choices=['active', 'inactive', 'all'],
|
|
421
|
+
default='active',
|
|
422
|
+
help='Filter products by status (default: active)',
|
|
423
|
+
)
|
|
424
|
+
list_parser.add_argument(
|
|
425
|
+
'--search-term',
|
|
426
|
+
help='Keyword to match against product name or descriptions',
|
|
427
|
+
)
|
|
428
|
+
list_parser.add_argument(
|
|
429
|
+
'--limit',
|
|
430
|
+
type=int,
|
|
431
|
+
help='Maximum number of products to return',
|
|
432
|
+
)
|
|
433
|
+
list_parser.add_argument(
|
|
434
|
+
'--sort',
|
|
435
|
+
choices=['name', 'created_at', 'updated_at'],
|
|
436
|
+
default='name',
|
|
437
|
+
help='Sort order for results (default: name)',
|
|
438
|
+
)
|
|
418
439
|
|
|
419
440
|
def _add_settings_arguments(self, parser: argparse.ArgumentParser) -> None:
|
|
420
441
|
"""Add settings management arguments."""
|
|
@@ -1029,13 +1050,19 @@ Examples:
|
|
|
1029
1050
|
f"Product not found: {args.product_id}", file=sys.stderr)
|
|
1030
1051
|
return 1
|
|
1031
1052
|
|
|
1032
|
-
elif action == 'list':
|
|
1033
|
-
products = data_manager.
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1053
|
+
elif action == 'list':
|
|
1054
|
+
products = data_manager.search_products(
|
|
1055
|
+
org_id=args.org_id,
|
|
1056
|
+
status=getattr(args, 'status', 'active'),
|
|
1057
|
+
search_term=getattr(args, 'search_term', None),
|
|
1058
|
+
limit=getattr(args, 'limit', None),
|
|
1059
|
+
sort=getattr(args, 'sort', 'name'),
|
|
1060
|
+
)
|
|
1061
|
+
if products:
|
|
1062
|
+
print(f"Products for organization {args.org_id}:")
|
|
1063
|
+
for product in products:
|
|
1064
|
+
print(
|
|
1065
|
+
f" {product['product_id']}: {product['product_name']} - {product.get('short_description', 'No description')}")
|
|
1039
1066
|
else:
|
|
1040
1067
|
print(f"No products found for organization {args.org_id}")
|
|
1041
1068
|
return 0
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from fusesell_local.utils.data_manager import LocalDataManager
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@pytest.fixture
|
|
7
|
+
def data_manager(tmp_path):
|
|
8
|
+
"""Provide an isolated LocalDataManager instance backed by a temporary directory."""
|
|
9
|
+
LocalDataManager._initialized_databases.clear()
|
|
10
|
+
LocalDataManager._initialization_lock = False
|
|
11
|
+
return LocalDataManager(data_dir=str(tmp_path))
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _sample_product_payload(product_id: str = "prod-001", **overrides):
|
|
5
|
+
payload = {
|
|
6
|
+
"product_id": product_id,
|
|
7
|
+
"org_id": "org-123",
|
|
8
|
+
"org_name": "FuseSell Org",
|
|
9
|
+
"project_code": "proj-001",
|
|
10
|
+
"productName": "FuseSell AI Suite",
|
|
11
|
+
"shortDescription": "AI-powered sales automation",
|
|
12
|
+
"longDescription": "Comprehensive automation platform for revenue teams.",
|
|
13
|
+
"category": "Software",
|
|
14
|
+
"subcategory": "Sales Automation",
|
|
15
|
+
"targetUsers": ["Sales reps"],
|
|
16
|
+
"keyFeatures": ["Automation", "CRM sync"],
|
|
17
|
+
"uniqueSellingPoints": ["Privacy-first"],
|
|
18
|
+
"pricing": {"monthly": 199},
|
|
19
|
+
"pricingRules": {"currency": "USD"},
|
|
20
|
+
"productWebsite": "https://fusesell.test/ai-suite",
|
|
21
|
+
}
|
|
22
|
+
payload.update(overrides)
|
|
23
|
+
return payload
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_save_and_get_product_roundtrip(data_manager):
|
|
27
|
+
payload = _sample_product_payload()
|
|
28
|
+
product_id = data_manager.save_product(payload)
|
|
29
|
+
|
|
30
|
+
assert product_id == payload["product_id"]
|
|
31
|
+
|
|
32
|
+
stored = data_manager.get_product(product_id)
|
|
33
|
+
assert stored is not None
|
|
34
|
+
assert stored["product_name"] == payload["productName"]
|
|
35
|
+
assert stored["org_id"] == payload["org_id"]
|
|
36
|
+
assert stored["key_features"] == payload["keyFeatures"]
|
|
37
|
+
assert stored["pricing"] == payload["pricing"]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_update_product_merges_changes(data_manager):
|
|
41
|
+
payload = _sample_product_payload()
|
|
42
|
+
product_id = data_manager.save_product(payload)
|
|
43
|
+
|
|
44
|
+
updated = {
|
|
45
|
+
"shortDescription": "Updated messaging",
|
|
46
|
+
"pricing": {"monthly": 249, "yearly": 2490},
|
|
47
|
+
"keyFeatures": ["Automation", "Analytics"],
|
|
48
|
+
}
|
|
49
|
+
assert data_manager.update_product(product_id, updated) is True
|
|
50
|
+
|
|
51
|
+
stored = data_manager.get_product(product_id)
|
|
52
|
+
assert stored["short_description"] == updated["shortDescription"]
|
|
53
|
+
assert stored["pricing"]["monthly"] == 249
|
|
54
|
+
assert stored["key_features"] == updated["keyFeatures"]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_get_products_by_org_returns_active_products_only(data_manager):
|
|
58
|
+
active = _sample_product_payload("prod-active")
|
|
59
|
+
inactive = _sample_product_payload("prod-inactive")
|
|
60
|
+
|
|
61
|
+
data_manager.save_product(active)
|
|
62
|
+
data_manager.save_product(inactive)
|
|
63
|
+
|
|
64
|
+
# Mark one product inactive directly to exercise status filtering
|
|
65
|
+
with sqlite3.connect(data_manager.db_path) as conn:
|
|
66
|
+
conn.execute(
|
|
67
|
+
"UPDATE products SET status = 'inactive' WHERE product_id = ?",
|
|
68
|
+
(inactive["product_id"],),
|
|
69
|
+
)
|
|
70
|
+
conn.commit()
|
|
71
|
+
|
|
72
|
+
results = data_manager.get_products_by_org(active["org_id"])
|
|
73
|
+
assert len(results) == 1
|
|
74
|
+
assert results[0]["product_id"] == active["product_id"]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_search_products_filters_by_keyword(data_manager):
|
|
78
|
+
alpha = _sample_product_payload(
|
|
79
|
+
"prod-alpha",
|
|
80
|
+
productName="Alpha CRM",
|
|
81
|
+
shortDescription="CRM automation platform",
|
|
82
|
+
keywords=["CRM", "pipeline"],
|
|
83
|
+
)
|
|
84
|
+
beta = _sample_product_payload(
|
|
85
|
+
"prod-beta",
|
|
86
|
+
productName="Beta Ops",
|
|
87
|
+
shortDescription="Operations toolkit",
|
|
88
|
+
keywords=["ops"],
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
data_manager.save_product(alpha)
|
|
92
|
+
data_manager.save_product(beta)
|
|
93
|
+
|
|
94
|
+
results = data_manager.search_products(
|
|
95
|
+
org_id="org-123",
|
|
96
|
+
search_term="crm",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
assert len(results) == 1
|
|
100
|
+
assert results[0]["product_id"] == "prod-alpha"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_search_products_limit_and_sort(data_manager):
|
|
104
|
+
first = _sample_product_payload("prod-c", productName="Charlie Suite")
|
|
105
|
+
second = _sample_product_payload("prod-a", productName="Alpha Suite")
|
|
106
|
+
third = _sample_product_payload("prod-b", productName="Bravo Suite")
|
|
107
|
+
|
|
108
|
+
data_manager.save_product(first)
|
|
109
|
+
data_manager.save_product(second)
|
|
110
|
+
data_manager.save_product(third)
|
|
111
|
+
|
|
112
|
+
# Update timestamps to control order
|
|
113
|
+
with sqlite3.connect(data_manager.db_path) as conn:
|
|
114
|
+
conn.execute(
|
|
115
|
+
"UPDATE products SET updated_at = ? WHERE product_id = ?",
|
|
116
|
+
("2024-01-01 10:00:00", "prod-a"),
|
|
117
|
+
)
|
|
118
|
+
conn.execute(
|
|
119
|
+
"UPDATE products SET updated_at = ? WHERE product_id = ?",
|
|
120
|
+
("2024-01-02 10:00:00", "prod-b"),
|
|
121
|
+
)
|
|
122
|
+
conn.execute(
|
|
123
|
+
"UPDATE products SET updated_at = ? WHERE product_id = ?",
|
|
124
|
+
("2024-01-03 10:00:00", "prod-c"),
|
|
125
|
+
)
|
|
126
|
+
conn.commit()
|
|
127
|
+
|
|
128
|
+
by_name = data_manager.search_products(
|
|
129
|
+
org_id="org-123",
|
|
130
|
+
sort="name",
|
|
131
|
+
limit=2,
|
|
132
|
+
)
|
|
133
|
+
assert [p["product_id"] for p in by_name] == ["prod-a", "prod-b"]
|
|
134
|
+
|
|
135
|
+
by_updated = data_manager.search_products(
|
|
136
|
+
org_id="org-123",
|
|
137
|
+
sort="updated_at",
|
|
138
|
+
limit=2,
|
|
139
|
+
)
|
|
140
|
+
assert [p["product_id"] for p in by_updated] == ["prod-c", "prod-b"]
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
def _create_task(data_manager, task_id: str, customer: str = "Acme Corp", status: str = "running"):
|
|
2
|
+
request_body = {"customer_info": customer, "org_name": "FuseSell Org"}
|
|
3
|
+
data_manager.create_task(
|
|
4
|
+
task_id=task_id,
|
|
5
|
+
plan_id="plan-456",
|
|
6
|
+
org_id="org-123",
|
|
7
|
+
request_body=request_body,
|
|
8
|
+
status=status,
|
|
9
|
+
)
|
|
10
|
+
return request_body
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_create_task_and_get_task_by_id(data_manager):
|
|
14
|
+
task_id = "task-001"
|
|
15
|
+
request_body = _create_task(data_manager, task_id)
|
|
16
|
+
|
|
17
|
+
record = data_manager.get_task_by_id(task_id)
|
|
18
|
+
assert record is not None
|
|
19
|
+
assert record["task_id"] == task_id
|
|
20
|
+
assert record["status"] == "running"
|
|
21
|
+
assert record["request_body"]["customer_info"] == request_body["customer_info"]
|
|
22
|
+
assert record["messages"] == []
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_update_task_status_tracks_runtime(data_manager):
|
|
26
|
+
task_id = "task-002"
|
|
27
|
+
_create_task(data_manager, task_id)
|
|
28
|
+
|
|
29
|
+
data_manager.update_task_status(task_id, "completed", runtime_index=4)
|
|
30
|
+
|
|
31
|
+
record = data_manager.get_task_by_id(task_id)
|
|
32
|
+
assert record["status"] == "completed"
|
|
33
|
+
assert record["current_runtime_index"] == 4
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_create_operation_and_update_status(data_manager):
|
|
37
|
+
task_id = "task-003"
|
|
38
|
+
_create_task(data_manager, task_id)
|
|
39
|
+
|
|
40
|
+
operation_id = data_manager.create_operation(
|
|
41
|
+
task_id, "gs_161_data_acquisition", runtime_index=0, chain_index=0, input_data={"step": 1}
|
|
42
|
+
)
|
|
43
|
+
operation = data_manager.get_operation(operation_id)
|
|
44
|
+
assert operation is not None
|
|
45
|
+
assert operation["execution_status"] == "running"
|
|
46
|
+
assert operation["input_data"]["step"] == 1
|
|
47
|
+
|
|
48
|
+
data_manager.update_operation_status(operation_id, "done", {"result": "ok"})
|
|
49
|
+
updated = data_manager.get_operation(operation_id)
|
|
50
|
+
assert updated["execution_status"] == "done"
|
|
51
|
+
assert updated["output_data"]["result"] == "ok"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_get_task_with_operations_returns_summary(data_manager):
|
|
55
|
+
task_id = "task-ops"
|
|
56
|
+
_create_task(data_manager, task_id)
|
|
57
|
+
|
|
58
|
+
op_a = data_manager.create_operation(
|
|
59
|
+
task_id, "gs_161_data_acquisition", runtime_index=0, chain_index=0, input_data={"stage": "acquire"}
|
|
60
|
+
)
|
|
61
|
+
op_b = data_manager.create_operation(
|
|
62
|
+
task_id, "gs_161_data_preparation", runtime_index=0, chain_index=1, input_data={"stage": "prepare"}
|
|
63
|
+
)
|
|
64
|
+
data_manager.update_operation_status(op_a, "done", {"status": "ok"})
|
|
65
|
+
data_manager.update_operation_status(op_b, "running")
|
|
66
|
+
|
|
67
|
+
record = data_manager.get_task_with_operations(task_id)
|
|
68
|
+
assert record is not None
|
|
69
|
+
assert record["task_id"] == task_id
|
|
70
|
+
assert len(record["operations"]) == 2
|
|
71
|
+
assert record["summary"]["completed_operations"] == 1
|
|
72
|
+
assert record["summary"]["running_operations"] == 1
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_find_sales_processes_by_customer(data_manager):
|
|
76
|
+
task_acme = "task-acme"
|
|
77
|
+
task_beta = "task-beta"
|
|
78
|
+
_create_task(data_manager, task_acme, customer="Acme Corp")
|
|
79
|
+
_create_task(data_manager, task_beta, customer="Beta LLC")
|
|
80
|
+
|
|
81
|
+
results = data_manager.find_sales_processes_by_customer("Acme")
|
|
82
|
+
assert len(results) == 1
|
|
83
|
+
assert results[0]["task_id"] == task_acme
|
|
84
|
+
assert results[0]["request_body"]["customer_info"] == "Acme Corp"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_list_tasks_filters_by_status(data_manager):
|
|
88
|
+
running_id = "task-running"
|
|
89
|
+
completed_id = "task-completed"
|
|
90
|
+
_create_task(data_manager, running_id, status="running")
|
|
91
|
+
_create_task(data_manager, completed_id, status="running")
|
|
92
|
+
data_manager.update_task_status(completed_id, "completed")
|
|
93
|
+
|
|
94
|
+
running_tasks = data_manager.list_tasks(status="running")
|
|
95
|
+
completed_tasks = data_manager.list_tasks(status="completed")
|
|
96
|
+
|
|
97
|
+
assert {task["task_id"] for task in running_tasks} == {running_id}
|
|
98
|
+
assert {task["task_id"] for task in completed_tasks} == {completed_id}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_get_execution_timeline_orders_by_indices(data_manager):
|
|
102
|
+
task_id = "task-timeline"
|
|
103
|
+
_create_task(data_manager, task_id)
|
|
104
|
+
|
|
105
|
+
op_first = data_manager.create_operation(
|
|
106
|
+
task_id, "gs_161_data_acquisition", runtime_index=0, chain_index=0, input_data={"order": 1}
|
|
107
|
+
)
|
|
108
|
+
op_second = data_manager.create_operation(
|
|
109
|
+
task_id, "gs_161_data_preparation", runtime_index=0, chain_index=1, input_data={"order": 2}
|
|
110
|
+
)
|
|
111
|
+
data_manager.update_operation_status(op_first, "done")
|
|
112
|
+
data_manager.update_operation_status(op_second, "done")
|
|
113
|
+
|
|
114
|
+
timeline = data_manager.get_execution_timeline(task_id)
|
|
115
|
+
assert [entry["input_data"]["order"] for entry in timeline] == [1, 2]
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
def _team_payload(team_id: str = "team-001", **overrides):
|
|
2
|
+
payload = {
|
|
3
|
+
"team_id": team_id,
|
|
4
|
+
"org_id": "org-123",
|
|
5
|
+
"org_name": "FuseSell Org",
|
|
6
|
+
"plan_id": "plan-456",
|
|
7
|
+
"plan_name": "FuseSell AI",
|
|
8
|
+
"project_code": "proj-001",
|
|
9
|
+
"name": "Outbound Squad",
|
|
10
|
+
"description": "Primary outreach team",
|
|
11
|
+
"avatar": "https://fusesell.test/avatar.png",
|
|
12
|
+
}
|
|
13
|
+
payload.update(overrides)
|
|
14
|
+
return payload
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_save_and_get_team_roundtrip(data_manager):
|
|
18
|
+
payload = _team_payload()
|
|
19
|
+
data_manager.save_team(**payload)
|
|
20
|
+
|
|
21
|
+
stored = data_manager.get_team(payload["team_id"])
|
|
22
|
+
assert stored is not None
|
|
23
|
+
assert stored["team_id"] == payload["team_id"]
|
|
24
|
+
assert stored["name"] == payload["name"]
|
|
25
|
+
assert stored["plan_id"] == payload["plan_id"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_update_team_modifies_selected_fields(data_manager):
|
|
29
|
+
payload = _team_payload()
|
|
30
|
+
data_manager.save_team(**payload)
|
|
31
|
+
|
|
32
|
+
updated_name = "Expansion Squad"
|
|
33
|
+
updated_plan = "FuseSell AI Enterprise"
|
|
34
|
+
assert data_manager.update_team(
|
|
35
|
+
payload["team_id"], name=updated_name, plan_name=updated_plan
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
stored = data_manager.get_team(payload["team_id"])
|
|
39
|
+
assert stored["name"] == updated_name
|
|
40
|
+
assert stored["plan_name"] == updated_plan
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_list_teams_returns_all_for_org(data_manager):
|
|
44
|
+
first = _team_payload("team-001")
|
|
45
|
+
second = _team_payload("team-002", name="Inbound Squad")
|
|
46
|
+
data_manager.save_team(**first)
|
|
47
|
+
data_manager.save_team(**second)
|
|
48
|
+
|
|
49
|
+
teams = data_manager.list_teams(first["org_id"])
|
|
50
|
+
identifiers = {team["team_id"] for team in teams}
|
|
51
|
+
assert identifiers == {first["team_id"], second["team_id"]}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_save_and_get_team_settings_roundtrip(data_manager):
|
|
55
|
+
payload = _team_payload()
|
|
56
|
+
data_manager.save_team(**payload)
|
|
57
|
+
|
|
58
|
+
data_manager.save_team_settings(
|
|
59
|
+
team_id=payload["team_id"],
|
|
60
|
+
org_id=payload["org_id"],
|
|
61
|
+
plan_id=payload["plan_id"],
|
|
62
|
+
team_name=payload["name"],
|
|
63
|
+
gs_team_organization={"sales_regions": ["NA", "EU"]},
|
|
64
|
+
gs_team_rep=[{"name": "Alex"}],
|
|
65
|
+
gs_team_product=[{"product_id": "prod-001"}],
|
|
66
|
+
gs_team_schedule_time={"timezone": "UTC"},
|
|
67
|
+
gs_team_initial_outreach={"enabled": True},
|
|
68
|
+
gs_team_follow_up={"sequence": 2},
|
|
69
|
+
gs_team_auto_interaction={"mode": "assist"},
|
|
70
|
+
gs_team_followup_schedule_time={"window": "business_hours"},
|
|
71
|
+
gs_team_birthday_email={"enabled": False},
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
settings = data_manager.get_team_settings(payload["team_id"])
|
|
75
|
+
assert settings is not None
|
|
76
|
+
assert settings["org_id"] == payload["org_id"]
|
|
77
|
+
assert settings["gs_team_organization"]["sales_regions"] == ["NA", "EU"]
|
|
78
|
+
assert settings["gs_team_rep"][0]["name"] == "Alex"
|
|
79
|
+
assert settings["gs_team_product"][0]["product_id"] == "prod-001"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_save_team_settings_updates_existing_record(data_manager):
|
|
83
|
+
payload = _team_payload()
|
|
84
|
+
data_manager.save_team(**payload)
|
|
85
|
+
|
|
86
|
+
data_manager.save_team_settings(
|
|
87
|
+
team_id=payload["team_id"],
|
|
88
|
+
org_id=payload["org_id"],
|
|
89
|
+
plan_id=payload["plan_id"],
|
|
90
|
+
team_name=payload["name"],
|
|
91
|
+
gs_team_schedule_time={"timezone": "UTC"},
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
data_manager.save_team_settings(
|
|
95
|
+
team_id=payload["team_id"],
|
|
96
|
+
org_id=payload["org_id"],
|
|
97
|
+
plan_id=payload["plan_id"],
|
|
98
|
+
team_name=payload["name"],
|
|
99
|
+
gs_team_schedule_time={"timezone": "America/New_York"},
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
settings = data_manager.get_team_settings(payload["team_id"])
|
|
103
|
+
assert settings["gs_team_schedule_time"]["timezone"] == "America/New_York"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_get_products_by_team_uses_team_settings(data_manager):
|
|
107
|
+
team = _team_payload()
|
|
108
|
+
data_manager.save_team(**team)
|
|
109
|
+
|
|
110
|
+
product_payload = {
|
|
111
|
+
"product_id": "prod-001",
|
|
112
|
+
"org_id": team["org_id"],
|
|
113
|
+
"org_name": team["org_name"],
|
|
114
|
+
"project_code": team["project_code"],
|
|
115
|
+
"productName": "FuseSell Starter",
|
|
116
|
+
"shortDescription": "Entry-level automation",
|
|
117
|
+
"longDescription": "Starter bundle.",
|
|
118
|
+
"category": "Software",
|
|
119
|
+
"targetUsers": ["SMB"],
|
|
120
|
+
}
|
|
121
|
+
data_manager.save_product(product_payload)
|
|
122
|
+
|
|
123
|
+
data_manager.save_team_settings(
|
|
124
|
+
team_id=team["team_id"],
|
|
125
|
+
org_id=team["org_id"],
|
|
126
|
+
plan_id=team["plan_id"],
|
|
127
|
+
team_name=team["name"],
|
|
128
|
+
gs_team_product=[{"product_id": product_payload["product_id"]}],
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
products = data_manager.get_products_by_team(team["team_id"])
|
|
132
|
+
assert len(products) == 1
|
|
133
|
+
assert products[0]["product_id"] == product_payload["product_id"]
|
|
@@ -13,15 +13,40 @@ import logging
|
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
class LocalDataManager:
|
|
17
|
-
"""
|
|
18
|
-
Manages local data storage using SQLite database and JSON files.
|
|
19
|
-
Provides interface for storing execution results, customer data, and configurations.
|
|
20
|
-
"""
|
|
21
|
-
|
|
22
|
-
# Class-level tracking to prevent multiple initializations
|
|
23
|
-
_initialized_databases = set()
|
|
24
|
-
_initialization_lock = False
|
|
16
|
+
class LocalDataManager:
|
|
17
|
+
"""
|
|
18
|
+
Manages local data storage using SQLite database and JSON files.
|
|
19
|
+
Provides interface for storing execution results, customer data, and configurations.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
# Class-level tracking to prevent multiple initializations
|
|
23
|
+
_initialized_databases = set()
|
|
24
|
+
_initialization_lock = False
|
|
25
|
+
_product_json_fields = [
|
|
26
|
+
'target_users',
|
|
27
|
+
'key_features',
|
|
28
|
+
'unique_selling_points',
|
|
29
|
+
'pain_points_solved',
|
|
30
|
+
'competitive_advantages',
|
|
31
|
+
'pricing',
|
|
32
|
+
'pricing_rules',
|
|
33
|
+
'sales_metrics',
|
|
34
|
+
'customer_feedback',
|
|
35
|
+
'keywords',
|
|
36
|
+
'related_products',
|
|
37
|
+
'seasonal_demand',
|
|
38
|
+
'market_insights',
|
|
39
|
+
'case_studies',
|
|
40
|
+
'testimonials',
|
|
41
|
+
'success_metrics',
|
|
42
|
+
'product_variants',
|
|
43
|
+
'technical_specifications',
|
|
44
|
+
'compatibility',
|
|
45
|
+
'support_info',
|
|
46
|
+
'regulatory_compliance',
|
|
47
|
+
'localization',
|
|
48
|
+
'shipping_info'
|
|
49
|
+
]
|
|
25
50
|
|
|
26
51
|
def __init__(self, data_dir: str = "./fusesell_data"):
|
|
27
52
|
"""
|
|
@@ -1342,50 +1367,72 @@ class LocalDataManager:
|
|
|
1342
1367
|
self.logger.error(f"Failed to save team settings: {str(e)}")
|
|
1343
1368
|
raise
|
|
1344
1369
|
|
|
1345
|
-
def get_team_settings(self, team_id: str) -> Optional[Dict[str, Any]]:
|
|
1346
|
-
"""
|
|
1347
|
-
Get team settings by team ID.
|
|
1348
|
-
|
|
1349
|
-
Args:
|
|
1350
|
-
team_id: Team identifier
|
|
1351
|
-
|
|
1352
|
-
Returns:
|
|
1353
|
-
Team settings dictionary or None if not found
|
|
1354
|
-
"""
|
|
1355
|
-
try:
|
|
1356
|
-
with sqlite3.connect(self.db_path) as conn:
|
|
1357
|
-
conn.row_factory = sqlite3.Row
|
|
1358
|
-
cursor = conn.cursor()
|
|
1359
|
-
cursor.execute(
|
|
1360
|
-
"SELECT * FROM team_settings WHERE team_id = ?", (team_id,))
|
|
1361
|
-
row = cursor.fetchone()
|
|
1362
|
-
|
|
1363
|
-
if row:
|
|
1364
|
-
result = dict(row)
|
|
1365
|
-
# Parse JSON fields
|
|
1366
|
-
json_fields = [
|
|
1367
|
-
'gs_team_organization', 'gs_team_rep', 'gs_team_product',
|
|
1368
|
-
'gs_team_schedule_time', 'gs_team_initial_outreach', 'gs_team_follow_up',
|
|
1369
|
-
'gs_team_auto_interaction', 'gs_team_followup_schedule_time', 'gs_team_birthday_email'
|
|
1370
|
-
]
|
|
1371
|
-
|
|
1372
|
-
for field in json_fields:
|
|
1373
|
-
if result[field]:
|
|
1374
|
-
try:
|
|
1375
|
-
result[field] = json.loads(result[field])
|
|
1376
|
-
except json.JSONDecodeError:
|
|
1377
|
-
result[field] = None
|
|
1378
|
-
|
|
1379
|
-
return result
|
|
1380
|
-
return None
|
|
1381
|
-
|
|
1382
|
-
except Exception as e:
|
|
1383
|
-
self.logger.error(f"Failed to get team settings: {str(e)}")
|
|
1384
|
-
raise
|
|
1385
|
-
|
|
1386
|
-
def
|
|
1387
|
-
"""
|
|
1388
|
-
|
|
1370
|
+
def get_team_settings(self, team_id: str) -> Optional[Dict[str, Any]]:
|
|
1371
|
+
"""
|
|
1372
|
+
Get team settings by team ID.
|
|
1373
|
+
|
|
1374
|
+
Args:
|
|
1375
|
+
team_id: Team identifier
|
|
1376
|
+
|
|
1377
|
+
Returns:
|
|
1378
|
+
Team settings dictionary or None if not found
|
|
1379
|
+
"""
|
|
1380
|
+
try:
|
|
1381
|
+
with sqlite3.connect(self.db_path) as conn:
|
|
1382
|
+
conn.row_factory = sqlite3.Row
|
|
1383
|
+
cursor = conn.cursor()
|
|
1384
|
+
cursor.execute(
|
|
1385
|
+
"SELECT * FROM team_settings WHERE team_id = ?", (team_id,))
|
|
1386
|
+
row = cursor.fetchone()
|
|
1387
|
+
|
|
1388
|
+
if row:
|
|
1389
|
+
result = dict(row)
|
|
1390
|
+
# Parse JSON fields
|
|
1391
|
+
json_fields = [
|
|
1392
|
+
'gs_team_organization', 'gs_team_rep', 'gs_team_product',
|
|
1393
|
+
'gs_team_schedule_time', 'gs_team_initial_outreach', 'gs_team_follow_up',
|
|
1394
|
+
'gs_team_auto_interaction', 'gs_team_followup_schedule_time', 'gs_team_birthday_email'
|
|
1395
|
+
]
|
|
1396
|
+
|
|
1397
|
+
for field in json_fields:
|
|
1398
|
+
if result[field]:
|
|
1399
|
+
try:
|
|
1400
|
+
result[field] = json.loads(result[field])
|
|
1401
|
+
except json.JSONDecodeError:
|
|
1402
|
+
result[field] = None
|
|
1403
|
+
|
|
1404
|
+
return result
|
|
1405
|
+
return None
|
|
1406
|
+
|
|
1407
|
+
except Exception as e:
|
|
1408
|
+
self.logger.error(f"Failed to get team settings: {str(e)}")
|
|
1409
|
+
raise
|
|
1410
|
+
|
|
1411
|
+
def _deserialize_product_row(self, row: sqlite3.Row) -> Dict[str, Any]:
|
|
1412
|
+
"""
|
|
1413
|
+
Convert a product row into a dictionary with JSON fields parsed.
|
|
1414
|
+
|
|
1415
|
+
Args:
|
|
1416
|
+
row: SQLite row containing product data
|
|
1417
|
+
|
|
1418
|
+
Returns:
|
|
1419
|
+
Dictionary representation of the row with JSON fields decoded
|
|
1420
|
+
"""
|
|
1421
|
+
product = dict(row)
|
|
1422
|
+
|
|
1423
|
+
for field in self._product_json_fields:
|
|
1424
|
+
value = product.get(field)
|
|
1425
|
+
if value:
|
|
1426
|
+
try:
|
|
1427
|
+
product[field] = json.loads(value)
|
|
1428
|
+
except (json.JSONDecodeError, TypeError):
|
|
1429
|
+
product[field] = None
|
|
1430
|
+
|
|
1431
|
+
return product
|
|
1432
|
+
|
|
1433
|
+
def save_product(self, product_data: Dict[str, Any]) -> str:
|
|
1434
|
+
"""
|
|
1435
|
+
Save or update product information.
|
|
1389
1436
|
|
|
1390
1437
|
Args:
|
|
1391
1438
|
product_data: Product information dictionary
|
|
@@ -1569,50 +1616,119 @@ class LocalDataManager:
|
|
|
1569
1616
|
self.logger.error(f"Failed to save product: {str(e)}")
|
|
1570
1617
|
raise
|
|
1571
1618
|
|
|
1572
|
-
def
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1619
|
+
def search_products(
|
|
1620
|
+
self,
|
|
1621
|
+
org_id: str,
|
|
1622
|
+
status: Optional[str] = "active",
|
|
1623
|
+
search_term: Optional[str] = None,
|
|
1624
|
+
limit: Optional[int] = None,
|
|
1625
|
+
sort: Optional[str] = "name"
|
|
1626
|
+
) -> List[Dict[str, Any]]:
|
|
1627
|
+
"""
|
|
1628
|
+
Search products for an organization with optional filters.
|
|
1629
|
+
|
|
1630
|
+
Args:
|
|
1631
|
+
org_id: Organization identifier
|
|
1632
|
+
status: Product status filter ("active", "inactive", or "all")
|
|
1633
|
+
search_term: Keyword to match against name, descriptions, or keywords
|
|
1634
|
+
limit: Maximum number of products to return
|
|
1635
|
+
sort: Sort order ("name", "created_at", "updated_at")
|
|
1636
|
+
|
|
1637
|
+
Returns:
|
|
1638
|
+
List of product dictionaries
|
|
1639
|
+
"""
|
|
1640
|
+
try:
|
|
1641
|
+
def _is_placeholder(value: Any) -> bool:
|
|
1642
|
+
return isinstance(value, str) and value.strip().startswith("{{") and value.strip().endswith("}}")
|
|
1643
|
+
|
|
1644
|
+
# Normalize status
|
|
1645
|
+
normalized_status: Optional[str] = status
|
|
1646
|
+
if _is_placeholder(normalized_status):
|
|
1647
|
+
normalized_status = None
|
|
1648
|
+
if isinstance(normalized_status, str):
|
|
1649
|
+
normalized_status = normalized_status.strip().lower()
|
|
1650
|
+
if normalized_status not in {'active', 'inactive', 'all'}:
|
|
1651
|
+
normalized_status = 'active'
|
|
1652
|
+
|
|
1653
|
+
# Normalize sort
|
|
1654
|
+
normalized_sort: Optional[str] = sort
|
|
1655
|
+
if _is_placeholder(normalized_sort):
|
|
1656
|
+
normalized_sort = None
|
|
1657
|
+
if isinstance(normalized_sort, str):
|
|
1658
|
+
normalized_sort = normalized_sort.strip().lower()
|
|
1659
|
+
sort_map = {
|
|
1660
|
+
'name': ("product_name COLLATE NOCASE", "ASC"),
|
|
1661
|
+
'created_at': ("datetime(created_at)", "DESC"),
|
|
1662
|
+
'updated_at': ("datetime(updated_at)", "DESC"),
|
|
1663
|
+
}
|
|
1664
|
+
order_by, direction = sort_map.get(normalized_sort, sort_map['name'])
|
|
1665
|
+
|
|
1666
|
+
# Normalize search term
|
|
1667
|
+
normalized_search: Optional[str] = None
|
|
1668
|
+
if not _is_placeholder(search_term) and search_term is not None:
|
|
1669
|
+
normalized_search = str(search_term).strip()
|
|
1670
|
+
if normalized_search == "":
|
|
1671
|
+
normalized_search = None
|
|
1672
|
+
|
|
1673
|
+
# Normalize limit
|
|
1674
|
+
normalized_limit: Optional[int] = None
|
|
1675
|
+
if not _is_placeholder(limit) and limit is not None:
|
|
1676
|
+
try:
|
|
1677
|
+
normalized_limit = int(limit)
|
|
1678
|
+
if normalized_limit <= 0:
|
|
1679
|
+
normalized_limit = None
|
|
1680
|
+
except (TypeError, ValueError):
|
|
1681
|
+
normalized_limit = None
|
|
1682
|
+
|
|
1683
|
+
where_clauses = ["org_id = ?"]
|
|
1684
|
+
params: List[Any] = [org_id]
|
|
1685
|
+
|
|
1686
|
+
if normalized_status != 'all':
|
|
1687
|
+
where_clauses.append("status = ?")
|
|
1688
|
+
params.append(normalized_status)
|
|
1689
|
+
|
|
1690
|
+
query = "SELECT * FROM products WHERE " + " AND ".join(where_clauses)
|
|
1691
|
+
|
|
1692
|
+
if normalized_search:
|
|
1693
|
+
like_value = f"%{normalized_search.lower()}%"
|
|
1694
|
+
query += (
|
|
1695
|
+
" AND ("
|
|
1696
|
+
"LOWER(product_name) LIKE ? OR "
|
|
1697
|
+
"LOWER(COALESCE(short_description, '')) LIKE ? OR "
|
|
1698
|
+
"LOWER(COALESCE(long_description, '')) LIKE ? OR "
|
|
1699
|
+
"LOWER(COALESCE(keywords, '')) LIKE ?)"
|
|
1700
|
+
)
|
|
1701
|
+
params.extend([like_value] * 4)
|
|
1702
|
+
|
|
1703
|
+
query += f" ORDER BY {order_by} {direction}"
|
|
1704
|
+
|
|
1705
|
+
if normalized_limit is not None:
|
|
1706
|
+
query += " LIMIT ?"
|
|
1707
|
+
params.append(normalized_limit)
|
|
1708
|
+
|
|
1709
|
+
with sqlite3.connect(self.db_path) as conn:
|
|
1710
|
+
conn.row_factory = sqlite3.Row
|
|
1711
|
+
cursor = conn.cursor()
|
|
1712
|
+
cursor.execute(query, params)
|
|
1713
|
+
rows = cursor.fetchall()
|
|
1714
|
+
|
|
1715
|
+
return [self._deserialize_product_row(row) for row in rows]
|
|
1716
|
+
|
|
1717
|
+
except Exception as e:
|
|
1718
|
+
self.logger.error(f"Failed to search products: {str(e)}")
|
|
1719
|
+
raise
|
|
1720
|
+
|
|
1721
|
+
def get_products_by_org(self, org_id: str) -> List[Dict[str, Any]]:
|
|
1722
|
+
"""
|
|
1723
|
+
Backward-compatible helper that returns active products for an organization.
|
|
1724
|
+
|
|
1725
|
+
Args:
|
|
1726
|
+
org_id: Organization identifier
|
|
1727
|
+
|
|
1728
|
+
Returns:
|
|
1729
|
+
List of active product dictionaries
|
|
1730
|
+
"""
|
|
1731
|
+
return self.search_products(org_id=org_id, status="active")
|
|
1616
1732
|
|
|
1617
1733
|
def get_products_by_team(self, team_id: str) -> List[Dict[str, Any]]:
|
|
1618
1734
|
"""
|
|
@@ -1649,34 +1765,13 @@ class LocalDataManager:
|
|
|
1649
1765
|
cursor.execute(
|
|
1650
1766
|
f"SELECT * FROM products WHERE product_id IN ({placeholders}) AND status = 'active'", product_ids)
|
|
1651
1767
|
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
'customer_feedback', 'keywords', 'related_products', 'seasonal_demand',
|
|
1660
|
-
'market_insights', 'case_studies', 'testimonials', 'success_metrics',
|
|
1661
|
-
'product_variants', 'technical_specifications', 'compatibility', 'support_info',
|
|
1662
|
-
'regulatory_compliance', 'localization', 'shipping_info'
|
|
1663
|
-
]
|
|
1664
|
-
|
|
1665
|
-
for field in json_fields:
|
|
1666
|
-
if product[field]:
|
|
1667
|
-
try:
|
|
1668
|
-
product[field] = json.loads(product[field])
|
|
1669
|
-
except json.JSONDecodeError:
|
|
1670
|
-
product[field] = None
|
|
1671
|
-
|
|
1672
|
-
products.append(product)
|
|
1673
|
-
|
|
1674
|
-
return products
|
|
1675
|
-
|
|
1676
|
-
except Exception as e:
|
|
1677
|
-
self.logger.error(f"Failed to get products by team: {str(e)}")
|
|
1678
|
-
raise
|
|
1679
|
-
|
|
1768
|
+
return [self._deserialize_product_row(row)
|
|
1769
|
+
for row in cursor.fetchall()]
|
|
1770
|
+
|
|
1771
|
+
except Exception as e:
|
|
1772
|
+
self.logger.error(f"Failed to get products by team: {str(e)}")
|
|
1773
|
+
raise
|
|
1774
|
+
|
|
1680
1775
|
def get_product(self, product_id: str) -> Optional[Dict[str, Any]]:
|
|
1681
1776
|
"""
|
|
1682
1777
|
Get product by ID.
|
|
@@ -1692,33 +1787,15 @@ class LocalDataManager:
|
|
|
1692
1787
|
conn.row_factory = sqlite3.Row
|
|
1693
1788
|
cursor = conn.cursor()
|
|
1694
1789
|
cursor.execute("SELECT * FROM products WHERE product_id = ?", (product_id,))
|
|
1695
|
-
row = cursor.fetchone()
|
|
1696
|
-
|
|
1697
|
-
if row:
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
'market_insights', 'case_studies', 'testimonials', 'success_metrics',
|
|
1705
|
-
'product_variants', 'technical_specifications', 'compatibility', 'support_info',
|
|
1706
|
-
'regulatory_compliance', 'localization', 'shipping_info'
|
|
1707
|
-
]
|
|
1708
|
-
|
|
1709
|
-
for field in json_fields:
|
|
1710
|
-
if product[field]:
|
|
1711
|
-
try:
|
|
1712
|
-
product[field] = json.loads(product[field])
|
|
1713
|
-
except json.JSONDecodeError:
|
|
1714
|
-
product[field] = None
|
|
1715
|
-
|
|
1716
|
-
return product
|
|
1717
|
-
return None
|
|
1718
|
-
|
|
1719
|
-
except Exception as e:
|
|
1720
|
-
self.logger.error(f"Error getting product {product_id}: {str(e)}")
|
|
1721
|
-
raise
|
|
1790
|
+
row = cursor.fetchone()
|
|
1791
|
+
|
|
1792
|
+
if row:
|
|
1793
|
+
return self._deserialize_product_row(row)
|
|
1794
|
+
return None
|
|
1795
|
+
|
|
1796
|
+
except Exception as e:
|
|
1797
|
+
self.logger.error(f"Error getting product {product_id}: {str(e)}")
|
|
1798
|
+
raise
|
|
1722
1799
|
|
|
1723
1800
|
def update_product(self, product_id: str, product_data: Dict[str, Any]) -> bool:
|
|
1724
1801
|
"""
|
|
@@ -2135,73 +2212,101 @@ class LocalDataManager:
|
|
|
2135
2212
|
"SELECT COUNT(*) FROM team_settings WHERE org_id = 'rta'")
|
|
2136
2213
|
team_count = cursor.fetchone()[0]
|
|
2137
2214
|
|
|
2138
|
-
if team_count == 0:
|
|
2139
|
-
default_team_settings = {
|
|
2140
|
-
'id': 'team_rta_default_settings',
|
|
2141
|
-
'team_id': 'team_rta_default',
|
|
2142
|
-
'org_id': 'rta',
|
|
2143
|
-
'plan_id': '569cdcbd-cf6d-4e33-b0b2-d2f6f15a0832',
|
|
2144
|
-
'plan_name': 'FuseSell AI (v1.025)',
|
|
2145
|
-
'project_code': 'FUSESELL',
|
|
2146
|
-
'team_name': 'RTA Default Team',
|
|
2147
|
-
'
|
|
2148
|
-
'name': 'RTA',
|
|
2149
|
-
'industry': 'Technology',
|
|
2150
|
-
'website': 'https://rta.vn'
|
|
2151
|
-
}),
|
|
2152
|
-
'
|
|
2153
|
-
'name': 'Sales Team',
|
|
2154
|
-
'email': 'sales@rta.vn',
|
|
2155
|
-
'position': 'Sales Representative',
|
|
2156
|
-
'is_primary': True
|
|
2157
|
-
}]),
|
|
2158
|
-
'
|
|
2159
|
-
{'product_id': 'prod-12345678-1234-1234-1234-123456789012',
|
|
2160
|
-
|
|
2161
|
-
{'product_id': 'prod-87654321-4321-4321-4321-210987654321',
|
|
2162
|
-
|
|
2163
|
-
]),
|
|
2164
|
-
'
|
|
2165
|
-
'business_hours_start': '08:00',
|
|
2166
|
-
'business_hours_end': '20:00',
|
|
2167
|
-
'default_delay_hours': 2,
|
|
2168
|
-
'respect_weekends': True
|
|
2169
|
-
}),
|
|
2170
|
-
'
|
|
2171
|
-
'default_tone': 'professional',
|
|
2172
|
-
'approaches': [
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
'
|
|
2179
|
-
})
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2215
|
+
if team_count == 0:
|
|
2216
|
+
default_team_settings = {
|
|
2217
|
+
'id': 'team_rta_default_settings',
|
|
2218
|
+
'team_id': 'team_rta_default',
|
|
2219
|
+
'org_id': 'rta',
|
|
2220
|
+
'plan_id': '569cdcbd-cf6d-4e33-b0b2-d2f6f15a0832',
|
|
2221
|
+
'plan_name': 'FuseSell AI (v1.025)',
|
|
2222
|
+
'project_code': 'FUSESELL',
|
|
2223
|
+
'team_name': 'RTA Default Team',
|
|
2224
|
+
'gs_team_organization': json.dumps({
|
|
2225
|
+
'name': 'RTA',
|
|
2226
|
+
'industry': 'Technology',
|
|
2227
|
+
'website': 'https://rta.vn'
|
|
2228
|
+
}),
|
|
2229
|
+
'gs_team_rep': json.dumps([{
|
|
2230
|
+
'name': 'Sales Team',
|
|
2231
|
+
'email': 'sales@rta.vn',
|
|
2232
|
+
'position': 'Sales Representative',
|
|
2233
|
+
'is_primary': True
|
|
2234
|
+
}]),
|
|
2235
|
+
'gs_team_product': json.dumps([
|
|
2236
|
+
{'product_id': 'prod-12345678-1234-1234-1234-123456789012',
|
|
2237
|
+
'enabled': True, 'priority': 1},
|
|
2238
|
+
{'product_id': 'prod-87654321-4321-4321-4321-210987654321',
|
|
2239
|
+
'enabled': True, 'priority': 2}
|
|
2240
|
+
]),
|
|
2241
|
+
'gs_team_schedule_time': json.dumps({
|
|
2242
|
+
'business_hours_start': '08:00',
|
|
2243
|
+
'business_hours_end': '20:00',
|
|
2244
|
+
'default_delay_hours': 2,
|
|
2245
|
+
'respect_weekends': True
|
|
2246
|
+
}),
|
|
2247
|
+
'gs_team_initial_outreach': json.dumps({
|
|
2248
|
+
'default_tone': 'professional',
|
|
2249
|
+
'approaches': [
|
|
2250
|
+
'professional_direct',
|
|
2251
|
+
'consultative',
|
|
2252
|
+
'industry_expert',
|
|
2253
|
+
'relationship_building'
|
|
2254
|
+
],
|
|
2255
|
+
'subject_line_variations': 4
|
|
2256
|
+
}),
|
|
2257
|
+
'gs_team_follow_up': json.dumps({
|
|
2258
|
+
'max_follow_ups': 5,
|
|
2259
|
+
'default_interval_days': 3,
|
|
2260
|
+
'strategies': [
|
|
2261
|
+
'gentle_reminder',
|
|
2262
|
+
'value_add',
|
|
2263
|
+
'alternative_approach',
|
|
2264
|
+
'final_attempt',
|
|
2265
|
+
'graceful_farewell'
|
|
2266
|
+
]
|
|
2267
|
+
}),
|
|
2268
|
+
'gs_team_auto_interaction': json.dumps({
|
|
2269
|
+
'enabled': True,
|
|
2270
|
+
'handoff_threshold': 0.8,
|
|
2271
|
+
'monitoring': 'standard'
|
|
2272
|
+
}),
|
|
2273
|
+
'gs_team_followup_schedule_time': json.dumps({
|
|
2274
|
+
'timezone': 'Asia/Ho_Chi_Minh',
|
|
2275
|
+
'window': 'business_hours'
|
|
2276
|
+
}),
|
|
2277
|
+
'gs_team_birthday_email': json.dumps({
|
|
2278
|
+
'enabled': True,
|
|
2279
|
+
'template': 'birthday_2025'
|
|
2280
|
+
})
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
cursor.execute("""
|
|
2284
|
+
INSERT INTO team_settings
|
|
2285
|
+
(id, team_id, org_id, plan_id, plan_name, project_code, team_name,
|
|
2286
|
+
gs_team_organization, gs_team_rep, gs_team_product,
|
|
2287
|
+
gs_team_schedule_time, gs_team_initial_outreach, gs_team_follow_up,
|
|
2288
|
+
gs_team_auto_interaction, gs_team_followup_schedule_time, gs_team_birthday_email)
|
|
2289
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2290
|
+
""", (
|
|
2291
|
+
default_team_settings['id'],
|
|
2292
|
+
default_team_settings['team_id'],
|
|
2293
|
+
default_team_settings['org_id'],
|
|
2294
|
+
default_team_settings['plan_id'],
|
|
2295
|
+
default_team_settings['plan_name'],
|
|
2296
|
+
default_team_settings['project_code'],
|
|
2297
|
+
default_team_settings['team_name'],
|
|
2298
|
+
default_team_settings['gs_team_organization'],
|
|
2299
|
+
default_team_settings['gs_team_rep'],
|
|
2300
|
+
default_team_settings['gs_team_product'],
|
|
2301
|
+
default_team_settings['gs_team_schedule_time'],
|
|
2302
|
+
default_team_settings['gs_team_initial_outreach'],
|
|
2303
|
+
default_team_settings['gs_team_follow_up'],
|
|
2304
|
+
default_team_settings['gs_team_auto_interaction'],
|
|
2305
|
+
default_team_settings['gs_team_followup_schedule_time'],
|
|
2306
|
+
default_team_settings['gs_team_birthday_email']
|
|
2307
|
+
))
|
|
2308
|
+
|
|
2309
|
+
self.logger.debug("Initialized default team settings")
|
|
2205
2310
|
|
|
2206
2311
|
conn.commit()
|
|
2207
2312
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|