test: add gift card integration tests; update docs and CLAUDE.md
7 integration tests for gift card models, cart integration, deduction, transactions, coupon+gift card combo, validation, and JSON purchases. Update architecture.md (feature toggle matrix, ticket index, diagram). Update design-system.md (gift card component section). Add post-work chores checklist to CLAUDE.md.
This commit is contained in:
parent
4935e41448
commit
46c1b2d521
4 changed files with 295 additions and 0 deletions
18
CLAUDE.md
18
CLAUDE.md
|
|
@ -69,6 +69,12 @@ This project uses a Makefile for most development operations. Use `make` command
|
|||
- `make_post_sell/views/cart.py` - Cart and checkout logic
|
||||
- `development.ini` - Configuration file
|
||||
|
||||
### Design System Files
|
||||
- `static/css/tokens.css` — Design tokens (colors, typography, spacing, shape, elevation, motion, z-index), base resets, utility classes, animations. Single source of truth. Light mode `:root`, dark mode `[data-theme="dark"]`.
|
||||
- `static/css/common.css` — Component styles consuming tokens via `var(--token, fallback)`.
|
||||
- `templates/styleguide.j2` — Live component reference at `/styleguide` (view: `views/misc.py:23`).
|
||||
- `docs/design-system.md` — Full design system reference doc (token tables, architecture diagram, conventions).
|
||||
|
||||
## Testing Notes
|
||||
|
||||
The project uses pytest with unittest framework. There are three types of tests:
|
||||
|
|
@ -232,6 +238,8 @@ Always use `uuid_str` when you need a string copy of the identifier. Models inhe
|
|||
|
||||
**CSS LAYOUT REQUIREMENTS**: This project uses CSS Grid exclusively for layout. NEVER use Flexbox (flex) for layout. Always use CSS Grid properties for positioning and alignment.
|
||||
|
||||
**DESIGN TOKENS**: All new styles must consume tokens from `tokens.css` — never hardcode colors, spacing, radii, shadows, or font sizes. Use `var(--token-name)` or `var(--token-name, fallback)`. The token scale uses a 4px spacing base and major third (1.250) type scale.
|
||||
|
||||
**STYLEGUIDE**: When creating new UI components (buttons, wells, alerts, layout patterns, etc.), add a live example to `/styleguide` (`make_post_sell/templates/styleguide.j2`). The styleguide is the single source of truth for the component library. If it's not in the styleguide, it doesn't exist as a pattern.
|
||||
|
||||
**CSS MEDIA SIZING**: Never combine `width: 100%` with `max-height` on media elements (img, video). `width: 100%` forces the element to span the full container even when `max-height` constrains the rendered content, creating dead whitespace. Use `width: auto` + `max-width: 100%` + `max-height` instead — the element shrinks to match the actual content aspect ratio within both constraints.
|
||||
|
|
@ -254,6 +262,16 @@ Disabling or removing tests weakens the codebase and is unacceptable. Tests are
|
|||
|
||||
**AUTO-PUSH**: When you write new tests to cover new code paths and the full test suite passes, commit and push without asking. Bump GIT_HASH after pushing.
|
||||
|
||||
## Post-Work Chores
|
||||
|
||||
After completing a feature or significant change, always perform these chores before considering the work done:
|
||||
|
||||
1. **Tests** — Write unit tests (`test_models.py`), integration tests (`test_integration.py`), and functional tests (`test_functional.py`) covering the new code paths. All three layers are required for new features.
|
||||
2. **Docs** — Update `docs/architecture.md` (feature toggle matrix, ticket index, diagrams) and `docs/design-system.md` (new components/sections) to reflect the change.
|
||||
3. **Portal** — Update the marketing site at `~/git/www.makepostsell.com` (feature cards in `index.html`, includes list in `pricing.html`) when a user-facing feature is added.
|
||||
4. **CLAUDE.md** — Update this file if the change introduces new patterns, form sections, model columns, or conventions that future work needs to know about.
|
||||
5. **Commit & push** — Per AUTO-PUSH, commit and push when tests pass. Bump GIT_HASH.
|
||||
|
||||
## Commit Message Guidelines
|
||||
|
||||
**CRITICAL**: Do not include Claude Code attribution in commit messages. Attributing human work to Claude is inappropriate and misrepresents the actual authorship of the code. All code changes should be attributed to the human developer who reviewed, approved, and committed the work.
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@
|
|||
│ models │ │ media │ │ Stripe │ │ │
|
||||
│ sessions│ │ thumbs │ │ PayPal │ │ Presigned │
|
||||
│ signals │ │ assets │ │ Crypto │ │ URLs only │
|
||||
│ │ │ │ │ Gift Cards │ │ │
|
||||
└─────────┘ └────────────┘ └──────────────┘ └───────────────┘
|
||||
```
|
||||
|
||||
|
|
@ -185,6 +186,7 @@ mps_page_session (raw rows)
|
|||
| Stripe | `shop.stripe_enabled` | `stripe-settings` | On |
|
||||
| PayPal | `shop.paypal_*` | `paypal-settings` | Off |
|
||||
| Crypto | `shop.monero_*` / `shop.dogecoin_*` | `crypto-settings` | Off |
|
||||
| Gift cards | `shop.gift_card_enabled` | `gift-card-settings` | Off |
|
||||
| S3 mirror | `shop.mirror_s3_*` | `mirror-settings` | Off |
|
||||
| Discovery ring | `shop.discovery_ring` | Automatic | Auto-computed |
|
||||
| Subscriptions | `shop.subscription_*` | `ribbon-settings` | Off |
|
||||
|
|
@ -203,6 +205,10 @@ mps_page_session (raw rows)
|
|||
| [MPS-7](tickets/mps-7.md) | Sandbox Mode — Creative Filter System | Complete |
|
||||
| [MPS-8](tickets/mps-8.md) | User S3 Bucket + Artifact Storage | Complete |
|
||||
| [MPS-9](tickets/mps-9.md) | Shop S3 Mirror Bucket | Complete |
|
||||
| [MPS-10](tickets/mps-10.md) | Gift Card — Models & Migration | Complete |
|
||||
| [MPS-11](tickets/mps-11.md) | Gift Card — Purchase Flow | Complete |
|
||||
| [MPS-12](tickets/mps-12.md) | Gift Card — Redemption at Checkout | Complete |
|
||||
| [MPS-13](tickets/mps-13.md) | Gift Card — Shop Admin & Settings | Complete |
|
||||
|
||||
## Related Docs
|
||||
|
||||
|
|
|
|||
|
|
@ -234,6 +234,7 @@ All components are documented with live examples at `/styleguide`. The styleguid
|
|||
| Status | `#status` | Status indicators |
|
||||
| Product Cards | `#cards` | Product grid cards |
|
||||
| Cart | `#cart` | Cart and checkout components |
|
||||
| Gift Cards | `#gift-cards` | Gift card purchase, balance check, management |
|
||||
| Comments | `#comments` | Comment form and list |
|
||||
| Toggle | `#toggle` | Toggle switches |
|
||||
| Ribbon | `#ribbon` | Shop ribbon banner |
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@ from ..models.price import Price
|
|||
from ..models.crypto_payment import CryptoPayment
|
||||
from ..models.user_crypto_refund_address import UserCryptoRefundAddress
|
||||
from ..models.shop_location import ShopLocation
|
||||
from ..models.gift_card import GiftCard, get_gift_card_by_id, get_gift_card_by_code, get_gift_cards_by_shop
|
||||
from ..models.gift_card_transaction import GiftCardTransaction
|
||||
from ..models.cart_gift_card import CartGiftCard
|
||||
import json
|
||||
import time
|
||||
|
||||
|
||||
|
|
@ -3419,3 +3423,269 @@ class TestKaraokeTrackAclIntegration(DatabaseIntegrationTests):
|
|||
product_acl,
|
||||
f"vocals ACL mismatch at visibility={vis}",
|
||||
)
|
||||
|
||||
|
||||
class TestGiftCardIntegration(DatabaseIntegrationTests):
|
||||
"""Integration tests for gift card functionality with real ORM objects."""
|
||||
|
||||
def _make_shop(self, gift_card_enabled=True):
|
||||
"""Helper to create a shop with gift card support."""
|
||||
shop = Shop(
|
||||
name="Gift Card Shop",
|
||||
phone_number="555-555-5555",
|
||||
billing_address="123 Test St",
|
||||
description="A shop with gift cards",
|
||||
)
|
||||
shop.stripe_public_api_key = "pk_test_123"
|
||||
shop.stripe_secret_api_key = "sk_test_123"
|
||||
shop.domain_name = "giftcards.test.com"
|
||||
shop.gift_card_enabled = gift_card_enabled
|
||||
self.dbsession.add(shop)
|
||||
self.dbsession.flush()
|
||||
return shop
|
||||
|
||||
def _make_product(self, shop, price_in_cents=1000):
|
||||
"""Helper to create a product."""
|
||||
product = Product(title="Test Product", description="Test product")
|
||||
product.shop_id = shop.id
|
||||
product.price_in_cents = price_in_cents
|
||||
product.is_physical = False
|
||||
self.dbsession.add(product)
|
||||
self.dbsession.flush()
|
||||
return product
|
||||
|
||||
def _make_user(self):
|
||||
"""Helper to create a user."""
|
||||
user = get_or_create_user_by_email(self.dbsession, "test@example.com")
|
||||
self.dbsession.add(user)
|
||||
self.dbsession.flush()
|
||||
return user
|
||||
|
||||
def _make_cart(self, user, shop):
|
||||
"""Helper to create a cart."""
|
||||
cart = Cart(user=user)
|
||||
cart.shop = shop
|
||||
self.dbsession.add(cart)
|
||||
self.dbsession.flush()
|
||||
return cart
|
||||
|
||||
def test_gift_card_with_real_shop_integration(self):
|
||||
"""Test gift card creation and lookup with real shop."""
|
||||
shop = self._make_shop(gift_card_enabled=True)
|
||||
|
||||
gift_card = GiftCard(shop=shop, amount_in_cents=5000)
|
||||
self.dbsession.add(gift_card)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Test get_gift_card_by_id
|
||||
found = get_gift_card_by_id(self.dbsession, gift_card.id)
|
||||
self.assertIsNotNone(found)
|
||||
self.assertEqual(found.id, gift_card.id)
|
||||
self.assertEqual(found.initial_amount_in_cents, 5000)
|
||||
self.assertEqual(found.balance_in_cents, 5000)
|
||||
|
||||
# Test get_gift_card_by_code with shop filter
|
||||
found_by_code = get_gift_card_by_code(
|
||||
self.dbsession, gift_card.code, shop=shop
|
||||
)
|
||||
self.assertIsNotNone(found_by_code)
|
||||
self.assertEqual(found_by_code.id, gift_card.id)
|
||||
|
||||
# Test get_gift_cards_by_shop returns this gift card
|
||||
shop_cards = get_gift_cards_by_shop(self.dbsession, shop).all()
|
||||
self.assertEqual(len(shop_cards), 1)
|
||||
self.assertEqual(shop_cards[0].id, gift_card.id)
|
||||
|
||||
transaction.commit()
|
||||
|
||||
def test_cart_with_gift_card_integration(self):
|
||||
"""Test cart with attached gift card reduces totals correctly."""
|
||||
user = self._make_user()
|
||||
shop = self._make_shop()
|
||||
product = self._make_product(shop, price_in_cents=1000) # $10
|
||||
cart = self._make_cart(user, shop)
|
||||
|
||||
cart.add_product(product)
|
||||
|
||||
# Create gift card with $50 balance
|
||||
gift_card = GiftCard(shop=shop, amount_in_cents=5000)
|
||||
self.dbsession.add(gift_card)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Attach gift card to cart via CartGiftCard
|
||||
cart_gift_card = CartGiftCard(cart=cart, gift_card=gift_card)
|
||||
self.dbsession.add(cart_gift_card)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Test gift card appears in cart.gift_cards
|
||||
self.assertEqual(len(list(cart.gift_cards)), 1)
|
||||
|
||||
# Test cart is discounted
|
||||
self.assertTrue(cart.is_discounted)
|
||||
|
||||
# Test discounted_shop_totals_in_cents shows reduced amount
|
||||
shop_uuid = shop.uuid_str
|
||||
discounted = cart.discounted_shop_totals_in_cents
|
||||
self.assertEqual(discounted[shop_uuid], 0) # $10 - $50 gift card = $0
|
||||
|
||||
# Test gift_card_deductions dict has the gift card's uuid_str as key
|
||||
deductions = cart.gift_card_deductions
|
||||
self.assertIn(gift_card.uuid_str, deductions)
|
||||
self.assertEqual(deductions[gift_card.uuid_str], 1000) # Deducted $10
|
||||
|
||||
transaction.commit()
|
||||
|
||||
def test_gift_card_deduction_integration(self):
|
||||
"""Test gift card deduct method reduces balance correctly."""
|
||||
shop = self._make_shop()
|
||||
|
||||
gift_card = GiftCard(shop=shop, amount_in_cents=2000) # $20
|
||||
self.dbsession.add(gift_card)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Deduct $15
|
||||
result = gift_card.deduct(1500)
|
||||
self.assertEqual(result, 1500)
|
||||
self.assertEqual(gift_card.balance_in_cents, 500)
|
||||
self.assertTrue(gift_card.is_valid)
|
||||
|
||||
# Deduct remaining $5
|
||||
result = gift_card.deduct(500)
|
||||
self.assertEqual(result, 500)
|
||||
self.assertEqual(gift_card.balance_in_cents, 0)
|
||||
self.assertTrue(gift_card.is_fully_redeemed)
|
||||
self.assertFalse(gift_card.is_valid)
|
||||
|
||||
transaction.commit()
|
||||
|
||||
def test_gift_card_transaction_integration(self):
|
||||
"""Test gift card transaction records are linked correctly."""
|
||||
user = self._make_user()
|
||||
shop = self._make_shop()
|
||||
product = self._make_product(shop, price_in_cents=1000)
|
||||
|
||||
gift_card = GiftCard(shop=shop, amount_in_cents=5000)
|
||||
self.dbsession.add(gift_card)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Create an invoice for the transaction
|
||||
invoice = Invoice(user=user)
|
||||
self.dbsession.add(invoice)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Create a GiftCardTransaction
|
||||
txn = GiftCardTransaction(
|
||||
gift_card=gift_card, invoice=invoice, amount_in_cents=1000
|
||||
)
|
||||
self.dbsession.add(txn)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Assert transaction appears in gift_card.transactions
|
||||
transactions = gift_card.transactions.all()
|
||||
self.assertEqual(len(transactions), 1)
|
||||
self.assertEqual(transactions[0].amount_in_cents, 1000)
|
||||
|
||||
transaction.commit()
|
||||
|
||||
def test_cart_with_gift_card_and_coupon_integration(self):
|
||||
"""Test coupon applies first, then gift card reduces remaining total."""
|
||||
user = self._make_user()
|
||||
shop = self._make_shop()
|
||||
product = self._make_product(shop, price_in_cents=2000) # $20
|
||||
cart = self._make_cart(user, shop)
|
||||
|
||||
cart.add_product(product)
|
||||
|
||||
# Create 50% off coupon ($10 off via dollar-off for predictability)
|
||||
coupon = Coupon(
|
||||
shop=shop,
|
||||
code="HALF",
|
||||
description="$10 off",
|
||||
action_type="dollar-off",
|
||||
action_value=10, # $10.00 off
|
||||
max_redemptions=100,
|
||||
max_redemptions_per_user=1,
|
||||
cart_qualifier=0,
|
||||
)
|
||||
self.dbsession.add(coupon)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Attach coupon to cart
|
||||
cart_coupon = CartCoupon(cart=cart, coupon=coupon)
|
||||
self.dbsession.add(cart_coupon)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Create gift card with $5 balance
|
||||
gift_card = GiftCard(shop=shop, amount_in_cents=500)
|
||||
self.dbsession.add(gift_card)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Attach gift card to cart
|
||||
cart_gift_card = CartGiftCard(cart=cart, gift_card=gift_card)
|
||||
self.dbsession.add(cart_gift_card)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Coupon reduces $20 to $10, gift card reduces $10 to $5
|
||||
shop_uuid = shop.uuid_str
|
||||
discounted = cart.discounted_shop_totals_in_cents
|
||||
self.assertEqual(discounted[shop_uuid], 500) # $5.00
|
||||
|
||||
transaction.commit()
|
||||
|
||||
def test_gift_card_validate_attached_integration(self):
|
||||
"""Test validation catches disabled gift cards attached to cart."""
|
||||
user = self._make_user()
|
||||
shop = self._make_shop()
|
||||
product = self._make_product(shop, price_in_cents=1000)
|
||||
cart = self._make_cart(user, shop)
|
||||
|
||||
cart.add_product(product)
|
||||
|
||||
# Create a disabled gift card
|
||||
gift_card = GiftCard(shop=shop, amount_in_cents=5000)
|
||||
gift_card.disabled = True
|
||||
self.dbsession.add(gift_card)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Attach disabled gift card to cart
|
||||
cart_gift_card = CartGiftCard(cart=cart, gift_card=gift_card)
|
||||
self.dbsession.add(cart_gift_card)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Validate should return errors about disabled card
|
||||
errors = cart.validate_attached_gift_cards()
|
||||
self.assertTrue(len(errors) > 0)
|
||||
self.assertTrue(
|
||||
any("disabled" in err.lower() for err in errors),
|
||||
f"Expected 'disabled' in error messages, got: {errors}",
|
||||
)
|
||||
|
||||
transaction.commit()
|
||||
|
||||
def test_gift_card_purchases_in_cart_integration(self):
|
||||
"""Test cart.json_gift_cards parses gift card purchase entries."""
|
||||
user = self._make_user()
|
||||
shop = self._make_shop()
|
||||
cart = self._make_cart(user, shop)
|
||||
|
||||
# Set json_gift_cards with one purchase entry
|
||||
cart.json_gift_cards = json.dumps([
|
||||
{
|
||||
"shop_id": shop.uuid_str,
|
||||
"amount_in_cents": 2500,
|
||||
"gift_email": "friend@test.com",
|
||||
"gift_message": "Enjoy!",
|
||||
}
|
||||
])
|
||||
self.dbsession.flush()
|
||||
|
||||
# Test gift_card_purchases returns parsed list
|
||||
purchases = cart.gift_card_purchases
|
||||
self.assertEqual(len(purchases), 1)
|
||||
self.assertEqual(purchases[0]["amount_in_cents"], 2500)
|
||||
self.assertEqual(purchases[0]["gift_email"], "friend@test.com")
|
||||
|
||||
# Test total
|
||||
self.assertEqual(cart.gift_card_purchases_total_in_cents, 2500)
|
||||
|
||||
transaction.commit()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue