diff --git a/migrations/.gitignore b/migrations/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/migrations/Version20260514043343.php b/migrations/Version20260514043343.php new file mode 100644 index 0000000..ca877e4 --- /dev/null +++ b/migrations/Version20260514043343.php @@ -0,0 +1,165 @@ +addSql('CREATE SCHEMA app'); + $this->addSql('CREATE TABLE app.ai_pipeline_jobs (id UUID NOT NULL, type VARCHAR(255) NOT NULL, status VARCHAR(255) NOT NULL, attempt_count INT NOT NULL, input_data JSON NOT NULL, output_data JSON NOT NULL, missing_fields TEXT DEFAULT NULL, error_message TEXT DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, completed_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, article_id UUID DEFAULT NULL, PRIMARY KEY (id))'); + $this->addSql('CREATE INDEX IDX_4A59A35D7294869C ON app.ai_pipeline_jobs (article_id)'); + $this->addSql('CREATE TABLE app.api_keys (id UUID NOT NULL, label VARCHAR(255) NOT NULL, key_hash VARCHAR(255) NOT NULL, permissions JSON NOT NULL, is_active BOOLEAN NOT NULL, last_used_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, expires_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, user_id UUID NOT NULL, PRIMARY KEY (id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_7A21852457BFB971 ON app.api_keys (key_hash)'); + $this->addSql('CREATE INDEX IDX_7A218524A76ED395 ON app.api_keys (user_id)'); + $this->addSql('CREATE TABLE app.article_photos (id UUID NOT NULL, filename VARCHAR(500) NOT NULL, is_main BOOLEAN NOT NULL, sort_order INT NOT NULL, article_id UUID NOT NULL, storage_path_id UUID NOT NULL, PRIMARY KEY (id))'); + $this->addSql('CREATE INDEX IDX_2ECE4E37294869C ON app.article_photos (article_id)'); + $this->addSql('CREATE INDEX IDX_2ECE4E3AE506509 ON app.article_photos (storage_path_id)'); + $this->addSql('CREATE TABLE app.article_type_platform_configs (id UUID NOT NULL, category_id VARCHAR(255) NOT NULL, article_type_id UUID NOT NULL, platform_id UUID NOT NULL, PRIMARY KEY (id))'); + $this->addSql('CREATE INDEX IDX_76C76D86289EC824 ON app.article_type_platform_configs (article_type_id)'); + $this->addSql('CREATE INDEX IDX_76C76D86FFE6496F ON app.article_type_platform_configs (platform_id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_76C76D86289EC824FFE6496F ON app.article_type_platform_configs (article_type_id, platform_id)'); + $this->addSql('CREATE TABLE app.article_types (id UUID NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY (id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_C39A94BE5E237E06 ON app.article_types (name)'); + $this->addSql('CREATE TABLE app.article_type_attributes (article_type_id UUID NOT NULL, attribute_definition_id UUID NOT NULL, PRIMARY KEY (article_type_id, attribute_definition_id))'); + $this->addSql('CREATE INDEX IDX_F9EE06F7289EC824 ON app.article_type_attributes (article_type_id)'); + $this->addSql('CREATE INDEX IDX_F9EE06F77492F274 ON app.article_type_attributes (attribute_definition_id)'); + $this->addSql('CREATE TABLE app.articles (id UUID NOT NULL, sku VARCHAR(255) NOT NULL, inventory_number VARCHAR(100) NOT NULL, status VARCHAR(255) NOT NULL, stock INT NOT NULL, condition VARCHAR(255) NOT NULL, condition_notes TEXT DEFAULT NULL, listing_price NUMERIC(10, 2) DEFAULT NULL, serial_number VARCHAR(255) DEFAULT NULL, ebay_listing_id VARCHAR(255) DEFAULT NULL, ebay_title TEXT DEFAULT NULL, ebay_description TEXT DEFAULT NULL, article_type_id UUID NOT NULL, PRIMARY KEY (id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_50858653F9038C4 ON app.articles (sku)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_50858653964C83FF ON app.articles (inventory_number)'); + $this->addSql('CREATE INDEX IDX_50858653289EC824 ON app.articles (article_type_id)'); + $this->addSql('CREATE TABLE app.attribute_definitions (id UUID NOT NULL, name VARCHAR(255) NOT NULL, type VARCHAR(255) NOT NULL, unit VARCHAR(50) DEFAULT NULL, options JSON DEFAULT NULL, PRIMARY KEY (id))'); + $this->addSql('CREATE TABLE app.attribute_mappings (id UUID NOT NULL, transformer VARCHAR(100) DEFAULT NULL, platform_config_id UUID NOT NULL, attribute_definition_id UUID NOT NULL, channel_field_id UUID NOT NULL, PRIMARY KEY (id))'); + $this->addSql('CREATE INDEX IDX_459533FFD7E707CB ON app.attribute_mappings (platform_config_id)'); + $this->addSql('CREATE INDEX IDX_459533FF7492F274 ON app.attribute_mappings (attribute_definition_id)'); + $this->addSql('CREATE INDEX IDX_459533FF33877419 ON app.attribute_mappings (channel_field_id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_459533FFD7E707CB7492F274 ON app.attribute_mappings (platform_config_id, attribute_definition_id)'); + $this->addSql('CREATE TABLE app.attribute_values (id UUID NOT NULL, value TEXT NOT NULL, article_id UUID NOT NULL, attribute_definition_id UUID NOT NULL, PRIMARY KEY (id))'); + $this->addSql('CREATE INDEX IDX_6F6A862D7294869C ON app.attribute_values (article_id)'); + $this->addSql('CREATE INDEX IDX_6F6A862D7492F274 ON app.attribute_values (attribute_definition_id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6F6A862D7294869C7492F274 ON app.attribute_values (article_id, attribute_definition_id)'); + $this->addSql('CREATE TABLE app.channel_fields (id UUID NOT NULL, label VARCHAR(255) NOT NULL, path VARCHAR(500) NOT NULL, platform_id UUID NOT NULL, PRIMARY KEY (id))'); + $this->addSql('CREATE INDEX IDX_E0DB4A4CFFE6496F ON app.channel_fields (platform_id)'); + $this->addSql('CREATE TABLE app.customers (id UUID NOT NULL, name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, address JSON NOT NULL, frappe_customer_id VARCHAR(255) DEFAULT NULL, platform_ids JSON NOT NULL, PRIMARY KEY (id))'); + $this->addSql('CREATE TABLE app.invoices (id UUID NOT NULL, frappe_invoice_id VARCHAR(255) NOT NULL, filename VARCHAR(500) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, emailed_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, order_id UUID NOT NULL, storage_path_id UUID NOT NULL, PRIMARY KEY (id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_857798AE8D9F6D38 ON app.invoices (order_id)'); + $this->addSql('CREATE INDEX IDX_857798AEAE506509 ON app.invoices (storage_path_id)'); + $this->addSql('CREATE TABLE app.orders (id UUID NOT NULL, platform_order_id VARCHAR(255) NOT NULL, status VARCHAR(255) NOT NULL, sale_price NUMERIC(10, 2) NOT NULL, sale_date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, tracking_number VARCHAR(255) DEFAULT NULL, carrier VARCHAR(100) DEFAULT NULL, shipped_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, tracking_pushed_to_ebay_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, article_id UUID NOT NULL, customer_id UUID NOT NULL, platform_id UUID NOT NULL, PRIMARY KEY (id))'); + $this->addSql('CREATE INDEX IDX_4A334BF17294869C ON app.orders (article_id)'); + $this->addSql('CREATE INDEX IDX_4A334BF19395C3F3 ON app.orders (customer_id)'); + $this->addSql('CREATE INDEX IDX_4A334BF1FFE6496F ON app.orders (platform_id)'); + $this->addSql('CREATE TABLE app.platforms (id UUID NOT NULL, type VARCHAR(100) NOT NULL, label VARCHAR(255) NOT NULL, config JSON NOT NULL, PRIMARY KEY (id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_A66537708CDE5729 ON app.platforms (type)'); + $this->addSql('CREATE TABLE app.storage_paths (id UUID NOT NULL, label VARCHAR(255) NOT NULL, base_path VARCHAR(500) NOT NULL, quota_bytes BIGINT NOT NULL, priority INT NOT NULL, is_active BOOLEAN NOT NULL, PRIMARY KEY (id))'); + $this->addSql('CREATE TABLE app.users (id UUID NOT NULL, email VARCHAR(255) NOT NULL, password_hash VARCHAR(255) NOT NULL, totp_secret VARCHAR(255) DEFAULT NULL, permissions JSON NOT NULL, is_active BOOLEAN NOT NULL, PRIMARY KEY (id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_22EF640E7927C74 ON app.users (email)'); + $this->addSql('ALTER TABLE app.ai_pipeline_jobs ADD CONSTRAINT FK_4A59A35D7294869C FOREIGN KEY (article_id) REFERENCES app.articles (id) NOT DEFERRABLE'); + $this->addSql('ALTER TABLE app.api_keys ADD CONSTRAINT FK_7A218524A76ED395 FOREIGN KEY (user_id) REFERENCES app.users (id) NOT DEFERRABLE'); + $this->addSql('ALTER TABLE app.article_photos ADD CONSTRAINT FK_2ECE4E37294869C FOREIGN KEY (article_id) REFERENCES app.articles (id) ON DELETE CASCADE NOT DEFERRABLE'); + $this->addSql('ALTER TABLE app.article_photos ADD CONSTRAINT FK_2ECE4E3AE506509 FOREIGN KEY (storage_path_id) REFERENCES app.storage_paths (id) NOT DEFERRABLE'); + $this->addSql('ALTER TABLE app.article_type_platform_configs ADD CONSTRAINT FK_76C76D86289EC824 FOREIGN KEY (article_type_id) REFERENCES app.article_types (id) NOT DEFERRABLE'); + $this->addSql('ALTER TABLE app.article_type_platform_configs ADD CONSTRAINT FK_76C76D86FFE6496F FOREIGN KEY (platform_id) REFERENCES app.platforms (id) NOT DEFERRABLE'); + $this->addSql('ALTER TABLE app.article_type_attributes ADD CONSTRAINT FK_F9EE06F7289EC824 FOREIGN KEY (article_type_id) REFERENCES app.article_types (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE app.article_type_attributes ADD CONSTRAINT FK_F9EE06F77492F274 FOREIGN KEY (attribute_definition_id) REFERENCES app.attribute_definitions (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE app.articles ADD CONSTRAINT FK_50858653289EC824 FOREIGN KEY (article_type_id) REFERENCES app.article_types (id) NOT DEFERRABLE'); + $this->addSql('ALTER TABLE app.attribute_mappings ADD CONSTRAINT FK_459533FFD7E707CB FOREIGN KEY (platform_config_id) REFERENCES app.article_type_platform_configs (id) ON DELETE CASCADE NOT DEFERRABLE'); + $this->addSql('ALTER TABLE app.attribute_mappings ADD CONSTRAINT FK_459533FF7492F274 FOREIGN KEY (attribute_definition_id) REFERENCES app.attribute_definitions (id) NOT DEFERRABLE'); + $this->addSql('ALTER TABLE app.attribute_mappings ADD CONSTRAINT FK_459533FF33877419 FOREIGN KEY (channel_field_id) REFERENCES app.channel_fields (id) NOT DEFERRABLE'); + $this->addSql('ALTER TABLE app.attribute_values ADD CONSTRAINT FK_6F6A862D7294869C FOREIGN KEY (article_id) REFERENCES app.articles (id) ON DELETE CASCADE NOT DEFERRABLE'); + $this->addSql('ALTER TABLE app.attribute_values ADD CONSTRAINT FK_6F6A862D7492F274 FOREIGN KEY (attribute_definition_id) REFERENCES app.attribute_definitions (id) NOT DEFERRABLE'); + $this->addSql('ALTER TABLE app.channel_fields ADD CONSTRAINT FK_E0DB4A4CFFE6496F FOREIGN KEY (platform_id) REFERENCES app.platforms (id) NOT DEFERRABLE'); + $this->addSql('ALTER TABLE app.invoices ADD CONSTRAINT FK_857798AE8D9F6D38 FOREIGN KEY (order_id) REFERENCES app.orders (id) NOT DEFERRABLE'); + $this->addSql('ALTER TABLE app.invoices ADD CONSTRAINT FK_857798AEAE506509 FOREIGN KEY (storage_path_id) REFERENCES app.storage_paths (id) NOT DEFERRABLE'); + $this->addSql('ALTER TABLE app.orders ADD CONSTRAINT FK_4A334BF17294869C FOREIGN KEY (article_id) REFERENCES app.articles (id) NOT DEFERRABLE'); + $this->addSql('ALTER TABLE app.orders ADD CONSTRAINT FK_4A334BF19395C3F3 FOREIGN KEY (customer_id) REFERENCES app.customers (id) NOT DEFERRABLE'); + $this->addSql('ALTER TABLE app.orders ADD CONSTRAINT FK_4A334BF1FFE6496F FOREIGN KEY (platform_id) REFERENCES app.platforms (id) NOT DEFERRABLE'); + + $this->addSql('CREATE SCHEMA IF NOT EXISTS logs'); + $this->addSql('CREATE SCHEMA IF NOT EXISTS logs_archive'); + + $this->addSql("CREATE TABLE logs.log_entry ( + id BIGSERIAL NOT NULL, + level VARCHAR(20) NOT NULL, + channel VARCHAR(100) NOT NULL, + message TEXT NOT NULL, + context JSON NOT NULL DEFAULT '[]', + message_search TSVECTOR GENERATED ALWAYS AS (to_tsvector('german', message)) STORED, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + PRIMARY KEY(id) + )"); + $this->addSql('CREATE INDEX idx_log_level ON logs.log_entry (level)'); + $this->addSql('CREATE INDEX idx_log_created ON logs.log_entry (created_at)'); + $this->addSql('CREATE INDEX idx_log_fts ON logs.log_entry USING GIN (message_search)'); + + $this->addSql("CREATE TABLE logs_archive.log_entry ( + id BIGSERIAL NOT NULL, + level VARCHAR(20) NOT NULL, + channel VARCHAR(100) NOT NULL, + message TEXT NOT NULL, + context JSON NOT NULL DEFAULT '[]', + message_search TSVECTOR GENERATED ALWAYS AS (to_tsvector('german', message)) STORED, + created_at TIMESTAMP NOT NULL, + PRIMARY KEY(id) + )"); + $this->addSql('CREATE INDEX idx_log_archive_created ON logs_archive.log_entry (created_at)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE app.ai_pipeline_jobs DROP CONSTRAINT FK_4A59A35D7294869C'); + $this->addSql('ALTER TABLE app.api_keys DROP CONSTRAINT FK_7A218524A76ED395'); + $this->addSql('ALTER TABLE app.article_photos DROP CONSTRAINT FK_2ECE4E37294869C'); + $this->addSql('ALTER TABLE app.article_photos DROP CONSTRAINT FK_2ECE4E3AE506509'); + $this->addSql('ALTER TABLE app.article_type_platform_configs DROP CONSTRAINT FK_76C76D86289EC824'); + $this->addSql('ALTER TABLE app.article_type_platform_configs DROP CONSTRAINT FK_76C76D86FFE6496F'); + $this->addSql('ALTER TABLE app.article_type_attributes DROP CONSTRAINT FK_F9EE06F7289EC824'); + $this->addSql('ALTER TABLE app.article_type_attributes DROP CONSTRAINT FK_F9EE06F77492F274'); + $this->addSql('ALTER TABLE app.articles DROP CONSTRAINT FK_50858653289EC824'); + $this->addSql('ALTER TABLE app.attribute_mappings DROP CONSTRAINT FK_459533FFD7E707CB'); + $this->addSql('ALTER TABLE app.attribute_mappings DROP CONSTRAINT FK_459533FF7492F274'); + $this->addSql('ALTER TABLE app.attribute_mappings DROP CONSTRAINT FK_459533FF33877419'); + $this->addSql('ALTER TABLE app.attribute_values DROP CONSTRAINT FK_6F6A862D7294869C'); + $this->addSql('ALTER TABLE app.attribute_values DROP CONSTRAINT FK_6F6A862D7492F274'); + $this->addSql('ALTER TABLE app.channel_fields DROP CONSTRAINT FK_E0DB4A4CFFE6496F'); + $this->addSql('ALTER TABLE app.invoices DROP CONSTRAINT FK_857798AE8D9F6D38'); + $this->addSql('ALTER TABLE app.invoices DROP CONSTRAINT FK_857798AEAE506509'); + $this->addSql('ALTER TABLE app.orders DROP CONSTRAINT FK_4A334BF17294869C'); + $this->addSql('ALTER TABLE app.orders DROP CONSTRAINT FK_4A334BF19395C3F3'); + $this->addSql('ALTER TABLE app.orders DROP CONSTRAINT FK_4A334BF1FFE6496F'); + $this->addSql('DROP TABLE app.ai_pipeline_jobs'); + $this->addSql('DROP TABLE app.api_keys'); + $this->addSql('DROP TABLE app.article_photos'); + $this->addSql('DROP TABLE app.article_type_platform_configs'); + $this->addSql('DROP TABLE app.article_types'); + $this->addSql('DROP TABLE app.article_type_attributes'); + $this->addSql('DROP TABLE app.articles'); + $this->addSql('DROP TABLE app.attribute_definitions'); + $this->addSql('DROP TABLE app.attribute_mappings'); + $this->addSql('DROP TABLE app.attribute_values'); + $this->addSql('DROP TABLE app.channel_fields'); + $this->addSql('DROP TABLE app.customers'); + $this->addSql('DROP TABLE app.invoices'); + $this->addSql('DROP TABLE app.orders'); + $this->addSql('DROP TABLE app.platforms'); + $this->addSql('DROP TABLE app.storage_paths'); + $this->addSql('DROP TABLE app.users'); + $this->addSql('DROP SCHEMA IF EXISTS logs_archive CASCADE'); + $this->addSql('DROP SCHEMA IF EXISTS logs CASCADE'); + $this->addSql('DROP SCHEMA IF EXISTS app CASCADE'); + } +} diff --git a/src/Domain/Article/Article.php b/src/Domain/Article/Article.php index 91243af..e18c07c 100644 --- a/src/Domain/Article/Article.php +++ b/src/Domain/Article/Article.php @@ -59,7 +59,8 @@ class Article private Collection $attributeValues; /** @var Collection */ - #[ORM\OneToMany(mappedBy: 'article', targetEntity: ArticlePhoto::class, cascade: ['persist', 'remove'], orderBy: ['sortOrder' => 'ASC'])] + #[ORM\OneToMany(mappedBy: 'article', targetEntity: ArticlePhoto::class, cascade: ['persist', 'remove'])] + #[ORM\OrderBy(['sortOrder' => 'ASC'])] private Collection $photos; public function __construct(