# CLAUDE.md

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

## Purpose

Processes acknowledgment (ACK) XML files that TikTok places in a TikTok-owned S3 bucket (`bytedance-s3-va-projectm-upload`) after receiving audio-fingerprinting deliveries. Each Lambda run transitions delivery records in Snowflake through a state machine: `unacked → awaited → success | error | missing`.

## Development Commands

All commands run from this directory (`lambda/sr-delivery-tiktok-ack-processor/`).

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

# With exit codes (CI-like)
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 single test
TEST_ARGS="-k test_invoke_lambda" docker compose up --build lint-and-test

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

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

# Quick local invocation without Docker (uses real S3 but mocked resolvers)
python simulate_event.py
```

`docker-compose-local.yaml` is an alternate compose file for local dev that mounts `simulate_event.py` directly into the container.

## Architecture

```
handler() in src/app.py
    └─ common.ack_processor.processor.process()
           ├─ ack_storage.acks(unacked_deliveries)     # S3: downloads ACK XMLs from TikTok bucket
           ├─ acks_splitter                             # classifies XMLs into success/error/awaited
           ├─ resolve_awaited_as_success()              # Snowflake UPDATE ack='success'
           ├─ resolve_awaited_as_error()                # Snowflake UPDATE ack='error'
           └─ resolve_awaited_as_missing()              # Snowflake UPDATE ack='missing'
```

**Snowflake tables:**
- `ORCHARD_SOUND_RECORDING_DELIVERY_HISTORY` — delivery events; joined to find unacked/awaited rows
- `TIKTOK_DELIVERY_ACK` — ACK state per `(sr_version_id, timestamp)`; columns: `ack`, `message`

**ACK states:** `null` (unacked) → `awaited` → `success` | `error` | `missing`

**S3 connector** (`src/connectors/s3.py`): uses TikTok-specific AWS credentials (not the Lambda's own role) to access the TikTok-owned bucket. Searches `S3_ACK_PREFIXES` for a directory matching `{prefix}/{sfn_execution_id}/` and downloads any file with `ACK_` in its name and a `.xml` extension to `/tmp/acks/`.

**Core processing logic** lives in `common/ack_processor/` (symlinked as `src/common/`), shared across multiple ACK-processor Lambdas. Changes there affect all of them.

## Config-Driven Behavior

Key environment variables and their defaults (see `config.py`):

| Variable | Default | Purpose |
|---|---|---|
| `MISSING_HOURS_THRESHOLD` | `72` | Hours before an awaited record becomes `missing` |
| `ACKS_FILTER_AWAITED_HOURS` | `72` | Lookback window for selecting awaited records |
| `LIMIT` | `1000` | Max records processed per run |
| `ACK_SERVICE` | `TikTok (Audio Fingerprinting)` | Service name filter in Snowflake queries |
| `AUTO_RETRY_PATTERNS` | `please wait for automatic retry` | Comma-separated error substrings; matching errors are re-classified as `awaited` within the threshold window |
| `S3_ACK_BUCKET` | `bytedance-s3-va-projectm-upload` | TikTok-owned S3 bucket |

Snowflake credentials come from `LambdaSecretsManager` in QA/PROD and from env vars in dev. The private key is PEM-encoded and decrypted at import time in `config.py`.

## Dependency Injection for Testing

`handler()` accepts an optional `dependencies` dict to override real integrations:

```python
handler(event, context, dependencies={
    'ack_storage': my_mock_storage,       # replaces S3()
    'awaited_deliveries': list_of_dicts,  # replaces snowflake_ack.mark_unacked_as_awaited()
    'awaited_resolver': lambda acks: None,
    'awaited_as_missing_resolver': lambda acks: None,
    'show_data': True,                    # include processed records in return value
})
```

`simulate_event.py` uses this pattern with hardcoded sample deliveries and no-op resolvers to test S3 lookups locally without touching Snowflake.

## Type Checking

`mypy.ini` is at the Lambda root. mypy is run on `src/app.py` and `tests/unit/` during lint-and-test.
