fusesell 1.2.7__py3-none-any.whl → 1.2.9__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.7.dist-info → fusesell-1.2.9.dist-info}/METADATA +1 -1
- {fusesell-1.2.7.dist-info → fusesell-1.2.9.dist-info}/RECORD +9 -9
- fusesell_local/__init__.py +1 -1
- fusesell_local/stages/initial_outreach.py +176 -29
- fusesell_local/utils/event_scheduler.py +42 -14
- {fusesell-1.2.7.dist-info → fusesell-1.2.9.dist-info}/WHEEL +0 -0
- {fusesell-1.2.7.dist-info → fusesell-1.2.9.dist-info}/entry_points.txt +0 -0
- {fusesell-1.2.7.dist-info → fusesell-1.2.9.dist-info}/licenses/LICENSE +0 -0
- {fusesell-1.2.7.dist-info → fusesell-1.2.9.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
fusesell.py,sha256=t5PjkhWEJGINp4k517u0EX0ge7lzuHOUHHro-BE1mGk,596
|
|
2
|
-
fusesell-1.2.
|
|
3
|
-
fusesell_local/__init__.py,sha256=
|
|
2
|
+
fusesell-1.2.9.dist-info/licenses/LICENSE,sha256=GDz1ZoC4lB0kwjERpzqc_OdA_awYVso2aBnUH-ErW_w,1070
|
|
3
|
+
fusesell_local/__init__.py,sha256=Ajbk9gus0-G2gR6WAo1F_QTApGpLzz01I2LL2iE6t8c,967
|
|
4
4
|
fusesell_local/api.py,sha256=AcPune5YJdgi7nsMeusCUqc49z5UiycsQb6n3yiV_No,10839
|
|
5
5
|
fusesell_local/cli.py,sha256=MYnVxuEf5KTR4VcO3sc-VtP9NkWlSixJsYfOWST2Ds0,65859
|
|
6
6
|
fusesell_local/pipeline.py,sha256=RMF_kgwNEc1ka8-CDJyzIOTSo8PGtR_zPKAgRevhlNo,39913
|
|
@@ -12,7 +12,7 @@ fusesell_local/stages/base_stage.py,sha256=ldo5xuHZto7ceEg3i_3rxAx0xPccK4n2jaxEJ
|
|
|
12
12
|
fusesell_local/stages/data_acquisition.py,sha256=Td3mwakJRoEYbi3od4v2ZzKOHLgLSgccZVxH3ezs1_4,71081
|
|
13
13
|
fusesell_local/stages/data_preparation.py,sha256=XWLg9b1w2NrMxLcrWDqB95mRmLQmVIMXpKNaBNr98TQ,52751
|
|
14
14
|
fusesell_local/stages/follow_up.py,sha256=H9Xek6EoIbHrerQvGTRswXDNFH6zq71DcRxxj0zpo2g,77747
|
|
15
|
-
fusesell_local/stages/initial_outreach.py,sha256=
|
|
15
|
+
fusesell_local/stages/initial_outreach.py,sha256=yoXAVaPgQXZc3bMq4U363Z4ARTsnSzOqbodKB3tke3A,123593
|
|
16
16
|
fusesell_local/stages/lead_scoring.py,sha256=ir3l849eMGrGLf0OYUcmA1F3FwyYhAplS4niU3R2GRY,60658
|
|
17
17
|
fusesell_local/tests/conftest.py,sha256=TWUtlP6cNPVOYkTPz-j9BzS_KnXdPWy8D-ObPLHvXYs,366
|
|
18
18
|
fusesell_local/tests/test_api.py,sha256=763rUVb5pAuAQOovug6Ka0T9eGK8-WVOC_J08M7TETo,1827
|
|
@@ -23,13 +23,13 @@ fusesell_local/tests/test_data_manager_teams.py,sha256=kjk4V4r9ja4EVREIiQMxkuZd4
|
|
|
23
23
|
fusesell_local/utils/__init__.py,sha256=TVemlo0wpckhNUxP3a1Tky3ekswy8JdIHaXBlkKXKBQ,330
|
|
24
24
|
fusesell_local/utils/birthday_email_manager.py,sha256=NKLoUyzPedyhewZPma21SOoU8p9wPquehloer7TRA9U,20478
|
|
25
25
|
fusesell_local/utils/data_manager.py,sha256=60CLOVkVB76AQx1wQyja0PFmA1t-YJITiGNni14IPOs,188449
|
|
26
|
-
fusesell_local/utils/event_scheduler.py,sha256=
|
|
26
|
+
fusesell_local/utils/event_scheduler.py,sha256=tP-rnx9Hixfcm6ZTqloLy_EgSPII89v5dSycRHrCTLE,39824
|
|
27
27
|
fusesell_local/utils/llm_client.py,sha256=FVc25UlGt6hro7h5Iw7PHSXY3E3_67Xc-SUbHuMSRs0,10437
|
|
28
28
|
fusesell_local/utils/logger.py,sha256=sWlV8Tjyz_Z8J4zXKOnNalh8_iD6ytfrwPZpD-wcEOs,6259
|
|
29
29
|
fusesell_local/utils/timezone_detector.py,sha256=0cAE4c8ZXqCA8AvxRKm6PrFKmAmsbq3HOHR6w-mW3KQ,39997
|
|
30
30
|
fusesell_local/utils/validators.py,sha256=Z1VzeoxFsnuzlIA_ZaMWoy-0Cgyqupd47kIdljlMDbs,15438
|
|
31
|
-
fusesell-1.2.
|
|
32
|
-
fusesell-1.2.
|
|
33
|
-
fusesell-1.2.
|
|
34
|
-
fusesell-1.2.
|
|
35
|
-
fusesell-1.2.
|
|
31
|
+
fusesell-1.2.9.dist-info/METADATA,sha256=lsdyRarHbom144vFp0amieA9fQuDQF3-ZL9H-5A04DY,35074
|
|
32
|
+
fusesell-1.2.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
33
|
+
fusesell-1.2.9.dist-info/entry_points.txt,sha256=Vqek7tbiX7iF4rQkCRBZvT5WrB0HUduqKTsI2ZjhsXo,53
|
|
34
|
+
fusesell-1.2.9.dist-info/top_level.txt,sha256=VP9y1K6DEq6gNq2UgLd7ChujxViF6OzeAVCK7IUBXPA,24
|
|
35
|
+
fusesell-1.2.9.dist-info/RECORD,,
|
fusesell_local/__init__.py
CHANGED
|
@@ -12,13 +12,17 @@ from datetime import datetime
|
|
|
12
12
|
from .base_stage import BaseStage
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
class InitialOutreachStage(BaseStage):
|
|
16
|
-
"""
|
|
17
|
-
Initial Outreach stage with full server executor schema compliance.
|
|
18
|
-
Supports: draft_write, draft_rewrite, send, close actions.
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
def
|
|
15
|
+
class InitialOutreachStage(BaseStage):
|
|
16
|
+
"""
|
|
17
|
+
Initial Outreach stage with full server executor schema compliance.
|
|
18
|
+
Supports: draft_write, draft_rewrite, send, close actions.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, *args, **kwargs):
|
|
22
|
+
super().__init__(*args, **kwargs)
|
|
23
|
+
self._active_rep_profile: Dict[str, Any] = {}
|
|
24
|
+
|
|
25
|
+
def execute(self, context: Dict[str, Any]) -> Dict[str, Any]:
|
|
22
26
|
"""
|
|
23
27
|
Execute initial outreach stage with action-based routing (matching server executor).
|
|
24
28
|
|
|
@@ -99,9 +103,21 @@ class InitialOutreachStage(BaseStage):
|
|
|
99
103
|
if not recommended_product:
|
|
100
104
|
raise ValueError("No product recommendation available for email generation")
|
|
101
105
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
106
|
+
rep_profile = self._resolve_primary_sales_rep(context)
|
|
107
|
+
self._active_rep_profile = rep_profile or {}
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
# Generate multiple email drafts
|
|
111
|
+
email_drafts = self._generate_email_drafts(
|
|
112
|
+
customer_data,
|
|
113
|
+
recommended_product,
|
|
114
|
+
scoring_data,
|
|
115
|
+
context,
|
|
116
|
+
rep_profile=self._active_rep_profile
|
|
117
|
+
)
|
|
118
|
+
finally:
|
|
119
|
+
self._active_rep_profile = {}
|
|
120
|
+
|
|
105
121
|
# Save drafts to local files and database
|
|
106
122
|
saved_drafts = self._save_email_drafts(context, email_drafts)
|
|
107
123
|
|
|
@@ -624,16 +640,31 @@ class InitialOutreachStage(BaseStage):
|
|
|
624
640
|
self.logger.error(f"Failed to get auto interaction config for team {team_id}: {str(e)}")
|
|
625
641
|
return default_config
|
|
626
642
|
|
|
627
|
-
def _generate_email_drafts(self, customer_data: Dict[str, Any], recommended_product: Dict[str, Any], scoring_data: Dict[str, Any], context: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
628
|
-
"""Generate multiple personalized email drafts using LLM."""
|
|
629
|
-
if self.is_dry_run():
|
|
630
|
-
return self._get_mock_email_drafts(customer_data, recommended_product, context)
|
|
631
|
-
|
|
632
|
-
try:
|
|
633
|
-
input_data = context.get('input_data', {})
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
643
|
+
def _generate_email_drafts(self, customer_data: Dict[str, Any], recommended_product: Dict[str, Any], scoring_data: Dict[str, Any], context: Dict[str, Any], rep_profile: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
|
|
644
|
+
"""Generate multiple personalized email drafts using LLM."""
|
|
645
|
+
if self.is_dry_run():
|
|
646
|
+
return self._get_mock_email_drafts(customer_data, recommended_product, context)
|
|
647
|
+
|
|
648
|
+
try:
|
|
649
|
+
input_data = context.get('input_data', {})
|
|
650
|
+
rep_profile = rep_profile or {}
|
|
651
|
+
if rep_profile:
|
|
652
|
+
primary_name = rep_profile.get('name')
|
|
653
|
+
if primary_name:
|
|
654
|
+
input_data['staff_name'] = primary_name
|
|
655
|
+
self.config['staff_name'] = primary_name
|
|
656
|
+
if rep_profile.get('email'):
|
|
657
|
+
input_data.setdefault('staff_email', rep_profile.get('email'))
|
|
658
|
+
if rep_profile.get('phone') or rep_profile.get('primary_phone'):
|
|
659
|
+
input_data.setdefault('staff_phone', rep_profile.get('phone') or rep_profile.get('primary_phone'))
|
|
660
|
+
if rep_profile.get('position'):
|
|
661
|
+
input_data.setdefault('staff_title', rep_profile.get('position'))
|
|
662
|
+
if rep_profile.get('website'):
|
|
663
|
+
input_data.setdefault('staff_website', rep_profile.get('website'))
|
|
664
|
+
|
|
665
|
+
company_info = customer_data.get('companyInfo', {})
|
|
666
|
+
contact_info = customer_data.get('primaryContact', {})
|
|
667
|
+
pain_points = customer_data.get('painPoints', [])
|
|
637
668
|
|
|
638
669
|
prompt_drafts = self._generate_email_drafts_from_prompt(
|
|
639
670
|
customer_data,
|
|
@@ -873,6 +904,7 @@ class InitialOutreachStage(BaseStage):
|
|
|
873
904
|
first_name = name_parts[0]
|
|
874
905
|
else:
|
|
875
906
|
first_name = contact_name or ''
|
|
907
|
+
context.setdefault('customer_first_name', first_name or contact_name or '')
|
|
876
908
|
|
|
877
909
|
action = input_data.get('action', 'draft_write')
|
|
878
910
|
action_labels = {
|
|
@@ -1000,12 +1032,123 @@ class InitialOutreachStage(BaseStage):
|
|
|
1000
1032
|
f'Start with "Hi {en_name}," or "Hello {en_name}," and do not use the surname or placeholders.'
|
|
1001
1033
|
)
|
|
1002
1034
|
return 'If the recipient name is unknown, use a neutral greeting like "Hi there," without placeholders.'
|
|
1003
|
-
|
|
1004
|
-
def
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1035
|
+
|
|
1036
|
+
def _resolve_primary_sales_rep(self, context: Dict[str, Any]) -> Dict[str, Any]:
|
|
1037
|
+
team_id = context.get('input_data', {}).get('team_id') or self.config.get('team_id')
|
|
1038
|
+
if not team_id:
|
|
1039
|
+
return {}
|
|
1040
|
+
reps = self.get_team_setting('gs_team_rep', team_id, [])
|
|
1041
|
+
if not isinstance(reps, list):
|
|
1042
|
+
return {}
|
|
1043
|
+
for rep in reps:
|
|
1044
|
+
if rep and rep.get('is_primary'):
|
|
1045
|
+
return rep
|
|
1046
|
+
return reps[0] if reps else {}
|
|
1047
|
+
|
|
1048
|
+
def _sanitize_email_body(self, html: str, staff_name: str, rep_profile: Dict[str, Any], customer_first_name: str) -> str:
|
|
1049
|
+
if not html:
|
|
1050
|
+
return ''
|
|
1051
|
+
|
|
1052
|
+
replacements = {
|
|
1053
|
+
'[Your Name]': rep_profile.get('name') or staff_name,
|
|
1054
|
+
'[Your Email]': rep_profile.get('email'),
|
|
1055
|
+
'[Your Phone Number]': rep_profile.get('phone') or rep_profile.get('primary_phone'),
|
|
1056
|
+
'[Your Phone]': rep_profile.get('phone') or rep_profile.get('primary_phone'),
|
|
1057
|
+
'[Your Title]': rep_profile.get('position'),
|
|
1058
|
+
'[Your LinkedIn Profile]': rep_profile.get('linkedin') or rep_profile.get('linkedin_profile'),
|
|
1059
|
+
'[Your LinkedIn Profile URL]': rep_profile.get('linkedin') or rep_profile.get('linkedin_profile'),
|
|
1060
|
+
'[Your Website]': rep_profile.get('website'),
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
for placeholder, value in replacements.items():
|
|
1064
|
+
if value:
|
|
1065
|
+
html = html.replace(placeholder, str(value))
|
|
1066
|
+
else:
|
|
1067
|
+
html = html.replace(placeholder, '')
|
|
1068
|
+
|
|
1069
|
+
html = re.sub(r'\[Your[^<\]]+\]?', '', html, flags=re.IGNORECASE)
|
|
1070
|
+
|
|
1071
|
+
if '<p' not in html.lower():
|
|
1072
|
+
lines = [line.strip() for line in html.splitlines() if line.strip()]
|
|
1073
|
+
if lines:
|
|
1074
|
+
html = ''.join(f'<p>{line}</p>' for line in lines)
|
|
1075
|
+
|
|
1076
|
+
html = self._deduplicate_greeting(html, customer_first_name or '')
|
|
1077
|
+
html = re.sub(r'(<p>\s*</p>)+', '', html, flags=re.IGNORECASE)
|
|
1078
|
+
return html
|
|
1079
|
+
|
|
1080
|
+
def _deduplicate_greeting(self, html: str, customer_first_name: str) -> str:
|
|
1081
|
+
paragraphs = re.findall(r'(<p.*?>.*?</p>)', html, flags=re.IGNORECASE | re.DOTALL)
|
|
1082
|
+
if not paragraphs:
|
|
1083
|
+
return html
|
|
1084
|
+
|
|
1085
|
+
greeting_seen = False
|
|
1086
|
+
cleaned: List[str] = []
|
|
1087
|
+
for para in paragraphs:
|
|
1088
|
+
text = self._strip_html_tags(para).strip()
|
|
1089
|
+
normalized_para = para
|
|
1090
|
+
if self._looks_like_greeting(text):
|
|
1091
|
+
normalized_para = self._standardize_greeting_paragraph(para, customer_first_name)
|
|
1092
|
+
if greeting_seen:
|
|
1093
|
+
continue
|
|
1094
|
+
greeting_seen = True
|
|
1095
|
+
cleaned.append(normalized_para)
|
|
1096
|
+
|
|
1097
|
+
remainder = re.sub(r'(<p.*?>.*?</p>)', '__PARA__', html, flags=re.IGNORECASE | re.DOTALL)
|
|
1098
|
+
rebuilt = ''
|
|
1099
|
+
idx = 0
|
|
1100
|
+
for segment in remainder.split('__PARA__'):
|
|
1101
|
+
rebuilt += segment
|
|
1102
|
+
if idx < len(cleaned):
|
|
1103
|
+
rebuilt += cleaned[idx]
|
|
1104
|
+
idx += 1
|
|
1105
|
+
if idx < len(cleaned):
|
|
1106
|
+
rebuilt += ''.join(cleaned[idx:])
|
|
1107
|
+
return rebuilt
|
|
1108
|
+
|
|
1109
|
+
def _looks_like_greeting(self, text: str) -> bool:
|
|
1110
|
+
lowered = text.lower().replace('\xa0', ' ').strip()
|
|
1111
|
+
return lowered.startswith(('hi ', 'hello ', 'dear '))
|
|
1112
|
+
|
|
1113
|
+
def _standardize_greeting_paragraph(self, paragraph_html: str, customer_first_name: str) -> str:
|
|
1114
|
+
text = self._strip_html_tags(paragraph_html).strip()
|
|
1115
|
+
lowered = text.lower()
|
|
1116
|
+
first_word = next((candidate.title() for candidate in ('dear', 'hello', 'hi') if lowered.startswith(candidate)), 'Hi')
|
|
1117
|
+
|
|
1118
|
+
if customer_first_name:
|
|
1119
|
+
greeting = f"{first_word} {customer_first_name},"
|
|
1120
|
+
else:
|
|
1121
|
+
greeting = f"{first_word} there,"
|
|
1122
|
+
|
|
1123
|
+
remainder = ''
|
|
1124
|
+
match = re.match(r' *(hi|hello|dear)\b[^,]*,(.*)', text, flags=re.IGNORECASE | re.DOTALL)
|
|
1125
|
+
if match:
|
|
1126
|
+
remainder = match.group(2).lstrip()
|
|
1127
|
+
elif lowered.startswith(('hi', 'hello', 'dear')):
|
|
1128
|
+
parts = text.split(',', 1)
|
|
1129
|
+
if len(parts) > 1:
|
|
1130
|
+
remainder = parts[1].lstrip()
|
|
1131
|
+
else:
|
|
1132
|
+
remainder = text[len(text.split(' ', 1)[0]):].lstrip()
|
|
1133
|
+
|
|
1134
|
+
if remainder:
|
|
1135
|
+
sanitized_text = f"{greeting} {remainder}".strip()
|
|
1136
|
+
else:
|
|
1137
|
+
sanitized_text = greeting
|
|
1138
|
+
|
|
1139
|
+
return re.sub(
|
|
1140
|
+
r'(<p.*?>).*?(</p>)',
|
|
1141
|
+
lambda m: f"{m.group(1)}{sanitized_text}{m.group(2)}",
|
|
1142
|
+
paragraph_html,
|
|
1143
|
+
count=1,
|
|
1144
|
+
flags=re.IGNORECASE | re.DOTALL
|
|
1145
|
+
)
|
|
1146
|
+
|
|
1147
|
+
def _extract_first_name(self, full_name: str) -> str:
|
|
1148
|
+
if not full_name:
|
|
1149
|
+
return ''
|
|
1150
|
+
parts = full_name.strip().split()
|
|
1151
|
+
return parts[-1] if parts else full_name
|
|
1009
1152
|
|
|
1010
1153
|
def _strip_code_fences(self, text: str) -> str:
|
|
1011
1154
|
if not text:
|
|
@@ -1102,9 +1245,13 @@ class InitialOutreachStage(BaseStage):
|
|
|
1102
1245
|
tags = [tags]
|
|
1103
1246
|
tags = [str(tag).strip() for tag in tags if str(tag).strip()]
|
|
1104
1247
|
|
|
1105
|
-
call_to_action = self._extract_call_to_action(email_body)
|
|
1106
|
-
personalization_score = self._calculate_personalization_score(email_body, customer_data)
|
|
1107
|
-
message_type = entry.get('message_type') or 'Email'
|
|
1248
|
+
call_to_action = self._extract_call_to_action(email_body)
|
|
1249
|
+
personalization_score = self._calculate_personalization_score(email_body, customer_data)
|
|
1250
|
+
message_type = entry.get('message_type') or 'Email'
|
|
1251
|
+
rep_profile = getattr(self, '_active_rep_profile', {}) or {}
|
|
1252
|
+
staff_name = context.get('input_data', {}).get('staff_name') or self.config.get('staff_name', 'Sales Team')
|
|
1253
|
+
first_name = context.get('customer_first_name') or context.get('input_data', {}).get('customer_name') or ''
|
|
1254
|
+
email_body = self._sanitize_email_body(email_body, staff_name, rep_profile, first_name)
|
|
1108
1255
|
|
|
1109
1256
|
metadata = {
|
|
1110
1257
|
'customer_company': customer_data.get('companyInfo', {}).get('name', 'Unknown'),
|
|
@@ -84,6 +84,7 @@ class EventScheduler:
|
|
|
84
84
|
status TEXT NOT NULL,
|
|
85
85
|
task TEXT NOT NULL,
|
|
86
86
|
cron TEXT NOT NULL,
|
|
87
|
+
cron_ts INTEGER,
|
|
87
88
|
room_id TEXT,
|
|
88
89
|
tags TEXT,
|
|
89
90
|
customextra TEXT,
|
|
@@ -115,6 +116,11 @@ class EventScheduler:
|
|
|
115
116
|
CREATE INDEX IF NOT EXISTS idx_reminder_task_cron
|
|
116
117
|
ON reminder_task(cron)
|
|
117
118
|
""")
|
|
119
|
+
|
|
120
|
+
cursor.execute("PRAGMA table_info(reminder_task)")
|
|
121
|
+
columns = {row[1] for row in cursor.fetchall()}
|
|
122
|
+
if 'cron_ts' not in columns:
|
|
123
|
+
cursor.execute("ALTER TABLE reminder_task ADD COLUMN cron_ts INTEGER")
|
|
118
124
|
|
|
119
125
|
conn.commit()
|
|
120
126
|
conn.close()
|
|
@@ -191,6 +197,19 @@ class EventScheduler:
|
|
|
191
197
|
except ValueError:
|
|
192
198
|
return value_str
|
|
193
199
|
|
|
200
|
+
def _to_unix_timestamp(self, value: Union[str, datetime, None]) -> Optional[int]:
|
|
201
|
+
"""
|
|
202
|
+
Convert a datetime-like value to a Unix timestamp (seconds).
|
|
203
|
+
"""
|
|
204
|
+
iso_value = self._format_datetime(value)
|
|
205
|
+
try:
|
|
206
|
+
parsed = datetime.fromisoformat(iso_value)
|
|
207
|
+
except ValueError:
|
|
208
|
+
return None
|
|
209
|
+
if parsed.tzinfo is None:
|
|
210
|
+
parsed = parsed.replace(tzinfo=timezone.utc)
|
|
211
|
+
return int(parsed.timestamp())
|
|
212
|
+
|
|
194
213
|
def _build_reminder_payload(
|
|
195
214
|
self,
|
|
196
215
|
base_context: Dict[str, Any],
|
|
@@ -296,11 +315,13 @@ class EventScheduler:
|
|
|
296
315
|
|
|
297
316
|
cron_value = self._format_datetime(cron_value or send_time)
|
|
298
317
|
scheduled_time_str = self._format_datetime(scheduled_time_value or send_time)
|
|
318
|
+
cron_ts = self._to_unix_timestamp(cron_value)
|
|
299
319
|
|
|
300
320
|
return {
|
|
301
321
|
'status': status,
|
|
302
322
|
'task': task_label,
|
|
303
323
|
'cron': cron_value,
|
|
324
|
+
'cron_ts': cron_ts,
|
|
304
325
|
'room_id': room_id,
|
|
305
326
|
'tags': tags,
|
|
306
327
|
'customextra': customextra,
|
|
@@ -346,15 +367,20 @@ class EventScheduler:
|
|
|
346
367
|
conn = sqlite3.connect(self.main_db_path)
|
|
347
368
|
cursor = conn.cursor()
|
|
348
369
|
|
|
370
|
+
cron_ts = payload.get('cron_ts')
|
|
371
|
+
if cron_ts is None:
|
|
372
|
+
cron_ts = self._to_unix_timestamp(payload.get('cron'))
|
|
373
|
+
|
|
349
374
|
cursor.execute("""
|
|
350
375
|
INSERT INTO reminder_task
|
|
351
|
-
(id, status, task, cron, room_id, tags, customextra, org_id, customer_id, task_id, import_uuid, scheduled_time)
|
|
352
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
376
|
+
(id, status, task, cron, cron_ts, room_id, tags, customextra, org_id, customer_id, task_id, import_uuid, scheduled_time)
|
|
377
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
353
378
|
""", (
|
|
354
379
|
reminder_id,
|
|
355
380
|
payload.get('status', 'published'),
|
|
356
381
|
payload.get('task') or 'FuseSell Reminder',
|
|
357
382
|
self._format_datetime(payload.get('cron')),
|
|
383
|
+
cron_ts,
|
|
358
384
|
payload.get('room_id'),
|
|
359
385
|
tags_str,
|
|
360
386
|
customextra_str,
|
|
@@ -455,6 +481,7 @@ class EventScheduler:
|
|
|
455
481
|
draft_id=draft_id,
|
|
456
482
|
customer_timezone=customer_timezone
|
|
457
483
|
)
|
|
484
|
+
reminder_payload.setdefault('cron_ts', self._to_unix_timestamp(reminder_payload.get('cron')))
|
|
458
485
|
reminder_task_id = self._insert_reminder_task(reminder_payload)
|
|
459
486
|
|
|
460
487
|
# Log the scheduling
|
|
@@ -585,6 +612,7 @@ class EventScheduler:
|
|
|
585
612
|
draft_id=original_draft_id,
|
|
586
613
|
customer_timezone=event_data['customer_timezone']
|
|
587
614
|
)
|
|
615
|
+
reminder_payload.setdefault('cron_ts', self._to_unix_timestamp(reminder_payload.get('cron')))
|
|
588
616
|
reminder_task_id = self._insert_reminder_task(reminder_payload)
|
|
589
617
|
|
|
590
618
|
self.logger.info(f"Scheduled follow-up event {followup_event_id} for {follow_up_time}")
|
|
@@ -834,18 +862,18 @@ class EventScheduler:
|
|
|
834
862
|
|
|
835
863
|
# Convert to list of dictionaries
|
|
836
864
|
events = []
|
|
837
|
-
for row in rows:
|
|
838
|
-
event = dict(zip(columns, row))
|
|
839
|
-
# Parse event_data JSON
|
|
840
|
-
if event['event_data']:
|
|
841
|
-
try:
|
|
842
|
-
event['event_data'] = json.loads(event['event_data'])
|
|
843
|
-
except json.JSONDecodeError:
|
|
844
|
-
pass
|
|
845
|
-
events.append(event)
|
|
846
|
-
|
|
847
|
-
return events
|
|
848
|
-
|
|
865
|
+
for row in rows:
|
|
866
|
+
event = dict(zip(columns, row))
|
|
867
|
+
# Parse event_data JSON
|
|
868
|
+
if event['event_data']:
|
|
869
|
+
try:
|
|
870
|
+
event['event_data'] = json.loads(event['event_data'])
|
|
871
|
+
except json.JSONDecodeError:
|
|
872
|
+
pass
|
|
873
|
+
events.append(event)
|
|
874
|
+
|
|
875
|
+
return events
|
|
876
|
+
|
|
849
877
|
except Exception as e:
|
|
850
878
|
self.logger.error(f"Failed to get scheduled events: {str(e)}")
|
|
851
879
|
return []
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|