feat: ShopItem + Purchase models + seed tithe-1 / tithe-5 / band-1 + wallet-shop Applet — Chunk 2 of [[project-wallet-shop-expansion]]. ShopItem is the admin-managed catalog: slug, name, description, icon (FA class), badge_text (eg "×5"), price_cents, granted_token_type (any Token type), granted_count, granted_writs (default 0), max_owned (nullable; BAND=1), display_order, active. is_available_for(user) enforces max_owned by comparing user's owned-count of the granted token type. price_display() renders cents → "$1" / "$4.20" for tooltip prose. Purchase is the per-tx audit trail: user + shop_item + stripe_payment_intent_id (unique) + status (PENDING/SUCCEEDED/FAILED/REFUNDED) + amount_cents snapshot + granted_writs snapshot + granted_token_ids JSONField (PKs of minted tokens) + created_at + succeeded_at. fulfill() is idempotent — short-circuits if status==SUCCEEDED + refuses non-PENDING rows so a webhook + sync /shop/confirm racing each other can't double-mint. Schema migration lyric/0008_shopitem_purchase autogenerated. Seed migration lyric/0009_seed_shop_items populates the 3 starting items per locked decisions: tithe-1 ($1 → 1 TITHE + 144 writs, no cap, order=10); tithe-5 ($4 → 5 TITHE + 750 writs, no cap, badge "×5", order=20); band-1 ($20 → 1 BAND + 0 writs, max_owned=1, order=30). Applet migration applets/0011_seed_wallet_shop_applet adds the wallet-shop Applet (context=wallet, 12 cols × 3 rows). Stub _applet-wallet-shop.html lands w. just <section id="id_wallet_shop"> + <h2>Shop</h2>_applets.html's auto-include-by-slug pattern would 500 the wallet page on TemplateDoesNotExist otherwise (caught mid-Chunk-2 by the full app suite). Chunk 4 fills in the shop-tile grid + BUY-ITEM microtooltip + Stripe.js wiring. TDD — 22 ITs in test_shop_models.py: ShopItemModelTest (9 cases — minimal create, defaults for granted_writs / max_owned / active, is_available_for w/ + w/o max_owned cap, str repr), PurchaseModelTest (8 cases — minimal create, PI ID uniqueness constraint, fulfill mints tokens + grants writs + marks SUCCEEDED + records granted_token_ids + is idempotent on re-fire + creates N tokens for bundle), SeededShopCatalogTest (4 cases pin tithe-1 / tithe-5 / band-1 row shapes + display_order ascending), SeededWalletShopAppletTest (1 case pins Applet seeded). 1191 IT/UT green

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-05-22 00:30:59 -04:00
parent 8e476f5658
commit 849ef3c310
6 changed files with 579 additions and 0 deletions

View File

@@ -0,0 +1,43 @@
"""Seed the wallet Shop applet — Chunk 2 of the wallet expansion sprint.
Locked spec from [[project-wallet-shop-expansion]]: 4 rows total in the
wallet context (Shop atop, Balances + Tokens + Payment beneath); 12 cols
in landscape (full-width row). Mimics the existing wallet applets'
grid_cols=12 / grid_rows=3 shape.
`display_order` is NOT a field on Applet — applet ordering is dictated
by the wallet template's include order in `_applets.html` + the applet
slug alphabetical fallback in `applet_context()`. The template's include
order is set in Chunk 4; this migration just ensures the row exists.
"""
from django.db import migrations
def seed(apps, schema_editor):
Applet = apps.get_model("applets", "Applet")
Applet.objects.update_or_create(
slug="wallet-shop",
defaults={
"name": "Shop",
"context": "wallet",
"default_visible": True,
"grid_cols": 12,
"grid_rows": 3,
},
)
def unseed(apps, schema_editor):
Applet = apps.get_model("applets", "Applet")
Applet.objects.filter(slug="wallet-shop").delete()
class Migration(migrations.Migration):
dependencies = [
("applets", "0010_rename_my_sign_applet"),
]
operations = [
migrations.RunPython(seed, unseed),
]