constec 0.4.1__py3-none-any.whl → 0.5.1__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.
@@ -0,0 +1,270 @@
1
+ # Generated manually on 2026-02-03 for automations redesign (Fase 1)
2
+
3
+ import uuid
4
+ from django.conf import settings
5
+ from django.db import migrations, models
6
+ import django.db.models.deletion
7
+
8
+
9
+ class Migration(migrations.Migration):
10
+
11
+ dependencies = [
12
+ ('constec_db', '0005_event'),
13
+ ]
14
+
15
+ operations = [
16
+ # Create Automation model
17
+ migrations.CreateModel(
18
+ name='Automation',
19
+ fields=[
20
+ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
21
+ ('created_at', models.DateTimeField(auto_now_add=True)),
22
+ ('updated_at', models.DateTimeField(auto_now=True)),
23
+ ('name', models.CharField(max_length=255)),
24
+ ('description', models.TextField(blank=True)),
25
+ ('status', models.CharField(
26
+ choices=[('active', 'Active'), ('paused', 'Paused'), ('archived', 'Archived'), ('draft', 'Draft')],
27
+ default='draft',
28
+ max_length=20
29
+ )),
30
+ ('execution_mode', models.CharField(
31
+ choices=[('sequential', 'Sequential'), ('parallel', 'Parallel')],
32
+ default='sequential',
33
+ max_length=20
34
+ )),
35
+ ('max_executions', models.IntegerField(
36
+ blank=True,
37
+ help_text='Máximo número de ejecuciones (null = ilimitado)',
38
+ null=True
39
+ )),
40
+ ('execution_count', models.IntegerField(default=0)),
41
+ ('on_error', models.CharField(
42
+ choices=[('stop', 'Stop'), ('continue', 'Continue'), ('retry', 'Retry')],
43
+ default='stop',
44
+ max_length=20
45
+ )),
46
+ ('retry_count', models.IntegerField(default=3)),
47
+ ('retry_delay_seconds', models.IntegerField(default=60)),
48
+ ('last_executed_at', models.DateTimeField(blank=True, null=True)),
49
+ ('next_execution_at', models.DateTimeField(blank=True, db_index=True, null=True)),
50
+ ('tags', models.JSONField(blank=True, default=list)),
51
+ ('metadata', models.JSONField(blank=True, default=dict)),
52
+ ('company', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='constec_db.company')),
53
+ ('created_by', models.ForeignKey(
54
+ on_delete=django.db.models.deletion.CASCADE,
55
+ related_name='created_automations',
56
+ to=settings.AUTH_USER_MODEL
57
+ )),
58
+ ],
59
+ options={
60
+ 'db_table': '"automations"."automations"',
61
+ 'ordering': ['-created_at'],
62
+ 'indexes': [
63
+ models.Index(fields=['company', 'status'], name='automations_auto_company_status_idx'),
64
+ models.Index(fields=['status', 'next_execution_at'], name='automations_auto_status_next_idx'),
65
+ ],
66
+ },
67
+ bases=(models.Model,),
68
+ ),
69
+
70
+ # Create Trigger model
71
+ migrations.CreateModel(
72
+ name='Trigger',
73
+ fields=[
74
+ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
75
+ ('created_at', models.DateTimeField(auto_now_add=True)),
76
+ ('updated_at', models.DateTimeField(auto_now=True)),
77
+ ('trigger_type', models.CharField(
78
+ choices=[('schedule', 'Schedule'), ('webhook', 'Webhook'), ('manual', 'Manual'), ('event', 'Event')],
79
+ max_length=20
80
+ )),
81
+ ('name', models.CharField(max_length=255)),
82
+ ('description', models.TextField(blank=True)),
83
+ ('is_enabled', models.BooleanField(default=True)),
84
+ ('config', models.JSONField(
85
+ default=dict,
86
+ help_text='Configuración específica del tipo de trigger (schedule, webhook, etc.)'
87
+ )),
88
+ ('last_triggered_at', models.DateTimeField(blank=True, null=True)),
89
+ ('trigger_count', models.IntegerField(default=0)),
90
+ ('priority', models.IntegerField(default=50, help_text='0-100, mayor = más prioridad')),
91
+ ('automation', models.ForeignKey(
92
+ on_delete=django.db.models.deletion.CASCADE,
93
+ related_name='triggers',
94
+ to='constec_db.automation'
95
+ )),
96
+ ],
97
+ options={
98
+ 'db_table': '"automations"."triggers"',
99
+ 'ordering': ['-priority', 'created_at'],
100
+ 'indexes': [
101
+ models.Index(fields=['automation', 'is_enabled'], name='automations_trig_automation_enabled_idx'),
102
+ models.Index(fields=['trigger_type', 'is_enabled'], name='automations_trig_type_enabled_idx'),
103
+ ],
104
+ },
105
+ bases=(models.Model,),
106
+ ),
107
+
108
+ # Create Action model
109
+ migrations.CreateModel(
110
+ name='Action',
111
+ fields=[
112
+ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
113
+ ('created_at', models.DateTimeField(auto_now_add=True)),
114
+ ('updated_at', models.DateTimeField(auto_now=True)),
115
+ ('action_type', models.CharField(
116
+ choices=[('notification', 'Notification'), ('webhook', 'Webhook'), ('batch_task', 'Batch Task'), ('flow_execution', 'Flow Execution')],
117
+ max_length=20
118
+ )),
119
+ ('name', models.CharField(max_length=255)),
120
+ ('description', models.TextField(blank=True)),
121
+ ('is_enabled', models.BooleanField(default=True)),
122
+ ('order', models.IntegerField(default=0, help_text='Orden de ejecución')),
123
+ ('config', models.JSONField(
124
+ default=dict,
125
+ help_text='Configuración específica del tipo de acción'
126
+ )),
127
+ ('on_error', models.CharField(
128
+ blank=True,
129
+ choices=[('stop', 'Stop'), ('continue', 'Continue'), ('retry', 'Retry')],
130
+ help_text='Override del on_error de la automation',
131
+ max_length=20,
132
+ null=True
133
+ )),
134
+ ('retry_count', models.IntegerField(
135
+ blank=True,
136
+ help_text='Override del retry_count de la automation',
137
+ null=True
138
+ )),
139
+ ('condition', models.JSONField(
140
+ blank=True,
141
+ help_text='Condición JSONLogic para ejecutar esta acción',
142
+ null=True
143
+ )),
144
+ ('last_executed_at', models.DateTimeField(blank=True, null=True)),
145
+ ('execution_count', models.IntegerField(default=0)),
146
+ ('success_count', models.IntegerField(default=0)),
147
+ ('failure_count', models.IntegerField(default=0)),
148
+ ('automation', models.ForeignKey(
149
+ on_delete=django.db.models.deletion.CASCADE,
150
+ related_name='actions',
151
+ to='constec_db.automation'
152
+ )),
153
+ ],
154
+ options={
155
+ 'db_table': '"automations"."actions"',
156
+ 'ordering': ['order', 'created_at'],
157
+ 'indexes': [
158
+ models.Index(fields=['automation', 'order'], name='automations_act_automation_order_idx'),
159
+ models.Index(fields=['action_type', 'is_enabled'], name='automations_act_type_enabled_idx'),
160
+ ],
161
+ },
162
+ bases=(models.Model,),
163
+ ),
164
+
165
+ # Create ExecutionLog model
166
+ migrations.CreateModel(
167
+ name='ExecutionLog',
168
+ fields=[
169
+ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
170
+ ('created_at', models.DateTimeField(auto_now_add=True)),
171
+ ('updated_at', models.DateTimeField(auto_now=True)),
172
+ ('status', models.CharField(
173
+ choices=[('pending', 'Pending'), ('running', 'Running'), ('completed', 'Completed'), ('failed', 'Failed'), ('partial', 'Partial')],
174
+ default='pending',
175
+ max_length=20
176
+ )),
177
+ ('triggered_by', models.CharField(
178
+ choices=[('schedule', 'Schedule'), ('webhook', 'Webhook'), ('manual', 'Manual'), ('system', 'System')],
179
+ max_length=20
180
+ )),
181
+ ('started_at', models.DateTimeField(auto_now_add=True)),
182
+ ('completed_at', models.DateTimeField(blank=True, null=True)),
183
+ ('duration_seconds', models.IntegerField(blank=True, null=True)),
184
+ ('execution_context', models.JSONField(
185
+ default=dict,
186
+ help_text='Variables y contexto disponible durante la ejecución'
187
+ )),
188
+ ('actions_total', models.IntegerField(default=0)),
189
+ ('actions_succeeded', models.IntegerField(default=0)),
190
+ ('actions_failed', models.IntegerField(default=0)),
191
+ ('actions_skipped', models.IntegerField(default=0)),
192
+ ('action_logs', models.JSONField(
193
+ default=list,
194
+ help_text='Array de logs detallados de cada acción ejecutada'
195
+ )),
196
+ ('error_message', models.TextField(blank=True)),
197
+ ('error_traceback', models.TextField(blank=True)),
198
+ ('automation', models.ForeignKey(
199
+ on_delete=django.db.models.deletion.CASCADE,
200
+ related_name='execution_logs',
201
+ to='constec_db.automation'
202
+ )),
203
+ ('trigger', models.ForeignKey(
204
+ blank=True,
205
+ null=True,
206
+ on_delete=django.db.models.deletion.SET_NULL,
207
+ related_name='execution_logs',
208
+ to='constec_db.trigger'
209
+ )),
210
+ ('triggered_by_user', models.ForeignKey(
211
+ blank=True,
212
+ null=True,
213
+ on_delete=django.db.models.deletion.SET_NULL,
214
+ related_name='triggered_executions',
215
+ to=settings.AUTH_USER_MODEL
216
+ )),
217
+ ],
218
+ options={
219
+ 'db_table': '"automations"."execution_logs"',
220
+ 'ordering': ['-started_at'],
221
+ 'indexes': [
222
+ models.Index(fields=['automation', 'started_at'], name='automations_exec_automation_started_idx'),
223
+ models.Index(fields=['status', 'started_at'], name='automations_exec_status_started_idx'),
224
+ models.Index(fields=['triggered_by', 'started_at'], name='automations_exec_triggered_started_idx'),
225
+ ],
226
+ },
227
+ bases=(models.Model,),
228
+ ),
229
+
230
+ # Create NotificationTemplate model
231
+ migrations.CreateModel(
232
+ name='NotificationTemplate',
233
+ fields=[
234
+ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
235
+ ('created_at', models.DateTimeField(auto_now_add=True)),
236
+ ('updated_at', models.DateTimeField(auto_now=True)),
237
+ ('name', models.CharField(max_length=255)),
238
+ ('description', models.TextField(blank=True)),
239
+ ('channel', models.CharField(
240
+ choices=[('email', 'Email'), ('whatsapp', 'WhatsApp'), ('sms', 'SMS'), ('push', 'Push Notification')],
241
+ max_length=20
242
+ )),
243
+ ('subject', models.CharField(
244
+ blank=True,
245
+ help_text='Asunto (solo para email)',
246
+ max_length=255
247
+ )),
248
+ ('body', models.TextField(help_text='Cuerpo del mensaje con variables {{variable}}')),
249
+ ('variables', models.JSONField(
250
+ default=list,
251
+ help_text="Array de nombres de variables esperadas ['customer_name', 'amount']"
252
+ )),
253
+ # is_active inherited from UUIDModel
254
+ ('company', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='constec_db.company')),
255
+ ('created_by', models.ForeignKey(
256
+ on_delete=django.db.models.deletion.CASCADE,
257
+ related_name='created_templates',
258
+ to=settings.AUTH_USER_MODEL
259
+ )),
260
+ ],
261
+ options={
262
+ 'db_table': '"automations"."notification_templates"',
263
+ 'ordering': ['name'],
264
+ 'indexes': [
265
+ models.Index(fields=['company', 'channel', 'is_active'], name='automations_tmpl_company_channel_idx'),
266
+ ],
267
+ },
268
+ bases=(models.Model,),
269
+ ),
270
+ ]
@@ -42,7 +42,7 @@ from .flow import FlowTemplate, Flow
42
42
  from .session import Session, Message
