diff --git a/backend/igny8_core/business/automation/migrations/0001_initial.py b/backend/igny8_core/business/automation/migrations/0001_initial.py new file mode 100644 index 00000000..b8273313 --- /dev/null +++ b/backend/igny8_core/business/automation/migrations/0001_initial.py @@ -0,0 +1,100 @@ +# Generated manually for Phase 2: Automation System + +import django.core.validators +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('igny8_core_auth', '0008_passwordresettoken_alter_industry_options_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='AutomationRule', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(help_text='Rule name', max_length=255)), + ('description', models.TextField(blank=True, help_text='Rule description', null=True)), + ('trigger', models.CharField(choices=[('schedule', 'Schedule'), ('event', 'Event'), ('manual', 'Manual')], default='manual', max_length=50)), + ('schedule', models.CharField(blank=True, help_text="Cron-like schedule string (e.g., '0 0 * * *' for daily at midnight)", max_length=100, null=True)), + ('conditions', models.JSONField(default=list, help_text='List of conditions that must be met for rule to execute')), + ('actions', models.JSONField(default=list, help_text='List of actions to execute when rule triggers')), + ('is_active', models.BooleanField(default=True, help_text='Whether rule is active')), + ('status', models.CharField(choices=[('active', 'Active'), ('inactive', 'Inactive'), ('paused', 'Paused')], default='active', max_length=50)), + ('last_executed_at', models.DateTimeField(blank=True, null=True)), + ('execution_count', models.IntegerField(default=0, validators=[django.core.validators.MinValueValidator(0)])), + ('metadata', models.JSONField(default=dict, help_text='Additional metadata')), + ('account', models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.account')), + ('site', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='igny8_core_auth.site')), + ('sector', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='igny8_core_auth.sector')), + ], + options={ + 'db_table': 'igny8_automation_rules', + 'ordering': ['-created_at'], + 'verbose_name': 'Automation Rule', + 'verbose_name_plural': 'Automation Rules', + }, + ), + migrations.CreateModel( + name='ScheduledTask', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('scheduled_at', models.DateTimeField(help_text='When the task is scheduled to run')), + ('executed_at', models.DateTimeField(blank=True, help_text='When the task was actually executed', null=True)), + ('status', models.CharField(choices=[('pending', 'Pending'), ('running', 'Running'), ('completed', 'Completed'), ('failed', 'Failed'), ('cancelled', 'Cancelled')], default='pending', max_length=50)), + ('result', models.JSONField(default=dict, help_text='Execution result data')), + ('error_message', models.TextField(blank=True, help_text='Error message if execution failed', null=True)), + ('metadata', models.JSONField(default=dict, help_text='Additional metadata')), + ('account', models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.account')), + ('automation_rule', models.ForeignKey(help_text='The automation rule this task belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_tasks', to='automation.automationrule')), + ], + options={ + 'db_table': 'igny8_scheduled_tasks', + 'ordering': ['-scheduled_at'], + 'verbose_name': 'Scheduled Task', + 'verbose_name_plural': 'Scheduled Tasks', + }, + ), + migrations.AddIndex( + model_name='automationrule', + index=models.Index(fields=['trigger', 'is_active'], name='igny8_autom_trigger_123abc_idx'), + ), + migrations.AddIndex( + model_name='automationrule', + index=models.Index(fields=['status'], name='igny8_autom_status_456def_idx'), + ), + migrations.AddIndex( + model_name='automationrule', + index=models.Index(fields=['site', 'sector'], name='igny8_autom_site_id_789ghi_idx'), + ), + migrations.AddIndex( + model_name='automationrule', + index=models.Index(fields=['trigger', 'is_active', 'status'], name='igny8_autom_trigger_0abjkl_idx'), + ), + migrations.AddIndex( + model_name='scheduledtask', + index=models.Index(fields=['automation_rule', 'status'], name='igny8_sched_automation_123abc_idx'), + ), + migrations.AddIndex( + model_name='scheduledtask', + index=models.Index(fields=['scheduled_at', 'status'], name='igny8_sched_scheduled_456def_idx'), + ), + migrations.AddIndex( + model_name='scheduledtask', + index=models.Index(fields=['account', 'status'], name='igny8_sched_account_789ghi_idx'), + ), + migrations.AddIndex( + model_name='scheduledtask', + index=models.Index(fields=['status', 'scheduled_at'], name='igny8_sched_status_0abjkl_idx'), + ), + ] + diff --git a/backend/igny8_core/business/automation/migrations/__init__.py b/backend/igny8_core/business/automation/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/igny8_core/business/automation/models.py b/backend/igny8_core/business/automation/models.py index 4e2be51c..e4121520 100644 --- a/backend/igny8_core/business/automation/models.py +++ b/backend/igny8_core/business/automation/models.py @@ -74,6 +74,7 @@ class AutomationRule(SiteSectorBaseModel): updated_at = models.DateTimeField(auto_now=True) class Meta: + app_label = 'automation' db_table = 'igny8_automation_rules' ordering = ['-created_at'] verbose_name = 'Automation Rule' @@ -125,6 +126,7 @@ class ScheduledTask(AccountBaseModel): updated_at = models.DateTimeField(auto_now=True) class Meta: + app_label = 'automation' db_table = 'igny8_scheduled_tasks' ordering = ['-scheduled_at'] verbose_name = 'Scheduled Task' diff --git a/backend/igny8_core/modules/automation/apps.py b/backend/igny8_core/modules/automation/apps.py new file mode 100644 index 00000000..6e6a07cf --- /dev/null +++ b/backend/igny8_core/modules/automation/apps.py @@ -0,0 +1,13 @@ +""" +Automation App Configuration +""" +from django.apps import AppConfig + + +class AutomationConfig(AppConfig): + """Configuration for automation module""" + default_auto_field = 'django.db.models.BigAutoField' + name = 'igny8_core.modules.automation' + label = 'automation' + verbose_name = 'Automation' + diff --git a/backend/igny8_core/modules/automation/migrations/0001_initial.py b/backend/igny8_core/modules/automation/migrations/0001_initial.py new file mode 100644 index 00000000..b8273313 --- /dev/null +++ b/backend/igny8_core/modules/automation/migrations/0001_initial.py @@ -0,0 +1,100 @@ +# Generated manually for Phase 2: Automation System + +import django.core.validators +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('igny8_core_auth', '0008_passwordresettoken_alter_industry_options_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='AutomationRule', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(help_text='Rule name', max_length=255)), + ('description', models.TextField(blank=True, help_text='Rule description', null=True)), + ('trigger', models.CharField(choices=[('schedule', 'Schedule'), ('event', 'Event'), ('manual', 'Manual')], default='manual', max_length=50)), + ('schedule', models.CharField(blank=True, help_text="Cron-like schedule string (e.g., '0 0 * * *' for daily at midnight)", max_length=100, null=True)), + ('conditions', models.JSONField(default=list, help_text='List of conditions that must be met for rule to execute')), + ('actions', models.JSONField(default=list, help_text='List of actions to execute when rule triggers')), + ('is_active', models.BooleanField(default=True, help_text='Whether rule is active')), + ('status', models.CharField(choices=[('active', 'Active'), ('inactive', 'Inactive'), ('paused', 'Paused')], default='active', max_length=50)), + ('last_executed_at', models.DateTimeField(blank=True, null=True)), + ('execution_count', models.IntegerField(default=0, validators=[django.core.validators.MinValueValidator(0)])), + ('metadata', models.JSONField(default=dict, help_text='Additional metadata')), + ('account', models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.account')), + ('site', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='igny8_core_auth.site')), + ('sector', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='igny8_core_auth.sector')), + ], + options={ + 'db_table': 'igny8_automation_rules', + 'ordering': ['-created_at'], + 'verbose_name': 'Automation Rule', + 'verbose_name_plural': 'Automation Rules', + }, + ), + migrations.CreateModel( + name='ScheduledTask', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('scheduled_at', models.DateTimeField(help_text='When the task is scheduled to run')), + ('executed_at', models.DateTimeField(blank=True, help_text='When the task was actually executed', null=True)), + ('status', models.CharField(choices=[('pending', 'Pending'), ('running', 'Running'), ('completed', 'Completed'), ('failed', 'Failed'), ('cancelled', 'Cancelled')], default='pending', max_length=50)), + ('result', models.JSONField(default=dict, help_text='Execution result data')), + ('error_message', models.TextField(blank=True, help_text='Error message if execution failed', null=True)), + ('metadata', models.JSONField(default=dict, help_text='Additional metadata')), + ('account', models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.account')), + ('automation_rule', models.ForeignKey(help_text='The automation rule this task belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_tasks', to='automation.automationrule')), + ], + options={ + 'db_table': 'igny8_scheduled_tasks', + 'ordering': ['-scheduled_at'], + 'verbose_name': 'Scheduled Task', + 'verbose_name_plural': 'Scheduled Tasks', + }, + ), + migrations.AddIndex( + model_name='automationrule', + index=models.Index(fields=['trigger', 'is_active'], name='igny8_autom_trigger_123abc_idx'), + ), + migrations.AddIndex( + model_name='automationrule', + index=models.Index(fields=['status'], name='igny8_autom_status_456def_idx'), + ), + migrations.AddIndex( + model_name='automationrule', + index=models.Index(fields=['site', 'sector'], name='igny8_autom_site_id_789ghi_idx'), + ), + migrations.AddIndex( + model_name='automationrule', + index=models.Index(fields=['trigger', 'is_active', 'status'], name='igny8_autom_trigger_0abjkl_idx'), + ), + migrations.AddIndex( + model_name='scheduledtask', + index=models.Index(fields=['automation_rule', 'status'], name='igny8_sched_automation_123abc_idx'), + ), + migrations.AddIndex( + model_name='scheduledtask', + index=models.Index(fields=['scheduled_at', 'status'], name='igny8_sched_scheduled_456def_idx'), + ), + migrations.AddIndex( + model_name='scheduledtask', + index=models.Index(fields=['account', 'status'], name='igny8_sched_account_789ghi_idx'), + ), + migrations.AddIndex( + model_name='scheduledtask', + index=models.Index(fields=['status', 'scheduled_at'], name='igny8_sched_status_0abjkl_idx'), + ), + ] + diff --git a/backend/igny8_core/modules/automation/migrations/__init__.py b/backend/igny8_core/modules/automation/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/igny8_core/modules/automation/models.py b/backend/igny8_core/modules/automation/models.py new file mode 100644 index 00000000..d1540065 --- /dev/null +++ b/backend/igny8_core/modules/automation/models.py @@ -0,0 +1,5 @@ +# Backward compatibility alias - models moved to business/automation/ +from igny8_core.business.automation.models import AutomationRule, ScheduledTask + +__all__ = ['AutomationRule', 'ScheduledTask'] + diff --git a/backend/igny8_core/settings.py b/backend/igny8_core/settings.py index 4c20acb8..b2fd8df8 100644 --- a/backend/igny8_core/settings.py +++ b/backend/igny8_core/settings.py @@ -51,6 +51,7 @@ INSTALLED_APPS = [ 'igny8_core.modules.writer.apps.WriterConfig', 'igny8_core.modules.system.apps.SystemConfig', 'igny8_core.modules.billing.apps.BillingConfig', + 'igny8_core.modules.automation.apps.AutomationConfig', ] # System module needs explicit registration for admin