# County Onboarding — Tax Liens

How to add a new county tax-sale source to PropFlow.

## 1. Register the county

Counties → Add County. Provide:
- County name, state
- Sale type (tax lien / tax deed / hybrid)
- Source URL (official notice, portal, PDF, CSV)
- Parser key (slug used by the adapter — e.g. `nj-essex-pdf`, `fl-broward-portal`)

This inserts a row into `county_source` with `is_active = true`.

## 2. Author a parser adapter

Every county needs three functions behind `CountyAdapter`:

```ts
interface CountyAdapter {
  fetchSource(): Promise<SourceArtifact>
  parseSource(artifact: SourceArtifact): Promise<RawCountyRecord[]>
  normalize(records: RawCountyRecord[]): Promise<NormalizedTaxSaleRecord[]>
}
```

- `fetchSource` — downloads the source artifact and stores raw bytes in object storage. Returns `{ url, bytes, mime, fetched_at }`.
- `parseSource` — converts artifact to an array of flat records (key/value per column). Use `pdfplumber` for PDFs, `openpyxl` for xlsx, `pandas` for CSV, `playwright` for portals.
- `normalize` — maps raw columns to the canonical schema (`parcel_id_normalized`, `property_address`, `minimum_bid`, etc.).

Keep county-specific logic out of the core domain layer. Adapters live under `automation/scripts/tax-liens/adapters/<parser_key>.py`.

## 3. Test parser

Run:

```bash
python automation/scripts/tax-liens/run_adapter.py --county-id <uuid> --dry-run
```

Writes to staging only; does not touch `tax_sale_property`. Inspect output and iterate until normalized records are clean.

## 4. First live run

When happy:

```bash
python automation/scripts/tax-liens/run_adapter.py --county-id <uuid>
```

Writes to `tax_sale_property` via upsert on `(county_source_id, parcel_id_raw, tax_sale_event_id)`. Logs a row in `county_run_log`.

## 5. Schedule

Set `schedule_cron` on the `county_source` row (e.g. `0 6 * * *` for daily at 6 AM). The n8n scheduler picks up active counties on that cadence.

## Environment variables

- `SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY` — writes to lien tables
- `ATTOM_API_KEY`, `RENTCAST_API_KEY` — enrichment
- `GEOCODER_API_KEY` — address → lat/lon
- `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID` — alerts

## Troubleshooting

- Parser fails on layout change → snapshot artifact, open in new issue, adjust adapter.
- Empty result set → confirm source URL hasn't moved; check `county_run_log.error_summary`.
- Duplicates across runs → tighten upsert key (usually `parcel_id_normalized + sale_date`).
