Skip to content

Femi236/Team-9-Hackathon

Repository files navigation

War Child Mailroom

A transparent, rules-driven donor communication engine built to replace opaque Salesforce flows. Upload a CSV of donor events, watch every message get matched to a rule and previewed before delivery, then commit — emails go out via SES, postal letters are dispatched via Pingen, and a Salesforce-importable send-log drops as a CSV.

Built in 24 hours at a hackathon by Team Mailroom.

Live demo: https://mailroom.nalam.chat/


Why it matters

War Child's donor communications were locked inside opaque Salesforce automation — hard to understand, harder to change, and impossible to audit. Mailroom replaces that with a system the team can actually own:

  • Solves the real problem — donor communication logic is now visible, versioned, and editable by the War Child team without touching Salesforce configuration or involving a developer.
  • Rules in plain English — describe a rule in natural language ("send a formal welcome letter to new donors giving more than €100 per month") and the LLM interprets it into a structured rule instantly. No JSON required.
  • Template editor anyone can use — a clean in-browser editor lets non-technical staff write and update email and letter content, with live merge-field highlighting and a per-donor preview before anything goes out.
  • See exactly what will be sent before you send it — every CSV upload shows a full dry-run: which rule matched each donor, which template they'll receive, and a condition-by-condition trace explaining why. No surprises.
  • Low-friction Salesforce migration — input is a standard Salesforce CSV export, output is a Salesforce-importable send-log. No API credentials, no custom connectors — just the export file you already have.

What it does

  1. Rules engine — JSON-defined rules with AND/OR condition trees, evaluated in priority order (first match wins). Create rules through the UI or describe them in plain English and let the LLM interpret them.
  2. Template engine — Liquid templates with Dutch merge fields ({{ contact.first_name }}, {{ agreement.amount | money }}). Supports {{ ai: "Write a warm opener for {{ contact.first_name }}" }} variables resolved by Claude Haiku at send time.
  3. Dry-run preview — Before any message goes out, every CSV row is matched, rendered, and shown in a table with per-row trace (which conditions passed/failed and why).
  4. Commit & send — Email via AWS SES, postal letters via Pingen (opt-in), send-log written to Postgres and downloadable as CSV.
  5. Idempotency — Re-uploading the same CSV skips already-processed rows; same run ID returns the original log.

Tech stack

Layer Choice
Backend Python 3.12, FastAPI, SQLAlchemy 2 + asyncpg
Database PostgreSQL (Supabase in prod, Docker locally)
Templating python-liquid + custom money / date_nl filters
PDF WeasyPrint
Email AWS SES (SMTP fallback for local dev)
Postal Pingen API (opt-in, staging by default)
LLM Anthropic SDK — Sonnet 4.6 (NL→rule), Haiku 4.5 (AI template vars)
Frontend Vite + React + TypeScript
Deploy AWS Lambda (ARM64) + API Gateway + S3/CloudFront via SAM

Running locally

Prerequisites

  • Docker + Docker Compose
  • An Anthropic API key (optional — AI features degrade gracefully without it)

Steps

# 1. Clone and enter
git clone https://github.com/Femi236/Team-9-Hackathon
cd Team-9-Hackathon

# 2. Configure environment
cp .env.example .env
# Edit .env — at minimum set ANTHROPIC_API_KEY

# 3. Start API + Postgres
docker compose up --build

# API is now at http://localhost:8001
# Frontend dev server (separate):
cd frontend && npm install && npm run dev
# Frontend at http://localhost:5173

Seed demo data

# Load 8 Dutch templates + 4 sample rules (idempotent, safe to run multiple times)
curl -X POST http://localhost:8001/api/v1/seed

Run the demo pipeline

# Upload the sample CSV (7 rows, all 4 event types)
curl -X POST http://localhost:8001/api/v1/runs?dry_run=true \
  -F file=@tests/fixtures/sample.csv | python3 -m json.tool

Or use the web UI: upload tests/fixtures/sample.csv, review the dry-run table, click Send all.


API overview

All endpoints are under /api/v1.

Method Path Description
POST /runs?dry_run=true Preview: parse CSV, match rules, return per-row trace
POST /runs Commit: send emails/letters, write send-log
GET /runs/{run_id} Run details + per-row log
GET /runs/{run_id}/send-log.csv Download Salesforce-importable CSV
GET /rules List rules (priority order)
POST /rules Create rule
PUT /rules/{id} Update rule
DELETE /rules/{id} Delete rule
POST /rules/parse-nl Natural language → rule JSON (Claude Sonnet)
GET /templates List templates
POST /templates Create template
PUT /templates/{id} Update template
GET /schema Full field schema (DonorContext paths)
POST /seed Load demo templates + rules
POST /gdpr/erase Anonymise all send-log records for a donor (GDPR Art. 17)

