# apps/progression/models.py import uuid from django.conf import settings from django.db import models class TaskCompletion(models.Model): class Status(models.TextChoices): PENDING = "pending", "En attente" ACCEPTED = "accepted", "Acceptée" REJECTED = "rejected", "Refusée" CANCELLED = "cancelled", "Annulée" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) player = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="task_completions", ) task = models.ForeignKey( "skills.Task", on_delete=models.PROTECT, related_name="completions", ) status = models.CharField( max_length=16, choices=Status.choices, default=Status.ACCEPTED, ) completed_at = models.DateTimeField() submitted_at = models.DateTimeField(auto_now_add=True) note = models.TextField(blank=True) proof_url = models.URLField(blank=True) # Évite les doubles créations lors d’un retry de l’API. idempotency_key = models.CharField( max_length=100, null=True, blank=True, ) # Snapshot au moment de la validation task_name_snapshot = models.CharField(max_length=160) xp_snapshot = models.PositiveIntegerField(default=0) validated_by = models.ForeignKey( settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL, related_name="validated_completions", ) validated_at = models.DateTimeField(null=True, blank=True) class Meta: constraints = [ models.UniqueConstraint( fields=("player", "idempotency_key"), condition=models.Q(idempotency_key__isnull=False), name="unique_player_idempotency_key", ) ] indexes = [ models.Index(fields=("player", "-completed_at")), models.Index(fields=("task", "-completed_at")), ] class XPTransaction(models.Model): class Reason(models.TextChoices): TASK = "task", "Réalisation d’une tâche" ACHIEVEMENT = "achievement", "Succès" BONUS = "bonus", "Bonus" CORRECTION = "correction", "Correction" REVERSAL = "reversal", "Annulation" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) player = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="xp_transactions", ) skill = models.ForeignKey( "skills.Skill", on_delete=models.PROTECT, related_name="xp_transactions", ) # Signé : positif ou négatif amount = models.IntegerField() reason = models.CharField(max_length=20, choices=Reason.choices) completion = models.ForeignKey( TaskCompletion, null=True, blank=True, on_delete=models.PROTECT, related_name="xp_transactions", ) reversed_transaction = models.OneToOneField( "self", null=True, blank=True, on_delete=models.PROTECT, related_name="reversal", ) metadata = models.JSONField(default=dict, blank=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: indexes = [ models.Index(fields=("player", "skill")), models.Index(fields=("player", "-created_at")), ] class PlayerSkillProgress(models.Model): player = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="skill_progress", ) skill = models.ForeignKey( "skills.Skill", on_delete=models.CASCADE, related_name="player_progress", ) xp = models.PositiveBigIntegerField(default=0) level = models.PositiveIntegerField(default=0) unlocked_at = models.DateTimeField(null=True, blank=True) updated_at = models.DateTimeField(auto_now=True) class Meta: constraints = [ models.UniqueConstraint( fields=("player", "skill"), name="unique_player_skill_progress", ) ]