Files
sam-api/tests/Feature/Production/BendingLotPipelineTest.php
김보곤 c942788119 test: [orders] 테스트 인프라 정비 및 수주 API 테스트 추가
- TestCase 공통화: setUpAuthenticatedUser(), api(), assertApiSuccess(), assertApiPaginated()
- 기존 10개 테스트 파일의 중복 setUp 코드 → TestCase 상속으로 전환
- Factory 3개 추가: TenantFactory, ClientFactory, OrderFactory
- OrderApiTest 12개 테스트 신규 작성 (목록/생성/조회/수정/삭제/상태변경/인증)
- 발견: 빈 데이터로 수주 생성 가능 (FormRequest 검증 강화 필요)
2026-03-14 14:42:22 +09:00

277 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Tests\Feature\Production;
use App\DTOs\Production\DynamicBomEntry;
use App\Services\Production\PrefixResolver;
use App\Services\WorkOrderService;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
/**
* 절곡 자재투입 LOT 매핑 파이프라인 통합 테스트
*
* getMaterials() → dynamic_bom 우선 체크 → 세부품목 반환 → 자재투입 플로우 검증
*
* 실행 조건: Docker 환경 + 로컬 DB 접속 필요
*/
class BendingLotPipelineTest extends TestCase
{
private const TENANT_ID = 287;
private PrefixResolver $resolver;
protected function setUp(): void
{
parent::setUp();
$this->resolver = new PrefixResolver;
}
// ─────────────────────────────────────────────────
// PrefixResolver → items.id 조회 통합
// ─────────────────────────────────────────────────
/**
* BD-* 품목이 items 테이블에 실제 존재하는지 확인
*/
public function test_prefix_resolver_resolves_existing_bd_items(): void
{
$testCodes = [
'BD-RS-43', 'BD-RM-30', 'BD-RC-35', 'BD-RD-40',
'BD-SS-43', 'BD-SM-30', 'BD-SC-35', 'BD-SD-40',
'BD-BE-30', 'BD-BS-40', 'BD-LA-30',
'BD-CF-30', 'BD-CL-24', 'BD-CP-30', 'BD-CB-30',
'BD-GI-53', 'BD-GI-84',
'BD-XX-30', 'BD-YY-43', 'BD-HH-30',
];
$foundCount = 0;
$missingCodes = [];
foreach ($testCodes as $code) {
$id = $this->resolver->resolveItemId($code, self::TENANT_ID);
if ($id !== null) {
$foundCount++;
$this->assertGreaterThan(0, $id, "Item ID for {$code} must be positive");
} else {
$missingCodes[] = $code;
}
}
// Phase 0에서 전부 등록했으므로 모두 존재해야 함
$this->assertEmpty(
$missingCodes,
'Missing BD items: '.implode(', ', $missingCodes)
);
$this->assertCount(count($testCodes), array_diff($testCodes, $missingCodes));
}
/**
* resolveItemId 캐시 동작 확인
*/
public function test_resolve_item_id_uses_cache(): void
{
$code = 'BD-RS-43';
$id1 = $this->resolver->resolveItemId($code, self::TENANT_ID);
$id2 = $this->resolver->resolveItemId($code, self::TENANT_ID);
$this->assertNotNull($id1);
$this->assertSame($id1, $id2, 'Cached result should be identical');
}
// ─────────────────────────────────────────────────
// dynamic_bom 생성 → JSON 구조 검증
// ─────────────────────────────────────────────────
/**
* DynamicBomEntry 배열이 올바른 JSON 구조로 변환되는지 확인
*/
public function test_dynamic_bom_entries_produce_valid_json_structure(): void
{
$entries = [];
// 가이드레일 벽면형 KSS01 (SUS) 4300mm
$testCombinations = [
['finish', 'wall', 'KSS01', 4300, 'guideRail', 'SUS'],
['body', 'wall', 'KSS01', 4300, 'guideRail', 'EGI'],
['c_type', 'wall', 'KSS01', 4300, 'guideRail', 'EGI'],
['d_type', 'wall', 'KSS01', 4300, 'guideRail', 'EGI'],
['base', 'wall', 'KSS01', 4300, 'guideRail', 'EGI'],
];
foreach ($testCombinations as [$partType, $guideType, $productCode, $lengthMm, $category, $materialType]) {
$prefix = $this->resolver->resolveGuideRailPrefix($partType, $guideType, $productCode);
$itemCode = $this->resolver->buildItemCode($prefix, $lengthMm);
$this->assertNotNull($itemCode, "buildItemCode failed for {$prefix}/{$lengthMm}");
$itemId = $this->resolver->resolveItemId($itemCode, self::TENANT_ID);
if ($itemId === null) {
$this->markTestSkipped("Item {$itemCode} not found in DB — run Phase 0 first");
}
$entries[] = DynamicBomEntry::fromArray([
'child_item_id' => $itemId,
'child_item_code' => $itemCode,
'lot_prefix' => $prefix,
'part_type' => PrefixResolver::partTypeName($partType),
'category' => $category,
'material_type' => $materialType,
'length_mm' => $lengthMm,
'qty' => 1,
]);
}
$json = DynamicBomEntry::toArrayList($entries);
$this->assertCount(5, $json);
$this->assertEquals('BD-RS-43', $json[0]['child_item_code']);
$this->assertEquals('BD-RM-43', $json[1]['child_item_code']);
$this->assertEquals('BD-RC-43', $json[2]['child_item_code']);
$this->assertEquals('BD-RD-43', $json[3]['child_item_code']);
$this->assertEquals('BD-XX-43', $json[4]['child_item_code']);
// JSON 인코딩/디코딩 정합성
$encoded = json_encode($json, JSON_UNESCAPED_UNICODE);
$decoded = json_decode($encoded, true);
$this->assertEquals($json, $decoded, 'JSON round-trip should be identical');
}
// ─────────────────────────────────────────────────
// getMaterials dynamic_bom 우선 체크
// ─────────────────────────────────────────────────
/**
* work_order_items.options.dynamic_bom이 있는 경우
* getMaterials가 세부품목을 반환하는지 확인
*/
public function test_get_materials_returns_dynamic_bom_items(): void
{
// 절곡 작업지시 찾기 (dynamic_bom이 있는)
$woItem = DB::table('work_order_items')
->where('tenant_id', self::TENANT_ID)
->whereNotNull('options')
->whereRaw("JSON_EXTRACT(options, '$.dynamic_bom') IS NOT NULL")
->first();
if (! $woItem) {
$this->markTestSkipped('No work_order_items with dynamic_bom found — create a bending work order first');
}
$options = is_string($woItem->options) ? json_decode($woItem->options, true) : ($woItem->options ?? []);
$dynamicBom = $options['dynamic_bom'] ?? [];
$this->assertNotEmpty($dynamicBom, 'dynamic_bom should not be empty');
// dynamic_bom 각 항목 구조 검증
foreach ($dynamicBom as $entry) {
$this->assertArrayHasKey('child_item_id', $entry);
$this->assertArrayHasKey('child_item_code', $entry);
$this->assertArrayHasKey('lot_prefix', $entry);
$this->assertArrayHasKey('part_type', $entry);
$this->assertArrayHasKey('category', $entry);
$this->assertGreaterThan(0, $entry['child_item_id']);
$this->assertMatchesRegularExpression('/^BD-[A-Z]{2}-\d{2}$/', $entry['child_item_code']);
}
}
/**
* getMaterials API 응답에 work_order_item_id 필드가 포함되는지 확인
*/
public function test_get_materials_api_includes_work_order_item_id(): void
{
// 절곡 작업지시 찾기
$wo = DB::table('work_orders')
->where('tenant_id', self::TENANT_ID)
->whereExists(function ($query) {
$query->select(DB::raw(1))
->from('work_order_items')
->whereColumn('work_order_items.work_order_id', 'work_orders.id')
->whereRaw("JSON_EXTRACT(options, '$.dynamic_bom') IS NOT NULL");
})
->first();
if (! $wo) {
$this->markTestSkipped('No work order with dynamic_bom items found');
}
// WorkOrderService 직접 호출로 getMaterials 검증
$service = app(WorkOrderService::class);
$service->setContext(self::TENANT_ID, 1);
$materials = $service->getMaterials($wo->id);
// dynamic_bom 품목에는 work_order_item_id가 포함되어야 함
$dynamicBomMaterials = array_filter($materials, fn ($m) => isset($m['work_order_item_id']));
if (empty($dynamicBomMaterials)) {
$this->markTestSkipped('getMaterials returned no dynamic_bom materials');
}
foreach ($dynamicBomMaterials as $material) {
$this->assertArrayHasKey('work_order_item_id', $material);
$this->assertArrayHasKey('lot_prefix', $material);
$this->assertArrayHasKey('category', $material);
$this->assertGreaterThan(0, $material['work_order_item_id']);
}
}
// ─────────────────────────────────────────────────
// 전체 prefix × lengthCode 마스터 검증 (Phase 0 검증 재확인)
// ─────────────────────────────────────────────────
/**
* 19종 prefix × 해당 lengthCode 조합이 모두 items 테이블에 존재하는지 확인
*/
public function test_all_prefix_length_combinations_exist_in_items(): void
{
$standardLengths = [30, 35, 40, 43];
$boxLengths = [12, 24, 30, 35, 40, 41];
$prefixLengthMap = [
// 가이드레일 벽면형
'RS' => $standardLengths, 'RM' => array_merge($standardLengths, [24, 35]),
'RC' => array_merge($standardLengths, [24, 35]), 'RD' => array_merge($standardLengths, [24, 35]),
'RT' => [30, 43],
// 가이드레일 측면형
'SS' => [30, 35, 40, 43], 'SM' => [30, 35, 40, 43, 24],
'SC' => [30, 35, 40, 43, 24], 'SD' => [30, 35, 40, 43, 24],
'ST' => [43], 'SU' => [30, 35, 40, 43],
// 하단마감재
'BE' => [30, 40], 'BS' => [30, 35, 40, 43, 24],
'TS' => [40, 43],
'LA' => [30, 40],
// 셔터박스 (표준 길이: 43 제외 — 4300mm는 가이드레일 전용)
'CF' => $boxLengths, 'CL' => $boxLengths,
'CP' => $boxLengths, 'CB' => $boxLengths,
// 연기차단재
'GI' => [53, 54, 83, 84, 30, 35, 40],
// 공통
'XX' => array_merge($boxLengths, [43]), 'YY' => $standardLengths,
'HH' => [30, 40],
];
$missing = [];
foreach ($prefixLengthMap as $prefix => $codes) {
foreach ($codes as $code) {
$itemCode = "BD-{$prefix}-{$code}";
$exists = DB::table('items')
->where('tenant_id', self::TENANT_ID)
->where('code', $itemCode)
->whereNull('deleted_at')
->exists();
if (! $exists) {
$missing[] = $itemCode;
}
}
}
$this->assertEmpty(
$missing,
'Missing BD items in items table: '.implode(', ', $missing)
);
}
}