diff --git a/.github/workflows/assets_artifact_build.yml b/.github/workflows/assets_artifact_build.yml
index 447f95bf..c950375b 100644
--- a/.github/workflows/assets_artifact_build.yml
+++ b/.github/workflows/assets_artifact_build.yml
@@ -60,7 +60,7 @@ jobs:
${{ runner.os }}-yarn-
- name: Setup node
- uses: actions/setup-node@v5
+ uses: actions/setup-node@v4
with:
node-version: '20'
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index c7c0965b..66e2f40c 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -21,7 +21,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- php-versions: ['8.2', '8.3', '8.4', '8.5' ]
+ php-versions: ['8.2', '8.3', '8.4' ]
db-type: [ 'mysql', 'sqlite', 'postgres' ]
env:
@@ -104,7 +104,7 @@ jobs:
run: composer install --prefer-dist --no-progress
- name: Setup node
- uses: actions/setup-node@v5
+ uses: actions/setup-node@v4
with:
node-version: '20'
diff --git a/.gitignore b/.gitignore
index dd5c43db..76655919 100644
--- a/.gitignore
+++ b/.gitignore
@@ -48,6 +48,3 @@ yarn-error.log
###> phpstan/phpstan ###
phpstan.neon
###< phpstan/phpstan ###
-
-.claude/
-CLAUDE.md
\ No newline at end of file
diff --git a/Makefile b/Makefile
deleted file mode 100644
index bc4d0bf3..00000000
--- a/Makefile
+++ /dev/null
@@ -1,91 +0,0 @@
-# PartDB Makefile for Test Environment Management
-
-.PHONY: help deps-install lint format format-check test coverage pre-commit all test-typecheck \
-test-setup test-clean test-db-create test-db-migrate test-cache-clear test-fixtures test-run test-reset \
-section-dev dev-setup dev-clean dev-db-create dev-db-migrate dev-cache-clear dev-warmup dev-reset
-
-# Default target
-help: ## Show this help
- @awk 'BEGIN {FS = ":.*##"}; /^[a-zA-Z0-9][a-zA-Z0-9_-]+:.*##/ {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
-
-# Dependencies
-deps-install: ## Install PHP dependencies with unlimited memory
- @echo "π¦ Installing PHP dependencies..."
- COMPOSER_MEMORY_LIMIT=-1 composer install
- yarn install
- @echo "β
Dependencies installed"
-
-# Complete test environment setup
-test-setup: test-clean test-db-create test-db-migrate test-fixtures ## Complete test setup (clean, create DB, migrate, fixtures)
- @echo "β
Test environment setup complete!"
-
-# Clean test environment
-test-clean: ## Clean test cache and database files
- @echo "π§Ή Cleaning test environment..."
- rm -rf var/cache/test
- rm -f var/app_test.db
- @echo "β
Test environment cleaned"
-
-# Create test database
-test-db-create: ## Create test database (if not exists)
- @echo "ποΈ Creating test database..."
- -php bin/console doctrine:database:create --if-not-exists --env test || echo "β οΈ Database creation failed (expected for SQLite) - continuing..."
-
-# Run database migrations for test environment
-test-db-migrate: ## Run database migrations for test environment
- @echo "π Running database migrations..."
- COMPOSER_MEMORY_LIMIT=-1 php bin/console doctrine:migrations:migrate -n --env test
-
-# Clear test cache
-test-cache-clear: ## Clear test cache
- @echo "ποΈ Clearing test cache..."
- rm -rf var/cache/test
- @echo "β
Test cache cleared"
-
-# Load test fixtures
-test-fixtures: ## Load test fixtures
- @echo "π¦ Loading test fixtures..."
- php bin/console partdb:fixtures:load -n --env test
-
-# Run PHPUnit tests
-test-run: ## Run PHPUnit tests
- @echo "π§ͺ Running tests..."
- php bin/phpunit
-
-# Quick test reset (clean + migrate + fixtures, skip DB creation)
-test-reset: test-cache-clear test-db-migrate test-fixtures
- @echo "β
Test environment reset complete!"
-
-test-typecheck: ## Run static analysis (PHPStan)
- @echo "π§ͺ Running type checks..."
- COMPOSER_MEMORY_LIMIT=-1 composer phpstan
-
-# Development helpers
-dev-setup: dev-clean dev-db-create dev-db-migrate dev-warmup ## Complete development setup (clean, create DB, migrate, warmup)
- @echo "β
Development environment setup complete!"
-
-dev-clean: ## Clean development cache and database files
- @echo "π§Ή Cleaning development environment..."
- rm -rf var/cache/dev
- rm -f var/app_dev.db
- @echo "β
Development environment cleaned"
-
-dev-db-create: ## Create development database (if not exists)
- @echo "ποΈ Creating development database..."
- -php bin/console doctrine:database:create --if-not-exists --env dev || echo "β οΈ Database creation failed (expected for SQLite) - continuing..."
-
-dev-db-migrate: ## Run database migrations for development environment
- @echo "π Running database migrations..."
- COMPOSER_MEMORY_LIMIT=-1 php bin/console doctrine:migrations:migrate -n --env dev
-
-dev-cache-clear: ## Clear development cache
- @echo "ποΈ Clearing development cache..."
- rm -rf var/cache/dev
- @echo "β
Development cache cleared"
-
-dev-warmup: ## Warm up development cache
- @echo "π₯ Warming up development cache..."
- COMPOSER_MEMORY_LIMIT=-1 php -d memory_limit=1G bin/console cache:warmup --env dev -n
-
-dev-reset: dev-cache-clear dev-db-migrate ## Quick development reset (cache clear + migrate)
- @echo "β
Development environment reset complete!"
\ No newline at end of file
diff --git a/assets/controllers/bulk_import_controller.js b/assets/controllers/bulk_import_controller.js
deleted file mode 100644
index 49e4d60f..00000000
--- a/assets/controllers/bulk_import_controller.js
+++ /dev/null
@@ -1,359 +0,0 @@
-import { Controller } from "@hotwired/stimulus"
-import { generateCsrfHeaders } from "./csrf_protection_controller"
-
-export default class extends Controller {
- static targets = ["progressBar", "progressText"]
- static values = {
- jobId: Number,
- partId: Number,
- researchUrl: String,
- researchAllUrl: String,
- markCompletedUrl: String,
- markSkippedUrl: String,
- markPendingUrl: String
- }
-
- connect() {
- // Auto-refresh progress if job is in progress
- if (this.hasProgressBarTarget) {
- this.startProgressUpdates()
- }
-
- // Restore scroll position after page reload (if any)
- this.restoreScrollPosition()
- }
-
- getHeaders() {
- const headers = {
- 'Content-Type': 'application/json',
- 'X-Requested-With': 'XMLHttpRequest'
- }
-
- // Add CSRF headers if available
- const form = document.querySelector('form')
- if (form) {
- const csrfHeaders = generateCsrfHeaders(form)
- Object.assign(headers, csrfHeaders)
- }
-
- return headers
- }
-
- async fetchWithErrorHandling(url, options = {}, timeout = 30000) {
- const controller = new AbortController()
- const timeoutId = setTimeout(() => controller.abort(), timeout)
-
- try {
- const response = await fetch(url, {
- ...options,
- headers: { ...this.getHeaders(), ...options.headers },
- signal: controller.signal
- })
-
- clearTimeout(timeoutId)
-
- if (!response.ok) {
- const errorText = await response.text()
- throw new Error(`Server error (${response.status}): ${errorText}`)
- }
-
- return await response.json()
- } catch (error) {
- clearTimeout(timeoutId)
-
- if (error.name === 'AbortError') {
- throw new Error('Request timed out. Please try again.')
- } else if (error.message.includes('Failed to fetch')) {
- throw new Error('Network error. Please check your connection and try again.')
- } else {
- throw error
- }
- }
- }
-
- disconnect() {
- if (this.progressInterval) {
- clearInterval(this.progressInterval)
- }
- }
-
- startProgressUpdates() {
- // Progress updates are handled via page reload for better reliability
- // No need for periodic updates since state changes trigger page refresh
- }
-
- restoreScrollPosition() {
- const savedPosition = sessionStorage.getItem('bulkImportScrollPosition')
- if (savedPosition) {
- // Restore scroll position after a small delay to ensure page is fully loaded
- setTimeout(() => {
- window.scrollTo(0, parseInt(savedPosition))
- // Clear the saved position so it doesn't interfere with normal navigation
- sessionStorage.removeItem('bulkImportScrollPosition')
- }, 100)
- }
- }
-
- async markCompleted(event) {
- const partId = event.currentTarget.dataset.partId
-
- try {
- const url = this.markCompletedUrlValue.replace('__PART_ID__', partId)
- const data = await this.fetchWithErrorHandling(url, { method: 'POST' })
-
- if (data.success) {
- this.updateProgressDisplay(data)
- this.markRowAsCompleted(partId)
-
- if (data.job_completed) {
- this.showJobCompletedMessage()
- }
- } else {
- this.showErrorMessage(data.error || 'Failed to mark part as completed')
- }
- } catch (error) {
- console.error('Error marking part as completed:', error)
- this.showErrorMessage(error.message || 'Failed to mark part as completed')
- }
- }
-
- async markSkipped(event) {
- const partId = event.currentTarget.dataset.partId
- const reason = prompt('Reason for skipping (optional):') || ''
-
- try {
- const url = this.markSkippedUrlValue.replace('__PART_ID__', partId)
- const data = await this.fetchWithErrorHandling(url, {
- method: 'POST',
- body: JSON.stringify({ reason })
- })
-
- if (data.success) {
- this.updateProgressDisplay(data)
- this.markRowAsSkipped(partId)
- } else {
- this.showErrorMessage(data.error || 'Failed to mark part as skipped')
- }
- } catch (error) {
- console.error('Error marking part as skipped:', error)
- this.showErrorMessage(error.message || 'Failed to mark part as skipped')
- }
- }
-
- async markPending(event) {
- const partId = event.currentTarget.dataset.partId
-
- try {
- const url = this.markPendingUrlValue.replace('__PART_ID__', partId)
- const data = await this.fetchWithErrorHandling(url, { method: 'POST' })
-
- if (data.success) {
- this.updateProgressDisplay(data)
- this.markRowAsPending(partId)
- } else {
- this.showErrorMessage(data.error || 'Failed to mark part as pending')
- }
- } catch (error) {
- console.error('Error marking part as pending:', error)
- this.showErrorMessage(error.message || 'Failed to mark part as pending')
- }
- }
-
- updateProgressDisplay(data) {
- if (this.hasProgressBarTarget) {
- this.progressBarTarget.style.width = `${data.progress}%`
- this.progressBarTarget.setAttribute('aria-valuenow', data.progress)
- }
-
- if (this.hasProgressTextTarget) {
- this.progressTextTarget.textContent = `${data.completed_count} / ${data.total_count} completed`
- }
- }
-
- markRowAsCompleted(partId) {
- // Save scroll position and refresh page to show updated state
- sessionStorage.setItem('bulkImportScrollPosition', window.scrollY.toString())
- window.location.reload()
- }
-
- markRowAsSkipped(partId) {
- // Save scroll position and refresh page to show updated state
- sessionStorage.setItem('bulkImportScrollPosition', window.scrollY.toString())
- window.location.reload()
- }
-
- markRowAsPending(partId) {
- // Save scroll position and refresh page to show updated state
- sessionStorage.setItem('bulkImportScrollPosition', window.scrollY.toString())
- window.location.reload()
- }
-
- showJobCompletedMessage() {
- const alert = document.createElement('div')
- alert.className = 'alert alert-success alert-dismissible fade show'
- alert.innerHTML = `
-
- Job completed! All parts have been processed.
-
- `
-
- const container = document.querySelector('.card-body')
- container.insertBefore(alert, container.firstChild)
- }
-
- async researchPart(event) {
- event.preventDefault()
- event.stopPropagation()
-
- const partId = event.currentTarget.dataset.partId
- const spinner = event.currentTarget.querySelector(`[data-research-spinner="${partId}"]`)
- const button = event.currentTarget
-
- // Show loading state
- if (spinner) {
- spinner.style.display = 'inline-block'
- }
- button.disabled = true
-
- try {
- const url = this.researchUrlValue.replace('__PART_ID__', partId)
- const controller = new AbortController()
- const timeoutId = setTimeout(() => controller.abort(), 30000) // 30 second timeout
-
- const response = await fetch(url, {
- method: 'POST',
- headers: this.getHeaders(),
- signal: controller.signal
- })
-
- clearTimeout(timeoutId)
-
- if (!response.ok) {
- const errorText = await response.text()
- throw new Error(`Server error (${response.status}): ${errorText}`)
- }
-
- const data = await response.json()
-
- if (data.success) {
- this.showSuccessMessage(`Research completed for part. Found ${data.results_count} results.`)
- // Save scroll position and reload to show updated results
- sessionStorage.setItem('bulkImportScrollPosition', window.scrollY.toString())
- window.location.reload()
- } else {
- this.showErrorMessage(data.error || 'Research failed')
- }
- } catch (error) {
- console.error('Error researching part:', error)
-
- if (error.name === 'AbortError') {
- this.showErrorMessage('Research timed out. Please try again.')
- } else if (error.message.includes('Failed to fetch')) {
- this.showErrorMessage('Network error. Please check your connection and try again.')
- } else {
- this.showErrorMessage(error.message || 'Research failed due to an unexpected error')
- }
- } finally {
- // Hide loading state
- if (spinner) {
- spinner.style.display = 'none'
- }
- button.disabled = false
- }
- }
-
- async researchAllParts(event) {
- event.preventDefault()
- event.stopPropagation()
-
- const spinner = document.getElementById('research-all-spinner')
- const button = event.currentTarget
-
- // Show loading state
- if (spinner) {
- spinner.style.display = 'inline-block'
- }
- button.disabled = true
-
- try {
- const controller = new AbortController()
- const timeoutId = setTimeout(() => controller.abort(), 120000) // 2 minute timeout for bulk operations
-
- const response = await fetch(this.researchAllUrlValue, {
- method: 'POST',
- headers: this.getHeaders(),
- signal: controller.signal
- })
-
- clearTimeout(timeoutId)
-
- if (!response.ok) {
- const errorText = await response.text()
- throw new Error(`Server error (${response.status}): ${errorText}`)
- }
-
- const data = await response.json()
-
- if (data.success) {
- this.showSuccessMessage(`Research completed for ${data.researched_count} parts.`)
- // Save scroll position and reload to show updated results
- sessionStorage.setItem('bulkImportScrollPosition', window.scrollY.toString())
- window.location.reload()
- } else {
- this.showErrorMessage(data.error || 'Bulk research failed')
- }
- } catch (error) {
- console.error('Error researching all parts:', error)
-
- if (error.name === 'AbortError') {
- this.showErrorMessage('Bulk research timed out. This may happen with large batches. Please try again or process smaller batches.')
- } else if (error.message.includes('Failed to fetch')) {
- this.showErrorMessage('Network error. Please check your connection and try again.')
- } else {
- this.showErrorMessage(error.message || 'Bulk research failed due to an unexpected error')
- }
- } finally {
- // Hide loading state
- if (spinner) {
- spinner.style.display = 'none'
- }
- button.disabled = false
- }
- }
-
- showSuccessMessage(message) {
- this.showToast('success', message)
- }
-
- showErrorMessage(message) {
- this.showToast('error', message)
- }
-
- showToast(type, message) {
- // Create a simple alert that doesn't disrupt layout
- const alertId = 'alert-' + Date.now()
- const iconClass = type === 'success' ? 'fa-check-circle' : 'fa-exclamation-triangle'
- const alertClass = type === 'success' ? 'alert-success' : 'alert-danger'
-
- const alertHTML = `
-
-
- ${message}
-
-
- `
-
- // Add alert to body
- document.body.insertAdjacentHTML('beforeend', alertHTML)
-
- // Auto-remove after 5 seconds
- setTimeout(() => {
- const alertElement = document.getElementById(alertId)
- if (alertElement) {
- alertElement.remove()
- }
- }, 5000)
- }
-}
\ No newline at end of file
diff --git a/assets/controllers/bulk_job_manage_controller.js b/assets/controllers/bulk_job_manage_controller.js
deleted file mode 100644
index c26e37c6..00000000
--- a/assets/controllers/bulk_job_manage_controller.js
+++ /dev/null
@@ -1,92 +0,0 @@
-import { Controller } from "@hotwired/stimulus"
-import { generateCsrfHeaders } from "./csrf_protection_controller"
-
-export default class extends Controller {
- static values = {
- deleteUrl: String,
- stopUrl: String,
- deleteConfirmMessage: String,
- stopConfirmMessage: String
- }
-
- connect() {
- // Controller initialized
- }
- getHeaders() {
- const headers = {
- 'X-Requested-With': 'XMLHttpRequest'
- }
-
- // Add CSRF headers if available
- const form = document.querySelector('form')
- if (form) {
- const csrfHeaders = generateCsrfHeaders(form)
- Object.assign(headers, csrfHeaders)
- }
-
- return headers
- }
- async deleteJob(event) {
- const jobId = event.currentTarget.dataset.jobId
- const confirmMessage = this.deleteConfirmMessageValue || 'Are you sure you want to delete this job?'
-
- if (confirm(confirmMessage)) {
- try {
- const deleteUrl = this.deleteUrlValue.replace('__JOB_ID__', jobId)
-
- const response = await fetch(deleteUrl, {
- method: 'DELETE',
- headers: this.getHeaders()
- })
-
- if (!response.ok) {
- const errorText = await response.text()
- throw new Error(`HTTP ${response.status}: ${errorText}`)
- }
-
- const data = await response.json()
-
- if (data.success) {
- location.reload()
- } else {
- alert('Error deleting job: ' + (data.error || 'Unknown error'))
- }
- } catch (error) {
- console.error('Error deleting job:', error)
- alert('Error deleting job: ' + error.message)
- }
- }
- }
-
- async stopJob(event) {
- const jobId = event.currentTarget.dataset.jobId
- const confirmMessage = this.stopConfirmMessageValue || 'Are you sure you want to stop this job?'
-
- if (confirm(confirmMessage)) {
- try {
- const stopUrl = this.stopUrlValue.replace('__JOB_ID__', jobId)
-
- const response = await fetch(stopUrl, {
- method: 'POST',
- headers: this.getHeaders()
- })
-
- if (!response.ok) {
- const errorText = await response.text()
- throw new Error(`HTTP ${response.status}: ${errorText}`)
- }
-
- const data = await response.json()
-
- if (data.success) {
- location.reload()
- } else {
- alert('Error stopping job: ' + (data.error || 'Unknown error'))
- }
- } catch (error) {
- console.error('Error stopping job:', error)
- alert('Error stopping job: ' + error.message)
- }
- }
- }
-}
\ No newline at end of file
diff --git a/assets/controllers/field_mapping_controller.js b/assets/controllers/field_mapping_controller.js
deleted file mode 100644
index 9c9c8ac6..00000000
--- a/assets/controllers/field_mapping_controller.js
+++ /dev/null
@@ -1,136 +0,0 @@
-import { Controller } from "@hotwired/stimulus"
-
-export default class extends Controller {
- static targets = ["tbody", "addButton", "submitButton"]
- static values = {
- mappingIndex: Number,
- maxMappings: Number,
- prototype: String,
- maxMappingsReachedMessage: String
- }
-
- connect() {
- this.updateAddButtonState()
- this.updateFieldOptions()
- this.attachEventListeners()
- }
-
- attachEventListeners() {
- // Add event listeners to existing field selects
- const fieldSelects = this.tbodyTarget.querySelectorAll('select[name*="[field]"]')
- fieldSelects.forEach(select => {
- select.addEventListener('change', this.updateFieldOptions.bind(this))
- })
-
- // Note: Add button click is handled by Stimulus action in template (data-action="click->field-mapping#addMapping")
- // No manual event listener needed
-
- // Form submit handler
- const form = this.element.querySelector('form')
- if (form && this.hasSubmitButtonTarget) {
- form.addEventListener('submit', this.handleFormSubmit.bind(this))
- }
- }
-
- addMapping() {
- const currentMappings = this.tbodyTarget.querySelectorAll('.mapping-row').length
-
- if (currentMappings >= this.maxMappingsValue) {
- alert(this.maxMappingsReachedMessageValue)
- return
- }
-
- const newRowHtml = this.prototypeValue.replace(/__name__/g, this.mappingIndexValue)
- const tempDiv = document.createElement('div')
- tempDiv.innerHTML = newRowHtml
-
- const fieldWidget = tempDiv.querySelector('select[name*="[field]"]') || tempDiv.children[0]
- const providerWidget = tempDiv.querySelector('select[name*="[providers]"]') || tempDiv.children[1]
- const priorityWidget = tempDiv.querySelector('input[name*="[priority]"]') || tempDiv.children[2]
-
- const newRow = document.createElement('tr')
- newRow.className = 'mapping-row'
- newRow.innerHTML = `
- ${fieldWidget ? fieldWidget.outerHTML : ''}
- ${providerWidget ? providerWidget.outerHTML : ''}
- ${priorityWidget ? priorityWidget.outerHTML : ''}
-
-
-
-
-
- `
-
- this.tbodyTarget.appendChild(newRow)
- this.mappingIndexValue++
-
- const newFieldSelect = newRow.querySelector('select[name*="[field]"]')
- if (newFieldSelect) {
- newFieldSelect.value = ''
- newFieldSelect.addEventListener('change', this.updateFieldOptions.bind(this))
- }
-
- this.updateFieldOptions()
- this.updateAddButtonState()
- }
-
- removeMapping(event) {
- const row = event.target.closest('tr')
- row.remove()
- this.updateFieldOptions()
- this.updateAddButtonState()
- }
-
- updateFieldOptions() {
- const fieldSelects = this.tbodyTarget.querySelectorAll('select[name*="[field]"]')
-
- const selectedFields = Array.from(fieldSelects)
- .map(select => select.value)
- .filter(value => value && value !== '')
-
- fieldSelects.forEach(select => {
- Array.from(select.options).forEach(option => {
- const isCurrentValue = option.value === select.value
- const isEmptyOption = !option.value || option.value === ''
- const isAlreadySelected = selectedFields.includes(option.value)
-
- if (!isEmptyOption && isAlreadySelected && !isCurrentValue) {
- option.disabled = true
- option.style.display = 'none'
- } else {
- option.disabled = false
- option.style.display = ''
- }
- })
- })
- }
-
- updateAddButtonState() {
- const currentMappings = this.tbodyTarget.querySelectorAll('.mapping-row').length
-
- if (this.hasAddButtonTarget) {
- if (currentMappings >= this.maxMappingsValue) {
- this.addButtonTarget.disabled = true
- this.addButtonTarget.title = this.maxMappingsReachedMessageValue
- } else {
- this.addButtonTarget.disabled = false
- this.addButtonTarget.title = ''
- }
- }
- }
-
- handleFormSubmit(event) {
- if (this.hasSubmitButtonTarget) {
- this.submitButtonTarget.disabled = true
-
- // Disable the entire form to prevent changes during processing
- const form = event.target
- const formElements = form.querySelectorAll('input, select, textarea, button')
- formElements.forEach(element => {
- if (element !== this.submitButtonTarget) {
- element.disabled = true
- }
- })
- }
- }
-}
\ No newline at end of file
diff --git a/assets/css/app/tables.css b/assets/css/app/tables.css
index b2d8882c..8d4b200c 100644
--- a/assets/css/app/tables.css
+++ b/assets/css/app/tables.css
@@ -94,11 +94,6 @@ th.select-checkbox {
display: inline-flex;
}
-/** Add spacing between column visibility button and length menu */
-.buttons-colvis {
- margin-right: 0.2em !important;
-}
-
/** Fix datatables select-checkbox position */
table.dataTable tr.selected td.select-checkbox:after
{
diff --git a/composer.json b/composer.json
index 9f335f94..80b413f8 100644
--- a/composer.json
+++ b/composer.json
@@ -25,6 +25,7 @@
"doctrine/doctrine-migrations-bundle": "^3.0",
"doctrine/orm": "^3.2.0",
"dompdf/dompdf": "^v3.0.0",
+ "part-db/swap-bundle": "^6.0.0",
"gregwar/captcha-bundle": "^2.1.0",
"hshn/base64-encoded-file": "^5.0",
"jbtronics/2fa-webauthn": "^3.0.0",
@@ -36,7 +37,6 @@
"league/csv": "^9.8.0",
"league/html-to-markdown": "^5.0.1",
"liip/imagine-bundle": "^2.2",
- "maennchen/zipstream-php": "2.1",
"nbgrp/onelogin-saml-bundle": "^v2.0.2",
"nelexa/zip": "^4.0",
"nelmio/cors-bundle": "^2.3",
@@ -45,8 +45,6 @@
"omines/datatables-bundle": "^0.10.0",
"paragonie/sodium_compat": "^1.21",
"part-db/label-fonts": "^1.0",
- "part-db/swap-bundle": "^6.0.0",
- "phpoffice/phpspreadsheet": "^5.0.0",
"rhukster/dom-sanitizer": "^1.0",
"runtime/frankenphp-symfony": "^0.2.0",
"s9e/text-formatter": "^2.1",
@@ -159,7 +157,7 @@
"post-update-cmd": [
"@auto-scripts"
],
- "phpstan": "php -d memory_limit=1G vendor/bin/phpstan analyse src --level 5"
+ "phpstan": "vendor/bin/phpstan analyse src --level 5 --memory-limit 1G"
},
"conflict": {
"symfony/symfony": "*"
diff --git a/composer.lock b/composer.lock
index 22f1f60f..1f67b80f 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "10fd1b276a868a4f195721ac5fcd82de",
+ "content-hash": "fe6dfc229f551945cfa6be8ca26a437e",
"packages": [
{
"name": "amphp/amp",
@@ -968,16 +968,16 @@
},
{
"name": "api-platform/doctrine-common",
- "version": "v4.2.0",
+ "version": "v4.1.23",
"source": {
"type": "git",
"url": "https://github.com/api-platform/doctrine-common.git",
- "reference": "8acbed7c2768f7c15a5b030018132e454f895e55"
+ "reference": "e0ef3f5d1c4a9d023da519ea120a1d7732e0b1a7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/api-platform/doctrine-common/zipball/8acbed7c2768f7c15a5b030018132e454f895e55",
- "reference": "8acbed7c2768f7c15a5b030018132e454f895e55",
+ "url": "https://api.github.com/repos/api-platform/doctrine-common/zipball/e0ef3f5d1c4a9d023da519ea120a1d7732e0b1a7",
+ "reference": "e0ef3f5d1c4a9d023da519ea120a1d7732e0b1a7",
"shasum": ""
},
"require": {
@@ -995,8 +995,7 @@
"doctrine/mongodb-odm": "^2.10",
"doctrine/orm": "^2.17 || ^3.0",
"phpspec/prophecy-phpunit": "^2.2",
- "phpunit/phpunit": "11.5.x-dev",
- "symfony/type-info": "^7.3"
+ "phpunit/phpunit": "11.5.x-dev"
},
"suggest": {
"api-platform/graphql": "For GraphQl mercure subscriptions.",
@@ -1018,8 +1017,7 @@
"branch-alias": {
"dev-3.4": "3.4.x-dev",
"dev-4.1": "4.1.x-dev",
- "dev-4.2": "4.2.x-dev",
- "dev-main": "4.3.x-dev"
+ "dev-main": "4.2.x-dev"
}
},
"autoload": {
@@ -1052,31 +1050,31 @@
"rest"
],
"support": {
- "source": "https://github.com/api-platform/doctrine-common/tree/v4.2.0"
+ "source": "https://github.com/api-platform/doctrine-common/tree/v4.1.23"
},
- "time": "2025-08-27T12:34:14+00:00"
+ "time": "2025-08-18T13:30:43+00:00"
},
{
"name": "api-platform/doctrine-orm",
- "version": "v4.2.0",
+ "version": "v4.1.23",
"source": {
"type": "git",
"url": "https://github.com/api-platform/doctrine-orm.git",
- "reference": "23b0de35bb7d2903854c6ee3ac300b7f5056c12b"
+ "reference": "61a199da6f6014dba2da43ea1a66b2c9dda27263"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/api-platform/doctrine-orm/zipball/23b0de35bb7d2903854c6ee3ac300b7f5056c12b",
- "reference": "23b0de35bb7d2903854c6ee3ac300b7f5056c12b",
+ "url": "https://api.github.com/repos/api-platform/doctrine-orm/zipball/61a199da6f6014dba2da43ea1a66b2c9dda27263",
+ "reference": "61a199da6f6014dba2da43ea1a66b2c9dda27263",
"shasum": ""
},
"require": {
- "api-platform/doctrine-common": "^4.2.0-alpha.3@alpha",
+ "api-platform/doctrine-common": "^4.1.11",
"api-platform/metadata": "^4.1.11",
"api-platform/state": "^4.1.11",
"doctrine/orm": "^2.17 || ^3.0",
"php": ">=8.2",
- "symfony/type-info": "^7.3"
+ "symfony/property-info": "^6.4 || ^7.1"
},
"require-dev": {
"doctrine/doctrine-bundle": "^2.11",
@@ -1087,7 +1085,6 @@
"symfony/cache": "^6.4 || ^7.0",
"symfony/framework-bundle": "^6.4 || ^7.0",
"symfony/property-access": "^6.4 || ^7.0",
- "symfony/property-info": "^6.4 || ^7.1",
"symfony/serializer": "^6.4 || ^7.0",
"symfony/uid": "^6.4 || ^7.0",
"symfony/validator": "^6.4 || ^7.0",
@@ -1105,8 +1102,7 @@
"branch-alias": {
"dev-3.4": "3.4.x-dev",
"dev-4.1": "4.1.x-dev",
- "dev-4.2": "4.2.x-dev",
- "dev-main": "4.3.x-dev"
+ "dev-main": "4.2.x-dev"
}
},
"autoload": {
@@ -1139,22 +1135,22 @@
"rest"
],
"support": {
- "source": "https://github.com/api-platform/doctrine-orm/tree/v4.2.0"
+ "source": "https://github.com/api-platform/doctrine-orm/tree/v4.1.23"
},
- "time": "2025-09-16T12:49:22+00:00"
+ "time": "2025-06-06T14:56:47+00:00"
},
{
"name": "api-platform/documentation",
- "version": "v4.2.0",
+ "version": "v4.1.23",
"source": {
"type": "git",
"url": "https://github.com/api-platform/documentation.git",
- "reference": "c5a54336d8c51271aa5d54e57147cdee7162ab3a"
+ "reference": "1a0ac988d659008ef8667d05bc9978863026bab8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/api-platform/documentation/zipball/c5a54336d8c51271aa5d54e57147cdee7162ab3a",
- "reference": "c5a54336d8c51271aa5d54e57147cdee7162ab3a",
+ "url": "https://api.github.com/repos/api-platform/documentation/zipball/1a0ac988d659008ef8667d05bc9978863026bab8",
+ "reference": "1a0ac988d659008ef8667d05bc9978863026bab8",
"shasum": ""
},
"require": {
@@ -1176,8 +1172,7 @@
"branch-alias": {
"dev-3.4": "3.4.x-dev",
"dev-4.1": "4.1.x-dev",
- "dev-4.2": "4.2.x-dev",
- "dev-main": "4.3.x-dev"
+ "dev-main": "4.2.x-dev"
}
},
"autoload": {
@@ -1202,22 +1197,22 @@
],
"description": "API Platform documentation controller.",
"support": {
- "source": "https://github.com/api-platform/documentation/tree/v4.2.0"
+ "source": "https://github.com/api-platform/documentation/tree/v4.2.0-alpha.1"
},
- "time": "2025-08-19T08:04:29+00:00"
+ "time": "2025-06-06T14:56:47+00:00"
},
{
"name": "api-platform/http-cache",
- "version": "v4.2.0",
+ "version": "v4.1.23",
"source": {
"type": "git",
"url": "https://github.com/api-platform/http-cache.git",
- "reference": "aef434b026b861ea451d814c86838b5470b8bfb4"
+ "reference": "f65f092c90311a87ebb6dda87db3ca08b57c10d6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/api-platform/http-cache/zipball/aef434b026b861ea451d814c86838b5470b8bfb4",
- "reference": "aef434b026b861ea451d814c86838b5470b8bfb4",
+ "url": "https://api.github.com/repos/api-platform/http-cache/zipball/f65f092c90311a87ebb6dda87db3ca08b57c10d6",
+ "reference": "f65f092c90311a87ebb6dda87db3ca08b57c10d6",
"shasum": ""
},
"require": {
@@ -1231,8 +1226,7 @@
"phpspec/prophecy-phpunit": "^2.2",
"phpunit/phpunit": "11.5.x-dev",
"symfony/dependency-injection": "^6.4 || ^7.0",
- "symfony/http-client": "^6.4 || ^7.0",
- "symfony/type-info": "^7.3"
+ "symfony/http-client": "^6.4 || ^7.0"
},
"type": "library",
"extra": {
@@ -1246,8 +1240,7 @@
"branch-alias": {
"dev-3.4": "3.4.x-dev",
"dev-4.1": "4.1.x-dev",
- "dev-4.2": "4.2.x-dev",
- "dev-main": "4.3.x-dev"
+ "dev-main": "4.2.x-dev"
}
},
"autoload": {
@@ -1282,33 +1275,32 @@
"rest"
],
"support": {
- "source": "https://github.com/api-platform/http-cache/tree/v4.2.0"
+ "source": "https://github.com/api-platform/http-cache/tree/v4.1.23"
},
- "time": "2025-09-16T12:51:08+00:00"
+ "time": "2025-06-06T14:56:47+00:00"
},
{
"name": "api-platform/hydra",
- "version": "v4.2.0",
+ "version": "v4.1.23",
"source": {
"type": "git",
"url": "https://github.com/api-platform/hydra.git",
- "reference": "5061103e7a5f019097993e6370232c46e24b9b42"
+ "reference": "8c75b814af143c95ffc1857565169ff5b6f1b421"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/api-platform/hydra/zipball/5061103e7a5f019097993e6370232c46e24b9b42",
- "reference": "5061103e7a5f019097993e6370232c46e24b9b42",
+ "url": "https://api.github.com/repos/api-platform/hydra/zipball/8c75b814af143c95ffc1857565169ff5b6f1b421",
+ "reference": "8c75b814af143c95ffc1857565169ff5b6f1b421",
"shasum": ""
},
"require": {
- "api-platform/documentation": "^4.1",
- "api-platform/json-schema": "^4.2@beta",
- "api-platform/jsonld": "^4.1",
- "api-platform/metadata": "^4.2@beta",
- "api-platform/serializer": "^4.1",
- "api-platform/state": "^4.1.8",
+ "api-platform/documentation": "^4.1.11",
+ "api-platform/json-schema": "^4.1.11",
+ "api-platform/jsonld": "^4.1.11",
+ "api-platform/metadata": "^4.1.11",
+ "api-platform/serializer": "^4.1.11",
+ "api-platform/state": "^4.1.11",
"php": ">=8.2",
- "symfony/type-info": "^7.3",
"symfony/web-link": "^6.4 || ^7.1"
},
"require-dev": {
@@ -1331,8 +1323,7 @@
"branch-alias": {
"dev-3.4": "3.4.x-dev",
"dev-4.1": "4.1.x-dev",
- "dev-4.2": "4.2.x-dev",
- "dev-main": "4.3.x-dev"
+ "dev-main": "4.2.x-dev"
}
},
"autoload": {
@@ -1369,40 +1360,38 @@
"rest"
],
"support": {
- "source": "https://github.com/api-platform/hydra/tree/v4.2.0"
+ "source": "https://github.com/api-platform/hydra/tree/v4.1.23"
},
- "time": "2025-09-16T12:49:22+00:00"
+ "time": "2025-07-15T14:10:59+00:00"
},
{
"name": "api-platform/json-api",
- "version": "v4.2.0",
+ "version": "v4.1.23",
"source": {
"type": "git",
"url": "https://github.com/api-platform/json-api.git",
- "reference": "e8da698d55fb1702b25c63d7c821d1760159912e"
+ "reference": "7ea9bbe5f801f58b3f78730f6e6cd4b168b450d4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/api-platform/json-api/zipball/e8da698d55fb1702b25c63d7c821d1760159912e",
- "reference": "e8da698d55fb1702b25c63d7c821d1760159912e",
+ "url": "https://api.github.com/repos/api-platform/json-api/zipball/7ea9bbe5f801f58b3f78730f6e6cd4b168b450d4",
+ "reference": "7ea9bbe5f801f58b3f78730f6e6cd4b168b450d4",
"shasum": ""
},
"require": {
"api-platform/documentation": "^4.1.11",
- "api-platform/json-schema": "^4.2@beta",
- "api-platform/metadata": "^4.2@beta",
+ "api-platform/json-schema": "^4.1.11",
+ "api-platform/metadata": "^4.1.11",
"api-platform/serializer": "^4.1.11",
"api-platform/state": "^4.1.11",
"php": ">=8.2",
"symfony/error-handler": "^6.4 || ^7.0",
- "symfony/http-foundation": "^6.4 || ^7.0",
- "symfony/type-info": "^7.3"
+ "symfony/http-foundation": "^6.4 || ^7.0"
},
"require-dev": {
"phpspec/prophecy": "^1.19",
"phpspec/prophecy-phpunit": "^2.2",
- "phpunit/phpunit": "11.5.x-dev",
- "symfony/type-info": "^7.3"
+ "phpunit/phpunit": "11.5.x-dev"
},
"type": "library",
"extra": {
@@ -1416,8 +1405,7 @@
"branch-alias": {
"dev-3.4": "3.4.x-dev",
"dev-4.1": "4.1.x-dev",
- "dev-4.2": "4.2.x-dev",
- "dev-main": "4.3.x-dev"
+ "dev-main": "4.2.x-dev"
}
},
"autoload": {
@@ -1451,31 +1439,30 @@
"rest"
],
"support": {
- "source": "https://github.com/api-platform/json-api/tree/v4.2.0"
+ "source": "https://github.com/api-platform/json-api/tree/v4.1.23"
},
- "time": "2025-09-16T12:49:22+00:00"
+ "time": "2025-08-06T07:56:58+00:00"
},
{
"name": "api-platform/json-schema",
- "version": "v4.2.0",
+ "version": "v4.1.23",
"source": {
"type": "git",
"url": "https://github.com/api-platform/json-schema.git",
- "reference": "e25a8d95b3958abdbe07055833bd69f5f3ed4aeb"
+ "reference": "1d1c6eaa4841f3989e2bec4cdf8167fb0ca42a8f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/api-platform/json-schema/zipball/e25a8d95b3958abdbe07055833bd69f5f3ed4aeb",
- "reference": "e25a8d95b3958abdbe07055833bd69f5f3ed4aeb",
+ "url": "https://api.github.com/repos/api-platform/json-schema/zipball/1d1c6eaa4841f3989e2bec4cdf8167fb0ca42a8f",
+ "reference": "1d1c6eaa4841f3989e2bec4cdf8167fb0ca42a8f",
"shasum": ""
},
"require": {
- "api-platform/metadata": "^4.2@beta",
+ "api-platform/metadata": "^4.1.11",
"php": ">=8.2",
"symfony/console": "^6.4 || ^7.0",
"symfony/property-info": "^6.4 || ^7.1",
"symfony/serializer": "^6.4 || ^7.0",
- "symfony/type-info": "^7.3",
"symfony/uid": "^6.4 || ^7.0"
},
"require-dev": {
@@ -1494,8 +1481,7 @@
"branch-alias": {
"dev-3.4": "3.4.x-dev",
"dev-4.1": "4.1.x-dev",
- "dev-4.2": "4.2.x-dev",
- "dev-main": "4.3.x-dev"
+ "dev-main": "4.2.x-dev"
}
},
"autoload": {
@@ -1532,22 +1518,22 @@
"swagger"
],
"support": {
- "source": "https://github.com/api-platform/json-schema/tree/v4.2.0"
+ "source": "https://github.com/api-platform/json-schema/tree/v4.1.23"
},
- "time": "2025-09-16T12:49:22+00:00"
+ "time": "2025-06-29T12:24:14+00:00"
},
{
"name": "api-platform/jsonld",
- "version": "v4.2.0",
+ "version": "v4.1.23",
"source": {
"type": "git",
"url": "https://github.com/api-platform/jsonld.git",
- "reference": "0f4c79c1f57680cbbcaaf7219e4e0aa2865a2c51"
+ "reference": "e122bf1f04f895e80e6469e0f09d1f06f7508ca6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/api-platform/jsonld/zipball/0f4c79c1f57680cbbcaaf7219e4e0aa2865a2c51",
- "reference": "0f4c79c1f57680cbbcaaf7219e4e0aa2865a2c51",
+ "url": "https://api.github.com/repos/api-platform/jsonld/zipball/e122bf1f04f895e80e6469e0f09d1f06f7508ca6",
+ "reference": "e122bf1f04f895e80e6469e0f09d1f06f7508ca6",
"shasum": ""
},
"require": {
@@ -1557,8 +1543,7 @@
"php": ">=8.2"
},
"require-dev": {
- "phpunit/phpunit": "11.5.x-dev",
- "symfony/type-info": "^7.3"
+ "phpunit/phpunit": "11.5.x-dev"
},
"type": "library",
"extra": {
@@ -1572,8 +1557,7 @@
"branch-alias": {
"dev-3.4": "3.4.x-dev",
"dev-4.1": "4.1.x-dev",
- "dev-4.2": "4.2.x-dev",
- "dev-main": "4.3.x-dev"
+ "dev-main": "4.2.x-dev"
}
},
"autoload": {
@@ -1612,22 +1596,22 @@
"rest"
],
"support": {
- "source": "https://github.com/api-platform/jsonld/tree/v4.2.0"
+ "source": "https://github.com/api-platform/jsonld/tree/v4.1.23"
},
- "time": "2025-09-09T12:23:22+00:00"
+ "time": "2025-07-25T10:05:30+00:00"
},
{
"name": "api-platform/metadata",
- "version": "v4.2.0",
+ "version": "v4.1.23",
"source": {
"type": "git",
"url": "https://github.com/api-platform/metadata.git",
- "reference": "71db1e169d3c0b28d1d3eab5b572720285ad1a6c"
+ "reference": "58b25f9a82c12727afab09b5a311828aacff8e88"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/api-platform/metadata/zipball/71db1e169d3c0b28d1d3eab5b572720285ad1a6c",
- "reference": "71db1e169d3c0b28d1d3eab5b572720285ad1a6c",
+ "url": "https://api.github.com/repos/api-platform/metadata/zipball/58b25f9a82c12727afab09b5a311828aacff8e88",
+ "reference": "58b25f9a82c12727afab09b5a311828aacff8e88",
"shasum": ""
},
"require": {
@@ -1669,8 +1653,7 @@
"branch-alias": {
"dev-3.4": "3.4.x-dev",
"dev-4.1": "4.1.x-dev",
- "dev-4.2": "4.2.x-dev",
- "dev-main": "4.3.x-dev"
+ "dev-main": "4.2.x-dev"
}
},
"autoload": {
@@ -1710,42 +1693,40 @@
"swagger"
],
"support": {
- "source": "https://github.com/api-platform/metadata/tree/v4.2.0"
+ "source": "https://github.com/api-platform/metadata/tree/v4.1.23"
},
- "time": "2025-09-15T12:27:38+00:00"
+ "time": "2025-09-05T09:06:52+00:00"
},
{
"name": "api-platform/openapi",
- "version": "v4.2.0",
+ "version": "v4.1.23",
"source": {
"type": "git",
"url": "https://github.com/api-platform/openapi.git",
- "reference": "8e400e24ef695f17dbeeaeb60442707ba9c1dbd1"
+ "reference": "793b53e51a5c24076d4024b6aa77de29e74015cd"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/api-platform/openapi/zipball/8e400e24ef695f17dbeeaeb60442707ba9c1dbd1",
- "reference": "8e400e24ef695f17dbeeaeb60442707ba9c1dbd1",
+ "url": "https://api.github.com/repos/api-platform/openapi/zipball/793b53e51a5c24076d4024b6aa77de29e74015cd",
+ "reference": "793b53e51a5c24076d4024b6aa77de29e74015cd",
"shasum": ""
},
"require": {
- "api-platform/json-schema": "^4.2@beta",
- "api-platform/metadata": "^4.2@beta",
- "api-platform/state": "^4.2@beta",
+ "api-platform/json-schema": "^4.1.11",
+ "api-platform/metadata": "^4.1.11",
+ "api-platform/state": "^4.1.11",
"php": ">=8.2",
"symfony/console": "^6.4 || ^7.0",
"symfony/filesystem": "^6.4 || ^7.0",
"symfony/property-access": "^6.4 || ^7.0",
- "symfony/serializer": "^6.4 || ^7.0",
- "symfony/type-info": "^7.3"
+ "symfony/serializer": "^6.4 || ^7.0"
},
"require-dev": {
"api-platform/doctrine-common": "^4.1",
"api-platform/doctrine-odm": "^4.1",
"api-platform/doctrine-orm": "^4.1",
"phpspec/prophecy-phpunit": "^2.2",
- "phpunit/phpunit": "11.5.x-dev",
- "symfony/type-info": "^7.3"
+ "phpunit/phpunit": "11.5.x-dev"
},
"type": "library",
"extra": {
@@ -1759,8 +1740,7 @@
"branch-alias": {
"dev-3.4": "3.4.x-dev",
"dev-4.1": "4.1.x-dev",
- "dev-4.2": "4.2.x-dev",
- "dev-main": "4.3.x-dev"
+ "dev-main": "4.2.x-dev"
}
},
"autoload": {
@@ -1800,26 +1780,26 @@
"swagger"
],
"support": {
- "source": "https://github.com/api-platform/openapi/tree/v4.2.0"
+ "source": "https://github.com/api-platform/openapi/tree/v4.1.23"
},
- "time": "2025-09-16T12:49:22+00:00"
+ "time": "2025-07-29T08:53:27+00:00"
},
{
"name": "api-platform/serializer",
- "version": "v4.2.0",
+ "version": "v4.1.23",
"source": {
"type": "git",
"url": "https://github.com/api-platform/serializer.git",
- "reference": "8c94416556df14fd20203975d000c0213a308e2d"
+ "reference": "70dbdeac9584870be444d78c1a796b6edb9e46a5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/api-platform/serializer/zipball/8c94416556df14fd20203975d000c0213a308e2d",
- "reference": "8c94416556df14fd20203975d000c0213a308e2d",
+ "url": "https://api.github.com/repos/api-platform/serializer/zipball/70dbdeac9584870be444d78c1a796b6edb9e46a5",
+ "reference": "70dbdeac9584870be444d78c1a796b6edb9e46a5",
"shasum": ""
},
"require": {
- "api-platform/metadata": "^4.1.16",
+ "api-platform/metadata": "^4.1.11",
"api-platform/state": "^4.1.11",
"php": ">=8.2",
"symfony/property-access": "^6.4 || ^7.0",
@@ -1837,7 +1817,6 @@
"phpspec/prophecy-phpunit": "^2.2",
"phpunit/phpunit": "11.5.x-dev",
"symfony/mercure-bundle": "*",
- "symfony/type-info": "^7.3",
"symfony/var-dumper": "^6.4 || ^7.0",
"symfony/yaml": "^6.4 || ^7.0"
},
@@ -1857,8 +1836,7 @@
"branch-alias": {
"dev-3.4": "3.4.x-dev",
"dev-4.1": "4.1.x-dev",
- "dev-4.2": "4.2.x-dev",
- "dev-main": "4.3.x-dev"
+ "dev-main": "4.2.x-dev"
}
},
"autoload": {
@@ -1893,22 +1871,22 @@
"serializer"
],
"support": {
- "source": "https://github.com/api-platform/serializer/tree/v4.2.0"
+ "source": "https://github.com/api-platform/serializer/tree/v4.1.23"
},
- "time": "2025-09-15T13:20:40+00:00"
+ "time": "2025-08-29T15:13:26+00:00"
},
{
"name": "api-platform/state",
- "version": "v4.2.0",
+ "version": "v4.1.23",
"source": {
"type": "git",
"url": "https://github.com/api-platform/state.git",
- "reference": "439b0c542e4c2e921f909f2f2e01d077ed0248f4"
+ "reference": "056b07285cdc904984fb44c2614f7df8f4620a95"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/api-platform/state/zipball/439b0c542e4c2e921f909f2f2e01d077ed0248f4",
- "reference": "439b0c542e4c2e921f909f2f2e01d077ed0248f4",
+ "url": "https://api.github.com/repos/api-platform/state/zipball/056b07285cdc904984fb44c2614f7df8f4620a95",
+ "reference": "056b07285cdc904984fb44c2614f7df8f4620a95",
"shasum": ""
},
"require": {
@@ -1920,11 +1898,9 @@
"symfony/translation-contracts": "^3.0"
},
"require-dev": {
- "api-platform/serializer": "^4.1",
"api-platform/validator": "^4.1",
"phpunit/phpunit": "11.5.x-dev",
"symfony/http-foundation": "^6.4 || ^7.0",
- "symfony/type-info": "^7.3",
"symfony/web-link": "^6.4 || ^7.1",
"willdurand/negotiation": "^3.1"
},
@@ -1947,8 +1923,7 @@
"branch-alias": {
"dev-3.4": "3.4.x-dev",
"dev-4.1": "4.1.x-dev",
- "dev-4.2": "4.2.x-dev",
- "dev-main": "4.3.x-dev"
+ "dev-main": "4.2.x-dev"
}
},
"autoload": {
@@ -1988,22 +1963,22 @@
"swagger"
],
"support": {
- "source": "https://github.com/api-platform/state/tree/v4.2.0"
+ "source": "https://github.com/api-platform/state/tree/v4.1.23"
},
- "time": "2025-09-17T08:54:53+00:00"
+ "time": "2025-07-16T14:01:52+00:00"
},
{
"name": "api-platform/symfony",
- "version": "v4.2.0",
+ "version": "v4.1.23",
"source": {
"type": "git",
"url": "https://github.com/api-platform/symfony.git",
- "reference": "c3fa7d2176bd9c36e2961d88c4a69a5a7e948b83"
+ "reference": "e35839489b4e76ffc5fc2b0cbadbbaece75b9ad1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/api-platform/symfony/zipball/c3fa7d2176bd9c36e2961d88c4a69a5a7e948b83",
- "reference": "c3fa7d2176bd9c36e2961d88c4a69a5a7e948b83",
+ "url": "https://api.github.com/repos/api-platform/symfony/zipball/e35839489b4e76ffc5fc2b0cbadbbaece75b9ad1",
+ "reference": "e35839489b4e76ffc5fc2b0cbadbbaece75b9ad1",
"shasum": ""
},
"require": {
@@ -2012,13 +1987,12 @@
"api-platform/hydra": "^4.1.11",
"api-platform/json-schema": "^4.1.11",
"api-platform/jsonld": "^4.1.11",
- "api-platform/metadata": "^4.2@beta",
+ "api-platform/metadata": "^4.1.11",
"api-platform/openapi": "^4.1.11",
"api-platform/serializer": "^4.1.11",
- "api-platform/state": "^4.2@beta",
+ "api-platform/state": "^4.1.11",
"api-platform/validator": "^4.1.11",
"php": ">=8.2",
- "symfony/finder": "^6.4 || ^7.0",
"symfony/property-access": "^6.4 || ^7.0",
"symfony/property-info": "^6.4 || ^7.1",
"symfony/security-core": "^6.4 || ^7.0",
@@ -2035,11 +2009,8 @@
"phpspec/prophecy-phpunit": "^2.2",
"phpunit/phpunit": "11.5.x-dev",
"symfony/expression-language": "^6.4 || ^7.0",
- "symfony/intl": "^6.4 || ^7.0",
"symfony/mercure-bundle": "*",
- "symfony/object-mapper": "^7.0",
"symfony/routing": "^6.4 || ^7.0",
- "symfony/type-info": "^7.3",
"symfony/validator": "^6.4 || ^7.0",
"webonyx/graphql-php": "^15.0"
},
@@ -2048,7 +2019,6 @@
"api-platform/doctrine-orm": "To support Doctrine ORM.",
"api-platform/elasticsearch": "To support Elasticsearch.",
"api-platform/graphql": "To support GraphQL.",
- "api-platform/hal": "to support the HAL format",
"api-platform/ramsey-uuid": "To support Ramsey's UUID identifiers.",
"ocramius/package-versions": "To display the API Platform's version in the debug bar.",
"phpstan/phpdoc-parser": "To support extracting metadata from PHPDoc.",
@@ -2076,8 +2046,7 @@
"branch-alias": {
"dev-3.4": "3.4.x-dev",
"dev-4.1": "4.1.x-dev",
- "dev-4.2": "4.2.x-dev",
- "dev-main": "4.3.x-dev"
+ "dev-main": "4.2.x-dev"
}
},
"autoload": {
@@ -2118,22 +2087,22 @@
"symfony"
],
"support": {
- "source": "https://github.com/api-platform/symfony/tree/v4.2.0"
+ "source": "https://github.com/api-platform/symfony/tree/v4.1.23"
},
- "time": "2025-09-16T12:49:22+00:00"
+ "time": "2025-09-05T07:30:37+00:00"
},
{
"name": "api-platform/validator",
- "version": "v4.2.0",
+ "version": "v4.1.23",
"source": {
"type": "git",
"url": "https://github.com/api-platform/validator.git",
- "reference": "562f97b0acdacef462ff9ececd62158ae4709530"
+ "reference": "9f0bde95dccf1d86e6a6165543d601a4a46eaa9a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/api-platform/validator/zipball/562f97b0acdacef462ff9ececd62158ae4709530",
- "reference": "562f97b0acdacef462ff9ececd62158ae4709530",
+ "url": "https://api.github.com/repos/api-platform/validator/zipball/9f0bde95dccf1d86e6a6165543d601a4a46eaa9a",
+ "reference": "9f0bde95dccf1d86e6a6165543d601a4a46eaa9a",
"shasum": ""
},
"require": {
@@ -2141,7 +2110,7 @@
"php": ">=8.2",
"symfony/http-kernel": "^6.4 || ^7.1",
"symfony/serializer": "^6.4 || ^7.1",
- "symfony/type-info": "^7.3",
+ "symfony/type-info": "^7.2",
"symfony/validator": "^6.4 || ^7.1",
"symfony/web-link": "^6.4 || ^7.1"
},
@@ -2161,8 +2130,7 @@
"branch-alias": {
"dev-3.4": "3.4.x-dev",
"dev-4.1": "4.1.x-dev",
- "dev-4.2": "4.2.x-dev",
- "dev-main": "4.3.x-dev"
+ "dev-main": "4.2.x-dev"
}
},
"autoload": {
@@ -2194,9 +2162,9 @@
"validator"
],
"support": {
- "source": "https://github.com/api-platform/validator/tree/v4.2.0"
+ "source": "https://github.com/api-platform/validator/tree/v4.1.23"
},
- "time": "2025-09-05T08:12:26+00:00"
+ "time": "2025-07-16T14:01:52+00:00"
},
{
"name": "beberlei/assert",
@@ -2532,85 +2500,6 @@
],
"time": "2022-01-17T14:14:24+00:00"
},
- {
- "name": "composer/pcre",
- "version": "3.3.2",
- "source": {
- "type": "git",
- "url": "https://github.com/composer/pcre.git",
- "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
- "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
- "shasum": ""
- },
- "require": {
- "php": "^7.4 || ^8.0"
- },
- "conflict": {
- "phpstan/phpstan": "<1.11.10"
- },
- "require-dev": {
- "phpstan/phpstan": "^1.12 || ^2",
- "phpstan/phpstan-strict-rules": "^1 || ^2",
- "phpunit/phpunit": "^8 || ^9"
- },
- "type": "library",
- "extra": {
- "phpstan": {
- "includes": [
- "extension.neon"
- ]
- },
- "branch-alias": {
- "dev-main": "3.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Composer\\Pcre\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Jordi Boggiano",
- "email": "j.boggiano@seld.be",
- "homepage": "http://seld.be"
- }
- ],
- "description": "PCRE wrapping library that offers type-safe preg_* replacements.",
- "keywords": [
- "PCRE",
- "preg",
- "regex",
- "regular expression"
- ],
- "support": {
- "issues": "https://github.com/composer/pcre/issues",
- "source": "https://github.com/composer/pcre/tree/3.3.2"
- },
- "funding": [
- {
- "url": "https://packagist.com",
- "type": "custom"
- },
- {
- "url": "https://github.com/composer",
- "type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/composer/composer",
- "type": "tidelift"
- }
- ],
- "time": "2024-11-12T16:29:46+00:00"
- },
{
"name": "daverandom/libdns",
"version": "v2.1.0",
@@ -3146,16 +3035,16 @@
},
{
"name": "doctrine/doctrine-bundle",
- "version": "2.16.2",
+ "version": "2.16.1",
"source": {
"type": "git",
"url": "https://github.com/doctrine/DoctrineBundle.git",
- "reference": "1c10de0fe995f01eca6b073d1c2549ef0b603a7f"
+ "reference": "152d5083f0cd205a278131dc4351a8c94d007fe1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/1c10de0fe995f01eca6b073d1c2549ef0b603a7f",
- "reference": "1c10de0fe995f01eca6b073d1c2549ef0b603a7f",
+ "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/152d5083f0cd205a278131dc4351a8c94d007fe1",
+ "reference": "152d5083f0cd205a278131dc4351a8c94d007fe1",
"shasum": ""
},
"require": {
@@ -3189,11 +3078,12 @@
"phpstan/phpstan": "2.1.1",
"phpstan/phpstan-phpunit": "2.0.3",
"phpstan/phpstan-strict-rules": "^2",
- "phpunit/phpunit": "^10.5.53",
+ "phpunit/phpunit": "^9.6.22",
"psr/log": "^1.1.4 || ^2.0 || ^3.0",
"symfony/doctrine-messenger": "^6.4 || ^7.0",
"symfony/expression-language": "^6.4 || ^7.0",
"symfony/messenger": "^6.4 || ^7.0",
+ "symfony/phpunit-bridge": "^7.2",
"symfony/property-info": "^6.4 || ^7.0",
"symfony/security-bundle": "^6.4 || ^7.0",
"symfony/stopwatch": "^6.4 || ^7.0",
@@ -3248,7 +3138,7 @@
],
"support": {
"issues": "https://github.com/doctrine/DoctrineBundle/issues",
- "source": "https://github.com/doctrine/DoctrineBundle/tree/2.16.2"
+ "source": "https://github.com/doctrine/DoctrineBundle/tree/2.16.1"
},
"funding": [
{
@@ -3264,7 +3154,7 @@
"type": "tidelift"
}
],
- "time": "2025-09-10T19:14:48+00:00"
+ "time": "2025-09-05T15:24:53+00:00"
},
{
"name": "doctrine/doctrine-migrations-bundle",
@@ -4022,16 +3912,16 @@
},
{
"name": "dompdf/dompdf",
- "version": "v3.1.1",
+ "version": "v3.1.0",
"source": {
"type": "git",
"url": "https://github.com/dompdf/dompdf.git",
- "reference": "794ec856134a73d2a69a474c5d4faa47e1e645b1"
+ "reference": "a51bd7a063a65499446919286fb18b518177155a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/dompdf/dompdf/zipball/794ec856134a73d2a69a474c5d4faa47e1e645b1",
- "reference": "794ec856134a73d2a69a474c5d4faa47e1e645b1",
+ "url": "https://api.github.com/repos/dompdf/dompdf/zipball/a51bd7a063a65499446919286fb18b518177155a",
+ "reference": "a51bd7a063a65499446919286fb18b518177155a",
"shasum": ""
},
"require": {
@@ -4080,9 +3970,9 @@
"homepage": "https://github.com/dompdf/dompdf",
"support": {
"issues": "https://github.com/dompdf/dompdf/issues",
- "source": "https://github.com/dompdf/dompdf/tree/v3.1.1"
+ "source": "https://github.com/dompdf/dompdf/tree/v3.1.0"
},
- "time": "2025-09-20T17:30:31+00:00"
+ "time": "2025-01-15T14:09:04+00:00"
},
{
"name": "dompdf/php-font-lib",
@@ -5381,16 +5271,16 @@
},
{
"name": "knpuniversity/oauth2-client-bundle",
- "version": "v2.19.0",
+ "version": "v2.18.4",
"source": {
"type": "git",
"url": "https://github.com/knpuniversity/oauth2-client-bundle.git",
- "reference": "cd1cb6945a46df81be6e94944872546ca4bf335c"
+ "reference": "2f48e1ff7969ef0252482d0f6af874eca639ea2d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/knpuniversity/oauth2-client-bundle/zipball/cd1cb6945a46df81be6e94944872546ca4bf335c",
- "reference": "cd1cb6945a46df81be6e94944872546ca4bf335c",
+ "url": "https://api.github.com/repos/knpuniversity/oauth2-client-bundle/zipball/2f48e1ff7969ef0252482d0f6af874eca639ea2d",
+ "reference": "2f48e1ff7969ef0252482d0f6af874eca639ea2d",
"shasum": ""
},
"require": {
@@ -5434,9 +5324,9 @@
],
"support": {
"issues": "https://github.com/knpuniversity/oauth2-client-bundle/issues",
- "source": "https://github.com/knpuniversity/oauth2-client-bundle/tree/v2.19.0"
+ "source": "https://github.com/knpuniversity/oauth2-client-bundle/tree/v2.18.4"
},
- "time": "2025-09-17T15:00:36+00:00"
+ "time": "2025-08-18T15:33:00+00:00"
},
{
"name": "lcobucci/clock",
@@ -5766,16 +5656,16 @@
},
{
"name": "league/csv",
- "version": "9.25.0",
+ "version": "9.24.1",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/csv.git",
- "reference": "f856f532866369fb1debe4e7c5a1db185f40ef86"
+ "reference": "e0221a3f16aa2a823047d59fab5809d552e29bc8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/csv/zipball/f856f532866369fb1debe4e7c5a1db185f40ef86",
- "reference": "f856f532866369fb1debe4e7c5a1db185f40ef86",
+ "url": "https://api.github.com/repos/thephpleague/csv/zipball/e0221a3f16aa2a823047d59fab5809d552e29bc8",
+ "reference": "e0221a3f16aa2a823047d59fab5809d552e29bc8",
"shasum": ""
},
"require": {
@@ -5791,7 +5681,7 @@
"phpstan/phpstan-deprecation-rules": "^1.2.1",
"phpstan/phpstan-phpunit": "^1.4.2",
"phpstan/phpstan-strict-rules": "^1.6.2",
- "phpunit/phpunit": "^10.5.16 || ^11.5.22 || ^12.3.6",
+ "phpunit/phpunit": "^10.5.16 || ^11.5.22",
"symfony/var-dumper": "^6.4.8 || ^7.3.0"
},
"suggest": {
@@ -5853,7 +5743,7 @@
"type": "github"
}
],
- "time": "2025-09-11T08:29:08+00:00"
+ "time": "2025-06-25T14:53:51+00:00"
},
{
"name": "league/html-to-markdown",
@@ -6425,188 +6315,6 @@
},
"time": "2023-07-31T13:36:50+00:00"
},
- {
- "name": "maennchen/zipstream-php",
- "version": "2.1.0",
- "source": {
- "type": "git",
- "url": "https://github.com/maennchen/ZipStream-PHP.git",
- "reference": "c4c5803cc1f93df3d2448478ef79394a5981cc58"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/c4c5803cc1f93df3d2448478ef79394a5981cc58",
- "reference": "c4c5803cc1f93df3d2448478ef79394a5981cc58",
- "shasum": ""
- },
- "require": {
- "myclabs/php-enum": "^1.5",
- "php": ">= 7.1",
- "psr/http-message": "^1.0",
- "symfony/polyfill-mbstring": "^1.0"
- },
- "require-dev": {
- "ext-zip": "*",
- "guzzlehttp/guzzle": ">= 6.3",
- "mikey179/vfsstream": "^1.6",
- "phpunit/phpunit": ">= 7.5"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "ZipStream\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Paul Duncan",
- "email": "pabs@pablotron.org"
- },
- {
- "name": "Jonatan MΓ€nnchen",
- "email": "jonatan@maennchen.ch"
- },
- {
- "name": "Jesse Donat",
- "email": "donatj@gmail.com"
- },
- {
- "name": "AndrΓ‘s KolesΓ‘r",
- "email": "kolesar@kolesar.hu"
- }
- ],
- "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.",
- "keywords": [
- "stream",
- "zip"
- ],
- "support": {
- "issues": "https://github.com/maennchen/ZipStream-PHP/issues",
- "source": "https://github.com/maennchen/ZipStream-PHP/tree/2.1.0"
- },
- "funding": [
- {
- "url": "https://github.com/maennchen",
- "type": "github"
- },
- {
- "url": "https://opencollective.com/zipstream",
- "type": "open_collective"
- }
- ],
- "time": "2020-05-30T13:11:16+00:00"
- },
- {
- "name": "markbaker/complex",
- "version": "3.0.2",
- "source": {
- "type": "git",
- "url": "https://github.com/MarkBaker/PHPComplex.git",
- "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
- "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
- "shasum": ""
- },
- "require": {
- "php": "^7.2 || ^8.0"
- },
- "require-dev": {
- "dealerdirect/phpcodesniffer-composer-installer": "dev-master",
- "phpcompatibility/php-compatibility": "^9.3",
- "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
- "squizlabs/php_codesniffer": "^3.7"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Complex\\": "classes/src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Mark Baker",
- "email": "mark@lange.demon.co.uk"
- }
- ],
- "description": "PHP Class for working with complex numbers",
- "homepage": "https://github.com/MarkBaker/PHPComplex",
- "keywords": [
- "complex",
- "mathematics"
- ],
- "support": {
- "issues": "https://github.com/MarkBaker/PHPComplex/issues",
- "source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2"
- },
- "time": "2022-12-06T16:21:08+00:00"
- },
- {
- "name": "markbaker/matrix",
- "version": "3.0.1",
- "source": {
- "type": "git",
- "url": "https://github.com/MarkBaker/PHPMatrix.git",
- "reference": "728434227fe21be27ff6d86621a1b13107a2562c"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c",
- "reference": "728434227fe21be27ff6d86621a1b13107a2562c",
- "shasum": ""
- },
- "require": {
- "php": "^7.1 || ^8.0"
- },
- "require-dev": {
- "dealerdirect/phpcodesniffer-composer-installer": "dev-master",
- "phpcompatibility/php-compatibility": "^9.3",
- "phpdocumentor/phpdocumentor": "2.*",
- "phploc/phploc": "^4.0",
- "phpmd/phpmd": "2.*",
- "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
- "sebastian/phpcpd": "^4.0",
- "squizlabs/php_codesniffer": "^3.7"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Matrix\\": "classes/src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Mark Baker",
- "email": "mark@demon-angel.eu"
- }
- ],
- "description": "PHP Class for working with matrices",
- "homepage": "https://github.com/MarkBaker/PHPMatrix",
- "keywords": [
- "mathematics",
- "matrix",
- "vector"
- ],
- "support": {
- "issues": "https://github.com/MarkBaker/PHPMatrix/issues",
- "source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1"
- },
- "time": "2022-12-02T22:17:43+00:00"
- },
{
"name": "masterminds/html5",
"version": "2.10.0",
@@ -6777,81 +6485,18 @@
],
"time": "2025-03-24T10:02:05+00:00"
},
- {
- "name": "myclabs/php-enum",
- "version": "1.8.5",
- "source": {
- "type": "git",
- "url": "https://github.com/myclabs/php-enum.git",
- "reference": "e7be26966b7398204a234f8673fdad5ac6277802"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/myclabs/php-enum/zipball/e7be26966b7398204a234f8673fdad5ac6277802",
- "reference": "e7be26966b7398204a234f8673fdad5ac6277802",
- "shasum": ""
- },
- "require": {
- "ext-json": "*",
- "php": "^7.3 || ^8.0"
- },
- "require-dev": {
- "phpunit/phpunit": "^9.5",
- "squizlabs/php_codesniffer": "1.*",
- "vimeo/psalm": "^4.6.2 || ^5.2"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "MyCLabs\\Enum\\": "src/"
- },
- "classmap": [
- "stubs/Stringable.php"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "PHP Enum contributors",
- "homepage": "https://github.com/myclabs/php-enum/graphs/contributors"
- }
- ],
- "description": "PHP Enum implementation",
- "homepage": "https://github.com/myclabs/php-enum",
- "keywords": [
- "enum"
- ],
- "support": {
- "issues": "https://github.com/myclabs/php-enum/issues",
- "source": "https://github.com/myclabs/php-enum/tree/1.8.5"
- },
- "funding": [
- {
- "url": "https://github.com/mnapoli",
- "type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/myclabs/php-enum",
- "type": "tidelift"
- }
- ],
- "time": "2025-01-14T11:49:03+00:00"
- },
{
"name": "nbgrp/onelogin-saml-bundle",
- "version": "v2.0.3",
+ "version": "v2.0.2",
"source": {
"type": "git",
"url": "https://github.com/nbgrp/onelogin-saml-bundle.git",
- "reference": "cbf58a8742ee8179dce0547e6f2f826cd19b525f"
+ "reference": "d2feeb7de6ab5b98e69deeea31ad0ceb20a1c4dc"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nbgrp/onelogin-saml-bundle/zipball/cbf58a8742ee8179dce0547e6f2f826cd19b525f",
- "reference": "cbf58a8742ee8179dce0547e6f2f826cd19b525f",
+ "url": "https://api.github.com/repos/nbgrp/onelogin-saml-bundle/zipball/d2feeb7de6ab5b98e69deeea31ad0ceb20a1c4dc",
+ "reference": "d2feeb7de6ab5b98e69deeea31ad0ceb20a1c4dc",
"shasum": ""
},
"require": {
@@ -6899,9 +6544,9 @@
],
"support": {
"issues": "https://github.com/nbgrp/onelogin-saml-bundle/issues",
- "source": "https://github.com/nbgrp/onelogin-saml-bundle/tree/v2.0.3"
+ "source": "https://github.com/nbgrp/onelogin-saml-bundle/tree/v2.0.2"
},
- "time": "2025-09-19T14:08:21+00:00"
+ "time": "2024-09-01T22:16:27+00:00"
},
{
"name": "nelexa/zip",
@@ -7040,16 +6685,16 @@
},
{
"name": "nelmio/security-bundle",
- "version": "v3.6.0",
+ "version": "v3.5.1",
"source": {
"type": "git",
"url": "https://github.com/nelmio/NelmioSecurityBundle.git",
- "reference": "f3a7bf628a0873788172a0d05d20c0224080f5eb"
+ "reference": "b1c5e323d71152bc1a61a4f8fbf7d88c6fa3e2e7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nelmio/NelmioSecurityBundle/zipball/f3a7bf628a0873788172a0d05d20c0224080f5eb",
- "reference": "f3a7bf628a0873788172a0d05d20c0224080f5eb",
+ "url": "https://api.github.com/repos/nelmio/NelmioSecurityBundle/zipball/b1c5e323d71152bc1a61a4f8fbf7d88c6fa3e2e7",
+ "reference": "b1c5e323d71152bc1a61a4f8fbf7d88c6fa3e2e7",
"shasum": ""
},
"require": {
@@ -7108,9 +6753,9 @@
],
"support": {
"issues": "https://github.com/nelmio/NelmioSecurityBundle/issues",
- "source": "https://github.com/nelmio/NelmioSecurityBundle/tree/v3.6.0"
+ "source": "https://github.com/nelmio/NelmioSecurityBundle/tree/v3.5.1"
},
- "time": "2025-09-19T08:24:46+00:00"
+ "time": "2025-03-13T09:17:16+00:00"
},
{
"name": "nette/schema",
@@ -7705,16 +7350,16 @@
},
{
"name": "paragonie/sodium_compat",
- "version": "v1.21.2",
+ "version": "v1.21.1",
"source": {
"type": "git",
"url": "https://github.com/paragonie/sodium_compat.git",
- "reference": "d3043fd10faacb72e9eeb2df4c21a13214b45c33"
+ "reference": "bb312875dcdd20680419564fe42ba1d9564b9e37"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/d3043fd10faacb72e9eeb2df4c21a13214b45c33",
- "reference": "d3043fd10faacb72e9eeb2df4c21a13214b45c33",
+ "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/bb312875dcdd20680419564fe42ba1d9564b9e37",
+ "reference": "bb312875dcdd20680419564fe42ba1d9564b9e37",
"shasum": ""
},
"require": {
@@ -7785,9 +7430,9 @@
],
"support": {
"issues": "https://github.com/paragonie/sodium_compat/issues",
- "source": "https://github.com/paragonie/sodium_compat/tree/v1.21.2"
+ "source": "https://github.com/paragonie/sodium_compat/tree/v1.21.1"
},
- "time": "2025-09-19T16:14:19+00:00"
+ "time": "2024-04-22T22:05:04+00:00"
},
{
"name": "part-db/exchanger",
@@ -8465,112 +8110,6 @@
},
"time": "2024-11-09T15:12:26+00:00"
},
- {
- "name": "phpoffice/phpspreadsheet",
- "version": "5.1.0",
- "source": {
- "type": "git",
- "url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
- "reference": "fd26e45a814e94ae2aad0df757d9d1739c4bf2e0"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/fd26e45a814e94ae2aad0df757d9d1739c4bf2e0",
- "reference": "fd26e45a814e94ae2aad0df757d9d1739c4bf2e0",
- "shasum": ""
- },
- "require": {
- "composer/pcre": "^1||^2||^3",
- "ext-ctype": "*",
- "ext-dom": "*",
- "ext-fileinfo": "*",
- "ext-gd": "*",
- "ext-iconv": "*",
- "ext-libxml": "*",
- "ext-mbstring": "*",
- "ext-simplexml": "*",
- "ext-xml": "*",
- "ext-xmlreader": "*",
- "ext-xmlwriter": "*",
- "ext-zip": "*",
- "ext-zlib": "*",
- "maennchen/zipstream-php": "^2.1 || ^3.0",
- "markbaker/complex": "^3.0",
- "markbaker/matrix": "^3.0",
- "php": "^8.1",
- "psr/http-client": "^1.0",
- "psr/http-factory": "^1.0",
- "psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
- },
- "require-dev": {
- "dealerdirect/phpcodesniffer-composer-installer": "dev-main",
- "dompdf/dompdf": "^2.0 || ^3.0",
- "friendsofphp/php-cs-fixer": "^3.2",
- "mitoteam/jpgraph": "^10.3",
- "mpdf/mpdf": "^8.1.1",
- "phpcompatibility/php-compatibility": "^9.3",
- "phpstan/phpstan": "^1.1 || ^2.0",
- "phpstan/phpstan-deprecation-rules": "^1.0 || ^2.0",
- "phpstan/phpstan-phpunit": "^1.0 || ^2.0",
- "phpunit/phpunit": "^10.5",
- "squizlabs/php_codesniffer": "^3.7",
- "tecnickcom/tcpdf": "^6.5"
- },
- "suggest": {
- "dompdf/dompdf": "Option for rendering PDF with PDF Writer",
- "ext-intl": "PHP Internationalization Functions",
- "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers",
- "mpdf/mpdf": "Option for rendering PDF with PDF Writer",
- "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Maarten Balliauw",
- "homepage": "https://blog.maartenballiauw.be"
- },
- {
- "name": "Mark Baker",
- "homepage": "https://markbakeruk.net"
- },
- {
- "name": "Franck Lefevre",
- "homepage": "https://rootslabs.net"
- },
- {
- "name": "Erik Tilt"
- },
- {
- "name": "Adrien Crivelli"
- }
- ],
- "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
- "homepage": "https://github.com/PHPOffice/PhpSpreadsheet",
- "keywords": [
- "OpenXML",
- "excel",
- "gnumeric",
- "ods",
- "php",
- "spreadsheet",
- "xls",
- "xlsx"
- ],
- "support": {
- "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
- "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/5.1.0"
- },
- "time": "2025-09-04T05:34:49+00:00"
- },
{
"name": "phpstan/phpdoc-parser",
"version": "2.3.0",
@@ -8927,16 +8466,16 @@
},
{
"name": "psr/http-message",
- "version": "1.1",
+ "version": "2.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-message.git",
- "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba"
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba",
- "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba",
+ "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
"shasum": ""
},
"require": {
@@ -8945,7 +8484,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.1.x-dev"
+ "dev-master": "2.0.x-dev"
}
},
"autoload": {
@@ -8960,7 +8499,7 @@
"authors": [
{
"name": "PHP-FIG",
- "homepage": "http://www.php-fig.org/"
+ "homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for HTTP messages",
@@ -8974,9 +8513,9 @@
"response"
],
"support": {
- "source": "https://github.com/php-fig/http-message/tree/1.1"
+ "source": "https://github.com/php-fig/http-message/tree/2.0"
},
- "time": "2023-04-04T09:50:52+00:00"
+ "time": "2023-04-04T09:54:51+00:00"
},
{
"name": "psr/link",
@@ -16434,16 +15973,16 @@
},
{
"name": "symplify/easy-coding-standard",
- "version": "12.6.0",
+ "version": "12.5.24",
"source": {
"type": "git",
"url": "https://github.com/easy-coding-standard/easy-coding-standard.git",
- "reference": "781e6124dc7e14768ae999a8f5309566bbe62004"
+ "reference": "4b90f2b6efed9508000968eac2397ac7aff34354"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/easy-coding-standard/easy-coding-standard/zipball/781e6124dc7e14768ae999a8f5309566bbe62004",
- "reference": "781e6124dc7e14768ae999a8f5309566bbe62004",
+ "url": "https://api.github.com/repos/easy-coding-standard/easy-coding-standard/zipball/4b90f2b6efed9508000968eac2397ac7aff34354",
+ "reference": "4b90f2b6efed9508000968eac2397ac7aff34354",
"shasum": ""
},
"require": {
@@ -16479,7 +16018,7 @@
],
"support": {
"issues": "https://github.com/easy-coding-standard/easy-coding-standard/issues",
- "source": "https://github.com/easy-coding-standard/easy-coding-standard/tree/12.6.0"
+ "source": "https://github.com/easy-coding-standard/easy-coding-standard/tree/12.5.24"
},
"funding": [
{
@@ -16491,7 +16030,7 @@
"type": "github"
}
],
- "time": "2025-09-10T14:21:58+00:00"
+ "time": "2025-08-21T06:57:14+00:00"
},
{
"name": "tecnickcom/tc-lib-barcode",
@@ -18286,16 +17825,16 @@
},
{
"name": "phpstan/phpstan",
- "version": "2.1.28",
+ "version": "2.1.22",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
- "reference": "578fa296a166605d97b94091f724f1257185d278"
+ "reference": "41600c8379eb5aee63e9413fe9e97273e25d57e4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpstan/zipball/578fa296a166605d97b94091f724f1257185d278",
- "reference": "578fa296a166605d97b94091f724f1257185d278",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/41600c8379eb5aee63e9413fe9e97273e25d57e4",
+ "reference": "41600c8379eb5aee63e9413fe9e97273e25d57e4",
"shasum": ""
},
"require": {
@@ -18340,20 +17879,20 @@
"type": "github"
}
],
- "time": "2025-09-19T08:58:49+00:00"
+ "time": "2025-08-04T19:17:37+00:00"
},
{
"name": "phpstan/phpstan-doctrine",
- "version": "2.0.6",
+ "version": "2.0.5",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan-doctrine.git",
- "reference": "934f5734812341358fc41c44006b30fa00c785f0"
+ "reference": "eeff19808f8ae3a6f7c4e43e388a2848eb2b0865"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/934f5734812341358fc41c44006b30fa00c785f0",
- "reference": "934f5734812341358fc41c44006b30fa00c785f0",
+ "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/eeff19808f8ae3a6f7c4e43e388a2848eb2b0865",
+ "reference": "eeff19808f8ae3a6f7c4e43e388a2848eb2b0865",
"shasum": ""
},
"require": {
@@ -18410,9 +17949,9 @@
"description": "Doctrine extensions for PHPStan",
"support": {
"issues": "https://github.com/phpstan/phpstan-doctrine/issues",
- "source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.6"
+ "source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.5"
},
- "time": "2025-09-10T07:06:30+00:00"
+ "time": "2025-09-07T11:52:30+00:00"
},
{
"name": "phpstan/phpstan-strict-rules",
@@ -18870,16 +18409,16 @@
},
{
"name": "phpunit/phpunit",
- "version": "11.5.39",
+ "version": "11.5.36",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "ad5597f79d8489d2870073ac0bc0dd0ad1fa9931"
+ "reference": "264a87c7ef68b1ab9af7172357740dc266df5957"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/ad5597f79d8489d2870073ac0bc0dd0ad1fa9931",
- "reference": "ad5597f79d8489d2870073ac0bc0dd0ad1fa9931",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/264a87c7ef68b1ab9af7172357740dc266df5957",
+ "reference": "264a87c7ef68b1ab9af7172357740dc266df5957",
"shasum": ""
},
"require": {
@@ -18951,7 +18490,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
- "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.39"
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.36"
},
"funding": [
{
@@ -18975,20 +18514,20 @@
"type": "tidelift"
}
],
- "time": "2025-09-14T06:20:41+00:00"
+ "time": "2025-09-03T06:24:17+00:00"
},
{
"name": "rector/rector",
- "version": "2.1.7",
+ "version": "2.1.6",
"source": {
"type": "git",
"url": "https://github.com/rectorphp/rector.git",
- "reference": "c34cc07c4698f007a20dc5c99ff820089ae413ce"
+ "reference": "729aabc0ec66e700ef164e26454a1357f222a2f3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/rectorphp/rector/zipball/c34cc07c4698f007a20dc5c99ff820089ae413ce",
- "reference": "c34cc07c4698f007a20dc5c99ff820089ae413ce",
+ "url": "https://api.github.com/repos/rectorphp/rector/zipball/729aabc0ec66e700ef164e26454a1357f222a2f3",
+ "reference": "729aabc0ec66e700ef164e26454a1357f222a2f3",
"shasum": ""
},
"require": {
@@ -19027,7 +18566,7 @@
],
"support": {
"issues": "https://github.com/rectorphp/rector/issues",
- "source": "https://github.com/rectorphp/rector/tree/2.1.7"
+ "source": "https://github.com/rectorphp/rector/tree/2.1.6"
},
"funding": [
{
@@ -19035,7 +18574,7 @@
"type": "github"
}
],
- "time": "2025-09-10T11:13:58+00:00"
+ "time": "2025-09-05T15:43:08+00:00"
},
{
"name": "roave/security-advisories",
@@ -19043,12 +18582,12 @@
"source": {
"type": "git",
"url": "https://github.com/Roave/SecurityAdvisories.git",
- "reference": "f48b3e601515b060334744b4b495f0d6b3cc2e6b"
+ "reference": "dc5c4ede5c331ae21fb68947ff89672df9b7cc7d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/f48b3e601515b060334744b4b495f0d6b3cc2e6b",
- "reference": "f48b3e601515b060334744b4b495f0d6b3cc2e6b",
+ "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/dc5c4ede5c331ae21fb68947ff89672df9b7cc7d",
+ "reference": "dc5c4ede5c331ae21fb68947ff89672df9b7cc7d",
"shasum": ""
},
"conflict": {
@@ -19186,7 +18725,6 @@
"dapphp/securimage": "<3.6.6",
"darylldoyle/safe-svg": "<1.9.10",
"datadog/dd-trace": ">=0.30,<0.30.2",
- "datahihi1/tiny-env": "<1.0.3|>=1.0.9,<1.0.11",
"datatables/datatables": "<1.10.10",
"david-garcia/phpwhois": "<=4.3.1",
"dbrisinajumi/d2files": "<1",
@@ -19437,7 +18975,6 @@
"laravel/socialite": ">=1,<2.0.10",
"latte/latte": "<2.10.8",
"lavalite/cms": "<=9|==10.1",
- "lavitto/typo3-form-to-database": "<2.2.5|>=3,<3.2.2|>=4,<4.2.3|>=5,<5.0.2",
"lcobucci/jwt": ">=3.4,<3.4.6|>=4,<4.0.4|>=4.1,<4.1.5",
"league/commonmark": "<2.7",
"league/flysystem": "<1.1.4|>=2,<2.1.1",
@@ -19459,14 +18996,13 @@
"luyadev/yii-helpers": "<1.2.1",
"macropay-solutions/laravel-crud-wizard-free": "<3.4.17",
"maestroerror/php-heic-to-jpg": "<1.0.5",
- "magento/community-edition": "<=2.4.5.0-patch14|==2.4.6|>=2.4.6.0-patch1,<=2.4.6.0-patch12|>=2.4.7.0-beta1,<=2.4.7.0-patch7|>=2.4.8.0-beta1,<=2.4.8.0-patch2|>=2.4.9.0-alpha1,<=2.4.9.0-alpha2|==2.4.9",
+ "magento/community-edition": "<2.4.5.0-patch14|==2.4.6|>=2.4.6.0-patch1,<2.4.6.0-patch12|>=2.4.7.0-beta1,<2.4.7.0-patch7|>=2.4.8.0-beta1,<2.4.8.0-patch1",
"magento/core": "<=1.9.4.5",
"magento/magento1ce": "<1.9.4.3-dev",
"magento/magento1ee": ">=1,<1.14.4.3-dev",
"magento/product-community-edition": "<2.4.4.0-patch9|>=2.4.5,<2.4.5.0-patch8|>=2.4.6,<2.4.6.0-patch6|>=2.4.7,<2.4.7.0-patch1",
"magento/project-community-edition": "<=2.0.2",
"magneto/core": "<1.9.4.4-dev",
- "mahocommerce/maho": "<25.9",
"maikuolan/phpmussel": ">=1,<1.6",
"mainwp/mainwp": "<=4.4.3.3",
"manogi/nova-tiptap": "<=3.2.6",
@@ -19679,10 +19215,10 @@
"setasign/fpdi": "<2.6.4",
"sfroemken/url_redirect": "<=1.2.1",
"sheng/yiicms": "<1.2.1",
- "shopware/core": "<6.5.8.18-dev|>=6.6,<6.6.10.3-dev|>=6.7,<6.7.2.1-dev",
+ "shopware/core": "<6.5.8.18-dev|>=6.6,<6.6.10.3-dev|>=6.7.0.0-RC1-dev,<6.7.0.0-RC2-dev",
"shopware/platform": "<=6.6.10.4|>=6.7.0.0-RC1-dev,<6.7.0.0-RC2-dev",
"shopware/production": "<=6.3.5.2",
- "shopware/shopware": "<=5.7.17|>=6.7,<6.7.2.1-dev",
+ "shopware/shopware": "<=5.7.17",
"shopware/storefront": "<=6.4.8.1|>=6.5.8,<6.5.8.7-dev",
"shopxo/shopxo": "<=6.4",
"showdoc/showdoc": "<2.10.4",
@@ -19724,7 +19260,7 @@
"slim/slim": "<2.6",
"slub/slub-events": "<3.0.3",
"smarty/smarty": "<4.5.3|>=5,<5.1.1",
- "snipe/snipe-it": "<8.1.18",
+ "snipe/snipe-it": "<8.1",
"socalnick/scn-social-auth": "<1.15.2",
"socialiteproviders/steam": "<1.1",
"solspace/craft-freeform": ">=5,<5.10.16",
@@ -19830,14 +19366,14 @@
"tribalsystems/zenario": "<=9.7.61188",
"truckersmp/phpwhois": "<=4.3.1",
"ttskch/pagination-service-provider": "<1",
- "twbs/bootstrap": "<3.4.1|>=4,<=4.6.2",
+ "twbs/bootstrap": "<=3.4.1|>=4,<=4.6.2",
"twig/twig": "<3.11.2|>=3.12,<3.14.1|>=3.16,<3.19",
"typo3/cms": "<9.5.29|>=10,<10.4.35|>=11,<11.5.23|>=12,<12.2",
- "typo3/cms-backend": "<4.1.14|>=4.2,<4.2.15|>=4.3,<4.3.7|>=4.4,<4.4.4|>=7,<=7.6.50|>=8,<=8.7.39|>=9,<9.5.55|>=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18",
+ "typo3/cms-backend": "<4.1.14|>=4.2,<4.2.15|>=4.3,<4.3.7|>=4.4,<4.4.4|>=7,<=7.6.50|>=8,<=8.7.39|>=9,<=9.5.24|>=10,<10.4.46|>=11,<11.5.40|>=12,<=12.4.30|>=13,<=13.4.11",
"typo3/cms-belog": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2",
- "typo3/cms-beuser": ">=9,<9.5.55|>=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18",
- "typo3/cms-core": "<=8.7.56|>=9,<9.5.55|>=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18",
- "typo3/cms-dashboard": ">=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18",
+ "typo3/cms-beuser": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2",
+ "typo3/cms-core": "<=8.7.56|>=9,<=9.5.50|>=10,<=10.4.49|>=11,<=11.5.43|>=12,<=12.4.30|>=13,<=13.4.11",
+ "typo3/cms-dashboard": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2",
"typo3/cms-extbase": "<6.2.24|>=7,<7.6.8|==8.1.1",
"typo3/cms-extensionmanager": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2",
"typo3/cms-felogin": ">=4.2,<4.2.3",
@@ -19847,13 +19383,10 @@
"typo3/cms-indexed-search": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2",
"typo3/cms-install": "<4.1.14|>=4.2,<4.2.16|>=4.3,<4.3.9|>=4.4,<4.4.5|>=12.2,<12.4.8|==13.4.2",
"typo3/cms-lowlevel": ">=11,<=11.5.41",
- "typo3/cms-recordlist": ">=11,<11.5.48",
- "typo3/cms-recycler": ">=9,<9.5.55|>=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18",
"typo3/cms-rte-ckeditor": ">=9.5,<9.5.42|>=10,<10.4.39|>=11,<11.5.30",
"typo3/cms-scheduler": ">=11,<=11.5.41",
"typo3/cms-setup": ">=9,<=9.5.50|>=10,<=10.4.49|>=11,<=11.5.43|>=12,<=12.4.30|>=13,<=13.4.11",
"typo3/cms-webhooks": ">=12,<=12.4.30|>=13,<=13.4.11",
- "typo3/cms-workspaces": ">=9,<9.5.55|>=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18",
"typo3/flow": ">=1,<1.0.4|>=1.1,<1.1.1|>=2,<2.0.1|>=2.3,<2.3.16|>=3,<3.0.12|>=3.1,<3.1.10|>=3.2,<3.2.13|>=3.3,<3.3.13|>=4,<4.0.6",
"typo3/html-sanitizer": ">=1,<=1.5.2|>=2,<=2.1.3",
"typo3/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4|>=2.3,<2.3.99|>=3,<3.0.20|>=3.1,<3.1.18|>=3.2,<3.2.14|>=3.3,<3.3.23|>=4,<4.0.17|>=4.1,<4.1.16|>=4.2,<4.2.12|>=4.3,<4.3.3",
@@ -19914,7 +19447,7 @@
"xataface/xataface": "<3",
"xpressengine/xpressengine": "<3.0.15",
"yab/quarx": "<2.4.5",
- "yeswiki/yeswiki": "<=4.5.4",
+ "yeswiki/yeswiki": "<4.5.4",
"yetiforce/yetiforce-crm": "<6.5",
"yidashi/yii2cmf": "<=2",
"yii2mod/yii2-cms": "<1.9.2",
@@ -20006,7 +19539,7 @@
"type": "tidelift"
}
],
- "time": "2025-09-19T18:07:33+00:00"
+ "time": "2025-09-04T20:05:35+00:00"
},
{
"name": "sebastian/cli-parser",
@@ -21516,9 +21049,9 @@
"ext-json": "*",
"ext-mbstring": "*"
},
- "platform-dev": {},
+ "platform-dev": [],
"platform-overrides": {
"php": "8.2.0"
},
- "plugin-api-version": "2.6.0"
+ "plugin-api-version": "2.3.0"
}
diff --git a/config/packages/nelmio_security.yaml b/config/packages/nelmio_security.yaml
index 6b2b7337..c283cd8e 100644
--- a/config/packages/nelmio_security.yaml
+++ b/config/packages/nelmio_security.yaml
@@ -20,6 +20,12 @@ nelmio_security:
- 'digikey.com'
- 'nexar.com'
+ # forces Microsoft's XSS-Protection with
+ # its block mode
+ xss_protection:
+ enabled: true
+ mode_block: true
+
# Send a full URL in the `Referer` header when performing a same-origin request,
# only send the origin of the document to secure destination (HTTPS->HTTPS),
# and send no header to a less secure destination (HTTPS->HTTP).
diff --git a/config/parameters.yaml b/config/parameters.yaml
index 5b40899d..154fbd8a 100644
--- a/config/parameters.yaml
+++ b/config/parameters.yaml
@@ -104,9 +104,3 @@ parameters:
env(SAML_ROLE_MAPPING): '{}'
env(DATABASE_EMULATE_NATURAL_SORT): 0
-
- ######################################################################################################################
- # Bulk Info Provider Import Configuration
- ######################################################################################################################
- partdb.bulk_import.batch_size: 20 # Number of parts to process in each batch during bulk operations
- partdb.bulk_import.max_parts_per_operation: 1000 # Maximum number of parts allowed per bulk import operation
diff --git a/docs/assets/usage/import_export/part_import_example.csv b/docs/assets/usage/import_export/part_import_example.csv
index 14d4500f..08701426 100644
--- a/docs/assets/usage/import_export/part_import_example.csv
+++ b/docs/assets/usage/import_export/part_import_example.csv
@@ -1,7 +1,4 @@
-name;description;category;notes;footprint;tags;quantity;storage_location;mass;ipn;mpn;manufacturing_status;manufacturer;supplier;spn;price;favorite;needs_review;minamount;partUnit;eda_info.reference_prefix;eda_info.value;eda_info.visibility;eda_info.exclude_from_bom;eda_info.exclude_from_board;eda_info.exclude_from_sim;eda_info.kicad_symbol;eda_info.kicad_footprint
-"MLCC; 0603; 0.22uF";Multilayer ceramic capacitor;Electrical Components->Passive Components->Capacitors_SMD;High quality MLCC;0603;Capacitor,SMD,MLCC,0603;500;Room 1->Shelf 1->Box 2;0.1;CL10B224KO8NNNC;CL10B224KO8NNNC;active;Samsung;LCSC;C160828;0.0023;0;0;1;pcs;C;0.22uF;1;0;0;0;Device:C;Capacitor_SMD:C_0603_1608Metric
-"MLCC; 0402; 10pF";Small MLCC for high frequency;Electrical Components->Passive Components->Capacitors_SMD;;0402;Capacitor,SMD,MLCC,0402;500;Room 1->Shelf 1->Box 3;0.05;FCC0402N100J500AT;FCC0402N100J500AT;active;Fenghua;LCSC;C5137557;0.0015;0;0;1;pcs;C;10pF;1;0;0;0;Device:C;Capacitor_SMD:C_0402_1005Metric
-"Diode; 1N4148W";Fast switching diode;Electrical Components->Semiconductors->Diodes;Fast recovery time;Diode_SMD:D_SOD-123;Diode,SMD,Schottky;100;Room 2->Box 1;0.2;1N4148W;1N4148W;active;Vishay;LCSC;C917030;0.008;0;0;1;pcs;D;1N4148W;1;0;0;0;Device:D;Diode_SMD:D_SOD-123
-BC547;NPN transistor;Transistors->NPN;very important notes;TO->TO-92;NPN,Transistor;5;Room 1->Shelf 1->Box 2;10;BC547;BC547;active;Generic;LCSC;BC547C;2.3;0;0;1;pcs;Q;BC547;1;0;0;0;Device:Q_NPN_EBC;TO_SOT_Packages_SMD:TO-92_HandSolder
-BC557;PNP transistor;Transistors->PNP;PNP complement to BC547;TO->TO-92;PNP,Transistor;10;Room 2->Box 3;10;BC557;BC557;active;Generic;LCSC;BC557C;2.1;0;0;1;pcs;Q;BC557;1;0;0;0;Device:Q_PNP_EBC;TO_SOT_Packages_SMD:TO-92_HandSolder
-Copper Wire;Bare copper wire;Wire->Copper;For prototyping;Wire;Wire,Copper;50;Room 3->Spool Rack;0.5;CW-22AWG;CW-22AWG;active;Generic;Local Supplier;LS-CW-22;0.15;0;0;1;Meter;W;22AWG;1;0;0;0;Device:Wire;Connector_PinHeader_2.54mm:PinHeader_1x01_P2.54mm_Vertical
+name;description;category;notes;footprint;tags;quantity;storage_location;mass;ipn;mpn;manufacturing_status;manufacturer;supplier;spn;price;favorite;needs_review;minamount;partUnit;manufacturing_status
+BC547;NPN transistor;Transistors -> NPN;very important notes;TO -> TO-92;NPN,Transistor;5;Room 1 -> Shelf 1 -> Box 2;10;;;Manufacturer;;You need to fill this line, to use spn and price;BC547C;2,3;0;;;;
+BC557;PNP transistor;HTML ;;TO -> TO-92;PNP,Transistor;10;Room 2-> Box 3;;Internal1234;;;;;;;;1;;;active
+Copper Wire;;Wire;;;;;;;;;;;;;;;;;Meter;
\ No newline at end of file
diff --git a/docs/usage/import_export.md b/docs/usage/import_export.md
index 136624e2..0534221f 100644
--- a/docs/usage/import_export.md
+++ b/docs/usage/import_export.md
@@ -142,9 +142,6 @@ You can select between the following export formats:
efficiently.
* **YAML** (Yet Another Markup Language): Very similar to JSON
* **XML** (Extensible Markup Language): Good support with nested data structures. Similar use cases as JSON and YAML.
-* **Excel**: Similar to CSV, but in a native Excel format. Can be opened in Excel and LibreOffice Calc. Does not support nested
- data structures or sub-data (like parameters, attachments, etc.), very well (many columns are generated, as every
- possible sub-data is exported as a separate column).
Also, you can select between the following export levels:
diff --git a/docs/usage/information_provider_system.md b/docs/usage/information_provider_system.md
index bc6fe76e..953db409 100644
--- a/docs/usage/information_provider_system.md
+++ b/docs/usage/information_provider_system.md
@@ -68,13 +68,6 @@ If you already have attachment types for images and datasheets and want the info
can
add the alternative names "Datasheet" and "Image" to the alternative names field of the attachment types.
-## Bulk import
-
-If you want to update the information of multiple parts, you can use the bulk import system: Go to a part table and select
-the parts you want to update. In the bulk actions dropdown select "Bulk info provider import" and click "Apply".
-You will be redirected to a page, where you can select how part fields should be mapped to info provider fields, and the
-results will be shown.
-
## Data providers
The system tries to be as flexible as possible, so many different information sources can be used.
diff --git a/makefile b/makefile
index bc4d0bf3..9041ba0f 100644
--- a/makefile
+++ b/makefile
@@ -1,91 +1,112 @@
# PartDB Makefile for Test Environment Management
-.PHONY: help deps-install lint format format-check test coverage pre-commit all test-typecheck \
-test-setup test-clean test-db-create test-db-migrate test-cache-clear test-fixtures test-run test-reset \
-section-dev dev-setup dev-clean dev-db-create dev-db-migrate dev-cache-clear dev-warmup dev-reset
+.PHONY: help test-setup test-clean test-db-create test-db-migrate test-cache-clear test-fixtures test-run dev-setup dev-clean dev-db-create dev-db-migrate dev-cache-clear dev-warmup dev-reset deps-install
# Default target
-help: ## Show this help
- @awk 'BEGIN {FS = ":.*##"}; /^[a-zA-Z0-9][a-zA-Z0-9_-]+:.*##/ {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
+help:
+ @echo "PartDB Test Environment Management"
+ @echo "=================================="
+ @echo ""
+ @echo "Available targets:"
+ @echo " deps-install - Install PHP dependencies with unlimited memory"
+ @echo ""
+ @echo "Development Environment:"
+ @echo " dev-setup - Complete development environment setup (clean, create DB, migrate, warmup)"
+ @echo " dev-clean - Clean development cache and database files"
+ @echo " dev-db-create - Create development database (if not exists)"
+ @echo " dev-db-migrate - Run database migrations for development environment"
+ @echo " dev-cache-clear - Clear development cache"
+ @echo " dev-warmup - Warm up development cache"
+ @echo " dev-reset - Quick development reset (clean + migrate)"
+ @echo ""
+ @echo "Test Environment:"
+ @echo " test-setup - Complete test environment setup (clean, create DB, migrate, load fixtures)"
+ @echo " test-clean - Clean test cache and database files"
+ @echo " test-db-create - Create test database (if not exists)"
+ @echo " test-db-migrate - Run database migrations for test environment"
+ @echo " test-cache-clear- Clear test cache"
+ @echo " test-fixtures - Load test fixtures"
+ @echo " test-run - Run PHPUnit tests"
+ @echo ""
+ @echo " help - Show this help message"
-# Dependencies
-deps-install: ## Install PHP dependencies with unlimited memory
+# Install PHP dependencies with unlimited memory
+deps-install:
@echo "π¦ Installing PHP dependencies..."
COMPOSER_MEMORY_LIMIT=-1 composer install
- yarn install
@echo "β
Dependencies installed"
# Complete test environment setup
-test-setup: test-clean test-db-create test-db-migrate test-fixtures ## Complete test setup (clean, create DB, migrate, fixtures)
+test-setup: deps-install test-clean test-db-create test-db-migrate test-fixtures
@echo "β
Test environment setup complete!"
# Clean test environment
-test-clean: ## Clean test cache and database files
+test-clean:
@echo "π§Ή Cleaning test environment..."
rm -rf var/cache/test
rm -f var/app_test.db
@echo "β
Test environment cleaned"
# Create test database
-test-db-create: ## Create test database (if not exists)
+test-db-create:
@echo "ποΈ Creating test database..."
-php bin/console doctrine:database:create --if-not-exists --env test || echo "β οΈ Database creation failed (expected for SQLite) - continuing..."
# Run database migrations for test environment
-test-db-migrate: ## Run database migrations for test environment
+test-db-migrate:
@echo "π Running database migrations..."
- COMPOSER_MEMORY_LIMIT=-1 php bin/console doctrine:migrations:migrate -n --env test
+ php -d memory_limit=1G bin/console doctrine:migrations:migrate -n --env test
# Clear test cache
-test-cache-clear: ## Clear test cache
+test-cache-clear:
@echo "ποΈ Clearing test cache..."
rm -rf var/cache/test
@echo "β
Test cache cleared"
# Load test fixtures
-test-fixtures: ## Load test fixtures
+test-fixtures:
@echo "π¦ Loading test fixtures..."
php bin/console partdb:fixtures:load -n --env test
# Run PHPUnit tests
-test-run: ## Run PHPUnit tests
+test-run:
@echo "π§ͺ Running tests..."
php bin/phpunit
+test-typecheck:
+ @echo "π§ͺ Running type checks..."
+ COMPOSER_MEMORY_LIMIT=-1 composer phpstan
+
# Quick test reset (clean + migrate + fixtures, skip DB creation)
test-reset: test-cache-clear test-db-migrate test-fixtures
@echo "β
Test environment reset complete!"
-test-typecheck: ## Run static analysis (PHPStan)
- @echo "π§ͺ Running type checks..."
- COMPOSER_MEMORY_LIMIT=-1 composer phpstan
-
# Development helpers
-dev-setup: dev-clean dev-db-create dev-db-migrate dev-warmup ## Complete development setup (clean, create DB, migrate, warmup)
+dev-setup: deps-install dev-clean dev-db-create dev-db-migrate dev-warmup
@echo "β
Development environment setup complete!"
-dev-clean: ## Clean development cache and database files
+dev-clean:
@echo "π§Ή Cleaning development environment..."
rm -rf var/cache/dev
rm -f var/app_dev.db
@echo "β
Development environment cleaned"
-dev-db-create: ## Create development database (if not exists)
+dev-db-create:
@echo "ποΈ Creating development database..."
-php bin/console doctrine:database:create --if-not-exists --env dev || echo "β οΈ Database creation failed (expected for SQLite) - continuing..."
-dev-db-migrate: ## Run database migrations for development environment
+dev-db-migrate:
@echo "π Running database migrations..."
- COMPOSER_MEMORY_LIMIT=-1 php bin/console doctrine:migrations:migrate -n --env dev
+ php -d memory_limit=1G bin/console doctrine:migrations:migrate -n --env dev
-dev-cache-clear: ## Clear development cache
+dev-cache-clear:
@echo "ποΈ Clearing development cache..."
- rm -rf var/cache/dev
+ php -d memory_limit=1G bin/console cache:clear --env dev -n
@echo "β
Development cache cleared"
-dev-warmup: ## Warm up development cache
+dev-warmup:
@echo "π₯ Warming up development cache..."
- COMPOSER_MEMORY_LIMIT=-1 php -d memory_limit=1G bin/console cache:warmup --env dev -n
+ php -d memory_limit=1G bin/console cache:warmup --env dev -n
-dev-reset: dev-cache-clear dev-db-migrate ## Quick development reset (cache clear + migrate)
+dev-reset: dev-cache-clear dev-db-migrate
@echo "β
Development environment reset complete!"
\ No newline at end of file
diff --git a/migrations/Version20250802205143.php b/migrations/Version20250802205143.php
deleted file mode 100644
index 5eb09a77..00000000
--- a/migrations/Version20250802205143.php
+++ /dev/null
@@ -1,70 +0,0 @@
-addSql('CREATE TABLE bulk_info_provider_import_jobs (id INT AUTO_INCREMENT NOT NULL, name LONGTEXT NOT NULL, field_mappings LONGTEXT NOT NULL, search_results LONGTEXT NOT NULL, status VARCHAR(20) NOT NULL, created_at DATETIME NOT NULL, completed_at DATETIME DEFAULT NULL, prefetch_details TINYINT(1) NOT NULL, created_by_id INT NOT NULL, CONSTRAINT FK_7F58C1EDB03A8386 FOREIGN KEY (created_by_id) REFERENCES `users` (id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
- $this->addSql('CREATE INDEX IDX_7F58C1EDB03A8386 ON bulk_info_provider_import_jobs (created_by_id)');
-
- $this->addSql('CREATE TABLE bulk_info_provider_import_job_parts (id INT AUTO_INCREMENT NOT NULL, status VARCHAR(20) NOT NULL, reason LONGTEXT DEFAULT NULL, completed_at DATETIME DEFAULT NULL, job_id INT NOT NULL, part_id INT NOT NULL, CONSTRAINT FK_CD93F28FBE04EA9 FOREIGN KEY (job_id) REFERENCES bulk_info_provider_import_jobs (id), CONSTRAINT FK_CD93F28F4CE34BEC FOREIGN KEY (part_id) REFERENCES `parts` (id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
- $this->addSql('CREATE INDEX IDX_CD93F28FBE04EA9 ON bulk_info_provider_import_job_parts (job_id)');
- $this->addSql('CREATE INDEX IDX_CD93F28F4CE34BEC ON bulk_info_provider_import_job_parts (part_id)');
- $this->addSql('CREATE UNIQUE INDEX unique_job_part ON bulk_info_provider_import_job_parts (job_id, part_id)');
- }
-
- public function mySQLDown(Schema $schema): void
- {
- $this->addSql('DROP TABLE bulk_info_provider_import_job_parts');
- $this->addSql('DROP TABLE bulk_info_provider_import_jobs');
- }
-
- public function sqLiteUp(Schema $schema): void
- {
- $this->addSql('CREATE TABLE bulk_info_provider_import_jobs (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name CLOB NOT NULL, field_mappings CLOB NOT NULL, search_results CLOB NOT NULL, status VARCHAR(20) NOT NULL, created_at DATETIME NOT NULL, completed_at DATETIME DEFAULT NULL, prefetch_details BOOLEAN NOT NULL, created_by_id INTEGER NOT NULL, CONSTRAINT FK_7F58C1EDB03A8386 FOREIGN KEY (created_by_id) REFERENCES "users" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
- $this->addSql('CREATE INDEX IDX_7F58C1EDB03A8386 ON bulk_info_provider_import_jobs (created_by_id)');
-
- $this->addSql('CREATE TABLE bulk_info_provider_import_job_parts (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, status VARCHAR(20) NOT NULL, reason CLOB DEFAULT NULL, completed_at DATETIME DEFAULT NULL, job_id INTEGER NOT NULL, part_id INTEGER NOT NULL, CONSTRAINT FK_CD93F28FBE04EA9 FOREIGN KEY (job_id) REFERENCES bulk_info_provider_import_jobs (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_CD93F28F4CE34BEC FOREIGN KEY (part_id) REFERENCES "parts" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
- $this->addSql('CREATE INDEX IDX_CD93F28FBE04EA9 ON bulk_info_provider_import_job_parts (job_id)');
- $this->addSql('CREATE INDEX IDX_CD93F28F4CE34BEC ON bulk_info_provider_import_job_parts (part_id)');
- $this->addSql('CREATE UNIQUE INDEX unique_job_part ON bulk_info_provider_import_job_parts (job_id, part_id)');
- }
-
- public function sqLiteDown(Schema $schema): void
- {
- $this->addSql('DROP TABLE bulk_info_provider_import_job_parts');
- $this->addSql('DROP TABLE bulk_info_provider_import_jobs');
- }
-
- public function postgreSQLUp(Schema $schema): void
- {
- $this->addSql('CREATE TABLE bulk_info_provider_import_jobs (id SERIAL PRIMARY KEY NOT NULL, name TEXT NOT NULL, field_mappings TEXT NOT NULL, search_results TEXT NOT NULL, status VARCHAR(20) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, completed_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, prefetch_details BOOLEAN NOT NULL, created_by_id INT NOT NULL, CONSTRAINT FK_7F58C1EDB03A8386 FOREIGN KEY (created_by_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
- $this->addSql('CREATE INDEX IDX_7F58C1EDB03A8386 ON bulk_info_provider_import_jobs (created_by_id)');
-
- $this->addSql('CREATE TABLE bulk_info_provider_import_job_parts (id SERIAL PRIMARY KEY NOT NULL, status VARCHAR(20) NOT NULL, reason TEXT DEFAULT NULL, completed_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, job_id INT NOT NULL, part_id INT NOT NULL, CONSTRAINT FK_CD93F28FBE04EA9 FOREIGN KEY (job_id) REFERENCES bulk_info_provider_import_jobs (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_CD93F28F4CE34BEC FOREIGN KEY (part_id) REFERENCES parts (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
- $this->addSql('CREATE INDEX IDX_CD93F28FBE04EA9 ON bulk_info_provider_import_job_parts (job_id)');
- $this->addSql('CREATE INDEX IDX_CD93F28F4CE34BEC ON bulk_info_provider_import_job_parts (part_id)');
- $this->addSql('CREATE UNIQUE INDEX unique_job_part ON bulk_info_provider_import_job_parts (job_id, part_id)');
- }
-
- public function postgreSQLDown(Schema $schema): void
- {
- $this->addSql('DROP TABLE bulk_info_provider_import_job_parts');
- $this->addSql('DROP TABLE bulk_info_provider_import_jobs');
- }
-}
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 3feb4940..4a37b420 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -4,7 +4,7 @@
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
colors="true"
- failOnDeprecation="false"
+ failOnDeprecation="true"
failOnNotice="true"
failOnWarning="true"
bootstrap="tests/bootstrap.php"
diff --git a/src/Controller/BulkInfoProviderImportController.php b/src/Controller/BulkInfoProviderImportController.php
deleted file mode 100644
index 2d3dd7f6..00000000
--- a/src/Controller/BulkInfoProviderImportController.php
+++ /dev/null
@@ -1,588 +0,0 @@
-.
- */
-
-declare(strict_types=1);
-
-namespace App\Controller;
-
-use App\Entity\InfoProviderSystem\BulkImportJobStatus;
-use App\Entity\InfoProviderSystem\BulkInfoProviderImportJob;
-use App\Entity\InfoProviderSystem\BulkInfoProviderImportJobPart;
-use App\Entity\Parts\Part;
-use App\Entity\Parts\Supplier;
-use App\Entity\UserSystem\User;
-use App\Form\InfoProviderSystem\GlobalFieldMappingType;
-use App\Services\InfoProviderSystem\BulkInfoProviderService;
-use App\Services\InfoProviderSystem\DTOs\BulkSearchFieldMappingDTO;
-use App\Services\InfoProviderSystem\DTOs\BulkSearchPartResultsDTO;
-use App\Services\InfoProviderSystem\DTOs\BulkSearchResponseDTO;
-use Doctrine\ORM\EntityManagerInterface;
-use Psr\Log\LoggerInterface;
-use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
-use Symfony\Component\DependencyInjection\Attribute\Autowire;
-use Symfony\Component\HttpFoundation\JsonResponse;
-use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\HttpFoundation\Response;
-use Symfony\Component\Routing\Attribute\Route;
-
-#[Route('/tools/bulk_info_provider_import')]
-class BulkInfoProviderImportController extends AbstractController
-{
- public function __construct(
- private readonly BulkInfoProviderService $bulkService,
- private readonly EntityManagerInterface $entityManager,
- private readonly LoggerInterface $logger,
- #[Autowire(param: 'partdb.bulk_import.batch_size')]
- private readonly int $bulkImportBatchSize,
- #[Autowire(param: 'partdb.bulk_import.max_parts_per_operation')]
- private readonly int $bulkImportMaxParts
- ) {
- }
-
- /**
- * Convert field mappings from array format to FieldMappingDTO[].
- *
- * @param array $fieldMappings Array of field mapping arrays
- * @return BulkSearchFieldMappingDTO[] Array of FieldMappingDTO objects
- */
- private function convertFieldMappingsToDto(array $fieldMappings): array
- {
- $dtos = [];
- foreach ($fieldMappings as $mapping) {
- $dtos[] = new BulkSearchFieldMappingDTO(field: $mapping['field'], providers: $mapping['providers'], priority: $mapping['priority'] ?? 1);
- }
- return $dtos;
- }
-
- private function createErrorResponse(string $message, int $statusCode = 400, array $context = []): JsonResponse
- {
- $this->logger->warning('Bulk import operation failed', array_merge([
- 'error' => $message,
- 'user' => $this->getUser()?->getUserIdentifier(),
- ], $context));
-
- return $this->json([
- 'success' => false,
- 'error' => $message
- ], $statusCode);
- }
-
- private function validateJobAccess(int $jobId): ?BulkInfoProviderImportJob
- {
- $this->denyAccessUnlessGranted('@info_providers.create_parts');
-
- $job = $this->entityManager->getRepository(BulkInfoProviderImportJob::class)->find($jobId);
-
- if (!$job) {
- return null;
- }
-
- if ($job->getCreatedBy() !== $this->getUser()) {
- return null;
- }
-
- return $job;
- }
-
- private function updatePartSearchResults(BulkInfoProviderImportJob $job, ?BulkSearchPartResultsDTO $newResults): void
- {
- if ($newResults === null) {
- return;
- }
-
- // Only deserialize and update if we have new results
- $allResults = $job->getSearchResults($this->entityManager);
-
- // Find and update the results for this specific part
- $allResults = $allResults->replaceResultsForPart($newResults);
-
- // Save updated results back to job
- $job->setSearchResults($allResults);
- }
-
- #[Route('/step1', name: 'bulk_info_provider_step1')]
- public function step1(Request $request): Response
- {
- $this->denyAccessUnlessGranted('@info_providers.create_parts');
-
- set_time_limit(600);
-
- $ids = $request->query->get('ids');
- if (!$ids) {
- $this->addFlash('error', 'No parts selected for bulk import');
- return $this->redirectToRoute('homepage');
- }
-
- $partIds = explode(',', $ids);
- $partRepository = $this->entityManager->getRepository(Part::class);
- $parts = $partRepository->getElementsFromIDArray($partIds);
-
- if (empty($parts)) {
- $this->addFlash('error', 'No valid parts found for bulk import');
- return $this->redirectToRoute('homepage');
- }
-
- // Validate against configured maximum
- if (count($parts) > $this->bulkImportMaxParts) {
- $this->addFlash('error', sprintf(
- 'Too many parts selected (%d). Maximum allowed is %d parts per operation.',
- count($parts),
- $this->bulkImportMaxParts
- ));
- return $this->redirectToRoute('homepage');
- }
-
- if (count($parts) > ($this->bulkImportMaxParts / 2)) {
- $this->addFlash('warning', 'Processing ' . count($parts) . ' parts may take several minutes and could timeout. Consider processing smaller batches.');
- }
-
- // Generate field choices
- $fieldChoices = [
- 'info_providers.bulk_search.field.mpn' => 'mpn',
- 'info_providers.bulk_search.field.name' => 'name',
- ];
-
- // Add dynamic supplier fields
- $suppliers = $this->entityManager->getRepository(Supplier::class)->findAll();
- foreach ($suppliers as $supplier) {
- $supplierKey = strtolower(str_replace([' ', '-', '_'], '_', $supplier->getName()));
- $fieldChoices["Supplier: " . $supplier->getName() . " (SPN)"] = $supplierKey . '_spn';
- }
-
- // Initialize form with useful default mappings
- $initialData = [
- 'field_mappings' => [
- ['field' => 'mpn', 'providers' => [], 'priority' => 1]
- ],
- 'prefetch_details' => false
- ];
-
- $form = $this->createForm(GlobalFieldMappingType::class, $initialData, [
- 'field_choices' => $fieldChoices
- ]);
- $form->handleRequest($request);
-
- $searchResults = null;
-
- if ($form->isSubmitted() && $form->isValid()) {
- $formData = $form->getData();
- $fieldMappingDtos = $this->convertFieldMappingsToDto($formData['field_mappings']);
- $prefetchDetails = $formData['prefetch_details'] ?? false;
-
- $user = $this->getUser();
- if (!$user instanceof User) {
- throw new \RuntimeException('User must be authenticated and of type User');
- }
-
- // Validate part count against configuration limit
- if (count($parts) > $this->bulkImportMaxParts) {
- $this->addFlash('error', "Too many parts selected. Maximum allowed: {$this->bulkImportMaxParts}");
- $partIds = array_map(fn($part) => $part->getId(), $parts);
- return $this->redirectToRoute('bulk_info_provider_step1', ['ids' => implode(',', $partIds)]);
- }
-
- // Create and save the job
- $job = new BulkInfoProviderImportJob();
- $job->setFieldMappings($fieldMappingDtos);
- $job->setPrefetchDetails($prefetchDetails);
- $job->setCreatedBy($user);
-
- foreach ($parts as $part) {
- $jobPart = new BulkInfoProviderImportJobPart($job, $part);
- $job->addJobPart($jobPart);
- }
-
- $this->entityManager->persist($job);
- $this->entityManager->flush();
-
- try {
- $searchResultsDto = $this->bulkService->performBulkSearch($parts, $fieldMappingDtos, $prefetchDetails);
-
- // Save search results to job
- $job->setSearchResults($searchResultsDto);
- $job->markAsInProgress();
- $this->entityManager->flush();
-
- // Prefetch details if requested
- if ($prefetchDetails) {
- $this->bulkService->prefetchDetailsForResults($searchResultsDto);
- }
-
- return $this->redirectToRoute('bulk_info_provider_step2', ['jobId' => $job->getId()]);
-
- } catch (\Exception $e) {
- $this->logger->error('Critical error during bulk import search', [
- 'job_id' => $job->getId(),
- 'error' => $e->getMessage(),
- 'exception' => $e
- ]);
-
- $this->entityManager->remove($job);
- $this->entityManager->flush();
-
- $this->addFlash('error', 'Search failed due to an error: ' . $e->getMessage());
- $partIds = array_map(fn($part) => $part->getId(), $parts);
- return $this->redirectToRoute('bulk_info_provider_step1', ['ids' => implode(',', $partIds)]);
- }
- }
-
- // Get existing in-progress jobs for current user
- $existingJobs = $this->entityManager->getRepository(BulkInfoProviderImportJob::class)
- ->findBy(['createdBy' => $this->getUser(), 'status' => BulkImportJobStatus::IN_PROGRESS], ['createdAt' => 'DESC'], 10);
-
- return $this->render('info_providers/bulk_import/step1.html.twig', [
- 'form' => $form,
- 'parts' => $parts,
- 'search_results' => $searchResults,
- 'existing_jobs' => $existingJobs,
- 'fieldChoices' => $fieldChoices
- ]);
- }
-
- #[Route('/manage', name: 'bulk_info_provider_manage')]
- public function manageBulkJobs(): Response
- {
- $this->denyAccessUnlessGranted('@info_providers.create_parts');
-
- // Get all jobs for current user
- $allJobs = $this->entityManager->getRepository(BulkInfoProviderImportJob::class)
- ->findBy([], ['createdAt' => 'DESC']);
-
- // Check and auto-complete jobs that should be completed
- // Also clean up jobs with no results (failed searches)
- $updatedJobs = false;
- $jobsToDelete = [];
-
- foreach ($allJobs as $job) {
- if ($job->isAllPartsCompleted() && !$job->isCompleted()) {
- $job->markAsCompleted();
- $updatedJobs = true;
- }
-
- // Mark jobs with no results for deletion (failed searches)
- if ($job->getResultCount() === 0 && $job->isInProgress()) {
- $jobsToDelete[] = $job;
- }
- }
-
- // Delete failed jobs
- foreach ($jobsToDelete as $job) {
- $this->entityManager->remove($job);
- $updatedJobs = true;
- }
-
- // Flush changes if any jobs were updated
- if ($updatedJobs) {
- $this->entityManager->flush();
-
- if (!empty($jobsToDelete)) {
- $this->addFlash('info', 'Cleaned up ' . count($jobsToDelete) . ' failed job(s) with no results.');
- }
- }
-
- return $this->render('info_providers/bulk_import/manage.html.twig', [
- 'jobs' => $this->entityManager->getRepository(BulkInfoProviderImportJob::class)
- ->findBy([], ['createdAt' => 'DESC']) // Refetch after cleanup
- ]);
- }
-
- #[Route('/job/{jobId}/delete', name: 'bulk_info_provider_delete', methods: ['DELETE'])]
- public function deleteJob(int $jobId): Response
- {
- $job = $this->validateJobAccess($jobId);
- if (!$job) {
- return $this->createErrorResponse('Job not found or access denied', 404, ['job_id' => $jobId]);
- }
-
- // Only allow deletion of completed, failed, or stopped jobs
- if (!$job->isCompleted() && !$job->isFailed() && !$job->isStopped()) {
- return $this->json(['error' => 'Cannot delete active job'], 400);
- }
-
- $this->entityManager->remove($job);
- $this->entityManager->flush();
-
- return $this->json(['success' => true]);
- }
-
- #[Route('/job/{jobId}/stop', name: 'bulk_info_provider_stop', methods: ['POST'])]
- public function stopJob(int $jobId): Response
- {
- $job = $this->validateJobAccess($jobId);
- if (!$job) {
- return $this->createErrorResponse('Job not found or access denied', 404, ['job_id' => $jobId]);
- }
-
- // Only allow stopping of pending or in-progress jobs
- if (!$job->canBeStopped()) {
- return $this->json(['error' => 'Cannot stop job in current status'], 400);
- }
-
- $job->markAsStopped();
- $this->entityManager->flush();
-
- return $this->json(['success' => true]);
- }
-
-
- #[Route('/step2/{jobId}', name: 'bulk_info_provider_step2')]
- public function step2(int $jobId): Response
- {
- $this->denyAccessUnlessGranted('@info_providers.create_parts');
-
- $job = $this->entityManager->getRepository(BulkInfoProviderImportJob::class)->find($jobId);
-
- if (!$job) {
- $this->addFlash('error', 'Bulk import job not found');
- return $this->redirectToRoute('bulk_info_provider_step1');
- }
-
- // Check if user owns this job
- if ($job->getCreatedBy() !== $this->getUser()) {
- $this->addFlash('error', 'Access denied to this bulk import job');
- return $this->redirectToRoute('bulk_info_provider_step1');
- }
-
- // Get the parts and deserialize search results
- $parts = $job->getJobParts()->map(fn($jobPart) => $jobPart->getPart())->toArray();
- $searchResults = $job->getSearchResults($this->entityManager);
-
- return $this->render('info_providers/bulk_import/step2.html.twig', [
- 'job' => $job,
- 'parts' => $parts,
- 'search_results' => $searchResults,
- ]);
- }
-
-
- #[Route('/job/{jobId}/part/{partId}/mark-completed', name: 'bulk_info_provider_mark_completed', methods: ['POST'])]
- public function markPartCompleted(int $jobId, int $partId): Response
- {
- $job = $this->validateJobAccess($jobId);
- if (!$job) {
- return $this->createErrorResponse('Job not found or access denied', 404, ['job_id' => $jobId]);
- }
-
- $job->markPartAsCompleted($partId);
-
- // Auto-complete job if all parts are done
- if ($job->isAllPartsCompleted() && !$job->isCompleted()) {
- $job->markAsCompleted();
- }
-
- $this->entityManager->flush();
-
- return $this->json([
- 'success' => true,
- 'progress' => $job->getProgressPercentage(),
- 'completed_count' => $job->getCompletedPartsCount(),
- 'total_count' => $job->getPartCount(),
- 'job_completed' => $job->isCompleted()
- ]);
- }
-
- #[Route('/job/{jobId}/part/{partId}/mark-skipped', name: 'bulk_info_provider_mark_skipped', methods: ['POST'])]
- public function markPartSkipped(int $jobId, int $partId, Request $request): Response
- {
- $job = $this->validateJobAccess($jobId);
- if (!$job) {
- return $this->createErrorResponse('Job not found or access denied', 404, ['job_id' => $jobId]);
- }
-
- $reason = $request->request->get('reason', '');
- $job->markPartAsSkipped($partId, $reason);
-
- // Auto-complete job if all parts are done
- if ($job->isAllPartsCompleted() && !$job->isCompleted()) {
- $job->markAsCompleted();
- }
-
- $this->entityManager->flush();
-
- return $this->json([
- 'success' => true,
- 'progress' => $job->getProgressPercentage(),
- 'completed_count' => $job->getCompletedPartsCount(),
- 'skipped_count' => $job->getSkippedPartsCount(),
- 'total_count' => $job->getPartCount(),
- 'job_completed' => $job->isCompleted()
- ]);
- }
-
- #[Route('/job/{jobId}/part/{partId}/mark-pending', name: 'bulk_info_provider_mark_pending', methods: ['POST'])]
- public function markPartPending(int $jobId, int $partId): Response
- {
- $job = $this->validateJobAccess($jobId);
- if (!$job) {
- return $this->createErrorResponse('Job not found or access denied', 404, ['job_id' => $jobId]);
- }
-
- $job->markPartAsPending($partId);
- $this->entityManager->flush();
-
- return $this->json([
- 'success' => true,
- 'progress' => $job->getProgressPercentage(),
- 'completed_count' => $job->getCompletedPartsCount(),
- 'skipped_count' => $job->getSkippedPartsCount(),
- 'total_count' => $job->getPartCount(),
- 'job_completed' => $job->isCompleted()
- ]);
- }
-
- #[Route('/job/{jobId}/part/{partId}/research', name: 'bulk_info_provider_research_part', methods: ['POST'])]
- public function researchPart(int $jobId, int $partId): JsonResponse
- {
- $job = $this->validateJobAccess($jobId);
- if (!$job) {
- return $this->createErrorResponse('Job not found or access denied', 404, ['job_id' => $jobId]);
- }
-
- $part = $this->entityManager->getRepository(Part::class)->find($partId);
- if (!$part) {
- return $this->createErrorResponse('Part not found', 404, ['part_id' => $partId]);
- }
-
- // Only refresh if the entity might be stale (optional optimization)
- if ($this->entityManager->getUnitOfWork()->isScheduledForUpdate($part)) {
- $this->entityManager->refresh($part);
- }
-
- try {
- // Use the job's field mappings to perform the search
- $fieldMappingDtos = $job->getFieldMappings();
- $prefetchDetails = $job->isPrefetchDetails();
-
- try {
- $searchResultsDto = $this->bulkService->performBulkSearch([$part], $fieldMappingDtos, $prefetchDetails);
- } catch (\Exception $searchException) {
- // Handle "no search results found" as a normal case, not an error
- if (str_contains($searchException->getMessage(), 'No search results found')) {
- $searchResultsDto = null;
- } else {
- throw $searchException;
- }
- }
-
- // Update the job's search results for this specific part efficiently
- $this->updatePartSearchResults($job, $searchResultsDto[0] ?? null);
-
- // Prefetch details if requested
- if ($prefetchDetails && $searchResultsDto !== null) {
- $this->bulkService->prefetchDetailsForResults($searchResultsDto);
- }
-
- $this->entityManager->flush();
-
- // Return the new results for this part
- $newResults = $searchResultsDto[0] ?? null;
-
- return $this->json([
- 'success' => true,
- 'part_id' => $partId,
- 'results_count' => $newResults ? $newResults->getResultCount() : 0,
- 'errors_count' => $newResults ? $newResults->getErrorCount() : 0,
- 'message' => 'Part research completed successfully'
- ]);
-
- } catch (\Exception $e) {
- return $this->createErrorResponse(
- 'Research failed: ' . $e->getMessage(),
- 500,
- [
- 'job_id' => $jobId,
- 'part_id' => $partId,
- 'exception' => $e->getMessage()
- ]
- );
- }
- }
-
- #[Route('/job/{jobId}/research-all', name: 'bulk_info_provider_research_all', methods: ['POST'])]
- public function researchAllParts(int $jobId): JsonResponse
- {
- $job = $this->validateJobAccess($jobId);
- if (!$job) {
- return $this->createErrorResponse('Job not found or access denied', 404, ['job_id' => $jobId]);
- }
-
- // Get all parts that are not completed or skipped
- $parts = [];
- foreach ($job->getJobParts() as $jobPart) {
- if (!$jobPart->isCompleted() && !$jobPart->isSkipped()) {
- $parts[] = $jobPart->getPart();
- }
- }
-
- if (empty($parts)) {
- return $this->json([
- 'success' => true,
- 'message' => 'No parts to research',
- 'researched_count' => 0
- ]);
- }
-
- try {
- $fieldMappingDtos = $job->getFieldMappings();
- $prefetchDetails = $job->isPrefetchDetails();
-
- // Process in batches to reduce memory usage for large operations
- $allResults = new BulkSearchResponseDTO(partResults: []);
- $batches = array_chunk($parts, $this->bulkImportBatchSize);
-
- foreach ($batches as $batch) {
- $batchResultsDto = $this->bulkService->performBulkSearch($batch, $fieldMappingDtos, $prefetchDetails);
- $allResults = BulkSearchResponseDTO::merge($allResults, $batchResultsDto);
-
- // Properly manage entity manager memory without losing state
- $jobId = $job->getId();
- //$this->entityManager->clear(); //TODO: This seems to cause problems with the user relation, when trying to flush later
- $job = $this->entityManager->find(BulkInfoProviderImportJob::class, $jobId);
- }
-
- // Update the job's search results
- $job->setSearchResults($allResults);
-
- // Prefetch details if requested
- if ($prefetchDetails) {
- $this->bulkService->prefetchDetailsForResults($allResults);
- }
-
- $this->entityManager->flush();
-
- return $this->json([
- 'success' => true,
- 'researched_count' => count($parts),
- 'message' => sprintf('Successfully researched %d parts', count($parts))
- ]);
-
- } catch (\Exception $e) {
- return $this->createErrorResponse(
- 'Bulk research failed: ' . $e->getMessage(),
- 500,
- [
- 'job_id' => $jobId,
- 'part_count' => count($parts),
- 'exception' => $e->getMessage()
- ]
- );
- }
- }
-}
diff --git a/src/Controller/PartController.php b/src/Controller/PartController.php
index aeb2664e..6708ed4c 100644
--- a/src/Controller/PartController.php
+++ b/src/Controller/PartController.php
@@ -64,17 +64,14 @@ use Symfony\Contracts\Translation\TranslatorInterface;
use function Symfony\Component\Translation\t;
#[Route(path: '/part')]
-final class PartController extends AbstractController
+class PartController extends AbstractController
{
- public function __construct(
- private readonly PricedetailHelper $pricedetailHelper,
- private readonly PartPreviewGenerator $partPreviewGenerator,
+ public function __construct(protected PricedetailHelper $pricedetailHelper,
+ protected PartPreviewGenerator $partPreviewGenerator,
private readonly TranslatorInterface $translator,
- private readonly AttachmentSubmitHandler $attachmentSubmitHandler,
- private readonly EntityManagerInterface $em,
- private readonly EventCommentHelper $commentHelper,
- private readonly PartInfoSettings $partInfoSettings,
- ) {
+ private readonly AttachmentSubmitHandler $attachmentSubmitHandler, private readonly EntityManagerInterface $em,
+ protected EventCommentHelper $commentHelper, private readonly PartInfoSettings $partInfoSettings)
+ {
}
/**
@@ -83,16 +80,9 @@ final class PartController extends AbstractController
*/
#[Route(path: '/{id}/info/{timestamp}', name: 'part_info')]
#[Route(path: '/{id}', requirements: ['id' => '\d+'])]
- public function show(
- Part $part,
- Request $request,
- TimeTravel $timeTravel,
- HistoryHelper $historyHelper,
- DataTableFactory $dataTable,
- ParameterExtractor $parameterExtractor,
- PartLotWithdrawAddHelper $withdrawAddHelper,
- ?string $timestamp = null
- ): Response {
+ public function show(Part $part, Request $request, TimeTravel $timeTravel, HistoryHelper $historyHelper,
+ DataTableFactory $dataTable, ParameterExtractor $parameterExtractor, PartLotWithdrawAddHelper $withdrawAddHelper, ?string $timestamp = null): Response
+ {
$this->denyAccessUnlessGranted('read', $part);
$timeTravel_timestamp = null;
@@ -142,43 +132,7 @@ final class PartController extends AbstractController
{
$this->denyAccessUnlessGranted('edit', $part);
- // Check if this is part of a bulk import job
- $jobId = $request->query->get('jobId');
- $bulkJob = null;
- if ($jobId) {
- $bulkJob = $this->em->getRepository(\App\Entity\InfoProviderSystem\BulkInfoProviderImportJob::class)->find($jobId);
- // Verify user owns this job
- if ($bulkJob && $bulkJob->getCreatedBy() !== $this->getUser()) {
- $bulkJob = null;
- }
- }
-
- return $this->renderPartForm('edit', $request, $part, [], [
- 'bulk_job' => $bulkJob
- ]);
- }
-
- #[Route(path: '/{id}/bulk-import-complete/{jobId}', name: 'part_bulk_import_complete', methods: ['POST'])]
- public function markBulkImportComplete(Part $part, int $jobId, Request $request): Response
- {
- $this->denyAccessUnlessGranted('edit', $part);
-
- if (!$this->isCsrfTokenValid('bulk_complete_' . $part->getId(), $request->request->get('_token'))) {
- throw $this->createAccessDeniedException('Invalid CSRF token');
- }
-
- $bulkJob = $this->em->getRepository(\App\Entity\InfoProviderSystem\BulkInfoProviderImportJob::class)->find($jobId);
- if (!$bulkJob || $bulkJob->getCreatedBy() !== $this->getUser()) {
- throw $this->createNotFoundException('Bulk import job not found');
- }
-
- $bulkJob->markPartAsCompleted($part->getId());
- $this->em->persist($bulkJob);
- $this->em->flush();
-
- $this->addFlash('success', 'Part marked as completed in bulk import');
-
- return $this->redirectToRoute('bulk_info_provider_step2', ['jobId' => $jobId]);
+ return $this->renderPartForm('edit', $request, $part);
}
#[Route(path: '/{id}/delete', name: 'part_delete', methods: ['DELETE'])]
@@ -186,7 +140,7 @@ final class PartController extends AbstractController
{
$this->denyAccessUnlessGranted('delete', $part);
- if ($this->isCsrfTokenValid('delete' . $part->getID(), $request->request->get('_token'))) {
+ if ($this->isCsrfTokenValid('delete'.$part->getID(), $request->request->get('_token'))) {
$this->commentHelper->setMessage($request->request->get('log_comment', null));
@@ -205,15 +159,11 @@ final class PartController extends AbstractController
#[Route(path: '/new', name: 'part_new')]
#[Route(path: '/{id}/clone', name: 'part_clone')]
#[Route(path: '/new_build_part/{project_id}', name: 'part_new_build_part')]
- public function new(
- Request $request,
- EntityManagerInterface $em,
- TranslatorInterface $translator,
- AttachmentSubmitHandler $attachmentSubmitHandler,
- ProjectBuildPartHelper $projectBuildPartHelper,
+ public function new(Request $request, EntityManagerInterface $em, TranslatorInterface $translator,
+ AttachmentSubmitHandler $attachmentSubmitHandler, ProjectBuildPartHelper $projectBuildPartHelper,
#[MapEntity(mapping: ['id' => 'id'])] ?Part $part = null,
- #[MapEntity(mapping: ['project_id' => 'id'])] ?Project $project = null
- ): Response {
+ #[MapEntity(mapping: ['project_id' => 'id'])] ?Project $project = null): Response
+ {
if ($part instanceof Part) {
//Clone part
@@ -308,14 +258,9 @@ final class PartController extends AbstractController
}
#[Route(path: '/{id}/from_info_provider/{providerKey}/{providerId}/update', name: 'info_providers_update_part', requirements: ['providerId' => '.+'])]
- public function updateFromInfoProvider(
- Part $part,
- Request $request,
- string $providerKey,
- string $providerId,
- PartInfoRetriever $infoRetriever,
- PartMerger $partMerger
- ): Response {
+ public function updateFromInfoProvider(Part $part, Request $request, string $providerKey, string $providerId,
+ PartInfoRetriever $infoRetriever, PartMerger $partMerger): Response
+ {
$this->denyAccessUnlessGranted('edit', $part);
$this->denyAccessUnlessGranted('@info_providers.create_parts');
@@ -329,22 +274,10 @@ final class PartController extends AbstractController
$this->addFlash('notice', t('part.merge.flash.please_review'));
- // Check if this is part of a bulk import job
- $jobId = $request->query->get('jobId');
- $bulkJob = null;
- if ($jobId) {
- $bulkJob = $this->em->getRepository(\App\Entity\InfoProviderSystem\BulkInfoProviderImportJob::class)->find($jobId);
- // Verify user owns this job
- if ($bulkJob && $bulkJob->getCreatedBy() !== $this->getUser()) {
- $bulkJob = null;
- }
- }
-
return $this->renderPartForm('update_from_ip', $request, $part, [
'info_provider_dto' => $dto,
], [
- 'tname_before' => $old_name,
- 'bulk_job' => $bulkJob
+ 'tname_before' => $old_name
]);
}
@@ -379,7 +312,7 @@ final class PartController extends AbstractController
} catch (AttachmentDownloadException $attachmentDownloadException) {
$this->addFlash(
'error',
- $this->translator->trans('attachment.download_failed') . ' ' . $attachmentDownloadException->getMessage()
+ $this->translator->trans('attachment.download_failed').' '.$attachmentDownloadException->getMessage()
);
}
}
@@ -420,12 +353,6 @@ final class PartController extends AbstractController
return $this->redirectToRoute('part_new');
}
- // Check if we're in bulk import mode and preserve jobId
- $jobId = $request->query->get('jobId');
- if ($jobId && isset($merge_infos['bulk_job'])) {
- return $this->redirectToRoute('part_edit', ['id' => $new_part->getID(), 'jobId' => $jobId]);
- }
-
return $this->redirectToRoute('part_edit', ['id' => $new_part->getID()]);
}
@@ -444,17 +371,13 @@ final class PartController extends AbstractController
$template = 'parts/edit/update_from_ip.html.twig';
}
- return $this->render(
- $template,
+ return $this->render($template,
[
'part' => $new_part,
'form' => $form,
'merge_old_name' => $merge_infos['tname_before'] ?? null,
- 'merge_other' => $merge_infos['other_part'] ?? null,
- 'bulk_job' => $merge_infos['bulk_job'] ?? null,
- 'jobId' => $request->query->get('jobId')
- ]
- );
+ 'merge_other' => $merge_infos['other_part'] ?? null
+ ]);
}
@@ -464,17 +387,17 @@ final class PartController extends AbstractController
if ($this->isCsrfTokenValid('part_withraw' . $part->getID(), $request->request->get('_csfr'))) {
//Retrieve partlot from the request
$partLot = $em->find(PartLot::class, $request->request->get('lot_id'));
- if (!$partLot instanceof PartLot) {
+ if(!$partLot instanceof PartLot) {
throw new \RuntimeException('Part lot not found!');
}
//Ensure that the partlot belongs to the part
- if ($partLot->getPart() !== $part) {
+ if($partLot->getPart() !== $part) {
throw new \RuntimeException("The origin partlot does not belong to the part!");
}
//Try to determine the target lot (used for move actions), if the parameter is existing
$targetId = $request->request->get('target_id', null);
- $targetLot = $targetId ? $em->find(PartLot::class, $targetId) : null;
+ $targetLot = $targetId ? $em->find(PartLot::class, $targetId) : null;
if ($targetLot && $targetLot->getPart() !== $part) {
throw new \RuntimeException("The target partlot does not belong to the part!");
}
@@ -488,12 +411,12 @@ final class PartController extends AbstractController
$timestamp = null;
$timestamp_str = $request->request->getString('timestamp', '');
//Try to parse the timestamp
- if ($timestamp_str !== '') {
+ if($timestamp_str !== '') {
$timestamp = new DateTime($timestamp_str);
}
//Ensure that the timestamp is not in the future
- if ($timestamp !== null && $timestamp > new DateTime("+20min")) {
+ if($timestamp !== null && $timestamp > new DateTime("+20min")) {
throw new \LogicException("The timestamp must not be in the future!");
}
@@ -537,7 +460,7 @@ final class PartController extends AbstractController
err:
//If a redirect was passed, then redirect there
- if ($request->request->get('_redirect')) {
+ if($request->request->get('_redirect')) {
return $this->redirect($request->request->get('_redirect'));
}
//Otherwise just redirect to the part page
diff --git a/src/DataTables/Filters/Constraints/Part/BulkImportJobExistsConstraint.php b/src/DataTables/Filters/Constraints/Part/BulkImportJobExistsConstraint.php
deleted file mode 100644
index 9d21dd58..00000000
--- a/src/DataTables/Filters/Constraints/Part/BulkImportJobExistsConstraint.php
+++ /dev/null
@@ -1,59 +0,0 @@
-.
- */
-
-namespace App\DataTables\Filters\Constraints\Part;
-
-use App\DataTables\Filters\Constraints\BooleanConstraint;
-use App\Entity\InfoProviderSystem\BulkInfoProviderImportJobPart;
-use Doctrine\ORM\QueryBuilder;
-
-class BulkImportJobExistsConstraint extends BooleanConstraint
-{
-
- public function __construct()
- {
- parent::__construct('bulk_import_job_exists');
- }
-
- public function apply(QueryBuilder $queryBuilder): void
- {
- // Do not apply a filter if value is null (filter is set to ignore)
- if (!$this->isEnabled()) {
- return;
- }
-
- // Use EXISTS subquery to avoid join conflicts
- $existsSubquery = $queryBuilder->getEntityManager()->createQueryBuilder();
- $existsSubquery->select('1')
- ->from(BulkInfoProviderImportJobPart::class, 'bip_exists')
- ->where('bip_exists.part = part.id');
-
- if ($this->value === true) {
- // Filter for parts that ARE in bulk import jobs
- $queryBuilder->andWhere('EXISTS (' . $existsSubquery->getDQL() . ')');
- } else {
- // Filter for parts that are NOT in bulk import jobs
- $queryBuilder->andWhere('NOT EXISTS (' . $existsSubquery->getDQL() . ')');
- }
- }
-}
diff --git a/src/DataTables/Filters/Constraints/Part/BulkImportJobStatusConstraint.php b/src/DataTables/Filters/Constraints/Part/BulkImportJobStatusConstraint.php
deleted file mode 100644
index d9451577..00000000
--- a/src/DataTables/Filters/Constraints/Part/BulkImportJobStatusConstraint.php
+++ /dev/null
@@ -1,64 +0,0 @@
-.
- */
-
-namespace App\DataTables\Filters\Constraints\Part;
-
-use App\DataTables\Filters\Constraints\AbstractConstraint;
-use App\DataTables\Filters\Constraints\ChoiceConstraint;
-use App\Entity\InfoProviderSystem\BulkInfoProviderImportJobPart;
-use Doctrine\ORM\QueryBuilder;
-
-class BulkImportJobStatusConstraint extends ChoiceConstraint
-{
-
- public function __construct()
- {
- parent::__construct('bulk_import_job_status');
- }
-
- public function apply(QueryBuilder $queryBuilder): void
- {
- // Do not apply a filter if values are empty or operator is null
- if (!$this->isEnabled()) {
- return;
- }
-
- // Use EXISTS subquery to check if part has a job with the specified status(es)
- $existsSubquery = $queryBuilder->getEntityManager()->createQueryBuilder();
- $existsSubquery->select('1')
- ->from(BulkInfoProviderImportJobPart::class, 'bip_status')
- ->join('bip_status.job', 'job_status')
- ->where('bip_status.part = part.id');
-
- // Add status conditions based on operator
- if ($this->operator === 'ANY') {
- $existsSubquery->andWhere('job_status.status IN (:job_status_values)');
- $queryBuilder->andWhere('EXISTS (' . $existsSubquery->getDQL() . ')');
- $queryBuilder->setParameter('job_status_values', $this->value);
- } elseif ($this->operator === 'NONE') {
- $existsSubquery->andWhere('job_status.status IN (:job_status_values)');
- $queryBuilder->andWhere('NOT EXISTS (' . $existsSubquery->getDQL() . ')');
- $queryBuilder->setParameter('job_status_values', $this->value);
- }
- }
-}
diff --git a/src/DataTables/Filters/Constraints/Part/BulkImportPartStatusConstraint.php b/src/DataTables/Filters/Constraints/Part/BulkImportPartStatusConstraint.php
deleted file mode 100644
index 7656a290..00000000
--- a/src/DataTables/Filters/Constraints/Part/BulkImportPartStatusConstraint.php
+++ /dev/null
@@ -1,61 +0,0 @@
-.
- */
-
-namespace App\DataTables\Filters\Constraints\Part;
-
-use App\DataTables\Filters\Constraints\ChoiceConstraint;
-use App\Entity\InfoProviderSystem\BulkInfoProviderImportJobPart;
-use Doctrine\ORM\QueryBuilder;
-
-class BulkImportPartStatusConstraint extends ChoiceConstraint
-{
- public function __construct()
- {
- parent::__construct('bulk_import_part_status');
- }
-
- public function apply(QueryBuilder $queryBuilder): void
- {
- // Do not apply a filter if values are empty or operator is null
- if (!$this->isEnabled()) {
- return;
- }
-
- // Use EXISTS subquery to check if part has the specified status(es)
- $existsSubquery = $queryBuilder->getEntityManager()->createQueryBuilder();
- $existsSubquery->select('1')
- ->from(BulkInfoProviderImportJobPart::class, 'bip_part_status')
- ->where('bip_part_status.part = part.id');
-
- // Add status conditions based on operator
- if ($this->operator === 'ANY') {
- $existsSubquery->andWhere('bip_part_status.status IN (:part_status_values)');
- $queryBuilder->andWhere('EXISTS (' . $existsSubquery->getDQL() . ')');
- $queryBuilder->setParameter('part_status_values', $this->value);
- } elseif ($this->operator === 'NONE') {
- $existsSubquery->andWhere('bip_part_status.status IN (:part_status_values)');
- $queryBuilder->andWhere('NOT EXISTS (' . $existsSubquery->getDQL() . ')');
- $queryBuilder->setParameter('part_status_values', $this->value);
- }
- }
-}
diff --git a/src/DataTables/Filters/PartFilter.php b/src/DataTables/Filters/PartFilter.php
index e44cf69d..8dcbd6b3 100644
--- a/src/DataTables/Filters/PartFilter.php
+++ b/src/DataTables/Filters/PartFilter.php
@@ -29,9 +29,6 @@ use App\DataTables\Filters\Constraints\DateTimeConstraint;
use App\DataTables\Filters\Constraints\EntityConstraint;
use App\DataTables\Filters\Constraints\IntConstraint;
use App\DataTables\Filters\Constraints\NumberConstraint;
-use App\DataTables\Filters\Constraints\Part\BulkImportJobExistsConstraint;
-use App\DataTables\Filters\Constraints\Part\BulkImportJobStatusConstraint;
-use App\DataTables\Filters\Constraints\Part\BulkImportPartStatusConstraint;
use App\DataTables\Filters\Constraints\Part\LessThanDesiredConstraint;
use App\DataTables\Filters\Constraints\Part\ParameterConstraint;
use App\DataTables\Filters\Constraints\Part\TagsConstraint;
@@ -105,14 +102,6 @@ class PartFilter implements FilterInterface
public readonly TextConstraint $bomName;
public readonly TextConstraint $bomComment;
- /*************************************************
- * Bulk Import Job tab
- *************************************************/
-
- public readonly BulkImportJobExistsConstraint $inBulkImportJob;
- public readonly BulkImportJobStatusConstraint $bulkImportJobStatus;
- public readonly BulkImportPartStatusConstraint $bulkImportPartStatus;
-
public function __construct(NodesListBuilder $nodesListBuilder)
{
//Must be done for every new set of attachment filters, to ensure deterministic parameter names.
@@ -141,7 +130,7 @@ class PartFilter implements FilterInterface
*/
$this->amountSum = (new IntConstraint('(
SELECT COALESCE(SUM(__partLot.amount), 0.0)
- FROM ' . PartLot::class . ' __partLot
+ FROM '.PartLot::class.' __partLot
WHERE __partLot.part = part.id
AND __partLot.instock_unknown = false
AND (__partLot.expiration_date IS NULL OR __partLot.expiration_date > CURRENT_DATE())
@@ -177,11 +166,6 @@ class PartFilter implements FilterInterface
$this->bomName = new TextConstraint('_projectBomEntries.name');
$this->bomComment = new TextConstraint('_projectBomEntries.comment');
- // Bulk Import Job filters
- $this->inBulkImportJob = new BulkImportJobExistsConstraint();
- $this->bulkImportJobStatus = new BulkImportJobStatusConstraint();
- $this->bulkImportPartStatus = new BulkImportPartStatusConstraint();
-
}
public function apply(QueryBuilder $queryBuilder): void
diff --git a/src/DataTables/PartsDataTable.php b/src/DataTables/PartsDataTable.php
index a97762b1..f0decf27 100644
--- a/src/DataTables/PartsDataTable.php
+++ b/src/DataTables/PartsDataTable.php
@@ -142,25 +142,23 @@ final class PartsDataTable implements DataTableTypeInterface
'label' => $this->translator->trans('part.table.storeLocations'),
//We need to use a aggregate function to get the first store location, as we have a one-to-many relation
'orderField' => 'NATSORT(MIN(_storelocations.name))',
- 'render' => fn($value, Part $context) => $this->partDataTableHelper->renderStorageLocations($context),
+ 'render' => fn ($value, Part $context) => $this->partDataTableHelper->renderStorageLocations($context),
], alias: 'storage_location')
->add('amount', TextColumn::class, [
'label' => $this->translator->trans('part.table.amount'),
- 'render' => fn($value, Part $context) => $this->partDataTableHelper->renderAmount($context),
+ 'render' => fn ($value, Part $context) => $this->partDataTableHelper->renderAmount($context),
'orderField' => 'amountSum'
])
->add('minamount', TextColumn::class, [
'label' => $this->translator->trans('part.table.minamount'),
- 'render' => fn($value, Part $context): string => htmlspecialchars($this->amountFormatter->format(
- $value,
- $context->getPartUnit()
- )),
+ 'render' => fn($value, Part $context): string => htmlspecialchars($this->amountFormatter->format($value,
+ $context->getPartUnit())),
])
->add('partUnit', TextColumn::class, [
'label' => $this->translator->trans('part.table.partUnit'),
'orderField' => 'NATSORT(_partUnit.name)',
- 'render' => function ($value, Part $context): string {
+ 'render' => function($value, Part $context): string {
$partUnit = $context->getPartUnit();
if ($partUnit === null) {
return '';
@@ -169,7 +167,7 @@ final class PartsDataTable implements DataTableTypeInterface
$tmp = htmlspecialchars($partUnit->getName());
if ($partUnit->getUnit()) {
- $tmp .= ' (' . htmlspecialchars($partUnit->getUnit()) . ')';
+ $tmp .= ' ('.htmlspecialchars($partUnit->getUnit()).')';
}
return $tmp;
}
@@ -232,7 +230,7 @@ final class PartsDataTable implements DataTableTypeInterface
}
if (count($projects) > $max) {
- $tmp .= ", + " . (count($projects) - $max);
+ $tmp .= ", + ".(count($projects) - $max);
}
return $tmp;
@@ -368,7 +366,7 @@ final class PartsDataTable implements DataTableTypeInterface
$builder->addSelect(
'(
SELECT COALESCE(SUM(partLot.amount), 0.0)
- FROM ' . PartLot::class . ' partLot
+ FROM '.PartLot::class.' partLot
WHERE partLot.part = part.id
AND partLot.instock_unknown = false
AND (partLot.expiration_date IS NULL OR partLot.expiration_date > CURRENT_DATE())
@@ -425,13 +423,6 @@ final class PartsDataTable implements DataTableTypeInterface
//Do not group by many-to-* relations, as it would restrict the COUNT having clauses to be maximum 1
//$builder->addGroupBy('_projectBomEntries');
}
- if (str_contains($dql, '_jobPart')) {
- $builder->leftJoin('part.bulkImportJobParts', '_jobPart');
- $builder->leftJoin('_jobPart.job', '_bulkImportJob');
- //Do not group by many-to-* relations, as it would restrict the COUNT having clauses to be maximum 1
- //$builder->addGroupBy('_jobPart');
- //$builder->addGroupBy('_bulkImportJob');
- }
return $builder;
}
diff --git a/src/Entity/InfoProviderSystem/BulkImportJobStatus.php b/src/Entity/InfoProviderSystem/BulkImportJobStatus.php
deleted file mode 100644
index 7a88802f..00000000
--- a/src/Entity/InfoProviderSystem/BulkImportJobStatus.php
+++ /dev/null
@@ -1,35 +0,0 @@
-.
- */
-
-declare(strict_types=1);
-
-namespace App\Entity\InfoProviderSystem;
-
-use Symfony\Contracts\Translation\TranslatableInterface;
-use Symfony\Contracts\Translation\TranslatorInterface;
-
-enum BulkImportJobStatus: string
-{
- case PENDING = 'pending';
- case IN_PROGRESS = 'in_progress';
- case COMPLETED = 'completed';
- case STOPPED = 'stopped';
- case FAILED = 'failed';
-}
diff --git a/src/Entity/InfoProviderSystem/BulkImportPartStatus.php b/src/Entity/InfoProviderSystem/BulkImportPartStatus.php
deleted file mode 100644
index 0eedc553..00000000
--- a/src/Entity/InfoProviderSystem/BulkImportPartStatus.php
+++ /dev/null
@@ -1,32 +0,0 @@
-.
- */
-
-declare(strict_types=1);
-
-namespace App\Entity\InfoProviderSystem;
-
-
-enum BulkImportPartStatus: string
-{
- case PENDING = 'pending';
- case COMPLETED = 'completed';
- case SKIPPED = 'skipped';
- case FAILED = 'failed';
-}
diff --git a/src/Entity/InfoProviderSystem/BulkInfoProviderImportJob.php b/src/Entity/InfoProviderSystem/BulkInfoProviderImportJob.php
deleted file mode 100644
index bc842a26..00000000
--- a/src/Entity/InfoProviderSystem/BulkInfoProviderImportJob.php
+++ /dev/null
@@ -1,449 +0,0 @@
-.
- */
-
-declare(strict_types=1);
-
-namespace App\Entity\InfoProviderSystem;
-
-use App\Entity\Base\AbstractDBElement;
-use App\Entity\Parts\Part;
-use App\Entity\UserSystem\User;
-use App\Services\InfoProviderSystem\DTOs\BulkSearchFieldMappingDTO;
-use App\Services\InfoProviderSystem\DTOs\BulkSearchResponseDTO;
-use Doctrine\Common\Collections\ArrayCollection;
-use Doctrine\Common\Collections\Collection;
-use Doctrine\DBAL\Types\Types;
-use Doctrine\ORM\EntityManagerInterface;
-use Doctrine\ORM\Mapping as ORM;
-
-#[ORM\Entity]
-#[ORM\Table(name: 'bulk_info_provider_import_jobs')]
-class BulkInfoProviderImportJob extends AbstractDBElement
-{
- #[ORM\Column(type: Types::TEXT)]
- private string $name = '';
-
- #[ORM\Column(type: Types::JSON)]
- private array $fieldMappings = [];
-
- /**
- * @var BulkSearchFieldMappingDTO[] The deserialized field mappings DTOs, cached for performance
- */
- private ?array $fieldMappingsDTO = null;
-
- #[ORM\Column(type: Types::JSON)]
- private array $searchResults = [];
-
- /**
- * @var BulkSearchResponseDTO|null The deserialized search results DTO, cached for performance
- */
- private ?BulkSearchResponseDTO $searchResultsDTO = null;
-
- #[ORM\Column(type: Types::STRING, length: 20, enumType: BulkImportJobStatus::class)]
- private BulkImportJobStatus $status = BulkImportJobStatus::PENDING;
-
- #[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
- private \DateTimeImmutable $createdAt;
-
- #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
- private ?\DateTimeImmutable $completedAt = null;
-
- #[ORM\Column(type: Types::BOOLEAN)]
- private bool $prefetchDetails = false;
-
- #[ORM\ManyToOne(targetEntity: User::class)]
- #[ORM\JoinColumn(nullable: false)]
- private ?User $createdBy = null;
-
- /** @var Collection */
- #[ORM\OneToMany(targetEntity: BulkInfoProviderImportJobPart::class, mappedBy: 'job', cascade: ['persist', 'remove'], orphanRemoval: true)]
- private Collection $jobParts;
-
- public function __construct()
- {
- $this->createdAt = new \DateTimeImmutable();
- $this->jobParts = new ArrayCollection();
- }
-
- public function getName(): string
- {
- return $this->name;
- }
-
- public function getDisplayNameKey(): string
- {
- return 'info_providers.bulk_import.job_name_template';
- }
-
- public function getDisplayNameParams(): array
- {
- return ['%count%' => $this->getPartCount()];
- }
-
- public function getFormattedTimestamp(): string
- {
- return $this->createdAt->format('Y-m-d H:i:s');
- }
-
- public function setName(string $name): self
- {
- $this->name = $name;
- return $this;
- }
-
- public function getJobParts(): Collection
- {
- return $this->jobParts;
- }
-
- public function addJobPart(BulkInfoProviderImportJobPart $jobPart): self
- {
- if (!$this->jobParts->contains($jobPart)) {
- $this->jobParts->add($jobPart);
- $jobPart->setJob($this);
- }
- return $this;
- }
-
- public function removeJobPart(BulkInfoProviderImportJobPart $jobPart): self
- {
- if ($this->jobParts->removeElement($jobPart)) {
- if ($jobPart->getJob() === $this) {
- $jobPart->setJob(null);
- }
- }
- return $this;
- }
-
- public function getPartIds(): array
- {
- return $this->jobParts->map(fn($jobPart) => $jobPart->getPart()->getId())->toArray();
- }
-
- public function setPartIds(array $partIds): self
- {
- // This method is kept for backward compatibility but should be replaced with addJobPart
- // Clear existing job parts
- $this->jobParts->clear();
-
- // Add new job parts (this would need the actual Part entities, not just IDs)
- // This is a simplified implementation - in practice, you'd want to pass Part entities
- return $this;
- }
-
- public function addPart(Part $part): self
- {
- $jobPart = new BulkInfoProviderImportJobPart($this, $part);
- $this->addJobPart($jobPart);
- return $this;
- }
-
- /**
- * @return BulkSearchFieldMappingDTO[] The deserialized field mappings
- */
- public function getFieldMappings(): array
- {
- if ($this->fieldMappingsDTO === null) {
- // Lazy load the DTOs from the raw JSON data
- $this->fieldMappingsDTO = array_map(
- static fn($data) => BulkSearchFieldMappingDTO::fromSerializableArray($data),
- $this->fieldMappings
- );
- }
-
- return $this->fieldMappingsDTO;
- }
-
- /**
- * @param BulkSearchFieldMappingDTO[] $fieldMappings
- * @return $this
- */
- public function setFieldMappings(array $fieldMappings): self
- {
- //Ensure that we are dealing with the objects here
- if (count($fieldMappings) > 0 && !$fieldMappings[0] instanceof BulkSearchFieldMappingDTO) {
- throw new \InvalidArgumentException('Expected an array of FieldMappingDTO objects');
- }
-
- $this->fieldMappingsDTO = $fieldMappings;
-
- $this->fieldMappings = array_map(
- static fn(BulkSearchFieldMappingDTO $dto) => $dto->toSerializableArray(),
- $fieldMappings
- );
- return $this;
- }
-
- public function getSearchResultsRaw(): array
- {
- return $this->searchResults;
- }
-
- public function setSearchResultsRaw(array $searchResults): self
- {
- $this->searchResults = $searchResults;
- return $this;
- }
-
- public function setSearchResults(BulkSearchResponseDTO $searchResponse): self
- {
- $this->searchResultsDTO = $searchResponse;
- $this->searchResults = $searchResponse->toSerializableRepresentation();
- return $this;
- }
-
- public function getSearchResults(EntityManagerInterface $entityManager): BulkSearchResponseDTO
- {
- if ($this->searchResultsDTO === null) {
- // Lazy load the DTO from the raw JSON data
- $this->searchResultsDTO = BulkSearchResponseDTO::fromSerializableRepresentation($this->searchResults, $entityManager);
- }
- return $this->searchResultsDTO;
- }
-
- public function hasSearchResults(): bool
- {
- return !empty($this->searchResults);
- }
-
- public function getStatus(): BulkImportJobStatus
- {
- return $this->status;
- }
-
- public function setStatus(BulkImportJobStatus $status): self
- {
- $this->status = $status;
- return $this;
- }
-
- public function getCreatedAt(): \DateTimeImmutable
- {
- return $this->createdAt;
- }
-
- public function getCompletedAt(): ?\DateTimeImmutable
- {
- return $this->completedAt;
- }
-
- public function setCompletedAt(?\DateTimeImmutable $completedAt): self
- {
- $this->completedAt = $completedAt;
- return $this;
- }
-
- public function isPrefetchDetails(): bool
- {
- return $this->prefetchDetails;
- }
-
- public function setPrefetchDetails(bool $prefetchDetails): self
- {
- $this->prefetchDetails = $prefetchDetails;
- return $this;
- }
-
- public function getCreatedBy(): User
- {
- return $this->createdBy;
- }
-
- public function setCreatedBy(User $createdBy): self
- {
- $this->createdBy = $createdBy;
- return $this;
- }
-
- public function getProgress(): array
- {
- $progress = [];
- foreach ($this->jobParts as $jobPart) {
- $progressData = [
- 'status' => $jobPart->getStatus()->value
- ];
-
- // Only include completed_at if it's not null
- if ($jobPart->getCompletedAt() !== null) {
- $progressData['completed_at'] = $jobPart->getCompletedAt()->format('c');
- }
-
- // Only include reason if it's not null
- if ($jobPart->getReason() !== null) {
- $progressData['reason'] = $jobPart->getReason();
- }
-
- $progress[$jobPart->getPart()->getId()] = $progressData;
- }
- return $progress;
- }
-
- public function markAsCompleted(): self
- {
- $this->status = BulkImportJobStatus::COMPLETED;
- $this->completedAt = new \DateTimeImmutable();
- return $this;
- }
-
- public function markAsFailed(): self
- {
- $this->status = BulkImportJobStatus::FAILED;
- $this->completedAt = new \DateTimeImmutable();
- return $this;
- }
-
- public function markAsStopped(): self
- {
- $this->status = BulkImportJobStatus::STOPPED;
- $this->completedAt = new \DateTimeImmutable();
- return $this;
- }
-
- public function markAsInProgress(): self
- {
- $this->status = BulkImportJobStatus::IN_PROGRESS;
- return $this;
- }
-
- public function isPending(): bool
- {
- return $this->status === BulkImportJobStatus::PENDING;
- }
-
- public function isInProgress(): bool
- {
- return $this->status === BulkImportJobStatus::IN_PROGRESS;
- }
-
- public function isCompleted(): bool
- {
- return $this->status === BulkImportJobStatus::COMPLETED;
- }
-
- public function isFailed(): bool
- {
- return $this->status === BulkImportJobStatus::FAILED;
- }
-
- public function isStopped(): bool
- {
- return $this->status === BulkImportJobStatus::STOPPED;
- }
-
- public function canBeStopped(): bool
- {
- return $this->status === BulkImportJobStatus::PENDING || $this->status === BulkImportJobStatus::IN_PROGRESS;
- }
-
- public function getPartCount(): int
- {
- return $this->jobParts->count();
- }
-
- public function getResultCount(): int
- {
- $count = 0;
- foreach ($this->searchResults as $partResult) {
- $count += count($partResult['search_results'] ?? []);
- }
- return $count;
- }
-
- public function markPartAsCompleted(int $partId): self
- {
- $jobPart = $this->findJobPartByPartId($partId);
- if ($jobPart) {
- $jobPart->markAsCompleted();
- }
- return $this;
- }
-
- public function markPartAsSkipped(int $partId, string $reason = ''): self
- {
- $jobPart = $this->findJobPartByPartId($partId);
- if ($jobPart) {
- $jobPart->markAsSkipped($reason);
- }
- return $this;
- }
-
- public function markPartAsPending(int $partId): self
- {
- $jobPart = $this->findJobPartByPartId($partId);
- if ($jobPart) {
- $jobPart->markAsPending();
- }
- return $this;
- }
-
- public function isPartCompleted(int $partId): bool
- {
- $jobPart = $this->findJobPartByPartId($partId);
- return $jobPart ? $jobPart->isCompleted() : false;
- }
-
- public function isPartSkipped(int $partId): bool
- {
- $jobPart = $this->findJobPartByPartId($partId);
- return $jobPart ? $jobPart->isSkipped() : false;
- }
-
- public function getCompletedPartsCount(): int
- {
- return $this->jobParts->filter(fn($jobPart) => $jobPart->isCompleted())->count();
- }
-
- public function getSkippedPartsCount(): int
- {
- return $this->jobParts->filter(fn($jobPart) => $jobPart->isSkipped())->count();
- }
-
- private function findJobPartByPartId(int $partId): ?BulkInfoProviderImportJobPart
- {
- foreach ($this->jobParts as $jobPart) {
- if ($jobPart->getPart()->getId() === $partId) {
- return $jobPart;
- }
- }
- return null;
- }
-
- public function getProgressPercentage(): float
- {
- $total = $this->getPartCount();
- if ($total === 0) {
- return 100.0;
- }
-
- $completed = $this->getCompletedPartsCount() + $this->getSkippedPartsCount();
- return round(($completed / $total) * 100, 1);
- }
-
- public function isAllPartsCompleted(): bool
- {
- $total = $this->getPartCount();
- if ($total === 0) {
- return true;
- }
-
- $completed = $this->getCompletedPartsCount() + $this->getSkippedPartsCount();
- return $completed >= $total;
- }
-}
diff --git a/src/Entity/InfoProviderSystem/BulkInfoProviderImportJobPart.php b/src/Entity/InfoProviderSystem/BulkInfoProviderImportJobPart.php
deleted file mode 100644
index 90519561..00000000
--- a/src/Entity/InfoProviderSystem/BulkInfoProviderImportJobPart.php
+++ /dev/null
@@ -1,182 +0,0 @@
-.
- */
-
-declare(strict_types=1);
-
-/*
- * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
- *
- * Copyright (C) 2019 - 2023 Jan BΓΆhmer (https://github.com/jbtronics)
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published
- * by the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- */
-
-namespace App\Entity\InfoProviderSystem;
-
-use App\Entity\Base\AbstractDBElement;
-use App\Entity\Parts\Part;
-use Doctrine\DBAL\Types\Types;
-use Doctrine\ORM\Mapping as ORM;
-
-#[ORM\Entity]
-#[ORM\Table(name: 'bulk_info_provider_import_job_parts')]
-#[ORM\UniqueConstraint(name: 'unique_job_part', columns: ['job_id', 'part_id'])]
-class BulkInfoProviderImportJobPart extends AbstractDBElement
-{
- #[ORM\ManyToOne(targetEntity: BulkInfoProviderImportJob::class, inversedBy: 'jobParts')]
- #[ORM\JoinColumn(nullable: false)]
- private BulkInfoProviderImportJob $job;
-
- #[ORM\ManyToOne(targetEntity: Part::class, inversedBy: 'bulkImportJobParts')]
- #[ORM\JoinColumn(nullable: false)]
- private Part $part;
-
- #[ORM\Column(type: Types::STRING, length: 20, enumType: BulkImportPartStatus::class)]
- private BulkImportPartStatus $status = BulkImportPartStatus::PENDING;
-
- #[ORM\Column(type: Types::TEXT, nullable: true)]
- private ?string $reason = null;
-
- #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
- private ?\DateTimeImmutable $completedAt = null;
-
- public function __construct(BulkInfoProviderImportJob $job, Part $part)
- {
- $this->job = $job;
- $this->part = $part;
- }
-
- public function getJob(): BulkInfoProviderImportJob
- {
- return $this->job;
- }
-
- public function setJob(?BulkInfoProviderImportJob $job): self
- {
- $this->job = $job;
- return $this;
- }
-
- public function getPart(): Part
- {
- return $this->part;
- }
-
- public function setPart(?Part $part): self
- {
- $this->part = $part;
- return $this;
- }
-
- public function getStatus(): BulkImportPartStatus
- {
- return $this->status;
- }
-
- public function setStatus(BulkImportPartStatus $status): self
- {
- $this->status = $status;
- return $this;
- }
-
- public function getReason(): ?string
- {
- return $this->reason;
- }
-
- public function setReason(?string $reason): self
- {
- $this->reason = $reason;
- return $this;
- }
-
- public function getCompletedAt(): ?\DateTimeImmutable
- {
- return $this->completedAt;
- }
-
- public function setCompletedAt(?\DateTimeImmutable $completedAt): self
- {
- $this->completedAt = $completedAt;
- return $this;
- }
-
- public function markAsCompleted(): self
- {
- $this->status = BulkImportPartStatus::COMPLETED;
- $this->completedAt = new \DateTimeImmutable();
- return $this;
- }
-
- public function markAsSkipped(string $reason = ''): self
- {
- $this->status = BulkImportPartStatus::SKIPPED;
- $this->reason = $reason;
- $this->completedAt = new \DateTimeImmutable();
- return $this;
- }
-
- public function markAsFailed(string $reason = ''): self
- {
- $this->status = BulkImportPartStatus::FAILED;
- $this->reason = $reason;
- $this->completedAt = new \DateTimeImmutable();
- return $this;
- }
-
- public function markAsPending(): self
- {
- $this->status = BulkImportPartStatus::PENDING;
- $this->reason = null;
- $this->completedAt = null;
- return $this;
- }
-
- public function isPending(): bool
- {
- return $this->status === BulkImportPartStatus::PENDING;
- }
-
- public function isCompleted(): bool
- {
- return $this->status === BulkImportPartStatus::COMPLETED;
- }
-
- public function isSkipped(): bool
- {
- return $this->status === BulkImportPartStatus::SKIPPED;
- }
-
- public function isFailed(): bool
- {
- return $this->status === BulkImportPartStatus::FAILED;
- }
-}
diff --git a/src/Entity/LogSystem/LogTargetType.php b/src/Entity/LogSystem/LogTargetType.php
index 61a2b081..1c6e4f8c 100644
--- a/src/Entity/LogSystem/LogTargetType.php
+++ b/src/Entity/LogSystem/LogTargetType.php
@@ -24,8 +24,6 @@ namespace App\Entity\LogSystem;
use App\Entity\Attachments\Attachment;
use App\Entity\Attachments\AttachmentType;
-use App\Entity\InfoProviderSystem\BulkInfoProviderImportJob;
-use App\Entity\InfoProviderSystem\BulkInfoProviderImportJobPart;
use App\Entity\LabelSystem\LabelProfile;
use App\Entity\Parameters\AbstractParameter;
use App\Entity\Parts\Category;
@@ -69,8 +67,6 @@ enum LogTargetType: int
case LABEL_PROFILE = 19;
case PART_ASSOCIATION = 20;
- case BULK_INFO_PROVIDER_IMPORT_JOB = 21;
- case BULK_INFO_PROVIDER_IMPORT_JOB_PART = 22;
/**
* Returns the class name of the target type or null if the target type is NONE.
@@ -100,8 +96,6 @@ enum LogTargetType: int
self::PARAMETER => AbstractParameter::class,
self::LABEL_PROFILE => LabelProfile::class,
self::PART_ASSOCIATION => PartAssociation::class,
- self::BULK_INFO_PROVIDER_IMPORT_JOB => BulkInfoProviderImportJob::class,
- self::BULK_INFO_PROVIDER_IMPORT_JOB_PART => BulkInfoProviderImportJobPart::class,
};
}
diff --git a/src/Entity/Parts/Part.php b/src/Entity/Parts/Part.php
index 2f274a8a..14a7903f 100644
--- a/src/Entity/Parts/Part.php
+++ b/src/Entity/Parts/Part.php
@@ -22,6 +22,8 @@ declare(strict_types=1);
namespace App\Entity\Parts;
+use App\ApiPlatform\Filter\TagFilter;
+use Doctrine\Common\Collections\Criteria;
use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface;
use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
@@ -38,12 +40,10 @@ use ApiPlatform\Serializer\Filter\PropertyFilter;
use App\ApiPlatform\Filter\EntityFilter;
use App\ApiPlatform\Filter\LikeFilter;
use App\ApiPlatform\Filter\PartStoragelocationFilter;
-use App\ApiPlatform\Filter\TagFilter;
use App\Entity\Attachments\Attachment;
use App\Entity\Attachments\AttachmentContainingDBElement;
use App\Entity\Attachments\PartAttachment;
use App\Entity\EDA\EDAPartInfo;
-use App\Entity\InfoProviderSystem\BulkInfoProviderImportJobPart;
use App\Entity\Parameters\ParametersTrait;
use App\Entity\Parameters\PartParameter;
use App\Entity\Parts\PartTraits\AdvancedPropertyTrait;
@@ -59,7 +59,6 @@ use App\Repository\PartRepository;
use App\Validator\Constraints\UniqueObjectCollection;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
-use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Serializer\Annotation\Groups;
@@ -84,18 +83,8 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
#[ORM\Index(columns: ['ipn'], name: 'parts_idx_ipn')]
#[ApiResource(
operations: [
- new Get(normalizationContext: [
- 'groups' => [
- 'part:read',
- 'provider_reference:read',
- 'api:basic:read',
- 'part_lot:read',
- 'orderdetail:read',
- 'pricedetail:read',
- 'parameter:read',
- 'attachment:read',
- 'eda_info:read'
- ],
+ new Get(normalizationContext: ['groups' => ['part:read', 'provider_reference:read', 'api:basic:read', 'part_lot:read',
+ 'orderdetail:read', 'pricedetail:read', 'parameter:read', 'attachment:read', 'eda_info:read'],
'openapi_definition_name' => 'Read',
], security: 'is_granted("read", object)'),
new GetCollection(security: 'is_granted("@parts.read")'),
@@ -103,7 +92,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
new Patch(security: 'is_granted("edit", object)'),
new Delete(security: 'is_granted("delete", object)'),
],
- normalizationContext: ['groups' => ['part:read', 'provider_reference:read', 'api:basic:read', 'part_lot:read'], 'openapi_definition_name' => 'Read'],
+ normalizationContext: ['groups' => ['part:read', 'provider_reference:read', 'api:basic:read', 'part_lot:read'], 'openapi_definition_name' => 'Read'],
denormalizationContext: ['groups' => ['part:write', 'api:basic:write', 'eda_info:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
)]
#[ApiFilter(PropertyFilter::class)]
@@ -111,7 +100,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
#[ApiFilter(PartStoragelocationFilter::class, properties: ["storage_location"])]
#[ApiFilter(LikeFilter::class, properties: ["name", "comment", "description", "ipn", "manufacturer_product_number"])]
#[ApiFilter(TagFilter::class, properties: ["tags"])]
-#[ApiFilter(BooleanFilter::class, properties: ["favorite", "needs_review"])]
+#[ApiFilter(BooleanFilter::class, properties: ["favorite" , "needs_review"])]
#[ApiFilter(RangeFilter::class, properties: ["mass", "minamount"])]
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]
#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])]
@@ -171,12 +160,6 @@ class Part extends AttachmentContainingDBElement
#[Groups(['part:read'])]
protected ?\DateTimeImmutable $lastModified = null;
- /**
- * @var Collection
- */
- #[ORM\OneToMany(mappedBy: 'part', targetEntity: BulkInfoProviderImportJobPart::class, cascade: ['remove'], orphanRemoval: true)]
- protected Collection $bulkImportJobParts;
-
public function __construct()
{
@@ -189,7 +172,6 @@ class Part extends AttachmentContainingDBElement
$this->associated_parts_as_owner = new ArrayCollection();
$this->associated_parts_as_other = new ArrayCollection();
- $this->bulkImportJobParts = new ArrayCollection();
//By default, the part has no provider
$this->providerReference = InfoProviderReference::noProvider();
@@ -248,38 +230,4 @@ class Part extends AttachmentContainingDBElement
}
}
}
-
- /**
- * Get all bulk import job parts for this part
- * @return Collection
- */
- public function getBulkImportJobParts(): Collection
- {
- return $this->bulkImportJobParts;
- }
-
- /**
- * Add a bulk import job part to this part
- */
- public function addBulkImportJobPart(BulkInfoProviderImportJobPart $jobPart): self
- {
- if (!$this->bulkImportJobParts->contains($jobPart)) {
- $this->bulkImportJobParts->add($jobPart);
- $jobPart->setPart($this);
- }
- return $this;
- }
-
- /**
- * Remove a bulk import job part from this part
- */
- public function removeBulkImportJobPart(BulkInfoProviderImportJobPart $jobPart): self
- {
- if ($this->bulkImportJobParts->removeElement($jobPart)) {
- if ($jobPart->getPart() === $this) {
- $jobPart->setPart(null);
- }
- }
- return $this;
- }
}
diff --git a/src/EventListener/LogSystem/EventLoggerListener.php b/src/EventListener/LogSystem/EventLoggerListener.php
index f5029c28..96c6ef51 100644
--- a/src/EventListener/LogSystem/EventLoggerListener.php
+++ b/src/EventListener/LogSystem/EventLoggerListener.php
@@ -170,7 +170,6 @@ class EventLoggerListener
public function hasFieldRestrictions(AbstractDBElement $element): bool
{
foreach (array_keys(static::FIELD_BLACKLIST) as $class) {
- /** @var string $class */
if ($element instanceof $class) {
return true;
}
@@ -185,7 +184,6 @@ class EventLoggerListener
public function shouldFieldBeSaved(AbstractDBElement $element, string $field_name): bool
{
foreach (static::FIELD_BLACKLIST as $class => $blacklist) {
- /** @var string $class */
if ($element instanceof $class && in_array($field_name, $blacklist, true)) {
return false;
}
@@ -217,7 +215,6 @@ class EventLoggerListener
$mappings = $metadata->getAssociationMappings();
//Check if class is whitelisted for CollectionElementDeleted entry
foreach (static::TRIGGER_ASSOCIATION_LOG_WHITELIST as $class => $whitelist) {
- /** @var string $class */
if ($entity instanceof $class) {
//Check names
foreach ($mappings as $field => $mapping) {
diff --git a/src/Form/AdminPages/ImportType.php b/src/Form/AdminPages/ImportType.php
index 0bd3cea1..3e87812c 100644
--- a/src/Form/AdminPages/ImportType.php
+++ b/src/Form/AdminPages/ImportType.php
@@ -59,8 +59,6 @@ class ImportType extends AbstractType
'XML' => 'xml',
'CSV' => 'csv',
'YAML' => 'yaml',
- 'XLSX' => 'xlsx',
- 'XLS' => 'xls',
],
'label' => 'export.format',
'disabled' => $disabled,
diff --git a/src/Form/Filters/LogFilterType.php b/src/Form/Filters/LogFilterType.php
index c973ad0f..42b367b7 100644
--- a/src/Form/Filters/LogFilterType.php
+++ b/src/Form/Filters/LogFilterType.php
@@ -100,7 +100,7 @@ class LogFilterType extends AbstractType
]);
$builder->add('user', UserEntityConstraintType::class, [
- 'label' => 'log.user',
+ 'label' => 'log.user',
]);
$builder->add('targetType', EnumConstraintType::class, [
@@ -128,13 +128,11 @@ class LogFilterType extends AbstractType
LogTargetType::PARAMETER => 'parameter.label',
LogTargetType::LABEL_PROFILE => 'label_profile.label',
LogTargetType::PART_ASSOCIATION => 'part_association.label',
- LogTargetType::BULK_INFO_PROVIDER_IMPORT_JOB => 'bulk_info_provider_import_job.label',
- LogTargetType::BULK_INFO_PROVIDER_IMPORT_JOB_PART => 'bulk_info_provider_import_job_part.label',
},
]);
$builder->add('targetId', NumberConstraintType::class, [
- 'label' => 'log.target_id',
+ 'label' => 'log.target_id',
'min' => 1,
'step' => 1,
]);
diff --git a/src/Form/Filters/PartFilterType.php b/src/Form/Filters/PartFilterType.php
index 871f9b07..dfe449d1 100644
--- a/src/Form/Filters/PartFilterType.php
+++ b/src/Form/Filters/PartFilterType.php
@@ -22,12 +22,9 @@ declare(strict_types=1);
*/
namespace App\Form\Filters;
-use App\DataTables\Filters\Constraints\Part\BulkImportPartStatusConstraint;
use App\DataTables\Filters\Constraints\Part\ParameterConstraint;
use App\DataTables\Filters\PartFilter;
use App\Entity\Attachments\AttachmentType;
-use App\Entity\InfoProviderSystem\BulkImportJobStatus;
-use App\Entity\InfoProviderSystem\BulkImportPartStatus;
use App\Entity\Parts\Category;
use App\Entity\Parts\Footprint;
use App\Entity\Parts\Manufacturer;
@@ -36,12 +33,8 @@ use App\Entity\Parts\StorageLocation;
use App\Entity\Parts\Supplier;
use App\Entity\ProjectSystem\Project;
use App\Form\Filters\Constraints\BooleanConstraintType;
-use App\Form\Filters\Constraints\BulkImportJobExistsConstraintType;
-use App\Form\Filters\Constraints\BulkImportJobStatusConstraintType;
-use App\Form\Filters\Constraints\BulkImportPartStatusConstraintType;
use App\Form\Filters\Constraints\ChoiceConstraintType;
use App\Form\Filters\Constraints\DateTimeConstraintType;
-use App\Form\Filters\Constraints\EnumConstraintType;
use App\Form\Filters\Constraints\NumberConstraintType;
use App\Form\Filters\Constraints\ParameterConstraintType;
use App\Form\Filters\Constraints\StructuralEntityConstraintType;
@@ -57,8 +50,6 @@ use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
-use function Symfony\Component\Translation\t;
-
class PartFilterType extends AbstractType
{
public function __construct(private readonly Security $security)
@@ -307,31 +298,6 @@ class PartFilterType extends AbstractType
}
- /**************************************************************************
- * Bulk Import Job tab
- **************************************************************************/
- if ($this->security->isGranted('@info_providers.create_parts')) {
- $builder
- ->add('inBulkImportJob', BooleanConstraintType::class, [
- 'label' => 'part.filter.in_bulk_import_job',
- ])
- ->add('bulkImportJobStatus', EnumConstraintType::class, [
- 'enum_class' => BulkImportJobStatus::class,
- 'label' => 'part.filter.bulk_import_job_status',
- 'choice_label' => function (BulkImportJobStatus $value) {
- return t('bulk_import.status.' . $value->value);
- },
- ])
- ->add('bulkImportPartStatus', EnumConstraintType::class, [
- 'enum_class' => BulkImportPartStatus::class,
- 'label' => 'part.filter.bulk_import_part_status',
- 'choice_label' => function (BulkImportPartStatus $value) {
- return t('bulk_import.part_status.' . $value->value);
- },
- ])
- ;
- }
-
$builder->add('submit', SubmitType::class, [
'label' => 'filter.submit',
diff --git a/src/Form/InfoProviderSystem/BulkProviderSearchType.php b/src/Form/InfoProviderSystem/BulkProviderSearchType.php
deleted file mode 100644
index 24a3cfb4..00000000
--- a/src/Form/InfoProviderSystem/BulkProviderSearchType.php
+++ /dev/null
@@ -1,62 +0,0 @@
-.
- */
-
-declare(strict_types=1);
-
-namespace App\Form\InfoProviderSystem;
-
-use App\Entity\Parts\Part;
-use Symfony\Component\Form\AbstractType;
-use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
-use Symfony\Component\Form\Extension\Core\Type\CollectionType;
-use Symfony\Component\Form\Extension\Core\Type\HiddenType;
-use Symfony\Component\Form\Extension\Core\Type\SubmitType;
-use Symfony\Component\Form\FormBuilderInterface;
-use Symfony\Component\OptionsResolver\OptionsResolver;
-
-class BulkProviderSearchType extends AbstractType
-{
- public function buildForm(FormBuilderInterface $builder, array $options): void
- {
- $parts = $options['parts'];
-
- $builder->add('part_configurations', CollectionType::class, [
- 'entry_type' => PartProviderConfigurationType::class,
- 'entry_options' => [
- 'label' => false,
- ],
- 'allow_add' => false,
- 'allow_delete' => false,
- 'label' => false,
- ]);
-
- $builder->add('submit', SubmitType::class, [
- 'label' => 'info_providers.bulk_search.submit'
- ]);
- }
-
- public function configureOptions(OptionsResolver $resolver): void
- {
- $resolver->setDefaults([
- 'parts' => [],
- ]);
- $resolver->setRequired('parts');
- }
-}
\ No newline at end of file
diff --git a/src/Form/InfoProviderSystem/FieldToProviderMappingType.php b/src/Form/InfoProviderSystem/FieldToProviderMappingType.php
deleted file mode 100644
index 13e9581e..00000000
--- a/src/Form/InfoProviderSystem/FieldToProviderMappingType.php
+++ /dev/null
@@ -1,75 +0,0 @@
-.
- */
-
-declare(strict_types=1);
-
-namespace App\Form\InfoProviderSystem;
-
-use Symfony\Component\Form\AbstractType;
-use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
-use Symfony\Component\Form\Extension\Core\Type\IntegerType;
-use Symfony\Component\Form\FormBuilderInterface;
-use Symfony\Component\OptionsResolver\OptionsResolver;
-
-class FieldToProviderMappingType extends AbstractType
-{
- public function buildForm(FormBuilderInterface $builder, array $options): void
- {
- $fieldChoices = $options['field_choices'] ?? [];
-
- $builder->add('field', ChoiceType::class, [
- 'label' => 'info_providers.bulk_search.search_field',
- 'choices' => $fieldChoices,
- 'expanded' => false,
- 'multiple' => false,
- 'required' => false,
- 'placeholder' => 'info_providers.bulk_search.field.select',
- ]);
-
- $builder->add('providers', ProviderSelectType::class, [
- 'label' => 'info_providers.bulk_search.providers',
- 'help' => 'info_providers.bulk_search.providers.help',
- 'required' => false,
- ]);
-
- $builder->add('priority', IntegerType::class, [
- 'label' => 'info_providers.bulk_search.priority',
- 'help' => 'info_providers.bulk_search.priority.help',
- 'required' => false,
- 'data' => 1, // Default priority
- 'attr' => [
- 'min' => 1,
- 'max' => 10,
- 'class' => 'form-control-sm',
- 'style' => 'width: 80px;'
- ],
- 'constraints' => [
- new \Symfony\Component\Validator\Constraints\Range(['min' => 1, 'max' => 10]),
- ],
- ]);
- }
-
- public function configureOptions(OptionsResolver $resolver): void
- {
- $resolver->setDefaults([
- 'field_choices' => [],
- ]);
- }
-}
diff --git a/src/Form/InfoProviderSystem/GlobalFieldMappingType.php b/src/Form/InfoProviderSystem/GlobalFieldMappingType.php
deleted file mode 100644
index ea70284f..00000000
--- a/src/Form/InfoProviderSystem/GlobalFieldMappingType.php
+++ /dev/null
@@ -1,67 +0,0 @@
-.
- */
-
-declare(strict_types=1);
-
-namespace App\Form\InfoProviderSystem;
-
-use Symfony\Component\Form\AbstractType;
-use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
-use Symfony\Component\Form\Extension\Core\Type\CollectionType;
-use Symfony\Component\Form\Extension\Core\Type\SubmitType;
-use Symfony\Component\Form\FormBuilderInterface;
-use Symfony\Component\OptionsResolver\OptionsResolver;
-
-class GlobalFieldMappingType extends AbstractType
-{
- public function buildForm(FormBuilderInterface $builder, array $options): void
- {
- $fieldChoices = $options['field_choices'] ?? [];
-
- $builder->add('field_mappings', CollectionType::class, [
- 'entry_type' => FieldToProviderMappingType::class,
- 'entry_options' => [
- 'label' => false,
- 'field_choices' => $fieldChoices,
- ],
- 'allow_add' => true,
- 'allow_delete' => true,
- 'prototype' => true,
- 'label' => false,
- ]);
-
- $builder->add('prefetch_details', CheckboxType::class, [
- 'label' => 'info_providers.bulk_import.prefetch_details',
- 'required' => false,
- 'help' => 'info_providers.bulk_import.prefetch_details_help',
- ]);
-
- $builder->add('submit', SubmitType::class, [
- 'label' => 'info_providers.bulk_import.search.submit'
- ]);
- }
-
- public function configureOptions(OptionsResolver $resolver): void
- {
- $resolver->setDefaults([
- 'field_choices' => [],
- ]);
- }
-}
\ No newline at end of file
diff --git a/src/Form/InfoProviderSystem/PartProviderConfigurationType.php b/src/Form/InfoProviderSystem/PartProviderConfigurationType.php
deleted file mode 100644
index cecf62a3..00000000
--- a/src/Form/InfoProviderSystem/PartProviderConfigurationType.php
+++ /dev/null
@@ -1,55 +0,0 @@
-.
- */
-
-declare(strict_types=1);
-
-namespace App\Form\InfoProviderSystem;
-
-use Symfony\Component\Form\AbstractType;
-use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
-use Symfony\Component\Form\Extension\Core\Type\HiddenType;
-use Symfony\Component\Form\FormBuilderInterface;
-
-class PartProviderConfigurationType extends AbstractType
-{
- public function buildForm(FormBuilderInterface $builder, array $options): void
- {
- $builder->add('part_id', HiddenType::class);
-
- $builder->add('search_field', ChoiceType::class, [
- 'label' => 'info_providers.bulk_search.search_field',
- 'choices' => [
- 'info_providers.bulk_search.field.mpn' => 'mpn',
- 'info_providers.bulk_search.field.name' => 'name',
- 'info_providers.bulk_search.field.digikey_spn' => 'digikey_spn',
- 'info_providers.bulk_search.field.mouser_spn' => 'mouser_spn',
- 'info_providers.bulk_search.field.lcsc_spn' => 'lcsc_spn',
- 'info_providers.bulk_search.field.farnell_spn' => 'farnell_spn',
- ],
- 'expanded' => false,
- 'multiple' => false,
- ]);
-
- $builder->add('providers', ProviderSelectType::class, [
- 'label' => 'info_providers.bulk_search.providers',
- 'help' => 'info_providers.bulk_search.providers.help',
- ]);
- }
-}
\ No newline at end of file
diff --git a/src/Services/Attachments/AttachmentSubmitHandler.php b/src/Services/Attachments/AttachmentSubmitHandler.php
index 9fbc3fe3..a30163ae 100644
--- a/src/Services/Attachments/AttachmentSubmitHandler.php
+++ b/src/Services/Attachments/AttachmentSubmitHandler.php
@@ -57,9 +57,6 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
*/
class AttachmentSubmitHandler
{
- /**
- * @var array The mapping used to determine which folder will be used for an attachment type
- */
protected array $folder_mapping;
private ?int $max_upload_size_bytes = null;
@@ -163,7 +160,6 @@ class AttachmentSubmitHandler
} else {
//If not, check for instance of:
foreach ($this->folder_mapping as $class => $folder) {
- /** @var string $class */
if ($attachment instanceof $class) {
$prefix = $folder;
break;
diff --git a/src/Services/ElementTypeNameGenerator.php b/src/Services/ElementTypeNameGenerator.php
index 326707b7..14247145 100644
--- a/src/Services/ElementTypeNameGenerator.php
+++ b/src/Services/ElementTypeNameGenerator.php
@@ -22,13 +22,13 @@ declare(strict_types=1);
namespace App\Services;
-use App\Entity\Attachments\Attachment;
use App\Entity\Attachments\AttachmentContainingDBElement;
+use App\Entity\Attachments\Attachment;
use App\Entity\Attachments\AttachmentType;
use App\Entity\Base\AbstractDBElement;
use App\Entity\Contracts\NamedElementInterface;
-use App\Entity\InfoProviderSystem\BulkInfoProviderImportJob;
-use App\Entity\InfoProviderSystem\BulkInfoProviderImportJobPart;
+use App\Entity\Parts\PartAssociation;
+use App\Entity\ProjectSystem\Project;
use App\Entity\LabelSystem\LabelProfile;
use App\Entity\Parameters\AbstractParameter;
use App\Entity\Parts\Category;
@@ -36,14 +36,12 @@ use App\Entity\Parts\Footprint;
use App\Entity\Parts\Manufacturer;
use App\Entity\Parts\MeasurementUnit;
use App\Entity\Parts\Part;
-use App\Entity\Parts\PartAssociation;
use App\Entity\Parts\PartLot;
use App\Entity\Parts\StorageLocation;
use App\Entity\Parts\Supplier;
use App\Entity\PriceInformations\Currency;
use App\Entity\PriceInformations\Orderdetail;
use App\Entity\PriceInformations\Pricedetail;
-use App\Entity\ProjectSystem\Project;
use App\Entity\ProjectSystem\ProjectBOMEntry;
use App\Entity\UserSystem\Group;
use App\Entity\UserSystem\User;
@@ -81,8 +79,6 @@ class ElementTypeNameGenerator
AbstractParameter::class => $this->translator->trans('parameter.label'),
LabelProfile::class => $this->translator->trans('label_profile.label'),
PartAssociation::class => $this->translator->trans('part_association.label'),
- BulkInfoProviderImportJob::class => $this->translator->trans('bulk_info_provider_import_job.label'),
- BulkInfoProviderImportJobPart::class => $this->translator->trans('bulk_info_provider_import_job_part.label'),
];
}
@@ -134,10 +130,10 @@ class ElementTypeNameGenerator
{
$type = $this->getLocalizedTypeLabel($entity);
if ($use_html) {
- return '' . $type . ': ' . htmlspecialchars($entity->getName());
+ return ''.$type.': '.htmlspecialchars($entity->getName());
}
- return $type . ': ' . $entity->getName();
+ return $type.': '.$entity->getName();
}
diff --git a/src/Services/EntityMergers/Mergers/PartMerger.php b/src/Services/EntityMergers/Mergers/PartMerger.php
index 01b53e25..4ce779e8 100644
--- a/src/Services/EntityMergers/Mergers/PartMerger.php
+++ b/src/Services/EntityMergers/Mergers/PartMerger.php
@@ -100,8 +100,7 @@ class PartMerger implements EntityMergerInterface
return $target;
}
- private function comparePartAssociations(PartAssociation $t, PartAssociation $o): bool
- {
+ private function comparePartAssociations(PartAssociation $t, PartAssociation $o): bool {
//We compare the translation keys, as it contains info about the type and other type info
return $t->getOther() === $o->getOther()
&& $t->getTypeTranslationKey() === $o->getTypeTranslationKey();
@@ -142,39 +141,40 @@ class PartMerger implements EntityMergerInterface
$owner->addAssociatedPartsAsOwner($clone);
}
- // Merge orderdetails, considering same supplier+part number as duplicates
$this->mergeCollections($target, $other, 'orderdetails', function (Orderdetail $t, Orderdetail $o) {
- // If supplier and part number match, merge the orderdetails
- if ($t->getSupplier() === $o->getSupplier() && $t->getSupplierPartNr() === $o->getSupplierPartNr()) {
- // Update URL if target doesn't have one
- if (empty($t->getSupplierProductUrl(false)) && !empty($o->getSupplierProductUrl(false))) {
- $t->setSupplierProductUrl($o->getSupplierProductUrl(false));
- }
- // Merge price details: add new ones, update empty ones, keep existing non-empty ones
- foreach ($o->getPricedetails() as $otherPrice) {
- $found = false;
- foreach ($t->getPricedetails() as $targetPrice) {
- if ($targetPrice->getMinDiscountQuantity() === $otherPrice->getMinDiscountQuantity()
- && $targetPrice->getCurrency() === $otherPrice->getCurrency()) {
- // Only update price if the existing one is zero/empty (most logical)
- if ($targetPrice->getPrice()->isZero()) {
- $targetPrice->setPrice($otherPrice->getPrice());
- $targetPrice->setPriceRelatedQuantity($otherPrice->getPriceRelatedQuantity());
- }
- $found = true;
- break;
- }
- }
- // Add completely new price tiers
- if (!$found) {
- $clonedPrice = clone $otherPrice;
- $clonedPrice->setOrderdetail($t);
- $t->addPricedetail($clonedPrice);
- }
- }
- return true; // Consider them equal so the other one gets skipped
+ //First check that the orderdetails infos are equal
+ $tmp = $t->getSupplier() === $o->getSupplier()
+ && $t->getSupplierPartNr() === $o->getSupplierPartNr()
+ && $t->getSupplierProductUrl(false) === $o->getSupplierProductUrl(false);
+
+ if (!$tmp) {
+ return false;
}
- return false; // Different supplier/part number, add as new
+
+ //Check if the pricedetails are equal
+ $t_pricedetails = $t->getPricedetails();
+ $o_pricedetails = $o->getPricedetails();
+ //Ensure that both pricedetails have the same length
+ if (count($t_pricedetails) !== count($o_pricedetails)) {
+ return false;
+ }
+
+ //Check if all pricedetails are equal
+ for ($n=0, $nMax = count($t_pricedetails); $n< $nMax; $n++) {
+ $t_price = $t_pricedetails->get($n);
+ $o_price = $o_pricedetails->get($n);
+
+ if (!$t_price->getPrice()->isEqualTo($o_price->getPrice())
+ || $t_price->getCurrency() !== $o_price->getCurrency()
+ || $t_price->getPriceRelatedQuantity() !== $o_price->getPriceRelatedQuantity()
+ || $t_price->getMinDiscountQuantity() !== $o_price->getMinDiscountQuantity()
+ ) {
+ return false;
+ }
+ }
+
+ //If all pricedetails are equal, the orderdetails are equal
+ return true;
});
//The pricedetails are not correctly assigned to the new orderdetails, so fix that
foreach ($target->getOrderdetails() as $orderdetail) {
diff --git a/src/Services/ImportExportSystem/EntityExporter.php b/src/Services/ImportExportSystem/EntityExporter.php
index 70feb8e6..271642da 100644
--- a/src/Services/ImportExportSystem/EntityExporter.php
+++ b/src/Services/ImportExportSystem/EntityExporter.php
@@ -38,9 +38,6 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\Serializer\SerializerInterface;
use function Symfony\Component\String\u;
-use PhpOffice\PhpSpreadsheet\Spreadsheet;
-use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
-use PhpOffice\PhpSpreadsheet\Writer\Xls;
/**
* Use this class to export an entity to multiple file formats.
@@ -55,7 +52,7 @@ class EntityExporter
protected function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefault('format', 'csv');
- $resolver->setAllowedValues('format', ['csv', 'json', 'xml', 'yaml', 'xlsx', 'xls']);
+ $resolver->setAllowedValues('format', ['csv', 'json', 'xml', 'yaml']);
$resolver->setDefault('csv_delimiter', ';');
$resolver->setAllowedTypes('csv_delimiter', 'string');
@@ -91,35 +88,28 @@ class EntityExporter
$options = $resolver->resolve($options);
- //Handle Excel formats by converting from CSV
- if (in_array($options['format'], ['xlsx', 'xls'], true)) {
- return $this->exportToExcel($entities, $options);
- }
-
//If include children is set, then we need to add the include_children group
$groups = [$options['level']];
if ($options['include_children']) {
$groups[] = 'include_children';
}
- return $this->serializer->serialize(
- $entities,
- $options['format'],
+ return $this->serializer->serialize($entities, $options['format'],
[
'groups' => $groups,
'as_collection' => true,
'csv_delimiter' => $options['csv_delimiter'],
'xml_root_node_name' => 'PartDBExport',
'partdb_export' => true,
- //Skip the item normalizer, so that we dont get IRIs in the output
+ //Skip the item normalizer, so that we dont get IRIs in the output
SkippableItemNormalizer::DISABLE_ITEM_NORMALIZER => true,
- //Handle circular references
+ //Handle circular references
AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER => $this->handleCircularReference(...),
]
);
}
- private function handleCircularReference(object $object): string
+ private function handleCircularReference(object $object, string $format, array $context): string
{
if ($object instanceof AbstractStructuralDBElement) {
return $object->getFullPath("->");
@@ -129,75 +119,7 @@ class EntityExporter
return $object->__toString();
}
- throw new CircularReferenceException('Circular reference detected for object of type ' . get_class($object));
- }
-
- /**
- * Exports entities to Excel format (xlsx or xls).
- *
- * @param AbstractNamedDBElement[] $entities The entities to export
- * @param array $options The export options
- *
- * @return string The Excel file content as binary string
- */
- protected function exportToExcel(array $entities, array $options): string
- {
- //First get CSV data using existing serializer
- $groups = [$options['level']];
- if ($options['include_children']) {
- $groups[] = 'include_children';
- }
-
- $csvData = $this->serializer->serialize(
- $entities,
- 'csv',
- [
- 'groups' => $groups,
- 'as_collection' => true,
- 'csv_delimiter' => $options['csv_delimiter'],
- 'partdb_export' => true,
- SkippableItemNormalizer::DISABLE_ITEM_NORMALIZER => true,
- AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER => $this->handleCircularReference(...),
- ]
- );
-
- //Convert CSV to Excel
- $spreadsheet = new Spreadsheet();
- $worksheet = $spreadsheet->getActiveSheet();
-
- $rows = explode("\n", $csvData);
- $rowIndex = 1;
-
- foreach ($rows as $row) {
- if (trim($row) === '') {
- continue;
- }
-
- $columns = str_getcsv($row, $options['csv_delimiter'], '"', '\\');
- $colIndex = 1;
-
- foreach ($columns as $column) {
- $cellCoordinate = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($colIndex) . $rowIndex;
- $worksheet->setCellValue($cellCoordinate, $column);
- $colIndex++;
- }
- $rowIndex++;
- }
-
- //Save to memory stream
- $writer = $options['format'] === 'xlsx' ? new Xlsx($spreadsheet) : new Xls($spreadsheet);
-
- $memFile = fopen("php://temp", 'r+b');
- $writer->save($memFile);
- rewind($memFile);
- $content = stream_get_contents($memFile);
- fclose($memFile);
-
- if ($content === false) {
- throw new \RuntimeException('Failed to read Excel content from memory stream.');
- }
-
- return $content;
+ throw new CircularReferenceException('Circular reference detected for object of type '.get_class($object));
}
/**
@@ -234,15 +156,19 @@ class EntityExporter
//Determine the content type for the response
+ //Plain text should work for all types
+ $content_type = 'text/plain';
+
//Try to use better content types based on the format
$format = $options['format'];
- $content_type = match ($format) {
- 'xml' => 'application/xml',
- 'json' => 'application/json',
- 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
- 'xls' => 'application/vnd.ms-excel',
- default => 'text/plain',
- };
+ switch ($format) {
+ case 'xml':
+ $content_type = 'application/xml';
+ break;
+ case 'json':
+ $content_type = 'application/json';
+ break;
+ }
$response->headers->set('Content-Type', $content_type);
//If view option is not specified, then download the file.
@@ -260,7 +186,7 @@ class EntityExporter
$level = $options['level'];
- $filename = "export_{$entity_name}_{$level}.{$format}";
+ $filename = 'export_'.$entity_name.'_'.$level.'.'.$format;
//Sanitize the filename
$filename = FilenameSanatizer::sanitizeFilename($filename);
diff --git a/src/Services/ImportExportSystem/EntityImporter.php b/src/Services/ImportExportSystem/EntityImporter.php
index 459866ba..11915cfb 100644
--- a/src/Services/ImportExportSystem/EntityImporter.php
+++ b/src/Services/ImportExportSystem/EntityImporter.php
@@ -38,9 +38,6 @@ use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
-use PhpOffice\PhpSpreadsheet\IOFactory;
-use PhpOffice\PhpSpreadsheet\Spreadsheet;
-use Psr\Log\LoggerInterface;
/**
* @see \App\Tests\Services\ImportExportSystem\EntityImporterTest
@@ -53,7 +50,7 @@ class EntityImporter
*/
private const ENCODINGS = ["ASCII", "UTF-8", "ISO-8859-1", "ISO-8859-15", "Windows-1252", "UTF-16", "UTF-32"];
- public function __construct(protected SerializerInterface $serializer, protected EntityManagerInterface $em, protected ValidatorInterface $validator, protected LoggerInterface $logger)
+ public function __construct(protected SerializerInterface $serializer, protected EntityManagerInterface $em, protected ValidatorInterface $validator)
{
}
@@ -105,7 +102,7 @@ class EntityImporter
foreach ($names as $name) {
//Count indentation level (whitespace characters at the beginning of the line)
- $identSize = strlen($name) - strlen(ltrim($name));
+ $identSize = strlen($name)-strlen(ltrim($name));
//If the line is intended more than the last line, we have a new parent element
if ($identSize > end($indentations)) {
@@ -198,20 +195,16 @@ class EntityImporter
}
//The [] behind class_name denotes that we expect an array.
- $entities = $this->serializer->deserialize(
- $data,
- $options['class'] . '[]',
- $options['format'],
+ $entities = $this->serializer->deserialize($data, $options['class'].'[]', $options['format'],
[
'groups' => $groups,
'csv_delimiter' => $options['csv_delimiter'],
'create_unknown_datastructures' => $options['create_unknown_datastructures'],
'path_delimiter' => $options['path_delimiter'],
'partdb_import' => true,
- //Disable API Platform normalizer, as we don't want to use it here
+ //Disable API Platform normalizer, as we don't want to use it here
SkippableItemNormalizer::DISABLE_ITEM_NORMALIZER => true,
- ]
- );
+ ]);
//Ensure we have an array of entity elements.
if (!is_array($entities)) {
@@ -286,7 +279,7 @@ class EntityImporter
'path_delimiter' => '->', //The delimiter used to separate the path elements in the name of a structural element
]);
- $resolver->setAllowedValues('format', ['csv', 'json', 'xml', 'yaml', 'xlsx', 'xls']);
+ $resolver->setAllowedValues('format', ['csv', 'json', 'xml', 'yaml']);
$resolver->setAllowedTypes('csv_delimiter', 'string');
$resolver->setAllowedTypes('preserve_children', 'bool');
$resolver->setAllowedTypes('class', 'string');
@@ -342,33 +335,6 @@ class EntityImporter
*/
public function importFile(File $file, array $options = [], array &$errors = []): array
{
- $resolver = new OptionsResolver();
- $this->configureOptions($resolver);
- $options = $resolver->resolve($options);
-
- if (in_array($options['format'], ['xlsx', 'xls'], true)) {
- $this->logger->info('Converting Excel file to CSV', [
- 'filename' => $file->getFilename(),
- 'format' => $options['format'],
- 'delimiter' => $options['csv_delimiter']
- ]);
-
- $csvData = $this->convertExcelToCsv($file, $options['csv_delimiter']);
- $options['format'] = 'csv';
-
- $this->logger->debug('Excel to CSV conversion completed', [
- 'csv_length' => strlen($csvData),
- 'csv_lines' => substr_count($csvData, "\n") + 1
- ]);
-
- // Log the converted CSV for debugging (first 1000 characters)
- $this->logger->debug('Converted CSV preview', [
- 'csv_preview' => substr($csvData, 0, 1000) . (strlen($csvData) > 1000 ? '...' : '')
- ]);
-
- return $this->importString($csvData, $options, $errors);
- }
-
return $this->importString($file->getContent(), $options, $errors);
}
@@ -388,103 +354,10 @@ class EntityImporter
'xml' => 'xml',
'csv', 'tsv' => 'csv',
'yaml', 'yml' => 'yaml',
- 'xlsx' => 'xlsx',
- 'xls' => 'xls',
default => null,
};
}
- /**
- * Converts Excel file to CSV format using PhpSpreadsheet.
- *
- * @param File $file The Excel file to convert
- * @param string $delimiter The CSV delimiter to use
- *
- * @return string The CSV data as string
- */
- protected function convertExcelToCsv(File $file, string $delimiter = ';'): string
- {
- try {
- $this->logger->debug('Loading Excel file', ['path' => $file->getPathname()]);
- $spreadsheet = IOFactory::load($file->getPathname());
- $worksheet = $spreadsheet->getActiveSheet();
-
- $csvData = [];
- $highestRow = $worksheet->getHighestRow();
- $highestColumn = $worksheet->getHighestColumn();
-
- $this->logger->debug('Excel file dimensions', [
- 'rows' => $highestRow,
- 'columns_detected' => $highestColumn,
- 'worksheet_title' => $worksheet->getTitle()
- ]);
-
- $highestColumnIndex = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($highestColumn);
-
- for ($row = 1; $row <= $highestRow; $row++) {
- $rowData = [];
-
- // Read all columns using numeric index
- for ($colIndex = 1; $colIndex <= $highestColumnIndex; $colIndex++) {
- $col = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($colIndex);
- try {
- $cellValue = $worksheet->getCell("{$col}{$row}")->getCalculatedValue();
- $rowData[] = $cellValue ?? '';
-
- } catch (\Exception $e) {
- $this->logger->warning('Error reading cell value', [
- 'cell' => "{$col}{$row}",
- 'error' => $e->getMessage()
- ]);
- $rowData[] = '';
- }
- }
-
- $csvRow = implode($delimiter, array_map(function ($value) use ($delimiter) {
- $value = (string) $value;
- if (strpos($value, $delimiter) !== false || strpos($value, '"') !== false || strpos($value, "\n") !== false) {
- return '"' . str_replace('"', '""', $value) . '"';
- }
- return $value;
- }, $rowData));
-
- $csvData[] = $csvRow;
-
- // Log first few rows for debugging
- if ($row <= 3) {
- $this->logger->debug("Row {$row} converted", [
- 'original_data' => $rowData,
- 'csv_row' => $csvRow,
- 'first_cell_raw' => $worksheet->getCell("A{$row}")->getValue(),
- 'first_cell_calculated' => $worksheet->getCell("A{$row}")->getCalculatedValue()
- ]);
- }
- }
-
- $result = implode("\n", $csvData);
-
- $this->logger->info('Excel to CSV conversion successful', [
- 'total_rows' => count($csvData),
- 'total_characters' => strlen($result)
- ]);
-
- $this->logger->debug('Full CSV data', [
- 'csv_data' => $result
- ]);
-
- return $result;
-
- } catch (\Exception $e) {
- $this->logger->error('Failed to convert Excel to CSV', [
- 'file' => $file->getFilename(),
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
- throw $e;
- }
- }
-
-
/**
* This functions corrects the parent setting based on the children value of the parent.
*
diff --git a/src/Services/InfoProviderSystem/BulkInfoProviderService.php b/src/Services/InfoProviderSystem/BulkInfoProviderService.php
deleted file mode 100644
index 586fb873..00000000
--- a/src/Services/InfoProviderSystem/BulkInfoProviderService.php
+++ /dev/null
@@ -1,380 +0,0 @@
- Cache for normalized supplier names */
- private array $supplierCache = [];
-
- public function __construct(
- private readonly PartInfoRetriever $infoRetriever,
- private readonly ExistingPartFinder $existingPartFinder,
- private readonly ProviderRegistry $providerRegistry,
- private readonly EntityManagerInterface $entityManager,
- private readonly LoggerInterface $logger
- ) {}
-
- /**
- * Perform bulk search across multiple parts and providers.
- *
- * @param Part[] $parts Array of parts to search for
- * @param BulkSearchFieldMappingDTO[] $fieldMappings Array of field mappings defining search strategy
- * @param bool $prefetchDetails Whether to prefetch detailed information for results
- * @return BulkSearchResponseDTO Structured response containing all search results
- * @throws \InvalidArgumentException If no valid parts provided
- * @throws \RuntimeException If no search results found for any parts
- */
- public function performBulkSearch(array $parts, array $fieldMappings, bool $prefetchDetails = false): BulkSearchResponseDTO
- {
- if (empty($parts)) {
- throw new \InvalidArgumentException('No valid parts found for bulk import');
- }
-
- $partResults = [];
- $hasAnyResults = false;
-
- // Group providers by batch capability
- $batchProviders = [];
- $regularProviders = [];
-
- foreach ($fieldMappings as $mapping) {
- foreach ($mapping->providers as $providerKey) {
- if (!is_string($providerKey)) {
- $this->logger->error('Invalid provider key type', [
- 'providerKey' => $providerKey,
- 'type' => gettype($providerKey)
- ]);
- continue;
- }
-
- $provider = $this->providerRegistry->getProviderByKey($providerKey);
- if ($provider instanceof BatchInfoProviderInterface) {
- $batchProviders[$providerKey] = $provider;
- } else {
- $regularProviders[$providerKey] = $provider;
- }
- }
- }
-
- // Process batch providers first (more efficient)
- $batchResults = $this->processBatchProviders($parts, $fieldMappings, $batchProviders);
-
- // Process regular providers
- $regularResults = $this->processRegularProviders($parts, $fieldMappings, $regularProviders, $batchResults);
-
- // Combine and format results for each part
- foreach ($parts as $part) {
- $searchResults = [];
-
- // Get results from batch and regular processing
- $allResults = array_merge(
- $batchResults[$part->getId()] ?? [],
- $regularResults[$part->getId()] ?? []
- );
-
- if (!empty($allResults)) {
- $hasAnyResults = true;
- $searchResults = $this->formatSearchResults($allResults);
- }
-
- $partResults[] = new BulkSearchPartResultsDTO(
- part: $part,
- searchResults: $searchResults,
- errors: []
- );
- }
-
- if (!$hasAnyResults) {
- throw new \RuntimeException('No search results found for any of the selected parts');
- }
-
- $response = new BulkSearchResponseDTO($partResults);
-
- // Prefetch details if requested
- if ($prefetchDetails) {
- $this->prefetchDetailsForResults($response);
- }
-
- return $response;
- }
-
- /**
- * Process parts using batch-capable info providers.
- *
- * @param Part[] $parts Array of parts to search for
- * @param BulkSearchFieldMappingDTO[] $fieldMappings Array of field mapping configurations
- * @param array $batchProviders Batch providers indexed by key
- * @return array Results indexed by part ID
- */
- private function processBatchProviders(array $parts, array $fieldMappings, array $batchProviders): array
- {
- $batchResults = [];
-
- foreach ($batchProviders as $providerKey => $provider) {
- $keywords = $this->collectKeywordsForProvider($parts, $fieldMappings, $providerKey);
-
- if (empty($keywords)) {
- continue;
- }
-
- try {
- $providerResults = $provider->searchByKeywordsBatch($keywords);
-
- // Map results back to parts
- foreach ($parts as $part) {
- foreach ($fieldMappings as $mapping) {
- if (!in_array($providerKey, $mapping->providers, true)) {
- continue;
- }
-
- $keyword = $this->getKeywordFromField($part, $mapping->field);
- if ($keyword && isset($providerResults[$keyword])) {
- foreach ($providerResults[$keyword] as $dto) {
- $batchResults[$part->getId()][] = new BulkSearchPartResultDTO(
- searchResult: $dto,
- sourceField: $mapping->field,
- sourceKeyword: $keyword,
- localPart: $this->existingPartFinder->findFirstExisting($dto),
- priority: $mapping->priority
- );
- }
- }
- }
- }
- } catch (\Exception $e) {
- $this->logger->error('Batch search failed for provider ' . $providerKey, [
- 'error' => $e->getMessage(),
- 'provider' => $providerKey
- ]);
- }
- }
-
- return $batchResults;
- }
-
- /**
- * Process parts using regular (non-batch) info providers.
- *
- * @param Part[] $parts Array of parts to search for
- * @param BulkSearchFieldMappingDTO[] $fieldMappings Array of field mapping configurations
- * @param array $regularProviders Regular providers indexed by key
- * @param array $excludeResults Results to exclude (from batch processing)
- * @return array Results indexed by part ID
- */
- private function processRegularProviders(array $parts, array $fieldMappings, array $regularProviders, array $excludeResults): array
- {
- $regularResults = [];
-
- foreach ($parts as $part) {
- $regularResults[$part->getId()] = [];
-
- // Skip if we already have batch results for this part
- if (!empty($excludeResults[$part->getId()] ?? [])) {
- continue;
- }
-
- foreach ($fieldMappings as $mapping) {
- $providers = array_intersect($mapping->providers, array_keys($regularProviders));
-
- if (empty($providers)) {
- continue;
- }
-
- $keyword = $this->getKeywordFromField($part, $mapping->field);
- if (!$keyword) {
- continue;
- }
-
- try {
- $dtos = $this->infoRetriever->searchByKeyword($keyword, $providers);
-
- foreach ($dtos as $dto) {
- $regularResults[$part->getId()][] = new BulkSearchPartResultDTO(
- searchResult: $dto,
- sourceField: $mapping->field,
- sourceKeyword: $keyword,
- localPart: $this->existingPartFinder->findFirstExisting($dto),
- priority: $mapping->priority
- );
- }
- } catch (ClientException $e) {
- $this->logger->error('Regular search failed', [
- 'part_id' => $part->getId(),
- 'field' => $mapping->field,
- 'error' => $e->getMessage()
- ]);
- }
- }
- }
-
- return $regularResults;
- }
-
- /**
- * Collect unique keywords for a specific provider from all parts and field mappings.
- *
- * @param Part[] $parts Array of parts to collect keywords from
- * @param BulkSearchFieldMappingDTO[] $fieldMappings Array of field mapping configurations
- * @param string $providerKey The provider key to collect keywords for
- * @return string[] Array of unique keywords
- */
- private function collectKeywordsForProvider(array $parts, array $fieldMappings, string $providerKey): array
- {
- $keywords = [];
-
- foreach ($parts as $part) {
- foreach ($fieldMappings as $mapping) {
- if (!in_array($providerKey, $mapping->providers, true)) {
- continue;
- }
-
- $keyword = $this->getKeywordFromField($part, $mapping->field);
- if ($keyword && !in_array($keyword, $keywords, true)) {
- $keywords[] = $keyword;
- }
- }
- }
-
- return $keywords;
- }
-
- private function getKeywordFromField(Part $part, string $field): ?string
- {
- return match ($field) {
- 'mpn' => $part->getManufacturerProductNumber(),
- 'name' => $part->getName(),
- default => $this->getSupplierPartNumber($part, $field)
- };
- }
-
- private function getSupplierPartNumber(Part $part, string $field): ?string
- {
- if (!str_ends_with($field, '_spn')) {
- return null;
- }
-
- $supplierKey = substr($field, 0, -4);
- $supplier = $this->getSupplierByNormalizedName($supplierKey);
-
- if (!$supplier) {
- return null;
- }
-
- $orderDetail = $part->getOrderdetails()->filter(
- fn($od) => $od->getSupplier()?->getId() === $supplier->getId()
- )->first();
-
- return $orderDetail !== false ? $orderDetail->getSupplierpartnr() : null;
- }
-
- /**
- * Get supplier by normalized name with caching to prevent N+1 queries.
- *
- * @param string $normalizedKey The normalized supplier key to search for
- * @return Supplier|null The matching supplier or null if not found
- */
- private function getSupplierByNormalizedName(string $normalizedKey): ?Supplier
- {
- // Check cache first
- if (isset($this->supplierCache[$normalizedKey])) {
- return $this->supplierCache[$normalizedKey];
- }
-
- // Use efficient database query with PHP normalization
- // Since DQL doesn't support REPLACE, we'll load all suppliers once and cache the normalization
- if (empty($this->supplierCache)) {
- $this->loadSuppliersIntoCache();
- }
-
- $supplier = $this->supplierCache[$normalizedKey] ?? null;
-
- // Cache the result (including null results to prevent repeated queries)
- $this->supplierCache[$normalizedKey] = $supplier;
-
- return $supplier;
- }
-
- /**
- * Load all suppliers into cache with normalized names to avoid N+1 queries.
- */
- private function loadSuppliersIntoCache(): void
- {
- /** @var Supplier[] $suppliers */
- $suppliers = $this->entityManager->getRepository(Supplier::class)->findAll();
-
- foreach ($suppliers as $supplier) {
- $normalizedName = strtolower(str_replace([' ', '-', '_'], '_', $supplier->getName()));
- $this->supplierCache[$normalizedName] = $supplier;
- }
- }
-
- /**
- * Format and deduplicate search results.
- *
- * @param BulkSearchPartResultDTO[] $bulkResults Array of bulk search results
- * @return BulkSearchPartResultDTO[] Array of formatted search results with metadata
- */
- private function formatSearchResults(array $bulkResults): array
- {
- // Sort by priority and remove duplicates
- usort($bulkResults, fn($a, $b) => $a->priority <=> $b->priority);
-
- $uniqueResults = [];
- $seenKeys = [];
-
- foreach ($bulkResults as $result) {
- $key = "{$result->searchResult->provider_key}|{$result->searchResult->provider_id}";
- if (!in_array($key, $seenKeys, true)) {
- $seenKeys[] = $key;
- $uniqueResults[] = $result;
- }
- }
-
- return $uniqueResults;
- }
-
- /**
- * Prefetch detailed information for search results.
- *
- * @param BulkSearchResponseDTO $searchResults Search results (supports both new DTO and legacy array format)
- */
- public function prefetchDetailsForResults(BulkSearchResponseDTO $searchResults): void
- {
- $prefetchCount = 0;
-
- // Handle both new DTO format and legacy array format for backwards compatibility
- foreach ($searchResults->partResults as $partResult) {
- foreach ($partResult->searchResults as $result) {
- $dto = $result->searchResult;
-
- try {
- $this->infoRetriever->getDetails($dto->provider_key, $dto->provider_id);
- $prefetchCount++;
- } catch (\Exception $e) {
- $this->logger->warning('Failed to prefetch details for provider part', [
- 'provider_key' => $dto->provider_key,
- 'provider_id' => $dto->provider_id,
- 'error' => $e->getMessage()
- ]);
- }
- }
- }
-
- $this->logger->info("Prefetched details for {$prefetchCount} search results");
- }
-}
diff --git a/src/Services/InfoProviderSystem/DTOs/BulkSearchFieldMappingDTO.php b/src/Services/InfoProviderSystem/DTOs/BulkSearchFieldMappingDTO.php
deleted file mode 100644
index 50b7f4cf..00000000
--- a/src/Services/InfoProviderSystem/DTOs/BulkSearchFieldMappingDTO.php
+++ /dev/null
@@ -1,91 +0,0 @@
-.
- */
-
-declare(strict_types=1);
-
-namespace App\Services\InfoProviderSystem\DTOs;
-
-/**
- * Represents a mapping between a part field and the info providers that should search in that field.
- */
-readonly class BulkSearchFieldMappingDTO
-{
- /**
- * @param string $field The field to search in (e.g., 'mpn', 'name', or supplier-specific fields like 'digikey_spn')
- * @param string[] $providers Array of provider keys to search with (e.g., ['digikey', 'farnell'])
- * @param int $priority Priority for this field mapping (1-10, lower numbers = higher priority)
- */
- public function __construct(
- public string $field,
- public array $providers,
- public int $priority = 1
- ) {
- if ($priority < 1 || $priority > 10) {
- throw new \InvalidArgumentException('Priority must be between 1 and 10');
- }
- }
-
- /**
- * Create a FieldMappingDTO from legacy array format.
- * @param array{field: string, providers: string[], priority?: int} $data
- */
- public static function fromSerializableArray(array $data): self
- {
- return new self(
- field: $data['field'],
- providers: $data['providers'] ?? [],
- priority: $data['priority'] ?? 1
- );
- }
-
- /**
- * Convert this DTO to the legacy array format for backwards compatibility.
- * @return array{field: string, providers: string[], priority: int}
- */
- public function toSerializableArray(): array
- {
- return [
- 'field' => $this->field,
- 'providers' => $this->providers,
- 'priority' => $this->priority,
- ];
- }
-
- /**
- * Check if this field mapping is for a supplier part number field.
- */
- public function isSupplierPartNumberField(): bool
- {
- return str_ends_with($this->field, '_spn');
- }
-
- /**
- * Get the supplier key from a supplier part number field.
- * Returns null if this is not a supplier part number field.
- */
- public function getSupplierKey(): ?string
- {
- if (!$this->isSupplierPartNumberField()) {
- return null;
- }
-
- return substr($this->field, 0, -4);
- }
-}
diff --git a/src/Services/InfoProviderSystem/DTOs/BulkSearchPartResultDTO.php b/src/Services/InfoProviderSystem/DTOs/BulkSearchPartResultDTO.php
deleted file mode 100644
index d46624d4..00000000
--- a/src/Services/InfoProviderSystem/DTOs/BulkSearchPartResultDTO.php
+++ /dev/null
@@ -1,44 +0,0 @@
-.
- */
-
-declare(strict_types=1);
-
-namespace App\Services\InfoProviderSystem\DTOs;
-
-use App\Entity\Parts\Part;
-
-/**
- * Represents a single search result from bulk search with additional context information, like how the part was found.
- */
-readonly class BulkSearchPartResultDTO
-{
- public function __construct(
- /** The base search result DTO containing provider data */
- public SearchResultDTO $searchResult,
- /** The field that was used to find this result */
- public ?string $sourceField = null,
- /** The actual keyword that was searched for */
- public ?string $sourceKeyword = null,
- /** Local part that matches this search result, if any */
- public ?Part $localPart = null,
- /** Priority for this search result */
- public int $priority = 1
- ) {}
-}
diff --git a/src/Services/InfoProviderSystem/DTOs/BulkSearchPartResultsDTO.php b/src/Services/InfoProviderSystem/DTOs/BulkSearchPartResultsDTO.php
deleted file mode 100644
index 8614f4ec..00000000
--- a/src/Services/InfoProviderSystem/DTOs/BulkSearchPartResultsDTO.php
+++ /dev/null
@@ -1,83 +0,0 @@
-.
- */
-
-declare(strict_types=1);
-
-namespace App\Services\InfoProviderSystem\DTOs;
-
-use App\Entity\Parts\Part;
-
-/**
- * Represents the search results for a single part from bulk info provider search.
- * It contains multiple search results, that match the part.
- */
-readonly class BulkSearchPartResultsDTO
-{
- /**
- * @param Part $part The part that was searched for
- * @param BulkSearchPartResultDTO[] $searchResults Array of search results found for this part
- * @param string[] $errors Array of error messages encountered during search
- */
- public function __construct(
- public Part $part,
- public array $searchResults = [],
- public array $errors = []
- ) {}
-
- /**
- * Check if this part has any search results.
- */
- public function hasResults(): bool
- {
- return !empty($this->searchResults);
- }
-
- /**
- * Check if this part has any errors.
- */
- public function hasErrors(): bool
- {
- return !empty($this->errors);
- }
-
- /**
- * Get the number of search results for this part.
- */
- public function getResultCount(): int
- {
- return count($this->searchResults);
- }
-
- public function getErrorCount(): int
- {
- return count($this->errors);
- }
-
- /**
- * Get search results sorted by priority (ascending).
- * @return BulkSearchPartResultDTO[]
- */
- public function getResultsSortedByPriority(): array
- {
- $results = $this->searchResults;
- usort($results, static fn(BulkSearchPartResultDTO $a, BulkSearchPartResultDTO $b) => $a->priority <=> $b->priority);
- return $results;
- }
-}
diff --git a/src/Services/InfoProviderSystem/DTOs/BulkSearchResponseDTO.php b/src/Services/InfoProviderSystem/DTOs/BulkSearchResponseDTO.php
deleted file mode 100644
index 58e9e240..00000000
--- a/src/Services/InfoProviderSystem/DTOs/BulkSearchResponseDTO.php
+++ /dev/null
@@ -1,231 +0,0 @@
-.
- */
-
-declare(strict_types=1);
-
-namespace App\Services\InfoProviderSystem\DTOs;
-
-use App\Entity\Parts\Part;
-use Doctrine\ORM\EntityManagerInterface;
-use Traversable;
-
-/**
- * Represents the complete response from a bulk info provider search operation.
- * It contains a list of PartSearchResultDTOs, one for each part searched.
- */
-readonly class BulkSearchResponseDTO implements \ArrayAccess, \IteratorAggregate
-{
- /**
- * @param BulkSearchPartResultsDTO[] $partResults Array of search results for each part
- */
- public function __construct(
- public array $partResults
- ) {}
-
- /**
- * Replaces the search results for a specific part, and returns a new instance.
- * The part to replaced, is identified by the part property of the new_results parameter.
- * The original instance remains unchanged.
- * @param BulkSearchPartResultsDTO $new_results
- * @return BulkSearchResponseDTO
- */
- public function replaceResultsForPart(BulkSearchPartResultsDTO $new_results): self
- {
- $array = $this->partResults;
- $replaced = false;
- foreach ($array as $index => $partResult) {
- if ($partResult->part === $new_results->part) {
- $array[$index] = $new_results;
- $replaced = true;
- break;
- }
- }
-
- if (!$replaced) {
- throw new \InvalidArgumentException("Part not found in existing results.");
- }
-
- return new self($array);
- }
-
- /**
- * Check if any parts have search results.
- */
- public function hasAnyResults(): bool
- {
- foreach ($this->partResults as $partResult) {
- if ($partResult->hasResults()) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Get the total number of search results across all parts.
- */
- public function getTotalResultCount(): int
- {
- $count = 0;
- foreach ($this->partResults as $partResult) {
- $count += $partResult->getResultCount();
- }
- return $count;
- }
-
- /**
- * Get all parts that have search results.
- * @return BulkSearchPartResultsDTO[]
- */
- public function getPartsWithResults(): array
- {
- return array_filter($this->partResults, fn($result) => $result->hasResults());
- }
-
- /**
- * Get all parts that have errors.
- * @return BulkSearchPartResultsDTO[]
- */
- public function getPartsWithErrors(): array
- {
- return array_filter($this->partResults, fn($result) => $result->hasErrors());
- }
-
- /**
- * Get the number of parts processed.
- */
- public function getPartCount(): int
- {
- return count($this->partResults);
- }
-
- /**
- * Get the number of parts with successful results.
- */
- public function getSuccessfulPartCount(): int
- {
- return count($this->getPartsWithResults());
- }
-
- /**
- * Merge multiple BulkSearchResponseDTO instances into one.
- * @param BulkSearchResponseDTO ...$responses
- * @return BulkSearchResponseDTO
- */
- public static function merge(BulkSearchResponseDTO ...$responses): BulkSearchResponseDTO
- {
- $mergedResults = [];
- foreach ($responses as $response) {
- foreach ($response->partResults as $partResult) {
- $mergedResults[] = $partResult;
- }
- }
- return new BulkSearchResponseDTO($mergedResults);
- }
-
- /**
- * Convert this DTO to a serializable representation suitable for storage in the database
- * @return array
- */
- public function toSerializableRepresentation(): array
- {
- $serialized = [];
-
- foreach ($this->partResults as $partResult) {
- $partData = [
- 'part_id' => $partResult->part->getId(),
- 'search_results' => [],
- 'errors' => $partResult->errors ?? []
- ];
-
- foreach ($partResult->searchResults as $result) {
- $partData['search_results'][] = [
- 'dto' => $result->searchResult->toNormalizedSearchResultArray(),
- 'source_field' => $result->sourceField ?? null,
- 'source_keyword' => $result->sourceKeyword ?? null,
- 'localPart' => $result->localPart?->getId(),
- 'priority' => $result->priority
- ];
- }
-
- $serialized[] = $partData;
- }
-
- return $serialized;
- }
-
- /**
- * Creates a BulkSearchResponseDTO from a serializable representation.
- * @param array $data
- * @param EntityManagerInterface $entityManager
- * @return BulkSearchResponseDTO
- * @throws \Doctrine\ORM\Exception\ORMException
- */
- public static function fromSerializableRepresentation(array $data, EntityManagerInterface $entityManager): BulkSearchResponseDTO
- {
- $partResults = [];
- foreach ($data as $partData) {
- $partResults[] = new BulkSearchPartResultsDTO(
- part: $entityManager->getReference(Part::class, $partData['part_id']),
- searchResults: array_map(fn($result) => new BulkSearchPartResultDTO(
- searchResult: SearchResultDTO::fromNormalizedSearchResultArray($result['dto']),
- sourceField: $result['source_field'] ?? null,
- sourceKeyword: $result['source_keyword'] ?? null,
- localPart: isset($result['localPart']) ? $entityManager->getReference(Part::class, $result['localPart']) : null,
- priority: $result['priority'] ?? null
- ), $partData['search_results'] ?? []),
- errors: $partData['errors'] ?? []
- );
- }
-
- return new BulkSearchResponseDTO($partResults);
- }
-
- public function offsetExists(mixed $offset): bool
- {
- if (!is_int($offset)) {
- throw new \InvalidArgumentException("Offset must be an integer.");
- }
- return isset($this->partResults[$offset]);
- }
-
- public function offsetGet(mixed $offset): ?BulkSearchPartResultsDTO
- {
- if (!is_int($offset)) {
- throw new \InvalidArgumentException("Offset must be an integer.");
- }
- return $this->partResults[$offset] ?? null;
- }
-
- public function offsetSet(mixed $offset, mixed $value): void
- {
- throw new \LogicException("BulkSearchResponseDTO is immutable.");
- }
-
- public function offsetUnset(mixed $offset): void
- {
- throw new \LogicException('BulkSearchResponseDTO is immutable.');
- }
-
- public function getIterator(): Traversable
- {
- return new \ArrayIterator($this->partResults);
- }
-}
diff --git a/src/Services/InfoProviderSystem/DTOs/FileDTO.php b/src/Services/InfoProviderSystem/DTOs/FileDTO.php
index 84eed0c9..0d1db76a 100644
--- a/src/Services/InfoProviderSystem/DTOs/FileDTO.php
+++ b/src/Services/InfoProviderSystem/DTOs/FileDTO.php
@@ -28,12 +28,12 @@ namespace App\Services\InfoProviderSystem\DTOs;
* This could be a datasheet, a 3D model, a picture or similar.
* @see \App\Tests\Services\InfoProviderSystem\DTOs\FileDTOTest
*/
-readonly class FileDTO
+class FileDTO
{
/**
* @var string The URL where to get this file
*/
- public string $url;
+ public readonly string $url;
/**
* @param string $url The URL where to get this file
@@ -41,7 +41,7 @@ readonly class FileDTO
*/
public function __construct(
string $url,
- public ?string $name = null,
+ public readonly ?string $name = null,
) {
//Find all occurrences of non URL safe characters and replace them with their URL encoded version.
//We only want to replace characters which can not have a valid meaning in a URL (what would break the URL).
@@ -50,4 +50,4 @@ readonly class FileDTO
}
-}
+}
\ No newline at end of file
diff --git a/src/Services/InfoProviderSystem/DTOs/ParameterDTO.php b/src/Services/InfoProviderSystem/DTOs/ParameterDTO.php
index f5868039..0b54d1a9 100644
--- a/src/Services/InfoProviderSystem/DTOs/ParameterDTO.php
+++ b/src/Services/InfoProviderSystem/DTOs/ParameterDTO.php
@@ -28,17 +28,17 @@ namespace App\Services\InfoProviderSystem\DTOs;
* This could be a voltage, a current, a temperature or similar.
* @see \App\Tests\Services\InfoProviderSystem\DTOs\ParameterDTOTest
*/
-readonly class ParameterDTO
+class ParameterDTO
{
public function __construct(
- public string $name,
- public ?string $value_text = null,
- public ?float $value_typ = null,
- public ?float $value_min = null,
- public ?float $value_max = null,
- public ?string $unit = null,
- public ?string $symbol = null,
- public ?string $group = null,
+ public readonly string $name,
+ public readonly ?string $value_text = null,
+ public readonly ?float $value_typ = null,
+ public readonly ?float $value_min = null,
+ public readonly ?float $value_max = null,
+ public readonly ?string $unit = null,
+ public readonly ?string $symbol = null,
+ public readonly ?string $group = null,
) {
}
diff --git a/src/Services/InfoProviderSystem/DTOs/PartDetailDTO.php b/src/Services/InfoProviderSystem/DTOs/PartDetailDTO.php
index 41d50510..9f365f1e 100644
--- a/src/Services/InfoProviderSystem/DTOs/PartDetailDTO.php
+++ b/src/Services/InfoProviderSystem/DTOs/PartDetailDTO.php
@@ -70,4 +70,4 @@ class PartDetailDTO extends SearchResultDTO
footprint: $footprint,
);
}
-}
+}
\ No newline at end of file
diff --git a/src/Services/InfoProviderSystem/DTOs/PriceDTO.php b/src/Services/InfoProviderSystem/DTOs/PriceDTO.php
index 2acf3e57..f1eb28f7 100644
--- a/src/Services/InfoProviderSystem/DTOs/PriceDTO.php
+++ b/src/Services/InfoProviderSystem/DTOs/PriceDTO.php
@@ -28,21 +28,21 @@ use Brick\Math\BigDecimal;
/**
* This DTO represents a price for a single unit in a certain discount range
*/
-readonly class PriceDTO
+class PriceDTO
{
- private BigDecimal $price_as_big_decimal;
+ private readonly BigDecimal $price_as_big_decimal;
public function __construct(
/** @var float The minimum amount that needs to get ordered for this price to be valid */
- public float $minimum_discount_amount,
+ public readonly float $minimum_discount_amount,
/** @var string The price as string (with .) */
- public string $price,
+ public readonly string $price,
/** @var string The currency of the used ISO code of this price detail */
- public ?string $currency_iso_code,
+ public readonly ?string $currency_iso_code,
/** @var bool If the price includes tax */
- public ?bool $includes_tax = true,
+ public readonly ?bool $includes_tax = true,
/** @var float the price related quantity */
- public ?float $price_related_quantity = 1.0,
+ public readonly ?float $price_related_quantity = 1.0,
)
{
$this->price_as_big_decimal = BigDecimal::of($this->price);
diff --git a/src/Services/InfoProviderSystem/DTOs/PurchaseInfoDTO.php b/src/Services/InfoProviderSystem/DTOs/PurchaseInfoDTO.php
index 9ac142ff..bcd8be43 100644
--- a/src/Services/InfoProviderSystem/DTOs/PurchaseInfoDTO.php
+++ b/src/Services/InfoProviderSystem/DTOs/PurchaseInfoDTO.php
@@ -27,15 +27,15 @@ namespace App\Services\InfoProviderSystem\DTOs;
* This DTO represents a purchase information for a part (supplier name, order number and prices).
* @see \App\Tests\Services\InfoProviderSystem\DTOs\PurchaseInfoDTOTest
*/
-readonly class PurchaseInfoDTO
+class PurchaseInfoDTO
{
public function __construct(
- public string $distributor_name,
- public string $order_number,
+ public readonly string $distributor_name,
+ public readonly string $order_number,
/** @var PriceDTO[] */
- public array $prices,
+ public readonly array $prices,
/** @var string|null An url to the product page of the vendor */
- public ?string $product_url = null,
+ public readonly ?string $product_url = null,
)
{
//Ensure that the prices are PriceDTO instances
@@ -45,4 +45,4 @@ readonly class PurchaseInfoDTO
}
}
}
-}
+}
\ No newline at end of file
diff --git a/src/Services/InfoProviderSystem/DTOs/SearchResultDTO.php b/src/Services/InfoProviderSystem/DTOs/SearchResultDTO.php
index a70b2486..28943702 100644
--- a/src/Services/InfoProviderSystem/DTOs/SearchResultDTO.php
+++ b/src/Services/InfoProviderSystem/DTOs/SearchResultDTO.php
@@ -59,8 +59,8 @@ class SearchResultDTO
public readonly ?string $provider_url = null,
/** @var string|null A footprint representation of the providers page */
public readonly ?string $footprint = null,
- )
- {
+ ) {
+
if ($preview_image_url !== null) {
//Utilize the escaping mechanism of FileDTO to ensure that the preview image URL is correctly encoded
//See issue #521: https://github.com/Part-DB/Part-DB-server/issues/521
@@ -71,47 +71,4 @@ class SearchResultDTO
$this->preview_image_url = null;
}
}
-
- /**
- * This method creates a normalized array representation of the DTO.
- * @return array
- */
- public function toNormalizedSearchResultArray(): array
- {
- return [
- 'provider_key' => $this->provider_key,
- 'provider_id' => $this->provider_id,
- 'name' => $this->name,
- 'description' => $this->description,
- 'category' => $this->category,
- 'manufacturer' => $this->manufacturer,
- 'mpn' => $this->mpn,
- 'preview_image_url' => $this->preview_image_url,
- 'manufacturing_status' => $this->manufacturing_status?->value,
- 'provider_url' => $this->provider_url,
- 'footprint' => $this->footprint,
- ];
- }
-
- /**
- * Creates a SearchResultDTO from a normalized array representation.
- * @param array $data
- * @return self
- */
- public static function fromNormalizedSearchResultArray(array $data): self
- {
- return new self(
- provider_key: $data['provider_key'],
- provider_id: $data['provider_id'],
- name: $data['name'],
- description: $data['description'],
- category: $data['category'] ?? null,
- manufacturer: $data['manufacturer'] ?? null,
- mpn: $data['mpn'] ?? null,
- preview_image_url: $data['preview_image_url'] ?? null,
- manufacturing_status: isset($data['manufacturing_status']) ? ManufacturingStatus::tryFrom($data['manufacturing_status']) : null,
- provider_url: $data['provider_url'] ?? null,
- footprint: $data['footprint'] ?? null,
- );
- }
-}
+}
\ No newline at end of file
diff --git a/src/Services/InfoProviderSystem/Providers/BatchInfoProviderInterface.php b/src/Services/InfoProviderSystem/Providers/BatchInfoProviderInterface.php
deleted file mode 100644
index 549f117a..00000000
--- a/src/Services/InfoProviderSystem/Providers/BatchInfoProviderInterface.php
+++ /dev/null
@@ -1,40 +0,0 @@
-.
- */
-
-declare(strict_types=1);
-
-
-namespace App\Services\InfoProviderSystem\Providers;
-
-use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
-
-/**
- * This interface marks a provider as a info provider which can provide information directly in batch operations
- */
-interface BatchInfoProviderInterface extends InfoProviderInterface
-{
- /**
- * Search for multiple keywords in a single batch operation and return the results, ordered by the keywords.
- * This allows for a more efficient search compared to running multiple single searches.
- * @param string[] $keywords
- * @return array An associative array where the key is the keyword and the value is the search results for that keyword
- */
- public function searchByKeywordsBatch(array $keywords): array;
-}
diff --git a/src/Services/InfoProviderSystem/Providers/EmptyProvider.php b/src/Services/InfoProviderSystem/Providers/EmptyProvider.php
deleted file mode 100644
index e0de9772..00000000
--- a/src/Services/InfoProviderSystem/Providers/EmptyProvider.php
+++ /dev/null
@@ -1,76 +0,0 @@
-.
- */
-
-declare(strict_types=1);
-
-
-namespace App\Services\InfoProviderSystem\Providers;
-
-use App\Services\InfoProviderSystem\DTOs\FileDTO;
-use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
-use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
-use Symfony\Component\DependencyInjection\Attribute\When;
-
-/**
- * This is a provider, which is used during tests. It always returns no results.
- */
-#[When(env: 'test')]
-class EmptyProvider implements InfoProviderInterface
-{
- public function getProviderInfo(): array
- {
- return [
- 'name' => 'Empty Provider',
- 'description' => 'This is a test provider',
- //'url' => 'https://example.com',
- 'disabled_help' => 'This provider is disabled for testing purposes'
- ];
- }
-
- public function getProviderKey(): string
- {
- return 'empty';
- }
-
- public function isActive(): bool
- {
- return true;
- }
-
- public function searchByKeyword(string $keyword): array
- {
- return [
-
- ];
- }
-
- public function getCapabilities(): array
- {
- return [
- ProviderCapabilities::BASIC,
- ProviderCapabilities::FOOTPRINT,
- ];
- }
-
- public function getDetails(string $id): PartDetailDTO
- {
- throw new \RuntimeException('No part details available');
- }
-}
diff --git a/src/Services/InfoProviderSystem/Providers/LCSCProvider.php b/src/Services/InfoProviderSystem/Providers/LCSCProvider.php
index ede34eb8..2d83fc7c 100755
--- a/src/Services/InfoProviderSystem/Providers/LCSCProvider.php
+++ b/src/Services/InfoProviderSystem/Providers/LCSCProvider.php
@@ -33,7 +33,7 @@ use App\Settings\InfoProviderSystem\LCSCSettings;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Contracts\HttpClient\HttpClientInterface;
-class LCSCProvider implements BatchInfoProviderInterface
+class LCSCProvider implements InfoProviderInterface
{
private const ENDPOINT_URL = 'https://wmsc.lcsc.com/ftps/wm';
@@ -69,10 +69,9 @@ class LCSCProvider implements BatchInfoProviderInterface
/**
* @param string $id
- * @param bool $lightweight If true, skip expensive operations like datasheet resolution
* @return PartDetailDTO
*/
- private function queryDetail(string $id, bool $lightweight = false): PartDetailDTO
+ private function queryDetail(string $id): PartDetailDTO
{
$response = $this->lcscClient->request('GET', self::ENDPOINT_URL . "/product/detail", [
'headers' => [
@@ -90,7 +89,7 @@ class LCSCProvider implements BatchInfoProviderInterface
throw new \RuntimeException('Could not find product code: ' . $id);
}
- return $this->getPartDetail($product, $lightweight);
+ return $this->getPartDetail($product);
}
/**
@@ -100,42 +99,30 @@ class LCSCProvider implements BatchInfoProviderInterface
private function getRealDatasheetUrl(?string $url): string
{
if ($url !== null && trim($url) !== '' && preg_match("/^https:\/\/(datasheet\.lcsc\.com|www\.lcsc\.com\/datasheet)\/.*(C\d+)\.pdf$/", $url, $matches) > 0) {
- if (preg_match("/^https:\/\/datasheet\.lcsc\.com\/lcsc\/(.*\.pdf)$/", $url, $rewriteMatches) > 0) {
- $url = 'https://www.lcsc.com/datasheet/lcsc_datasheet_' . $rewriteMatches[1];
- }
- $response = $this->lcscClient->request('GET', $url, [
- 'headers' => [
- 'Referer' => 'https://www.lcsc.com/product-detail/_' . $matches[2] . '.html'
- ],
- ]);
- if (preg_match('/(previewPdfUrl): ?("[^"]+wmsc\.lcsc\.com[^"]+\.pdf")/', $response->getContent(), $matches) > 0) {
- //HACKY: The URL string contains escaped characters like \u002F, etc. To decode it, the JSON decoding is reused
- //See https://github.com/Part-DB/Part-DB-server/pull/582#issuecomment-2033125934
- $jsonObj = json_decode('{"' . $matches[1] . '": ' . $matches[2] . '}');
- $url = $jsonObj->previewPdfUrl;
- }
+ if (preg_match("/^https:\/\/datasheet\.lcsc\.com\/lcsc\/(.*\.pdf)$/", $url, $rewriteMatches) > 0) {
+ $url = 'https://www.lcsc.com/datasheet/lcsc_datasheet_' . $rewriteMatches[1];
+ }
+ $response = $this->lcscClient->request('GET', $url, [
+ 'headers' => [
+ 'Referer' => 'https://www.lcsc.com/product-detail/_' . $matches[2] . '.html'
+ ],
+ ]);
+ if (preg_match('/(previewPdfUrl): ?("[^"]+wmsc\.lcsc\.com[^"]+\.pdf")/', $response->getContent(), $matches) > 0) {
+ //HACKY: The URL string contains escaped characters like \u002F, etc. To decode it, the JSON decoding is reused
+ //See https://github.com/Part-DB/Part-DB-server/pull/582#issuecomment-2033125934
+ $jsonObj = json_decode('{"' . $matches[1] . '": ' . $matches[2] . '}');
+ $url = $jsonObj->previewPdfUrl;
+ }
}
return $url;
}
/**
* @param string $term
- * @param bool $lightweight If true, skip expensive operations like datasheet resolution
* @return PartDetailDTO[]
*/
- private function queryByTerm(string $term, bool $lightweight = false): array
+ private function queryByTerm(string $term): array
{
- // Optimize: If term looks like an LCSC part number (starts with C followed by digits),
- // use direct detail query instead of slower search
- if (preg_match('/^C\d+$/i', trim($term))) {
- try {
- return [$this->queryDetail(trim($term), $lightweight)];
- } catch (\Exception $e) {
- // If direct lookup fails, fall back to search
- // This handles cases where the C-code might not exist
- }
- }
-
$response = $this->lcscClient->request('POST', self::ENDPOINT_URL . "/search/v2/global", [
'headers' => [
'Cookie' => new Cookie('currencyCode', $this->settings->currency)
@@ -158,11 +145,11 @@ class LCSCProvider implements BatchInfoProviderInterface
// detailed product listing. It does so utilizing a product tip field.
// If product tip exists and there are no products in the product list try a detail query
if (count($products) === 0 && $tipProductCode !== null) {
- $result[] = $this->queryDetail($tipProductCode, $lightweight);
+ $result[] = $this->queryDetail($tipProductCode);
}
foreach ($products as $product) {
- $result[] = $this->getPartDetail($product, $lightweight);
+ $result[] = $this->getPartDetail($product);
}
return $result;
@@ -191,7 +178,7 @@ class LCSCProvider implements BatchInfoProviderInterface
* @param array $product
* @return PartDetailDTO
*/
- private function getPartDetail(array $product, bool $lightweight = false): PartDetailDTO
+ private function getPartDetail(array $product): PartDetailDTO
{
// Get product images in advance
$product_images = $this->getProductImages($product['productImages'] ?? null);
@@ -227,10 +214,10 @@ class LCSCProvider implements BatchInfoProviderInterface
manufacturing_status: null,
provider_url: $this->getProductShortURL($product['productCode']),
footprint: $this->sanitizeField($footprint),
- datasheets: $lightweight ? [] : $this->getProductDatasheets($product['pdfUrl'] ?? null),
- images: $product_images, // Always include images - users need to see them
- parameters: $lightweight ? [] : $this->attributesToParameters($product['paramVOList'] ?? []),
- vendor_infos: $lightweight ? [] : $this->pricesToVendorInfo($product['productCode'], $this->getProductShortURL($product['productCode']), $product['productPriceList'] ?? []),
+ datasheets: $this->getProductDatasheets($product['pdfUrl'] ?? null),
+ images: $product_images,
+ parameters: $this->attributesToParameters($product['paramVOList'] ?? []),
+ vendor_infos: $this->pricesToVendorInfo($product['productCode'], $this->getProductShortURL($product['productCode']), $product['productPriceList'] ?? []),
mass: $product['weight'] ?? null,
);
}
@@ -299,7 +286,7 @@ class LCSCProvider implements BatchInfoProviderInterface
*/
private function getProductShortURL(string $product_code): string
{
- return 'https://www.lcsc.com/product-detail/' . $product_code . '.html';
+ return 'https://www.lcsc.com/product-detail/' . $product_code .'.html';
}
/**
@@ -340,7 +327,7 @@ class LCSCProvider implements BatchInfoProviderInterface
//Skip this attribute if it's empty
if (in_array(trim((string) $attribute['paramValueEn']), ['', '-'], true)) {
- continue;
+ continue;
}
$result[] = ParameterDTO::parseValueIncludingUnit(name: $attribute['paramNameEn'], value: $attribute['paramValueEn'], group: null);
@@ -351,86 +338,12 @@ class LCSCProvider implements BatchInfoProviderInterface
public function searchByKeyword(string $keyword): array
{
- return $this->queryByTerm($keyword, true); // Use lightweight mode for search
- }
-
- /**
- * Batch search multiple keywords asynchronously (like JavaScript Promise.all)
- * @param array $keywords Array of keywords to search
- * @return array Results indexed by keyword
- */
- public function searchByKeywordsBatch(array $keywords): array
- {
- if (empty($keywords)) {
- return [];
- }
-
- $responses = [];
- $results = [];
-
- // Start all requests immediately (like JavaScript promises without await)
- foreach ($keywords as $keyword) {
- if (preg_match('/^C\d+$/i', trim($keyword))) {
- // Direct detail API call for C-codes
- $responses[$keyword] = $this->lcscClient->request('GET', self::ENDPOINT_URL . "/product/detail", [
- 'headers' => [
- 'Cookie' => new Cookie('currencyCode', $this->settings->currency)
- ],
- 'query' => [
- 'productCode' => trim($keyword),
- ],
- ]);
- } else {
- // Search API call for other terms
- $responses[$keyword] = $this->lcscClient->request('POST', self::ENDPOINT_URL . "/search/v2/global", [
- 'headers' => [
- 'Cookie' => new Cookie('currencyCode', $this->settings->currency)
- ],
- 'json' => [
- 'keyword' => $keyword,
- ],
- ]);
- }
- }
-
- // Now collect all results (like .then() in JavaScript)
- foreach ($responses as $keyword => $response) {
- try {
- $arr = $response->toArray(); // This waits for the response
- $results[$keyword] = $this->processSearchResponse($arr, $keyword);
- } catch (\Exception $e) {
- $results[$keyword] = []; // Empty results on error
- }
- }
-
- return $results;
- }
-
- private function processSearchResponse(array $arr, string $keyword): array
- {
- $result = [];
-
- // Check if this looks like a detail response (direct C-code lookup)
- if (isset($arr['result']['productCode'])) {
- $product = $arr['result'];
- $result[] = $this->getPartDetail($product, true); // lightweight mode
- } else {
- // This is a search response
- $products = $arr['result']['productSearchResultVO']['productList'] ?? [];
- $tipProductCode = $arr['result']['tipProductDetailUrlVO']['productCode'] ?? null;
-
- // If no products but has tip, we'd need another API call - skip for batch mode
- foreach ($products as $product) {
- $result[] = $this->getPartDetail($product, true); // lightweight mode
- }
- }
-
- return $result;
+ return $this->queryByTerm($keyword);
}
public function getDetails(string $id): PartDetailDTO
{
- $tmp = $this->queryByTerm($id, false);
+ $tmp = $this->queryByTerm($id);
if (count($tmp) === 0) {
throw new \RuntimeException('No part found with ID ' . $id);
}
diff --git a/src/Services/InfoProviderSystem/Providers/MouserProvider.php b/src/Services/InfoProviderSystem/Providers/MouserProvider.php
index 3171c994..6639e5c1 100644
--- a/src/Services/InfoProviderSystem/Providers/MouserProvider.php
+++ b/src/Services/InfoProviderSystem/Providers/MouserProvider.php
@@ -132,15 +132,6 @@ class MouserProvider implements InfoProviderInterface
],
]);
- // Check for API errors before processing response
- if ($response->getStatusCode() !== 200) {
- throw new \RuntimeException(sprintf(
- 'Mouser API returned HTTP %d: %s',
- $response->getStatusCode(),
- $response->getContent(false)
- ));
- }
-
return $this->responseToDTOArray($response);
}
@@ -178,16 +169,6 @@ class MouserProvider implements InfoProviderInterface
]
],
]);
-
- // Check for API errors before processing response
- if ($response->getStatusCode() !== 200) {
- throw new \RuntimeException(sprintf(
- 'Mouser API returned HTTP %d: %s',
- $response->getStatusCode(),
- $response->getContent(false)
- ));
- }
-
$tmp = $this->responseToDTOArray($response);
//Ensure that we have exactly one result
@@ -305,17 +286,6 @@ class MouserProvider implements InfoProviderInterface
return (float)$val;
}
- private function mapCurrencyCode(string $currency): string
- {
- //Mouser uses "RMB" for Chinese Yuan, but the correct ISO code is "CNY"
- if ($currency === "RMB") {
- return "CNY";
- }
-
- //For all other currencies, we assume that the ISO code is correct
- return $currency;
- }
-
/**
* Converts the pricing (StandardPricing field) from the Mouser API to an array of PurchaseInfoDTOs
* @param array $price_breaks
@@ -332,7 +302,7 @@ class MouserProvider implements InfoProviderInterface
$prices[] = new PriceDTO(
minimum_discount_amount: $price_break['Quantity'],
price: (string)$number,
- currency_iso_code: $this->mapCurrencyCode($price_break['Currency'])
+ currency_iso_code: $price_break['Currency']
);
}
diff --git a/src/Services/LabelSystem/Barcodes/BarcodeContentGenerator.php b/src/Services/LabelSystem/Barcodes/BarcodeContentGenerator.php
index 3df7d227..7ceb30dd 100644
--- a/src/Services/LabelSystem/Barcodes/BarcodeContentGenerator.php
+++ b/src/Services/LabelSystem/Barcodes/BarcodeContentGenerator.php
@@ -95,11 +95,6 @@ final class BarcodeContentGenerator
return $prefix.$id;
}
- /**
- * @param array $map
- * @param object $target
- * @return string
- */
private function classToString(array $map, object $target): string
{
$class = $target::class;
diff --git a/src/Services/Parts/PartsTableActionHandler.php b/src/Services/Parts/PartsTableActionHandler.php
index 945cff7b..616df229 100644
--- a/src/Services/Parts/PartsTableActionHandler.php
+++ b/src/Services/Parts/PartsTableActionHandler.php
@@ -30,11 +30,13 @@ use App\Entity\Parts\Manufacturer;
use App\Entity\Parts\MeasurementUnit;
use App\Entity\Parts\Part;
use App\Entity\Parts\PartLot;
+use App\Repository\PartRepository;
use Doctrine\ORM\EntityManagerInterface;
use InvalidArgumentException;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
+use Symfony\Contracts\Translation\TranslatableInterface;
use function Symfony\Component\Translation\t;
@@ -98,7 +100,7 @@ implode(',', array_map(static fn (PartLot $lot) => $lot->getID(), $part->getPart
//When action starts with "export_" we have to redirect to the export controller
$matches = [];
- if (preg_match('/^export_(json|yaml|xml|csv|xlsx)$/', $action, $matches)) {
+ if (preg_match('/^export_(json|yaml|xml|csv)$/', $action, $matches)) {
$ids = implode(',', array_map(static fn (Part $part) => $part->getID(), $selected_parts));
$level = match ($target_id) {
2 => 'extended',
@@ -117,16 +119,6 @@ implode(',', array_map(static fn (PartLot $lot) => $lot->getID(), $part->getPart
);
}
- if ($action === 'bulk_info_provider_import') {
- $ids = implode(',', array_map(static fn (Part $part) => $part->getID(), $selected_parts));
- return new RedirectResponse(
- $this->urlGenerator->generate('bulk_info_provider_step1', [
- 'ids' => $ids,
- '_redirect' => $redirect_url
- ])
- );
- }
-
//Iterate over the parts and apply the action to it:
foreach ($selected_parts as $part) {
diff --git a/src/Services/Trees/ToolsTreeBuilder.php b/src/Services/Trees/ToolsTreeBuilder.php
index 036797f6..f7a9d1c4 100644
--- a/src/Services/Trees/ToolsTreeBuilder.php
+++ b/src/Services/Trees/ToolsTreeBuilder.php
@@ -138,11 +138,6 @@ class ToolsTreeBuilder
$this->translator->trans('info_providers.search.title'),
$this->urlGenerator->generate('info_providers_search')
))->setIcon('fa-treeview fa-fw fa-solid fa-cloud-arrow-down');
-
- $nodes[] = (new TreeViewNode(
- $this->translator->trans('info_providers.bulk_import.manage_jobs'),
- $this->urlGenerator->generate('bulk_info_provider_manage')
- ))->setIcon('fa-treeview fa-fw fa-solid fa-tasks');
}
return $nodes;
diff --git a/src/Twig/Sandbox/InheritanceSecurityPolicy.php b/src/Twig/Sandbox/InheritanceSecurityPolicy.php
index 06ab3a1f..93e874e9 100644
--- a/src/Twig/Sandbox/InheritanceSecurityPolicy.php
+++ b/src/Twig/Sandbox/InheritanceSecurityPolicy.php
@@ -34,14 +34,9 @@ use function is_array;
*/
final class InheritanceSecurityPolicy implements SecurityPolicyInterface
{
- /**
- * @var array
- */
private array $allowedMethods;
- public function __construct(private array $allowedTags = [], private array $allowedFilters = [], array $allowedMethods = [],
- /** @var array */
- private array $allowedProperties = [], private array $allowedFunctions = [])
+ public function __construct(private array $allowedTags = [], private array $allowedFilters = [], array $allowedMethods = [], private array $allowedProperties = [], private array $allowedFunctions = [])
{
$this->setAllowedMethods($allowedMethods);
}
diff --git a/templates/components/datatables.macro.html.twig b/templates/components/datatables.macro.html.twig
index d7873498..009f815e 100644
--- a/templates/components/datatables.macro.html.twig
+++ b/templates/components/datatables.macro.html.twig
@@ -30,6 +30,8 @@
+ {#
#}
+
@@ -39,7 +41,7 @@
-
+
{% trans %}part_list.action.action.favorite{% endtrans %}
{% trans %}part_list.action.action.unfavorite{% endtrans %}
@@ -70,10 +72,6 @@
{% trans %}part_list.action.export_csv{% endtrans %}
{% trans %}part_list.action.export_yaml{% endtrans %}
{% trans %}part_list.action.export_xml{% endtrans %}
- {% trans %}part_list.action.export_xlsx{% endtrans %}
-
-
- {% trans %}part_list.action.bulk_info_provider_import{% endtrans %}
diff --git a/templates/info_providers/bulk_import/manage.html.twig b/templates/info_providers/bulk_import/manage.html.twig
deleted file mode 100644
index 9bbed906..00000000
--- a/templates/info_providers/bulk_import/manage.html.twig
+++ /dev/null
@@ -1,124 +0,0 @@
-{% extends "main_card.html.twig" %}
-
-{% block title %}
- {% trans %}info_providers.bulk_import.manage_jobs{% endtrans %}
-{% endblock %}
-
-{% block card_title %}
-
{% trans %}info_providers.bulk_import.manage_jobs{% endtrans %}
-{% endblock %}
-
-{% block card_content %}
-
-
-
-
-
- {% trans %}info_providers.bulk_import.manage_jobs_description{% endtrans %}
-
-
-
- {% if jobs is not empty %}
-
-
-
-
- {% trans %}info_providers.bulk_import.job_name{% endtrans %}
- {% trans %}info_providers.bulk_import.parts_count{% endtrans %}
- {% trans %}info_providers.bulk_import.results_count{% endtrans %}
- {% trans %}info_providers.bulk_import.progress{% endtrans %}
- {% trans %}info_providers.bulk_import.status{% endtrans %}
- {% trans %}info_providers.bulk_import.created_by{% endtrans %}
- {% trans %}info_providers.bulk_import.created_at{% endtrans %}
- {% trans %}info_providers.bulk_import.completed_at{% endtrans %}
- {% trans %}info_providers.bulk_import.action.label{% endtrans %}
-
-
-
- {% for job in jobs %}
-
-
- {{ job.displayNameKey|trans(job.displayNameParams) }} - {{ job.formattedTimestamp }}
- {% if job.isInProgress %}
- Active
- {% endif %}
-
- {{ job.partCount }}
- {{ job.resultCount }}
-
-
-
-
{{ job.progressPercentage }}%
-
-
- {% trans with {'%current%': job.completedPartsCount + job.skippedPartsCount, '%total%': job.partCount} %}info_providers.bulk_import.progress_label{% endtrans %}
-
-
-
- {% if job.isPending %}
- {% trans %}info_providers.bulk_import.status.pending{% endtrans %}
- {% elseif job.isInProgress %}
- {% trans %}info_providers.bulk_import.status.in_progress{% endtrans %}
- {% elseif job.isCompleted %}
- {% trans %}info_providers.bulk_import.status.completed{% endtrans %}
- {% elseif job.isStopped %}
- {% trans %}info_providers.bulk_import.status.stopped{% endtrans %}
- {% elseif job.isFailed %}
- {% trans %}info_providers.bulk_import.status.failed{% endtrans %}
- {% endif %}
-
- {{ job.createdBy.fullName(true) }}
- {{ job.createdAt|format_datetime('short') }}
-
- {% if job.completedAt %}
- {{ job.completedAt|format_datetime('short') }}
- {% else %}
- -
- {% endif %}
-
-
-
- {% if job.isInProgress or job.isCompleted or job.isStopped %}
-
- {% trans %}info_providers.bulk_import.view_results{% endtrans %}
-
- {% endif %}
- {% if job.canBeStopped %}
-
- {% trans %}info_providers.bulk_import.action.stop{% endtrans %}
-
- {% endif %}
- {% if job.isCompleted or job.isFailed or job.isStopped %}
-
- {% trans %}info_providers.bulk_import.action.delete{% endtrans %}
-
- {% endif %}
-
-
-
- {% endfor %}
-
-
-
- {% else %}
-
-
- {% trans %}info_providers.bulk_import.no_jobs_found{% endtrans %}
- {% trans %}info_providers.bulk_import.create_first_job{% endtrans %}
-
- {% endif %}
-
-
-
-{% endblock %}
diff --git a/templates/info_providers/bulk_import/step1.html.twig b/templates/info_providers/bulk_import/step1.html.twig
deleted file mode 100644
index bb9bb351..00000000
--- a/templates/info_providers/bulk_import/step1.html.twig
+++ /dev/null
@@ -1,304 +0,0 @@
-{% extends "main_card.html.twig" %}
-
-{% import "info_providers/providers.macro.html.twig" as providers_macro %}
-{% import "helper.twig" as helper %}
-
-{% block title %}
- {% trans %}info_providers.bulk_import.step1.title{% endtrans %}
-{% endblock %}
-
-{% block card_title %}
-
{% trans %}info_providers.bulk_import.step1.title{% endtrans %}
-
{{ parts|length }} {% trans %}info_providers.bulk_import.parts_selected{% endtrans %}
-{% endblock %}
-
-{% block card_content %}
-
-
-
-
- {% if existing_jobs is not empty %}
-
-
-
-
-
-
-
- {% trans %}info_providers.bulk_import.job_name{% endtrans %}
- {% trans %}info_providers.bulk_import.parts_count{% endtrans %}
- {% trans %}info_providers.bulk_import.results_count{% endtrans %}
- {% trans %}info_providers.bulk_import.progress{% endtrans %}
- {% trans %}info_providers.bulk_import.status{% endtrans %}
- {% trans %}info_providers.bulk_import.created_at{% endtrans %}
- {% trans %}info_providers.bulk_import.action.label{% endtrans %}
-
-
-
- {% for job in existing_jobs %}
-
- {{ job.displayNameKey|trans(job.displayNameParams) }} - {{ job.formattedTimestamp }}
- {{ job.partCount }}
- {{ job.resultCount }}
-
-
-
-
{{ job.progressPercentage }}%
-
- {{ job.completedPartsCount }}/{{ job.partCount }}
-
-
- {% if job.isPending %}
- {% trans %}info_providers.bulk_import.status.pending{% endtrans %}
- {% elseif job.isInProgress %}
- {% trans %}info_providers.bulk_import.status.in_progress{% endtrans %}
- {% elseif job.isCompleted %}
- {% trans %}info_providers.bulk_import.status.completed{% endtrans %}
- {% elseif job.isFailed %}
- {% trans %}info_providers.bulk_import.status.failed{% endtrans %}
- {% endif %}
-
- {{ job.createdAt|date('Y-m-d H:i') }}
-
- {% if job.isInProgress or job.isCompleted %}
-
- {% trans %}info_providers.bulk_import.view_results{% endtrans %}
-
- {% endif %}
-
-
- {% endfor %}
-
-
-
-
-
- {% endif %}
-
-
-
- {% trans %}info_providers.bulk_import.step1.global_mapping_description{% endtrans %}
-
-
-
-
- {% trans %}info_providers.bulk_import.priority_system.title{% endtrans %}: {% trans %}info_providers.bulk_import.priority_system.description{% endtrans %}
-
- {% trans %}info_providers.bulk_import.priority_system.example{% endtrans %}
-
-
-
-
-
- {% trans %}info_providers.bulk_import.step1.spn_recommendation{% endtrans %}
-
-
-
-
-
-
-
- {% for part in parts %}
- {% set hasNoIdentifiers = part.manufacturerProductNumber is empty and part.orderdetails is empty %}
-
- {% endfor %}
-
-
-
-
- {{ form_start(form) }}
-
-
-
-
-
-
-
- {% trans %}info_providers.bulk_search.search_field{% endtrans %}
- {% trans %}info_providers.bulk_search.providers{% endtrans %}
- {% trans %}info_providers.bulk_search.priority{% endtrans %}
- {% trans %}info_providers.bulk_import.actions.label{% endtrans %}
-
-
-
- {% for mapping in form.field_mappings %}
-
- {{ form_widget(mapping.field) }}{{ form_errors(mapping.field) }}
- {{ form_widget(mapping.providers) }}{{ form_errors(mapping.providers) }}
- {{ form_widget(mapping.priority) }}{{ form_errors(mapping.priority) }}
-
-
-
-
-
-
- {% endfor %}
-
-
-
- {% trans %}info_providers.bulk_import.add_mapping{% endtrans %}
-
-
-
-
-
-
-
-
- {{ form_widget(form.prefetch_details, {'attr': {'class': 'form-check-input'}}) }}
- {{ form_label(form.prefetch_details, null, {'label_attr': {'class': 'form-check-label'}}) }}
- {{ form_help(form.prefetch_details) }}
-
-
- {{ form_widget(form.submit, {'attr': {'class': 'btn btn-primary', 'data-field-mapping-target': 'submitButton'}}) }}
-
-
- {{ form_end(form) }}
-
- {% if search_results is not null %}
-
-
{% trans %}info_providers.bulk_import.search_results.title{% endtrans %}
-
- {% for part_result in search_results %}
- {% set part = part_result.part %}
-
-
-
- {% if part_result.errors is not empty %}
- {% for error in part_result.errors %}
-
-
- {{ error }}
-
- {% endfor %}
- {% endif %}
-
- {% if part_result.search_results|length > 0 %}
-
-
-
-
-
- {% trans %}name.label{% endtrans %}
- {% trans %}description.label{% endtrans %}
- {% trans %}manufacturer.label{% endtrans %}
- {% trans %}info_providers.table.provider.label{% endtrans %}
- {% trans %}info_providers.bulk_import.source_field{% endtrans %}
- {% trans %}info_providers.bulk_import.action.label{% endtrans %}
-
-
-
- {% for result in part_result.search_results %}
- {% set dto = result.dto %}
- {% set localPart = result.localPart %}
-
-
-
-
-
- {% if dto.provider_url is not null %}
- {{ dto.name }}
- {% else %}
- {{ dto.name }}
- {% endif %}
- {% if dto.mpn is not null %}
- {{ dto.mpn }}
- {% endif %}
-
- {{ dto.description }}
- {{ dto.manufacturer ?? '' }}
-
- {{ info_provider_label(dto.provider_key)|default(dto.provider_key) }}
- {{ dto.provider_id }}
-
-
- {{ result.source_field ?? 'unknown' }}
- {% if result.source_keyword %}
- {{ result.source_keyword }}
- {% endif %}
-
-
-
-
-
- {% endfor %}
-
-
-
- {% else %}
-
- {% trans %}info_providers.search.no_results{% endtrans %}
-
- {% endif %}
-
-
- {% endfor %}
- {% endif %}
-
-
-
-{% endblock %}
-
diff --git a/templates/info_providers/bulk_import/step2.html.twig b/templates/info_providers/bulk_import/step2.html.twig
deleted file mode 100644
index 559ca20a..00000000
--- a/templates/info_providers/bulk_import/step2.html.twig
+++ /dev/null
@@ -1,240 +0,0 @@
-{% extends "main_card.html.twig" %}
-
-{% import "info_providers/providers.macro.html.twig" as providers_macro %}
-{% import "helper.twig" as helper %}
-
-{% block title %}
- {% trans %}info_providers.bulk_import.step2.title{% endtrans %}
-{% endblock %}
-
-{% block card_title %}
-
{% trans %}info_providers.bulk_import.step2.title{% endtrans %}
-
{{ job.displayNameKey|trans(job.displayNameParams) }} - {{ job.formattedTimestamp }}
-{% endblock %}
-
-{% block card_content %}
-
-
-
-
-
{{ job.displayNameKey|trans(job.displayNameParams) }} - {{ job.formattedTimestamp }}
-
- {{ job.partCount }} {% trans %}info_providers.bulk_import.parts{% endtrans %} β’
- {{ job.resultCount }} {% trans %}info_providers.bulk_import.results{% endtrans %} β’
- {% trans %}info_providers.bulk_import.created_at{% endtrans %}: {{ job.createdAt|date('Y-m-d H:i') }}
-
-
-
- {% if job.isPending %}
- {% trans %}info_providers.bulk_import.status.pending{% endtrans %}
- {% elseif job.isInProgress %}
- {% trans %}info_providers.bulk_import.status.in_progress{% endtrans %}
- {% elseif job.isCompleted %}
- {% trans %}info_providers.bulk_import.status.completed{% endtrans %}
- {% elseif job.isFailed %}
- {% trans %}info_providers.bulk_import.status.failed{% endtrans %}
- {% endif %}
-
-
-
-
-
-
-
-
Progress
- {{ job.completedPartsCount }} / {{ job.partCount }} completed
-
-
-
-
- {{ job.completedPartsCount }} {% trans %}info_providers.bulk_import.completed{% endtrans %} β’
- {{ job.skippedPartsCount }} {% trans %}info_providers.bulk_import.skipped{% endtrans %}
-
- {{ job.progressPercentage }}%
-
-
-
-
-
-
-
- {% trans %}info_providers.bulk_import.step2.instructions.title{% endtrans %}
-
-
{% trans %}info_providers.bulk_import.step2.instructions.description{% endtrans %}
-
- {% trans %}info_providers.bulk_import.step2.instructions.step1{% endtrans %}
- {% trans %}info_providers.bulk_import.step2.instructions.step2{% endtrans %}
- {% trans %}info_providers.bulk_import.step2.instructions.step3{% endtrans %}
-
-
-
-
-
-
-
-
-
{% trans %}info_providers.bulk_import.research.title{% endtrans %}
- {% trans %}info_providers.bulk_import.research.description{% endtrans %}
-
-
-
-
- {% trans %}info_providers.bulk_import.research.all_pending{% endtrans %}
-
-
-
-
-
-
- {% for part_result in search_results %}
- {# @var part_result \App\Services\InfoProviderSystem\DTOs\BulkSearchPartResultsDTO #}
-
- {% set part = part_result.part %}
- {% set isCompleted = job.isPartCompleted(part.id) %}
- {% set isSkipped = job.isPartSkipped(part.id) %}
-
-
-
- {% if part_result.errors is not empty %}
- {% for error in part_result.errors %}
-
-
- {{ error }}
-
- {% endfor %}
- {% endif %}
-
- {% if part_result.searchResults|length > 0 %}
-
-
-
-
-
- {% trans %}name.label{% endtrans %}
- {% trans %}description.label{% endtrans %}
- {% trans %}manufacturer.label{% endtrans %}
- {% trans %}info_providers.table.provider.label{% endtrans %}
- {% trans %}info_providers.bulk_import.source_field{% endtrans %}
- {% trans %}info_providers.bulk_import.action.label{% endtrans %}
-
-
-
- {% for result in part_result.searchResults %}
- {# @var result \App\Services\InfoProviderSystem\DTOs\BulkSearchPartResultDTO #}
- {% set dto = result.searchResult %}
- {% set localPart = result.localPart %}
-
-
-
-
-
- {% if dto.provider_url is not null %}
- {{ dto.name }}
- {% else %}
- {{ dto.name }}
- {% endif %}
- {% if dto.mpn is not null %}
- {{ dto.mpn }}
- {% endif %}
-
- {{ dto.description }}
- {{ dto.manufacturer ?? '' }}
-
- {{ info_provider_label(dto.provider_key)|default(dto.provider_key) }}
- {{ dto.provider_id }}
-
-
- {{ result.sourceField ?? 'unknown' }}
- {% if result.sourceKeyword %}
- {{ result.sourceKeyword }}
- {% endif %}
-
-
-
-
-
- {% endfor %}
-
-
-
- {% else %}
-
- {% trans %}info_providers.search.no_results{% endtrans %}
-
- {% endif %}
-
-
- {% endfor %}
-
-
-{% endblock %}
-
diff --git a/templates/parts/edit/edit_part_info.html.twig b/templates/parts/edit/edit_part_info.html.twig
index 28a88132..20cddbd7 100644
--- a/templates/parts/edit/edit_part_info.html.twig
+++ b/templates/parts/edit/edit_part_info.html.twig
@@ -4,32 +4,6 @@
{% trans with {'%name%': part.name|escape } %}part.edit.title{% endtrans %}
{% endblock %}
-{% block before_card %}
- {% if bulk_job and jobId %}
-
- {% endif %}
-{% endblock %}
-
{% block card_title %}
{% trans with {'%name%': part.name|escape } %}part.edit.card_title{% endtrans %}
diff --git a/templates/parts/edit/update_from_ip.html.twig b/templates/parts/edit/update_from_ip.html.twig
index 1ab2ca59..fb1dfad3 100644
--- a/templates/parts/edit/update_from_ip.html.twig
+++ b/templates/parts/edit/update_from_ip.html.twig
@@ -5,19 +5,6 @@
{% block card_border %}border-info{% endblock %}
{% block card_type %}bg-info text-bg-info{% endblock %}
-{% block before_card %}
- {% if bulk_job and jobId %}
-
-
-
-
- {% trans %}info_providers.bulk_import.editing_part{% endtrans %}
-
-
-
- {% endif %}
-{% endblock %}
-
{% block title %}
{% trans %}info_providers.update_part.title{% endtrans %}: {{ merge_old_name }}
{% endblock %}
diff --git a/templates/parts/lists/_filter.html.twig b/templates/parts/lists/_filter.html.twig
index ba9168d1..c29e8ecd 100644
--- a/templates/parts/lists/_filter.html.twig
+++ b/templates/parts/lists/_filter.html.twig
@@ -31,11 +31,6 @@
{% trans %}project.labelp{% endtrans %}
{% endif %}
- {% if filterForm.inBulkImportJob is defined %}
-
- {% trans %}part.edit.tab.bulk_import{% endtrans %}
-
- {% endif %}
{{ form_start(filterForm, {"attr": {"data-controller": "helpers--form-cleanup", "data-action": "helpers--form-cleanup#submit"}}) }}
@@ -131,13 +126,6 @@
{{ form_row(filterForm.bomComment) }}
{% endif %}
- {% if filterForm.inBulkImportJob is defined %}
-
- {{ form_row(filterForm.inBulkImportJob) }}
- {{ form_row(filterForm.bulkImportJobStatus) }}
- {{ form_row(filterForm.bulkImportPartStatus) }}
-
- {% endif %}
diff --git a/tests/Controller/BulkInfoProviderImportControllerTest.php b/tests/Controller/BulkInfoProviderImportControllerTest.php
deleted file mode 100644
index e71a5fa2..00000000
--- a/tests/Controller/BulkInfoProviderImportControllerTest.php
+++ /dev/null
@@ -1,889 +0,0 @@
-.
- */
-
-declare(strict_types=1);
-
-namespace App\Tests\Controller;
-
-use App\Entity\InfoProviderSystem\BulkImportJobStatus;
-use App\Entity\InfoProviderSystem\BulkInfoProviderImportJob;
-use App\Entity\Parts\Part;
-use App\Entity\UserSystem\User;
-use App\Services\InfoProviderSystem\DTOs\BulkSearchPartResultDTO;
-use App\Services\InfoProviderSystem\DTOs\BulkSearchPartResultsDTO;
-use App\Services\InfoProviderSystem\DTOs\BulkSearchResponseDTO;
-use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
-use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
-use Symfony\Component\HttpFoundation\Response;
-
-/**
- * @group slow
- * @group DB
- */
-class BulkInfoProviderImportControllerTest extends WebTestCase
-{
- public function testStep1WithoutIds(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $client->request('GET', '/tools/bulk_info_provider_import/step1');
-
- self::assertResponseRedirects();
- }
-
- public function testStep1WithInvalidIds(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $client->request('GET', '/tools/bulk_info_provider_import/step1?ids=999999,888888');
-
- self::assertResponseRedirects();
- }
-
- public function testManagePage(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $client->request('GET', '/tools/bulk_info_provider_import/manage');
-
- // Follow any redirects (like locale redirects)
- if ($client->getResponse()->isRedirect()) {
- $client->followRedirect();
- }
-
- self::assertResponseStatusCodeSame(Response::HTTP_OK);
- }
-
- public function testAccessControlForStep1(): void
- {
- $client = static::createClient();
-
- $client->request('GET', '/tools/bulk_info_provider_import/step1?ids=1');
- self::assertResponseRedirects();
-
- $this->loginAsUser($client, 'noread');
- $client->request('GET', '/tools/bulk_info_provider_import/step1?ids=1');
-
- // Follow redirects if any, then check for 403 or final response
- if ($client->getResponse()->isRedirect()) {
- $client->followRedirect();
- }
-
- // The user might get redirected to an error page instead of direct 403
- $this->assertTrue(
- $client->getResponse()->getStatusCode() === Response::HTTP_FORBIDDEN ||
- $client->getResponse()->getStatusCode() === Response::HTTP_OK
- );
- }
-
- public function testAccessControlForManage(): void
- {
- $client = static::createClient();
-
- $client->request('GET', '/tools/bulk_info_provider_import/manage');
- self::assertResponseRedirects();
-
- $this->loginAsUser($client, 'noread');
- $client->request('GET', '/tools/bulk_info_provider_import/manage');
-
- // Follow redirects if any, then check for 403 or final response
- if ($client->getResponse()->isRedirect()) {
- $client->followRedirect();
- }
-
- // The user might get redirected to an error page instead of direct 403
- $this->assertTrue(
- $client->getResponse()->getStatusCode() === Response::HTTP_FORBIDDEN ||
- $client->getResponse()->getStatusCode() === Response::HTTP_OK
- );
- }
-
- public function testStep2TemplateRendering(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = static::getContainer()->get('doctrine')->getManager();
-
- // Use an existing part from test fixtures (ID 1 should exist)
- $partRepository = $entityManager->getRepository(Part::class);
- $part = $partRepository->find(1);
-
- if (!$part) {
- $this->markTestSkipped('Test part with ID 1 not found in fixtures');
- }
-
- // Get the admin user for the createdBy field
- $userRepository = $entityManager->getRepository(User::class);
- $user = $userRepository->findOneBy(['name' => 'admin']);
-
- if (!$user) {
- $this->markTestSkipped('Admin user not found in fixtures');
- }
-
- // Create a test job with search results that include source_field and source_keyword
- $job = new BulkInfoProviderImportJob();
- $job->setCreatedBy($user);
- $job->addPart($part);
- $job->setStatus(BulkImportJobStatus::IN_PROGRESS);
-
- $searchResults = new BulkSearchResponseDTO(partResults: [
- new BulkSearchPartResultsDTO(part: $part,
- searchResults: [new BulkSearchPartResultDTO(
- searchResult: new SearchResultDTO(provider_key: 'test_provider', provider_id: 'TEST123', name: 'Test Component', description: 'Test component description', manufacturer: 'Test Manufacturer', mpn: 'TEST-MPN-123', provider_url: 'https://example.com/test', preview_image_url: null,),
- sourceField: 'test_field',
- sourceKeyword: 'test_keyword',
- localPart: null,
- )]
- )
- ]);
-
- $job->setSearchResults($searchResults);
-
- $entityManager->persist($job);
- $entityManager->flush();
-
- // Test that step2 renders correctly with the search results
- $client->request('GET', '/tools/bulk_info_provider_import/step2/' . $job->getId());
-
- // Follow any redirects (like locale redirects)
- if ($client->getResponse()->isRedirect()) {
- $client->followRedirect();
- }
-
- self::assertResponseStatusCodeSame(Response::HTTP_OK);
-
- // Verify the template rendered the source_field and source_keyword correctly
- $content = $client->getResponse()->getContent();
- $this->assertStringContainsString('test_field', $content);
- $this->assertStringContainsString('test_keyword', $content);
-
- // Clean up - find by ID to avoid detached entity issues
- $jobId = $job->getId();
- $entityManager->clear(); // Clear all entities
- $jobToRemove = $entityManager->find(BulkInfoProviderImportJob::class, $jobId);
- if ($jobToRemove) {
- $entityManager->remove($jobToRemove);
- $entityManager->flush();
- }
- }
-
- public function testStep1WithValidIds(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $partRepository = $entityManager->getRepository(Part::class);
- $part = $partRepository->find(1);
-
- if (!$part) {
- $this->markTestSkipped('Test part with ID 1 not found in fixtures');
- }
-
- $client->request('GET', '/tools/bulk_info_provider_import/step1?ids=' . $part->getId());
-
- if ($client->getResponse()->isRedirect()) {
- $client->followRedirect();
- }
-
- $this->assertResponseStatusCodeSame(Response::HTTP_OK);
- }
-
-
- public function testDeleteJobWithValidJob(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = self::getContainer()->get('doctrine')->getManager();
- $userRepository = $entityManager->getRepository(User::class);
- $user = $userRepository->findOneBy(['name' => 'admin']);
-
- if (!$user) {
- $this->markTestSkipped('Admin user not found in fixtures');
- }
-
- // Get a test part
- $partRepository = $entityManager->getRepository(Part::class);
- $part = $partRepository->find(1);
-
- if (!$part) {
- $this->markTestSkipped('Test part with ID 1 not found in fixtures');
- }
-
- // Create a completed job
- $job = new BulkInfoProviderImportJob();
- $job->setCreatedBy($user);
- $job->addPart($part);
- $job->setStatus(BulkImportJobStatus::COMPLETED);
- $job->setSearchResults(new BulkSearchResponseDTO([]));
-
- $entityManager->persist($job);
- $entityManager->flush();
-
- $client->request('DELETE', '/en/tools/bulk_info_provider_import/job/' . $job->getId() . '/delete');
-
- $this->assertResponseStatusCodeSame(Response::HTTP_OK);
- $response = json_decode($client->getResponse()->getContent(), true);
- $this->assertTrue($response['success']);
- }
-
- public function testDeleteJobWithNonExistentJob(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $client->request('DELETE', '/en/tools/bulk_info_provider_import/job/999999/delete');
-
- $this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
- $response = json_decode($client->getResponse()->getContent(), true);
- $this->assertArrayHasKey('error', $response);
- }
-
- public function testDeleteJobWithActiveJob(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = self::getContainer()->get('doctrine')->getManager();
- $userRepository = $entityManager->getRepository(User::class);
- $user = $userRepository->findOneBy(['name' => 'admin']);
-
- if (!$user) {
- $this->markTestSkipped('Admin user not found in fixtures');
- }
-
- // Get test parts
- $parts = $this->getTestParts($entityManager, [1]);
-
- // Create an active job
- $job = new BulkInfoProviderImportJob();
- $job->setCreatedBy($user);
- foreach ($parts as $part) {
- $job->addPart($part);
- }
- $job->setStatus(BulkImportJobStatus::IN_PROGRESS);
- $job->setSearchResults(new BulkSearchResponseDTO([]));
-
- $entityManager->persist($job);
- $entityManager->flush();
-
- $client->request('DELETE', '/en/tools/bulk_info_provider_import/job/' . $job->getId() . '/delete');
-
- $this->assertResponseStatusCodeSame(Response::HTTP_BAD_REQUEST);
- $response = json_decode($client->getResponse()->getContent(), true);
- $this->assertArrayHasKey('error', $response);
-
- // Clean up
- $entityManager->remove($job);
- $entityManager->flush();
- }
-
- public function testStopJobWithValidJob(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = self::getContainer()->get('doctrine')->getManager();
- $userRepository = $entityManager->getRepository(User::class);
- $user = $userRepository->findOneBy(['name' => 'admin']);
-
- if (!$user) {
- $this->markTestSkipped('Admin user not found in fixtures');
- }
-
- // Get test parts
- $parts = $this->getTestParts($entityManager, [1]);
-
- // Create an active job
- $job = new BulkInfoProviderImportJob();
- $job->setCreatedBy($user);
- foreach ($parts as $part) {
- $job->addPart($part);
- }
- $job->setStatus(BulkImportJobStatus::IN_PROGRESS);
- $job->setSearchResults(new BulkSearchResponseDTO([]));
-
- $entityManager->persist($job);
- $entityManager->flush();
-
- $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $job->getId() . '/stop');
-
- $this->assertResponseStatusCodeSame(Response::HTTP_OK);
- $response = json_decode($client->getResponse()->getContent(), true);
- $this->assertTrue($response['success']);
-
- // Clean up
- $entityManager->remove($job);
- $entityManager->flush();
- }
-
- public function testStopJobWithNonExistentJob(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $client->request('POST', '/en/tools/bulk_info_provider_import/job/999999/stop');
-
- $this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
- $response = json_decode($client->getResponse()->getContent(), true);
- $this->assertArrayHasKey('error', $response);
- }
-
- public function testMarkPartCompleted(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $userRepository = $entityManager->getRepository(User::class);
- $user = $userRepository->findOneBy(['name' => 'admin']);
-
- if (!$user) {
- $this->markTestSkipped('Admin user not found in fixtures');
- }
-
- // Get test parts
- $parts = $this->getTestParts($entityManager, [1, 2]);
-
- $job = new BulkInfoProviderImportJob();
- $job->setCreatedBy($user);
- foreach ($parts as $part) {
- $job->addPart($part);
- }
- $job->setStatus(BulkImportJobStatus::IN_PROGRESS);
- $job->setSearchResults(new BulkSearchResponseDTO([]));
-
- $entityManager->persist($job);
- $entityManager->flush();
-
- $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $job->getId() . '/part/1/mark-completed');
-
- $this->assertResponseStatusCodeSame(Response::HTTP_OK);
- $response = json_decode($client->getResponse()->getContent(), true);
- $this->assertTrue($response['success']);
- $this->assertArrayHasKey('progress', $response);
- $this->assertArrayHasKey('completed_count', $response);
-
- // Clean up
- $entityManager->remove($job);
- $entityManager->flush();
- }
-
- public function testMarkPartSkipped(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $userRepository = $entityManager->getRepository(User::class);
- $user = $userRepository->findOneBy(['name' => 'admin']);
-
- if (!$user) {
- $this->markTestSkipped('Admin user not found in fixtures');
- }
-
- // Get test parts
- $parts = $this->getTestParts($entityManager, [1, 2]);
-
- $job = new BulkInfoProviderImportJob();
- $job->setCreatedBy($user);
- foreach ($parts as $part) {
- $job->addPart($part);
- }
- $job->setStatus(BulkImportJobStatus::IN_PROGRESS);
- $job->setSearchResults(new BulkSearchResponseDTO([]));
-
- $entityManager->persist($job);
- $entityManager->flush();
-
- $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $job->getId() . '/part/1/mark-skipped', [
- 'reason' => 'Test skip reason'
- ]);
-
- $this->assertResponseStatusCodeSame(Response::HTTP_OK);
- $response = json_decode($client->getResponse()->getContent(), true);
- $this->assertTrue($response['success']);
- $this->assertArrayHasKey('skipped_count', $response);
-
- // Clean up
- $entityManager->remove($job);
- $entityManager->flush();
- }
-
- public function testMarkPartPending(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $userRepository = $entityManager->getRepository(User::class);
- $user = $userRepository->findOneBy(['name' => 'admin']);
-
- if (!$user) {
- $this->markTestSkipped('Admin user not found in fixtures');
- }
-
- // Get test parts
- $parts = $this->getTestParts($entityManager, [1]);
-
- $job = new BulkInfoProviderImportJob();
- $job->setCreatedBy($user);
- foreach ($parts as $part) {
- $job->addPart($part);
- }
- $job->setStatus(BulkImportJobStatus::IN_PROGRESS);
- $job->setSearchResults(new BulkSearchResponseDTO([]));
-
- $entityManager->persist($job);
- $entityManager->flush();
-
- $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $job->getId() . '/part/1/mark-pending');
-
- $this->assertResponseStatusCodeSame(Response::HTTP_OK);
- $response = json_decode($client->getResponse()->getContent(), true);
- $this->assertTrue($response['success']);
-
- // Clean up
- $entityManager->remove($job);
- $entityManager->flush();
- }
-
- public function testStep2WithNonExistentJob(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $client->request('GET', '/tools/bulk_info_provider_import/step2/999999');
-
- $this->assertResponseRedirects();
- }
-
- public function testStep2WithUnauthorizedAccess(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $userRepository = $entityManager->getRepository(User::class);
- $admin = $userRepository->findOneBy(['name' => 'admin']);
- $readonly = $userRepository->findOneBy(['name' => 'noread']);
-
- if (!$admin || !$readonly) {
- $this->markTestSkipped('Required test users not found in fixtures');
- }
-
- // Get test parts
- $parts = $this->getTestParts($entityManager, [1]);
-
- // Create job as admin
- $job = new BulkInfoProviderImportJob();
- $job->setCreatedBy($admin);
- foreach ($parts as $part) {
- $job->addPart($part);
- }
- $job->setStatus(BulkImportJobStatus::IN_PROGRESS);
- $job->setSearchResults(new BulkSearchResponseDTO([]));
-
- $entityManager->persist($job);
- $entityManager->flush();
-
- // Try to access as readonly user
- $this->loginAsUser($client, 'noread');
- $client->request('GET', '/tools/bulk_info_provider_import/step2/' . $job->getId());
-
- $this->assertResponseRedirects();
-
- // Clean up
- $entityManager->remove($job);
- $entityManager->flush();
- }
-
- public function testJobAccessControlForDelete(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $userRepository = $entityManager->getRepository(User::class);
- $admin = $userRepository->findOneBy(['name' => 'admin']);
- $readonly = $userRepository->findOneBy(['name' => 'noread']);
-
- if (!$admin || !$readonly) {
- $this->markTestSkipped('Required test users not found in fixtures');
- }
-
- // Get test parts
- $parts = $this->getTestParts($entityManager, [1]);
-
- // Create job as readonly user
- $job = new BulkInfoProviderImportJob();
- $job->setCreatedBy($readonly);
- foreach ($parts as $part) {
- $job->addPart($part);
- }
- $job->setStatus(BulkImportJobStatus::COMPLETED);
- $job->setSearchResults(new BulkSearchResponseDTO([]));
-
- $entityManager->persist($job);
- $entityManager->flush();
-
- // Try to delete as admin (should fail due to ownership)
- $client->request('DELETE', '/en/tools/bulk_info_provider_import/job/' . $job->getId() . '/delete');
-
- $this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
-
- // Clean up
- $entityManager->remove($job);
- $entityManager->flush();
- }
-
- private function loginAsUser($client, string $username): void
- {
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $userRepository = $entityManager->getRepository(User::class);
- $user = $userRepository->findOneBy(['name' => $username]);
-
- if (!$user) {
- $this->markTestSkipped("User {$username} not found");
- }
-
- $client->loginUser($user);
- }
-
- private function getTestParts($entityManager, array $ids): array
- {
- $partRepository = $entityManager->getRepository(Part::class);
- $parts = [];
-
- foreach ($ids as $id) {
- $part = $partRepository->find($id);
- if (!$part) {
- $this->markTestSkipped("Test part with ID {$id} not found in fixtures");
- }
- $parts[] = $part;
- }
-
- return $parts;
- }
-
- public function testStep1Form(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $partRepository = $entityManager->getRepository(Part::class);
- $part = $partRepository->find(1);
-
- if (!$part) {
- $this->markTestSkipped('Test part with ID 1 not found in fixtures');
- }
-
- $client->request('GET', '/tools/bulk_info_provider_import/step1?ids=' . $part->getId());
-
- if ($client->getResponse()->isRedirect()) {
- $client->followRedirect();
- }
-
- $this->assertResponseStatusCodeSame(Response::HTTP_OK);
- $this->assertStringContainsString('Bulk Info Provider Import', $client->getResponse()->getContent());
- }
-
- public function testStep1FormSubmissionWithErrors(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $partRepository = $entityManager->getRepository(Part::class);
- $part = $partRepository->find(1);
-
- if (!$part) {
- $this->markTestSkipped('Test part with ID 1 not found in fixtures');
- }
-
- $client->request('GET', '/tools/bulk_info_provider_import/step1?ids=' . $part->getId());
-
- if ($client->getResponse()->isRedirect()) {
- $client->followRedirect();
- }
-
- $this->assertResponseStatusCodeSame(Response::HTTP_OK);
- $this->assertStringContainsString('Bulk Info Provider Import', $client->getResponse()->getContent());
- }
-
- public function testBulkInfoProviderServiceKeywordExtraction(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $partRepository = $entityManager->getRepository(Part::class);
- $part = $partRepository->find(1);
-
- if (!$part) {
- $this->markTestSkipped('Test part with ID 1 not found in fixtures');
- }
-
- // Test that the service can extract keywords from parts
- $bulkService = $client->getContainer()->get(\App\Services\InfoProviderSystem\BulkInfoProviderService::class);
-
- // Create field mappings to verify the service works
- $fieldMappings = [
- new \App\Services\InfoProviderSystem\DTOs\BulkSearchFieldMappingDTO('name', ['test'], 1),
- new \App\Services\InfoProviderSystem\DTOs\BulkSearchFieldMappingDTO('mpn', ['test'], 2)
- ];
-
- // The service may return an empty result or throw when no results are found
- try {
- $result = $bulkService->performBulkSearch([$part], $fieldMappings, false);
- $this->assertInstanceOf(\App\Services\InfoProviderSystem\DTOs\BulkSearchResponseDTO::class, $result);
- } catch (\RuntimeException $e) {
- $this->assertStringContainsString('No search results found', $e->getMessage());
- }
- }
-
- public function testManagePageWithJobCleanup(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $userRepository = $entityManager->getRepository(User::class);
- $user = $userRepository->findOneBy(['name' => 'admin']);
-
- if (!$user) {
- $this->markTestSkipped('Admin user not found in fixtures');
- }
-
- $partRepository = $entityManager->getRepository(Part::class);
- $part = $partRepository->find(1);
-
- if (!$part) {
- $this->markTestSkipped('Test part with ID 1 not found in fixtures');
- }
-
- $job = new BulkInfoProviderImportJob();
- $job->setCreatedBy($user);
- $job->addPart($part);
- $job->setStatus(BulkImportJobStatus::IN_PROGRESS);
- $job->setSearchResults(new BulkSearchResponseDTO([]));
-
- $entityManager->persist($job);
- $entityManager->flush();
-
- $client->request('GET', '/tools/bulk_info_provider_import/manage');
-
- if ($client->getResponse()->isRedirect()) {
- $client->followRedirect();
- }
-
- self::assertResponseStatusCodeSame(Response::HTTP_OK);
-
- // Find job from database to avoid detached entity errors
- $jobId = $job->getId();
- $entityManager->clear();
- $persistedJob = $entityManager->find(BulkInfoProviderImportJob::class, $jobId);
- if ($persistedJob) {
- $entityManager->remove($persistedJob);
- $entityManager->flush();
- }
- }
-
- public function testBulkInfoProviderServiceSupplierPartNumberExtraction(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $partRepository = $entityManager->getRepository(Part::class);
- $part = $partRepository->find(1);
-
- if (!$part) {
- $this->markTestSkipped('Test part with ID 1 not found in fixtures');
- }
-
- // Test that the service can handle supplier part number fields
- $bulkService = $client->getContainer()->get(\App\Services\InfoProviderSystem\BulkInfoProviderService::class);
-
- // Create field mappings with supplier SPN field mapping
- $fieldMappings = [
- new \App\Services\InfoProviderSystem\DTOs\BulkSearchFieldMappingDTO('invalid_field', ['test'], 1),
- new \App\Services\InfoProviderSystem\DTOs\BulkSearchFieldMappingDTO('test_supplier_spn', ['test'], 2)
- ];
-
- // The service should be able to process the request and throw an exception when no results are found
- try {
- $bulkService->performBulkSearch([$part], $fieldMappings, false);
- $this->fail('Expected RuntimeException to be thrown when no search results are found');
- } catch (\RuntimeException $e) {
- $this->assertStringContainsString('No search results found', $e->getMessage());
- }
- }
-
- public function testBulkInfoProviderServiceBatchProcessing(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $partRepository = $entityManager->getRepository(Part::class);
- $part = $partRepository->find(1);
-
- if (!$part) {
- $this->markTestSkipped('Test part with ID 1 not found in fixtures');
- }
-
- // Test that the service can handle batch processing
- $bulkService = $client->getContainer()->get(\App\Services\InfoProviderSystem\BulkInfoProviderService::class);
-
- // Create field mappings with multiple keywords
- $fieldMappings = [
- new \App\Services\InfoProviderSystem\DTOs\BulkSearchFieldMappingDTO('empty', ['test'], 1)
- ];
-
- // The service should be able to process the request and throw an exception when no results are found
- try {
- $response = $bulkService->performBulkSearch([$part], $fieldMappings, false);
- $this->fail('Expected RuntimeException to be thrown when no search results are found');
- } catch (\RuntimeException $e) {
- $this->assertStringContainsString('No search results found', $e->getMessage());
- }
- }
-
- public function testBulkInfoProviderServicePrefetchDetails(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $partRepository = $entityManager->getRepository(Part::class);
- $part = $partRepository->find(1);
-
- if (!$part) {
- $this->markTestSkipped('Test part with ID 1 not found in fixtures');
- }
-
- // Test that the service can handle prefetch details
- $bulkService = $client->getContainer()->get(\App\Services\InfoProviderSystem\BulkInfoProviderService::class);
-
- // Create empty search results to test prefetch method
- $searchResults = new BulkSearchResponseDTO([
- new BulkSearchPartResultsDTO(part: $part, searchResults: [], errors: [])
- ]);
-
- // The prefetch method should not throw any errors
- $bulkService->prefetchDetailsForResults($searchResults);
-
- // If we get here, the method executed successfully
- $this->assertTrue(true);
- }
-
- public function testJobAccessControlForStopAndMarkOperations(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $userRepository = $entityManager->getRepository(User::class);
- $admin = $userRepository->findOneBy(['name' => 'admin']);
- $readonly = $userRepository->findOneBy(['name' => 'noread']);
-
- if (!$admin || !$readonly) {
- $this->markTestSkipped('Required test users not found in fixtures');
- }
-
- $parts = $this->getTestParts($entityManager, [1]);
-
- $job = new BulkInfoProviderImportJob();
- $job->setCreatedBy($readonly);
- foreach ($parts as $part) {
- $job->addPart($part);
- }
- $job->setStatus(BulkImportJobStatus::IN_PROGRESS);
- $job->setSearchResults(new BulkSearchResponseDTO([]));
-
- $entityManager->persist($job);
- $entityManager->flush();
-
- $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $job->getId() . '/stop');
- $this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
-
- $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $job->getId() . '/part/1/mark-completed');
- $this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
-
- $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $job->getId() . '/part/1/mark-skipped', [
- 'reason' => 'Test reason'
- ]);
- $this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
-
- $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $job->getId() . '/part/1/mark-pending');
- $this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
-
- // Find job from database to avoid detached entity errors
- $jobId = $job->getId();
- $entityManager->clear();
- $persistedJob = $entityManager->find(BulkInfoProviderImportJob::class, $jobId);
- if ($persistedJob) {
- $entityManager->remove($persistedJob);
- $entityManager->flush();
- }
- }
-
- public function testOperationsOnCompletedJob(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $userRepository = $entityManager->getRepository(User::class);
- $user = $userRepository->findOneBy(['name' => 'admin']);
-
- if (!$user) {
- $this->markTestSkipped('Admin user not found in fixtures');
- }
-
- $parts = $this->getTestParts($entityManager, [1]);
-
- $job = new BulkInfoProviderImportJob();
- $job->setCreatedBy($user);
- foreach ($parts as $part) {
- $job->addPart($part);
- }
- $job->setStatus(BulkImportJobStatus::COMPLETED);
- $job->setSearchResults(new BulkSearchResponseDTO([]));
-
- $entityManager->persist($job);
- $entityManager->flush();
-
- $client->request('POST', '/en/tools/bulk_info_provider_import/job/' . $job->getId() . '/stop');
- $this->assertResponseStatusCodeSame(Response::HTTP_BAD_REQUEST);
- $response = json_decode($client->getResponse()->getContent(), true);
- $this->assertArrayHasKey('error', $response);
-
- $entityManager->remove($job);
- $entityManager->flush();
- }
-}
diff --git a/tests/Controller/PartControllerTest.php b/tests/Controller/PartControllerTest.php
deleted file mode 100644
index c47c62f8..00000000
--- a/tests/Controller/PartControllerTest.php
+++ /dev/null
@@ -1,334 +0,0 @@
-.
- */
-
-declare(strict_types=1);
-
-namespace App\Tests\Controller;
-
-use App\Entity\InfoProviderSystem\BulkImportJobStatus;
-use App\Entity\InfoProviderSystem\BulkInfoProviderImportJob;
-use App\Entity\Parts\Category;
-use App\Entity\Parts\Footprint;
-use App\Entity\Parts\Manufacturer;
-use App\Entity\Parts\Part;
-use App\Entity\Parts\StorageLocation;
-use App\Entity\Parts\Supplier;
-use App\Entity\UserSystem\User;
-use App\Services\InfoProviderSystem\DTOs\BulkSearchResponseDTO;
-use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
-use Symfony\Component\HttpFoundation\Response;
-
-/**
- * @group slow
- * @group DB
- */
-class PartControllerTest extends WebTestCase
-{
- public function testShowPart(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $partRepository = $entityManager->getRepository(Part::class);
- $part = $partRepository->find(1);
-
- if (!$part) {
- $this->markTestSkipped('Test part with ID 1 not found in fixtures');
- }
-
- $client->request('GET', '/en/part/' . $part->getId());
-
- $this->assertResponseStatusCodeSame(Response::HTTP_OK);
- $this->assertResponseStatusCodeSame(Response::HTTP_OK);
- }
-
- public function testShowPartWithTimestamp(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $partRepository = $entityManager->getRepository(Part::class);
- $part = $partRepository->find(1);
-
- if (!$part) {
- $this->markTestSkipped('Test part with ID 1 not found in fixtures');
- }
-
- $timestamp = time();
- $client->request('GET', "/en/part/{$part->getId()}/info/{$timestamp}");
-
- $this->assertResponseStatusCodeSame(Response::HTTP_OK);
- }
-
- public function testEditPart(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $partRepository = $entityManager->getRepository(Part::class);
- $part = $partRepository->find(1);
-
- if (!$part) {
- $this->markTestSkipped('Test part with ID 1 not found in fixtures');
- }
-
- $client->request('GET', '/en/part/' . $part->getId() . '/edit');
-
- $this->assertResponseStatusCodeSame(Response::HTTP_OK);
- $this->assertSelectorExists('form[name="part_base"]');
- }
-
- public function testEditPartWithBulkJob(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $partRepository = $entityManager->getRepository(Part::class);
- $part = $partRepository->find(1);
-
- $userRepository = $entityManager->getRepository(User::class);
- $user = $userRepository->findOneBy(['name' => 'admin']);
-
- if (!$part || !$user) {
- $this->markTestSkipped('Required test data not found in fixtures');
- }
-
- // Create a bulk job
- $job = new BulkInfoProviderImportJob();
- $job->setCreatedBy($user);
- $job->setPartIds([$part->getId()]);
- $job->setStatus(BulkImportJobStatus::IN_PROGRESS);
- $job->setSearchResults(new BulkSearchResponseDTO([]));
-
- $entityManager->persist($job);
- $entityManager->flush();
-
- $client->request('GET', '/en/part/' . $part->getId() . '/edit?jobId=' . $job->getId());
-
- $this->assertResponseStatusCodeSame(Response::HTTP_OK);
-
- // Clean up
- $entityManager->remove($job);
- $entityManager->flush();
- }
-
-
-
- public function testNewPart(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $client->request('GET', '/en/part/new');
-
- $this->assertResponseStatusCodeSame(Response::HTTP_OK);
- $this->assertSelectorExists('form[name="part_base"]');
- }
-
- public function testNewPartWithCategory(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $categoryRepository = $entityManager->getRepository(Category::class);
- $category = $categoryRepository->find(1);
-
- if (!$category) {
- $this->markTestSkipped('Test category with ID 1 not found in fixtures');
- }
-
- $client->request('GET', '/en/part/new?category=' . $category->getId());
-
- $this->assertResponseStatusCodeSame(Response::HTTP_OK);
- }
-
- public function testNewPartWithFootprint(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $footprintRepository = $entityManager->getRepository(Footprint::class);
- $footprint = $footprintRepository->find(1);
-
- if (!$footprint) {
- $this->markTestSkipped('Test footprint with ID 1 not found in fixtures');
- }
-
- $client->request('GET', '/en/part/new?footprint=' . $footprint->getId());
-
- $this->assertResponseStatusCodeSame(Response::HTTP_OK);
- }
-
- public function testNewPartWithManufacturer(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $manufacturerRepository = $entityManager->getRepository(Manufacturer::class);
- $manufacturer = $manufacturerRepository->find(1);
-
- if (!$manufacturer) {
- $this->markTestSkipped('Test manufacturer with ID 1 not found in fixtures');
- }
-
- $client->request('GET', '/en/part/new?manufacturer=' . $manufacturer->getId());
-
- $this->assertResponseStatusCodeSame(Response::HTTP_OK);
- }
-
- public function testNewPartWithStorageLocation(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $storageLocationRepository = $entityManager->getRepository(StorageLocation::class);
- $storageLocation = $storageLocationRepository->find(1);
-
- if (!$storageLocation) {
- $this->markTestSkipped('Test storage location with ID 1 not found in fixtures');
- }
-
- $client->request('GET', '/en/part/new?storelocation=' . $storageLocation->getId());
-
- $this->assertResponseStatusCodeSame(Response::HTTP_OK);
- }
-
- public function testNewPartWithSupplier(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $supplierRepository = $entityManager->getRepository(Supplier::class);
- $supplier = $supplierRepository->find(1);
-
- if (!$supplier) {
- $this->markTestSkipped('Test supplier with ID 1 not found in fixtures');
- }
-
- $client->request('GET', '/en/part/new?supplier=' . $supplier->getId());
-
- $this->assertResponseStatusCodeSame(Response::HTTP_OK);
- }
-
- public function testClonePart(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $partRepository = $entityManager->getRepository(Part::class);
- $part = $partRepository->find(1);
-
- if (!$part) {
- $this->markTestSkipped('Test part with ID 1 not found in fixtures');
- }
-
- $client->request('GET', '/en/part/' . $part->getId() . '/clone');
-
- $this->assertResponseStatusCodeSame(Response::HTTP_OK);
- $this->assertSelectorExists('form[name="part_base"]');
- }
-
- public function testMergeParts(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'admin');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $categoryRepository = $entityManager->getRepository(Category::class);
- $category = $categoryRepository->find(1);
-
- if (!$category) {
- $this->markTestSkipped('Test category with ID 1 not found in fixtures');
- }
-
- // Create two test parts
- $targetPart = new Part();
- $targetPart->setName('Target Part');
- $targetPart->setCategory($category);
-
- $otherPart = new Part();
- $otherPart->setName('Other Part');
- $otherPart->setCategory($category);
-
- $entityManager->persist($targetPart);
- $entityManager->persist($otherPart);
- $entityManager->flush();
-
- $client->request('GET', "/en/part/{$targetPart->getId()}/merge/{$otherPart->getId()}");
-
- $this->assertResponseStatusCodeSame(Response::HTTP_OK);
- $this->assertSelectorExists('form[name="part_base"]');
-
- // Clean up
- $entityManager->remove($targetPart);
- $entityManager->remove($otherPart);
- $entityManager->flush();
- }
-
-
-
-
-
- public function testAccessControlForUnauthorizedUser(): void
- {
- $client = static::createClient();
- $this->loginAsUser($client, 'noread');
-
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $partRepository = $entityManager->getRepository(Part::class);
- $part = $partRepository->find(1);
-
- if (!$part) {
- $this->markTestSkipped('Test part with ID 1 not found in fixtures');
- }
-
- $client->request('GET', '/en/part/' . $part->getId());
-
- // Should either be forbidden or redirected to error page
- $this->assertTrue(
- $client->getResponse()->getStatusCode() === Response::HTTP_FORBIDDEN ||
- $client->getResponse()->isRedirect()
- );
- }
-
- private function loginAsUser($client, string $username): void
- {
- $entityManager = $client->getContainer()->get('doctrine')->getManager();
- $userRepository = $entityManager->getRepository(User::class);
- $user = $userRepository->findOneBy(['name' => $username]);
-
- if (!$user) {
- $this->markTestSkipped("User {$username} not found");
- }
-
- $client->loginUser($user);
- }
-
-}
diff --git a/tests/DataTables/Filters/Constraints/Part/BulkImportJobStatusConstraintTest.php b/tests/DataTables/Filters/Constraints/Part/BulkImportJobStatusConstraintTest.php
deleted file mode 100644
index 816a8035..00000000
--- a/tests/DataTables/Filters/Constraints/Part/BulkImportJobStatusConstraintTest.php
+++ /dev/null
@@ -1,250 +0,0 @@
-.
- */
-
-declare(strict_types=1);
-
-namespace App\Tests\DataTables\Filters\Constraints\Part;
-
-use App\DataTables\Filters\Constraints\Part\BulkImportJobStatusConstraint;
-use App\Entity\InfoProviderSystem\BulkInfoProviderImportJobPart;
-use Doctrine\ORM\EntityManagerInterface;
-use Doctrine\ORM\QueryBuilder;
-use PHPUnit\Framework\TestCase;
-
-class BulkImportJobStatusConstraintTest extends TestCase
-{
- private BulkImportJobStatusConstraint $constraint;
- private QueryBuilder $queryBuilder;
- private EntityManagerInterface $entityManager;
-
- protected function setUp(): void
- {
- $this->constraint = new BulkImportJobStatusConstraint();
- $this->entityManager = $this->createMock(EntityManagerInterface::class);
- $this->queryBuilder = $this->createMock(QueryBuilder::class);
-
- $this->queryBuilder->method('getEntityManager')
- ->willReturn($this->entityManager);
- }
-
- public function testConstructor(): void
- {
- $this->assertEquals([], $this->constraint->getValue());
- $this->assertEmpty($this->constraint->getOperator());
- $this->assertFalse($this->constraint->isEnabled());
- }
-
- public function testGetAndSetValues(): void
- {
- $values = ['pending', 'in_progress'];
- $this->constraint->setValue($values);
-
- $this->assertEquals($values, $this->constraint->getValue());
- }
-
- public function testGetAndSetOperator(): void
- {
- $operator = 'ANY';
- $this->constraint->setOperator($operator);
-
- $this->assertEquals($operator, $this->constraint->getOperator());
- }
-
- public function testIsEnabledWithEmptyValues(): void
- {
- $this->constraint->setOperator('ANY');
-
- $this->assertFalse($this->constraint->isEnabled());
- }
-
- public function testIsEnabledWithNullOperator(): void
- {
- $this->constraint->setValue(['pending']);
-
- $this->assertFalse($this->constraint->isEnabled());
- }
-
- public function testIsEnabledWithValuesAndOperator(): void
- {
- $this->constraint->setValue(['pending']);
- $this->constraint->setOperator('ANY');
-
- $this->assertTrue($this->constraint->isEnabled());
- }
-
- public function testApplyWithEmptyValues(): void
- {
- $this->constraint->setOperator('ANY');
-
- $this->queryBuilder->expects($this->never())
- ->method('andWhere');
-
- $this->constraint->apply($this->queryBuilder);
- }
-
- public function testApplyWithNullOperator(): void
- {
- $this->constraint->setValue(['pending']);
-
- $this->queryBuilder->expects($this->never())
- ->method('andWhere');
-
- $this->constraint->apply($this->queryBuilder);
- }
-
- public function testApplyWithAnyOperator(): void
- {
- $this->constraint->setValue(['pending', 'in_progress']);
- $this->constraint->setOperator('ANY');
-
- $subQueryBuilder = $this->createMock(QueryBuilder::class);
- $subQueryBuilder->method('select')->willReturnSelf();
- $subQueryBuilder->method('from')->willReturnSelf();
- $subQueryBuilder->method('join')->willReturnSelf();
- $subQueryBuilder->method('where')->willReturnSelf();
- $subQueryBuilder->method('andWhere')->willReturnSelf();
- $subQueryBuilder->method('getDQL')->willReturn('EXISTS_SUBQUERY_DQL');
-
- $this->entityManager->method('createQueryBuilder')
- ->willReturn($subQueryBuilder);
-
- $this->queryBuilder->expects($this->once())
- ->method('andWhere')
- ->with('EXISTS (EXISTS_SUBQUERY_DQL)');
-
- $this->queryBuilder->expects($this->once())
- ->method('setParameter')
- ->with('job_status_values', ['pending', 'in_progress']);
-
- $this->constraint->apply($this->queryBuilder);
- }
-
- public function testApplyWithNoneOperator(): void
- {
- $this->constraint->setValue(['completed']);
- $this->constraint->setOperator('NONE');
-
- $subQueryBuilder = $this->createMock(QueryBuilder::class);
- $subQueryBuilder->method('select')->willReturnSelf();
- $subQueryBuilder->method('from')->willReturnSelf();
- $subQueryBuilder->method('join')->willReturnSelf();
- $subQueryBuilder->method('where')->willReturnSelf();
- $subQueryBuilder->method('andWhere')->willReturnSelf();
- $subQueryBuilder->method('getDQL')->willReturn('EXISTS_SUBQUERY_DQL');
-
- $this->entityManager->method('createQueryBuilder')
- ->willReturn($subQueryBuilder);
-
- $this->queryBuilder->expects($this->once())
- ->method('andWhere')
- ->with('NOT EXISTS (EXISTS_SUBQUERY_DQL)');
-
- $this->queryBuilder->expects($this->once())
- ->method('setParameter')
- ->with('job_status_values', ['completed']);
-
- $this->constraint->apply($this->queryBuilder);
- }
-
- public function testApplyWithUnsupportedOperator(): void
- {
- $this->constraint->setValue(['pending']);
- $this->constraint->setOperator('UNKNOWN');
-
- $subQueryBuilder = $this->createMock(QueryBuilder::class);
- $subQueryBuilder->method('select')->willReturnSelf();
- $subQueryBuilder->method('from')->willReturnSelf();
- $subQueryBuilder->method('join')->willReturnSelf();
- $subQueryBuilder->method('where')->willReturnSelf();
- $subQueryBuilder->method('getDQL')->willReturn('EXISTS_SUBQUERY_DQL');
-
- $this->entityManager->method('createQueryBuilder')
- ->willReturn($subQueryBuilder);
-
- // Should not call andWhere for unsupported operator
- $this->queryBuilder->expects($this->never())
- ->method('andWhere');
-
- $this->constraint->apply($this->queryBuilder);
- }
-
- public function testSubqueryStructure(): void
- {
- $this->constraint->setValue(['pending']);
- $this->constraint->setOperator('ANY');
-
- $subQueryBuilder = $this->createMock(QueryBuilder::class);
-
- $subQueryBuilder->expects($this->once())
- ->method('select')
- ->with('1')
- ->willReturnSelf();
-
- $subQueryBuilder->expects($this->once())
- ->method('from')
- ->with(BulkInfoProviderImportJobPart::class, 'bip_status')
- ->willReturnSelf();
-
- $subQueryBuilder->expects($this->once())
- ->method('join')
- ->with('bip_status.job', 'job_status')
- ->willReturnSelf();
-
- $subQueryBuilder->expects($this->once())
- ->method('where')
- ->with('bip_status.part = part.id')
- ->willReturnSelf();
-
- $subQueryBuilder->expects($this->once())
- ->method('andWhere')
- ->with('job_status.status IN (:job_status_values)')
- ->willReturnSelf();
-
- $subQueryBuilder->method('getDQL')->willReturn('EXISTS_SUBQUERY_DQL');
-
- $this->entityManager->method('createQueryBuilder')
- ->willReturn($subQueryBuilder);
-
- $this->queryBuilder->method('andWhere');
- $this->queryBuilder->method('setParameter');
-
- $this->constraint->apply($this->queryBuilder);
- }
-
- public function testValuesAndOperatorMutation(): void
- {
- // Test that values and operator can be changed after creation
- $this->constraint->setValue(['pending']);
- $this->constraint->setOperator('ANY');
- $this->assertTrue($this->constraint->isEnabled());
-
- $this->constraint->setValue([]);
- $this->assertFalse($this->constraint->isEnabled());
-
- $this->constraint->setValue(['completed']);
- $this->assertTrue($this->constraint->isEnabled());
-
- $this->constraint->setOperator('');
- $this->assertFalse($this->constraint->isEnabled());
-
- $this->constraint->setOperator('NONE');
- $this->assertTrue($this->constraint->isEnabled());
- }
-}
diff --git a/tests/DataTables/Filters/Constraints/Part/BulkImportPartStatusConstraintTest.php b/tests/DataTables/Filters/Constraints/Part/BulkImportPartStatusConstraintTest.php
deleted file mode 100644
index bc110eda..00000000
--- a/tests/DataTables/Filters/Constraints/Part/BulkImportPartStatusConstraintTest.php
+++ /dev/null
@@ -1,299 +0,0 @@
-.
- */
-
-declare(strict_types=1);
-
-namespace App\Tests\DataTables\Filters\Constraints\Part;
-
-use App\DataTables\Filters\Constraints\Part\BulkImportPartStatusConstraint;
-use App\Entity\InfoProviderSystem\BulkInfoProviderImportJobPart;
-use Doctrine\ORM\EntityManagerInterface;
-use Doctrine\ORM\QueryBuilder;
-use PHPUnit\Framework\TestCase;
-
-class BulkImportPartStatusConstraintTest extends TestCase
-{
- private BulkImportPartStatusConstraint $constraint;
- private QueryBuilder $queryBuilder;
- private EntityManagerInterface $entityManager;
-
- protected function setUp(): void
- {
- $this->constraint = new BulkImportPartStatusConstraint();
- $this->entityManager = $this->createMock(EntityManagerInterface::class);
- $this->queryBuilder = $this->createMock(QueryBuilder::class);
-
- $this->queryBuilder->method('getEntityManager')
- ->willReturn($this->entityManager);
- }
-
- public function testConstructor(): void
- {
- $this->assertEquals([], $this->constraint->getValue());
- $this->assertEmpty($this->constraint->getOperator());
- $this->assertFalse($this->constraint->isEnabled());
- }
-
- public function testGetAndSetValues(): void
- {
- $values = ['pending', 'completed', 'skipped'];
- $this->constraint->setValue($values);
-
- $this->assertEquals($values, $this->constraint->getValue());
- }
-
- public function testGetAndSetOperator(): void
- {
- $operator = 'ANY';
- $this->constraint->setOperator($operator);
-
- $this->assertEquals($operator, $this->constraint->getOperator());
- }
-
- public function testIsEnabledWithEmptyValues(): void
- {
- $this->constraint->setOperator('ANY');
-
- $this->assertFalse($this->constraint->isEnabled());
- }
-
- public function testIsEnabledWithNullOperator(): void
- {
- $this->constraint->setValue(['pending']);
-
- $this->assertFalse($this->constraint->isEnabled());
- }
-
- public function testIsEnabledWithValuesAndOperator(): void
- {
- $this->constraint->setValue(['pending']);
- $this->constraint->setOperator('ANY');
-
- $this->assertTrue($this->constraint->isEnabled());
- }
-
- public function testApplyWithEmptyValues(): void
- {
- $this->constraint->setOperator('ANY');
-
- $this->queryBuilder->expects($this->never())
- ->method('andWhere');
-
- $this->constraint->apply($this->queryBuilder);
- }
-
- public function testApplyWithNullOperator(): void
- {
- $this->constraint->setValue(['pending']);
-
- $this->queryBuilder->expects($this->never())
- ->method('andWhere');
-
- $this->constraint->apply($this->queryBuilder);
- }
-
- public function testApplyWithAnyOperator(): void
- {
- $this->constraint->setValue(['pending', 'completed']);
- $this->constraint->setOperator('ANY');
-
- $subQueryBuilder = $this->createMock(QueryBuilder::class);
- $subQueryBuilder->method('select')->willReturnSelf();
- $subQueryBuilder->method('from')->willReturnSelf();
- $subQueryBuilder->method('where')->willReturnSelf();
- $subQueryBuilder->method('andWhere')->willReturnSelf();
- $subQueryBuilder->method('getDQL')->willReturn('EXISTS_SUBQUERY_DQL');
-
- $this->entityManager->method('createQueryBuilder')
- ->willReturn($subQueryBuilder);
-
- $this->queryBuilder->expects($this->once())
- ->method('andWhere')
- ->with('EXISTS (EXISTS_SUBQUERY_DQL)');
-
- $this->queryBuilder->expects($this->once())
- ->method('setParameter')
- ->with('part_status_values', ['pending', 'completed']);
-
- $this->constraint->apply($this->queryBuilder);
- }
-
- public function testApplyWithNoneOperator(): void
- {
- $this->constraint->setValue(['failed']);
- $this->constraint->setOperator('NONE');
-
- $subQueryBuilder = $this->createMock(QueryBuilder::class);
- $subQueryBuilder->method('select')->willReturnSelf();
- $subQueryBuilder->method('from')->willReturnSelf();
- $subQueryBuilder->method('where')->willReturnSelf();
- $subQueryBuilder->method('andWhere')->willReturnSelf();
- $subQueryBuilder->method('getDQL')->willReturn('EXISTS_SUBQUERY_DQL');
-
- $this->entityManager->method('createQueryBuilder')
- ->willReturn($subQueryBuilder);
-
- $this->queryBuilder->expects($this->once())
- ->method('andWhere')
- ->with('NOT EXISTS (EXISTS_SUBQUERY_DQL)');
-
- $this->queryBuilder->expects($this->once())
- ->method('setParameter')
- ->with('part_status_values', ['failed']);
-
- $this->constraint->apply($this->queryBuilder);
- }
-
- public function testApplyWithUnsupportedOperator(): void
- {
- $this->constraint->setValue(['pending']);
- $this->constraint->setOperator('UNKNOWN');
-
- $subQueryBuilder = $this->createMock(QueryBuilder::class);
- $subQueryBuilder->method('select')->willReturnSelf();
- $subQueryBuilder->method('from')->willReturnSelf();
- $subQueryBuilder->method('where')->willReturnSelf();
- $subQueryBuilder->method('getDQL')->willReturn('EXISTS_SUBQUERY_DQL');
-
- $this->entityManager->method('createQueryBuilder')
- ->willReturn($subQueryBuilder);
-
- // Should not call andWhere for unsupported operator
- $this->queryBuilder->expects($this->never())
- ->method('andWhere');
-
- $this->constraint->apply($this->queryBuilder);
- }
-
- public function testSubqueryStructure(): void
- {
- $this->constraint->setValue(['completed', 'skipped']);
- $this->constraint->setOperator('ANY');
-
- $subQueryBuilder = $this->createMock(QueryBuilder::class);
-
- $subQueryBuilder->expects($this->once())
- ->method('select')
- ->with('1')
- ->willReturnSelf();
-
- $subQueryBuilder->expects($this->once())
- ->method('from')
- ->with(BulkInfoProviderImportJobPart::class, 'bip_part_status')
- ->willReturnSelf();
-
- $subQueryBuilder->expects($this->once())
- ->method('where')
- ->with('bip_part_status.part = part.id')
- ->willReturnSelf();
-
- $subQueryBuilder->expects($this->once())
- ->method('andWhere')
- ->with('bip_part_status.status IN (:part_status_values)')
- ->willReturnSelf();
-
- $subQueryBuilder->method('getDQL')->willReturn('EXISTS_SUBQUERY_DQL');
-
- $this->entityManager->method('createQueryBuilder')
- ->willReturn($subQueryBuilder);
-
- $this->queryBuilder->method('andWhere');
- $this->queryBuilder->method('setParameter');
-
- $this->constraint->apply($this->queryBuilder);
- }
-
- public function testValuesAndOperatorMutation(): void
- {
- // Test that values and operator can be changed after creation
- $this->constraint->setValue(['pending']);
- $this->constraint->setOperator('ANY');
- $this->assertTrue($this->constraint->isEnabled());
-
- $this->constraint->setValue([]);
- $this->assertFalse($this->constraint->isEnabled());
-
- $this->constraint->setValue(['completed', 'skipped']);
- $this->assertTrue($this->constraint->isEnabled());
-
- $this->constraint->setOperator("");
- $this->assertFalse($this->constraint->isEnabled());
-
- $this->constraint->setOperator('NONE');
- $this->assertTrue($this->constraint->isEnabled());
- }
-
- public function testDifferentFromJobStatusConstraint(): void
- {
- // This constraint should work differently from BulkImportJobStatusConstraint
- // It queries the part status directly, not the job status
- $this->constraint->setValue(['pending']);
- $this->constraint->setOperator('ANY');
-
- $subQueryBuilder = $this->createMock(QueryBuilder::class);
- $subQueryBuilder->method('select')->willReturnSelf();
- $subQueryBuilder->method('from')->willReturnSelf();
- $subQueryBuilder->method('where')->willReturnSelf();
- $subQueryBuilder->method('andWhere')->willReturnSelf();
- $subQueryBuilder->method('getDQL')->willReturn('EXISTS_SUBQUERY_DQL');
-
- $this->entityManager->method('createQueryBuilder')
- ->willReturn($subQueryBuilder);
-
- // Should use different alias than job status constraint
- $subQueryBuilder->expects($this->once())
- ->method('from')
- ->with(BulkInfoProviderImportJobPart::class, 'bip_part_status');
-
- // Should not join with job table like job status constraint does
- $subQueryBuilder->expects($this->never())
- ->method('join');
-
- $this->queryBuilder->method('andWhere');
- $this->queryBuilder->method('setParameter');
-
- $this->constraint->apply($this->queryBuilder);
- }
-
- public function testMultipleStatusValues(): void
- {
- $statusValues = ['pending', 'completed', 'skipped', 'failed'];
- $this->constraint->setValue($statusValues);
- $this->constraint->setOperator('ANY');
-
- $subQueryBuilder = $this->createMock(QueryBuilder::class);
- $subQueryBuilder->method('select')->willReturnSelf();
- $subQueryBuilder->method('from')->willReturnSelf();
- $subQueryBuilder->method('where')->willReturnSelf();
- $subQueryBuilder->method('andWhere')->willReturnSelf();
- $subQueryBuilder->method('getDQL')->willReturn('EXISTS_SUBQUERY_DQL');
-
- $this->entityManager->method('createQueryBuilder')
- ->willReturn($subQueryBuilder);
-
- $this->queryBuilder->expects($this->once())
- ->method('setParameter')
- ->with('part_status_values', $statusValues);
-
- $this->constraint->apply($this->queryBuilder);
-
- $this->assertEquals($statusValues, $this->constraint->getValue());
- }
-}
diff --git a/tests/Entity/BulkImportJobStatusTest.php b/tests/Entity/BulkImportJobStatusTest.php
deleted file mode 100644
index e8b4a977..00000000
--- a/tests/Entity/BulkImportJobStatusTest.php
+++ /dev/null
@@ -1,71 +0,0 @@
-.
- */
-
-declare(strict_types=1);
-
-namespace App\Tests\Entity;
-
-use App\Entity\InfoProviderSystem\BulkImportJobStatus;
-use PHPUnit\Framework\TestCase;
-
-class BulkImportJobStatusTest extends TestCase
-{
- public function testEnumValues(): void
- {
- $this->assertEquals('pending', BulkImportJobStatus::PENDING->value);
- $this->assertEquals('in_progress', BulkImportJobStatus::IN_PROGRESS->value);
- $this->assertEquals('completed', BulkImportJobStatus::COMPLETED->value);
- $this->assertEquals('stopped', BulkImportJobStatus::STOPPED->value);
- $this->assertEquals('failed', BulkImportJobStatus::FAILED->value);
- }
-
- public function testEnumCases(): void
- {
- $cases = BulkImportJobStatus::cases();
-
- $this->assertCount(5, $cases);
- $this->assertContains(BulkImportJobStatus::PENDING, $cases);
- $this->assertContains(BulkImportJobStatus::IN_PROGRESS, $cases);
- $this->assertContains(BulkImportJobStatus::COMPLETED, $cases);
- $this->assertContains(BulkImportJobStatus::STOPPED, $cases);
- $this->assertContains(BulkImportJobStatus::FAILED, $cases);
- }
-
- public function testFromString(): void
- {
- $this->assertEquals(BulkImportJobStatus::PENDING, BulkImportJobStatus::from('pending'));
- $this->assertEquals(BulkImportJobStatus::IN_PROGRESS, BulkImportJobStatus::from('in_progress'));
- $this->assertEquals(BulkImportJobStatus::COMPLETED, BulkImportJobStatus::from('completed'));
- $this->assertEquals(BulkImportJobStatus::STOPPED, BulkImportJobStatus::from('stopped'));
- $this->assertEquals(BulkImportJobStatus::FAILED, BulkImportJobStatus::from('failed'));
- }
-
- public function testTryFromInvalidValue(): void
- {
- $this->assertNull(BulkImportJobStatus::tryFrom('invalid'));
- $this->assertNull(BulkImportJobStatus::tryFrom(''));
- }
-
- public function testFromInvalidValueThrowsException(): void
- {
- $this->expectException(\ValueError::class);
- BulkImportJobStatus::from('invalid');
- }
-}
diff --git a/tests/Entity/BulkInfoProviderImportJobPartTest.php b/tests/Entity/BulkInfoProviderImportJobPartTest.php
deleted file mode 100644
index dd9600dd..00000000
--- a/tests/Entity/BulkInfoProviderImportJobPartTest.php
+++ /dev/null
@@ -1,301 +0,0 @@
-.
- */
-
-declare(strict_types=1);
-
-namespace App\Tests\Entity;
-
-use App\Entity\InfoProviderSystem\BulkImportPartStatus;
-use App\Entity\InfoProviderSystem\BulkInfoProviderImportJob;
-use App\Entity\InfoProviderSystem\BulkInfoProviderImportJobPart;
-use App\Entity\Parts\Part;
-use PHPUnit\Framework\TestCase;
-
-class BulkInfoProviderImportJobPartTest extends TestCase
-{
- private BulkInfoProviderImportJob $job;
- private Part $part;
- private BulkInfoProviderImportJobPart $jobPart;
-
- protected function setUp(): void
- {
- $this->job = $this->createMock(BulkInfoProviderImportJob::class);
- $this->part = $this->createMock(Part::class);
-
- $this->jobPart = new BulkInfoProviderImportJobPart($this->job, $this->part);
- }
-
- public function testConstructor(): void
- {
- $this->assertSame($this->job, $this->jobPart->getJob());
- $this->assertSame($this->part, $this->jobPart->getPart());
- $this->assertEquals(BulkImportPartStatus::PENDING, $this->jobPart->getStatus());
- $this->assertNull($this->jobPart->getReason());
- $this->assertNull($this->jobPart->getCompletedAt());
- }
-
- public function testGetAndSetJob(): void
- {
- $newJob = $this->createMock(BulkInfoProviderImportJob::class);
-
- $result = $this->jobPart->setJob($newJob);
-
- $this->assertSame($this->jobPart, $result);
- $this->assertSame($newJob, $this->jobPart->getJob());
- }
-
- public function testGetAndSetPart(): void
- {
- $newPart = $this->createMock(Part::class);
-
- $result = $this->jobPart->setPart($newPart);
-
- $this->assertSame($this->jobPart, $result);
- $this->assertSame($newPart, $this->jobPart->getPart());
- }
-
- public function testGetAndSetStatus(): void
- {
- $result = $this->jobPart->setStatus(BulkImportPartStatus::COMPLETED);
-
- $this->assertSame($this->jobPart, $result);
- $this->assertEquals(BulkImportPartStatus::COMPLETED, $this->jobPart->getStatus());
- }
-
- public function testGetAndSetReason(): void
- {
- $reason = 'Test reason';
-
- $result = $this->jobPart->setReason($reason);
-
- $this->assertSame($this->jobPart, $result);
- $this->assertEquals($reason, $this->jobPart->getReason());
- }
-
- public function testGetAndSetCompletedAt(): void
- {
- $completedAt = new \DateTimeImmutable();
-
- $result = $this->jobPart->setCompletedAt($completedAt);
-
- $this->assertSame($this->jobPart, $result);
- $this->assertSame($completedAt, $this->jobPart->getCompletedAt());
- }
-
- public function testMarkAsCompleted(): void
- {
- $beforeTime = new \DateTimeImmutable();
-
- $result = $this->jobPart->markAsCompleted();
-
- $afterTime = new \DateTimeImmutable();
-
- $this->assertSame($this->jobPart, $result);
- $this->assertEquals(BulkImportPartStatus::COMPLETED, $this->jobPart->getStatus());
- $this->assertInstanceOf(\DateTimeImmutable::class, $this->jobPart->getCompletedAt());
- $this->assertGreaterThanOrEqual($beforeTime, $this->jobPart->getCompletedAt());
- $this->assertLessThanOrEqual($afterTime, $this->jobPart->getCompletedAt());
- }
-
- public function testMarkAsSkipped(): void
- {
- $reason = 'Skipped for testing';
- $beforeTime = new \DateTimeImmutable();
-
- $result = $this->jobPart->markAsSkipped($reason);
-
- $afterTime = new \DateTimeImmutable();
-
- $this->assertSame($this->jobPart, $result);
- $this->assertEquals(BulkImportPartStatus::SKIPPED, $this->jobPart->getStatus());
- $this->assertEquals($reason, $this->jobPart->getReason());
- $this->assertInstanceOf(\DateTimeImmutable::class, $this->jobPart->getCompletedAt());
- $this->assertGreaterThanOrEqual($beforeTime, $this->jobPart->getCompletedAt());
- $this->assertLessThanOrEqual($afterTime, $this->jobPart->getCompletedAt());
- }
-
- public function testMarkAsSkippedWithoutReason(): void
- {
- $result = $this->jobPart->markAsSkipped();
-
- $this->assertSame($this->jobPart, $result);
- $this->assertEquals(BulkImportPartStatus::SKIPPED, $this->jobPart->getStatus());
- $this->assertEquals('', $this->jobPart->getReason());
- $this->assertInstanceOf(\DateTimeImmutable::class, $this->jobPart->getCompletedAt());
- }
-
- public function testMarkAsFailed(): void
- {
- $reason = 'Failed for testing';
- $beforeTime = new \DateTimeImmutable();
-
- $result = $this->jobPart->markAsFailed($reason);
-
- $afterTime = new \DateTimeImmutable();
-
- $this->assertSame($this->jobPart, $result);
- $this->assertEquals(BulkImportPartStatus::FAILED, $this->jobPart->getStatus());
- $this->assertEquals($reason, $this->jobPart->getReason());
- $this->assertInstanceOf(\DateTimeImmutable::class, $this->jobPart->getCompletedAt());
- $this->assertGreaterThanOrEqual($beforeTime, $this->jobPart->getCompletedAt());
- $this->assertLessThanOrEqual($afterTime, $this->jobPart->getCompletedAt());
- }
-
- public function testMarkAsFailedWithoutReason(): void
- {
- $result = $this->jobPart->markAsFailed();
-
- $this->assertSame($this->jobPart, $result);
- $this->assertEquals(BulkImportPartStatus::FAILED, $this->jobPart->getStatus());
- $this->assertEquals('', $this->jobPart->getReason());
- $this->assertInstanceOf(\DateTimeImmutable::class, $this->jobPart->getCompletedAt());
- }
-
- public function testMarkAsPending(): void
- {
- // First mark as completed to have something to reset
- $this->jobPart->markAsCompleted();
-
- $result = $this->jobPart->markAsPending();
-
- $this->assertSame($this->jobPart, $result);
- $this->assertEquals(BulkImportPartStatus::PENDING, $this->jobPart->getStatus());
- $this->assertNull($this->jobPart->getReason());
- $this->assertNull($this->jobPart->getCompletedAt());
- }
-
- public function testIsPending(): void
- {
- $this->assertTrue($this->jobPart->isPending());
-
- $this->jobPart->setStatus(BulkImportPartStatus::COMPLETED);
- $this->assertFalse($this->jobPart->isPending());
-
- $this->jobPart->setStatus(BulkImportPartStatus::SKIPPED);
- $this->assertFalse($this->jobPart->isPending());
-
- $this->jobPart->setStatus(BulkImportPartStatus::FAILED);
- $this->assertFalse($this->jobPart->isPending());
- }
-
- public function testIsCompleted(): void
- {
- $this->assertFalse($this->jobPart->isCompleted());
-
- $this->jobPart->setStatus(BulkImportPartStatus::COMPLETED);
- $this->assertTrue($this->jobPart->isCompleted());
-
- $this->jobPart->setStatus(BulkImportPartStatus::SKIPPED);
- $this->assertFalse($this->jobPart->isCompleted());
-
- $this->jobPart->setStatus(BulkImportPartStatus::FAILED);
- $this->assertFalse($this->jobPart->isCompleted());
- }
-
- public function testIsSkipped(): void
- {
- $this->assertFalse($this->jobPart->isSkipped());
-
- $this->jobPart->setStatus(BulkImportPartStatus::SKIPPED);
- $this->assertTrue($this->jobPart->isSkipped());
-
- $this->jobPart->setStatus(BulkImportPartStatus::COMPLETED);
- $this->assertFalse($this->jobPart->isSkipped());
-
- $this->jobPart->setStatus(BulkImportPartStatus::FAILED);
- $this->assertFalse($this->jobPart->isSkipped());
- }
-
- public function testIsFailed(): void
- {
- $this->assertFalse($this->jobPart->isFailed());
-
- $this->jobPart->setStatus(BulkImportPartStatus::FAILED);
- $this->assertTrue($this->jobPart->isFailed());
-
- $this->jobPart->setStatus(BulkImportPartStatus::COMPLETED);
- $this->assertFalse($this->jobPart->isFailed());
-
- $this->jobPart->setStatus(BulkImportPartStatus::SKIPPED);
- $this->assertFalse($this->jobPart->isFailed());
- }
-
- public function testBulkImportPartStatusEnum(): void
- {
- $this->assertEquals('pending', BulkImportPartStatus::PENDING->value);
- $this->assertEquals('completed', BulkImportPartStatus::COMPLETED->value);
- $this->assertEquals('skipped', BulkImportPartStatus::SKIPPED->value);
- $this->assertEquals('failed', BulkImportPartStatus::FAILED->value);
- }
-
- public function testStatusTransitions(): void
- {
- // Test pending -> completed
- $this->assertTrue($this->jobPart->isPending());
- $this->jobPart->markAsCompleted();
- $this->assertTrue($this->jobPart->isCompleted());
-
- // Test completed -> pending
- $this->jobPart->markAsPending();
- $this->assertTrue($this->jobPart->isPending());
-
- // Test pending -> skipped
- $this->jobPart->markAsSkipped('Test reason');
- $this->assertTrue($this->jobPart->isSkipped());
-
- // Test skipped -> pending
- $this->jobPart->markAsPending();
- $this->assertTrue($this->jobPart->isPending());
-
- // Test pending -> failed
- $this->jobPart->markAsFailed('Test error');
- $this->assertTrue($this->jobPart->isFailed());
-
- // Test failed -> pending
- $this->jobPart->markAsPending();
- $this->assertTrue($this->jobPart->isPending());
- }
-
- public function testReasonAndCompletedAtConsistency(): void
- {
- // Initially no reason or completion time
- $this->assertNull($this->jobPart->getReason());
- $this->assertNull($this->jobPart->getCompletedAt());
-
- // After marking as skipped, should have reason and completion time
- $this->jobPart->markAsSkipped('Skipped reason');
- $this->assertEquals('Skipped reason', $this->jobPart->getReason());
- $this->assertInstanceOf(\DateTimeImmutable::class, $this->jobPart->getCompletedAt());
-
- // After marking as pending, reason and completion time should be cleared
- $this->jobPart->markAsPending();
- $this->assertNull($this->jobPart->getReason());
- $this->assertNull($this->jobPart->getCompletedAt());
-
- // After marking as failed, should have reason and completion time
- $this->jobPart->markAsFailed('Failed reason');
- $this->assertEquals('Failed reason', $this->jobPart->getReason());
- $this->assertInstanceOf(\DateTimeImmutable::class, $this->jobPart->getCompletedAt());
-
- // After marking as completed, should have completion time (reason may remain from previous state)
- $this->jobPart->markAsCompleted();
- $this->assertInstanceOf(\DateTimeImmutable::class, $this->jobPart->getCompletedAt());
- }
-}
diff --git a/tests/Entity/BulkInfoProviderImportJobTest.php b/tests/Entity/BulkInfoProviderImportJobTest.php
deleted file mode 100644
index c9841ac4..00000000
--- a/tests/Entity/BulkInfoProviderImportJobTest.php
+++ /dev/null
@@ -1,368 +0,0 @@
-.
- */
-
-declare(strict_types=1);
-
-namespace App\Tests\Entity;
-
-use App\Entity\InfoProviderSystem\BulkImportJobStatus;
-use App\Entity\InfoProviderSystem\BulkInfoProviderImportJob;
-use App\Entity\UserSystem\User;
-use App\Services\InfoProviderSystem\DTOs\BulkSearchFieldMappingDTO;
-use App\Services\InfoProviderSystem\DTOs\BulkSearchPartResultDTO;
-use App\Services\InfoProviderSystem\DTOs\BulkSearchResponseDTO;
-use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
-use PHPUnit\Framework\TestCase;
-
-class BulkInfoProviderImportJobTest extends TestCase
-{
- private BulkInfoProviderImportJob $job;
- private User $user;
-
- protected function setUp(): void
- {
- $this->user = new User();
- $this->user->setName('test_user');
-
- $this->job = new BulkInfoProviderImportJob();
- $this->job->setCreatedBy($this->user);
- }
-
- private function createMockPart(int $id): \App\Entity\Parts\Part
- {
- $part = $this->createMock(\App\Entity\Parts\Part::class);
- $part->method('getId')->willReturn($id);
- $part->method('getName')->willReturn("Test Part {$id}");
- return $part;
- }
-
- public function testConstruct(): void
- {
- $job = new BulkInfoProviderImportJob();
-
- $this->assertInstanceOf(\DateTimeImmutable::class, $job->getCreatedAt());
- $this->assertEquals(BulkImportJobStatus::PENDING, $job->getStatus());
- $this->assertEmpty($job->getPartIds());
- $this->assertEmpty($job->getFieldMappings());
- $this->assertEmpty($job->getSearchResultsRaw());
- $this->assertEmpty($job->getProgress());
- $this->assertNull($job->getCompletedAt());
- $this->assertFalse($job->isPrefetchDetails());
- }
-
- public function testBasicGettersSetters(): void
- {
- $this->job->setName('Test Job');
- $this->assertEquals('Test Job', $this->job->getName());
-
- // Test with actual parts - this is what actually works
- $parts = [$this->createMockPart(1), $this->createMockPart(2), $this->createMockPart(3)];
- foreach ($parts as $part) {
- $this->job->addPart($part);
- }
- $this->assertEquals([1, 2, 3], $this->job->getPartIds());
-
- $fieldMappings = [new BulkSearchFieldMappingDTO(field: 'field1', providers: ['provider1', 'provider2'])];
- $this->job->setFieldMappings($fieldMappings);
- $this->assertEquals($fieldMappings, $this->job->getFieldMappings());
-
- $this->job->setPrefetchDetails(true);
- $this->assertTrue($this->job->isPrefetchDetails());
-
- $this->assertEquals($this->user, $this->job->getCreatedBy());
- }
-
- public function testStatusTransitions(): void
- {
- $this->assertTrue($this->job->isPending());
- $this->assertFalse($this->job->isInProgress());
- $this->assertFalse($this->job->isCompleted());
- $this->assertFalse($this->job->isFailed());
- $this->assertFalse($this->job->isStopped());
-
- $this->job->markAsInProgress();
- $this->assertEquals(BulkImportJobStatus::IN_PROGRESS, $this->job->getStatus());
- $this->assertTrue($this->job->isInProgress());
- $this->assertFalse($this->job->isPending());
-
- $this->job->markAsCompleted();
- $this->assertEquals(BulkImportJobStatus::COMPLETED, $this->job->getStatus());
- $this->assertTrue($this->job->isCompleted());
- $this->assertNotNull($this->job->getCompletedAt());
-
- $job2 = new BulkInfoProviderImportJob();
- $job2->markAsFailed();
- $this->assertEquals(BulkImportJobStatus::FAILED, $job2->getStatus());
- $this->assertTrue($job2->isFailed());
- $this->assertNotNull($job2->getCompletedAt());
-
- $job3 = new BulkInfoProviderImportJob();
- $job3->markAsStopped();
- $this->assertEquals(BulkImportJobStatus::STOPPED, $job3->getStatus());
- $this->assertTrue($job3->isStopped());
- $this->assertNotNull($job3->getCompletedAt());
- }
-
- public function testCanBeStopped(): void
- {
- $this->assertTrue($this->job->canBeStopped());
-
- $this->job->markAsInProgress();
- $this->assertTrue($this->job->canBeStopped());
-
- $this->job->markAsCompleted();
- $this->assertFalse($this->job->canBeStopped());
-
- $this->job->setStatus(BulkImportJobStatus::FAILED);
- $this->assertFalse($this->job->canBeStopped());
-
- $this->job->setStatus(BulkImportJobStatus::STOPPED);
- $this->assertFalse($this->job->canBeStopped());
- }
-
- public function testPartCount(): void
- {
- $this->assertEquals(0, $this->job->getPartCount());
-
- // Test with actual parts - setPartIds doesn't actually add parts
- $parts = [
- $this->createMockPart(1),
- $this->createMockPart(2),
- $this->createMockPart(3),
- $this->createMockPart(4),
- $this->createMockPart(5)
- ];
- foreach ($parts as $part) {
- $this->job->addPart($part);
- }
- $this->assertEquals(5, $this->job->getPartCount());
- }
-
- public function testResultCount(): void
- {
- $this->assertEquals(0, $this->job->getResultCount());
-
- $searchResults = new BulkSearchResponseDTO([
- new \App\Services\InfoProviderSystem\DTOs\BulkSearchPartResultsDTO(
- part: $this->createMockPart(1),
- searchResults: [new BulkSearchPartResultDTO(searchResult: new SearchResultDTO(provider_key: 'dummy', provider_id: '1234', name: 'Part 1', description: 'A part'))]
- ),
- new \App\Services\InfoProviderSystem\DTOs\BulkSearchPartResultsDTO(
- part: $this->createMockPart(2),
- searchResults: [new BulkSearchPartResultDTO(searchResult: new SearchResultDTO(provider_key: 'dummy', provider_id: '1234', name: 'Part 2', description: 'A part')),
- new BulkSearchPartResultDTO(searchResult: new SearchResultDTO(provider_key: 'dummy', provider_id: '5678', name: 'Part 2 Alt', description: 'Another part'))]
- ),
- new \App\Services\InfoProviderSystem\DTOs\BulkSearchPartResultsDTO(
- part: $this->createMockPart(3),
- searchResults: []
- )
- ]);
-
- $this->job->setSearchResults($searchResults);
- $this->assertEquals(3, $this->job->getResultCount());
- }
-
- public function testPartProgressTracking(): void
- {
- // Test with actual parts - setPartIds doesn't actually add parts
- $parts = [
- $this->createMockPart(1),
- $this->createMockPart(2),
- $this->createMockPart(3),
- $this->createMockPart(4)
- ];
- foreach ($parts as $part) {
- $this->job->addPart($part);
- }
-
- $this->assertFalse($this->job->isPartCompleted(1));
- $this->assertFalse($this->job->isPartSkipped(1));
-
- $this->job->markPartAsCompleted(1);
- $this->assertTrue($this->job->isPartCompleted(1));
- $this->assertFalse($this->job->isPartSkipped(1));
-
- $this->job->markPartAsSkipped(2, 'Not found');
- $this->assertFalse($this->job->isPartCompleted(2));
- $this->assertTrue($this->job->isPartSkipped(2));
-
- $this->job->markPartAsPending(1);
- $this->assertFalse($this->job->isPartCompleted(1));
- $this->assertFalse($this->job->isPartSkipped(1));
- }
-
- public function testProgressCounts(): void
- {
- // Test with actual parts - setPartIds doesn't actually add parts
- $parts = [
- $this->createMockPart(1),
- $this->createMockPart(2),
- $this->createMockPart(3),
- $this->createMockPart(4),
- $this->createMockPart(5)
- ];
- foreach ($parts as $part) {
- $this->job->addPart($part);
- }
-
- $this->assertEquals(0, $this->job->getCompletedPartsCount());
- $this->assertEquals(0, $this->job->getSkippedPartsCount());
-
- $this->job->markPartAsCompleted(1);
- $this->job->markPartAsCompleted(2);
- $this->job->markPartAsSkipped(3, 'Error');
-
- $this->assertEquals(2, $this->job->getCompletedPartsCount());
- $this->assertEquals(1, $this->job->getSkippedPartsCount());
- }
-
- public function testProgressPercentage(): void
- {
- $emptyJob = new BulkInfoProviderImportJob();
- $this->assertEquals(100.0, $emptyJob->getProgressPercentage());
-
- // Test with actual parts - setPartIds doesn't actually add parts
- $parts = [
- $this->createMockPart(1),
- $this->createMockPart(2),
- $this->createMockPart(3),
- $this->createMockPart(4),
- $this->createMockPart(5)
- ];
- foreach ($parts as $part) {
- $this->job->addPart($part);
- }
-
- $this->assertEquals(0.0, $this->job->getProgressPercentage());
-
- $this->job->markPartAsCompleted(1);
- $this->job->markPartAsCompleted(2);
- $this->assertEquals(40.0, $this->job->getProgressPercentage());
-
- $this->job->markPartAsSkipped(3, 'Error');
- $this->assertEquals(60.0, $this->job->getProgressPercentage());
-
- $this->job->markPartAsCompleted(4);
- $this->job->markPartAsCompleted(5);
- $this->assertEquals(100.0, $this->job->getProgressPercentage());
- }
-
- public function testIsAllPartsCompleted(): void
- {
- $emptyJob = new BulkInfoProviderImportJob();
- $this->assertTrue($emptyJob->isAllPartsCompleted());
-
- // Test with actual parts - setPartIds doesn't actually add parts
- $parts = [
- $this->createMockPart(1),
- $this->createMockPart(2),
- $this->createMockPart(3)
- ];
- foreach ($parts as $part) {
- $this->job->addPart($part);
- }
-
- $this->assertFalse($this->job->isAllPartsCompleted());
-
- $this->job->markPartAsCompleted(1);
- $this->assertFalse($this->job->isAllPartsCompleted());
-
- $this->job->markPartAsCompleted(2);
- $this->job->markPartAsSkipped(3, 'Error');
- $this->assertTrue($this->job->isAllPartsCompleted());
- }
-
- public function testDisplayNameMethods(): void
- {
- // Test with actual parts - setPartIds doesn't actually add parts
- $parts = [
- $this->createMockPart(1),
- $this->createMockPart(2),
- $this->createMockPart(3)
- ];
- foreach ($parts as $part) {
- $this->job->addPart($part);
- }
-
- $this->assertEquals('info_providers.bulk_import.job_name_template', $this->job->getDisplayNameKey());
- $this->assertEquals(['%count%' => 3], $this->job->getDisplayNameParams());
- }
-
- public function testFormattedTimestamp(): void
- {
- $timestampRegex = '/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/';
- $this->assertMatchesRegularExpression($timestampRegex, $this->job->getFormattedTimestamp());
- }
-
- public function testProgressDataStructure(): void
- {
- $parts = [
- $this->createMockPart(1),
- $this->createMockPart(2),
- $this->createMockPart(3)
- ];
- foreach ($parts as $part) {
- $this->job->addPart($part);
- }
-
- $this->job->markPartAsCompleted(1);
- $this->job->markPartAsSkipped(2, 'Test reason');
-
- $progress = $this->job->getProgress();
-
- // The progress array should have keys for all part IDs, even if not completed/skipped
- $this->assertArrayHasKey(1, $progress, 'Progress should contain key for part 1');
- $this->assertArrayHasKey(2, $progress, 'Progress should contain key for part 2');
- $this->assertArrayHasKey(3, $progress, 'Progress should contain key for part 3');
-
- // Part 1: completed
- $this->assertEquals('completed', $progress[1]['status']);
- $this->assertArrayHasKey('completed_at', $progress[1]);
- $this->assertArrayNotHasKey('reason', $progress[1]);
-
- // Part 2: skipped
- $this->assertEquals('skipped', $progress[2]['status']);
- $this->assertEquals('Test reason', $progress[2]['reason']);
- $this->assertArrayHasKey('completed_at', $progress[2]);
-
- // Part 3: should be present but not completed/skipped
- $this->assertEquals('pending', $progress[3]['status']);
- $this->assertArrayNotHasKey('completed_at', $progress[3]);
- $this->assertArrayNotHasKey('reason', $progress[3]);
- }
-
- public function testCompletedAtTimestamp(): void
- {
- $this->assertNull($this->job->getCompletedAt());
-
- $beforeCompletion = new \DateTimeImmutable();
- $this->job->markAsCompleted();
- $afterCompletion = new \DateTimeImmutable();
-
- $completedAt = $this->job->getCompletedAt();
- $this->assertNotNull($completedAt);
- $this->assertGreaterThanOrEqual($beforeCompletion, $completedAt);
- $this->assertLessThanOrEqual($afterCompletion, $completedAt);
-
- $customTime = new \DateTimeImmutable('2023-01-01 12:00:00');
- $this->job->setCompletedAt($customTime);
- $this->assertEquals($customTime, $this->job->getCompletedAt());
- }
-}
diff --git a/tests/Form/InfoProviderSystem/GlobalFieldMappingTypeTest.php b/tests/Form/InfoProviderSystem/GlobalFieldMappingTypeTest.php
deleted file mode 100644
index 52e0b1d2..00000000
--- a/tests/Form/InfoProviderSystem/GlobalFieldMappingTypeTest.php
+++ /dev/null
@@ -1,68 +0,0 @@
-.
- */
-
-declare(strict_types=1);
-
-namespace App\Tests\Form\InfoProviderSystem;
-
-use App\Form\InfoProviderSystem\GlobalFieldMappingType;
-use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
-use Symfony\Component\Form\FormFactoryInterface;
-
-/**
- * @group slow
- * @group DB
- */
-class GlobalFieldMappingTypeTest extends KernelTestCase
-{
- private FormFactoryInterface $formFactory;
-
- protected function setUp(): void
- {
- self::bootKernel();
- $this->formFactory = static::getContainer()->get(FormFactoryInterface::class);
- }
-
- public function testFormCreation(): void
- {
- $form = $this->formFactory->create(GlobalFieldMappingType::class, null, [
- 'field_choices' => [
- 'MPN' => 'mpn',
- 'Name' => 'name'
- ],
- 'csrf_protection' => false
- ]);
-
- $this->assertTrue($form->has('field_mappings'));
- $this->assertTrue($form->has('prefetch_details'));
- $this->assertTrue($form->has('submit'));
- }
-
- public function testFormOptions(): void
- {
- $form = $this->formFactory->create(GlobalFieldMappingType::class, null, [
- 'field_choices' => [],
- 'csrf_protection' => false
- ]);
-
- $view = $form->createView();
- $this->assertFalse($view['prefetch_details']->vars['required']);
- }
-}
\ No newline at end of file
diff --git a/tests/Repository/LogEntryRepositoryTest.php b/tests/Repository/LogEntryRepositoryTest.php
index f6cc991d..fc31faf5 100644
--- a/tests/Repository/LogEntryRepositoryTest.php
+++ b/tests/Repository/LogEntryRepositoryTest.php
@@ -112,8 +112,7 @@ class LogEntryRepositoryTest extends KernelTestCase
$this->assertCount(2, $logs);
//The first one must be newer than the second one
- $this->assertGreaterThanOrEqual($logs[1]->getTimestamp(), $logs[0]->getTimestamp());
- $this->assertGreaterThanOrEqual($logs[1]->getID(), $logs[0]->getID());
+ $this->assertGreaterThanOrEqual($logs[0]->getTimestamp(), $logs[1]->getTimestamp());
}
public function testGetElementExistedAtTimestamp(): void
diff --git a/tests/Services/ElementTypeNameGeneratorTest.php b/tests/Services/ElementTypeNameGeneratorTest.php
index f99b0676..934a3bbd 100644
--- a/tests/Services/ElementTypeNameGeneratorTest.php
+++ b/tests/Services/ElementTypeNameGeneratorTest.php
@@ -25,12 +25,11 @@ namespace App\Tests\Services;
use App\Entity\Attachments\PartAttachment;
use App\Entity\Base\AbstractDBElement;
use App\Entity\Base\AbstractNamedDBElement;
-use App\Entity\InfoProviderSystem\BulkInfoProviderImportJob;
use App\Entity\Parts\Category;
use App\Entity\Parts\Part;
use App\Exceptions\EntityNotSupportedException;
-use App\Services\ElementTypeNameGenerator;
use App\Services\Formatters\AmountFormatter;
+use App\Services\ElementTypeNameGenerator;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class ElementTypeNameGeneratorTest extends WebTestCase
@@ -51,18 +50,16 @@ class ElementTypeNameGeneratorTest extends WebTestCase
//We only test in english
$this->assertSame('Part', $this->service->getLocalizedTypeLabel(new Part()));
$this->assertSame('Category', $this->service->getLocalizedTypeLabel(new Category()));
- $this->assertSame('Bulk info provider import', $this->service->getLocalizedTypeLabel(new BulkInfoProviderImportJob()));
//Test inheritance
$this->assertSame('Attachment', $this->service->getLocalizedTypeLabel(new PartAttachment()));
//Test for class name
$this->assertSame('Part', $this->service->getLocalizedTypeLabel(Part::class));
- $this->assertSame('Bulk info provider import', $this->service->getLocalizedTypeLabel(BulkInfoProviderImportJob::class));
//Test exception for unknpwn type
$this->expectException(EntityNotSupportedException::class);
- $this->service->getLocalizedTypeLabel(new class () extends AbstractDBElement {
+ $this->service->getLocalizedTypeLabel(new class() extends AbstractDBElement {
});
}
@@ -77,7 +74,7 @@ class ElementTypeNameGeneratorTest extends WebTestCase
//Test exception
$this->expectException(EntityNotSupportedException::class);
- $this->service->getTypeNameCombination(new class () extends AbstractNamedDBElement {
+ $this->service->getTypeNameCombination(new class() extends AbstractNamedDBElement {
public function getIDString(): string
{
return 'Stub';
diff --git a/tests/Services/ImportExportSystem/EntityExporterTest.php b/tests/Services/ImportExportSystem/EntityExporterTest.php
index e9b924b1..004971ab 100644
--- a/tests/Services/ImportExportSystem/EntityExporterTest.php
+++ b/tests/Services/ImportExportSystem/EntityExporterTest.php
@@ -26,7 +26,6 @@ use App\Entity\Parts\Category;
use App\Services\ImportExportSystem\EntityExporter;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\HttpFoundation\Request;
-use PhpOffice\PhpSpreadsheet\IOFactory;
class EntityExporterTest extends WebTestCase
{
@@ -77,40 +76,7 @@ class EntityExporterTest extends WebTestCase
$this->assertSame('application/json', $response->headers->get('Content-Type'));
$this->assertNotEmpty($response->headers->get('Content-Disposition'));
- }
- public function testExportToExcel(): void
- {
- $entities = $this->getEntities();
- $xlsxData = $this->service->exportEntities($entities, ['format' => 'xlsx', 'level' => 'simple']);
- $this->assertNotEmpty($xlsxData);
-
- $tempFile = tempnam(sys_get_temp_dir(), 'test_export') . '.xlsx';
- file_put_contents($tempFile, $xlsxData);
-
- $spreadsheet = IOFactory::load($tempFile);
- $worksheet = $spreadsheet->getActiveSheet();
-
- $this->assertSame('name', $worksheet->getCell('A1')->getValue());
- $this->assertSame('full_name', $worksheet->getCell('B1')->getValue());
-
- $this->assertSame('Enitity 1', $worksheet->getCell('A2')->getValue());
- $this->assertSame('Enitity 1', $worksheet->getCell('B2')->getValue());
-
- unlink($tempFile);
- }
-
- public function testExportExcelFromRequest(): void
- {
- $entities = $this->getEntities();
-
- $request = new Request();
- $request->request->set('format', 'xlsx');
- $request->request->set('level', 'simple');
- $response = $this->service->exportEntityFromRequest($entities, $request);
-
- $this->assertSame('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', $response->headers->get('Content-Type'));
- $this->assertStringContainsString('export_Category_simple.xlsx', $response->headers->get('Content-Disposition'));
}
}
diff --git a/tests/Services/ImportExportSystem/EntityImporterTest.php b/tests/Services/ImportExportSystem/EntityImporterTest.php
index 83367f80..fd5e8b9e 100644
--- a/tests/Services/ImportExportSystem/EntityImporterTest.php
+++ b/tests/Services/ImportExportSystem/EntityImporterTest.php
@@ -36,9 +36,6 @@ use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Component\Validator\ConstraintViolationListInterface;
-use Symfony\Component\HttpFoundation\File\File;
-use PhpOffice\PhpSpreadsheet\Spreadsheet;
-use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
#[Group('DB')]
class EntityImporterTest extends WebTestCase
@@ -210,10 +207,6 @@ EOT;
yield ['json', 'json'];
yield ['yaml', 'yml'];
yield ['yaml', 'YAML'];
- yield ['xlsx', 'xlsx'];
- yield ['xlsx', 'XLSX'];
- yield ['xls', 'xls'];
- yield ['xls', 'XLS'];
}
#[DataProvider('formatDataProvider')]
@@ -349,41 +342,4 @@ EOT;
$this->assertSame($category, $results[0]->getCategory());
$this->assertSame('test,test2', $results[0]->getTags());
}
-
- public function testImportExcelFileProjects(): void
- {
- $spreadsheet = new Spreadsheet();
- $worksheet = $spreadsheet->getActiveSheet();
-
- $worksheet->setCellValue('A1', 'name');
- $worksheet->setCellValue('B1', 'comment');
- $worksheet->setCellValue('A2', 'Test Excel 1');
- $worksheet->setCellValue('B2', 'Test Excel 1 notes');
- $worksheet->setCellValue('A3', 'Test Excel 2');
- $worksheet->setCellValue('B3', 'Test Excel 2 notes');
-
- $tempFile = tempnam(sys_get_temp_dir(), 'test_excel') . '.xlsx';
- $writer = new Xlsx($spreadsheet);
- $writer->save($tempFile);
-
- $file = new File($tempFile);
-
- $errors = [];
- $results = $this->service->importFile($file, [
- 'class' => Project::class,
- 'format' => 'xlsx',
- 'csv_delimiter' => ';',
- ], $errors);
-
- $this->assertCount(2, $results);
- $this->assertEmpty($errors);
- $this->assertContainsOnlyInstancesOf(Project::class, $results);
-
- $this->assertSame('Test Excel 1', $results[0]->getName());
- $this->assertSame('Test Excel 1 notes', $results[0]->getComment());
- $this->assertSame('Test Excel 2', $results[1]->getName());
- $this->assertSame('Test Excel 2 notes', $results[1]->getComment());
-
- unlink($tempFile);
- }
}
diff --git a/tests/Services/InfoProviderSystem/DTOs/BulkSearchFieldMappingDTOTest.php b/tests/Services/InfoProviderSystem/DTOs/BulkSearchFieldMappingDTOTest.php
deleted file mode 100644
index a2101938..00000000
--- a/tests/Services/InfoProviderSystem/DTOs/BulkSearchFieldMappingDTOTest.php
+++ /dev/null
@@ -1,63 +0,0 @@
-.
- */
-
-namespace App\Tests\Services\InfoProviderSystem\DTOs;
-
-use App\Services\InfoProviderSystem\DTOs\BulkSearchFieldMappingDTO;
-use PHPUnit\Framework\TestCase;
-
-class BulkSearchFieldMappingDTOTest extends TestCase
-{
-
- public function testIsSupplierPartNumberField(): void
- {
- $fieldMapping = new BulkSearchFieldMappingDTO(field: 'reichelt_spn', providers: ['provider1'], priority: 1);
- $this->assertTrue($fieldMapping->isSupplierPartNumberField());
-
- $fieldMapping = new BulkSearchFieldMappingDTO(field: 'partNumber', providers: ['provider1'], priority: 1);
- $this->assertFalse($fieldMapping->isSupplierPartNumberField());
- }
-
- public function testToSerializableArray(): void
- {
- $fieldMapping = new BulkSearchFieldMappingDTO(field: 'test', providers: ['provider1', 'provider2'], priority: 3);
- $array = $fieldMapping->toSerializableArray();
- $this->assertIsArray($array);
- $this->assertSame([
- 'field' => 'test',
- 'providers' => ['provider1', 'provider2'],
- 'priority' => 3,
- ], $array);
- }
-
- public function testFromSerializableArray(): void
- {
- $data = [
- 'field' => 'test',
- 'providers' => ['provider1', 'provider2'],
- 'priority' => 3,
- ];
- $fieldMapping = BulkSearchFieldMappingDTO::fromSerializableArray($data);
- $this->assertInstanceOf(BulkSearchFieldMappingDTO::class, $fieldMapping);
- $this->assertSame('test', $fieldMapping->field);
- $this->assertSame(['provider1', 'provider2'], $fieldMapping->providers);
- $this->assertSame(3, $fieldMapping->priority);
- }
-}
diff --git a/tests/Services/InfoProviderSystem/DTOs/BulkSearchPartResultsDTOTest.php b/tests/Services/InfoProviderSystem/DTOs/BulkSearchPartResultsDTOTest.php
deleted file mode 100644
index 09fa4973..00000000
--- a/tests/Services/InfoProviderSystem/DTOs/BulkSearchPartResultsDTOTest.php
+++ /dev/null
@@ -1,63 +0,0 @@
-.
- */
-
-namespace App\Tests\Services\InfoProviderSystem\DTOs;
-
-use App\Services\InfoProviderSystem\DTOs\BulkSearchPartResultsDTO;
-use PHPUnit\Framework\TestCase;
-
-class BulkSearchPartResultsDTOTest extends TestCase
-{
-
- public function testHasErrors(): void
- {
- $test = new BulkSearchPartResultsDTO($this->createMock(\App\Entity\Parts\Part::class), [], []);
- $this->assertFalse($test->hasErrors());
- $test = new BulkSearchPartResultsDTO($this->createMock(\App\Entity\Parts\Part::class), [], ['error1']);
- $this->assertTrue($test->hasErrors());
- }
-
- public function testGetErrorCount(): void
- {
- $test = new BulkSearchPartResultsDTO($this->createMock(\App\Entity\Parts\Part::class), [], []);
- $this->assertCount(0, $test->errors);
- $test = new BulkSearchPartResultsDTO($this->createMock(\App\Entity\Parts\Part::class), [], ['error1', 'error2']);
- $this->assertCount(2, $test->errors);
- }
-
- public function testHasResults(): void
- {
- $test = new BulkSearchPartResultsDTO($this->createMock(\App\Entity\Parts\Part::class), [], []);
- $this->assertFalse($test->hasResults());
- $test = new BulkSearchPartResultsDTO($this->createMock(\App\Entity\Parts\Part::class), [ $this->createMock(\App\Services\InfoProviderSystem\DTOs\BulkSearchPartResultDTO::class) ], []);
- $this->assertTrue($test->hasResults());
- }
-
- public function testGetResultCount(): void
- {
- $test = new BulkSearchPartResultsDTO($this->createMock(\App\Entity\Parts\Part::class), [], []);
- $this->assertCount(0, $test->searchResults);
- $test = new BulkSearchPartResultsDTO($this->createMock(\App\Entity\Parts\Part::class), [
- $this->createMock(\App\Services\InfoProviderSystem\DTOs\BulkSearchPartResultDTO::class),
- $this->createMock(\App\Services\InfoProviderSystem\DTOs\BulkSearchPartResultDTO::class)
- ], []);
- $this->assertCount(2, $test->searchResults);
- }
-}
diff --git a/tests/Services/InfoProviderSystem/DTOs/BulkSearchResponseDTOTest.php b/tests/Services/InfoProviderSystem/DTOs/BulkSearchResponseDTOTest.php
deleted file mode 100644
index b4dc0dea..00000000
--- a/tests/Services/InfoProviderSystem/DTOs/BulkSearchResponseDTOTest.php
+++ /dev/null
@@ -1,258 +0,0 @@
-.
- */
-
-namespace App\Tests\Services\InfoProviderSystem\DTOs;
-
-use App\Entity\Parts\Part;
-use App\Services\InfoProviderSystem\DTOs\BulkSearchResponseDTO;
-use App\Services\InfoProviderSystem\DTOs\BulkSearchPartResultDTO;
-use App\Services\InfoProviderSystem\DTOs\BulkSearchPartResultsDTO;
-use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
-use Doctrine\ORM\EntityManagerInterface;
-use PHPUnit\Framework\TestCase;
-use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
-
-class BulkSearchResponseDTOTest extends KernelTestCase
-{
-
- private EntityManagerInterface $entityManager;
-
- private BulkSearchResponseDTO $dummyEmpty;
- private BulkSearchResponseDTO $dummy;
-
- protected function setUp(): void
- {
- self::bootKernel();
- $this->entityManager = self::getContainer()->get(EntityManagerInterface::class);
-
- $this->dummyEmpty = new BulkSearchResponseDTO(partResults: []);
- $this->dummy = new BulkSearchResponseDTO(partResults: [
- new BulkSearchPartResultsDTO(
- part: $this->entityManager->find(Part::class, 1),
- searchResults: [
- new BulkSearchPartResultDTO(
- searchResult: new SearchResultDTO(provider_key: "dummy", provider_id: "1234", name: "Test Part", description: "A part for testing"),
- sourceField: "mpn", sourceKeyword: "1234", priority: 1
- ),
- new BulkSearchPartResultDTO(
- searchResult: new SearchResultDTO(provider_key: "test", provider_id: "test", name: "Test Part2", description: "A part for testing"),
- sourceField: "name", sourceKeyword: "1234",
- localPart: $this->entityManager->find(Part::class, 2), priority: 2,
- ),
- ],
- errors: ['Error 1']
- )
- ]);
- }
-
- public function testSerializationBackAndForthEmpty(): void
- {
- $serialized = $this->dummyEmpty->toSerializableRepresentation();
- //Ensure that it is json_encodable
- $json = json_encode($serialized, JSON_THROW_ON_ERROR);
- $this->assertJson($json);
- $deserialized = BulkSearchResponseDTO::fromSerializableRepresentation(json_decode($json), $this->entityManager);
-
- $this->assertEquals($this->dummyEmpty, $deserialized);
- }
-
- public function testSerializationBackAndForth(): void
- {
- $serialized = $this->dummy->toSerializableRepresentation();
- //Ensure that it is json_encodable
- $this->assertJson(json_encode($serialized, JSON_THROW_ON_ERROR));
- $deserialized = BulkSearchResponseDTO::fromSerializableRepresentation($serialized, $this->entityManager);
-
- $this->assertEquals($this->dummy, $deserialized);
- }
-
- public function testToSerializableRepresentation(): void
- {
- $serialized = $this->dummy->toSerializableRepresentation();
-
- $expected = array (
- 0 =>
- array (
- 'part_id' => 1,
- 'search_results' =>
- array (
- 0 =>
- array (
- 'dto' =>
- array (
- 'provider_key' => 'dummy',
- 'provider_id' => '1234',
- 'name' => 'Test Part',
- 'description' => 'A part for testing',
- 'category' => NULL,
- 'manufacturer' => NULL,
- 'mpn' => NULL,
- 'preview_image_url' => NULL,
- 'manufacturing_status' => NULL,
- 'provider_url' => NULL,
- 'footprint' => NULL,
- ),
- 'source_field' => 'mpn',
- 'source_keyword' => '1234',
- 'localPart' => NULL,
- 'priority' => 1,
- ),
- 1 =>
- array (
- 'dto' =>
- array (
- 'provider_key' => 'test',
- 'provider_id' => 'test',
- 'name' => 'Test Part2',
- 'description' => 'A part for testing',
- 'category' => NULL,
- 'manufacturer' => NULL,
- 'mpn' => NULL,
- 'preview_image_url' => NULL,
- 'manufacturing_status' => NULL,
- 'provider_url' => NULL,
- 'footprint' => NULL,
- ),
- 'source_field' => 'name',
- 'source_keyword' => '1234',
- 'localPart' => 2,
- 'priority' => 2,
- ),
- ),
- 'errors' =>
- array (
- 0 => 'Error 1',
- ),
- ),
- );
-
- $this->assertEquals($expected, $serialized);
- }
-
- public function testFromSerializableRepresentation(): void
- {
- $input = array (
- 0 =>
- array (
- 'part_id' => 1,
- 'search_results' =>
- array (
- 0 =>
- array (
- 'dto' =>
- array (
- 'provider_key' => 'dummy',
- 'provider_id' => '1234',
- 'name' => 'Test Part',
- 'description' => 'A part for testing',
- 'category' => NULL,
- 'manufacturer' => NULL,
- 'mpn' => NULL,
- 'preview_image_url' => NULL,
- 'manufacturing_status' => NULL,
- 'provider_url' => NULL,
- 'footprint' => NULL,
- ),
- 'source_field' => 'mpn',
- 'source_keyword' => '1234',
- 'localPart' => NULL,
- 'priority' => 1,
- ),
- 1 =>
- array (
- 'dto' =>
- array (
- 'provider_key' => 'test',
- 'provider_id' => 'test',
- 'name' => 'Test Part2',
- 'description' => 'A part for testing',
- 'category' => NULL,
- 'manufacturer' => NULL,
- 'mpn' => NULL,
- 'preview_image_url' => NULL,
- 'manufacturing_status' => NULL,
- 'provider_url' => NULL,
- 'footprint' => NULL,
- ),
- 'source_field' => 'name',
- 'source_keyword' => '1234',
- 'localPart' => 2,
- 'priority' => 2,
- ),
- ),
- 'errors' =>
- array (
- 0 => 'Error 1',
- ),
- ),
- );
-
- $deserialized = BulkSearchResponseDTO::fromSerializableRepresentation($input, $this->entityManager);
- $this->assertEquals($this->dummy, $deserialized);
- }
-
- public function testMerge(): void
- {
- $merged = BulkSearchResponseDTO::merge($this->dummy, $this->dummyEmpty);
- $this->assertCount(1, $merged->partResults);
-
- $merged = BulkSearchResponseDTO::merge($this->dummyEmpty, $this->dummyEmpty);
- $this->assertCount(0, $merged->partResults);
-
- $merged = BulkSearchResponseDTO::merge($this->dummy, $this->dummy, $this->dummy);
- $this->assertCount(3, $merged->partResults);
- }
-
- public function testReplaceResultsForPart(): void
- {
- $newPartResults = new BulkSearchPartResultsDTO(
- part: $this->entityManager->find(Part::class, 1),
- searchResults: [
- new BulkSearchPartResultDTO(
- searchResult: new SearchResultDTO(provider_key: "new", provider_id: "new", name: "New Part", description: "A new part"),
- sourceField: "mpn", sourceKeyword: "new", priority: 1
- )
- ],
- errors: ['New Error']
- );
-
- $replaced = $this->dummy->replaceResultsForPart($newPartResults);
- $this->assertCount(1, $replaced->partResults);
- $this->assertSame($newPartResults, $replaced->partResults[0]);
- }
-
- public function testReplaceResultsForPartNotExisting(): void
- {
- $newPartResults = new BulkSearchPartResultsDTO(
- part: $this->entityManager->find(Part::class, 1),
- searchResults: [
- new BulkSearchPartResultDTO(
- searchResult: new SearchResultDTO(provider_key: "new", provider_id: "new", name: "New Part", description: "A new part"),
- sourceField: "mpn", sourceKeyword: "new", priority: 1
- )
- ],
- errors: ['New Error']
- );
-
- $this->expectException(\InvalidArgumentException::class);
-
- $replaced = $this->dummyEmpty->replaceResultsForPart($newPartResults);
- }
-}
diff --git a/tests/Services/InfoProviderSystem/Providers/LCSCProviderTest.php b/tests/Services/InfoProviderSystem/Providers/LCSCProviderTest.php
deleted file mode 100644
index 57527f57..00000000
--- a/tests/Services/InfoProviderSystem/Providers/LCSCProviderTest.php
+++ /dev/null
@@ -1,540 +0,0 @@
-.
- */
-
-declare(strict_types=1);
-
-namespace App\Tests\Services\InfoProviderSystem\Providers;
-
-use App\Services\InfoProviderSystem\DTOs\FileDTO;
-use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
-use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
-use App\Services\InfoProviderSystem\DTOs\PriceDTO;
-use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
-use App\Services\InfoProviderSystem\Providers\LCSCProvider;
-use App\Services\InfoProviderSystem\Providers\ProviderCapabilities;
-use App\Settings\InfoProviderSystem\LCSCSettings;
-use App\Tests\SettingsTestHelper;
-use PHPUnit\Framework\TestCase;
-use Symfony\Component\HttpClient\MockHttpClient;
-use Symfony\Component\HttpClient\Response\MockResponse;
-use Symfony\Contracts\HttpClient\HttpClientInterface;
-
-class LCSCProviderTest extends TestCase
-{
- private LCSCSettings $settings;
- private LCSCProvider $provider;
- private MockHttpClient $httpClient;
-
- protected function setUp(): void
- {
- $this->httpClient = new MockHttpClient();
- $this->settings = SettingsTestHelper::createSettingsDummy(LCSCSettings::class);
- $this->settings->currency = 'USD';
- $this->settings->enabled = true;
- $this->provider = new LCSCProvider($this->httpClient, $this->settings);
- }
-
- public function testGetProviderInfo(): void
- {
- $info = $this->provider->getProviderInfo();
-
- $this->assertIsArray($info);
- $this->assertArrayHasKey('name', $info);
- $this->assertArrayHasKey('description', $info);
- $this->assertArrayHasKey('url', $info);
- $this->assertArrayHasKey('disabled_help', $info);
- $this->assertEquals('LCSC', $info['name']);
- $this->assertEquals('https://www.lcsc.com/', $info['url']);
- }
-
- public function testGetProviderKey(): void
- {
- $this->assertEquals('lcsc', $this->provider->getProviderKey());
- }
-
- public function testIsActiveWhenEnabled(): void
- {
- //Ensure that the settings are enabled
- $this->settings->enabled = true;
- $this->assertTrue($this->provider->isActive());
- }
-
- public function testIsActiveWhenDisabled(): void
- {
- //Ensure that the settings are disabled
- $this->settings->enabled = false;
- $this->assertFalse($this->provider->isActive());
- }
-
- public function testGetCapabilities(): void
- {
- $capabilities = $this->provider->getCapabilities();
-
- $this->assertIsArray($capabilities);
- $this->assertContains(ProviderCapabilities::BASIC, $capabilities);
- $this->assertContains(ProviderCapabilities::PICTURE, $capabilities);
- $this->assertContains(ProviderCapabilities::DATASHEET, $capabilities);
- $this->assertContains(ProviderCapabilities::PRICE, $capabilities);
- $this->assertContains(ProviderCapabilities::FOOTPRINT, $capabilities);
- }
-
- public function testSearchByKeywordWithCCode(): void
- {
- $mockResponse = new MockResponse(json_encode([
- 'result' => [
- 'productCode' => 'C123456',
- 'productModel' => 'Test Component',
- 'productIntroEn' => 'Test description',
- 'brandNameEn' => 'Test Manufacturer',
- 'encapStandard' => '0603',
- 'productImageUrl' => 'https://example.com/image.jpg',
- 'productImages' => ['https://example.com/image1.jpg'],
- 'productPriceList' => [
- ['ladder' => 1, 'productPrice' => '0.10', 'currencySymbol' => 'US$']
- ],
- 'paramVOList' => [
- ['paramNameEn' => 'Resistance', 'paramValueEn' => '1kΞ©']
- ],
- 'pdfUrl' => 'https://example.com/datasheet.pdf',
- 'weight' => 0.001
- ]
- ]));
-
- $this->httpClient->setResponseFactory([$mockResponse]);
-
- $results = $this->provider->searchByKeyword('C123456');
-
- $this->assertIsArray($results);
- $this->assertCount(1, $results);
- $this->assertInstanceOf(PartDetailDTO::class, $results[0]);
- $this->assertEquals('C123456', $results[0]->provider_id);
- $this->assertEquals('Test Component', $results[0]->name);
- }
-
- public function testSearchByKeywordWithRegularTerm(): void
- {
- $mockResponse = new MockResponse(json_encode([
- 'result' => [
- 'productSearchResultVO' => [
- 'productList' => [
- [
- 'productCode' => 'C789012',
- 'productModel' => 'Regular Component',
- 'productIntroEn' => 'Regular description',
- 'brandNameEn' => 'Regular Manufacturer',
- 'encapStandard' => '0805',
- 'productImageUrl' => 'https://example.com/regular.jpg',
- 'productImages' => ['https://example.com/regular1.jpg'],
- 'productPriceList' => [
- ['ladder' => 10, 'productPrice' => '0.08', 'currencySymbol' => 'β¬']
- ],
- 'paramVOList' => [],
- 'pdfUrl' => null,
- 'weight' => null
- ]
- ]
- ]
- ]
- ]));
-
- $this->httpClient->setResponseFactory([$mockResponse]);
-
- $results = $this->provider->searchByKeyword('resistor');
-
- $this->assertIsArray($results);
- $this->assertCount(1, $results);
- $this->assertInstanceOf(PartDetailDTO::class, $results[0]);
- $this->assertEquals('C789012', $results[0]->provider_id);
- $this->assertEquals('Regular Component', $results[0]->name);
- }
-
- public function testSearchByKeywordWithTipProduct(): void
- {
- $mockResponse = new MockResponse(json_encode([
- 'result' => [
- 'productSearchResultVO' => [
- 'productList' => []
- ],
- 'tipProductDetailUrlVO' => [
- 'productCode' => 'C555555'
- ]
- ]
- ]));
-
- $detailResponse = new MockResponse(json_encode([
- 'result' => [
- 'productCode' => 'C555555',
- 'productModel' => 'Tip Component',
- 'productIntroEn' => 'Tip description',
- 'brandNameEn' => 'Tip Manufacturer',
- 'encapStandard' => '1206',
- 'productImageUrl' => null,
- 'productImages' => [],
- 'productPriceList' => [],
- 'paramVOList' => [],
- 'pdfUrl' => null,
- 'weight' => null
- ]
- ]));
-
- $this->httpClient->setResponseFactory([$mockResponse, $detailResponse]);
-
- $results = $this->provider->searchByKeyword('special');
-
- $this->assertIsArray($results);
- $this->assertCount(1, $results);
- $this->assertInstanceOf(PartDetailDTO::class, $results[0]);
- $this->assertEquals('C555555', $results[0]->provider_id);
- $this->assertEquals('Tip Component', $results[0]->name);
- }
-
- public function testSearchByKeywordsBatch(): void
- {
- $mockResponse1 = new MockResponse(json_encode([
- 'result' => [
- 'productCode' => 'C123456',
- 'productModel' => 'Batch Component 1',
- 'productIntroEn' => 'Batch description 1',
- 'brandNameEn' => 'Batch Manufacturer',
- 'encapStandard' => '0603',
- 'productImageUrl' => null,
- 'productImages' => [],
- 'productPriceList' => [],
- 'paramVOList' => [],
- 'pdfUrl' => null,
- 'weight' => null
- ]
- ]));
-
- $mockResponse2 = new MockResponse(json_encode([
- 'result' => [
- 'productSearchResultVO' => [
- 'productList' => [
- [
- 'productCode' => 'C789012',
- 'productModel' => 'Batch Component 2',
- 'productIntroEn' => 'Batch description 2',
- 'brandNameEn' => 'Batch Manufacturer',
- 'encapStandard' => '0805',
- 'productImageUrl' => null,
- 'productImages' => [],
- 'productPriceList' => [],
- 'paramVOList' => [],
- 'pdfUrl' => null,
- 'weight' => null
- ]
- ]
- ]
- ]
- ]));
-
- $this->httpClient->setResponseFactory([$mockResponse1, $mockResponse2]);
-
- $results = $this->provider->searchByKeywordsBatch(['C123456', 'resistor']);
-
- $this->assertIsArray($results);
- $this->assertArrayHasKey('C123456', $results);
- $this->assertArrayHasKey('resistor', $results);
- $this->assertCount(1, $results['C123456']);
- $this->assertCount(1, $results['resistor']);
- $this->assertEquals('C123456', $results['C123456'][0]->provider_id);
- $this->assertEquals('C789012', $results['resistor'][0]->provider_id);
- }
-
- public function testGetDetails(): void
- {
- $mockResponse = new MockResponse(json_encode([
- 'result' => [
- 'productCode' => 'C123456',
- 'productModel' => 'Detailed Component',
- 'productIntroEn' => 'Detailed description',
- 'brandNameEn' => 'Detailed Manufacturer',
- 'encapStandard' => '0603',
- 'productImageUrl' => 'https://example.com/detail.jpg',
- 'productImages' => ['https://example.com/detail1.jpg'],
- 'productPriceList' => [
- ['ladder' => 1, 'productPrice' => '0.10', 'currencySymbol' => 'US$'],
- ['ladder' => 10, 'productPrice' => '0.08', 'currencySymbol' => 'US$']
- ],
- 'paramVOList' => [
- ['paramNameEn' => 'Resistance', 'paramValueEn' => '1kΞ©'],
- ['paramNameEn' => 'Tolerance', 'paramValueEn' => '1%']
- ],
- 'pdfUrl' => 'https://example.com/datasheet.pdf',
- 'weight' => 0.001
- ]
- ]));
-
- $this->httpClient->setResponseFactory([$mockResponse]);
-
- $result = $this->provider->getDetails('C123456');
-
- $this->assertInstanceOf(PartDetailDTO::class, $result);
- $this->assertEquals('C123456', $result->provider_id);
- $this->assertEquals('Detailed Component', $result->name);
- $this->assertEquals('Detailed description', $result->description);
- $this->assertEquals('Detailed Manufacturer', $result->manufacturer);
- $this->assertEquals('0603', $result->footprint);
- $this->assertEquals('https://www.lcsc.com/product-detail/C123456.html', $result->provider_url);
- $this->assertCount(1, $result->images);
- $this->assertCount(2, $result->parameters);
- $this->assertCount(1, $result->vendor_infos);
- $this->assertEquals('0.001', $result->mass);
- }
-
- public function testGetDetailsWithNoResults(): void
- {
- $mockResponse = new MockResponse(json_encode([
- 'result' => [
- 'productSearchResultVO' => [
- 'productList' => []
- ]
- ]
- ]));
-
- $this->httpClient->setResponseFactory([$mockResponse]);
-
- $this->expectException(\RuntimeException::class);
- $this->expectExceptionMessage('No part found with ID INVALID');
-
- $this->provider->getDetails('INVALID');
- }
-
- public function testGetDetailsWithMultipleResults(): void
- {
- $mockResponse = new MockResponse(json_encode([
- 'result' => [
- 'productSearchResultVO' => [
- 'productList' => [
- [
- 'productCode' => 'C123456',
- 'productModel' => 'Component 1',
- 'productIntroEn' => 'Description 1',
- 'brandNameEn' => 'Manufacturer 1',
- 'encapStandard' => '0603',
- 'productImageUrl' => null,
- 'productImages' => [],
- 'productPriceList' => [],
- 'paramVOList' => [],
- 'pdfUrl' => null,
- 'weight' => null
- ],
- [
- 'productCode' => 'C789012',
- 'productModel' => 'Component 2',
- 'productIntroEn' => 'Description 2',
- 'brandNameEn' => 'Manufacturer 2',
- 'encapStandard' => '0805',
- 'productImageUrl' => null,
- 'productImages' => [],
- 'productPriceList' => [],
- 'paramVOList' => [],
- 'pdfUrl' => null,
- 'weight' => null
- ]
- ]
- ]
- ]
- ]));
-
- $this->httpClient->setResponseFactory([$mockResponse]);
-
- $this->expectException(\RuntimeException::class);
- $this->expectExceptionMessage('Multiple parts found with ID ambiguous');
-
- $this->provider->getDetails('ambiguous');
- }
-
- public function testSanitizeFieldPrivateMethod(): void
- {
- $reflection = new \ReflectionClass($this->provider);
- $method = $reflection->getMethod('sanitizeField');
- $method->setAccessible(true);
-
- $this->assertNull($method->invokeArgs($this->provider, [null]));
- $this->assertEquals('Clean text', $method->invokeArgs($this->provider, ['Clean text']));
- $this->assertEquals('Text without tags', $method->invokeArgs($this->provider, ['Text without tags ']));
- }
-
- public function testGetUsedCurrencyPrivateMethod(): void
- {
- $reflection = new \ReflectionClass($this->provider);
- $method = $reflection->getMethod('getUsedCurrency');
- $method->setAccessible(true);
-
- $this->assertEquals('USD', $method->invokeArgs($this->provider, ['US$']));
- $this->assertEquals('USD', $method->invokeArgs($this->provider, ['$']));
- $this->assertEquals('EUR', $method->invokeArgs($this->provider, ['β¬']));
- $this->assertEquals('GBP', $method->invokeArgs($this->provider, ['Β£']));
- $this->assertEquals('USD', $method->invokeArgs($this->provider, ['UNKNOWN'])); // fallback to configured currency
- }
-
- public function testGetProductShortURLPrivateMethod(): void
- {
- $reflection = new \ReflectionClass($this->provider);
- $method = $reflection->getMethod('getProductShortURL');
- $method->setAccessible(true);
-
- $result = $method->invokeArgs($this->provider, ['C123456']);
- $this->assertEquals('https://www.lcsc.com/product-detail/C123456.html', $result);
- }
-
- public function testGetProductDatasheetsPrivateMethod(): void
- {
- $reflection = new \ReflectionClass($this->provider);
- $method = $reflection->getMethod('getProductDatasheets');
- $method->setAccessible(true);
-
- $result = $method->invokeArgs($this->provider, [null]);
- $this->assertIsArray($result);
- $this->assertEmpty($result);
-
- $result = $method->invokeArgs($this->provider, ['https://example.com/datasheet.pdf']);
- $this->assertIsArray($result);
- $this->assertCount(1, $result);
- $this->assertInstanceOf(FileDTO::class, $result[0]);
- }
-
- public function testGetProductImagesPrivateMethod(): void
- {
- $reflection = new \ReflectionClass($this->provider);
- $method = $reflection->getMethod('getProductImages');
- $method->setAccessible(true);
-
- $result = $method->invokeArgs($this->provider, [null]);
- $this->assertIsArray($result);
- $this->assertEmpty($result);
-
- $result = $method->invokeArgs($this->provider, [['https://example.com/image1.jpg', 'https://example.com/image2.jpg']]);
- $this->assertIsArray($result);
- $this->assertCount(2, $result);
- $this->assertInstanceOf(FileDTO::class, $result[0]);
- $this->assertInstanceOf(FileDTO::class, $result[1]);
- }
-
- public function testAttributesToParametersPrivateMethod(): void
- {
- $reflection = new \ReflectionClass($this->provider);
- $method = $reflection->getMethod('attributesToParameters');
- $method->setAccessible(true);
-
- $attributes = [
- ['paramNameEn' => 'Resistance', 'paramValueEn' => '1kΞ©'],
- ['paramNameEn' => 'Tolerance', 'paramValueEn' => '1%'],
- ['paramNameEn' => 'Empty', 'paramValueEn' => ''],
- ['paramNameEn' => 'Dash', 'paramValueEn' => '-']
- ];
-
- $result = $method->invokeArgs($this->provider, [$attributes]);
- $this->assertIsArray($result);
- $this->assertCount(2, $result); // Only non-empty values
- $this->assertInstanceOf(ParameterDTO::class, $result[0]);
- $this->assertInstanceOf(ParameterDTO::class, $result[1]);
- }
-
- public function testPricesToVendorInfoPrivateMethod(): void
- {
- $reflection = new \ReflectionClass($this->provider);
- $method = $reflection->getMethod('pricesToVendorInfo');
- $method->setAccessible(true);
-
- $prices = [
- ['ladder' => 1, 'productPrice' => '0.10', 'currencySymbol' => 'US$'],
- ['ladder' => 10, 'productPrice' => '0.08', 'currencySymbol' => 'US$']
- ];
-
- $result = $method->invokeArgs($this->provider, ['C123456', 'https://example.com', $prices]);
- $this->assertIsArray($result);
- $this->assertCount(1, $result);
- $this->assertInstanceOf(PurchaseInfoDTO::class, $result[0]);
- $this->assertEquals('LCSC', $result[0]->distributor_name);
- $this->assertEquals('C123456', $result[0]->order_number);
- $this->assertCount(2, $result[0]->prices);
- }
-
- public function testCategoryBuilding(): void
- {
- $mockResponse = new MockResponse(json_encode([
- 'result' => [
- 'productCode' => 'C123456',
- 'productModel' => 'Test Component',
- 'productIntroEn' => 'Test description',
- 'brandNameEn' => 'Test Manufacturer',
- 'parentCatalogName' => 'Electronic Components',
- 'catalogName' => 'Resistors (SMT)',
- 'encapStandard' => '0603',
- 'productImageUrl' => null,
- 'productImages' => [],
- 'productPriceList' => [],
- 'paramVOList' => [],
- 'pdfUrl' => null,
- 'weight' => null
- ]
- ]));
-
- $this->httpClient->setResponseFactory([$mockResponse]);
-
- $result = $this->provider->getDetails('C123456');
- $this->assertEquals('Electronic Components -> Resistors (SMT)', $result->category);
- }
-
- public function testEmptyFootprintHandling(): void
- {
- $mockResponse = new MockResponse(json_encode([
- 'result' => [
- 'productCode' => 'C123456',
- 'productModel' => 'Test Component',
- 'productIntroEn' => 'Test description',
- 'brandNameEn' => 'Test Manufacturer',
- 'encapStandard' => '-',
- 'productImageUrl' => null,
- 'productImages' => [],
- 'productPriceList' => [],
- 'paramVOList' => [],
- 'pdfUrl' => null,
- 'weight' => null
- ]
- ]));
-
- $this->httpClient->setResponseFactory([$mockResponse]);
-
- $result = $this->provider->getDetails('C123456');
- $this->assertNull($result->footprint);
- }
-
- public function testSearchByKeywordsBatchWithEmptyKeywords(): void
- {
- $result = $this->provider->searchByKeywordsBatch([]);
- $this->assertIsArray($result);
- $this->assertEmpty($result);
- }
-
- public function testSearchByKeywordsBatchWithException(): void
- {
- $mockResponse = new MockResponse('', ['http_code' => 500]);
- $this->httpClient->setResponseFactory([$mockResponse]);
-
- $results = $this->provider->searchByKeywordsBatch(['error']);
- $this->assertIsArray($results);
- $this->assertArrayHasKey('error', $results);
- $this->assertEmpty($results['error']);
- }
-}
diff --git a/tests/Services/Parts/PartsTableActionHandlerTest.php b/tests/Services/Parts/PartsTableActionHandlerTest.php
deleted file mode 100644
index c5105cd7..00000000
--- a/tests/Services/Parts/PartsTableActionHandlerTest.php
+++ /dev/null
@@ -1,62 +0,0 @@
-.
- */
-namespace App\Tests\Services\Parts;
-
-use App\Entity\Parts\Part;
-use App\Services\Parts\PartsTableActionHandler;
-use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
-use Symfony\Component\HttpFoundation\RedirectResponse;
-
-class PartsTableActionHandlerTest extends WebTestCase
-{
- private PartsTableActionHandler $service;
-
- protected function setUp(): void
- {
- self::bootKernel();
- $this->service = self::getContainer()->get(PartsTableActionHandler::class);
- }
-
- public function testExportActionsRedirectToExportController(): void
- {
- // Mock a Part entity with required properties
- $part = $this->createMock(Part::class);
- $part->method('getId')->willReturn(1);
- $part->method('getName')->willReturn('Test Part');
-
- $selected_parts = [$part];
-
- // Test each export format, focusing on our new xlsx format
- $formats = ['json', 'csv', 'xml', 'yaml', 'xlsx'];
-
- foreach ($formats as $format) {
- $action = "export_{$format}";
- $result = $this->service->handleAction($action, $selected_parts, 1, '/test');
-
- $this->assertInstanceOf(RedirectResponse::class, $result);
- $this->assertStringContainsString('parts/export', $result->getTargetUrl());
- $this->assertStringContainsString("format={$format}", $result->getTargetUrl());
- }
- }
-
-}
\ No newline at end of file
diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf
index b109eb6f..be1e6348 100644
--- a/translations/messages.en.xlf
+++ b/translations/messages.en.xlf
@@ -8925,12 +8925,6 @@ Element 1 -> Element 1.2
Edit part
-
-
- part_list.action.scrollable_hint
- Scroll to see all actions
-
-
part_list.action.action.title
@@ -9321,84 +9315,6 @@ Element 1 -> Element 1.2
Attachment name
-
-
- filter.bulk_import_job.label
- Bulk Import Job
-
-
-
-
- filter.bulk_import_job.job_status
- Job Status
-
-
-
-
- filter.bulk_import_job.part_status_in_job
- Part Status in Job
-
-
-
-
- filter.bulk_import_job.status.any
- Any Status
-
-
-
-
- filter.bulk_import_job.status.pending
- Pending
-
-
-
-
- filter.bulk_import_job.status.in_progress
- In Progress
-
-
-
-
- filter.bulk_import_job.status.completed
- Completed
-
-
-
-
- filter.bulk_import_job.status.stopped
- Stopped
-
-
-
-
- filter.bulk_import_job.status.failed
- Failed
-
-
-
-
- filter.bulk_import_job.part_status.any
- Any Part Status
-
-
-
-
- filter.bulk_import_job.part_status.pending
- Pending
-
-
-
-
- filter.bulk_import_job.part_status.completed
- Completed
-
-
-
-
- filter.bulk_import_job.part_status.skipped
- Skipped
-
-
filter.choice_constraint.operator.ANY
@@ -10971,12 +10887,6 @@ Element 1 -> Element 1.2
Export to XML
-
-
- part_list.action.export_xlsx
- Export to Excel
-
-
parts.import.title
@@ -12284,7 +12194,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g
info_providers.search.no_results
- No results found
+ No results found at the selected providers! Check your search term or try to choose additional providers.
@@ -12404,7 +12314,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g
settings.ips.element14.apiKey.help
- https://partner.element14.com/.]]>
+ You can register for an API key on <a href="https://partner.element14.com/">https://partner.element14.com/</a>.
@@ -12416,7 +12326,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g
settings.ips.element14.storeId.help
- here for a list of valid domains.]]>
+ The store domain to retrieve the data from. This decides the language and currency of results. See <a href="https://partner.element14.com/docs/Product_Search_API_REST__Description">here</a> for a list of valid domains.
@@ -12434,7 +12344,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g
settings.ips.tme.token.help
- https://developers.tme.eu/en/.]]>
+ You can get an API token and secret on <a href="https://developers.tme.eu/en/">https://developers.tme.eu/en/</a>.
@@ -12482,7 +12392,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g
settings.ips.mouser.apiKey.help
- https://eu.mouser.com/api-hub/.]]>
+ You can register for an API key on <a href="https://eu.mouser.com/api-hub/">https://eu.mouser.com/api-hub/</a>.
@@ -12560,7 +12470,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g
settings.system.attachments
-
+ Attachments & Files
@@ -12584,7 +12494,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g
settings.system.attachments.allowDownloads.help
- Attention: This can be a security issue, as it might allow users to access intranet ressources via Part-DB!]]>
+ With this option users can download external files into Part-DB by providing an URL. <b>Attention: This can be a security issue, as it might allow users to access intranet ressources via Part-DB!</b>
@@ -12758,8 +12668,8 @@ Please note, that you can not impersonate a disabled user. If you try you will g
settings.system.localization.base_currency_description
- Please note that the currencies are not converted, when changing this value. So changing the default currency after you already added price information, will result in wrong prices!]]>
+ The currency that is used to store price information and exchange rates in. This currency is assumed, when no currency is set for a price information.
+<b>Please note that the currencies are not converted, when changing this value. So changing the default currency after you already added price information, will result in wrong prices!</b>
@@ -12789,7 +12699,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g
settings.misc.kicad_eda.category_depth.help
- 0 to show more levels. Set to -1, to show all parts of Part-DB inside a sigle cnategory in KiCad.]]>
+ This value determines the depth of the category tree, that is visible inside KiCad. 0 means that only the top level categories are visible. Set to a value > 0 to show more levels. Set to -1, to show all parts of Part-DB inside a sigle cnategory in KiCad.
@@ -12807,7 +12717,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g
settings.behavior.sidebar.items.help
-
+ The menus which appear at the sidebar by default. Order of items can be changed via drag & drop.
@@ -12855,7 +12765,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g
settings.behavior.table.parts_default_columns.help
-
+ The columns to show by default in part tables. Order of items can be changed via drag & drop.
@@ -12909,7 +12819,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g
settings.ips.oemsecrets.sortMode.M
-
+ Completeness & Manufacturer name
@@ -13572,803 +13482,5 @@ Please note, that you can not impersonate a disabled user. If you try you will g
Preview image min width (px)
-
-
- info_providers.bulk_import.step1.title
- Bulk Info Provider Import - Step 1
-
-
-
-
- info_providers.bulk_import.parts_selected
- parts selected
-
-
-
-
- info_providers.bulk_import.step1.global_mapping_description
- Configure field mappings that will be applied to all selected parts. For example: "MPN β LCSC + Mouser" means search LCSC and Mouser providers using each part's MPN field.
-
-
-
-
- info_providers.bulk_import.selected_parts
- Selected Parts
-
-
-
-
- info_providers.bulk_import.field_mappings
- Field Mappings
-
-
-
-
- info_providers.bulk_import.field_mappings_help
- Define which part fields to search with which info providers. Multiple mappings will be combined.
-
-
-
-
- info_providers.bulk_import.add_mapping
- Add Mapping
-
-
-
-
- info_providers.bulk_import.search_results.title
- Search Results
-
-
-
-
- info_providers.bulk_import.errors
- errors
-
-
-
-
- info_providers.bulk_import.results_found
- %count% results found
-
-
-
-
- info_providers.bulk_import.source_field
- Source Field
-
-
-
-
- info_providers.bulk_import.create_part
- Create Part
-
-
-
-
- info_providers.bulk_import.view_existing
- View Existing
-
-
-
-
- info_providers.bulk_search.search_field
- Search Field
-
-
-
-
- info_providers.bulk_search.providers
- Info Providers
-
-
-
-
- info_providers.bulk_import.actions.label
- Actions
-
-
-
-
- info_providers.bulk_search.providers.help
- Select which info providers to search when parts have this field.
-
-
-
-
- info_providers.bulk_search.submit
- Search All Parts
-
-
-
-
- info_providers.bulk_search.field.select
- Select a field to search by
-
-
-
-
- info_providers.bulk_search.field.mpn
- Manufacturer Part Number (MPN)
-
-
-
-
- info_providers.bulk_search.field.name
- Part Name
-
-
-
-
- part_list.action.action.info_provider
- Info Provider
-
-
-
-
- part_list.action.bulk_info_provider_import
- Bulk Info Provider Import
-
-
-
-
- info_providers.bulk_import.clear_selections
- Clear All Selections
-
-
-
-
- info_providers.bulk_import.clear_row
- Clear this row's selections
-
-
-
-
- info_providers.bulk_import.step1.spn_recommendation
- SPN (Supplier Part Number) is recommended for better results. Add a mapping for each supplier to use their SPNs.
-
-
-
-
- info_providers.bulk_import.update_part
- Update Part
-
-
-
-
- info_providers.bulk_import.prefetch_details
- Prefetch Details
-
-
-
-
- info_providers.bulk_import.prefetch_details_help
- Prefetch details for all results. This will take longer, but will speed up workflow for updating parts.
-
-
-
-
- info_providers.bulk_import.step2.title
- Bulk import from info providers
-
-
-
-
- info_providers.bulk_import.step2.card_title
- Bulk import for %count% parts - %date%
-
-
-
-
- info_providers.bulk_import.parts
- parts
-
-
-
-
- info_providers.bulk_import.results
- results
-
-
-
-
- info_providers.bulk_import.created_at
- Created at
-
-
-
-
- info_providers.bulk_import.status.in_progress
- In Progress
-
-
-
-
- info_providers.bulk_import.status.completed
- Completed
-
-
-
-
- info_providers.bulk_import.status.failed
- Failed
-
-
-
-
- info_providers.bulk_import.table.name
- Name
-
-
-
-
- info_providers.bulk_import.table.description
- Description
-
-
-
-
- info_providers.bulk_import.table.manufacturer
- Manufacturer
-
-
-
-
- info_providers.bulk_import.table.provider
- Provider
-
-
-
-
- info_providers.bulk_import.table.source_field
- Source Field
-
-
-
-
- info_providers.bulk_import.table.action
- Action
-
-
-
-
- info_providers.bulk_import.action.select
- Select
-
-
-
-
- info_providers.bulk_import.action.deselect
- Deselect
-
-
-
-
- info_providers.bulk_import.action.view_details
- View Details
-
-
-
-
- info_providers.bulk_import.no_results
- No results found
-
-
-
-
- info_providers.bulk_import.processing
- Processing...
-
-
-
-
- info_providers.bulk_import.error
- Error occurred during import
-
-
-
-
- info_providers.bulk_import.success
- Import completed successfully
-
-
-
-
- info_providers.bulk_import.partial_success
- Import completed with some errors
-
-
-
-
- info_providers.bulk_import.retry
- Retry
-
-
-
-
- info_providers.bulk_import.cancel
- Cancel
-
-
-
-
- info_providers.bulk_import.confirm
- Confirm Import
-
-
-
-
- info_providers.bulk_import.back
- Back
-
-
-
-
- info_providers.bulk_import.next
- Next
-
-
-
-
- info_providers.bulk_import.finish
- Finish
-
-
-
-
- info_providers.bulk_import.progress
- Progress:
-
-
-
-
- info_providers.bulk_import.time_remaining
- Estimated time remaining: %time%
-
-
-
-
- info_providers.bulk_import.details_modal.title
- Part Details
-
-
-
-
- info_providers.bulk_import.details_modal.close
- Close
-
-
-
-
- info_providers.bulk_import.details_modal.select_this_part
- Select This Part
-
-
-
-
- info_providers.bulk_import.status.pending
- Pending
-
-
-
-
- info_providers.bulk_import.completed
- completed
-
-
-
-
- info_providers.bulk_import.skipped
- skipped
-
-
-
-
- info_providers.bulk_import.mark_completed
- Mark Completed
-
-
-
-
- info_providers.bulk_import.mark_skipped
- Mark Skipped
-
-
-
-
- info_providers.bulk_import.mark_pending
- Mark Pending
-
-
-
-
- info_providers.bulk_import.skip_reason
- Skip reason
-
-
-
-
- info_providers.bulk_import.editing_part
- Editing part as part of bulk import
-
-
-
-
- info_providers.bulk_import.complete
- Complete
-
-
-
-
- info_providers.bulk_import.existing_jobs
- Existing Jobs
-
-
-
-
- info_providers.bulk_import.job_name
- Job Name
-
-
-
-
- info_providers.bulk_import.parts_count
- Parts Count
-
-
-
-
- info_providers.bulk_import.results_count
- Results Count
-
-
-
-
- info_providers.bulk_import.progress_label
- Progress: %current%/%total%
-
-
-
-
- info_providers.bulk_import.manage_jobs
- Manage Bulk Import Jobs
-
-
-
-
- info_providers.bulk_import.view_results
- View Results
-
-
-
-
- info_providers.bulk_import.status
- Status
-
-
-
-
- info_providers.bulk_import.manage_jobs_description
- View and manage all your bulk import jobs. To create a new job, select parts and click "Bulk import from info providers".
-
-
-
-
- info_providers.bulk_import.no_jobs_found
- No bulk import jobs found.
-
-
-
-
- info_providers.bulk_import.create_first_job
- Create your first bulk import job by selecting multiple parts in a part table and select the "Bulk info provider import" option.
-
-
-
-
- info_providers.bulk_import.confirm_delete_job
- Are you sure you want to delete this job?
-
-
-
-
- info_providers.bulk_import.job_name_template
- Bulk import for %count% parts
-
-
-
-
- info_providers.bulk_import.step2.instructions.title
- How to use bulk import
-
-
-
-
- info_providers.bulk_import.step2.instructions.description
- Follow these steps to efficiently update your parts:
-
-
-
-
- info_providers.bulk_import.step2.instructions.step1
- Click "Update Part" to edit a part with the supplier data
-
-
-
-
- info_providers.bulk_import.step2.instructions.step2
- Review and modify the part information as needed. Note: You need to click "Save" twice to save the changes.
-
-
-
-
- info_providers.bulk_import.step2.instructions.step3
- Click "Complete" to mark the part as done and return to this overview
-
-
-
-
- info_providers.bulk_import.created_by
- Created By
-
-
-
-
- info_providers.bulk_import.completed_at
- Completed At
-
-
-
-
- info_providers.bulk_import.action.label
- Action
-
-
-
-
- info_providers.bulk_import.action.delete
- Delete
-
-
-
-
- info_providers.bulk_import.status.active
- Active
-
-
-
-
- info_providers.bulk_import.progress.title
- Progress
-
-
-
-
- info_providers.bulk_import.progress.completed_text
- %completed% / %total% completed
-
-
-
-
- info_providers.bulk_import.error.deleting_job
- Error deleting job
-
-
-
-
- info_providers.bulk_import.error.unknown
- Unknown error
-
-
-
-
- info_providers.bulk_import.error.deleting_job_with_details
- Error deleting job: %error%
-
-
-
-
- info_providers.bulk_import.status.stopped
- Stopped
-
-
-
-
- info_providers.bulk_import.action.stop
- Stop
-
-
-
-
- info_providers.bulk_import.confirm_stop_job
- Are you sure you want to stop this job?
-
-
-
-
- part.filter.in_bulk_import_job
- In Bulk Import Job
-
-
-
-
- part.filter.in_bulk_import_job.yes
- Yes
-
-
-
-
- part.filter.in_bulk_import_job.no
- No
-
-
-
-
- part.filter.bulk_import_job_status
- Bulk Import Job Status
-
-
-
-
- part.filter.bulk_import_part_status
- Bulk Import Part Status
-
-
-
-
- part.edit.tab.bulk_import
- Bulk Import Job
-
-
-
-
- bulk_import.status.pending
- Pending
-
-
-
-
- bulk_import.status.in_progress
- In Progress
-
-
-
-
- bulk_import.status.completed
- Completed
-
-
-
-
- bulk_import.status.stopped
- Stopped
-
-
-
-
- bulk_import.status.failed
- Failed
-
-
-
-
- bulk_import.part_status.pending
- Pending
-
-
-
-
- bulk_import.part_status.completed
- Completed
-
-
-
-
- bulk_import.part_status.skipped
- Skipped
-
-
-
-
- bulk_import.part_status.failed
- Failed
-
-
-
-
- filter.operator
- Operator
-
-
-
-
- bulk_info_provider_import_job.label
- Bulk info provider import
-
-
-
-
- bulk_info_provider_import_job_part.label
- Bulk Import Job Part
-
-
-
-
- info_providers.bulk_search.priority
- Priority
-
-
-
-
- info_providers.bulk_search.priority.help
- Lower numbers = higher priority. Same priority = combine results. Different priorities = try highest first, fallback if no results.
-
-
-
-
- info_providers.bulk_import.priority_system.title
- Priority System
-
-
-
-
- info_providers.bulk_import.priority_system.description
- Lower numbers = higher priority. Same priority = combine results. Different priorities = try highest first, fallback if no results.
-
-
-
-
- info_providers.bulk_import.priority_system.example
- Example: Priority 1: "LCSC SPN β LCSC", Priority 2: "MPN β LCSC + Mouser", Priority 3: "Name β All providers"
-
-
-
-
- info_providers.bulk_import.search.submit
- Search Providers
-
-
-
-
- info_providers.bulk_import.searching
- Searching
-
-
-
-
- info_providers.bulk_import.research.title
- Research Parts
-
-
-
-
- info_providers.bulk_import.research.description
- Re-search for parts using updated information (e.g., new MPNs). Uses the same field mappings as the original search.
-
-
-
-
- info_providers.bulk_import.research.all_pending
- Research All Pending Parts
-
-
-
-
- info_providers.bulk_import.research.part
- Research
-
-
-
-
- info_providers.bulk_import.research.part_tooltip
- Research this part with updated information
-
-
-
-
- info_providers.bulk_import.max_mappings_reached
- Maximum number of mappings reached
-
-