# Handoff #2 Response — Mukesh / Inteligenz

**Date:** 2026-09-07  
**In reply to:** `HANDOFF 2 — For Mukesh, Block-Lot Diagnosis and Step 1 API Requirements (2026-09-07).md`  
**Repo:** `https://github.com/Inteligenz-codebase/WestrockAttom.git`

---

## Authorization — fetchSoundscoreData()

**Authorized.** Transportation Noise / Soundscore calls are disabled by default.

- `config/attom.php` → `transportation_noise_enabled` (default `false`)
- `.env` → `ATTOM_TRANSPORTATION_NOISE_ENABLED=false`
- `fetchSoundscoreData()` returns `null` immediately when disabled — no live calls to the unlicensed package

---

## §2 PRIORITY ONE — Block and lot search

### 1. Elasticsearch status

| Check | Result |
|-------|--------|
| Configured URL | `https://34.132.0.223:9200` (from `.env` `ELASTIC_URL`) |
| API key present | Yes (`ELASTIC_API_KEY`) |
| Reachable from dev machine | **No** — connection timed out after 10s on `/_cluster/health` |
| Document count | **Could not query** — cluster unreachable from this environment |

**Conclusion:** S. Romer's hypothesis is supported. Block/lot search is ES-first; when ES is down, the app falls through to ATTOM fallback via `recoverResultsWhenElasticsearchUnavailable()` and `tryLegalIdAttomFallback()`. Address search can still work via direct ATTOM paths; block/lot was failing because **NJ APN variant formats were incomplete**, not because the fallback path was missing.

### 2. Block/lot UI tab and field names

| Item | Answer |
|------|--------|
| Tab | **Legal ID** (`property-search.blade.php`, sub-tab `legal-id`) |
| Form action | `GET /property-search/results` with hidden `search_tab=legal-id` |
| Block field | `name="block"` |
| Lot field | `name="lot"` |
| State / county | Hidden fields `state`, `ddlCountiesByState` (from header location picker) |
| Also on tab | `apn`, `parcel_id`, `bbl`, `cook_pin`, `attom_id` |

**Important:** For non-NYC block/lot (e.g. NJ), the user **must select State + County** in the header before searching. Without county, `resolveLegalIdFipsList()` returns no FIPS and ATTOM fallback cannot run.

### 3. Live test result (verbatim)

**Note on test case:** Handoff suggested Bloomfield Block **1000** Lot **26**. ATTOM's record for the Race Street subject (`13 RACE ST # 27`) actually carries **Block 313, Lot 32**, APN `02-00313-0000-00032`, AttomId `190242422`. Block 1000 / Lot 26 returns ATTOM `total=0`.

**Request (simulated Legal ID search):**

```
GET /property-search/results
  search_tab=legal-id
  block=313
  lot=32
  state=NJ
  ddlCountiesByState=Essex
```

**ATTOM APN variants tried (before fix):** `1000-26`, `01000-0026` → all `total=0`  
**Root cause:** NJ uses `{muni}-{block5}-0000-{lot5}` (e.g. `02-00313-0000-00032`). `buildBlockLotApnVariants()` only produced `{block}-{lot}` padded forms.

**Fix applied:** Extended `ParcelLookupService::buildBlockLotApnVariants()` for NJ to emit municipal-prefixed variants (`01-00313-0000-00032` through `30-...`).

**Result after fix:**

```
REDIRECT: http://127.0.0.1:8001/property/190242422?from=legal-id&state=NJ&ddlCountiesByState=Essex
```

Resolves to the Race Street subject property via pure ATTOM (no Elasticsearch).

**Step 1 API equivalent (live-tested):**

```
POST /api/v1/property/resolve
  job_number=TEST-JOB-002
  block=313, lot=32, state=NJ, county=Essex
→ attom_id=190242422, matched_variant=apn:02-00313-0000-00032@fips:34013
```

### 4. ES blocker / next design step

- **Do not block on ES** for block/lot in NJ — ATTOM APN path works once variants are correct.
- **NYC** may still benefit from ES when populated; ATTOM BBL/APN fallback remains in `tryLegalIdAttomFallback()`.
- **Recommend:** Prioritize ATTOM-first block/lot for NJ (and extend municipal prefix table or county→muni mapping later to reduce API calls). Discuss before building a parallel path.

---

## §3 PRIORITY TWO — Step 1 API surface

**Status: implemented and live-tested on dev.**

All four endpoints are wired under `/api/v1/...` with Bearer token auth, job-number tracking, per-token rate limits, and monthly ATTOM call ceilings. Every call is logged to `property_search_logs` with `search_source = api`, `job_number`, and `api_token_id`.

### Route shapes

| Method | Path | Scope | Notes |
|--------|------|-------|-------|
| `POST` | `/api/v1/property/resolve` | `property:resolve` | Address, block/lot, APN, BBL, or attom_id |
| `GET` | `/api/v1/property/{attomId}/verification-bundle` | `property:verification` | Sales history, assessment, allevents, expanded profile |
| `GET` | `/api/v1/property/{attomId}/comparables` | `property:comparables` | Paginated (`page`, `per_page`, `miles`, `sale_date_range`) |
| `GET` | `/api/v1/property/{attomId}/assessment-history` | `property:assessment` | Assessment history only |

### Common requirements (every call)

| Requirement | How |
|-------------|-----|
| **Job number** | Required — body field `job_number`, query param, or header `X-WestRock-Job-Number` |
| **Auth** | `Authorization: Bearer wrk_...` |
| **Raw ATTOM payload** | Append `?raw=1` (or `"raw": true` on POST resolve) |
| **Rate limit** | Per-token, default 60 req/min |
| **Monthly ceiling** | Per-token, default 3000 ATTOM calls/month |

