# 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:

```python
# 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).
