# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Commands

```bash
# Install dependencies
poetry install

# Run locally (QA-only for safety)
make run_streamlit_locally

# Format code
make fmt          # ruff check --fix && ruff format

# Lint code
make lint         # mypy (strict) + ruff check + ruff format --check

# Deploy
make deploy_qa_streamlit    # Deploy to Snowflake QA
make deploy_prod_streamlit  # Deploy to Snowflake PROD
```

## Architecture

Streamlit app for managing artist rosters in Snowflake. Deployed as a Snowflake Native App via `snowflake.yml`.

### Module layout (`streamlit_app/`)

- **`main.py`** — Entry point and welcome page
- **`pages/`** — Streamlit pages (roster view, add artist, bulk add, virtual participants, upgrade pending artist)
- **`common/`** — Shared code:
  - **`environment.py`** — Centralized env detection. Exports `TARGET_SCHEMA`, `IS_QA`, `IS_PROD`. Deployed apps auto-detect schema from Snowflake session; local runs use `target` env var (defaults to `qa`)
  - **`db.py`** — Snowpark session (cached via `@st.cache_resource`), `execute_query()`, `execute_non_query()`, `transaction()`, `get_current_user()`
  - **`services.py`** — Business logic: vendor/artist search, brand detection, validation, CRUD. MERGE queries prevent duplicates
  - **`queries.py`** — SQL templates (environment-aware via imported constants)
  - **`types.py`** — Pydantic V2 models with `.validate_fields()` methods
  - **`utils.py`** — UI helpers (pagination, Spotify ID parsing, country code validation via pycountry)
  - **`constants_qa.py` / `constants_prod.py` / `constants_general.py`** — Schema and table name constants

### Data model

Three roster tables in `FANSIFTER_APP_REPORTING.{QA|PROD}`:

| Table | For | Unique key |
|-------|-----|-----------|
| `ARTIST_ROSTER_MAIN_REP` | SME vendors | `(VENDOR_ID, GLOBAL_PARTICIPANT_ID, SUBACCOUNT_ID)` |
| `ARTIST_ROSTER_LOCAL_REP` | SME vendors | `(VENDOR_ID, GLOBAL_PARTICIPANT_ID, SUBACCOUNT_ID, COUNTRY_CODE)` |
| `ARTIST_ROSTER` | Non-SME vendors | `(VENDOR_ID, GLOBAL_PARTICIPANT_ID, SUBACCOUNT_ID)` |

Brand classification (SME vs non-SME) is determined by `COMPANY_BRAND.DISPLAY_NAME = 'Sony Music'` via the `COMPANY_BRAND_HAS_LABEL_VENDOR` join — not by roster table presence.

Additional tables: `VIRTUAL_PARTICIPANT` and `CUSTOM_LIST` for pending artists and custom lists.

### Key conventions

- **Environment safety**: Local runs are always QA. PROD is only accessible via Snowflake deployment.
- **SQL injection prevention**: String values escaped with `.replace("'", "''")`. MERGE queries used for upserts.
- **Type safety**: mypy strict mode with Pydantic plugin. All code must type-check.
- **Ruff config**: 88 char line length, Python 3.11 target, Google docstring convention, isort with `streamlit_app.common` as forced-separate section.
- **Scope**: Both mypy and ruff are scoped to `streamlit_app/common/*.py`, `streamlit_app/pages/*.py`, and `streamlit_app/main.py` via `pyproject.toml`.
- **User tracking**: `CREATED_BY` field populated via `CURRENT_USER()`.
- **Deleted records**: Queries filter out `_FIVETRAN_DELETED` records.