fusesell 1.2.4__py3-none-any.whl → 1.2.5__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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fusesell
3
- Version: 1.2.4
3
+ Version: 1.2.5
4
4
  Summary: Local implementation of FuseSell AI sales automation pipeline
5
5
  Author-email: FuseSell Team <team@fusesell.ai>
6
6
  License-Expression: MIT
@@ -1,9 +1,9 @@
1
1
  fusesell.py,sha256=t5PjkhWEJGINp4k517u0EX0ge7lzuHOUHHro-BE1mGk,596
2
- fusesell-1.2.4.dist-info/licenses/LICENSE,sha256=GDz1ZoC4lB0kwjERpzqc_OdA_awYVso2aBnUH-ErW_w,1070
3
- fusesell_local/__init__.py,sha256=5kIksTuzPSk57Yq7vqJC3pFHs9ZOyxaGSkA9QxqzEq0,966
2
+ fusesell-1.2.5.dist-info/licenses/LICENSE,sha256=GDz1ZoC4lB0kwjERpzqc_OdA_awYVso2aBnUH-ErW_w,1070
3
+ fusesell_local/__init__.py,sha256=_0lDJ4-RcAmyLE81anufpib0EESnvDjc_Q2-IK_xefg,966
4
4
  fusesell_local/api.py,sha256=AcPune5YJdgi7nsMeusCUqc49z5UiycsQb6n3yiV_No,10839
5
5
  fusesell_local/cli.py,sha256=MYnVxuEf5KTR4VcO3sc-VtP9NkWlSixJsYfOWST2Ds0,65859
6
- fusesell_local/pipeline.py,sha256=KO5oAIHZ3L_uAZWOszauJyv0QWlsQMIDNGRuwQSxNmQ,39531
6
+ fusesell_local/pipeline.py,sha256=zhAy8dtfcfjmRFqW9Dxr5fCGuxqvejFrIrJFw7s7hVU,39664
7
7
  fusesell_local/config/__init__.py,sha256=0ErO7QiSDqKn-LHcjIRdLZzh5QaRTkRsIlwfgpkkDz8,209
8
8
  fusesell_local/config/prompts.py,sha256=5O3Y2v3GCi9d9FEyR6Ekc1UXVq2TcZp3Rrspvx4bkac,10164
9
9
  fusesell_local/config/settings.py,sha256=rbjGPLQTFFr7DiWrPnZDFaOSNsdEMMYFx6pn7b13xGs,10743
@@ -11,8 +11,8 @@ fusesell_local/stages/__init__.py,sha256=2mAmzcMlVKZdseOR5Jju4PaPdKGsBT1ePqvt5RS
11
11
  fusesell_local/stages/base_stage.py,sha256=ldo5xuHZto7ceEg3i_3rxAx0xPccK4n2jaxEJA96RUE,22069
12
12
  fusesell_local/stages/data_acquisition.py,sha256=Td3mwakJRoEYbi3od4v2ZzKOHLgLSgccZVxH3ezs1_4,71081
13
13
  fusesell_local/stages/data_preparation.py,sha256=XWLg9b1w2NrMxLcrWDqB95mRmLQmVIMXpKNaBNr98TQ,52751
14
- fusesell_local/stages/follow_up.py,sha256=2CSen5SHJ5k6KMHXpqoRBb3n3IrcMRdDnuyTsOeuRTA,74625
15
- fusesell_local/stages/initial_outreach.py,sha256=jox2caveSwI3xIfjn8FGYprkjTbW8YhDNvCzz9wNcBE,107503
14
+ fusesell_local/stages/follow_up.py,sha256=H9Xek6EoIbHrerQvGTRswXDNFH6zq71DcRxxj0zpo2g,77747
15
+ fusesell_local/stages/initial_outreach.py,sha256=Y-WLtIGAg6nRlrCIs91l22giRrraOF6AGc1RlMDNqAM,110236
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
@@ -22,14 +22,14 @@ fusesell_local/tests/test_data_manager_sales_process.py,sha256=NbwxQ9oBKCZfrkRQY
22
22
  fusesell_local/tests/test_data_manager_teams.py,sha256=kjk4V4r9ja4EVREIiQMxkuZd470SSwRHJAvpHln9KO4,4578
23
23
  fusesell_local/utils/__init__.py,sha256=TVemlo0wpckhNUxP3a1Tky3ekswy8JdIHaXBlkKXKBQ,330
