This file is a merged representation of the entire codebase, combined into a single document by Repomix.

================================================================
File Summary
================================================================

Purpose:
--------
This file contains a packed representation of the entire repository's contents.
It is designed to be easily consumable by AI systems for analysis, code review,
or other automated processes.

File Format:
------------
The content is organized as follows:
1. This summary section
2. Repository information
3. Directory structure
4. Multiple file entries, each consisting of:
  a. A separator line (================)
  b. The file path (File: path/to/file)
  c. Another separator line
  d. The full contents of the file
  e. A blank line

Usage Guidelines:
-----------------
- This file should be treated as read-only. Any changes should be made to the
  original repository files, not this packed version.
- When processing this file, use the file path to distinguish
  between different files in the repository.
- Be aware that this file may contain sensitive information. Handle it with
  the same level of security as you would the original repository.

Notes:
------
- Some files may have been excluded based on .gitignore rules and Repomix's configuration
- Binary files are not included in this packed representation. Please refer to the Repository Structure section for a complete list of file paths, including binary files
- Files matching patterns in .gitignore are excluded
- Files matching default ignore patterns are excluded

Additional Info:
----------------

================================================================
Directory Structure
================================================================
ai_docs/
  CONVENTIONS-PHP.md
src/
  Command/
    ImportDemoProductsCommand.php
    ImportProductsCsvCommand.php
    UseWebserviceDemoCredentialsCommand.php
  Controller/
    TopdataDemoDataAdminApiController.php
  DTO/
    CsvConfiguration.php
  Resources/
    app/
      administration/
        src/
          module/
            topdata-demo-data/
              page/
                topdata-demo-data-index/
                  index.js
                  topdata-demo-data-index.html.twig
              service/
                DemoDataApiService.ts
              snippet/
                de-DE.json
                en-GB.json
              index.js
          main.js
    config/
      config.xml
      routes.xml
      services.xml
    demo-data/
      demo-products.csv
  Service/
    DemoDataImportService.php
    ProductCsvReader.php
    ProductService.php
  TopdataDemoDataImporterSW6.php
.gitignore
.sw-zip-blacklist
CHANGELOG.md
composer.json
README.md
rector.md
rector.php

================================================================
Files
================================================================

================
File: ai_docs/CONVENTIONS-PHP.md
================
# PHP code style conventions

- the names of a private method should start with an underscore, e.g. `_myPrivateMethod()`
- each class should have a docblock explaining what the class does
- each method should have a docblock explaining what the method does, except setters and getters
- Do NOT add redundant PHPDoc tags to docblocks, e.g. `@return void` or `@param string $foo` without any additional information
- the signature of a method should have type hints for all parameters and the return type 
- when injecting services, use constructor property promotion, and use the `private readonly` visibility modifier for the injected service

================
File: src/Command/ImportDemoProductsCommand.php
================
<?php

declare(strict_types=1);

namespace Topdata\TopdataDemoDataImporterSW6\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Topdata\TopdataDemoDataImporterSW6\Service\DemoDataImportService;
use Topdata\TopdataFoundationSW6\Command\AbstractTopdataCommand;

/**
 * Command to import products from a CSV file into Shopware 6
 * 
 * 11/2024 created
 */
#[AsCommand(
    name: 'topdata:demo-data-importer:import-demo-products',
    description: 'Import demo products into the shop'
)]
class ImportDemoProductsCommand extends AbstractTopdataCommand
{
    public function __construct(
        private readonly DemoDataImportService $demoDataImportService
    )
    {
        parent::__construct();
    }

    public function execute(InputInterface $input, OutputInterface $output): int
    {
        $result = $this->demoDataImportService->installDemoData();

        dump($result);

        $this->cliStyle->success('Demo data imported successfully!');
        $this->cliStyle->writeln("Consider to run <info>topdata:connector:import</info> command to enrich the products with additional data.");

        $this->done();

        return Command::SUCCESS;
    }

}

================
File: src/Command/ImportProductsCsvCommand.php
================
<?php

declare(strict_types=1);

namespace Topdata\TopdataDemoDataImporterSW6\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Topdata\TopdataDemoDataImporterSW6\DTO\CsvConfiguration;
use Topdata\TopdataDemoDataImporterSW6\Service\ProductService;
use Topdata\TopdataFoundationSW6\Command\AbstractTopdataCommand;

/**
 * Command to import products from a CSV file into Shopware 6
 * 
 * 11/2024 moved from TopdataConnectorSW6::ProductsCommand to TopdataDemoDataImporterSW6::ImportProductsCsvCommand
 */
class ImportProductsCsvCommand extends AbstractTopdataCommand
{
    public function __construct(
        private readonly ProductService $productService
    )
    {
        parent::__construct();
    }

