# MPS-14: Shop Environment — Dev & Stage Shops

## Summary

Add an `environment` column to Shop so owners can create development and staging
shops for practicing thumbnails, videos, product staging, and testing checkout
flows. Non-production shops are fully independent (no sync to production) and
invisible to the public.

Every paid production shop seat includes 2 free dev/stage shops.

## Model Changes

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

Add column:

```python
environment = Column(BigInteger, default=0)
# 0 = production (default)
# 1 = staging
# 2 = development
```

Add properties:

```python
@property
def is_production(self):
    return self.environment == 0

@property
def is_staging(self):
    return self.environment == 1

@property
def is_development(self):
    return self.environment == 2

@property
def is_non_production(self):
    return self.environment != 0

@property
def environment_label(self):
    return {0: "Production", 1: "Staging", 2: "Development"}.get(self.environment, "Production")
```

### Migration

- Add `environment` column to `mps_shop` (BigInteger, server_default="0", NOT NULL)
- Idempotent guard with `_column_exists`

## Exclusion Points

Non-production shops (environment != 0) must be excluded from:

1. **Search results** — `views/shop.py:129` `search()` — filter query to `shop.environment == 0`
2. **Discovery ring** — `models/shop.py:656` `_build_discovery_ring()` — already scoped to shop's own products, but ring should not be reforged for non-production shops
3. **RSS/Atom/Sitemap** — `views/feeds.py:204,222,242` — skip non-production shops entirely (return empty feed or 404)
4. **Subscription digests** — `models/shop_subscription.py` query helpers — filter by `shop.environment == 0`
5. **Public shop listings** — any place shops are listed publicly

Non-production shops still fully function for the owner: product upload, cart,
checkout, settings, analytics — all work normally.

## Banner

Display a persistent environment banner for non-production shops, similar to
the ribbon pattern. In `base.j2` or `snippets/ribbon.j2`:

```html
{% if request.shop and request.shop.is_non_production %}
<div class="environment-banner environment-{{ request.shop.environment_label|lower }}">
  {{ request.shop.environment_label }} Shop
</div>
{% endif %}
```

CSS in `common.css`:
- Staging banner: amber/yellow background
- Development banner: blue/purple background
- Always visible, not dismissible

## Settings UI

Add environment selector to shop settings. New form section `environment-settings`
or add to existing `shop-settings` section.

Radio buttons or select:
- Production (default)
- Staging
- Development

Changing from non-production to production should warn: "This shop will become
publicly visible."

## Shop Creation Flow

On `/s/new`, add an optional environment selector (default: production).
This lets users create dev/stage shops directly during onboarding.

## Enforcement: 2 Free Dev/Stage Per Production Shop

Each paid production shop seat entitles the user to 2 free non-production shops.

### Counting logic

```python
def non_production_shop_allowance(user):
    production_count = sum(1 for s in user.shops if s.is_production)
    allowed_non_production = production_count * 2
    current_non_production = sum(1 for s in user.shops if s.is_non_production)
    return allowed_non_production - current_non_production
```

### Enforcement points

- **Shop creation** (`views/shop.py` POST `/s/new`): if environment != 0 and
  allowance <= 0, flash error and reject
- **Environment change** (`views/shop.py` settings POST): if changing to
  non-production and allowance <= 0, reject; if changing to production, always allow

## Tests

- Unit: `test_models.py` — environment properties, environment_label
- Integration: `test_integration.py` — non-production shop excluded from discovery ring query, allowance counting
- Functional: `test_functional.py` — create dev shop, verify search excludes it, verify banner appears, verify allowance enforcement
