# CLAUDE.md

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

## Purpose

`sr-delivery-youtube-filter` is a Kafka-triggered Lambda that determines whether sound recording (OSR) change events are eligible for delivery to YouTube (Audio Fingerprinting). Eligible events are routed to a downstream AWS Step Function that performs the actual delivery. Ineligible events are logged to a Kafka topic for audit purposes.

## Development Commands

Run from this directory (`lambda/sr-delivery-youtube-filter/`):

```bash
# Run all tests and linting
docker compose up --build lint-and-test

# Run tests only (skip lint)
SKIP_LINT=true docker compose up --build lint-and-test

# Run a specific test by name
TEST_ARGS="-k test_new_osr_full_delivery" docker compose up --build lint-and-test

# Run with HTML coverage report (served on port 8000)
COV_REPORT=html docker compose up --build lint-and-test

# Start the function locally (HTTP on port 9000)
docker compose up --build -d function

# Invoke locally
curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d @tests/sample_event.json
```

Linting runs `mypy`, `flake8`, and `yamllint` in that order. mypy covers `src/app.py` and `tests/unit/` only.

## Architecture

### Event Flow

1. MSK (Kafka) delivers base64-encoded JSON records; `src/utils/input_events.py` decodes the batch.
2. `src/delivery_eligibility.py::get_eligible_events()` is the core decision engine — it takes the decoded event batch and returns a list of `OutputEvent` objects representing SFN executions to start.
3. `src/utils/step_function.py` handles throttling (`throttle()`) and execution (`execute_sfn()`), starting one SFN execution per eligible event.

### Eligibility Decision Logic (in `delivery_eligibility.py`)

Eligibility is determined in two phases:

**Phase 1 — `_preliminary_checks()`** (runs before fetching delivery history):
- OSR must have a version in S3 (`s3_sound_recordings.get_sound_recording_version`)
- Must have at least one asset with an allowed extension (`flac` or `wav`, in preference order)
- For new OSRs (no `previous_version_id`): must be in `ows-masters-registry` AND have non-carveout-only fingerprint rules for `youtube` → triggers `FULL_DELIVERY` immediately or logs ineligibility

**Phase 2 — delivery history logic** (runs against GraphQL delivery history):
- Fetches prior delivery history via `src/connectors/graphql.py::get_delivery_history()` (filtered to `YouTube (Audio Fingerprinting)`, `SUCCESS` status)
- OSR removed from registry or all rules are carveouts → `TAKEDOWN_DELIVERY` (unless already taken down)
- OSR removed its youtube fingerprint rules → `TAKEDOWN_DELIVERY`
- No prior `FULL_DELIVERY` in history → `FULL_DELIVERY`
- Metadata unchanged (per `are_sound_recording_versions_equal`) → skip
- Previous delivery was a takedown → `FULL_DELIVERY`
- Otherwise → `METADATA_UPDATE`

The `upload_asset` flag on takedowns is `True` only when no prior `FULL_DELIVERY` exists in history (i.e. YouTube never received an asset file).

### Fingerprint Rules

Rules live at three levels on each track: track-level, subaccount-level, and vendor-level (`src/utils/rules.py`). A carveout at *any* level for a territory "wins" over monetize. A global territory (`*`) carveout makes the entire SR ineligible.

### Metadata Comparison (`src/utils/metadata_filter.py`)

Before deciding to send a `METADATA_UPDATE`, the current and previous SR versions are compared after stripping fields YouTube does not care about. Excluded track fields include `original_rights_holder_country`, `recording_country`, `label_sound_recording`, `audio_attributes`, `rights_attributes`, `offer_type`, `ownership_rights`. Excluded rule audit fields: `created_by`, `created_date`, `last_modified_by`, `last_modified_date`. Excluded product fields: `display_upc`, `ingestion_completed`, `sale_start_date`.

### SFN Throttling

Before starting executions, `sfn.throttle()` polls `list_executions(statusFilter=RUNNING)` against `SFN_DELIVERY_ARN`. If `running + new > SFN_DELIVERY_MAX_RUNNING` (default 500), it sleeps 30 s and retries. AWS `ThrottlingException` on the list call triggers exponential backoff.

### Ineligibility Logging

All rejected events are published to a Kafka topic (`config.KAFKA_TOPIC`) via `src/utils/kafka_logger.py::log_not_eligible_delivery()` with a `reason` string matching one of the constants in `src/constants.py` (`no_assets`, `no_eligible_assets`, `not_in_registry`, `no_fingerprint_rules`, `no_s3_version`).

## Key Configuration (`config.py`)

| Variable | Source | Notes |
|---|---|---|
| `KAFKA_BROKERS` | env | Required for ineligibility logging |
| `KAFKA_TOPIC` | env | Topic for ineligibility audit messages |
| `SFN_DELIVERY_ARN` | env | ARN of the YouTube delivery Step Function |
| `SFN_DELIVERY_MAX_RUNNING` | env (default 500) | Throttle ceiling |
| `ADMIN_IDENTITY_ID` / `ADMIN_PROFILE_*` | hardcoded | Used as GraphQL request headers |

## Testing Approach

All unit tests live in `tests/unit/`. `tests/conftest.py` provides shared fixtures: `sound_recording_metadata`, `previous_sound_recording_metadata`, carveout variants (track/subaccount/vendor/global), and MSK-format event wrappers. The key test file is `tests/unit/test_delivery_eligibility.py`, which covers all branching paths in `get_eligible_events()`.

Integration tests in `tests/integration/` are skipped in CI and require real AWS/GraphQL access.
