# CLAUDE.md

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

## What This Lambda Does

`sr-delivery-tiktok` delivers sound recording metadata and audio assets to TikTok's Audio Fingerprinting service via S3. Given a sound recording ID (and optionally a specific version), the handler:

1. Fetches the `OrchardSoundRecording` JSON blob from S3
2. Optionally checks for a duplicate delivery by comparing the newly generated DDEX XML against the last successfully delivered XML (controlled by `PERFORM_FINAL_DDEX_COMPARISON`)
3. Generates a DDEX XML file via `soundrecording_utils.ddex.generate.generate_ddex` with `RuleService.TIKTOK`
4. Writes the DDEX XML to both a delivery-audit S3 bucket (for historical lookup) and the TikTok S3 delivery bucket (or a QA staging bucket in non-prod)
5. Optionally downloads the audio asset from S3 and uploads it alongside the XML
6. Writes a `BatchComplete_{batch_id}.xml` sentinel file to signal delivery completion

In `dev`/`qa`, `write_file` routes to an internal QA staging bucket. In `prod`, it uses TikTok's own S3 bucket with credentials fetched via `LambdaSecretsManager`.

## Development Commands

All commands run from this directory (`lambda/sr-delivery-tiktok/`):

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

# Run with exit codes (CI-style)
docker compose up --exit-code-from lint-and-test --abort-on-container-exit --build lint-and-test

# 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

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

# Start the function locally
docker compose up --build -d function
curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d @tests/dev_sample_event.json

# Quick local invocation without Docker
python simulate_event.py
```

Linting runs `yamllint` on `docker-compose.yaml`, `mypy src/app.py tests/unit/`, and `flake8 src/ tests/ config.py` (max line 120, single quotes, Google import order).

## Key Configuration (`config.py`)

| Variable | Default | Notes |
|---|---|---|
| `ENVIRONMENT` | `dev` | Controls S3 routing: `prod` → TikTok bucket, else → QA bucket |
| `PROCESS_ASSET` | `true` | Set to `false` to skip audio asset upload (metadata-only delivery) |
| `PERFORM_FINAL_DDEX_COMPARISON` | `false` | When `true`, skips re-delivery if generated DDEX matches the last successful delivery |
| `DDEX_FILE_VERSION` | `3.8` | DDEX schema version passed to `generate_ddex` |
| `TIKTOK_S3_BUCKET_NAME` | — | Required in prod |
| `TIKTOK_S3_BASE_FOLDERNAME` | — | S3 key prefix for TikTok delivery bucket |

Secrets (`TIKTOK_FINGERPRINT_DELIVERY_USER_NAME`, `TIKTOK_FINGERPRINT_DELIVERY_PASSWORD`, `SENTRY_DSN`) are resolved via `LambdaSecretsManager` at import time.

## S3 File Layout

Each delivery writes to a path keyed by `batch_id` (timestamp + 4-digit random suffix) and the recording's ISRC:

```
{batch_id}/
  {isrc}/{isrc}.xml                         # DDEX XML
  {isrc}/resources/{filename}.{ext}         # audio asset (if upload_asset=true)
  BatchComplete_{batch_id}.xml              # delivery sentinel
```

In prod, this path is prefixed with `TIKTOK_S3_BASE_FOLDERNAME` and `execution_name` (step function execution ID).

## Duplicate DDEX Detection

When `PERFORM_FINAL_DDEX_COMPARISON=true`, the handler:
1. Calls `graphql.get_delivery_histories` to find the last successful delivery for the store `"TikTok (Audio Fingerprinting)"`
2. Only proceeds with the check if the last delivery status was `SUCCESS`
3. Fetches the previously delivered XML from the audit bucket via `graphql.get_last_delivered_xml_location`
4. Strips `<MessageCreatedDateTime>` and `<MessageId>` (which always differ) before comparing
5. Returns `{'details': {'undelivered': True, 'reason': 'duplicate'}}` if identical

## GraphQL Queries (`src/utils/graphql.py`)

Two identity/profile contexts are used:
- **Tracks query** (`TRACKS_PROFILE_TYPE = 'OrchAdminProfile'`): fetches track metadata by ISRC
- **History/delivery queries** (`SR_PROFILE_TYPE = 'ContentProfile'`, role `view_orchard_sound_recording`): fetches `OrchardSoundRecording` delivery history

`TransportServerError` with codes 502/503/504 and `TransportQueryError` are wrapped as `RetryableException` (the step function will retry).

## `NoAssets` Exception

Raised when `upload_asset=True` but the `OrchardSoundRecording` has no assets. This exception is intentionally **not** logged to Sentry — instead, `sentry_sdk.init()` is called with no DSN to reset the SDK before re-raising, suppressing any auto-capture.

## Testing Notes

Unit tests mock all external I/O (`s3_sound_recordings`, `s3_assets`, `s3_asset_delivery`, `s3_delivery_audit`, `generate_ddex`, `graphql.*`). Use `freezegun` for time-dependent assertions. The integration test (`tests/integration/test_lambda.py`) is a placeholder and always fails — write real integration tests before relying on it.
