# @classytic/bd-areas

Bangladesh delivery taxonomy as static data — zero network, zero API calls. Two entry points:

- **`@classytic/bd-areas`** — unified shape (8 divisions, 64 districts, 2,836 areas with RedX IDs). Best for cascading address dropdowns built around the RedX area model.
- **`@classytic/bd-areas/pathao`** — Pathao-native taxonomy (64 cities, 1,258 zones). Best for Pathao-only flows (cascading dropdowns, CSV exports). ~80 KB instead of ~400 KB.

Both subpaths are independent — import only the one your UI needs and tree-shaking does the rest.

## Install

```bash
npm install @classytic/bd-areas
```

---

## Subpath: `@classytic/bd-areas` (unified)

Used by RedX flows. `Area` records have a `providers.redx` numeric ID for every entry; `providers.pathao` and `providers.steadfast` are mostly empty (Pathao IDs don't fit the unified `Area` model — see the `/pathao` subpath instead).

```ts
import {
  getDivisions, getDistrictsByDivision, getAreasByDistrict,
  getArea, searchAreas, resolveArea,
} from '@classytic/bd-areas';

const divisions = getDivisions();              // 8 entries
const districts = getDistrictsByDivision('dhaka');
const areas = getAreasByDistrict('dhaka');     // [{ internalId, name, providers: { redx }, ... }]
const area = getArea(1206);                    // by internalId
const matches = searchAreas('mirpur');         // free-text, default limit 20
const full = resolveArea(1206);                // adds full division + district objects
```

### API summary

| Function | Purpose |
|---|---|
| `getDivisions()` / `getDivisionById(id)` / `getDivisionByName(name)` | 8 BD divisions |
| `getDistrictsByDivision(divisionId)` / `getDistrictById(id)` / `getAllDistricts()` | 64 districts |
| `getArea(internalId)` / `getAllAreas()` | 2,836 areas |
| `getAreasByDistrict(id)` / `getAreasByDivision(id)` / `getAreasByPostCode(code)` | filters |
| `searchAreas(query, limit?)` | free-text autocomplete |
| `getAreaByProvider('redx', id)` | inverse lookup |
| `convertProviderId('redx', id, 'pathao')` | cross-provider (Pathao mostly null today) |
| `resolveArea(internalId)` | area + full division + district |
| `getStats()` | dataset coverage report |

### Types

```ts
interface Area {
  internalId: number;       // store this in your DB
  name: string;
  postCode: number | null;
  zoneId: number;            // internal zone (1-6) for tier pricing
  districtId: string;
  districtName: string;
  divisionId: string;
  divisionName: string;
  providers: { redx?: number; pathao?: number; steadfast?: number };
}
```

---

## Subpath: `@classytic/bd-areas/pathao` (Pathao-native)

A static snapshot of `merchant.pathao.com` — exactly the data the Pathao web UI uses to populate its own dropdowns. Use this for any Pathao flow:

- Cascading city → zone dropdowns in checkout / address forms
- CSV bulk-upload exports (Pathao's CSV uses **city + zone names**, not IDs)
- Validating recipient city/zone before calling the Pathao API

```ts
import {
  PATHAO_CITIES,           // PathaoCity[] — alphabetical, all 64 cities
  PATHAO_ZONES_BY_CITY,    // Record<cityId, PathaoZone[]>
  findCity,                // by cityId
  findCityByName,          // case-insensitive, falls back to substring
  getZonesByCity,          // PathaoZone[] for a city
  findZone,                // (cityId, zoneId)
  findZoneByName,          // (cityId, name)
  searchZones,             // cross-city zone search, returns { ...zone, city }
  getPathaoStats,          // diagnostic counts per city
} from '@classytic/bd-areas/pathao';

// Cascading dropdown
const [cityId, setCityId] = useState<number>();
const zones = cityId ? getZonesByCity(cityId) : [];

// CSV row from address-with-IDs
const city = findCity(addr.cityId);
const zone = city ? findZone(city.cityId, addr.zoneId) : undefined;
csv.push({ RecipientCity: city?.cityName, RecipientZone: zone?.zoneName });
```

### Why a separate subpath?

- **Granularity mismatch.** Pathao zones are landmark/neighborhood-level (Gulshan, Banani) — they don't map 1:1 to the unified `Area` shape (which is thana-level). Forcing them into one shape loses information.
- **Bundle weight.** A Pathao-only checkout shouldn't ship 2,836 RedX areas (~400 KB unified) — the `/pathao` subpath is ~80 KB.
- **Update cadence.** Pathao adds zones every few months; refreshing this subpath doesn't require touching the unified dataset.

### Refresh: snapshot script

The `/pathao` data is regenerated by `scripts/snapshot-pathao-merchant.mjs`. Pathao changes infrequently — run this manually every few months and republish.

```bash
# 1. Get a fresh JWT from https://merchant.pathao.com (DevTools → any
#    address-search request → copy `Authorization: Bearer <token>`)
# 2. Set in .env:
PATHAO_MERCHANT_TOKEN=<that token>

# 3. Run (resumable, hard-fails on rate limit, ~3 min):
node scripts/snapshot-pathao-merchant.mjs

# 4. Commit src/pathao/cities.ts + src/pathao/zones.ts + bump version.
```

The snapshot writes after every city, so a mid-run rate-limit just resumes from `_progress.json` on the next run. Pass `PATHAO_SNAPSHOT_MAX_CITIES=10` for a smoke run.

---

## Tests

Three tiers per `testing-infrastructure.md`. Single `vitest.config.ts` with `projects: [unit, integration, e2e]`.

| Command | What runs |
|---|---|
| `npm test` | unit + integration (default) — pure data assertions + cross-package shape tests |
| `npm run test:unit` | dataset shape, helpers |
| `npm run test:integration` | uses real `RedXAdapter` + composite resolver via mocked fetch — proves the dataset is shaped correctly for `@classytic/carrier-bd` |
| `npm run test:e2e` | env-gated live Pathao audit — reports drift between bd-areas and the live merchant API |

`tests/unit` and `tests/integration` MUST stay network-free.

---

## Best practices

### Storing addresses

```ts
// Unified (RedX) — store internalId
await saveAddress({ areaId: area.internalId, areaName: area.name, ... });

// Pathao — store cityId + zoneId
await saveAddress({ pathaoCityId: city.cityId, pathaoZoneId: zone.zoneId, ... });
```

### Calling carrier APIs (via `@classytic/carrier-bd`)

```ts
import { getArea } from '@classytic/bd-areas';
import { findZone } from '@classytic/bd-areas/pathao';

// RedX
const area = getArea(savedAddress.areaId);
await redxAdapter.buyLabel({ destination, packages, metadata: {
  deliveryAreaId: area.providers.redx,
  deliveryAreaName: area.name,
} }, ctx);

// Pathao
await pathaoAdapter.buyLabel({ destination, packages, metadata: {
  deliveryCityId: savedAddress.pathaoCityId,
  deliveryZoneId: savedAddress.pathaoZoneId,
} }, ctx);
```

## License

MIT © [Classytic](https://github.com/classytic)
