make_post_sell/docs/tickets/mps-12.md
russell@unturf.com 5d501652c2 feat: add gift card system for shops (MPS-10 through MPS-13)
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).
2026-03-07 15:38:40 -05:00

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:

  1. User enters gift card code
  2. Server validates: code exists, belongs to a shop in the cart, has balance, not disabled
  3. If valid, attach to cart and show the discount
  4. 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):

  1. For each attached gift card, deduct _pending_deduction from balance_in_cents
  2. Create a MpsGiftCardTransaction record
  3. 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_payment returns 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).