fusesell 1.2.4__py3-none-any.whl → 1.2.6__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.4.dist-info → fusesell-1.2.6.dist-info}/METADATA +1 -1
- {fusesell-1.2.4.dist-info → fusesell-1.2.6.dist-info}/RECORD +12 -12
- fusesell_local/__init__.py +1 -1
- fusesell_local/pipeline.py +11 -5
- fusesell_local/stages/follow_up.py +98 -22
- fusesell_local/stages/initial_outreach.py +224 -42
- fusesell_local/utils/data_manager.py +265 -78
- fusesell_local/utils/event_scheduler.py +386 -76
- {fusesell-1.2.4.dist-info → fusesell-1.2.6.dist-info}/WHEEL +0 -0
- {fusesell-1.2.4.dist-info → fusesell-1.2.6.dist-info}/entry_points.txt +0 -0
- {fusesell-1.2.4.dist-info → fusesell-1.2.6.dist-info}/licenses/LICENSE +0 -0
- {fusesell-1.2.4.dist-info → fusesell-1.2.6.dist-info}/top_level.txt +0 -0
|
@@ -3,14 +3,14 @@ Event Scheduler - Database-based event scheduling system
|
|
|
3
3
|
Creates scheduled events in database for external app to handle
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
import logging
|
|
7
|
-
from datetime import datetime, timedelta
|
|
8
|
-
from typing import Dict, Any, Optional, List
|
|
9
|
-
import pytz
|
|
10
|
-
import json
|
|
11
|
-
import sqlite3
|
|
12
|
-
import uuid
|
|
13
|
-
from pathlib import Path
|
|
6
|
+
import logging
|
|
7
|
+
from datetime import datetime, timedelta
|
|
8
|
+
from typing import Dict, Any, Optional, List, Union
|
|
9
|
+
import pytz
|
|
10
|
+
import json
|
|
11
|
+
import sqlite3
|
|
12
|
+
import uuid
|
|
13
|
+
from pathlib import Path
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class EventScheduler:
|
|
@@ -73,15 +73,53 @@ class EventScheduler:
|
|
|
73
73
|
ON scheduled_events(scheduled_time, status)
|
|
74
74
|
""")
|
|
75
75
|
|
|
76
|
-
cursor.execute("""
|
|
77
|
-
CREATE INDEX IF NOT EXISTS idx_scheduled_events_org_team
|
|
78
|
-
ON scheduled_events(org_id, team_id)
|
|
79
|
-
""")
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
76
|
+
cursor.execute("""
|
|
77
|
+
CREATE INDEX IF NOT EXISTS idx_scheduled_events_org_team
|
|
78
|
+
ON scheduled_events(org_id, team_id)
|
|
79
|
+
""")
|
|
80
|
+
|
|
81
|
+
cursor.execute("""
|
|
82
|
+
CREATE TABLE IF NOT EXISTS reminder_task (
|
|
83
|
+
id TEXT PRIMARY KEY,
|
|
84
|
+
status TEXT NOT NULL,
|
|
85
|
+
task TEXT NOT NULL,
|
|
86
|
+
cron TEXT NOT NULL,
|
|
87
|
+
room_id TEXT,
|
|
88
|
+
tags TEXT,
|
|
89
|
+
customextra TEXT,
|
|
90
|
+
org_id TEXT,
|
|
91
|
+
customer_id TEXT,
|
|
92
|
+
task_id TEXT,
|
|
93
|
+
import_uuid TEXT,
|
|
94
|
+
scheduled_time TIMESTAMP,
|
|
95
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
96
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
97
|
+
executed_at TIMESTAMP,
|
|
98
|
+
error_message TEXT
|
|
99
|
+
)
|
|
100
|
+
""")
|
|
101
|
+
|
|
102
|
+
cursor.execute("""
|
|
103
|
+
CREATE INDEX IF NOT EXISTS idx_reminder_task_status
|
|
104
|
+
ON reminder_task(status)
|
|
105
|
+
""")
|
|
106
|
+
cursor.execute("""
|
|
107
|
+
CREATE INDEX IF NOT EXISTS idx_reminder_task_org_id
|
|
108
|
+
ON reminder_task(org_id)
|
|
109
|
+
""")
|
|
110
|
+
cursor.execute("""
|
|
111
|
+
CREATE INDEX IF NOT EXISTS idx_reminder_task_task_id
|
|
112
|
+
ON reminder_task(task_id)
|
|
113
|
+
""")
|
|
114
|
+
cursor.execute("""
|
|
115
|
+
CREATE INDEX IF NOT EXISTS idx_reminder_task_cron
|
|
116
|
+
ON reminder_task(cron)
|
|
117
|
+
""")
|
|
118
|
+
|
|
119
|
+
conn.commit()
|
|
120
|
+
conn.close()
|
|
121
|
+
|
|
122
|
+
self.logger.info("Scheduled events database initialized")
|
|
85
123
|
|
|
86
124
|
except Exception as e:
|
|
87
125
|
self.logger.error(f"Failed to initialize scheduled events DB: {str(e)}")
|
|
@@ -122,15 +160,225 @@ class EventScheduler:
|
|
|
122
160
|
conn.commit()
|
|
123
161
|
conn.close()
|
|
124
162
|
|
|
125
|
-
self.logger.info("Scheduling rules database initialized")
|
|
126
|
-
|
|
127
|
-
except Exception as e:
|
|
128
|
-
self.logger.error(f"Failed to initialize scheduling rules DB: {str(e)}")
|
|
129
|
-
raise
|
|
163
|
+
self.logger.info("Scheduling rules database initialized")
|
|
164
|
+
|
|
165
|
+
except Exception as e:
|
|
166
|
+
self.logger.error(f"Failed to initialize scheduling rules DB: {str(e)}")
|
|
167
|
+
raise
|
|
168
|
+
|
|
169
|
+
def _format_datetime(self, value: Union[str, datetime, None]) -> str:
|
|
170
|
+
"""
|
|
171
|
+
Normalize datetime-like values to ISO 8601 strings.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
value: Datetime, ISO string, or None.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
ISO 8601 formatted string.
|
|
178
|
+
"""
|
|
179
|
+
if isinstance(value, datetime):
|
|
180
|
+
return value.isoformat()
|
|
181
|
+
if value is None:
|
|
182
|
+
return datetime.utcnow().isoformat()
|
|
183
|
+
|
|
184
|
+
value_str = str(value).strip()
|
|
185
|
+
if not value_str:
|
|
186
|
+
return datetime.utcnow().isoformat()
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
parsed = datetime.fromisoformat(value_str)
|
|
190
|
+
return parsed.isoformat()
|
|
191
|
+
except ValueError:
|
|
192
|
+
return value_str
|
|
193
|
+
|
|
194
|
+
def _build_reminder_payload(
|
|
195
|
+
self,
|
|
196
|
+
base_context: Dict[str, Any],
|
|
197
|
+
*,
|
|
198
|
+
event_id: str,
|
|
199
|
+
send_time: datetime,
|
|
200
|
+
email_type: str,
|
|
201
|
+
org_id: str,
|
|
202
|
+
recipient_address: str,
|
|
203
|
+
recipient_name: str,
|
|
204
|
+
draft_id: str,
|
|
205
|
+
customer_timezone: str,
|
|
206
|
+
follow_up_iteration: Optional[int] = None
|
|
207
|
+
) -> Optional[Dict[str, Any]]:
|
|
208
|
+
"""
|
|
209
|
+
Construct reminder_task payload mirroring server implementation.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
base_context: Context data supplied by caller.
|
|
213
|
+
event_id: Scheduled event identifier.
|
|
214
|
+
send_time: Planned send time (UTC).
|
|
215
|
+
email_type: 'initial' or 'follow_up'.
|
|
216
|
+
org_id: Organization identifier.
|
|
217
|
+
recipient_address: Recipient email.
|
|
218
|
+
recipient_name: Recipient name.
|
|
219
|
+
draft_id: Draft identifier.
|
|
220
|
+
customer_timezone: Customer timezone.
|
|
221
|
+
follow_up_iteration: Optional follow-up iteration counter.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Reminder payload dictionary or None if insufficient data.
|
|
225
|
+
"""
|
|
226
|
+
if not base_context:
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
context = dict(base_context)
|
|
230
|
+
customextra_raw = context.pop('customextra', {}) or {}
|
|
231
|
+
if isinstance(customextra_raw, str):
|
|
232
|
+
try:
|
|
233
|
+
customextra = json.loads(customextra_raw)
|
|
234
|
+
except (json.JSONDecodeError, TypeError):
|
|
235
|
+
customextra = {}
|
|
236
|
+
elif isinstance(customextra_raw, dict):
|
|
237
|
+
customextra = dict(customextra_raw)
|
|
238
|
+
else:
|
|
239
|
+
customextra = {}
|
|
240
|
+
|
|
241
|
+
status = context.pop('status', 'published') or 'published'
|
|
242
|
+
cron_value = context.pop('cron', None)
|
|
243
|
+
scheduled_time_value = context.pop('scheduled_time', None)
|
|
244
|
+
room_id = context.pop('room_id', context.pop('room', None))
|
|
245
|
+
tags = context.pop('tags', None)
|
|
246
|
+
task_label = context.pop('task', None)
|
|
247
|
+
org_id_override = context.pop('org_id', None) or org_id
|
|
248
|
+
customer_id = context.pop('customer_id', None) or customextra.get('customer_id')
|
|
249
|
+
task_id = context.pop('task_id', None) or context.pop('execution_id', None) or customextra.get('task_id')
|
|
250
|
+
customer_name = context.pop('customer_name', None)
|
|
251
|
+
language = context.pop('language', None)
|
|
252
|
+
team_id = context.pop('team_id', None)
|
|
253
|
+
team_name = context.pop('team_name', None)
|
|
254
|
+
staff_name = context.pop('staff_name', None)
|
|
255
|
+
import_uuid = context.pop('import_uuid', None) or customextra.get('import_uuid')
|
|
256
|
+
|
|
257
|
+
customextra.setdefault('reminder_content', 'draft_send' if email_type == 'initial' else 'follow_up')
|
|
258
|
+
customextra.setdefault('org_id', org_id_override)
|
|
259
|
+
customextra.setdefault('customer_id', customer_id)
|
|
260
|
+
customextra.setdefault('task_id', task_id)
|
|
261
|
+
customextra.setdefault('event_id', event_id)
|
|
262
|
+
customextra.setdefault('email_type', email_type)
|
|
263
|
+
customextra.setdefault('recipient_address', recipient_address)
|
|
264
|
+
customextra.setdefault('recipient_name', recipient_name)
|
|
265
|
+
customextra.setdefault('draft_id', draft_id)
|
|
266
|
+
customextra.setdefault('customer_timezone', customer_timezone)
|
|
267
|
+
customextra.setdefault('scheduled_time_utc', send_time.isoformat())
|
|
268
|
+
|
|
269
|
+
if team_id and 'team_id' not in customextra:
|
|
270
|
+
customextra['team_id'] = team_id
|
|
271
|
+
if team_name and 'team_name' not in customextra:
|
|
272
|
+
customextra['team_name'] = team_name
|
|
273
|
+
if language and 'language' not in customextra:
|
|
274
|
+
customextra['language'] = language
|
|
275
|
+
if staff_name and 'staff_name' not in customextra:
|
|
276
|
+
customextra['staff_name'] = staff_name
|
|
277
|
+
if customer_name and 'customer_name' not in customextra:
|
|
278
|
+
customextra['customer_name'] = customer_name
|
|
279
|
+
|
|
280
|
+
iteration = follow_up_iteration or context.pop('current_follow_up_time', None)
|
|
281
|
+
if iteration is not None and 'current_follow_up_time' not in customextra:
|
|
282
|
+
customextra['current_follow_up_time'] = iteration
|
|
283
|
+
|
|
284
|
+
if not import_uuid:
|
|
285
|
+
import_uuid = f"{customextra.get('org_id', '')}_{customextra.get('customer_id', '')}_{customextra.get('task_id', '')}_{event_id}"
|
|
286
|
+
customextra.setdefault('import_uuid', import_uuid)
|
|
287
|
+
|
|
288
|
+
if not tags:
|
|
289
|
+
tags = ['fusesell', 'init-outreach' if email_type == 'initial' else 'follow-up']
|
|
290
|
+
|
|
291
|
+
if not task_label:
|
|
292
|
+
readable_type = "Initial Outreach" if email_type == 'initial' else "Follow-up"
|
|
293
|
+
identifier = customextra.get('customer_name') or customextra.get('customer_id') or customer_id or 'customer'
|
|
294
|
+
tracking_id = customextra.get('task_id') or task_id or draft_id
|
|
295
|
+
task_label = f"FuseSell {readable_type} {identifier} - {tracking_id}"
|
|
296
|
+
|
|
297
|
+
cron_value = self._format_datetime(cron_value or send_time)
|
|
298
|
+
scheduled_time_str = self._format_datetime(scheduled_time_value or send_time)
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
'status': status,
|
|
302
|
+
'task': task_label,
|
|
303
|
+
'cron': cron_value,
|
|
304
|
+
'room_id': room_id,
|
|
305
|
+
'tags': tags,
|
|
306
|
+
'customextra': customextra,
|
|
307
|
+
'org_id': customextra.get('org_id'),
|
|
308
|
+
'customer_id': customextra.get('customer_id'),
|
|
309
|
+
'task_id': customextra.get('task_id'),
|
|
310
|
+
'import_uuid': customextra.get('import_uuid'),
|
|
311
|
+
'scheduled_time': scheduled_time_str
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
def _insert_reminder_task(self, payload: Optional[Dict[str, Any]]) -> Optional[str]:
|
|
315
|
+
"""
|
|
316
|
+
Insert reminder_task record into local database.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
payload: Reminder payload produced by _build_reminder_payload.
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
Reminder task identifier or None on failure.
|
|
323
|
+
"""
|
|
324
|
+
if not payload:
|
|
325
|
+
return None
|
|
326
|
+
|
|
327
|
+
try:
|
|
328
|
+
reminder_id = payload.get('id') or f"uuid:{str(uuid.uuid4())}"
|
|
329
|
+
|
|
330
|
+
tags_value = payload.get('tags')
|
|
331
|
+
if isinstance(tags_value, (list, tuple)):
|
|
332
|
+
tags_str = json.dumps(list(tags_value))
|
|
333
|
+
elif isinstance(tags_value, str):
|
|
334
|
+
tags_str = tags_value
|
|
335
|
+
else:
|
|
336
|
+
tags_str = json.dumps([])
|
|
337
|
+
|
|
338
|
+
customextra_value = payload.get('customextra') or {}
|
|
339
|
+
if isinstance(customextra_value, dict):
|
|
340
|
+
customextra_str = json.dumps(customextra_value)
|
|
341
|
+
elif isinstance(customextra_value, str):
|
|
342
|
+
customextra_str = customextra_value
|
|
343
|
+
else:
|
|
344
|
+
customextra_str = json.dumps({})
|
|
345
|
+
|
|
346
|
+
conn = sqlite3.connect(self.main_db_path)
|
|
347
|
+
cursor = conn.cursor()
|
|
348
|
+
|
|
349
|
+
cursor.execute("""
|
|
350
|
+
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
353
|
+
""", (
|
|
354
|
+
reminder_id,
|
|
355
|
+
payload.get('status', 'published'),
|
|
356
|
+
payload.get('task') or 'FuseSell Reminder',
|
|
357
|
+
self._format_datetime(payload.get('cron')),
|
|
358
|
+
payload.get('room_id'),
|
|
359
|
+
tags_str,
|
|
360
|
+
customextra_str,
|
|
361
|
+
payload.get('org_id'),
|
|
362
|
+
payload.get('customer_id'),
|
|
363
|
+
payload.get('task_id'),
|
|
364
|
+
payload.get('import_uuid'),
|
|
365
|
+
self._format_datetime(payload.get('scheduled_time'))
|
|
366
|
+
))
|
|
367
|
+
|
|
368
|
+
conn.commit()
|
|
369
|
+
conn.close()
|
|
370
|
+
|
|
371
|
+
self.logger.debug(f"Created reminder_task record {reminder_id}")
|
|
372
|
+
return reminder_id
|
|
373
|
+
|
|
374
|
+
except Exception as exc:
|
|
375
|
+
self.logger.error(f"Failed to create reminder_task record: {str(exc)}")
|
|
376
|
+
return None
|
|
130
377
|
|
|
131
|
-
def schedule_email_event(self, draft_id: str, recipient_address: str, recipient_name: str,
|
|
132
|
-
org_id: str, team_id: str = None, customer_timezone: str = None,
|
|
133
|
-
email_type: str = 'initial', send_immediately: bool = False
|
|
378
|
+
def schedule_email_event(self, draft_id: str, recipient_address: str, recipient_name: str,
|
|
379
|
+
org_id: str, team_id: str = None, customer_timezone: str = None,
|
|
380
|
+
email_type: str = 'initial', send_immediately: bool = False,
|
|
381
|
+
reminder_context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
134
382
|
"""
|
|
135
383
|
Schedule an email event in the database for external app to handle.
|
|
136
384
|
|
|
@@ -142,7 +390,8 @@ class EventScheduler:
|
|
|
142
390
|
team_id: Team ID (optional)
|
|
143
391
|
customer_timezone: Customer's timezone (optional)
|
|
144
392
|
email_type: Type of email ('initial' or 'follow_up')
|
|
145
|
-
send_immediately: If True, schedule for immediate sending
|
|
393
|
+
send_immediately: If True, schedule for immediate sending
|
|
394
|
+
reminder_context: Optional metadata for reminder_task mirroring server behaviour
|
|
146
395
|
|
|
147
396
|
Returns:
|
|
148
397
|
Event creation result with event ID and scheduled time
|
|
@@ -180,40 +429,82 @@ class EventScheduler:
|
|
|
180
429
|
conn = sqlite3.connect(self.main_db_path)
|
|
181
430
|
cursor = conn.cursor()
|
|
182
431
|
|
|
183
|
-
cursor.execute("""
|
|
184
|
-
INSERT INTO scheduled_events
|
|
185
|
-
(id, event_id, event_type, scheduled_time, org_id, team_id, draft_id,
|
|
186
|
-
recipient_address, recipient_name, customer_timezone, event_data)
|
|
187
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
432
|
+
cursor.execute("""
|
|
433
|
+
INSERT INTO scheduled_events
|
|
434
|
+
(id, event_id, event_type, scheduled_time, org_id, team_id, draft_id,
|
|
435
|
+
recipient_address, recipient_name, customer_timezone, event_data)
|
|
436
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
188
437
|
""", (
|
|
189
438
|
f"uuid:{str(uuid.uuid4())}", event_id, 'email_send', send_time, org_id, team_id, draft_id,
|
|
190
439
|
recipient_address, recipient_name, customer_timezone, json.dumps(event_data)
|
|
191
440
|
))
|
|
192
441
|
|
|
193
|
-
conn.commit()
|
|
194
|
-
conn.close()
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
442
|
+
conn.commit()
|
|
443
|
+
conn.close()
|
|
444
|
+
|
|
445
|
+
reminder_task_id = None
|
|
446
|
+
if reminder_context:
|
|
447
|
+
reminder_payload = self._build_reminder_payload(
|
|
448
|
+
dict(reminder_context),
|
|
449
|
+
event_id=event_id,
|
|
450
|
+
send_time=send_time,
|
|
451
|
+
email_type=email_type,
|
|
452
|
+
org_id=org_id,
|
|
453
|
+
recipient_address=recipient_address,
|
|
454
|
+
recipient_name=recipient_name,
|
|
455
|
+
draft_id=draft_id,
|
|
456
|
+
customer_timezone=customer_timezone
|
|
457
|
+
)
|
|
458
|
+
reminder_task_id = self._insert_reminder_task(reminder_payload)
|
|
459
|
+
|
|
460
|
+
# Log the scheduling
|
|
461
|
+
self.logger.info(f"Scheduled email event {event_id} for {send_time} (draft: {draft_id})")
|
|
462
|
+
|
|
463
|
+
# Schedule follow-up if this is an initial email
|
|
464
|
+
follow_up_event_id = None
|
|
465
|
+
follow_up_reminder_id = None
|
|
466
|
+
if email_type == 'initial' and not send_immediately:
|
|
467
|
+
follow_up_context = None
|
|
468
|
+
if reminder_context:
|
|
469
|
+
follow_up_context = dict(reminder_context)
|
|
470
|
+
follow_up_extra = dict(follow_up_context.get('customextra', {}) or {})
|
|
471
|
+
follow_up_extra['reminder_content'] = 'follow_up'
|
|
472
|
+
follow_up_extra.setdefault('current_follow_up_time', 1)
|
|
473
|
+
follow_up_context['customextra'] = follow_up_extra
|
|
474
|
+
follow_up_context['tags'] = follow_up_context.get('tags') or ['fusesell', 'follow-up']
|
|
475
|
+
|
|
476
|
+
follow_up_result = self._schedule_follow_up_event(
|
|
477
|
+
draft_id,
|
|
478
|
+
recipient_address,
|
|
479
|
+
recipient_name,
|
|
480
|
+
org_id,
|
|
481
|
+
team_id,
|
|
482
|
+
customer_timezone,
|
|
483
|
+
reminder_context=follow_up_context
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
if follow_up_result.get('success'):
|
|
487
|
+
follow_up_event_id = follow_up_result.get('event_id')
|
|
488
|
+
follow_up_reminder_id = follow_up_result.get('reminder_task_id')
|
|
489
|
+
else:
|
|
490
|
+
self.logger.warning(
|
|
491
|
+
"Follow-up scheduling failed for event %s: %s",
|
|
492
|
+
event_id,
|
|
493
|
+
follow_up_result.get('error', 'unknown error')
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
return {
|
|
497
|
+
'success': True,
|
|
498
|
+
'event_id': event_id,
|
|
499
|
+
'scheduled_time': send_time.isoformat(),
|
|
500
|
+
'recipient_address': recipient_address,
|
|
501
|
+
'recipient_name': recipient_name,
|
|
502
|
+
'draft_id': draft_id,
|
|
503
|
+
'email_type': email_type,
|
|
504
|
+
'reminder_task_id': reminder_task_id,
|
|
505
|
+
'follow_up_event_id': follow_up_event_id,
|
|
506
|
+
'follow_up_reminder_task_id': follow_up_reminder_id
|
|
507
|
+
}
|
|
217
508
|
|
|
218
509
|
except Exception as e:
|
|
219
510
|
self.logger.error(f"Failed to schedule email event: {str(e)}")
|
|
@@ -222,9 +513,10 @@ class EventScheduler:
|
|
|
222
513
|
'error': str(e)
|
|
223
514
|
}
|
|
224
515
|
|
|
225
|
-
def _schedule_follow_up_event(self, original_draft_id: str, recipient_address: str,
|
|
226
|
-
recipient_name: str, org_id: str, team_id: str = None,
|
|
227
|
-
customer_timezone: str = None
|
|
516
|
+
def _schedule_follow_up_event(self, original_draft_id: str, recipient_address: str,
|
|
517
|
+
recipient_name: str, org_id: str, team_id: str = None,
|
|
518
|
+
customer_timezone: str = None,
|
|
519
|
+
reminder_context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
228
520
|
"""
|
|
229
521
|
Schedule follow-up email event after initial email.
|
|
230
522
|
|
|
@@ -234,7 +526,8 @@ class EventScheduler:
|
|
|
234
526
|
recipient_name: Name of recipient
|
|
235
527
|
org_id: Organization ID
|
|
236
528
|
team_id: Team ID (optional)
|
|
237
|
-
customer_timezone: Customer's timezone (optional)
|
|
529
|
+
customer_timezone: Customer's timezone (optional)
|
|
530
|
+
reminder_context: Optional metadata for reminder_task rows
|
|
238
531
|
|
|
239
532
|
Returns:
|
|
240
533
|
Follow-up event creation result
|
|
@@ -275,24 +568,41 @@ class EventScheduler:
|
|
|
275
568
|
original_draft_id, recipient_address, recipient_name,
|
|
276
569
|
customer_timezone, json.dumps(event_data)
|
|
277
570
|
))
|
|
278
|
-
|
|
279
|
-
conn.commit()
|
|
280
|
-
conn.close()
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
571
|
+
|
|
572
|
+
conn.commit()
|
|
573
|
+
conn.close()
|
|
574
|
+
|
|
575
|
+
reminder_task_id = None
|
|
576
|
+
if reminder_context:
|
|
577
|
+
reminder_payload = self._build_reminder_payload(
|
|
578
|
+
dict(reminder_context),
|
|
579
|
+
event_id=followup_event_id,
|
|
580
|
+
send_time=follow_up_time,
|
|
581
|
+
email_type='follow_up',
|
|
582
|
+
org_id=org_id,
|
|
583
|
+
recipient_address=recipient_address,
|
|
584
|
+
recipient_name=recipient_name,
|
|
585
|
+
draft_id=original_draft_id,
|
|
586
|
+
customer_timezone=event_data['customer_timezone']
|
|
587
|
+
)
|
|
588
|
+
reminder_task_id = self._insert_reminder_task(reminder_payload)
|
|
589
|
+
|
|
590
|
+
self.logger.info(f"Scheduled follow-up event {followup_event_id} for {follow_up_time}")
|
|
591
|
+
|
|
592
|
+
return {
|
|
593
|
+
'success': True,
|
|
594
|
+
'event_id': followup_event_id,
|
|
595
|
+
'scheduled_time': follow_up_time.isoformat(),
|
|
596
|
+
'reminder_task_id': reminder_task_id
|
|
597
|
+
}
|
|
289
598
|
|
|
290
599
|
except Exception as e:
|
|
291
|
-
self.logger.error(f"Failed to schedule follow-up event: {str(e)}")
|
|
292
|
-
return {
|
|
293
|
-
'success': False,
|
|
294
|
-
'error': str(e)
|
|
295
|
-
|
|
600
|
+
self.logger.error(f"Failed to schedule follow-up event: {str(e)}")
|
|
601
|
+
return {
|
|
602
|
+
'success': False,
|
|
603
|
+
'error': str(e),
|
|
604
|
+
'reminder_task_id': None
|
|
605
|
+
}
|
|
296
606
|
|
|
297
607
|
def _get_scheduling_rule(self, org_id: str, team_id: str = None) -> Dict[str, Any]:
|
|
298
608
|
"""
|
|
@@ -615,4 +925,4 @@ class EventScheduler:
|
|
|
615
925
|
|
|
616
926
|
except Exception as e:
|
|
617
927
|
self.logger.error(f"Failed to create scheduling rule: {str(e)}")
|
|
618
|
-
return False
|
|
928
|
+
return False
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|