24
24
  fusesell_local/utils/birthday_email_manager.py,sha256=NKLoUyzPedyhewZPma21SOoU8p9wPquehloer7TRA9U,20478
25
- fusesell_local/utils/data_manager.py,sha256=MVozSojGXbIr9mhaTIt5bQrFO_1o-4BJux03FPW0jxA,180881
26
- fusesell_local/utils/event_scheduler.py,sha256=rjtWwtYQoJP0YwoN1-43t6K9GpLfqRq3c7Fv4papvbI,25725
25
+ fusesell_local/utils/data_manager.py,sha256=iG22AHat7rdz_tuhALagOCeHRVaBym6sl041ovVBlw0,185719
26
+ fusesell_local/utils/event_scheduler.py,sha256=YwWIdkvRdWFdDLX-sepI5AXJOhEIullIclpk9njvZAA,38577
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.4.dist-info/METADATA,sha256=cBzLD9FHG3yQu300mnPm48TKgNH1adgcF66gIF9vqHk,35074
32
- fusesell-1.2.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
33
- fusesell-1.2.4.dist-info/entry_points.txt,sha256=Vqek7tbiX7iF4rQkCRBZvT5WrB0HUduqKTsI2ZjhsXo,53
34
- fusesell-1.2.4.dist-info/top_level.txt,sha256=VP9y1K6DEq6gNq2UgLd7ChujxViF6OzeAVCK7IUBXPA,24
35
- fusesell-1.2.4.dist-info/RECORD,,
31
+ fusesell-1.2.5.dist-info/METADATA,sha256=3tPP66Wc1jK9LWT3Bi6_rpK7rg1nxZ-yiSZ7ad2l1V4,35074
32
+ fusesell-1.2.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
33
+ fusesell-1.2.5.dist-info/entry_points.txt,sha256=Vqek7tbiX7iF4rQkCRBZvT5WrB0HUduqKTsI2ZjhsXo,53
34
+ fusesell-1.2.5.dist-info/top_level.txt,sha256=VP9y1K6DEq6gNq2UgLd7ChujxViF6OzeAVCK7IUBXPA,24
35
+ fusesell-1.2.5.dist-info/RECORD,,
@@ -32,6 +32,6 @@ __all__ = [
32
32
  "validate_config",
33
33
  ]
34
34
 
35
- __version__ = "1.2.4"
35
+ __version__ = "1.2.5"
36
36
  __author__ = "FuseSell Team"
37
37
  __description__ = "Local implementation of FuseSell AI sales automation pipeline"
@@ -251,11 +251,17 @@ class FuseSellPipeline:
251
251
 
252
252
  self.logger.info("-" * 40)
253
253
  self.logger.info("TIMING VALIDATION:")
254
- if discrepancy_percentage < 5.0:
255
- self.logger.info(f"✅ Timing validation PASSED (discrepancy: {discrepancy_percentage:.1f}%)")
256
- else:
257
- self.logger.warning(f"⚠️ Timing validation WARNING (discrepancy: {discrepancy_percentage:.1f}%)")
258
- self.logger.warning(f" Expected ~{total_stage_time:.2f}s, got {total_duration:.2f}s")
254
+ if discrepancy_percentage < 5.0:
255
+ self.logger.info(
256
+ f"[OK] Timing validation PASSED (discrepancy: {discrepancy_percentage:.1f}%)"
257
+ )
258
+ else:
259
+ self.logger.warning(
260
+ f"[WARN] Timing validation WARNING (discrepancy: {discrepancy_percentage:.1f}%)"
261
+ )
262
+ self.logger.warning(
263
+ f" Expected ~{total_stage_time:.2f}s, got {total_duration:.2f}s"
264
+ )
259
265
 
260
266
  self.logger.info("=" * 60)
261
267
 