Rule schema

Rules are stored as JSON with three sections: trigger, conditions, action.

{
  "name": "High-value new agreement — formal postal",
  "enabled": true,
  "priority": 200,
  "trigger": { "event_type": "agreement.new" },
  "conditions": {
    "type": "group",
    "op": "AND",
    "conditions": [
      { "type": "field_condition", "field": "agreement.amount_cents", "operator": "gte", "value": 10000 },
      { "type": "field_condition", "field": "contact.communication_style", "operator": "eq", "value": "formal" }
    ]
  },
  "action": { "channel": "postal", "template_id": "welcome_formal" }
}

Supported operators: eq, neq, gt, gte, lt, lte, in, not_in, contains, starts_with, exists, not_exists

Event types: agreement.new, agreement.change, agreement.resignation, payment.confirmation


MCP server

Mailroom exposes an MCP (Model Context Protocol) server at /mcp — attach Claude Desktop or Cursor and create rules and templates with natural language directly from your AI assistant.

See MCP_SETUP.md for connection instructions and available tools.


Project structure

mailroom/
├── api/routers/        # FastAPI routers (rules, templates, runs, brand, auth, seed, webhooks)
├── engine/             # Rule evaluator, condition tree, operator registry
├── ingest/             # CSV reader + mapper (Salesforce columns → DonorContext)
├── actions/            # send_email, generate_pdf, registry
├── templates/          # Liquid renderer with AI variable resolution
├── models/             # SQLAlchemy ORM (Rule, Template, SendLog, BrandSettings)
└── tests/              # 87 test cases (evaluator, conditions, operators, mapper, idempotency)

frontend/src/
├── screens/            # RulesScreen, TemplatesScreen, RunScreen, BrandScreen
├── components/         # Shell, primitives (Button, Badge, Toggle, ChannelIcon…)
└── api.ts              # Typed API client

Environment variables

See .env.example for the full list with documentation. Key vars:

Variable Required Description
DATABASE_URL Yes PostgreSQL connection string (asyncpg)
ANTHROPIC_API_KEY Recommended Powers NL→rule and AI template vars; degrades gracefully without it
EMAIL_BACKEND Yes ses (prod) or smtp (local)
SES_FROM_ADDRESS If SES Verified SES sender address
PINGEN_ENABLED No Set true to enable physical postal dispatch

Tests

pip install -e ".[dev]"
pytest mailroom/tests/ -v

87 tests covering: rule evaluation, condition operators, CSV mapper, template rendering, idempotency.


GDPR / PII compliance

Mailroom is designed to handle donor PII responsibly and comply with Dutch data protection law (AVG/GDPR).

Right to erasure (Art. 17 GDPR)

The GDPR screen in the dashboard accepts a Salesforce Donor ID and anonymises all send log records for that donor on request:

  • donor_id is replaced with a one-way SHA-256 hash (anon:{first 12 hex chars})
  • agreement_id, payment_id, rule trace data, and error messages are nulled out
  • The audit row itself (run ID, event type, channel, template, status, timestamps) is retained — required for financial audit compliance under Dutch law (7-year retention obligation)
  • anonymised_at is recorded as a tamper-evident timestamp

API endpoint:

POST /api/v1/gdpr/erase
Content-Type: application/json

{ "donor_id": "003Wc00000B4e5fGH6" }

Response:

{
  "donor_id_requested": "003Wc00000B4e5fGH6",
  "rows_anonymised": 4,
  "message": "Successfully anonymised 4 record(s). Donor ID replaced with anon:7f3c9a1b2e8d."
}

What PII Mailroom stores

Donor PII only flows into send_log during a run — it is never written to rules or templates. The fields stored per row are: donor_id, agreement_id, payment_id, rule trace (which may contain field values), and any error messages. All of these are erased by the erasure endpoint above.

Retention defaults

Data Retention Reason
send_log audit row 7 years Dutch financial compliance
PII fields in send_log Until erasure request Erased on Art. 17 request
Templates / rules Indefinite No PII content

Postal delivery via Pingen

Opt-in physical postal dispatch. When PINGEN_ENABLED=true, the postal branch renders the template to PDF (WeasyPrint) and uploads to Pingen. Staging endpoints are used by default — a misconfigured run cannot hit the production API.

On failure: a warning is logged and the row is still recorded as success (PDF is on disk).

Setup: sign up at app-staging.pingen.com, create an OAuth2 client, copy the 9 PINGEN_* vars from .env.example.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors