# Invoice Match (Agent 1)

Internal, **read-only** tool that reconciles the current month's planned billing
in the Project Planning sheet against the invoices actually raised in Odoo, and
flags every planned line **green / amber / red** for a person to review. It is
Agent 1 — the first bake-off payload for the client-agent-kit — built on
**Strands** and surfaced through a **CodeIgniter 4** UI, with **MySQL** for state,
all isolated in **Docker**.

See `docs/CONTRACT.md` for the authoritative schema, API and matching spec, and
`BUILD_PLAN.md` for the task breakdown and roadmap.

## Architecture

```
browser ──► web (nginx) ──► app (CodeIgniter 4 + Shield)  ──┐
                                   │                         │ shared
                                   ▼                         ▼
                          service (FastAPI + Strands) ───► mysql 8
                                   │
                        read-only: Odoo REST + model provider
```

- The browser only ever talks to the PHP app.
- The Python **service** is the only component that reaches Odoo or a model
  provider — both calls read-only and egress-controlled.
- PHP and the service share one MySQL instance for run state, mappings, memory
  and traces.
- Everything is one Docker Compose project on a private network; only the web
  container publishes a port.

**Read-only is enforced at three layers:** the Odoo credentials are read-only,
the agent tool set contains no writer, and the single write (the report Excel) is
code after the agent returns — never a model action. The prompt is never the
guardrail.

## Quick start

```bash
cp .env.example .env          # fill in MySQL passwords, Odoo + provider creds
docker compose up -d --build  # brings up web, app, service, mysql
# first boot: the app container waits for MySQL then runs migrations
docker compose exec app php spark migrate --all   # if not auto-run
docker compose exec app php spark db:seed InitialSeeder
open http://localhost:8080
```

Log in with the seeded admin (printed by the seeder), upload a month's Excel,
pick a provider, and press Run.

## Repository layout

| Path | What |
| --- | --- |
| `docs/CONTRACT.md` | Single source of truth: schema, API, SSE, matching, config |
| `service/` | Python FastAPI + Strands service (matching core, Odoo, agent, OTEL) |
| `service/app/matching/` | **Deterministic core** — pure (openpyxl + stdlib), offline-testable, reused by the Pi-track build |
| `app/` | CodeIgniter 4 + Shield UI and MySQL-owned state (migrations, memory) |
| `infra/nginx/` | Reverse-proxy config |
| `sample_data/` | Synthetic workbook + invoice/name-map fixtures for offline testing |
| `docker-compose.yml` | The one isolated project |

## Testing without external systems

The deterministic matching core needs no Odoo, DB or model. Generate fixtures and
run a full offline reconciliation:

```bash
python3 -m venv .venv && . .venv/bin/activate
pip install -r service/requirements.txt          # or just: pip install openpyxl pytest
python sample_data/generate_sample.py            # writes planning_sample.xlsx + JSON
cd service && pytest -q                           # unit tests for the core
python -m app.runner --offline \
  --sheet ../sample_data/planning_sample.xlsx \
  --invoices ../sample_data/invoices_sample.json \
  --name-map ../sample_data/name_map_sample.json \
  --month 2026-10                                 # prints the variance table
```

## Security & isolation

- Odoo account carries invoice-read scope only; the Planning sheet is an upload,
  never written back. No write path to either source.
- Secrets live in the environment / a secret store and are referenced
  (`connections.value_ref`), never stored in code or MySQL as plaintext.
- One Docker Compose project, private network, own volumes; depends on nothing on
  the host beyond Docker. Only the web container publishes a port.
- The workbook exposes company-wide revenue/targets, so uploads sit on an
  access-controlled volume and the UI is restricted to internal reviewers (Shield).

## Open questions

Four items in `docs/CONTRACT.md §7` carry chosen defaults pending Jaco's
confirmation (Software-invoice rule, Odoo REST flavour, unresolved-client policy,
and the green/amber amount tolerance). All are configuration, changeable without
code edits.