@@ -877,22 +877,29 @@ Generate only the email content, no additional commentary:"""
877
877
  input_data = context.get('input_data', {})
878
878
 
879
879
  # Initialize event scheduler
880
- scheduler = EventScheduler(self.config.get('data_dir', './fusesell_data'))
881
-
882
- # Check if immediate sending is requested
883
- send_immediately = input_data.get('send_immediately', False)
884
-
885
- # Schedule the follow-up email event
886
- schedule_result = scheduler.schedule_email_event(
887
- draft_id=draft.get('draft_id'),
888
- recipient_address=recipient_address,
889
- recipient_name=recipient_name,
890
- org_id=input_data.get('org_id', 'default'),
891
- team_id=input_data.get('team_id'),
892
- customer_timezone=input_data.get('customer_timezone'),
893
- email_type='follow_up',
894
- send_immediately=send_immediately
895
- )
880
+ scheduler = EventScheduler(self.config.get('data_dir', './fusesell_data'))
881
+
882
+ # Check if immediate sending is requested
883
+ send_immediately = input_data.get('send_immediately', False)
884
+ reminder_context = self._build_follow_up_reminder_context(
885
+ draft,
886
+ recipient_address,
887
+ recipient_name,
888
+ context
889
+ )
890
+
891
+ # Schedule the follow-up email event
892
+ schedule_result = scheduler.schedule_email_event(
893
+ draft_id=draft.get('draft_id'),
894
+ recipient_address=recipient_address,
895
+ recipient_name=recipient_name,
896
+ org_id=input_data.get('org_id', 'default'),
897
+ team_id=input_data.get('team_id'),
898
+ customer_timezone=input_data.get('customer_timezone'),
899
+ email_type='follow_up',
900
+ send_immediately=send_immediately,
901
+ reminder_context=reminder_context
902
+ )
896
903
 
897
904
  if schedule_result['success']:
898
905
  self.logger.info(f"Follow-up email event scheduled successfully: {schedule_result['event_id']} for {schedule_result['scheduled_time']}")
@@ -912,12 +919,81 @@ Generate only the email content, no additional commentary:"""
912
919
  }
913
920
 
914
921
  except Exception as e:
915
- self.logger.error(f"Follow-up email scheduling failed: {str(e)}")
916
- return {
917
- 'success': False,
918
- 'message': f'Follow-up email scheduling failed: {str(e)}',
919
- 'error': str(e)
920
- }
922
+ self.logger.error(f"Follow-up email scheduling failed: {str(e)}")
923
+ return {
924
+ 'success': False,
925
+ 'message': f'Follow-up email scheduling failed: {str(e)}',
926
+ 'error': str(e)
927
+ }
928
+
929
+ def _build_follow_up_reminder_context(
930
+ self,
931
+ draft: Dict[str, Any],
932
+ recipient_address: str,
933
+ recipient_name: str,
934
+ context: Dict[str, Any]
935
+ ) -> Dict[str, Any]:
936
+ """
937
+ Build reminder_task metadata for scheduled follow-up emails.
938
+ """
939
+ input_data = context.get('input_data', {})
940
+ org_id = input_data.get('org_id', 'default') or 'default'
941
+ customer_id = input_data.get('customer_id') or context.get('execution_id') or 'unknown'
942
+ task_id = context.get('execution_id') or input_data.get('task_id') or 'unknown_task'
943
+ team_id = input_data.get('team_id')
944
+ team_name = input_data.get('team_name')
945
+ language = input_data.get('language')
946
+ customer_name = input_data.get('customer_name')
947
+ staff_name = input_data.get('staff_name')
948
+ interaction_type = input_data.get('interaction_type', 'follow_up')
949
+ follow_up_iteration = input_data.get('current_follow_up_time') or 1
950
+ reminder_room = self.config.get('reminder_room_id') or input_data.get('reminder_room_id')
951
+ draft_id = draft.get('draft_id') or 'unknown_draft'
952
+ product_name = draft.get('product_name') or input_data.get('product_name')
953
+
954
+ customextra = {
955
+ 'reminder_content': 'follow_up',
956
+ 'org_id': org_id,
957
+ 'customer_id': customer_id,
958
+ 'task_id': task_id,
959
+ 'customer_name': customer_name,
960
+ 'language': language,
961
+ 'recipient_address': recipient_address,
962
+ 'recipient_name': recipient_name,
963
+ 'staff_name': staff_name,
964
+ 'team_id': team_id,
965
+ 'team_name': team_name,
966
+ 'interaction_type': interaction_type,
967
+ 'action_status': 'scheduled',
968
+ 'current_follow_up_time': follow_up_iteration,
969
+ 'draft_id': draft_id,
970
+ 'import_uuid': f"{org_id}_{customer_id}_{task_id}_{draft_id}"
971
+ }
972
+
973
+ if product_name:
974
+ customextra['product_name'] = product_name
975
+ if draft.get('approach'):
976
+ customextra['approach'] = draft.get('approach')
977
+ if draft.get('mail_tone'):
978
+ customextra['mail_tone'] = draft.get('mail_tone')
979
+ if draft.get('message_type'):
980
+ customextra['message_type'] = draft.get('message_type')
981
+
982
+ return {
983
+ 'status': 'published',
984
+ 'task': f"FuseSell follow-up {org_id}_{customer_id} - {task_id}",
985
+ 'tags': ['fusesell', 'follow-up'],
986
+ 'room_id': reminder_room,
987
+ 'org_id': org_id,
988
+ 'customer_id': customer_id,
989
+ 'task_id': task_id,
990
+ 'team_id': team_id,
991
+ 'team_name': team_name,
992
+ 'language': language,
993
+ 'customer_name': customer_name,
994
+ 'staff_name': staff_name,
995
+ 'customextra': customextra
996
+ }
921
997
  # Data access methods (similar to initial outreach)
