diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 000000000..a2d6e26e2 --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,116 @@ +name: Unit Tests + +on: + push: + paths: + - 'app/**/*.php' + - 'tests/**/*.php' + - '.github/workflows/unit-tests.yml' + pull_request: + paths: + - 'app/**/*.php' + - 'tests/**/*.php' + - '.github/workflows/unit-tests.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + test: + name: PHP ${{ matrix.php-version }} Unit Tests + runs-on: ubuntu-22.04 + + strategy: + fail-fast: false + matrix: + php-version: + - '8.1' + - '8.2' + - '8.3' + + services: + mysql: + image: mysql:8.0 + env: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: ospos_test + MYSQL_USER: ospos + MYSQL_PASSWORD: ospos + ports: + - 3306:3306 + options: >- + --health-cmd="mysqladmin ping --silent" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + extensions: intl, mysqli, pdo_mysql, mbstring, json, dom, xml + coverage: xdebug + + - name: Get composer cache directory + run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV + + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: ${{ env.COMPOSER_CACHE_FILES_DIR }} + key: ${{ runner.os }}-${{ matrix.php-version }}-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-${{ matrix.php-version }}- + ${{ runner.os }}- + + - name: Install dependencies + run: composer install --no-progress --ansi --no-interaction + + - name: Wait for MySQL + run: | + while ! mysqladmin ping -h"127.0.0.1" --silent; do + echo "Waiting for MySQL..." + sleep 1 + done + + - name: Setup test database + run: | + mysql -h 127.0.0.1 -u root -proot -e "CREATE DATABASE IF NOT EXISTS ospos_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" + mysql -h 127.0.0.1 -u root -proot -e "GRANT ALL PRIVILEGES ON ospos_test.* TO 'ospos'@'%' IDENTIFIED BY 'ospos';" + mysql -h 127.0.0.1 -u root -proot -e "FLUSH PRIVILEGES;" + + - name: Copy test environment config + run: | + if [ -f ".env.testing" ]; then + cp .env.testing .env + else + cp .env.example .env + fi + + - name: Run migrations + run: php spark migrate --all || true + + - name: Run unit tests + run: vendor/bin/phpunit --configuration tests/phpunit.xml --testsuite Helpers,Models,Controllers --colors=always --verbose + + - name: Generate test report + if: always() + run: | + vendor/bin/phpunit --configuration tests/phpunit.xml --testsuite Helpers,Models,Controllers --log-junit build/logs/junit.xml --coverage-clover build/logs/clover.xml || true + echo "Test run completed" + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-php-${{ matrix.php-version }} + path: build/logs/ + retention-days: 30 \ No newline at end of file diff --git a/tests/Controllers/ItemsCsvImportTest.php b/tests/Controllers/ItemsCsvImportTest.php new file mode 100644 index 000000000..b05416ab1 --- /dev/null +++ b/tests/Controllers/ItemsCsvImportTest.php @@ -0,0 +1,788 @@ +item = model(Item::class); + $this->item_quantity = model(Item_quantity::class); + $this->inventory = model(Inventory::class); + $this->item_taxes = model(Item_taxes::class); + $this->attribute = model(Attribute::class); + $this->stock_location = model(Stock_location::class); + $this->supplier = model(Supplier::class); + } + + protected function tearDown(): void + { + parent::tearDown(); + } + + public function testGenerateCsvHeaderBasic(): void + { + $stock_locations = ['Warehouse']; + $attributes = []; + + $csv = generate_import_items_csv($stock_locations, $attributes); + + $this->assertStringContainsString('Id,Barcode,"Item Name"', $csv); + $this->assertStringContainsString('Category,"Supplier ID"', $csv); + $this->assertStringContainsString('"Cost Price","Unit Price"', $csv); + $this->assertStringContainsString('"Tax 1 Name","Tax 1 Percent"', $csv); + $this->assertStringContainsString('"Tax 2 Name","Tax 2 Percent"', $csv); + $this->assertStringContainsString('"Reorder Level"', $csv); + $this->assertStringContainsString('Description,"Allow Alt Description"', $csv); + $this->assertStringContainsString('"Item has Serial Number"', $csv); + $this->assertStringContainsString('Image,HSN', $csv); + $this->assertStringContainsString('"location_Warehouse"', $csv); + $this->assertStringContainsString("\xEF\xBB\xBF", $csv); + } + + public function testGenerateCsvHeaderMultipleLocations(): void + { + $stock_locations = ['Warehouse', 'Store', 'Backroom']; + $attributes = []; + + $csv = generate_import_items_csv($stock_locations, $attributes); + + $this->assertStringContainsString('"location_Warehouse"', $csv); + $this->assertStringContainsString('"location_Store"', $csv); + $this->assertStringContainsString('"location_Backroom"', $csv); + } + + public function testGenerateCsvHeaderWithAttributes(): void + { + $stock_locations = ['Warehouse']; + $attributes = ['Color', 'Size', 'Weight']; + + $csv = generate_import_items_csv($stock_locations, $attributes); + + $this->assertStringContainsString('"attribute_Color"', $csv); + $this->assertStringContainsString('"attribute_Size"', $csv); + $this->assertStringContainsString('"attribute_Weight"', $csv); + } + + public function testGenerateStockLocationHeaders(): void + { + $locations = ['Warehouse', 'Store']; + + $headers = generate_stock_location_headers($locations); + + $this->assertEquals(',"location_Warehouse","location_Store"', $headers); + } + + public function testGenerateAttributeHeaders(): void + { + $attributes = ['Color', 'Size']; + + $headers = generate_attribute_headers($attributes); + + $this->assertEquals(',"attribute_Color","attribute_Size"', $headers); + } + + public function testGenerateAttributeHeadersRemovesNegativeOneIndex(): void + { + $attributes = [-1 => 'None', 'Color' => 'Color']; + unset($attributes[-1]); + + $headers = generate_attribute_headers($attributes); + + $this->assertStringContainsString('"attribute_Color"', $headers); + } + + public function testGetCsvFileBasic(): void + { + $csv_content = "Id,Barcode,\"Item Name\",Category,\"Supplier ID\",\"Cost Price\",\"Unit Price\",\"Tax 1 Name\",\"Tax 1 Percent\",\"Tax 2 Name\",\"Tax 2 Percent\",\"Reorder Level\",Description,\"Allow Alt Description\",\"Item has Serial Number\",Image,HSN\n"; + $csv_content .= ",ITEM001,Test Item,Electronics,1,10.00,15.00,,,,,5,Test Description,0,0,,HSN001\n"; + + $temp_file = tempnam(sys_get_temp_dir(), 'csv_test_'); + file_put_contents($temp_file, $csv_content); + + $rows = get_csv_file($temp_file); + + $this->assertCount(1, $rows); + $this->assertEquals('', $rows[0]['Id']); + $this->assertEquals('ITEM001', $rows[0]['Barcode']); + $this->assertEquals('Test Item', $rows[0]['Item Name']); + $this->assertEquals('Electronics', $rows[0]['Category']); + + unlink($temp_file); + } + + public function testGetCsvFileWithBom(): void + { + $bom = pack('CCC', 0xef, 0xbb, 0xbf); + $csv_content = $bom . "Id,\"Item Name\",Category\n"; + $csv_content .= "1,Test Item,Electronics\n"; + + $temp_file = tempnam(sys_get_temp_dir(), 'csv_test_bom_'); + file_put_contents($temp_file, $csv_content); + + $rows = get_csv_file($temp_file); + + $this->assertCount(1, $rows); + $this->assertEquals('1', $rows[0]['Id']); + $this->assertEquals('Test Item', $rows[0]['Item Name']); + + unlink($temp_file); + } + + public function testGetCsvFileMultipleRows(): void + { + $csv_content = "Id,\"Item Name\",Category\n"; + $csv_content .= "1,Item One,Cat A\n"; + $csv_content .= "2,Item Two,Cat B\n"; + $csv_content .= "3,Item Three,Cat C\n"; + + $temp_file = tempnam(sys_get_temp_dir(), 'csv_test_multi_'); + file_put_contents($temp_file, $csv_content); + + $rows = get_csv_file($temp_file); + + $this->assertCount(3, $rows); + $this->assertEquals('Item One', $rows[0]['Item Name']); + $this->assertEquals('Item Two', $rows[1]['Item Name']); + $this->assertEquals('Item Three', $rows[2]['Item Name']); + + unlink($temp_file); + } + + public function testBomExists(): void + { + $bom = pack('CCC', 0xef, 0xbb, 0xbf); + $content_with_bom = $bom . "test content"; + + $temp_file = tempnam(sys_get_temp_dir(), 'bom_test_'); + file_put_contents($temp_file, $content_with_bom); + + $handle = fopen($temp_file, 'r'); + $result = bom_exists($handle); + fclose($handle); + + $this->assertTrue($result); + unlink($temp_file); + } + + public function testBomNotExists(): void + { + $content_without_bom = "test content without BOM"; + + $temp_file = tempnam(sys_get_temp_dir(), 'no_bom_test_'); + file_put_contents($temp_file, $content_without_bom); + + $handle = fopen($temp_file, 'r'); + $result = bom_exists($handle); + fclose($handle); + + $this->assertFalse($result); + unlink($temp_file); + } + + public function testImportItemBasicFields(): void + { + $item_data = [ + 'item_id' => null, + 'name' => 'CSV Imported Item', + 'description' => 'Description from CSV', + 'category' => 'Electronics', + 'cost_price' => 10.50, + 'unit_price' => 25.99, + 'reorder_level' => 5, + 'supplier_id' => null, + 'item_number' => 'CSV-ITEM-001', + 'allow_alt_description' => 0, + 'is_serialized' => 0, + 'deleted' => 0 + ]; + + $item_id = $this->item->save_value($item_data); + + $this->assertIsInt($item_id); + $this->assertGreaterThan(0, $item_id); + + $saved_item = $this->item->get_info($item_id); + $this->assertEquals('CSV Imported Item', $saved_item->name); + $this->assertEquals('Description from CSV', $saved_item->description); + $this->assertEquals('Electronics', $saved_item->category); + $this->assertEquals(10.50, (float)$saved_item->cost_price); + $this->assertEquals(25.99, (float)$saved_item->unit_price); + } + + public function testImportItemWithQuantity(): void + { + $item_data = [ + 'item_id' => null, + 'name' => 'Item With Quantity', + 'category' => 'Test Category', + 'cost_price' => 5.00, + 'unit_price' => 10.00, + 'reorder_level' => 2, + 'deleted' => 0 + ]; + + $item_id = $this->item->save_value($item_data); + + $location_id = 1; + $quantity = 100; + + $item_quantity_data = [ + 'item_id' => $item_id, + 'location_id' => $location_id, + 'quantity' => $quantity + ]; + + $result = $this->item_quantity->save_value($item_quantity_data, $item_id, $location_id); + $this->assertTrue($result); + + $saved_quantity = $this->item_quantity->get_item_quantity($item_id, $location_id); + $this->assertEquals($quantity, $saved_quantity->quantity); + } + + public function testImportItemCreatesInventoryRecord(): void + { + $item_data = [ + 'item_id' => null, + 'name' => 'Item With Inventory', + 'category' => 'Test', + 'cost_price' => 5.00, + 'unit_price' => 10.00, + 'deleted' => 0 + ]; + + $item_id = $this->item->save_value($item_data); + + $inventory_data = [ + 'trans_inventory' => 50, + 'trans_items' => $item_id, + 'trans_location' => 1, + 'trans_comment' => 'CSV Import', + 'trans_user' => 1 + ]; + + $trans_id = $this->inventory->insert($inventory_data); + + $this->assertIsInt($trans_id); + $this->assertGreaterThan(0, $trans_id); + + $inventory_records = $this->inventory->get_inventory_data_for_item($item_id, 1); + $this->assertGreaterThanOrEqual(1, $inventory_records->getNumRows()); + } + + public function testImportItemWithTaxes(): void + { + $item_data = [ + 'item_id' => null, + 'name' => 'Taxable Item', + 'category' => 'Test', + 'cost_price' => 100.00, + 'unit_price' => 150.00, + 'deleted' => 0 + ]; + + $item_id = $this->item->save_value($item_data); + + $taxes_data = [ + ['name' => 'VAT', 'percent' => 20], + ['name' => 'GST', 'percent' => 10] + ]; + + $result = $this->item_taxes->save_value($taxes_data, $item_id); + $this->assertTrue($result); + + $saved_taxes = $this->item_taxes->get_info($item_id); + + $tax_names = array_column($saved_taxes, 'name'); + $this->assertContains('VAT', $tax_names); + $this->assertContains('GST', $tax_names); + } + + public function testImportMultipleItemsFromSimulatedCsv(): void + { + $csv_data = [ + [ + 'Id' => '', + 'Barcode' => 'ITEM-A', + 'Item Name' => 'First Item', + 'Category' => 'Category A', + 'Supplier ID' => '', + 'Cost Price' => '10.00', + 'Unit Price' => '20.00', + 'Tax 1 Name' => '', + 'Tax 1 Percent' => '', + 'Tax 2 Name' => '', + 'Tax 2 Percent' => '', + 'Reorder Level' => '5', + 'Description' => 'First item description', + 'Allow Alt Description' => '0', + 'Item has Serial Number' => '0', + 'Image' => '', + 'HSN' => '', + 'location_Warehouse' => '100' + ], + [ + 'Id' => '', + 'Barcode' => 'ITEM-B', + 'Item Name' => 'Second Item', + 'Category' => 'Category B', + 'Supplier ID' => '', + 'Cost Price' => '15.00', + 'Unit Price' => '30.00', + 'Tax 1 Name' => '', + 'Tax 1 Percent' => '', + 'Tax 2 Name' => '', + 'Tax 2 Percent' => '', + 'Reorder Level' => '10', + 'Description' => 'Second item description', + 'Allow Alt Description' => '0', + 'Item has Serial Number' => '0', + 'Image' => '', + 'HSN' => '', + 'location_Warehouse' => '50' + ] + ]; + + $imported_item_ids = []; + + foreach ($csv_data as $row) { + $item_data = [ + 'item_id' => (int)$row['Id'] ?: null, + 'name' => $row['Item Name'], + 'description' => $row['Description'], + 'category' => $row['Category'], + 'cost_price' => (float)$row['Cost Price'], + 'unit_price' => (float)$row['Unit Price'], + 'reorder_level' => (int)$row['Reorder Level'], + 'item_number' => $row['Barcode'] ?: null, + 'allow_alt_description' => empty($row['Allow Alt Description']) ? '0' : '1', + 'is_serialized' => empty($row['Item has Serial Number']) ? '0' : '1', + 'deleted' => false + ]; + + $item_id = $this->item->save_value($item_data); + $imported_item_ids[] = $item_id; + } + + $this->assertCount(2, $imported_item_ids); + + $item1 = $this->item->get_info($imported_item_ids[0]); + $this->assertEquals('First Item', $item1->name); + $this->assertEquals(10.00, (float)$item1->cost_price); + + $item2 = $this->item->get_info($imported_item_ids[1]); + $this->assertEquals('Second Item', $item2->name); + $this->assertEquals(15.00, (float)$item2->cost_price); + } + + public function testImportUpdateExistingItem(): void + { + $original_data = [ + 'item_id' => null, + 'name' => 'Original Name', + 'category' => 'Original Category', + 'cost_price' => 10.00, + 'unit_price' => 20.00, + 'deleted' => 0 + ]; + + $item_id = $this->item->save_value($original_data); + + $updated_data = [ + 'item_id' => $item_id, + 'name' => 'Updated Name', + 'category' => 'Updated Category', + 'cost_price' => 15.00, + 'unit_price' => 30.00, + 'description' => 'New description', + 'reorder_level' => 10, + 'deleted' => 0 + ]; + + $this->item->save_value($updated_data); + + $updated_item = $this->item->get_info($item_id); + $this->assertEquals('Updated Name', $updated_item->name); + $this->assertEquals('Updated Category', $updated_item->category); + $this->assertEquals(15.00, (float)$updated_item->cost_price); + $this->assertEquals(30.00, (float)$updated_item->unit_price); + } + + public function testImportItemWithAttributeText(): void + { + $item_data = [ + 'item_id' => null, + 'name' => 'Item With Attribute', + 'category' => 'Test', + 'cost_price' => 10.00, + 'unit_price' => 20.00, + 'deleted' => 0 + ]; + + $item_id = $this->item->save_value($item_data); + + $definition_data = [ + 'definition_name' => 'Color', + 'definition_type' => TEXT, + 'definition_flags' => 0, + 'deleted' => 0 + ]; + $definition_id = $this->attribute->saveDefinition($definition_data); + + $attribute_value = 'Red'; + $attribute_id = $this->attribute->saveAttributeValue( + $attribute_value, + $definition_id, + $item_id, + false, + TEXT + ); + + $this->assertNotFalse($attribute_id); + + $saved_value = $this->attribute->getAttributeValue($item_id, $definition_id); + $this->assertEquals('Red', $saved_value->attribute_value); + } + + public function testImportItemWithAttributeDropdown(): void + { + $item_data = [ + 'item_id' => null, + 'name' => 'Item With Dropdown', + 'category' => 'Test', + 'cost_price' => 10.00, + 'unit_price' => 20.00, + 'deleted' => 0 + ]; + + $item_id = $this->item->save_value($item_data); + + $definition_data = [ + 'definition_name' => 'Size', + 'definition_type' => DROPDOWN, + 'definition_flags' => 0, + 'deleted' => 0 + ]; + $definition_id = $this->attribute->saveDefinition($definition_data); + + $dropdown_values = ['Small', 'Medium', 'Large']; + foreach ($dropdown_values as $i => $value) { + $this->db->table('attribute_values')->insert([ + 'attribute_value' => $value, + 'definition_id' => $definition_id, + 'definition_type' => DROPDOWN, + 'attribute_group' => $i, + 'deleted' => 0 + ]); + } + + $attribute_value = 'Medium'; + $attribute_id = $this->attribute->saveAttributeValue( + $attribute_value, + $definition_id, + $item_id, + false, + DROPDOWN + ); + + $this->assertNotFalse($attribute_id); + + $saved_value = $this->attribute->getAttributeValue($item_id, $definition_id); + $this->assertEquals('Medium', $saved_value->attribute_value); + } + + public function testImportItemQuantityZero(): void + { + $item_data = [ + 'item_id' => null, + 'name' => 'Item Zero Quantity', + 'category' => 'Test', + 'cost_price' => 5.00, + 'unit_price' => 10.00, + 'deleted' => 0 + ]; + + $item_id = $this->item->save_value($item_data); + + $location_id = 1; + + $item_quantity_data = [ + 'item_id' => $item_id, + 'location_id' => $location_id, + 'quantity' => 0 + ]; + + $result = $this->item_quantity->save_value($item_quantity_data, $item_id, $location_id); + $this->assertTrue($result); + + $saved_quantity = $this->item_quantity->get_item_quantity($item_id, $location_id); + $this->assertEquals(0, (int)$saved_quantity->quantity); + } + + public function testImportItemWithNegativeReorderLevel(): void + { + $item_data = [ + 'item_id' => null, + 'name' => 'Item Negative Reorder', + 'category' => 'Test', + 'cost_price' => 5.00, + 'unit_price' => 10.00, + 'reorder_level' => -1, + 'deleted' => 0 + ]; + + $item_id = $this->item->save_value($item_data); + + $saved_item = $this->item->get_info($item_id); + $this->assertEquals(-1, (int)$saved_item->reorder_level); + } + + public function testImportItemWithHighPrecisionPrices(): void + { + $item_data = [ + 'item_id' => null, + 'name' => 'High Precision Item', + 'category' => 'Test', + 'cost_price' => 10.123456, + 'unit_price' => 25.876543, + 'deleted' => 0 + ]; + + $item_id = $this->item->save_value($item_data); + + $saved_item = $this->item->get_info($item_id); + $cost_diff = abs(10.123456 - (float)$saved_item->cost_price); + $price_diff = abs(25.876543 - (float)$saved_item->unit_price); + + $this->assertLessThan(0.001, $cost_diff, 'Cost price should maintain precision'); + $this->assertLessThan(0.001, $price_diff, 'Unit price should maintain precision'); + } + + public function testImportItemWithHsnCode(): void + { + $item_data = [ + 'item_id' => null, + 'name' => 'Item With HSN', + 'category' => 'Test', + 'cost_price' => 10.00, + 'unit_price' => 20.00, + 'hsn_code' => '8471', + 'deleted' => 0 + ]; + + $item_id = $this->item->save_value($item_data); + + $saved_item = $this->item->get_info($item_id); + $this->assertEquals('8471', $saved_item->hsn_code); + } + + public function testImportItemQuantityMultipleLocations(): void + { + $item_data = [ + 'item_id' => null, + 'name' => 'Item Multi Location', + 'category' => 'Test', + 'cost_price' => 10.00, + 'unit_price' => 20.00, + 'deleted' => 0 + ]; + + $item_id = $this->item->save_value($item_data); + + $quantities = [ + ['location_id' => 1, 'quantity' => 100], + ['location_id' => 2, 'quantity' => 50], + ['location_id' => 3, 'quantity' => 25] + ]; + + foreach ($quantities as $q) { + $result = $this->item_quantity->save_value( + ['item_id' => $item_id, 'location_id' => $q['location_id'], 'quantity' => $q['quantity']], + $item_id, + $q['location_id'] + ); + $this->assertTrue($result); + } + + foreach ($quantities as $q) { + $saved = $this->item_quantity->get_item_quantity($item_id, $q['location_id']); + $this->assertEquals($q['quantity'], (int)$saved->quantity, "Quantity at location {$q['location_id']} should match"); + } + } + + public function testCsvImportQuantityValidationNumeric(): void + { + $csv_data = [ + 'Id' => '', + 'Barcode' => 'VALID-ITEM', + 'Item Name' => 'Valid Item', + 'Category' => 'Test', + 'Cost Price' => '10.00', + 'Unit Price' => '20.00', + 'location_Warehouse' => '100' + ]; + + $this->assertTrue(is_numeric($csv_data['location_Warehouse'])); + $this->assertTrue(is_numeric($csv_data['Cost Price'])); + $this->assertTrue(is_numeric($csv_data['Unit Price'])); + } + + public function testCsvImportEmptyBarcodeAllowed(): void + { + $item_data = [ + 'item_id' => null, + 'name' => 'Item Without Barcode', + 'category' => 'Test', + 'cost_price' => 10.00, + 'unit_price' => 20.00, + 'item_number' => null, + 'deleted' => 0 + ]; + + $item_id = $this->item->save_value($item_data); + + $this->assertIsInt($item_id); + $this->assertGreaterThan(0, $item_id); + + $saved_item = $this->item->get_info($item_id); + $this->assertEquals('Item Without Barcode', $saved_item->name); + } + + public function testCsvImportItemExistsCheck(): void + { + $item_data = [ + 'item_id' => null, + 'name' => 'Existing Item', + 'category' => 'Test', + 'cost_price' => 10.00, + 'unit_price' => 20.00, + 'deleted' => 0 + ]; + + $item_id = $this->item->save_value($item_data); + + $exists = $this->item->exists($item_id); + $this->assertTrue($exists); + + $not_exists = $this->item->exists(999999); + $this->assertFalse($not_exists); + } + + public function testFullCsvImportFlowSimulated(): void + { + $csv_row = [ + 'Id' => '', + 'Barcode' => 'FULL-TEST-001', + 'Item Name' => 'Complete Test Item', + 'Category' => 'Electronics', + 'Supplier ID' => '', + 'Cost Price' => '50.00', + 'Unit Price' => '100.00', + 'Tax 1 Name' => 'VAT', + 'Tax 1 Percent' => '20', + 'Tax 2 Name' => '', + 'Tax 2 Percent' => '', + 'Reorder Level' => '10', + 'Description' => 'A complete test item for CSV import', + 'Allow Alt Description' => '1', + 'Item has Serial Number' => '0', + 'Image' => '', + 'HSN' => '84713020' + ]; + + $item_data = [ + 'item_id' => (int)$csv_row['Id'] ?: null, + 'name' => $csv_row['Item Name'], + 'description' => $csv_row['Description'], + 'category' => $csv_row['Category'], + 'cost_price' => (float)$csv_row['Cost Price'], + 'unit_price' => (float)$csv_row['Unit Price'], + 'reorder_level' => (int)$csv_row['Reorder Level'], + 'item_number' => $csv_row['Barcode'] ?: null, + 'allow_alt_description' => empty($csv_row['Allow Alt Description']) ? '0' : '1', + 'is_serialized' => empty($csv_row['Item has Serial Number']) ? '0' : '1', + 'hsn_code' => $csv_row['HSN'], + 'deleted' => 0 + ]; + + $item_id = $this->item->save_value($item_data); + + $taxes_data = []; + if (is_numeric($csv_row['Tax 1 Percent']) && $csv_row['Tax 1 Name'] !== '') { + $taxes_data[] = ['name' => $csv_row['Tax 1 Name'], 'percent' => $csv_row['Tax 1 Percent']]; + } + if (is_numeric($csv_row['Tax 2 Percent']) && $csv_row['Tax 2 Name'] !== '') { + $taxes_data[] = ['name' => $csv_row['Tax 2 Name'], 'percent' => $csv_row['Tax 2 Percent']]; + } + + if (!empty($taxes_data)) { + $this->item_taxes->save_value($taxes_data, $item_id); + } + + $location_id = 1; + $quantity = 75; + + $quantity_data = [ + 'item_id' => $item_id, + 'location_id' => $location_id, + 'quantity' => $quantity + ]; + $this->item_quantity->save_value($quantity_data, $item_id, $location_id); + + $inventory_data = [ + 'trans_inventory' => $quantity, + 'trans_items' => $item_id, + 'trans_location' => $location_id, + 'trans_comment' => 'CSV import quantity', + 'trans_user' => 1 + ]; + $this->inventory->insert($inventory_data); + + $saved_item = $this->item->get_info($item_id); + $this->assertEquals('Complete Test Item', $saved_item->name); + $this->assertEquals('Electronics', $saved_item->category); + $this->assertEquals(50.00, (float)$saved_item->cost_price); + $this->assertEquals(100.00, (float)$saved_item->unit_price); + $this->assertEquals('84713020', $saved_item->hsn_code); + + $saved_quantity = $this->item_quantity->get_item_quantity($item_id, $location_id); + $this->assertEquals($quantity, (int)$saved_quantity->quantity); + + $saved_taxes = $this->item_taxes->get_info($item_id); + $this->assertCount(1, $saved_taxes); + $this->assertEquals('VAT', $saved_taxes[0]['name']); + $this->assertEquals(20, (float)$saved_taxes[0]['percent']); + + $inventory_records = $this->inventory->get_inventory_data_for_item($item_id, $location_id); + $this->assertGreaterThanOrEqual(1, $inventory_records->getNumRows()); + } +} \ No newline at end of file