# earnings_transfer

- **Owner:** Diego Salazar
- **Epic:** [ACC-9366](https://theorchard.atlassian.net/browse/ACC-7578)
- **Last Updated:** 2026-05-14

AWS Lambda that automates Transfer of Earnings (ToE) calculations. Fetches transfer configurations from the ows-royalties API, enriches them with contract balances from Snowflake, calculates transfer amounts, and outputs a Manual Adjustments XLSX file to S3.

## Pipeline

```mermaid
flowchart TD
    A["AWS CLI invoke"] --> B["app.handler()"]

    B --> C["parse_earnings_transfers()"]

    subgraph Parse & Enrich
        C --> D["GET ows-royalties/earnings-transfers/"]
        D --> E["Filter active records\nSkip cross-recoupment"]
        E --> F{"Validation\n(transfer_type, rate_type,\ntransfer_amount, input)"}
        F -- "Invalid" --> G["Raise InputValidationError\n(all errors collected)"]
        F -- "Valid" --> H["Fetch contract data\nfrom Snowflake"]
        H --> I["Enrich TransferRecords\n(balances, account names)"]
    end

    subgraph Calculate ["Calculate — for each TransferRecord"]
        I --> J["Select from_contract balance\n(closing_balance | net_revenue | gross_revenue)"]
        J --> K{"rate_type?"}
        K -- "percent" --> L["from_balance * transfer_amount\n(blocked if balance <= 0)"]
        K -- "flat_rate" --> M["Fixed dollar amount\n(blocked if balance < 0\nunless negative=true)"]
        L & M --> N1["calculated_amount"]
    end

    subgraph Output ["Output — map results to adjustment rows"]
        N1 --> FROM["FROM row (debit): from_contract\namount = -calculated_amount"]
        N1 --> TO["TO row (credit): to_contract\namount = +calculated_amount"]
        FROM & TO --> O["write_output_to_buffer()\nXLSX: Adjustments | Summary | Errors"]
        O --> P["Upload to S3"]
    end

    P --> Q["Return result\n(status, output_key, record_count)"]
    G --> R["Return ERROR\n(error message)"]
```

## Calculation Rules

Two rate types determine how `transfer_amount` is interpreted:

**percent** — `transfer_amount` is a ratio (0-1). Calculated amount = `balance * transfer_amount`. Blocked when balance is non-positive.

**flat_rate** — `transfer_amount` is a fixed dollar amount. When `negative=false` (default), blocked if balance < 0. When `negative=true`, always allowed regardless of balance.

The balance used is selected by the record's `input` field: `closing_balance`, `net_revenue`, or `gross_revenue`.

## Output

XLSX workbook with up to 3 sheets:
- **Adjustments** — FROM/TO rows (debit/credit pairs) in Manual Adjustments import format
- **Summary** — one row per transfer with calculation details
- **Errors** — records that failed enrichment (missing Snowflake data)

## Project Structure

```
app.py                          # Lambda handler entry point
config.py                       # Environment/config
src/
  types.py                      # Pydantic models (TransferRecord, CalculationResult, etc.)
  enums.py                      # TransferType, RateType
  constants.py                  # EPSILON, ALLOCATION_TOLERANCE, OUTPUT_PREFIX
  calculator.py                 # calculate_amount(), evaluate()
  processor.py                  # EarningsTransferProcessor (orchestrator)
  writer.py                     # XLSX output (Adjustments/Summary/Errors sheets)
  errors.py                     # InputValidationError, TransientError
  parsers/
    earnings_transfer_parser.py # OWS API fetch, validation, Snowflake enrichment
  connectors/
    ows_royalties.py            # GET /earnings-transfers/ HTTP client
    snowflake_query.py          # Bulk contract data fetch
    s3.py                       # S3 upload
tests/
  unit/                         # Unit tests per module
  integration/                  # Integration tests (Docker environment)
```

## Setup

```bash
# From lambda/earnings_transfer/
uv sync --group dev
```

## Commands

```bash
make env_dev              # Install dev dependencies
make test                 # Unit tests with coverage
make lint                 # Linter check
make lint_fix             # Linter auto-fix
make format               # Format check
make format_fix           # Format auto-fix
make clean                # Remove caches and build artifacts

# Docker
make docker_login         # ECR login (required first time)
make docker_test          # Lint + unit tests in Docker
make test_integration     # Integration tests in Docker
make docker_local         # Run lambda locally (invoke with make local_event)
```

Single test file:

```bash
uv run pytest tests/unit/test_calculator.py
```

## UAT Testing

Run the automated UAT script to invoke the QA lambda and download the output:

```bash
make uat
```

This runs `scripts/uat.sh`, which assumes the `accounting-role` via awsume, invokes the QA lambda, extracts the `output_key` from the response, and downloads the output XLSX to `./ToE-lambda-output.xlsx`. You can pass a custom output path: `./scripts/uat.sh ./my-output.xlsx`.

### Manual steps

If you prefer to run the steps individually:

#### 1. Invoke the lambda

```bash
awsume accounting-role
aws lambda invoke \
  --function-name qa-lambda-abacus-earnings-transfer \
  --payload '{}' \
  --cli-binary-format raw-in-base64-out \
  /dev/stdout
```

The response includes `status`, `output_key`, and `record_count`. On validation errors it returns `status: ERROR` with the error message.

#### 2. Download the output file

The output XLSX is uploaded to the S3 bucket (default: `qa-abacus-earnings-transfer`, configured via `S3_BUCKET_NAME` env var). Use the `output_key` from the invoke response:

```bash
aws s3 cp s3://qa-abacus-earnings-transfer/<output_key> ./output.xlsx
```

### 3. Verify

- Open `output.xlsx` and check the **Adjustments** sheet has correct FROM/TO pairs
- Confirm amounts match expected calculations (percent: `balance * rate`, flat_rate: fixed amount)
- Check the **Summary** sheet for per-record calculation details
- Check the **Errors** sheet (if present) for records missing Snowflake data
- Check the **Input** and **Projected Balance** columns show the correct balance field and post-transfer balance

## Tech Stack

Python 3.13, uv, pydantic, openpyxl, owsclient, snowflake-connector-etl, boto3