922
998
  def _get_customer_data(self, context: Dict[str, Any]) -> Dict[str, Any]:
923
999
  """Get customer data from previous stages or input."""
@@ -250,23 +250,30 @@ class InitialOutreachStage(BaseStage):
250
250
 
251
251
  input_data = context.get('input_data', {})
252
252
 
253
- # Initialize event scheduler
254
- scheduler = EventScheduler(self.config.get('data_dir', './fusesell_data'))
255
-
256
- # Check if immediate sending is requested
257
- send_immediately = input_data.get('send_immediately', False)
258
-
259
- # Schedule the email event
260
- schedule_result = scheduler.schedule_email_event(
261
- draft_id=draft.get('draft_id'),
262
- recipient_address=recipient_address,
263
- recipient_name=recipient_name,
264
- org_id=input_data.get('org_id', 'default'),
265
- team_id=input_data.get('team_id'),
266
- customer_timezone=input_data.get('customer_timezone'),
267
- email_type='initial',
268
- send_immediately=send_immediately
269
- )
253
+ # Initialize event scheduler
254
+ scheduler = EventScheduler(self.config.get('data_dir', './fusesell_data'))
255
+
256
+ # Check if immediate sending is requested
257
+ send_immediately = input_data.get('send_immediately', False)
258
+ reminder_context = self._build_initial_reminder_context(
259
+ draft,
260
+ recipient_address,
261
+ recipient_name,
262
+ context
263
+ )
264
+
265
+ # Schedule the email event
266
+ schedule_result = scheduler.schedule_email_event(
267
+ draft_id=draft.get('draft_id'),
268
+ recipient_address=recipient_address,
269
+ recipient_name=recipient_name,
270
+ org_id=input_data.get('org_id', 'default'),
271
+ team_id=input_data.get('team_id'),
272
+ customer_timezone=input_data.get('customer_timezone'),
273
+ email_type='initial',
274
+ send_immediately=send_immediately,
275
+ reminder_context=reminder_context
276
+ )
270
277
 
271
278
  if schedule_result['success']:
272
279
  self.logger.info(f"Email event scheduled successfully: {schedule_result['event_id']} for {schedule_result['scheduled_time']}")
@@ -287,18 +294,80 @@ class InitialOutreachStage(BaseStage):
287
294
  }
288
295
 
289
296
  except Exception as e:
