from django.conf import settings from django.db import models from infrasynth.shared.enums import ApprovalStrategy from infrasynth.tenancy.mixins import TenantOwnedModel class Workflow(TenantOwnedModel): slug = models.SlugField(max_length=100) name = models.CharField(max_length=200) description = models.TextField(blank=True) is_active = models.BooleanField(default=True) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="+", ) class Meta: db_table = "workflows_definition" constraints = [ models.UniqueConstraint(fields=["tenant", "slug"], name="uniq_workflow_slug_per_tenant"), ] def __str__(self): return self.name class WorkflowNode(TenantOwnedModel): class NodeType(models.TextChoices): START = "start", "Start" INTERMEDIATE = "intermediate", "Intermediate" END = "end", "End" workflow = models.ForeignKey(Workflow, on_delete=models.CASCADE, related_name="nodes") name = models.CharField(max_length=200) node_type = models.CharField(max_length=20, choices=NodeType.choices, default=NodeType.INTERMEDIATE) min_approvals = models.PositiveSmallIntegerField(default=1) approval_strategy = models.CharField( max_length=20, choices=ApprovalStrategy.choices, default=ApprovalStrategy.ALL, ) position_x = models.IntegerField(default=0) position_y = models.IntegerField(default=0) class Meta: db_table = "workflows_node" constraints = [models.UniqueConstraint(fields=["workflow", "name"], name="uniq_node_name_per_workflow")] def __str__(self): return f"{self.workflow.slug}:{self.name}" class Transition(TenantOwnedModel): from_node = models.ForeignKey(WorkflowNode, on_delete=models.CASCADE, related_name="outgoing_transitions") to_node = models.ForeignKey(WorkflowNode, on_delete=models.CASCADE, related_name="incoming_transitions") condition_slug = models.CharField(max_length=100, blank=True) is_default = models.BooleanField(default=False) class Meta: db_table = "workflows_transition" constraints = [ models.UniqueConstraint(fields=["from_node", "condition_slug"], name="uniq_transition_condition"), ] def __str__(self): return f"{self.from_node} \u2192 {self.to_node}" class WorkflowInstance(TenantOwnedModel): class Status(models.TextChoices): IN_PROGRESS = "in_progress", "In Progress" COMPLETED = "completed", "Completed" CANCELLED = "cancelled", "Cancelled" workflow = models.ForeignKey(Workflow, on_delete=models.CASCADE, related_name="instances") current_node = models.ForeignKey( WorkflowNode, on_delete=models.SET_NULL, null=True, blank=True, related_name="+", ) owner = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="+", ) status = models.CharField(max_length=20, choices=Status.choices, default=Status.IN_PROGRESS) started_at = models.DateTimeField(auto_now_add=True) completed_at = models.DateTimeField(null=True, blank=True) metadata = models.JSONField(default=dict) class Meta: db_table = "workflows_instance" indexes = [models.Index(fields=["tenant_id", "status"])] def __str__(self): return f"{self.workflow.slug}#{self.pk}" class NodeAssignment(TenantOwnedModel): instance = models.ForeignKey(WorkflowInstance, on_delete=models.CASCADE, related_name="assignments") node = models.ForeignKey(WorkflowNode, on_delete=models.CASCADE, related_name="assignments") user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="+") visit_number = models.IntegerField(default=1) is_required = models.BooleanField(default=True) has_processed = models.BooleanField(default=False) decision = models.CharField(max_length=50, null=True, blank=True) comments = models.TextField(blank=True) submitted_data = models.JSONField(default=dict, null=True, blank=True) processed_at = models.DateTimeField(null=True, blank=True) class Meta: db_table = "workflows_node_assignment" indexes = [models.Index(fields=["tenant_id", "has_processed"])] def __str__(self): return f"{self.instance}#{self.node.name}@{self.user}" class WorkflowObserver(TenantOwnedModel): instance = models.ForeignKey(WorkflowInstance, on_delete=models.CASCADE, related_name="observers") user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="+") class Meta: db_table = "workflows_observer" constraints = [models.UniqueConstraint(fields=["instance", "user"], name="uniq_observer_per_instance")] def __str__(self): return f"{self.instance}\u2192{self.user}" class WorkflowAwareModel(TenantOwnedModel): """Abstract mixin for domain models that participate in a workflow.""" workflow_instance = models.ForeignKey( WorkflowInstance, on_delete=models.SET_NULL, null=True, blank=True, related_name="+", ) class Meta: abstract = True