# MPS-15: 21-Day Free Trial

## Summary

New shops get a 21-day free trial. Trial includes 1 shop, 1 seat, all features.
After 21 days the shop enters a grace period, then becomes read-only until a
plan is chosen.

## Model Changes

### Shop model (`models/shop.py`)

Add columns:

```python
trial_started_timestamp = Column(BigInteger, nullable=True)
trial_ended = Column(Boolean, default=False)
plan_active = Column(Boolean, default=False)
```

- `trial_started_timestamp`: set to current time (ms) on shop creation
- `trial_ended`: flipped to True when trial expires (background check or request-time check)
- `plan_active`: True when a paid plan is active (billing integration, future ticket)

### Migration

- Add 3 columns to `mps_shop` with idempotent guards
- `trial_started_timestamp` nullable (existing shops get NULL = pre-trial era, treated as paid)
- `trial_ended` server_default="0"
- `plan_active` server_default="0"

### Properties

```python
TRIAL_DURATION_MS = 21 * 24 * 60 * 60 * 1000  # 21 days in milliseconds

@property
def trial_expiry_timestamp(self):
    if self.trial_started_timestamp is None:
        return None
    return self.trial_started_timestamp + self.TRIAL_DURATION_MS

@property
def is_trial_active(self):
    if self.plan_active:
        return False  # paid plan supersedes trial
    if self.trial_started_timestamp is None:
        return False  # pre-trial shop (existing shops)
    now = int(time.time() * 1000)
    return now < self.trial_expiry_timestamp

@property
def is_trial_expired(self):
    if self.plan_active or self.trial_started_timestamp is None:
        return False
    now = int(time.time() * 1000)
    return now >= self.trial_expiry_timestamp

@property
def trial_days_remaining(self):
    if not self.is_trial_active:
        return 0
    remaining_ms = self.trial_expiry_timestamp - int(time.time() * 1000)
    return max(0, remaining_ms // (24 * 60 * 60 * 1000))

@property
def is_active(self):
    """Shop can operate: either paid plan or active trial."""
    return self.plan_active or self.is_trial_active
```

## Trial Enforcement

### What trial shops CAN do (all features)
- Create products, upload files, set prices
- Accept payments (all 5 methods)
- Use watch mode, analytics, subscriptions, gift cards
- Create 2 dev/stage shops (per MPS-14)
- Full settings access

### What happens when trial expires
- Shop becomes **read-only**: products visible, downloads work for existing purchases
- New purchases blocked (checkout disabled)
- Product creation/editing disabled
- Settings page shows "Trial expired — choose a plan to continue"
- Flash message on every page: "Your 21-day trial has expired. Choose a plan to keep selling."

### Enforcement points

Request-time check (middleware or request method):

```python
def shop_trial_check(request):
    shop = request.shop
    if shop and shop.is_trial_expired and not shop.plan_active:
        # Allow read-only routes, block write routes
        ...
```

Write routes to block when trial expired:
- Product create/edit/delete
- Checkout completion (all 3 paths: Stripe, PayPal, Adyen)
- Gift card purchase
- Settings changes (except choosing a plan)

Read routes to allow:
- Product view, shop view, search
- Cart view (but not checkout)
- Settings view (read-only, plan selection enabled)
- Download (for existing purchases)

## Trial Banner

In `base.j2`, show trial status for shop owners:

```html
{% if request.shop and request.shop.is_trial_active and request.user in request.shop.owners %}
<div class="trial-banner">
  Free trial: {{ request.shop.trial_days_remaining }} days remaining.
  <a href="/s/{{ request.shop.id }}/settings#plan">Choose a plan</a>
</div>
{% endif %}

{% if request.shop and request.shop.is_trial_expired and not request.shop.plan_active %}
<div class="trial-banner trial-expired">
  Your 21-day trial has expired.
  <a href="/s/{{ request.shop.id }}/settings#plan">Choose a plan to keep selling</a>
</div>
{% endif %}
```

## Shop Creation Changes

In `views/shop.py` `shop_new()`:
- Set `shop.trial_started_timestamp = int(time.time() * 1000)` on creation
- Existing shops (NULL timestamp) are grandfathered as paid

## Tests

- Unit: trial properties (is_trial_active, is_trial_expired, trial_days_remaining, is_active)
- Integration: trial shop with real DB, verify expiry behavior
- Functional: create shop, verify trial banner, verify trial countdown
