infrasynth-backend-kit/infrasynth/workflows/models.py
jcv-dev 551b42eab5 feat: production-hardening pass across the kit
Close the gaps between the documented contract (API-STANDARD, TENANCY,
ENTITLEMENTS) and the implementation, and remove committed build artifacts.

Security:
- verify + process inbound webhooks (HMAC/handler verify, size limit,
  timestamp tolerance, idempotency via InboundEvent.external_id)
- real 2FA login flow (pre-auth challenge; tokens only after verify/recovery)
- wire HybridPermission into security/audit views; add API-key rotate and
  users/<id>/permissions|roles endpoints
- tenant-scoped throttling on by default; webhook replay protection
- verify MercadoPago webhook signatures
- login brute-force guard, configurable password policy, real ALTCHA PoW

Correctness:
- apply verified billing webhooks idempotently (subscription/entitlement/
  invoice/PaymentTransaction); scheduled payment lifecycle jobs
- capture audit update diffs automatically; add audit retention purge
- working notification retries, per-channel rate limits, log retention
- pluggable virus scanner, upload-size limit, pipeline toggle
- feature rollout %/environment targeting; settings-driven registrations
- workflow guards (instance cap, route depth, self-assignment, clone on re-entry)
- wire every previously-dead INFRASYNTH_* setting; drop truly dead ones

Delivery:
- README + CHANGELOG; CI format check + coverage gate
- keep test media out of the tree; untrack .coverage, __pycache__,
  egg-info, docs/ and invoice artifacts
2026-09-24 10:41:21 -05:00

151 lines
5.3 KiB
Python

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