Variable-amount gift cards purchasable with any payment method. Code-based redemption at checkout (applied to cart like coupons). Partial use across multiple purchases, never expire. Shop owners control min/max amounts and can disable individual cards. Models: GiftCard, GiftCardTransaction, CartGiftCard + migration. Views: purchase page, cart apply/remove, shop admin manage/detail/toggle. Templates: gift_card.j2, gift_card_manage.j2, gift_card_detail.j2. Cart integration: gift cards deduct after coupons in all checkout paths. Tests: 10 new unit tests covering model logic (677 total pass).
3.1 KiB
3.1 KiB
MPS-12: Gift Card System — Redemption at Checkout
Problem
Recipients need to apply a gift card code at checkout, just like a coupon code. The gift card balance should reduce the cart total for that shop. Partial use is supported — remaining balance stays on the card for future purchases.
Solution
Cart: gift card code entry
Add a "Gift Card" code input field alongside the existing coupon code field on the cart/checkout page. The flow mirrors coupon application:
- User enters gift card code
- Server validates: code exists, belongs to a shop in the cart, has balance, not disabled
- If valid, attach to cart and show the discount
- If invalid, show error message
Cart model changes
Add gift card tracking to the Cart model, parallel to how coupons work:
- New association:
MpsCartGiftCard(cart_id, gift_card_id) — many-to-many cart.gift_cards— association proxy to attached gift cards- Gift card discount is applied AFTER coupon discounts (coupons reduce the price first, then gift card balance covers the remainder)
Discount calculation
In Cart.discounted_shop_totals_in_cents, after coupon discounts:
# After coupon discounts are applied...
for gift_card in self.gift_cards:
shop_uuid = gift_card.shop_uuid_str
if shop_uuid in self._discounted_shop_totals_in_cents:
current = self._discounted_shop_totals_in_cents[shop_uuid]
deduction = min(gift_card.balance_in_cents, current)
self._discounted_shop_totals_in_cents[shop_uuid] = current - deduction
# Store deduction amount for checkout to record transaction
gift_card._pending_deduction = deduction
Checkout: balance deduction
After successful payment (or if requires_payment is False because gift card
covered the full amount):
- For each attached gift card, deduct
_pending_deductionfrombalance_in_cents - Create a
MpsGiftCardTransactionrecord - Detach gift card from cart
Validation
cart.validate_attached_gift_cards() checks:
- Gift card is not disabled
- Gift card has balance > 0
- Gift card belongs to a shop in the cart
Edge cases
- Gift card covers full amount:
requires_paymentreturns False (total <= 64 cents after gift card). Checkout proceeds without charging a card, same as a 100% coupon. - Gift card + coupon stacking: Allowed. Coupon applies first (percentage or dollar off), then gift card balance covers the remaining amount.
- Multiple gift cards: A buyer can apply one gift card per shop in the cart (same constraint as coupons — one per shop keeps it simple).
Files Changed
| File | Change |
|---|---|
models/cart_gift_card.py |
New: MpsCartGiftCard association model |
models/cart.py |
Gift card association proxy, discount integration, validation |
views/cart.py |
Gift card code apply/remove handlers |
templates/cart.j2 |
Gift card code input field, balance display |
templates/checkout.j2 |
Show gift card discount in order summary |
views/checkout.py |
Deduct balance, create transactions on successful checkout |
Depends On
MPS-10 (models), MPS-11 (gift card records exist to redeem).