# automated-emails-sender

## Overview

An AWS Lambda handler (invoked via an S3 event) that sends automated (triggered) emails to fans via SendGrid. On each invocation it:

- **Reads a fan CSV from S3** — the file contains one row per recipient with their email address, country, profile ID, and optional double opt-in metadata.
- **Validates and deduplicates** — each email address is validated with `emval` and deduplicated via Redis to prevent repeated sends within a TTL window.
- **Builds per-fan compliance data** — resolves legal entity, privacy links block, and compliance footer text (opt-in info, unsubscribe, profile link) for the fan's country.
- **Sends via SendGrid** — each fan is included in a single SendGrid API request with per-fan substitutions for the privacy footer, preview text, and double opt-in URL (when applicable).

## Getting Started

### Requirements

Minimum requirements and recommended tooling:

- Python: 3.13
- UV (astral `uv` tool) for environment and task running (see `pyproject.toml` `tool.uv` configuration)
- Docker and Docker Compose (used for CI targets)
- Make (for provided Makefile targets)

Developer tooling (used in CI / recommended):

- `pytest` (unit tests)
- `ruff` and `ty` (linting / type checks)
- Redis (required at runtime for deduplication)

### Installation

```bash
# Install uv (if not installed)
curl -LsSf https://astral.sh/uv/install.sh | sh

# Install project dependencies
make env
```

### Configuration

For local development, copy the example configuration:

```bash
cp .env.example .env
```

Then update `.env` with your settings. Key variables (see [app/config.py](app/config.py) and [.env.example](.env.example)):

- `ENVIRONMENT` — Environment name (`dev`, `qa`, `prod`)
- `SENDGRID_SME_API_KEY`, `SENDGRID_ORCHARD_API_KEY`, `SENDGRID_AWAL_API_KEY` — SendGrid API keys per brand
- `UNSUBSCRIBE_HOST` — Domain used to construct the `List-Unsubscribe` email header
- `PREFERENCE_CENTER_KEY_NAME` — AWS Secrets Manager key for the preference center JWT secret
- `REDIS_HOST`, `REDIS_PORT`, `REDIS_DB`, `REDIS_SSL` — Redis connection for deduplication
- `REDIS_DEDUP_TTL` — TTL in seconds for the deduplication cache (default: `600`)
- `SNOWFLAKE_ACCOUNT`, `SNOWFLAKE_DATABASE`, `SNOWFLAKE_SCHEMA`, `SNOWFLAKE_WAREHOUSE`, `SNOWFLAKE_ROLE`, `SNOWFLAKE_USER` — Snowflake connection settings
- `SNOWFLAKE_PRIVATE_KEY_LOCATION`, `SNOWFLAKE_PRIVATE_KEY_PASSWORD` — Snowflake authentication
- `SENTRY_DSN` — Sentry error tracking (required for QA/PROD)
- `APP_DEBUG` — Debug mode (default: `false`)

## Testing

### Unit tests

Run the project's unit tests with:

```bash
uv run pytest tests/unit
```

## Linting and formatting

Run the linter with:

```bash
make lint
```

Run the code formatter with:

```bash
make fmt
```

## Project Structure

- `app/` — Main application code
  - `main.py` — Lambda handler entry point; initialises Sentry and dispatches to `handler.handle`
  - `handler.py` — Core send logic; reads the CSV, deduplicates, and sends emails via SendGrid
  - `fandata.py` — `get_fan_data`: validates email, resolves compliance footer per fan's country, builds `FanData` for SendGrid
  - `dedup.py` — Redis-based deduplication to prevent duplicate sends
  - `config.py` — Configuration and settings
  - `models.py` — Pydantic/ORM models for automated emails and send batches
  - `types.py` — `CsvRow` and other data types
  - `adapters/` — External service adapters (AWS S3, Snowflake DB, Redis, OWS Account, SendGrid, Preference Center)
  - `locale/translations.json` — Email footer translations per language
- `tests/` — Test suite
  - `unit/` — Unit tests

## Processing Flow

1. Lambda is invoked with an S3 event pointing to the fan CSV file.
2. The CSV is read from S3 and parsed row by row into `CsvRow` objects.
3. For each row, `get_fan_data` validates the email address (skips invalid), then checks Redis deduplication (skips already-seen fans within the TTL window).
4. Compliance footer data (opt-in info, unsubscribe text, profile link) is resolved from `translations.json` based on the fan's country using `_get_compliance_footer` (LRU-cached per country).
5. A `FanData` object is built including the profile token, legal entity, privacy links, and footer texts.
6. All valid fans are batched into a single SendGrid API request (up to `MAX_EMAILS_TO_SEND = 1000`) with per-fan substitutions.
7. Send results are logged; errors are reported to Sentry.
