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:
@@ -1,9 +1,9 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
TrashIcon, PlusIcon, PencilIcon, TagIcon, MapPinIcon,
|
||||
BuildingOffice2Icon, HeartIcon, ShieldCheckIcon, WrenchScrewdriverIcon,
|
||||
PhotoIcon, XMarkIcon, ArrowDownTrayIcon,
|
||||
PhotoIcon, XMarkIcon, ArrowDownTrayIcon, ArrowUpTrayIcon, ExclamationTriangleIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
@@ -21,6 +21,8 @@ import SearchableSelect from '../components/ui/SearchableSelect';
|
||||
|
||||
type TabKey = 'provinces' | 'cities' | 'specialties' | 'doctor_services' | 'insurances' | 'tags';
|
||||
|
||||
interface ImportError { row?: number; field?: string; message: string }
|
||||
|
||||
interface TabConfig {
|
||||
key: TabKey;
|
||||
label: string;
|
||||
@@ -131,14 +133,21 @@ async function exportJson(url: string, filename: string, token: string | null) {
|
||||
|
||||
// ── Tab sub-component wrapper ──────────────────────────────────────────────────
|
||||
|
||||
function TabActions({ label, onClick, exportUrl, exportFile }: {
|
||||
function TabActions({ label, onClick, exportUrl, exportFile, bundle, entityLabel, onImported }: {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
exportUrl: string;
|
||||
exportFile: string;
|
||||
bundle: TabKey;
|
||||
entityLabel: string;
|
||||
onImported: () => void;
|
||||
}) {
|
||||
const token = useAuthStore((s) => s.token);
|
||||
const [exporting, setExporting] = useState(false);
|
||||
const [importing, setImporting] = useState(false);
|
||||
const [errors, setErrors] = useState<ImportError[] | null>(null);
|
||||
const [pendingItems, setPendingItems] = useState<unknown[] | null>(null);
|
||||
const fileRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleExport = async () => {
|
||||
setExporting(true);
|
||||
@@ -151,8 +160,62 @@ function TabActions({ label, onClick, exportUrl, exportFile }: {
|
||||
}
|
||||
};
|
||||
|
||||
const handleFile = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
e.target.value = '';
|
||||
if (!file) return;
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(await file.text());
|
||||
} catch {
|
||||
setErrors([{ message: 'فایل یک JSON معتبر نیست' }]);
|
||||
return;
|
||||
}
|
||||
let items: unknown[] | null = null;
|
||||
if (Array.isArray(parsed)) items = parsed;
|
||||
else if (parsed && typeof parsed === 'object' && Array.isArray((parsed as { items?: unknown[] }).items)) {
|
||||
items = (parsed as { items: unknown[] }).items;
|
||||
}
|
||||
if (!items || items.length === 0) {
|
||||
setErrors([{ message: 'فایل باید یک آرایهی غیرخالی از رکوردها باشد' }]);
|
||||
return;
|
||||
}
|
||||
setPendingItems(items);
|
||||
};
|
||||
|
||||
const doImport = async () => {
|
||||
if (!pendingItems) return;
|
||||
setImporting(true);
|
||||
try {
|
||||
const res = await fetch(`/api/v1/admin/categories/${bundle}/import`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) },
|
||||
body: JSON.stringify(pendingItems),
|
||||
});
|
||||
const json = await res.json().catch(() => ({}));
|
||||
if (res.ok && json.success) {
|
||||
toast.success(`${json.data?.imported ?? 0} رکورد با موفقیت وارد شد`);
|
||||
setPendingItems(null);
|
||||
onImported();
|
||||
} else {
|
||||
setErrors(json.errors?.length ? json.errors : [{ message: json.errors?.[0]?.message ?? 'خطا در ورود اطلاعات' }]);
|
||||
setPendingItems(null);
|
||||
}
|
||||
} catch {
|
||||
setErrors([{ message: 'خطا در ارتباط با سرور' }]);
|
||||
setPendingItems(null);
|
||||
} finally {
|
||||
setImporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, padding: 'var(--card-pad)' }}>
|
||||
<input ref={fileRef} type="file" accept=".json,application/json" style={{ display: 'none' }} onChange={handleFile} />
|
||||
<button onClick={() => fileRef.current?.click()} className="btn ghost sm">
|
||||
<ArrowUpTrayIcon style={{ width: 15, height: 15 }} />
|
||||
ورود JSON
|
||||
</button>
|
||||
<button onClick={handleExport} disabled={exporting} className="btn ghost sm">
|
||||
<ArrowDownTrayIcon style={{ width: 15, height: 15 }} />
|
||||
{exporting ? 'در حال دانلود...' : 'خروجی JSON'}
|
||||
@@ -160,6 +223,38 @@ function TabActions({ label, onClick, exportUrl, exportFile }: {
|
||||
<button onClick={onClick} className="btn primary sm">
|
||||
<PlusIcon style={{ width: 15, height: 15 }} /> {label}
|
||||
</button>
|
||||
|
||||
<ConfirmDialog
|
||||
open={!!pendingItems}
|
||||
title={`ورود ${entityLabel} از فایل`}
|
||||
message={`این عملیات همهی رکوردهای فعلی این بخش را حذف و با ${pendingItems?.length ?? 0} رکورد فایل جایگزین میکند. این کار بازگشتناپذیر است. ادامه میدهید؟`}
|
||||
confirmLabel="حذف و جایگزینی"
|
||||
danger
|
||||
loading={importing}
|
||||
onConfirm={doImport}
|
||||
onCancel={() => setPendingItems(null)}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
open={!!errors}
|
||||
title="فایل وارد نشد — خطاهای اعتبارسنجی"
|
||||
size="md"
|
||||
onClose={() => setErrors(null)}
|
||||
footer={<button onClick={() => setErrors(null)} className="btn ghost sm">بستن</button>}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12, color: 'var(--danger)' }}>
|
||||
<ExclamationTriangleIcon style={{ width: 18, height: 18, flexShrink: 0 }} />
|
||||
<span style={{ fontSize: 13 }}>به دلیل خطاهای زیر هیچ تغییری اعمال نشد. فایل را اصلاح و دوباره تلاش کنید.</span>
|
||||
</div>
|
||||
<div style={{ maxHeight: 360, overflow: 'auto', display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
{(errors ?? []).map((er, idx) => (
|
||||
<div key={idx} style={{ fontSize: 12, padding: '8px 10px', borderRadius: 8, background: 'var(--surface-2)', display: 'flex', gap: 8 }}>
|
||||
{er.field && <span className="chip" style={{ flexShrink: 0 }}>{er.field}</span>}
|
||||
<span style={{ color: 'var(--text-2)' }}>{er.message}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -228,7 +323,7 @@ function ProvincesTab() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<TabActions label="افزودن استان" onClick={() => { reset({ status: '1', weight: '0' }); setAddOpen(true); }} exportUrl="/api/v1/admin/provinces?limit=9999" exportFile="state.json" />
|
||||
<TabActions label="افزودن استان" onClick={() => { reset({ status: '1', weight: '0' }); setAddOpen(true); }} exportUrl="/api/v1/admin/categories/provinces/export" exportFile="state.json" bundle="provinces" entityLabel="استانها" onImported={() => qc.invalidateQueries({ queryKey: ['admin-provinces'] })} />
|
||||
<DataTable<Province> columns={columns} data={items} loading={isLoading} searchValue={search} onSearchChange={(v) => { setSearch(v); setPage(1); }} searchPlaceholder="جستجو در استانها..." emptyMessage="هیچ استانی یافت نشد"
|
||||
actions={(p) => (
|
||||
<>
|
||||
@@ -388,7 +483,7 @@ function CitiesTab() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<TabActions label="افزودن شهر" onClick={() => { reset({ status: '1', weight: '0' }); setAddOpen(true); }} exportUrl="/api/v1/admin/cities?limit=9999" exportFile="city.json" />
|
||||
<TabActions label="افزودن شهر" onClick={() => { reset({ status: '1', weight: '0' }); setAddOpen(true); }} exportUrl="/api/v1/admin/categories/cities/export" exportFile="city.json" bundle="cities" entityLabel="شهرها" onImported={() => qc.invalidateQueries({ queryKey: ['admin-cities'] })} />
|
||||
<DataTable<City> columns={columns} data={items} loading={isLoading} searchValue={search} onSearchChange={(v) => { setSearch(v); setPage(1); }} searchPlaceholder="جستجو در شهرها..." emptyMessage="هیچ شهری یافت نشد"
|
||||
actions={(c) => (
|
||||
<>
|
||||
@@ -548,7 +643,7 @@ function SpecialtiesTab() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<TabActions label="افزودن تخصص" onClick={() => { reset({ status: '1', weight: '0' }); setAddOpen(true); }} exportUrl="/api/v1/admin/specialties?limit=9999" exportFile="specialties.json" />
|
||||
<TabActions label="افزودن تخصص" onClick={() => { reset({ status: '1', weight: '0' }); setAddOpen(true); }} exportUrl="/api/v1/admin/categories/specialties/export" exportFile="specialties.json" bundle="specialties" entityLabel="تخصصها" onImported={() => { qc.invalidateQueries({ queryKey: ['admin-specialties'] }); qc.invalidateQueries({ queryKey: ['admin-specialties-roots'] }); }} />
|
||||
<DataTable<SpecialtyFull> columns={columns} data={items} loading={isLoading} searchValue={search} onSearchChange={(v) => { setSearch(v); setPage(1); }} searchPlaceholder="جستجو در تخصصها..." emptyMessage="هیچ تخصصی یافت نشد"
|
||||
actions={(s) => (
|
||||
<>
|
||||
@@ -669,7 +764,7 @@ function DoctorServicesTab() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<TabActions label="افزودن خدمت" onClick={() => { reset({ status: '1', weight: '0' }); setAddOpen(true); }} exportUrl="/api/v1/admin/doctor-services?limit=9999" exportFile="doctor-services.json" />
|
||||
<TabActions label="افزودن خدمت" onClick={() => { reset({ status: '1', weight: '0' }); setAddOpen(true); }} exportUrl="/api/v1/admin/categories/doctor_services/export" exportFile="doctor-services.json" bundle="doctor_services" entityLabel="خدمات پزشک" onImported={() => qc.invalidateQueries({ queryKey: ['admin-doctor-services'] })} />
|
||||
<DataTable<DoctorService> columns={columns} data={items} loading={isLoading} searchValue={search} onSearchChange={(v) => { setSearch(v); setPage(1); }} searchPlaceholder="جستجو در خدمات..." emptyMessage="هیچ خدمتی یافت نشد"
|
||||
actions={(s) => (
|
||||
<>
|
||||
@@ -795,7 +890,7 @@ function InsurancesTab() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<TabActions label="افزودن بیمه" onClick={() => { reset({ status: '1', type: 'basic' }); setLogoUrl(null); setUploadTarget(null); setAddOpen(true); }} exportUrl="/api/v1/admin/insurances?limit=9999" exportFile="insurances.json" />
|
||||
<TabActions label="افزودن بیمه" onClick={() => { reset({ status: '1', type: 'basic' }); setLogoUrl(null); setUploadTarget(null); setAddOpen(true); }} exportUrl="/api/v1/admin/categories/insurances/export" exportFile="insurances.json" bundle="insurances" entityLabel="بیمهها" onImported={() => qc.invalidateQueries({ queryKey: ['admin-insurances'] })} />
|
||||
<DataTable<Insurance> columns={columns} data={items} loading={isLoading} searchValue={search} onSearchChange={(v) => { setSearch(v); setPage(1); }} searchPlaceholder="جستجو در بیمهها..." emptyMessage="هیچ بیمهای یافت نشد"
|
||||
actions={(i) => (
|
||||
<>
|
||||
@@ -909,7 +1004,7 @@ function TagsTab() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<TabActions label="افزودن تگ" onClick={() => { reset({ status: '1' }); setAddOpen(true); }} exportUrl="/api/v1/admin/tags?limit=9999" exportFile="tags.json" />
|
||||
<TabActions label="افزودن تگ" onClick={() => { reset({ status: '1' }); setAddOpen(true); }} exportUrl="/api/v1/admin/categories/tags/export" exportFile="tags.json" bundle="tags" entityLabel="تگها" onImported={() => qc.invalidateQueries({ queryKey: ['admin-tags'] })} />
|
||||
<DataTable<Tag> columns={columns} data={items} loading={isLoading} searchValue={search} onSearchChange={(v) => { setSearch(v); setPage(1); }} searchPlaceholder="جستجو در تگها..." emptyMessage="هیچ تگی یافت نشد"
|
||||
actions={(t) => (
|
||||
<>
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -653,27 +653,15 @@
|
||||
"651": "Community 651",
|
||||
"652": "Community 652",
|
||||
"653": "Community 653",
|
||||
"654": "Community 654",
|
||||
"655": "Community 655",
|
||||
"656": "Community 656",
|
||||
"657": "Community 657",
|
||||
"658": "Community 658",
|
||||
"659": "Community 659",
|
||||
"660": "Community 660",
|
||||
"661": "Community 661",
|
||||
"662": "Community 662",
|
||||
"663": "Community 663",
|
||||
"664": "Community 664",
|
||||
"665": "Community 665",
|
||||
"666": "Community 666",
|
||||
"667": "Community 667",
|
||||
"668": "Community 668",
|
||||
"669": "Community 669",
|
||||
"670": "Community 670",
|
||||
"671": "Community 671",
|
||||
"672": "Community 672",
|
||||
"673": "Community 673",
|
||||
"674": "Community 674",
|
||||
"675": "Community 675",
|
||||
"676": "Community 676"
|
||||
"672": "Community 672"
|
||||
}
|
||||
|
||||
+103
-151
@@ -1,16 +1,16 @@
|
||||
# Graph Report - clinicpro (2026-06-29)
|
||||
# Graph Report - clinicpro (2026-06-30)
|
||||
|
||||
## Corpus Check
|
||||
- 654 files · ~450,645 words
|
||||
- 657 files · ~453,738 words
|
||||
- Verdict: corpus is large enough that graph structure adds value.
|
||||
|
||||
## Summary
|
||||
- 8274 nodes · 11437 edges · 677 communities (549 shown, 128 thin omitted)
|
||||
- 8314 nodes · 11496 edges · 665 communities (540 shown, 125 thin omitted)
|
||||
- Extraction: 98% EXTRACTED · 2% INFERRED · 0% AMBIGUOUS · INFERRED: 264 edges (avg confidence: 0.8)
|
||||
- Token cost: 0 input · 0 output
|
||||
|
||||
## Graph Freshness
|
||||
- Built from commit: `830f7e8d`
|
||||
- Built from commit: `80319610`
|
||||
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
|
||||
- Run `graphify update .` after code changes (no API cost).
|
||||
|
||||
@@ -655,32 +655,20 @@
|
||||
- [[_COMMUNITY_Community 651|Community 651]]
|
||||
- [[_COMMUNITY_Community 652|Community 652]]
|
||||
- [[_COMMUNITY_Community 653|Community 653]]
|
||||
- [[_COMMUNITY_Community 654|Community 654]]
|
||||
- [[_COMMUNITY_Community 655|Community 655]]
|
||||
- [[_COMMUNITY_Community 656|Community 656]]
|
||||
- [[_COMMUNITY_Community 657|Community 657]]
|
||||
- [[_COMMUNITY_Community 658|Community 658]]
|
||||
- [[_COMMUNITY_Community 659|Community 659]]
|
||||
- [[_COMMUNITY_Community 660|Community 660]]
|
||||
- [[_COMMUNITY_Community 661|Community 661]]
|
||||
- [[_COMMUNITY_Community 662|Community 662]]
|
||||
- [[_COMMUNITY_Community 663|Community 663]]
|
||||
- [[_COMMUNITY_Community 664|Community 664]]
|
||||
- [[_COMMUNITY_Community 665|Community 665]]
|
||||
- [[_COMMUNITY_Community 666|Community 666]]
|
||||
- [[_COMMUNITY_Community 667|Community 667]]
|
||||
- [[_COMMUNITY_Community 668|Community 668]]
|
||||
- [[_COMMUNITY_Community 669|Community 669]]
|
||||
- [[_COMMUNITY_Community 671|Community 671]]
|
||||
- [[_COMMUNITY_Community 672|Community 672]]
|
||||
- [[_COMMUNITY_Community 673|Community 673]]
|
||||
- [[_COMMUNITY_Community 674|Community 674]]
|
||||
- [[_COMMUNITY_Community 675|Community 675]]
|
||||
- [[_COMMUNITY_Community 676|Community 676]]
|
||||
|
||||
## God Nodes (most connected - your core abstractions)
|
||||
1. `BaseController` - 74 edges
|
||||
2. `ApiTestCase` - 70 edges
|
||||
1. `BaseController` - 76 edges
|
||||
2. `ApiTestCase` - 72 edges
|
||||
3. `api` - 55 edges
|
||||
4. `UserProfile` - 52 edges
|
||||
5. `Clinic` - 50 edges
|
||||
@@ -693,55 +681,55 @@
|
||||
## Surprising Connections (you probably didn't know these)
|
||||
- `ServiceTariffModal()` --calls--> `formatNumber()` [EXTRACTED]
|
||||
assets/admin/components/ServiceTariffModal.tsx → assets/admin/lib/utils.ts
|
||||
- `Pagination()` --calls--> `formatNumber()` [EXTRACTED]
|
||||
assets/admin/components/ui/Pagination.tsx → assets/admin/lib/utils.ts
|
||||
- `SettlementsPage()` --calls--> `formatRial()` [EXTRACTED]
|
||||
assets/admin/pages/SettlementsPage.tsx → assets/admin/lib/utils.ts
|
||||
- `LogsPage()` --calls--> `formatDateTime()` [EXTRACTED]
|
||||
assets/admin/pages/LogsPage.tsx → assets/admin/lib/utils.ts
|
||||
- `PersianDatePicker()` --calls--> `formatDate()` [EXTRACTED]
|
||||
assets/admin/components/ui/PersianDatePicker.tsx → assets/admin/lib/utils.ts
|
||||
- `SmsPage()` --calls--> `formatDateTime()` [EXTRACTED]
|
||||
assets/admin/pages/SmsPage.tsx → assets/admin/lib/utils.ts
|
||||
- `PrivateRoute()` --calls--> `useAuthStore` [EXTRACTED]
|
||||
assets/admin/App.tsx → assets/admin/stores/authStore.ts
|
||||
- `PublicRoute()` --calls--> `useAuthStore` [EXTRACTED]
|
||||
assets/admin/App.tsx → assets/admin/stores/authStore.ts
|
||||
|
||||
## Import Cycles
|
||||
- None detected.
|
||||
|
||||
## Communities (677 total, 128 thin omitted)
|
||||
## Communities (665 total, 125 thin omitted)
|
||||
|
||||
### Community 0 - "Community 0"
|
||||
Cohesion: 0.05
|
||||
Nodes (43): ALLOWED_ROLES, PrivateRoute(), PublicRoute(), RoleRoute(), queryClient, toastStyle, AddForm, addSchema (+35 more)
|
||||
Cohesion: 0.04
|
||||
Nodes (46): ALLOWED_ROLES, PrivateRoute(), PublicRoute(), RoleRoute(), queryClient, toastStyle, NewAppointmentModal(), LogoUploadField() (+38 more)
|
||||
|
||||
### Community 1 - "Community 1"
|
||||
Cohesion: 0.03
|
||||
Nodes (41): AddressData, AddrForm, addrSchema, AVATAR_COLORS, BookingMeta, CityOpt, DateOverrideData, DEFAULT_BOOKING_META (+33 more)
|
||||
|
||||
### Community 2 - "Community 2"
|
||||
Cohesion: 0.06
|
||||
Nodes (29): Contract, InsuranceOption, KIND_LABEL, get, api, ApiError, getToken(), refreshOnce() (+21 more)
|
||||
Cohesion: 0.10
|
||||
Nodes (19): get, BeforeInstallPromptEvent, usePwaInstall(), api, ApiError, getToken(), refreshOnce(), request() (+11 more)
|
||||
|
||||
### Community 3 - "Community 3"
|
||||
Cohesion: 0.05
|
||||
Nodes (42): ACTION_HEADERS, ALL_ACTIONS, ClinicDoctor, CreateForm, createSchema, DEFAULT_PERMISSIONS, PERMISSION_LABELS, PermSection (+34 more)
|
||||
Nodes (43): PaginatedResponse, formatDate(), STATUS_FILTERS, FILTERS, Breakdown, SOURCE_LABEL, Summary, STATUS_FILTERS (+35 more)
|
||||
|
||||
### Community 4 - "Community 4"
|
||||
Cohesion: 0.08
|
||||
Nodes (5): Doctor, Collection, self, User, WeeklySchedule
|
||||
Cohesion: 0.05
|
||||
Nodes (11): DoctorServiceController, DoctorService, Doctor, DoctorServiceRepository, Collection, self, User, WeeklySchedule (+3 more)
|
||||
|
||||
### Community 5 - "Community 5"
|
||||
Cohesion: 0.07
|
||||
Nodes (3): UserProfile, self, User
|
||||
|
||||
### Community 6 - "Community 6"
|
||||
Cohesion: 0.12
|
||||
Nodes (12): SettlementController, SettlementRepository, WalletTransactionRepository, Settlement, JsonResponse, Request, User, ManagerRegistry (+4 more)
|
||||
Cohesion: 0.17
|
||||
Nodes (8): SettlementController, SettlementRepository, Settlement, JsonResponse, Request, User, ManagerRegistry, User
|
||||
|
||||
### Community 7 - "Community 7"
|
||||
Cohesion: 0.07
|
||||
Nodes (5): Clinic, Collection, Doctor, self, User
|
||||
|
||||
### Community 8 - "Community 8"
|
||||
Cohesion: 0.07
|
||||
Nodes (28): useSubscription(), AdminLayout(), avatarBg(), buildSections(), HUES, Props, ROLE_LABELS, Section (+20 more)
|
||||
Cohesion: 0.08
|
||||
Nodes (30): useSubscription(), AdminLayout(), avatarBg(), buildSections(), HUES, Props, ROLE_LABELS, Section (+22 more)
|
||||
|
||||
### Community 9 - "Community 9"
|
||||
Cohesion: 0.04
|
||||
@@ -752,16 +740,16 @@ Cohesion: 0.04
|
||||
Nodes (46): Clinic Address Management, Clinic API, DELETE `/api/v1/admin/clinic/{clinicUuid}/doctor/{doctorUuid}`, `DELETE /api/v1/clinic/{clinicUuid}/address/{addressUuid}`, Errors, Errors, Errors, Errors (+38 more)
|
||||
|
||||
### Community 11 - "Community 11"
|
||||
Cohesion: 0.08
|
||||
Nodes (21): PaginatedResponse, STATUS_FILTERS, FILTERS, Breakdown, SOURCE_LABEL, Summary, LEVEL_FILTER_OPTIONS, LEVEL_META (+13 more)
|
||||
Cohesion: 0.06
|
||||
Nodes (35): FreeVisitPrice(), Pricing, usePaymentConfig(), ApiResponse, formatRial(), InsurancePricingPage(), FinancialSummary, MonthlyEntry (+27 more)
|
||||
|
||||
### Community 12 - "Community 12"
|
||||
Cohesion: 0.05
|
||||
Nodes (44): Clinic Doctor Invitation API, DELETE `/api/v1/admin/clinic/invitation/{invUuid}`, Errors, Errors, Errors, Errors, Errors, Errors (+36 more)
|
||||
|
||||
### Community 13 - "Community 13"
|
||||
Cohesion: 0.06
|
||||
Nodes (37): cn(), formatDate(), formatDateTime(), iranMobileOptionalSchema, iranMobileSchema, isValidIranMobile(), maskMobile(), sanitizeMobileInput() (+29 more)
|
||||
Cohesion: 0.08
|
||||
Nodes (28): cn(), formatDateTime(), iranMobileOptionalSchema, iranMobileSchema, isValidIranMobile(), maskMobile(), sanitizeMobileInput(), toDate() (+20 more)
|
||||
|
||||
### Community 14 - "Community 14"
|
||||
Cohesion: 0.10
|
||||
@@ -772,8 +760,8 @@ Cohesion: 0.10
|
||||
Nodes (13): PaymentController, MockGateway, SepGateway, JsonResponse, Payment, PaymentGatewayInterface, Request, Response (+5 more)
|
||||
|
||||
### Community 16 - "Community 16"
|
||||
Cohesion: 0.07
|
||||
Nodes (7): PatientSession, SmsWallet, Appointment, Collection, PatientRecord, self, SessionService
|
||||
Cohesion: 0.09
|
||||
Nodes (6): PatientSession, Appointment, Collection, PatientRecord, self, SessionService
|
||||
|
||||
### Community 17 - "Community 17"
|
||||
Cohesion: 0.05
|
||||
@@ -784,20 +772,20 @@ Cohesion: 0.05
|
||||
Nodes (38): API, API, API, API, API, Route, Route, Route (+30 more)
|
||||
|
||||
### Community 19 - "Community 19"
|
||||
Cohesion: 0.20
|
||||
Nodes (8): AdminUser, ChangeRoleModal(), getPrimaryRole(), HUES_LIST, ROLE_META, ROLE_TABS, RoleBadge(), UserStats
|
||||
Cohesion: 0.07
|
||||
Nodes (21): Contract, InsuranceOption, KIND_LABEL, ALL_STATUSES, AppointmentDetailPage(), FormData, schema, AdminUser (+13 more)
|
||||
|
||||
### Community 20 - "Community 20"
|
||||
Cohesion: 0.15
|
||||
Nodes (3): AdminApiController, JsonResponse, Request
|
||||
|
||||
### Community 21 - "Community 21"
|
||||
Cohesion: 0.04
|
||||
Nodes (49): FreeVisitPrice(), Pricing, formatNumber(), formatRial(), ClinicDetailPage(), AdminCharts, AdminDashboard(), AdminRecent (+41 more)
|
||||
Cohesion: 0.05
|
||||
Nodes (34): formatNumber(), ClinicDetailPage(), AdminCharts, AdminDashboard(), AdminRecent, AdminStats, APPT_CLS, APPT_COLOR (+26 more)
|
||||
|
||||
### Community 22 - "Community 22"
|
||||
Cohesion: 0.11
|
||||
Nodes (14): RatingController, Like, Rate, LikeRepository, RateRepository, JsonResponse, Request, User (+6 more)
|
||||
Cohesion: 0.15
|
||||
Nodes (9): RatingController, Like, LikeRepository, JsonResponse, Request, User, Comment, ManagerRegistry (+1 more)
|
||||
|
||||
### Community 23 - "Community 23"
|
||||
Cohesion: 0.05
|
||||
@@ -825,7 +813,7 @@ Nodes (4): Appointment, Doctor, self, User
|
||||
|
||||
### Community 29 - "Community 29"
|
||||
Cohesion: 0.06
|
||||
Nodes (33): DELETE `/api/v1/admin/city/{id}`, DELETE `/api/v1/admin/province/{id}`, Errors, Errors, Errors, Errors, GET `/api/v1/admin/cities`, GET `/api/v1/admin/provinces` (+25 more)
|
||||
Nodes (34): Bulk import / export, DELETE `/api/v1/admin/city/{id}`, DELETE `/api/v1/admin/province/{id}`, Errors, Errors, Errors, Errors, GET `/api/v1/admin/cities` (+26 more)
|
||||
|
||||
### Community 30 - "Community 30"
|
||||
Cohesion: 0.08
|
||||
@@ -848,8 +836,8 @@ Cohesion: 0.06
|
||||
Nodes (34): require, doctrine/doctrine-bundle, doctrine/doctrine-migrations-bundle, doctrine/orm, ext-ctype, ext-iconv, lexik/jwt-authentication-bundle, nelmio/api-doc-bundle (+26 more)
|
||||
|
||||
### Community 35 - "Community 35"
|
||||
Cohesion: 0.07
|
||||
Nodes (27): Errors, Errors, Errors, Errors, Errors, Errors, FinancialBreakdown (لاگ مالی), GET `/api/v1/settlement` (+19 more)
|
||||
Cohesion: 0.06
|
||||
Nodes (32): Errors, Errors, Errors, Errors, Errors, Errors, Errors, FinancialBreakdown (لاگ مالی) (+24 more)
|
||||
|
||||
### Community 36 - "Community 36"
|
||||
Cohesion: 0.06
|
||||
@@ -872,8 +860,8 @@ Cohesion: 0.09
|
||||
Nodes (4): User, PasswordAuthenticatedUserInterface, self, UserInterface
|
||||
|
||||
### Community 41 - "Community 41"
|
||||
Cohesion: 0.03
|
||||
Nodes (56): usePaymentConfig(), CityForm, citySchema, InsuranceForm, insuranceSchema, LogoUploadField(), ProvinceForm, provinceSchema (+48 more)
|
||||
Cohesion: 0.04
|
||||
Nodes (41): CityForm, citySchema, ImportError, InsuranceForm, insuranceSchema, ProvinceForm, provinceSchema, ServiceForm (+33 more)
|
||||
|
||||
### Community 42 - "Community 42"
|
||||
Cohesion: 0.07
|
||||
@@ -904,8 +892,8 @@ Cohesion: 0.11
|
||||
Nodes (4): Payment, Appointment, self, User
|
||||
|
||||
### Community 49 - "Community 49"
|
||||
Cohesion: 0.09
|
||||
Nodes (18): AppointmentsPage(), BookingSlot, CancelledBadge(), DateNavigator(), EMPTY_ARR, getPersianWeekDay(), navBtnSx, NewAppointmentModal() (+10 more)
|
||||
Cohesion: 0.07
|
||||
Nodes (20): AppointmentsPage(), BookingSlot, CancelledBadge(), DateNavigator(), EMPTY_ARR, getPersianWeekDay(), navBtnSx, SessionGroup (+12 more)
|
||||
|
||||
### Community 50 - "Community 50"
|
||||
Cohesion: 0.07
|
||||
@@ -957,7 +945,7 @@ Nodes (22): Billing API — صورتحساب (فاز ۴ سیستم صورتحس
|
||||
|
||||
### Community 62 - "Community 62"
|
||||
Cohesion: 0.08
|
||||
Nodes (23): DELETE `/api/v1/admin/specialty/{id}`, Errors, Errors, Errors, Errors, GET `/api/v1/admin/specialties`, GET `/api/v1/specialties`, GET `/api/v1/specialties/doctor-counts` (+15 more)
|
||||
Nodes (20): Authentication, ClinicPro — API Documentation Index, Error Code Reference, Modules, Standard Response Envelope, Bulk import / export, DELETE `/api/v1/admin/specialty/{id}`, Errors (+12 more)
|
||||
|
||||
### Community 63 - "Community 63"
|
||||
Cohesion: 0.22
|
||||
@@ -971,6 +959,10 @@ Nodes (4): SmsWalletController, JsonResponse, Request, User
|
||||
Cohesion: 0.08
|
||||
Nodes (24): 2. کاربر (User), 4. 🔵 `POST` verify code, 5. 🔵 `POST` send code, 6. 🔵 `POST` register, 7. 🔴 `DELETE` delete user, 8. 🟡 `PATCH` patch, 9. 🟢 `GET` list secretary, Request Body (+16 more)
|
||||
|
||||
### Community 66 - "Community 66"
|
||||
Cohesion: 0.07
|
||||
Nodes (4): ClinicStaff, SmsSettings, self, self
|
||||
|
||||
### Community 67 - "Community 67"
|
||||
Cohesion: 0.11
|
||||
Nodes (4): Invoice, Collection, InvoiceItem, self
|
||||
@@ -1007,6 +999,10 @@ Nodes (6): DoctorController, Doctor, DoctorAddress, JsonResponse, Request, User
|
||||
Cohesion: 0.16
|
||||
Nodes (5): DoctorSecretary, Clinic, Doctor, self, User
|
||||
|
||||
### Community 77 - "Community 77"
|
||||
Cohesion: 0.10
|
||||
Nodes (13): Claim, ClaimItem, DebtRow, InsuranceOption, KIND_LABEL, STATUS_FILTERS, STATUS_META, IbanItem (+5 more)
|
||||
|
||||
### Community 78 - "Community 78"
|
||||
Cohesion: 0.12
|
||||
Nodes (3): SubscriptionPeriod, self, SubscriptionPlan
|
||||
@@ -1020,8 +1016,8 @@ Cohesion: 0.09
|
||||
Nodes (22): dependencies, @ckeditor/ckeditor5-build-classic, @ckeditor/ckeditor5-react, @fontsource/vazirmatn, @heroicons/react, @hookform/resolvers, jalaali-js, leaflet (+14 more)
|
||||
|
||||
### Community 82 - "Community 82"
|
||||
Cohesion: 0.06
|
||||
Nodes (12): RepositoryClassMappingTest, KernelTestCase, LoggerInterface, DbLogger, KavehNegarProvider, RanginehProvider, ApiIrService, SmsService (+4 more)
|
||||
Cohesion: 0.05
|
||||
Nodes (14): CategoryImportTest, Connection, RepositoryClassMappingTest, KernelTestCase, LoggerInterface, DbLogger, KavehNegarProvider, RanginehProvider (+6 more)
|
||||
|
||||
### Community 83 - "Community 83"
|
||||
Cohesion: 0.09
|
||||
@@ -1036,8 +1032,8 @@ Cohesion: 0.07
|
||||
Nodes (29): devDependencies, @babel/core, @babel/preset-env, @babel/preset-react, @babel/preset-typescript, core-js, @hotwired/stimulus, jsdom (+21 more)
|
||||
|
||||
### Community 86 - "Community 86"
|
||||
Cohesion: 0.06
|
||||
Nodes (15): AppointmentExpiryServiceTest, SendCodeMobileRateLimitTest, ClaimsListPaginationTest, ServiceItemDeleteCleanupTest, ServiceItemStaffOwnershipTest, EntityManagerInterface, TenantInsuranceCleanupTest, KernelBrowser (+7 more)
|
||||
Cohesion: 0.07
|
||||
Nodes (13): AppointmentExpiryServiceTest, LowTierFixesTest, SendCodeMobileRateLimitTest, ClaimsListPaginationTest, ServiceItemStaffOwnershipTest, EntityManagerInterface, KernelBrowser, CommentPaginationTest (+5 more)
|
||||
|
||||
### Community 87 - "Community 87"
|
||||
Cohesion: 0.10
|
||||
@@ -1061,7 +1057,7 @@ Nodes (20): DELETE `/api/v1/admin/users/{uuid}`, Errors, Errors, GET `/api/v1/ad
|
||||
|
||||
### Community 92 - "Community 92"
|
||||
Cohesion: 0.10
|
||||
Nodes (19): DELETE `/api/v1/admin/doctor-service/{id}`, Doctor Service API, Errors, Errors, Errors, Errors, GET `/api/v1/admin/doctor-services`, GET `/api/v1/doctor-services` (+11 more)
|
||||
Nodes (20): Bulk import / export, DELETE `/api/v1/admin/doctor-service/{id}`, Doctor Service API, Errors, Errors, Errors, Errors, GET `/api/v1/admin/doctor-services` (+12 more)
|
||||
|
||||
### Community 93 - "Community 93"
|
||||
Cohesion: 0.10
|
||||
@@ -1072,8 +1068,8 @@ Cohesion: 0.10
|
||||
Nodes (20): 12. تنظیمات نوبت — تعطیلات, 76. 🔴 `DELETE` delete, 77. 🔵 `POST` post, 78. 🟡 `PATCH` patch, 79. 🔴 `DELETE` delete, 80. 🟢 `GET` get, Request Body, Request Body (+12 more)
|
||||
|
||||
### Community 95 - "Community 95"
|
||||
Cohesion: 0.15
|
||||
Nodes (4): ClinicSubscription, Payment, SubscriptionPeriod, SubscriptionPlan
|
||||
Cohesion: 0.09
|
||||
Nodes (7): ClinicSubscription, TenantInsuranceService, CoverageRule, TenantInsurance, Payment, SubscriptionPeriod, SubscriptionPlan
|
||||
|
||||
### Community 96 - "Community 96"
|
||||
Cohesion: 0.18
|
||||
@@ -1100,8 +1096,8 @@ Cohesion: 0.10
|
||||
Nodes (19): Backend — endpoint اشتراک, Frontend — App.tsx, Frontend — authStore, Frontend — Sidebar, اعمال محدودیتهای اشتراک در پنل ادمین (Frontend Gate), ترتیب اجرا, زمینه, فایلهای مرتبط (+11 more)
|
||||
|
||||
### Community 102 - "Community 102"
|
||||
Cohesion: 0.11
|
||||
Nodes (18): DELETE `/api/v1/admin/tag/{id}`, Errors, Errors, Errors, Errors, GET `/api/v1/admin/tags`, GET `/api/v1/tags`, PATCH `/api/v1/admin/tag/{id}` (+10 more)
|
||||
Cohesion: 0.10
|
||||
Nodes (19): Bulk import / export, DELETE `/api/v1/admin/tag/{id}`, Errors, Errors, Errors, Errors, GET `/api/v1/admin/tags`, GET `/api/v1/tags` (+11 more)
|
||||
|
||||
### Community 103 - "Community 103"
|
||||
Cohesion: 0.35
|
||||
@@ -1332,8 +1328,8 @@ Cohesion: 0.13
|
||||
Nodes (13): Architecture, Auth, Backend (PHP/Symfony), Backend — `src/`, Category / Bundle system, Commands, Database, First-time setup (+5 more)
|
||||
|
||||
### Community 163 - "Community 163"
|
||||
Cohesion: 0.13
|
||||
Nodes (14): autoload, autoload-dev, psr-4, psr-4, conflict, symfony/symfony, description, license (+6 more)
|
||||
Cohesion: 0.11
|
||||
Nodes (18): autoload, autoload-dev, psr-4, psr-4, conflict, symfony/symfony, description, extra (+10 more)
|
||||
|
||||
### Community 164 - "Community 164"
|
||||
Cohesion: 0.29
|
||||
@@ -1404,8 +1400,8 @@ Cohesion: 0.23
|
||||
Nodes (3): WeeklySchedule, Doctor, self
|
||||
|
||||
### Community 182 - "Community 182"
|
||||
Cohesion: 0.15
|
||||
Nodes (11): emptyFeatures(), FEATURE_KEYS, FEATURE_LABELS, PeriodForm, periodSchema, PLAN_DISPLAY, PlanForm, planSchema (+3 more)
|
||||
Cohesion: 0.13
|
||||
Nodes (13): emptyFeatures(), FEATURE_KEYS, FEATURE_LABELS, PeriodForm, periodSchema, PLAN_DISPLAY, PlanForm, planSchema (+5 more)
|
||||
|
||||
### Community 183 - "Community 183"
|
||||
Cohesion: 0.14
|
||||
@@ -1569,7 +1565,7 @@ Nodes (6): AbstractAuthenticator, Passport, PasswordAuthenticator, Request, Resp
|
||||
|
||||
### Community 228 - "Community 228"
|
||||
Cohesion: 0.17
|
||||
Nodes (11): DELETE `/api/v1/admin/insurance/{id}`, DELETE `/api/v1/insurance/{id}`, EntityInsurancePricing — قیمتگذاری ویزیت بر اساس بیمه, GET `/api/v1/insurance/{id}`, GET `/api/v1/insurance-pricing`, Insurance API, Response `200`, Response `200` (+3 more)
|
||||
Nodes (12): Bulk import / export, DELETE `/api/v1/admin/insurance/{id}`, DELETE `/api/v1/insurance/{id}`, EntityInsurancePricing — قیمتگذاری ویزیت بر اساس بیمه, GET `/api/v1/insurance/{id}`, GET `/api/v1/insurance-pricing`, Insurance API, Response `200` (+4 more)
|
||||
|
||||
### Community 229 - "Community 229"
|
||||
Cohesion: 0.15
|
||||
@@ -1632,8 +1628,8 @@ Cohesion: 0.32
|
||||
Nodes (3): SubscriptionService, ClinicSubscription, Payment
|
||||
|
||||
### Community 245 - "Community 245"
|
||||
Cohesion: 0.26
|
||||
Nodes (3): TenantInsuranceService, CoverageRule, TenantInsurance
|
||||
Cohesion: 0.13
|
||||
Nodes (12): AdminUserDetail, AVATAR_COLORS, EditForm, editSchema, GENDER_LABELS, getPrimaryRole(), MARITAL_LABELS, MEDICAL_SECTIONS (+4 more)
|
||||
|
||||
### Community 246 - "Community 246"
|
||||
Cohesion: 0.17
|
||||
@@ -1740,8 +1736,8 @@ Cohesion: 0.18
|
||||
Nodes (10): Endpoint های موجود که تغییر میکنند, GET /api/v1/admin/dashboard/charts?from=UNIX&to=UNIX, GET /api/v1/dashboard/clinic, GET /api/v1/dashboard/doctor, تسک ۱۶: داشبورد هوشمند — چارت + فیلتر زمانی, توضیح, زمان تخمینی, فیلتر بازه زمانی (+2 more)
|
||||
|
||||
### Community 274 - "Community 274"
|
||||
Cohesion: 0.28
|
||||
Nodes (5): Authentication, ClinicPro — API Documentation Index, Error Code Reference, Modules, Standard Response Envelope
|
||||
Cohesion: 0.22
|
||||
Nodes (3): CategoryImportController, JsonResponse, Request
|
||||
|
||||
### Community 275 - "Community 275"
|
||||
Cohesion: 0.38
|
||||
@@ -1856,8 +1852,8 @@ Cohesion: 0.12
|
||||
Nodes (16): آمادهسازی پروژه ClinicPro برای دیپلوی روی Coolify با Docker Compose, زمینه, فایلهای مرتبط, نکات مهم (محدودیتها و edge caseها), هدف, وظایف, ۱. ساخت `Dockerfile` چندمرحلهای, ۱۰. ساخت راهنمای `docs/deploy/coolify.md` (+8 more)
|
||||
|
||||
### Community 304 - "Community 304"
|
||||
Cohesion: 0.22
|
||||
Nodes (9): 30. 🟢 `GET` get 🆕, 33. 🔵 `POST` image_clinic, 36. 🟢 `GET` get my rate, 6. کلینیک (Clinic), هدرهای اضافی, هدرهای اضافی, پاسخها, پاسخها (+1 more)
|
||||
Cohesion: 0.04
|
||||
Nodes (56): 29. 🟢 `GET` clinic list 🆕, 30. 🟢 `GET` get 🆕, 31. 🟡 `PATCH` patch, 33. 🔵 `POST` image_clinic, 34. 🔵 `POST` image logo, 36. 🟢 `GET` get my rate, 37. 🔵 `POST` post, 39. 🟡 `PATCH` Comment confirmation (+48 more)
|
||||
|
||||
### Community 306 - "Community 306"
|
||||
Cohesion: 0.07
|
||||
@@ -1908,8 +1904,8 @@ Cohesion: 0.36
|
||||
Nodes (3): TariffRepository, ManagerRegistry, Tariff
|
||||
|
||||
### Community 318 - "Community 318"
|
||||
Cohesion: 0.13
|
||||
Nodes (7): EntityInsurancePricing, EntityInsurancePricingRepository, TenantServiceCoverageRepository, TenantInsuranceCleanupService, ManagerRegistry, ManagerRegistry, TenantServiceCoverage
|
||||
Cohesion: 0.15
|
||||
Nodes (5): EntityInsurancePricing, TenantInsuranceCleanupTest, EntityInsurancePricingRepository, TenantInsuranceCleanupService, ManagerRegistry
|
||||
|
||||
### Community 319 - "Community 319"
|
||||
Cohesion: 0.39
|
||||
@@ -1984,8 +1980,8 @@ Cohesion: 0.39
|
||||
Nodes (5): JsonContains, FunctionNode, Node, Parser, SqlWalker
|
||||
|
||||
### Community 340 - "Community 340"
|
||||
Cohesion: 0.43
|
||||
Nodes (5): BeforeInstallPromptEvent, usePwaInstall(), PwaInstallBanner(), detectIOS(), PwaLoginCard()
|
||||
Cohesion: 0.24
|
||||
Nodes (5): WalletTransactionRepository, WalletTransactionsPaginationTest, ManagerRegistry, User, WalletTransaction
|
||||
|
||||
### Community 341 - "Community 341"
|
||||
Cohesion: 0.12
|
||||
@@ -2020,8 +2016,8 @@ Cohesion: 0.39
|
||||
Nodes (3): ProvinceRepository, ManagerRegistry, Province
|
||||
|
||||
### Community 349 - "Community 349"
|
||||
Cohesion: 0.36
|
||||
Nodes (3): DoctorServiceController, JsonResponse, Request
|
||||
Cohesion: 0.20
|
||||
Nodes (10): Category Import / Export API, Errors, Errors, GET `/api/v1/admin/categories/{bundle}/export`, Per-row validation rules, POST `/api/v1/admin/categories/{bundle}/import`, Request Body (`application/json`), Response `200` (+2 more)
|
||||
|
||||
### Community 350 - "Community 350"
|
||||
Cohesion: 0.43
|
||||
@@ -2032,8 +2028,8 @@ Cohesion: 0.50
|
||||
Nodes (4): UserActiveContextRepository, ManagerRegistry, User, UserActiveContext
|
||||
|
||||
### Community 352 - "Community 352"
|
||||
Cohesion: 0.39
|
||||
Nodes (3): DoctorService, DoctorServiceRepository, ManagerRegistry
|
||||
Cohesion: 0.38
|
||||
Nodes (5): Rate, RateRepository, Doctor, ManagerRegistry, User
|
||||
|
||||
### Community 354 - "Community 354"
|
||||
Cohesion: 0.36
|
||||
@@ -2203,10 +2199,6 @@ Nodes (6): ClaimAmountBoundsTest, ClaimsListNPlusOneTest, ClaimItem, Claim, Doct
|
||||
Cohesion: 0.33
|
||||
Nodes (4): initiate(), verify(), PaymentInitResult, PaymentVerifyResult
|
||||
|
||||
### Community 399 - "Community 399"
|
||||
Cohesion: 0.23
|
||||
Nodes (5): AbstractMigration, Schema, Version20260609130407, Schema, Version20260619121047
|
||||
|
||||
### Community 405 - "Community 405"
|
||||
Cohesion: 0.47
|
||||
Nodes (3): CommissionService, Payment, Representation
|
||||
@@ -2216,8 +2208,8 @@ Cohesion: 0.17
|
||||
Nodes (11): دیپلوی ClinicPro روی Coolify (Docker Compose), رفع اشکال, مراحل دیپلوی, معماری دیپلوی, نکات عملیاتی, چند دامنه فرانتاند (مهم), ۱. ساخت Resource در Coolify, ۲. اختصاص دامنه (+3 more)
|
||||
|
||||
### Community 452 - "Community 452"
|
||||
Cohesion: 0.07
|
||||
Nodes (19): ApiResponse, ClinicAddress, ClinicDoctorItem, ClinicInvitation, EditForm, editSchema, HUES_LIST, INV_STATUS_MAP (+11 more)
|
||||
Cohesion: 0.10
|
||||
Nodes (12): ClinicAddress, ClinicDoctorItem, ClinicInvitation, EditForm, editSchema, HUES_LIST, INV_STATUS_MAP, IRAN_CENTER (+4 more)
|
||||
|
||||
### Community 456 - "Community 456"
|
||||
Cohesion: 0.29
|
||||
@@ -2331,10 +2323,6 @@ Nodes (5): Errors, GET `/api/v1/representation/dashboard/summary`, GET `/api/v1/
|
||||
Cohesion: 0.40
|
||||
Nodes (5): Admin Endpoints, GET /api/v1/admin/sms/settings/review, GET /api/v1/admin/sms/wallet-report, POST /api/v1/admin/sms/settings/{id}/approve, POST /api/v1/admin/sms/settings/{id}/reject
|
||||
|
||||
### Community 491 - "Community 491"
|
||||
Cohesion: 0.40
|
||||
Nodes (5): 31. 🟡 `PATCH` patch, Request Body, مثال Request, هدرهای اضافی, پاسخها
|
||||
|
||||
### Community 492 - "Community 492"
|
||||
Cohesion: 0.40
|
||||
Nodes (5): 32. 🔵 `POST` post, Request Body, مثال Request, هدرهای اضافی, پاسخها
|
||||
@@ -2347,10 +2335,6 @@ Nodes (9): Backend Audit Backlog — ClinicPro, ☐ CRITICAL, ✅ DONE (committe
|
||||
Cohesion: 0.50
|
||||
Nodes (4): Error Codes, POST `/api/v1/user/reset-password`, Request Body, Response `200`
|
||||
|
||||
### Community 495 - "Community 495"
|
||||
Cohesion: 0.40
|
||||
Nodes (5): 41. 🟡 `PATCH` patch, Request Body, مثال Request, هدرهای اضافی, پاسخها
|
||||
|
||||
### Community 496 - "Community 496"
|
||||
Cohesion: 0.50
|
||||
Nodes (4): Errors, POST `/api/v1/user/send-code`, Request Body, Response `200`
|
||||
@@ -2627,6 +2611,10 @@ Nodes (15): دیپلوی ClinicPro (Symfony) روی لیارا با Docker, زم
|
||||
Cohesion: 0.50
|
||||
Nodes (4): API های جدید که باید ساخته شوند, API های موجود (استفاده کن، تغییر نده), تغییر روی API موجود, وضعیت فعلی API (مهم — قبل از پیادهسازی بخوان)
|
||||
|
||||
### Community 583 - "Community 583"
|
||||
Cohesion: 0.23
|
||||
Nodes (5): AbstractMigration, Schema, Version20260609134112, Schema, Version20260628165710
|
||||
|
||||
### Community 584 - "Community 584"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): Entity: UserProfile, ساختار فایلها, معماری — تسک ۰۳: ماژول پروفایل کاربر
|
||||
@@ -2685,7 +2673,7 @@ Nodes (3): POST `/api/v1/admin/sms/template/{uuid}/approve`, Request Body (`appl
|
||||
|
||||
### Community 602 - "Community 602"
|
||||
Cohesion: 0.40
|
||||
Nodes (5): Errors, Path Parameters, POST `/api/v1/settlement/{uuid}/approve`, Request Body (`application/json`), Response `200`
|
||||
Nodes (5): Errors, PATCH `/api/v1/admin/specialty/{id}`, Path Parameters, Request Body (all optional), Response `200`
|
||||
|
||||
### Community 603 - "Community 603"
|
||||
Cohesion: 0.67
|
||||
@@ -2708,13 +2696,17 @@ Cohesion: 0.39
|
||||
Nodes (3): ClinicStaffRepository, ClinicStaff, ManagerRegistry
|
||||
|
||||
### Community 608 - "Community 608"
|
||||
Cohesion: 0.40
|
||||
Nodes (5): 37. 🔵 `POST` post, Request Body, مثال Request, هدرهای اضافی, پاسخها
|
||||
Cohesion: 0.50
|
||||
Nodes (4): Errors, POST `/api/v1/admin/specialty`, Request Body (`application/json`), Response `201`
|
||||
|
||||
### Community 612 - "Community 612"
|
||||
Cohesion: 0.67
|
||||
Nodes (3): بکاند, فرانتاند, وضعیت فعلی کد (مهم — قبل از تغییر بخوان)
|
||||
|
||||
### Community 618 - "Community 618"
|
||||
Cohesion: 0.16
|
||||
Nodes (5): ServiceItemDeleteCleanupTest, ServiceCoverageNPlusOneTest, TenantServiceCoverageRepository, ManagerRegistry, TenantServiceCoverage
|
||||
|
||||
### Community 631 - "Community 631"
|
||||
Cohesion: 0.50
|
||||
Nodes (4): DELETE `/api/v1/doctor/{uuid}`, Errors, Path Parameters, Response `200`
|
||||
@@ -2779,26 +2771,6 @@ Nodes (4): GET `/api/v1/admin/pre-registrations`, POST `/api/v1/admin/pre-regist
|
||||
Cohesion: 0.50
|
||||
Nodes (4): 38. 🟢 `GET` Unapproved comments, هدرهای اضافی, پارامترهای Query, پاسخها
|
||||
|
||||
### Community 654 - "Community 654"
|
||||
Cohesion: 0.50
|
||||
Nodes (4): 39. 🟡 `PATCH` Comment confirmation, مثال Request, هدرهای اضافی, پاسخها
|
||||
|
||||
### Community 655 - "Community 655"
|
||||
Cohesion: 0.50
|
||||
Nodes (4): 40. 🔵 `POST` post, مثال Request, هدرهای اضافی, پاسخها
|
||||
|
||||
### Community 656 - "Community 656"
|
||||
Cohesion: 0.50
|
||||
Nodes (4): 44. 🟢 `GET` list comment, هدرهای اضافی, پارامترهای Query, پاسخها
|
||||
|
||||
### Community 657 - "Community 657"
|
||||
Cohesion: 0.50
|
||||
Nodes (4): 45. 🔵 `POST` post, مثال Request, هدرهای اضافی, پاسخها
|
||||
|
||||
### Community 658 - "Community 658"
|
||||
Cohesion: 0.50
|
||||
Nodes (4): 46. 🟡 `PATCH` patch, مثال Request, هدرهای اضافی, پاسخها
|
||||
|
||||
### Community 659 - "Community 659"
|
||||
Cohesion: 0.50
|
||||
Nodes (4): GET /api/v1/appointment-settings/slots — دریافت اسلاتهای خالی, POST /api/v1/appointment-settings/holidays — ثبت تعطیلی (فقط ادمین), POST /api/v1/appointment-settings/overrides — ثبت Override توسط دکتر, Task-09: API تنظیمات نوبت
|
||||
@@ -2815,53 +2787,33 @@ Nodes (4): ۲.۱۳ نظرات، لایک و امتیازدهی, ۲.۱۳.۱ نظ
|
||||
Cohesion: 0.50
|
||||
Nodes (4): Application Logs, GET `/api/v1/admin/logs`, Query Parameters, Response `200`
|
||||
|
||||
### Community 664 - "Community 664"
|
||||
Cohesion: 0.67
|
||||
Nodes (3): 42. 🔴 `DELETE` delete, هدرهای اضافی, پاسخها
|
||||
|
||||
### Community 665 - "Community 665"
|
||||
Cohesion: 0.67
|
||||
Nodes (3): 43. 🟢 `GET` get, هدرهای اضافی, پاسخها
|
||||
|
||||
### Community 667 - "Community 667"
|
||||
Cohesion: 0.50
|
||||
Nodes (4): GET `/api/v1/admin/payments`, Payment Management, Query Parameters, Response `200`
|
||||
|
||||
### Community 671 - "Community 671"
|
||||
Cohesion: 0.67
|
||||
Nodes (3): 34. 🔵 `POST` image logo, هدرهای اضافی, پاسخها
|
||||
|
||||
### Community 672 - "Community 672"
|
||||
Cohesion: 0.50
|
||||
Nodes (4): GET `/api/v1/admin/settlements`, Query Parameters, Response `200`, Settlement Management
|
||||
|
||||
### Community 674 - "Community 674"
|
||||
Cohesion: 0.50
|
||||
Nodes (4): extra, symfony, allow-contrib, require
|
||||
|
||||
### Community 676 - "Community 676"
|
||||
Cohesion: 0.67
|
||||
Nodes (3): 29. 🟢 `GET` clinic list 🆕, پارامترهای Query, پاسخها
|
||||
|
||||
## Knowledge Gaps
|
||||
- **3550 isolated node(s):** `ALLOWED_ROLES`, `Pricing`, `TenantInsurance`, `CoverageRow`, `Draft` (+3545 more)
|
||||
- **3563 isolated node(s):** `ALLOWED_ROLES`, `Pricing`, `TenantInsurance`, `CoverageRow`, `Draft` (+3558 more)
|
||||
These have ≤1 connection - possible missing edges or undocumented components.
|
||||
- **128 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
|
||||
- **125 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
|
||||
|
||||
## Suggested Questions
|
||||
_Questions this graph is uniquely positioned to answer:_
|
||||
|
||||
- **Why does `BaseController` connect `Community 108` to `Community 6`, `Community 138`, `Community 139`, `Community 14`, `Community 15`, `Community 275`, `Community 20`, `Community 22`, `Community 26`, `Community 164`, `Community 295`, `Community 300`, `Community 301`, `Community 175`, `Community 176`, `Community 177`, `Community 58`, `Community 59`, `Community 63`, `Community 64`, `Community 70`, `Community 71`, `Community 75`, `Community 206`, `Community 349`, `Community 230`, `Community 103`, `Community 104`, `Community 107`, `Community 109`, `Community 121`, `Community 122`, `Community 252`?**
|
||||
_High betweenness centrality (0.027) - this node is a cross-community bridge._
|
||||
- **Why does `AppointmentRepository` connect `Community 119` to `Community 53`?**
|
||||
_High betweenness centrality (0.022) - this node is a cross-community bridge._
|
||||
- **Why does `Version20260614181657` connect `Community 431` to `Community 399`?**
|
||||
- **Why does `BaseController` connect `Community 108` to `Community 4`, `Community 6`, `Community 138`, `Community 139`, `Community 14`, `Community 15`, `Community 274`, `Community 275`, `Community 20`, `Community 22`, `Community 26`, `Community 164`, `Community 295`, `Community 300`, `Community 301`, `Community 175`, `Community 176`, `Community 177`, `Community 58`, `Community 59`, `Community 63`, `Community 64`, `Community 70`, `Community 71`, `Community 75`, `Community 206`, `Community 230`, `Community 103`, `Community 104`, `Community 107`, `Community 109`, `Community 121`, `Community 122`, `Community 252`?**
|
||||
_High betweenness centrality (0.037) - this node is a cross-community bridge._
|
||||
- **Why does `Version20260614181657` connect `Community 431` to `Community 583`?**
|
||||
_High betweenness centrality (0.017) - this node is a cross-community bridge._
|
||||
- **Why does `Version20260625150304` connect `Community 457` to `Community 583`?**
|
||||
_High betweenness centrality (0.017) - this node is a cross-community bridge._
|
||||
- **What connects `ALLOWED_ROLES`, `Pricing`, `TenantInsurance` to the rest of the system?**
|
||||
_3550 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||
_3563 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||
- **Should `Community 0` be split into smaller, more focused modules?**
|
||||
_Cohesion score 0.047107014848950336 - nodes in this community are weakly interconnected._
|
||||
_Cohesion score 0.04471153846153846 - nodes in this community are weakly interconnected._
|
||||
- **Should `Community 1` be split into smaller, more focused modules?**
|
||||
_Cohesion score 0.028985507246376812 - nodes in this community are weakly interconnected._
|
||||
- **Should `Community 2` be split into smaller, more focused modules?**
|
||||
_Cohesion score 0.05656108597285068 - nodes in this community are weakly interconnected._
|
||||
_Cohesion score 0.10037878787878787 - nodes in this community are weakly interconnected._
|
||||
graphify-out/cache/ast/v0.8.44/03c15491170644124dd399b100bbac0982d3ffad86ccffa9a2b9f4076bfabaad.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/26fd005455195480c3845ca99124dee7d2bc2949e7d0c56c1fd003aff3784eb8.json
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"nodes": [{"id": "users_hamed_pj_my_pj_clinic_pro_clinicpro_docs_api_category_import_md", "label": "category-import.md", "file_type": "document", "source_file": "docs/api/category-import.md", "source_location": "L1"}, {"id": "api_category_import_category_import_export_api", "label": "Category Import / Export API", "file_type": "document", "source_file": "docs/api/category-import.md", "source_location": "L1"}, {"id": "api_category_import_get_api_v1_admin_categories_bundle_export", "label": "GET `/api/v1/admin/categories/{bundle}/export`", "file_type": "document", "source_file": "docs/api/category-import.md", "source_location": "L26"}, {"id": "api_category_import_response_200", "label": "Response `200`", "file_type": "document", "source_file": "docs/api/category-import.md", "source_location": "L35"}, {"id": "api_category_import_errors", "label": "Errors", "file_type": "document", "source_file": "docs/api/category-import.md", "source_location": "L45"}, {"id": "api_category_import_post_api_v1_admin_categories_bundle_import", "label": "POST `/api/v1/admin/categories/{bundle}/import`", "file_type": "document", "source_file": "docs/api/category-import.md", "source_location": "L54"}, {"id": "api_category_import_request_body_application_json", "label": "Request Body (`application/json`)", "file_type": "document", "source_file": "docs/api/category-import.md", "source_location": "L64"}, {"id": "api_category_import_per_row_validation_rules", "label": "Per-row validation rules", "file_type": "document", "source_file": "docs/api/category-import.md", "source_location": "L74"}, {"id": "api_category_import_response_200_88", "label": "Response `200`", "file_type": "document", "source_file": "docs/api/category-import.md", "source_location": "L88"}, {"id": "api_category_import_response_422_validation_failed_nothing_written", "label": "Response `422` (validation failed \u2014 nothing written)", "file_type": "document", "source_file": "docs/api/category-import.md", "source_location": "L93"}, {"id": "api_category_import_errors_104", "label": "Errors", "file_type": "document", "source_file": "docs/api/category-import.md", "source_location": "L104"}], "edges": [{"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_docs_api_category_import_md", "target": "api_category_import_category_import_export_api", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/api/category-import.md", "source_location": "L1", "weight": 1.0}, {"source": "api_category_import_category_import_export_api", "target": "api_category_import_get_api_v1_admin_categories_bundle_export", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/api/category-import.md", "source_location": "L26", "weight": 1.0}, {"source": "api_category_import_get_api_v1_admin_categories_bundle_export", "target": "api_category_import_response_200", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/api/category-import.md", "source_location": "L35", "weight": 1.0}, {"source": "api_category_import_get_api_v1_admin_categories_bundle_export", "target": "api_category_import_errors", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/api/category-import.md", "source_location": "L45", "weight": 1.0}, {"source": "api_category_import_category_import_export_api", "target": "api_category_import_post_api_v1_admin_categories_bundle_import", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/api/category-import.md", "source_location": "L54", "weight": 1.0}, {"source": "api_category_import_post_api_v1_admin_categories_bundle_import", "target": "api_category_import_request_body_application_json", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/api/category-import.md", "source_location": "L64", "weight": 1.0}, {"source": "api_category_import_post_api_v1_admin_categories_bundle_import", "target": "api_category_import_per_row_validation_rules", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/api/category-import.md", "source_location": "L74", "weight": 1.0}, {"source": "api_category_import_post_api_v1_admin_categories_bundle_import", "target": "api_category_import_response_200_88", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/api/category-import.md", "source_location": "L88", "weight": 1.0}, {"source": "api_category_import_post_api_v1_admin_categories_bundle_import", "target": "api_category_import_response_422_validation_failed_nothing_written", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/api/category-import.md", "source_location": "L93", "weight": 1.0}, {"source": "api_category_import_post_api_v1_admin_categories_bundle_import", "target": "api_category_import_errors_104", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/api/category-import.md", "source_location": "L104", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
|
||||
graphify-out/cache/ast/v0.8.44/44d523853c3cf5679d907526a3b63b6fed530e55195a26cf4f433b4bfdc80b64.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/8db2c22f2fafdb62913657534ac8b49778de786e78cd200e4a35d5cef3cee0c2.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/a024e860e973def0803da5f84669451313079c9b087f2dc5e185ef1f676cd033.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/c6d9fbd080fb7897206b3d9dc7f5b49cf347fe29ca6428e30e9e9948727ccabc.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/d12c8b8c7291a2149985c804a34191bbbf7f2050bb2264b4efca1bbef3616251.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/de0f23dce595f64032e3422ab8ec643d7b27948d1b3a846aa718d1fad6df87dd.json
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+1657
-649
File diff suppressed because it is too large
Load Diff
+27
-12
@@ -190,8 +190,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"assets/admin/pages/CategoriesPage.tsx": {
|
||||
"mtime": 1782042929.639692,
|
||||
"ast_hash": "d6aa06ef7578f2601fede2a8c77dceb1",
|
||||
"mtime": 1782838987.6521325,
|
||||
"ast_hash": "e4e8f39e7e7a03b947ce2883ed595a37",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"assets/admin/pages/ClaimsPage.tsx": {
|
||||
@@ -2390,8 +2390,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/api/doctor-service.md": {
|
||||
"mtime": 1781158861.7234077,
|
||||
"ast_hash": "3f0d0549bcfcf6163d2762e85c3f3776",
|
||||
"mtime": 1782839237.8086488,
|
||||
"ast_hash": "3c3f4da25682c21bf769a2c170b9dc6f",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/api/doctor.md": {
|
||||
@@ -2400,13 +2400,13 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/api/insurance.md": {
|
||||
"mtime": 1782728407.109222,
|
||||
"ast_hash": "05f4fa9a2628811066a8f2cb90b6db4d",
|
||||
"mtime": 1782839237.8089066,
|
||||
"ast_hash": "66c99e8593c6e48fbbb065610b626e68",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/api/location.md": {
|
||||
"mtime": 1781158896.6071815,
|
||||
"ast_hash": "428efca3871276f0e7db9de61c32768c",
|
||||
"mtime": 1782839237.808474,
|
||||
"ast_hash": "9db8856184fd38eed6f377ac425584fa",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/api/patient.md": {
|
||||
@@ -2445,8 +2445,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/api/specialty.md": {
|
||||
"mtime": 1781636247.0762212,
|
||||
"ast_hash": "fdaed926f0d4c12f795071556c662070",
|
||||
"mtime": 1782839237.8082483,
|
||||
"ast_hash": "45c0b1034a9e6bd3ea4973796b5538c1",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/api/staff.md": {
|
||||
@@ -2460,8 +2460,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/api/tag.md": {
|
||||
"mtime": 1781158875.2883966,
|
||||
"ast_hash": "b727bc06ebe9ef4a464192be385b38f8",
|
||||
"mtime": 1782839237.8090699,
|
||||
"ast_hash": "0f848b663c3d80081a71fb46b8088c87",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/api/user-profile.md": {
|
||||
@@ -3413,5 +3413,20 @@
|
||||
"mtime": 1782749549.9584448,
|
||||
"ast_hash": "6e005bb338617608a0441724126a5bd3",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"src/Category/Controller/CategoryImportController.php": {
|
||||
"mtime": 1782839057.0287304,
|
||||
"ast_hash": "f4d8651689e73ca956bc2a9570bba695",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"tests/Category/CategoryImportTest.php": {
|
||||
"mtime": 1782839178.8459258,
|
||||
"ast_hash": "203f5500749a42f0be4f33dc2e3dbb1f",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/api/category-import.md": {
|
||||
"mtime": 1782839220.8153825,
|
||||
"ast_hash": "da0c24b873a9f46c97cd8760ac4eeba5",
|
||||
"semantic_hash": ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
<?php
|
||||
|
||||
namespace App\Category\Controller;
|
||||
|
||||
use App\Shared\Controller\BaseController;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
/**
|
||||
* Bulk JSON import for the admin "دستهبندیها" page (all six tabs).
|
||||
*
|
||||
* Consumes the exact JSON produced by each tab's "خروجی JSON" export
|
||||
* (an array of objects shaped like the entity's toArray()).
|
||||
*
|
||||
* Behaviour:
|
||||
* - Strict two-phase validation: the WHOLE file is validated first; if any
|
||||
* row is invalid, nothing is written and every error is returned (HTTP 422).
|
||||
* - Wipe + full replace: on success the target table is emptied and rebuilt
|
||||
* from the file, preserving the raw `id` of each row so relations
|
||||
* (parent_id / province_id / specialty_id) stay intact.
|
||||
* - The whole replace runs inside one transaction with FK checks disabled.
|
||||
*/
|
||||
#[OA\Tag(name: 'Categories')]
|
||||
#[IsGranted('ROLE_ADMIN')]
|
||||
class CategoryImportController extends BaseController
|
||||
{
|
||||
/** bundle => table name */
|
||||
private const TABLES = [
|
||||
'provinces' => 'provinces',
|
||||
'cities' => 'cities',
|
||||
'specialties' => 'specialties',
|
||||
'doctor_services' => 'doctor_services',
|
||||
'insurances' => 'insurances',
|
||||
'tags' => 'tags',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly Connection $db,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Complete export of a bundle — EVERY row, no pagination cap.
|
||||
*
|
||||
* The page's old "خروجی JSON" button pointed at the admin list endpoint,
|
||||
* which silently caps at 100 rows. Feeding that truncated file back into the
|
||||
* wipe+replace import deletes the tail. This endpoint returns the full table
|
||||
* so the export/import round-trip is lossless.
|
||||
*/
|
||||
#[Route('/api/v1/admin/categories/{bundle}/export', methods: ['GET'])]
|
||||
public function export(string $bundle): JsonResponse
|
||||
{
|
||||
$table = self::TABLES[$bundle] ?? null;
|
||||
if ($table === null) {
|
||||
return $this->error('ERR_BUNDLE_UNKNOWN', 'نوع دستهبندی نامعتبر است', 404, 'bundle');
|
||||
}
|
||||
|
||||
$rows = $this->db->fetchAllAssociative('SELECT * FROM ' . $table . ' ORDER BY id ASC');
|
||||
|
||||
// normalize numeric columns so the JSON matches the entity shape
|
||||
foreach ($rows as &$r) {
|
||||
foreach (['id', 'status', 'weight', 'parent_id', 'specialty_id', 'province_id', 'representation_id'] as $intCol) {
|
||||
if (array_key_exists($intCol, $r) && $r[$intCol] !== null) {
|
||||
$r[$intCol] = (int) $r[$intCol];
|
||||
}
|
||||
}
|
||||
}
|
||||
unset($r);
|
||||
|
||||
return $this->success($rows);
|
||||
}
|
||||
|
||||
#[Route('/api/v1/admin/categories/{bundle}/import', methods: ['POST'])]
|
||||
public function import(string $bundle, Request $request): JsonResponse
|
||||
{
|
||||
$table = self::TABLES[$bundle] ?? null;
|
||||
if ($table === null) {
|
||||
return $this->error('ERR_BUNDLE_UNKNOWN', 'نوع دستهبندی نامعتبر است', 404, 'bundle');
|
||||
}
|
||||
|
||||
$payload = json_decode($request->getContent(), true);
|
||||
// accept both a raw array and { "items": [...] }
|
||||
if (is_array($payload) && isset($payload['items']) && is_array($payload['items'])) {
|
||||
$payload = $payload['items'];
|
||||
}
|
||||
if (!is_array($payload) || $payload === [] || array_keys($payload) !== range(0, count($payload) - 1)) {
|
||||
return $this->error('ERR_IMPORT_FORMAT', 'فایل باید یک آرایهی غیرخالی از رکوردها باشد', 422, 'file');
|
||||
}
|
||||
if (count($payload) > 5000) {
|
||||
return $this->error('ERR_IMPORT_TOO_LARGE', 'حداکثر ۵۰۰۰ رکورد در هر import مجاز است', 422, 'file');
|
||||
}
|
||||
|
||||
// ── Phase 1: strict validation ───────────────────────────────────────
|
||||
$errors = [];
|
||||
$rows = []; // normalized, ready-to-insert column maps
|
||||
$seenId = []; // id => row index (uniqueness within file)
|
||||
$seenSlug = []; // slug => row index
|
||||
|
||||
foreach ($payload as $i => $raw) {
|
||||
$rowNo = $i + 1;
|
||||
$add = function (string $field, string $message) use (&$errors, $rowNo): void {
|
||||
$errors[] = ['row' => $rowNo, 'field' => $field, 'message' => "ردیف {$rowNo}: {$message}"];
|
||||
};
|
||||
|
||||
if (!is_array($raw) || array_keys($raw) === range(0, count($raw) - 1)) {
|
||||
$add('-', 'ساختار رکورد نامعتبر است');
|
||||
continue;
|
||||
}
|
||||
|
||||
// id — required, positive int, unique in file (relations rely on it)
|
||||
$id = $raw['id'] ?? null;
|
||||
if (!self::isPositiveInt($id)) {
|
||||
$add('id', 'شناسه (id) باید عدد صحیح مثبت باشد');
|
||||
} elseif (isset($seenId[(int) $id])) {
|
||||
$add('id', "شناسه {$id} تکراری است (ردیف {$seenId[(int) $id]})");
|
||||
} else {
|
||||
$seenId[(int) $id] = $rowNo;
|
||||
}
|
||||
|
||||
// name — required non-empty string ≤255
|
||||
$name = $raw['name'] ?? null;
|
||||
if (!is_string($name) || trim($name) === '') {
|
||||
$add('name', 'نام الزامی است');
|
||||
} elseif (mb_strlen($name) > 255) {
|
||||
$add('name', 'نام نباید بیش از ۲۵۵ نویسه باشد');
|
||||
}
|
||||
|
||||
// status — 0 or 1
|
||||
$status = self::normInt($raw['status'] ?? 1);
|
||||
if ($status === null || !in_array($status, [0, 1], true)) {
|
||||
$add('status', 'وضعیت باید ۰ یا ۱ باشد');
|
||||
}
|
||||
|
||||
$row = [
|
||||
'id' => self::isPositiveInt($id) ? (int) $id : null,
|
||||
'uuid' => (is_string($raw['uuid'] ?? null) && $raw['uuid'] !== '') ? mb_substr($raw['uuid'], 0, 36) : Uuid::v4()->toRfc4122(),
|
||||
'name' => is_string($name) ? trim($name) : '',
|
||||
'status' => $status ?? 1,
|
||||
];
|
||||
|
||||
// weight — int ≥0 (only bundles whose table has a weight column)
|
||||
if (in_array($bundle, ['provinces', 'cities', 'specialties', 'doctor_services'], true)) {
|
||||
$weight = self::normInt($raw['weight'] ?? 0);
|
||||
if ($weight === null || $weight < 0) {
|
||||
$add('weight', 'ترتیب نمایش باید عدد صحیح نامنفی باشد');
|
||||
}
|
||||
$row['weight'] = $weight ?? 0;
|
||||
}
|
||||
|
||||
// slug — required for bundles that have it, unique in file
|
||||
if (in_array($bundle, ['specialties', 'doctor_services', 'tags'], true)) {
|
||||
$slug = $raw['slug'] ?? null;
|
||||
if (!is_string($slug) || trim($slug) === '') {
|
||||
$add('slug', 'slug الزامی است');
|
||||
} elseif (mb_strlen($slug) > 255) {
|
||||
$add('slug', 'slug نباید بیش از ۲۵۵ نویسه باشد');
|
||||
} else {
|
||||
$slug = trim($slug);
|
||||
if (isset($seenSlug[$slug])) {
|
||||
$add('slug', "slug «{$slug}» تکراری است (ردیف {$seenSlug[$slug]})");
|
||||
} else {
|
||||
$seenSlug[$slug] = $rowNo;
|
||||
}
|
||||
$row['slug'] = $slug;
|
||||
}
|
||||
}
|
||||
|
||||
// bundle-specific columns
|
||||
switch ($bundle) {
|
||||
case 'specialties':
|
||||
$row['parent_id'] = self::nullableId($raw['parent_id'] ?? null, $add, 'parent_id');
|
||||
break;
|
||||
|
||||
case 'doctor_services':
|
||||
$row['specialty_id'] = self::nullableId($raw['specialty_id'] ?? null, $add, 'specialty_id');
|
||||
break;
|
||||
|
||||
case 'insurances':
|
||||
$type = $raw['type'] ?? null;
|
||||
if (!in_array($type, ['basic', 'supplementary'], true)) {
|
||||
$add('type', 'نوع بیمه باید basic یا supplementary باشد');
|
||||
}
|
||||
$row['type'] = is_string($type) ? $type : 'basic';
|
||||
$row['logo_url'] = self::optStr($raw['logo_url'] ?? null);
|
||||
break;
|
||||
|
||||
case 'cities':
|
||||
$row['province_id'] = self::nullableId($raw['province_id'] ?? null, $add, 'province_id');
|
||||
$row['representation_id'] = self::nullableId($raw['representation_id'] ?? null, $add, 'representation_id');
|
||||
$row['contact_phone'] = self::optStr($raw['contact_phone'] ?? null);
|
||||
$row['email'] = self::optStr($raw['email'] ?? null);
|
||||
$row['description'] = self::optStr($raw['description'] ?? null);
|
||||
$row['slogan'] = self::optStr($raw['slogan'] ?? null);
|
||||
$row['domain'] = self::optStr($raw['domain'] ?? null);
|
||||
$row['keywords'] = self::optStr($raw['keywords'] ?? null);
|
||||
$row['footer_description'] = self::optStr($raw['footer_description'] ?? null);
|
||||
$row['logo_url'] = self::optStr($raw['logo_url'] ?? null);
|
||||
$sm = $raw['social_media'] ?? null;
|
||||
$row['social_media'] = ($sm === null || $sm === '') ? null : (is_string($sm) ? $sm : json_encode($sm, JSON_UNESCAPED_UNICODE));
|
||||
break;
|
||||
}
|
||||
|
||||
$rows[] = $row;
|
||||
}
|
||||
|
||||
// referential integrity (only meaningful once ids are collected)
|
||||
$this->validateReferences($bundle, $rows, $seenId, $errors);
|
||||
|
||||
if ($errors !== []) {
|
||||
return new JsonResponse(['success' => false, 'data' => null, 'errors' => $errors], 422);
|
||||
}
|
||||
|
||||
// ── Phase 2: wipe + replace inside a transaction ─────────────────────
|
||||
$this->db->beginTransaction();
|
||||
try {
|
||||
$this->db->executeStatement('SET FOREIGN_KEY_CHECKS = 0');
|
||||
$this->db->executeStatement('DELETE FROM ' . $table);
|
||||
foreach ($rows as $row) {
|
||||
$this->db->insert($table, $row);
|
||||
}
|
||||
$this->db->executeStatement('SET FOREIGN_KEY_CHECKS = 1');
|
||||
$this->db->commit();
|
||||
} catch (\Throwable $e) {
|
||||
$this->db->rollBack();
|
||||
try { $this->db->executeStatement('SET FOREIGN_KEY_CHECKS = 1'); } catch (\Throwable) {}
|
||||
return $this->error('ERR_IMPORT_FAILED', 'خطا در ذخیرهسازی: ' . $e->getMessage(), 500);
|
||||
}
|
||||
|
||||
return $this->success(['imported' => count($rows)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cross-row / cross-table reference checks.
|
||||
* - specialties.parent_id must point to an id present in the same file.
|
||||
* - cities.province_id / cities.representation_id / doctor_services.specialty_id
|
||||
* must exist in their (un-wiped) reference tables.
|
||||
*/
|
||||
private function validateReferences(string $bundle, array $rows, array $seenId, array &$errors): void
|
||||
{
|
||||
$err = function (int $rowNo, string $field, string $message) use (&$errors): void {
|
||||
$errors[] = ['row' => $rowNo, 'field' => $field, 'message' => "ردیف {$rowNo}: {$message}"];
|
||||
};
|
||||
|
||||
if ($bundle === 'specialties') {
|
||||
foreach ($rows as $idx => $r) {
|
||||
$p = $r['parent_id'] ?? null;
|
||||
if ($p !== null && !isset($seenId[$p])) {
|
||||
$err($idx + 1, 'parent_id', "والد با شناسه {$p} در همین فایل وجود ندارد");
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if ($bundle === 'doctor_services') {
|
||||
$valid = $this->existingIds('specialties');
|
||||
foreach ($rows as $idx => $r) {
|
||||
$s = $r['specialty_id'] ?? null;
|
||||
if ($s !== null && !isset($valid[$s])) {
|
||||
$err($idx + 1, 'specialty_id', "تخصص با شناسه {$s} در سیستم وجود ندارد");
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if ($bundle === 'cities') {
|
||||
$validProv = $this->existingIds('provinces');
|
||||
$validRep = $this->existingIds('representations');
|
||||
foreach ($rows as $idx => $r) {
|
||||
$pv = $r['province_id'] ?? null;
|
||||
if ($pv !== null && !isset($validProv[$pv])) {
|
||||
$err($idx + 1, 'province_id', "استان با شناسه {$pv} وجود ندارد");
|
||||
}
|
||||
$rp = $r['representation_id'] ?? null;
|
||||
if ($rp !== null && !isset($validRep[$rp])) {
|
||||
$err($idx + 1, 'representation_id', "نماینده با شناسه {$rp} وجود ندارد");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @return array<int,true> set of existing ids in a table */
|
||||
private function existingIds(string $table): array
|
||||
{
|
||||
$ids = $this->db->fetchFirstColumn('SELECT id FROM ' . $table);
|
||||
$set = [];
|
||||
foreach ($ids as $id) {
|
||||
$set[(int) $id] = true;
|
||||
}
|
||||
return $set;
|
||||
}
|
||||
|
||||
private static function isPositiveInt(mixed $v): bool
|
||||
{
|
||||
return (is_int($v) || (is_string($v) && ctype_digit($v))) && (int) $v > 0;
|
||||
}
|
||||
|
||||
private static function normInt(mixed $v): ?int
|
||||
{
|
||||
if (is_int($v)) return $v;
|
||||
if (is_string($v) && preg_match('/^-?\d+$/', $v)) return (int) $v;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Validate an optional FK-style id: null stays null, otherwise must be positive int. */
|
||||
private static function nullableId(mixed $v, callable $add, string $field): ?int
|
||||
{
|
||||
if ($v === null || $v === '' || $v === 0 || $v === '0') return null;
|
||||
if (!self::isPositiveInt($v)) {
|
||||
$add($field, "{$field} باید عدد صحیح مثبت یا خالی باشد");
|
||||
return null;
|
||||
}
|
||||
return (int) $v;
|
||||
}
|
||||
|
||||
private static function optStr(mixed $v): ?string
|
||||
{
|
||||
if ($v === null) return null;
|
||||
if (!is_string($v)) return null;
|
||||
$v = trim($v);
|
||||
return $v === '' ? null : $v;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Category;
|
||||
|
||||
use App\Tests\ApiTestCase;
|
||||
use Doctrine\DBAL\Connection;
|
||||
|
||||
/**
|
||||
* POST /api/v1/admin/categories/{bundle}/import — bulk wipe+replace import.
|
||||
*
|
||||
* Guards the two behaviours the feature promises:
|
||||
* - strict validation: an invalid file is rejected whole, the table is untouched;
|
||||
* - referential integrity: self/cross references must resolve before any write.
|
||||
*
|
||||
* Runs against db_test (never reset), so every assertion is made against the
|
||||
* table the import owns, and the destructive cases verify the row count is the
|
||||
* SAME before and after a rejected import.
|
||||
*/
|
||||
class CategoryImportTest extends ApiTestCase
|
||||
{
|
||||
private function db(): Connection
|
||||
{
|
||||
return static::getContainer()->get(Connection::class);
|
||||
}
|
||||
|
||||
private function rowCount(string $table): int
|
||||
{
|
||||
return (int) $this->db()->fetchOne('SELECT COUNT(*) FROM ' . $table);
|
||||
}
|
||||
|
||||
public function testValidImportReplacesTheWholeTable(): void
|
||||
{
|
||||
$admin = $this->createUser(['ROLE_ADMIN']);
|
||||
|
||||
$body = $this->authJson('POST', '/api/v1/admin/categories/tags/import', $admin, [
|
||||
['id' => 90001, 'name' => 'تگ یک', 'slug' => 'imp-tag-1', 'status' => 1],
|
||||
['id' => 90002, 'name' => 'تگ دو', 'slug' => 'imp-tag-2', 'status' => 0],
|
||||
]);
|
||||
|
||||
$this->assertSame(200, $this->responseCode());
|
||||
$this->assertSame(2, $body['data']['imported'] ?? null);
|
||||
// wipe+replace: the table now holds exactly the imported rows
|
||||
$this->assertSame(2, $this->rowCount('tags'));
|
||||
$this->assertSame('imp-tag-1', $this->db()->fetchOne('SELECT slug FROM tags WHERE id = 90001'));
|
||||
}
|
||||
|
||||
public function testInvalidFileIsRejectedAndNothingIsWritten(): void
|
||||
{
|
||||
$admin = $this->createUser(['ROLE_ADMIN']);
|
||||
|
||||
// seed a known state, then attempt a bad import — count must not change
|
||||
$this->authJson('POST', '/api/v1/admin/categories/tags/import', $admin, [
|
||||
['id' => 91001, 'name' => 'پایه', 'slug' => 'seed-tag', 'status' => 1],
|
||||
]);
|
||||
$before = $this->rowCount('tags');
|
||||
|
||||
$body = $this->authJson('POST', '/api/v1/admin/categories/tags/import', $admin, [
|
||||
['id' => 1, 'name' => 'خوب', 'slug' => 'ok', 'status' => 1],
|
||||
['id' => 1, 'name' => '', 'slug' => 'ok', 'status' => 9], // dup id, empty name, dup slug, bad status
|
||||
]);
|
||||
|
||||
$this->assertSame(422, $this->responseCode());
|
||||
$this->assertNotEmpty($body['errors']);
|
||||
$this->assertSame($before, $this->rowCount('tags')); // table untouched
|
||||
}
|
||||
|
||||
public function testSpecialtyParentMustExistWithinTheFile(): void
|
||||
{
|
||||
$admin = $this->createUser(['ROLE_ADMIN']);
|
||||
|
||||
$body = $this->authJson('POST', '/api/v1/admin/categories/specialties/import', $admin, [
|
||||
['id' => 1, 'name' => 'ریشه', 'slug' => 'imp-root', 'status' => 1, 'weight' => 0, 'parent_id' => null],
|
||||
['id' => 2, 'name' => 'فرزند', 'slug' => 'imp-child', 'status' => 1, 'weight' => 0, 'parent_id' => 9999],
|
||||
]);
|
||||
|
||||
$this->assertSame(422, $this->responseCode());
|
||||
$fields = array_column($body['errors'], 'field');
|
||||
$this->assertContains('parent_id', $fields);
|
||||
}
|
||||
|
||||
public function testNonAdminIsForbidden(): void
|
||||
{
|
||||
$user = $this->createUser(['ROLE_USER']);
|
||||
$this->authJson('POST', '/api/v1/admin/categories/tags/import', $user, [
|
||||
['id' => 1, 'name' => 'x', 'slug' => 'x', 'status' => 1],
|
||||
]);
|
||||
$this->assertSame(403, $this->responseCode());
|
||||
}
|
||||
|
||||
public function testUnknownBundleIs404(): void
|
||||
{
|
||||
$admin = $this->createUser(['ROLE_ADMIN']);
|
||||
$this->authJson('POST', '/api/v1/admin/categories/nope/import', $admin, [
|
||||
['id' => 1, 'name' => 'x', 'slug' => 'x', 'status' => 1],
|
||||
]);
|
||||
$this->assertSame(404, $this->responseCode());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user