    /**
     * Configure the command options
     *
     * options for indexes of the columns in the CSV file:
     *     - Required options: file, name, number
     *     - Optional options: start, end, wsid, description, ean, mpn, brand, divider, trim
     */
    protected function configure(): void
    {
        $this
            ->setName('topdata:demo-data-importer:import-products-csv')
            ->setDescription('Import products from a CSV file')
            ->addOption('file', null, InputOption::VALUE_REQUIRED, 'Path to the CSV file')
            ->addOption('start', null, InputOption::VALUE_OPTIONAL, 'Start line number for import')
            ->addOption('end', null, InputOption::VALUE_OPTIONAL, 'End line number for import')
            ->addOption('name', null, InputOption::VALUE_REQUIRED, 'Column number for product name')
            ->addOption('number', null, InputOption::VALUE_REQUIRED, 'Column number for product number')
            ->addOption('wsid', null, InputOption::VALUE_OPTIONAL, 'Column number for Topdata webservice ID')
            ->addOption('description', null, InputOption::VALUE_OPTIONAL, 'Column number for product description')
            ->addOption('ean', null, InputOption::VALUE_OPTIONAL, 'Column number for EAN')
            ->addOption('mpn', null, InputOption::VALUE_OPTIONAL, 'Column number for MPN')
            ->addOption('brand', null, InputOption::VALUE_OPTIONAL, 'Column number for brand')
            ->addOption('divider', null, InputOption::VALUE_OPTIONAL, 'CSV column delimiter (default: ;)')
            ->addOption('trim', null, InputOption::VALUE_OPTIONAL, 'Character to trim from values (default: ")');
    }

    public function execute(InputInterface $input, OutputInterface $output): int
    {
        $file = $input->getOption('file');
        if (!$file) {
            echo "add file!\n";

            return Command::FAILURE;
        }

        echo $file . "\n";

        $columnMapping = [
            'number' => (int)$input->getOption('number'),
            'name' => (int)$input->getOption('name'),
            'wsid' => $input->getOption('wsid') ? (int)$input->getOption('wsid') : null,
            'description' => $input->getOption('description') ? (int)$input->getOption('description') : null,
            'ean' => $input->getOption('ean') ? (int)$input->getOption('ean') : null,
            'mpn' => $input->getOption('mpn') ? (int)$input->getOption('mpn') : null,
            'brand' => $input->getOption('brand') ? (int)$input->getOption('brand') : null,
        ];

        $csvConfig = new CsvConfiguration(
            $input->getOption('divider') ?? ';',
            $input->getOption('trim') ?? '"',
            (int)($input->getOption('start') ?? 1),
            $input->getOption('end') ? (int)$input->getOption('end') : null,
            $columnMapping
        );

        try {
            $products = $this->productService->parseProductsFromCsv($file, $csvConfig);
        } catch (\RuntimeException $e) {
            $this->cliStyle->error($e->getMessage());
            return 3;
        }

        $this->cliStyle->writeln('Products in file: ' . count($products));

        $products = $this->productService->clearExistingProductsByProductNumber($products);

        $this->cliStyle->writeln('Products not added yet: ' . count($products));

        if (count($products)) {
            $products = $this->productService->formProductsArray($products);
        } else {
            echo 'no products found';

            return 4;
        }
        $prods = array_chunk($products, 50);
        foreach ($prods as $key => $prods_chunk) {
            echo 'adding ' . ($key * 50 + count($prods_chunk)) . ' of ' . count($products) . " products...\n";
            $this->productService->createProducts($prods_chunk);
        }

        return 0;
    }

}

================
File: src/Command/UseWebserviceDemoCredentialsCommand.php
================
<?php

declare(strict_types=1);

namespace Topdata\TopdataDemoDataImporterSW6\Command;

use Doctrine\DBAL\Connection;
use Shopware\Core\Defaults;
use Shopware\Core\Framework\Uuid\Uuid;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Topdata\TopdataFoundationSW6\Command\AbstractTopdataCommand;
use Topdata\TopdataFoundationSW6\Service\PluginHelperService;

/**
 * 11/2024 created
 */
#[AsCommand(
    name: 'topdata:demo-data-importer:use-webservice-demo-credentials',
    description: 'Use the demo credentials for the Topdata webservice. It updates the system configuration with the demo credentials.',
)]
class UseWebserviceDemoCredentialsCommand extends AbstractTopdataCommand
{


    public function __construct(
        private readonly Connection          $connection,
        private readonly PluginHelperService $pluginHelperService
    )
    {
        parent::__construct();
    }

