Files
python-tdd/src/apps/dashboard
Disco DeDisco 750fef890e A.4 cont.: card-deck icon placeholder mode (no-deck-equipped) styled like empty dice slot — TDD. User polish 2026-05-25 PM after browser-verifying the kit-bag dialog: when no deck is equipped (kit-bag-placeholder branch fires) the new card-stack icon should not animate + should render w. the same dimmed rgba(--quaUser, 0.x) palette as the existing fa-dice empty slot next to it, not the bright --terUser stroke / --priUser fill of an equipped-deck icon. Two SCSS changes: (1) Removed .deck-stack-icon:hover + .deck-stack-icon:active from the splay-trigger selector list — only wrapper-based selectors (.token.deck-variant, .kit-bag-deck) fire the fan-out now. Placeholder icons land inside .kit-bag-placeholder (or game_kit applet's .kit-item empty-state) which aren't in the trigger list, so they stay static no matter where the user hovers. (2) New .kit-bag-placeholder .deck-stack-icon + .kit-item .deck-stack-icon descendant-selector rule: drops color (= stroke via currentColor) to rgba(--quaUser, 0.3) matching the existing .kit-bag-placeholder color (_game-kit.scss:143); drops .deck-stack-icon__card fill to rgba(--quaUser, 0.15) (lower than the stroke alpha — user-dialed 2026-05-25 PM for a subtler look, the cards read as faintly-present "absence" rather than bright-but-grey). 1 new IT in KitBagViewTest (clears equipped_deck → asserts .kit-bag-deck absent, .kit-bag-placeholder present + carries svg.deck-stack-icon + lacks fa-id-badge). Tests: 1 new green; 1298/1298 IT+UT total green (71s; +1 from d26c45b's 1297). Visual verify pending: refresh /gameboard/ + clear equipped deck → kit-bag dialog Deck slot should show a dimmed static card-stack icon matching the dice slot's washed-out look
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 01:07:24 -04:00
..
feat: wallet Shop polish — microtooltip extraction, Shop-first ordering, DRY tooltip styling, writs rebalance, "no expiry" on all items. Visual-pass tweaks landing atop the 5-chunk Shop rollout (commits 8e476f5d28cf7b). **Microtooltip extraction**: .tt-microbutton-portal (Chunk 4's wrap-inside-.tt) replaced w. a sibling .tt-micro div on each .shop-tile. wallet.js's initWalletTooltips clones BOTH into separate portals on hover — .tt#id_tooltip_portal (main card), .tt-micro#id_mini_tooltip_portal (small italic pill at bottom-right of main, mirroring Game Kit's Equipped/Unequipped/In-Use mini portal). Hover persistence covers both portals + the source tile w. a 200ms grace timer cancelled by mouseenter on any of the 3 zones. Capped items (BAND-owned) render NO btn at all — just "Already owned" microtext (mirrors Game Kit's status-only "Equipped" pill rather than the disabled-× pattern that lived in Chunk 4). **Tooltip-pin on guard open**: WalletTooltips.pin() / .unpin() exposed on window; wallet-shop.js's BUY click calls pin() before showGuard() + both onConfirm / onDismiss callbacks call unpin() → the item tooltip stays visible behind the guard's "Buy {name} for ${price}?" prompt instead of orphaning. **Shop-first applet ordering**: new Applet.display_order field (default 100, lower = earlier; PK tie-break preserves legacy insertion-order for the existing 3 applets); seed migration sets wallet-shop.display_order=10 so Shop renders atop Balances/Tokens/Payment. applet_context() updated to .order_by("display_order", "pk"). New WalletAppletOrderTest (2 ITs) pins Shop-first DOM order + view-context list. **DRY tooltip styling**: shop tooltip now uses the same 4-slot .tt-title / .tt-description / .tt-shoptalk / .tt-expiry classes as the Tokens row. New ShopItem.shoptalk field for the italic flavor line (band-1 = "Unlimited free entry (BYOB)" split out of description; tithes blank). New ShopItem.tooltip_expiry() method returns "no expiry" — eternal-stock convention (all current items; seasonal listings could override later). **Writs rebalance**: locked 2026-05-22 — tithe-1 144→12 writs, tithe-5 750→60 writs. Description text updated in lockstep ("1 Tithe Token + 12 Writs" / "5 Tithe Tokens + 60 Writs"). **Badge tweak**: ×N badge shrunk 2rem → 1.5rem + nudged further off-tile (top: -0.7rem, right: -1rem) so most of the underlying icon stays visible. **SCSS**: .tt-micro hidden in source DOM (portal-only); #id_mini_tooltip_portal mostly mirrors gameboard's mini at _gameboard.scss:140 but allows BUY-btn label to wrap onto multiple lines (white-space: normal on .tt-buy-btn); .tt-already-owned styled w. --secUser italic at 0.85rem to match Game Kit pills. **Migrations** — 5 new: lyric/0010_repricing_tithe_writs (writs + description), lyric/0011_shopitem_shoptalk (schema), lyric/0012_seed_shop_shoptalk (band split), applets/0012_applet_display_order (schema), applets/0013_wallet_shop_display_order (Shop atop). All idempotent. **TDD** — 5 new ITs across test_shop_models.py (shoptalk default + per-item assertions, tooltip_expiry method, updated tithe writs values, WalletAppletOrderTest), 1 new FT (test_shop_buy_guard_portal_pins_item_tooltip — programmatically dispatches mouseenter/mouseleave to exercise the pin/unpin race), 3 new Jasmine specs (T6 pin-on-click, T7 unpin-on-confirm, T8 unpin-on-dismiss). Existing FT band-owned assertion switched to .tt-micro (no .tt-buy-btn present), Jasmine T2 rewritten to assert no btn renders. **3 traps caught** mid-build: (a) multi-line {# #} comment leaked into DOM again (cf [[feedback-django-comments-single-line-only]]) — pinned the trap; (b) spyOn(window, 'fetch') Jasmine double-spy collision (cf trapped previously); (c) async pollution where afterEach restores window.Stripe=undefined before _doBuy's continuation hits it — fixed by per-test never-resolving fetch mock. 1211 IT/UT + 9 wallet FTs green; Jasmine SpecRunner verified visually (FT hangs Selenium-side on spec count). Pipeline will sweep all FTs
2026-05-22 02:21:10 -04:00
A.4 cont.: card-deck icon placeholder mode (no-deck-equipped) styled like empty dice slot — TDD. User polish 2026-05-25 PM after browser-verifying the kit-bag dialog: when no deck is equipped (kit-bag-placeholder branch fires) the new card-stack icon should not animate + should render w. the same dimmed rgba(--quaUser, 0.x) palette as the existing fa-dice empty slot next to it, not the bright --terUser stroke / --priUser fill of an equipped-deck icon. Two SCSS changes: (1) Removed .deck-stack-icon:hover + .deck-stack-icon:active from the splay-trigger selector list — only wrapper-based selectors (.token.deck-variant, .kit-bag-deck) fire the fan-out now. Placeholder icons land inside .kit-bag-placeholder (or game_kit applet's .kit-item empty-state) which aren't in the trigger list, so they stay static no matter where the user hovers. (2) New .kit-bag-placeholder .deck-stack-icon + .kit-item .deck-stack-icon descendant-selector rule: drops color (= stroke via currentColor) to rgba(--quaUser, 0.3) matching the existing .kit-bag-placeholder color (_game-kit.scss:143); drops .deck-stack-icon__card fill to rgba(--quaUser, 0.15) (lower than the stroke alpha — user-dialed 2026-05-25 PM for a subtler look, the cards read as faintly-present "absence" rather than bright-but-grey). 1 new IT in KitBagViewTest (clears equipped_deck → asserts .kit-bag-deck absent, .kit-bag-placeholder present + carries svg.deck-stack-icon + lacks fa-id-badge). Tests: 1 new green; 1298/1298 IT+UT total green (71s; +1 from d26c45b's 1297). Visual verify pending: refresh /gameboard/ + clear equipped deck → kit-bag dialog Deck slot should show a dimmed static card-stack icon matching the dice slot's washed-out look
2026-05-25 01:07:24 -04:00
feat: shop PaymentIntent flow — shop_buy + shop_confirm + stripe_webhook — Chunk 3 of [[project-wallet-shop-expansion]]. Three-endpoint split per the locked Stripe design: webhook is authoritative for fulfillment (resilient to 3DS, browser closes, network drops); sync /shop/confirm is a best-effort UX speedup (fulfills immediately when Stripe.js confirms client-side, no waiting for webhook delivery); both call Purchase.fulfill() which is idempotent — whichever lands first wins, the other becomes a no-op via the status==SUCCEEDED guard. **POST /dashboard/wallet/shop/buy** (form-encoded shop_item_slug): looks up active ShopItem (404 if missing/inactive); enforces max_owned via is_available_for(user) (409 if cap hit, eg already-owned BAND); requires a saved PaymentMethod (402 otherwise — picks most-recent via order_by('-pk').first() per the open-Q note in the scope doc); creates Stripe PaymentIntent (amount=item.price_cents, currency=usd, customer=user.stripe_customer_id, payment_method=pm.stripe_pm_id, automatic_payment_methods={enabled, allow_redirects=never} for in-window 3DS); creates Purchase w. pi.id; backfills pi.metadata.purchase_id via PaymentIntent.modify so the webhook handler can resolve back to the row; returns {client_secret, purchase_id} JSON for Stripe.js confirmCardPayment. **POST /dashboard/wallet/shop/confirm** (form-encoded purchase_id): retrieves PI from Stripe, if status=='succeeded' calls purchase.fulfill(); returns {status} JSON. 404 if the purchase doesn't belong to request.user. Idempotent — re-firing after fulfill is a safe no-op. **POST /stripe/webhook** (csrf_exempt, mounted at root /stripe/webhook so the URL stays stable across app-routing refactors w. Stripe's dashboard config): verifies signature via stripe.Webhook.construct_event against STRIPE_WEBHOOK_SECRET env var (400 on mismatch — Stripe won't retry on 4xx, only 5xx); on payment_intent.succeeded looks up Purchase by metadata.purchase_id w. fall-back to stripe_payment_intent_id (both unique). Unknown event types are no-op 200 (Stripe sends charge.dispute.created etc. + would retry indefinitely on 5xx). New STRIPE_WEBHOOK_SECRET = os.environ.get(...) setting; user swaps it on staging+prod per the live-mode env-var-only decision. TDD — 17 ITs in test_shop_views.py across 3 classes: ShopBuyViewTest (7 cases — login required, success path creates PI + Purchase w. correct shape, PI.create called w. correct args, unknown slug 404, inactive item 404, max_owned 409, no PM 402); ShopConfirmViewTest (5 cases — login required, succeeded PI triggers fulfill, processing PI leaves PENDING, idempotent on already-SUCCEEDED, other user's purchase 404); StripeWebhookViewTest (5 cases — sig mismatch 400, succeeded event triggers fulfill, unknown event type 2xx no-op, duplicate delivery idempotent, unknown purchase_id 2xx no-op). All Stripe API calls mocked via mock.patch('apps.dashboard.views.stripe'). 1208 IT/UT green
2026-05-22 00:42:09 -04:00
feat: wallet Shop applet — tile grid + BUY-ITEM microbutton + Stripe.js wiring — Chunk 4 of [[project-wallet-shop-expansion]]. The shop applet (slug wallet-shop, seeded in Chunk 2) now renders the catalog as a horizontal grid of .shop-tile icons: tithe-1 ($1, fa-piggy-bank), tithe-5 ($4, fa-piggy-bank w. ×5 badge), band-1 ($20, fa-ring). Each tile hosts a hover-portaled tooltip carrying name + description + price + a .tt-microbutton-portal w. a .btn-primary BUY ITEM button — clicking opens #id_guard_portal w. "Buy {name} for ${price}?" prompt; confirming triggers Stripe.js confirmCardPayment then POSTs to /shop/confirm + reloads. Items where the user's owned-count has hit max_owned (eg. BAND, owned=1, cap=1) render w. .btn-disabled + × glyph + "Already owned" microtooltip text — visible-but-unbuyable per the locked decision. View context — wallet view + toggle_wallet_applets view both pass shop_items (decorated w. per-user .available via the new _shop_items_for(user) helper) + default_payment_method_id + stripe_publishable_key. SCSS — .wallet-shop (flex column wrapping .shop-grid flex row), .shop-tile (inline-flex tooltip target), .shop-badge (2rem circle, --quaUser glyph on --quiUser bg, top-right corner per spec), .tt-microbutton-portal (column-flex, BUY btn + 'Already owned' caption styling). JS in wallet-shop.js exposes a singleton WalletShop module (matching the project's Brief / SeaDeal / StageCard module pattern) w. a tested initWalletShop() method — uses event delegation on the shop root (so portal-relocated buy btns still hit the handler) + a DOM-keyed data-shop-wired flag (not a module-level boolean) so per-test fixture rebuilds re-wire cleanly. Wired into wallet.html after wallet.js. **TDD** — 5 Jasmine specs in WalletShopSpec.js: T1 click-on-enabled-BUY opens guard w. correct prompt; T2 click-on-disabled-BUY no-op; T3 onConfirm POSTs shop_item_slug to /shop/buy; T4 init idempotent (calling twice doesn't double-wire); T5 missing-root no-throw. **2 Jasmine traps caught**: (a) spyOn(window, 'fetch') collides if another spec already spied on fetch — switched to save+restore via per-test _origFetch capture; (b) T3 async pollution — sync assertion passed, afterEach restored window.Stripe=undefined, then _doBuy's async continuation hit Stripe(pubKey) and threw "Unhandled promise rejection". Fixed by T3-local fetch mock returning a never-resolving promise so the chain pauses at the first await. **3 new FTs** in test_dash_wallet.py: tiles + icons + ×5 badge + tooltip prose; BUY click opens guard portal + NVM dismisses; BAND-already-owned shows disabled BUY w. 'Already owned' microtext (reads via textContent since .tt is display: none). FT trap caught: TransactionTestCase wipes both migration-seeded Applets + ShopItems → setUp must re-seed both manually (mirrors test_shop_views.py's _seed_starting_items pattern). 1208 IT/UT + 9 wallet FTs + 5 Jasmine specs green
2026-05-22 01:15:05 -04:00