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'); } }