    private function _deleteExistingCredentials(): void
    {
        $this->connection->executeStatement(
            'DELETE FROM system_config 
            WHERE configuration_key IN (
                :username,
                :apiKey,
                :apiSalt
            )', [
            'username' => 'TopdataConnectorSW6.config.apiUid',
            'apiKey'   => 'TopdataConnectorSW6.config.apiKey',
            'apiSalt'  => 'TopdataConnectorSW6.config.apiSalt',
        ]);
    }

    private function _insertCredentials(): void
    {
        $now = (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT);

        //    * API User ID: 6
        //    * API Password: nTI9kbsniVWT13Ns
        //    * API Security Key: oateouq974fpby5t6ldf8glzo85mr9t6aebozrox

        $configs = [
            [
                'id'                  => Uuid::randomBytes(),
                'configuration_key'   => 'TopdataConnectorSW6.config.apiUid',
                'configuration_value' => json_encode(['_value' => '6']),
                'created_at'          => $now
            ],
            [
                'id'                  => Uuid::randomBytes(),
                'configuration_key'   => 'TopdataConnectorSW6.config.apiPassword',
                'configuration_value' => json_encode(['_value' => 'nTI9kbsniVWT13Ns']),
                'created_at'          => $now
            ],
            [
                'id'                  => Uuid::randomBytes(),
                'configuration_key'   => 'TopdataConnectorSW6.config.apiSecurityKey',
                'configuration_value' => json_encode(['_value' => 'oateouq974fpby5t6ldf8glzo85mr9t6aebozrox']),
                'created_at'          => $now
            ],
        ];

        foreach ($configs as $config) {
            $this->connection->insert('system_config', $config);
        }
    }

    protected function configure(): void
    {
        $this->addOption('force', 'f', InputOption::VALUE_NONE, 'Force overriding of credentials that already exist in the database');
    }

    private function _doCredentialsExist(): bool
    {

        // Check if any of the configs exist
        $existingCount = $this->connection->fetchOne(
            'SELECT COUNT(*) FROM system_config 
                WHERE configuration_key IN (
                    :username,
                    :apiKey,
                    :apiSalt
                )',
            [
                'username' => 'TopdataConnectorSW6.config.apiUid',
                'apiKey'   => 'TopdataConnectorSW6.config.apiKey',
                'apiSalt'  => 'TopdataConnectorSW6.config.apiSalt',
            ]
        );

        return $existingCount > 0;
    }


    public function execute(InputInterface $input, OutputInterface $output): int
    {
        $force = (bool)$input->getOption('force');

        // ---- check if the plugin is installed
        if (!$this->pluginHelperService->isWebserviceConnectorPluginAvailable()) {
            $this->cliStyle->error('The Topdata Webservice Connector plugin is not installed.');
            return Command::FAILURE;
        }

        // ---- check if credentials already exist
        if ($this->_doCredentialsExist()) {
            if (!$force) {
                $this->cliStyle->error('Credentials already exist. Use --force to override.');
                return Command::FAILURE;
            }
            $this->_deleteExistingCredentials();
        }

        $this->_insertCredentials();

        $this->cliStyle->success('Credentials set');

        return Command::SUCCESS;
    }

}

================
File: src/Controller/TopdataDemoDataAdminApiController.php
================
<?php

declare(strict_types=1);

namespace Topdata\TopdataDemoDataImporterSW6\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;
use Topdata\TopdataDemoDataImporterSW6\Service\DemoDataImportService;

/**
 * 11/2024 extracted from TopdataWebserviceConnectorAdminApiController
 */
#[Route(
    path: '/api',
    defaults: ['_routeScope' => ['administration']],
)]
class TopdataDemoDataAdminApiController extends AbstractController
{

    public function __construct(
        private readonly DemoDataImportService $demoDataImportService,
    )
    {
    }


    /**
     * Install demo data.
     */
    #[Route(
        path: '/topdata-demo-data/install-demodata',
        methods: ['GET'] // TODO: use POST
    )]
    public function installDemoData(): JsonResponse
    {
        $result = $this->demoDataImportService->installDemoData();

        return new JsonResponse($result);
    }
}

================
File: src/DTO/CsvConfiguration.php
================
<?php

namespace Topdata\TopdataDemoDataImporterSW6\DTO;

/**
 * Configuration for CSV import
 */
readonly class CsvConfiguration
{

    /**
     * @param array<string, int|null> $columnMapping
     */
    public function __construct(
        private readonly string $delimiter,
        private readonly string $enclosure,
        private readonly int    $startLine,
        private readonly ?int   $endLine,
        private readonly array  $columnMapping
    )
    {
    }

    public function getDelimiter(): string
    {
        return $this->delimiter;
    }

    public function getEnclosure(): string
    {
        return $this->enclosure;
    }

    public function getStartLine(): int
    {
        return $this->startLine;
    }

    public function getEndLine(): ?int
    {
        return $this->endLine;
    }

    public function getColumnMapping(): array
    {
        return $this->columnMapping;
    }
}

================
File: src/Resources/app/administration/src/module/topdata-demo-data/page/topdata-demo-data-index/index.js
================
/**
 * Main component for the demo data import page
 * Handles the AJAX call to import demo data and displays notifications
 */

import template from './topdata-demo-data-index.html.twig';

Shopware.Component.register('topdata-demo-data-index', {
    template,

    inject: ['TopdataDemoDataApiService'],

    data() {
        return {
            isLoading: false
        };
    },

    methods: {
        /**
         * Triggers the demo data import via AJAX
         * Shows loading state and handles success/error notifications
         */
        importDemoData() {
            this.isLoading = true;

            // Use the demo data service to import demo data
            this.TopdataDemoDataApiService.installDemoData().finally(() => {
                this.isLoading = false;
            });
        }
    }
});

