# Alemeno Payments

[![Python Version](https://img.shields.io/badge/python-3.14-blue?style=flat-square&logo=python)](file:///home/sortedcord/Projects/alemno_payments/pyproject.toml)
[![FastAPI](https://img.shields.io/badge/FastAPI-0.139-009688?style=flat-square&logo=fastapi)](file:///home/sortedcord/Projects/alemno_payments/app/app.py)
[![Test CI Status](https://github.com/sortedcord/alemno-payments/actions/workflows/tests.yml/badge.svg)](https://github.com/sortedcord/alemno-payments/actions/workflows/tests.yml)
[![Lint CI Status](https://github.com/sortedcord/alemno-payments/actions/workflows/lint.yml/badge.svg)](https://github.com/sortedcord/alemno-payments/actions/workflows/lint.yml)
[![Docker CI Status](https://github.com/sortedcord/alemno-payments/actions/workflows/docker.yml/badge.svg)](https://github.com/sortedcord/alemno-payments/actions/workflows/docker.yml)
[![Security CI Status](https://github.com/sortedcord/alemno-payments/actions/workflows/security.yml/badge.svg)](https://github.com/sortedcord/alemno-payments/actions/workflows/security.yml)

An asynchronous transactional data cleaning, validation, and LLM powered categorization pipeline.

## Features

- Reads multi format dates and handles missing transaction IDs by auto generating unique fallbacks, and parses messy amounts with currency signs and commas cleanly.
- Offloads validation, spend conversion, and analysis to a Celery background worker backed by Redis.
- Integrates with the Google GenAI SDK (`gemini-2.5-flash-lite` 75% cheaper than `gemini-3.5-flash-lite with no tangible decrease in performance) using structured output schemas to categorize transaction records into database- defined custom categories.
- Uses Redis caching with a 24-hour TTL to store live exchange rates, falling back to a hardcoded rate of `93.0` if offline.
- Statistical Anomaly Detection:
  - Flags transactions exceeding 3 times the account's median spend as statistical outliers.
  - Flags domestic transactions in USD currency.
  - Flags high value transactions and transactions with fraud indicative keywords.

[![High Level Overview](./docs/assets/Overview.svg)](https://drive.google.com/file/d/1yk0-pXClrOp0ccTiaO7tbXPHZg4EH1eE/view?usp=sharing)

[Technical Video](https://www.youtube.com/watch?v=Xjfyf1ct-0U)

_Click on the image to view the [Draw.io](https://drive.google.com/file/d/1yk0-pXClrOp0ccTiaO7tbXPHZg4EH1eE/view?usp=sharing) file_

## Self Hosting & Deployment

We publish production-ready images to **GitHub Container Registry (GHCR)**. You can pull them directly or run them via Docker Compose.

Create a `docker-compose.yml` file to host the entire stack (or just clone the repo and directly run `docker compose up -d` :

```yaml
version: "3.8"

services:
  db:
    image: postgres:16-alpine
    container_name: alemno_db
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: alemno_payments
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -d alemno_payments"]
      interval: 5s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    container_name: alemno_redis
    ports:
      - "6379:6379"

  web:
    image: ghcr.io/sortedcord/alemno-payments/web:latest
    container_name: alemno_web
    ports:
      - "3000:3000"
    environment:
      - POSTGRES_HOST=db
      - POSTGRES_PORT=5432
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=alemno_payments
      - REDIS_HOST=redis
      - REDIS_PORT=6379
      - REDIS_DB=0
      - GEMINI_API_KEY=your-gemini-api-key
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started

  worker:
    image: ghcr.io/sortedcord/alemno-payments/worker:latest
    container_name: alemno_worker
    environment:
      - POSTGRES_HOST=db
      - POSTGRES_PORT=5432
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=alemno_payments
      - REDIS_HOST=redis
      - REDIS_PORT=6379
      - REDIS_DB=0
      - GEMINI_API_KEY=your-gemini-api-key
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started

volumes:
  postgres_data:
```

Launch the stack:

```bash
docker compose up -d
```

## Testing the API with curl

All API endpoints are prefixed with `/api/v1`. Below are copy-pasteable `curl` commands using the sample [transactions.csv](file:///home/sortedcord/Projects/alemno_payments/transactions.csv) dataset.

### 1. Upload CSV Transaction Data

Submit a CSV file for asynchronous processing.

```bash
curl -X POST -F "file=@transactions.csv" http://localhost:3000/api/v1/jobs/upload
```

_Response Example:_

```json
{
  "filename": "transactions.csv",
  "id": "c0249af1-c52d-4d49-9305-3d1a10f29e03",
  "status": "pending",
  "row_count_raw": null,
  "row_count_clean": null,
  "created_at": "2026-07-03T06:29:13.123456Z",
  "completed_at": null,
  "error_message": null
}
```

### 2. Check Job Status

Check the status of a job using the job ID returned by the upload endpoint.

```bash
curl http://localhost:3000/api/v1/jobs/c0249af1-c52d-4d49-9305-3d1a10f29e03/status
```

_Response Example (while processing):_

```json
{
  "id": "c0249af1-c52d-4d49-9305-3d1a10f29e03",
  "status": "processing",
  "summary": null
}
```

### 3. Get Full Job Results

Retrieve the high-level summary along with all categorized and parsed transactions once the job is completed.

```bash
curl http://localhost:3000/api/v1/jobs/c0249af1-c52d-4d49-9305-3d1a10f29e03/results
```

### 4. List All Jobs

List all historical processing jobs. You can optionally filter by status (e.g. `pending`, `processing`, `completed`, `failed`).

```bash
# List all jobs
curl http://localhost:3000/api/v1/jobs

# List completed jobs only
curl "http://localhost:3000/api/v1/jobs?status=completed"
```

### 5. List Categories

List all available transaction categories sorted alphabetically.

```bash
curl http://localhost:3000/api/v1/categories
```

### 6. Create Custom Category

Add a new custom category. Newly uploaded transactions will dynamically map to this category during classification if identified by the LLM.

```bash
curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"name": "Subscription"}' \
  http://localhost:3000/api/v1/categories
```