43
43
 
44
44
  # Automations models (automations schema)
45
- from .automation import Event
45
+ from .automation import Event, Automation, Trigger, Action, ExecutionLog, NotificationTemplate
46
46
 
47
47
 
48
48
  __all__ = [
@@ -94,4 +94,9 @@ __all__ = [
94
94
  'Message',
95
95
  # Automations (automations schema)
96
96
  'Event',
97
+ 'Automation',
98
+ 'Trigger',
99
+ 'Action',
100
+ 'ExecutionLog',
101
+ 'NotificationTemplate',
97
102
  ]
@@ -2,11 +2,378 @@
2
2
 
3
3
  import uuid
4
4
  from django.db import models
5
+ from django.core.exceptions import ValidationError
5
6
  from .base import UUIDModel
6
7
  from .company import Company
7
8
  from .user import User
8
9
 
9
10
 
11
+ class Automation(UUIDModel):
12
+ """
13
+ Automatización principal basada en el patrón Trigger-Action.
14
+
15
+ Una automatización define un conjunto de acciones que se ejecutan cuando
16
+ se activan uno o más triggers (disparadores).
17
+ """
18
+
19
+ STATUS_CHOICES = [
20
+ ('active', 'Active'),
21
+ ('paused', 'Paused'),
22
+ ('archived', 'Archived'),
23
+ ('draft', 'Draft'),
24
+ ]
25
+
26
+ EXECUTION_MODE_CHOICES = [
27
+ ('sequential', 'Sequential'),
28
+ ('parallel', 'Parallel'), # Futuro
29
+ ]
30
+
31
+ ON_ERROR_CHOICES = [
32
+ ('stop', 'Stop'),
33
+ ('continue', 'Continue'),
34
+ ('retry', 'Retry'),
35
+ ]
36
+
37
+ # FKs
38
+ company = models.ForeignKey(Company, on_delete=models.CASCADE, db_index=True)
39
+ created_by = models.ForeignKey(
40
+ User,
41
+ on_delete=models.CASCADE,
42
+ related_name='created_automations'
43
+ )
44
+
45
+ # Info básica
46
+ name = models.CharField(max_length=255)
47
+ description = models.TextField(blank=True)
48
+ status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft')
49
+
50
+ # Configuración de ejecución
51
+ execution_mode = models.CharField(
52
+ max_length=20,
53
+ choices=EXECUTION_MODE_CHOICES,
54
+ default='sequential'
55
+ )
56
+ max_executions = models.IntegerField(
57
+ null=True,
58
+ blank=True,
59
+ help_text="Máximo número de ejecuciones (null = ilimitado)"
60
+ )
61
+ execution_count = models.IntegerField(default=0)
62
+
63
+ # Manejo de errores
64
+ on_error = models.CharField(
65
+ max_length=20,
66
+ choices=ON_ERROR_CHOICES,
67
+ default='stop'
68
+ )
69
+ retry_count = models.IntegerField(default=3)
70
+ retry_delay_seconds = models.IntegerField(default=60)
71
+
72
+ # Tracking de ejecución
73
+ last_executed_at = models.DateTimeField(null=True, blank=True)
74
+ next_execution_at = models.DateTimeField(null=True, blank=True, db_index=True)
75
+
76
+ # Metadata
77
+ tags = models.JSONField(default=list, blank=True)
78
+ metadata = models.JSONField(default=dict, blank=True)
79
+
80
+ class Meta:
81
+ db_table = '"automations"."automations"'
82
+ app_label = 'constec_db'
83
+ indexes = [
84
+ models.Index(fields=['company', 'status']),
85
+ models.Index(fields=['status', 'next_execution_at']),
86
+ ]
87
+ ordering = ['-created_at']
88
+
89
+ def __str__(self):
90
+ return f"{self.name} ({self.status})"
91
+
92
+
93
+ class Trigger(UUIDModel):
94
+ """
95
+ Trigger (disparador) que inicia la ejecución de una automatización.
96
+
97
+ Tipos soportados:
98
+ - schedule: Programado por fecha/hora
99
+ - webhook: Disparado por HTTP POST
100
+ - manual: Ejecución manual del usuario
101
+ - event: Disparado por evento del sistema (futuro)
102
+ """
103
+
104
+ TRIGGER_TYPE_CHOICES = [
105
+ ('schedule', 'Schedule'),
106
+ ('webhook', 'Webhook'),
107
+ ('manual', 'Manual'),
108
+ ('event', 'Event'),
109
+ ]
110
+
111
+ # FKs
112
+ automation = models.ForeignKey(
113
+ Automation,
114
+ on_delete=models.CASCADE,
115
+ related_name='triggers'
116
+ )
117
+
118
+ # Info básica
119
+ trigger_type = models.CharField(max_length=20, choices=TRIGGER_TYPE_CHOICES)
120
+ name = models.CharField(max_length=255)
121
+ description = models.TextField(blank=True)
122
+ is_enabled = models.BooleanField(default=True)
123
+
124
+ # Configuración tipo-específica
125
+ config = models.JSONField(
126
+ default=dict,
127
+ help_text="Configuración específica del tipo de trigger (schedule, webhook, etc.)"
128
+ )
129
+
130
+ # Tracking
131
+ last_triggered_at = models.DateTimeField(null=True, blank=True)
132
+ trigger_count = models.IntegerField(default=0)
133
+ priority = models.IntegerField(default=50, help_text="0-100, mayor = más prioridad")
134
+
135
+ class Meta:
136
+ db_table = '"automations"."triggers"'
137
+ app_label = 'constec_db'
138
+ indexes = [
139
+ models.Index(fields=['automation', 'is_enabled']),
140
+ models.Index(fields=['trigger_type', 'is_enabled']),
141
+ ]
142
+ ordering = ['-priority', 'created_at']
143
+
144
+ def __str__(self):
145
+ return f"{self.trigger_type}: {self.name}"
146
+
147
+
148
+ class Action(UUIDModel):
149
+ """
150
+ Acción que se ejecuta como parte de una automatización.
151
+
152
+ Tipos soportados:
153
+ - notification: Enviar notificación (email, SMS, WhatsApp, push)
154
+ - webhook: Ejecutar HTTP request
155
+ - batch_task: Ejecutar tarea batch (futuro)
156
+ - flow_execution: Ejecutar workflow LangGraph (futuro)
157
+ """
158
+
159
+ ACTION_TYPE_CHOICES = [
160
+ ('notification', 'Notification'),
161
+ ('webhook', 'Webhook'),
162
+ ('batch_task', 'Batch Task'),
163
+ ('flow_execution', 'Flow Execution'),
164
+ ]
165
+
166
+ ON_ERROR_CHOICES = [
167
+ ('stop', 'Stop'),
168
+ ('continue', 'Continue'),
169
+ ('retry', 'Retry'),
170
+ ]
171
+
172
+ # FKs
173
+ automation = models.ForeignKey(
174
+ Automation,
175
+ on_delete=models.CASCADE,
176
+ related_name='actions'
177
+ )
178
+
179
+ # Info básica
180
+ action_type = models.CharField(max_length=20, choices=ACTION_TYPE_CHOICES)
181
+ name = models.CharField(max_length=255)
182
+ description = models.TextField(blank=True)
183
+ is_enabled = models.BooleanField(default=True)
184
+ order = models.IntegerField(default=0, help_text="Orden de ejecución")
185
+
186
+ # Configuración tipo-específica
187
+ config = models.JSONField(
188
+ default=dict,
189
+ help_text="Configuración específica del tipo de acción"
190
+ )
191
+
192
+ # Manejo de errores (override de automation)
193
+ on_error = models.CharField(
194
+ max_length=20,
195
+ choices=ON_ERROR_CHOICES,
196
+ null=True,
197
+ blank=True,
198
+ help_text="Override del on_error de la automation"
199
+ )
200
+ retry_count = models.IntegerField(
201
+ null=True,
202
+ blank=True,
203
+ help_text="Override del retry_count de la automation"
204
+ )
205
+
206
+ # Ejecución condicional (futuro)
207
+ condition = models.JSONField(
208
+ null=True,
209
+ blank=True,
210
+ help_text="Condición JSONLogic para ejecutar esta acción"
211
+ )
212
+
213
+ # Tracking
214
+ last_executed_at = models.DateTimeField(null=True, blank=True)
215
+ execution_count = models.IntegerField(default=0)
216
+ success_count = models.IntegerField(default=0)
217
+ failure_count = models.IntegerField(default=0)
218
+
219
+ class Meta:
220
+ db_table = '"automations"."actions"'
221
+ app_label = 'constec_db'
222
+ indexes = [
223
+ models.Index(fields=['automation', 'order']),
224
+ models.Index(fields=['action_type', 'is_enabled']),
225
+ ]
226
+ ordering = ['order', 'created_at']
227
+
228
+ def __str__(self):
229
+ return f"{self.action_type}: {self.name}"
230
+
231
+
232
+ class ExecutionLog(UUIDModel):
233
+ """
234
+ Log de ejecución de una automatización.
235
+
236
+ Registra cada ejecución con su estado, duración, errores y detalles
237
+ de cada acción ejecutada.
238
+ """
239
+
240
+ STATUS_CHOICES = [
241
+ ('pending', 'Pending'),
242
+ ('running', 'Running'),
243
+ ('completed', 'Completed'),
244
+ ('failed', 'Failed'),
245
+ ('partial', 'Partial'), # Algunas acciones fallaron
246
+ ]
247
+
248
+ TRIGGERED_BY_CHOICES = [
249
+ ('schedule', 'Schedule'),
250
+ ('webhook', 'Webhook'),
251
+ ('manual', 'Manual'),
252
+ ('system', 'System'),
253
+ ]
254
+
255
+ # FKs
256
+ automation = models.ForeignKey(
257
+ Automation,
258
+ on_delete=models.CASCADE,
259
+ related_name='execution_logs'
260
+ )
261
+ trigger = models.ForeignKey(
262
+ Trigger,
263
+ on_delete=models.SET_NULL,
264
+ null=True,
265
+ blank=True,
266
+ related_name='execution_logs'
267
+ )
268
+ triggered_by_user = models.ForeignKey(
269
+ User,
270
+ on_delete=models.SET_NULL,
271
+ null=True,
272
+ blank=True,
273
+ related_name='triggered_executions'
274
+ )
275
+
276
+ # Estado
277
+ status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
278
+ triggered_by = models.CharField(max_length=20, choices=TRIGGERED_BY_CHOICES)
279
+
280
+ # Timing
281
+ started_at = models.DateTimeField(auto_now_add=True)
282
+ completed_at = models.DateTimeField(null=True, blank=True)
283
+ duration_seconds = models.IntegerField(null=True, blank=True)
284
+
285
+ # Contexto de ejecución
286
+ execution_context = models.JSONField(
287
+ default=dict,
288
+ help_text="Variables y contexto disponible durante la ejecución"
289
+ )
290
+
291
+ # Estadísticas de acciones
292
+ actions_total = models.IntegerField(default=0)
293
+ actions_succeeded = models.IntegerField(default=0)
294
+ actions_failed = models.IntegerField(default=0)
295
+ actions_skipped = models.IntegerField(default=0)
296
+
297
+ # Log detallado de cada acción
298
+ action_logs = models.JSONField(
299
+ default=list,
300
+ help_text="Array de logs detallados de cada acción ejecutada"
301
+ )
302
+
303
+ # Errores
304
+ error_message = models.TextField(blank=True)
305
+ error_traceback = models.TextField(blank=True)
306
+
307
+ class Meta:
308
+ db_table = '"automations"."execution_logs"'
309
+ app_label = 'constec_db'
310
+ indexes = [
311
+ models.Index(fields=['automation', 'started_at']),
312
+ models.Index(fields=['status', 'started_at']),
313
+ models.Index(fields=['triggered_by', 'started_at']),
314
+ ]
315
+ ordering = ['-started_at']
316
+
317
+ def __str__(self):
318
+ return f"{self.automation.name} - {self.status} ({self.started_at})"
319
+
320
+
321
+ class NotificationTemplate(UUIDModel):
322
+ """
323
+ Template para notificaciones reutilizables.
324
+
325
+ Soporta variables en formato {{variable_name}} que se reemplazan
326
+ en tiempo de ejecución.
327
+ """
328
+
329
+ CHANNEL_CHOICES = [
330
+ ('email', 'Email'),
331
+ ('whatsapp', 'WhatsApp'),
332
+ ('sms', 'SMS'),
333
+ ('push', 'Push Notification'),
334
+ ]
335
+
336
+ # FKs
337
+ company = models.ForeignKey(Company, on_delete=models.CASCADE, db_index=True)
338
+ created_by = models.ForeignKey(
339
+ User,
340
+ on_delete=models.CASCADE,
341
+ related_name='created_templates'
342
+ )
343
+
344
+ # Info básica
345
+ name = models.CharField(max_length=255)
346
+ description = models.TextField(blank=True)
347
+ channel = models.CharField(max_length=20, choices=CHANNEL_CHOICES)
348
+
349
+ # Contenido
350
+ subject = models.CharField(
351
+ max_length=255,
352
+ blank=True,
353
+ help_text="Asunto (solo para email)"
354
+ )
355
+ body = models.TextField(help_text="Cuerpo del mensaje con variables {{variable}}")
356
+
357
+ # Variables esperadas
358
+ variables = models.JSONField(
359
+ default=list,
360
+ help_text="Array de nombres de variables esperadas ['customer_name', 'amount']"
361
+ )
362
+
363
+ # is_active inherited from UUIDModel
364
+
365
+ class Meta:
366
+ db_table = '"automations"."notification_templates"'
367
+ app_label = 'constec_db'
368
+ indexes = [
369
+ models.Index(fields=['company', 'channel', 'is_active']),
370
+ ]
371
+ ordering = ['name']
372
+
373
+ def __str__(self):
374
+ return f"{self.channel}: {self.name}"
375
+
376
+
10
377
  class Event(UUIDModel):
11
378
  """
12
379
  Evento programado con soporte para múltiples tipos y recurrencia flexible.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: constec
3
- Version: 0.4.1
3
+ Version: 0.5.1
4
4
  Summary: Base library for the Constec ecosystem - shared utilities, models, and namespace foundation
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/TpmyCT/constec-python
@@ -6,9 +6,10 @@ constec/db/migrations/0002_module_level.py,sha256=Es27KUOuI_qrlyL8xBTjwsJJHVduAE
6
6
  constec/db/migrations/0003_remove_module_level.py,sha256=3GX-VDLSZqqxaZf6KFtuc7U9nCkQ9EF8pSDxOnCz63g,275
7
7
  constec/db/migrations/0004_rename_entities_company_cuit_idx_entities_company_e2c50f_idx_and_more.py,sha256=TCkutatVjJhVKxiQwp-RLhBlbOo5rtwSV1qQqao1qCI,11874
8
8
  constec/db/migrations/0005_event.py,sha256=kyvEFOAbOJj3daDzOWcIhfXD8gl5EpIHYvRy3880cZc,2892
9
+ constec/db/migrations/0006_automation_trigger_action_executionlog_notificationtemplate.py,sha256=-BqCVpccMElsaYQzJ8fznoEQ8h1TC8M7DLlMEv-Is2c,13298
9
10
  constec/db/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- constec/db/models/__init__.py,sha256=2mRV2cN_UfQLuPwhd93OVg1yCbVuoa6F9GZkXECL8kg,2303
11
- constec/db/models/automation.py,sha256=4mmZfj-SSIOZLYC2BoMhRsKq0DFn4KperkjJNN3Jfuw,3051
11
+ constec/db/models/__init__.py,sha256=tzeMhD6Bxs8Z8dBHEGBeIBm1ZrMKk66TlZEOD92InKU,2463
12
+ constec/db/models/automation.py,sha256=_toRKPIGeTd54aLSnEsRUkCH42md18gXrj-xnnmTv-g,13712
12
13
  constec/db/models/base.py,sha256=Urbz9iyOtSxdTfgtEYbtBLmTiea2QAcLq0L40tAzEq8,736
13
14
  constec/db/models/company.py,sha256=APYsbiNoqQ-73wxFP0jq3AByHNHn3Jjbe_J_iwpD9gM,1047
14
15
  constec/db/models/contact.py,sha256=VJ66YgzCqp09dzYM5IK9rZZV1jqBepJQO8YK28gA98k,2106
@@ -29,8 +30,8 @@ constec/shared/exceptions.py,sha256=8Bih40RWoH0gVhto09mH2ppSQV_drHPnGWITcoD-0J0,
29
30
  constec/utils/__init__.py,sha256=brf-G4UvU-3CK_rKNPTaHwsVsxnoJSbml_QTZJSM7d0,458
30
31
  constec/utils/cuit.py,sha256=dQKGlA4pRQ5DyR-N4BiV8ZsvAle2Vgjif7PU72zHx_A,2753
31
32
  constec/utils/password.py,sha256=XNpTJ9xZQSoZNjXEAnexAEZuYkwW1dFgX4AY-B5Q0gA,1462
32
- constec-0.4.1.dist-info/licenses/LICENSE,sha256=a1R51ONDGq0UQfV-n3ybsNL7EGhcC2sQ1sXvRANaFVI,1064
33
- constec-0.4.1.dist-info/METADATA,sha256=acMzQCbZfW1kjj-QcE8lRadRUQLfsbg-rRGQWk6ZkpU,2954
34
- constec-0.4.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
35
- constec-0.4.1.dist-info/top_level.txt,sha256=bQ9AydOLlthShsr7tA7t7ivbLvlLPdhHOo0BdWgnh_Y,8
36
- constec-0.4.1.dist-info/RECORD,,
33
+ constec-0.5.1.dist-info/licenses/LICENSE,sha256=a1R51ONDGq0UQfV-n3ybsNL7EGhcC2sQ1sXvRANaFVI,1064
34
+ constec-0.5.1.dist-info/METADATA,sha256=QYP8HnKGHkj4CSqXNJJQotIOvKTMw68Fu8EglVUe_iU,2954
35
+ constec-0.5.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
36
+ constec-0.5.1.dist-info/top_level.txt,sha256=bQ9AydOLlthShsr7tA7t7ivbLvlLPdhHOo0BdWgnh_Y,8
37
+ constec-0.5.1.dist-info/RECORD,,