wallet shop: free ($0) RWS + Fiorentine decks — FREE ITEM claim unlocks to Game Kit — TDD
- model: DeckVariant.free_in_shop flag (0015 schema); data migration 0016
seeds RWS + Minchiate Fiorentine True (Earthman stays False — it's auto-
granted at signup, not shopped)
- view: _free_decks_for decorates the free-in-shop catalog w. a per-user
.owned flag; shop_claim_free POST endpoint adds the deck to unlocked_decks
(idempotent M2M add) — the free_in_shop filter is the guard that stops the
$0 endpoint unlocking paid/auto-granted decks (404 otherwise). free_decks
wired into both the wallet view + toggle_wallet_applets HX context
- url: wallet/shop/claim (action, no trailing slash)
- template: free-deck tiles reuse the deck's own Game Kit tooltip prose
(name / card-count / description / stock-version line) + a $0 .tt-price
pinned top-right like paid tiles; .tt-micro carries .tt-free-btn (FREE
ITEM) or the same .tt-already-owned pill once owned; reuses
_deck_stack_icon.html
- js: wallet-shop.js _onFreeClick → _doClaimFree POSTs deck_slug → reload
(server-rendered owned pill, same posture as the BUY reload). No guard
portal — free = one-click. Rides the SAME delegated roots as BUY +
idempotent wiring
- css: FREE ITEM wraps to 2 lines like BUY ITEM (extend the mini-portal
.tt-buy-btn white-space:normal rule to .tt-free-btn); shop deck tiles get
the Game Kit fan-out on hover/active by adding .shop-tile-deck to the
.deck-stack-icon splay trigger list — DRY, no transform duplication
- tests: 8 ITs (shop_claim_free behaviors + free_decks context owned flag);
FT claims RWS → 'Already owned' swap → id_kit_tarot_deck appears in Game
Kit; 3 Jasmine specs F1-F3 (claim POST / no-guard / idempotent wiring);
679 dashboard+epic green, no regressions
- trap: hover-hidden microtooltip btn → .text is '' under Selenium; read
get_attribute('textContent') instead [[feedback-selenium-opacity-zero]]
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
18
src/apps/epic/migrations/0015_deckvariant_free_in_shop.py
Normal file
18
src/apps/epic/migrations/0015_deckvariant_free_in_shop.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 6.0 on 2026-05-30 18:39
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('epic', '0014_rws_has_card_images_true'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='deckvariant',
|
||||
name='free_in_shop',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
31
src/apps/epic/migrations/0016_seed_free_in_shop_decks.py
Normal file
31
src/apps/epic/migrations/0016_seed_free_in_shop_decks.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""Mark RWS + Minchiate Fiorentine as free-in-shop ($0) decks.
|
||||
|
||||
These two stock decks are offered free in the wallet Shop applet — a one-
|
||||
click claim adds them to a user's `unlocked_decks` (no Stripe). Earthman is
|
||||
deliberately left False: it's auto-granted at signup, not shopped.
|
||||
"""
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
FREE_SLUGS = ("tarot-rider-waite-smith", "minchiate-fiorentine-1860-1890")
|
||||
|
||||
|
||||
def set_free_in_shop(apps, schema_editor):
|
||||
DeckVariant = apps.get_model("epic", "DeckVariant")
|
||||
DeckVariant.objects.filter(slug__in=FREE_SLUGS).update(free_in_shop=True)
|
||||
|
||||
|
||||
def unset_free_in_shop(apps, schema_editor):
|
||||
DeckVariant = apps.get_model("epic", "DeckVariant")
|
||||
DeckVariant.objects.filter(slug__in=FREE_SLUGS).update(free_in_shop=False)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("epic", "0015_deckvariant_free_in_shop"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(set_free_in_shop, unset_free_in_shop),
|
||||
]
|
||||
@@ -258,6 +258,11 @@ class DeckVariant(models.Model):
|
||||
family = models.CharField(max_length=10, choices=FAMILY_CHOICES, default=EARTHMAN)
|
||||
has_card_images = models.BooleanField(default=True)
|
||||
is_polarized = models.BooleanField(default=False)
|
||||
# When True, this deck is offered FREE ($0) in the wallet Shop applet —
|
||||
# a one-click claim adds it to the user's `unlocked_decks` (no Stripe).
|
||||
# Seeded True for RWS + Minchiate Fiorentine; Earthman stays False (it's
|
||||
# auto-granted at signup, not shopped). See migration 0016.
|
||||
free_in_shop = models.BooleanField(default=False)
|
||||
|
||||
@property
|
||||
def variant_dir_slug(self):
|
||||
|
||||
Reference in New Issue
Block a user