290
- self.logger.error(f"Email scheduling failed: {str(e)}")
291
- return {
292
- 'success': False,
293
- 'message': f'Email scheduling failed: {str(e)}',
294
- 'error': str(e)
295
- }
296
-
297
- def _handle_close(self, context: Dict[str, Any]) -> Dict[str, Any]:
298
- """
299
- Handle close action - Close outreach when customer feels negative.
300
-
301
- Args:
297
+ self.logger.error(f"Email scheduling failed: {str(e)}")
298
+ return {
299
+ 'success': False,
300
+ 'message': f'Email scheduling failed: {str(e)}',
301
+ 'error': str(e)
302
+ }
303
+
304
+ def _build_initial_reminder_context(
305
+ self,
306
+ draft: Dict[str, Any],
307
+ recipient_address: str,
308
+ recipient_name: str,
309
+ context: Dict[str, Any]
310
+ ) -> Dict[str, Any]:
311
+ """
312
+ Build reminder_task metadata for scheduled initial outreach emails.
313
+ """
314
+ input_data = context.get('input_data', {})
315
+ org_id = input_data.get('org_id', 'default') or 'default'
316
+ customer_id = input_data.get('customer_id') or context.get('execution_id') or 'unknown'
317
+ task_id = context.get('execution_id') or input_data.get('task_id') or 'unknown_task'
318
+ team_id = input_data.get('team_id')
319
+ team_name = input_data.get('team_name')
320
+ language = input_data.get('language')
321
+ customer_name = input_data.get('customer_name')
322
+ staff_name = input_data.get('staff_name')
323
+ reminder_room = self.config.get('reminder_room_id') or input_data.get('reminder_room_id')
324
+ draft_id = draft.get('draft_id') or 'unknown_draft'
325
+
326
+ customextra = {
327
+ 'reminder_content': 'draft_send',
328
+ 'org_id': org_id,
329
+ 'customer_id': customer_id,
330
+ 'task_id': task_id,
331
+ 'customer_name': customer_name,
332
+ 'language': language,
333
+ 'recipient_address': recipient_address,
334
+ 'recipient_name': recipient_name,
335
+ 'staff_name': staff_name,
336
+ 'team_id': team_id,
337
+ 'team_name': team_name,
338
+ 'interaction_type': input_data.get('interaction_type'),
339
+ 'draft_id': draft_id,
340
+ 'import_uuid': f"{org_id}_{customer_id}_{task_id}_{draft_id}"
341
+ }
342
+
343
+ if draft.get('product_name'):
344
+ customextra['product_name'] = draft.get('product_name')
345
+ if draft.get('approach'):
346
+ customextra['approach'] = draft.get('approach')
347
+ if draft.get('mail_tone'):
348
+ customextra['mail_tone'] = draft.get('mail_tone')
349
+
350
+ return {
351
+ 'status': 'published',
352
+ 'task': f"FuseSell initial outreach {org_id}_{customer_id} - {task_id}",
353
+ 'tags': ['fusesell', 'init-outreach'],
354
+ 'room_id': reminder_room,
355
+ 'org_id': org_id,
356
+ 'customer_id': customer_id,
357
+ 'task_id': task_id,
358
+ 'team_id': team_id,
359
+ 'team_name': team_name,
360
+ 'language': language,
361
+ 'customer_name': customer_name,
362
+ 'staff_name': staff_name,
363
+ 'customextra': customextra
364
+ }
365
+
366
+ def _handle_close(self, context: Dict[str, Any]) -> Dict[str, Any]:
367
+ """
368
+ Handle close action - Close outreach when customer feels negative.
369
+
370
+ Args:
302
371
  context: Execution context
303
372
 
304
373
  Returns:
@@ -454,11 +454,11 @@ class LocalDataManager:
454
454
  submission_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
455
455
  retrieved_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
456
456
  )
