Coverage for backend/django/core/auxiliary/models/SolveCompletionEmail.py: 100%
38 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-05-13 02:47 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-05-13 02:47 +0000
1"""Persistence models for solve-completion email attempts."""
3from django.db import models
5from authentication.user.models import User
6from core.auxiliary.enums.generalEnums import TaskStatus
7from core.managers import AccessControlManager
10class SolveCompletionEmailDeliveryStatus(models.TextChoices):
11 """Lifecycle states for an individual delivery attempt."""
13 PENDING = "pending"
14 SENT = "sent"
15 FAILED = "failed"
16 SKIPPED = "skipped"
19class SolveCompletionEmailOutcome(models.TextChoices):
20 """Template and summary classifications stored with each delivery record."""
22 SINGLE_SUCCESS = "single_success"
23 SINGLE_FAILURE = "single_failure"
24 SINGLE_CANCELLED = "single_cancelled"
25 MULTI_ALL_SUCCEEDED = "multi_all_succeeded"
26 MULTI_MIXED = "multi_mixed"
27 MULTI_ALL_FAILED = "multi_all_failed"
28 MULTI_CANCELLED = "multi_cancelled"
31class SolveCompletionEmail(models.Model):
32 """Tracks solve completion email attempts and prevents duplicate delivery.
34 The record is created before the email is sent so the unique constraint can
35 act as the dedupe boundary when duplicate completion events arrive.
36 """
38 task = models.ForeignKey(
39 "Task",
40 on_delete=models.CASCADE,
41 related_name="solve_completion_emails",
42 )
43 flowsheet = models.ForeignKey(
44 "Flowsheet",
45 on_delete=models.CASCADE,
46 related_name="solve_completion_emails",
47 )
48 scenario = models.ForeignKey(
49 "Scenario",
50 on_delete=models.CASCADE,
51 related_name="solve_completion_emails",
52 null=True,
53 blank=True,
54 )
55 recipient = models.ForeignKey(
56 User,
57 on_delete=models.CASCADE,
58 related_name="solve_completion_emails",
59 )
60 recipient_email = models.EmailField(max_length=255, null=True, blank=True)
61 terminal_status = models.CharField(max_length=32, choices=TaskStatus.choices)
62 outcome_key = models.CharField(max_length=64, choices=SolveCompletionEmailOutcome.choices)
63 is_multi_solve = models.BooleanField(default=False)
64 scheduled_count = models.PositiveIntegerField(default=0)
65 successful_count = models.PositiveIntegerField(default=0)
66 failed_count = models.PositiveIntegerField(default=0)
67 cancelled_count = models.PositiveIntegerField(default=0)
68 delivery_status = models.CharField(
69 max_length=32,
70 choices=SolveCompletionEmailDeliveryStatus.choices,
71 default=SolveCompletionEmailDeliveryStatus.PENDING,
72 )
73 sent_at = models.DateTimeField(null=True, blank=True)
74 error = models.TextField(null=True, blank=True)
75 created_at = models.DateTimeField(auto_now_add=True)
76 updated_at = models.DateTimeField(auto_now=True)
78 objects = AccessControlManager()
80 class Meta:
81 """Django model metadata for audit and dedupe behavior."""
83 constraints = [
84 models.UniqueConstraint(
85 fields=["task", "terminal_status"],
86 name="unique_solve_completion_email_per_terminal_task",
87 )
88 ]