# CLAUDE.md

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

## Purpose

This Lambda receives a compiled `OrchardSoundRecording` blob from S3, transforms it into a DDEX ERN 3.82 `NewReleaseMessage` XML document, and delivers that XML (and optionally an audio asset) to Meta via SFTP. It is invoked from a Step Function with an event that specifies a sound recording ID/version and whether to include an audio asset upload.

## Important Note

As of May 2026, this lambda is not in use and never has been. This file is for information only.

## Development Commands

All commands run from this directory (`lambda/sr-delivery-meta/`).

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

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

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

# Generate HTML coverage report
COV_REPORT=html docker compose up --build lint-and-test

# Start the function locally (SFTP server starts automatically)
docker compose up --build -d function

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

## Local Setup

Before running the function locally, set up the `.env` file:

```bash
cp .env.shadow .env
echo ID_RSA=\""$(cat localdev/local_id_rsa | sed 's/$/\\\\n/g' | tr -d '\n')"\" >> .env
```

The `fake-book` SFTP container starts automatically with `function`. Access it on `localhost:2223` with username `orchard` and the key at `localdev/local_id_rsa`. If volume errors occur: `rm -R ./localdev/share/`.

## Architecture

### Handler flow (`src/app.py`)

1. Reads `sound_recording.id` and `sound_recording.version` from the event
2. Fetches the compiled `OrchardSoundRecording` JSON from S3 (`common/connectors/s3_sound_recordings`)
3. Deserializes it using `typedload` with a custom enum handler (`src/utils/typedload.py`) — enums load by member name (`'Y'`, `'N'`) not value
4. Builds a batch ID: `YYYYMMDDHHMMSS` + 4-digit random suffix
5. If `upload_asset: true`, selects an asset preferring `flac` over `wav` (raises `NoAssets` if neither exists)
6. Calls `generate_ern_srr()` to produce DDEX XML
7. Writes the XML to S3 audit bucket (`common/connectors/s3_delivery_audit`)
8. Downloads audio bytes from S3 if uploading (`common/connectors/s3_assets`)
9. Uploads all files to Meta's SFTP: audio asset (if any), ISRC-named XML, and an empty `BatchComplete_<batch_id>.xml`

`NoAssets` errors are raised without Sentry logging (they re-init Sentry to suppress). All other exceptions log via `logger.exception` and re-raise.

### DDEX XML generation (`src/ddex/`)

The `generate_ern_srr()` entry point in `generate.py` assembles a DDEX ERN 3.82 `NewReleaseMessage` from four sub-generators:

- **`header.py`** — `MessageHeader` with Orchard sender, Meta recipient, batch timestamp
- **`sound_recording.py`** — `SoundRecording` resource with ISRC, duration, language/instrumental flag, per-territory details (including asset `File` reference on the first territory block only)
- **`release.py`** — `Release` with ISRC, title, display artists, label, genre, parental warning, P-lines (deduplicated across all `in_content` tracks)
- **`deal.py`** — `ReleaseDeal` grouping territories by start date; if no territories exist (takedown), emits `Worldwide` with `EndDate = today - 2 days`

All XML elements are built using `lxml.objectify.ElementMaker` (`src/ddex/e.py`). The `E()` callable is the single factory used throughout `src/ddex/`.

### Territory deduplication (`src/ddex/util.py`)

`deduplicate_territories()` sorts tracks by product release date ascending and assigns each territory to the earliest track that claims it. This prevents duplicate `TerritoryCode` elements in output. A "takedown" path (no territories on any track) emits `Worldwide` coverage.

### SFTP directory structure

```
<META_SFTP_BASE_DIRNAME>/          # env var, optional prefix
  <batch_id>/                      # e.g. 202406151245170010
    BatchComplete_<batch_id>.xml   # empty sentinel file
    <isrc>/
      <isrc>.xml                   # DDEX document
      resources/
        <asset_uuid>.<ext>         # audio file, if upload_asset=true
```

## Key Types

All metadata types come from `soundrecording_utils.metadata.types` (internal package). Core types used here:

- `OrchardSoundRecording` — top-level object with `track_connection.tracks`, `assets`, `isrc`
- `Track` — has `territories`, `participations`, `product`, `p_info`, `explicit`, `duration_*`, `primary` flag
- `Asset` — has `uuid`, `filename`, `extension`

`get_primary_track()` in `util.py` returns the track with `primary=True`; if the track list is empty (all detached), it returns a dummy `Track` with blank/default fields to satisfy required XML elements.

## Testing Patterns

Unit tests use `pytest` with `lxml` assertions comparing rendered XML strings. The `Helpers.print_xml()` method in `tests/unit/helpers.py` wraps an element in a `<TestRoot>` and strips it before comparison — use it when asserting on partial XML fragments.

Key fixtures in `tests/unit/conftest.py`: `mock_orchard_sound_recording`, `mock_track`, `mock_asset`, `mock_product`, `execution_meta`. Tests that reload `src.app` via `importlib.reload(index)` are testing Sentry initialization side effects at module load time.

The `pytest-sftpserver` fixture (`sftpserver`) provides an in-process SFTP server for connector tests.

## Type Checking

`mypy` runs on `src/app.py` and `tests/unit/` with `check_untyped_defs = True`. Third-party packages (`boto3`, `botocore`, `secrets_manager`, `soundrecording_utils`) have `ignore_missing_imports = True` in `mypy.ini`.