================
File: src/Resources/app/administration/src/module/topdata-demo-data/page/topdata-demo-data-index/topdata-demo-data-index.html.twig
================
{# Template for the demo data import page #}
{# Uses Shopware's UI components for consistent styling #}
<sw-page class="topdata-demo-data-index">
    <template #content>
        <sw-card-view>
            {# Main card containing the import button #}
            <sw-card
                :isLoading="isLoading"
                :large="true">
                {# Primary action button that triggers the import #}
                <sw-button
                    variant="primary"
                    size="large"
                    @click="importDemoData">
                    {{ $tc('topdata-demo-data.general.importButton') }}
                </sw-button>
            </sw-card>
        </sw-card-view>
    </template>
</sw-page>

================
File: src/Resources/app/administration/src/module/topdata-demo-data/service/DemoDataApiService.ts
================
import TopdataAdminApiClient from "../../../../../../../../../topdata-foundation-sw6/src/Resources/app/administration/src/service/TopdataAdminApiClient";

/**
 * Fix for "TS2304: Cannot find name Shopware"
 * TODO: check https://developer.shopware.com/docs/guides/plugins/plugins/administration/the-shopware-object.html
 */
declare var Shopware: any;

/**
 * Service for handling demo data operations in the admin interface
 */
export default class DemoDataApiService {
    private client: TopdataAdminApiClient;

    constructor() {
        this.client = Shopware.Service().get('TopdataAdminApiClient')
    }

    /**
     * Installs demo data via the API.
     * @returns {Promise} - A promise that resolves with the API response.
     */
    installDemoData(): Promise<any> {
        return this.client.get('/topdata-demo-data/install-demodata');
    }
}

================
File: src/Resources/app/administration/src/module/topdata-demo-data/snippet/de-DE.json
================
{
    "topdata-demo-data": {
        "general": {
            "mainMenuTitle": "Topdata Demo Daten",
            "descriptionTextModule": "Demo Daten importieren",
            "importButton": "Demo Daten importieren",
            "successTitle": "Erfolg",
            "successMessage": "Demo Daten wurden erfolgreich importiert",
            "errorTitle": "Fehler",
            "errorMessage": "Beim Importieren der Demo Daten ist ein Fehler aufgetreten"
        }
    }
}

================
File: src/Resources/app/administration/src/module/topdata-demo-data/snippet/en-GB.json
================
{
    "topdata-demo-data": {
        "general": {
            "mainMenuTitle": "Topdata Demo Data",
            "descriptionTextModule": "Import demo data",
            "importButton": "Import Demo Data",
            "successTitle": "Success",
            "successMessage": "Demo data has been imported successfully",
            "errorTitle": "Error",
            "errorMessage": "An error occurred while importing demo data"
        }
    }
}

================
File: src/Resources/app/administration/src/module/topdata-demo-data/index.js
================
/**
 * This module registers the Topdata Demo Data administration interface
 * It creates a new menu entry and defines the route for the demo data import page
 */

import './page/topdata-demo-data-index';
import DemoDataApiService from './service/DemoDataApiService';

Shopware.Module.register('topdata-demo-data', {
    type: 'plugin',
    name: 'Topdata Demo Data',
    title: 'topdata-demo-data.general.mainMenuTitle',
    description: 'topdata-demo-data.general.descriptionTextModule',
    color: '#ff3d58',

    // Define the available routes for this module
    routes: {
        index: {
            component: 'topdata-demo-data-index',
            path: 'index'
        }
    },

    // Configure the menu entry in the administration
    navigation: [{
        label: 'topdata-demo-data.general.mainMenuTitle',
        color: '#ff3d58',
        path: 'topdata.demo.data.index',
        icon: 'regular-database',
        position: 100,
        parent: 'sw-content'
    }]
});

// Register the demo data service
Shopware.Service().register('TopdataDemoDataApiService', () => {
    return new DemoDataApiService();
});

================
File: src/Resources/app/administration/src/main.js
================
// Import the demo data module
import './module/topdata-demo-data';

================
File: src/Resources/config/config.xml
================
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/shopware/platform/trunk/src/Core/System/SystemConfig/Schema/config.xsd">
    <card>
        <title>Basic Configuration</title>
        <title lang="de-DE">Grundeinstellungen</title>
        
        <input-field>
            <name>example</name>
            <label>Example Configuration</label>
            <label lang="de-DE">Beispiel Konfiguration</label>
        </input-field>
    </card>
</config>

================
File: src/Resources/config/routes.xml
================
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
        https://symfony.com/schema/routing/routing-1.0.xsd">

    <import resource="../../Controller/**/*Controller.php" type="attribute"/>

</routes>

================
File: src/Resources/config/services.xml
================
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>


        <!-- SERVICES -->
        <service id="Topdata\TopdataDemoDataImporterSW6\Service\ProductService" autowire="true"/>
        <service id="Topdata\TopdataDemoDataImporterSW6\Service\ProductCsvReader" autowire="true"/>
        <service id="Topdata\TopdataDemoDataImporterSW6\Service\DemoDataImportService" autowire="true"/>


        <!-- COMMANDS -->
        <service id="Topdata\TopdataDemoDataImporterSW6\Command\ImportProductsCsvCommand" autowire="true">
            <tag name="console.command"/>
        </service>

        <service id="Topdata\TopdataDemoDataImporterSW6\Command\ImportDemoProductsCommand" autowire="true">
            <tag name="console.command"/>
        </service>

        <service id="Topdata\TopdataDemoDataImporterSW6\Command\UseWebserviceDemoCredentialsCommand" autowire="true">
            <tag name="console.command"/>
        </service>


        <!-- CONTROLLERS -->
        <service id="Topdata\TopdataDemoDataImporterSW6\Controller\TopdataDemoDataAdminApiController" public="true" autowire="true">
            <call method="setContainer">
                <argument type="service" id="service_container"/>
            </call>
        </service>

    </services>
</container>

================
File: src/Resources/demo-data/demo-products.csv
================
article_no;short_desc;ean;oem
855328;Epson Tintenpatrone cyan SC (C13T12924010, T1292) ;8715946465494;C13T12924010
855330;Epson Tintenpatrone gelb SC (C13T12944010, T1294) ;8715946465555;C13T12944010
855329;Epson Tintenpatrone magenta SC (C13T12934010, T1293) ;8715946465524;C13T12934010
855327;Epson Tintenpatrone schwarz SC (C13T12914010, T1291) ;8715946465487;C13T12914010
855340;HP Tintendruckkopf schwarz HC (CH563EE, 301XL) ;884962894446;CH563EE
855339;HP Tintendruckkopf cyan/gelb/magenta HC (CH564EE, 301XL) ;884962894545;CH564EE
220930;Canon Tintenpatrone schwarz (4479A002, BCI-3EBK) ;4960999865300;4479A002
344900;Canon Tintenpatrone magenta (4707A002, BCI-6M) ;4960999864792;4707A002
344850;Canon Tintenpatrone gelb (4708A002, BCI-6Y) ;8714574960388;4708A002
344890;Canon Tintenpatrone cyan (4706A002, BCI-6C) ;8714574960562;4706A002
951589;Samsung Fixiereinheit (JC91-01080A) ;5711045606090;JC91-01080A
895788;Samsung Toner-Kit gelb, cyan, magenta, schwarz (CLT-P406C, P406C) ;8806085816572;CLT-P406C
878615;Samsung Toner-Kit schwarz (CLT-K406S, K406) ;8806085023642;CLT-K406S
878616;Samsung Toner-Kit magenta (CLT-M406S, M406) ;8806085023994;CLT-M406S
878617;Samsung Toner-Kit gelb (CLT-Y406S, Y406) ;8806085023635;CLT-Y406S
878614;Samsung Toner-Kit cyan (CLT-C406S, C406) ;8806085023871;CLT-C406S

================
File: src/Service/DemoDataImportService.php
================
<?php

declare(strict_types=1);

namespace Topdata\TopdataDemoDataImporterSW6\Service;

use Topdata\TopdataDemoDataImporterSW6\Service\ProductService;

/**
 * 10/2024 created (extracted from ProductService)
 */
class DemoDataImportService
{
    private ?int $columnNumber = null;
    private ?int $columnName = null;
    private ?int $columnEAN = null;
    private ?int $columnMPN = null;
    private string $divider;
    private string $trim;

    public function __construct(
        private readonly ProductService $productService
    )
    {
    }

    /**
     * 10/2024 extracted from ProductService
     *
     * Install demo data from file
     *
     * @param string $filename
     * @return array
     */
    public function installDemoData(string $filename = 'demo-products.csv'): array
    {
        $this->divider = ';';
        $this->trim = '"';
        if (!$filename) {
            return [
                'success'        => false,
                'additionalInfo' => 'file with demo data not found!',
            ];
        }

        $file = __DIR__ . '/../Resources/demo-data/' . $filename;
        $handle = fopen($file, 'r');
        if (!$handle) {
            return [
                'success'        => false,
                'additionalInfo' => 'file with demo not accessible!',
            ];
        }

        $line = fgets($handle);
        if ($line === false) {
            return [
                'success'        => false,
                'additionalInfo' => 'file is empty!',
            ];
        }

        $values = explode($this->divider, $line);
        foreach ($values as $key => $val) {
            $val = trim($val);
            if ($val === 'article_no') {
                $this->columnNumber = $key;
            }
            if ($val === 'short_desc') {
                $this->columnName = $key;
            }
            if ($val === 'ean') {
                $this->columnEAN = $key;
            }
            if ($val === 'oem') {
                $this->columnMPN = $key;
            }
        }

        if (is_null($this->columnNumber)) {
            return [
                'success'        => false,
                'additionalInfo' => 'article_no column not exists!',
            ];
        }

        if (is_null($this->columnName)) {
            return [
                'success'        => false,
                'additionalInfo' => 'short_desc column not exists!',
            ];
        }

        if (is_null($this->columnEAN)) {
            return [
                'success'        => false,
                'additionalInfo' => 'ean column not exists!',
            ];
        }

        if (is_null($this->columnMPN)) {
            return [
                'success'        => false,
                'additionalInfo' => 'oem column not exists!',
            ];
        }

        $products = [];

        while (($line = fgets($handle)) !== false) {
            $values = explode($this->divider, $line);
            foreach ($values as $key => $val) {
                $values[$key] = trim($val, $this->trim);
            }
            $products[$values[$this->columnNumber]] = [
                'productNumber' => $values[$this->columnNumber],
                'name'          => $values[$this->columnName],
                'ean'           => $values[$this->columnEAN],
                'mpn'           => $values[$this->columnMPN],
            ];
        }

        fclose($handle);

        $products = $this->productService->clearExistingProductsByProductNumber($products);
        if (count($products)) {
            $products = $this->productService->formProductsArray($products, 100000.0);
        } else {
            return [
                'success'        => true,
                'additionalInfo' => 'Nothing to add',
            ];
        }

        $this->productService->createProducts($products);

        return [
            'success'        => true,
            'additionalInfo' => count($products) . ' products has been added',
        ];
    }
}

================
File: src/Service/ProductCsvReader.php
================
<?php

declare(strict_types=1);

namespace Topdata\TopdataDemoDataImporterSW6\Service;

use RuntimeException;
use Topdata\TopdataDemoDataImporterSW6\DTO\CsvConfiguration;

/**
 * Service for reading products from CSV files
 * 11/2024 created
 */
class ProductCsvReader
{
    /**
     * Read products from a CSV file
     *
     * @throws RuntimeException if file cannot be read or is invalid
     */
    public function readProducts(string $filePath, CsvConfiguration $config): array
    {
        if (!file_exists($filePath)) {
            throw new RuntimeException('File not found: ' . $filePath);
        }

        $handle = fopen($filePath, 'r');
        if (!$handle) {
            throw new RuntimeException('Could not open file: ' . $filePath);
        }

        try {
            return $this->processFile($handle, $config);
        } finally {
            fclose($handle);
        }
    }

    /**
     * @throws RuntimeException if required columns are missing
     */
    private function processFile($handle, CsvConfiguration $config): array
    {
        $products = [];
        $lineNumber = 0;
        $mapping = $config->getColumnMapping();

        while (($line = fgets($handle)) !== false) {
            $lineNumber++;

            if ($lineNumber < $config->getStartLine()) {
                continue;
            }

            if ($config->getEndLine() !== null && $lineNumber > $config->getEndLine()) {
                break;
            }

            $values = array_map(
                fn($val) => trim($val, $config->getEnclosure()),
                explode($config->getDelimiter(), $line)
            );

            if (!isset($values[$mapping['number']]) || !isset($values[$mapping['name']])) {
                continue;
            }

            $products[$values[$mapping['number']]] = $this->mapRowToProduct($values, $mapping);
        }

        return $products;
    }

    private function mapRowToProduct(array $values, array $mapping): array
    {
        $product = [
            'productNumber' => $values[$mapping['number']],
            'name'          => $values[$mapping['name']],
        ];

        // Optional fields
        $optionalFields = [
            'wsid'        => 'topDataId',
            'description' => 'description',
            'ean'         => 'ean',
            'mpn'         => 'mpn',
            'brand'       => 'brand'
        ];

        foreach ($optionalFields as $csvField => $productField) {
            if (isset($mapping[$csvField]) && isset($values[$mapping[$csvField]])) {
                $product[$productField] = $values[$mapping[$csvField]];
            }
        }

        return $product;
    }
}

================
File: src/Service/ProductService.php
================
<?php

declare(strict_types=1);

namespace Topdata\TopdataDemoDataImporterSW6\Service;

use Doctrine\DBAL\Connection;
use Shopware\Core\Content\Product\Aggregate\ProductVisibility\ProductVisibilityDefinition;
use Shopware\Core\Defaults;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
use Shopware\Core\Framework\Uuid\Uuid;
use Topdata\TopdataDemoDataImporterSW6\DTO\CsvConfiguration;
use Topdata\TopdataFoundationSW6\Service\LocaleHelperService;

/**
 * 10/2024 created (extracted from "ProductsCommand")
 * 11/2024 moved from TopdataConnectorSW6 to TopdataDemoDataImporterSW6
 */
class ProductService
{
    private string $systemDefaultLocaleCode;
    private Context $context;


    public function __construct(
        private readonly EntityRepository    $productRepository,
        private readonly EntityRepository    $productManufacturerRepository,
        private readonly Connection          $connection,
        private readonly ProductCsvReader    $productCsvReader,
        private readonly LocaleHelperService $localeHelperService,
    )
    {
        $this->context = Context::createDefaultContext();
        $this->systemDefaultLocaleCode = $this->localeHelperService->getLocaleCodeOfSystemLanguage();
    }


    private function getTaxId(): string
    {
        $result = $this->connection->executeQuery('
            SELECT LOWER(HEX(COALESCE(
                (SELECT `id` FROM `tax` WHERE tax_rate = "19.00" LIMIT 1),
                (SELECT `id` FROM `tax`  LIMIT 1)
            )))
        ')->fetchOne();

        if (!$result) {
            throw new \RuntimeException('No tax found, please make sure that basic data is available by running the migrations.');
        }

        return (string)$result;
    }

    private function getStorefrontSalesChannel(): string
    {
        $result = $this->connection->executeQuery('
            SELECT LOWER(HEX(`id`))
            FROM `sales_channel`
            WHERE `type_id` = 0x' . Defaults::SALES_CHANNEL_TYPE_STOREFRONT . '
            ORDER BY `created_at` ASC            
        ')->fetchOne();

        if (!$result) {
            throw new \RuntimeException('No sale channel found.');
        }

        return (string)$result;
    }

    public function parseProductsFromCsv(string $filePath, CsvConfiguration $config): array
    {
        return $this->productCsvReader->readProducts($filePath, $config);
    }

    public function formProductsArray(array $input, float $price = 1.0): array
    {
        $output = [];
        $taxId = $this->getTaxId();
        $storefrontSalesChannel = $this->getStorefrontSalesChannel();
        $priceTax = $price * (1.19);

        foreach ($input as $in) {
            $prod = [
                'id'               => Uuid::randomHex(),
                'productNumber'    => $in['productNumber'],
                'active'           => true,
                'taxId'            => $taxId,
                'stock'            => 10,
                'shippingFree'     => false,
                'purchasePrice'    => $priceTax,
                'displayInListing' => true,
                'name'             => [
                    $this->systemDefaultLocaleCode => $in['name'],
                ],
                'price'            => [[
                    'net'        => $price,
                    'gross'      => $priceTax,
                    'linked'     => true,
                    'currencyId' => Defaults::CURRENCY,
                ]],
                'visibilities'     => [
                    [
                        'salesChannelId' => $storefrontSalesChannel,
                        'visibility'     => ProductVisibilityDefinition::VISIBILITY_ALL,
                    ],
                ],
            ];

            if (isset($in['description'])) {
                $prod['description'] = [
                    $this->systemDefaultLocaleCode => $in['description'],
                ];
            }

            if (isset($in['brand'])) {
                $prod['manufacturer'] = [
                    'id' => $this->getManufacturerIdByName($in['brand']),
                ];
            }

            if (isset($in['mpn'])) {
                $prod['manufacturerNumber'] = $in['mpn'];
            }

            if (isset($in['ean'])) {
                $prod['ean'] = $in['ean'];
            }

            if (isset($in['topDataId'])) {
                $prod['topdata'] = [
                    'topDataId' => $in['topDataId'],
                ];
            }

            $output[] = $prod;
        }

        return $output;
    }

    public function createProducts(array $products): void
    {
        $this->productRepository->create($products, $this->context);
    }

    public function clearExistingProductsByProductNumber(array $products): array
    {
        $rezProducts = $products;
        $product_arrays = array_chunk($products, 50, true);
        foreach ($product_arrays as $prods) {
            $criteria = new Criteria();
            $criteria->addFilter(new EqualsAnyFilter('productNumber', array_keys($prods)));
            $foundedProducts = $this->productRepository->search($criteria, $this->context)->getEntities();
            foreach ($foundedProducts as $foundedProd) {
                unset($rezProducts[$foundedProd->getProductNumber()]);
            }
        }

        return $rezProducts;
    }


}

================
File: src/TopdataDemoDataImporterSW6.php
================
<?php declare(strict_types=1);

namespace Topdata\TopdataDemoDataImporterSW6;

use Shopware\Core\Framework\Plugin;

class TopdataDemoDataImporterSW6 extends Plugin
{
}

================
File: .gitignore
================
.aider*
.env
builds/
vendor/
.rector-cache



# Ignore all generated asset directories in plugins
src/Resources/public/administration/assets
src/Resources/public/storefront/assets
.vite


repomix-output.txt

================
File: .sw-zip-blacklist
================
# vendor/* or vendor/ does not work with python's
vendor
composer.lock

================
File: CHANGELOG.md
================
# Changelog

All notable changes to this project will be documented in this file. 
The format is based on [Keep a Changelog](https://keepachangelog.com/) and this project adheres to [Semantic Versioning](https://semver.org/).

## [7.0.2] - 2024-11-05
### Added
- success message added to the import demo data command, hinting at topdata:connector:import command


## [7.0.1] - 2024-11-05
### Fixed
- icon fixed
### Added
- CHANGELOG.md added


## [7.0.0] - 2024-11-04
### Initial Release
- initial release, extracted from topdata-webservice-connector-sw6 with dependency on topdata-foundation-sw6

================
File: composer.json
================
{
    "name": "topdata/topdata-demo-data-importer",
    "description": "With this plugin you can import demo data",
    "version": "7.0.10",
    "type": "shopware-platform-plugin",
    "license": "MIT",
    "authors": [
        {
            "name": "TopData Software GmbH",
            "homepage": "https://www.topdata.de",
            "role": "Manufacturer"
        }
    ],
    "extra": {
        "shopware-plugin-class": "Topdata\\TopdataDemoDataImporterSW6\\TopdataDemoDataImporterSW6",
        "plugin-icon": "src/Resources/config/topdata-demo-data-importer-sw6-256x256.png",
        "copyright": "(c) by TopData Software GmbH",
        "label": {
            "de-DE": "Topdata Demo Data Importer",
            "en-GB": "Topdata Demo Data Importer"
        },
        "description": {
            "de-DE": "Mit diesem Plugin können Sie Demo-Daten importieren",
            "en-GB": "With this plugin you can import demo data"
        },
        "manufacturerLink": {
            "de-DE": "https://www.topdata.de",
            "en-GB": "https://www.topdata.de"
        }
    },
    "autoload": {
        "psr-4": {
            "Topdata\\TopdataDemoDataImporterSW6\\": "src/"
        }
    },
    "require": {
        "shopware/core": "6.5.* || 6.6.* || 6.7.*",
        "topdata/topdata-foundation-sw6": "^1.0"
    },
    "require-dev": {
        "rector/rector": "^1.2"
    }
}

================
File: README.md
================
# With this plugin you can import demo data

## Installation

1. Download the plugin
2. Upload to your Shopware 6 installation
3. Install and activate the plugin

## Requirements

- Shopware 6.5.* or 6.6.*


## Commands

### topdata:demo-data-importer:import-demo-products
- imports demo products from the bundled demo-products.csv file
- example usage:

```bash
bin/console topdata:demo-data-importer:import-demo-products
```

### topdata:demo-data-importer:use-webservice-demo-credentials
- sets up demo credentials for the Topdata webservice
- example usage:

```bash
bin/console topdata:demo-data-importer:use-webservice-demo-credentials
```

### topdata:demo-data-importer:import-products-csv
- a console command for import products from csv file
- example usage:

```bash
bin/console topdata:demo-data-importer:import-products-csv --file=prods2020-07-26.csv --start=1 --end=1000 --number=4 --wsid=4 --name=11 --brand=10
```

`--file`  specify filename

`--start`  start line of a file, default is 1 (first line is 0, it usually have column titles)

`--end`  end line of a file, by default file will be read until the end

`--number`  column with unique product number

`--wsid`  column with Webservice id (if csv is given from TopData it may have this column), if it is set product will be mapped to Top Data Webserivce products

`--name`  column with product name

`--brand`  column with product brand name (will be created if is not present yet)

It is recommended to limit product count with start/end, depending on server RAM. Then you can read next chunk of products in second command.



## License

MIT

================
File: rector.md
================
# Rector Usage Instructions

Rector is a tool for instant upgrades and automated refactoring of your PHP code. This document provides basic instructions on how to use Rector in this project.

## Running Rector

1. To perform a dry run (see what would be changed without actually changing files):
   ```
   vendor/bin/rector process src --dry-run
   ```

2. To apply changes:
   ```
   vendor/bin/rector process src
   ```

3. To process a specific file or directory:
   ```
   vendor/bin/rector process path/to/file/or/directory
   ```

## Configuration

The Rector configuration is stored in `rector.php`. You can modify this file to change the rules applied to your project.

## Integrating with Your Workflow

- Consider running Rector before committing your code or as part of your CI/CD pipeline.
- You can add a pre-commit hook to automatically run Rector on changed files.

For more detailed information, visit the [Rector documentation](https://getrector.org/).

================
File: rector.php
================
<?php

/**
 * This file is a config file for rector
 *
 * This script configures the rules and sets for the rector process.
 */

declare(strict_types=1);

use Rector\CodeQuality\Rector\BooleanAnd\RemoveUselessIsObjectCheckRector;
use Rector\CodeQuality\Rector\Class_\CompleteDynamicPropertiesRector;
use Rector\Config\RectorConfig;
use Rector\DeadCode\Rector\Property\RemoveUselessVarTagRector;
use Rector\Set\ValueObject\LevelSetList;
use Rector\Set\ValueObject\SetList;

return static function (RectorConfig $rectorConfig): void {

    $rectorConfig->paths([
        __DIR__ . '/src',
        // Add other directories you want to scan here
    ]);

    $rectorConfig->skip([
        __DIR__ . '/vendor',
    ]);

    $rectorConfig->sets([
        LevelSetList::UP_TO_PHP_74,
//         SetList::TYPE_DECLARATION, // it breaks return with types for method overwrites in inherited classes
    ]);

    // ---- Apply specific rules for adding typed properties and property promotion
    $rectorConfig->rule(RemoveUselessVarTagRector::class);
    $rectorConfig->rule(RemoveUselessIsObjectCheckRector::class);
    $rectorConfig->rule(CompleteDynamicPropertiesRector::class);

    $rectorConfig->ruleWithConfiguration(\Rector\Php80\Rector\Class_\AnnotationToAttributeRector::class, [
        new \Rector\Php80\ValueObject\AnnotationToAttribute('Symfony\\Component\\Routing\\Annotation\\Route'),
    ]);

    $rectorConfig->rule(\Rector\Symfony\Symfony61\Rector\Class_\CommandPropertyToAttributeRector::class);
    $rectorConfig->rule(\Rector\Php70\Rector\ClassMethod\Php4ConstructorRector::class);
    $rectorConfig->rule(\Rector\Php82\Rector\FuncCall\Utf8DecodeEncodeToMbConvertEncodingRector::class);

};



================================================================
End of Codebase
================================================================
