Files
clinicpro/docs/api/category-import.md
T
hamed 9eb5a03258 feat: add CategoryImporter service and SeedCategoriesCommand for seeding category data
- Implemented CategoryImporter service to handle bulk export/import logic for categories.
- Created SeedCategoriesCommand to seed category tables from JSON files in data/seed/.
- Added validation and normalization for category data during import.
- Ensured proper error handling and user feedback during the seeding process.
2026-06-30 21:58:44 +03:30

140 lines
5.0 KiB
Markdown

# Category Import / Export API
> **Prefix:** `/api/v1/admin/categories/{bundle}`
Bulk JSON export and import for the admin **«دسته‌بندی‌ها»** page (`CategoriesPage`),
backed by `App\Category\Controller\CategoryImportController`.
`{bundle}` is one of: `provinces`, `cities`, `specialties`, `doctor_services`,
`insurances`, `tags`.
**Permission:** `ROLE_ADMIN` (whole controller).
Each bundle maps to one table:
| bundle | table | has `slug` | has `weight` | references |
|---|---|:---:|:---:|---|
| `provinces` | `provinces` | ❌ | ✅ | — |
| `cities` | `cities` | ❌ | ✅ | `province_id` → provinces, `representation_id` → representations |
| `specialties` | `specialties` | ✅ | ✅ | `parent_id` → specialties (same file) |
| `doctor_services` | `doctor_services` | ✅ | ✅ | `specialty_id` → specialties |
| `insurances` | `insurances` | ❌ | ❌ | `type` ∈ {`basic`,`supplementary`} |
| `tags` | `tags` | ✅ | ❌ | — |
---
## GET `/api/v1/admin/categories/{bundle}/export`
Complete export of a bundle — **every** row, no pagination cap. Returns the raw
table columns (the same keys an import expects), so export → import is lossless.
> ⚠️ Do **not** use the paginated admin list endpoints (e.g. `/api/v1/admin/specialties?limit=9999`)
> as an export source for import: those cap at 100 rows, and feeding a truncated
> file into the wipe+replace import below deletes every row beyond the first 100.
### Response `200`
```json
{
"success": true,
"data": [
{ "id": 1, "uuid": "…", "name": "قلب", "slug": "ghalb", "status": 1, "weight": 0, "parent_id": null }
]
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_BUNDLE_UNKNOWN` | 404 | Unknown bundle |
---
## POST `/api/v1/admin/categories/{bundle}/import`
**Wipe + full replace** of the bundle's table from the uploaded JSON, inside a
single transaction (FK checks disabled during the swap). Raw `id` values from the
file are preserved so relations stay intact.
Validation is **strict and two-phase**: the whole file is validated first; if
**any** row is invalid, nothing is written and every error is returned. Unknown
extra keys (e.g. `province_name` produced by export) are ignored.
### Request Body (`application/json`)
A non-empty array of records, or `{ "items": [ … ] }`. Max **5000** rows.
```json
[
{ "id": 1, "name": "قلب", "slug": "ghalb", "status": 1, "weight": 0, "parent_id": null },
{ "id": 2, "name": "آریتمی", "slug": "arr", "status": 1, "weight": 1, "parent_id": 1 }
]
```
### Per-row validation rules
| Field | Rule |
|---|---|
| `id` | required, positive integer, unique within the file |
| `name` | required, non-empty string ≤ 255 |
| `status` | `0` or `1` (default `1`) |
| `weight` | integer ≥ 0 — bundles with a weight column only |
| `slug` | required, ≤ 255, unique within file — `specialties`/`doctor_services`/`tags` |
| `type` | `basic` or `supplementary``insurances` only |
| `uuid` | optional; generated if missing |
| `parent_id` | `specialties`: null, or an `id` present **in the same file** |
| `specialty_id` | `doctor_services`: null, or an existing `specialties.id` |
| `province_id` / `representation_id` | `cities`: null, or an existing id in the referenced table |
### Response `200`
```json
{ "success": true, "data": { "imported": 128 } }
```
### Response `422` (validation failed — nothing written)
```json
{
"success": false,
"data": null,
"errors": [
{ "row": 2, "field": "parent_id", "message": "ردیف 2: والد با شناسه 999 در همین فایل وجود ندارد" }
]
}
```
### Errors
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_BUNDLE_UNKNOWN` | 404 | Unknown bundle |
| `ERR_IMPORT_FORMAT` | 422 | Body is not a non-empty array of records |
| `ERR_IMPORT_TOO_LARGE` | 422 | More than 5000 rows |
| (field errors) | 422 | One entry per failed row/field; no rows written |
| `ERR_IMPORT_FAILED` | 500 | DB error during replace (transaction rolled back) |
---
## Seeding a from-scratch database
Canonical category data ships in the repo at `data/seed/*.json`
(`provinces.json`, `cities.json`, `specialties.json`, `doctor_services.json`).
After creating an empty schema, populate it with:
```bash
ddev exec php bin/console app:seed-categories
```
The command (`App\Category\Command\SeedCategoriesCommand`) reads those files and
runs the same strict validation + wipe-and-replace as the HTTP import, in
dependency order (provinces → cities, specialties → doctor_services). The shared
logic lives in `App\Category\Service\CategoryImporter`.
Full rebuild:
```bash
ddev exec php bin/console doctrine:database:drop --force
ddev exec php bin/console doctrine:database:create
ddev exec php bin/console doctrine:migrations:migrate --no-interaction
ddev exec php bin/console app:seed-categories
ddev exec php bin/console app:create-admin 09100000001 'Test@1234'
```