From 802a511aa0a0da03b654e6760b965e047bed5c4a Mon Sep 17 00:00:00 2001 From: kent Date: Tue, 30 Sep 2025 23:56:25 +0900 Subject: [PATCH] =?UTF-8?q?Revert=20"feat:=20DB=20=EC=97=B0=EA=B2=B0=20?= =?UTF-8?q?=EC=98=A4=EB=B2=84=EB=9D=BC=EC=9D=B4=EB=94=A9=20=EB=B0=8F=20?= =?UTF-8?q?=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20=ED=86=B5=EA=B3=84=20?= =?UTF-8?q?=EC=9C=84=EC=A0=AF=20=EC=B6=94=EA=B0=80"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit bf8036a64baf7a4df132e301355f4dda684f2bad. --- CURRENT_WORKS.md | 142 ++- .../V1/Design/BomConditionRuleController.php | 335 ----- .../Api/V1/Design/BomResolverController.php | 281 ----- .../Api/V1/Design/ModelFormulaController.php | 278 ---- .../V1/Design/ModelParameterController.php | 227 ---- .../V1/Design/ProductFromModelController.php | 258 ---- .../V1/Schemas/BomConditionRuleSchemas.php | 133 -- .../Api/V1/Schemas/BomResolverSchemas.php | 314 ----- .../Api/V1/Schemas/ModelFormulaSchemas.php | 80 -- .../Api/V1/Schemas/ModelParameterSchemas.php | 107 -- .../CreateBomConditionRuleRequest.php | 63 - .../IndexBomConditionRuleRequest.php | 44 - .../UpdateBomConditionRuleRequest.php | 66 - .../CreateProductFromModelRequest.php | 89 -- .../V1/BomResolver/ResolvePreviewRequest.php | 54 - .../V1/Design/BomConditionRuleFormRequest.php | 296 ----- .../Api/V1/Design/BomResolverFormRequest.php | 274 ---- .../Api/V1/Design/ModelFormulaFormRequest.php | 264 ---- .../V1/Design/ModelParameterFormRequest.php | 172 --- .../V1/Design/ProductFromModelFormRequest.php | 388 ------ .../CreateModelFormulaRequest.php | 67 - .../ModelFormula/IndexModelFormulaRequest.php | 30 - .../UpdateModelFormulaRequest.php | 70 - .../CreateModelParameterRequest.php | 83 -- .../IndexModelParameterRequest.php | 30 - .../UpdateModelParameterRequest.php | 86 -- app/Http/Requests/BomConditionRuleRequest.php | 169 --- app/Http/Requests/BomResolveRequest.php | 81 -- app/Http/Requests/ModelFormulaRequest.php | 214 ---- app/Http/Requests/ModelParameterRequest.php | 127 -- app/Http/Requests/ProductFromModelRequest.php | 138 -- app/Models/Design/BomConditionRule.php | 179 --- app/Models/Design/ModelFormula.php | 122 -- app/Models/Design/ModelParameter.php | 94 -- app/Services/BomConditionRuleService.php | 436 ------- app/Services/BomResolverService.php | 505 -------- .../Design/BomConditionRuleService.php | 492 -------- app/Services/Design/BomResolverService.php | 471 ------- app/Services/Design/ModelFormulaService.php | 461 ------- app/Services/Design/ModelParameterService.php | 344 ----- .../Design/ProductFromModelService.php | 422 ------- app/Services/ModelFormulaService.php | 505 -------- app/Services/ModelParameterService.php | 278 ---- app/Services/ProductFromModelService.php | 496 -------- .../factories/BomConditionRuleFactory.php | 167 --- database/factories/ModelFactory.php | 96 -- database/factories/ModelFormulaFactory.php | 151 --- database/factories/ModelParameterFactory.php | 173 --- ...00000_create_bom_template_groups_table.php | 50 - ...5_110000_create_model_parameters_table.php | 55 - ..._15_120000_create_model_formulas_table.php | 53 - ...30000_create_bom_condition_rules_table.php | 57 - ...140000_create_product_parameters_table.php | 52 - ...create_product_calculated_values_table.php | 54 - ...d_group_id_to_bom_template_items_table.php | 52 - ...dd_parametric_fields_to_products_table.php | 40 - database/seeders/KSS01ModelSeeder.php | 573 --------- .../seeders/ParameterBasedBomTestSeeder.php | 158 --- database/seeders/ParametricBomSeeder.php | 820 ------------ docs/parameter-based-bom-api.md | 651 ---------- docs/parameter-based-bom-endpoints.md | 264 ---- docs/parametric_bom_schema.md | 250 ---- scripts/validation/README.md | 424 ------- scripts/validation/performance_test.php | 559 -------- scripts/validation/test_kss01_scenarios.php | 581 --------- scripts/validation/validate_bom_system.php | 573 --------- tests/Feature/Design/BomConditionRuleTest.php | 538 -------- tests/Feature/Design/BomResolverTest.php | 708 ----------- tests/Feature/Design/ModelFormulaTest.php | 436 ------- tests/Feature/Design/ModelParameterTest.php | 339 ----- tests/Feature/Design/ProductFromModelTest.php | 679 ---------- tests/Feature/ParameterBasedBomApiTest.php | 606 --------- .../BomResolutionPerformanceTest.php | 490 ------- tests/README.md | 251 ---- tests/Security/ApiSecurityTest.php | 414 ------ tests/Unit/BomConditionRuleServiceTest.php | 436 ------- tests/Unit/ModelFormulaServiceTest.php | 383 ------ tests/Unit/ModelParameterServiceTest.php | 261 ---- tests/Unit/ProductFromModelServiceTest.php | 405 ------ .../parametric_bom.postman_collection.json | 1121 ----------------- .../parametric_bom.postman_environment.json | 49 - 81 files changed, 102 insertions(+), 22632 deletions(-) delete mode 100644 app/Http/Controllers/Api/V1/Design/BomConditionRuleController.php delete mode 100644 app/Http/Controllers/Api/V1/Design/BomResolverController.php delete mode 100644 app/Http/Controllers/Api/V1/Design/ModelFormulaController.php delete mode 100644 app/Http/Controllers/Api/V1/Design/ModelParameterController.php delete mode 100644 app/Http/Controllers/Api/V1/Design/ProductFromModelController.php delete mode 100644 app/Http/Controllers/Api/V1/Schemas/BomConditionRuleSchemas.php delete mode 100644 app/Http/Controllers/Api/V1/Schemas/BomResolverSchemas.php delete mode 100644 app/Http/Controllers/Api/V1/Schemas/ModelFormulaSchemas.php delete mode 100644 app/Http/Controllers/Api/V1/Schemas/ModelParameterSchemas.php delete mode 100644 app/Http/Requests/Api/V1/BomConditionRule/CreateBomConditionRuleRequest.php delete mode 100644 app/Http/Requests/Api/V1/BomConditionRule/IndexBomConditionRuleRequest.php delete mode 100644 app/Http/Requests/Api/V1/BomConditionRule/UpdateBomConditionRuleRequest.php delete mode 100644 app/Http/Requests/Api/V1/BomResolver/CreateProductFromModelRequest.php delete mode 100644 app/Http/Requests/Api/V1/BomResolver/ResolvePreviewRequest.php delete mode 100644 app/Http/Requests/Api/V1/Design/BomConditionRuleFormRequest.php delete mode 100644 app/Http/Requests/Api/V1/Design/BomResolverFormRequest.php delete mode 100644 app/Http/Requests/Api/V1/Design/ModelFormulaFormRequest.php delete mode 100644 app/Http/Requests/Api/V1/Design/ModelParameterFormRequest.php delete mode 100644 app/Http/Requests/Api/V1/Design/ProductFromModelFormRequest.php delete mode 100644 app/Http/Requests/Api/V1/ModelFormula/CreateModelFormulaRequest.php delete mode 100644 app/Http/Requests/Api/V1/ModelFormula/IndexModelFormulaRequest.php delete mode 100644 app/Http/Requests/Api/V1/ModelFormula/UpdateModelFormulaRequest.php delete mode 100644 app/Http/Requests/Api/V1/ModelParameter/CreateModelParameterRequest.php delete mode 100644 app/Http/Requests/Api/V1/ModelParameter/IndexModelParameterRequest.php delete mode 100644 app/Http/Requests/Api/V1/ModelParameter/UpdateModelParameterRequest.php delete mode 100644 app/Http/Requests/BomConditionRuleRequest.php delete mode 100644 app/Http/Requests/BomResolveRequest.php delete mode 100644 app/Http/Requests/ModelFormulaRequest.php delete mode 100644 app/Http/Requests/ModelParameterRequest.php delete mode 100644 app/Http/Requests/ProductFromModelRequest.php delete mode 100644 app/Models/Design/BomConditionRule.php delete mode 100644 app/Models/Design/ModelFormula.php delete mode 100644 app/Models/Design/ModelParameter.php delete mode 100644 app/Services/BomConditionRuleService.php delete mode 100644 app/Services/BomResolverService.php delete mode 100644 app/Services/Design/BomConditionRuleService.php delete mode 100644 app/Services/Design/BomResolverService.php delete mode 100644 app/Services/Design/ModelFormulaService.php delete mode 100644 app/Services/Design/ModelParameterService.php delete mode 100644 app/Services/Design/ProductFromModelService.php delete mode 100644 app/Services/ModelFormulaService.php delete mode 100644 app/Services/ModelParameterService.php delete mode 100644 app/Services/ProductFromModelService.php delete mode 100644 database/factories/BomConditionRuleFactory.php delete mode 100644 database/factories/ModelFactory.php delete mode 100644 database/factories/ModelFormulaFactory.php delete mode 100644 database/factories/ModelParameterFactory.php delete mode 100644 database/migrations/2024_01_15_100000_create_bom_template_groups_table.php delete mode 100644 database/migrations/2024_01_15_110000_create_model_parameters_table.php delete mode 100644 database/migrations/2024_01_15_120000_create_model_formulas_table.php delete mode 100644 database/migrations/2024_01_15_130000_create_bom_condition_rules_table.php delete mode 100644 database/migrations/2024_01_15_140000_create_product_parameters_table.php delete mode 100644 database/migrations/2024_01_15_150000_create_product_calculated_values_table.php delete mode 100644 database/migrations/2024_01_15_160000_add_group_id_to_bom_template_items_table.php delete mode 100644 database/migrations/2024_01_15_170000_add_parametric_fields_to_products_table.php delete mode 100644 database/seeders/KSS01ModelSeeder.php delete mode 100644 database/seeders/ParameterBasedBomTestSeeder.php delete mode 100644 database/seeders/ParametricBomSeeder.php delete mode 100644 docs/parameter-based-bom-api.md delete mode 100644 docs/parameter-based-bom-endpoints.md delete mode 100644 docs/parametric_bom_schema.md delete mode 100644 scripts/validation/README.md delete mode 100755 scripts/validation/performance_test.php delete mode 100755 scripts/validation/test_kss01_scenarios.php delete mode 100755 scripts/validation/validate_bom_system.php delete mode 100644 tests/Feature/Design/BomConditionRuleTest.php delete mode 100644 tests/Feature/Design/BomResolverTest.php delete mode 100644 tests/Feature/Design/ModelFormulaTest.php delete mode 100644 tests/Feature/Design/ModelParameterTest.php delete mode 100644 tests/Feature/Design/ProductFromModelTest.php delete mode 100644 tests/Feature/ParameterBasedBomApiTest.php delete mode 100644 tests/Performance/BomResolutionPerformanceTest.php delete mode 100644 tests/README.md delete mode 100644 tests/Security/ApiSecurityTest.php delete mode 100644 tests/Unit/BomConditionRuleServiceTest.php delete mode 100644 tests/Unit/ModelFormulaServiceTest.php delete mode 100644 tests/Unit/ModelParameterServiceTest.php delete mode 100644 tests/Unit/ProductFromModelServiceTest.php delete mode 100644 tests/postman/parametric_bom.postman_collection.json delete mode 100644 tests/postman/parametric_bom.postman_environment.json diff --git a/CURRENT_WORKS.md b/CURRENT_WORKS.md index d4e63d1..05c398e 100644 --- a/CURRENT_WORKS.md +++ b/CURRENT_WORKS.md @@ -1,55 +1,117 @@ # SAM API 저장소 작업 현황 -## 2025-09-30 (월) - DB 연결 환경변수 오버라이딩 설정 +## 2025-09-24 (화) - FK 제약조건 최적화 및 데이터베이스 성능 개선 ### 주요 작업 -- 로컬/Docker 환경 DB 연결 오버라이딩 설정 +- 데이터베이스 FK 제약조건 분석 및 최적화 +- 성능과 관리 편의성을 위한 비중요 FK 제거 +- 3단계 점진적 FK 제거 마이그레이션 구현 -### 수정된 파일 +### 추가된 파일: +- `database/migrations/2025_09_24_214146_remove_non_critical_foreign_keys_phase1.php` - 1차 FK 제거 (Classifications, Departments) +- `database/migrations/2025_09_24_214200_remove_estimate_foreign_keys_phase2.php` - 2차 FK 제거 (견적 시스템) +- `database/migrations/2025_09_24_214300_remove_material_foreign_key_phase3.php` - 3차 FK 제거 (제품-자재 관계) +- `CURRENT_WORKS.md` - 저장소별 작업 현황 추적 -#### 환경 설정 -- `.env` (라인 29) - DB_HOST 로컬 설정 (127.0.0.1) - - 기존: `DB_HOST=${DB_HOST:-mysql}` (환경변수 파싱 오류) - - 변경: `DB_HOST=127.0.0.1` (로컬 MySQL 컨테이너 접근) - - Docker 환경은 docker-compose.yml에서 자동 오버라이드 +### 수정된 파일: +- `CLAUDE.md` - CURRENT_WORKS.md 파일 위치 규칙 명확화 +- `database/migrations/2025_09_24_000002_create_dynamic_estimate_fields.php` - level 컬럼 제거로 마이그레이션 오류 해결 -### 작업 내용 +### 작업 내용: -#### DB 연결 오류 해결 -**문제**: -- `.env` 파일의 `${DB_HOST:-mysql}` 형식이 Laravel에서 리터럴 문자열로 인식 -- 에러: `php_network_getaddresses: getaddrinfo for ${DB_HOST failed` +#### 1. FK 제약조건 현황 분석 +- 현재 8개 마이그레이션에서 FK 제약조건 사용 확인 +- 권한 관리, 제품/자재 관리, 견적 시스템, 기타 시스템별 분류 +- 총 15+개의 FK 제약조건 식별 -**해결**: -1. `.env`: `DB_HOST=127.0.0.1` (로컬 기본값) -2. `docker-compose.yml`: 환경변수 `DB_HOST=mysql`로 오버라이드 -3. 로컬/Docker 모두 정상 연결 확인 +#### 2. 중요도별 테이블 분류 +**🔴 핵심 테이블 (FK 유지 필수):** +- 인증/권한 시스템: users, roles, permissions 관계 +- 제품/BOM 관리 핵심: products.category_id, product_components 내부 관계 +- 멀티테넌트 핵심: 모든 tenant_id 참조 -#### 환경변수 오버라이딩 구조 -**로컬 실행 시** (`php artisan serve`): -- `.env`의 `DB_HOST=127.0.0.1` 사용 -- 호스트에서 MySQL 컨테이너 포트 3306으로 직접 접근 +**🟡 중요 테이블 (FK 선택적 유지):** +- 견적 시스템: estimates, estimate_items 관계 +- 자재 관리: product_components.material_id -**Docker 컨테이너 실행 시**: -- docker-compose.yml 환경변수가 `.env` 값을 오버라이드 -- `DB_HOST=mysql`로 컨테이너 간 통신 -- `samnet` 네트워크를 통한 내부 DNS 해석 +**🟢 일반 테이블 (FK 제거 권장):** +- 분류/코드 관리: classifications.tenant_id +- 부서 관리: departments.parent_id (자기참조) +- 감사 로그: 모든 audit 관련 FK -### 품질 검증 -- ✅ 로컬 DB 연결: `php artisan tinker` 정상 작동 -- ✅ Docker DB 연결: 컨테이너 내부 연결 확인 -- ✅ 마이그레이션: `php artisan migrate:status` 성공 -- ✅ Tinker 테스트: `DB::connection()->getPdo()` 성공 +#### 3. 코드 영향도 분석 결과 +**✅ 중요 결론: 모델/컨트롤러/서비스 코드 수정 불필요!** +- Laravel Eloquent 관계가 FK 제약조건과 독립적으로 작동 +- 현재 코드가 CASCADE 동작에 의존하지 않음 +- BelongsToTenant 트레잇과 소프트 딜리트로 무결성 관리 +- 비즈니스 로직이 애플리케이션 레벨에서 처리됨 -### 현재 상태 -- ✅ API 서버 정상 작동 -- ✅ 로컬/Docker DB 연결 안정화 -- ✅ Swagger 문서 정상 접근 가능 -- ⚠️ Parameter-based BOM 파일들 untracked 상태 (개발 진행 중) +#### 4. 3단계 점진적 FK 제거 전략 -### 참고사항 -- API는 DB 스키마 관리 주체이므로 모든 마이그레이션은 API에서만 실행 -- Admin/Front는 데이터 CRUD만 가능, 테이블/컬럼 작업 금지 +**Phase 1 (즉시 적용 가능):** +- `classifications.tenant_id` → `tenants` +- `departments.parent_id` → `departments` (자기참조) +- 영향도: 낮음, 관리 편의성 증가 ---- -**업데이트**: 2025-09-30 23:30 KST \ No newline at end of file +**Phase 2 (견적 시스템):** +- `estimates.model_set_id` → `categories` +- `estimate_items.estimate_id` → `estimates` +- 영향도: 중간, 성능 향상 효과 +- 멀티테넌트 보안 FK는 유지 + +**Phase 3 (신중한 검토 필요):** +- `product_components.material_id` → `materials` +- 영향도: 중간, 자재 관리 유연성 증가 +- 핵심 제품 관계 FK는 유지 + +#### 5. 마이그레이션 특징 +- 동적 FK 이름 탐지로 안전한 제거 +- 성능을 위한 인덱스 유지/추가 +- 상세한 진행 상황 로깅 +- 완전한 롤백 기능 +- 각 단계별 영향도와 주의사항 문서화 + +### 데이터베이스 마이그레이션 상태: +- 기존 마이그레이션 오류 해결 완료 (level 컬럼 이슈) +- 새로운 FK 제거 마이그레이션 3개 생성 +- 롤백 가능한 안전한 구조로 설계 + +### 예상 효과: +1. **성능 향상**: 견적 시스템과 분류 관리에서 FK 검증 오버헤드 제거 +2. **관리 편의성**: 부서 구조 변경, 자재 관리 시 유연성 증가 +3. **개발 생산성**: 데이터 변경 시 FK 제약 에러 감소 +4. **확장성**: 향후 시스템 확장 시 유연한 스키마 변경 가능 + +### 향후 작업: +1. Phase 1 마이그레이션 개발 서버 테스트 +2. 각 단계별 성능 영향 모니터링 +3. Service 레벨에서 데이터 무결성 검증 로직 보강 검토 +4. 프로덕션 적용 전 백업 및 롤백 계획 수립 + +### 논리적 관계 자동화 시스템 구축: +- **자동화 도구 4개 생성**: 관계 문서 생성/업데이트/모델생성 명령어 +- **Provider 시스템**: 마이그레이션 후 자동 문서 업데이트 +- **간소화 문서**: 즉시 사용 가능한 관계 문서 생성 (LOGICAL_RELATIONSHIPS_SIMPLE.md) + +### 새로운 명령어: +- `php artisan db:update-relationships` - 모델에서 관계 자동 추출 +- `php artisan db:generate-simple-relationships` - 기본 관계 문서 생성 +- `php artisan make:model-with-docs` - 모델 생성 후 관계 문서 자동 업데이트 + +### ERD 생성 시스템: +- **ERD 생성 도구**: beyondcode/laravel-er-diagram-generator 활용 +- **GraphViz 설치**: `brew install graphviz`로 dot 명령어 지원 +- **모델 오류 해결**: BelongsToTenantTrait → BelongsToTenant 수정 +- **생성 결과**: 60개 모델의 완전한 관계도 생성 (`graph.png`, 4.1MB) +- **명령어**: `php artisan generate:erd --format=png` + +### 예상 효과 (업데이트): +1. **시각화 개선**: 복잡한 다중 테넌트 구조의 시각적 이해 향상 +2. **개발 생산성**: ERD를 통한 빠른 스키마 파악 및 설계 검증 +3. **문서화 자동화**: 스키마 변경 시 ERD 자동 업데이트 가능 +4. **기존 효과 유지**: 성능 향상, 관리 편의성, 확장성은 FK 제거로 달성 + +### Git 커밋: +- `cfd4c25` - fix: categories 테이블 level 컬럼 제거로 마이그레이션 오류 해결 +- `7dafab3` - docs: CURRENT_WORKS.md 파일 위치 규칙 명확화 +- `c63e676` - feat: 데이터베이스 FK 제약조건 최적화 및 3단계 마이그레이션 구현 \ No newline at end of file diff --git a/app/Http/Controllers/Api/V1/Design/BomConditionRuleController.php b/app/Http/Controllers/Api/V1/Design/BomConditionRuleController.php deleted file mode 100644 index c1be722..0000000 --- a/app/Http/Controllers/Api/V1/Design/BomConditionRuleController.php +++ /dev/null @@ -1,335 +0,0 @@ -service->getModelConditionRules($modelId, $request->validated()); - }, __('message.fetched')); - } - - /** - * @OA\Post( - * path="/v1/design/models/{modelId}/condition-rules", - * summary="Create BOM condition rule", - * description="Create a new condition rule for a specific model", - * security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}}, - * tags={"BOM Condition Rules"}, - * @OA\Parameter( - * name="modelId", - * description="Model ID", - * in="path", - * required=true, - * @OA\Schema(type="integer", example=1) - * ), - * @OA\RequestBody( - * required=true, - * @OA\JsonContent(ref="#/components/schemas/CreateBomConditionRuleRequest") - * ), - * @OA\Response( - * response=201, - * description="BOM condition rule created successfully", - * @OA\JsonContent( - * allOf={ - * @OA\Schema(ref="#/components/schemas/ApiResponse"), - * @OA\Schema( - * @OA\Property(property="data", ref="#/components/schemas/BomConditionRuleResource") - * ) - * } - * ) - * ), - * @OA\Response(ref="#/components/responses/ValidationErrorResponse", response=422), - * @OA\Response(ref="#/components/responses/ErrorResponse", response=400), - * @OA\Response(ref="#/components/responses/UnauthorizedResponse", response=401) - * ) - */ - public function store(BomConditionRuleFormRequest $request, int $modelId) - { - return ApiResponse::handle(function () use ($request, $modelId) { - return $this->service->createConditionRule($modelId, $request->validated()); - }, __('message.created')); - } - - /** - * @OA\Put( - * path="/v1/design/models/{modelId}/condition-rules/{ruleId}", - * summary="Update BOM condition rule", - * description="Update a specific BOM condition rule", - * security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}}, - * tags={"BOM Condition Rules"}, - * @OA\Parameter( - * name="modelId", - * description="Model ID", - * in="path", - * required=true, - * @OA\Schema(type="integer", example=1) - * ), - * @OA\Parameter( - * name="ruleId", - * description="Rule ID", - * in="path", - * required=true, - * @OA\Schema(type="integer", example=1) - * ), - * @OA\RequestBody( - * required=true, - * @OA\JsonContent(ref="#/components/schemas/UpdateBomConditionRuleRequest") - * ), - * @OA\Response( - * response=200, - * description="BOM condition rule updated successfully", - * @OA\JsonContent( - * allOf={ - * @OA\Schema(ref="#/components/schemas/ApiResponse"), - * @OA\Schema( - * @OA\Property(property="data", ref="#/components/schemas/BomConditionRuleResource") - * ) - * } - * ) - * ), - * @OA\Response(ref="#/components/responses/ValidationErrorResponse", response=422), - * @OA\Response(ref="#/components/responses/ErrorResponse", response=400), - * @OA\Response(ref="#/components/responses/UnauthorizedResponse", response=401), - * @OA\Response(ref="#/components/responses/NotFoundResponse", response=404) - * ) - */ - public function update(BomConditionRuleFormRequest $request, int $modelId, int $ruleId) - { - return ApiResponse::handle(function () use ($request, $modelId, $ruleId) { - return $this->service->updateConditionRule($modelId, $ruleId, $request->validated()); - }, __('message.updated')); - } - - /** - * @OA\Delete( - * path="/v1/design/models/{modelId}/condition-rules/{ruleId}", - * summary="Delete BOM condition rule", - * description="Delete a specific BOM condition rule", - * security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}}, - * tags={"BOM Condition Rules"}, - * @OA\Parameter( - * name="modelId", - * description="Model ID", - * in="path", - * required=true, - * @OA\Schema(type="integer", example=1) - * ), - * @OA\Parameter( - * name="ruleId", - * description="Rule ID", - * in="path", - * required=true, - * @OA\Schema(type="integer", example=1) - * ), - * @OA\Response( - * response=200, - * description="BOM condition rule deleted successfully", - * @OA\JsonContent(ref="#/components/schemas/ApiResponse") - * ), - * @OA\Response(ref="#/components/responses/ErrorResponse", response=400), - * @OA\Response(ref="#/components/responses/UnauthorizedResponse", response=401), - * @OA\Response(ref="#/components/responses/NotFoundResponse", response=404) - * ) - */ - public function destroy(int $modelId, int $ruleId) - { - return ApiResponse::handle(function () use ($modelId, $ruleId) { - $this->service->deleteConditionRule($modelId, $ruleId); - return null; - }, __('message.deleted')); - } - - /** - * @OA\Post( - * path="/v1/design/models/{modelId}/condition-rules/{ruleId}/validate", - * summary="Validate BOM condition rule", - * description="Validate condition rule expression and logic", - * security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}}, - * tags={"BOM Condition Rules"}, - * @OA\Parameter( - * name="modelId", - * description="Model ID", - * in="path", - * required=true, - * @OA\Schema(type="integer", example=1) - * ), - * @OA\Parameter( - * name="ruleId", - * description="Rule ID", - * in="path", - * required=true, - * @OA\Schema(type="integer", example=1) - * ), - * @OA\Response( - * response=200, - * description="Condition rule validation result", - * @OA\JsonContent( - * allOf={ - * @OA\Schema(ref="#/components/schemas/ApiResponse"), - * @OA\Schema( - * @OA\Property( - * property="data", - * @OA\Property(property="is_valid", type="boolean", example=true), - * @OA\Property( - * property="validation_errors", - * type="array", - * @OA\Items(type="string", example="Invalid condition syntax") - * ), - * @OA\Property( - * property="tested_scenarios", - * type="array", - * @OA\Items( - * @OA\Property(property="scenario", type="string", example="W0=1000, H0=800"), - * @OA\Property(property="result", type="boolean", example=true) - * ) - * ) - * ) - * ) - * } - * ) - * ), - * @OA\Response(ref="#/components/responses/ErrorResponse", response=400), - * @OA\Response(ref="#/components/responses/UnauthorizedResponse", response=401), - * @OA\Response(ref="#/components/responses/NotFoundResponse", response=404) - * ) - */ - public function validate(int $modelId, int $ruleId) - { - return ApiResponse::handle(function () use ($modelId, $ruleId) { - return $this->service->validateConditionRule($modelId, $ruleId); - }, __('message.condition_rule.validated')); - } - - /** - * @OA\Post( - * path="/v1/design/models/{modelId}/condition-rules/reorder", - * summary="Reorder BOM condition rules", - * description="Change the priority order of condition rules", - * security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}}, - * tags={"BOM Condition Rules"}, - * @OA\Parameter( - * name="modelId", - * description="Model ID", - * in="path", - * required=true, - * @OA\Schema(type="integer", example=1) - * ), - * @OA\RequestBody( - * required=true, - * @OA\JsonContent( - * @OA\Property( - * property="rule_orders", - * type="array", - * @OA\Items( - * @OA\Property(property="rule_id", type="integer", example=1), - * @OA\Property(property="priority", type="integer", example=1) - * ) - * ) - * ) - * ), - * @OA\Response( - * response=200, - * description="BOM condition rules reordered successfully", - * @OA\JsonContent(ref="#/components/schemas/ApiResponse") - * ), - * @OA\Response(ref="#/components/responses/ValidationErrorResponse", response=422), - * @OA\Response(ref="#/components/responses/ErrorResponse", response=400), - * @OA\Response(ref="#/components/responses/UnauthorizedResponse", response=401), - * @OA\Response(ref="#/components/responses/NotFoundResponse", response=404) - * ) - */ - public function reorder(int $modelId) - { - return ApiResponse::handle(function () use ($modelId) { - $ruleOrders = request()->input('rule_orders', []); - $this->service->reorderConditionRules($modelId, $ruleOrders); - return null; - }, __('message.reordered')); - } -} \ No newline at end of file diff --git a/app/Http/Controllers/Api/V1/Design/BomResolverController.php b/app/Http/Controllers/Api/V1/Design/BomResolverController.php deleted file mode 100644 index 69d9c3d..0000000 --- a/app/Http/Controllers/Api/V1/Design/BomResolverController.php +++ /dev/null @@ -1,281 +0,0 @@ -service->generatePreview($modelId, $request->validated()); - }, __('message.bom.preview_generated')); - } - - /** - * @OA\Post( - * path="/v1/design/models/{modelId}/validate-parameters", - * summary="Validate model parameters", - * description="Validate input parameters against model constraints before BOM resolution", - * security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}}, - * tags={"BOM Resolver"}, - * @OA\Parameter( - * name="modelId", - * description="Model ID", - * in="path", - * required=true, - * @OA\Schema(type="integer", example=1) - * ), - * @OA\RequestBody( - * required=true, - * @OA\JsonContent( - * @OA\Property( - * property="input_parameters", - * description="Input parameter values to validate", - * type="object", - * additionalProperties=@OA\Property(oneOf={ - * @OA\Schema(type="number"), - * @OA\Schema(type="string"), - * @OA\Schema(type="boolean") - * }), - * example={"W0": 1000, "H0": 800, "installation_type": "A"} - * ) - * ) - * ), - * @OA\Response( - * response=200, - * description="Parameter validation result", - * @OA\JsonContent( - * allOf={ - * @OA\Schema(ref="#/components/schemas/ApiResponse"), - * @OA\Schema( - * @OA\Property( - * property="data", - * @OA\Property(property="is_valid", type="boolean", example=true), - * @OA\Property( - * property="validation_errors", - * type="array", - * @OA\Items( - * @OA\Property(property="parameter", type="string", example="W0"), - * @OA\Property(property="error", type="string", example="Value must be between 500 and 2000") - * ) - * ), - * @OA\Property( - * property="warnings", - * type="array", - * @OA\Items( - * @OA\Property(property="parameter", type="string", example="H0"), - * @OA\Property(property="warning", type="string", example="Recommended range is 600-1500") - * ) - * ) - * ) - * ) - * } - * ) - * ), - * @OA\Response(ref="#/components/responses/ValidationErrorResponse", response=422), - * @OA\Response(ref="#/components/responses/ErrorResponse", response=400), - * @OA\Response(ref="#/components/responses/UnauthorizedResponse", response=401), - * @OA\Response(ref="#/components/responses/NotFoundResponse", response=404) - * ) - */ - public function validateParameters(int $modelId) - { - return ApiResponse::handle(function () use ($modelId) { - $inputParameters = request()->input('input_parameters', []); - return $this->service->validateParameters($modelId, $inputParameters); - }, __('message.parameters.validated')); - } - - /** - * @OA\Get( - * path="/v1/design/models/{modelId}/parameter-schema", - * summary="Get model parameter schema", - * description="Retrieve the input parameter schema for a specific model", - * security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}}, - * tags={"BOM Resolver"}, - * @OA\Parameter( - * name="modelId", - * description="Model ID", - * in="path", - * required=true, - * @OA\Schema(type="integer", example=1) - * ), - * @OA\Response( - * response=200, - * description="Model parameter schema retrieved successfully", - * @OA\JsonContent( - * allOf={ - * @OA\Schema(ref="#/components/schemas/ApiResponse"), - * @OA\Schema( - * @OA\Property( - * property="data", - * @OA\Property( - * property="input_parameters", - * type="array", - * @OA\Items( - * @OA\Property(property="name", type="string", example="W0"), - * @OA\Property(property="label", type="string", example="Width"), - * @OA\Property(property="data_type", type="string", example="INTEGER"), - * @OA\Property(property="unit", type="string", example="mm"), - * @OA\Property(property="min_value", type="number", example=500), - * @OA\Property(property="max_value", type="number", example=3000), - * @OA\Property(property="default_value", type="string", example="1000"), - * @OA\Property(property="is_required", type="boolean", example=true), - * @OA\Property(property="description", type="string", example="Product width in millimeters") - * ) - * ), - * @OA\Property( - * property="output_parameters", - * type="array", - * @OA\Items( - * @OA\Property(property="name", type="string", example="W1"), - * @OA\Property(property="label", type="string", example="Actual Width"), - * @OA\Property(property="data_type", type="string", example="INTEGER"), - * @OA\Property(property="unit", type="string", example="mm"), - * @OA\Property(property="description", type="string", example="Calculated actual width") - * ) - * ) - * ) - * ) - * } - * ) - * ), - * @OA\Response(ref="#/components/responses/ErrorResponse", response=400), - * @OA\Response(ref="#/components/responses/UnauthorizedResponse", response=401), - * @OA\Response(ref="#/components/responses/NotFoundResponse", response=404) - * ) - */ - public function getParameterSchema(int $modelId) - { - return ApiResponse::handle(function () use ($modelId) { - return $this->service->getParameterSchema($modelId); - }, __('message.fetched')); - } - - /** - * @OA\Post( - * path="/v1/design/models/{modelId}/calculate-preview", - * summary="Calculate output values preview", - * description="Calculate output parameter values based on input parameters using formulas", - * security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}}, - * tags={"BOM Resolver"}, - * @OA\Parameter( - * name="modelId", - * description="Model ID", - * in="path", - * required=true, - * @OA\Schema(type="integer", example=1) - * ), - * @OA\RequestBody( - * required=true, - * @OA\JsonContent( - * @OA\Property( - * property="input_parameters", - * description="Input parameter values", - * type="object", - * additionalProperties=@OA\Property(oneOf={ - * @OA\Schema(type="number"), - * @OA\Schema(type="string"), - * @OA\Schema(type="boolean") - * }), - * example={"W0": 1000, "H0": 800, "installation_type": "A"} - * ) - * ) - * ), - * @OA\Response( - * response=200, - * description="Output values calculated successfully", - * @OA\JsonContent( - * allOf={ - * @OA\Schema(ref="#/components/schemas/ApiResponse"), - * @OA\Schema( - * @OA\Property( - * property="data", - * @OA\Property( - * property="calculated_values", - * type="object", - * additionalProperties=@OA\Property(oneOf={ - * @OA\Schema(type="number"), - * @OA\Schema(type="string") - * }), - * example={"W1": 1050, "H1": 850, "area": 892500, "weight": 45.5} - * ), - * @OA\Property( - * property="calculation_steps", - * type="array", - * @OA\Items( - * @OA\Property(property="parameter", type="string", example="W1"), - * @OA\Property(property="formula", type="string", example="W0 + 50"), - * @OA\Property(property="result", oneOf={ - * @OA\Schema(type="number"), - * @OA\Schema(type="string") - * }, example=1050) - * ) - * ) - * ) - * ) - * } - * ) - * ), - * @OA\Response(ref="#/components/responses/ValidationErrorResponse", response=422), - * @OA\Response(ref="#/components/responses/ErrorResponse", response=400), - * @OA\Response(ref="#/components/responses/UnauthorizedResponse", response=401), - * @OA\Response(ref="#/components/responses/NotFoundResponse", response=404) - * ) - */ - public function calculatePreview(int $modelId) - { - return ApiResponse::handle(function () use ($modelId) { - $inputParameters = request()->input('input_parameters', []); - return $this->service->calculatePreview($modelId, $inputParameters); - }, __('message.calculated')); - } -} \ No newline at end of file diff --git a/app/Http/Controllers/Api/V1/Design/ModelFormulaController.php b/app/Http/Controllers/Api/V1/Design/ModelFormulaController.php deleted file mode 100644 index fee7f4b..0000000 --- a/app/Http/Controllers/Api/V1/Design/ModelFormulaController.php +++ /dev/null @@ -1,278 +0,0 @@ -service->getModelFormulas($modelId, $request->validated()); - }, __('message.fetched')); - } - - /** - * @OA\Post( - * path="/v1/design/models/{modelId}/formulas", - * summary="Create model formula", - * description="Create a new formula for a specific model", - * security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}}, - * tags={"Model Formulas"}, - * @OA\Parameter( - * name="modelId", - * description="Model ID", - * in="path", - * required=true, - * @OA\Schema(type="integer", example=1) - * ), - * @OA\RequestBody( - * required=true, - * @OA\JsonContent(ref="#/components/schemas/CreateModelFormulaRequest") - * ), - * @OA\Response( - * response=201, - * description="Model formula created successfully", - * @OA\JsonContent( - * allOf={ - * @OA\Schema(ref="#/components/schemas/ApiResponse"), - * @OA\Schema( - * @OA\Property(property="data", ref="#/components/schemas/ModelFormulaResource") - * ) - * } - * ) - * ), - * @OA\Response(ref="#/components/responses/ValidationErrorResponse", response=422), - * @OA\Response(ref="#/components/responses/ErrorResponse", response=400), - * @OA\Response(ref="#/components/responses/UnauthorizedResponse", response=401) - * ) - */ - public function store(ModelFormulaFormRequest $request, int $modelId) - { - return ApiResponse::handle(function () use ($request, $modelId) { - return $this->service->createFormula($modelId, $request->validated()); - }, __('message.created')); - } - - /** - * @OA\Put( - * path="/v1/design/models/{modelId}/formulas/{formulaId}", - * summary="Update model formula", - * description="Update a specific model formula", - * security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}}, - * tags={"Model Formulas"}, - * @OA\Parameter( - * name="modelId", - * description="Model ID", - * in="path", - * required=true, - * @OA\Schema(type="integer", example=1) - * ), - * @OA\Parameter( - * name="formulaId", - * description="Formula ID", - * in="path", - * required=true, - * @OA\Schema(type="integer", example=1) - * ), - * @OA\RequestBody( - * required=true, - * @OA\JsonContent(ref="#/components/schemas/UpdateModelFormulaRequest") - * ), - * @OA\Response( - * response=200, - * description="Model formula updated successfully", - * @OA\JsonContent( - * allOf={ - * @OA\Schema(ref="#/components/schemas/ApiResponse"), - * @OA\Schema( - * @OA\Property(property="data", ref="#/components/schemas/ModelFormulaResource") - * ) - * } - * ) - * ), - * @OA\Response(ref="#/components/responses/ValidationErrorResponse", response=422), - * @OA\Response(ref="#/components/responses/ErrorResponse", response=400), - * @OA\Response(ref="#/components/responses/UnauthorizedResponse", response=401), - * @OA\Response(ref="#/components/responses/NotFoundResponse", response=404) - * ) - */ - public function update(ModelFormulaFormRequest $request, int $modelId, int $formulaId) - { - return ApiResponse::handle(function () use ($request, $modelId, $formulaId) { - return $this->service->updateFormula($modelId, $formulaId, $request->validated()); - }, __('message.updated')); - } - - /** - * @OA\Delete( - * path="/v1/design/models/{modelId}/formulas/{formulaId}", - * summary="Delete model formula", - * description="Delete a specific model formula", - * security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}}, - * tags={"Model Formulas"}, - * @OA\Parameter( - * name="modelId", - * description="Model ID", - * in="path", - * required=true, - * @OA\Schema(type="integer", example=1) - * ), - * @OA\Parameter( - * name="formulaId", - * description="Formula ID", - * in="path", - * required=true, - * @OA\Schema(type="integer", example=1) - * ), - * @OA\Response( - * response=200, - * description="Model formula deleted successfully", - * @OA\JsonContent(ref="#/components/schemas/ApiResponse") - * ), - * @OA\Response(ref="#/components/responses/ErrorResponse", response=400), - * @OA\Response(ref="#/components/responses/UnauthorizedResponse", response=401), - * @OA\Response(ref="#/components/responses/NotFoundResponse", response=404) - * ) - */ - public function destroy(int $modelId, int $formulaId) - { - return ApiResponse::handle(function () use ($modelId, $formulaId) { - $this->service->deleteFormula($modelId, $formulaId); - return null; - }, __('message.deleted')); - } - - /** - * @OA\Post( - * path="/v1/design/models/{modelId}/formulas/{formulaId}/validate", - * summary="Validate model formula", - * description="Validate formula expression and dependencies", - * security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}}, - * tags={"Model Formulas"}, - * @OA\Parameter( - * name="modelId", - * description="Model ID", - * in="path", - * required=true, - * @OA\Schema(type="integer", example=1) - * ), - * @OA\Parameter( - * name="formulaId", - * description="Formula ID", - * in="path", - * required=true, - * @OA\Schema(type="integer", example=1) - * ), - * @OA\Response( - * response=200, - * description="Formula validation result", - * @OA\JsonContent( - * allOf={ - * @OA\Schema(ref="#/components/schemas/ApiResponse"), - * @OA\Schema( - * @OA\Property( - * property="data", - * @OA\Property(property="is_valid", type="boolean", example=true), - * @OA\Property( - * property="validation_errors", - * type="array", - * @OA\Items(type="string", example="Unknown variable: unknown_var") - * ), - * @OA\Property( - * property="dependency_chain", - * type="array", - * @OA\Items(type="string", example="W0") - * ) - * ) - * ) - * } - * ) - * ), - * @OA\Response(ref="#/components/responses/ErrorResponse", response=400), - * @OA\Response(ref="#/components/responses/UnauthorizedResponse", response=401), - * @OA\Response(ref="#/components/responses/NotFoundResponse", response=404) - * ) - */ - public function validate(int $modelId, int $formulaId) - { - return ApiResponse::handle(function () use ($modelId, $formulaId) { - return $this->service->validateFormula($modelId, $formulaId); - }, __('message.formula.validated')); - } -} \ No newline at end of file diff --git a/app/Http/Controllers/Api/V1/Design/ModelParameterController.php b/app/Http/Controllers/Api/V1/Design/ModelParameterController.php deleted file mode 100644 index d448bdb..0000000 --- a/app/Http/Controllers/Api/V1/Design/ModelParameterController.php +++ /dev/null @@ -1,227 +0,0 @@ -service->getModelParameters($modelId, $request->validated()); - }, __('message.fetched')); - } - - /** - * @OA\Post( - * path="/v1/design/models/{modelId}/parameters", - * summary="Create model parameter", - * description="Create a new parameter for a specific model", - * security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}}, - * tags={"Model Parameters"}, - * @OA\Parameter( - * name="modelId", - * description="Model ID", - * in="path", - * required=true, - * @OA\Schema(type="integer", example=1) - * ), - * @OA\RequestBody( - * required=true, - * @OA\JsonContent(ref="#/components/schemas/CreateModelParameterRequest") - * ), - * @OA\Response( - * response=201, - * description="Model parameter created successfully", - * @OA\JsonContent( - * allOf={ - * @OA\Schema(ref="#/components/schemas/ApiResponse"), - * @OA\Schema( - * @OA\Property(property="data", ref="#/components/schemas/ModelParameterResource") - * ) - * } - * ) - * ), - * @OA\Response(ref="#/components/responses/ValidationErrorResponse", response=422), - * @OA\Response(ref="#/components/responses/ErrorResponse", response=400), - * @OA\Response(ref="#/components/responses/UnauthorizedResponse", response=401) - * ) - */ - public function store(ModelParameterFormRequest $request, int $modelId) - { - return ApiResponse::handle(function () use ($request, $modelId) { - return $this->service->createParameter($modelId, $request->validated()); - }, __('message.created')); - } - - /** - * @OA\Put( - * path="/v1/design/models/{modelId}/parameters/{parameterId}", - * summary="Update model parameter", - * description="Update a specific model parameter", - * security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}}, - * tags={"Model Parameters"}, - * @OA\Parameter( - * name="modelId", - * description="Model ID", - * in="path", - * required=true, - * @OA\Schema(type="integer", example=1) - * ), - * @OA\Parameter( - * name="parameterId", - * description="Parameter ID", - * in="path", - * required=true, - * @OA\Schema(type="integer", example=1) - * ), - * @OA\RequestBody( - * required=true, - * @OA\JsonContent(ref="#/components/schemas/UpdateModelParameterRequest") - * ), - * @OA\Response( - * response=200, - * description="Model parameter updated successfully", - * @OA\JsonContent( - * allOf={ - * @OA\Schema(ref="#/components/schemas/ApiResponse"), - * @OA\Schema( - * @OA\Property(property="data", ref="#/components/schemas/ModelParameterResource") - * ) - * } - * ) - * ), - * @OA\Response(ref="#/components/responses/ValidationErrorResponse", response=422), - * @OA\Response(ref="#/components/responses/ErrorResponse", response=400), - * @OA\Response(ref="#/components/responses/UnauthorizedResponse", response=401), - * @OA\Response(ref="#/components/responses/NotFoundResponse", response=404) - * ) - */ - public function update(ModelParameterFormRequest $request, int $modelId, int $parameterId) - { - return ApiResponse::handle(function () use ($request, $modelId, $parameterId) { - return $this->service->updateParameter($modelId, $parameterId, $request->validated()); - }, __('message.updated')); - } - - /** - * @OA\Delete( - * path="/v1/design/models/{modelId}/parameters/{parameterId}", - * summary="Delete model parameter", - * description="Delete a specific model parameter", - * security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}}, - * tags={"Model Parameters"}, - * @OA\Parameter( - * name="modelId", - * description="Model ID", - * in="path", - * required=true, - * @OA\Schema(type="integer", example=1) - * ), - * @OA\Parameter( - * name="parameterId", - * description="Parameter ID", - * in="path", - * required=true, - * @OA\Schema(type="integer", example=1) - * ), - * @OA\Response( - * response=200, - * description="Model parameter deleted successfully", - * @OA\JsonContent(ref="#/components/schemas/ApiResponse") - * ), - * @OA\Response(ref="#/components/responses/ErrorResponse", response=400), - * @OA\Response(ref="#/components/responses/UnauthorizedResponse", response=401), - * @OA\Response(ref="#/components/responses/NotFoundResponse", response=404) - * ) - */ - public function destroy(int $modelId, int $parameterId) - { - return ApiResponse::handle(function () use ($modelId, $parameterId) { - $this->service->deleteParameter($modelId, $parameterId); - return null; - }, __('message.deleted')); - } -} \ No newline at end of file diff --git a/app/Http/Controllers/Api/V1/Design/ProductFromModelController.php b/app/Http/Controllers/Api/V1/Design/ProductFromModelController.php deleted file mode 100644 index 04c2aed..0000000 --- a/app/Http/Controllers/Api/V1/Design/ProductFromModelController.php +++ /dev/null @@ -1,258 +0,0 @@ -validated(); - $data['model_id'] = $modelId; - return $this->service->createProductWithBom($data); - }, __('message.product.created_from_model')); - } - - /** - * @OA\Get( - * path="/v1/products/{productId}/parameters", - * summary="Get product parameters", - * description="Retrieve parameters used to create this product", - * security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}}, - * tags={"Product from Model"}, - * @OA\Parameter( - * name="productId", - * description="Product ID", - * in="path", - * required=true, - * @OA\Schema(type="integer", example=1) - * ), - * @OA\Response( - * response=200, - * description="Product parameters retrieved successfully", - * @OA\JsonContent( - * allOf={ - * @OA\Schema(ref="#/components/schemas/ApiResponse"), - * @OA\Schema( - * @OA\Property(property="data", ref="#/components/schemas/ProductParametersResponse") - * ) - * } - * ) - * ), - * @OA\Response(ref="#/components/responses/ErrorResponse", response=400), - * @OA\Response(ref="#/components/responses/UnauthorizedResponse", response=401), - * @OA\Response(ref="#/components/responses/NotFoundResponse", response=404) - * ) - */ - public function getProductParameters(int $productId) - { - return ApiResponse::handle(function () use ($productId) { - return $this->service->getProductParameters($productId); - }, __('message.fetched')); - } - - /** - * @OA\Get( - * path="/v1/products/{productId}/calculated-values", - * summary="Get product calculated values", - * description="Retrieve calculated output values for this product", - * security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}}, - * tags={"Product from Model"}, - * @OA\Parameter( - * name="productId", - * description="Product ID", - * in="path", - * required=true, - * @OA\Schema(type="integer", example=1) - * ), - * @OA\Response( - * response=200, - * description="Product calculated values retrieved successfully", - * @OA\JsonContent( - * allOf={ - * @OA\Schema(ref="#/components/schemas/ApiResponse"), - * @OA\Schema( - * @OA\Property(property="data", ref="#/components/schemas/ProductCalculatedValuesResponse") - * ) - * } - * ) - * ), - * @OA\Response(ref="#/components/responses/ErrorResponse", response=400), - * @OA\Response(ref="#/components/responses/UnauthorizedResponse", response=401), - * @OA\Response(ref="#/components/responses/NotFoundResponse", response=404) - * ) - */ - public function getCalculatedValues(int $productId) - { - return ApiResponse::handle(function () use ($productId) { - return $this->service->getCalculatedValues($productId); - }, __('message.fetched')); - } - - /** - * @OA\Post( - * path="/v1/products/{productId}/recalculate", - * summary="Recalculate product values", - * description="Recalculate product BOM and values with updated parameters", - * security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}}, - * tags={"Product from Model"}, - * @OA\Parameter( - * name="productId", - * description="Product ID", - * in="path", - * required=true, - * @OA\Schema(type="integer", example=1) - * ), - * @OA\RequestBody( - * required=true, - * @OA\JsonContent( - * @OA\Property( - * property="input_parameters", - * description="Updated input parameter values", - * type="object", - * additionalProperties=@OA\Property(oneOf={ - * @OA\Schema(type="number"), - * @OA\Schema(type="string"), - * @OA\Schema(type="boolean") - * }), - * example={"W0": 1200, "H0": 900, "installation_type": "B"} - * ), - * @OA\Property( - * property="update_bom", - * description="Whether to update the BOM components", - * type="boolean", - * example=true - * ) - * ) - * ), - * @OA\Response( - * response=200, - * description="Product recalculated successfully", - * @OA\JsonContent( - * allOf={ - * @OA\Schema(ref="#/components/schemas/ApiResponse"), - * @OA\Schema( - * @OA\Property(property="data", ref="#/components/schemas/ProductWithBomResponse") - * ) - * } - * ) - * ), - * @OA\Response(ref="#/components/responses/ValidationErrorResponse", response=422), - * @OA\Response(ref="#/components/responses/ErrorResponse", response=400), - * @OA\Response(ref="#/components/responses/UnauthorizedResponse", response=401), - * @OA\Response(ref="#/components/responses/NotFoundResponse", response=404) - * ) - */ - public function recalculate(int $productId) - { - return ApiResponse::handle(function () use ($productId) { - $inputParameters = request()->input('input_parameters', []); - $updateBom = request()->boolean('update_bom', true); - - return $this->service->recalculateProduct($productId, $inputParameters, $updateBom); - }, __('message.product.recalculated')); - } - - /** - * @OA\Get( - * path="/v1/products/{productId}/model-info", - * summary="Get product model information", - * description="Retrieve the model information used to create this product", - * security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}}, - * tags={"Product from Model"}, - * @OA\Parameter( - * name="productId", - * description="Product ID", - * in="path", - * required=true, - * @OA\Schema(type="integer", example=1) - * ), - * @OA\Response( - * response=200, - * description="Product model information retrieved successfully", - * @OA\JsonContent( - * allOf={ - * @OA\Schema(ref="#/components/schemas/ApiResponse"), - * @OA\Schema( - * @OA\Property( - * property="data", - * @OA\Property(property="model_id", type="integer", example=1), - * @OA\Property(property="model_code", type="string", example="KSS01"), - * @OA\Property(property="model_name", type="string", example="Standard Screen"), - * @OA\Property(property="model_version", type="string", example="v1.0"), - * @OA\Property(property="creation_timestamp", type="string", format="date-time"), - * @OA\Property( - * property="input_parameters_used", - * type="object", - * additionalProperties=@OA\Property(oneOf={ - * @OA\Schema(type="number"), - * @OA\Schema(type="string"), - * @OA\Schema(type="boolean") - * }), - * example={"W0": 1000, "H0": 800, "installation_type": "A"} - * ) - * ) - * ) - * } - * ) - * ), - * @OA\Response(ref="#/components/responses/ErrorResponse", response=400), - * @OA\Response(ref="#/components/responses/UnauthorizedResponse", response=401), - * @OA\Response(ref="#/components/responses/NotFoundResponse", response=404) - * ) - */ - public function getModelInfo(int $productId) - { - return ApiResponse::handle(function () use ($productId) { - return $this->service->getProductModelInfo($productId); - }, __('message.fetched')); - } -} \ No newline at end of file diff --git a/app/Http/Controllers/Api/V1/Schemas/BomConditionRuleSchemas.php b/app/Http/Controllers/Api/V1/Schemas/BomConditionRuleSchemas.php deleted file mode 100644 index bbd6c5b..0000000 --- a/app/Http/Controllers/Api/V1/Schemas/BomConditionRuleSchemas.php +++ /dev/null @@ -1,133 +0,0 @@ -= 1000 && installation_type == "A"', - description: 'Boolean expression to determine if this rule applies' - ), - new OA\Property( - property: 'quantity_expression', - type: 'string', - nullable: true, - example: 'ceiling(W1 / 500)', - description: 'Expression to calculate required quantity' - ), - new OA\Property( - property: 'waste_rate_expression', - type: 'string', - nullable: true, - example: '0.05', - description: 'Expression to calculate waste rate (0.0-1.0)' - ), - new OA\Property(property: 'description', type: 'string', nullable: true, example: '가로 1000mm 이상, A타입 설치시 브라켓 적용'), - new OA\Property(property: 'priority', type: 'integer', example: 1), - new OA\Property(property: 'is_active', type: 'boolean', example: true), - new OA\Property(property: 'created_at', type: 'string', format: 'date-time', example: '2024-01-01T00:00:00Z'), - new OA\Property(property: 'updated_at', type: 'string', format: 'date-time', example: '2024-01-01T00:00:00Z'), - new OA\Property( - property: 'reference', - type: 'object', - description: 'Referenced material or product information', - properties: [ - new OA\Property(property: 'id', type: 'integer', example: 101), - new OA\Property(property: 'code', type: 'string', example: 'BR-001'), - new OA\Property(property: 'name', type: 'string', example: '표준 브라켓'), - new OA\Property(property: 'unit', type: 'string', example: 'EA') - ] - ) - ] -)] -class BomConditionRuleResource {} - -#[OA\Schema( - schema: 'CreateBomConditionRuleRequest', - description: 'Request schema for creating BOM condition rule', - required: ['name', 'ref_type', 'ref_id', 'condition_expression'], - properties: [ - new OA\Property(property: 'name', type: 'string', maxLength: 100, example: '브라켓 선택 규칙'), - new OA\Property(property: 'ref_type', type: 'string', enum: ['MATERIAL', 'PRODUCT'], example: 'MATERIAL'), - new OA\Property(property: 'ref_id', type: 'integer', minimum: 1, example: 101), - new OA\Property( - property: 'condition_expression', - type: 'string', - maxLength: 1000, - example: 'W1 >= 1000 && installation_type == "A"', - description: 'Boolean expression to determine if this rule applies' - ), - new OA\Property( - property: 'quantity_expression', - type: 'string', - maxLength: 500, - nullable: true, - example: 'ceiling(W1 / 500)', - description: 'Expression to calculate required quantity' - ), - new OA\Property( - property: 'waste_rate_expression', - type: 'string', - maxLength: 500, - nullable: true, - example: '0.05', - description: 'Expression to calculate waste rate (0.0-1.0)' - ), - new OA\Property(property: 'description', type: 'string', maxLength: 500, nullable: true, example: '가로 1000mm 이상, A타입 설치시 브라켓 적용'), - new OA\Property(property: 'priority', type: 'integer', minimum: 0, example: 1), - new OA\Property(property: 'is_active', type: 'boolean', example: true) - ] -)] -class CreateBomConditionRuleRequest {} - -#[OA\Schema( - schema: 'UpdateBomConditionRuleRequest', - description: 'Request schema for updating BOM condition rule', - properties: [ - new OA\Property(property: 'name', type: 'string', maxLength: 100, example: '브라켓 선택 규칙'), - new OA\Property(property: 'ref_type', type: 'string', enum: ['MATERIAL', 'PRODUCT'], example: 'MATERIAL'), - new OA\Property(property: 'ref_id', type: 'integer', minimum: 1, example: 101), - new OA\Property( - property: 'condition_expression', - type: 'string', - maxLength: 1000, - example: 'W1 >= 1000 && installation_type == "A"', - description: 'Boolean expression to determine if this rule applies' - ), - new OA\Property( - property: 'quantity_expression', - type: 'string', - maxLength: 500, - nullable: true, - example: 'ceiling(W1 / 500)', - description: 'Expression to calculate required quantity' - ), - new OA\Property( - property: 'waste_rate_expression', - type: 'string', - maxLength: 500, - nullable: true, - example: '0.05', - description: 'Expression to calculate waste rate (0.0-1.0)' - ), - new OA\Property(property: 'description', type: 'string', maxLength: 500, nullable: true, example: '가로 1000mm 이상, A타입 설치시 브라켓 적용'), - new OA\Property(property: 'priority', type: 'integer', minimum: 0, example: 1), - new OA\Property(property: 'is_active', type: 'boolean', example: true) - ] -)] -class UpdateBomConditionRuleRequest {} \ No newline at end of file diff --git a/app/Http/Controllers/Api/V1/Schemas/BomResolverSchemas.php b/app/Http/Controllers/Api/V1/Schemas/BomResolverSchemas.php deleted file mode 100644 index 3d82218..0000000 --- a/app/Http/Controllers/Api/V1/Schemas/BomResolverSchemas.php +++ /dev/null @@ -1,314 +0,0 @@ - 1000, - 'H0' => 800, - 'installation_type' => 'A', - 'power_source' => '220V' - ], - description: 'Input parameter values for BOM resolution' - ), - new OA\Property(property: 'bom_template_id', type: 'integer', minimum: 1, nullable: true, example: 1), - new OA\Property(property: 'include_calculated_values', type: 'boolean', example: true), - new OA\Property(property: 'include_bom_items', type: 'boolean', example: true) - ] -)] -class ResolvePreviewRequest {} - -#[OA\Schema( - schema: 'CreateProductFromModelRequest', - description: 'Request schema for creating product from model', - required: ['model_id', 'input_parameters', 'product_code', 'product_name'], - properties: [ - new OA\Property(property: 'model_id', type: 'integer', minimum: 1, example: 1), - new OA\Property( - property: 'input_parameters', - type: 'object', - additionalProperties: new OA\Property(oneOf: [ - new OA\Schema(type: 'number'), - new OA\Schema(type: 'string'), - new OA\Schema(type: 'boolean') - ]), - example: [ - 'W0' => 1000, - 'H0' => 800, - 'installation_type' => 'A', - 'power_source' => '220V' - ], - description: 'Input parameter values for BOM resolution' - ), - new OA\Property(property: 'bom_template_id', type: 'integer', minimum: 1, nullable: true, example: 1), - new OA\Property( - property: 'product_code', - type: 'string', - maxLength: 50, - example: 'KSS01-1000x800-A', - description: 'Product code (uppercase letters, numbers, underscore, hyphen only)' - ), - new OA\Property(property: 'product_name', type: 'string', maxLength: 100, example: 'KSS01 스크린 1000x800 A타입'), - new OA\Property(property: 'category_id', type: 'integer', minimum: 1, nullable: true, example: 1), - new OA\Property(property: 'description', type: 'string', maxLength: 1000, nullable: true, example: '매개변수 기반으로 생성된 맞춤형 스크린'), - new OA\Property(property: 'unit', type: 'string', maxLength: 20, nullable: true, example: 'EA'), - new OA\Property(property: 'min_order_qty', type: 'number', minimum: 0, nullable: true, example: 1), - new OA\Property(property: 'lead_time_days', type: 'integer', minimum: 0, nullable: true, example: 7), - new OA\Property(property: 'is_active', type: 'boolean', example: true), - new OA\Property(property: 'create_bom_items', type: 'boolean', example: true), - new OA\Property(property: 'validate_bom', type: 'boolean', example: true) - ] -)] -class CreateProductFromModelRequest {} - -#[OA\Schema( - schema: 'BomPreviewResponse', - description: 'BOM preview resolution response', - properties: [ - new OA\Property( - property: 'input_parameters', - type: 'object', - additionalProperties: new OA\Property(oneOf: [ - new OA\Schema(type: 'number'), - new OA\Schema(type: 'string'), - new OA\Schema(type: 'boolean') - ]), - example: [ - 'W0' => 1000, - 'H0' => 800, - 'installation_type' => 'A', - 'power_source' => '220V' - ] - ), - new OA\Property( - property: 'calculated_values', - type: 'object', - additionalProperties: new OA\Property(type: 'number'), - example: [ - 'W1' => 1050, - 'H1' => 850, - 'area' => 892500, - 'weight' => 25.5, - 'motor_power' => 120 - ] - ), - new OA\Property( - property: 'bom_items', - type: 'array', - items: new OA\Items(ref: '#/components/schemas/BomItemPreview') - ), - new OA\Property( - property: 'summary', - type: 'object', - properties: [ - new OA\Property(property: 'total_materials', type: 'integer', example: 15), - new OA\Property(property: 'total_cost', type: 'number', example: 125000.50), - new OA\Property(property: 'estimated_weight', type: 'number', example: 25.5) - ] - ), - new OA\Property( - property: 'validation_warnings', - type: 'array', - items: new OA\Items( - properties: [ - new OA\Property(property: 'type', type: 'string', example: 'PARAMETER_OUT_OF_RANGE'), - new OA\Property(property: 'message', type: 'string', example: 'W0 값이 권장 범위를 벗어났습니다'), - new OA\Property(property: 'parameter', type: 'string', example: 'W0'), - new OA\Property(property: 'current_value', type: 'number', example: 1000), - new OA\Property(property: 'recommended_range', type: 'string', example: '600-900') - ], - type: 'object' - ) - ) - ] -)] -class BomPreviewResponse {} - -#[OA\Schema( - schema: 'BomItemPreview', - description: 'BOM item preview', - properties: [ - new OA\Property(property: 'ref_type', type: 'string', enum: ['MATERIAL', 'PRODUCT'], example: 'MATERIAL'), - new OA\Property(property: 'ref_id', type: 'integer', example: 101), - new OA\Property(property: 'ref_code', type: 'string', example: 'BR-001'), - new OA\Property(property: 'ref_name', type: 'string', example: '표준 브라켓'), - new OA\Property(property: 'quantity', type: 'number', example: 2.0), - new OA\Property(property: 'waste_rate', type: 'number', example: 0.05), - new OA\Property(property: 'total_quantity', type: 'number', example: 2.1), - new OA\Property(property: 'unit', type: 'string', example: 'EA'), - new OA\Property(property: 'unit_cost', type: 'number', nullable: true, example: 5000.0), - new OA\Property(property: 'total_cost', type: 'number', nullable: true, example: 10500.0), - new OA\Property(property: 'applied_rule', type: 'string', nullable: true, example: '브라켓 선택 규칙'), - new OA\Property( - property: 'calculation_details', - type: 'object', - properties: [ - new OA\Property(property: 'condition_matched', type: 'boolean', example: true), - new OA\Property(property: 'quantity_expression', type: 'string', example: 'ceiling(W1 / 500)'), - new OA\Property(property: 'quantity_calculation', type: 'string', example: 'ceiling(1050 / 500) = 3') - ] - ) - ] -)] -class BomItemPreview {} - -#[OA\Schema( - schema: 'ProductWithBomResponse', - description: 'Product created with BOM response', - properties: [ - new OA\Property( - property: 'product', - type: 'object', - properties: [ - new OA\Property(property: 'id', type: 'integer', example: 123), - new OA\Property(property: 'code', type: 'string', example: 'KSS01-1000x800-A'), - new OA\Property(property: 'name', type: 'string', example: 'KSS01 스크린 1000x800 A타입'), - new OA\Property(property: 'type', type: 'string', example: 'PRODUCT'), - new OA\Property(property: 'category_id', type: 'integer', nullable: true, example: 1), - new OA\Property(property: 'description', type: 'string', nullable: true, example: '매개변수 기반으로 생성된 맞춤형 스크린'), - new OA\Property(property: 'unit', type: 'string', nullable: true, example: 'EA'), - new OA\Property(property: 'min_order_qty', type: 'number', nullable: true, example: 1), - new OA\Property(property: 'lead_time_days', type: 'integer', nullable: true, example: 7), - new OA\Property(property: 'is_active', type: 'boolean', example: true), - new OA\Property(property: 'created_at', type: 'string', format: 'date-time', example: '2024-01-01T00:00:00Z') - ] - ), - new OA\Property( - property: 'input_parameters', - type: 'object', - additionalProperties: new OA\Property(oneOf: [ - new OA\Schema(type: 'number'), - new OA\Schema(type: 'string'), - new OA\Schema(type: 'boolean') - ]), - example: [ - 'W0' => 1000, - 'H0' => 800, - 'installation_type' => 'A', - 'power_source' => '220V' - ] - ), - new OA\Property( - property: 'calculated_values', - type: 'object', - additionalProperties: new OA\Property(type: 'number'), - example: [ - 'W1' => 1050, - 'H1' => 850, - 'area' => 892500, - 'weight' => 25.5 - ] - ), - new OA\Property( - property: 'bom_items', - type: 'array', - items: new OA\Items( - properties: [ - new OA\Property(property: 'id', type: 'integer', example: 1), - new OA\Property(property: 'ref_type', type: 'string', enum: ['MATERIAL', 'PRODUCT'], example: 'MATERIAL'), - new OA\Property(property: 'ref_id', type: 'integer', example: 101), - new OA\Property(property: 'ref_code', type: 'string', example: 'BR-001'), - new OA\Property(property: 'ref_name', type: 'string', example: '표준 브라켓'), - new OA\Property(property: 'quantity', type: 'number', example: 2.0), - new OA\Property(property: 'waste_rate', type: 'number', example: 0.05), - new OA\Property(property: 'unit', type: 'string', example: 'EA') - ], - type: 'object' - ) - ), - new OA\Property( - property: 'summary', - type: 'object', - properties: [ - new OA\Property(property: 'total_bom_items', type: 'integer', example: 15), - new OA\Property(property: 'model_id', type: 'integer', example: 1), - new OA\Property(property: 'bom_template_id', type: 'integer', nullable: true, example: 1) - ] - ) - ] -)] -class ProductWithBomResponse {} - -#[OA\Schema( - schema: 'ProductParametersResponse', - description: 'Product parameters response', - properties: [ - new OA\Property(property: 'product_id', type: 'integer', example: 123), - new OA\Property(property: 'model_id', type: 'integer', example: 1), - new OA\Property( - property: 'input_parameters', - type: 'object', - additionalProperties: new OA\Property(oneOf: [ - new OA\Schema(type: 'number'), - new OA\Schema(type: 'string'), - new OA\Schema(type: 'boolean') - ]), - example: [ - 'W0' => 1000, - 'H0' => 800, - 'installation_type' => 'A', - 'power_source' => '220V' - ] - ), - new OA\Property( - property: 'parameter_definitions', - type: 'array', - items: new OA\Items(ref: '#/components/schemas/ModelParameterResource') - ) - ] -)] -class ProductParametersResponse {} - -#[OA\Schema( - schema: 'ProductCalculatedValuesResponse', - description: 'Product calculated values response', - properties: [ - new OA\Property(property: 'product_id', type: 'integer', example: 123), - new OA\Property(property: 'model_id', type: 'integer', example: 1), - new OA\Property( - property: 'calculated_values', - type: 'object', - additionalProperties: new OA\Property(type: 'number'), - example: [ - 'W1' => 1050, - 'H1' => 850, - 'area' => 892500, - 'weight' => 25.5, - 'motor_power' => 120 - ] - ), - new OA\Property( - property: 'formula_applications', - type: 'array', - items: new OA\Items( - properties: [ - new OA\Property(property: 'formula_name', type: 'string', example: '최종 가로 크기 계산'), - new OA\Property(property: 'target_parameter', type: 'string', example: 'W1'), - new OA\Property(property: 'expression', type: 'string', example: 'W0 + (installation_type == "A" ? 50 : 30)'), - new OA\Property(property: 'calculated_value', type: 'number', example: 1050), - new OA\Property(property: 'execution_order', type: 'integer', example: 1) - ], - type: 'object' - ) - ) - ] -)] -class ProductCalculatedValuesResponse {} \ No newline at end of file diff --git a/app/Http/Controllers/Api/V1/Schemas/ModelFormulaSchemas.php b/app/Http/Controllers/Api/V1/Schemas/ModelFormulaSchemas.php deleted file mode 100644 index 3ec4bb9..0000000 --- a/app/Http/Controllers/Api/V1/Schemas/ModelFormulaSchemas.php +++ /dev/null @@ -1,80 +0,0 @@ - ['required', 'string', 'max:100'], - 'ref_type' => ['required', 'string', 'in:MATERIAL,PRODUCT'], - 'ref_id' => ['required', 'integer', 'min:1'], - 'condition_expression' => ['required', 'string', 'max:1000'], - 'quantity_expression' => ['nullable', 'string', 'max:500'], - 'waste_rate_expression' => ['nullable', 'string', 'max:500'], - 'description' => ['nullable', 'string', 'max:500'], - 'priority' => ['integer', 'min:0'], - 'is_active' => ['boolean'], - ]; - } - - /** - * Get custom attribute names for validator errors. - */ - public function attributes(): array - { - return [ - 'name' => __('validation.attributes.rule_name'), - 'ref_type' => __('validation.attributes.ref_type'), - 'ref_id' => __('validation.attributes.ref_id'), - 'condition_expression' => __('validation.attributes.condition_expression'), - 'quantity_expression' => __('validation.attributes.quantity_expression'), - 'waste_rate_expression' => __('validation.attributes.waste_rate_expression'), - 'description' => __('validation.attributes.description'), - 'priority' => __('validation.attributes.priority'), - 'is_active' => __('validation.attributes.is_active'), - ]; - } - - /** - * Prepare the data for validation. - */ - protected function prepareForValidation(): void - { - $this->merge([ - 'is_active' => $this->boolean('is_active', true), - 'priority' => $this->integer('priority', 0), - ]); - } -} \ No newline at end of file diff --git a/app/Http/Requests/Api/V1/BomConditionRule/IndexBomConditionRuleRequest.php b/app/Http/Requests/Api/V1/BomConditionRule/IndexBomConditionRuleRequest.php deleted file mode 100644 index 5afd54e..0000000 --- a/app/Http/Requests/Api/V1/BomConditionRule/IndexBomConditionRuleRequest.php +++ /dev/null @@ -1,44 +0,0 @@ - ['sometimes', 'string', 'max:255'], - 'ref_type' => ['sometimes', 'string', 'in:MATERIAL,PRODUCT'], - 'is_active' => ['sometimes', 'boolean'], - ]); - } - - /** - * Get custom attribute names for validator errors. - */ - public function attributes(): array - { - return array_merge(parent::attributes(), [ - 'search' => __('validation.attributes.search'), - 'ref_type' => __('validation.attributes.ref_type'), - 'is_active' => __('validation.attributes.is_active'), - ]); - } - - /** - * Prepare the data for validation. - */ - protected function prepareForValidation(): void - { - parent::prepareForValidation(); - - if ($this->has('is_active')) { - $this->merge(['is_active' => $this->boolean('is_active')]); - } - } -} \ No newline at end of file diff --git a/app/Http/Requests/Api/V1/BomConditionRule/UpdateBomConditionRuleRequest.php b/app/Http/Requests/Api/V1/BomConditionRule/UpdateBomConditionRuleRequest.php deleted file mode 100644 index cabbdf9..0000000 --- a/app/Http/Requests/Api/V1/BomConditionRule/UpdateBomConditionRuleRequest.php +++ /dev/null @@ -1,66 +0,0 @@ - ['sometimes', 'string', 'max:100'], - 'ref_type' => ['sometimes', 'string', 'in:MATERIAL,PRODUCT'], - 'ref_id' => ['sometimes', 'integer', 'min:1'], - 'condition_expression' => ['sometimes', 'string', 'max:1000'], - 'quantity_expression' => ['nullable', 'string', 'max:500'], - 'waste_rate_expression' => ['nullable', 'string', 'max:500'], - 'description' => ['nullable', 'string', 'max:500'], - 'priority' => ['sometimes', 'integer', 'min:0'], - 'is_active' => ['sometimes', 'boolean'], - ]; - } - - /** - * Get custom attribute names for validator errors. - */ - public function attributes(): array - { - return [ - 'name' => __('validation.attributes.rule_name'), - 'ref_type' => __('validation.attributes.ref_type'), - 'ref_id' => __('validation.attributes.ref_id'), - 'condition_expression' => __('validation.attributes.condition_expression'), - 'quantity_expression' => __('validation.attributes.quantity_expression'), - 'waste_rate_expression' => __('validation.attributes.waste_rate_expression'), - 'description' => __('validation.attributes.description'), - 'priority' => __('validation.attributes.priority'), - 'is_active' => __('validation.attributes.is_active'), - ]; - } - - /** - * Prepare the data for validation. - */ - protected function prepareForValidation(): void - { - if ($this->has('is_active')) { - $this->merge(['is_active' => $this->boolean('is_active')]); - } - - if ($this->has('priority')) { - $this->merge(['priority' => $this->integer('priority')]); - } - } -} \ No newline at end of file diff --git a/app/Http/Requests/Api/V1/BomResolver/CreateProductFromModelRequest.php b/app/Http/Requests/Api/V1/BomResolver/CreateProductFromModelRequest.php deleted file mode 100644 index d2c3c8a..0000000 --- a/app/Http/Requests/Api/V1/BomResolver/CreateProductFromModelRequest.php +++ /dev/null @@ -1,89 +0,0 @@ - ['required', 'integer', 'min:1'], - 'input_parameters' => ['required', 'array', 'min:1'], - 'input_parameters.*' => ['required'], - 'bom_template_id' => ['sometimes', 'integer', 'min:1'], - - // Product data - 'product_code' => ['required', 'string', 'max:50', 'regex:/^[A-Z0-9_-]+$/'], - 'product_name' => ['required', 'string', 'max:100'], - 'category_id' => ['sometimes', 'integer', 'min:1'], - 'description' => ['nullable', 'string', 'max:1000'], - - // Product attributes - 'unit' => ['nullable', 'string', 'max:20'], - 'min_order_qty' => ['nullable', 'numeric', 'min:0'], - 'lead_time_days' => ['nullable', 'integer', 'min:0'], - 'is_active' => ['boolean'], - - // Additional options - 'create_bom_items' => ['boolean'], - 'validate_bom' => ['boolean'], - ]; - } - - /** - * Get custom messages for validator errors. - */ - public function messages(): array - { - return [ - 'product_code.regex' => __('validation.product.code_format'), - ]; - } - - /** - * Get custom attribute names for validator errors. - */ - public function attributes(): array - { - return [ - 'model_id' => __('validation.attributes.model_id'), - 'input_parameters' => __('validation.attributes.input_parameters'), - 'bom_template_id' => __('validation.attributes.bom_template_id'), - 'product_code' => __('validation.attributes.product_code'), - 'product_name' => __('validation.attributes.product_name'), - 'category_id' => __('validation.attributes.category_id'), - 'description' => __('validation.attributes.description'), - 'unit' => __('validation.attributes.unit'), - 'min_order_qty' => __('validation.attributes.min_order_qty'), - 'lead_time_days' => __('validation.attributes.lead_time_days'), - 'is_active' => __('validation.attributes.is_active'), - 'create_bom_items' => __('validation.attributes.create_bom_items'), - 'validate_bom' => __('validation.attributes.validate_bom'), - ]; - } - - /** - * Prepare the data for validation. - */ - protected function prepareForValidation(): void - { - $this->merge([ - 'is_active' => $this->boolean('is_active', true), - 'create_bom_items' => $this->boolean('create_bom_items', true), - 'validate_bom' => $this->boolean('validate_bom', true), - ]); - } -} \ No newline at end of file diff --git a/app/Http/Requests/Api/V1/BomResolver/ResolvePreviewRequest.php b/app/Http/Requests/Api/V1/BomResolver/ResolvePreviewRequest.php deleted file mode 100644 index 9296f65..0000000 --- a/app/Http/Requests/Api/V1/BomResolver/ResolvePreviewRequest.php +++ /dev/null @@ -1,54 +0,0 @@ - ['required', 'array', 'min:1'], - 'input_parameters.*' => ['required'], - 'bom_template_id' => ['sometimes', 'integer', 'min:1'], - 'include_calculated_values' => ['boolean'], - 'include_bom_items' => ['boolean'], - ]; - } - - /** - * Get custom attribute names for validator errors. - */ - public function attributes(): array - { - return [ - 'input_parameters' => __('validation.attributes.input_parameters'), - 'bom_template_id' => __('validation.attributes.bom_template_id'), - 'include_calculated_values' => __('validation.attributes.include_calculated_values'), - 'include_bom_items' => __('validation.attributes.include_bom_items'), - ]; - } - - /** - * Prepare the data for validation. - */ - protected function prepareForValidation(): void - { - $this->merge([ - 'include_calculated_values' => $this->boolean('include_calculated_values', true), - 'include_bom_items' => $this->boolean('include_bom_items', true), - ]); - } -} \ No newline at end of file diff --git a/app/Http/Requests/Api/V1/Design/BomConditionRuleFormRequest.php b/app/Http/Requests/Api/V1/Design/BomConditionRuleFormRequest.php deleted file mode 100644 index ac43720..0000000 --- a/app/Http/Requests/Api/V1/Design/BomConditionRuleFormRequest.php +++ /dev/null @@ -1,296 +0,0 @@ -route('modelId'); - $ruleId = $this->route('ruleId'); - - $rules = [ - 'rule_name' => [ - 'required', - 'string', - 'max:100', - Rule::unique('bom_condition_rules') - ->where('model_id', $modelId) - ->where('tenant_id', auth()->user()?->currentTenant?->id) - ->whereNull('deleted_at') - ], - 'condition_expression' => ['required', 'string', 'max:1000'], - 'action_type' => ['required', 'string', 'in:INCLUDE,EXCLUDE,MODIFY_QUANTITY'], - 'target_type' => ['required', 'string', 'in:MATERIAL,PRODUCT'], - 'target_id' => ['required', 'integer', 'min:1'], - 'quantity_multiplier' => ['nullable', 'numeric', 'min:0'], - 'is_active' => ['boolean'], - 'priority' => ['integer', 'min:0'], - 'description' => ['nullable', 'string', 'max:500'], - ]; - - // For update requests, ignore current record in unique validation - if ($ruleId) { - $rules['rule_name'][3] = $rules['rule_name'][3]->ignore($ruleId); - } - - return $rules; - } - - /** - * Get custom messages for validator errors. - */ - public function messages(): array - { - return [ - 'rule_name.required' => '규칙 이름은 필수입니다.', - 'rule_name.unique' => '해당 모델에 이미 동일한 규칙 이름이 존재합니다.', - 'condition_expression.required' => '조건 표현식은 필수입니다.', - 'condition_expression.max' => '조건 표현식은 1000자를 초과할 수 없습니다.', - 'action_type.required' => '액션 타입은 필수입니다.', - 'action_type.in' => '액션 타입은 INCLUDE, EXCLUDE, MODIFY_QUANTITY 중 하나여야 합니다.', - 'target_type.required' => '대상 타입은 필수입니다.', - 'target_type.in' => '대상 타입은 MATERIAL 또는 PRODUCT여야 합니다.', - 'target_id.required' => '대상 ID는 필수입니다.', - 'target_id.min' => '대상 ID는 1 이상이어야 합니다.', - 'quantity_multiplier.numeric' => '수량 배수는 숫자여야 합니다.', - 'quantity_multiplier.min' => '수량 배수는 0 이상이어야 합니다.', - 'priority.min' => '우선순위는 0 이상이어야 합니다.', - 'description.max' => '설명은 500자를 초과할 수 없습니다.', - ]; - } - - /** - * Get custom attribute names for validator errors. - */ - public function attributes(): array - { - return [ - 'rule_name' => '규칙 이름', - 'condition_expression' => '조건 표현식', - 'action_type' => '액션 타입', - 'target_type' => '대상 타입', - 'target_id' => '대상 ID', - 'quantity_multiplier' => '수량 배수', - 'is_active' => '활성 상태', - 'priority' => '우선순위', - 'description' => '설명', - ]; - } - - /** - * Prepare the data for validation. - */ - protected function prepareForValidation(): void - { - $this->merge([ - 'is_active' => $this->boolean('is_active', true), - 'priority' => $this->integer('priority', 0), - ]); - - // Set default quantity_multiplier for actions that require it - if ($this->input('action_type') === 'MODIFY_QUANTITY' && !$this->has('quantity_multiplier')) { - $this->merge(['quantity_multiplier' => 1.0]); - } - - // Clean up condition expression - if ($this->has('condition_expression')) { - $expression = preg_replace('/\s+/', ' ', trim($this->input('condition_expression'))); - $this->merge(['condition_expression' => $expression]); - } - } - - /** - * Configure the validator instance. - */ - public function withValidator($validator) - { - $validator->after(function ($validator) { - $this->validateConditionExpression($validator); - $this->validateTargetExists($validator); - $this->validateActionRequirements($validator); - }); - } - - /** - * Validate condition expression syntax and variables. - */ - private function validateConditionExpression($validator): void - { - $expression = $this->input('condition_expression'); - - if (!$expression) { - return; - } - - // Check for potentially dangerous characters or functions - $dangerousPatterns = [ - '/\b(eval|exec|system|shell_exec|passthru|file_get_contents|file_put_contents|fopen|fwrite)\b/i', - '/[;{}]/', // Semicolons and braces - '/\$[a-zA-Z_]/', // PHP variables - '/\bfunction\s*\(/i', // Function definitions - ]; - - foreach ($dangerousPatterns as $pattern) { - if (preg_match($pattern, $expression)) { - $validator->errors()->add('condition_expression', '조건 표현식에 허용되지 않는 문자나 함수가 포함되어 있습니다.'); - return; - } - } - - // Validate condition expression format - if (!$this->isValidConditionExpression($expression)) { - $validator->errors()->add('condition_expression', '조건 표현식의 형식이 올바르지 않습니다.'); - return; - } - - // Validate variables in expression exist as parameters - $this->validateConditionVariables($validator, $expression); - } - - /** - * Check if condition expression has valid syntax. - */ - private function isValidConditionExpression(string $expression): bool - { - // Allow comparison operators, logical operators, variables, numbers, strings - $patterns = [ - '/^.*(==|!=|>=|<=|>|<|\sIN\s|\sNOT\sIN\s|\sAND\s|\sOR\s).*$/i', - '/^(true|false|[0-9]+)$/i', // Simple boolean or number - ]; - - foreach ($patterns as $pattern) { - if (preg_match($pattern, $expression)) { - return true; - } - } - - return false; - } - - /** - * Validate that variables in condition exist as model parameters. - */ - private function validateConditionVariables($validator, string $expression): void - { - $modelId = $this->route('modelId'); - - // Extract variable names from expression (exclude operators and values) - preg_match_all('/\b[a-zA-Z][a-zA-Z0-9_]*\b/', $expression, $matches); - $variables = $matches[0]; - - // Remove logical operators and reserved words - $reservedWords = ['AND', 'OR', 'IN', 'NOT', 'TRUE', 'FALSE', 'true', 'false']; - $variables = array_diff($variables, $reservedWords); - - if (empty($variables)) { - return; - } - - // Get existing parameters for this model - $existingParameters = ModelParameter::where('model_id', $modelId) - ->where('tenant_id', auth()->user()?->currentTenant?->id) - ->whereNull('deleted_at') - ->pluck('parameter_name') - ->toArray(); - - // Check for undefined variables - $undefinedVariables = array_diff($variables, $existingParameters); - - if (!empty($undefinedVariables)) { - $validator->errors()->add('condition_expression', - '조건식에 정의되지 않은 변수가 사용되었습니다: ' . implode(', ', $undefinedVariables) - ); - } - } - - /** - * Validate that the target (MATERIAL or PRODUCT) exists. - */ - private function validateTargetExists($validator): void - { - $targetType = $this->input('target_type'); - $targetId = $this->input('target_id'); - - if (!$targetType || !$targetId) { - return; - } - - $tenantId = auth()->user()?->currentTenant?->id; - - switch ($targetType) { - case 'MATERIAL': - $exists = Material::where('id', $targetId) - ->where('tenant_id', $tenantId) - ->whereNull('deleted_at') - ->exists(); - - if (!$exists) { - $validator->errors()->add('target_id', '지정된 자재가 존재하지 않습니다.'); - } - break; - - case 'PRODUCT': - $exists = Product::where('id', $targetId) - ->where('tenant_id', $tenantId) - ->whereNull('deleted_at') - ->exists(); - - if (!$exists) { - $validator->errors()->add('target_id', '지정된 제품이 존재하지 않습니다.'); - } - break; - } - } - - /** - * Validate action-specific requirements. - */ - private function validateActionRequirements($validator): void - { - $actionType = $this->input('action_type'); - $quantityMultiplier = $this->input('quantity_multiplier'); - - switch ($actionType) { - case 'MODIFY_QUANTITY': - // MODIFY_QUANTITY action requires quantity_multiplier - if ($quantityMultiplier === null || $quantityMultiplier === '') { - $validator->errors()->add('quantity_multiplier', 'MODIFY_QUANTITY 액션에는 수량 배수가 필요합니다.'); - } elseif ($quantityMultiplier <= 0) { - $validator->errors()->add('quantity_multiplier', 'MODIFY_QUANTITY 액션의 수량 배수는 0보다 커야 합니다.'); - } - break; - - case 'INCLUDE': - // INCLUDE action can optionally have quantity_multiplier (default to 1) - if ($quantityMultiplier !== null && $quantityMultiplier <= 0) { - $validator->errors()->add('quantity_multiplier', 'INCLUDE 액션의 수량 배수는 0보다 커야 합니다.'); - } - break; - - case 'EXCLUDE': - // EXCLUDE action doesn't need quantity_multiplier - if ($quantityMultiplier !== null) { - $validator->errors()->add('quantity_multiplier', 'EXCLUDE 액션에는 수량 배수가 필요하지 않습니다.'); - } - break; - } - } -} \ No newline at end of file diff --git a/app/Http/Requests/Api/V1/Design/BomResolverFormRequest.php b/app/Http/Requests/Api/V1/Design/BomResolverFormRequest.php deleted file mode 100644 index a90407a..0000000 --- a/app/Http/Requests/Api/V1/Design/BomResolverFormRequest.php +++ /dev/null @@ -1,274 +0,0 @@ - ['required', 'array', 'min:1'], - 'input_parameters.*' => ['required'], - 'bom_template_id' => ['sometimes', 'integer', 'min:1'], - 'include_calculated_values' => ['boolean'], - 'include_bom_items' => ['boolean'], - 'include_condition_rules' => ['boolean'], - 'validate_before_resolve' => ['boolean'], - 'calculation_precision' => ['integer', 'min:0', 'max:10'], - ]; - } - - /** - * Get custom messages for validator errors. - */ - public function messages(): array - { - return [ - 'input_parameters.required' => '입력 매개변수는 필수입니다.', - 'input_parameters.array' => '입력 매개변수는 배열 형태여야 합니다.', - 'input_parameters.min' => '최소 하나 이상의 입력 매개변수가 필요합니다.', - 'input_parameters.*.required' => '모든 입력 매개변수 값은 필수입니다.', - 'bom_template_id.integer' => 'BOM 템플릿 ID는 정수여야 합니다.', - 'bom_template_id.min' => 'BOM 템플릿 ID는 1 이상이어야 합니다.', - 'calculation_precision.integer' => '계산 정밀도는 정수여야 합니다.', - 'calculation_precision.min' => '계산 정밀도는 0 이상이어야 합니다.', - 'calculation_precision.max' => '계산 정밀도는 10 이하여야 합니다.', - ]; - } - - /** - * Get custom attribute names for validator errors. - */ - public function attributes(): array - { - return [ - 'input_parameters' => '입력 매개변수', - 'bom_template_id' => 'BOM 템플릿 ID', - 'include_calculated_values' => '계산값 포함 여부', - 'include_bom_items' => 'BOM 아이템 포함 여부', - 'include_condition_rules' => '조건 규칙 포함 여부', - 'validate_before_resolve' => '해결 전 유효성 검사', - 'calculation_precision' => '계산 정밀도', - ]; - } - - /** - * Prepare the data for validation. - */ - protected function prepareForValidation(): void - { - $this->merge([ - 'include_calculated_values' => $this->boolean('include_calculated_values', true), - 'include_bom_items' => $this->boolean('include_bom_items', true), - 'include_condition_rules' => $this->boolean('include_condition_rules', true), - 'validate_before_resolve' => $this->boolean('validate_before_resolve', true), - 'calculation_precision' => $this->integer('calculation_precision', 2), - ]); - - // Ensure input_parameters is an array - if ($this->has('input_parameters') && !is_array($this->input('input_parameters'))) { - $params = json_decode($this->input('input_parameters'), true); - if (json_last_error() === JSON_ERROR_NONE) { - $this->merge(['input_parameters' => $params]); - } - } - } - - /** - * Configure the validator instance. - */ - public function withValidator($validator) - { - $validator->after(function ($validator) { - $this->validateInputParameters($validator); - $this->validateBomTemplate($validator); - $this->validateParameterValues($validator); - }); - } - - /** - * Validate input parameters against model parameter definitions. - */ - private function validateInputParameters($validator): void - { - $modelId = $this->route('modelId'); - $inputParameters = $this->input('input_parameters', []); - - if (empty($inputParameters)) { - return; - } - - // Get model's INPUT parameters - $modelParameters = ModelParameter::where('model_id', $modelId) - ->where('parameter_type', 'INPUT') - ->where('tenant_id', auth()->user()?->currentTenant?->id) - ->whereNull('deleted_at') - ->get() - ->keyBy('parameter_name'); - - // Check for required parameters - $requiredParams = $modelParameters->where('is_required', true)->pluck('parameter_name')->toArray(); - $providedParams = array_keys($inputParameters); - $missingRequired = array_diff($requiredParams, $providedParams); - - if (!empty($missingRequired)) { - $validator->errors()->add('input_parameters', - '다음 필수 매개변수가 누락되었습니다: ' . implode(', ', $missingRequired) - ); - } - - // Check for unknown parameters - $knownParams = $modelParameters->pluck('parameter_name')->toArray(); - $unknownParams = array_diff($providedParams, $knownParams); - - if (!empty($unknownParams)) { - $validator->errors()->add('input_parameters', - '알 수 없는 매개변수가 포함되어 있습니다: ' . implode(', ', $unknownParams) - ); - } - } - - /** - * Validate BOM template exists and belongs to the model. - */ - private function validateBomTemplate($validator): void - { - $bomTemplateId = $this->input('bom_template_id'); - $modelId = $this->route('modelId'); - - if (!$bomTemplateId) { - return; - } - - $template = BomTemplate::where('id', $bomTemplateId) - ->where('tenant_id', auth()->user()?->currentTenant?->id) - ->whereNull('deleted_at') - ->first(); - - if (!$template) { - $validator->errors()->add('bom_template_id', '지정된 BOM 템플릿이 존재하지 않습니다.'); - return; - } - - // Check if template belongs to the model (through model_version) - if ($template->modelVersion && $template->modelVersion->model_id != $modelId) { - $validator->errors()->add('bom_template_id', 'BOM 템플릿이 해당 모델에 속하지 않습니다.'); - } - } - - /** - * Validate parameter values against their constraints. - */ - private function validateParameterValues($validator): void - { - $modelId = $this->route('modelId'); - $inputParameters = $this->input('input_parameters', []); - - if (empty($inputParameters)) { - return; - } - - // Get model parameter definitions - $modelParameters = ModelParameter::where('model_id', $modelId) - ->where('parameter_type', 'INPUT') - ->where('tenant_id', auth()->user()?->currentTenant?->id) - ->whereNull('deleted_at') - ->get() - ->keyBy('parameter_name'); - - foreach ($inputParameters as $paramName => $value) { - $parameter = $modelParameters->get($paramName); - - if (!$parameter) { - continue; // Unknown parameter already handled above - } - - // Validate value against parameter constraints - $this->validateParameterValue($validator, $parameter, $paramName, $value); - } - } - - /** - * Validate individual parameter value. - */ - private function validateParameterValue($validator, $parameter, string $paramName, $value): void - { - // Check for null/empty required values - if ($parameter->is_required && ($value === null || $value === '')) { - $validator->errors()->add("input_parameters.{$paramName}", "{$paramName}은(는) 필수 매개변수입니다."); - return; - } - - // Validate data type - switch ($parameter->data_type ?? 'STRING') { - case 'INTEGER': - if (!is_numeric($value) || (int)$value != $value) { - $validator->errors()->add("input_parameters.{$paramName}", "{$paramName}은(는) 정수여야 합니다."); - return; - } - $value = (int)$value; - break; - - case 'DECIMAL': - if (!is_numeric($value)) { - $validator->errors()->add("input_parameters.{$paramName}", "{$paramName}은(는) 숫자여야 합니다."); - return; - } - $value = (float)$value; - break; - - case 'BOOLEAN': - if (!is_bool($value) && !in_array($value, [0, 1, '0', '1', 'true', 'false'])) { - $validator->errors()->add("input_parameters.{$paramName}", "{$paramName}은(는) 불린 값이어야 합니다."); - return; - } - break; - - case 'STRING': - if (!is_string($value) && !is_numeric($value)) { - $validator->errors()->add("input_parameters.{$paramName}", "{$paramName}은(는) 문자열이어야 합니다."); - return; - } - $value = (string)$value; - break; - } - - // Validate min/max values for numeric types - if (in_array($parameter->data_type, ['INTEGER', 'DECIMAL']) && is_numeric($value)) { - if ($parameter->min_value !== null && $value < $parameter->min_value) { - $validator->errors()->add("input_parameters.{$paramName}", - "{$paramName}은(는) {$parameter->min_value} 이상이어야 합니다." - ); - } - - if ($parameter->max_value !== null && $value > $parameter->max_value) { - $validator->errors()->add("input_parameters.{$paramName}", - "{$paramName}은(는) {$parameter->max_value} 이하여야 합니다." - ); - } - } - - // Validate options for select type parameters - if (!empty($parameter->options) && !in_array($value, $parameter->options)) { - $validOptions = implode(', ', $parameter->options); - $validator->errors()->add("input_parameters.{$paramName}", - "{$paramName}의 값은 다음 중 하나여야 합니다: {$validOptions}" - ); - } - } -} \ No newline at end of file diff --git a/app/Http/Requests/Api/V1/Design/ModelFormulaFormRequest.php b/app/Http/Requests/Api/V1/Design/ModelFormulaFormRequest.php deleted file mode 100644 index 058a1c5..0000000 --- a/app/Http/Requests/Api/V1/Design/ModelFormulaFormRequest.php +++ /dev/null @@ -1,264 +0,0 @@ -route('modelId'); - $formulaId = $this->route('formulaId'); - - $rules = [ - 'formula_name' => [ - 'required', - 'string', - 'max:100', - 'regex:/^[a-zA-Z][a-zA-Z0-9_\s]*$/', - Rule::unique('model_formulas') - ->where('model_id', $modelId) - ->where('tenant_id', auth()->user()?->currentTenant?->id) - ->whereNull('deleted_at') - ], - 'formula_expression' => ['required', 'string', 'max:1000'], - 'unit' => ['nullable', 'string', 'max:20'], - 'description' => ['nullable', 'string', 'max:500'], - 'calculation_order' => ['integer', 'min:0'], - 'dependencies' => ['nullable', 'array'], - 'dependencies.*' => ['string', 'max:50', 'regex:/^[a-zA-Z][a-zA-Z0-9_]*$/'], - ]; - - // For update requests, ignore current record in unique validation - if ($formulaId) { - $rules['formula_name'][4] = $rules['formula_name'][4]->ignore($formulaId); - } - - return $rules; - } - - /** - * Get custom messages for validator errors. - */ - public function messages(): array - { - return [ - 'formula_name.required' => '공식 이름은 필수입니다.', - 'formula_name.regex' => '공식 이름은 영문자로 시작하고 영문자, 숫자, 언더스코어, 공백만 사용할 수 있습니다.', - 'formula_name.unique' => '해당 모델에 이미 동일한 공식 이름이 존재합니다.', - 'formula_expression.required' => '공식 표현식은 필수입니다.', - 'formula_expression.max' => '공식 표현식은 1000자를 초과할 수 없습니다.', - 'unit.max' => '단위는 20자를 초과할 수 없습니다.', - 'description.max' => '설명은 500자를 초과할 수 없습니다.', - 'calculation_order.min' => '계산 순서는 0 이상이어야 합니다.', - 'dependencies.array' => '의존성은 배열 형태여야 합니다.', - 'dependencies.*.regex' => '의존성 변수명은 영문자로 시작하고 영문자, 숫자, 언더스코어만 사용할 수 있습니다.', - ]; - } - - /** - * Get custom attribute names for validator errors. - */ - public function attributes(): array - { - return [ - 'formula_name' => '공식 이름', - 'formula_expression' => '공식 표현식', - 'unit' => '단위', - 'description' => '설명', - 'calculation_order' => '계산 순서', - 'dependencies' => '의존성', - ]; - } - - /** - * Prepare the data for validation. - */ - protected function prepareForValidation(): void - { - $this->merge([ - 'calculation_order' => $this->integer('calculation_order', 0), - ]); - - // Convert dependencies to array if it's a string - if ($this->has('dependencies') && is_string($this->input('dependencies'))) { - $dependencies = json_decode($this->input('dependencies'), true); - if (json_last_error() === JSON_ERROR_NONE) { - $this->merge(['dependencies' => $dependencies]); - } - } - - // Clean up formula expression - remove extra whitespace - if ($this->has('formula_expression')) { - $expression = preg_replace('/\s+/', ' ', trim($this->input('formula_expression'))); - $this->merge(['formula_expression' => $expression]); - } - } - - /** - * Configure the validator instance. - */ - public function withValidator($validator) - { - $validator->after(function ($validator) { - $this->validateFormulaExpression($validator); - $this->validateDependencies($validator); - $this->validateNoCircularDependency($validator); - }); - } - - /** - * Validate formula expression syntax. - */ - private function validateFormulaExpression($validator): void - { - $expression = $this->input('formula_expression'); - - if (!$expression) { - return; - } - - // Check for potentially dangerous characters or functions - $dangerousPatterns = [ - '/\b(eval|exec|system|shell_exec|passthru|file_get_contents|file_put_contents|fopen|fwrite)\b/i', - '/[;{}]/', // Semicolons and braces - '/\$[a-zA-Z_]/', // PHP variables - '/\bfunction\s*\(/i', // Function definitions - ]; - - foreach ($dangerousPatterns as $pattern) { - if (preg_match($pattern, $expression)) { - $validator->errors()->add('formula_expression', '공식 표현식에 허용되지 않는 문자나 함수가 포함되어 있습니다.'); - return; - } - } - - // Validate mathematical expression format - if (!$this->isValidMathExpression($expression)) { - $validator->errors()->add('formula_expression', '공식 표현식의 형식이 올바르지 않습니다. 수학 연산자와 변수명만 사용할 수 있습니다.'); - } - - // Extract variables from expression and validate they exist as parameters - $this->validateExpressionVariables($validator, $expression); - } - - /** - * Check if expression contains valid mathematical operations. - */ - private function isValidMathExpression(string $expression): bool - { - // Allow: numbers, variables, basic math operators, parentheses, math functions - $allowedPattern = '/^[a-zA-Z0-9_\s\+\-\*\/\(\)\.\,]+$/'; - - // Allow common math functions - $mathFunctions = ['sin', 'cos', 'tan', 'log', 'exp', 'sqrt', 'pow', 'abs', 'ceil', 'floor', 'round', 'max', 'min']; - $functionPattern = '/\b(' . implode('|', $mathFunctions) . ')\s*\(/i'; - - // Remove math functions for basic pattern check - $cleanExpression = preg_replace($functionPattern, '', $expression); - - return preg_match($allowedPattern, $cleanExpression); - } - - /** - * Validate that variables in expression exist as model parameters. - */ - private function validateExpressionVariables($validator, string $expression): void - { - $modelId = $this->route('modelId'); - - // Extract variable names from expression - preg_match_all('/\b[a-zA-Z][a-zA-Z0-9_]*\b/', $expression, $matches); - $variables = $matches[0]; - - // Remove math functions from variables - $mathFunctions = ['sin', 'cos', 'tan', 'log', 'exp', 'sqrt', 'pow', 'abs', 'ceil', 'floor', 'round', 'max', 'min']; - $variables = array_diff($variables, $mathFunctions); - - if (empty($variables)) { - return; - } - - // Get existing parameters for this model - $existingParameters = ModelParameter::where('model_id', $modelId) - ->where('tenant_id', auth()->user()?->currentTenant?->id) - ->whereNull('deleted_at') - ->pluck('parameter_name') - ->toArray(); - - // Check for undefined variables - $undefinedVariables = array_diff($variables, $existingParameters); - - if (!empty($undefinedVariables)) { - $validator->errors()->add('formula_expression', - '공식에 정의되지 않은 변수가 사용되었습니다: ' . implode(', ', $undefinedVariables) - ); - } - } - - /** - * Validate dependencies array. - */ - private function validateDependencies($validator): void - { - $dependencies = $this->input('dependencies', []); - $modelId = $this->route('modelId'); - - if (empty($dependencies)) { - return; - } - - // Get existing parameters for this model - $existingParameters = ModelParameter::where('model_id', $modelId) - ->where('tenant_id', auth()->user()?->currentTenant?->id) - ->whereNull('deleted_at') - ->pluck('parameter_name') - ->toArray(); - - // Check that all dependencies exist as parameters - $invalidDependencies = array_diff($dependencies, $existingParameters); - - if (!empty($invalidDependencies)) { - $validator->errors()->add('dependencies', - '존재하지 않는 매개변수가 의존성에 포함되어 있습니다: ' . implode(', ', $invalidDependencies) - ); - } - } - - /** - * Validate there's no circular dependency. - */ - private function validateNoCircularDependency($validator): void - { - $dependencies = $this->input('dependencies', []); - $formulaName = $this->input('formula_name'); - - if (empty($dependencies) || !$formulaName) { - return; - } - - // Check for direct self-reference - if (in_array($formulaName, $dependencies)) { - $validator->errors()->add('dependencies', '공식이 자기 자신을 참조할 수 없습니다.'); - return; - } - - // For more complex circular dependency check, this would require - // analyzing all formulas in the model - simplified version here - // In production, implement full dependency graph analysis - } -} \ No newline at end of file diff --git a/app/Http/Requests/Api/V1/Design/ModelParameterFormRequest.php b/app/Http/Requests/Api/V1/Design/ModelParameterFormRequest.php deleted file mode 100644 index a3d881f..0000000 --- a/app/Http/Requests/Api/V1/Design/ModelParameterFormRequest.php +++ /dev/null @@ -1,172 +0,0 @@ -route('modelId'); - $parameterId = $this->route('parameterId'); - - $rules = [ - 'parameter_name' => [ - 'required', - 'string', - 'max:50', - 'regex:/^[a-zA-Z][a-zA-Z0-9_]*$/', - Rule::unique('model_parameters') - ->where('model_id', $modelId) - ->where('tenant_id', auth()->user()?->currentTenant?->id) - ->whereNull('deleted_at') - ], - 'parameter_type' => ['required', 'string', 'in:INPUT,OUTPUT'], - 'is_required' => ['boolean'], - 'default_value' => ['nullable', 'string', 'max:255'], - 'min_value' => ['nullable', 'numeric'], - 'max_value' => ['nullable', 'numeric', 'gte:min_value'], - 'unit' => ['nullable', 'string', 'max:20'], - 'options' => ['nullable', 'array'], - 'options.*' => ['string', 'max:100'], - 'description' => ['nullable', 'string', 'max:500'], - 'sort_order' => ['integer', 'min:0'], - ]; - - // For update requests, ignore current record in unique validation - if ($parameterId) { - $rules['parameter_name'][4] = $rules['parameter_name'][4]->ignore($parameterId); - } - - return $rules; - } - - /** - * Get custom messages for validator errors. - */ - public function messages(): array - { - return [ - 'parameter_name.required' => '매개변수 이름은 필수입니다.', - 'parameter_name.regex' => '매개변수 이름은 영문자로 시작하고 영문자, 숫자, 언더스코어만 사용할 수 있습니다.', - 'parameter_name.unique' => '해당 모델에 이미 동일한 매개변수 이름이 존재합니다.', - 'parameter_type.required' => '매개변수 타입은 필수입니다.', - 'parameter_type.in' => '매개변수 타입은 INPUT 또는 OUTPUT이어야 합니다.', - 'min_value.numeric' => '최소값은 숫자여야 합니다.', - 'max_value.numeric' => '최대값은 숫자여야 합니다.', - 'max_value.gte' => '최대값은 최소값보다 크거나 같아야 합니다.', - 'unit.max' => '단위는 20자를 초과할 수 없습니다.', - 'description.max' => '설명은 500자를 초과할 수 없습니다.', - 'sort_order.min' => '정렬 순서는 0 이상이어야 합니다.', - ]; - } - - /** - * Get custom attribute names for validator errors. - */ - public function attributes(): array - { - return [ - 'parameter_name' => '매개변수 이름', - 'parameter_type' => '매개변수 타입', - 'is_required' => '필수 여부', - 'default_value' => '기본값', - 'min_value' => '최소값', - 'max_value' => '최대값', - 'unit' => '단위', - 'options' => '옵션 목록', - 'description' => '설명', - 'sort_order' => '정렬 순서', - ]; - } - - /** - * Prepare the data for validation. - */ - protected function prepareForValidation(): void - { - $this->merge([ - 'is_required' => $this->boolean('is_required', false), - 'sort_order' => $this->integer('sort_order', 0), - ]); - - // Convert options to array if it's a string - if ($this->has('options') && is_string($this->input('options'))) { - $options = json_decode($this->input('options'), true); - if (json_last_error() === JSON_ERROR_NONE) { - $this->merge(['options' => $options]); - } - } - } - - /** - * Configure the validator instance. - */ - public function withValidator($validator) - { - $validator->after(function ($validator) { - // Validate that INPUT parameters can have default values and constraints - if ($this->input('parameter_type') === 'INPUT') { - $this->validateInputParameterConstraints($validator); - } - - // Validate that OUTPUT parameters don't have input-specific fields - if ($this->input('parameter_type') === 'OUTPUT') { - $this->validateOutputParameterConstraints($validator); - } - - // Validate min/max value relationship - $this->validateMinMaxValues($validator); - }); - } - - /** - * Validate INPUT parameter specific constraints. - */ - private function validateInputParameterConstraints($validator): void - { - // INPUT parameters can have all constraints - // No additional validation needed for INPUT type - } - - /** - * Validate OUTPUT parameter specific constraints. - */ - private function validateOutputParameterConstraints($validator): void - { - // OUTPUT parameters should not have certain input-specific fields - if ($this->filled('is_required') && $this->input('is_required')) { - $validator->errors()->add('is_required', 'OUTPUT 매개변수는 필수 항목이 될 수 없습니다.'); - } - - if ($this->filled('default_value')) { - $validator->errors()->add('default_value', 'OUTPUT 매개변수는 기본값을 가질 수 없습니다.'); - } - } - - /** - * Validate min/max value relationship. - */ - private function validateMinMaxValues($validator): void - { - $minValue = $this->input('min_value'); - $maxValue = $this->input('max_value'); - - if ($minValue !== null && $maxValue !== null && $minValue > $maxValue) { - $validator->errors()->add('max_value', '최대값은 최소값보다 크거나 같아야 합니다.'); - } - } -} \ No newline at end of file diff --git a/app/Http/Requests/Api/V1/Design/ProductFromModelFormRequest.php b/app/Http/Requests/Api/V1/Design/ProductFromModelFormRequest.php deleted file mode 100644 index 26c50d2..0000000 --- a/app/Http/Requests/Api/V1/Design/ProductFromModelFormRequest.php +++ /dev/null @@ -1,388 +0,0 @@ -user()?->currentTenant?->id; - - return [ - // Model and BOM configuration - 'input_parameters' => ['required', 'array', 'min:1'], - 'input_parameters.*' => ['required'], - 'bom_template_id' => ['sometimes', 'integer', 'min:1'], - - // Product basic information - 'product_code' => [ - 'required', - 'string', - 'max:50', - 'regex:/^[A-Z0-9_-]+$/', - Rule::unique('products') - ->where('tenant_id', $tenantId) - ->whereNull('deleted_at') - ], - 'product_name' => ['required', 'string', 'max:100'], - 'description' => ['nullable', 'string', 'max:1000'], - - // Product categorization - 'category_id' => ['sometimes', 'integer', 'min:1'], - 'product_type' => ['nullable', 'string', 'in:PRODUCT,PART,SUBASSEMBLY'], - - // Product specifications - 'unit' => ['nullable', 'string', 'max:20'], - 'min_order_qty' => ['nullable', 'numeric', 'min:0'], - 'lead_time_days' => ['nullable', 'integer', 'min:0', 'max:365'], - 'is_active' => ['boolean'], - - // Product creation options - 'create_bom_items' => ['boolean'], - 'validate_bom' => ['boolean'], - 'save_parameters' => ['boolean'], - 'auto_generate_variants' => ['boolean'], - - // Pricing and cost - 'base_cost' => ['nullable', 'numeric', 'min:0'], - 'markup_percentage' => ['nullable', 'numeric', 'min:0', 'max:1000'], - - // Additional attributes (dynamic based on category) - 'attributes' => ['sometimes', 'array'], - 'attributes.*' => ['nullable'], - - // Tags and classification - 'tags' => ['sometimes', 'array'], - 'tags.*' => ['string', 'max:50'], - ]; - } - - /** - * Get custom messages for validator errors. - */ - public function messages(): array - { - return [ - 'input_parameters.required' => '입력 매개변수는 필수입니다.', - 'input_parameters.array' => '입력 매개변수는 배열 형태여야 합니다.', - 'input_parameters.min' => '최소 하나 이상의 입력 매개변수가 필요합니다.', - 'input_parameters.*.required' => '모든 입력 매개변수 값은 필수입니다.', - - 'product_code.required' => '제품 코드는 필수입니다.', - 'product_code.regex' => '제품 코드는 대문자, 숫자, 언더스코어, 하이픈만 사용할 수 있습니다.', - 'product_code.unique' => '이미 존재하는 제품 코드입니다.', - - 'product_name.required' => '제품명은 필수입니다.', - 'product_name.max' => '제품명은 100자를 초과할 수 없습니다.', - - 'description.max' => '설명은 1000자를 초과할 수 없습니다.', - - 'category_id.integer' => '카테고리 ID는 정수여야 합니다.', - 'category_id.min' => '카테고리 ID는 1 이상이어야 합니다.', - - 'product_type.in' => '제품 타입은 PRODUCT, PART, SUBASSEMBLY 중 하나여야 합니다.', - - 'unit.max' => '단위는 20자를 초과할 수 없습니다.', - 'min_order_qty.numeric' => '최소 주문 수량은 숫자여야 합니다.', - 'min_order_qty.min' => '최소 주문 수량은 0 이상이어야 합니다.', - - 'lead_time_days.integer' => '리드타임은 정수여야 합니다.', - 'lead_time_days.min' => '리드타임은 0 이상이어야 합니다.', - 'lead_time_days.max' => '리드타임은 365일을 초과할 수 없습니다.', - - 'base_cost.numeric' => '기본 원가는 숫자여야 합니다.', - 'base_cost.min' => '기본 원가는 0 이상이어야 합니다.', - - 'markup_percentage.numeric' => '마크업 비율은 숫자여야 합니다.', - 'markup_percentage.min' => '마크업 비율은 0 이상이어야 합니다.', - 'markup_percentage.max' => '마크업 비율은 1000%를 초과할 수 없습니다.', - - 'bom_template_id.integer' => 'BOM 템플릿 ID는 정수여야 합니다.', - 'bom_template_id.min' => 'BOM 템플릿 ID는 1 이상이어야 합니다.', - - 'tags.array' => '태그는 배열 형태여야 합니다.', - 'tags.*.max' => '각 태그는 50자를 초과할 수 없습니다.', - ]; - } - - /** - * Get custom attribute names for validator errors. - */ - public function attributes(): array - { - return [ - 'input_parameters' => '입력 매개변수', - 'bom_template_id' => 'BOM 템플릿 ID', - 'product_code' => '제품 코드', - 'product_name' => '제품명', - 'description' => '설명', - 'category_id' => '카테고리 ID', - 'product_type' => '제품 타입', - 'unit' => '단위', - 'min_order_qty' => '최소 주문 수량', - 'lead_time_days' => '리드타임', - 'is_active' => '활성 상태', - 'create_bom_items' => 'BOM 아이템 생성', - 'validate_bom' => 'BOM 유효성 검사', - 'save_parameters' => '매개변수 저장', - 'auto_generate_variants' => '자동 변형 생성', - 'base_cost' => '기본 원가', - 'markup_percentage' => '마크업 비율', - 'attributes' => '추가 속성', - 'tags' => '태그', - ]; - } - - /** - * Prepare the data for validation. - */ - protected function prepareForValidation(): void - { - $this->merge([ - 'is_active' => $this->boolean('is_active', true), - 'create_bom_items' => $this->boolean('create_bom_items', true), - 'validate_bom' => $this->boolean('validate_bom', true), - 'save_parameters' => $this->boolean('save_parameters', true), - 'auto_generate_variants' => $this->boolean('auto_generate_variants', false), - ]); - - // Ensure input_parameters is an array - if ($this->has('input_parameters') && !is_array($this->input('input_parameters'))) { - $params = json_decode($this->input('input_parameters'), true); - if (json_last_error() === JSON_ERROR_NONE) { - $this->merge(['input_parameters' => $params]); - } - } - - // Ensure attributes is an array - if ($this->has('attributes') && !is_array($this->input('attributes'))) { - $attributes = json_decode($this->input('attributes'), true); - if (json_last_error() === JSON_ERROR_NONE) { - $this->merge(['attributes' => $attributes]); - } - } - - // Ensure tags is an array - if ($this->has('tags') && !is_array($this->input('tags'))) { - $tags = json_decode($this->input('tags'), true); - if (json_last_error() === JSON_ERROR_NONE) { - $this->merge(['tags' => $tags]); - } - } - - // Set default product_type if not provided - if (!$this->has('product_type')) { - $this->merge(['product_type' => 'PRODUCT']); - } - - // Convert product_code to uppercase - if ($this->has('product_code')) { - $this->merge(['product_code' => strtoupper($this->input('product_code'))]); - } - } - - /** - * Configure the validator instance. - */ - public function withValidator($validator) - { - $validator->after(function ($validator) { - $this->validateInputParameters($validator); - $this->validateBomTemplate($validator); - $this->validateCategory($validator); - $this->validateParameterValues($validator); - $this->validateBusinessRules($validator); - }); - } - - /** - * Validate input parameters against model parameter definitions. - */ - private function validateInputParameters($validator): void - { - $modelId = $this->route('modelId'); - $inputParameters = $this->input('input_parameters', []); - - if (empty($inputParameters)) { - return; - } - - // Get model's INPUT parameters - $modelParameters = ModelParameter::where('model_id', $modelId) - ->where('parameter_type', 'INPUT') - ->where('tenant_id', auth()->user()?->currentTenant?->id) - ->whereNull('deleted_at') - ->get() - ->keyBy('parameter_name'); - - // Check for required parameters - $requiredParams = $modelParameters->where('is_required', true)->pluck('parameter_name')->toArray(); - $providedParams = array_keys($inputParameters); - $missingRequired = array_diff($requiredParams, $providedParams); - - if (!empty($missingRequired)) { - $validator->errors()->add('input_parameters', - '다음 필수 매개변수가 누락되었습니다: ' . implode(', ', $missingRequired) - ); - } - - // Check for unknown parameters - $knownParams = $modelParameters->pluck('parameter_name')->toArray(); - $unknownParams = array_diff($providedParams, $knownParams); - - if (!empty($unknownParams)) { - $validator->errors()->add('input_parameters', - '알 수 없는 매개변수가 포함되어 있습니다: ' . implode(', ', $unknownParams) - ); - } - } - - /** - * Validate BOM template exists and belongs to the model. - */ - private function validateBomTemplate($validator): void - { - $bomTemplateId = $this->input('bom_template_id'); - $modelId = $this->route('modelId'); - - if (!$bomTemplateId) { - return; - } - - $template = BomTemplate::where('id', $bomTemplateId) - ->where('tenant_id', auth()->user()?->currentTenant?->id) - ->whereNull('deleted_at') - ->first(); - - if (!$template) { - $validator->errors()->add('bom_template_id', '지정된 BOM 템플릿이 존재하지 않습니다.'); - return; - } - - // Check if template belongs to the model (through model_version) - if ($template->modelVersion && $template->modelVersion->model_id != $modelId) { - $validator->errors()->add('bom_template_id', 'BOM 템플릿이 해당 모델에 속하지 않습니다.'); - } - } - - /** - * Validate category exists and is accessible. - */ - private function validateCategory($validator): void - { - $categoryId = $this->input('category_id'); - - if (!$categoryId) { - return; - } - - $category = Category::where('id', $categoryId) - ->where('tenant_id', auth()->user()?->currentTenant?->id) - ->whereNull('deleted_at') - ->first(); - - if (!$category) { - $validator->errors()->add('category_id', '지정된 카테고리가 존재하지 않습니다.'); - } - } - - /** - * Validate parameter values against their constraints. - */ - private function validateParameterValues($validator): void - { - $modelId = $this->route('modelId'); - $inputParameters = $this->input('input_parameters', []); - - if (empty($inputParameters)) { - return; - } - - // Get model parameter definitions - $modelParameters = ModelParameter::where('model_id', $modelId) - ->where('parameter_type', 'INPUT') - ->where('tenant_id', auth()->user()?->currentTenant?->id) - ->whereNull('deleted_at') - ->get() - ->keyBy('parameter_name'); - - foreach ($inputParameters as $paramName => $value) { - $parameter = $modelParameters->get($paramName); - - if (!$parameter) { - continue; // Unknown parameter already handled above - } - - // Validate value against parameter constraints - $this->validateParameterValue($validator, $parameter, $paramName, $value); - } - } - - /** - * Validate individual parameter value. - */ - private function validateParameterValue($validator, $parameter, string $paramName, $value): void - { - // Check for null/empty required values - if ($parameter->is_required && ($value === null || $value === '')) { - $validator->errors()->add("input_parameters.{$paramName}", "{$paramName}은(는) 필수 매개변수입니다."); - return; - } - - // Skip validation for empty optional parameters - if (!$parameter->is_required && ($value === null || $value === '')) { - return; - } - - // Use model's validation method if available - if (method_exists($parameter, 'validateValue') && !$parameter->validateValue($value)) { - $validator->errors()->add("input_parameters.{$paramName}", "{$paramName}의 값이 유효하지 않습니다."); - } - } - - /** - * Validate business rules specific to product creation. - */ - private function validateBusinessRules($validator): void - { - // If validate_bom is true, ensure we have enough data for BOM validation - if ($this->input('validate_bom', true) && !$this->input('bom_template_id')) { - // Could check if model has default BOM template or condition rules - // For now, just warn - } - - // If auto_generate_variants is true, validate variant generation is possible - if ($this->input('auto_generate_variants', false)) { - // Check if model has variant-generating parameters - // This would require checking parameter configurations - } - - // Validate pricing logic - $baseCost = $this->input('base_cost'); - $markupPercentage = $this->input('markup_percentage'); - - if ($baseCost !== null && $markupPercentage !== null) { - if ($baseCost == 0 && $markupPercentage > 0) { - $validator->errors()->add('markup_percentage', '기본 원가가 0인 경우 마크업을 설정할 수 없습니다.'); - } - } - } -} \ No newline at end of file diff --git a/app/Http/Requests/Api/V1/ModelFormula/CreateModelFormulaRequest.php b/app/Http/Requests/Api/V1/ModelFormula/CreateModelFormulaRequest.php deleted file mode 100644 index c5291d1..0000000 --- a/app/Http/Requests/Api/V1/ModelFormula/CreateModelFormulaRequest.php +++ /dev/null @@ -1,67 +0,0 @@ - ['required', 'string', 'max:100'], - 'target_parameter' => ['required', 'string', 'max:50', 'regex:/^[a-zA-Z][a-zA-Z0-9_]*$/'], - 'expression' => ['required', 'string', 'max:1000'], - 'description' => ['nullable', 'string', 'max:500'], - 'is_active' => ['boolean'], - 'execution_order' => ['integer', 'min:0'], - ]; - } - - /** - * Get custom messages for validator errors. - */ - public function messages(): array - { - return [ - 'target_parameter.regex' => __('validation.model_formula.target_parameter_format'), - ]; - } - - /** - * Get custom attribute names for validator errors. - */ - public function attributes(): array - { - return [ - 'name' => __('validation.attributes.formula_name'), - 'target_parameter' => __('validation.attributes.target_parameter'), - 'expression' => __('validation.attributes.formula_expression'), - 'description' => __('validation.attributes.description'), - 'is_active' => __('validation.attributes.is_active'), - 'execution_order' => __('validation.attributes.execution_order'), - ]; - } - - /** - * Prepare the data for validation. - */ - protected function prepareForValidation(): void - { - $this->merge([ - 'is_active' => $this->boolean('is_active', true), - 'execution_order' => $this->integer('execution_order', 0), - ]); - } -} \ No newline at end of file diff --git a/app/Http/Requests/Api/V1/ModelFormula/IndexModelFormulaRequest.php b/app/Http/Requests/Api/V1/ModelFormula/IndexModelFormulaRequest.php deleted file mode 100644 index ff0e7f9..0000000 --- a/app/Http/Requests/Api/V1/ModelFormula/IndexModelFormulaRequest.php +++ /dev/null @@ -1,30 +0,0 @@ - ['sometimes', 'string', 'max:255'], - 'target_parameter' => ['sometimes', 'string', 'max:50'], - ]); - } - - /** - * Get custom attribute names for validator errors. - */ - public function attributes(): array - { - return array_merge(parent::attributes(), [ - 'search' => __('validation.attributes.search'), - 'target_parameter' => __('validation.attributes.target_parameter'), - ]); - } -} \ No newline at end of file diff --git a/app/Http/Requests/Api/V1/ModelFormula/UpdateModelFormulaRequest.php b/app/Http/Requests/Api/V1/ModelFormula/UpdateModelFormulaRequest.php deleted file mode 100644 index 3b14116..0000000 --- a/app/Http/Requests/Api/V1/ModelFormula/UpdateModelFormulaRequest.php +++ /dev/null @@ -1,70 +0,0 @@ - ['sometimes', 'string', 'max:100'], - 'target_parameter' => ['sometimes', 'string', 'max:50', 'regex:/^[a-zA-Z][a-zA-Z0-9_]*$/'], - 'expression' => ['sometimes', 'string', 'max:1000'], - 'description' => ['nullable', 'string', 'max:500'], - 'is_active' => ['sometimes', 'boolean'], - 'execution_order' => ['sometimes', 'integer', 'min:0'], - ]; - } - - /** - * Get custom messages for validator errors. - */ - public function messages(): array - { - return [ - 'target_parameter.regex' => __('validation.model_formula.target_parameter_format'), - ]; - } - - /** - * Get custom attribute names for validator errors. - */ - public function attributes(): array - { - return [ - 'name' => __('validation.attributes.formula_name'), - 'target_parameter' => __('validation.attributes.target_parameter'), - 'expression' => __('validation.attributes.formula_expression'), - 'description' => __('validation.attributes.description'), - 'is_active' => __('validation.attributes.is_active'), - 'execution_order' => __('validation.attributes.execution_order'), - ]; - } - - /** - * Prepare the data for validation. - */ - protected function prepareForValidation(): void - { - if ($this->has('is_active')) { - $this->merge(['is_active' => $this->boolean('is_active')]); - } - - if ($this->has('execution_order')) { - $this->merge(['execution_order' => $this->integer('execution_order')]); - } - } -} \ No newline at end of file diff --git a/app/Http/Requests/Api/V1/ModelParameter/CreateModelParameterRequest.php b/app/Http/Requests/Api/V1/ModelParameter/CreateModelParameterRequest.php deleted file mode 100644 index 5cc3f29..0000000 --- a/app/Http/Requests/Api/V1/ModelParameter/CreateModelParameterRequest.php +++ /dev/null @@ -1,83 +0,0 @@ - ['required', 'string', 'max:50', 'regex:/^[a-zA-Z][a-zA-Z0-9_]*$/'], - 'label' => ['required', 'string', 'max:100'], - 'type' => ['required', 'string', 'in:INPUT,OUTPUT'], - 'data_type' => ['required', 'string', 'in:INTEGER,DECIMAL,STRING,BOOLEAN'], - 'unit' => ['nullable', 'string', 'max:20'], - 'default_value' => ['nullable', 'string', 'max:255'], - 'min_value' => ['nullable', 'numeric'], - 'max_value' => ['nullable', 'numeric', 'gte:min_value'], - 'enum_values' => ['nullable', 'array'], - 'enum_values.*' => ['string', 'max:100'], - 'validation_rules' => ['nullable', 'string', 'max:500'], - 'description' => ['nullable', 'string', 'max:500'], - 'is_required' => ['boolean'], - 'display_order' => ['integer', 'min:0'], - ]; - } - - /** - * Get custom messages for validator errors. - */ - public function messages(): array - { - return [ - 'name.regex' => __('validation.model_parameter.name_format'), - 'max_value.gte' => __('validation.model_parameter.max_value_gte_min'), - ]; - } - - /** - * Get custom attribute names for validator errors. - */ - public function attributes(): array - { - return [ - 'name' => __('validation.attributes.parameter_name'), - 'label' => __('validation.attributes.parameter_label'), - 'type' => __('validation.attributes.parameter_type'), - 'data_type' => __('validation.attributes.data_type'), - 'unit' => __('validation.attributes.unit'), - 'default_value' => __('validation.attributes.default_value'), - 'min_value' => __('validation.attributes.min_value'), - 'max_value' => __('validation.attributes.max_value'), - 'enum_values' => __('validation.attributes.enum_values'), - 'validation_rules' => __('validation.attributes.validation_rules'), - 'description' => __('validation.attributes.description'), - 'is_required' => __('validation.attributes.is_required'), - 'display_order' => __('validation.attributes.display_order'), - ]; - } - - /** - * Prepare the data for validation. - */ - protected function prepareForValidation(): void - { - $this->merge([ - 'is_required' => $this->boolean('is_required'), - 'display_order' => $this->integer('display_order', 0), - ]); - } -} \ No newline at end of file diff --git a/app/Http/Requests/Api/V1/ModelParameter/IndexModelParameterRequest.php b/app/Http/Requests/Api/V1/ModelParameter/IndexModelParameterRequest.php deleted file mode 100644 index 3687b2a..0000000 --- a/app/Http/Requests/Api/V1/ModelParameter/IndexModelParameterRequest.php +++ /dev/null @@ -1,30 +0,0 @@ - ['sometimes', 'string', 'max:255'], - 'type' => ['sometimes', 'string', 'in:INPUT,OUTPUT'], - ]); - } - - /** - * Get custom attribute names for validator errors. - */ - public function attributes(): array - { - return array_merge(parent::attributes(), [ - 'search' => __('validation.attributes.search'), - 'type' => __('validation.attributes.parameter_type'), - ]); - } -} \ No newline at end of file diff --git a/app/Http/Requests/Api/V1/ModelParameter/UpdateModelParameterRequest.php b/app/Http/Requests/Api/V1/ModelParameter/UpdateModelParameterRequest.php deleted file mode 100644 index 525323c..0000000 --- a/app/Http/Requests/Api/V1/ModelParameter/UpdateModelParameterRequest.php +++ /dev/null @@ -1,86 +0,0 @@ - ['sometimes', 'string', 'max:50', 'regex:/^[a-zA-Z][a-zA-Z0-9_]*$/'], - 'label' => ['sometimes', 'string', 'max:100'], - 'type' => ['sometimes', 'string', 'in:INPUT,OUTPUT'], - 'data_type' => ['sometimes', 'string', 'in:INTEGER,DECIMAL,STRING,BOOLEAN'], - 'unit' => ['nullable', 'string', 'max:20'], - 'default_value' => ['nullable', 'string', 'max:255'], - 'min_value' => ['nullable', 'numeric'], - 'max_value' => ['nullable', 'numeric', 'gte:min_value'], - 'enum_values' => ['nullable', 'array'], - 'enum_values.*' => ['string', 'max:100'], - 'validation_rules' => ['nullable', 'string', 'max:500'], - 'description' => ['nullable', 'string', 'max:500'], - 'is_required' => ['sometimes', 'boolean'], - 'display_order' => ['sometimes', 'integer', 'min:0'], - ]; - } - - /** - * Get custom messages for validator errors. - */ - public function messages(): array - { - return [ - 'name.regex' => __('validation.model_parameter.name_format'), - 'max_value.gte' => __('validation.model_parameter.max_value_gte_min'), - ]; - } - - /** - * Get custom attribute names for validator errors. - */ - public function attributes(): array - { - return [ - 'name' => __('validation.attributes.parameter_name'), - 'label' => __('validation.attributes.parameter_label'), - 'type' => __('validation.attributes.parameter_type'), - 'data_type' => __('validation.attributes.data_type'), - 'unit' => __('validation.attributes.unit'), - 'default_value' => __('validation.attributes.default_value'), - 'min_value' => __('validation.attributes.min_value'), - 'max_value' => __('validation.attributes.max_value'), - 'enum_values' => __('validation.attributes.enum_values'), - 'validation_rules' => __('validation.attributes.validation_rules'), - 'description' => __('validation.attributes.description'), - 'is_required' => __('validation.attributes.is_required'), - 'display_order' => __('validation.attributes.display_order'), - ]; - } - - /** - * Prepare the data for validation. - */ - protected function prepareForValidation(): void - { - if ($this->has('is_required')) { - $this->merge(['is_required' => $this->boolean('is_required')]); - } - - if ($this->has('display_order')) { - $this->merge(['display_order' => $this->integer('display_order')]); - } - } -} \ No newline at end of file diff --git a/app/Http/Requests/BomConditionRuleRequest.php b/app/Http/Requests/BomConditionRuleRequest.php deleted file mode 100644 index 3c48d8d..0000000 --- a/app/Http/Requests/BomConditionRuleRequest.php +++ /dev/null @@ -1,169 +0,0 @@ - 'required|integer|exists:models,id', - 'name' => 'required|string|max:100', - 'condition_expression' => 'required|string|max:1000', - 'action' => 'required|string|in:' . implode(',', BomConditionRule::ACTIONS), - 'target_items' => 'required|array|min:1', - 'target_items.*.product_id' => 'nullable|integer|exists:products,id', - 'target_items.*.material_id' => 'nullable|integer|exists:materials,id', - 'target_items.*.quantity' => 'nullable|numeric|min:0', - 'target_items.*.waste_rate' => 'nullable|numeric|min:0|max:100', - 'target_items.*.unit' => 'nullable|string|max:20', - 'target_items.*.memo' => 'nullable|string|max:200', - 'priority' => 'nullable|integer|min:1', - 'description' => 'nullable|string|max:500', - 'is_active' => 'boolean', - ]; - } - - /** - * Get custom messages for validator errors. - */ - public function messages(): array - { - return [ - 'condition_expression.required' => __('error.condition_expression_required'), - 'target_items.required' => __('error.target_items_required'), - 'target_items.min' => __('error.target_items_required'), - 'target_items.*.quantity.min' => __('error.quantity_must_be_positive'), - 'target_items.*.waste_rate.max' => __('error.waste_rate_too_high'), - ]; - } - - /** - * Configure the validator instance. - */ - public function withValidator($validator): void - { - $validator->after(function ($validator) { - $this->validateRuleNameUnique($validator); - $this->validateConditionExpression($validator); - $this->validateTargetItems($validator); - }); - } - - /** - * 규칙명 중복 검증 - */ - private function validateRuleNameUnique($validator): void - { - if (!$this->input('model_id') || !$this->input('name')) { - return; - } - - $query = BomConditionRule::where('model_id', $this->input('model_id')) - ->where('name', $this->input('name')); - - // 수정 시 자기 자신 제외 - if ($this->route('id')) { - $query->where('id', '!=', $this->route('id')); - } - - if ($query->exists()) { - $validator->errors()->add('name', __('error.rule_name_duplicate')); - } - } - - /** - * 조건식 검증 - */ - private function validateConditionExpression($validator): void - { - if (!$this->input('condition_expression')) { - return; - } - - $tempRule = new BomConditionRule([ - 'condition_expression' => $this->input('condition_expression'), - 'model_id' => $this->input('model_id'), - ]); - - $conditionErrors = $tempRule->validateConditionExpression(); - - if (!empty($conditionErrors)) { - foreach ($conditionErrors as $error) { - $validator->errors()->add('condition_expression', $error); - } - } - } - - /** - * 대상 아이템 검증 - */ - private function validateTargetItems($validator): void - { - $targetItems = $this->input('target_items', []); - $action = $this->input('action'); - - foreach ($targetItems as $index => $item) { - // 제품 또는 자재 참조 필수 - if (empty($item['product_id']) && empty($item['material_id'])) { - $validator->errors()->add( - "target_items.{$index}", - __('error.target_item_missing_reference') - ); - } - - // 제품과 자재 동시 참조 불가 - if (!empty($item['product_id']) && !empty($item['material_id'])) { - $validator->errors()->add( - "target_items.{$index}", - __('error.target_item_multiple_reference') - ); - } - - // REPLACE 액션의 경우 replace_from 필수 - if ($action === BomConditionRule::ACTION_REPLACE && empty($item['replace_from'])) { - $validator->errors()->add( - "target_items.{$index}.replace_from", - __('error.replace_from_required') - ); - } - - // replace_from 검증 - if (!empty($item['replace_from'])) { - if (empty($item['replace_from']['product_id']) && empty($item['replace_from']['material_id'])) { - $validator->errors()->add( - "target_items.{$index}.replace_from", - __('error.replace_from_missing_reference') - ); - } - } - } - } - - /** - * Prepare the data for validation. - */ - protected function prepareForValidation(): void - { - // JSON 문자열인 경우 배열로 변환 - if ($this->has('target_items') && is_string($this->input('target_items'))) { - $this->merge([ - 'target_items' => json_decode($this->input('target_items'), true) ?? [] - ]); - } - } -} \ No newline at end of file diff --git a/app/Http/Requests/BomResolveRequest.php b/app/Http/Requests/BomResolveRequest.php deleted file mode 100644 index 23fae73..0000000 --- a/app/Http/Requests/BomResolveRequest.php +++ /dev/null @@ -1,81 +0,0 @@ - 'required|integer|exists:models,id', - 'parameters' => 'required|array', - 'parameters.*' => 'required', - 'preview_only' => 'boolean', - 'use_cache' => 'boolean', - ]; - } - - /** - * Get custom messages for validator errors. - */ - public function messages(): array - { - return [ - 'model_id.required' => __('error.model_id_required'), - 'model_id.exists' => __('error.model_not_found'), - 'parameters.required' => __('error.parameters_required'), - 'parameters.array' => __('error.parameters_must_be_array'), - ]; - } - - /** - * Configure the validator instance. - */ - public function withValidator($validator): void - { - $validator->after(function ($validator) { - $this->validateParameters($validator); - }); - } - - /** - * 매개변수 검증 - */ - private function validateParameters($validator): void - { - if (!$this->input('model_id') || !$this->input('parameters')) { - return; - } - - try { - $parameterService = new \App\Services\ModelParameterService(); - $errors = $parameterService->validateParameterValues( - $this->input('model_id'), - $this->input('parameters') - ); - - if (!empty($errors)) { - foreach ($errors as $paramName => $paramErrors) { - foreach ($paramErrors as $error) { - $validator->errors()->add("parameters.{$paramName}", $error); - } - } - } - } catch (\Throwable $e) { - $validator->errors()->add('parameters', __('error.parameter_validation_failed')); - } - } -} \ No newline at end of file diff --git a/app/Http/Requests/ModelFormulaRequest.php b/app/Http/Requests/ModelFormulaRequest.php deleted file mode 100644 index d182d58..0000000 --- a/app/Http/Requests/ModelFormulaRequest.php +++ /dev/null @@ -1,214 +0,0 @@ - 'required|integer|exists:models,id', - 'name' => 'required|string|max:50|regex:/^[a-zA-Z][a-zA-Z0-9_]*$/', - 'label' => 'required|string|max:100', - 'expression' => 'required|string|max:1000', - 'unit' => 'nullable|string|max:20', - 'order' => 'nullable|integer|min:1', - 'description' => 'nullable|string|max:500', - 'is_active' => 'boolean', - ]; - } - - /** - * Get custom messages for validator errors. - */ - public function messages(): array - { - return [ - 'name.regex' => __('error.formula_name_format'), - 'expression.required' => __('error.formula_expression_required'), - ]; - } - - /** - * Configure the validator instance. - */ - public function withValidator($validator): void - { - $validator->after(function ($validator) { - $this->validateFormulaNameUnique($validator); - $this->validateFormulaExpression($validator); - $this->validateDependencies($validator); - }); - } - - /** - * 공식명 중복 검증 - */ - private function validateFormulaNameUnique($validator): void - { - if (!$this->input('model_id') || !$this->input('name')) { - return; - } - - $query = ModelFormula::where('model_id', $this->input('model_id')) - ->where('name', $this->input('name')); - - // 수정 시 자기 자신 제외 - if ($this->route('id')) { - $query->where('id', '!=', $this->route('id')); - } - - if ($query->exists()) { - $validator->errors()->add('name', __('error.formula_name_duplicate')); - } - - // 매개변수명과 중복 검증 - $parameterExists = \Shared\Models\Products\ModelParameter::where('model_id', $this->input('model_id')) - ->where('name', $this->input('name')) - ->exists(); - - if ($parameterExists) { - $validator->errors()->add('name', __('error.formula_name_conflicts_with_parameter')); - } - } - - /** - * 공식 표현식 검증 - */ - private function validateFormulaExpression($validator): void - { - if (!$this->input('expression')) { - return; - } - - $tempFormula = new ModelFormula([ - 'expression' => $this->input('expression'), - 'model_id' => $this->input('model_id'), - ]); - - $expressionErrors = $tempFormula->validateExpression(); - - if (!empty($expressionErrors)) { - foreach ($expressionErrors as $error) { - $validator->errors()->add('expression', $error); - } - } - } - - /** - * 의존성 검증 - */ - private function validateDependencies($validator): void - { - if (!$this->input('model_id') || !$this->input('expression')) { - return; - } - - $tempFormula = new ModelFormula([ - 'expression' => $this->input('expression'), - 'model_id' => $this->input('model_id'), - ]); - - $dependencies = $tempFormula->extractVariables(); - - if (empty($dependencies)) { - return; - } - - // 매개변수 목록 가져오기 - $parameters = \Shared\Models\Products\ModelParameter::where('model_id', $this->input('model_id')) - ->active() - ->pluck('name') - ->toArray(); - - // 기존 공식 목록 가져오기 (자기 자신 제외) - $formulasQuery = ModelFormula::where('model_id', $this->input('model_id')) - ->active(); - - if ($this->route('id')) { - $formulasQuery->where('id', '!=', $this->route('id')); - } - - $formulas = $formulasQuery->pluck('name')->toArray(); - - $validNames = array_merge($parameters, $formulas); - - foreach ($dependencies as $dep) { - if (!in_array($dep, $validNames)) { - $validator->errors()->add('expression', __('error.dependency_not_found', ['name' => $dep])); - } - } - - // 순환 의존성 검증 - $this->validateCircularDependency($validator, $dependencies); - } - - /** - * 순환 의존성 검증 - */ - private function validateCircularDependency($validator, array $dependencies): void - { - if (!$this->input('model_id') || !$this->input('name')) { - return; - } - - $allFormulasQuery = ModelFormula::where('model_id', $this->input('model_id')) - ->active(); - - if ($this->route('id')) { - $allFormulasQuery->where('id', '!=', $this->route('id')); - } - - $allFormulas = $allFormulasQuery->get(); - - // 현재 공식을 임시로 추가 - $tempFormula = new ModelFormula([ - 'name' => $this->input('name'), - 'dependencies' => $dependencies, - ]); - $allFormulas->push($tempFormula); - - if ($this->hasCircularDependency($tempFormula, $allFormulas->toArray())) { - $validator->errors()->add('expression', __('error.circular_dependency_detected')); - } - } - - /** - * 순환 의존성 검사 - */ - private function hasCircularDependency(ModelFormula $formula, array $allFormulas, array $visited = []): bool - { - if (in_array($formula->name, $visited)) { - return true; - } - - $visited[] = $formula->name; - - foreach ($formula->dependencies ?? [] as $dep) { - foreach ($allFormulas as $depFormula) { - if ($depFormula->name === $dep) { - if ($this->hasCircularDependency($depFormula, $allFormulas, $visited)) { - return true; - } - break; - } - } - } - - return false; - } -} \ No newline at end of file diff --git a/app/Http/Requests/ModelParameterRequest.php b/app/Http/Requests/ModelParameterRequest.php deleted file mode 100644 index 48862df..0000000 --- a/app/Http/Requests/ModelParameterRequest.php +++ /dev/null @@ -1,127 +0,0 @@ - 'required|integer|exists:models,id', - 'name' => 'required|string|max:50|regex:/^[a-zA-Z][a-zA-Z0-9_]*$/', - 'label' => 'required|string|max:100', - 'type' => 'required|string|in:' . implode(',', ModelParameter::TYPES), - 'unit' => 'nullable|string|max:20', - 'validation_rules' => 'nullable|array', - 'options' => 'nullable|array', - 'default_value' => 'nullable', - 'order' => 'nullable|integer|min:1', - 'description' => 'nullable|string|max:500', - 'is_required' => 'boolean', - 'is_active' => 'boolean', - ]; - - // 타입별 세부 검증 - if ($this->input('type') === ModelParameter::TYPE_NUMBER) { - $rules['validation_rules.min'] = 'nullable|numeric'; - $rules['validation_rules.max'] = 'nullable|numeric|gte:validation_rules.min'; - $rules['default_value'] = 'nullable|numeric'; - } - - if ($this->input('type') === ModelParameter::TYPE_SELECT) { - $rules['options'] = 'required|array|min:1'; - $rules['options.*'] = 'required|string|max:100'; - $rules['default_value'] = 'nullable|string|in_array:options'; - } - - if ($this->input('type') === ModelParameter::TYPE_BOOLEAN) { - $rules['default_value'] = 'nullable|boolean'; - } - - if ($this->input('type') === ModelParameter::TYPE_TEXT) { - $rules['validation_rules.max_length'] = 'nullable|integer|min:1|max:1000'; - $rules['validation_rules.pattern'] = 'nullable|string|max:200'; - $rules['default_value'] = 'nullable|string'; - } - - return $rules; - } - - /** - * Get custom messages for validator errors. - */ - public function messages(): array - { - return [ - 'name.regex' => __('error.parameter_name_format'), - 'validation_rules.max.gte' => __('error.max_must_be_greater_than_min'), - 'options.required' => __('error.select_type_requires_options'), - 'options.min' => __('error.select_type_requires_options'), - 'default_value.in_array' => __('error.default_value_not_in_options'), - ]; - } - - /** - * Configure the validator instance. - */ - public function withValidator($validator): void - { - $validator->after(function ($validator) { - // 추가 검증 로직 - $this->validateParameterNameUnique($validator); - $this->validateValidationRules($validator); - }); - } - - /** - * 매개변수명 중복 검증 - */ - private function validateParameterNameUnique($validator): void - { - if (!$this->input('model_id') || !$this->input('name')) { - return; - } - - $query = ModelParameter::where('model_id', $this->input('model_id')) - ->where('name', $this->input('name')); - - // 수정 시 자기 자신 제외 - if ($this->route('id')) { - $query->where('id', '!=', $this->route('id')); - } - - if ($query->exists()) { - $validator->errors()->add('name', __('error.parameter_name_duplicate')); - } - } - - /** - * 검증 규칙 유효성 검증 - */ - private function validateValidationRules($validator): void - { - $type = $this->input('type'); - $validationRules = $this->input('validation_rules', []); - - if ($type === ModelParameter::TYPE_TEXT && isset($validationRules['pattern'])) { - // 정규식 패턴 검증 - if (@preg_match($validationRules['pattern'], '') === false) { - $validator->errors()->add('validation_rules.pattern', __('error.invalid_regex_pattern')); - } - } - } -} \ No newline at end of file diff --git a/app/Http/Requests/ProductFromModelRequest.php b/app/Http/Requests/ProductFromModelRequest.php deleted file mode 100644 index 135be8f..0000000 --- a/app/Http/Requests/ProductFromModelRequest.php +++ /dev/null @@ -1,138 +0,0 @@ - 'required|integer|exists:models,id', - 'parameters' => 'required|array', - 'parameters.*' => 'required', - 'product_data' => 'nullable|array', - 'product_data.name' => 'nullable|string|max:200', - 'product_data.code' => 'nullable|string|max:100|unique:products,code', - 'product_data.description' => 'nullable|string|max:1000', - 'product_data.category_id' => 'nullable|integer|exists:categories,id', - 'product_data.memo' => 'nullable|string|max:500', - ]; - } - - /** - * Get custom messages for validator errors. - */ - public function messages(): array - { - return [ - 'model_id.required' => __('error.model_id_required'), - 'model_id.exists' => __('error.model_not_found'), - 'parameters.required' => __('error.parameters_required'), - 'parameters.array' => __('error.parameters_must_be_array'), - 'product_data.code.unique' => __('error.product_code_duplicate'), - ]; - } - - /** - * Configure the validator instance. - */ - public function withValidator($validator): void - { - $validator->after(function ($validator) { - $this->validateParameters($validator); - $this->validateProductData($validator); - }); - } - - /** - * 매개변수 검증 - */ - private function validateParameters($validator): void - { - if (!$this->input('model_id') || !$this->input('parameters')) { - return; - } - - try { - $parameterService = new \App\Services\ModelParameterService(); - $errors = $parameterService->validateParameterValues( - $this->input('model_id'), - $this->input('parameters') - ); - - if (!empty($errors)) { - foreach ($errors as $paramName => $paramErrors) { - foreach ($paramErrors as $error) { - $validator->errors()->add("parameters.{$paramName}", $error); - } - } - } - } catch (\Throwable $e) { - $validator->errors()->add('parameters', __('error.parameter_validation_failed')); - } - } - - /** - * 제품 데이터 검증 - */ - private function validateProductData($validator): void - { - $productData = $this->input('product_data', []); - - // 제품 코드 중복 검증 (수정 시 제외) - if (!empty($productData['code'])) { - $query = \Shared\Models\Products\Product::where('code', $productData['code']); - - if ($this->route('id')) { - $query->where('id', '!=', $this->route('id')); - } - - if ($query->exists()) { - $validator->errors()->add('product_data.code', __('error.product_code_duplicate')); - } - } - - // 카테고리 존재 확인 - if (!empty($productData['category_id'])) { - $categoryExists = \Shared\Models\Products\Category::where('id', $productData['category_id']) - ->where('is_active', true) - ->exists(); - - if (!$categoryExists) { - $validator->errors()->add('product_data.category_id', __('error.category_not_found')); - } - } - } - - /** - * Prepare the data for validation. - */ - protected function prepareForValidation(): void - { - // JSON 문자열인 경우 배열로 변환 - if ($this->has('parameters') && is_string($this->input('parameters'))) { - $this->merge([ - 'parameters' => json_decode($this->input('parameters'), true) ?? [] - ]); - } - - if ($this->has('product_data') && is_string($this->input('product_data'))) { - $this->merge([ - 'product_data' => json_decode($this->input('product_data'), true) ?? [] - ]); - } - } -} \ No newline at end of file diff --git a/app/Models/Design/BomConditionRule.php b/app/Models/Design/BomConditionRule.php deleted file mode 100644 index 55ff092..0000000 --- a/app/Models/Design/BomConditionRule.php +++ /dev/null @@ -1,179 +0,0 @@ - 'decimal:6', - 'is_active' => 'boolean', - 'priority' => 'integer', - ]; - - /** - * 조건 규칙이 속한 모델 - */ - public function designModel() - { - return $this->belongsTo(DesignModel::class, 'model_id'); - } - - /** - * 조건식 평가 - */ - public function evaluateCondition(array $parameters): bool - { - $expression = $this->condition_expression; - - // 매개변수 값으로 치환 - foreach ($parameters as $param => $value) { - // 문자열 값은 따옴표로 감싸기 - if (is_string($value)) { - $value = "'" . addslashes($value) . "'"; - } elseif (is_bool($value)) { - $value = $value ? 'true' : 'false'; - } - - $expression = str_replace($param, (string) $value, $expression); - } - - // 안전한 조건식 평가 - return $this->evaluateSimpleCondition($expression); - } - - /** - * 간단한 조건식 평가기 - */ - private function evaluateSimpleCondition(string $expression): bool - { - // 공백 제거 - $expression = trim($expression); - - // 간단한 비교 연산자들 처리 - $operators = ['==', '!=', '>=', '<=', '>', '<']; - - foreach ($operators as $operator) { - if (strpos($expression, $operator) !== false) { - $parts = explode($operator, $expression, 2); - if (count($parts) === 2) { - $left = trim($parts[0]); - $right = trim($parts[1]); - - // 따옴표 제거 - $left = trim($left, "'\""); - $right = trim($right, "'\""); - - // 숫자 변환 시도 - if (is_numeric($left)) $left = (float) $left; - if (is_numeric($right)) $right = (float) $right; - - switch ($operator) { - case '==': - return $left == $right; - case '!=': - return $left != $right; - case '>=': - return $left >= $right; - case '<=': - return $left <= $right; - case '>': - return $left > $right; - case '<': - return $left < $right; - } - } - } - } - - // IN 연산자 처리 - if (preg_match('/(.+)\s+IN\s+\((.+)\)/i', $expression, $matches)) { - $value = trim($matches[1], "'\""); - $list = array_map('trim', explode(',', $matches[2])); - $list = array_map(function($item) { - return trim($item, "'\""); - }, $list); - - return in_array($value, $list); - } - - // NOT IN 연산자 처리 - if (preg_match('/(.+)\s+NOT\s+IN\s+\((.+)\)/i', $expression, $matches)) { - $value = trim($matches[1], "'\""); - $list = array_map('trim', explode(',', $matches[2])); - $list = array_map(function($item) { - return trim($item, "'\""); - }, $list); - - return !in_array($value, $list); - } - - // 불린 값 처리 - if (in_array(strtolower($expression), ['true', '1'])) { - return true; - } - if (in_array(strtolower($expression), ['false', '0'])) { - return false; - } - - throw new \InvalidArgumentException('Invalid condition expression: ' . $this->condition_expression); - } - - /** - * 조건 규칙 액션 실행 - */ - public function executeAction(array $currentBom): array - { - switch ($this->action_type) { - case 'INCLUDE': - // 아이템 포함 - $currentBom[] = [ - 'target_type' => $this->target_type, - 'target_id' => $this->target_id, - 'quantity' => $this->quantity_multiplier ?? 1, - 'reason' => $this->rule_name, - ]; - break; - - case 'EXCLUDE': - // 아이템 제외 - $currentBom = array_filter($currentBom, function($item) { - return !($item['target_type'] === $this->target_type && $item['target_id'] === $this->target_id); - }); - break; - - case 'MODIFY_QUANTITY': - // 수량 변경 - foreach ($currentBom as &$item) { - if ($item['target_type'] === $this->target_type && $item['target_id'] === $this->target_id) { - $item['quantity'] = ($item['quantity'] ?? 1) * ($this->quantity_multiplier ?? 1); - $item['reason'] = $this->rule_name; - } - } - break; - } - - return array_values($currentBom); // 인덱스 재정렬 - } -} \ No newline at end of file diff --git a/app/Models/Design/ModelFormula.php b/app/Models/Design/ModelFormula.php deleted file mode 100644 index cd6a91d..0000000 --- a/app/Models/Design/ModelFormula.php +++ /dev/null @@ -1,122 +0,0 @@ - 'integer', - 'dependencies' => 'array', - ]; - - /** - * 공식이 속한 모델 - */ - public function designModel() - { - return $this->belongsTo(DesignModel::class, 'model_id'); - } - - /** - * 공식에서 변수 추출 - */ - public function extractVariables(): array - { - // 간단한 변수 추출 (영문자로 시작하는 단어들) - preg_match_all('/\b[a-zA-Z][a-zA-Z0-9_]*\b/', $this->formula_expression, $matches); - - // 수학 함수 제외 - $mathFunctions = ['sin', 'cos', 'tan', 'log', 'exp', 'sqrt', 'pow', 'abs', 'ceil', 'floor', 'round', 'max', 'min']; - $variables = array_diff($matches[0], $mathFunctions); - - return array_unique($variables); - } - - /** - * 공식 계산 (안전한 eval 대신 간단한 파서 사용) - */ - public function calculate(array $values): float - { - $expression = $this->formula_expression; - - // 변수를 값으로 치환 - foreach ($values as $variable => $value) { - $expression = str_replace($variable, (string) $value, $expression); - } - - // 간단한 수식 계산 (보안상 eval 사용 금지) - return $this->evaluateSimpleExpression($expression); - } - - /** - * 간단한 수식 계산기 (덧셈, 뺄셈, 곱셈, 나눗셈) - */ - private function evaluateSimpleExpression(string $expression): float - { - // 공백 제거 - $expression = preg_replace('/\s+/', '', $expression); - - // 간단한 사칙연산만 허용 - if (!preg_match('/^[0-9+\-*\/\(\)\.]+$/', $expression)) { - throw new \InvalidArgumentException('Invalid expression: ' . $expression); - } - - // 안전한 계산을 위해 제한된 연산만 허용 - try { - // 실제 프로덕션에서는 더 안전한 수식 파서 라이브러리 사용 권장 - return (float) eval("return $expression;"); - } catch (\Throwable $e) { - throw new \InvalidArgumentException('Formula calculation error: ' . $e->getMessage()); - } - } - - /** - * 의존성 순환 체크 - */ - public function hasCircularDependency(array $allFormulas): bool - { - $visited = []; - $recursionStack = []; - - return $this->dfsCheckCircular($allFormulas, $visited, $recursionStack); - } - - private function dfsCheckCircular(array $allFormulas, array &$visited, array &$recursionStack): bool - { - $visited[$this->formula_name] = true; - $recursionStack[$this->formula_name] = true; - - foreach ($this->dependencies as $dependency) { - if (!isset($visited[$dependency])) { - $dependentFormula = collect($allFormulas)->firstWhere('formula_name', $dependency); - if ($dependentFormula && $dependentFormula->dfsCheckCircular($allFormulas, $visited, $recursionStack)) { - return true; - } - } elseif (isset($recursionStack[$dependency])) { - return true; // 순환 의존성 발견 - } - } - - unset($recursionStack[$this->formula_name]); - return false; - } -} \ No newline at end of file diff --git a/app/Models/Design/ModelParameter.php b/app/Models/Design/ModelParameter.php deleted file mode 100644 index e40765f..0000000 --- a/app/Models/Design/ModelParameter.php +++ /dev/null @@ -1,94 +0,0 @@ - 'boolean', - 'min_value' => 'decimal:6', - 'max_value' => 'decimal:6', - 'options' => 'array', - 'sort_order' => 'integer', - ]; - - /** - * 매개변수가 속한 모델 - */ - public function designModel() - { - return $this->belongsTo(DesignModel::class, 'model_id'); - } - - /** - * 매개변수 타입별 검증 - */ - public function validateValue($value) - { - switch ($this->parameter_type) { - case 'NUMBER': - if (!is_numeric($value)) { - return false; - } - if ($this->min_value !== null && $value < $this->min_value) { - return false; - } - if ($this->max_value !== null && $value > $this->max_value) { - return false; - } - return true; - - case 'SELECT': - return in_array($value, $this->options ?? []); - - case 'BOOLEAN': - return is_bool($value) || in_array($value, [0, 1, '0', '1', 'true', 'false']); - - case 'TEXT': - return is_string($value); - - default: - return true; - } - } - - /** - * 매개변수 값 형변환 - */ - public function castValue($value) - { - switch ($this->parameter_type) { - case 'NUMBER': - return (float) $value; - case 'BOOLEAN': - return (bool) $value; - case 'TEXT': - case 'SELECT': - default: - return (string) $value; - } - } -} \ No newline at end of file diff --git a/app/Services/BomConditionRuleService.php b/app/Services/BomConditionRuleService.php deleted file mode 100644 index 184a14a..0000000 --- a/app/Services/BomConditionRuleService.php +++ /dev/null @@ -1,436 +0,0 @@ -validateModelAccess($modelId); - - $query = BomConditionRule::where('model_id', $modelId) - ->active() - ->byPriority() - ->with('model'); - - if ($paginate) { - return $query->paginate($perPage); - } - - return $query->get(); - } - - /** - * 조건 규칙 상세 조회 - */ - public function getRule(int $id): BomConditionRule - { - $rule = BomConditionRule::where('tenant_id', $this->tenantId()) - ->findOrFail($id); - - $this->validateModelAccess($rule->model_id); - - return $rule; - } - - /** - * 조건 규칙 생성 - */ - public function createRule(array $data): BomConditionRule - { - $this->validateModelAccess($data['model_id']); - - // 기본값 설정 - $data['tenant_id'] = $this->tenantId(); - $data['created_by'] = $this->apiUserId(); - - // 우선순위가 지정되지 않은 경우 마지막으로 설정 - if (!isset($data['priority'])) { - $maxPriority = BomConditionRule::where('tenant_id', $this->tenantId()) - ->where('model_id', $data['model_id']) - ->max('priority') ?? 0; - $data['priority'] = $maxPriority + 1; - } - - // 규칙명 중복 체크 - $this->validateRuleNameUnique($data['model_id'], $data['name']); - - // 조건식 검증 - $rule = new BomConditionRule($data); - $conditionErrors = $rule->validateConditionExpression(); - - if (!empty($conditionErrors)) { - throw new \InvalidArgumentException(__('error.invalid_condition_expression') . ': ' . implode(', ', $conditionErrors)); - } - - // 대상 아이템 처리 - if (isset($data['target_items']) && is_string($data['target_items'])) { - $data['target_items'] = json_decode($data['target_items'], true); - } - - // 대상 아이템 검증 - $this->validateTargetItems($data['target_items'] ?? [], $data['action']); - - $rule = BomConditionRule::create($data); - - return $rule->fresh(); - } - - /** - * 조건 규칙 수정 - */ - public function updateRule(int $id, array $data): BomConditionRule - { - $rule = $this->getRule($id); - - // 규칙명 변경 시 중복 체크 - if (isset($data['name']) && $data['name'] !== $rule->name) { - $this->validateRuleNameUnique($rule->model_id, $data['name'], $id); - } - - // 조건식 변경 시 검증 - if (isset($data['condition_expression'])) { - $tempRule = new BomConditionRule(array_merge($rule->toArray(), $data)); - $conditionErrors = $tempRule->validateConditionExpression(); - - if (!empty($conditionErrors)) { - throw new \InvalidArgumentException(__('error.invalid_condition_expression') . ': ' . implode(', ', $conditionErrors)); - } - } - - // 대상 아이템 처리 - if (isset($data['target_items']) && is_string($data['target_items'])) { - $data['target_items'] = json_decode($data['target_items'], true); - } - - // 대상 아이템 검증 - if (isset($data['target_items']) || isset($data['action'])) { - $action = $data['action'] ?? $rule->action; - $targetItems = $data['target_items'] ?? $rule->target_items; - $this->validateTargetItems($targetItems, $action); - } - - $data['updated_by'] = $this->apiUserId(); - $rule->update($data); - - return $rule->fresh(); - } - - /** - * 조건 규칙 삭제 - */ - public function deleteRule(int $id): bool - { - $rule = $this->getRule($id); - - $rule->update(['deleted_by' => $this->apiUserId()]); - $rule->delete(); - - return true; - } - - /** - * 조건 규칙 우선순위 변경 - */ - public function reorderRules(int $modelId, array $orderData): bool - { - $this->validateModelAccess($modelId); - - foreach ($orderData as $item) { - BomConditionRule::where('tenant_id', $this->tenantId()) - ->where('model_id', $modelId) - ->where('id', $item['id']) - ->update([ - 'priority' => $item['priority'], - 'updated_by' => $this->apiUserId() - ]); - } - - return true; - } - - /** - * 조건 규칙 복사 (다른 모델로) - */ - public function copyRulesToModel(int $sourceModelId, int $targetModelId): Collection - { - $this->validateModelAccess($sourceModelId); - $this->validateModelAccess($targetModelId); - - $sourceRules = $this->getRulesByModel($sourceModelId); - $copiedRules = collect(); - - foreach ($sourceRules as $sourceRule) { - $data = $sourceRule->toArray(); - unset($data['id'], $data['created_at'], $data['updated_at'], $data['deleted_at']); - - $data['model_id'] = $targetModelId; - $data['created_by'] = $this->apiUserId(); - - // 이름 중복 시 수정 - $originalName = $data['name']; - $counter = 1; - while ($this->isRuleNameExists($targetModelId, $data['name'])) { - $data['name'] = $originalName . '_' . $counter; - $counter++; - } - - $copiedRule = BomConditionRule::create($data); - $copiedRules->push($copiedRule); - } - - return $copiedRules; - } - - /** - * 조건 평가 및 적용할 규칙 찾기 - */ - public function getApplicableRules(int $modelId, array $variables): Collection - { - $this->validateModelAccess($modelId); - - $rules = $this->getRulesByModel($modelId); - $applicableRules = collect(); - - foreach ($rules as $rule) { - if ($rule->evaluateCondition($variables)) { - $applicableRules->push($rule); - } - } - - return $applicableRules; - } - - /** - * 조건 규칙을 BOM 아이템에 적용 - */ - public function applyRulesToBomItems(int $modelId, array $bomItems, array $variables): array - { - $applicableRules = $this->getApplicableRules($modelId, $variables); - - // 우선순위 순서대로 규칙 적용 - foreach ($applicableRules as $rule) { - $bomItems = $rule->applyAction($bomItems); - } - - return $bomItems; - } - - /** - * 조건식 검증 (문법 체크) - */ - public function validateConditionExpression(int $modelId, string $expression): array - { - $this->validateModelAccess($modelId); - - $tempRule = new BomConditionRule([ - 'condition_expression' => $expression, - 'model_id' => $modelId - ]); - - return $tempRule->validateConditionExpression(); - } - - /** - * 조건식 테스트 (실제 변수값으로 평가) - */ - public function testConditionExpression(int $modelId, string $expression, array $variables): array - { - $this->validateModelAccess($modelId); - - $result = [ - 'valid' => false, - 'result' => null, - 'error' => null - ]; - - try { - $tempRule = new BomConditionRule([ - 'condition_expression' => $expression, - 'model_id' => $modelId - ]); - - $validationErrors = $tempRule->validateConditionExpression(); - if (!empty($validationErrors)) { - $result['error'] = implode(', ', $validationErrors); - return $result; - } - - $evaluationResult = $tempRule->evaluateCondition($variables); - $result['valid'] = true; - $result['result'] = $evaluationResult; - } catch (\Throwable $e) { - $result['error'] = $e->getMessage(); - } - - return $result; - } - - /** - * 모델의 사용 가능한 변수 목록 조회 (매개변수 + 공식) - */ - public function getAvailableVariables(int $modelId): array - { - $this->validateModelAccess($modelId); - - $parameterService = new ModelParameterService(); - $formulaService = new ModelFormulaService(); - - $parameters = $parameterService->getParametersByModel($modelId); - $formulas = $formulaService->getFormulasByModel($modelId); - - $variables = []; - - foreach ($parameters as $parameter) { - $variables[] = [ - 'name' => $parameter->name, - 'label' => $parameter->label, - 'type' => $parameter->type, - 'source' => 'parameter' - ]; - } - - foreach ($formulas as $formula) { - $variables[] = [ - 'name' => $formula->name, - 'label' => $formula->label, - 'type' => 'NUMBER', // 공식 결과는 숫자 - 'source' => 'formula' - ]; - } - - return $variables; - } - - /** - * 모델 접근 권한 검증 - */ - private function validateModelAccess(int $modelId): void - { - $model = ModelMaster::where('tenant_id', $this->tenantId()) - ->findOrFail($modelId); - } - - /** - * 규칙명 중복 검증 - */ - private function validateRuleNameUnique(int $modelId, string $name, ?int $excludeId = null): void - { - $query = BomConditionRule::where('tenant_id', $this->tenantId()) - ->where('model_id', $modelId) - ->where('name', $name); - - if ($excludeId) { - $query->where('id', '!=', $excludeId); - } - - if ($query->exists()) { - throw new \InvalidArgumentException(__('error.rule_name_duplicate')); - } - } - - /** - * 규칙명 존재 여부 확인 - */ - private function isRuleNameExists(int $modelId, string $name): bool - { - return BomConditionRule::where('tenant_id', $this->tenantId()) - ->where('model_id', $modelId) - ->where('name', $name) - ->exists(); - } - - /** - * 대상 아이템 검증 - */ - private function validateTargetItems(array $targetItems, string $action): void - { - if (empty($targetItems)) { - throw new \InvalidArgumentException(__('error.target_items_required')); - } - - foreach ($targetItems as $index => $item) { - if (!isset($item['product_id']) && !isset($item['material_id'])) { - throw new \InvalidArgumentException(__('error.target_item_missing_reference', ['index' => $index])); - } - - // REPLACE 액션의 경우 replace_from 필요 - if ($action === BomConditionRule::ACTION_REPLACE && !isset($item['replace_from'])) { - throw new \InvalidArgumentException(__('error.replace_from_required', ['index' => $index])); - } - - // 수량 검증 - if (isset($item['quantity']) && (!is_numeric($item['quantity']) || $item['quantity'] <= 0)) { - throw new \InvalidArgumentException(__('error.invalid_quantity', ['index' => $index])); - } - - // 낭비율 검증 - if (isset($item['waste_rate']) && (!is_numeric($item['waste_rate']) || $item['waste_rate'] < 0 || $item['waste_rate'] > 100)) { - throw new \InvalidArgumentException(__('error.invalid_waste_rate', ['index' => $index])); - } - } - } - - /** - * 규칙 활성화/비활성화 - */ - public function toggleRuleStatus(int $id): BomConditionRule - { - $rule = $this->getRule($id); - - $rule->update([ - 'is_active' => !$rule->is_active, - 'updated_by' => $this->apiUserId() - ]); - - return $rule->fresh(); - } - - /** - * 규칙 실행 로그 (디버깅용) - */ - public function getRuleExecutionLog(int $modelId, array $variables): array - { - $this->validateModelAccess($modelId); - - $rules = $this->getRulesByModel($modelId); - $log = []; - - foreach ($rules as $rule) { - $logEntry = [ - 'rule_id' => $rule->id, - 'rule_name' => $rule->name, - 'condition' => $rule->condition_expression, - 'priority' => $rule->priority, - 'evaluated' => false, - 'result' => false, - 'action' => $rule->action, - 'error' => null - ]; - - try { - $logEntry['evaluated'] = true; - $logEntry['result'] = $rule->evaluateCondition($variables); - } catch (\Throwable $e) { - $logEntry['error'] = $e->getMessage(); - } - - $log[] = $logEntry; - } - - return $log; - } -} \ No newline at end of file diff --git a/app/Services/BomResolverService.php b/app/Services/BomResolverService.php deleted file mode 100644 index 96e04c8..0000000 --- a/app/Services/BomResolverService.php +++ /dev/null @@ -1,505 +0,0 @@ -parameterService = new ModelParameterService(); - $this->formulaService = new ModelFormulaService(); - $this->conditionRuleService = new BomConditionRuleService(); - } - - /** - * 매개변수 기반 BOM 해석 (미리보기) - */ - public function resolveBom(int $modelId, array $inputParameters): array - { - $this->validateModelAccess($modelId); - - // 1. 매개변수 검증 및 타입 변환 - $validatedParameters = $this->validateAndCastParameters($modelId, $inputParameters); - - // 2. 공식 계산 - $calculatedValues = $this->calculateFormulas($modelId, $validatedParameters); - - // 3. 기본 BOM 템플릿 가져오기 - $baseBomItems = $this->getBaseBomItems($modelId); - - // 4. 조건 규칙 적용 - $resolvedBomItems = $this->applyConditionRules($modelId, $baseBomItems, $calculatedValues); - - // 5. 수량 및 공식 계산 적용 - $finalBomItems = $this->calculateItemQuantities($resolvedBomItems, $calculatedValues); - - return [ - 'model_id' => $modelId, - 'input_parameters' => $validatedParameters, - 'calculated_values' => $calculatedValues, - 'bom_items' => $finalBomItems, - 'summary' => $this->generateBomSummary($finalBomItems), - 'resolved_at' => now()->toISOString(), - ]; - } - - /** - * 실시간 미리보기 (캐시 활용) - */ - public function previewBom(int $modelId, array $inputParameters): array - { - // 캐시 키 생성 - $cacheKey = $this->generateCacheKey($modelId, $inputParameters); - - return Cache::remember($cacheKey, 300, function () use ($modelId, $inputParameters) { - return $this->resolveBom($modelId, $inputParameters); - }); - } - - /** - * BOM 검증 (오류 체크) - */ - public function validateBom(int $modelId, array $inputParameters): array - { - $errors = []; - $warnings = []; - - try { - // 매개변수 검증 - $parameterErrors = $this->parameterService->validateParameterValues($modelId, $inputParameters); - if (!empty($parameterErrors)) { - $errors['parameters'] = $parameterErrors; - } - - // 기본 BOM 템플릿 존재 확인 - if (!$this->hasBaseBomTemplate($modelId)) { - $warnings[] = 'No base BOM template found for this model'; - } - - // 공식 계산 가능 여부 확인 - try { - $validatedParameters = $this->validateAndCastParameters($modelId, $inputParameters); - $this->calculateFormulas($modelId, $validatedParameters); - } catch (\Throwable $e) { - $errors['formulas'] = ['Formula calculation failed: ' . $e->getMessage()]; - } - - // 조건 규칙 평가 가능 여부 확인 - try { - $calculatedValues = $this->calculateFormulas($modelId, $validatedParameters ?? []); - $this->conditionRuleService->getApplicableRules($modelId, $calculatedValues); - } catch (\Throwable $e) { - $errors['condition_rules'] = ['Condition rule evaluation failed: ' . $e->getMessage()]; - } - - } catch (\Throwable $e) { - $errors['general'] = [$e->getMessage()]; - } - - return [ - 'valid' => empty($errors), - 'errors' => $errors, - 'warnings' => $warnings, - ]; - } - - /** - * BOM 비교 (다른 매개변수값과 비교) - */ - public function compareBom(int $modelId, array $parameters1, array $parameters2): array - { - $bom1 = $this->resolveBom($modelId, $parameters1); - $bom2 = $this->resolveBom($modelId, $parameters2); - - return [ - 'parameters_diff' => $this->compareParameters($bom1['calculated_values'], $bom2['calculated_values']), - 'bom_items_diff' => $this->compareBomItems($bom1['bom_items'], $bom2['bom_items']), - 'summary_diff' => $this->compareSummary($bom1['summary'], $bom2['summary']), - ]; - } - - /** - * 대량 BOM 해석 (여러 매개변수 조합) - */ - public function resolveBomBatch(int $modelId, array $parameterSets): array - { - $results = []; - - DB::transaction(function () use ($modelId, $parameterSets, &$results) { - foreach ($parameterSets as $index => $parameters) { - try { - $results[$index] = $this->resolveBom($modelId, $parameters); - } catch (\Throwable $e) { - $results[$index] = [ - 'error' => $e->getMessage(), - 'parameters' => $parameters, - ]; - } - } - }); - - return $results; - } - - /** - * BOM 성능 최적화 제안 - */ - public function getOptimizationSuggestions(int $modelId, array $inputParameters): array - { - $resolvedBom = $this->resolveBom($modelId, $inputParameters); - $suggestions = []; - - // 1. 불필요한 조건 규칙 탐지 - $unusedRules = $this->findUnusedRules($modelId, $resolvedBom['calculated_values']); - if (!empty($unusedRules)) { - $suggestions[] = [ - 'type' => 'unused_rules', - 'message' => 'Found unused condition rules that could be removed', - 'details' => $unusedRules, - ]; - } - - // 2. 복잡한 공식 탐지 - $complexFormulas = $this->findComplexFormulas($modelId); - if (!empty($complexFormulas)) { - $suggestions[] = [ - 'type' => 'complex_formulas', - 'message' => 'Found complex formulas that might impact performance', - 'details' => $complexFormulas, - ]; - } - - // 3. 중복 BOM 아이템 탐지 - $duplicateItems = $this->findDuplicateBomItems($resolvedBom['bom_items']); - if (!empty($duplicateItems)) { - $suggestions[] = [ - 'type' => 'duplicate_items', - 'message' => 'Found duplicate BOM items that could be consolidated', - 'details' => $duplicateItems, - ]; - } - - return $suggestions; - } - - /** - * 매개변수 검증 및 타입 변환 - */ - private function validateAndCastParameters(int $modelId, array $inputParameters): array - { - $errors = $this->parameterService->validateParameterValues($modelId, $inputParameters); - - if (!empty($errors)) { - throw new \InvalidArgumentException(__('error.invalid_parameters') . ': ' . json_encode($errors)); - } - - return $this->parameterService->castParameterValues($modelId, $inputParameters); - } - - /** - * 공식 계산 - */ - private function calculateFormulas(int $modelId, array $parameters): array - { - return $this->formulaService->calculateFormulas($modelId, $parameters); - } - - /** - * 기본 BOM 템플릿 아이템 가져오기 - */ - private function getBaseBomItems(int $modelId): array - { - $model = ModelMaster::where('tenant_id', $this->tenantId()) - ->findOrFail($modelId); - - // 현재 활성 버전의 BOM 템플릿 가져오기 - $bomTemplate = BomTemplate::where('tenant_id', $this->tenantId()) - ->where('model_id', $modelId) - ->where('is_active', true) - ->orderBy('created_at', 'desc') - ->first(); - - if (!$bomTemplate) { - return []; - } - - $bomItems = BomTemplateItem::where('tenant_id', $this->tenantId()) - ->where('bom_template_id', $bomTemplate->id) - ->orderBy('order') - ->get() - ->map(function ($item) { - return [ - 'id' => $item->id, - 'product_id' => $item->product_id, - 'material_id' => $item->material_id, - 'ref_type' => $item->ref_type, - 'quantity' => $item->quantity, - 'quantity_formula' => $item->quantity_formula, - 'waste_rate' => $item->waste_rate, - 'unit' => $item->unit, - 'memo' => $item->memo, - 'order' => $item->order, - ]; - }) - ->toArray(); - - return $bomItems; - } - - /** - * 조건 규칙 적용 - */ - private function applyConditionRules(int $modelId, array $bomItems, array $calculatedValues): array - { - return $this->conditionRuleService->applyRulesToBomItems($modelId, $bomItems, $calculatedValues); - } - - /** - * 아이템별 수량 계산 - */ - private function calculateItemQuantities(array $bomItems, array $calculatedValues): array - { - foreach ($bomItems as &$item) { - // 수량 공식이 있는 경우 계산 - if (!empty($item['quantity_formula'])) { - $calculatedQuantity = $this->evaluateQuantityFormula($item['quantity_formula'], $calculatedValues); - if ($calculatedQuantity !== null) { - $item['calculated_quantity'] = $calculatedQuantity; - } - } else { - $item['calculated_quantity'] = $item['quantity'] ?? 1; - } - - // 낭비율 적용 - if (isset($item['waste_rate']) && $item['waste_rate'] > 0) { - $item['total_quantity'] = $item['calculated_quantity'] * (1 + $item['waste_rate'] / 100); - } else { - $item['total_quantity'] = $item['calculated_quantity']; - } - - // 반올림 (소수점 3자리) - $item['calculated_quantity'] = round($item['calculated_quantity'], 3); - $item['total_quantity'] = round($item['total_quantity'], 3); - } - - return $bomItems; - } - - /** - * 수량 공식 계산 - */ - private function evaluateQuantityFormula(string $formula, array $variables): ?float - { - try { - $expression = $formula; - - // 변수값 치환 - foreach ($variables as $name => $value) { - if (is_numeric($value)) { - $expression = preg_replace('/\b' . preg_quote($name, '/') . '\b/', $value, $expression); - } - } - - // 안전한 계산 실행 - if (preg_match('/^[0-9+\-*\/().,\s]+$/', $expression)) { - return eval("return $expression;"); - } - - return null; - } catch (\Throwable $e) { - return null; - } - } - - /** - * BOM 요약 정보 생성 - */ - private function generateBomSummary(array $bomItems): array - { - $summary = [ - 'total_items' => count($bomItems), - 'materials_count' => 0, - 'products_count' => 0, - 'total_cost' => 0, // 향후 가격 정보 추가 시 - ]; - - foreach ($bomItems as $item) { - if (!empty($item['material_id'])) { - $summary['materials_count']++; - } else { - $summary['products_count']++; - } - } - - return $summary; - } - - /** - * 캐시 키 생성 - */ - private function generateCacheKey(int $modelId, array $parameters): string - { - ksort($parameters); // 매개변수 순서 정규화 - $hash = md5(json_encode($parameters)); - return "bom_preview_{$this->tenantId()}_{$modelId}_{$hash}"; - } - - /** - * 기본 BOM 템플릿 존재 여부 확인 - */ - private function hasBaseBomTemplate(int $modelId): bool - { - return BomTemplate::where('tenant_id', $this->tenantId()) - ->where('model_id', $modelId) - ->where('is_active', true) - ->exists(); - } - - /** - * 모델 접근 권한 검증 - */ - private function validateModelAccess(int $modelId): void - { - $model = ModelMaster::where('tenant_id', $this->tenantId()) - ->findOrFail($modelId); - } - - /** - * 매개변수 비교 - */ - private function compareParameters(array $params1, array $params2): array - { - $diff = []; - - $allKeys = array_unique(array_merge(array_keys($params1), array_keys($params2))); - - foreach ($allKeys as $key) { - $value1 = $params1[$key] ?? null; - $value2 = $params2[$key] ?? null; - - if ($value1 !== $value2) { - $diff[$key] = [ - 'set1' => $value1, - 'set2' => $value2, - ]; - } - } - - return $diff; - } - - /** - * BOM 아이템 비교 - */ - private function compareBomItems(array $items1, array $items2): array - { - // 간단한 비교 로직 (실제로는 더 정교한 비교 필요) - return [ - 'count_diff' => count($items1) - count($items2), - 'items_only_in_set1' => array_diff_key($items1, $items2), - 'items_only_in_set2' => array_diff_key($items2, $items1), - ]; - } - - /** - * 요약 정보 비교 - */ - private function compareSummary(array $summary1, array $summary2): array - { - $diff = []; - - foreach ($summary1 as $key => $value) { - if (isset($summary2[$key]) && $value !== $summary2[$key]) { - $diff[$key] = [ - 'set1' => $value, - 'set2' => $summary2[$key], - 'diff' => $value - $summary2[$key], - ]; - } - } - - return $diff; - } - - /** - * 사용되지 않는 규칙 찾기 - */ - private function findUnusedRules(int $modelId, array $calculatedValues): array - { - $allRules = $this->conditionRuleService->getRulesByModel($modelId); - $applicableRules = $this->conditionRuleService->getApplicableRules($modelId, $calculatedValues); - - $unusedRules = $allRules->diff($applicableRules); - - return $unusedRules->map(function ($rule) { - return [ - 'id' => $rule->id, - 'name' => $rule->name, - 'condition' => $rule->condition_expression, - ]; - })->toArray(); - } - - /** - * 복잡한 공식 찾기 - */ - private function findComplexFormulas(int $modelId): array - { - $formulas = $this->formulaService->getFormulasByModel($modelId); - - return $formulas->filter(function ($formula) { - // 복잡성 기준 (의존성 수, 표현식 길이 등) - $dependencyCount = count($formula->dependencies ?? []); - $expressionLength = strlen($formula->expression); - - return $dependencyCount > 5 || $expressionLength > 100; - })->map(function ($formula) { - return [ - 'id' => $formula->id, - 'name' => $formula->name, - 'complexity_score' => strlen($formula->expression) + count($formula->dependencies ?? []) * 10, - ]; - })->toArray(); - } - - /** - * 중복 BOM 아이템 찾기 - */ - private function findDuplicateBomItems(array $bomItems): array - { - $seen = []; - $duplicates = []; - - foreach ($bomItems as $item) { - $key = ($item['product_id'] ?? 'null') . '_' . ($item['material_id'] ?? 'null'); - - if (isset($seen[$key])) { - $duplicates[] = [ - 'product_id' => $item['product_id'], - 'material_id' => $item['material_id'], - 'occurrences' => ++$seen[$key], - ]; - } else { - $seen[$key] = 1; - } - } - - return $duplicates; - } -} \ No newline at end of file diff --git a/app/Services/Design/BomConditionRuleService.php b/app/Services/Design/BomConditionRuleService.php deleted file mode 100644 index ffa4ed1..0000000 --- a/app/Services/Design/BomConditionRuleService.php +++ /dev/null @@ -1,492 +0,0 @@ -tenantId(); - - // 모델 존재 확인 - $model = DesignModel::where('tenant_id', $tenantId)->where('id', $modelId)->first(); - if (!$model) { - throw new NotFoundHttpException(__('error.not_found')); - } - - $query = BomConditionRule::query() - ->where('tenant_id', $tenantId) - ->where('model_id', $modelId); - - if ($q !== '') { - $query->where(function ($w) use ($q) { - $w->where('rule_name', 'like', "%{$q}%") - ->orWhere('condition_expression', 'like', "%{$q}%") - ->orWhere('description', 'like', "%{$q}%"); - }); - } - - return $query->orderBy('priority')->orderBy('id')->paginate($size, ['*'], 'page', $page); - } - - /** - * 조건 규칙 조회 - */ - public function show(int $ruleId): BomConditionRule - { - $tenantId = $this->tenantId(); - - $rule = BomConditionRule::where('tenant_id', $tenantId)->where('id', $ruleId)->first(); - if (!$rule) { - throw new NotFoundHttpException(__('error.not_found')); - } - - // 연관된 타겟 정보도 함께 조회 - $rule->load(['designModel']); - - return $rule; - } - - /** - * 조건 규칙 생성 - */ - public function create(array $data): BomConditionRule - { - $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); - - // 모델 존재 확인 - $model = DesignModel::where('tenant_id', $tenantId)->where('id', $data['model_id'])->first(); - if (!$model) { - throw new NotFoundHttpException(__('error.not_found')); - } - - // 타겟 존재 확인 - $this->validateTarget($data['target_type'], $data['target_id']); - - // 같은 모델 내에서 규칙명 중복 체크 - $exists = BomConditionRule::query() - ->where('tenant_id', $tenantId) - ->where('model_id', $data['model_id']) - ->where('rule_name', $data['rule_name']) - ->exists(); - - if ($exists) { - throw ValidationException::withMessages(['rule_name' => __('error.duplicate')]); - } - - // 조건식 문법 검증 - $this->validateConditionExpression($data['condition_expression']); - - return DB::transaction(function () use ($tenantId, $userId, $data) { - // priority가 없으면 자동 설정 - if (!isset($data['priority'])) { - $maxPriority = BomConditionRule::where('tenant_id', $tenantId) - ->where('model_id', $data['model_id']) - ->max('priority') ?? 0; - $data['priority'] = $maxPriority + 1; - } - - $payload = array_merge($data, [ - 'tenant_id' => $tenantId, - 'created_by' => $userId, - 'updated_by' => $userId, - ]); - - return BomConditionRule::create($payload); - }); - } - - /** - * 조건 규칙 수정 - */ - public function update(int $ruleId, array $data): BomConditionRule - { - $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); - - $rule = BomConditionRule::where('tenant_id', $tenantId)->where('id', $ruleId)->first(); - if (!$rule) { - throw new NotFoundHttpException(__('error.not_found')); - } - - // 규칙명 변경 시 중복 체크 - if (isset($data['rule_name']) && $data['rule_name'] !== $rule->rule_name) { - $exists = BomConditionRule::query() - ->where('tenant_id', $tenantId) - ->where('model_id', $rule->model_id) - ->where('rule_name', $data['rule_name']) - ->where('id', '!=', $ruleId) - ->exists(); - - if ($exists) { - throw ValidationException::withMessages(['rule_name' => __('error.duplicate')]); - } - } - - // 타겟 변경 시 존재 확인 - if (isset($data['target_type']) || isset($data['target_id'])) { - $targetType = $data['target_type'] ?? $rule->target_type; - $targetId = $data['target_id'] ?? $rule->target_id; - $this->validateTarget($targetType, $targetId); - } - - // 조건식 변경 시 문법 검증 - if (isset($data['condition_expression'])) { - $this->validateConditionExpression($data['condition_expression']); - } - - return DB::transaction(function () use ($rule, $userId, $data) { - $payload = array_merge($data, ['updated_by' => $userId]); - $rule->update($payload); - - return $rule->fresh(); - }); - } - - /** - * 조건 규칙 삭제 - */ - public function delete(int $ruleId): bool - { - $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); - - $rule = BomConditionRule::where('tenant_id', $tenantId)->where('id', $ruleId)->first(); - if (!$rule) { - throw new NotFoundHttpException(__('error.not_found')); - } - - return DB::transaction(function () use ($rule, $userId) { - $rule->update(['deleted_by' => $userId]); - return $rule->delete(); - }); - } - - /** - * 조건 규칙 활성화/비활성화 - */ - public function toggle(int $ruleId): BomConditionRule - { - $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); - - $rule = BomConditionRule::where('tenant_id', $tenantId)->where('id', $ruleId)->first(); - if (!$rule) { - throw new NotFoundHttpException(__('error.not_found')); - } - - return DB::transaction(function () use ($rule, $userId) { - $rule->update([ - 'is_active' => !$rule->is_active, - 'updated_by' => $userId, - ]); - - return $rule->fresh(); - }); - } - - /** - * 조건 규칙 우선순위 변경 - */ - public function reorder(int $modelId, array $ruleIds): array - { - $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); - - // 모델 존재 확인 - $model = DesignModel::where('tenant_id', $tenantId)->where('id', $modelId)->first(); - if (!$model) { - throw new NotFoundHttpException(__('error.not_found')); - } - - return DB::transaction(function () use ($tenantId, $userId, $modelId, $ruleIds) { - $priority = 1; - $updated = []; - - foreach ($ruleIds as $ruleId) { - $rule = BomConditionRule::query() - ->where('tenant_id', $tenantId) - ->where('model_id', $modelId) - ->where('id', $ruleId) - ->first(); - - if ($rule) { - $rule->update([ - 'priority' => $priority, - 'updated_by' => $userId, - ]); - $updated[] = $rule->fresh(); - $priority++; - } - } - - return $updated; - }); - } - - /** - * 조건 규칙 대량 저장 (upsert) - */ - public function bulkUpsert(int $modelId, array $rules): array - { - $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); - - // 모델 존재 확인 - $model = DesignModel::where('tenant_id', $tenantId)->where('id', $modelId)->first(); - if (!$model) { - throw new NotFoundHttpException(__('error.not_found')); - } - - return DB::transaction(function () use ($tenantId, $userId, $modelId, $rules) { - $result = []; - - foreach ($rules as $index => $ruleData) { - $ruleData['model_id'] = $modelId; - - // 타겟 및 조건식 검증 - if (isset($ruleData['target_type']) && isset($ruleData['target_id'])) { - $this->validateTarget($ruleData['target_type'], $ruleData['target_id']); - } - - if (isset($ruleData['condition_expression'])) { - $this->validateConditionExpression($ruleData['condition_expression']); - } - - // ID가 있으면 업데이트, 없으면 생성 - if (isset($ruleData['id']) && $ruleData['id']) { - $rule = BomConditionRule::query() - ->where('tenant_id', $tenantId) - ->where('model_id', $modelId) - ->where('id', $ruleData['id']) - ->first(); - - if ($rule) { - $rule->update(array_merge($ruleData, ['updated_by' => $userId])); - $result[] = $rule->fresh(); - } - } else { - // 새로운 규칙 생성 - $exists = BomConditionRule::query() - ->where('tenant_id', $tenantId) - ->where('model_id', $modelId) - ->where('rule_name', $ruleData['rule_name']) - ->exists(); - - if (!$exists) { - if (!isset($ruleData['priority'])) { - $ruleData['priority'] = $index + 1; - } - - $payload = array_merge($ruleData, [ - 'tenant_id' => $tenantId, - 'created_by' => $userId, - 'updated_by' => $userId, - ]); - - $result[] = BomConditionRule::create($payload); - } - } - } - - return $result; - }); - } - - /** - * 조건 규칙 평가 실행 - */ - public function evaluateRules(int $modelId, array $parameters): array - { - $tenantId = $this->tenantId(); - - // 활성 조건 규칙들을 우선순위 순으로 조회 - $rules = BomConditionRule::query() - ->where('tenant_id', $tenantId) - ->where('model_id', $modelId) - ->where('is_active', true) - ->orderBy('priority') - ->get(); - - $matchedRules = []; - $bomActions = []; - - foreach ($rules as $rule) { - try { - if ($rule->evaluateCondition($parameters)) { - $matchedRules[] = [ - 'rule_id' => $rule->id, - 'rule_name' => $rule->rule_name, - 'action_type' => $rule->action_type, - 'target_type' => $rule->target_type, - 'target_id' => $rule->target_id, - 'quantity_multiplier' => $rule->quantity_multiplier, - ]; - - $bomActions[] = [ - 'action_type' => $rule->action_type, - 'target_type' => $rule->target_type, - 'target_id' => $rule->target_id, - 'quantity_multiplier' => $rule->quantity_multiplier, - 'rule_name' => $rule->rule_name, - ]; - } - } catch (\Exception $e) { - // 조건 평가 실패 시 로그 남기고 건너뜀 - \Log::warning("Rule evaluation failed: {$rule->rule_name}", [ - 'error' => $e->getMessage(), - 'parameters' => $parameters, - ]); - } - } - - return [ - 'matched_rules' => $matchedRules, - 'bom_actions' => $bomActions, - ]; - } - - /** - * 조건식 테스트 - */ - public function testCondition(int $ruleId, array $parameters): array - { - $tenantId = $this->tenantId(); - - $rule = BomConditionRule::where('tenant_id', $tenantId)->where('id', $ruleId)->first(); - if (!$rule) { - throw new NotFoundHttpException(__('error.not_found')); - } - - try { - $result = $rule->evaluateCondition($parameters); - - return [ - 'rule_name' => $rule->rule_name, - 'condition_expression' => $rule->condition_expression, - 'parameters' => $parameters, - 'result' => $result, - 'action_type' => $rule->action_type, - 'target_type' => $rule->target_type, - 'target_id' => $rule->target_id, - ]; - } catch (\Exception $e) { - throw ValidationException::withMessages([ - 'condition_expression' => __('error.condition_evaluation_failed', ['error' => $e->getMessage()]) - ]); - } - } - - /** - * 타겟 유효성 검증 - */ - private function validateTarget(string $targetType, int $targetId): void - { - $tenantId = $this->tenantId(); - - switch ($targetType) { - case 'MATERIAL': - $exists = Material::where('tenant_id', $tenantId)->where('id', $targetId)->exists(); - if (!$exists) { - throw ValidationException::withMessages(['target_id' => __('error.material_not_found')]); - } - break; - - case 'PRODUCT': - $exists = Product::where('tenant_id', $tenantId)->where('id', $targetId)->exists(); - if (!$exists) { - throw ValidationException::withMessages(['target_id' => __('error.product_not_found')]); - } - break; - - default: - throw ValidationException::withMessages(['target_type' => __('error.invalid_target_type')]); - } - } - - /** - * 조건식 문법 검증 - */ - private function validateConditionExpression(string $expression): void - { - // 기본적인 문법 검증 - $expression = trim($expression); - - if (empty($expression)) { - throw ValidationException::withMessages(['condition_expression' => __('error.condition_expression_required')]); - } - - // 허용된 패턴들 검증 - $allowedPatterns = [ - '/^.+\s*(==|!=|>=|<=|>|<)\s*.+$/', // 비교 연산자 - '/^.+\s+IN\s+\(.+\)$/i', // IN 연산자 - '/^.+\s+NOT\s+IN\s+\(.+\)$/i', // NOT IN 연산자 - '/^(true|false|1|0)$/i', // 불린 값 - ]; - - $isValid = false; - foreach ($allowedPatterns as $pattern) { - if (preg_match($pattern, $expression)) { - $isValid = true; - break; - } - } - - if (!$isValid) { - throw ValidationException::withMessages([ - 'condition_expression' => __('error.invalid_condition_expression') - ]); - } - } - - /** - * 모델의 규칙 템플릿 조회 (자주 사용되는 패턴들) - */ - public function getRuleTemplates(): array - { - return [ - [ - 'name' => '크기별 브라켓 개수', - 'description' => '폭/높이에 따른 브라켓 개수 결정', - 'condition_example' => 'W1 > 1000', - 'action_type' => 'INCLUDE', - 'target_type' => 'MATERIAL', - ], - [ - 'name' => '스크린 타입별 자재', - 'description' => '스크린 종류에 따른 자재 선택', - 'condition_example' => "screen_type == 'STEEL'", - 'action_type' => 'INCLUDE', - 'target_type' => 'MATERIAL', - ], - [ - 'name' => '설치 방식별 부품', - 'description' => '설치 타입에 따른 추가 부품', - 'condition_example' => "install_type IN ('CEILING', 'WALL')", - 'action_type' => 'INCLUDE', - 'target_type' => 'PRODUCT', - ], - [ - 'name' => '면적별 수량 배수', - 'description' => '면적에 비례하는 자재 수량', - 'condition_example' => 'area > 10', - 'action_type' => 'MODIFY_QUANTITY', - 'target_type' => 'MATERIAL', - ], - ]; - } -} \ No newline at end of file diff --git a/app/Services/Design/BomResolverService.php b/app/Services/Design/BomResolverService.php deleted file mode 100644 index 9f3b1b5..0000000 --- a/app/Services/Design/BomResolverService.php +++ /dev/null @@ -1,471 +0,0 @@ -parameterService = $parameterService; - $this->formulaService = $formulaService; - $this->ruleService = $ruleService; - } - - /** - * 매개변수 기반 BOM 해석 (전체 프로세스) - */ - public function resolveBom(int $modelId, array $inputParameters, ?int $templateId = null): array - { - $tenantId = $this->tenantId(); - - // 1. 모델 존재 확인 - $model = DesignModel::where('tenant_id', $tenantId)->where('id', $modelId)->first(); - if (!$model) { - throw new NotFoundHttpException(__('error.model_not_found')); - } - - // 2. 매개변수 검증 및 기본값 적용 - $validatedParameters = $this->parameterService->validateParameters($modelId, $inputParameters); - - // 3. 공식 계산 실행 - $calculatedValues = $this->formulaService->calculateFormulas($modelId, $validatedParameters); - - // 4. 조건 규칙 평가 - $ruleResults = $this->ruleService->evaluateRules($modelId, $calculatedValues); - - // 5. 기본 BOM 템플릿 조회 (지정된 템플릿 또는 최신 버전) - $baseBom = $this->getBaseBomTemplate($modelId, $templateId); - - // 6. 조건 규칙 적용으로 BOM 변환 - $resolvedBom = $this->applyRulesToBom($baseBom, $ruleResults['bom_actions'], $calculatedValues); - - // 7. BOM 아이템 정보 보강 (재료/제품 세부정보) - $enrichedBom = $this->enrichBomItems($resolvedBom); - - return [ - 'model' => [ - 'id' => $model->id, - 'code' => $model->code, - 'name' => $model->name, - ], - 'input_parameters' => $validatedParameters, - 'calculated_values' => $calculatedValues, - 'matched_rules' => $ruleResults['matched_rules'], - 'base_bom_template_id' => $baseBom['template_id'] ?? null, - 'resolved_bom' => $enrichedBom, - 'summary' => $this->generateBomSummary($enrichedBom), - ]; - } - - /** - * BOM 해석 미리보기 (저장하지 않음) - */ - public function previewBom(int $modelId, array $inputParameters, ?int $templateId = null): array - { - return $this->resolveBom($modelId, $inputParameters, $templateId); - } - - /** - * 매개변수 변경에 따른 BOM 차이 분석 - */ - public function compareBomByParameters(int $modelId, array $parameters1, array $parameters2, ?int $templateId = null): array - { - // 두 매개변수 세트로 각각 BOM 해석 - $bom1 = $this->resolveBom($modelId, $parameters1, $templateId); - $bom2 = $this->resolveBom($modelId, $parameters2, $templateId); - - return [ - 'parameters_diff' => [ - 'set1' => $parameters1, - 'set2' => $parameters2, - 'changed' => array_diff_assoc($parameters2, $parameters1), - ], - 'calculated_values_diff' => [ - 'set1' => $bom1['calculated_values'], - 'set2' => $bom2['calculated_values'], - 'changed' => array_diff_assoc($bom2['calculated_values'], $bom1['calculated_values']), - ], - 'bom_diff' => $this->compareBomItems($bom1['resolved_bom'], $bom2['resolved_bom']), - 'summary_diff' => [ - 'set1' => $bom1['summary'], - 'set2' => $bom2['summary'], - ], - ]; - } - - /** - * 기본 BOM 템플릿 조회 - */ - private function getBaseBomTemplate(int $modelId, ?int $templateId = null): array - { - $tenantId = $this->tenantId(); - - if ($templateId) { - // 지정된 템플릿 사용 - $template = BomTemplate::where('tenant_id', $tenantId) - ->where('id', $templateId) - ->first(); - } else { - // 해당 모델의 최신 버전에서 BOM 템플릿 찾기 - $template = BomTemplate::query() - ->where('tenant_id', $tenantId) - ->whereHas('modelVersion', function ($q) use ($modelId) { - $q->where('model_id', $modelId) - ->where('status', 'RELEASED'); - }) - ->orderByDesc('created_at') - ->first(); - } - - if (!$template) { - // 기본 템플릿이 없으면 빈 BOM 반환 - return [ - 'template_id' => null, - 'items' => [], - ]; - } - - $items = BomTemplateItem::query() - ->where('bom_template_id', $template->id) - ->orderBy('order') - ->get() - ->map(function ($item) { - return [ - 'target_type' => $item->ref_type, - 'target_id' => $item->ref_id, - 'quantity' => $item->quantity, - 'waste_rate' => $item->waste_rate, - 'reason' => 'base_template', - ]; - }) - ->toArray(); - - return [ - 'template_id' => $template->id, - 'items' => $items, - ]; - } - - /** - * 조건 규칙을 BOM에 적용 - */ - private function applyRulesToBom(array $baseBom, array $bomActions, array $calculatedValues): array - { - $currentBom = $baseBom['items']; - - foreach ($bomActions as $action) { - switch ($action['action_type']) { - case 'INCLUDE': - // 새 아이템 추가 (중복 체크) - $exists = collect($currentBom)->contains(function ($item) use ($action) { - return $item['target_type'] === $action['target_type'] && - $item['target_id'] === $action['target_id']; - }); - - if (!$exists) { - $currentBom[] = [ - 'target_type' => $action['target_type'], - 'target_id' => $action['target_id'], - 'quantity' => $action['quantity_multiplier'] ?? 1, - 'waste_rate' => 0, - 'reason' => $action['rule_name'], - ]; - } - break; - - case 'EXCLUDE': - // 아이템 제외 - $currentBom = array_filter($currentBom, function ($item) use ($action) { - return !($item['target_type'] === $action['target_type'] && - $item['target_id'] === $action['target_id']); - }); - break; - - case 'MODIFY_QUANTITY': - // 수량 변경 - foreach ($currentBom as &$item) { - if ($item['target_type'] === $action['target_type'] && - $item['target_id'] === $action['target_id']) { - - $multiplier = $action['quantity_multiplier'] ?? 1; - - // 공식으로 계산된 값이 있으면 그것을 사용 - if (isset($calculatedValues['quantity_' . $action['target_id']])) { - $item['quantity'] = $calculatedValues['quantity_' . $action['target_id']]; - } else { - $item['quantity'] = $item['quantity'] * $multiplier; - } - - $item['reason'] = $action['rule_name']; - } - } - break; - } - } - - return array_values($currentBom); // 인덱스 재정렬 - } - - /** - * BOM 아이템 정보 보강 - */ - private function enrichBomItems(array $bomItems): array - { - $tenantId = $this->tenantId(); - $enriched = []; - - foreach ($bomItems as $item) { - $enrichedItem = $item; - - if ($item['target_type'] === 'MATERIAL') { - $material = Material::where('tenant_id', $tenantId) - ->where('id', $item['target_id']) - ->first(); - - if ($material) { - $enrichedItem['target_info'] = [ - 'id' => $material->id, - 'code' => $material->code, - 'name' => $material->name, - 'unit' => $material->unit, - 'type' => 'material', - ]; - } - } elseif ($item['target_type'] === 'PRODUCT') { - $product = Product::where('tenant_id', $tenantId) - ->where('id', $item['target_id']) - ->first(); - - if ($product) { - $enrichedItem['target_info'] = [ - 'id' => $product->id, - 'code' => $product->code, - 'name' => $product->name, - 'unit' => $product->unit, - 'type' => 'product', - ]; - } - } - - // 실제 필요 수량 계산 (폐기율 적용) - $baseQuantity = $enrichedItem['quantity']; - $wasteRate = $enrichedItem['waste_rate'] ?? 0; - $enrichedItem['actual_quantity'] = $baseQuantity * (1 + $wasteRate / 100); - - $enriched[] = $enrichedItem; - } - - return $enriched; - } - - /** - * BOM 요약 정보 생성 - */ - private function generateBomSummary(array $bomItems): array - { - $totalItems = count($bomItems); - $materialCount = 0; - $productCount = 0; - $totalValue = 0; // 나중에 가격 정보 추가 시 사용 - - foreach ($bomItems as $item) { - if ($item['target_type'] === 'MATERIAL') { - $materialCount++; - } elseif ($item['target_type'] === 'PRODUCT') { - $productCount++; - } - } - - return [ - 'total_items' => $totalItems, - 'material_count' => $materialCount, - 'product_count' => $productCount, - 'total_estimated_value' => $totalValue, - 'generated_at' => now()->toISOString(), - ]; - } - - /** - * BOM 아이템 비교 - */ - private function compareBomItems(array $bom1, array $bom2): array - { - $added = []; - $removed = []; - $modified = []; - - // BOM1에 있던 아이템들 체크 - foreach ($bom1 as $item1) { - $key = $item1['target_type'] . '_' . $item1['target_id']; - $found = false; - - foreach ($bom2 as $item2) { - if ($item2['target_type'] === $item1['target_type'] && - $item2['target_id'] === $item1['target_id']) { - $found = true; - - // 수량이 변경되었는지 체크 - if ($item1['quantity'] != $item2['quantity']) { - $modified[] = [ - 'target_type' => $item1['target_type'], - 'target_id' => $item1['target_id'], - 'target_info' => $item1['target_info'] ?? null, - 'old_quantity' => $item1['quantity'], - 'new_quantity' => $item2['quantity'], - 'change' => $item2['quantity'] - $item1['quantity'], - ]; - } - break; - } - } - - if (!$found) { - $removed[] = $item1; - } - } - - // BOM2에 새로 추가된 아이템들 - foreach ($bom2 as $item2) { - $found = false; - - foreach ($bom1 as $item1) { - if ($item1['target_type'] === $item2['target_type'] && - $item1['target_id'] === $item2['target_id']) { - $found = true; - break; - } - } - - if (!$found) { - $added[] = $item2; - } - } - - return [ - 'added' => $added, - 'removed' => $removed, - 'modified' => $modified, - 'summary' => [ - 'added_count' => count($added), - 'removed_count' => count($removed), - 'modified_count' => count($modified), - ], - ]; - } - - /** - * BOM 해석 결과 저장 (향후 주문/견적 연계용) - */ - public function saveBomResolution(int $modelId, array $inputParameters, array $bomResolution, string $purpose = 'ESTIMATION'): array - { - $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); - - return DB::transaction(function () use ($tenantId, $userId, $modelId, $inputParameters, $bomResolution, $purpose) { - // BOM 해석 결과를 데이터베이스에 저장 - // 향후 order_items, quotation_items 등과 연계할 수 있도록 구조 준비 - - $resolutionRecord = [ - 'tenant_id' => $tenantId, - 'model_id' => $modelId, - 'input_parameters' => json_encode($inputParameters), - 'calculated_values' => json_encode($bomResolution['calculated_values']), - 'resolved_bom' => json_encode($bomResolution['resolved_bom']), - 'matched_rules' => json_encode($bomResolution['matched_rules']), - 'summary' => json_encode($bomResolution['summary']), - 'purpose' => $purpose, - 'created_by' => $userId, - 'created_at' => now(), - ]; - - // 실제 테이블이 있다면 저장, 없으면 파일이나 캐시에 임시 저장 - $resolutionId = md5(json_encode($resolutionRecord)); - - // 임시로 캐시에 저장 (1시간) - cache()->put("bom_resolution_{$resolutionId}", $resolutionRecord, 3600); - - return [ - 'resolution_id' => $resolutionId, - 'saved_at' => now()->toISOString(), - 'purpose' => $purpose, - ]; - }); - } - - /** - * 저장된 BOM 해석 결과 조회 - */ - public function getBomResolution(string $resolutionId): ?array - { - return cache()->get("bom_resolution_{$resolutionId}"); - } - - /** - * KSS01 시나리오 테스트용 빠른 실행 - */ - public function resolveKSS01(array $parameters): array - { - // KSS01 모델이 있다고 가정하고 하드코딩된 로직 - $defaults = [ - 'W0' => 800, - 'H0' => 600, - 'screen_type' => 'FABRIC', - 'install_type' => 'WALL', - ]; - - $params = array_merge($defaults, $parameters); - - // 공식 계산 시뮬레이션 - $calculated = [ - 'W1' => $params['W0'] + 100, - 'H1' => $params['H0'] + 100, - ]; - $calculated['area'] = ($calculated['W1'] * $calculated['H1']) / 1000000; - - // 조건 규칙 시뮬레이션 - $bom = []; - - // 스크린 타입에 따른 자재 - if ($params['screen_type'] === 'FABRIC') { - $bom[] = ['target_type' => 'MATERIAL', 'target_id' => 1, 'quantity' => $calculated['area'], 'target_info' => ['name' => '패브릭 스크린']]; - } else { - $bom[] = ['target_type' => 'MATERIAL', 'target_id' => 2, 'quantity' => $calculated['area'], 'target_info' => ['name' => '스틸 스크린']]; - } - - // 브라켓 개수 (폭에 따라) - $bracketCount = $calculated['W1'] > 1000 ? 3 : 2; - $bom[] = ['target_type' => 'PRODUCT', 'target_id' => 10, 'quantity' => $bracketCount, 'target_info' => ['name' => '브라켓']]; - - // 가이드레일 - $railLength = ($calculated['W1'] + $calculated['H1']) * 2 / 1000; // m 단위 - $bom[] = ['target_type' => 'MATERIAL', 'target_id' => 3, 'quantity' => $railLength, 'target_info' => ['name' => '가이드레일']]; - - return [ - 'model' => ['code' => 'KSS01', 'name' => '기본 스크린 시스템'], - 'input_parameters' => $params, - 'calculated_values' => $calculated, - 'resolved_bom' => $bom, - 'summary' => ['total_items' => count($bom)], - ]; - } -} \ No newline at end of file diff --git a/app/Services/Design/ModelFormulaService.php b/app/Services/Design/ModelFormulaService.php deleted file mode 100644 index 8c9ca14..0000000 --- a/app/Services/Design/ModelFormulaService.php +++ /dev/null @@ -1,461 +0,0 @@ -tenantId(); - - // 모델 존재 확인 - $model = DesignModel::where('tenant_id', $tenantId)->where('id', $modelId)->first(); - if (!$model) { - throw new NotFoundHttpException(__('error.not_found')); - } - - $query = ModelFormula::query() - ->where('tenant_id', $tenantId) - ->where('model_id', $modelId); - - if ($q !== '') { - $query->where(function ($w) use ($q) { - $w->where('formula_name', 'like', "%{$q}%") - ->orWhere('formula_expression', 'like', "%{$q}%") - ->orWhere('description', 'like', "%{$q}%"); - }); - } - - return $query->orderBy('calculation_order')->orderBy('id')->paginate($size, ['*'], 'page', $page); - } - - /** - * 공식 조회 - */ - public function show(int $formulaId): ModelFormula - { - $tenantId = $this->tenantId(); - - $formula = ModelFormula::where('tenant_id', $tenantId)->where('id', $formulaId)->first(); - if (!$formula) { - throw new NotFoundHttpException(__('error.not_found')); - } - - return $formula; - } - - /** - * 공식 생성 - */ - public function create(array $data): ModelFormula - { - $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); - - // 모델 존재 확인 - $model = DesignModel::where('tenant_id', $tenantId)->where('id', $data['model_id'])->first(); - if (!$model) { - throw new NotFoundHttpException(__('error.not_found')); - } - - // 같은 모델 내에서 공식명 중복 체크 - $exists = ModelFormula::query() - ->where('tenant_id', $tenantId) - ->where('model_id', $data['model_id']) - ->where('formula_name', $data['formula_name']) - ->exists(); - - if ($exists) { - throw ValidationException::withMessages(['formula_name' => __('error.duplicate')]); - } - - return DB::transaction(function () use ($tenantId, $userId, $data) { - // calculation_order가 없으면 자동 설정 - if (!isset($data['calculation_order'])) { - $maxOrder = ModelFormula::where('tenant_id', $tenantId) - ->where('model_id', $data['model_id']) - ->max('calculation_order') ?? 0; - $data['calculation_order'] = $maxOrder + 1; - } - - // 공식에서 변수 추출 및 의존성 설정 - $tempFormula = new ModelFormula(['formula_expression' => $data['formula_expression']]); - $variables = $tempFormula->extractVariables(); - $data['dependencies'] = $variables; - - // 의존성 순환 체크 - $this->validateNoDependencyLoop($data['model_id'], $data['formula_name'], $variables); - - $payload = array_merge($data, [ - 'tenant_id' => $tenantId, - 'created_by' => $userId, - 'updated_by' => $userId, - ]); - - return ModelFormula::create($payload); - }); - } - - /** - * 공식 수정 - */ - public function update(int $formulaId, array $data): ModelFormula - { - $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); - - $formula = ModelFormula::where('tenant_id', $tenantId)->where('id', $formulaId)->first(); - if (!$formula) { - throw new NotFoundHttpException(__('error.not_found')); - } - - // 공식명 변경 시 중복 체크 - if (isset($data['formula_name']) && $data['formula_name'] !== $formula->formula_name) { - $exists = ModelFormula::query() - ->where('tenant_id', $tenantId) - ->where('model_id', $formula->model_id) - ->where('formula_name', $data['formula_name']) - ->where('id', '!=', $formulaId) - ->exists(); - - if ($exists) { - throw ValidationException::withMessages(['formula_name' => __('error.duplicate')]); - } - } - - return DB::transaction(function () use ($formula, $userId, $data) { - // 공식 표현식이 변경되면 의존성 재계산 - if (isset($data['formula_expression'])) { - $tempFormula = new ModelFormula(['formula_expression' => $data['formula_expression']]); - $variables = $tempFormula->extractVariables(); - $data['dependencies'] = $variables; - - // 의존성 순환 체크 (자기 자신 제외) - $formulaName = $data['formula_name'] ?? $formula->formula_name; - $this->validateNoDependencyLoop($formula->model_id, $formulaName, $variables, $formula->id); - } - - $payload = array_merge($data, ['updated_by' => $userId]); - $formula->update($payload); - - return $formula->fresh(); - }); - } - - /** - * 공식 삭제 - */ - public function delete(int $formulaId): bool - { - $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); - - $formula = ModelFormula::where('tenant_id', $tenantId)->where('id', $formulaId)->first(); - if (!$formula) { - throw new NotFoundHttpException(__('error.not_found')); - } - - // 다른 공식에서 이 공식을 의존하는지 체크 - $dependentFormulas = ModelFormula::query() - ->where('tenant_id', $tenantId) - ->where('model_id', $formula->model_id) - ->where('id', '!=', $formulaId) - ->get() - ->filter(function ($f) use ($formula) { - return in_array($formula->formula_name, $f->dependencies ?? []); - }); - - if ($dependentFormulas->isNotEmpty()) { - $dependentNames = $dependentFormulas->pluck('formula_name')->implode(', '); - throw ValidationException::withMessages([ - 'formula_name' => __('error.formula_in_use', ['formulas' => $dependentNames]) - ]); - } - - return DB::transaction(function () use ($formula, $userId) { - $formula->update(['deleted_by' => $userId]); - return $formula->delete(); - }); - } - - /** - * 공식 계산 순서 변경 - */ - public function reorder(int $modelId, array $formulaIds): array - { - $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); - - // 모델 존재 확인 - $model = DesignModel::where('tenant_id', $tenantId)->where('id', $modelId)->first(); - if (!$model) { - throw new NotFoundHttpException(__('error.not_found')); - } - - return DB::transaction(function () use ($tenantId, $userId, $modelId, $formulaIds) { - $order = 1; - $updated = []; - - foreach ($formulaIds as $formulaId) { - $formula = ModelFormula::query() - ->where('tenant_id', $tenantId) - ->where('model_id', $modelId) - ->where('id', $formulaId) - ->first(); - - if ($formula) { - $formula->update([ - 'calculation_order' => $order, - 'updated_by' => $userId, - ]); - $updated[] = $formula->fresh(); - $order++; - } - } - - return $updated; - }); - } - - /** - * 공식 대량 저장 (upsert) - */ - public function bulkUpsert(int $modelId, array $formulas): array - { - $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); - - // 모델 존재 확인 - $model = DesignModel::where('tenant_id', $tenantId)->where('id', $modelId)->first(); - if (!$model) { - throw new NotFoundHttpException(__('error.not_found')); - } - - return DB::transaction(function () use ($tenantId, $userId, $modelId, $formulas) { - $result = []; - - foreach ($formulas as $index => $formulaData) { - $formulaData['model_id'] = $modelId; - - // 공식에서 의존성 추출 - if (isset($formulaData['formula_expression'])) { - $tempFormula = new ModelFormula(['formula_expression' => $formulaData['formula_expression']]); - $formulaData['dependencies'] = $tempFormula->extractVariables(); - } - - // ID가 있으면 업데이트, 없으면 생성 - if (isset($formulaData['id']) && $formulaData['id']) { - $formula = ModelFormula::query() - ->where('tenant_id', $tenantId) - ->where('model_id', $modelId) - ->where('id', $formulaData['id']) - ->first(); - - if ($formula) { - $formula->update(array_merge($formulaData, ['updated_by' => $userId])); - $result[] = $formula->fresh(); - } - } else { - // 새로운 공식 생성 - $exists = ModelFormula::query() - ->where('tenant_id', $tenantId) - ->where('model_id', $modelId) - ->where('formula_name', $formulaData['formula_name']) - ->exists(); - - if (!$exists) { - if (!isset($formulaData['calculation_order'])) { - $formulaData['calculation_order'] = $index + 1; - } - - $payload = array_merge($formulaData, [ - 'tenant_id' => $tenantId, - 'created_by' => $userId, - 'updated_by' => $userId, - ]); - - $result[] = ModelFormula::create($payload); - } - } - } - - return $result; - }); - } - - /** - * 공식 계산 실행 - */ - public function calculateFormulas(int $modelId, array $inputValues): array - { - $tenantId = $this->tenantId(); - - // 모델의 모든 공식을 계산 순서대로 조회 - $formulas = ModelFormula::query() - ->where('tenant_id', $tenantId) - ->where('model_id', $modelId) - ->orderBy('calculation_order') - ->get(); - - $results = $inputValues; // 입력값을 결과에 포함 - $errors = []; - - foreach ($formulas as $formula) { - try { - // 의존하는 변수들이 모두 준비되었는지 확인 - $dependencies = $formula->dependencies ?? []; - $hasAllDependencies = true; - - foreach ($dependencies as $dependency) { - if (!array_key_exists($dependency, $results)) { - $hasAllDependencies = false; - break; - } - } - - if (!$hasAllDependencies) { - $errors[$formula->formula_name] = __('error.missing_dependencies'); - continue; - } - - // 공식 계산 실행 - $calculatedValue = $formula->calculate($results); - $results[$formula->formula_name] = $calculatedValue; - - } catch (\Exception $e) { - $errors[$formula->formula_name] = $e->getMessage(); - } - } - - if (!empty($errors)) { - throw ValidationException::withMessages($errors); - } - - return $results; - } - - /** - * 의존성 순환 검증 - */ - private function validateNoDependencyLoop(int $modelId, string $formulaName, array $dependencies, ?int $excludeFormulaId = null): void - { - $tenantId = $this->tenantId(); - - // 해당 모델의 모든 공식 조회 (수정 중인 공식 제외) - $query = ModelFormula::query() - ->where('tenant_id', $tenantId) - ->where('model_id', $modelId); - - if ($excludeFormulaId) { - $query->where('id', '!=', $excludeFormulaId); - } - - $allFormulas = $query->get()->toArray(); - - // 새로운 공식을 임시로 추가 - $allFormulas[] = [ - 'formula_name' => $formulaName, - 'dependencies' => $dependencies, - ]; - - // 각 의존성에 대해 순환 검사 - foreach ($dependencies as $dependency) { - if ($this->hasCircularDependency($formulaName, $dependency, $allFormulas, [])) { - throw ValidationException::withMessages([ - 'formula_expression' => __('error.circular_dependency', ['dependency' => $dependency]) - ]); - } - } - } - - /** - * 순환 의존성 검사 (DFS) - */ - private function hasCircularDependency(string $startFormula, string $currentFormula, array $allFormulas, array $visited): bool - { - if ($currentFormula === $startFormula) { - return true; // 순환 발견 - } - - if (in_array($currentFormula, $visited)) { - return false; // 이미 방문한 노드 - } - - $visited[] = $currentFormula; - - // 현재 공식의 의존성들을 확인 - $currentFormulaData = collect($allFormulas)->firstWhere('formula_name', $currentFormula); - if (!$currentFormulaData || empty($currentFormulaData['dependencies'])) { - return false; - } - - foreach ($currentFormulaData['dependencies'] as $dependency) { - if ($this->hasCircularDependency($startFormula, $dependency, $allFormulas, $visited)) { - return true; - } - } - - return false; - } - - /** - * 모델의 공식 의존성 그래프 조회 - */ - public function getDependencyGraph(int $modelId): array - { - $tenantId = $this->tenantId(); - - // 모델 존재 확인 - $model = DesignModel::where('tenant_id', $tenantId)->where('id', $modelId)->first(); - if (!$model) { - throw new NotFoundHttpException(__('error.not_found')); - } - - $formulas = ModelFormula::query() - ->where('tenant_id', $tenantId) - ->where('model_id', $modelId) - ->orderBy('calculation_order') - ->get(); - - $graph = [ - 'nodes' => [], - 'edges' => [], - ]; - - // 노드 생성 (공식들) - foreach ($formulas as $formula) { - $graph['nodes'][] = [ - 'id' => $formula->formula_name, - 'label' => $formula->formula_name, - 'expression' => $formula->formula_expression, - 'order' => $formula->calculation_order, - ]; - } - - // 엣지 생성 (의존성들) - foreach ($formulas as $formula) { - if (!empty($formula->dependencies)) { - foreach ($formula->dependencies as $dependency) { - $graph['edges'][] = [ - 'from' => $dependency, - 'to' => $formula->formula_name, - ]; - } - } - } - - return $graph; - } -} \ No newline at end of file diff --git a/app/Services/Design/ModelParameterService.php b/app/Services/Design/ModelParameterService.php deleted file mode 100644 index dd2f917..0000000 --- a/app/Services/Design/ModelParameterService.php +++ /dev/null @@ -1,344 +0,0 @@ -tenantId(); - - // 모델 존재 확인 - $model = DesignModel::where('tenant_id', $tenantId)->where('id', $modelId)->first(); - if (!$model) { - throw new NotFoundHttpException(__('error.not_found')); - } - - $query = ModelParameter::query() - ->where('tenant_id', $tenantId) - ->where('model_id', $modelId); - - if ($q !== '') { - $query->where(function ($w) use ($q) { - $w->where('parameter_name', 'like', "%{$q}%") - ->orWhere('description', 'like', "%{$q}%"); - }); - } - - return $query->orderBy('sort_order')->orderBy('id')->paginate($size, ['*'], 'page', $page); - } - - /** - * 매개변수 조회 - */ - public function show(int $parameterId): ModelParameter - { - $tenantId = $this->tenantId(); - - $parameter = ModelParameter::where('tenant_id', $tenantId)->where('id', $parameterId)->first(); - if (!$parameter) { - throw new NotFoundHttpException(__('error.not_found')); - } - - return $parameter; - } - - /** - * 매개변수 생성 - */ - public function create(array $data): ModelParameter - { - $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); - - // 모델 존재 확인 - $model = DesignModel::where('tenant_id', $tenantId)->where('id', $data['model_id'])->first(); - if (!$model) { - throw new NotFoundHttpException(__('error.not_found')); - } - - // 같은 모델 내에서 매개변수명 중복 체크 - $exists = ModelParameter::query() - ->where('tenant_id', $tenantId) - ->where('model_id', $data['model_id']) - ->where('parameter_name', $data['parameter_name']) - ->exists(); - - if ($exists) { - throw ValidationException::withMessages(['parameter_name' => __('error.duplicate')]); - } - - return DB::transaction(function () use ($tenantId, $userId, $data) { - // sort_order가 없으면 자동 설정 - if (!isset($data['sort_order'])) { - $maxOrder = ModelParameter::where('tenant_id', $tenantId) - ->where('model_id', $data['model_id']) - ->max('sort_order') ?? 0; - $data['sort_order'] = $maxOrder + 1; - } - - $payload = array_merge($data, [ - 'tenant_id' => $tenantId, - 'created_by' => $userId, - 'updated_by' => $userId, - ]); - - return ModelParameter::create($payload); - }); - } - - /** - * 매개변수 수정 - */ - public function update(int $parameterId, array $data): ModelParameter - { - $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); - - $parameter = ModelParameter::where('tenant_id', $tenantId)->where('id', $parameterId)->first(); - if (!$parameter) { - throw new NotFoundHttpException(__('error.not_found')); - } - - // 매개변수명 변경 시 중복 체크 - if (isset($data['parameter_name']) && $data['parameter_name'] !== $parameter->parameter_name) { - $exists = ModelParameter::query() - ->where('tenant_id', $tenantId) - ->where('model_id', $parameter->model_id) - ->where('parameter_name', $data['parameter_name']) - ->where('id', '!=', $parameterId) - ->exists(); - - if ($exists) { - throw ValidationException::withMessages(['parameter_name' => __('error.duplicate')]); - } - } - - return DB::transaction(function () use ($parameter, $userId, $data) { - $payload = array_merge($data, ['updated_by' => $userId]); - $parameter->update($payload); - - return $parameter->fresh(); - }); - } - - /** - * 매개변수 삭제 - */ - public function delete(int $parameterId): bool - { - $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); - - $parameter = ModelParameter::where('tenant_id', $tenantId)->where('id', $parameterId)->first(); - if (!$parameter) { - throw new NotFoundHttpException(__('error.not_found')); - } - - return DB::transaction(function () use ($parameter, $userId) { - $parameter->update(['deleted_by' => $userId]); - return $parameter->delete(); - }); - } - - /** - * 매개변수 순서 변경 - */ - public function reorder(int $modelId, array $parameterIds): array - { - $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); - - // 모델 존재 확인 - $model = DesignModel::where('tenant_id', $tenantId)->where('id', $modelId)->first(); - if (!$model) { - throw new NotFoundHttpException(__('error.not_found')); - } - - return DB::transaction(function () use ($tenantId, $userId, $modelId, $parameterIds) { - $order = 1; - $updated = []; - - foreach ($parameterIds as $parameterId) { - $parameter = ModelParameter::query() - ->where('tenant_id', $tenantId) - ->where('model_id', $modelId) - ->where('id', $parameterId) - ->first(); - - if ($parameter) { - $parameter->update([ - 'sort_order' => $order, - 'updated_by' => $userId, - ]); - $updated[] = $parameter->fresh(); - $order++; - } - } - - return $updated; - }); - } - - /** - * 매개변수 대량 저장 (upsert) - */ - public function bulkUpsert(int $modelId, array $parameters): array - { - $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); - - // 모델 존재 확인 - $model = DesignModel::where('tenant_id', $tenantId)->where('id', $modelId)->first(); - if (!$model) { - throw new NotFoundHttpException(__('error.not_found')); - } - - return DB::transaction(function () use ($tenantId, $userId, $modelId, $parameters) { - $result = []; - - foreach ($parameters as $index => $paramData) { - $paramData['model_id'] = $modelId; - - // ID가 있으면 업데이트, 없으면 생성 - if (isset($paramData['id']) && $paramData['id']) { - $parameter = ModelParameter::query() - ->where('tenant_id', $tenantId) - ->where('model_id', $modelId) - ->where('id', $paramData['id']) - ->first(); - - if ($parameter) { - $parameter->update(array_merge($paramData, ['updated_by' => $userId])); - $result[] = $parameter->fresh(); - } - } else { - // 새로운 매개변수 생성 - $exists = ModelParameter::query() - ->where('tenant_id', $tenantId) - ->where('model_id', $modelId) - ->where('parameter_name', $paramData['parameter_name']) - ->exists(); - - if (!$exists) { - if (!isset($paramData['sort_order'])) { - $paramData['sort_order'] = $index + 1; - } - - $payload = array_merge($paramData, [ - 'tenant_id' => $tenantId, - 'created_by' => $userId, - 'updated_by' => $userId, - ]); - - $result[] = ModelParameter::create($payload); - } - } - } - - return $result; - }); - } - - /** - * 매개변수 값 검증 - */ - public function validateParameters(int $modelId, array $values): array - { - $tenantId = $this->tenantId(); - - // 모델의 모든 매개변수 조회 - $parameters = ModelParameter::query() - ->where('tenant_id', $tenantId) - ->where('model_id', $modelId) - ->orderBy('sort_order') - ->get(); - - $errors = []; - $validated = []; - - foreach ($parameters as $parameter) { - $paramName = $parameter->parameter_name; - $value = $values[$paramName] ?? null; - - // 필수 매개변수 체크 - if ($parameter->is_required && ($value === null || $value === '')) { - $errors[$paramName] = __('error.required'); - continue; - } - - // 값이 없고 필수가 아니면 기본값 사용 - if ($value === null || $value === '') { - $validated[$paramName] = $parameter->default_value; - continue; - } - - // 타입별 검증 - if (!$parameter->validateValue($value)) { - $errors[$paramName] = __('error.invalid_value'); - continue; - } - - // 형변환 후 저장 - $validated[$paramName] = $parameter->castValue($value); - } - - if (!empty($errors)) { - throw ValidationException::withMessages($errors); - } - - return $validated; - } - - /** - * 모델의 매개변수 스키마 조회 (API용) - */ - public function getParameterSchema(int $modelId): array - { - $tenantId = $this->tenantId(); - - // 모델 존재 확인 - $model = DesignModel::where('tenant_id', $tenantId)->where('id', $modelId)->first(); - if (!$model) { - throw new NotFoundHttpException(__('error.not_found')); - } - - $parameters = ModelParameter::query() - ->where('tenant_id', $tenantId) - ->where('model_id', $modelId) - ->orderBy('sort_order') - ->get(); - - return [ - 'model' => [ - 'id' => $model->id, - 'code' => $model->code, - 'name' => $model->name, - ], - 'parameters' => $parameters->map(function ($param) { - return [ - 'name' => $param->parameter_name, - 'type' => $param->parameter_type, - 'required' => $param->is_required, - 'default' => $param->default_value, - 'min' => $param->min_value, - 'max' => $param->max_value, - 'unit' => $param->unit, - 'options' => $param->options, - 'description' => $param->description, - ]; - })->toArray(), - ]; - } -} \ No newline at end of file diff --git a/app/Services/Design/ProductFromModelService.php b/app/Services/Design/ProductFromModelService.php deleted file mode 100644 index 6a24ee8..0000000 --- a/app/Services/Design/ProductFromModelService.php +++ /dev/null @@ -1,422 +0,0 @@ -bomResolverService = $bomResolverService; - $this->productService = $productService; - } - - /** - * 모델과 매개변수로부터 제품 생성 - */ - public function createProductFromModel( - int $modelId, - array $parameters, - array $productData, - bool $includeComponents = true - ): Product { - $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); - - // 모델 존재 확인 - $model = DesignModel::where('tenant_id', $tenantId)->where('id', $modelId)->first(); - if (!$model) { - throw new NotFoundHttpException(__('error.model_not_found')); - } - - // BOM 해석 실행 - $bomResolution = $this->bomResolverService->resolveBom($modelId, $parameters); - - return DB::transaction(function () use ( - $tenantId, - $userId, - $model, - $parameters, - $productData, - $bomResolution, - $includeComponents - ) { - // 제품 기본 정보 설정 - $productPayload = array_merge([ - 'tenant_id' => $tenantId, - 'created_by' => $userId, - 'updated_by' => $userId, - 'product_type' => 'PRODUCT', - 'is_active' => true, - ], $productData); - - // 제품명에 모델 정보 포함 (명시적으로 지정되지 않은 경우) - if (!isset($productData['name'])) { - $productPayload['name'] = $model->name . ' (' . $this->formatParametersForName($parameters) . ')'; - } - - // 제품 코드 자동 생성 (명시적으로 지정되지 않은 경우) - if (!isset($productData['code'])) { - $productPayload['code'] = $this->generateProductCode($model, $parameters); - } - - // 제품 생성 - $product = Product::create($productPayload); - - // BOM 구성요소를 ProductComponent로 생성 - if ($includeComponents && !empty($bomResolution['resolved_bom'])) { - $this->createProductComponents($product, $bomResolution['resolved_bom']); - } - - // 모델 기반 제품임을 표시하는 메타 정보 저장 - $this->saveModelMetadata($product, $model, $parameters, $bomResolution); - - return $product->load('components'); - }); - } - - /** - * 기존 제품의 BOM 업데이트 - */ - public function updateProductBom(int $productId, array $newParameters): Product - { - $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); - - $product = Product::where('tenant_id', $tenantId)->where('id', $productId)->first(); - if (!$product) { - throw new NotFoundHttpException(__('error.product_not_found')); - } - - // 제품이 모델 기반으로 생성된 것인지 확인 - $metadata = $this->getModelMetadata($product); - if (!$metadata) { - throw ValidationException::withMessages([ - 'product_id' => __('error.product_not_model_based') - ]); - } - - $modelId = $metadata['model_id']; - - // 새로운 매개변수로 BOM 해석 - $bomResolution = $this->bomResolverService->resolveBom($modelId, $newParameters); - - return DB::transaction(function () use ($product, $userId, $newParameters, $bomResolution, $metadata) { - // 기존 ProductComponent 삭제 - ProductComponent::where('product_id', $product->id)->delete(); - - // 새로운 BOM으로 ProductComponent 생성 - $this->createProductComponents($product, $bomResolution['resolved_bom']); - - // 메타데이터 업데이트 - $this->updateModelMetadata($product, $newParameters, $bomResolution); - - $product->update(['updated_by' => $userId]); - - return $product->fresh()->load('components'); - }); - } - - /** - * 모델 기반 제품 복사 (매개변수 변경) - */ - public function cloneProductWithParameters( - int $sourceProductId, - array $newParameters, - array $productData = [] - ): Product { - $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); - - $sourceProduct = Product::where('tenant_id', $tenantId)->where('id', $sourceProductId)->first(); - if (!$sourceProduct) { - throw new NotFoundHttpException(__('error.product_not_found')); - } - - // 원본 제품의 모델 메타데이터 조회 - $metadata = $this->getModelMetadata($sourceProduct); - if (!$metadata) { - throw ValidationException::withMessages([ - 'product_id' => __('error.product_not_model_based') - ]); - } - - // 원본 제품 정보를 기반으로 새 제품 데이터 구성 - $newProductData = array_merge([ - 'name' => $sourceProduct->name . ' (복사본)', - 'description' => $sourceProduct->description, - 'category_id' => $sourceProduct->category_id, - 'unit' => $sourceProduct->unit, - 'product_type' => $sourceProduct->product_type, - ], $productData); - - return $this->createProductFromModel( - $metadata['model_id'], - $newParameters, - $newProductData, - true - ); - } - - /** - * 모델 매개변수 변경에 따른 제품들 일괄 업데이트 - */ - public function updateProductsByModel(int $modelId, array $updatedParameters = null): array - { - $tenantId = $this->tenantId(); - - // 해당 모델로 생성된 제품들 조회 - $modelBasedProducts = $this->getProductsByModel($modelId); - - $results = []; - - foreach ($modelBasedProducts as $product) { - try { - $metadata = $this->getModelMetadata($product); - $parameters = $updatedParameters ?? $metadata['parameters']; - - $updatedProduct = $this->updateProductBom($product->id, $parameters); - - $results[] = [ - 'product_id' => $product->id, - 'product_code' => $product->code, - 'status' => 'updated', - 'bom_items_count' => $updatedProduct->components->count(), - ]; - } catch (\Exception $e) { - $results[] = [ - 'product_id' => $product->id, - 'product_code' => $product->code, - 'status' => 'failed', - 'error' => $e->getMessage(), - ]; - } - } - - return $results; - } - - /** - * ProductComponent 생성 - */ - private function createProductComponents(Product $product, array $bomItems): void - { - $order = 1; - - foreach ($bomItems as $item) { - ProductComponent::create([ - 'product_id' => $product->id, - 'ref_type' => $item['target_type'], - 'ref_id' => $item['target_id'], - 'quantity' => $item['actual_quantity'] ?? $item['quantity'], - 'waste_rate' => $item['waste_rate'] ?? 0, - 'order' => $order, - 'note' => $item['reason'] ?? null, - ]); - - $order++; - } - } - - /** - * 모델 메타데이터 저장 - */ - private function saveModelMetadata(Product $product, DesignModel $model, array $parameters, array $bomResolution): void - { - $metadata = [ - 'model_id' => $model->id, - 'model_code' => $model->code, - 'parameters' => $parameters, - 'calculated_values' => $bomResolution['calculated_values'], - 'bom_resolution_summary' => $bomResolution['summary'], - 'created_at' => now()->toISOString(), - ]; - - // 실제로는 product_metadata 테이블이나 products 테이블의 metadata 컬럼에 저장 - // 임시로 캐시 사용 - cache()->put("product_model_metadata_{$product->id}", $metadata, 86400); // 24시간 - } - - /** - * 모델 메타데이터 업데이트 - */ - private function updateModelMetadata(Product $product, array $newParameters, array $bomResolution): void - { - $metadata = $this->getModelMetadata($product); - if ($metadata) { - $metadata['parameters'] = $newParameters; - $metadata['calculated_values'] = $bomResolution['calculated_values']; - $metadata['bom_resolution_summary'] = $bomResolution['summary']; - $metadata['updated_at'] = now()->toISOString(); - - cache()->put("product_model_metadata_{$product->id}", $metadata, 86400); - } - } - - /** - * 모델 메타데이터 조회 - */ - private function getModelMetadata(Product $product): ?array - { - return cache()->get("product_model_metadata_{$product->id}"); - } - - /** - * 매개변수를 제품명용 문자열로 포맷 - */ - private function formatParametersForName(array $parameters): string - { - $formatted = []; - - foreach ($parameters as $key => $value) { - if (is_numeric($value)) { - $formatted[] = "{$key}:{$value}"; - } else { - $formatted[] = "{$key}:{$value}"; - } - } - - return implode(', ', array_slice($formatted, 0, 3)); // 최대 3개 매개변수만 - } - - /** - * 제품 코드 자동 생성 - */ - private function generateProductCode(DesignModel $model, array $parameters): string - { - $tenantId = $this->tenantId(); - - // 모델 코드 + 매개변수 해시 - $paramHash = substr(md5(json_encode($parameters)), 0, 6); - $baseCode = $model->code . '-' . strtoupper($paramHash); - - // 중복 체크 후 순번 추가 - $counter = 1; - $finalCode = $baseCode; - - while (Product::where('tenant_id', $tenantId)->where('code', $finalCode)->exists()) { - $finalCode = $baseCode . '-' . str_pad($counter, 2, '0', STR_PAD_LEFT); - $counter++; - } - - return $finalCode; - } - - /** - * 모델로 생성된 제품들 조회 - */ - private function getProductsByModel(int $modelId): \Illuminate\Support\Collection - { - $tenantId = $this->tenantId(); - - $products = Product::where('tenant_id', $tenantId)->get(); - - return $products->filter(function ($product) use ($modelId) { - $metadata = $this->getModelMetadata($product); - return $metadata && $metadata['model_id'] == $modelId; - }); - } - - /** - * 제품의 모델 정보 조회 (API용) - */ - public function getProductModelInfo(int $productId): ?array - { - $tenantId = $this->tenantId(); - - $product = Product::where('tenant_id', $tenantId)->where('id', $productId)->first(); - if (!$product) { - throw new NotFoundHttpException(__('error.product_not_found')); - } - - $metadata = $this->getModelMetadata($product); - if (!$metadata) { - return null; - } - - // 모델 정보 추가 조회 - $model = DesignModel::where('tenant_id', $tenantId)->where('id', $metadata['model_id'])->first(); - - return [ - 'product' => [ - 'id' => $product->id, - 'code' => $product->code, - 'name' => $product->name, - ], - 'model' => [ - 'id' => $model->id, - 'code' => $model->code, - 'name' => $model->name, - ], - 'parameters' => $metadata['parameters'], - 'calculated_values' => $metadata['calculated_values'], - 'bom_summary' => $metadata['bom_resolution_summary'], - 'created_at' => $metadata['created_at'], - 'updated_at' => $metadata['updated_at'] ?? null, - ]; - } - - /** - * 모델 기반 제품 재생성 (모델/공식/규칙 변경 후) - */ - public function regenerateProduct(int $productId, bool $preserveCustomizations = false): Product - { - $tenantId = $this->tenantId(); - - $product = Product::where('tenant_id', $tenantId)->where('id', $productId)->first(); - if (!$product) { - throw new NotFoundHttpException(__('error.product_not_found')); - } - - $metadata = $this->getModelMetadata($product); - if (!$metadata) { - throw ValidationException::withMessages([ - 'product_id' => __('error.product_not_model_based') - ]); - } - - // 사용자 정의 변경사항 보존 로직 - $customComponents = []; - if ($preserveCustomizations) { - // 기존 컴포넌트 중 규칙에서 나온 것이 아닌 수동 추가 항목들 식별 - $customComponents = ProductComponent::where('product_id', $productId) - ->whereNull('note') // rule_name이 없는 항목들 - ->get() - ->toArray(); - } - - // 제품을 새로운 매개변수로 재생성 - $regeneratedProduct = $this->updateProductBom($productId, $metadata['parameters']); - - // 커스텀 컴포넌트 복원 - if (!empty($customComponents)) { - foreach ($customComponents as $customComponent) { - ProductComponent::create([ - 'product_id' => $productId, - 'ref_type' => $customComponent['ref_type'], - 'ref_id' => $customComponent['ref_id'], - 'quantity' => $customComponent['quantity'], - 'waste_rate' => $customComponent['waste_rate'], - 'order' => $customComponent['order'], - 'note' => 'custom_preserved', - ]); - } - } - - return $regeneratedProduct->fresh()->load('components'); - } -} \ No newline at end of file diff --git a/app/Services/ModelFormulaService.php b/app/Services/ModelFormulaService.php deleted file mode 100644 index d930cbe..0000000 --- a/app/Services/ModelFormulaService.php +++ /dev/null @@ -1,505 +0,0 @@ -validateModelAccess($modelId); - - $query = ModelFormula::where('model_id', $modelId) - ->active() - ->ordered() - ->with('model'); - - if ($paginate) { - return $query->paginate($perPage); - } - - return $query->get(); - } - - /** - * 공식 상세 조회 - */ - public function getFormula(int $id): ModelFormula - { - $formula = ModelFormula::where('tenant_id', $this->tenantId()) - ->findOrFail($id); - - $this->validateModelAccess($formula->model_id); - - return $formula; - } - - /** - * 공식 생성 - */ - public function createFormula(array $data): ModelFormula - { - $this->validateModelAccess($data['model_id']); - - // 기본값 설정 - $data['tenant_id'] = $this->tenantId(); - $data['created_by'] = $this->apiUserId(); - - // 순서가 지정되지 않은 경우 마지막으로 설정 - if (!isset($data['order'])) { - $maxOrder = ModelFormula::where('tenant_id', $this->tenantId()) - ->where('model_id', $data['model_id']) - ->max('order') ?? 0; - $data['order'] = $maxOrder + 1; - } - - // 공식명 중복 체크 - $this->validateFormulaNameUnique($data['model_id'], $data['name']); - - // 공식 검증 및 의존성 추출 - $formula = new ModelFormula($data); - $expressionErrors = $formula->validateExpression(); - - if (!empty($expressionErrors)) { - throw new \InvalidArgumentException(__('error.invalid_formula_expression') . ': ' . implode(', ', $expressionErrors)); - } - - // 의존성 검증 - $dependencies = $formula->extractVariables(); - $this->validateDependencies($data['model_id'], $dependencies); - $data['dependencies'] = $dependencies; - - // 순환 의존성 체크 - $this->validateCircularDependency($data['model_id'], $data['name'], $dependencies); - - $formula = ModelFormula::create($data); - - // 계산 순서 재정렬 - $this->recalculateOrder($data['model_id']); - - return $formula->fresh(); - } - - /** - * 공식 수정 - */ - public function updateFormula(int $id, array $data): ModelFormula - { - $formula = $this->getFormula($id); - - // 공식명 변경 시 중복 체크 - if (isset($data['name']) && $data['name'] !== $formula->name) { - $this->validateFormulaNameUnique($formula->model_id, $data['name'], $id); - } - - // 공식 표현식 변경 시 검증 - if (isset($data['expression'])) { - $tempFormula = new ModelFormula(array_merge($formula->toArray(), $data)); - $expressionErrors = $tempFormula->validateExpression(); - - if (!empty($expressionErrors)) { - throw new \InvalidArgumentException(__('error.invalid_formula_expression') . ': ' . implode(', ', $expressionErrors)); - } - - // 의존성 검증 - $dependencies = $tempFormula->extractVariables(); - $this->validateDependencies($formula->model_id, $dependencies); - $data['dependencies'] = $dependencies; - - // 순환 의존성 체크 (기존 공식 제외) - $this->validateCircularDependency($formula->model_id, $data['name'] ?? $formula->name, $dependencies, $id); - } - - $data['updated_by'] = $this->apiUserId(); - $formula->update($data); - - // 의존성이 변경된 경우 계산 순서 재정렬 - if (isset($data['expression'])) { - $this->recalculateOrder($formula->model_id); - } - - return $formula->fresh(); - } - - /** - * 공식 삭제 - */ - public function deleteFormula(int $id): bool - { - $formula = $this->getFormula($id); - - // 다른 공식에서 사용 중인지 확인 - $this->validateFormulaNotInUse($formula->model_id, $formula->name); - - $formula->update(['deleted_by' => $this->apiUserId()]); - $formula->delete(); - - // 계산 순서 재정렬 - $this->recalculateOrder($formula->model_id); - - return true; - } - - /** - * 공식 복사 (다른 모델로) - */ - public function copyFormulasToModel(int $sourceModelId, int $targetModelId): Collection - { - $this->validateModelAccess($sourceModelId); - $this->validateModelAccess($targetModelId); - - $sourceFormulas = $this->getFormulasByModel($sourceModelId); - $copiedFormulas = collect(); - - // 의존성 순서대로 복사 - $orderedFormulas = $this->sortFormulasByDependency($sourceFormulas); - - foreach ($orderedFormulas as $sourceFormula) { - $data = $sourceFormula->toArray(); - unset($data['id'], $data['created_at'], $data['updated_at'], $data['deleted_at']); - - $data['model_id'] = $targetModelId; - $data['created_by'] = $this->apiUserId(); - - // 이름 중복 시 수정 - $originalName = $data['name']; - $counter = 1; - while ($this->isFormulaNameExists($targetModelId, $data['name'])) { - $data['name'] = $originalName . '_' . $counter; - $counter++; - } - - // 대상 모델의 매개변수/공식에 맞게 의존성 재검증 - $dependencies = $this->extractVariablesFromExpression($data['expression']); - $validDependencies = $this->getValidDependencies($targetModelId, $dependencies); - $data['dependencies'] = $validDependencies; - - $copiedFormula = ModelFormula::create($data); - $copiedFormulas->push($copiedFormula); - } - - // 복사 완료 후 계산 순서 재정렬 - $this->recalculateOrder($targetModelId); - - return $copiedFormulas; - } - - /** - * 공식 계산 실행 - */ - public function calculateFormulas(int $modelId, array $inputValues): array - { - $this->validateModelAccess($modelId); - - $formulas = $this->getFormulasByModel($modelId); - $results = $inputValues; // 매개변수 값으로 시작 - - // 의존성 순서대로 계산 - $orderedFormulas = $this->sortFormulasByDependency($formulas); - - foreach ($orderedFormulas as $formula) { - try { - $result = $formula->calculate($results); - if ($result !== null) { - $results[$formula->name] = $result; - } - } catch (\Throwable $e) { - // 계산 실패 시 null로 설정 - $results[$formula->name] = null; - } - } - - return $results; - } - - /** - * 공식 검증 (문법 및 의존성) - */ - public function validateFormula(int $modelId, string $name, string $expression): array - { - $this->validateModelAccess($modelId); - - $errors = []; - - // 임시 공식 객체로 문법 검증 - $tempFormula = new ModelFormula([ - 'name' => $name, - 'expression' => $expression, - 'model_id' => $modelId - ]); - - $expressionErrors = $tempFormula->validateExpression(); - if (!empty($expressionErrors)) { - $errors['expression'] = $expressionErrors; - } - - // 의존성 검증 - $dependencies = $tempFormula->extractVariables(); - $dependencyErrors = $this->validateDependencies($modelId, $dependencies, false); - if (!empty($dependencyErrors)) { - $errors['dependencies'] = $dependencyErrors; - } - - // 순환 의존성 체크 - try { - $this->validateCircularDependency($modelId, $name, $dependencies); - } catch (\InvalidArgumentException $e) { - $errors['circular_dependency'] = [$e->getMessage()]; - } - - return $errors; - } - - /** - * 의존성 순서대로 공식 정렬 - */ - public function sortFormulasByDependency(Collection $formulas): Collection - { - $sorted = collect(); - $remaining = $formulas->keyBy('name'); - $processed = []; - - while ($remaining->count() > 0) { - $progress = false; - - foreach ($remaining as $formula) { - $dependencies = $formula->dependencies ?? []; - $canProcess = true; - - // 의존성이 모두 처리되었는지 확인 - foreach ($dependencies as $dep) { - if (!in_array($dep, $processed) && $remaining->has($dep)) { - $canProcess = false; - break; - } - } - - if ($canProcess) { - $sorted->push($formula); - $processed[] = $formula->name; - $remaining->forget($formula->name); - $progress = true; - } - } - - // 진행이 없으면 순환 의존성 - if (!$progress && $remaining->count() > 0) { - break; - } - } - - // 순환 의존성으로 처리되지 않은 공식들도 추가 - return $sorted->concat($remaining->values()); - } - - /** - * 모델 접근 권한 검증 - */ - private function validateModelAccess(int $modelId): void - { - $model = ModelMaster::where('tenant_id', $this->tenantId()) - ->findOrFail($modelId); - } - - /** - * 공식명 중복 검증 - */ - private function validateFormulaNameUnique(int $modelId, string $name, ?int $excludeId = null): void - { - $query = ModelFormula::where('tenant_id', $this->tenantId()) - ->where('model_id', $modelId) - ->where('name', $name); - - if ($excludeId) { - $query->where('id', '!=', $excludeId); - } - - if ($query->exists()) { - throw new \InvalidArgumentException(__('error.formula_name_duplicate')); - } - - // 매개변수명과도 중복되지 않아야 함 - $parameterExists = ModelParameter::where('tenant_id', $this->tenantId()) - ->where('model_id', $modelId) - ->where('name', $name) - ->exists(); - - if ($parameterExists) { - throw new \InvalidArgumentException(__('error.formula_name_conflicts_with_parameter')); - } - } - - /** - * 공식명 존재 여부 확인 - */ - private function isFormulaNameExists(int $modelId, string $name): bool - { - return ModelFormula::where('tenant_id', $this->tenantId()) - ->where('model_id', $modelId) - ->where('name', $name) - ->exists(); - } - - /** - * 의존성 검증 - */ - private function validateDependencies(int $modelId, array $dependencies, bool $throwException = true): array - { - $errors = []; - - // 매개변수 목록 가져오기 - $parameters = ModelParameter::where('tenant_id', $this->tenantId()) - ->where('model_id', $modelId) - ->active() - ->pluck('name') - ->toArray(); - - // 기존 공식 목록 가져오기 - $formulas = ModelFormula::where('tenant_id', $this->tenantId()) - ->where('model_id', $modelId) - ->active() - ->pluck('name') - ->toArray(); - - $validNames = array_merge($parameters, $formulas); - - foreach ($dependencies as $dep) { - if (!in_array($dep, $validNames)) { - $errors[] = "Dependency '{$dep}' not found in model parameters or formulas"; - } - } - - if (!empty($errors) && $throwException) { - throw new \InvalidArgumentException(__('error.invalid_formula_dependencies') . ': ' . implode(', ', $errors)); - } - - return $errors; - } - - /** - * 순환 의존성 검증 - */ - private function validateCircularDependency(int $modelId, string $formulaName, array $dependencies, ?int $excludeId = null): void - { - $allFormulas = ModelFormula::where('tenant_id', $this->tenantId()) - ->where('model_id', $modelId) - ->active(); - - if ($excludeId) { - $allFormulas->where('id', '!=', $excludeId); - } - - $allFormulas = $allFormulas->get(); - - // 현재 공식을 임시로 추가하여 순환 의존성 검사 - $tempFormula = new ModelFormula([ - 'name' => $formulaName, - 'dependencies' => $dependencies - ]); - $allFormulas->push($tempFormula); - - if ($this->hasCircularDependency($tempFormula, $allFormulas->toArray())) { - throw new \InvalidArgumentException(__('error.circular_dependency_detected')); - } - } - - /** - * 순환 의존성 검사 - */ - private function hasCircularDependency(ModelFormula $formula, array $allFormulas, array $visited = []): bool - { - if (in_array($formula->name, $visited)) { - return true; - } - - $visited[] = $formula->name; - - foreach ($formula->dependencies ?? [] as $dep) { - foreach ($allFormulas as $depFormula) { - if ($depFormula->name === $dep) { - if ($this->hasCircularDependency($depFormula, $allFormulas, $visited)) { - return true; - } - break; - } - } - } - - return false; - } - - /** - * 공식이 다른 공식에서 사용 중인지 확인 - */ - private function validateFormulaNotInUse(int $modelId, string $formulaName): void - { - $usageCount = ModelFormula::where('tenant_id', $this->tenantId()) - ->where('model_id', $modelId) - ->whereJsonContains('dependencies', $formulaName) - ->count(); - - if ($usageCount > 0) { - throw new \InvalidArgumentException(__('error.formula_in_use')); - } - } - - /** - * 계산 순서 재정렬 - */ - private function recalculateOrder(int $modelId): void - { - $formulas = $this->getFormulasByModel($modelId); - $orderedFormulas = $this->sortFormulasByDependency($formulas); - - foreach ($orderedFormulas as $index => $formula) { - $formula->update([ - 'order' => $index + 1, - 'updated_by' => $this->apiUserId() - ]); - } - } - - /** - * 표현식에서 변수 추출 - */ - private function extractVariablesFromExpression(string $expression): array - { - $tempFormula = new ModelFormula(['expression' => $expression]); - return $tempFormula->extractVariables(); - } - - /** - * 유효한 의존성만 필터링 - */ - private function getValidDependencies(int $modelId, array $dependencies): array - { - $parameters = ModelParameter::where('tenant_id', $this->tenantId()) - ->where('model_id', $modelId) - ->active() - ->pluck('name') - ->toArray(); - - $formulas = ModelFormula::where('tenant_id', $this->tenantId()) - ->where('model_id', $modelId) - ->active() - ->pluck('name') - ->toArray(); - - $validNames = array_merge($parameters, $formulas); - - return array_intersect($dependencies, $validNames); - } -} \ No newline at end of file diff --git a/app/Services/ModelParameterService.php b/app/Services/ModelParameterService.php deleted file mode 100644 index ad1b992..0000000 --- a/app/Services/ModelParameterService.php +++ /dev/null @@ -1,278 +0,0 @@ -validateModelAccess($modelId); - - $query = ModelParameter::where('model_id', $modelId) - ->active() - ->ordered() - ->with('model'); - - if ($paginate) { - return $query->paginate($perPage); - } - - return $query->get(); - } - - /** - * 매개변수 상세 조회 - */ - public function getParameter(int $id): ModelParameter - { - $parameter = ModelParameter::where('tenant_id', $this->tenantId()) - ->findOrFail($id); - - $this->validateModelAccess($parameter->model_id); - - return $parameter; - } - - /** - * 매개변수 생성 - */ - public function createParameter(array $data): ModelParameter - { - $this->validateModelAccess($data['model_id']); - - // 기본값 설정 - $data['tenant_id'] = $this->tenantId(); - $data['created_by'] = $this->apiUserId(); - - // 순서가 지정되지 않은 경우 마지막으로 설정 - if (!isset($data['order'])) { - $maxOrder = ModelParameter::where('tenant_id', $this->tenantId()) - ->where('model_id', $data['model_id']) - ->max('order') ?? 0; - $data['order'] = $maxOrder + 1; - } - - // 매개변수명 중복 체크 - $this->validateParameterNameUnique($data['model_id'], $data['name']); - - // 검증 규칙 처리 - if (isset($data['validation_rules']) && is_string($data['validation_rules'])) { - $data['validation_rules'] = json_decode($data['validation_rules'], true); - } - - // 옵션 처리 (SELECT 타입) - if (isset($data['options']) && is_string($data['options'])) { - $data['options'] = json_decode($data['options'], true); - } - - $parameter = ModelParameter::create($data); - - return $parameter->fresh(); - } - - /** - * 매개변수 수정 - */ - public function updateParameter(int $id, array $data): ModelParameter - { - $parameter = $this->getParameter($id); - - // 매개변수명 변경 시 중복 체크 - if (isset($data['name']) && $data['name'] !== $parameter->name) { - $this->validateParameterNameUnique($parameter->model_id, $data['name'], $id); - } - - // 검증 규칙 처리 - if (isset($data['validation_rules']) && is_string($data['validation_rules'])) { - $data['validation_rules'] = json_decode($data['validation_rules'], true); - } - - // 옵션 처리 - if (isset($data['options']) && is_string($data['options'])) { - $data['options'] = json_decode($data['options'], true); - } - - $data['updated_by'] = $this->apiUserId(); - $parameter->update($data); - - return $parameter->fresh(); - } - - /** - * 매개변수 삭제 - */ - public function deleteParameter(int $id): bool - { - $parameter = $this->getParameter($id); - - // 다른 공식에서 사용 중인지 확인 - $this->validateParameterNotInUse($parameter->model_id, $parameter->name); - - $parameter->update(['deleted_by' => $this->apiUserId()]); - $parameter->delete(); - - return true; - } - - /** - * 매개변수 순서 변경 - */ - public function reorderParameters(int $modelId, array $orderData): bool - { - $this->validateModelAccess($modelId); - - foreach ($orderData as $item) { - ModelParameter::where('tenant_id', $this->tenantId()) - ->where('model_id', $modelId) - ->where('id', $item['id']) - ->update([ - 'order' => $item['order'], - 'updated_by' => $this->apiUserId() - ]); - } - - return true; - } - - /** - * 매개변수 복사 (다른 모델로) - */ - public function copyParametersToModel(int $sourceModelId, int $targetModelId): Collection - { - $this->validateModelAccess($sourceModelId); - $this->validateModelAccess($targetModelId); - - $sourceParameters = $this->getParametersByModel($sourceModelId); - $copiedParameters = collect(); - - foreach ($sourceParameters as $sourceParam) { - $data = $sourceParam->toArray(); - unset($data['id'], $data['created_at'], $data['updated_at'], $data['deleted_at']); - - $data['model_id'] = $targetModelId; - $data['created_by'] = $this->apiUserId(); - - // 이름 중복 시 수정 - $originalName = $data['name']; - $counter = 1; - while ($this->isParameterNameExists($targetModelId, $data['name'])) { - $data['name'] = $originalName . '_' . $counter; - $counter++; - } - - $copiedParameter = ModelParameter::create($data); - $copiedParameters->push($copiedParameter); - } - - return $copiedParameters; - } - - /** - * 매개변수 값 검증 - */ - public function validateParameterValues(int $modelId, array $values): array - { - $this->validateModelAccess($modelId); - - $parameters = $this->getParametersByModel($modelId); - $errors = []; - - foreach ($parameters as $parameter) { - $value = $values[$parameter->name] ?? null; - $paramErrors = $parameter->validateValue($value); - - if (!empty($paramErrors)) { - $errors[$parameter->name] = $paramErrors; - } - } - - return $errors; - } - - /** - * 매개변수 값을 적절한 타입으로 변환 - */ - public function castParameterValues(int $modelId, array $values): array - { - $this->validateModelAccess($modelId); - - $parameters = $this->getParametersByModel($modelId); - $castedValues = []; - - foreach ($parameters as $parameter) { - $value = $values[$parameter->name] ?? null; - $castedValues[$parameter->name] = $parameter->castValue($value); - } - - return $castedValues; - } - - /** - * 모델 접근 권한 검증 - */ - private function validateModelAccess(int $modelId): void - { - $model = ModelMaster::where('tenant_id', $this->tenantId()) - ->findOrFail($modelId); - } - - /** - * 매개변수명 중복 검증 - */ - private function validateParameterNameUnique(int $modelId, string $name, ?int $excludeId = null): void - { - $query = ModelParameter::where('tenant_id', $this->tenantId()) - ->where('model_id', $modelId) - ->where('name', $name); - - if ($excludeId) { - $query->where('id', '!=', $excludeId); - } - - if ($query->exists()) { - throw new \InvalidArgumentException(__('error.parameter_name_duplicate')); - } - } - - /** - * 매개변수명 존재 여부 확인 - */ - private function isParameterNameExists(int $modelId, string $name): bool - { - return ModelParameter::where('tenant_id', $this->tenantId()) - ->where('model_id', $modelId) - ->where('name', $name) - ->exists(); - } - - /** - * 매개변수가 다른 공식에서 사용 중인지 확인 - */ - private function validateParameterNotInUse(int $modelId, string $parameterName): void - { - $formulaService = new ModelFormulaService(); - $formulas = $formulaService->getFormulasByModel($modelId); - - foreach ($formulas as $formula) { - if (in_array($parameterName, $formula->dependencies ?? [])) { - throw new \InvalidArgumentException(__('error.parameter_in_use_by_formula', [ - 'parameter' => $parameterName, - 'formula' => $formula->name - ])); - } - } - } -} \ No newline at end of file diff --git a/app/Services/ProductFromModelService.php b/app/Services/ProductFromModelService.php deleted file mode 100644 index ea9a31d..0000000 --- a/app/Services/ProductFromModelService.php +++ /dev/null @@ -1,496 +0,0 @@ -bomResolverService = new BomResolverService(); - } - - /** - * 모델에서 제품 생성 - */ - public function createProductFromModel(int $modelId, array $inputParameters, array $productData = []): Product - { - $this->validateModelAccess($modelId); - - return DB::transaction(function () use ($modelId, $inputParameters, $productData) { - // 1. BOM 해석 - $resolvedBom = $this->bomResolverService->resolveBom($modelId, $inputParameters); - - // 2. 제품 기본 정보 생성 - $product = $this->createProduct($modelId, $inputParameters, $resolvedBom, $productData); - - // 3. BOM 스냅샷 저장 - $this->saveBomSnapshot($product->id, $resolvedBom); - - // 4. 매개변수 및 계산값 저장 - $this->saveParameterSnapshot($product->id, $resolvedBom); - - return $product->fresh(['components']); - }); - } - - /** - * 제품 코드 생성 미리보기 - */ - public function previewProductCode(int $modelId, array $inputParameters): string - { - $this->validateModelAccess($modelId); - - $model = ModelMaster::where('tenant_id', $this->tenantId()) - ->findOrFail($modelId); - - // BOM 해석하여 계산값 가져오기 - $resolvedBom = $this->bomResolverService->resolveBom($modelId, $inputParameters); - - return $this->generateProductCode($model, $resolvedBom['calculated_values']); - } - - /** - * 제품 사양서 생성 - */ - public function generateProductSpecification(int $modelId, array $inputParameters): array - { - $this->validateModelAccess($modelId); - - $model = ModelMaster::where('tenant_id', $this->tenantId()) - ->findOrFail($modelId); - - $resolvedBom = $this->bomResolverService->resolveBom($modelId, $inputParameters); - - return [ - 'model' => [ - 'id' => $model->id, - 'code' => $model->code, - 'name' => $model->name, - 'version' => $model->current_version, - ], - 'parameters' => $this->formatParameters($resolvedBom['input_parameters']), - 'calculated_values' => $this->formatCalculatedValues($resolvedBom['calculated_values']), - 'bom_summary' => $resolvedBom['summary'], - 'specifications' => $this->generateSpecifications($model, $resolvedBom), - 'generated_at' => now()->toISOString(), - ]; - } - - /** - * 대량 제품 생성 - */ - public function createProductsBatch(int $modelId, array $parameterSets, array $baseProductData = []): array - { - $this->validateModelAccess($modelId); - - $results = []; - - DB::transaction(function () use ($modelId, $parameterSets, $baseProductData, &$results) { - foreach ($parameterSets as $index => $parameters) { - try { - $productData = array_merge($baseProductData, [ - 'name' => ($baseProductData['name'] ?? '') . ' #' . ($index + 1), - ]); - - $product = $this->createProductFromModel($modelId, $parameters, $productData); - - $results[$index] = [ - 'success' => true, - 'product_id' => $product->id, - 'product_code' => $product->code, - 'parameters' => $parameters, - ]; - } catch (\Throwable $e) { - $results[$index] = [ - 'success' => false, - 'error' => $e->getMessage(), - 'parameters' => $parameters, - ]; - } - } - }); - - return $results; - } - - /** - * 기존 제품의 BOM 업데이트 - */ - public function updateProductBom(int $productId, array $newParameters): Product - { - $product = Product::where('tenant_id', $this->tenantId()) - ->findOrFail($productId); - - if (!$product->source_model_id) { - throw new \InvalidArgumentException(__('error.product_not_from_model')); - } - - return DB::transaction(function () use ($product, $newParameters) { - // 1. 새 BOM 해석 - $resolvedBom = $this->bomResolverService->resolveBom($product->source_model_id, $newParameters); - - // 2. 기존 BOM 컴포넌트 삭제 - ProductComponent::where('product_id', $product->id)->delete(); - - // 3. 새 BOM 스냅샷 저장 - $this->saveBomSnapshot($product->id, $resolvedBom); - - // 4. 매개변수 업데이트 - $this->saveParameterSnapshot($product->id, $resolvedBom, true); - - // 5. 제품 정보 업데이트 - $product->update([ - 'updated_by' => $this->apiUserId(), - ]); - - return $product->fresh(['components']); - }); - } - - /** - * 제품 버전 생성 (기존 제품의 파라미터 변경) - */ - public function createProductVersion(int $productId, array $newParameters, string $versionNote = ''): Product - { - $originalProduct = Product::where('tenant_id', $this->tenantId()) - ->findOrFail($productId); - - if (!$originalProduct->source_model_id) { - throw new \InvalidArgumentException(__('error.product_not_from_model')); - } - - return DB::transaction(function () use ($originalProduct, $newParameters, $versionNote) { - // 원본 제품 복제 - $productData = $originalProduct->toArray(); - unset($productData['id'], $productData['created_at'], $productData['updated_at'], $productData['deleted_at']); - - // 버전 정보 추가 - $productData['name'] = $originalProduct->name . ' (v' . now()->format('YmdHis') . ')'; - $productData['parent_product_id'] = $originalProduct->id; - $productData['version_note'] = $versionNote; - - $newProduct = $this->createProductFromModel( - $originalProduct->source_model_id, - $newParameters, - $productData - ); - - return $newProduct; - }); - } - - /** - * 제품 생성 - */ - private function createProduct(int $modelId, array $inputParameters, array $resolvedBom, array $productData): Product - { - $model = ModelMaster::where('tenant_id', $this->tenantId()) - ->findOrFail($modelId); - - // 기본 제품 데이터 설정 - $defaultData = [ - 'tenant_id' => $this->tenantId(), - 'code' => $this->generateProductCode($model, $resolvedBom['calculated_values']), - 'name' => $productData['name'] ?? $this->generateProductName($model, $resolvedBom['calculated_values']), - 'type' => 'PRODUCT', - 'source_model_id' => $modelId, - 'model_version' => $model->current_version, - 'is_active' => true, - 'created_by' => $this->apiUserId(), - ]; - - $finalData = array_merge($defaultData, $productData); - - return Product::create($finalData); - } - - /** - * BOM 스냅샷 저장 - */ - private function saveBomSnapshot(int $productId, array $resolvedBom): void - { - foreach ($resolvedBom['bom_items'] as $index => $item) { - ProductComponent::create([ - 'tenant_id' => $this->tenantId(), - 'product_id' => $productId, - 'ref_type' => $item['ref_type'] ?? ($item['material_id'] ? 'MATERIAL' : 'PRODUCT'), - 'product_id_ref' => $item['product_id'] ?? null, - 'material_id' => $item['material_id'] ?? null, - 'quantity' => $item['total_quantity'], - 'base_quantity' => $item['calculated_quantity'], - 'waste_rate' => $item['waste_rate'] ?? 0, - 'unit' => $item['unit'] ?? 'ea', - 'memo' => $item['memo'] ?? null, - 'order' => $item['order'] ?? $index + 1, - 'created_by' => $this->apiUserId(), - ]); - } - } - - /** - * 매개변수 스냅샷 저장 - */ - private function saveParameterSnapshot(int $productId, array $resolvedBom, bool $update = false): void - { - $parameterData = [ - 'input_parameters' => $resolvedBom['input_parameters'], - 'calculated_values' => $resolvedBom['calculated_values'], - 'bom_summary' => $resolvedBom['summary'], - 'resolved_at' => $resolvedBom['resolved_at'], - ]; - - $product = Product::findOrFail($productId); - - if ($update) { - $existingSnapshot = $product->parameter_snapshot ?? []; - $parameterData = array_merge($existingSnapshot, $parameterData); - } - - $product->update([ - 'parameter_snapshot' => $parameterData, - 'updated_by' => $this->apiUserId(), - ]); - } - - /** - * 제품 코드 생성 - */ - private function generateProductCode(ModelMaster $model, array $calculatedValues): string - { - // 모델 코드 + 주요 치수값으로 코드 생성 - $code = $model->code; - - // 주요 치수값 추가 (W1, H1 등) - $keyDimensions = ['W1', 'H1', 'W0', 'H0']; - $dimensionParts = []; - - foreach ($keyDimensions as $dim) { - if (isset($calculatedValues[$dim]) && is_numeric($calculatedValues[$dim])) { - $dimensionParts[] = $dim . (int) $calculatedValues[$dim]; - } - } - - if (!empty($dimensionParts)) { - $code .= '_' . implode('_', $dimensionParts); - } - - // 고유성 보장을 위한 접미사 - $suffix = Str::upper(Str::random(4)); - $code .= '_' . $suffix; - - return $code; - } - - /** - * 제품명 생성 - */ - private function generateProductName(ModelMaster $model, array $calculatedValues): string - { - $name = $model->name; - - // 주요 치수값 추가 - $keyDimensions = ['W1', 'H1']; - $dimensionParts = []; - - foreach ($keyDimensions as $dim) { - if (isset($calculatedValues[$dim]) && is_numeric($calculatedValues[$dim])) { - $dimensionParts[] = (int) $calculatedValues[$dim]; - } - } - - if (!empty($dimensionParts)) { - $name .= ' (' . implode('×', $dimensionParts) . ')'; - } - - return $name; - } - - /** - * 매개변수 포맷팅 - */ - private function formatParameters(array $parameters): array - { - $formatted = []; - - foreach ($parameters as $name => $value) { - $formatted[] = [ - 'name' => $name, - 'value' => $value, - 'type' => is_numeric($value) ? 'number' : (is_bool($value) ? 'boolean' : 'text'), - ]; - } - - return $formatted; - } - - /** - * 계산값 포맷팅 - */ - private function formatCalculatedValues(array $calculatedValues): array - { - $formatted = []; - - foreach ($calculatedValues as $name => $value) { - if (is_numeric($value)) { - $formatted[] = [ - 'name' => $name, - 'value' => round((float) $value, 3), - 'type' => 'calculated', - ]; - } - } - - return $formatted; - } - - /** - * 사양서 생성 - */ - private function generateSpecifications(ModelMaster $model, array $resolvedBom): array - { - $specs = []; - - // 기본 치수 사양 - $dimensionSpecs = $this->generateDimensionSpecs($resolvedBom['calculated_values']); - if (!empty($dimensionSpecs)) { - $specs['dimensions'] = $dimensionSpecs; - } - - // 무게/면적 사양 - $physicalSpecs = $this->generatePhysicalSpecs($resolvedBom['calculated_values']); - if (!empty($physicalSpecs)) { - $specs['physical'] = $physicalSpecs; - } - - // BOM 요약 - $bomSpecs = $this->generateBomSpecs($resolvedBom['bom_items']); - if (!empty($bomSpecs)) { - $specs['components'] = $bomSpecs; - } - - return $specs; - } - - /** - * 치수 사양 생성 - */ - private function generateDimensionSpecs(array $calculatedValues): array - { - $specs = []; - $dimensionKeys = ['W0', 'H0', 'W1', 'H1', 'D1', 'L1']; - - foreach ($dimensionKeys as $key) { - if (isset($calculatedValues[$key]) && is_numeric($calculatedValues[$key])) { - $specs[$key] = [ - 'value' => round((float) $calculatedValues[$key], 1), - 'unit' => 'mm', - 'label' => $this->getDimensionLabel($key), - ]; - } - } - - return $specs; - } - - /** - * 물리적 사양 생성 - */ - private function generatePhysicalSpecs(array $calculatedValues): array - { - $specs = []; - - if (isset($calculatedValues['area']) && is_numeric($calculatedValues['area'])) { - $specs['area'] = [ - 'value' => round((float) $calculatedValues['area'], 3), - 'unit' => 'm²', - 'label' => '면적', - ]; - } - - if (isset($calculatedValues['weight']) && is_numeric($calculatedValues['weight'])) { - $specs['weight'] = [ - 'value' => round((float) $calculatedValues['weight'], 2), - 'unit' => 'kg', - 'label' => '중량', - ]; - } - - return $specs; - } - - /** - * BOM 사양 생성 - */ - private function generateBomSpecs(array $bomItems): array - { - $specs = [ - 'total_components' => count($bomItems), - 'materials' => 0, - 'products' => 0, - 'major_components' => [], - ]; - - foreach ($bomItems as $item) { - if (!empty($item['material_id'])) { - $specs['materials']++; - } else { - $specs['products']++; - } - - // 주요 구성품 (수량이 많거나 중요한 것들) - if (($item['total_quantity'] ?? 0) >= 10 || !empty($item['memo'])) { - $specs['major_components'][] = [ - 'type' => !empty($item['material_id']) ? 'material' : 'product', - 'id' => $item['material_id'] ?? $item['product_id'], - 'quantity' => $item['total_quantity'] ?? 0, - 'unit' => $item['unit'] ?? 'ea', - 'memo' => $item['memo'] ?? '', - ]; - } - } - - return $specs; - } - - /** - * 치수 라벨 가져오기 - */ - private function getDimensionLabel(string $key): string - { - $labels = [ - 'W0' => '입력 폭', - 'H0' => '입력 높이', - 'W1' => '실제 폭', - 'H1' => '실제 높이', - 'D1' => '깊이', - 'L1' => '길이', - ]; - - return $labels[$key] ?? $key; - } - - /** - * 모델 접근 권한 검증 - */ - private function validateModelAccess(int $modelId): void - { - $model = ModelMaster::where('tenant_id', $this->tenantId()) - ->findOrFail($modelId); - } -} \ No newline at end of file diff --git a/database/factories/BomConditionRuleFactory.php b/database/factories/BomConditionRuleFactory.php deleted file mode 100644 index 51e4dff..0000000 --- a/database/factories/BomConditionRuleFactory.php +++ /dev/null @@ -1,167 +0,0 @@ - - */ -class BomConditionRuleFactory extends Factory -{ - protected $model = BomConditionRule::class; - - /** - * Define the model's default state. - * - * @return array - */ - public function definition(): array - { - return [ - 'tenant_id' => 1, - 'model_id' => Model::factory(), - 'name' => $this->faker->words(3, true), - 'description' => $this->faker->sentence(), - 'condition_expression' => 'area > 5', - 'component_code' => 'BRK-001', - 'quantity_expression' => '2', - 'priority' => $this->faker->numberBetween(1, 100), - 'is_active' => true, - 'created_by' => 1, - 'updated_by' => 1, - ]; - } - - /** - * Screen model condition rules. - */ - public function screenRules(): static - { - return $this->sequence( - [ - 'name' => '케이스 규칙', - 'description' => '크기에 따른 케이스 선택', - 'condition_expression' => 'area <= 3', - 'component_code' => 'CASE-SMALL', - 'quantity_expression' => '1', - 'priority' => 10, - ], - [ - 'name' => '케이스 규칙 (중형)', - 'description' => '중형 크기 케이스', - 'condition_expression' => 'area > 3 AND area <= 6', - 'component_code' => 'CASE-MEDIUM', - 'quantity_expression' => '1', - 'priority' => 11, - ], - [ - 'name' => '케이스 규칙 (대형)', - 'description' => '대형 크기 케이스', - 'condition_expression' => 'area > 6', - 'component_code' => 'CASE-LARGE', - 'quantity_expression' => '1', - 'priority' => 12, - ], - [ - 'name' => '바텀 규칙', - 'description' => '바텀 개수', - 'condition_expression' => 'TRUE', - 'component_code' => 'BOTTOM-001', - 'quantity_expression' => 'CEIL(W1 / 1000)', - 'priority' => 20, - ], - [ - 'name' => '샤프트 규칙', - 'description' => '샤프트 길이', - 'condition_expression' => 'TRUE', - 'component_code' => 'SHAFT-001', - 'quantity_expression' => 'W1 / 1000', - 'priority' => 30, - ], - [ - 'name' => '파이프 규칙', - 'description' => '파이프 길이', - 'condition_expression' => 'screen_type = "SCREEN"', - 'component_code' => 'PIPE-SCREEN', - 'quantity_expression' => 'W1 / 1000', - 'priority' => 40, - ], - [ - 'name' => '슬라트 파이프 규칙', - 'description' => '슬라트용 파이프', - 'condition_expression' => 'screen_type = "SLAT"', - 'component_code' => 'PIPE-SLAT', - 'quantity_expression' => 'W1 / 1000', - 'priority' => 41, - ] - ); - } - - /** - * Steel model condition rules. - */ - public function steelRules(): static - { - return $this->sequence( - [ - 'name' => '프레임 규칙', - 'description' => '프레임 길이', - 'condition_expression' => 'TRUE', - 'component_code' => 'FRAME-STEEL', - 'quantity_expression' => '(W1 + H1) * 2 / 1000', - 'priority' => 10, - ], - [ - 'name' => '패널 규칙', - 'description' => '패널 개수', - 'condition_expression' => 'TRUE', - 'component_code' => 'PANEL-STEEL', - 'quantity_expression' => 'CEIL(area)', - 'priority' => 20, - ] - ); - } - - /** - * High priority rule. - */ - public function highPriority(): static - { - return $this->state(fn (array $attributes) => [ - 'priority' => $this->faker->numberBetween(1, 10), - ]); - } - - /** - * Low priority rule. - */ - public function lowPriority(): static - { - return $this->state(fn (array $attributes) => [ - 'priority' => $this->faker->numberBetween(90, 100), - ]); - } - - /** - * Active rule. - */ - public function active(): static - { - return $this->state(fn (array $attributes) => [ - 'is_active' => true, - ]); - } - - /** - * Inactive rule. - */ - public function inactive(): static - { - return $this->state(fn (array $attributes) => [ - 'is_active' => false, - ]); - } -} \ No newline at end of file diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php deleted file mode 100644 index 460f6db..0000000 --- a/database/factories/ModelFactory.php +++ /dev/null @@ -1,96 +0,0 @@ - - */ -class ModelFactory extends Factory -{ - protected $model = Model::class; - - /** - * Define the model's default state. - * - * @return array - */ - public function definition(): array - { - return [ - 'tenant_id' => 1, - 'code' => 'KSS' . $this->faker->numberBetween(10, 99), - 'name' => $this->faker->words(3, true), - 'description' => $this->faker->sentence(), - 'status' => $this->faker->randomElement(['DRAFT', 'RELEASED', 'ARCHIVED']), - 'product_family' => $this->faker->randomElement(['SCREEN', 'STEEL']), - 'is_active' => true, - 'created_by' => 1, - 'updated_by' => 1, - ]; - } - - /** - * Indicate that the model is active. - */ - public function active(): static - { - return $this->state(fn (array $attributes) => [ - 'is_active' => true, - ]); - } - - /** - * Indicate that the model is inactive. - */ - public function inactive(): static - { - return $this->state(fn (array $attributes) => [ - 'is_active' => false, - ]); - } - - /** - * Indicate that the model is released. - */ - public function released(): static - { - return $this->state(fn (array $attributes) => [ - 'status' => 'RELEASED', - ]); - } - - /** - * Indicate that the model is draft. - */ - public function draft(): static - { - return $this->state(fn (array $attributes) => [ - 'status' => 'DRAFT', - ]); - } - - /** - * Screen type model. - */ - public function screen(): static - { - return $this->state(fn (array $attributes) => [ - 'product_family' => 'SCREEN', - 'code' => 'KSS' . $this->faker->numberBetween(10, 99), - ]); - } - - /** - * Steel type model. - */ - public function steel(): static - { - return $this->state(fn (array $attributes) => [ - 'product_family' => 'STEEL', - 'code' => 'KST' . $this->faker->numberBetween(10, 99), - ]); - } -} \ No newline at end of file diff --git a/database/factories/ModelFormulaFactory.php b/database/factories/ModelFormulaFactory.php deleted file mode 100644 index 18c25fb..0000000 --- a/database/factories/ModelFormulaFactory.php +++ /dev/null @@ -1,151 +0,0 @@ - - */ -class ModelFormulaFactory extends Factory -{ - protected $model = ModelFormula::class; - - /** - * Define the model's default state. - * - * @return array - */ - public function definition(): array - { - return [ - 'tenant_id' => 1, - 'model_id' => Model::factory(), - 'name' => $this->faker->word(), - 'expression' => 'W0 * H0', - 'description' => $this->faker->sentence(), - 'return_type' => 'NUMBER', - 'sort_order' => $this->faker->numberBetween(1, 10), - 'is_active' => true, - 'created_by' => 1, - 'updated_by' => 1, - ]; - } - - /** - * Screen model formulas. - */ - public function screenFormulas(): static - { - return $this->sequence( - [ - 'name' => 'W1', - 'expression' => 'W0 + 120', - 'description' => '최종 가로 크기', - 'return_type' => 'NUMBER', - 'sort_order' => 1, - ], - [ - 'name' => 'H1', - 'expression' => 'H0 + 100', - 'description' => '최종 세로 크기', - 'return_type' => 'NUMBER', - 'sort_order' => 2, - ], - [ - 'name' => 'area', - 'expression' => 'W1 * H1 / 1000000', - 'description' => '면적 (㎡)', - 'return_type' => 'NUMBER', - 'sort_order' => 3, - ], - [ - 'name' => 'weight', - 'expression' => 'area * 15', - 'description' => '중량 (kg)', - 'return_type' => 'NUMBER', - 'sort_order' => 4, - ], - [ - 'name' => 'motor', - 'expression' => 'IF(area <= 3, "0.5HP", IF(area <= 6, "1HP", "2HP"))', - 'description' => '모터 용량', - 'return_type' => 'STRING', - 'sort_order' => 5, - ], - [ - 'name' => 'bracket', - 'expression' => 'CEIL(W1 / 600)', - 'description' => '브라켓 개수', - 'return_type' => 'NUMBER', - 'sort_order' => 6, - ], - [ - 'name' => 'guide', - 'expression' => 'H1 / 1000 * 2', - 'description' => '가이드 길이 (m)', - 'return_type' => 'NUMBER', - 'sort_order' => 7, - ] - ); - } - - /** - * Steel model formulas. - */ - public function steelFormulas(): static - { - return $this->sequence( - [ - 'name' => 'W1', - 'expression' => 'W0 + 50', - 'description' => '최종 가로 크기', - 'return_type' => 'NUMBER', - 'sort_order' => 1, - ], - [ - 'name' => 'H1', - 'expression' => 'H0 + 50', - 'description' => '최종 세로 크기', - 'return_type' => 'NUMBER', - 'sort_order' => 2, - ], - [ - 'name' => 'area', - 'expression' => 'W1 * H1 / 1000000', - 'description' => '면적 (㎡)', - 'return_type' => 'NUMBER', - 'sort_order' => 3, - ], - [ - 'name' => 'weight', - 'expression' => 'area * thickness * 7.85', - 'description' => '중량 (kg)', - 'return_type' => 'NUMBER', - 'sort_order' => 4, - ] - ); - } - - /** - * Number type formula. - */ - public function number(): static - { - return $this->state(fn (array $attributes) => [ - 'return_type' => 'NUMBER', - ]); - } - - /** - * String type formula. - */ - public function string(): static - { - return $this->state(fn (array $attributes) => [ - 'return_type' => 'STRING', - ]); - } -} \ No newline at end of file diff --git a/database/factories/ModelParameterFactory.php b/database/factories/ModelParameterFactory.php deleted file mode 100644 index a8193db..0000000 --- a/database/factories/ModelParameterFactory.php +++ /dev/null @@ -1,173 +0,0 @@ - - */ -class ModelParameterFactory extends Factory -{ - protected $model = ModelParameter::class; - - /** - * Define the model's default state. - * - * @return array - */ - public function definition(): array - { - return [ - 'tenant_id' => 1, - 'model_id' => Model::factory(), - 'name' => $this->faker->word(), - 'label' => $this->faker->words(2, true), - 'type' => $this->faker->randomElement(['NUMBER', 'SELECT', 'BOOLEAN']), - 'default_value' => '0', - 'validation_rules' => json_encode(['required' => true]), - 'options' => null, - 'sort_order' => $this->faker->numberBetween(1, 10), - 'is_required' => true, - 'is_active' => true, - 'created_by' => 1, - 'updated_by' => 1, - ]; - } - - /** - * Screen model parameters. - */ - public function screenParameters(): static - { - return $this->sequence( - [ - 'name' => 'W0', - 'label' => '가로(mm)', - 'type' => 'NUMBER', - 'default_value' => '1000', - 'validation_rules' => json_encode(['required' => true, 'numeric' => true, 'min' => 500, 'max' => 3000]), - 'sort_order' => 1, - ], - [ - 'name' => 'H0', - 'label' => '세로(mm)', - 'type' => 'NUMBER', - 'default_value' => '800', - 'validation_rules' => json_encode(['required' => true, 'numeric' => true, 'min' => 400, 'max' => 2000]), - 'sort_order' => 2, - ], - [ - 'name' => 'screen_type', - 'label' => '스크린 타입', - 'type' => 'SELECT', - 'default_value' => 'SCREEN', - 'validation_rules' => json_encode(['required' => true, 'in' => ['SCREEN', 'SLAT']]), - 'options' => json_encode([ - ['value' => 'SCREEN', 'label' => '스크린'], - ['value' => 'SLAT', 'label' => '슬라트'] - ]), - 'sort_order' => 3, - ], - [ - 'name' => 'install_type', - 'label' => '설치 방식', - 'type' => 'SELECT', - 'default_value' => 'WALL', - 'validation_rules' => json_encode(['required' => true, 'in' => ['WALL', 'SIDE', 'MIXED']]), - 'options' => json_encode([ - ['value' => 'WALL', 'label' => '벽면 설치'], - ['value' => 'SIDE', 'label' => '측면 설치'], - ['value' => 'MIXED', 'label' => '혼합 설치'] - ]), - 'sort_order' => 4, - ], - [ - 'name' => 'power_source', - 'label' => '전원', - 'type' => 'SELECT', - 'default_value' => 'AC', - 'validation_rules' => json_encode(['required' => true, 'in' => ['AC', 'DC', 'MANUAL']]), - 'options' => json_encode([ - ['value' => 'AC', 'label' => 'AC 전원'], - ['value' => 'DC', 'label' => 'DC 전원'], - ['value' => 'MANUAL', 'label' => '수동'] - ]), - 'sort_order' => 5, - ] - ); - } - - /** - * Steel model parameters. - */ - public function steelParameters(): static - { - return $this->sequence( - [ - 'name' => 'W0', - 'label' => '가로(mm)', - 'type' => 'NUMBER', - 'default_value' => '1200', - 'validation_rules' => json_encode(['required' => true, 'numeric' => true, 'min' => 800, 'max' => 4000]), - 'sort_order' => 1, - ], - [ - 'name' => 'H0', - 'label' => '세로(mm)', - 'type' => 'NUMBER', - 'default_value' => '1000', - 'validation_rules' => json_encode(['required' => true, 'numeric' => true, 'min' => 600, 'max' => 3000]), - 'sort_order' => 2, - ], - [ - 'name' => 'thickness', - 'label' => '두께(mm)', - 'type' => 'NUMBER', - 'default_value' => '50', - 'validation_rules' => json_encode(['required' => true, 'numeric' => true, 'min' => 20, 'max' => 100]), - 'sort_order' => 3, - ] - ); - } - - /** - * Number type parameter. - */ - public function number(): static - { - return $this->state(fn (array $attributes) => [ - 'type' => 'NUMBER', - 'validation_rules' => json_encode(['required' => true, 'numeric' => true]), - 'options' => null, - ]); - } - - /** - * Select type parameter. - */ - public function select(): static - { - return $this->state(fn (array $attributes) => [ - 'type' => 'SELECT', - 'options' => json_encode([ - ['value' => 'option1', 'label' => 'Option 1'], - ['value' => 'option2', 'label' => 'Option 2'], - ]), - ]); - } - - /** - * Boolean type parameter. - */ - public function boolean(): static - { - return $this->state(fn (array $attributes) => [ - 'type' => 'BOOLEAN', - 'default_value' => 'false', - 'options' => null, - ]); - } -} \ No newline at end of file diff --git a/database/migrations/2024_01_15_100000_create_bom_template_groups_table.php b/database/migrations/2024_01_15_100000_create_bom_template_groups_table.php deleted file mode 100644 index d31ed10..0000000 --- a/database/migrations/2024_01_15_100000_create_bom_template_groups_table.php +++ /dev/null @@ -1,50 +0,0 @@ -id(); - $table->unsignedBigInteger('tenant_id')->comment('테넌트 ID'); - $table->unsignedBigInteger('model_id')->comment('모델 ID'); - $table->string('group_name', 100)->comment('그룹명 (본체, 절곡물, 가이드레일 등)'); - $table->unsignedBigInteger('parent_group_id')->nullable()->comment('상위 그룹 ID (계층 구조)'); - $table->integer('display_order')->default(0)->comment('표시 순서'); - $table->text('description')->nullable()->comment('그룹 설명'); - $table->boolean('is_active')->default(true)->comment('활성 여부'); - - // 공통 컬럼 - $table->unsignedBigInteger('created_by')->nullable()->comment('생성자 ID'); - $table->unsignedBigInteger('updated_by')->nullable()->comment('수정자 ID'); - $table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자 ID'); - $table->timestamps(); - $table->softDeletes(); - - // 인덱스 - $table->index(['tenant_id', 'model_id']); - $table->index(['tenant_id', 'model_id', 'parent_group_id']); - $table->unique(['tenant_id', 'model_id', 'group_name'], 'unique_group_per_model'); - - // 외래키 (설계용, 프로덕션에서는 제거 고려) - $table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('cascade'); - $table->foreign('model_id')->references('id')->on('models')->onDelete('cascade'); - $table->foreign('parent_group_id')->references('id')->on('bom_template_groups')->onDelete('set null'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('bom_template_groups'); - } -}; \ No newline at end of file diff --git a/database/migrations/2024_01_15_110000_create_model_parameters_table.php b/database/migrations/2024_01_15_110000_create_model_parameters_table.php deleted file mode 100644 index 0f06617..0000000 --- a/database/migrations/2024_01_15_110000_create_model_parameters_table.php +++ /dev/null @@ -1,55 +0,0 @@ -id(); - $table->unsignedBigInteger('tenant_id')->comment('테넌트 ID'); - $table->unsignedBigInteger('model_id')->comment('모델 ID'); - $table->string('parameter_name', 50)->comment('매개변수명 (W0, H0, screen_type 등)'); - $table->enum('parameter_type', ['INTEGER', 'DECIMAL', 'STRING', 'BOOLEAN'])->comment('매개변수 타입'); - $table->boolean('is_required')->default(true)->comment('필수 여부'); - $table->string('default_value')->nullable()->comment('기본값'); - $table->decimal('min_value', 15, 4)->nullable()->comment('최소값 (숫자형)'); - $table->decimal('max_value', 15, 4)->nullable()->comment('최대값 (숫자형)'); - $table->json('allowed_values')->nullable()->comment('허용값 목록 (선택형)'); - $table->string('unit', 20)->nullable()->comment('단위 (mm, kg, 개 등)'); - $table->text('description')->nullable()->comment('매개변수 설명'); - $table->integer('display_order')->default(0)->comment('표시 순서'); - $table->boolean('is_active')->default(true)->comment('활성 여부'); - - // 공통 컬럼 - $table->unsignedBigInteger('created_by')->nullable()->comment('생성자 ID'); - $table->unsignedBigInteger('updated_by')->nullable()->comment('수정자 ID'); - $table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자 ID'); - $table->timestamps(); - $table->softDeletes(); - - // 인덱스 - $table->index(['tenant_id', 'model_id']); - $table->index(['tenant_id', 'model_id', 'display_order']); - $table->unique(['tenant_id', 'model_id', 'parameter_name'], 'unique_parameter_per_model'); - - // 외래키 (설계용) - $table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('cascade'); - $table->foreign('model_id')->references('id')->on('models')->onDelete('cascade'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('model_parameters'); - } -}; \ No newline at end of file diff --git a/database/migrations/2024_01_15_120000_create_model_formulas_table.php b/database/migrations/2024_01_15_120000_create_model_formulas_table.php deleted file mode 100644 index ceb2a31..0000000 --- a/database/migrations/2024_01_15_120000_create_model_formulas_table.php +++ /dev/null @@ -1,53 +0,0 @@ -id(); - $table->unsignedBigInteger('tenant_id')->comment('테넌트 ID'); - $table->unsignedBigInteger('model_id')->comment('모델 ID'); - $table->string('formula_name', 50)->comment('공식명 (W1, H1, area, weight 등)'); - $table->text('formula_expression')->comment('공식 표현식 (W0 + 100, W0 * H0 / 1000000 등)'); - $table->enum('result_type', ['INTEGER', 'DECIMAL'])->default('DECIMAL')->comment('결과 타입'); - $table->integer('decimal_places')->default(2)->comment('소수점 자릿수'); - $table->string('unit', 20)->nullable()->comment('결과 단위 (mm, kg, m² 등)'); - $table->text('description')->nullable()->comment('공식 설명'); - $table->integer('calculation_order')->default(0)->comment('계산 순서 (의존성 관리)'); - $table->json('dependencies')->nullable()->comment('의존 변수 목록'); - $table->boolean('is_active')->default(true)->comment('활성 여부'); - - // 공통 컬럼 - $table->unsignedBigInteger('created_by')->nullable()->comment('생성자 ID'); - $table->unsignedBigInteger('updated_by')->nullable()->comment('수정자 ID'); - $table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자 ID'); - $table->timestamps(); - $table->softDeletes(); - - // 인덱스 - $table->index(['tenant_id', 'model_id']); - $table->index(['tenant_id', 'model_id', 'calculation_order']); - $table->unique(['tenant_id', 'model_id', 'formula_name'], 'unique_formula_per_model'); - - // 외래키 (설계용) - $table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('cascade'); - $table->foreign('model_id')->references('id')->on('models')->onDelete('cascade'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('model_formulas'); - } -}; \ No newline at end of file diff --git a/database/migrations/2024_01_15_130000_create_bom_condition_rules_table.php b/database/migrations/2024_01_15_130000_create_bom_condition_rules_table.php deleted file mode 100644 index 7fab728..0000000 --- a/database/migrations/2024_01_15_130000_create_bom_condition_rules_table.php +++ /dev/null @@ -1,57 +0,0 @@ -id(); - $table->unsignedBigInteger('tenant_id')->comment('테넌트 ID'); - $table->unsignedBigInteger('model_id')->comment('모델 ID'); - $table->string('rule_name', 100)->comment('규칙명'); - $table->text('condition_expression')->comment('조건 표현식 (screen_type == "SCREEN", W0 > 1000 등)'); - $table->enum('action_type', ['INCLUDE_PRODUCT', 'EXCLUDE_PRODUCT', 'SET_QUANTITY', 'MODIFY_QUANTITY'])->comment('액션 타입'); - $table->unsignedBigInteger('target_product_id')->nullable()->comment('대상 제품 ID'); - $table->unsignedBigInteger('target_group_id')->nullable()->comment('대상 그룹 ID'); - $table->text('quantity_formula')->nullable()->comment('수량 공식 (ceiling(W0/1000), 2 * count 등)'); - $table->decimal('fixed_quantity', 15, 4)->nullable()->comment('고정 수량'); - $table->text('description')->nullable()->comment('규칙 설명'); - $table->integer('priority')->default(100)->comment('실행 우선순위 (낮을수록 먼저 실행)'); - $table->boolean('is_active')->default(true)->comment('활성 여부'); - - // 공통 컬럼 - $table->unsignedBigInteger('created_by')->nullable()->comment('생성자 ID'); - $table->unsignedBigInteger('updated_by')->nullable()->comment('수정자 ID'); - $table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자 ID'); - $table->timestamps(); - $table->softDeletes(); - - // 인덱스 - $table->index(['tenant_id', 'model_id']); - $table->index(['tenant_id', 'model_id', 'priority']); - $table->index(['tenant_id', 'target_product_id']); - $table->index(['tenant_id', 'target_group_id']); - - // 외래키 (설계용) - $table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('cascade'); - $table->foreign('model_id')->references('id')->on('models')->onDelete('cascade'); - $table->foreign('target_product_id')->references('id')->on('products')->onDelete('cascade'); - $table->foreign('target_group_id')->references('id')->on('bom_template_groups')->onDelete('cascade'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('bom_condition_rules'); - } -}; \ No newline at end of file diff --git a/database/migrations/2024_01_15_140000_create_product_parameters_table.php b/database/migrations/2024_01_15_140000_create_product_parameters_table.php deleted file mode 100644 index f2687d9..0000000 --- a/database/migrations/2024_01_15_140000_create_product_parameters_table.php +++ /dev/null @@ -1,52 +0,0 @@ -id(); - $table->unsignedBigInteger('tenant_id')->comment('테넌트 ID'); - $table->unsignedBigInteger('model_id')->comment('모델 ID'); - $table->unsignedBigInteger('model_version_id')->nullable()->comment('모델 버전 ID'); - $table->string('product_code', 100)->nullable()->comment('제품 코드 (참조용)'); - $table->json('parameter_values')->comment('매개변수 값들 {W0: 1200, H0: 800, screen_type: "SCREEN"}'); - $table->text('notes')->nullable()->comment('비고'); - $table->enum('status', ['DRAFT', 'CALCULATED', 'APPROVED'])->default('DRAFT')->comment('상태'); - - // 공통 컬럼 - $table->unsignedBigInteger('created_by')->nullable()->comment('생성자 ID'); - $table->unsignedBigInteger('updated_by')->nullable()->comment('수정자 ID'); - $table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자 ID'); - $table->timestamps(); - $table->softDeletes(); - - // 인덱스 - $table->index(['tenant_id', 'model_id']); - $table->index(['tenant_id', 'model_id', 'model_version_id']); - $table->index(['tenant_id', 'product_code']); - $table->index(['tenant_id', 'status']); - $table->index(['created_at']); - - // 외래키 (설계용) - $table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('cascade'); - $table->foreign('model_id')->references('id')->on('models')->onDelete('cascade'); - $table->foreign('model_version_id')->references('id')->on('model_versions')->onDelete('set null'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('product_parameters'); - } -}; \ No newline at end of file diff --git a/database/migrations/2024_01_15_150000_create_product_calculated_values_table.php b/database/migrations/2024_01_15_150000_create_product_calculated_values_table.php deleted file mode 100644 index 3e41ac8..0000000 --- a/database/migrations/2024_01_15_150000_create_product_calculated_values_table.php +++ /dev/null @@ -1,54 +0,0 @@ -id(); - $table->unsignedBigInteger('tenant_id')->comment('테넌트 ID'); - $table->unsignedBigInteger('product_parameter_id')->comment('제품 매개변수 ID'); - $table->json('calculated_values')->comment('계산된 값들 {W1: 1300, H1: 900, area: 1.17, weight: 45.2}'); - $table->json('bom_snapshot')->nullable()->comment('계산된 BOM 결과 스냅샷'); - $table->decimal('total_cost', 15, 4)->nullable()->comment('총 비용'); - $table->decimal('total_weight', 15, 4)->nullable()->comment('총 중량'); - $table->integer('total_items')->nullable()->comment('총 아이템 수'); - $table->timestamp('calculation_date')->useCurrent()->comment('계산 일시'); - $table->boolean('is_valid')->default(true)->comment('계산 유효성 여부'); - $table->text('calculation_errors')->nullable()->comment('계산 오류 메시지'); - $table->string('calculation_version', 50)->nullable()->comment('계산 엔진 버전'); - - // 공통 컬럼 - $table->unsignedBigInteger('created_by')->nullable()->comment('생성자 ID'); - $table->unsignedBigInteger('updated_by')->nullable()->comment('수정자 ID'); - $table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자 ID'); - $table->timestamps(); - $table->softDeletes(); - - // 인덱스 - $table->index(['tenant_id', 'product_parameter_id']); - $table->index(['tenant_id', 'is_valid']); - $table->index(['calculation_date']); - $table->unique(['tenant_id', 'product_parameter_id'], 'unique_calculation_per_parameter'); - - // 외래키 (설계용) - $table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('cascade'); - $table->foreign('product_parameter_id')->references('id')->on('product_parameters')->onDelete('cascade'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('product_calculated_values'); - } -}; \ No newline at end of file diff --git a/database/migrations/2024_01_15_160000_add_group_id_to_bom_template_items_table.php b/database/migrations/2024_01_15_160000_add_group_id_to_bom_template_items_table.php deleted file mode 100644 index f988048..0000000 --- a/database/migrations/2024_01_15_160000_add_group_id_to_bom_template_items_table.php +++ /dev/null @@ -1,52 +0,0 @@ -unsignedBigInteger('group_id')->nullable()->after('bom_template_id')->comment('BOM 그룹 ID'); - } - - // 나머지 컬럼들 추가 - if (!Schema::hasColumn('bom_template_items', 'is_conditional')) { - $table->boolean('is_conditional')->default(false)->after('qty')->comment('조건부 아이템 여부'); - } - if (!Schema::hasColumn('bom_template_items', 'condition_expression')) { - $table->text('condition_expression')->nullable()->after('is_conditional')->comment('조건 표현식'); - } - if (!Schema::hasColumn('bom_template_items', 'quantity_formula')) { - $table->text('quantity_formula')->nullable()->after('condition_expression')->comment('수량 계산 공식'); - } - }); - - // 인덱스와 외래키는 별도로 처리 - Schema::table('bom_template_items', function (Blueprint $table) { - if (!Schema::hasIndex('bom_template_items', ['tenant_id', 'group_id'])) { - $table->index(['tenant_id', 'group_id']); - } - // 외래키 제약조건은 생산 환경에서 제거 (SAM 규칙) - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('bom_template_items', function (Blueprint $table) { - $table->dropForeign(['group_id']); - $table->dropIndex(['tenant_id', 'group_id']); - $table->dropColumn(['group_id', 'is_conditional', 'condition_expression', 'quantity_formula']); - }); - } -}; \ No newline at end of file diff --git a/database/migrations/2024_01_15_170000_add_parametric_fields_to_products_table.php b/database/migrations/2024_01_15_170000_add_parametric_fields_to_products_table.php deleted file mode 100644 index 60601e9..0000000 --- a/database/migrations/2024_01_15_170000_add_parametric_fields_to_products_table.php +++ /dev/null @@ -1,40 +0,0 @@ -boolean('is_parametric')->default(false)->after('product_type')->comment('매개변수 기반 제품 여부'); - $table->unsignedBigInteger('base_model_id')->nullable()->after('is_parametric')->comment('기반 모델 ID (매개변수 제품용)'); - $table->json('parameter_values')->nullable()->after('base_model_id')->comment('매개변수 값들 (매개변수 제품용)'); - $table->json('calculated_values')->nullable()->after('parameter_values')->comment('계산된 값들 (매개변수 제품용)'); - - // 인덱스 추가 - $table->index(['tenant_id', 'is_parametric']); - $table->index(['tenant_id', 'base_model_id']); - - // 외래키 제약조건은 생산 환경에서 제거 (SAM 규칙) - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('products', function (Blueprint $table) { - $table->dropForeign(['base_model_id']); - $table->dropIndex(['tenant_id', 'is_parametric']); - $table->dropIndex(['tenant_id', 'base_model_id']); - $table->dropColumn(['is_parametric', 'base_model_id', 'parameter_values', 'calculated_values']); - }); - } -}; \ No newline at end of file diff --git a/database/seeders/KSS01ModelSeeder.php b/database/seeders/KSS01ModelSeeder.php deleted file mode 100644 index 255a464..0000000 --- a/database/seeders/KSS01ModelSeeder.php +++ /dev/null @@ -1,573 +0,0 @@ - 'KSS_DEMO'], - [ - 'name' => 'KSS Demo Tenant', - 'description' => 'Demonstration tenant for KSS01 model', - 'is_active' => true - ] - ); - - // Get or create test user - $user = User::firstOrCreate( - ['email' => 'demo@kss01.com'], - [ - 'name' => 'KSS01 Demo User', - 'password' => Hash::make('kss01demo'), - 'email_verified_at' => now() - ] - ); - - // Associate user with tenant - if (!$user->tenants()->where('tenant_id', $tenant->id)->exists()) { - $user->tenants()->attach($tenant->id, [ - 'is_active' => true, - 'is_default' => true - ]); - } - - // Create screen door category - $category = Category::firstOrCreate( - [ - 'tenant_id' => $tenant->id, - 'code' => 'SCREEN_DOORS' - ], - [ - 'name' => 'Screen Doors', - 'description' => 'Automatic screen door systems', - 'is_active' => true - ] - ); - - // Create KSS01 specific materials and products - $this->createKSS01Materials($tenant); - $this->createKSS01Products($tenant); - - // Create the KSS01 model - $kss01Model = DesignModel::firstOrCreate( - [ - 'tenant_id' => $tenant->id, - 'code' => 'KSS01' - ], - [ - 'name' => 'KSS01 Screen Door System', - 'category_id' => $category->id, - 'lifecycle' => 'ACTIVE', - 'description' => 'Production KSS01 automatic screen door system with parametric BOM', - 'is_active' => true - ] - ); - - // Create KSS01 parameters (matching the codebase expectations) - $this->createKSS01Parameters($tenant, $kss01Model); - - // Create KSS01 formulas (matching the service expectations) - $this->createKSS01Formulas($tenant, $kss01Model); - - // Create KSS01 condition rules - $this->createKSS01ConditionRules($tenant, $kss01Model); - - // Create BOM template - $this->createKSS01BomTemplate($tenant, $kss01Model); - - $this->command->info('KSS01 model seeded successfully!'); - $this->command->info('Demo tenant: ' . $tenant->code); - $this->command->info('Demo user: ' . $user->email . ' / password: kss01demo'); - } - - /** - * Create KSS01 specific materials - */ - private function createKSS01Materials(Tenant $tenant): void - { - $materials = [ - [ - 'code' => 'FABRIC_KSS01', - 'name' => 'KSS01 Fabric Screen', - 'description' => 'Specialized fabric for KSS01 screen door', - 'unit' => 'M2', - 'density' => 0.35, - 'color' => 'CHARCOAL' - ], - [ - 'code' => 'STEEL_KSS01', - 'name' => 'KSS01 Steel Mesh', - 'description' => 'Security steel mesh for KSS01', - 'unit' => 'M2', - 'density' => 2.8, - 'color' => 'GRAPHITE' - ], - [ - 'code' => 'RAIL_KSS01_GUIDE', - 'name' => 'KSS01 Guide Rail', - 'description' => 'Precision guide rail for KSS01 system', - 'unit' => 'M', - 'density' => 1.2, - 'color' => 'ANODIZED' - ], - [ - 'code' => 'CABLE_KSS01_LIFT', - 'name' => 'KSS01 Lift Cable', - 'description' => 'High-strength lift cable for KSS01', - 'unit' => 'M', - 'density' => 0.15, - 'color' => 'STAINLESS' - ], - [ - 'code' => 'SEAL_KSS01_WEATHER', - 'name' => 'KSS01 Weather Seal', - 'description' => 'Weather sealing strip for KSS01', - 'unit' => 'M', - 'density' => 0.08, - 'color' => 'BLACK' - ] - ]; - - foreach ($materials as $materialData) { - Material::firstOrCreate( - [ - 'tenant_id' => $tenant->id, - 'code' => $materialData['code'] - ], - array_merge($materialData, [ - 'is_active' => true, - 'created_by' => 1 - ]) - ); - } - } - - /** - * Create KSS01 specific products - */ - private function createKSS01Products(Tenant $tenant): void - { - $products = [ - [ - 'code' => 'BRACKET_KSS01_WALL', - 'name' => 'KSS01 Wall Bracket', - 'description' => 'Heavy-duty wall mounting bracket for KSS01', - 'unit' => 'EA', - 'weight' => 0.8, - 'color' => 'POWDER_COATED' - ], - [ - 'code' => 'BRACKET_KSS01_CEILING', - 'name' => 'KSS01 Ceiling Bracket', - 'description' => 'Ceiling mounting bracket for KSS01', - 'unit' => 'EA', - 'weight' => 1.0, - 'color' => 'POWDER_COATED' - ], - [ - 'code' => 'MOTOR_KSS01_STD', - 'name' => 'KSS01 Standard Motor', - 'description' => 'Standard 12V motor for KSS01 (up to 4m²)', - 'unit' => 'EA', - 'weight' => 2.8, - 'color' => 'BLACK' - ], - [ - 'code' => 'MOTOR_KSS01_HEAVY', - 'name' => 'KSS01 Heavy Duty Motor', - 'description' => 'Heavy duty 24V motor for large KSS01 systems', - 'unit' => 'EA', - 'weight' => 4.2, - 'color' => 'BLACK' - ], - [ - 'code' => 'CONTROLLER_KSS01', - 'name' => 'KSS01 Smart Controller', - 'description' => 'Smart controller with app connectivity for KSS01', - 'unit' => 'EA', - 'weight' => 0.25, - 'color' => 'WHITE' - ], - [ - 'code' => 'CASE_KSS01_HEAD', - 'name' => 'KSS01 Head Case', - 'description' => 'Aluminum head case housing for KSS01', - 'unit' => 'EA', - 'weight' => 2.5, - 'color' => 'ANODIZED' - ], - [ - 'code' => 'BOTTOM_BAR_KSS01', - 'name' => 'KSS01 Bottom Bar', - 'description' => 'Weighted bottom bar for KSS01 screen', - 'unit' => 'EA', - 'weight' => 1.5, - 'color' => 'ANODIZED' - ], - [ - 'code' => 'PIPE_KSS01_ROLLER', - 'name' => 'KSS01 Roller Pipe', - 'description' => 'Precision roller pipe for KSS01 screen', - 'unit' => 'EA', - 'weight' => 1.0, - 'color' => 'ANODIZED' - ] - ]; - - foreach ($products as $productData) { - Product::firstOrCreate( - [ - 'tenant_id' => $tenant->id, - 'code' => $productData['code'] - ], - array_merge($productData, [ - 'is_active' => true, - 'created_by' => 1 - ]) - ); - } - } - - /** - * Create KSS01 parameters (matching BomResolverService expectations) - */ - private function createKSS01Parameters(Tenant $tenant, DesignModel $model): void - { - $parameters = [ - [ - 'parameter_name' => 'W0', - 'parameter_type' => 'NUMBER', - 'is_required' => true, - 'default_value' => '800', - 'min_value' => 600, - 'max_value' => 3000, - 'unit' => 'mm', - 'description' => 'Opening width (clear opening)', - 'sort_order' => 1 - ], - [ - 'parameter_name' => 'H0', - 'parameter_type' => 'NUMBER', - 'is_required' => true, - 'default_value' => '600', - 'min_value' => 400, - 'max_value' => 2500, - 'unit' => 'mm', - 'description' => 'Opening height (clear opening)', - 'sort_order' => 2 - ], - [ - 'parameter_name' => 'screen_type', - 'parameter_type' => 'SELECT', - 'is_required' => true, - 'default_value' => 'FABRIC', - 'options' => ['FABRIC', 'STEEL'], - 'description' => 'Screen material type', - 'sort_order' => 3 - ], - [ - 'parameter_name' => 'install_type', - 'parameter_type' => 'SELECT', - 'is_required' => true, - 'default_value' => 'WALL', - 'options' => ['WALL', 'CEILING', 'RECESSED'], - 'description' => 'Installation method', - 'sort_order' => 4 - ], - [ - 'parameter_name' => 'power_source', - 'parameter_type' => 'SELECT', - 'is_required' => false, - 'default_value' => 'AC', - 'options' => ['AC', 'DC', 'BATTERY'], - 'description' => 'Power source type', - 'sort_order' => 5 - ] - ]; - - foreach ($parameters as $paramData) { - ModelParameter::firstOrCreate( - [ - 'tenant_id' => $tenant->id, - 'model_id' => $model->id, - 'parameter_name' => $paramData['parameter_name'] - ], - $paramData - ); - } - } - - /** - * Create KSS01 formulas (matching BomResolverService::resolveKSS01) - */ - private function createKSS01Formulas(Tenant $tenant, DesignModel $model): void - { - $formulas = [ - [ - 'formula_name' => 'W1', - 'expression' => 'W0 + 100', - 'description' => 'Overall width (opening + frame)', - 'sort_order' => 1 - ], - [ - 'formula_name' => 'H1', - 'expression' => 'H0 + 100', - 'description' => 'Overall height (opening + frame)', - 'sort_order' => 2 - ], - [ - 'formula_name' => 'area', - 'expression' => '(W1 * H1) / 1000000', - 'description' => 'Total area in square meters', - 'sort_order' => 3 - ], - [ - 'formula_name' => 'weight', - 'expression' => 'area * 8 + W1 / 1000 * 2.5', - 'description' => 'Estimated total weight in kg', - 'sort_order' => 4 - ], - [ - 'formula_name' => 'motor_load', - 'expression' => 'weight * 1.5 + area * 3', - 'description' => 'Motor load calculation', - 'sort_order' => 5 - ], - [ - 'formula_name' => 'bracket_count', - 'expression' => 'if(W1 > 1000, 3, 2)', - 'description' => 'Number of brackets required', - 'sort_order' => 6 - ], - [ - 'formula_name' => 'rail_length', - 'expression' => '(W1 + H1) * 2 / 1000', - 'description' => 'Guide rail length in meters', - 'sort_order' => 7 - ] - ]; - - foreach ($formulas as $formulaData) { - ModelFormula::firstOrCreate( - [ - 'tenant_id' => $tenant->id, - 'model_id' => $model->id, - 'formula_name' => $formulaData['formula_name'] - ], - $formulaData - ); - } - } - - /** - * Create KSS01 condition rules - */ - private function createKSS01ConditionRules(Tenant $tenant, DesignModel $model): void - { - $rules = [ - // Screen material selection - [ - 'rule_name' => 'Fabric Screen Material', - 'condition_expression' => 'screen_type == "FABRIC"', - 'action_type' => 'INCLUDE', - 'target_type' => 'MATERIAL', - 'target_id' => Material::where('tenant_id', $tenant->id)->where('code', 'FABRIC_KSS01')->first()->id, - 'quantity_multiplier' => 'area', - 'description' => 'Include fabric screen material', - 'sort_order' => 1 - ], - [ - 'rule_name' => 'Steel Screen Material', - 'condition_expression' => 'screen_type == "STEEL"', - 'action_type' => 'INCLUDE', - 'target_type' => 'MATERIAL', - 'target_id' => Material::where('tenant_id', $tenant->id)->where('code', 'STEEL_KSS01')->first()->id, - 'quantity_multiplier' => 'area', - 'description' => 'Include steel mesh material', - 'sort_order' => 2 - ], - // Motor selection based on load - [ - 'rule_name' => 'Standard Motor', - 'condition_expression' => 'motor_load <= 20', - 'action_type' => 'INCLUDE', - 'target_type' => 'PRODUCT', - 'target_id' => Product::where('tenant_id', $tenant->id)->where('code', 'MOTOR_KSS01_STD')->first()->id, - 'quantity_multiplier' => 1, - 'description' => 'Standard motor for normal loads', - 'sort_order' => 3 - ], - [ - 'rule_name' => 'Heavy Duty Motor', - 'condition_expression' => 'motor_load > 20', - 'action_type' => 'INCLUDE', - 'target_type' => 'PRODUCT', - 'target_id' => Product::where('tenant_id', $tenant->id)->where('code', 'MOTOR_KSS01_HEAVY')->first()->id, - 'quantity_multiplier' => 1, - 'description' => 'Heavy duty motor for high loads', - 'sort_order' => 4 - ], - // Bracket selection based on installation - [ - 'rule_name' => 'Wall Brackets', - 'condition_expression' => 'install_type == "WALL"', - 'action_type' => 'INCLUDE', - 'target_type' => 'PRODUCT', - 'target_id' => Product::where('tenant_id', $tenant->id)->where('code', 'BRACKET_KSS01_WALL')->first()->id, - 'quantity_multiplier' => 'bracket_count', - 'description' => 'Wall mounting brackets', - 'sort_order' => 5 - ], - [ - 'rule_name' => 'Ceiling Brackets', - 'condition_expression' => 'install_type == "CEILING"', - 'action_type' => 'INCLUDE', - 'target_type' => 'PRODUCT', - 'target_id' => Product::where('tenant_id', $tenant->id)->where('code', 'BRACKET_KSS01_CEILING')->first()->id, - 'quantity_multiplier' => 'bracket_count', - 'description' => 'Ceiling mounting brackets', - 'sort_order' => 6 - ], - // Guide rail - [ - 'rule_name' => 'Guide Rail', - 'condition_expression' => 'true', // Always include - 'action_type' => 'INCLUDE', - 'target_type' => 'MATERIAL', - 'target_id' => Material::where('tenant_id', $tenant->id)->where('code', 'RAIL_KSS01_GUIDE')->first()->id, - 'quantity_multiplier' => 'rail_length', - 'description' => 'Guide rail for screen movement', - 'sort_order' => 7 - ], - // Weather sealing for large openings - [ - 'rule_name' => 'Weather Seal for Large Openings', - 'condition_expression' => 'area > 3.0', - 'action_type' => 'INCLUDE', - 'target_type' => 'MATERIAL', - 'target_id' => Material::where('tenant_id', $tenant->id)->where('code', 'SEAL_KSS01_WEATHER')->first()->id, - 'quantity_multiplier' => 'rail_length', - 'description' => 'Weather sealing for large openings', - 'sort_order' => 8 - ] - ]; - - foreach ($rules as $ruleData) { - BomConditionRule::firstOrCreate( - [ - 'tenant_id' => $tenant->id, - 'model_id' => $model->id, - 'rule_name' => $ruleData['rule_name'] - ], - $ruleData - ); - } - } - - /** - * Create KSS01 BOM template - */ - private function createKSS01BomTemplate(Tenant $tenant, DesignModel $model): void - { - // Create model version - $modelVersion = ModelVersion::firstOrCreate( - [ - 'tenant_id' => $tenant->id, - 'model_id' => $model->id, - 'version_no' => '1.0' - ], - [ - 'status' => 'RELEASED', - 'description' => 'Initial release of KSS01', - 'created_by' => 1 - ] - ); - - // Create BOM template - $bomTemplate = BomTemplate::firstOrCreate( - [ - 'tenant_id' => $tenant->id, - 'model_version_id' => $modelVersion->id, - 'name' => 'KSS01 Base BOM' - ], - [ - 'description' => 'Base BOM template for KSS01 screen door system', - 'is_active' => true, - 'created_by' => 1 - ] - ); - - // Create base BOM items (always included components) - $baseItems = [ - [ - 'ref_type' => 'PRODUCT', - 'ref_id' => Product::where('tenant_id', $tenant->id)->where('code', 'CONTROLLER_KSS01')->first()->id, - 'quantity' => 1, - 'waste_rate' => 0, - 'order' => 1 - ], - [ - 'ref_type' => 'PRODUCT', - 'ref_id' => Product::where('tenant_id', $tenant->id)->where('code', 'CASE_KSS01_HEAD')->first()->id, - 'quantity' => 1, - 'waste_rate' => 0, - 'order' => 2 - ], - [ - 'ref_type' => 'PRODUCT', - 'ref_id' => Product::where('tenant_id', $tenant->id)->where('code', 'BOTTOM_BAR_KSS01')->first()->id, - 'quantity' => 1, - 'waste_rate' => 0, - 'order' => 3 - ], - [ - 'ref_type' => 'PRODUCT', - 'ref_id' => Product::where('tenant_id', $tenant->id)->where('code', 'PIPE_KSS01_ROLLER')->first()->id, - 'quantity' => 1, - 'waste_rate' => 0, - 'order' => 4 - ], - [ - 'ref_type' => 'MATERIAL', - 'ref_id' => Material::where('tenant_id', $tenant->id)->where('code', 'CABLE_KSS01_LIFT')->first()->id, - 'quantity' => 2, // Two cables - 'waste_rate' => 10, - 'order' => 5 - ] - ]; - - foreach ($baseItems as $itemData) { - BomTemplateItem::firstOrCreate( - [ - 'bom_template_id' => $bomTemplate->id, - 'ref_type' => $itemData['ref_type'], - 'ref_id' => $itemData['ref_id'], - 'order' => $itemData['order'] - ], - $itemData - ); - } - } -} diff --git a/database/seeders/ParameterBasedBomTestSeeder.php b/database/seeders/ParameterBasedBomTestSeeder.php deleted file mode 100644 index 355747c..0000000 --- a/database/seeders/ParameterBasedBomTestSeeder.php +++ /dev/null @@ -1,158 +0,0 @@ -screen() - ->released() - ->create([ - 'code' => 'KSS01', - 'name' => '스크린 블라인드 표준형', - 'description' => '매개변수 기반 스크린 블라인드', - 'product_family' => 'SCREEN', - ]); - - // Create parameters for KSS01 - ModelParameter::factory() - ->screenParameters() - ->create(['model_id' => $kss01->id]); - - // Create formulas for KSS01 - ModelFormula::factory() - ->screenFormulas() - ->create(['model_id' => $kss01->id]); - - // Create condition rules for KSS01 - BomConditionRule::factory() - ->screenRules() - ->create(['model_id' => $kss01->id]); - - // Create KST01 (Steel) Model - $kst01 = Model::factory() - ->steel() - ->released() - ->create([ - 'code' => 'KST01', - 'name' => '스틸 도어 표준형', - 'description' => '매개변수 기반 스틸 도어', - 'product_family' => 'STEEL', - ]); - - // Create parameters for KST01 - ModelParameter::factory() - ->steelParameters() - ->create(['model_id' => $kst01->id]); - - // Create formulas for KST01 - ModelFormula::factory() - ->steelFormulas() - ->create(['model_id' => $kst01->id]); - - // Create condition rules for KST01 - BomConditionRule::factory() - ->steelRules() - ->create(['model_id' => $kst01->id]); - - // Create additional test models for edge cases - $testModels = [ - // Draft model for testing incomplete states - [ - 'code' => 'TEST-DRAFT', - 'name' => '테스트 드래프트 모델', - 'status' => 'DRAFT', - 'product_family' => 'SCREEN', - ], - // Inactive model for testing filtering - [ - 'code' => 'TEST-INACTIVE', - 'name' => '테스트 비활성 모델', - 'status' => 'RELEASED', - 'product_family' => 'SCREEN', - 'is_active' => false, - ], - // Complex model for performance testing - [ - 'code' => 'COMPLEX-01', - 'name' => '복합 테스트 모델', - 'status' => 'RELEASED', - 'product_family' => 'SCREEN', - ], - ]; - - foreach ($testModels as $modelData) { - $model = Model::factory()->create($modelData); - - // Add basic parameters - ModelParameter::factory() - ->count(3) - ->number() - ->create(['model_id' => $model->id]); - - // Add basic formulas - ModelFormula::factory() - ->count(2) - ->number() - ->create(['model_id' => $model->id]); - - // Add condition rules - BomConditionRule::factory() - ->count(2) - ->create(['model_id' => $model->id]); - } - - // Create models for multi-tenant testing - $tenant2Models = Model::factory() - ->count(3) - ->screen() - ->create(['tenant_id' => 2]); - - foreach ($tenant2Models as $model) { - ModelParameter::factory() - ->count(2) - ->create(['model_id' => $model->id, 'tenant_id' => 2]); - - ModelFormula::factory() - ->count(1) - ->create(['model_id' => $model->id, 'tenant_id' => 2]); - } - - // Create performance test data (large dataset) - if (app()->environment('testing')) { - $performanceModel = Model::factory() - ->screen() - ->create([ - 'code' => 'PERF-TEST', - 'name' => '성능 테스트 모델', - ]); - - // Large number of parameters for performance testing - ModelParameter::factory() - ->count(50) - ->create(['model_id' => $performanceModel->id]); - - // Large number of formulas - ModelFormula::factory() - ->count(30) - ->create(['model_id' => $performanceModel->id]); - - // Large number of condition rules - BomConditionRule::factory() - ->count(100) - ->create(['model_id' => $performanceModel->id]); - } - } -} \ No newline at end of file diff --git a/database/seeders/ParametricBomSeeder.php b/database/seeders/ParametricBomSeeder.php deleted file mode 100644 index 26b8baf..0000000 --- a/database/seeders/ParametricBomSeeder.php +++ /dev/null @@ -1,820 +0,0 @@ - 'TEST_TENANT'], - [ - 'name' => 'Test Tenant for Parametric BOM', - 'description' => 'Tenant for testing parametric BOM system', - 'is_active' => true - ] - ); - - // Create test user - $user = User::firstOrCreate( - ['email' => 'test@parametric-bom.com'], - [ - 'name' => 'Parametric BOM Test User', - 'password' => Hash::make('password'), - 'email_verified_at' => now() - ] - ); - - // Associate user with tenant - if (!$user->tenants()->where('tenant_id', $tenant->id)->exists()) { - $user->tenants()->attach($tenant->id, [ - 'is_active' => true, - 'is_default' => true - ]); - } - - // Create categories - $screenCategory = Category::firstOrCreate( - [ - 'tenant_id' => $tenant->id, - 'code' => 'SCREEN_SYSTEMS' - ], - [ - 'name' => 'Screen Door Systems', - 'description' => 'Automatic screen door systems', - 'is_active' => true - ] - ); - - $materialsCategory = Category::firstOrCreate( - [ - 'tenant_id' => $tenant->id, - 'code' => 'MATERIALS' - ], - [ - 'name' => 'Raw Materials', - 'description' => 'Raw materials and components', - 'is_active' => true - ] - ); - - $componentsCategory = Category::firstOrCreate( - [ - 'tenant_id' => $tenant->id, - 'code' => 'COMPONENTS' - ], - [ - 'name' => 'Components', - 'description' => 'Manufactured components and parts', - 'is_active' => true - ] - ); - - // Create test products - $this->createTestProducts($tenant, $componentsCategory); - - // Create test materials - $this->createTestMaterials($tenant, $materialsCategory); - - // Create design models - $this->createDesignModels($tenant, $screenCategory); - - $this->command->info('Parametric BOM test data seeded successfully!'); - } - - /** - * Create test products - */ - private function createTestProducts(Tenant $tenant, Category $category): void - { - $products = [ - [ - 'code' => 'BRACKET_WALL_STD', - 'name' => 'Standard Wall Bracket', - 'description' => 'Standard mounting bracket for wall installation', - 'unit' => 'EA', - 'weight' => 0.5, - 'color' => 'WHITE' - ], - [ - 'code' => 'BRACKET_CEILING_STD', - 'name' => 'Standard Ceiling Bracket', - 'description' => 'Standard mounting bracket for ceiling installation', - 'unit' => 'EA', - 'weight' => 0.7, - 'color' => 'WHITE' - ], - [ - 'code' => 'MOTOR_DC_12V', - 'name' => 'DC Motor 12V', - 'description' => 'DC motor for screen operation, 12V', - 'unit' => 'EA', - 'weight' => 2.5, - 'color' => 'BLACK' - ], - [ - 'code' => 'MOTOR_DC_24V', - 'name' => 'DC Motor 24V', - 'description' => 'DC motor for screen operation, 24V (for larger screens)', - 'unit' => 'EA', - 'weight' => 3.2, - 'color' => 'BLACK' - ], - [ - 'code' => 'CONTROLLER_BASIC', - 'name' => 'Basic Controller', - 'description' => 'Basic remote controller for screen operation', - 'unit' => 'EA', - 'weight' => 0.2, - 'color' => 'WHITE' - ], - [ - 'code' => 'CONTROLLER_SMART', - 'name' => 'Smart Controller', - 'description' => 'Smart controller with app connectivity', - 'unit' => 'EA', - 'weight' => 0.3, - 'color' => 'BLACK' - ], - [ - 'code' => 'CASE_ALUMINUM', - 'name' => 'Aluminum Case', - 'description' => 'Aluminum housing case for screen mechanism', - 'unit' => 'EA', - 'weight' => 1.8, - 'color' => 'SILVER' - ], - [ - 'code' => 'CASE_PLASTIC', - 'name' => 'Plastic Case', - 'description' => 'Plastic housing case for screen mechanism', - 'unit' => 'EA', - 'weight' => 1.2, - 'color' => 'WHITE' - ] - ]; - - foreach ($products as $productData) { - Product::firstOrCreate( - [ - 'tenant_id' => $tenant->id, - 'code' => $productData['code'] - ], - array_merge($productData, [ - 'category_id' => $category->id, - 'is_active' => true, - 'created_by' => 1 - ]) - ); - } - } - - /** - * Create test materials - */ - private function createTestMaterials(Tenant $tenant, Category $category): void - { - $materials = [ - [ - 'code' => 'FABRIC_SCREEN_STD', - 'name' => 'Standard Screen Fabric', - 'description' => 'Standard fabric material for screen doors', - 'unit' => 'M2', - 'density' => 0.3, // kg/m2 - 'color' => 'GRAY' - ], - [ - 'code' => 'FABRIC_SCREEN_PREMIUM', - 'name' => 'Premium Screen Fabric', - 'description' => 'Premium fabric material with enhanced durability', - 'unit' => 'M2', - 'density' => 0.4, - 'color' => 'CHARCOAL' - ], - [ - 'code' => 'STEEL_MESH_FINE', - 'name' => 'Fine Steel Mesh', - 'description' => 'Fine steel mesh for security screens', - 'unit' => 'M2', - 'density' => 2.5, - 'color' => 'SILVER' - ], - [ - 'code' => 'STEEL_MESH_COARSE', - 'name' => 'Coarse Steel Mesh', - 'description' => 'Coarse steel mesh for ventilation screens', - 'unit' => 'M2', - 'density' => 2.0, - 'color' => 'SILVER' - ], - [ - 'code' => 'RAIL_GUIDE_ALU', - 'name' => 'Aluminum Guide Rail', - 'description' => 'Aluminum guide rail for screen movement', - 'unit' => 'M', - 'density' => 0.8, // kg/m - 'color' => 'SILVER' - ], - [ - 'code' => 'RAIL_GUIDE_PLASTIC', - 'name' => 'Plastic Guide Rail', - 'description' => 'Plastic guide rail for lightweight screens', - 'unit' => 'M', - 'density' => 0.3, - 'color' => 'WHITE' - ], - [ - 'code' => 'CABLE_STEEL', - 'name' => 'Steel Cable', - 'description' => 'Steel cable for screen lifting mechanism', - 'unit' => 'M', - 'density' => 0.1, - 'color' => 'SILVER' - ], - [ - 'code' => 'SPRING_TENSION', - 'name' => 'Tension Spring', - 'description' => 'Spring for screen tension adjustment', - 'unit' => 'EA', - 'weight' => 0.05, - 'color' => 'SILVER' - ] - ]; - - foreach ($materials as $materialData) { - Material::firstOrCreate( - [ - 'tenant_id' => $tenant->id, - 'code' => $materialData['code'] - ], - array_merge($materialData, [ - 'category_id' => $category->id, - 'is_active' => true, - 'created_by' => 1 - ]) - ); - } - } - - /** - * Create design models with parameters, formulas, and rules - */ - private function createDesignModels(Tenant $tenant, Category $category): void - { - // Create basic screen door model - $basicModel = DesignModel::firstOrCreate( - [ - 'tenant_id' => $tenant->id, - 'code' => 'BSD01' - ], - [ - 'name' => 'Basic Screen Door', - 'category_id' => $category->id, - 'lifecycle' => 'ACTIVE', - 'description' => 'Basic parametric screen door model for testing', - 'is_active' => true - ] - ); - - $this->createModelParameters($tenant, $basicModel); - $this->createModelFormulas($tenant, $basicModel); - $this->createBomConditionRules($tenant, $basicModel); - $this->createBomTemplate($tenant, $basicModel); - - // Create advanced screen door model - $advancedModel = DesignModel::firstOrCreate( - [ - 'tenant_id' => $tenant->id, - 'code' => 'ASD01' - ], - [ - 'name' => 'Advanced Screen Door', - 'category_id' => $category->id, - 'lifecycle' => 'ACTIVE', - 'description' => 'Advanced parametric screen door model with more features', - 'is_active' => true - ] - ); - - $this->createAdvancedModelParameters($tenant, $advancedModel); - $this->createAdvancedModelFormulas($tenant, $advancedModel); - $this->createAdvancedBomConditionRules($tenant, $advancedModel); - } - - /** - * Create basic model parameters - */ - private function createModelParameters(Tenant $tenant, DesignModel $model): void - { - $parameters = [ - [ - 'parameter_name' => 'width', - 'parameter_type' => 'NUMBER', - 'is_required' => true, - 'default_value' => '1000', - 'min_value' => 500, - 'max_value' => 3000, - 'unit' => 'mm', - 'description' => 'Screen width', - 'sort_order' => 1 - ], - [ - 'parameter_name' => 'height', - 'parameter_type' => 'NUMBER', - 'is_required' => true, - 'default_value' => '2000', - 'min_value' => 1000, - 'max_value' => 3000, - 'unit' => 'mm', - 'description' => 'Screen height', - 'sort_order' => 2 - ], - [ - 'parameter_name' => 'screen_type', - 'parameter_type' => 'SELECT', - 'is_required' => true, - 'default_value' => 'FABRIC', - 'options' => ['FABRIC', 'STEEL_FINE', 'STEEL_COARSE'], - 'description' => 'Type of screen material', - 'sort_order' => 3 - ], - [ - 'parameter_name' => 'installation_type', - 'parameter_type' => 'SELECT', - 'is_required' => true, - 'default_value' => 'WALL', - 'options' => ['WALL', 'CEILING', 'RECESSED'], - 'description' => 'Installation method', - 'sort_order' => 4 - ], - [ - 'parameter_name' => 'motor_power', - 'parameter_type' => 'SELECT', - 'is_required' => false, - 'default_value' => 'AUTO', - 'options' => ['AUTO', '12V', '24V'], - 'description' => 'Motor power selection (AUTO = based on size)', - 'sort_order' => 5 - ] - ]; - - foreach ($parameters as $paramData) { - ModelParameter::firstOrCreate( - [ - 'tenant_id' => $tenant->id, - 'model_id' => $model->id, - 'parameter_name' => $paramData['parameter_name'] - ], - $paramData - ); - } - } - - /** - * Create basic model formulas - */ - private function createModelFormulas(Tenant $tenant, DesignModel $model): void - { - $formulas = [ - [ - 'formula_name' => 'outer_width', - 'expression' => 'width + 100', - 'description' => 'Outer width including frame', - 'sort_order' => 1 - ], - [ - 'formula_name' => 'outer_height', - 'expression' => 'height + 150', - 'description' => 'Outer height including frame', - 'sort_order' => 2 - ], - [ - 'formula_name' => 'screen_area', - 'expression' => '(width * height) / 1000000', - 'description' => 'Screen area in square meters', - 'sort_order' => 3 - ], - [ - 'formula_name' => 'frame_perimeter', - 'expression' => '(outer_width + outer_height) * 2 / 1000', - 'description' => 'Frame perimeter in meters', - 'sort_order' => 4 - ], - [ - 'formula_name' => 'total_weight', - 'expression' => 'screen_area * 0.5 + frame_perimeter * 0.8', - 'description' => 'Estimated total weight in kg', - 'sort_order' => 5 - ] - ]; - - foreach ($formulas as $formulaData) { - ModelFormula::firstOrCreate( - [ - 'tenant_id' => $tenant->id, - 'model_id' => $model->id, - 'formula_name' => $formulaData['formula_name'] - ], - $formulaData - ); - } - } - - /** - * Create BOM condition rules for basic model - */ - private function createBomConditionRules(Tenant $tenant, DesignModel $model): void - { - $rules = [ - // Motor selection based on size - [ - 'rule_name' => 'Large Screen Motor', - 'condition_expression' => 'screen_area > 4.0 OR motor_power == "24V"', - 'action_type' => 'INCLUDE', - 'target_type' => 'PRODUCT', - 'target_id' => Product::where('tenant_id', $tenant->id)->where('code', 'MOTOR_DC_24V')->first()->id, - 'quantity_multiplier' => 1, - 'description' => 'Use 24V motor for large screens', - 'sort_order' => 1 - ], - [ - 'rule_name' => 'Standard Screen Motor', - 'condition_expression' => 'screen_area <= 4.0 AND motor_power != "24V"', - 'action_type' => 'INCLUDE', - 'target_type' => 'PRODUCT', - 'target_id' => Product::where('tenant_id', $tenant->id)->where('code', 'MOTOR_DC_12V')->first()->id, - 'quantity_multiplier' => 1, - 'description' => 'Use 12V motor for standard screens', - 'sort_order' => 2 - ], - // Screen material selection - [ - 'rule_name' => 'Fabric Screen Material', - 'condition_expression' => 'screen_type == "FABRIC"', - 'action_type' => 'INCLUDE', - 'target_type' => 'MATERIAL', - 'target_id' => Material::where('tenant_id', $tenant->id)->where('code', 'FABRIC_SCREEN_STD')->first()->id, - 'quantity_multiplier' => 1.1, // 10% waste - 'description' => 'Standard fabric screen material', - 'sort_order' => 3 - ], - [ - 'rule_name' => 'Fine Steel Screen Material', - 'condition_expression' => 'screen_type == "STEEL_FINE"', - 'action_type' => 'INCLUDE', - 'target_type' => 'MATERIAL', - 'target_id' => Material::where('tenant_id', $tenant->id)->where('code', 'STEEL_MESH_FINE')->first()->id, - 'quantity_multiplier' => 1.05, // 5% waste - 'description' => 'Fine steel mesh material', - 'sort_order' => 4 - ], - // Bracket selection based on installation - [ - 'rule_name' => 'Wall Brackets', - 'condition_expression' => 'installation_type == "WALL"', - 'action_type' => 'INCLUDE', - 'target_type' => 'PRODUCT', - 'target_id' => Product::where('tenant_id', $tenant->id)->where('code', 'BRACKET_WALL_STD')->first()->id, - 'quantity_multiplier' => 2, // Always 2 brackets - 'description' => 'Wall mounting brackets', - 'sort_order' => 5 - ], - [ - 'rule_name' => 'Ceiling Brackets', - 'condition_expression' => 'installation_type == "CEILING"', - 'action_type' => 'INCLUDE', - 'target_type' => 'PRODUCT', - 'target_id' => Product::where('tenant_id', $tenant->id)->where('code', 'BRACKET_CEILING_STD')->first()->id, - 'quantity_multiplier' => 2, - 'description' => 'Ceiling mounting brackets', - 'sort_order' => 6 - ], - // Extra brackets for wide screens - [ - 'rule_name' => 'Extra Brackets for Wide Screens', - 'condition_expression' => 'width > 2000', - 'action_type' => 'MODIFY_QUANTITY', - 'target_type' => 'PRODUCT', - 'target_id' => Product::where('tenant_id', $tenant->id)->where('code', 'BRACKET_WALL_STD')->first()->id, - 'quantity_multiplier' => 1.5, // Add 50% more brackets - 'description' => 'Additional brackets for wide screens', - 'sort_order' => 7 - ] - ]; - - foreach ($rules as $ruleData) { - BomConditionRule::firstOrCreate( - [ - 'tenant_id' => $tenant->id, - 'model_id' => $model->id, - 'rule_name' => $ruleData['rule_name'] - ], - $ruleData - ); - } - } - - /** - * Create BOM template for basic model - */ - private function createBomTemplate(Tenant $tenant, DesignModel $model): void - { - // Create model version first - $modelVersion = ModelVersion::firstOrCreate( - [ - 'tenant_id' => $tenant->id, - 'model_id' => $model->id, - 'version_no' => '1.0' - ], - [ - 'status' => 'RELEASED', - 'description' => 'Initial release version', - 'created_by' => 1 - ] - ); - - // Create BOM template - $bomTemplate = BomTemplate::firstOrCreate( - [ - 'tenant_id' => $tenant->id, - 'model_version_id' => $modelVersion->id, - 'name' => 'Basic Screen Door BOM' - ], - [ - 'description' => 'Base BOM template for basic screen door', - 'is_active' => true, - 'created_by' => 1 - ] - ); - - // Create BOM template items (base components always included) - $baseItems = [ - [ - 'ref_type' => 'MATERIAL', - 'ref_id' => Material::where('tenant_id', $tenant->id)->where('code', 'RAIL_GUIDE_ALU')->first()->id, - 'quantity' => 1, // Will be calculated by formula - 'waste_rate' => 5, - 'order' => 1 - ], - [ - 'ref_type' => 'MATERIAL', - 'ref_id' => Material::where('tenant_id', $tenant->id)->where('code', 'CABLE_STEEL')->first()->id, - 'quantity' => 2, // Height * 2 - 'waste_rate' => 10, - 'order' => 2 - ], - [ - 'ref_type' => 'MATERIAL', - 'ref_id' => Material::where('tenant_id', $tenant->id)->where('code', 'SPRING_TENSION')->first()->id, - 'quantity' => 2, - 'waste_rate' => 0, - 'order' => 3 - ], - [ - 'ref_type' => 'PRODUCT', - 'ref_id' => Product::where('tenant_id', $tenant->id)->where('code', 'CONTROLLER_BASIC')->first()->id, - 'quantity' => 1, - 'waste_rate' => 0, - 'order' => 4 - ], - [ - 'ref_type' => 'PRODUCT', - 'ref_id' => Product::where('tenant_id', $tenant->id)->where('code', 'CASE_ALUMINUM')->first()->id, - 'quantity' => 1, - 'waste_rate' => 0, - 'order' => 5 - ] - ]; - - foreach ($baseItems as $itemData) { - BomTemplateItem::firstOrCreate( - [ - 'bom_template_id' => $bomTemplate->id, - 'ref_type' => $itemData['ref_type'], - 'ref_id' => $itemData['ref_id'], - 'order' => $itemData['order'] - ], - $itemData - ); - } - } - - /** - * Create advanced model parameters - */ - private function createAdvancedModelParameters(Tenant $tenant, DesignModel $model): void - { - $parameters = [ - [ - 'parameter_name' => 'width', - 'parameter_type' => 'NUMBER', - 'is_required' => true, - 'default_value' => '1200', - 'min_value' => 600, - 'max_value' => 4000, - 'unit' => 'mm', - 'description' => 'Screen width', - 'sort_order' => 1 - ], - [ - 'parameter_name' => 'height', - 'parameter_type' => 'NUMBER', - 'is_required' => true, - 'default_value' => '2200', - 'min_value' => 1200, - 'max_value' => 3500, - 'unit' => 'mm', - 'description' => 'Screen height', - 'sort_order' => 2 - ], - [ - 'parameter_name' => 'screen_type', - 'parameter_type' => 'SELECT', - 'is_required' => true, - 'default_value' => 'FABRIC_PREMIUM', - 'options' => ['FABRIC_STD', 'FABRIC_PREMIUM', 'STEEL_FINE', 'STEEL_COARSE'], - 'description' => 'Type of screen material', - 'sort_order' => 3 - ], - [ - 'parameter_name' => 'controller_type', - 'parameter_type' => 'SELECT', - 'is_required' => true, - 'default_value' => 'SMART', - 'options' => ['BASIC', 'SMART'], - 'description' => 'Controller type', - 'sort_order' => 4 - ], - [ - 'parameter_name' => 'case_material', - 'parameter_type' => 'SELECT', - 'is_required' => true, - 'default_value' => 'ALUMINUM', - 'options' => ['ALUMINUM', 'PLASTIC'], - 'description' => 'Case material', - 'sort_order' => 5 - ], - [ - 'parameter_name' => 'wind_resistance', - 'parameter_type' => 'NUMBER', - 'is_required' => false, - 'default_value' => '80', - 'min_value' => 40, - 'max_value' => 120, - 'unit' => 'km/h', - 'description' => 'Required wind resistance', - 'sort_order' => 6 - ] - ]; - - foreach ($parameters as $paramData) { - ModelParameter::firstOrCreate( - [ - 'tenant_id' => $tenant->id, - 'model_id' => $model->id, - 'parameter_name' => $paramData['parameter_name'] - ], - $paramData - ); - } - } - - /** - * Create advanced model formulas - */ - private function createAdvancedModelFormulas(Tenant $tenant, DesignModel $model): void - { - $formulas = [ - [ - 'formula_name' => 'outer_width', - 'expression' => 'width + 120', - 'description' => 'Outer width including reinforced frame', - 'sort_order' => 1 - ], - [ - 'formula_name' => 'outer_height', - 'expression' => 'height + 180', - 'description' => 'Outer height including reinforced frame', - 'sort_order' => 2 - ], - [ - 'formula_name' => 'screen_area', - 'expression' => '(width * height) / 1000000', - 'description' => 'Screen area in square meters', - 'sort_order' => 3 - ], - [ - 'formula_name' => 'wind_load', - 'expression' => 'screen_area * wind_resistance * 0.6', - 'description' => 'Wind load calculation in Newtons', - 'sort_order' => 4 - ], - [ - 'formula_name' => 'required_motor_torque', - 'expression' => 'wind_load * 0.1 + screen_area * 5', - 'description' => 'Required motor torque in Nm', - 'sort_order' => 5 - ], - [ - 'formula_name' => 'frame_sections_count', - 'expression' => 'ceiling(width / 600) + ceiling(height / 800)', - 'description' => 'Number of frame sections needed', - 'sort_order' => 6 - ] - ]; - - foreach ($formulas as $formulaData) { - ModelFormula::firstOrCreate( - [ - 'tenant_id' => $tenant->id, - 'model_id' => $model->id, - 'formula_name' => $formulaData['formula_name'] - ], - $formulaData - ); - } - } - - /** - * Create advanced BOM condition rules - */ - private function createAdvancedBomConditionRules(Tenant $tenant, DesignModel $model): void - { - $rules = [ - // Motor selection based on calculated torque - [ - 'rule_name' => 'High Torque Motor Required', - 'condition_expression' => 'required_motor_torque > 15 OR wind_resistance > 100', - 'action_type' => 'INCLUDE', - 'target_type' => 'PRODUCT', - 'target_id' => Product::where('tenant_id', $tenant->id)->where('code', 'MOTOR_DC_24V')->first()->id, - 'quantity_multiplier' => 1, - 'description' => 'Use high-power motor for demanding conditions', - 'sort_order' => 1 - ], - // Screen material selection - [ - 'rule_name' => 'Premium Fabric Material', - 'condition_expression' => 'screen_type == "FABRIC_PREMIUM"', - 'action_type' => 'INCLUDE', - 'target_type' => 'MATERIAL', - 'target_id' => Material::where('tenant_id', $tenant->id)->where('code', 'FABRIC_SCREEN_PREMIUM')->first()->id, - 'quantity_multiplier' => 1.05, - 'description' => 'Premium fabric screen material', - 'sort_order' => 2 - ], - // Controller selection - [ - 'rule_name' => 'Smart Controller', - 'condition_expression' => 'controller_type == "SMART"', - 'action_type' => 'INCLUDE', - 'target_type' => 'PRODUCT', - 'target_id' => Product::where('tenant_id', $tenant->id)->where('code', 'CONTROLLER_SMART')->first()->id, - 'quantity_multiplier' => 1, - 'description' => 'Smart controller with app connectivity', - 'sort_order' => 3 - ], - // Case material selection - [ - 'rule_name' => 'Plastic Case for Light Duty', - 'condition_expression' => 'case_material == "PLASTIC" AND wind_resistance <= 60', - 'action_type' => 'INCLUDE', - 'target_type' => 'PRODUCT', - 'target_id' => Product::where('tenant_id', $tenant->id)->where('code', 'CASE_PLASTIC')->first()->id, - 'quantity_multiplier' => 1, - 'description' => 'Plastic case for light-duty applications', - 'sort_order' => 4 - ] - ]; - - foreach ($rules as $ruleData) { - BomConditionRule::firstOrCreate( - [ - 'tenant_id' => $tenant->id, - 'model_id' => $model->id, - 'rule_name' => $ruleData['rule_name'] - ], - $ruleData - ); - } - } -} diff --git a/docs/parameter-based-bom-api.md b/docs/parameter-based-bom-api.md deleted file mode 100644 index 693d246..0000000 --- a/docs/parameter-based-bom-api.md +++ /dev/null @@ -1,651 +0,0 @@ -# 매개변수 기반 BOM 시스템 API 문서 - -## 개요 - -SAM 프로젝트의 매개변수 기반 BOM 시스템은 입력 매개변수를 통해 동적으로 제품 사양과 BOM을 계산하여 자동으로 제품을 생성하는 시스템입니다. - -## 시스템 구조 - -``` -Model (KSS01) -├── Parameters (W0, H0, installation_type, ...) -├── Formulas (W1 = W0 + margin, area = W1 * H1, ...) -└── BOM Template - └── Condition Rules (브라켓, 모터, 가이드, ...) -``` - -## KSS01 모델 시나리오 - -### 모델 정의 -- **모델명**: KSS01 (전동 스크린 시스템) -- **제품군**: 전동 스크린 -- **특징**: 크기와 설치 타입에 따른 다양한 구성 - -### 입력 매개변수 - -| 매개변수 | 타입 | 설명 | 단위 | 범위 | 기본값 | -|---------|------|------|------|------|--------| -| W0 | DECIMAL | 원본 가로 크기 | mm | 500-2000 | 1000 | -| H0 | DECIMAL | 원본 세로 크기 | mm | 400-1500 | 800 | -| installation_type | STRING | 설치 타입 | - | A, B, C | A | -| power_source | STRING | 전원 타입 | - | 220V, 110V | 220V | -| color | STRING | 색상 | - | WHITE, BLACK, GRAY | WHITE | - -### 계산 공식 - -| 출력 매개변수 | 공식 | 설명 | -|-------------|------|------| -| W1 | `W0 + (installation_type == "A" ? 50 : 30)` | 최종 가로 크기 | -| H1 | `H0 + (installation_type == "A" ? 50 : 30)` | 최종 세로 크기 | -| area | `W1 * H1` | 전체 면적 | -| weight | `area * 0.000025 + 5` | 예상 무게 (kg) | -| motor_power | `weight > 20 ? 150 : 120` | 모터 파워 (W) | - -### BOM 조건 규칙 - -| 규칙명 | 조건 | 자재/제품 | 수량 공식 | 설명 | -|--------|------|-----------|----------|------| -| 표준 브라켓 | `installation_type == "A"` | BR-001 (브라켓) | `ceiling(W1 / 500)` | A타입은 표준 브라켓 | -| 경량 브라켓 | `installation_type != "A"` | BR-002 (경량 브라켓) | `ceiling(W1 / 600)` | B/C타입은 경량 브라켓 | -| 고출력 모터 | `motor_power >= 150` | MT-002 (고출력 모터) | `1` | 150W 이상시 고출력 | -| 표준 모터 | `motor_power < 150` | MT-001 (표준 모터) | `1` | 150W 미만시 표준 | -| 가이드 레일 | `true` | GD-001 (가이드 레일) | `ceiling(H1 / 1000) * 2` | 항상 필요 | -| 컨트롤러 | `power_source == "220V"` | CT-001 (220V 컨트롤러) | `1` | 220V 전용 | -| 컨트롤러 | `power_source == "110V"` | CT-002 (110V 컨트롤러) | `1` | 110V 전용 | - -## API 사용 예시 - -### 1. 모델 매개변수 설정 - -#### 매개변수 목록 조회 -```http -GET /v1/design/models/1/parameters -Authorization: Bearer {token} -X-API-KEY: {api-key} -``` - -#### 매개변수 생성 -```http -POST /v1/design/models/1/parameters -Authorization: Bearer {token} -X-API-KEY: {api-key} -Content-Type: application/json - -{ - "name": "W0", - "label": "원본 가로 크기", - "type": "INPUT", - "data_type": "DECIMAL", - "unit": "mm", - "default_value": "1000", - "min_value": 500, - "max_value": 2000, - "description": "제품의 원본 가로 크기를 입력하세요", - "is_required": true, - "display_order": 1 -} -``` - -### 2. 공식 설정 - -#### 공식 생성 -```http -POST /v1/design/models/1/formulas -Authorization: Bearer {token} -X-API-KEY: {api-key} -Content-Type: application/json - -{ - "name": "최종 가로 크기 계산", - "target_parameter": "W1", - "expression": "W0 + (installation_type == \"A\" ? 50 : 30)", - "description": "설치 타입에 따른 최종 가로 크기 계산", - "is_active": true, - "execution_order": 1 -} -``` - -#### 공식 검증 -```http -POST /v1/design/models/1/formulas/1/validate -Authorization: Bearer {token} -X-API-KEY: {api-key} -Content-Type: application/json - -{ - "test_values": { - "W0": 1000, - "installation_type": "A" - } -} -``` - -**응답:** -```json -{ - "success": true, - "message": "formula.validated", - "data": { - "is_valid": true, - "result": 1050, - "error_message": null - } -} -``` - -### 3. BOM 조건 규칙 설정 - -#### 조건 규칙 생성 -```http -POST /v1/design/bom-templates/1/condition-rules -Authorization: Bearer {token} -X-API-KEY: {api-key} -Content-Type: application/json - -{ - "name": "표준 브라켓 선택", - "ref_type": "MATERIAL", - "ref_id": 101, - "condition_expression": "installation_type == \"A\"", - "quantity_expression": "ceiling(W1 / 500)", - "waste_rate_expression": "0.05", - "description": "A타입 설치시 표준 브라켓 적용", - "priority": 1, - "is_active": true -} -``` - -### 4. BOM 미리보기 생성 - -#### 실시간 BOM 해석 -```http -POST /v1/products/models/1/resolve-preview -Authorization: Bearer {token} -X-API-KEY: {api-key} -Content-Type: application/json - -{ - "input_parameters": { - "W0": 1000, - "H0": 800, - "installation_type": "A", - "power_source": "220V", - "color": "WHITE" - }, - "include_calculated_values": true, - "include_bom_items": true -} -``` - -**응답:** -```json -{ - "success": true, - "message": "bom.preview_generated", - "data": { - "input_parameters": { - "W0": 1000, - "H0": 800, - "installation_type": "A", - "power_source": "220V", - "color": "WHITE" - }, - "calculated_values": { - "W1": 1050, - "H1": 850, - "area": 892500, - "weight": 27.31, - "motor_power": 150 - }, - "bom_items": [ - { - "ref_type": "MATERIAL", - "ref_id": 101, - "ref_code": "BR-001", - "ref_name": "표준 브라켓", - "quantity": 3.0, - "waste_rate": 0.05, - "total_quantity": 3.15, - "unit": "EA", - "unit_cost": 5000.0, - "total_cost": 15750.0, - "applied_rule": "표준 브라켓 선택", - "calculation_details": { - "condition_matched": true, - "quantity_expression": "ceiling(W1 / 500)", - "quantity_calculation": "ceiling(1050 / 500) = 3" - } - }, - { - "ref_type": "MATERIAL", - "ref_id": 202, - "ref_code": "MT-002", - "ref_name": "고출력 모터", - "quantity": 1.0, - "waste_rate": 0.0, - "total_quantity": 1.0, - "unit": "EA", - "unit_cost": 45000.0, - "total_cost": 45000.0, - "applied_rule": "고출력 모터", - "calculation_details": { - "condition_matched": true, - "quantity_expression": "1", - "quantity_calculation": "1" - } - }, - { - "ref_type": "MATERIAL", - "ref_id": 301, - "ref_code": "GD-001", - "ref_name": "가이드 레일", - "quantity": 2.0, - "waste_rate": 0.03, - "total_quantity": 2.06, - "unit": "EA", - "unit_cost": 12000.0, - "total_cost": 24720.0, - "applied_rule": "가이드 레일", - "calculation_details": { - "condition_matched": true, - "quantity_expression": "ceiling(H1 / 1000) * 2", - "quantity_calculation": "ceiling(850 / 1000) * 2 = 2" - } - }, - { - "ref_type": "MATERIAL", - "ref_id": 401, - "ref_code": "CT-001", - "ref_name": "220V 컨트롤러", - "quantity": 1.0, - "waste_rate": 0.0, - "total_quantity": 1.0, - "unit": "EA", - "unit_cost": 25000.0, - "total_cost": 25000.0, - "applied_rule": "220V 컨트롤러", - "calculation_details": { - "condition_matched": true, - "quantity_expression": "1", - "quantity_calculation": "1" - } - } - ], - "summary": { - "total_materials": 4, - "total_cost": 110470.0, - "estimated_weight": 27.31 - }, - "validation_warnings": [] - } -} -``` - -### 5. 실제 제품 생성 - -#### 매개변수 기반 제품 생성 -```http -POST /v1/products/create-from-model -Authorization: Bearer {token} -X-API-KEY: {api-key} -Content-Type: application/json - -{ - "model_id": 1, - "input_parameters": { - "W0": 1000, - "H0": 800, - "installation_type": "A", - "power_source": "220V", - "color": "WHITE" - }, - "product_code": "KSS01-1000x800-A", - "product_name": "KSS01 스크린 1000x800 A타입", - "category_id": 1, - "description": "매개변수 기반으로 생성된 맞춤형 전동 스크린", - "unit": "EA", - "min_order_qty": 1, - "lead_time_days": 7, - "is_active": true, - "create_bom_items": true, - "validate_bom": true -} -``` - -**응답:** -```json -{ - "success": true, - "message": "product.created_from_model", - "data": { - "product": { - "id": 123, - "code": "KSS01-1000x800-A", - "name": "KSS01 스크린 1000x800 A타입", - "type": "PRODUCT", - "category_id": 1, - "description": "매개변수 기반으로 생성된 맞춤형 전동 스크린", - "unit": "EA", - "min_order_qty": 1, - "lead_time_days": 7, - "is_active": true, - "created_at": "2024-01-01T00:00:00Z" - }, - "input_parameters": { - "W0": 1000, - "H0": 800, - "installation_type": "A", - "power_source": "220V", - "color": "WHITE" - }, - "calculated_values": { - "W1": 1050, - "H1": 850, - "area": 892500, - "weight": 27.31, - "motor_power": 150 - }, - "bom_items": [ - { - "id": 1, - "ref_type": "MATERIAL", - "ref_id": 101, - "ref_code": "BR-001", - "ref_name": "표준 브라켓", - "quantity": 3.0, - "waste_rate": 0.05, - "unit": "EA" - }, - { - "id": 2, - "ref_type": "MATERIAL", - "ref_id": 202, - "ref_code": "MT-002", - "ref_name": "고출력 모터", - "quantity": 1.0, - "waste_rate": 0.0, - "unit": "EA" - }, - { - "id": 3, - "ref_type": "MATERIAL", - "ref_id": 301, - "ref_code": "GD-001", - "ref_name": "가이드 레일", - "quantity": 2.0, - "waste_rate": 0.03, - "unit": "EA" - }, - { - "id": 4, - "ref_type": "MATERIAL", - "ref_id": 401, - "ref_code": "CT-001", - "ref_name": "220V 컨트롤러", - "quantity": 1.0, - "waste_rate": 0.0, - "unit": "EA" - } - ], - "summary": { - "total_bom_items": 4, - "model_id": 1, - "bom_template_id": 1 - } - } -} -``` - -### 6. 제품 매개변수 조회 - -#### 생성된 제품의 매개변수 확인 -```http -GET /v1/products/123/parameters -Authorization: Bearer {token} -X-API-KEY: {api-key} -``` - -**응답:** -```json -{ - "success": true, - "message": "fetched", - "data": { - "product_id": 123, - "model_id": 1, - "input_parameters": { - "W0": 1000, - "H0": 800, - "installation_type": "A", - "power_source": "220V", - "color": "WHITE" - }, - "parameter_definitions": [ - { - "id": 1, - "name": "W0", - "label": "원본 가로 크기", - "type": "INPUT", - "data_type": "DECIMAL", - "unit": "mm", - "default_value": "1000", - "min_value": 500, - "max_value": 2000, - "description": "제품의 원본 가로 크기를 입력하세요" - } - ] - } -} -``` - -### 7. 계산된 값 조회 - -#### 생성된 제품의 계산값 확인 -```http -GET /v1/products/123/calculated-values -Authorization: Bearer {token} -X-API-KEY: {api-key} -``` - -**응답:** -```json -{ - "success": true, - "message": "fetched", - "data": { - "product_id": 123, - "model_id": 1, - "calculated_values": { - "W1": 1050, - "H1": 850, - "area": 892500, - "weight": 27.31, - "motor_power": 150 - }, - "formula_applications": [ - { - "formula_name": "최종 가로 크기 계산", - "target_parameter": "W1", - "expression": "W0 + (installation_type == \"A\" ? 50 : 30)", - "calculated_value": 1050, - "execution_order": 1 - }, - { - "formula_name": "최종 세로 크기 계산", - "target_parameter": "H1", - "expression": "H0 + (installation_type == \"A\" ? 50 : 30)", - "calculated_value": 850, - "execution_order": 2 - }, - { - "formula_name": "면적 계산", - "target_parameter": "area", - "expression": "W1 * H1", - "calculated_value": 892500, - "execution_order": 3 - }, - { - "formula_name": "무게 계산", - "target_parameter": "weight", - "expression": "area * 0.000025 + 5", - "calculated_value": 27.31, - "execution_order": 4 - }, - { - "formula_name": "모터 파워 계산", - "target_parameter": "motor_power", - "expression": "weight > 20 ? 150 : 120", - "calculated_value": 150, - "execution_order": 5 - } - ] - } -} -``` - -## 시나리오별 테스트 케이스 - -### 시나리오 1: 소형 B타입 스크린 - -**입력 매개변수:** -```json -{ - "W0": 600, - "H0": 500, - "installation_type": "B", - "power_source": "110V", - "color": "BLACK" -} -``` - -**예상 결과:** -- W1: 630 (600 + 30) -- H1: 530 (500 + 30) -- area: 333900 -- weight: 13.35 kg -- motor_power: 120W (경량이므로 표준 모터) -- 브라켓: 경량 브라켓 2개 (ceiling(630/600) = 2) -- 컨트롤러: 110V 컨트롤러 - -### 시나리오 2: 대형 A타입 스크린 - -**입력 매개변수:** -```json -{ - "W0": 1800, - "H0": 1200, - "installation_type": "A", - "power_source": "220V", - "color": "GRAY" -} -``` - -**예상 결과:** -- W1: 1850 (1800 + 50) -- H1: 1250 (1200 + 50) -- area: 2312500 -- weight: 62.81 kg -- motor_power: 150W (중량이므로 고출력 모터) -- 브라켓: 표준 브라켓 4개 (ceiling(1850/500) = 4) -- 가이드 레일: 4개 (ceiling(1250/1000) * 2 = 4) - -### 시나리오 3: 경계값 테스트 - -**입력 매개변수:** -```json -{ - "W0": 500, - "H0": 400, - "installation_type": "C", - "power_source": "220V", - "color": "WHITE" -} -``` - -**예상 결과:** -- W1: 530 (500 + 30) -- H1: 430 (400 + 30) -- area: 227900 -- weight: 10.70 kg -- motor_power: 120W -- 브라켓: 경량 브라켓 1개 (ceiling(530/600) = 1) -- 가이드 레일: 2개 (ceiling(430/1000) * 2 = 2) - -## 에러 처리 - -### 매개변수 검증 실패 -```http -POST /v1/design/models/1/validate-parameters -Content-Type: application/json - -{ - "input_parameters": { - "W0": 3000, // 범위 초과 - "H0": 200, // 범위 미달 - "installation_type": "D" // 잘못된 값 - } -} -``` - -**응답:** -```json -{ - "success": true, - "message": "parameters.validated", - "data": { - "is_valid": false, - "validation_errors": [ - { - "parameter": "W0", - "error": "Value must be between 500 and 2000" - }, - { - "parameter": "H0", - "error": "Value must be between 400 and 1500" - }, - { - "parameter": "installation_type", - "error": "Value must be one of: A, B, C" - } - ], - "warnings": [] - } -} -``` - -### 공식 계산 오류 -```json -{ - "success": false, - "message": "error.formula.calculation_failed", - "errors": { - "code": "FORMULA_ERROR", - "message": "Division by zero in formula 'area_calculation'", - "details": { - "formula_id": 3, - "expression": "W1 * H1 / 0", - "input_values": {"W1": 1050, "H1": 850} - } - } -} -``` - -## 성능 고려사항 - -### 캐싱 전략 -- 모델별 매개변수 정의: Redis 캐시 (TTL: 1시간) -- 공식 표현식: 메모리 캐시 (세션 기반) -- BOM 템플릿: Redis 캐시 (TTL: 30분) - -### 최적화 -- 공식 실행 순서 최적화 (의존성 그래프) -- BOM 규칙 우선순위 기반 조기 종료 -- 대량 생성시 배치 처리 지원 - -### 제한사항 -- 동시 요청 제한: 테넌트당 100 req/min -- 매개변수 개수 제한: 모델당 최대 50개 -- 공식 복잡도 제한: 중첩 깊이 최대 10단계 -- BOM 항목 제한: 템플릿당 최대 200개 - -이 API 문서는 KSS01 모델을 예시로 한 완전한 매개변수 기반 BOM 시스템의 사용법을 제공합니다. 실제 구현시에는 각 비즈니스 요구사항에 맞게 매개변수와 규칙을 조정하여 사용할 수 있습니다. \ No newline at end of file diff --git a/docs/parameter-based-bom-endpoints.md b/docs/parameter-based-bom-endpoints.md deleted file mode 100644 index a591691..0000000 --- a/docs/parameter-based-bom-endpoints.md +++ /dev/null @@ -1,264 +0,0 @@ -# 매개변수 기반 BOM 시스템 API 엔드포인트 - -## 엔드포인트 개요 - -이 문서는 SAM 프로젝트의 매개변수 기반 BOM 시스템을 위한 모든 RESTful API 엔드포인트를 요약합니다. - -## 인증 및 권한 - -- **API Key**: 모든 요청에 `X-API-KEY` 헤더 필요 -- **Bearer Token**: 사용자 컨텍스트가 필요한 요청에 `Authorization: Bearer {token}` 헤더 필요 -- **미들웨어**: `auth.apikey` + `auth:sanctum` - -## 매개변수 관리 API - -### 모델 매개변수 - -| 메서드 | 엔드포인트 | 설명 | 권한 | -|-------|-----------|------|------| -| `GET` | `/v1/design/models/{modelId}/parameters` | 매개변수 목록 조회 | READ | -| `POST` | `/v1/design/models/{modelId}/parameters` | 매개변수 생성 | CREATE | -| `PUT` | `/v1/design/models/{modelId}/parameters/{parameterId}` | 매개변수 수정 | UPDATE | -| `DELETE` | `/v1/design/models/{modelId}/parameters/{parameterId}` | 매개변수 삭제 | DELETE | - -**쿼리 파라미터 (GET):** -- `page`: 페이지 번호 (기본값: 1) -- `per_page`: 페이지당 항목 수 (기본값: 20) -- `search`: 매개변수명/라벨 검색 -- `type`: 매개변수 타입 필터 (`INPUT`, `OUTPUT`) - -## 공식 관리 API - -### 모델 공식 - -| 메서드 | 엔드포인트 | 설명 | 권한 | -|-------|-----------|------|------| -| `GET` | `/v1/design/models/{modelId}/formulas` | 공식 목록 조회 | READ | -| `POST` | `/v1/design/models/{modelId}/formulas` | 공식 생성 | CREATE | -| `PUT` | `/v1/design/models/{modelId}/formulas/{formulaId}` | 공식 수정 | UPDATE | -| `DELETE` | `/v1/design/models/{modelId}/formulas/{formulaId}` | 공식 삭제 | DELETE | -| `POST` | `/v1/design/models/{modelId}/formulas/{formulaId}/validate` | 공식 검증 | READ | - -**쿼리 파라미터 (GET):** -- `page`: 페이지 번호 -- `per_page`: 페이지당 항목 수 -- `search`: 공식명/대상 매개변수 검색 -- `target_parameter`: 대상 매개변수 필터 - -## 조건 규칙 관리 API - -### BOM 조건 규칙 - -| 메서드 | 엔드포인트 | 설명 | 권한 | -|-------|-----------|------|------| -| `GET` | `/v1/design/bom-templates/{bomTemplateId}/condition-rules` | 조건 규칙 목록 조회 | READ | -| `POST` | `/v1/design/bom-templates/{bomTemplateId}/condition-rules` | 조건 규칙 생성 | CREATE | -| `PUT` | `/v1/design/bom-templates/{bomTemplateId}/condition-rules/{ruleId}` | 조건 규칙 수정 | UPDATE | -| `DELETE` | `/v1/design/bom-templates/{bomTemplateId}/condition-rules/{ruleId}` | 조건 규칙 삭제 | DELETE | -| `POST` | `/v1/design/bom-templates/{bomTemplateId}/condition-rules/{ruleId}/toggle` | 규칙 활성/비활성 토글 | UPDATE | - -**쿼리 파라미터 (GET):** -- `page`: 페이지 번호 -- `per_page`: 페이지당 항목 수 -- `search`: 규칙명/대상 컴포넌트 검색 -- `ref_type`: 참조 타입 필터 (`MATERIAL`, `PRODUCT`) -- `is_active`: 활성 상태 필터 - -## 핵심 BOM 해석 API - -### BOM 해석 및 제품 생성 - -| 메서드 | 엔드포인트 | 설명 | 권한 | -|-------|-----------|------|------| -| `POST` | `/v1/products/models/{modelId}/resolve-preview` | **실시간 BOM 미리보기** | READ | -| `POST` | `/v1/products/create-from-model` | **제품 생성 + BOM 적용** | CREATE | -| `POST` | `/v1/design/models/{modelId}/validate-parameters` | 매개변수 검증 | READ | - -### 제품 조회 API - -| 메서드 | 엔드포인트 | 설명 | 권한 | -|-------|-----------|------|------| -| `GET` | `/v1/products/{productId}/parameters` | 제품별 매개변수 조회 | READ | -| `GET` | `/v1/products/{productId}/calculated-values` | 제품별 산출값 조회 | READ | - -## 주요 요청/응답 스키마 - -### 실시간 BOM 미리보기 요청 - -```json -{ - "input_parameters": { - "W0": 1000, - "H0": 800, - "installation_type": "A", - "power_source": "220V" - }, - "include_calculated_values": true, - "include_bom_items": true -} -``` - -### BOM 미리보기 응답 - -```json -{ - "success": true, - "message": "bom.preview_generated", - "data": { - "input_parameters": { ... }, - "calculated_values": { - "W1": 1050, - "H1": 850, - "area": 892500, - "weight": 27.31, - "motor_power": 150 - }, - "bom_items": [ - { - "ref_type": "MATERIAL", - "ref_code": "BR-001", - "ref_name": "표준 브라켓", - "quantity": 3.0, - "waste_rate": 0.05, - "total_quantity": 3.15, - "unit_cost": 5000.0, - "total_cost": 15750.0, - "applied_rule": "표준 브라켓 선택" - } - ], - "summary": { - "total_materials": 4, - "total_cost": 110470.0, - "estimated_weight": 27.31 - } - } -} -``` - -### 제품 생성 요청 - -```json -{ - "model_id": 1, - "input_parameters": { - "W0": 1000, - "H0": 800, - "installation_type": "A", - "power_source": "220V" - }, - "product_code": "KSS01-1000x800-A", - "product_name": "KSS01 스크린 1000x800 A타입", - "category_id": 1, - "description": "매개변수 기반으로 생성된 맞춤형 스크린", - "create_bom_items": true, - "validate_bom": true -} -``` - -### 제품 생성 응답 - -```json -{ - "success": true, - "message": "product.created_from_model", - "data": { - "product": { - "id": 123, - "code": "KSS01-1000x800-A", - "name": "KSS01 스크린 1000x800 A타입", - "type": "PRODUCT" - }, - "input_parameters": { ... }, - "calculated_values": { ... }, - "bom_items": [ ... ], - "summary": { - "total_bom_items": 4, - "model_id": 1, - "bom_template_id": 1 - } - } -} -``` - -## 매개변수 데이터 타입 - -### 입력 매개변수 타입 - -| 타입 | 설명 | 예시 | -|------|------|------| -| `INTEGER` | 정수 | `1000` | -| `DECIMAL` | 소수 | `1000.5` | -| `STRING` | 문자열 | `"A"` | -| `BOOLEAN` | 불린 | `true` | - -### 공식 표현식 예시 - -| 표현식 | 설명 | -|--------|------| -| `W0 + 50` | 단순 산술 연산 | -| `W0 * H0` | 곱셈 연산 | -| `installation_type == "A" ? 50 : 30` | 조건부 연산 | -| `ceiling(W1 / 500)` | 수학 함수 | -| `W0 > 1000 && H0 > 800` | 논리 연산 | - -### BOM 조건 규칙 예시 - -| 조건 | 수량 공식 | 설명 | -|------|-----------|------| -| `installation_type == "A"` | `ceiling(W1 / 500)` | A타입일 때 너비 기반 수량 | -| `motor_power >= 150` | `1` | 고출력 모터 조건 | -| `true` | `ceiling(H1 / 1000) * 2` | 항상 적용되는 가이드 레일 | - -## 에러 코드 - -### 일반 에러 - -| 코드 | 메시지 | 설명 | -|------|--------|------| -| `400` | `Bad Request` | 잘못된 요청 | -| `401` | `Unauthorized` | 인증 실패 | -| `403` | `Forbidden` | 권한 없음 | -| `404` | `Not Found` | 리소스 없음 | -| `422` | `Validation Error` | 입력 검증 실패 | - -### 비즈니스 로직 에러 - -| 코드 | 메시지 키 | 설명 | -|------|-----------|------| -| `400` | `error.model.not_found` | 모델을 찾을 수 없음 | -| `400` | `error.parameter.invalid_type` | 잘못된 매개변수 타입 | -| `400` | `error.formula.syntax_error` | 공식 구문 오류 | -| `400` | `error.bom.resolution_failed` | BOM 해석 실패 | -| `400` | `error.product.duplicate_code` | 제품 코드 중복 | - -## 제한사항 - -### Rate Limiting -- **일반 요청**: 테넌트당 1000 req/hour -- **BOM 해석**: 테넌트당 100 req/hour -- **제품 생성**: 테넌트당 50 req/hour - -### 데이터 제한 -- **매개변수**: 모델당 최대 50개 -- **공식**: 모델당 최대 100개 -- **조건 규칙**: BOM 템플릿당 최대 200개 -- **BOM 항목**: 제품당 최대 500개 - -### 성능 고려사항 -- **복잡한 공식**: 중첩 깊이 최대 10단계 -- **대용량 BOM**: 1000개 이상 항목시 배치 처리 권장 -- **동시 생성**: 동일 모델 기반 제품 동시 생성시 순차 처리 - -## Swagger 문서 - -전체 API 스펙은 Swagger UI에서 확인할 수 있습니다: -- **Swagger UI**: `/api-docs/index.html` -- **JSON Spec**: `/docs/api-docs.json` - -### 주요 태그 -- `Model Parameters`: 모델 매개변수 관리 -- `Model Formulas`: 모델 공식 관리 -- `BOM Condition Rules`: BOM 조건 규칙 관리 -- `BOM Resolver`: BOM 해석 및 제품 생성 - -각 엔드포인트는 상세한 요청/응답 스키마와 예시를 포함하고 있습니다. \ No newline at end of file diff --git a/docs/parametric_bom_schema.md b/docs/parametric_bom_schema.md deleted file mode 100644 index da84f39..0000000 --- a/docs/parametric_bom_schema.md +++ /dev/null @@ -1,250 +0,0 @@ -# KSS01 매개변수 기반 BOM 시스템 데이터베이스 스키마 - -## 개요 - -KSS01 모델을 위한 매개변수 기반 BOM 시스템의 데이터베이스 스키마입니다. -기존 SAM 프로젝트의 models, model_versions, bom_templates, bom_template_items 구조를 확장하여 -동적 매개변수 입력과 공식 기반 BOM 생성을 지원합니다. - -## 테이블 구조 - -### 1. bom_template_groups -BOM 아이템들의 그룹핑을 지원하는 테이블입니다. - -**주요 기능:** -- 계층적 그룹 구조 지원 (parent_group_id를 통한 self-referencing) -- 본체, 절곡물, 가이드레일 등의 논리적 그룹핑 -- 표시 순서 관리 - -**핵심 컬럼:** -- `group_name`: 그룹명 (본체, 절곡물, 가이드레일 등) -- `parent_group_id`: 상위 그룹 ID (계층 구조) -- `display_order`: 표시 순서 - -### 2. model_parameters -모델별 입력 매개변수 정의를 관리하는 테이블입니다. - -**주요 기능:** -- 매개변수 타입별 유효성 검사 (INTEGER, DECIMAL, STRING, BOOLEAN) -- 숫자형 매개변수의 범위 제한 (min_value, max_value) -- 선택형 매개변수의 허용값 정의 (allowed_values JSON) -- 단위 및 설명 관리 - -**핵심 컬럼:** -- `parameter_name`: 매개변수명 (W0, H0, screen_type 등) -- `parameter_type`: 매개변수 타입 -- `allowed_values`: 허용값 목록 (JSON) -- `min_value/max_value`: 숫자형 범위 제한 - -**JSON 구조 예시:** -```json -{ - "allowed_values": ["SCREEN", "STEEL", "ALUMINUM"] -} -``` - -### 3. model_formulas -매개변수를 기반으로 한 계산 공식 정의를 관리하는 테이블입니다. - -**주요 기능:** -- 수학적 공식 표현식 저장 -- 계산 순서 관리 (의존성 해결) -- 결과 타입 및 소수점 자릿수 제어 -- 의존 변수 추적 - -**핵심 컬럼:** -- `formula_name`: 공식명 (W1, H1, area, weight 등) -- `formula_expression`: 공식 표현식 ("W0 + 100", "W0 * H0 / 1000000") -- `calculation_order`: 계산 순서 -- `dependencies`: 의존 변수 목록 (JSON) - -**공식 예시:** -``` -W1 = W0 + 100 -H1 = H0 + 50 -area = W1 * H1 / 1000000 -weight = area * thickness * density -``` - -### 4. bom_condition_rules -조건부 BOM 구성 규칙을 관리하는 테이블입니다. - -**주요 기능:** -- 조건식 기반 제품 포함/제외 -- 수량 계산 공식 적용 -- 우선순위 기반 규칙 실행 -- 그룹 또는 개별 제품 대상 지정 - -**핵심 컬럼:** -- `condition_expression`: 조건 표현식 ("screen_type == 'SCREEN'") -- `action_type`: 액션 타입 (INCLUDE_PRODUCT, EXCLUDE_PRODUCT, SET_QUANTITY) -- `quantity_formula`: 수량 계산 공식 -- `priority`: 실행 우선순위 - -**조건 규칙 예시:** -``` -IF screen_type == "SCREEN" THEN INCLUDE_PRODUCT 실리카겔 -IF W0 > 1000 THEN SET_QUANTITY 가이드레일 = ceiling(W0/1000) -``` - -### 5. product_parameters -실제 제품별 매개변수 값을 저장하는 테이블입니다. - -**주요 기능:** -- 견적/주문별 매개변수 값 저장 -- 상태 관리 (DRAFT, CALCULATED, APPROVED) -- 제품 코드 연결 -- 버전 관리 지원 - -**핵심 컬럼:** -- `parameter_values`: 매개변수 값들 (JSON) -- `status`: 상태 관리 -- `product_code`: 제품 코드 참조 - -**JSON 구조 예시:** -```json -{ - "W0": 1200, - "H0": 800, - "screen_type": "SCREEN", - "power_source": "AC220V", - "installation_type": "WALL_MOUNT" -} -``` - -### 6. product_calculated_values -매개변수 기반 계산 결과를 저장하는 테이블입니다. - -**주요 기능:** -- 공식 계산 결과 캐싱 -- BOM 스냅샷 저장 -- 계산 유효성 관리 -- 비용/중량 집계 - -**핵심 컬럼:** -- `calculated_values`: 계산된 값들 (JSON) -- `bom_snapshot`: BOM 결과 스냅샷 (JSON) -- `is_valid`: 계산 유효성 -- `total_cost/total_weight`: 집계 정보 - -**JSON 구조 예시:** -```json -{ - "calculated_values": { - "W1": 1300, - "H1": 850, - "area": 1.105, - "weight": 45.2 - }, - "bom_snapshot": [ - { - "product_id": 1001, - "product_name": "스크린 프레임", - "quantity": 1, - "group_name": "본체" - }, - { - "product_id": 2001, - "product_name": "가이드레일", - "quantity": 2, - "group_name": "가이드" - } - ] -} -``` - -## 기존 테이블 수정사항 - -### bom_template_items 테이블 확장 -- `group_id`: BOM 그룹 연결 -- `is_conditional`: 조건부 아이템 여부 -- `condition_expression`: 조건 표현식 -- `quantity_formula`: 수량 계산 공식 - -### products 테이블 확장 -- `is_parametric`: 매개변수 기반 제품 여부 -- `base_model_id`: 기반 모델 연결 -- `parameter_values`: 매개변수 값 (JSON) -- `calculated_values`: 계산값 (JSON) - -## 데이터 플로우 - -### 1. 설계 단계 -``` -models → model_parameters (매개변수 정의) - → model_formulas (공식 정의) - → bom_template_groups (그룹 정의) - → bom_condition_rules (조건 규칙) - → bom_template_items (기본 BOM + 그룹 연결) -``` - -### 2. 견적/주문 단계 -``` -견적 요청 → product_parameters (매개변수 입력) - → 공식 계산 엔진 실행 - → product_calculated_values (결과 저장) - → BOM 생성 및 스냅샷 저장 -``` - -### 3. 계산 엔진 프로세스 -1. **매개변수 검증**: model_parameters 기반 유효성 검사 -2. **공식 계산**: model_formulas의 calculation_order 순서로 실행 -3. **조건 평가**: bom_condition_rules의 priority 순서로 평가 -4. **BOM 구성**: 조건 결과에 따른 제품 포함/제외 및 수량 계산 -5. **결과 저장**: product_calculated_values에 스냅샷 저장 - -## 성능 최적화 - -### 인덱스 전략 -- **복합 인덱스**: (tenant_id, model_id) 기반 조회 최적화 -- **정렬 인덱스**: display_order, priority, calculation_order -- **유니크 인덱스**: 논리적 중복 방지 - -### 캐싱 전략 -- **계산 결과 캐싱**: product_calculated_values로 반복 계산 방지 -- **BOM 스냅샷**: 조건부 계산 결과 캐싱 -- **유효성 플래그**: is_valid로 재계산 필요 여부 판단 - -### JSON 컬럼 활용 -- **스키마 유연성**: 매개변수/계산값의 동적 구조 지원 -- **성능 고려**: 필요시 가상 컬럼으로 자주 조회되는 값 추출 가능 - -## 보안 및 제약사항 - -### 공식 표현식 보안 -- **안전한 연산만 허용**: 수학 연산자, 함수명 화이트리스트 -- **코드 실행 방지**: eval() 등 동적 코드 실행 금지 -- **입력 검증**: 공식 구문 분석 및 검증 - -### 다중 테넌트 지원 -- **테넌트 격리**: 모든 테이블에 tenant_id 적용 -- **글로벌 스코프**: BelongsToTenant 자동 적용 -- **권한 관리**: 테넌트별 접근 제어 - -## 확장성 고려사항 - -### 모델 타입 확장 -- **다른 제품군**: 현재 스키마로 다양한 제품 모델 지원 가능 -- **공식 엔진**: 모델별 독립적인 공식 정의 -- **조건 규칙**: 제품군별 특화된 비즈니스 로직 구현 - -### 계산 엔진 확장 -- **외부 API**: 복잡한 계산을 위한 외부 서비스 연동 가능 -- **배치 처리**: 대량 계산 요청 처리 지원 -- **이력 관리**: 계산 과정 및 결과 이력 추적 가능 - -## 예상 API 엔드포인트 - -### 설계용 API -- `GET /v1/models/{id}/parameters` - 모델 매개변수 목록 -- `POST /v1/models/{id}/parameters` - 매개변수 정의 추가 -- `GET /v1/models/{id}/formulas` - 공식 목록 -- `POST /v1/models/{id}/formulas` - 공식 정의 추가 -- `GET /v1/models/{id}/bom-rules` - 조건 규칙 목록 -- `POST /v1/models/{id}/bom-rules` - 조건 규칙 추가 - -### 계산용 API -- `POST /v1/models/{id}/calculate` - 매개변수 기반 BOM 계산 -- `GET /v1/product-parameters/{id}/calculated` - 계산 결과 조회 -- `POST /v1/product-parameters/{id}/recalculate` - 재계산 요청 -- `GET /v1/models/{id}/bom-preview` - BOM 미리보기 (매개변수별) \ No newline at end of file diff --git a/scripts/validation/README.md b/scripts/validation/README.md deleted file mode 100644 index ad21404..0000000 --- a/scripts/validation/README.md +++ /dev/null @@ -1,424 +0,0 @@ -# Parametric BOM Validation Scripts - -This directory contains standalone validation scripts for comprehensive testing of the parametric BOM system. - -## Scripts Overview - -### 1. validate_bom_system.php - -**Purpose**: Complete system validation across all components - -**Features**: -- Database connectivity testing -- Model operations validation -- Parameter validation testing -- Formula calculation testing -- Condition rule evaluation testing -- BOM resolution testing -- Performance benchmarking -- Error handling validation - -**Usage**: -```bash -php scripts/validation/validate_bom_system.php -``` - -**Exit Codes**: -- `0` - All tests passed (≥90% success rate) -- `1` - Some issues found (75-89% success rate) -- `2` - Critical issues found (<75% success rate) - -### 2. test_kss01_scenarios.php - -**Purpose**: Business scenario testing for KSS01 model - -**Features**: -- Residential scenario testing -- Commercial scenario testing -- Edge case scenario testing -- Material type validation -- Installation type validation -- Performance scenario testing - -**Usage**: -```bash -php scripts/validation/test_kss01_scenarios.php -``` - -**Test Scenarios**: -- Small bedroom window (800x600) -- Standard patio door (1800x2100) -- Large living room window (2400x1500) -- Restaurant storefront (3000x2500) -- Office building entrance (2200x2400) -- Warehouse opening (4000x3000) -- Minimum size opening (600x400) -- Maximum size opening (3000x2500) - -**Exit Codes**: -- `0` - All scenarios passed (≥95% success rate) -- `1` - Some edge cases failed (85-94% success rate) -- `2` - Critical business logic issues (<85% success rate) - -### 3. performance_test.php - -**Purpose**: Performance and scalability testing - -**Features**: -- Single resolution performance testing -- Batch resolution performance testing -- Memory usage analysis -- Database query efficiency testing -- Parameter variation performance testing -- Concurrent resolution simulation -- Large dataset throughput testing - -**Usage**: -```bash -php scripts/validation/performance_test.php -``` - -**Performance Thresholds**: -- Single resolution: <200ms -- Batch 10 resolutions: <1.5s -- Batch 100 resolutions: <12s -- Memory usage: <50MB -- DB queries per resolution: <20 -- Concurrent resolution: <500ms avg -- Throughput: ≥10 resolutions/second - -**Exit Codes**: -- `0` - Performance requirements met (≥90% tests passed) -- `1` - Performance issues detected (70-89% tests passed) -- `2` - Critical performance issues (<70% tests passed) - -## Prerequisites - -### 1. Test Data Setup - -Run the KSS01ModelSeeder to create required test data: - -```bash -php artisan db:seed --class=KSS01ModelSeeder -``` - -This creates: -- KSS_DEMO tenant -- demo@kss01.com user (password: kss01demo) -- KSS01 model with parameters, formulas, and rules -- Test materials and products - -### 2. Environment Configuration - -Ensure your `.env` file has proper database configuration: - -```env -DB_CONNECTION=mysql -DB_HOST=127.0.0.1 -DB_PORT=3306 -DB_DATABASE=sam_api -DB_USERNAME=your_username -DB_PASSWORD=your_password -``` - -### 3. API Configuration - -Ensure API keys are properly configured for testing. - -## Running All Validations - -Run all validation scripts in sequence: - -```bash -#!/bin/bash -echo "Starting comprehensive BOM system validation..." - -echo "\n=== 1. System Validation ===" -php scripts/validation/validate_bom_system.php -SYSTEM_EXIT=$? - -echo "\n=== 2. KSS01 Scenarios ===" -php scripts/validation/test_kss01_scenarios.php -SCENARIOS_EXIT=$? - -echo "\n=== 3. Performance Testing ===" -php scripts/validation/performance_test.php -PERFORMANCE_EXIT=$? - -echo "\n=== FINAL RESULTS ===" -echo "System Validation: $([ $SYSTEM_EXIT -eq 0 ] && echo "PASS" || echo "FAIL")" -echo "KSS01 Scenarios: $([ $SCENARIOS_EXIT -eq 0 ] && echo "PASS" || echo "FAIL")" -echo "Performance Tests: $([ $PERFORMANCE_EXIT -eq 0 ] && echo "PASS" || echo "FAIL")" - -# Overall result -if [ $SYSTEM_EXIT -eq 0 ] && [ $SCENARIOS_EXIT -eq 0 ] && [ $PERFORMANCE_EXIT -eq 0 ]; then - echo "\n🎉 ALL VALIDATIONS PASSED - System ready for production" - exit 0 -else - echo "\n❌ SOME VALIDATIONS FAILED - Review required" - exit 1 -fi -``` - -## Output Examples - -### Successful Validation - -``` -=== Parametric BOM System Validation === -Starting comprehensive system validation... - -🔍 Testing Database Connectivity... - ✅ Database connection - ✅ Tenant table access - ✅ Design models table access - -🔍 Testing Model Operations... - ✅ KSS01 model exists - ✅ Model has parameters - ✅ Model has formulas - ✅ Model has condition rules - -🔍 Testing Parameter Validation... - ✅ Valid parameters accepted - ✅ Parameter range validation - ✅ Required parameter validation - -🔍 Testing Formula Calculations... - ✅ Basic formula calculations - ✅ Formula dependency resolution - -🔍 Testing Condition Rule Evaluation... - ✅ Basic rule evaluation - ✅ Rule action generation - ✅ Material type rule evaluation - -🔍 Testing BOM Resolution... - ✅ Complete BOM resolution - ✅ BOM items have required fields - ✅ BOM preview functionality - ✅ BOM comparison functionality - -🔍 Testing Performance Benchmarks... - ✅ Single BOM resolution performance (<500ms) - Duration: 156ms - ✅ Multiple BOM resolutions performance (10 iterations <2s) - Duration: 892ms - -============================================================ -VALIDATION REPORT -============================================================ -Total Tests: 18 -Passed: 18 -Failed: 0 -Success Rate: 100.0% - -🎉 VALIDATION PASSED - System is ready for production -``` - -### Failed Validation - -``` -❌ Single BOM resolution performance (<500ms) - Duration: 650ms -❌ BOM comparison functionality - Error: Undefined index: bom_diff - -============================================================ -VALIDATION REPORT -============================================================ -Total Tests: 18 -Passed: 15 -Failed: 3 -Success Rate: 83.3% - -❌ FAILED TESTS: - • Single BOM resolution performance (<500ms) - • BOM comparison functionality - Undefined index: bom_diff - • Multiple BOM resolutions performance (10 iterations <2s) - -⚠️ VALIDATION WARNING - Some issues found, review required -``` - -## Customization - -### Adding New Test Scenarios - -To add new test scenarios to `test_kss01_scenarios.php`: - -```php -// Add to testResidentialScenarios() method -$this->testScenario('Custom Scenario Name', [ - 'W0' => 1500, - 'H0' => 1000, - 'screen_type' => 'FABRIC', - 'install_type' => 'WALL' -], [ - 'expectMotor' => 'MOTOR_KSS01_STD', - 'expectBrackets' => 2, - 'expectMaterial' => 'FABRIC_KSS01' -]); -``` - -### Adjusting Performance Thresholds - -Modify the `THRESHOLDS` constant in `performance_test.php`: - -```php -private const THRESHOLDS = [ - 'single_resolution_ms' => 300, // Increase from 200ms - 'batch_10_resolution_ms' => 2000, // Increase from 1500ms - 'memory_usage_mb' => 75, // Increase from 50MB - // ... other thresholds -]; -``` - -### Adding Custom Validation Tests - -Add new test methods to `validate_bom_system.php`: - -```php -private function testCustomValidation(): void -{ - $this->output("\n🔍 Testing Custom Validation..."); - - $this->test('Custom test name', function() { - // Your test logic here - return true; // or false - }); -} -``` - -## Integration with CI/CD - -### GitHub Actions Example - -```yaml -name: BOM System Validation - -on: [push, pull_request] - -jobs: - validate: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: 8.2 - extensions: pdo, mysql - - - name: Install Dependencies - run: composer install - - - name: Setup Database - run: | - php artisan migrate - php artisan db:seed --class=KSS01ModelSeeder - - - name: Run System Validation - run: php scripts/validation/validate_bom_system.php - - - name: Run Scenario Tests - run: php scripts/validation/test_kss01_scenarios.php - - - name: Run Performance Tests - run: php scripts/validation/performance_test.php -``` - -### Docker Integration - -```dockerfile -# Add to Dockerfile for validation image -COPY scripts/validation /app/scripts/validation -RUN chmod +x /app/scripts/validation/*.php - -# Validation command -CMD ["php", "scripts/validation/validate_bom_system.php"] -``` - -## Monitoring and Alerts - -### Log Analysis - -All validation scripts log to Laravel's logging system. Monitor logs for: - -```bash -grep "BOM Validation" storage/logs/laravel.log -grep "KSS01 Scenarios" storage/logs/laravel.log -grep "BOM Performance" storage/logs/laravel.log -``` - -### Automated Monitoring - -Set up automated monitoring to run validations periodically: - -```bash -# Crontab entry for daily validation -0 2 * * * cd /path/to/project && php scripts/validation/validate_bom_system.php >> /var/log/bom_validation.log 2>&1 -``` - -### Alerting on Failures - -```bash -#!/bin/bash -# validation_monitor.sh -php scripts/validation/validate_bom_system.php -if [ $? -ne 0 ]; then - curl -X POST -H 'Content-type: application/json' \ - --data '{"text":"BOM System Validation Failed!"}' \ - YOUR_SLACK_WEBHOOK_URL -fi -``` - -## Troubleshooting - -### Common Issues - -1. **"KSS_DEMO tenant not found"** - ```bash - php artisan db:seed --class=KSS01ModelSeeder - ``` - -2. **Memory limit exceeded** - ```bash - php -d memory_limit=512M scripts/validation/performance_test.php - ``` - -3. **Database connection errors** - - Check database credentials in `.env` - - Verify database server is running - - Check network connectivity - -4. **Performance test failures** - - Check system load and available resources - - Verify database indexes are created - - Review query optimization - -### Debug Mode - -Enable debug output in validation scripts: - -```php -// Add to script top -define('DEBUG_MODE', true); - -// Use in test methods -if (defined('DEBUG_MODE') && DEBUG_MODE) { - $this->output("Debug: " . json_encode($result)); -} -``` - -## Contributing - -When adding new validation scripts: - -1. Follow the existing error handling patterns -2. Use consistent exit codes -3. Provide clear progress output -4. Include performance considerations -5. Add comprehensive documentation -6. Test with various data scenarios \ No newline at end of file diff --git a/scripts/validation/performance_test.php b/scripts/validation/performance_test.php deleted file mode 100755 index c1a56db..0000000 --- a/scripts/validation/performance_test.php +++ /dev/null @@ -1,559 +0,0 @@ -#!/usr/bin/env php - 200, // Single BOM resolution should be under 200ms - 'batch_10_resolution_ms' => 1500, // 10 resolutions should be under 1.5s - 'batch_100_resolution_ms' => 12000, // 100 resolutions should be under 12s - 'memory_usage_mb' => 50, // Memory usage should stay under 50MB - 'db_queries_per_resolution' => 20, // Should not exceed 20 DB queries per resolution - 'concurrent_resolution_ms' => 500 // Concurrent resolutions should complete under 500ms - ]; - - public function __construct() - { - $this->output("\n=== Parametric BOM Performance Testing ==="); - $this->output("Testing system performance and scalability...\n"); - - $this->setupServices(); - } - - private function setupServices(): void - { - // Find test tenant and model - $this->tenant = Tenant::where('code', 'KSS_DEMO')->first(); - if (!$this->tenant) { - throw new Exception('KSS_DEMO tenant not found. Please run KSS01ModelSeeder first.'); - } - - $this->model = DesignModel::where('tenant_id', $this->tenant->id) - ->where('code', 'KSS01') - ->first(); - if (!$this->model) { - throw new Exception('KSS01 model not found. Please run KSS01ModelSeeder first.'); - } - - // Setup BOM resolver with query logging - $this->bomResolver = new BomResolverService( - new ModelParameterService(), - new ModelFormulaService(), - new BomConditionRuleService() - ); - $this->bomResolver->setTenantId($this->tenant->id); - $this->bomResolver->setApiUserId(1); - } - - public function run(): void - { - try { - // Enable query logging for database performance analysis - DB::enableQueryLog(); - - // Test single resolution performance - $this->testSingleResolutionPerformance(); - - // Test batch resolution performance - $this->testBatchResolutionPerformance(); - - // Test memory usage - $this->testMemoryUsage(); - - // Test database query efficiency - $this->testDatabaseQueryEfficiency(); - - // Test different parameter combinations - $this->testParameterVariationPerformance(); - - // Test concurrent resolution (simulated) - $this->testConcurrentResolution(); - - // Test large dataset performance - $this->testLargeDatasetPerformance(); - - // Generate performance report - $this->generatePerformanceReport(); - - } catch (Exception $e) { - $this->output("\n❌ Critical Error: " . $e->getMessage()); - exit(1); - } - } - - private function testSingleResolutionPerformance(): void - { - $this->output("\n🏁 Testing Single Resolution Performance..."); - - $testParams = [ - 'W0' => 1200, - 'H0' => 800, - 'screen_type' => 'FABRIC', - 'install_type' => 'WALL' - ]; - - // Warm-up run - $this->bomResolver->resolveBom($this->model->id, $testParams); - - // Performance test runs - $times = []; - $runs = 10; - - for ($i = 0; $i < $runs; $i++) { - $startTime = microtime(true); - $result = $this->bomResolver->resolveBom($this->model->id, $testParams); - $endTime = microtime(true); - - $times[] = ($endTime - $startTime) * 1000; - } - - $avgTime = array_sum($times) / count($times); - $minTime = min($times); - $maxTime = max($times); - - $this->performanceResults['single_resolution'] = [ - 'avg_time_ms' => $avgTime, - 'min_time_ms' => $minTime, - 'max_time_ms' => $maxTime, - 'runs' => $runs - ]; - - $passed = $avgTime < self::THRESHOLDS['single_resolution_ms']; - $this->recordTest('Single Resolution Performance', $passed); - - $this->output(sprintf(" Avg: %.2fms, Min: %.2fms, Max: %.2fms (Target: <%.0fms)", - $avgTime, $minTime, $maxTime, self::THRESHOLDS['single_resolution_ms'])); - - if ($passed) { - $this->output(" ✅ Single resolution performance within threshold"); - } else { - $this->output(" ❌ Single resolution performance exceeds threshold"); - } - } - - private function testBatchResolutionPerformance(): void - { - $this->output("\n📈 Testing Batch Resolution Performance..."); - - $scenarios = $this->generateTestScenarios(10); - - // Test batch of 10 - $startTime = microtime(true); - foreach ($scenarios as $params) { - $this->bomResolver->resolveBom($this->model->id, $params); - } - $batch10Time = (microtime(true) - $startTime) * 1000; - - $this->performanceResults['batch_10_resolution'] = [ - 'total_time_ms' => $batch10Time, - 'avg_per_resolution_ms' => $batch10Time / 10 - ]; - - $passed10 = $batch10Time < self::THRESHOLDS['batch_10_resolution_ms']; - $this->recordTest('Batch 10 Resolution Performance', $passed10); - - $this->output(sprintf(" Batch 10: %.2fms total, %.2fms avg (Target: <%.0fms total)", - $batch10Time, $batch10Time / 10, self::THRESHOLDS['batch_10_resolution_ms'])); - - // Test batch of 100 - $largeBatch = $this->generateTestScenarios(100); - - $startTime = microtime(true); - foreach ($largeBatch as $params) { - $this->bomResolver->resolveBom($this->model->id, $params); - } - $batch100Time = (microtime(true) - $startTime) * 1000; - - $this->performanceResults['batch_100_resolution'] = [ - 'total_time_ms' => $batch100Time, - 'avg_per_resolution_ms' => $batch100Time / 100 - ]; - - $passed100 = $batch100Time < self::THRESHOLDS['batch_100_resolution_ms']; - $this->recordTest('Batch 100 Resolution Performance', $passed100); - - $this->output(sprintf(" Batch 100: %.2fms total, %.2fms avg (Target: <%.0fms total)", - $batch100Time, $batch100Time / 100, self::THRESHOLDS['batch_100_resolution_ms'])); - } - - private function testMemoryUsage(): void - { - $this->output("\n💾 Testing Memory Usage..."); - - $initialMemory = memory_get_usage(true); - $peakMemory = memory_get_peak_usage(true); - - // Perform multiple resolutions and monitor memory - $scenarios = $this->generateTestScenarios(50); - - foreach ($scenarios as $params) { - $this->bomResolver->resolveBom($this->model->id, $params); - } - - $finalMemory = memory_get_usage(true); - $finalPeakMemory = memory_get_peak_usage(true); - - $memoryUsed = ($finalMemory - $initialMemory) / 1024 / 1024; - $peakMemoryUsed = ($finalPeakMemory - $peakMemory) / 1024 / 1024; - - $this->performanceResults['memory_usage'] = [ - 'memory_used_mb' => $memoryUsed, - 'peak_memory_used_mb' => $peakMemoryUsed, - 'initial_memory_mb' => $initialMemory / 1024 / 1024, - 'final_memory_mb' => $finalMemory / 1024 / 1024 - ]; - - $passed = $peakMemoryUsed < self::THRESHOLDS['memory_usage_mb']; - $this->recordTest('Memory Usage', $passed); - - $this->output(sprintf(" Memory used: %.2fMB, Peak: %.2fMB (Target: <%.0fMB)", - $memoryUsed, $peakMemoryUsed, self::THRESHOLDS['memory_usage_mb'])); - - if ($passed) { - $this->output(" ✅ Memory usage within acceptable limits"); - } else { - $this->output(" ❌ Memory usage exceeds threshold"); - } - } - - private function testDatabaseQueryEfficiency(): void - { - $this->output("\n📊 Testing Database Query Efficiency..."); - - // Clear query log - DB::flushQueryLog(); - - $testParams = [ - 'W0' => 1500, - 'H0' => 1000, - 'screen_type' => 'STEEL', - 'install_type' => 'CEILING' - ]; - - // Perform resolution and count queries - $this->bomResolver->resolveBom($this->model->id, $testParams); - - $queries = DB::getQueryLog(); - $queryCount = count($queries); - - // Analyze query types - $selectQueries = 0; - $totalQueryTime = 0; - - foreach ($queries as $query) { - if (stripos($query['query'], 'select') === 0) { - $selectQueries++; - } - $totalQueryTime += $query['time']; - } - - $this->performanceResults['database_efficiency'] = [ - 'total_queries' => $queryCount, - 'select_queries' => $selectQueries, - 'total_query_time_ms' => $totalQueryTime, - 'avg_query_time_ms' => $queryCount > 0 ? $totalQueryTime / $queryCount : 0 - ]; - - $passed = $queryCount <= self::THRESHOLDS['db_queries_per_resolution']; - $this->recordTest('Database Query Efficiency', $passed); - - $this->output(sprintf(" Total queries: %d, Select queries: %d, Total time: %.2fms (Target: <%d queries)", - $queryCount, $selectQueries, $totalQueryTime, self::THRESHOLDS['db_queries_per_resolution'])); - - if ($passed) { - $this->output(" ✅ Database query count within threshold"); - } else { - $this->output(" ❌ Too many database queries per resolution"); - } - - // Show some sample queries for analysis - if (count($queries) > 0) { - $this->output(" Sample queries:"); - $sampleCount = min(3, count($queries)); - for ($i = 0; $i < $sampleCount; $i++) { - $query = $queries[$i]; - $shortQuery = substr($query['query'], 0, 80) . (strlen($query['query']) > 80 ? '...' : ''); - $this->output(sprintf(" %s (%.2fms)", $shortQuery, $query['time'])); - } - } - } - - private function testParameterVariationPerformance(): void - { - $this->output("\n🔄 Testing Parameter Variation Performance..."); - - $variations = [ - 'Small' => ['W0' => 600, 'H0' => 400, 'screen_type' => 'FABRIC', 'install_type' => 'WALL'], - 'Medium' => ['W0' => 1200, 'H0' => 800, 'screen_type' => 'FABRIC', 'install_type' => 'WALL'], - 'Large' => ['W0' => 2400, 'H0' => 1800, 'screen_type' => 'STEEL', 'install_type' => 'CEILING'], - 'Extra Large' => ['W0' => 3000, 'H0' => 2500, 'screen_type' => 'STEEL', 'install_type' => 'RECESSED'] - ]; - - $variationResults = []; - - foreach ($variations as $name => $params) { - $times = []; - $runs = 5; - - for ($i = 0; $i < $runs; $i++) { - $startTime = microtime(true); - $result = $this->bomResolver->resolveBom($this->model->id, $params); - $endTime = microtime(true); - - $times[] = ($endTime - $startTime) * 1000; - } - - $avgTime = array_sum($times) / count($times); - $bomItemCount = count($result['resolved_bom']); - - $variationResults[$name] = [ - 'avg_time_ms' => $avgTime, - 'bom_items' => $bomItemCount, - 'area' => $result['calculated_values']['area'] - ]; - - $this->output(sprintf(" %s: %.2fms, %d BOM items, %.2fm²", - $name, $avgTime, $bomItemCount, $result['calculated_values']['area'])); - } - - $this->performanceResults['parameter_variations'] = $variationResults; - - // Check if performance is consistent across variations - $times = array_column($variationResults, 'avg_time_ms'); - $maxVariation = max($times) - min($times); - $avgVariation = array_sum($times) / count($times); - - $passed = $maxVariation < ($avgVariation * 2); // Variation should not exceed 200% of average - $this->recordTest('Parameter Variation Performance Consistency', $passed); - - if ($passed) { - $this->output(" ✅ Performance consistent across parameter variations"); - } else { - $this->output(" ❌ Performance varies significantly with different parameters"); - } - } - - private function testConcurrentResolution(): void - { - $this->output("\n🚀 Testing Concurrent Resolution (Simulated)..."); - - // Simulate concurrent requests by rapidly executing multiple resolutions - $concurrentCount = 5; - $scenarios = $this->generateTestScenarios($concurrentCount); - - $startTime = microtime(true); - - // Simulate concurrent processing - $results = []; - foreach ($scenarios as $i => $params) { - $resolveStart = microtime(true); - $result = $this->bomResolver->resolveBom($this->model->id, $params); - $resolveEnd = microtime(true); - - $results[] = [ - 'index' => $i, - 'time_ms' => ($resolveEnd - $resolveStart) * 1000, - 'bom_items' => count($result['resolved_bom']) - ]; - } - - $totalTime = (microtime(true) - $startTime) * 1000; - $avgConcurrentTime = array_sum(array_column($results, 'time_ms')) / count($results); - - $this->performanceResults['concurrent_resolution'] = [ - 'concurrent_count' => $concurrentCount, - 'total_time_ms' => $totalTime, - 'avg_resolution_time_ms' => $avgConcurrentTime, - 'results' => $results - ]; - - $passed = $avgConcurrentTime < self::THRESHOLDS['concurrent_resolution_ms']; - $this->recordTest('Concurrent Resolution Performance', $passed); - - $this->output(sprintf(" %d concurrent resolutions: %.2fms total, %.2fms avg (Target: <%.0fms avg)", - $concurrentCount, $totalTime, $avgConcurrentTime, self::THRESHOLDS['concurrent_resolution_ms'])); - - if ($passed) { - $this->output(" ✅ Concurrent resolution performance acceptable"); - } else { - $this->output(" ❌ Concurrent resolution performance degraded"); - } - } - - private function testLargeDatasetPerformance(): void - { - $this->output("\n📊 Testing Large Dataset Performance..."); - - // Test with large number of variations - $largeDataset = $this->generateTestScenarios(200); - - $batchSize = 20; - $batchTimes = []; - - for ($i = 0; $i < count($largeDataset); $i += $batchSize) { - $batch = array_slice($largeDataset, $i, $batchSize); - - $batchStart = microtime(true); - foreach ($batch as $params) { - $this->bomResolver->resolveBom($this->model->id, $params); - } - $batchEnd = microtime(true); - - $batchTimes[] = ($batchEnd - $batchStart) * 1000; - } - - $totalTime = array_sum($batchTimes); - $avgBatchTime = $totalTime / count($batchTimes); - $throughput = count($largeDataset) / ($totalTime / 1000); // resolutions per second - - $this->performanceResults['large_dataset'] = [ - 'total_resolutions' => count($largeDataset), - 'batch_size' => $batchSize, - 'total_time_ms' => $totalTime, - 'avg_batch_time_ms' => $avgBatchTime, - 'throughput_per_second' => $throughput - ]; - - $passed = $throughput >= 10; // At least 10 resolutions per second - $this->recordTest('Large Dataset Throughput', $passed); - - $this->output(sprintf(" %d resolutions: %.2fms total, %.2f resolutions/sec (Target: >=10/sec)", - count($largeDataset), $totalTime, $throughput)); - - if ($passed) { - $this->output(" ✅ Large dataset throughput acceptable"); - } else { - $this->output(" ❌ Large dataset throughput too low"); - } - } - - private function generateTestScenarios(int $count): array - { - $scenarios = []; - $widths = [600, 800, 1000, 1200, 1500, 1800, 2000, 2400, 3000]; - $heights = [400, 600, 800, 1000, 1200, 1500, 1800, 2000, 2500]; - $screenTypes = ['FABRIC', 'STEEL']; - $installTypes = ['WALL', 'CEILING', 'RECESSED']; - - for ($i = 0; $i < $count; $i++) { - $scenarios[] = [ - 'W0' => $widths[array_rand($widths)], - 'H0' => $heights[array_rand($heights)], - 'screen_type' => $screenTypes[array_rand($screenTypes)], - 'install_type' => $installTypes[array_rand($installTypes)] - ]; - } - - return $scenarios; - } - - private function recordTest(string $name, bool $passed): void - { - $this->totalTests++; - if ($passed) { - $this->passedTests++; - } - } - - private function output(string $message): void - { - echo $message . "\n"; - Log::info("BOM Performance: " . $message); - } - - private function generatePerformanceReport(): void - { - $this->output("\n" . str_repeat("=", 70)); - $this->output("PARAMETRIC BOM PERFORMANCE REPORT"); - $this->output(str_repeat("=", 70)); - - $successRate = $this->totalTests > 0 ? round(($this->passedTests / $this->totalTests) * 100, 1) : 0; - - $this->output("Total Performance Tests: {$this->totalTests}"); - $this->output("Passed: {$this->passedTests}"); - $this->output("Failed: " . ($this->totalTests - $this->passedTests)); - $this->output("Success Rate: {$successRate}%"); - - $this->output("\n📊 PERFORMANCE METRICS:"); - - // Single resolution performance - if (isset($this->performanceResults['single_resolution'])) { - $single = $this->performanceResults['single_resolution']; - $this->output(sprintf(" Single Resolution: %.2fms avg (Target: <%.0fms)", - $single['avg_time_ms'], self::THRESHOLDS['single_resolution_ms'])); - } - - // Batch performance - if (isset($this->performanceResults['batch_100_resolution'])) { - $batch = $this->performanceResults['batch_100_resolution']; - $this->output(sprintf(" Batch 100 Resolutions: %.2fms total, %.2fms avg", - $batch['total_time_ms'], $batch['avg_per_resolution_ms'])); - } - - // Memory usage - if (isset($this->performanceResults['memory_usage'])) { - $memory = $this->performanceResults['memory_usage']; - $this->output(sprintf(" Memory Usage: %.2fMB peak (Target: <%.0fMB)", - $memory['peak_memory_used_mb'], self::THRESHOLDS['memory_usage_mb'])); - } - - // Database efficiency - if (isset($this->performanceResults['database_efficiency'])) { - $db = $this->performanceResults['database_efficiency']; - $this->output(sprintf(" Database Queries: %d per resolution (Target: <%d)", - $db['total_queries'], self::THRESHOLDS['db_queries_per_resolution'])); - } - - // Throughput - if (isset($this->performanceResults['large_dataset'])) { - $throughput = $this->performanceResults['large_dataset']; - $this->output(sprintf(" Throughput: %.2f resolutions/second (Target: >=10/sec)", - $throughput['throughput_per_second'])); - } - - $this->output("\n" . str_repeat("=", 70)); - - if ($successRate >= 90) { - $this->output("🎉 PERFORMANCE TESTS PASSED - System meets performance requirements"); - exit(0); - } elseif ($successRate >= 70) { - $this->output("⚠️ PERFORMANCE WARNING - Some performance issues detected"); - exit(1); - } else { - $this->output("❌ PERFORMANCE TESTS FAILED - Critical performance issues found"); - exit(2); - } - } -} - -// Run the performance tests -$tester = new BomPerformanceTester(); -$tester->run(); diff --git a/scripts/validation/test_kss01_scenarios.php b/scripts/validation/test_kss01_scenarios.php deleted file mode 100755 index e3eb0c3..0000000 --- a/scripts/validation/test_kss01_scenarios.php +++ /dev/null @@ -1,581 +0,0 @@ -#!/usr/bin/env php -output("\n=== KSS01 Scenario Testing ==="); - $this->output("Testing real-world KSS01 model scenarios...\n"); - - $this->setupServices(); - } - - private function setupServices(): void - { - // Find KSS01 tenant and model - $this->tenant = Tenant::where('code', 'KSS_DEMO')->first(); - if (!$this->tenant) { - throw new Exception('KSS_DEMO tenant not found. Please run KSS01ModelSeeder first.'); - } - - $this->model = DesignModel::where('tenant_id', $this->tenant->id) - ->where('code', 'KSS01') - ->first(); - if (!$this->model) { - throw new Exception('KSS01 model not found. Please run KSS01ModelSeeder first.'); - } - - // Setup BOM resolver - $this->bomResolver = new BomResolverService( - new ModelParameterService(), - new ModelFormulaService(), - new BomConditionRuleService() - ); - $this->bomResolver->setTenantId($this->tenant->id); - $this->bomResolver->setApiUserId(1); - } - - public function run(): void - { - try { - // Test standard residential scenarios - $this->testResidentialScenarios(); - - // Test commercial scenarios - $this->testCommercialScenarios(); - - // Test edge case scenarios - $this->testEdgeCaseScenarios(); - - // Test material type scenarios - $this->testMaterialTypeScenarios(); - - // Test installation type scenarios - $this->testInstallationTypeScenarios(); - - // Test performance under different loads - $this->testPerformanceScenarios(); - - // Generate scenario report - $this->generateScenarioReport(); - - } catch (Exception $e) { - $this->output("\n❌ Critical Error: " . $e->getMessage()); - exit(1); - } - } - - private function testResidentialScenarios(): void - { - $this->output("\n🏠 Testing Residential Scenarios..."); - - // Small window screen (typical bedroom) - $this->testScenario('Small Bedroom Window', [ - 'W0' => 800, - 'H0' => 600, - 'screen_type' => 'FABRIC', - 'install_type' => 'WALL' - ], [ - 'expectMotor' => 'MOTOR_KSS01_STD', - 'expectBrackets' => 2, - 'expectMaterial' => 'FABRIC_KSS01', - 'maxWeight' => 15.0 - ]); - - // Standard patio door - $this->testScenario('Standard Patio Door', [ - 'W0' => 1800, - 'H0' => 2100, - 'screen_type' => 'FABRIC', - 'install_type' => 'WALL' - ], [ - 'expectMotor' => 'MOTOR_KSS01_HEAVY', // Large area needs heavy motor - 'expectBrackets' => 3, // Wide opening needs extra brackets - 'expectMaterial' => 'FABRIC_KSS01' - ]); - - // Large living room window - $this->testScenario('Large Living Room Window', [ - 'W0' => 2400, - 'H0' => 1500, - 'screen_type' => 'FABRIC', - 'install_type' => 'CEILING' - ], [ - 'expectMotor' => 'MOTOR_KSS01_HEAVY', - 'expectBrackets' => 3, - 'expectMaterial' => 'FABRIC_KSS01', - 'installationType' => 'CEILING' - ]); - } - - private function testCommercialScenarios(): void - { - $this->output("\n🏢 Testing Commercial Scenarios..."); - - // Restaurant storefront - $this->testScenario('Restaurant Storefront', [ - 'W0' => 3000, - 'H0' => 2500, - 'screen_type' => 'STEEL', - 'install_type' => 'RECESSED' - ], [ - 'expectMotor' => 'MOTOR_KSS01_HEAVY', - 'expectMaterial' => 'STEEL_KSS01', - 'installationType' => 'RECESSED', - 'requiresWeatherSeal' => true - ]); - - // Office building entrance - $this->testScenario('Office Building Entrance', [ - 'W0' => 2200, - 'H0' => 2400, - 'screen_type' => 'STEEL', - 'install_type' => 'WALL' - ], [ - 'expectMotor' => 'MOTOR_KSS01_HEAVY', - 'expectMaterial' => 'STEEL_KSS01', - 'expectBrackets' => 3 - ]); - - // Warehouse opening - $this->testScenario('Warehouse Opening', [ - 'W0' => 4000, - 'H0' => 3000, - 'screen_type' => 'STEEL', - 'install_type' => 'CEILING' - ], [ - 'expectMotor' => 'MOTOR_KSS01_HEAVY', - 'expectMaterial' => 'STEEL_KSS01', - 'maxArea' => 15.0 // Large commercial area - ]); - } - - private function testEdgeCaseScenarios(): void - { - $this->output("\n⚠️ Testing Edge Case Scenarios..."); - - // Minimum size - $this->testScenario('Minimum Size Opening', [ - 'W0' => 600, // Minimum width - 'H0' => 400, // Minimum height - 'screen_type' => 'FABRIC', - 'install_type' => 'WALL' - ], [ - 'expectMotor' => 'MOTOR_KSS01_STD', - 'expectBrackets' => 2 - ]); - - // Maximum size - $this->testScenario('Maximum Size Opening', [ - 'W0' => 3000, // Maximum width - 'H0' => 2500, // Maximum height - 'screen_type' => 'STEEL', - 'install_type' => 'CEILING' - ], [ - 'expectMotor' => 'MOTOR_KSS01_HEAVY', - 'expectBrackets' => 3, - 'requiresWeatherSeal' => true - ]); - - // Very wide but short - $this->testScenario('Wide Short Opening', [ - 'W0' => 2800, - 'H0' => 800, - 'screen_type' => 'FABRIC', - 'install_type' => 'WALL' - ], [ - 'expectMotor' => 'MOTOR_KSS01_STD', // Area might still be small - 'expectBrackets' => 3 // But width requires extra brackets - ]); - - // Narrow but tall - $this->testScenario('Narrow Tall Opening', [ - 'W0' => 800, - 'H0' => 2400, - 'screen_type' => 'FABRIC', - 'install_type' => 'WALL' - ], [ - 'expectMotor' => 'MOTOR_KSS01_STD', - 'expectBrackets' => 2 - ]); - } - - private function testMaterialTypeScenarios(): void - { - $this->output("\n🧩 Testing Material Type Scenarios..."); - - // Fabric vs Steel comparison - $baseParams = ['W0' => 1200, 'H0' => 1800, 'install_type' => 'WALL']; - - $fabricResult = $this->resolveBom(array_merge($baseParams, ['screen_type' => 'FABRIC'])); - $steelResult = $this->resolveBom(array_merge($baseParams, ['screen_type' => 'STEEL'])); - - $this->testComparison('Fabric vs Steel Material Selection', $fabricResult, $steelResult, [ - 'fabricHasFabricMaterial' => true, - 'steelHasSteelMaterial' => true, - 'differentMaterials' => true - ]); - - // Verify material quantities match area - $this->testScenario('Fabric Material Quantity Calculation', [ - 'W0' => 1000, - 'H0' => 800, - 'screen_type' => 'FABRIC', - 'install_type' => 'WALL' - ], [ - 'verifyMaterialQuantity' => true - ]); - } - - private function testInstallationTypeScenarios(): void - { - $this->output("\n🔧 Testing Installation Type Scenarios..."); - - $baseParams = ['W0' => 1500, 'H0' => 2000, 'screen_type' => 'FABRIC']; - - // Wall installation - $this->testScenario('Wall Installation', array_merge($baseParams, ['install_type' => 'WALL']), [ - 'expectBrackets' => 3, - 'bracketType' => 'BRACKET_KSS01_WALL' - ]); - - // Ceiling installation - $this->testScenario('Ceiling Installation', array_merge($baseParams, ['install_type' => 'CEILING']), [ - 'expectBrackets' => 3, - 'bracketType' => 'BRACKET_KSS01_CEILING' - ]); - - // Recessed installation - $this->testScenario('Recessed Installation', array_merge($baseParams, ['install_type' => 'RECESSED']), [ - 'installationType' => 'RECESSED' - // Recessed might not need brackets or different bracket type - ]); - } - - private function testPerformanceScenarios(): void - { - $this->output("\n🏁 Testing Performance Scenarios..."); - - // Test batch processing - $scenarios = [ - ['W0' => 800, 'H0' => 600, 'screen_type' => 'FABRIC', 'install_type' => 'WALL'], - ['W0' => 1200, 'H0' => 800, 'screen_type' => 'STEEL', 'install_type' => 'CEILING'], - ['W0' => 1600, 'H0' => 1200, 'screen_type' => 'FABRIC', 'install_type' => 'RECESSED'], - ['W0' => 2000, 'H0' => 1500, 'screen_type' => 'STEEL', 'install_type' => 'WALL'], - ['W0' => 2400, 'H0' => 1800, 'screen_type' => 'FABRIC', 'install_type' => 'CEILING'] - ]; - - $this->testBatchPerformance('Batch BOM Resolution', $scenarios, [ - 'maxTotalTime' => 2000, // 2 seconds for all scenarios - 'maxAvgTime' => 400 // 400ms average per scenario - ]); - } - - private function testScenario(string $name, array $parameters, array $expectations): void - { - $this->totalScenarios++; - - try { - $result = $this->resolveBom($parameters); - $passed = $this->validateExpectations($result, $expectations); - - if ($passed) { - $this->passedScenarios++; - $this->output(" ✅ {$name}"); - $this->testResults[] = ['scenario' => $name, 'status' => 'PASS', 'result' => $result]; - } else { - $this->output(" ❌ {$name}"); - $this->testResults[] = ['scenario' => $name, 'status' => 'FAIL', 'result' => $result, 'expectations' => $expectations]; - } - - // Output key metrics - $this->outputScenarioMetrics($name, $result, $parameters); - - } catch (Exception $e) { - $this->output(" ❌ {$name} - Exception: {$e->getMessage()}"); - $this->testResults[] = ['scenario' => $name, 'status' => 'ERROR', 'error' => $e->getMessage()]; - } - } - - private function testComparison(string $name, array $result1, array $result2, array $expectations): void - { - $this->totalScenarios++; - - try { - $passed = $this->validateComparisonExpectations($result1, $result2, $expectations); - - if ($passed) { - $this->passedScenarios++; - $this->output(" ✅ {$name}"); - } else { - $this->output(" ❌ {$name}"); - } - - } catch (Exception $e) { - $this->output(" ❌ {$name} - Exception: {$e->getMessage()}"); - } - } - - private function testBatchPerformance(string $name, array $scenarios, array $expectations): void - { - $this->totalScenarios++; - - $startTime = microtime(true); - $times = []; - - try { - foreach ($scenarios as $params) { - $scenarioStart = microtime(true); - $this->resolveBom($params); - $times[] = (microtime(true) - $scenarioStart) * 1000; - } - - $totalTime = (microtime(true) - $startTime) * 1000; - $avgTime = array_sum($times) / count($times); - - $passed = $totalTime <= $expectations['maxTotalTime'] && - $avgTime <= $expectations['maxAvgTime']; - - if ($passed) { - $this->passedScenarios++; - $this->output(" ✅ {$name}"); - } else { - $this->output(" ❌ {$name}"); - } - - $this->output(" Total time: {$totalTime}ms, Avg time: {$avgTime}ms"); - - } catch (Exception $e) { - $this->output(" ❌ {$name} - Exception: {$e->getMessage()}"); - } - } - - private function resolveBom(array $parameters): array - { - return $this->bomResolver->resolveBom($this->model->id, $parameters); - } - - private function validateExpectations(array $result, array $expectations): bool - { - foreach ($expectations as $key => $expected) { - switch ($key) { - case 'expectMotor': - if (!$this->hasProductWithCode($result['resolved_bom'], $expected)) { - $this->output(" Expected motor {$expected} not found"); - return false; - } - break; - - case 'expectMaterial': - if (!$this->hasMaterialWithCode($result['resolved_bom'], $expected)) { - $this->output(" Expected material {$expected} not found"); - return false; - } - break; - - case 'expectBrackets': - $bracketCount = $this->countBrackets($result['resolved_bom']); - if ($bracketCount != $expected) { - $this->output(" Expected {$expected} brackets, found {$bracketCount}"); - return false; - } - break; - - case 'maxWeight': - if ($result['calculated_values']['weight'] > $expected) { - $this->output(" Weight {$result['calculated_values']['weight']}kg exceeds max {$expected}kg"); - return false; - } - break; - - case 'requiresWeatherSeal': - if ($expected && !$this->hasMaterialWithCode($result['resolved_bom'], 'SEAL_KSS01_WEATHER')) { - $this->output(" Expected weather seal not found"); - return false; - } - break; - - case 'verifyMaterialQuantity': - if (!$this->verifyMaterialQuantity($result)) { - $this->output(" Material quantity doesn't match area"); - return false; - } - break; - } - } - - return true; - } - - private function validateComparisonExpectations(array $result1, array $result2, array $expectations): bool - { - foreach ($expectations as $key => $expected) { - switch ($key) { - case 'differentMaterials': - $materials1 = $this->extractMaterialCodes($result1['resolved_bom']); - $materials2 = $this->extractMaterialCodes($result2['resolved_bom']); - if ($materials1 === $materials2) { - $this->output(" Expected different materials, but got same"); - return false; - } - break; - } - } - - return true; - } - - private function hasProductWithCode(array $bomItems, string $productCode): bool - { - foreach ($bomItems as $item) { - if ($item['target_type'] === 'PRODUCT' && - isset($item['target_info']['code']) && - $item['target_info']['code'] === $productCode) { - return true; - } - } - return false; - } - - private function hasMaterialWithCode(array $bomItems, string $materialCode): bool - { - foreach ($bomItems as $item) { - if ($item['target_type'] === 'MATERIAL' && - isset($item['target_info']['code']) && - $item['target_info']['code'] === $materialCode) { - return true; - } - } - return false; - } - - private function countBrackets(array $bomItems): int - { - $count = 0; - foreach ($bomItems as $item) { - if ($item['target_type'] === 'PRODUCT' && - isset($item['target_info']['code']) && - strpos($item['target_info']['code'], 'BRACKET') !== false) { - $count += $item['quantity']; - } - } - return $count; - } - - private function verifyMaterialQuantity(array $result): bool - { - $area = $result['calculated_values']['area']; - - foreach ($result['resolved_bom'] as $item) { - if ($item['target_type'] === 'MATERIAL' && - strpos($item['target_info']['code'], 'FABRIC') !== false || - strpos($item['target_info']['code'], 'STEEL') !== false) { - // Material quantity should roughly match area (allowing for waste) - return abs($item['quantity'] - $area) < ($area * 0.2); // 20% tolerance - } - } - - return true; - } - - private function extractMaterialCodes(array $bomItems): array - { - $codes = []; - foreach ($bomItems as $item) { - if ($item['target_type'] === 'MATERIAL') { - $codes[] = $item['target_info']['code'] ?? 'unknown'; - } - } - sort($codes); - return $codes; - } - - private function outputScenarioMetrics(string $name, array $result, array $parameters): void - { - $metrics = [ - 'Area' => round($result['calculated_values']['area'], 2) . 'm²', - 'Weight' => round($result['calculated_values']['weight'], 1) . 'kg', - 'BOM Items' => count($result['resolved_bom']) - ]; - - $metricsStr = implode(', ', array_map(function($k, $v) { - return "{$k}: {$v}"; - }, array_keys($metrics), $metrics)); - - $this->output(" {$metricsStr}"); - } - - private function output(string $message): void - { - echo $message . "\n"; - Log::info("KSS01 Scenarios: " . $message); - } - - private function generateScenarioReport(): void - { - $this->output("\n" . str_repeat("=", 60)); - $this->output("KSS01 SCENARIO TEST REPORT"); - $this->output(str_repeat("=", 60)); - - $successRate = $this->totalScenarios > 0 ? round(($this->passedScenarios / $this->totalScenarios) * 100, 1) : 0; - - $this->output("Total Scenarios: {$this->totalScenarios}"); - $this->output("Passed: {$this->passedScenarios}"); - $this->output("Failed: " . ($this->totalScenarios - $this->passedScenarios)); - $this->output("Success Rate: {$successRate}%"); - - // Show failed scenarios - $failedScenarios = array_filter($this->testResults, fn($r) => $r['status'] !== 'PASS'); - if (!empty($failedScenarios)) { - $this->output("\n❌ FAILED SCENARIOS:"); - foreach ($failedScenarios as $failed) { - $error = isset($failed['error']) ? " - {$failed['error']}" : ''; - $this->output(" • {$failed['scenario']}{$error}"); - } - } - - $this->output("\n" . str_repeat("=", 60)); - - if ($successRate >= 95) { - $this->output("🎉 KSS01 SCENARIOS PASSED - All business cases validated"); - exit(0); - } elseif ($successRate >= 85) { - $this->output("⚠️ KSS01 SCENARIOS WARNING - Some edge cases failed"); - exit(1); - } else { - $this->output("❌ KSS01 SCENARIOS FAILED - Critical business logic issues"); - exit(2); - } - } -} - -// Run the scenario tests -$tester = new KSS01ScenarioTester(); -$tester->run(); diff --git a/scripts/validation/validate_bom_system.php b/scripts/validation/validate_bom_system.php deleted file mode 100755 index 0886b3b..0000000 --- a/scripts/validation/validate_bom_system.php +++ /dev/null @@ -1,573 +0,0 @@ -#!/usr/bin/env php -output("\n=== Parametric BOM System Validation ==="); - $this->output("Starting comprehensive system validation...\n"); - } - - public function run(): void - { - try { - // Test database connectivity - $this->testDatabaseConnectivity(); - - // Test basic model operations - $this->testModelOperations(); - - // Test parameter validation - $this->testParameterValidation(); - - // Test formula calculations - $this->testFormulaCalculations(); - - // Test condition rule evaluation - $this->testConditionRuleEvaluation(); - - // Test BOM resolution - $this->testBomResolution(); - - // Test performance benchmarks - $this->testPerformanceBenchmarks(); - - // Test error handling - $this->testErrorHandling(); - - // Generate final report - $this->generateReport(); - - } catch (Exception $e) { - $this->output("\n❌ Critical Error: " . $e->getMessage()); - $this->output("Stack trace: " . $e->getTraceAsString()); - exit(1); - } - } - - private function testDatabaseConnectivity(): void - { - $this->output("\n🔍 Testing Database Connectivity..."); - - $this->test('Database connection', function() { - return DB::connection()->getPdo() !== null; - }); - - $this->test('Tenant table access', function() { - return Tenant::query()->exists(); - }); - - $this->test('Design models table access', function() { - return DesignModel::query()->exists(); - }); - } - - private function testModelOperations(): void - { - $this->output("\n🔍 Testing Model Operations..."); - - // Find test tenant and model - $tenant = Tenant::where('code', 'KSS_DEMO')->first(); - if (!$tenant) { - $this->test('KSS_DEMO tenant exists', function() { return false; }); - $this->output(" ⚠️ Please run KSS01ModelSeeder first"); - return; - } - - $model = DesignModel::where('tenant_id', $tenant->id) - ->where('code', 'KSS01') - ->first(); - - $this->test('KSS01 model exists', function() use ($model) { - return $model !== null; - }); - - if (!$model) { - $this->output(" ⚠️ Please run KSS01ModelSeeder first"); - return; - } - - $this->test('Model has parameters', function() use ($model) { - return $model->parameters()->count() > 0; - }); - - $this->test('Model has formulas', function() use ($model) { - return ModelFormula::where('model_id', $model->id)->count() > 0; - }); - - $this->test('Model has condition rules', function() use ($model) { - return BomConditionRule::where('model_id', $model->id)->count() > 0; - }); - } - - private function testParameterValidation(): void - { - $this->output("\n🔍 Testing Parameter Validation..."); - - $tenant = Tenant::where('code', 'KSS_DEMO')->first(); - $model = DesignModel::where('tenant_id', $tenant->id)->where('code', 'KSS01')->first(); - - if (!$model) { - $this->output(" ⚠️ Skipping parameter tests - no KSS01 model"); - return; - } - - $parameterService = new ModelParameterService(); - $parameterService->setTenantId($tenant->id); - $parameterService->setApiUserId(1); - - // Test valid parameters - $this->test('Valid parameters accepted', function() use ($parameterService, $model) { - $validParams = [ - 'W0' => 1200, - 'H0' => 800, - 'screen_type' => 'FABRIC', - 'install_type' => 'WALL' - ]; - - try { - $result = $parameterService->validateParameters($model->id, $validParams); - return is_array($result) && count($result) > 0; - } catch (Exception $e) { - $this->output(" Error: " . $e->getMessage()); - return false; - } - }); - - // Test parameter range validation - $this->test('Parameter range validation', function() use ($parameterService, $model) { - $invalidParams = [ - 'W0' => 5000, // Above max - 'H0' => 800, - 'screen_type' => 'FABRIC', - 'install_type' => 'WALL' - ]; - - try { - $parameterService->validateParameters($model->id, $invalidParams); - return false; // Should have thrown exception - } catch (Exception $e) { - return true; // Expected exception - } - }); - - // Test required parameter validation - $this->test('Required parameter validation', function() use ($parameterService, $model) { - $incompleteParams = [ - 'W0' => 1200, - // Missing H0 - 'screen_type' => 'FABRIC' - ]; - - try { - $parameterService->validateParameters($model->id, $incompleteParams); - return false; // Should have thrown exception - } catch (Exception $e) { - return true; // Expected exception - } - }); - } - - private function testFormulaCalculations(): void - { - $this->output("\n🔍 Testing Formula Calculations..."); - - $tenant = Tenant::where('code', 'KSS_DEMO')->first(); - $model = DesignModel::where('tenant_id', $tenant->id)->where('code', 'KSS01')->first(); - - if (!$model) { - $this->output(" ⚠️ Skipping formula tests - no KSS01 model"); - return; - } - - $formulaService = new ModelFormulaService(); - $formulaService->setTenantId($tenant->id); - $formulaService->setApiUserId(1); - - $testParams = [ - 'W0' => 1200, - 'H0' => 800, - 'screen_type' => 'FABRIC', - 'install_type' => 'WALL' - ]; - - $this->test('Basic formula calculations', function() use ($formulaService, $model, $testParams) { - try { - $results = $formulaService->calculateFormulas($model->id, $testParams); - - // Check expected calculated values - return isset($results['W1']) && $results['W1'] == 1300 && // 1200 + 100 - isset($results['H1']) && $results['H1'] == 900 && // 800 + 100 - isset($results['area']) && abs($results['area'] - 1.17) < 0.01; // (1300*900)/1000000 - } catch (Exception $e) { - $this->output(" Error: " . $e->getMessage()); - return false; - } - }); - - $this->test('Formula dependency resolution', function() use ($formulaService, $model, $testParams) { - try { - $results = $formulaService->calculateFormulas($model->id, $testParams); - - // Ensure dependent formulas are calculated in correct order - return isset($results['W1']) && isset($results['H1']) && isset($results['area']); - } catch (Exception $e) { - return false; - } - }); - - // Test circular dependency detection - $this->test('Circular dependency detection', function() use ($formulaService, $model) { - // This would require creating circular formulas, skipping for now - return true; - }); - } - - private function testConditionRuleEvaluation(): void - { - $this->output("\n🔍 Testing Condition Rule Evaluation..."); - - $tenant = Tenant::where('code', 'KSS_DEMO')->first(); - $model = DesignModel::where('tenant_id', $tenant->id)->where('code', 'KSS01')->first(); - - if (!$model) { - $this->output(" ⚠️ Skipping rule tests - no KSS01 model"); - return; - } - - $ruleService = new BomConditionRuleService(); - $ruleService->setTenantId($tenant->id); - $ruleService->setApiUserId(1); - - $calculatedValues = [ - 'W0' => 1200, - 'H0' => 800, - 'W1' => 1300, - 'H1' => 900, - 'area' => 1.17, - 'screen_type' => 'FABRIC', - 'install_type' => 'WALL' - ]; - - $this->test('Basic rule evaluation', function() use ($ruleService, $model, $calculatedValues) { - try { - $results = $ruleService->evaluateRules($model->id, $calculatedValues); - return isset($results['matched_rules']) && is_array($results['matched_rules']); - } catch (Exception $e) { - $this->output(" Error: " . $e->getMessage()); - return false; - } - }); - - $this->test('Rule action generation', function() use ($ruleService, $model, $calculatedValues) { - try { - $results = $ruleService->evaluateRules($model->id, $calculatedValues); - return isset($results['bom_actions']) && is_array($results['bom_actions']); - } catch (Exception $e) { - return false; - } - }); - - // Test different parameter scenarios - $steelParams = array_merge($calculatedValues, ['screen_type' => 'STEEL']); - $this->test('Material type rule evaluation', function() use ($ruleService, $model, $steelParams) { - try { - $results = $ruleService->evaluateRules($model->id, $steelParams); - $matchedRules = collect($results['matched_rules']); - return $matchedRules->contains(function($rule) { - return strpos($rule['rule_name'], 'Steel') !== false; - }); - } catch (Exception $e) { - return false; - } - }); - } - - private function testBomResolution(): void - { - $this->output("\n🔍 Testing BOM Resolution..."); - - $tenant = Tenant::where('code', 'KSS_DEMO')->first(); - $model = DesignModel::where('tenant_id', $tenant->id)->where('code', 'KSS01')->first(); - - if (!$model) { - $this->output(" ⚠️ Skipping BOM tests - no KSS01 model"); - return; - } - - $bomResolver = new BomResolverService( - new ModelParameterService(), - new ModelFormulaService(), - new BomConditionRuleService() - ); - $bomResolver->setTenantId($tenant->id); - $bomResolver->setApiUserId(1); - - $inputParams = [ - 'W0' => 1200, - 'H0' => 800, - 'screen_type' => 'FABRIC', - 'install_type' => 'WALL' - ]; - - $this->test('Complete BOM resolution', function() use ($bomResolver, $model, $inputParams) { - try { - $result = $bomResolver->resolveBom($model->id, $inputParams); - - return isset($result['model']) && - isset($result['input_parameters']) && - isset($result['calculated_values']) && - isset($result['resolved_bom']) && - is_array($result['resolved_bom']); - } catch (Exception $e) { - $this->output(" Error: " . $e->getMessage()); - return false; - } - }); - - $this->test('BOM items have required fields', function() use ($bomResolver, $model, $inputParams) { - try { - $result = $bomResolver->resolveBom($model->id, $inputParams); - - if (empty($result['resolved_bom'])) { - return true; // Empty BOM is valid - } - - foreach ($result['resolved_bom'] as $item) { - if (!isset($item['target_type']) || !isset($item['target_id']) || !isset($item['quantity'])) { - return false; - } - } - return true; - } catch (Exception $e) { - return false; - } - }); - - $this->test('BOM preview functionality', function() use ($bomResolver, $model, $inputParams) { - try { - $result = $bomResolver->previewBom($model->id, $inputParams); - return isset($result['resolved_bom']); - } catch (Exception $e) { - return false; - } - }); - - $this->test('BOM comparison functionality', function() use ($bomResolver, $model) { - try { - $params1 = ['W0' => 800, 'H0' => 600, 'screen_type' => 'FABRIC', 'install_type' => 'WALL']; - $params2 = ['W0' => 1200, 'H0' => 800, 'screen_type' => 'STEEL', 'install_type' => 'CEILING']; - - $result = $bomResolver->compareBomByParameters($model->id, $params1, $params2); - - return isset($result['parameters_diff']) && - isset($result['bom_diff']) && - isset($result['bom_diff']['added']) && - isset($result['bom_diff']['removed']); - } catch (Exception $e) { - return false; - } - }); - } - - private function testPerformanceBenchmarks(): void - { - $this->output("\n🔍 Testing Performance Benchmarks..."); - - $tenant = Tenant::where('code', 'KSS_DEMO')->first(); - $model = DesignModel::where('tenant_id', $tenant->id)->where('code', 'KSS01')->first(); - - if (!$model) { - $this->output(" ⚠️ Skipping performance tests - no KSS01 model"); - return; - } - - $bomResolver = new BomResolverService( - new ModelParameterService(), - new ModelFormulaService(), - new BomConditionRuleService() - ); - $bomResolver->setTenantId($tenant->id); - $bomResolver->setApiUserId(1); - - // Test single BOM resolution performance - $this->test('Single BOM resolution performance (<500ms)', function() use ($bomResolver, $model) { - $inputParams = [ - 'W0' => 1200, - 'H0' => 800, - 'screen_type' => 'FABRIC', - 'install_type' => 'WALL' - ]; - - $startTime = microtime(true); - try { - $bomResolver->resolveBom($model->id, $inputParams); - $duration = (microtime(true) - $startTime) * 1000; - $this->output(" Duration: {$duration}ms"); - return $duration < 500; - } catch (Exception $e) { - return false; - } - }); - - // Test multiple BOM resolutions - $this->test('Multiple BOM resolutions performance (10 iterations <2s)', function() use ($bomResolver, $model) { - $scenarios = [ - ['W0' => 800, 'H0' => 600, 'screen_type' => 'FABRIC', 'install_type' => 'WALL'], - ['W0' => 1200, 'H0' => 800, 'screen_type' => 'STEEL', 'install_type' => 'CEILING'], - ['W0' => 1500, 'H0' => 1000, 'screen_type' => 'FABRIC', 'install_type' => 'RECESSED'], - ['W0' => 2000, 'H0' => 1200, 'screen_type' => 'STEEL', 'install_type' => 'WALL'], - ['W0' => 900, 'H0' => 700, 'screen_type' => 'FABRIC', 'install_type' => 'CEILING'] - ]; - - $startTime = microtime(true); - try { - for ($i = 0; $i < 10; $i++) { - foreach ($scenarios as $params) { - $bomResolver->resolveBom($model->id, $params); - } - } - $duration = (microtime(true) - $startTime) * 1000; - $this->output(" Duration: {$duration}ms"); - return $duration < 2000; - } catch (Exception $e) { - return false; - } - }); - } - - private function testErrorHandling(): void - { - $this->output("\n🔍 Testing Error Handling..."); - - $tenant = Tenant::where('code', 'KSS_DEMO')->first(); - - $bomResolver = new BomResolverService( - new ModelParameterService(), - new ModelFormulaService(), - new BomConditionRuleService() - ); - $bomResolver->setTenantId($tenant->id); - $bomResolver->setApiUserId(1); - - $this->test('Non-existent model handling', function() use ($bomResolver) { - try { - $bomResolver->resolveBom(99999, ['W0' => 1000, 'H0' => 800]); - return false; // Should have thrown exception - } catch (Exception $e) { - return true; // Expected exception - } - }); - - $this->test('Invalid parameters handling', function() use ($bomResolver, $tenant) { - $model = DesignModel::where('tenant_id', $tenant->id)->where('code', 'KSS01')->first(); - if (!$model) return true; - - try { - $bomResolver->resolveBom($model->id, ['invalid_param' => 'invalid_value']); - return false; // Should have thrown exception - } catch (Exception $e) { - return true; // Expected exception - } - }); - } - - private function test(string $name, callable $testFunction): void - { - $this->totalTests++; - - try { - $result = $testFunction(); - if ($result) { - $this->passedTests++; - $this->output(" ✅ {$name}"); - $this->results[] = ['test' => $name, 'status' => 'PASS']; - } else { - $this->failedTests++; - $this->output(" ❌ {$name}"); - $this->results[] = ['test' => $name, 'status' => 'FAIL']; - } - } catch (Exception $e) { - $this->failedTests++; - $this->output(" ❌ {$name} - Exception: {$e->getMessage()}"); - $this->results[] = ['test' => $name, 'status' => 'ERROR', 'error' => $e->getMessage()]; - } - } - - private function output(string $message): void - { - echo $message . "\n"; - Log::info("BOM Validation: " . $message); - } - - private function generateReport(): void - { - $this->output("\n" . str_repeat("=", 60)); - $this->output("VALIDATION REPORT"); - $this->output(str_repeat("=", 60)); - - $successRate = $this->totalTests > 0 ? round(($this->passedTests / $this->totalTests) * 100, 1) : 0; - - $this->output("Total Tests: {$this->totalTests}"); - $this->output("Passed: {$this->passedTests}"); - $this->output("Failed: {$this->failedTests}"); - $this->output("Success Rate: {$successRate}%"); - - if ($this->failedTests > 0) { - $this->output("\n❌ FAILED TESTS:"); - foreach ($this->results as $result) { - if ($result['status'] !== 'PASS') { - $error = isset($result['error']) ? " - {$result['error']}" : ''; - $this->output(" • {$result['test']}{$error}"); - } - } - } - - $this->output("\n" . str_repeat("=", 60)); - - if ($successRate >= 90) { - $this->output("🎉 VALIDATION PASSED - System is ready for production"); - exit(0); - } elseif ($successRate >= 75) { - $this->output("⚠️ VALIDATION WARNING - Some issues found, review required"); - exit(1); - } else { - $this->output("❌ VALIDATION FAILED - Critical issues found, system not ready"); - exit(2); - } - } -} - -// Run the validation -$validator = new BomSystemValidator(); -$validator->run(); diff --git a/tests/Feature/Design/BomConditionRuleTest.php b/tests/Feature/Design/BomConditionRuleTest.php deleted file mode 100644 index 4750ac4..0000000 --- a/tests/Feature/Design/BomConditionRuleTest.php +++ /dev/null @@ -1,538 +0,0 @@ -tenant = Tenant::factory()->create(); - $this->user = User::factory()->create(); - - // Associate user with tenant - $this->user->tenants()->attach($this->tenant->id, ['is_active' => true, 'is_default' => true]); - - // Create test design model - $this->model = DesignModel::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'code' => 'TEST01', - 'name' => 'Test Model', - 'is_active' => true - ]); - - // Create test product and material - $this->product = Product::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'code' => 'PROD001', - 'name' => 'Test Product' - ]); - - $this->material = Material::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'code' => 'MAT001', - 'name' => 'Test Material' - ]); - - // Create test parameters - ModelParameter::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'parameter_name' => 'width', - 'parameter_type' => 'NUMBER' - ]); - - ModelParameter::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'parameter_name' => 'screen_type', - 'parameter_type' => 'SELECT', - 'options' => ['FABRIC', 'STEEL', 'PLASTIC'] - ]); - - // Authenticate user - Sanctum::actingAs($this->user, ['*']); - Auth::login($this->user); - } - - /** @test */ - public function can_create_bom_condition_rule() - { - $ruleData = [ - 'model_id' => $this->model->id, - 'rule_name' => 'Large Width Rule', - 'condition_expression' => 'width > 1000', - 'action_type' => 'INCLUDE', - 'target_type' => 'PRODUCT', - 'target_id' => $this->product->id, - 'quantity_multiplier' => 2.0, - 'description' => 'Add extra product for large widths', - 'sort_order' => 1 - ]; - - $response = $this->postJson('/api/v1/design/bom-condition-rules', $ruleData); - - $response->assertStatus(201) - ->assertJson([ - 'success' => true, - 'message' => 'message.created' - ]); - - $this->assertDatabaseHas('bom_condition_rules', [ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'rule_name' => 'Large Width Rule', - 'condition_expression' => 'width > 1000', - 'action_type' => 'INCLUDE' - ]); - } - - /** @test */ - public function can_list_bom_condition_rules() - { - // Create test rules - BomConditionRule::factory()->count(3)->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id - ]); - - $response = $this->getJson('/api/v1/design/bom-condition-rules?model_id=' . $this->model->id); - - $response->assertStatus(200) - ->assertJson([ - 'success' => true, - 'message' => 'message.fetched' - ]) - ->assertJsonStructure([ - 'data' => [ - 'data' => [ - '*' => [ - 'id', - 'rule_name', - 'condition_expression', - 'action_type', - 'target_type', - 'target_id', - 'quantity_multiplier', - 'description', - 'sort_order' - ] - ] - ] - ]); - } - - /** @test */ - public function can_update_bom_condition_rule() - { - $rule = BomConditionRule::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'rule_name' => 'Original Rule', - 'condition_expression' => 'width > 500' - ]); - - $updateData = [ - 'rule_name' => 'Updated Rule', - 'condition_expression' => 'width > 800', - 'quantity_multiplier' => 1.5, - 'description' => 'Updated description' - ]; - - $response = $this->putJson('/api/v1/design/bom-condition-rules/' . $rule->id, $updateData); - - $response->assertStatus(200) - ->assertJson([ - 'success' => true, - 'message' => 'message.updated' - ]); - - $this->assertDatabaseHas('bom_condition_rules', [ - 'id' => $rule->id, - 'rule_name' => 'Updated Rule', - 'condition_expression' => 'width > 800', - 'quantity_multiplier' => 1.5 - ]); - } - - /** @test */ - public function can_delete_bom_condition_rule() - { - $rule = BomConditionRule::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id - ]); - - $response = $this->deleteJson('/api/v1/design/bom-condition-rules/' . $rule->id); - - $response->assertStatus(200) - ->assertJson([ - 'success' => true, - 'message' => 'message.deleted' - ]); - - $this->assertSoftDeleted('bom_condition_rules', [ - 'id' => $rule->id - ]); - } - - /** @test */ - public function validates_condition_expression_syntax() - { - // Test invalid expression - $invalidData = [ - 'model_id' => $this->model->id, - 'rule_name' => 'Invalid Rule', - 'condition_expression' => 'width >>> 1000', // Invalid syntax - 'action_type' => 'INCLUDE', - 'target_type' => 'PRODUCT', - 'target_id' => $this->product->id - ]; - - $response = $this->postJson('/api/v1/design/bom-condition-rules', $invalidData); - $response->assertStatus(422); - - // Test valid expression - $validData = [ - 'model_id' => $this->model->id, - 'rule_name' => 'Valid Rule', - 'condition_expression' => 'width > 1000 AND screen_type == "FABRIC"', - 'action_type' => 'INCLUDE', - 'target_type' => 'PRODUCT', - 'target_id' => $this->product->id - ]; - - $response = $this->postJson('/api/v1/design/bom-condition-rules', $validData); - $response->assertStatus(201); - } - - /** @test */ - public function can_evaluate_simple_conditions() - { - // Create simple numeric rule - BomConditionRule::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'rule_name' => 'Width Rule', - 'condition_expression' => 'width > 1000', - 'action_type' => 'INCLUDE', - 'target_type' => 'PRODUCT', - 'target_id' => $this->product->id - ]); - - $parameters = ['width' => 1200]; - - $response = $this->postJson('/api/v1/design/bom-condition-rules/evaluate', [ - 'model_id' => $this->model->id, - 'parameters' => $parameters - ]); - - $response->assertStatus(200) - ->assertJson([ - 'success' => true, - 'data' => [ - 'matched_rules' => [ - [ - 'rule_name' => 'Width Rule', - 'condition_result' => true - ] - ] - ] - ]); - } - - /** @test */ - public function can_evaluate_complex_conditions() - { - // Create complex rule with multiple conditions - BomConditionRule::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'rule_name' => 'Complex Rule', - 'condition_expression' => 'width > 800 AND screen_type == "FABRIC"', - 'action_type' => 'INCLUDE', - 'target_type' => 'MATERIAL', - 'target_id' => $this->material->id - ]); - - // Test case 1: Both conditions true - $parameters1 = ['width' => 1000, 'screen_type' => 'FABRIC']; - $response1 = $this->postJson('/api/v1/design/bom-condition-rules/evaluate', [ - 'model_id' => $this->model->id, - 'parameters' => $parameters1 - ]); - $response1->assertStatus(200); - $this->assertTrue($response1->json('data.matched_rules.0.condition_result')); - - // Test case 2: One condition false - $parameters2 = ['width' => 1000, 'screen_type' => 'STEEL']; - $response2 = $this->postJson('/api/v1/design/bom-condition-rules/evaluate', [ - 'model_id' => $this->model->id, - 'parameters' => $parameters2 - ]); - $response2->assertStatus(200); - $this->assertFalse($response2->json('data.matched_rules.0.condition_result')); - } - - /** @test */ - public function can_handle_different_action_types() - { - // Create rules for each action type - $includeRule = BomConditionRule::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'rule_name' => 'Include Rule', - 'condition_expression' => 'width > 1000', - 'action_type' => 'INCLUDE', - 'target_type' => 'PRODUCT', - 'target_id' => $this->product->id, - 'quantity_multiplier' => 2.0 - ]); - - $excludeRule = BomConditionRule::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'rule_name' => 'Exclude Rule', - 'condition_expression' => 'screen_type == "PLASTIC"', - 'action_type' => 'EXCLUDE', - 'target_type' => 'MATERIAL', - 'target_id' => $this->material->id - ]); - - $modifyRule = BomConditionRule::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'rule_name' => 'Modify Rule', - 'condition_expression' => 'width > 1500', - 'action_type' => 'MODIFY_QUANTITY', - 'target_type' => 'PRODUCT', - 'target_id' => $this->product->id, - 'quantity_multiplier' => 1.5 - ]); - - $parameters = ['width' => 1600, 'screen_type' => 'PLASTIC']; - - $response = $this->postJson('/api/v1/design/bom-condition-rules/evaluate', [ - 'model_id' => $this->model->id, - 'parameters' => $parameters - ]); - - $response->assertStatus(200); - - $data = $response->json('data'); - $this->assertCount(3, $data['bom_actions']); // All three rules should match - - // Check action types - $actionTypes = collect($data['bom_actions'])->pluck('action_type')->toArray(); - $this->assertContains('INCLUDE', $actionTypes); - $this->assertContains('EXCLUDE', $actionTypes); - $this->assertContains('MODIFY_QUANTITY', $actionTypes); - } - - /** @test */ - public function can_handle_rule_dependencies_and_order() - { - // Create rules with different priorities (sort_order) - BomConditionRule::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'rule_name' => 'High Priority Rule', - 'condition_expression' => 'width > 500', - 'action_type' => 'INCLUDE', - 'target_type' => 'PRODUCT', - 'target_id' => $this->product->id, - 'sort_order' => 1 - ]); - - BomConditionRule::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'rule_name' => 'Low Priority Rule', - 'condition_expression' => 'width > 500', - 'action_type' => 'EXCLUDE', - 'target_type' => 'PRODUCT', - 'target_id' => $this->product->id, - 'sort_order' => 2 - ]); - - $parameters = ['width' => 1000]; - - $response = $this->postJson('/api/v1/design/bom-condition-rules/evaluate', [ - 'model_id' => $this->model->id, - 'parameters' => $parameters - ]); - - $response->assertStatus(200); - - $actions = $response->json('data.bom_actions'); - $this->assertEquals('INCLUDE', $actions[0]['action_type']); // Higher priority first - $this->assertEquals('EXCLUDE', $actions[1]['action_type']); - } - - /** @test */ - public function can_validate_target_references() - { - // Test invalid product reference - $invalidProductData = [ - 'model_id' => $this->model->id, - 'rule_name' => 'Invalid Product Rule', - 'condition_expression' => 'width > 1000', - 'action_type' => 'INCLUDE', - 'target_type' => 'PRODUCT', - 'target_id' => 99999 // Non-existent product - ]; - - $response = $this->postJson('/api/v1/design/bom-condition-rules', $invalidProductData); - $response->assertStatus(422); - - // Test invalid material reference - $invalidMaterialData = [ - 'model_id' => $this->model->id, - 'rule_name' => 'Invalid Material Rule', - 'condition_expression' => 'width > 1000', - 'action_type' => 'INCLUDE', - 'target_type' => 'MATERIAL', - 'target_id' => 99999 // Non-existent material - ]; - - $response = $this->postJson('/api/v1/design/bom-condition-rules', $invalidMaterialData); - $response->assertStatus(422); - } - - /** @test */ - public function enforces_tenant_isolation() - { - // Create rule for different tenant - $otherTenant = Tenant::factory()->create(); - $otherRule = BomConditionRule::factory()->create([ - 'tenant_id' => $otherTenant->id - ]); - - // Should not be able to access other tenant's rule - $response = $this->getJson('/api/v1/design/bom-condition-rules/' . $otherRule->id); - $response->assertStatus(404); - } - - /** @test */ - public function can_test_rule_against_multiple_scenarios() - { - BomConditionRule::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'rule_name' => 'Multi Test Rule', - 'condition_expression' => 'width > 1000 OR screen_type == "STEEL"', - 'action_type' => 'INCLUDE', - 'target_type' => 'PRODUCT', - 'target_id' => $this->product->id - ]); - - $testScenarios = [ - ['width' => 1200, 'screen_type' => 'FABRIC'], // Should match (width > 1000) - ['width' => 800, 'screen_type' => 'STEEL'], // Should match (screen_type == STEEL) - ['width' => 800, 'screen_type' => 'FABRIC'], // Should not match - ['width' => 1200, 'screen_type' => 'STEEL'] // Should match (both conditions) - ]; - - $response = $this->postJson('/api/v1/design/bom-condition-rules/test-scenarios', [ - 'model_id' => $this->model->id, - 'scenarios' => $testScenarios - ]); - - $response->assertStatus(200); - - $results = $response->json('data.scenario_results'); - $this->assertTrue($results[0]['matched']); - $this->assertTrue($results[1]['matched']); - $this->assertFalse($results[2]['matched']); - $this->assertTrue($results[3]['matched']); - } - - /** @test */ - public function can_clone_rules_between_models() - { - // Create source rules - $sourceRules = BomConditionRule::factory()->count(3)->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id - ]); - - // Create target model - $targetModel = DesignModel::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'code' => 'TARGET01' - ]); - - $response = $this->postJson('/api/v1/design/bom-condition-rules/clone', [ - 'source_model_id' => $this->model->id, - 'target_model_id' => $targetModel->id - ]); - - $response->assertStatus(200) - ->assertJson([ - 'success' => true, - 'message' => 'message.cloned' - ]); - - // Verify rules were cloned - $this->assertDatabaseCount('bom_condition_rules', 6); // 3 original + 3 cloned - - $clonedRules = BomConditionRule::where('model_id', $targetModel->id)->get(); - $this->assertCount(3, $clonedRules); - } - - /** @test */ - public function can_handle_expression_with_calculated_values() - { - // Create formula that will be used in condition - ModelFormula::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'formula_name' => 'area', - 'expression' => 'width * 600' - ]); - - // Create rule that uses calculated value - BomConditionRule::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'rule_name' => 'Area Rule', - 'condition_expression' => 'area > 600000', - 'action_type' => 'INCLUDE', - 'target_type' => 'MATERIAL', - 'target_id' => $this->material->id - ]); - - $parameters = ['width' => 1200]; // area = 1200 * 600 = 720000 - - $response = $this->postJson('/api/v1/design/bom-condition-rules/evaluate', [ - 'model_id' => $this->model->id, - 'parameters' => $parameters - ]); - - $response->assertStatus(200); - $this->assertTrue($response->json('data.matched_rules.0.condition_result')); - } -} diff --git a/tests/Feature/Design/BomResolverTest.php b/tests/Feature/Design/BomResolverTest.php deleted file mode 100644 index 184330f..0000000 --- a/tests/Feature/Design/BomResolverTest.php +++ /dev/null @@ -1,708 +0,0 @@ -tenant = Tenant::factory()->create(); - $this->user = User::factory()->create(); - - // Associate user with tenant - $this->user->tenants()->attach($this->tenant->id, ['is_active' => true, 'is_default' => true]); - - // Create test design model - $this->model = DesignModel::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'code' => 'KSS01', - 'name' => 'Screen Door System', - 'is_active' => true - ]); - - // Create model version - $this->modelVersion = ModelVersion::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'version_no' => '1.0', - 'status' => 'RELEASED' - ]); - - // Create products and materials - $this->product1 = Product::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'code' => 'BRACKET001', - 'name' => 'Wall Bracket', - 'unit' => 'EA' - ]); - - $this->product2 = Product::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'code' => 'MOTOR001', - 'name' => 'DC Motor', - 'unit' => 'EA' - ]); - - $this->material1 = Material::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'code' => 'FABRIC001', - 'name' => 'Screen Fabric', - 'unit' => 'M2' - ]); - - $this->material2 = Material::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'code' => 'RAIL001', - 'name' => 'Guide Rail', - 'unit' => 'M' - ]); - - // Create BOM template - $this->bomTemplate = BomTemplate::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_version_id' => $this->modelVersion->id, - 'name' => 'Base BOM Template' - ]); - - // Create BOM template items - BomTemplateItem::factory()->create([ - 'bom_template_id' => $this->bomTemplate->id, - 'ref_type' => 'PRODUCT', - 'ref_id' => $this->product1->id, - 'quantity' => 2, - 'waste_rate' => 5, - 'order' => 1 - ]); - - BomTemplateItem::factory()->create([ - 'bom_template_id' => $this->bomTemplate->id, - 'ref_type' => 'MATERIAL', - 'ref_id' => $this->material1->id, - 'quantity' => 1, - 'waste_rate' => 10, - 'order' => 2 - ]); - - // Create test parameters - ModelParameter::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'parameter_name' => 'W0', - 'parameter_type' => 'NUMBER', - 'default_value' => '800', - 'min_value' => 500, - 'max_value' => 2000, - 'unit' => 'mm' - ]); - - ModelParameter::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'parameter_name' => 'H0', - 'parameter_type' => 'NUMBER', - 'default_value' => '600', - 'min_value' => 400, - 'max_value' => 1500, - 'unit' => 'mm' - ]); - - ModelParameter::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'parameter_name' => 'screen_type', - 'parameter_type' => 'SELECT', - 'options' => ['FABRIC', 'STEEL', 'PLASTIC'], - 'default_value' => 'FABRIC' - ]); - - // Create formulas - ModelFormula::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'formula_name' => 'W1', - 'expression' => 'W0 + 100', - 'sort_order' => 1 - ]); - - ModelFormula::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'formula_name' => 'H1', - 'expression' => 'H0 + 100', - 'sort_order' => 2 - ]); - - ModelFormula::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'formula_name' => 'area', - 'expression' => '(W1 * H1) / 1000000', - 'sort_order' => 3 - ]); - - // Authenticate user - Sanctum::actingAs($this->user, ['*']); - Auth::login($this->user); - } - - /** @test */ - public function can_resolve_basic_bom_without_rules() - { - $inputParameters = [ - 'W0' => 1000, - 'H0' => 800 - ]; - - $response = $this->postJson('/api/v1/design/bom-resolver/resolve', [ - 'model_id' => $this->model->id, - 'parameters' => $inputParameters - ]); - - $response->assertStatus(200) - ->assertJsonStructure([ - 'success', - 'data' => [ - 'model' => ['id', 'code', 'name'], - 'input_parameters', - 'calculated_values', - 'matched_rules', - 'base_bom_template_id', - 'resolved_bom', - 'summary' - ] - ]); - - $data = $response->json('data'); - - // Check calculated values - $this->assertEquals(1100, $data['calculated_values']['W1']); // 1000 + 100 - $this->assertEquals(900, $data['calculated_values']['H1']); // 800 + 100 - $this->assertEquals(0.99, $data['calculated_values']['area']); // (1100 * 900) / 1000000 - - // Check resolved BOM contains base template items - $this->assertCount(2, $data['resolved_bom']); - $this->assertEquals('PRODUCT', $data['resolved_bom'][0]['target_type']); - $this->assertEquals($this->product1->id, $data['resolved_bom'][0]['target_id']); - $this->assertEquals(2, $data['resolved_bom'][0]['quantity']); - } - - /** @test */ - public function can_resolve_bom_with_include_rules() - { - // Create rule to include motor for large widths - BomConditionRule::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'rule_name' => 'Large Width Motor', - 'condition_expression' => 'W0 > 1200', - 'action_type' => 'INCLUDE', - 'target_type' => 'PRODUCT', - 'target_id' => $this->product2->id, - 'quantity_multiplier' => 1, - 'sort_order' => 1 - ]); - - $inputParameters = [ - 'W0' => 1500, // Triggers the rule - 'H0' => 800 - ]; - - $response = $this->postJson('/api/v1/design/bom-resolver/resolve', [ - 'model_id' => $this->model->id, - 'parameters' => $inputParameters - ]); - - $response->assertStatus(200); - $data = $response->json('data'); - - // Should have base template items + motor - $this->assertCount(3, $data['resolved_bom']); - - // Check if motor was added - $motorItem = collect($data['resolved_bom'])->firstWhere('target_id', $this->product2->id); - $this->assertNotNull($motorItem); - $this->assertEquals('PRODUCT', $motorItem['target_type']); - $this->assertEquals(1, $motorItem['quantity']); - $this->assertEquals('Large Width Motor', $motorItem['reason']); - } - - /** @test */ - public function can_resolve_bom_with_exclude_rules() - { - // Create rule to exclude fabric for steel type - BomConditionRule::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'rule_name' => 'Steel Type Exclude Fabric', - 'condition_expression' => 'screen_type == "STEEL"', - 'action_type' => 'EXCLUDE', - 'target_type' => 'MATERIAL', - 'target_id' => $this->material1->id, - 'sort_order' => 1 - ]); - - $inputParameters = [ - 'W0' => 1000, - 'H0' => 800, - 'screen_type' => 'STEEL' - ]; - - $response = $this->postJson('/api/v1/design/bom-resolver/resolve', [ - 'model_id' => $this->model->id, - 'parameters' => $inputParameters - ]); - - $response->assertStatus(200); - $data = $response->json('data'); - - // Should only have bracket (fabric excluded) - $this->assertCount(1, $data['resolved_bom']); - $this->assertEquals($this->product1->id, $data['resolved_bom'][0]['target_id']); - - // Verify fabric is not in BOM - $fabricItem = collect($data['resolved_bom'])->firstWhere('target_id', $this->material1->id); - $this->assertNull($fabricItem); - } - - /** @test */ - public function can_resolve_bom_with_modify_quantity_rules() - { - // Create rule to modify bracket quantity for large areas - BomConditionRule::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'rule_name' => 'Large Area Extra Brackets', - 'condition_expression' => 'area > 1.0', - 'action_type' => 'MODIFY_QUANTITY', - 'target_type' => 'PRODUCT', - 'target_id' => $this->product1->id, - 'quantity_multiplier' => 2.0, - 'sort_order' => 1 - ]); - - $inputParameters = [ - 'W0' => 1200, // area = (1300 * 900) / 1000000 = 1.17 - 'H0' => 800 - ]; - - $response = $this->postJson('/api/v1/design/bom-resolver/resolve', [ - 'model_id' => $this->model->id, - 'parameters' => $inputParameters - ]); - - $response->assertStatus(200); - $data = $response->json('data'); - - // Find bracket item - $bracketItem = collect($data['resolved_bom'])->firstWhere('target_id', $this->product1->id); - $this->assertNotNull($bracketItem); - $this->assertEquals(4, $bracketItem['quantity']); // 2 * 2.0 - $this->assertEquals('Large Area Extra Brackets', $bracketItem['reason']); - } - - /** @test */ - public function can_resolve_bom_with_multiple_rules() - { - // Create multiple rules that should all apply - BomConditionRule::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'rule_name' => 'Add Motor', - 'condition_expression' => 'W0 > 1000', - 'action_type' => 'INCLUDE', - 'target_type' => 'PRODUCT', - 'target_id' => $this->product2->id, - 'quantity_multiplier' => 1, - 'sort_order' => 1 - ]); - - BomConditionRule::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'rule_name' => 'Add Rail', - 'condition_expression' => 'screen_type == "FABRIC"', - 'action_type' => 'INCLUDE', - 'target_type' => 'MATERIAL', - 'target_id' => $this->material2->id, - 'quantity_multiplier' => 2.0, - 'sort_order' => 2 - ]); - - $inputParameters = [ - 'W0' => 1200, // Triggers first rule - 'H0' => 800, - 'screen_type' => 'FABRIC' // Triggers second rule - ]; - - $response = $this->postJson('/api/v1/design/bom-resolver/resolve', [ - 'model_id' => $this->model->id, - 'parameters' => $inputParameters - ]); - - $response->assertStatus(200); - $data = $response->json('data'); - - // Should have all items: bracket, fabric (base) + motor, rail (rules) - $this->assertCount(4, $data['resolved_bom']); - - // Check all matched rules - $this->assertCount(2, $data['matched_rules']); - - // Verify motor was added - $motorItem = collect($data['resolved_bom'])->firstWhere('target_id', $this->product2->id); - $this->assertNotNull($motorItem); - - // Verify rail was added - $railItem = collect($data['resolved_bom'])->firstWhere('target_id', $this->material2->id); - $this->assertNotNull($railItem); - $this->assertEquals(2.0, $railItem['quantity']); - } - - /** @test */ - public function can_preview_bom_without_saving() - { - $inputParameters = [ - 'W0' => 1000, - 'H0' => 800 - ]; - - $response = $this->postJson('/api/v1/design/bom-resolver/preview', [ - 'model_id' => $this->model->id, - 'parameters' => $inputParameters - ]); - - $response->assertStatus(200) - ->assertJsonStructure([ - 'success', - 'data' => [ - 'model', - 'input_parameters', - 'calculated_values', - 'resolved_bom', - 'summary' - ] - ]); - - // Verify it's the same as resolve but without persistence - $data = $response->json('data'); - $this->assertIsArray($data['resolved_bom']); - $this->assertIsArray($data['summary']); - } - - /** @test */ - public function can_compare_bom_by_different_parameters() - { - $parameters1 = [ - 'W0' => 800, - 'H0' => 600, - 'screen_type' => 'FABRIC' - ]; - - $parameters2 = [ - 'W0' => 1200, - 'H0' => 800, - 'screen_type' => 'STEEL' - ]; - - $response = $this->postJson('/api/v1/design/bom-resolver/compare', [ - 'model_id' => $this->model->id, - 'parameters1' => $parameters1, - 'parameters2' => $parameters2 - ]); - - $response->assertStatus(200) - ->assertJsonStructure([ - 'success', - 'data' => [ - 'parameters_diff' => [ - 'set1', - 'set2', - 'changed' - ], - 'calculated_values_diff' => [ - 'set1', - 'set2', - 'changed' - ], - 'bom_diff' => [ - 'added', - 'removed', - 'modified', - 'summary' - ], - 'summary_diff' - ] - ]); - - $data = $response->json('data'); - - // Check parameter differences - $this->assertEquals(400, $data['parameters_diff']['changed']['W0']); // 1200 - 800 - $this->assertEquals(200, $data['parameters_diff']['changed']['H0']); // 800 - 600 - $this->assertEquals('STEEL', $data['parameters_diff']['changed']['screen_type']); - } - - /** @test */ - public function can_save_and_retrieve_bom_resolution() - { - $inputParameters = [ - 'W0' => 1000, - 'H0' => 800 - ]; - - // First resolve and save - $resolveResponse = $this->postJson('/api/v1/design/bom-resolver/resolve', [ - 'model_id' => $this->model->id, - 'parameters' => $inputParameters, - 'save_resolution' => true, - 'purpose' => 'ESTIMATION' - ]); - - $resolveResponse->assertStatus(200); - $resolutionId = $resolveResponse->json('data.resolution_id'); - $this->assertNotNull($resolutionId); - - // Then retrieve saved resolution - $retrieveResponse = $this->getJson('/api/v1/design/bom-resolver/resolution/' . $resolutionId); - - $retrieveResponse->assertStatus(200) - ->assertJsonStructure([ - 'success', - 'data' => [ - 'resolution_id', - 'model_id', - 'input_parameters', - 'calculated_values', - 'resolved_bom', - 'purpose', - 'saved_at' - ] - ]); - } - - /** @test */ - public function handles_missing_bom_template_gracefully() - { - // Delete the BOM template - $this->bomTemplate->delete(); - - $inputParameters = [ - 'W0' => 1000, - 'H0' => 800 - ]; - - $response = $this->postJson('/api/v1/design/bom-resolver/resolve', [ - 'model_id' => $this->model->id, - 'parameters' => $inputParameters - ]); - - $response->assertStatus(200); - $data = $response->json('data'); - - // Should return empty BOM with calculated values - $this->assertEmpty($data['resolved_bom']); - $this->assertNull($data['base_bom_template_id']); - $this->assertNotEmpty($data['calculated_values']); - } - - /** @test */ - public function validates_input_parameters() - { - // Test missing required parameter - $invalidParameters = [ - 'H0' => 800 - // Missing W0 - ]; - - $response = $this->postJson('/api/v1/design/bom-resolver/resolve', [ - 'model_id' => $this->model->id, - 'parameters' => $invalidParameters - ]); - - $response->assertStatus(422); - - // Test parameter out of range - $outOfRangeParameters = [ - 'W0' => 3000, // Max is 2000 - 'H0' => 800 - ]; - - $response = $this->postJson('/api/v1/design/bom-resolver/resolve', [ - 'model_id' => $this->model->id, - 'parameters' => $outOfRangeParameters - ]); - - $response->assertStatus(422); - } - - /** @test */ - public function enforces_tenant_isolation() - { - // Create model for different tenant - $otherTenant = Tenant::factory()->create(); - $otherModel = DesignModel::factory()->create([ - 'tenant_id' => $otherTenant->id - ]); - - $inputParameters = [ - 'W0' => 1000, - 'H0' => 800 - ]; - - $response = $this->postJson('/api/v1/design/bom-resolver/resolve', [ - 'model_id' => $otherModel->id, - 'parameters' => $inputParameters - ]); - - $response->assertStatus(404); - } - - /** @test */ - public function can_handle_bom_with_waste_rates() - { - $inputParameters = [ - 'W0' => 1000, - 'H0' => 800 - ]; - - $response = $this->postJson('/api/v1/design/bom-resolver/resolve', [ - 'model_id' => $this->model->id, - 'parameters' => $inputParameters - ]); - - $response->assertStatus(200); - $data = $response->json('data'); - - // Check waste rate calculation - $bracketItem = collect($data['resolved_bom'])->firstWhere('target_id', $this->product1->id); - $this->assertNotNull($bracketItem); - $this->assertEquals(2, $bracketItem['quantity']); // Base quantity - $this->assertEquals(2.1, $bracketItem['actual_quantity']); // 2 * (1 + 5/100) - - $fabricItem = collect($data['resolved_bom'])->firstWhere('target_id', $this->material1->id); - $this->assertNotNull($fabricItem); - $this->assertEquals(1, $fabricItem['quantity']); // Base quantity - $this->assertEquals(1.1, $fabricItem['actual_quantity']); // 1 * (1 + 10/100) - } - - /** @test */ - public function can_test_kss01_specific_scenario() - { - // Test KSS01 specific logic using the built-in test method - $kssParameters = [ - 'W0' => 1200, - 'H0' => 800, - 'screen_type' => 'FABRIC', - 'install_type' => 'WALL' - ]; - - $response = $this->postJson('/api/v1/design/bom-resolver/kss01-test', [ - 'parameters' => $kssParameters - ]); - - $response->assertStatus(200) - ->assertJsonStructure([ - 'success', - 'data' => [ - 'model' => ['code', 'name'], - 'input_parameters', - 'calculated_values' => ['W1', 'H1', 'area'], - 'resolved_bom', - 'summary' - ] - ]); - - $data = $response->json('data'); - $this->assertEquals('KSS01', $data['model']['code']); - $this->assertEquals(1300, $data['calculated_values']['W1']); // 1200 + 100 - $this->assertEquals(900, $data['calculated_values']['H1']); // 800 + 100 - } - - /** @test */ - public function can_handle_formula_calculation_errors() - { - // Create formula with potential division by zero - ModelFormula::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'formula_name' => 'ratio', - 'expression' => 'W0 / (H0 - 800)', // Division by zero when H0 = 800 - 'sort_order' => 4 - ]); - - $inputParameters = [ - 'W0' => 1000, - 'H0' => 800 // This will cause division by zero - ]; - - $response = $this->postJson('/api/v1/design/bom-resolver/resolve', [ - 'model_id' => $this->model->id, - 'parameters' => $inputParameters - ]); - - // Should handle the error gracefully - $response->assertStatus(422) - ->assertJsonFragment([ - 'error' => 'Formula calculation error' - ]); - } - - /** @test */ - public function generates_comprehensive_summary() - { - $inputParameters = [ - 'W0' => 1000, - 'H0' => 800 - ]; - - $response = $this->postJson('/api/v1/design/bom-resolver/resolve', [ - 'model_id' => $this->model->id, - 'parameters' => $inputParameters - ]); - - $response->assertStatus(200); - $data = $response->json('data'); - - $summary = $data['summary']; - $this->assertArrayHasKey('total_items', $summary); - $this->assertArrayHasKey('material_count', $summary); - $this->assertArrayHasKey('product_count', $summary); - $this->assertArrayHasKey('total_estimated_value', $summary); - $this->assertArrayHasKey('generated_at', $summary); - - $this->assertEquals(2, $summary['total_items']); - $this->assertEquals(1, $summary['material_count']); - $this->assertEquals(1, $summary['product_count']); - } -} diff --git a/tests/Feature/Design/ModelFormulaTest.php b/tests/Feature/Design/ModelFormulaTest.php deleted file mode 100644 index 878ab47..0000000 --- a/tests/Feature/Design/ModelFormulaTest.php +++ /dev/null @@ -1,436 +0,0 @@ -tenant = Tenant::factory()->create(); - $this->user = User::factory()->create(); - - // Associate user with tenant - $this->user->tenants()->attach($this->tenant->id, ['is_active' => true, 'is_default' => true]); - - // Create test design model - $this->model = DesignModel::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'code' => 'TEST01', - 'name' => 'Test Model', - 'is_active' => true - ]); - - // Create test parameters - ModelParameter::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'parameter_name' => 'W0', - 'parameter_type' => 'NUMBER' - ]); - - ModelParameter::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'parameter_name' => 'H0', - 'parameter_type' => 'NUMBER' - ]); - - // Authenticate user - Sanctum::actingAs($this->user, ['*']); - Auth::login($this->user); - } - - /** @test */ - public function can_create_model_formula() - { - $formulaData = [ - 'model_id' => $this->model->id, - 'formula_name' => 'W1', - 'expression' => 'W0 + 100', - 'description' => 'Outer width calculation', - 'sort_order' => 1 - ]; - - $response = $this->postJson('/api/v1/design/model-formulas', $formulaData); - - $response->assertStatus(201) - ->assertJson([ - 'success' => true, - 'message' => 'message.created' - ]); - - $this->assertDatabaseHas('model_formulas', [ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'formula_name' => 'W1', - 'expression' => 'W0 + 100' - ]); - } - - /** @test */ - public function can_list_model_formulas() - { - // Create test formulas - ModelFormula::factory()->count(3)->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id - ]); - - $response = $this->getJson('/api/v1/design/model-formulas?model_id=' . $this->model->id); - - $response->assertStatus(200) - ->assertJson([ - 'success' => true, - 'message' => 'message.fetched' - ]) - ->assertJsonStructure([ - 'data' => [ - 'data' => [ - '*' => [ - 'id', - 'formula_name', - 'expression', - 'description', - 'dependencies', - 'sort_order' - ] - ] - ] - ]); - } - - /** @test */ - public function can_update_model_formula() - { - $formula = ModelFormula::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'formula_name' => 'area', - 'expression' => 'W0 * H0' - ]); - - $updateData = [ - 'formula_name' => 'area_updated', - 'expression' => '(W0 * H0) / 1000000', - 'description' => 'Area in square meters' - ]; - - $response = $this->putJson('/api/v1/design/model-formulas/' . $formula->id, $updateData); - - $response->assertStatus(200) - ->assertJson([ - 'success' => true, - 'message' => 'message.updated' - ]); - - $this->assertDatabaseHas('model_formulas', [ - 'id' => $formula->id, - 'formula_name' => 'area_updated', - 'expression' => '(W0 * H0) / 1000000' - ]); - } - - /** @test */ - public function can_delete_model_formula() - { - $formula = ModelFormula::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id - ]); - - $response = $this->deleteJson('/api/v1/design/model-formulas/' . $formula->id); - - $response->assertStatus(200) - ->assertJson([ - 'success' => true, - 'message' => 'message.deleted' - ]); - - $this->assertSoftDeleted('model_formulas', [ - 'id' => $formula->id - ]); - } - - /** @test */ - public function validates_formula_expression_syntax() - { - // Test invalid expression - $invalidData = [ - 'model_id' => $this->model->id, - 'formula_name' => 'invalid_formula', - 'expression' => 'W0 +++ H0' // Invalid syntax - ]; - - $response = $this->postJson('/api/v1/design/model-formulas', $invalidData); - $response->assertStatus(422); - - // Test valid expression - $validData = [ - 'model_id' => $this->model->id, - 'formula_name' => 'valid_formula', - 'expression' => 'sqrt(W0^2 + H0^2)' - ]; - - $response = $this->postJson('/api/v1/design/model-formulas', $validData); - $response->assertStatus(201); - } - - /** @test */ - public function can_calculate_formulas_with_dependencies() - { - // Create formulas with dependencies - ModelFormula::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'formula_name' => 'W1', - 'expression' => 'W0 + 100', - 'sort_order' => 1 - ]); - - ModelFormula::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'formula_name' => 'H1', - 'expression' => 'H0 + 100', - 'sort_order' => 2 - ]); - - ModelFormula::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'formula_name' => 'area', - 'expression' => 'W1 * H1', - 'sort_order' => 3 - ]); - - $inputParameters = [ - 'W0' => 800, - 'H0' => 600 - ]; - - $response = $this->postJson('/api/v1/design/model-formulas/calculate', [ - 'model_id' => $this->model->id, - 'parameters' => $inputParameters - ]); - - $response->assertStatus(200) - ->assertJson([ - 'success' => true, - 'data' => [ - 'W1' => 900, // 800 + 100 - 'H1' => 700, // 600 + 100 - 'area' => 630000 // 900 * 700 - ] - ]); - } - - /** @test */ - public function can_detect_circular_dependencies() - { - // Create circular dependency: A depends on B, B depends on A - ModelFormula::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'formula_name' => 'A', - 'expression' => 'B + 10' - ]); - - ModelFormula::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'formula_name' => 'B', - 'expression' => 'A + 20' - ]); - - $response = $this->postJson('/api/v1/design/model-formulas/calculate', [ - 'model_id' => $this->model->id, - 'parameters' => ['W0' => 100] - ]); - - $response->assertStatus(422) // Validation error for circular dependency - ->assertJsonFragment([ - 'error' => 'Circular dependency detected' - ]); - } - - /** @test */ - public function can_handle_complex_mathematical_expressions() - { - // Test various mathematical functions - $complexFormulas = [ - ['name' => 'sqrt_test', 'expression' => 'sqrt(W0^2 + H0^2)'], - ['name' => 'trig_test', 'expression' => 'sin(W0 * pi() / 180)'], - ['name' => 'conditional_test', 'expression' => 'if(W0 > 1000, W0 * 1.2, W0 * 1.1)'], - ['name' => 'round_test', 'expression' => 'round(W0 / 100) * 100'], - ['name' => 'max_test', 'expression' => 'max(W0, H0)'] - ]; - - foreach ($complexFormulas as $formulaData) { - ModelFormula::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'formula_name' => $formulaData['name'], - 'expression' => $formulaData['expression'] - ]); - } - - $response = $this->postJson('/api/v1/design/model-formulas/calculate', [ - 'model_id' => $this->model->id, - 'parameters' => ['W0' => 1200, 'H0' => 800] - ]); - - $response->assertStatus(200) - ->assertJsonStructure([ - 'success', - 'data' => [ - 'sqrt_test', - 'trig_test', - 'conditional_test', - 'round_test', - 'max_test' - ] - ]); - } - - /** @test */ - public function can_validate_formula_dependencies() - { - // Create formula that references non-existent parameter - $invalidFormulaData = [ - 'model_id' => $this->model->id, - 'formula_name' => 'invalid_ref', - 'expression' => 'NONEXISTENT_PARAM + 100' - ]; - - $response = $this->postJson('/api/v1/design/model-formulas', $invalidFormulaData); - $response->assertStatus(422); - - // Create valid formula that references existing parameter - $validFormulaData = [ - 'model_id' => $this->model->id, - 'formula_name' => 'valid_ref', - 'expression' => 'W0 + 100' - ]; - - $response = $this->postJson('/api/v1/design/model-formulas', $validFormulaData); - $response->assertStatus(201); - } - - /** @test */ - public function enforces_tenant_isolation() - { - // Create formula for different tenant - $otherTenant = Tenant::factory()->create(); - $otherFormula = ModelFormula::factory()->create([ - 'tenant_id' => $otherTenant->id - ]); - - // Should not be able to access other tenant's formula - $response = $this->getJson('/api/v1/design/model-formulas/' . $otherFormula->id); - $response->assertStatus(404); - } - - /** @test */ - public function can_export_and_import_formulas() - { - // Create test formulas - ModelFormula::factory()->count(3)->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id - ]); - - // Export formulas - $response = $this->getJson('/api/v1/design/model-formulas/export?model_id=' . $this->model->id); - $response->assertStatus(200) - ->assertJsonStructure([ - 'success', - 'data' => [ - 'formulas' => [ - '*' => [ - 'formula_name', - 'expression', - 'description', - 'sort_order' - ] - ] - ] - ]); - - $exportData = $response->json('data.formulas'); - - // Import formulas to new model - $newModel = DesignModel::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'code' => 'TEST02' - ]); - - $response = $this->postJson('/api/v1/design/model-formulas/import', [ - 'model_id' => $newModel->id, - 'formulas' => $exportData - ]); - - $response->assertStatus(200) - ->assertJson([ - 'success' => true, - 'message' => 'message.bulk_import' - ]); - - $this->assertDatabaseCount('model_formulas', 6); // 3 original + 3 imported - } - - /** @test */ - public function can_reorder_formulas_for_calculation_sequence() - { - $formula1 = ModelFormula::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'formula_name' => 'area', - 'expression' => 'W1 * H1', - 'sort_order' => 1 - ]); - - $formula2 = ModelFormula::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'formula_name' => 'W1', - 'expression' => 'W0 + 100', - 'sort_order' => 2 - ]); - - // Reorder so W1 is calculated before area - $reorderData = [ - ['id' => $formula2->id, 'sort_order' => 1], - ['id' => $formula1->id, 'sort_order' => 2] - ]; - - $response = $this->postJson('/api/v1/design/model-formulas/reorder', [ - 'items' => $reorderData - ]); - - $response->assertStatus(200); - - $this->assertDatabaseHas('model_formulas', [ - 'id' => $formula2->id, - 'sort_order' => 1 - ]); - } -} diff --git a/tests/Feature/Design/ModelParameterTest.php b/tests/Feature/Design/ModelParameterTest.php deleted file mode 100644 index dc37fc5..0000000 --- a/tests/Feature/Design/ModelParameterTest.php +++ /dev/null @@ -1,339 +0,0 @@ -tenant = Tenant::factory()->create(); - $this->user = User::factory()->create(); - - // Associate user with tenant - $this->user->tenants()->attach($this->tenant->id, ['is_active' => true, 'is_default' => true]); - - // Create test design model - $this->model = DesignModel::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'code' => 'TEST01', - 'name' => 'Test Model', - 'is_active' => true - ]); - - // Authenticate user - Sanctum::actingAs($this->user, ['*']); - Auth::login($this->user); - } - - /** @test */ - public function can_create_model_parameter() - { - $parameterData = [ - 'model_id' => $this->model->id, - 'parameter_name' => 'width', - 'parameter_type' => 'NUMBER', - 'is_required' => true, - 'default_value' => '800', - 'min_value' => 100, - 'max_value' => 2000, - 'unit' => 'mm', - 'description' => 'Width parameter for model', - 'sort_order' => 1 - ]; - - $response = $this->postJson('/api/v1/design/model-parameters', $parameterData); - - $response->assertStatus(201) - ->assertJson([ - 'success' => true, - 'message' => 'message.created' - ]); - - $this->assertDatabaseHas('model_parameters', [ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'parameter_name' => 'width', - 'parameter_type' => 'NUMBER' - ]); - } - - /** @test */ - public function can_list_model_parameters() - { - // Create test parameters - ModelParameter::factory()->count(3)->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id - ]); - - $response = $this->getJson('/api/v1/design/model-parameters?model_id=' . $this->model->id); - - $response->assertStatus(200) - ->assertJson([ - 'success' => true, - 'message' => 'message.fetched' - ]) - ->assertJsonStructure([ - 'data' => [ - 'data' => [ - '*' => [ - 'id', - 'parameter_name', - 'parameter_type', - 'is_required', - 'default_value', - 'min_value', - 'max_value', - 'unit', - 'description', - 'sort_order' - ] - ] - ] - ]); - } - - /** @test */ - public function can_show_model_parameter() - { - $parameter = ModelParameter::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'parameter_name' => 'height', - 'parameter_type' => 'NUMBER' - ]); - - $response = $this->getJson('/api/v1/design/model-parameters/' . $parameter->id); - - $response->assertStatus(200) - ->assertJson([ - 'success' => true, - 'message' => 'message.fetched', - 'data' => [ - 'id' => $parameter->id, - 'parameter_name' => 'height', - 'parameter_type' => 'NUMBER' - ] - ]); - } - - /** @test */ - public function can_update_model_parameter() - { - $parameter = ModelParameter::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'parameter_name' => 'depth', - 'min_value' => 50 - ]); - - $updateData = [ - 'parameter_name' => 'depth_updated', - 'min_value' => 100, - 'max_value' => 500, - 'description' => 'Updated depth parameter' - ]; - - $response = $this->putJson('/api/v1/design/model-parameters/' . $parameter->id, $updateData); - - $response->assertStatus(200) - ->assertJson([ - 'success' => true, - 'message' => 'message.updated' - ]); - - $this->assertDatabaseHas('model_parameters', [ - 'id' => $parameter->id, - 'parameter_name' => 'depth_updated', - 'min_value' => 100, - 'max_value' => 500 - ]); - } - - /** @test */ - public function can_delete_model_parameter() - { - $parameter = ModelParameter::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id - ]); - - $response = $this->deleteJson('/api/v1/design/model-parameters/' . $parameter->id); - - $response->assertStatus(200) - ->assertJson([ - 'success' => true, - 'message' => 'message.deleted' - ]); - - $this->assertSoftDeleted('model_parameters', [ - 'id' => $parameter->id - ]); - } - - /** @test */ - public function validates_parameter_type_and_constraints() - { - // Test NUMBER type with invalid range - $invalidData = [ - 'model_id' => $this->model->id, - 'parameter_name' => 'invalid_width', - 'parameter_type' => 'NUMBER', - 'min_value' => 1000, - 'max_value' => 500 // max < min - ]; - - $response = $this->postJson('/api/v1/design/model-parameters', $invalidData); - $response->assertStatus(422); - - // Test SELECT type with options - $validSelectData = [ - 'model_id' => $this->model->id, - 'parameter_name' => 'screen_type', - 'parameter_type' => 'SELECT', - 'options' => ['FABRIC', 'STEEL', 'PLASTIC'], - 'default_value' => 'FABRIC' - ]; - - $response = $this->postJson('/api/v1/design/model-parameters', $validSelectData); - $response->assertStatus(201); - } - - /** @test */ - public function can_validate_parameter_values() - { - // Create NUMBER parameter - $numberParam = ModelParameter::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'parameter_name' => 'test_number', - 'parameter_type' => 'NUMBER', - 'min_value' => 100, - 'max_value' => 1000 - ]); - - // Test valid value - $this->assertTrue($numberParam->validateValue(500)); - - // Test invalid values - $this->assertFalse($numberParam->validateValue(50)); // below min - $this->assertFalse($numberParam->validateValue(1500)); // above max - $this->assertFalse($numberParam->validateValue('abc')); // non-numeric - - // Create SELECT parameter - $selectParam = ModelParameter::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'parameter_name' => 'test_select', - 'parameter_type' => 'SELECT', - 'options' => ['OPTION1', 'OPTION2', 'OPTION3'] - ]); - - // Test valid and invalid options - $this->assertTrue($selectParam->validateValue('OPTION1')); - $this->assertFalse($selectParam->validateValue('INVALID')); - } - - /** @test */ - public function can_cast_parameter_values() - { - $numberParam = ModelParameter::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'parameter_type' => 'NUMBER' - ]); - - $booleanParam = ModelParameter::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'parameter_type' => 'BOOLEAN' - ]); - - $textParam = ModelParameter::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'parameter_type' => 'TEXT' - ]); - - // Test casting - $this->assertSame(123.5, $numberParam->castValue('123.5')); - $this->assertSame(true, $booleanParam->castValue('1')); - $this->assertSame('test', $textParam->castValue('test')); - } - - /** @test */ - public function enforces_tenant_isolation() - { - // Create parameter for different tenant - $otherTenant = Tenant::factory()->create(); - $otherParameter = ModelParameter::factory()->create([ - 'tenant_id' => $otherTenant->id - ]); - - // Should not be able to access other tenant's parameter - $response = $this->getJson('/api/v1/design/model-parameters/' . $otherParameter->id); - $response->assertStatus(404); - - // Should not be able to list other tenant's parameters - $response = $this->getJson('/api/v1/design/model-parameters'); - $response->assertStatus(200); - - $data = $response->json('data.data'); - $this->assertEmpty(collect($data)->where('tenant_id', $otherTenant->id)); - } - - /** @test */ - public function can_reorder_parameters() - { - $param1 = ModelParameter::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'sort_order' => 1 - ]); - - $param2 = ModelParameter::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'sort_order' => 2 - ]); - - $reorderData = [ - ['id' => $param1->id, 'sort_order' => 2], - ['id' => $param2->id, 'sort_order' => 1] - ]; - - $response = $this->postJson('/api/v1/design/model-parameters/reorder', [ - 'items' => $reorderData - ]); - - $response->assertStatus(200); - - $this->assertDatabaseHas('model_parameters', [ - 'id' => $param1->id, - 'sort_order' => 2 - ]); - - $this->assertDatabaseHas('model_parameters', [ - 'id' => $param2->id, - 'sort_order' => 1 - ]); - } -} diff --git a/tests/Feature/Design/ProductFromModelTest.php b/tests/Feature/Design/ProductFromModelTest.php deleted file mode 100644 index f6c89b9..0000000 --- a/tests/Feature/Design/ProductFromModelTest.php +++ /dev/null @@ -1,679 +0,0 @@ -tenant = Tenant::factory()->create(); - $this->user = User::factory()->create(); - - // Associate user with tenant - $this->user->tenants()->attach($this->tenant->id, ['is_active' => true, 'is_default' => true]); - - // Create test category - $this->category = Category::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'name' => 'Screen Systems', - 'code' => 'SCREENS' - ]); - - // Create test design model - $this->model = DesignModel::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'code' => 'KSS01', - 'name' => 'Screen Door System', - 'category_id' => $this->category->id, - 'is_active' => true - ]); - - // Create base materials and products for BOM - $this->baseMaterial = Material::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'code' => 'FABRIC001', - 'name' => 'Screen Fabric' - ]); - - $this->baseProduct = Product::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'code' => 'BRACKET001', - 'name' => 'Wall Bracket' - ]); - - // Create test parameters - ModelParameter::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'parameter_name' => 'width', - 'parameter_type' => 'NUMBER', - 'unit' => 'mm' - ]); - - ModelParameter::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'parameter_name' => 'height', - 'parameter_type' => 'NUMBER', - 'unit' => 'mm' - ]); - - ModelParameter::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'parameter_name' => 'screen_type', - 'parameter_type' => 'SELECT', - 'options' => ['FABRIC', 'STEEL'] - ]); - - // Create test formulas - ModelFormula::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'formula_name' => 'outer_width', - 'expression' => 'width + 100' - ]); - - ModelFormula::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'formula_name' => 'outer_height', - 'expression' => 'height + 100' - ]); - - // Authenticate user - Sanctum::actingAs($this->user, ['*']); - Auth::login($this->user); - } - - /** @test */ - public function can_create_product_from_model_with_parameters() - { - $productData = [ - 'model_id' => $this->model->id, - 'parameters' => [ - 'width' => 1200, - 'height' => 800, - 'screen_type' => 'FABRIC' - ], - 'product_data' => [ - 'code' => 'KSS01-1200x800-FABRIC', - 'name' => 'Screen Door 1200x800 Fabric', - 'category_id' => $this->category->id, - 'description' => 'Custom screen door based on KSS01 model', - 'unit' => 'EA' - ], - 'generate_bom' => true - ]; - - $response = $this->postJson('/api/v1/design/product-from-model', $productData); - - $response->assertStatus(201) - ->assertJson([ - 'success' => true, - 'message' => 'message.created' - ]) - ->assertJsonStructure([ - 'data' => [ - 'product' => [ - 'id', - 'code', - 'name', - 'category_id', - 'description' - ], - 'model_reference' => [ - 'model_id', - 'input_parameters', - 'calculated_values' - ], - 'bom_created', - 'bom_items_count' - ] - ]); - - // Verify product was created - $this->assertDatabaseHas('products', [ - 'tenant_id' => $this->tenant->id, - 'code' => 'KSS01-1200x800-FABRIC', - 'name' => 'Screen Door 1200x800 Fabric' - ]); - - $data = $response->json('data'); - $this->assertEquals($this->model->id, $data['model_reference']['model_id']); - $this->assertEquals(1200, $data['model_reference']['input_parameters']['width']); - $this->assertEquals(1300, $data['model_reference']['calculated_values']['outer_width']); // width + 100 - } - - /** @test */ - public function can_create_product_with_auto_generated_code() - { - $productData = [ - 'model_id' => $this->model->id, - 'parameters' => [ - 'width' => 1000, - 'height' => 600, - 'screen_type' => 'STEEL' - ], - 'product_data' => [ - 'name' => 'Custom Steel Screen', - 'category_id' => $this->category->id, - 'auto_generate_code' => true - ] - ]; - - $response = $this->postJson('/api/v1/design/product-from-model', $productData); - - $response->assertStatus(201); - $data = $response->json('data'); - - // Code should be auto-generated based on model and parameters - $expectedCode = 'KSS01-1000x600-STEEL'; - $this->assertEquals($expectedCode, $data['product']['code']); - - $this->assertDatabaseHas('products', [ - 'tenant_id' => $this->tenant->id, - 'code' => $expectedCode - ]); - } - - /** @test */ - public function can_create_product_with_bom_generation() - { - // Create rule for BOM generation - BomConditionRule::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'model_id' => $this->model->id, - 'rule_name' => 'Add Fabric Material', - 'condition_expression' => 'screen_type == "FABRIC"', - 'action_type' => 'INCLUDE', - 'target_type' => 'MATERIAL', - 'target_id' => $this->baseMaterial->id, - 'quantity_multiplier' => 1.2 - ]); - - $productData = [ - 'model_id' => $this->model->id, - 'parameters' => [ - 'width' => 1200, - 'height' => 800, - 'screen_type' => 'FABRIC' - ], - 'product_data' => [ - 'code' => 'TEST-PRODUCT-001', - 'name' => 'Test Product with BOM', - 'category_id' => $this->category->id - ], - 'generate_bom' => true - ]; - - $response = $this->postJson('/api/v1/design/product-from-model', $productData); - - $response->assertStatus(201); - $data = $response->json('data'); - - $this->assertTrue($data['bom_created']); - $this->assertGreaterThan(0, $data['bom_items_count']); - - // Verify BOM was created in product_components table - $product = Product::where('code', 'TEST-PRODUCT-001')->first(); - $this->assertDatabaseHas('product_components', [ - 'tenant_id' => $this->tenant->id, - 'product_id' => $product->id, - 'ref_type' => 'MATERIAL', - 'ref_id' => $this->baseMaterial->id - ]); - } - - /** @test */ - public function can_create_product_without_bom_generation() - { - $productData = [ - 'model_id' => $this->model->id, - 'parameters' => [ - 'width' => 1000, - 'height' => 600, - 'screen_type' => 'STEEL' - ], - 'product_data' => [ - 'code' => 'TEST-NO-BOM', - 'name' => 'Test Product without BOM', - 'category_id' => $this->category->id - ], - 'generate_bom' => false - ]; - - $response = $this->postJson('/api/v1/design/product-from-model', $productData); - - $response->assertStatus(201); - $data = $response->json('data'); - - $this->assertFalse($data['bom_created']); - $this->assertEquals(0, $data['bom_items_count']); - - // Verify no BOM components were created - $product = Product::where('code', 'TEST-NO-BOM')->first(); - $this->assertDatabaseMissing('product_components', [ - 'product_id' => $product->id - ]); - } - - /** @test */ - public function can_preview_product_before_creation() - { - $previewData = [ - 'model_id' => $this->model->id, - 'parameters' => [ - 'width' => 1500, - 'height' => 1000, - 'screen_type' => 'FABRIC' - ], - 'product_data' => [ - 'name' => 'Preview Product', - 'category_id' => $this->category->id, - 'auto_generate_code' => true - ], - 'generate_bom' => true - ]; - - $response = $this->postJson('/api/v1/design/product-from-model/preview', $previewData); - - $response->assertStatus(200) - ->assertJsonStructure([ - 'success', - 'data' => [ - 'preview_product' => [ - 'code', - 'name', - 'category_id' - ], - 'model_reference' => [ - 'model_id', - 'input_parameters', - 'calculated_values' - ], - 'preview_bom' => [ - 'items', - 'summary' - ] - ] - ]); - - // Verify no actual product was created - $data = $response->json('data'); - $this->assertDatabaseMissing('products', [ - 'code' => $data['preview_product']['code'] - ]); - } - - /** @test */ - public function validates_required_parameters() - { - // Missing required width parameter - $invalidData = [ - 'model_id' => $this->model->id, - 'parameters' => [ - 'height' => 800, // Missing width - 'screen_type' => 'FABRIC' - ], - 'product_data' => [ - 'code' => 'INVALID-PRODUCT', - 'name' => 'Invalid Product' - ] - ]; - - $response = $this->postJson('/api/v1/design/product-from-model', $invalidData); - $response->assertStatus(422); - - // Parameter out of valid range - $outOfRangeData = [ - 'model_id' => $this->model->id, - 'parameters' => [ - 'width' => -100, // Invalid negative value - 'height' => 800, - 'screen_type' => 'FABRIC' - ], - 'product_data' => [ - 'code' => 'OUT-OF-RANGE', - 'name' => 'Out of Range Product' - ] - ]; - - $response = $this->postJson('/api/v1/design/product-from-model', $outOfRangeData); - $response->assertStatus(422); - } - - /** @test */ - public function validates_product_code_uniqueness() - { - // Create first product - $productData1 = [ - 'model_id' => $this->model->id, - 'parameters' => [ - 'width' => 1000, - 'height' => 600, - 'screen_type' => 'FABRIC' - ], - 'product_data' => [ - 'code' => 'DUPLICATE-CODE', - 'name' => 'First Product', - 'category_id' => $this->category->id - ] - ]; - - $response1 = $this->postJson('/api/v1/design/product-from-model', $productData1); - $response1->assertStatus(201); - - // Try to create second product with same code - $productData2 = [ - 'model_id' => $this->model->id, - 'parameters' => [ - 'width' => 1200, - 'height' => 800, - 'screen_type' => 'STEEL' - ], - 'product_data' => [ - 'code' => 'DUPLICATE-CODE', // Same code - 'name' => 'Second Product', - 'category_id' => $this->category->id - ] - ]; - - $response2 = $this->postJson('/api/v1/design/product-from-model', $productData2); - $response2->assertStatus(422); - } - - /** @test */ - public function can_create_product_with_custom_attributes() - { - $productData = [ - 'model_id' => $this->model->id, - 'parameters' => [ - 'width' => 1200, - 'height' => 800, - 'screen_type' => 'FABRIC' - ], - 'product_data' => [ - 'code' => 'CUSTOM-ATTRS', - 'name' => 'Product with Custom Attributes', - 'category_id' => $this->category->id, - 'description' => 'Product with extended attributes', - 'unit' => 'SET', - 'weight' => 15.5, - 'color' => 'WHITE', - 'material_grade' => 'A-GRADE' - ], - 'custom_attributes' => [ - 'installation_difficulty' => 'MEDIUM', - 'warranty_period' => '2_YEARS', - 'fire_rating' => 'B1' - ] - ]; - - $response = $this->postJson('/api/v1/design/product-from-model', $productData); - - $response->assertStatus(201); - - $this->assertDatabaseHas('products', [ - 'tenant_id' => $this->tenant->id, - 'code' => 'CUSTOM-ATTRS', - 'weight' => 15.5, - 'color' => 'WHITE' - ]); - } - - /** @test */ - public function can_create_multiple_products_from_same_model() - { - $baseData = [ - 'model_id' => $this->model->id, - 'generate_bom' => false - ]; - - $products = [ - [ - 'parameters' => ['width' => 800, 'height' => 600, 'screen_type' => 'FABRIC'], - 'code' => 'KSS01-800x600-FABRIC' - ], - [ - 'parameters' => ['width' => 1000, 'height' => 800, 'screen_type' => 'STEEL'], - 'code' => 'KSS01-1000x800-STEEL' - ], - [ - 'parameters' => ['width' => 1200, 'height' => 1000, 'screen_type' => 'FABRIC'], - 'code' => 'KSS01-1200x1000-FABRIC' - ] - ]; - - foreach ($products as $index => $productSpec) { - $productData = array_merge($baseData, [ - 'parameters' => $productSpec['parameters'], - 'product_data' => [ - 'code' => $productSpec['code'], - 'name' => 'Product ' . ($index + 1), - 'category_id' => $this->category->id - ] - ]); - - $response = $this->postJson('/api/v1/design/product-from-model', $productData); - $response->assertStatus(201); - } - - // Verify all products were created - foreach ($products as $productSpec) { - $this->assertDatabaseHas('products', [ - 'tenant_id' => $this->tenant->id, - 'code' => $productSpec['code'] - ]); - } - } - - /** @test */ - public function can_list_products_created_from_model() - { - // Create some products from the model - $this->createTestProductFromModel('PROD-1', ['width' => 800, 'height' => 600]); - $this->createTestProductFromModel('PROD-2', ['width' => 1000, 'height' => 800]); - - // Create a product not from model - Product::factory()->create([ - 'tenant_id' => $this->tenant->id, - 'code' => 'NON-MODEL-PROD' - ]); - - $response = $this->getJson('/api/v1/design/product-from-model/list?model_id=' . $this->model->id); - - $response->assertStatus(200) - ->assertJsonStructure([ - 'success', - 'data' => [ - 'data' => [ - '*' => [ - 'id', - 'code', - 'name', - 'model_reference' => [ - 'model_id', - 'input_parameters', - 'calculated_values' - ], - 'created_at' - ] - ] - ] - ]); - - $data = $response->json('data.data'); - $this->assertCount(2, $data); // Only products created from model - } - - /** @test */ - public function enforces_tenant_isolation() - { - // Create model for different tenant - $otherTenant = Tenant::factory()->create(); - $otherModel = DesignModel::factory()->create([ - 'tenant_id' => $otherTenant->id - ]); - - $productData = [ - 'model_id' => $otherModel->id, - 'parameters' => [ - 'width' => 1000, - 'height' => 800 - ], - 'product_data' => [ - 'code' => 'TENANT-TEST', - 'name' => 'Tenant Test Product' - ] - ]; - - $response = $this->postJson('/api/v1/design/product-from-model', $productData); - $response->assertStatus(404); - } - - /** @test */ - public function can_update_existing_product_from_model() - { - // First create a product - $createData = [ - 'model_id' => $this->model->id, - 'parameters' => [ - 'width' => 1000, - 'height' => 600, - 'screen_type' => 'FABRIC' - ], - 'product_data' => [ - 'code' => 'UPDATE-TEST', - 'name' => 'Original Product', - 'category_id' => $this->category->id - ] - ]; - - $createResponse = $this->postJson('/api/v1/design/product-from-model', $createData); - $createResponse->assertStatus(201); - $productId = $createResponse->json('data.product.id'); - - // Then update it with new parameters - $updateData = [ - 'parameters' => [ - 'width' => 1200, // Changed - 'height' => 800, // Changed - 'screen_type' => 'STEEL' // Changed - ], - 'product_data' => [ - 'name' => 'Updated Product', // Changed - 'description' => 'Updated description' - ], - 'regenerate_bom' => true - ]; - - $updateResponse = $this->putJson('/api/v1/design/product-from-model/' . $productId, $updateData); - - $updateResponse->assertStatus(200) - ->assertJson([ - 'success' => true, - 'message' => 'message.updated' - ]); - - // Verify product was updated - $this->assertDatabaseHas('products', [ - 'id' => $productId, - 'name' => 'Updated Product' - ]); - } - - /** @test */ - public function can_clone_product_with_modified_parameters() - { - // Create original product - $originalProductId = $this->createTestProductFromModel('ORIGINAL', [ - 'width' => 1000, - 'height' => 600, - 'screen_type' => 'FABRIC' - ]); - - // Clone with modified parameters - $cloneData = [ - 'source_product_id' => $originalProductId, - 'parameters' => [ - 'width' => 1200, // Modified - 'height' => 600, // Same - 'screen_type' => 'STEEL' // Modified - ], - 'product_data' => [ - 'code' => 'CLONED-PRODUCT', - 'name' => 'Cloned Product', - 'category_id' => $this->category->id - ] - ]; - - $response = $this->postJson('/api/v1/design/product-from-model/clone', $cloneData); - - $response->assertStatus(201) - ->assertJson([ - 'success' => true, - 'message' => 'message.cloned' - ]); - - // Verify clone was created - $this->assertDatabaseHas('products', [ - 'tenant_id' => $this->tenant->id, - 'code' => 'CLONED-PRODUCT' - ]); - - // Verify original product still exists - $this->assertDatabaseHas('products', [ - 'id' => $originalProductId - ]); - } - - /** - * Helper method to create a test product from model - */ - private function createTestProductFromModel(string $code, array $parameters): int - { - $productData = [ - 'model_id' => $this->model->id, - 'parameters' => array_merge([ - 'width' => 1000, - 'height' => 600, - 'screen_type' => 'FABRIC' - ], $parameters), - 'product_data' => [ - 'code' => $code, - 'name' => 'Test Product ' . $code, - 'category_id' => $this->category->id - ] - ]; - - $response = $this->postJson('/api/v1/design/product-from-model', $productData); - return $response->json('data.product.id'); - } -} diff --git a/tests/Feature/ParameterBasedBomApiTest.php b/tests/Feature/ParameterBasedBomApiTest.php deleted file mode 100644 index 4f0563a..0000000 --- a/tests/Feature/ParameterBasedBomApiTest.php +++ /dev/null @@ -1,606 +0,0 @@ -artisan('migrate'); - $this->seed(ParameterBasedBomTestSeeder::class); - - // Create test user and authenticate - $this->user = User::factory()->create(['tenant_id' => 1]); - Sanctum::actingAs($this->user); - - // Set required headers - $this->withHeaders([ - 'X-API-KEY' => config('app.api_key', 'test-api-key'), - 'Accept' => 'application/json', - 'Content-Type' => 'application/json', - ]); - - $this->kss01Model = Model::where('code', 'KSS01')->first(); - } - - /** @test */ - public function it_can_get_model_parameters() - { - // Act - $response = $this->getJson("/api/v1/design/models/{$this->kss01Model->id}/parameters"); - - // Assert - $response->assertOk() - ->assertJsonStructure([ - 'success', - 'message', - 'data' => [ - '*' => [ - 'id', - 'name', - 'label', - 'type', - 'default_value', - 'validation_rules', - 'options', - 'sort_order', - 'is_required', - 'is_active', - ] - ] - ]); - - $parameters = $response->json('data'); - $this->assertCount(5, $parameters); // W0, H0, screen_type, install_type, power_source - - // Check parameter order - $this->assertEquals('W0', $parameters[0]['name']); - $this->assertEquals('H0', $parameters[1]['name']); - $this->assertEquals('screen_type', $parameters[2]['name']); - } - - /** @test */ - public function it_can_get_model_formulas() - { - // Act - $response = $this->getJson("/api/v1/design/models/{$this->kss01Model->id}/formulas"); - - // Assert - $response->assertOk() - ->assertJsonStructure([ - 'success', - 'message', - 'data' => [ - '*' => [ - 'id', - 'name', - 'expression', - 'description', - 'return_type', - 'sort_order', - 'is_active', - ] - ] - ]); - - $formulas = $response->json('data'); - $this->assertGreaterThanOrEqual(7, count($formulas)); // W1, H1, area, weight, motor, bracket, guide - - // Check formula order - $this->assertEquals('W1', $formulas[0]['name']); - $this->assertEquals('H1', $formulas[1]['name']); - $this->assertEquals('area', $formulas[2]['name']); - } - - /** @test */ - public function it_can_get_model_condition_rules() - { - // Act - $response = $this->getJson("/api/v1/design/models/{$this->kss01Model->id}/rules"); - - // Assert - $response->assertOk() - ->assertJsonStructure([ - 'success', - 'message', - 'data' => [ - '*' => [ - 'id', - 'name', - 'description', - 'condition_expression', - 'component_code', - 'quantity_expression', - 'priority', - 'is_active', - ] - ] - ]); - - $rules = $response->json('data'); - $this->assertGreaterThanOrEqual(7, count($rules)); // Various case, bottom, shaft, pipe rules - - // Check rules are ordered by priority - $priorities = collect($rules)->pluck('priority'); - $this->assertEquals($priorities->sort()->values(), $priorities->values()); - } - - /** @test */ - public function it_can_resolve_bom_for_small_screen() - { - // Arrange - $inputParams = [ - 'W0' => 1000, - 'H0' => 800, - 'screen_type' => 'SCREEN', - 'install_type' => 'WALL', - 'power_source' => 'AC', - ]; - - // Act - $response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/bom/resolve", [ - 'parameters' => $inputParams - ]); - - // Assert - $response->assertOk() - ->assertJsonStructure([ - 'success', - 'message', - 'data' => [ - 'model_id', - 'input_parameters', - 'calculated_formulas' => [ - 'W1', - 'H1', - 'area', - 'weight', - 'motor', - 'bracket', - 'guide', - ], - 'bom_items' => [ - '*' => [ - 'component_code', - 'quantity', - 'rule_name', - 'condition_expression', - ] - ], - 'summary' => [ - 'total_components', - 'total_weight', - 'component_categories', - ] - ] - ]); - - $data = $response->json('data'); - - // Check calculated formulas - $formulas = $data['calculated_formulas']; - $this->assertEquals(1120, $formulas['W1']); // 1000 + 120 - $this->assertEquals(900, $formulas['H1']); // 800 + 100 - $this->assertEquals(1.008, $formulas['area']); // 1120 * 900 / 1000000 - $this->assertEquals('0.5HP', $formulas['motor']); // area <= 3 - - // Check BOM items - $bomItems = $data['bom_items']; - $this->assertGreaterThan(0, count($bomItems)); - - // Check specific components - $caseItem = collect($bomItems)->first(fn($item) => str_contains($item['component_code'], 'CASE')); - $this->assertEquals('CASE-SMALL', $caseItem['component_code']); - - $screenPipe = collect($bomItems)->first(fn($item) => $item['component_code'] === 'PIPE-SCREEN'); - $this->assertNotNull($screenPipe); - - $slatPipe = collect($bomItems)->first(fn($item) => $item['component_code'] === 'PIPE-SLAT'); - $this->assertNull($slatPipe); - } - - /** @test */ - public function it_can_resolve_bom_for_large_screen() - { - // Arrange - $inputParams = [ - 'W0' => 2500, - 'H0' => 1500, - 'screen_type' => 'SCREEN', - 'install_type' => 'SIDE', - 'power_source' => 'AC', - ]; - - // Act - $response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/bom/resolve", [ - 'parameters' => $inputParams - ]); - - // Assert - $response->assertOk(); - - $data = $response->json('data'); - $formulas = $data['calculated_formulas']; - - // Check large screen calculations - $this->assertEquals(2620, $formulas['W1']); // 2500 + 120 - $this->assertEquals(1600, $formulas['H1']); // 1500 + 100 - $this->assertEquals(4.192, $formulas['area']); // 2620 * 1600 / 1000000 - $this->assertEquals('1HP', $formulas['motor']); // 3 < area <= 6 - - // Check medium case is selected - $bomItems = $data['bom_items']; - $caseItem = collect($bomItems)->first(fn($item) => str_contains($item['component_code'], 'CASE')); - $this->assertEquals('CASE-MEDIUM', $caseItem['component_code']); - - // Check bracket quantity - $bottomItem = collect($bomItems)->first(fn($item) => $item['component_code'] === 'BOTTOM-001'); - $this->assertEquals(3, $bottomItem['quantity']); // CEIL(2620 / 1000) - } - - /** @test */ - public function it_can_resolve_bom_for_slat_type() - { - // Arrange - $inputParams = [ - 'W0' => 1500, - 'H0' => 1000, - 'screen_type' => 'SLAT', - 'install_type' => 'WALL', - 'power_source' => 'AC', - ]; - - // Act - $response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/bom/resolve", [ - 'parameters' => $inputParams - ]); - - // Assert - $response->assertOk(); - - $bomItems = $response->json('data.bom_items'); - - // Check that SLAT pipe is used - $screenPipe = collect($bomItems)->first(fn($item) => $item['component_code'] === 'PIPE-SCREEN'); - $slatPipe = collect($bomItems)->first(fn($item) => $item['component_code'] === 'PIPE-SLAT'); - - $this->assertNull($screenPipe); - $this->assertNotNull($slatPipe); - $this->assertEquals('PIPE-SLAT', $slatPipe['component_code']); - } - - /** @test */ - public function it_validates_missing_required_parameters() - { - // Arrange - Missing H0 parameter - $incompleteParams = [ - 'W0' => 1000, - 'screen_type' => 'SCREEN', - 'install_type' => 'WALL', - 'power_source' => 'AC', - ]; - - // Act - $response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/bom/resolve", [ - 'parameters' => $incompleteParams - ]); - - // Assert - $response->assertStatus(422) - ->assertJsonValidationErrors(['parameters.H0']); - } - - /** @test */ - public function it_validates_parameter_ranges() - { - // Arrange - W0 below minimum - $invalidParams = [ - 'W0' => 100, // Below minimum (500) - 'H0' => 800, - 'screen_type' => 'SCREEN', - 'install_type' => 'WALL', - 'power_source' => 'AC', - ]; - - // Act - $response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/bom/resolve", [ - 'parameters' => $invalidParams - ]); - - // Assert - $response->assertStatus(422) - ->assertJsonValidationErrors(['parameters.W0']); - } - - /** @test */ - public function it_validates_select_parameter_options() - { - // Arrange - Invalid screen_type - $invalidParams = [ - 'W0' => 1000, - 'H0' => 800, - 'screen_type' => 'INVALID_TYPE', - 'install_type' => 'WALL', - 'power_source' => 'AC', - ]; - - // Act - $response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/bom/resolve", [ - 'parameters' => $invalidParams - ]); - - // Assert - $response->assertStatus(422) - ->assertJsonValidationErrors(['parameters.screen_type']); - } - - /** @test */ - public function it_can_create_product_from_model() - { - // Arrange - $inputParams = [ - 'W0' => 1200, - 'H0' => 900, - 'screen_type' => 'SCREEN', - 'install_type' => 'WALL', - 'power_source' => 'AC', - ]; - - $productData = [ - 'code' => 'KSS01-TEST-001', - 'name' => '테스트 스크린 블라인드 1200x900', - 'description' => 'API 테스트로 생성된 제품', - ]; - - // Act - $response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/products", [ - 'parameters' => $inputParams, - 'product' => $productData, - ]); - - // Assert - $response->assertCreated() - ->assertJsonStructure([ - 'success', - 'message', - 'data' => [ - 'product' => [ - 'id', - 'code', - 'name', - 'description', - 'product_type', - 'model_id', - 'parameter_values', - ], - 'bom_items' => [ - '*' => [ - 'id', - 'product_id', - 'component_code', - 'component_type', - 'quantity', - 'unit', - ] - ], - 'summary' => [ - 'total_components', - 'calculated_formulas', - ] - ] - ]); - - $data = $response->json('data'); - $product = $data['product']; - - // Check product data - $this->assertEquals('KSS01-TEST-001', $product['code']); - $this->assertEquals('테스트 스크린 블라인드 1200x900', $product['name']); - $this->assertEquals($this->kss01Model->id, $product['model_id']); - - // Check parameter values are stored - $parameterValues = json_decode($product['parameter_values'], true); - $this->assertEquals(1200, $parameterValues['W0']); - $this->assertEquals(900, $parameterValues['H0']); - - // Check BOM items - $bomItems = $data['bom_items']; - $this->assertGreaterThan(0, count($bomItems)); - - foreach ($bomItems as $item) { - $this->assertEquals($product['id'], $item['product_id']); - $this->assertNotEmpty($item['component_code']); - $this->assertGreaterThan(0, $item['quantity']); - } - } - - /** @test */ - public function it_can_preview_product_without_creating() - { - // Arrange - $inputParams = [ - 'W0' => 1800, - 'H0' => 1200, - 'screen_type' => 'SCREEN', - 'install_type' => 'SIDE', - 'power_source' => 'DC', - ]; - - // Act - $response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/preview", [ - 'parameters' => $inputParams - ]); - - // Assert - $response->assertOk() - ->assertJsonStructure([ - 'success', - 'message', - 'data' => [ - 'suggested_product' => [ - 'code', - 'name', - 'description', - ], - 'calculated_values' => [ - 'W1', - 'H1', - 'area', - 'weight', - 'motor', - ], - 'bom_preview' => [ - 'total_components', - 'components' => [ - '*' => [ - 'component_code', - 'quantity', - 'description', - ] - ] - ], - 'cost_estimate' => [ - 'total_material_cost', - 'estimated_labor_cost', - 'total_estimated_cost', - ] - ] - ]); - - $data = $response->json('data'); - - // Check suggested product info - $suggestedProduct = $data['suggested_product']; - $this->assertStringContains('KSS01', $suggestedProduct['code']); - $this->assertStringContains('1800x1200', $suggestedProduct['name']); - - // Check calculated values - $calculatedValues = $data['calculated_values']; - $this->assertEquals(1920, $calculatedValues['W1']); // 1800 + 120 - $this->assertEquals(1300, $calculatedValues['H1']); // 1200 + 100 - - // Check BOM preview - $bomPreview = $data['bom_preview']; - $this->assertGreaterThan(0, $bomPreview['total_components']); - $this->assertNotEmpty($bomPreview['components']); - } - - /** @test */ - public function it_handles_authentication_properly() - { - // Arrange - Remove authentication - Sanctum::actingAs(null); - - // Act - $response = $this->getJson("/api/v1/design/models/{$this->kss01Model->id}/parameters"); - - // Assert - $response->assertUnauthorized(); - } - - /** @test */ - public function it_handles_tenant_isolation() - { - // Arrange - Create user with different tenant - $otherTenantUser = User::factory()->create(['tenant_id' => 2]); - Sanctum::actingAs($otherTenantUser); - - // Act - $response = $this->getJson("/api/v1/design/models/{$this->kss01Model->id}/parameters"); - - // Assert - $response->assertNotFound(); - } - - /** @test */ - public function it_handles_performance_for_large_datasets() - { - // Arrange - Use performance test model with many parameters/formulas/rules - $performanceModel = Model::where('code', 'PERF-TEST')->first(); - - if (!$performanceModel) { - $this->markTestSkipped('Performance test model not found. Run in testing environment.'); - } - - $inputParams = array_fill_keys( - ModelParameter::where('model_id', $performanceModel->id)->pluck('name')->toArray(), - 100 - ); - - // Act - $startTime = microtime(true); - $response = $this->postJson("/api/v1/design/models/{$performanceModel->id}/bom/resolve", [ - 'parameters' => $inputParams - ]); - $executionTime = microtime(true) - $startTime; - - // Assert - $response->assertOk(); - $this->assertLessThan(2.0, $executionTime, 'BOM resolution should complete within 2 seconds'); - - $data = $response->json('data'); - $this->assertArrayHasKey('calculated_formulas', $data); - $this->assertArrayHasKey('bom_items', $data); - } - - /** @test */ - public function it_returns_appropriate_error_for_nonexistent_model() - { - // Act - $response = $this->getJson('/api/v1/design/models/999999/parameters'); - - // Assert - $response->assertNotFound() - ->assertJson([ - 'success' => false, - 'message' => 'error.model_not_found', - ]); - } - - /** @test */ - public function it_handles_concurrent_requests_safely() - { - // Arrange - $inputParams = [ - 'W0' => 1000, - 'H0' => 800, - 'screen_type' => 'SCREEN', - 'install_type' => 'WALL', - 'power_source' => 'AC', - ]; - - // Act - Simulate concurrent requests - $promises = []; - for ($i = 0; $i < 5; $i++) { - $promises[] = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/bom/resolve", [ - 'parameters' => $inputParams - ]); - } - - // Assert - All should succeed with same results - foreach ($promises as $response) { - $response->assertOk(); - $formulas = $response->json('data.calculated_formulas'); - $this->assertEquals(1120, $formulas['W1']); - $this->assertEquals(900, $formulas['H1']); - } - } -} \ No newline at end of file diff --git a/tests/Performance/BomResolutionPerformanceTest.php b/tests/Performance/BomResolutionPerformanceTest.php deleted file mode 100644 index ad6500d..0000000 --- a/tests/Performance/BomResolutionPerformanceTest.php +++ /dev/null @@ -1,490 +0,0 @@ -user = User::factory()->create(['tenant_id' => 1]); - Sanctum::actingAs($this->user); - - $this->service = new ProductFromModelService(); - $this->service->setTenantId(1)->setApiUserId(1); - - // Set headers - $this->withHeaders([ - 'X-API-KEY' => config('app.api_key', 'test-api-key'), - 'Accept' => 'application/json', - ]); - - // Seed test data - $this->seed(ParameterBasedBomTestSeeder::class); - - // Create performance test model with large dataset - $this->createPerformanceTestModel(); - } - - private function createPerformanceTestModel(): void - { - $this->performanceModel = Model::factory()->create([ - 'code' => 'PERF-TEST', - 'name' => 'Performance Test Model', - 'product_family' => 'SCREEN', - 'tenant_id' => 1, - ]); - - // Create many parameters (50) - for ($i = 1; $i <= 50; $i++) { - ModelParameter::factory()->create([ - 'model_id' => $this->performanceModel->id, - 'name' => "param_{$i}", - 'label' => "Parameter {$i}", - 'type' => 'NUMBER', - 'default_value' => '100', - 'sort_order' => $i, - 'tenant_id' => 1, - ]); - } - - // Create many formulas (30) - for ($i = 1; $i <= 30; $i++) { - ModelFormula::factory()->create([ - 'model_id' => $this->performanceModel->id, - 'name' => "formula_{$i}", - 'expression' => "param_1 + param_2 + {$i}", - 'description' => "Formula {$i}", - 'return_type' => 'NUMBER', - 'sort_order' => $i, - 'tenant_id' => 1, - ]); - } - - // Create many condition rules (100) - for ($i = 1; $i <= 100; $i++) { - BomConditionRule::factory()->create([ - 'model_id' => $this->performanceModel->id, - 'name' => "rule_{$i}", - 'condition_expression' => "formula_1 > {$i}", - 'component_code' => "COMPONENT_{$i}", - 'quantity_expression' => '1', - 'priority' => $i, - 'tenant_id' => 1, - ]); - } - } - - /** @test */ - public function it_resolves_simple_bom_within_performance_threshold() - { - // Use KSS01 model for simple test - $kss01 = Model::where('code', 'KSS01')->first(); - - $inputParams = [ - 'W0' => 1000, - 'H0' => 800, - 'screen_type' => 'SCREEN', - 'install_type' => 'WALL', - 'power_source' => 'AC', - ]; - - $startTime = microtime(true); - - $response = $this->postJson("/api/v1/design/models/{$kss01->id}/bom/resolve", [ - 'parameters' => $inputParams - ]); - - $executionTime = microtime(true) - $startTime; - - // Should complete within 500ms for simple model - $this->assertLessThan(0.5, $executionTime, 'Simple BOM resolution took too long'); - $response->assertOk(); - } - - /** @test */ - public function it_handles_complex_bom_resolution_efficiently() - { - // Create parameters for all 50 parameters - $inputParams = []; - for ($i = 1; $i <= 50; $i++) { - $inputParams["param_{$i}"] = 100 + $i; - } - - $startTime = microtime(true); - - $response = $this->postJson("/api/v1/design/models/{$this->performanceModel->id}/bom/resolve", [ - 'parameters' => $inputParams - ]); - - $executionTime = microtime(true) - $startTime; - - // Should complete within 2 seconds even for complex model - $this->assertLessThan(2.0, $executionTime, 'Complex BOM resolution took too long'); - $response->assertOk(); - } - - /** @test */ - public function it_handles_concurrent_bom_resolutions() - { - $kss01 = Model::where('code', 'KSS01')->first(); - - $inputParams = [ - 'W0' => 1000, - 'H0' => 800, - 'screen_type' => 'SCREEN', - 'install_type' => 'WALL', - 'power_source' => 'AC', - ]; - - $startTime = microtime(true); - $responses = []; - - // Simulate 10 concurrent requests - for ($i = 0; $i < 10; $i++) { - $responses[] = $this->postJson("/api/v1/design/models/{$kss01->id}/bom/resolve", [ - 'parameters' => $inputParams - ]); - } - - $totalTime = microtime(true) - $startTime; - - // All requests should complete successfully - foreach ($responses as $response) { - $response->assertOk(); - } - - // Total time for 10 concurrent requests should be reasonable - $this->assertLessThan(5.0, $totalTime, 'Concurrent BOM resolutions took too long'); - } - - /** @test */ - public function it_optimizes_formula_evaluation_with_caching() - { - $kss01 = Model::where('code', 'KSS01')->first(); - - $inputParams = [ - 'W0' => 1000, - 'H0' => 800, - 'screen_type' => 'SCREEN', - 'install_type' => 'WALL', - 'power_source' => 'AC', - ]; - - // First request (cold cache) - $startTime1 = microtime(true); - $response1 = $this->postJson("/api/v1/design/models/{$kss01->id}/bom/resolve", [ - 'parameters' => $inputParams - ]); - $time1 = microtime(true) - $startTime1; - - // Second request with same parameters (warm cache) - $startTime2 = microtime(true); - $response2 = $this->postJson("/api/v1/design/models/{$kss01->id}/bom/resolve", [ - 'parameters' => $inputParams - ]); - $time2 = microtime(true) - $startTime2; - - // Both should succeed - $response1->assertOk(); - $response2->assertOk(); - - // Results should be identical - $this->assertEquals($response1->json('data'), $response2->json('data')); - - // Second request should be faster (with caching) - $this->assertLessThan($time1, $time2 * 1.5, 'Caching is not improving performance'); - } - - /** @test */ - public function it_measures_memory_usage_during_bom_resolution() - { - $kss01 = Model::where('code', 'KSS01')->first(); - - $inputParams = [ - 'W0' => 1000, - 'H0' => 800, - 'screen_type' => 'SCREEN', - 'install_type' => 'WALL', - 'power_source' => 'AC', - ]; - - $memoryBefore = memory_get_usage(true); - - $response = $this->postJson("/api/v1/design/models/{$kss01->id}/bom/resolve", [ - 'parameters' => $inputParams - ]); - - $memoryAfter = memory_get_usage(true); - $memoryUsed = $memoryAfter - $memoryBefore; - - $response->assertOk(); - - // Memory usage should be reasonable (less than 50MB for simple BOM) - $this->assertLessThan(50 * 1024 * 1024, $memoryUsed, 'Excessive memory usage detected'); - } - - /** @test */ - public function it_handles_large_datasets_efficiently() - { - // Test with the performance model (50 params, 30 formulas, 100 rules) - $inputParams = []; - for ($i = 1; $i <= 50; $i++) { - $inputParams["param_{$i}"] = rand(50, 200); - } - - $memoryBefore = memory_get_usage(true); - $startTime = microtime(true); - - $response = $this->postJson("/api/v1/design/models/{$this->performanceModel->id}/bom/resolve", [ - 'parameters' => $inputParams - ]); - - $executionTime = microtime(true) - $startTime; - $memoryAfter = memory_get_usage(true); - $memoryUsed = $memoryAfter - $memoryBefore; - - $response->assertOk(); - - // Performance thresholds for large datasets - $this->assertLessThan(5.0, $executionTime, 'Large dataset processing took too long'); - $this->assertLessThan(100 * 1024 * 1024, $memoryUsed, 'Excessive memory usage for large dataset'); - - // Should return reasonable amount of data - $data = $response->json('data'); - $this->assertArrayHasKey('calculated_formulas', $data); - $this->assertArrayHasKey('bom_items', $data); - $this->assertGreaterThan(0, count($data['bom_items'])); - } - - /** @test */ - public function it_benchmarks_formula_evaluation_complexity() - { - // Test various formula complexities - $complexityTests = [ - 'simple' => 'param_1 + param_2', - 'medium' => 'param_1 * param_2 + param_3 / param_4', - 'complex' => 'CEIL(param_1 / 600) + FLOOR(param_2 * 1.5) + IF(param_3 > 100, param_4, param_5)', - ]; - - $benchmarks = []; - - foreach ($complexityTests as $complexity => $expression) { - // Create test formula - $formula = ModelFormula::factory()->create([ - 'model_id' => $this->performanceModel->id, - 'name' => "benchmark_{$complexity}", - 'expression' => $expression, - 'sort_order' => 999, - 'tenant_id' => 1, - ]); - - $inputParams = [ - 'param_1' => 1000, - 'param_2' => 800, - 'param_3' => 150, - 'param_4' => 200, - 'param_5' => 50, - ]; - - $startTime = microtime(true); - - // Evaluate formula multiple times to get average - for ($i = 0; $i < 100; $i++) { - try { - $this->service->evaluateFormula($formula, $inputParams); - } catch (\Exception $e) { - // Some complex formulas might fail, that's okay for benchmarking - } - } - - $avgTime = (microtime(true) - $startTime) / 100; - $benchmarks[$complexity] = $avgTime; - - // Cleanup - $formula->delete(); - } - - // Complex formulas should still execute reasonably fast - $this->assertLessThan(0.01, $benchmarks['simple'], 'Simple formula evaluation too slow'); - $this->assertLessThan(0.02, $benchmarks['medium'], 'Medium formula evaluation too slow'); - $this->assertLessThan(0.05, $benchmarks['complex'], 'Complex formula evaluation too slow'); - } - - /** @test */ - public function it_scales_with_increasing_rule_count() - { - // Test BOM resolution with different rule counts - $scalingTests = [10, 50, 100]; - $scalingResults = []; - - foreach ($scalingTests as $ruleCount) { - // Create test model with specific rule count - $testModel = Model::factory()->create([ - 'code' => "SCALE_TEST_{$ruleCount}", - 'tenant_id' => 1, - ]); - - // Create basic parameters - ModelParameter::factory()->create([ - 'model_id' => $testModel->id, - 'name' => 'test_param', - 'type' => 'NUMBER', - 'tenant_id' => 1, - ]); - - // Create test formula - ModelFormula::factory()->create([ - 'model_id' => $testModel->id, - 'name' => 'test_formula', - 'expression' => 'test_param * 2', - 'tenant_id' => 1, - ]); - - // Create specified number of rules - for ($i = 1; $i <= $ruleCount; $i++) { - BomConditionRule::factory()->create([ - 'model_id' => $testModel->id, - 'condition_expression' => "test_formula > {$i}", - 'component_code' => "COMP_{$i}", - 'priority' => $i, - 'tenant_id' => 1, - ]); - } - - $startTime = microtime(true); - - $response = $this->postJson("/api/v1/design/models/{$testModel->id}/bom/resolve", [ - 'parameters' => ['test_param' => 100] - ]); - - $executionTime = microtime(true) - $startTime; - $scalingResults[$ruleCount] = $executionTime; - - $response->assertOk(); - - // Cleanup - $testModel->delete(); - } - - // Execution time should scale reasonably (not exponentially) - $ratio50to10 = $scalingResults[50] / $scalingResults[10]; - $ratio100to50 = $scalingResults[100] / $scalingResults[50]; - - // Should not scale worse than linearly - $this->assertLessThan(10, $ratio50to10, 'Poor scaling from 10 to 50 rules'); - $this->assertLessThan(5, $ratio100to50, 'Poor scaling from 50 to 100 rules'); - } - - /** @test */ - public function it_handles_stress_test_scenarios() - { - $kss01 = Model::where('code', 'KSS01')->first(); - - // Stress test with many rapid requests - $stressTestCount = 50; - $successCount = 0; - $errors = []; - $totalTime = 0; - - for ($i = 0; $i < $stressTestCount; $i++) { - $inputParams = [ - 'W0' => rand(500, 3000), - 'H0' => rand(400, 2000), - 'screen_type' => rand(0, 1) ? 'SCREEN' : 'SLAT', - 'install_type' => ['WALL', 'SIDE', 'MIXED'][rand(0, 2)], - 'power_source' => ['AC', 'DC', 'MANUAL'][rand(0, 2)], - ]; - - $startTime = microtime(true); - - try { - $response = $this->postJson("/api/v1/design/models/{$kss01->id}/bom/resolve", [ - 'parameters' => $inputParams - ]); - - $executionTime = microtime(true) - $startTime; - $totalTime += $executionTime; - - if ($response->isSuccessful()) { - $successCount++; - } else { - $errors[] = $response->status(); - } - } catch (\Exception $e) { - $errors[] = $e->getMessage(); - } - } - - $avgTime = $totalTime / $stressTestCount; - $successRate = ($successCount / $stressTestCount) * 100; - - // Stress test requirements - $this->assertGreaterThanOrEqual(95, $successRate, 'Success rate too low under stress'); - $this->assertLessThan(1.0, $avgTime, 'Average response time too high under stress'); - - if (count($errors) > 0) { - $this->addToAssertionCount(1); // Just to show we're tracking errors - // Log errors for analysis - error_log('Stress test errors: ' . json_encode(array_unique($errors))); - } - } - - /** @test */ - public function it_monitors_database_query_performance() - { - $kss01 = Model::where('code', 'KSS01')->first(); - - $inputParams = [ - 'W0' => 1000, - 'H0' => 800, - 'screen_type' => 'SCREEN', - 'install_type' => 'WALL', - 'power_source' => 'AC', - ]; - - // Enable query logging - \DB::enableQueryLog(); - - $response = $this->postJson("/api/v1/design/models/{$kss01->id}/bom/resolve", [ - 'parameters' => $inputParams - ]); - - $queries = \DB::getQueryLog(); - \DB::disableQueryLog(); - - $response->assertOk(); - - // Analyze query performance - $queryCount = count($queries); - $totalQueryTime = array_sum(array_column($queries, 'time')); - - // Should not have excessive queries (N+1 problem) - $this->assertLessThan(50, $queryCount, 'Too many database queries'); - - // Total query time should be reasonable - $this->assertLessThan(500, $totalQueryTime, 'Database queries taking too long'); - - // Check for slow queries - $slowQueries = array_filter($queries, fn($query) => $query['time'] > 100); - $this->assertEmpty($slowQueries, 'Slow queries detected: ' . json_encode($slowQueries)); - } -} \ No newline at end of file diff --git a/tests/README.md b/tests/README.md deleted file mode 100644 index d291e19..0000000 --- a/tests/README.md +++ /dev/null @@ -1,251 +0,0 @@ -# Parametric BOM System Test Suite - -This directory contains comprehensive test files for the parametric BOM system, including unit tests, integration tests, performance tests, and API validation. - -## Test Structure - -### PHPUnit Tests (`tests/Feature/Design/`) - -Comprehensive PHPUnit feature tests for all Design domain components: - -- **ModelParameterTest.php** - Tests parameter CRUD operations, validation, and type casting -- **ModelFormulaTest.php** - Tests formula creation, calculation, and dependency resolution -- **BomConditionRuleTest.php** - Tests condition rule evaluation and BOM manipulation -- **BomResolverTest.php** - Tests complete BOM resolution workflows -- **ProductFromModelTest.php** - Tests product creation from parametric models - -### Database Seeders (`database/seeders/`) - -Test data seeders for comprehensive testing: - -- **ParametricBomSeeder.php** - Creates comprehensive test data with multiple models -- **KSS01ModelSeeder.php** - Creates specific KSS01 model with realistic parameters - -### Validation Scripts (`scripts/validation/`) - -Standalone validation scripts for system testing: - -- **validate_bom_system.php** - Complete system validation -- **test_kss01_scenarios.php** - KSS01-specific business scenario testing -- **performance_test.php** - Performance and scalability testing - -### API Testing (`tests/postman/`) - -Postman collection for API testing: - -- **parametric_bom.postman_collection.json** - Complete API test collection -- **parametric_bom.postman_environment.json** - Environment variables - -## Running Tests - -### Prerequisites - -1. **Seed Test Data** - ```bash - php artisan db:seed --class=KSS01ModelSeeder - # or for comprehensive test data: - php artisan db:seed --class=ParametricBomSeeder - ``` - -2. **Ensure API Key Configuration** - - Set up valid API keys in your environment - - Configure test tenant and user credentials - -### PHPUnit Tests - -Run individual test suites: - -```bash -# All Design domain tests -php artisan test tests/Feature/Design/ - -# Specific test classes -php artisan test tests/Feature/Design/ModelParameterTest.php -php artisan test tests/Feature/Design/BomResolverTest.php - -# Run with coverage -php artisan test --coverage-html coverage-report/ -``` - -### Validation Scripts - -Run system validation scripts: - -```bash -# Complete system validation -php scripts/validation/validate_bom_system.php - -# KSS01 business scenarios -php scripts/validation/test_kss01_scenarios.php - -# Performance testing -php scripts/validation/performance_test.php -``` - -### Postman API Tests - -1. Import the collection: `tests/postman/parametric_bom.postman_collection.json` -2. Import the environment: `tests/postman/parametric_bom.postman_environment.json` -3. Update environment variables: - - `api_key` - Your API key - - `user_email` - Test user email (default: demo@kss01.com) - - `user_password` - Test user password (default: kss01demo) -4. Run the collection - -## Test Coverage - -### Core Functionality - -- ✅ Parameter validation and type checking -- ✅ Formula calculation and dependency resolution -- ✅ Condition rule evaluation and BOM manipulation -- ✅ Complete BOM resolution workflows -- ✅ Product creation from parametric models -- ✅ Tenant isolation and security -- ✅ Error handling and edge cases - -### Business Scenarios - -- ✅ Residential applications (small windows, patio doors) -- ✅ Commercial applications (storefronts, office buildings) -- ✅ Edge cases (minimum/maximum sizes, unusual dimensions) -- ✅ Material type variations (fabric vs steel) -- ✅ Installation type variations (wall/ceiling/recessed) - -### Performance Testing - -- ✅ Single BOM resolution performance -- ✅ Batch resolution performance -- ✅ Memory usage analysis -- ✅ Database query efficiency -- ✅ Concurrent operation simulation -- ✅ Large dataset throughput - -### API Validation - -- ✅ Authentication and authorization -- ✅ CRUD operations for all entities -- ✅ BOM resolution workflows -- ✅ Error handling and validation -- ✅ Performance benchmarks - -## Performance Targets - -| Metric | Target | Test Location | -|--------|--------|---------------| -| Single BOM Resolution | < 200ms | performance_test.php | -| Batch 10 Resolutions | < 1.5s | performance_test.php | -| Batch 100 Resolutions | < 12s | performance_test.php | -| Memory Usage | < 50MB | performance_test.php | -| DB Queries per Resolution | < 20 | performance_test.php | -| Throughput | ≥ 10/sec | performance_test.php | - -## Quality Gates - -### System Validation Success Criteria - -- ✅ ≥90% test pass rate = Production Ready -- ⚠️ 75-89% test pass rate = Review Required -- ❌ <75% test pass rate = Not Ready - -### Business Scenario Success Criteria - -- ✅ ≥95% scenario pass rate = Business Logic Validated -- ⚠️ 85-94% scenario pass rate = Edge Cases Need Review -- ❌ <85% scenario pass rate = Critical Issues - -### Performance Success Criteria - -- ✅ ≥90% performance tests pass = Performance Requirements Met -- ⚠️ 70-89% performance tests pass = Performance Issues Detected -- ❌ <70% performance tests pass = Critical Performance Issues - -## Troubleshooting - -### Common Issues - -1. **"KSS_DEMO tenant not found"** - - Run KSS01ModelSeeder: `php artisan db:seed --class=KSS01ModelSeeder` - -2. **API key authentication failures** - - Verify API key is correctly set in environment - - Check API key middleware configuration - -3. **Test database issues** - - Ensure test database is properly configured - - Run migrations: `php artisan migrate --env=testing` - -4. **Performance test failures** - - Check database indexes are created - - Verify system resources (CPU, memory) - - Review query optimization - -### Debugging Tips - -1. **Enable Query Logging** - ```php - DB::enableQueryLog(); - // Run operations - $queries = DB::getQueryLog(); - ``` - -2. **Check Memory Usage** - ```php - echo memory_get_usage(true) / 1024 / 1024 . " MB\n"; - echo memory_get_peak_usage(true) / 1024 / 1024 . " MB peak\n"; - ``` - -3. **Profile Performance** - ```bash - php -d xdebug.profiler_enable=1 scripts/validation/performance_test.php - ``` - -## Continuous Integration - -### GitHub Actions / CI Pipeline - -```yaml -# Example CI configuration -- name: Run PHPUnit Tests - run: php artisan test --coverage-clover coverage.xml - -- name: Run System Validation - run: php scripts/validation/validate_bom_system.php - -- name: Run Performance Tests - run: php scripts/validation/performance_test.php - -- name: Check Coverage - run: | - if [ $(php -r "echo coverage_percentage();") -lt 80 ]; then - echo "Coverage below 80%" - exit 1 - fi -``` - -### Quality Metrics - -- **Code Coverage**: Minimum 80% line coverage -- **Test Pass Rate**: Minimum 95% pass rate -- **Performance**: All performance targets met -- **Security**: No security vulnerabilities in tests - -## Contributing - -When adding new tests: - -1. Follow existing naming conventions -2. Include both positive and negative test cases -3. Add performance considerations for new features -4. Update this README with new test documentation -5. Ensure tenant isolation in all tests -6. Include edge case testing - -## Support - -For test-related issues: - -1. Check logs in `storage/logs/laravel.log` -2. Review test output for specific failure details -3. Verify test data seeding completed successfully -4. Check database connection and permissions \ No newline at end of file diff --git a/tests/Security/ApiSecurityTest.php b/tests/Security/ApiSecurityTest.php deleted file mode 100644 index 3b87cab..0000000 --- a/tests/Security/ApiSecurityTest.php +++ /dev/null @@ -1,414 +0,0 @@ -user = User::factory()->create(['tenant_id' => 1]); - $this->otherTenantUser = User::factory()->create(['tenant_id' => 2]); - - // Create test model - $this->model = Model::factory()->create(['tenant_id' => 1]); - - // Set required headers - $this->withHeaders([ - 'X-API-KEY' => config('app.api_key', 'test-api-key'), - 'Accept' => 'application/json', - 'Content-Type' => 'application/json', - ]); - } - - /** @test */ - public function it_requires_api_key_for_all_endpoints() - { - // Remove API key header - $this->withHeaders([ - 'Accept' => 'application/json', - 'Content-Type' => 'application/json', - ]); - - // Test various endpoints - $endpoints = [ - ['GET', '/api/v1/design/models'], - ['GET', "/api/v1/design/models/{$this->model->id}/parameters"], - ['POST', "/api/v1/design/models/{$this->model->id}/bom/resolve"], - ]; - - foreach ($endpoints as [$method, $endpoint]) { - $response = $this->json($method, $endpoint); - $response->assertUnauthorized(); - } - } - - /** @test */ - public function it_rejects_invalid_api_keys() - { - $this->withHeaders([ - 'X-API-KEY' => 'invalid-api-key', - 'Accept' => 'application/json', - ]); - - $response = $this->getJson('/api/v1/design/models'); - $response->assertUnauthorized(); - } - - /** @test */ - public function it_requires_authentication_for_protected_routes() - { - // Test endpoints that require user authentication - $protectedEndpoints = [ - ['GET', "/api/v1/design/models/{$this->model->id}/parameters"], - ['POST', "/api/v1/design/models/{$this->model->id}/parameters"], - ['POST', "/api/v1/design/models/{$this->model->id}/bom/resolve"], - ['POST', "/api/v1/design/models/{$this->model->id}/products"], - ]; - - foreach ($protectedEndpoints as [$method, $endpoint]) { - $response = $this->json($method, $endpoint); - $response->assertUnauthorized(); - } - } - - /** @test */ - public function it_enforces_tenant_isolation() - { - // Authenticate as user from tenant 1 - Sanctum::actingAs($this->user); - - // Try to access model from different tenant - $otherTenantModel = Model::factory()->create(['tenant_id' => 2]); - - $response = $this->getJson("/api/v1/design/models/{$otherTenantModel->id}/parameters"); - $response->assertNotFound(); - } - - /** @test */ - public function it_prevents_sql_injection_in_parameters() - { - Sanctum::actingAs($this->user); - - // Test SQL injection attempts in various inputs - $sqlInjectionPayloads = [ - "'; DROP TABLE models; --", - "' UNION SELECT * FROM users --", - "1' OR '1'='1", - "", - ]; - - foreach ($sqlInjectionPayloads as $payload) { - // Test in BOM resolution parameters - $response = $this->postJson("/api/v1/design/models/{$this->model->id}/bom/resolve", [ - 'parameters' => [ - 'W0' => $payload, - 'H0' => 800, - 'screen_type' => 'SCREEN', - ] - ]); - - // Should either validate and reject, or handle safely - $this->assertTrue( - $response->status() === 422 || $response->status() === 400, - "SQL injection payload was not properly handled: {$payload}" - ); - } - } - - /** @test */ - public function it_sanitizes_formula_expressions() - { - Sanctum::actingAs($this->user); - - // Test dangerous expressions that could execute arbitrary code - $dangerousExpressions = [ - 'system("rm -rf /")', - 'eval("malicious code")', - 'exec("ls -la")', - '__import__("os").system("pwd")', - 'file_get_contents("/etc/passwd")', - ]; - - foreach ($dangerousExpressions as $expression) { - $response = $this->postJson("/api/v1/design/models/{$this->model->id}/formulas", [ - 'name' => 'test_formula', - 'expression' => $expression, - 'description' => 'Test formula', - 'return_type' => 'NUMBER', - 'sort_order' => 1, - ]); - - // Should reject dangerous expressions - $response->assertStatus(422); - $response->assertJsonValidationErrors(['expression']); - } - } - - /** @test */ - public function it_prevents_xss_in_user_inputs() - { - Sanctum::actingAs($this->user); - - $xssPayloads = [ - '', - 'javascript:alert("xss")', - '', - '', - ]; - - foreach ($xssPayloads as $payload) { - $response = $this->postJson("/api/v1/design/models/{$this->model->id}/parameters", [ - 'name' => 'test_param', - 'label' => $payload, - 'type' => 'NUMBER', - 'default_value' => '0', - 'sort_order' => 1, - ]); - - // Check that XSS payload is not reflected in response - if ($response->isSuccessful()) { - $responseData = $response->json(); - $this->assertStringNotContainsString('