### Resolve payload (`POST /api/v1/property/resolve`)

```json
{
  "job_number": "JOB-12345",
  "address": "13 Race Street, Bloomfield, NJ 07003",
  "address1": "13 RACE ST # 27",
  "address2": "BLOOMFIELD, NJ 07003",
  "block": "313",
  "lot": "32",
  "state": "NJ",
  "county": "Essex",
  "apn": "02-00313-0000-00032",
  "bbl": "",
  "attom_id": "190242422",
  "classification": "",
  "raw": false
}
```

**Success response (200):**

```json
{
  "job_number": "JOB-12345",
  "match": { "...ATTOM property object..." },
  "matched_variant": "attom_id:190242422",
  "attom_id": "190242422",
  "confidence": "exact"
}
```

**Not found (404):** `{ "job_number": "...", "match": null, "confidence": "not_found" }`

### Comparables query params

`page` (default 1), `per_page` (default 50, max 500), `miles` (default 2.0), `sale_date_range` (default 24 months), `raw`, `job_number`

### Token provisioning

Tokens are issued via Artisan on the server:

```bash
php artisan api:issue-token step1-client --step1
```

This creates a token named `step1-client` with read-only scopes:

- `property:resolve`
- `property:verification`
- `property:comparables`
- `property:assessment`

The plain-text token is shown once at issuance. Revoking a token does not affect other tokens or dashboard sessions.

### Ask for Mukesh — dedicated Step 1 token

When you review these route shapes and we move to production provisioning, **please generate a dedicated token for the Step 1 client**:

- **Its own token** — name it `step1-client`; do not reuse a shared or dashboard token
- **Read-only scoped** — the four scopes above only; no write or admin access
- **Independently revocable** — so Step 1 access can be cut off without affecting the UI, other integrations, or other API consumers

Command on the production host:

```bash
php artisan api:issue-token step1-client --step1
```

Deliver the plain-text token to S. Romer / Main365 through a secure channel (not email or Slack in clear text). Store only the hash in the database; the prefix (`wrk_xxxxxxxx`) is enough to identify it in logs.

---

## §4 PRIORITY THREE — Pagination

No hidden technical blocker found — snapshot pagination was implemented as page-1-only, likely for cost/latency during UI development. Endpoint #3 (`/comparables`) now supports caller-controlled pagination with `page`, `per_page`, and `has_more` in the response. Default ceiling is 500 comps per request; callers can loop pages for larger sets.

---

## §5 Carry-overs

| Item | Status |
|------|--------|
| **fetchSoundscoreData()** | **Done** — disabled (see above) |
| **`.env` line 7 empty `ATTOM_API_KEY`** | **Done** — removed duplicate empty keys |
| **`BatchLookupController`** | **Intentionally not restored** — see note below |
| **Step 1 API (`/api/v1/*`)** | **Done** — implemented, tested, token system in place |
| **ATTOM success rate (50 lookups)** | Pending post-deploy monitoring |
| **`verify => false` cleanup** | In progress — removed from `fetchAttomPropertyDetailByApnFips()`; broader pass still TODO |
| **`.env` in ShareSync** | With S. Romer — unchanged |
| **ATTOM Ts&Cs PDF** | Still gated — with S. Romer |

### Batch lookup — decision

The batch CSV upload/download routes (`/property-search/batch`) are **left removed**. Rationale:

- There is **no UI** wired to these routes — no button, link, or form points to them
- Restoring the controller only stops a 500 if someone hits the URL directly; nobody currently does
- The **Step 1 API** (`/api/v1/...`) is the intended path for "pull a lot of comp data for one job" — job-tracked, paginated, rate-limited, built for Main365's full-population goal
- A revived ad-hoc "paste CSV, download CSV" panel would be a second, cruder duplicate that would likely go stale when the API ships
- **`BatchLookupService` remains in the codebase** — restoring routes later is a small task if appraisal staff specifically need a manual bulk tool independent of the API

---

## §6 Working arrangements (GitHub read access)

Noted. Mukesh/S. Romer decision — no action taken from build side.

---

## §1 Correction on B.2

Acknowledged — Oversight verifies **per-parcel** via ATTOM (`saleshistory/expandedhistory`, etc.), not per-population from this middleware. Population comes from CoStar/Main365. B.2 finding stands; inference corrected per Handoff #2 §1.

---

## Files added/changed (Step 1 API + follow-ups)

**New**

- `app/Models/ApiToken.php`
- `app/Services/ApiTokenService.php`
- `app/Services/AttomGatewayService.php`
- `app/Services/Step1PropertyApiService.php`
- `app/Http/Controllers/Api/V1/PropertyController.php`
- `app/Http/Middleware/AuthenticateApiToken.php`
- `app/Http/Middleware/RequireApiJobNumber.php`
- `app/Console/Commands/IssueApiTokenCommand.php`
- `database/migrations/2026_09_07_000005_create_api_tokens_table.php`
- `database/migrations/2026_09_07_000006_add_api_fields_to_property_search_logs_table.php`
- `tests/Feature/Step1PropertyApiTest.php`

**Updated**

- `routes/api.php` — v1 Step 1 routes
- `bootstrap/app.php` — `api.token`, `api.job` middleware aliases
- `config/attom.php` — API rate limit / monthly ceiling defaults
- `app/Services/PropertySearchLogService.php` — `SOURCE_API`, `job_number`, `api_token_id`
- `app/Models/PropertySearchLog.php` — fillable + `apiToken()` relation
- `routes/web.php` — batch lookup routes removed (intentional)

---

*Prepared by Mukesh Ahuja / Inteligenz — 2026-09-07*
