Anchor DLC purchases to a base game instead of a parent purchase

Add-on purchases (DLC, Season Pass, Battle Pass) previously linked to a
parent *purchase* via the `related_purchase` self-FK. When the base game
was bought inside a multi-game purchase (e.g. a bundle), there was no
per-game purchase to point at — only the whole bundle.

Replace it with a `related_game` FK (Game -> Game): an add-on belongs to
a *game*, which is unambiguous regardless of how the base game was bought.

- models: drop `related_purchase`; add `related_game`
  (SET_NULL, related_name="addon_purchases"); require it for non-GAME
  types in `save()`.
- forms: replace the parent-purchase picker with a flat `related_game`
  game search (reusing SearchSelectWidget/_game_options); drop the now
  unused related_purchase_queryset/RelatedPurchaseChoiceField.
- views/urls: remove the obsolete related_purchase_by_game endpoint.
- add_purchase.js: drop the parent-dropdown refetch; keep platform
  auto-fill; retarget the type toggle to #id_related_game.
- migration 0020: add -> backfill (related_game = parent's first game by
  sort_name) -> remove related_purchase.
- tests: model validation unit tests + an e2e test for the flat picker.

related_game is deliberately game->game so it can later be synced from
IGDB's parent_game without schema changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 23:17:27 +02:00
parent 62f0c6c261
commit dfccfbff51
9 changed files with 130 additions and 86 deletions
+6 -5
View File
@@ -198,12 +198,13 @@ class Purchase(models.Model):
)
type = models.CharField(max_length=255, choices=TYPES, default=GAME)
name = models.CharField(max_length=255, blank=True, default="")
related_purchase = models.ForeignKey(
"self",
related_game = models.ForeignKey(
Game,
on_delete=models.SET_NULL,
default=None,
null=True,
related_name="related_purchases",
blank=True,
related_name="addon_purchases",
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@@ -252,9 +253,9 @@ class Purchase(models.Model):
self.save()
def save(self, *args, **kwargs):
if self.type != Purchase.GAME and not self.related_purchase:
if self.type != Purchase.GAME and not self.related_game:
raise ValidationError(
f"{self.get_type_display()} must have a related purchase."
f"{self.get_type_display()} must have a related game."
)
super().save(*args, **kwargs)