457
- """)
458
-
459
- # Create scheduler_rules table (equivalent to gs_scheduler)
460
- cursor.execute("""
461
- CREATE TABLE IF NOT EXISTS scheduler_rules (
457
+ """)
458
+
459
+ # Create scheduler_rules table (equivalent to gs_scheduler)
460
+ cursor.execute("""
461
+ CREATE TABLE IF NOT EXISTS scheduler_rules (
462
462
  id TEXT PRIMARY KEY,
463
463
  org_id TEXT NOT NULL,
464
464
  org_name TEXT,
@@ -478,14 +478,36 @@ class LocalDataManager:
478
478
  username TEXT,
479
479
  fullname TEXT,
480
480
  instance_id TEXT,
481
- submission_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
482
- )
483
- """)
484
-
485
- # Create extracted_files table (equivalent to gs_plan_setting_extracted_file)
486
- cursor.execute("""
487
- CREATE TABLE IF NOT EXISTS extracted_files (
488
- id TEXT PRIMARY KEY,
481
+ submission_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
482
+ )
483
+ """)
484
+
485
+ # Create reminder_task table (equivalent to Directus reminder_task)
486
+ cursor.execute("""
487
+ CREATE TABLE IF NOT EXISTS reminder_task (
488
+ id TEXT PRIMARY KEY,
489
+ status TEXT NOT NULL,
490
+ task TEXT NOT NULL,
491
+ cron TEXT NOT NULL,
492
+ room_id TEXT,
493
+ tags TEXT,
494
+ customextra TEXT,
495
+ org_id TEXT,
496
+ customer_id TEXT,
497
+ task_id TEXT,
498
+ import_uuid TEXT,
499
+ scheduled_time TIMESTAMP,
500
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
501
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
502
+ executed_at TIMESTAMP,
503
+ error_message TEXT
504
+ )
505
+ """)
506
+
507
+ # Create extracted_files table (equivalent to gs_plan_setting_extracted_file)
508
+ cursor.execute("""
509
+ CREATE TABLE IF NOT EXISTS extracted_files (
510
+ id TEXT PRIMARY KEY,
489
511
  org_id TEXT NOT NULL,
490
512
  plan_id TEXT,
491
513
  team_id TEXT,
@@ -585,17 +607,25 @@ class LocalDataManager:
585
607
  "CREATE INDEX IF NOT EXISTS idx_team_settings_team_id ON team_settings(team_id)")
586
608
  cursor.execute(
587
609
  "CREATE INDEX IF NOT EXISTS idx_products_org_id ON products(org_id)")
588
- cursor.execute(
589
- "CREATE INDEX IF NOT EXISTS idx_gs_customer_llmtask_task_id ON gs_customer_llmtask(task_id)")
590
- cursor.execute(
591
- "CREATE INDEX IF NOT EXISTS idx_prompts_org_id ON prompts(org_id)")
592
- cursor.execute(
593
- "CREATE INDEX IF NOT EXISTS idx_scheduler_rules_org_id ON scheduler_rules(org_id)")
594
- cursor.execute(
595
- "CREATE INDEX IF NOT EXISTS idx_extracted_files_org_id ON extracted_files(org_id)")
596
- cursor.execute(
597
- "CREATE INDEX IF NOT EXISTS idx_llm_worker_plan_org_id ON llm_worker_plan(org_id)")
598
- cursor.execute(
610
+ cursor.execute(
611
+ "CREATE INDEX IF NOT EXISTS idx_gs_customer_llmtask_task_id ON gs_customer_llmtask(task_id)")
612
+ cursor.execute(
613
+ "CREATE INDEX IF NOT EXISTS idx_prompts_org_id ON prompts(org_id)")
614
+ cursor.execute(
615
+ "CREATE INDEX IF NOT EXISTS idx_scheduler_rules_org_id ON scheduler_rules(org_id)")
616
+ cursor.execute(
617
+ "CREATE INDEX IF NOT EXISTS idx_reminder_task_status ON reminder_task(status)")
618
+ cursor.execute(
619
+ "CREATE INDEX IF NOT EXISTS idx_reminder_task_org_id ON reminder_task(org_id)")
620
+ cursor.execute(
621
+ "CREATE INDEX IF NOT EXISTS idx_reminder_task_task_id ON reminder_task(task_id)")
622
+ cursor.execute(
623
+ "CREATE INDEX IF NOT EXISTS idx_reminder_task_cron ON reminder_task(cron)")
624
+ cursor.execute(
625
+ "CREATE INDEX IF NOT EXISTS idx_extracted_files_org_id ON extracted_files(org_id)")
626
+ cursor.execute(
627
+ "CREATE INDEX IF NOT EXISTS idx_llm_worker_plan_org_id ON llm_worker_plan(org_id)")
628
+ cursor.execute(
599
629
  "CREATE INDEX IF NOT EXISTS idx_gs_company_criteria_org_id ON gs_company_criteria(org_id)")
600
630
 
601
631
  # Create compatibility views for backward compatibility
@@ -1463,6 +1493,59 @@ class LocalDataManager:
1463
1493
  'gs_team_product',
1464
1494
  'gs_team_auto_interaction',
1465
1495
  }
1496
+ list_field_defaults = {
1497
+ 'gs_team_organization': {
1498
+ 'org_name': None,
1499
+ 'address': None,
1500
+ 'website': None,
1501
+ 'industry': None,
1502
+ 'description': None,
1503
+ 'logo': None,
1504
+ 'primary_email': None,
1505
+ 'primary_phone': None,
1506
+ 'primary_color': None,
1507
+ 'is_active': False,
1508
+ 'avg_rating': None,
1509
+ 'total_sales': None,
1510
+ 'total_products': None,
1511
+ 'date_joined': None,
1512
+ 'last_active': None,
1513
+ 'social_media_links': [],
1514
+ },
1515
+ 'gs_team_rep': {
1516
+ 'name': None,
1517
+ 'email': None,
1518
+ 'phone': None,
1519
+ 'position': None,
1520
+ 'website': None,
1521
+ 'logo': None,
1522
+ 'username': None,
1523
+ 'is_primary': False,
1524
+ 'primary_color': None,
1525
+ 'primary_phone': None,
1526
+ },
1527
+ 'gs_team_product': {
1528
+ 'product_id': None,
1529
+ 'product_name': None,
1530
+ 'image_url': None,
1531
+ 'enabled': True,
1532
+ 'priority': None,
1533
+ },
1534
+ 'gs_team_auto_interaction': {
1535
+ 'from_email': '',
1536
+ 'from_name': '',
1537
+ 'from_number': '',
1538
+ 'tool_type': 'Email',
1539
+ 'email_cc': '',
1540
+ 'email_bcc': '',
1541
+ },
1542
+ }
1543
+ alias_fields = {
1544
+ 'gs_team_organization': {
1545
+ 'name': 'org_name',
1546
+ 'brand_palette': 'primary_color',
1547
+ },
1548
+ }
1466
1549
 
1467
1550
  snapshot: Dict[str, Any] = {}
1468
1551
  for field in fields_to_include:
@@ -1472,9 +1555,35 @@ class LocalDataManager:
1472
1555
 
1473
1556
  if field in list_like_fields:
1474
1557
  if isinstance(value, list):
1475
- snapshot[field] = value
1558
+ normalized_items = []
1559
+ defaults = list_field_defaults.get(field, {})
1560
+ aliases = alias_fields.get(field, {})
1561
+ for item in value:
1562
+ if not isinstance(item, dict):
1563
+ continue
1564
+ normalized = {}
1565
+ for key, default_val in defaults.items():
1566
+ if key == 'social_media_links':
1567
+ current = item.get(key)
1568
+ normalized[key] = current if isinstance(current, list) else []
1569
+ else:
1570
+ normalized[key] = item.get(key, default_val)
1571
+ for legacy_key, target_key in aliases.items():
1572
+ if normalized.get(target_key) in (None, '', []):
1573
+ if legacy_key in item:
1574
+ normalized[target_key] = item[legacy_key]
1575
+ # include any additional keys that might exist
1576
+ normalized_items.append(normalized)
1577
+ snapshot[field] = normalized_items
1476
1578
  elif value:
1477
- snapshot[field] = [value]
1579
+ defaults = list_field_defaults.get(field, {})
1580
+ aliases = alias_fields.get(field, {})
1581
+ normalized = {key: value.get(key, default_val) for key, default_val in defaults.items()}
1582
+ for legacy_key, target_key in aliases.items():
1583
+ if normalized.get(target_key) in (None, '', []):
1584
+ if legacy_key in value:
1585
+ normalized[target_key] = value[legacy_key]
1586
+ snapshot[field] = [normalized]
1478
1587
  else:
1479
1588
  snapshot[field] = []
1480
1589
  else:
@@ -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
- conn.commit()
82
- conn.close()
83
-
84
- self.logger.info("Scheduled events database initialized")
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) -> Dict[str, Any]:
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
- # Log the scheduling
197
- self.logger.info(f"Scheduled email event {event_id} for {send_time} (draft: {draft_id})")
198
-
199
- # Schedule follow-up if this is an initial email
200
- follow_up_event_id = None
201
- if email_type == 'initial' and not send_immediately:
202
- follow_up_result = self._schedule_follow_up_event(
203
- draft_id, recipient_address, recipient_name, org_id, team_id, customer_timezone
204
- )
205
- follow_up_event_id = follow_up_result.get('event_id')
206
-
207
- return {
208
- 'success': True,
209
- 'event_id': event_id,
210
- 'scheduled_time': send_time.isoformat(),
211
- 'recipient_address': recipient_address,
212
- 'recipient_name': recipient_name,
213
- 'draft_id': draft_id,
214
- 'email_type': email_type,
215
- 'follow_up_event_id': follow_up_event_id
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) -> Dict[str, Any]:
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
- self.logger.info(f"Scheduled follow-up event {followup_event_id} for {follow_up_time}")
283
-
284
- return {
285
- 'success': True,
286
- 'event_id': followup_event_id,
287
- 'scheduled_time': follow_up_time.isoformat()
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