feat: add CategoryImportController for bulk JSON import and export of categories
- Implemented export functionality to retrieve all rows from specified category tables. - Developed import functionality with strict validation and referential integrity checks. - Added error handling for various import scenarios including invalid formats and duplicate entries. - Introduced tests for import functionality to ensure correct behavior and validation.
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
# 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) |
|
||||
@@ -144,3 +144,10 @@ Delete a doctor service.
|
||||
| `ERR_AUTH_001` | 401 | Missing token |
|
||||
| `ERR_AUTH_006` | 403 | Not admin |
|
||||
| `ERR_NOT_FOUND_001` | 404 | Service not found |
|
||||
|
||||
---
|
||||
|
||||
## Bulk import / export
|
||||
|
||||
Full-table JSON export and strict wipe+replace import for this category live under
|
||||
`/api/v1/admin/categories/{bundle}/{export|import}` — see [category-import.md](category-import.md).
|
||||
|
||||
@@ -456,3 +456,10 @@ override پوشش یک خدمت خاص تحت قرارداد یک بیمه. فی
|
||||
خطاها: `404 ERR_NOT_FOUND_001` قرارداد یافت نشد · `422 ERR_VALIDATION_001` سرویس یافت نشد · `403 ERR_FORBIDDEN_001` سرویس متعلق به شما نیست.
|
||||
|
||||
> منطق resolve: `TenantInsuranceService::coverageRuleForService()` ابتدا **پرچم `ServiceItem.insurance_covered`** را چک میکند؛ اگر این خدمت «شامل بیمه» نباشد، بدون توجه به override یا قرارداد، `CoverageRule::notCovered()` برمیگردد (gate نهایی). سپس override خدمت بررسی میشود؛ اگر `covered=false` → `notCovered()`؛ در غیر این صورت فیلدهای null از قرارداد پر میشوند. این `CoverageRule` ورودی `BillingCalculator` است و سهم بیمهی هر `InvoiceItem` را تعیین میکند؛ همان سهمها در `ClaimService::createFromInvoice()` به `ClaimItem` (مطالبات بیمه) تبدیل میشوند. پنل ادمین این endpoint را از مودال «پوشش بیمه» در صفحه سرویسهای کلینیک فراخوانی میکند.
|
||||
|
||||
---
|
||||
|
||||
## Bulk import / export
|
||||
|
||||
Full-table JSON export and strict wipe+replace import for this category live under
|
||||
`/api/v1/admin/categories/{bundle}/{export|import}` — see [category-import.md](category-import.md).
|
||||
|
||||
@@ -247,3 +247,10 @@ Legacy endpoint that proxies to the new endpoints.
|
||||
|
||||
> ⚠️ Response is **triple-nested**: `data?.data?.data ?? []`
|
||||
> Note: `categorys` (not `categories`) is intentional — legacy route name.
|
||||
|
||||
---
|
||||
|
||||
## Bulk import / export
|
||||
|
||||
Full-table JSON export and strict wipe+replace import for this category live under
|
||||
`/api/v1/admin/categories/{bundle}/{export|import}` — see [category-import.md](category-import.md).
|
||||
|
||||
@@ -193,3 +193,10 @@ Delete a specialty.
|
||||
| `ERR_AUTH_001` | 401 | Missing token |
|
||||
| `ERR_AUTH_006` | 403 | Not admin |
|
||||
| `ERR_NOT_FOUND_001` | 404 | Specialty not found |
|
||||
|
||||
---
|
||||
|
||||
## Bulk import / export
|
||||
|
||||
Full-table JSON export and strict wipe+replace import for this category live under
|
||||
`/api/v1/admin/categories/{bundle}/{export|import}` — see [category-import.md](category-import.md).
|
||||
|
||||
@@ -130,3 +130,10 @@ Delete a tag.
|
||||
| `ERR_AUTH_001` | 401 | Missing token |
|
||||
| `ERR_AUTH_006` | 403 | Not admin |
|
||||
| `ERR_NOT_FOUND_001` | 404 | Tag not found |
|
||||
|
||||
---
|
||||
|
||||
## Bulk import / export
|
||||
|
||||
Full-table JSON export and strict wipe+replace import for this category live under
|
||||
`/api/v1/admin/categories/{bundle}/{export|import}` — see [category-import.md](category-import.md).
|
||||
|
||||
Reference in New Issue
Block a user