- ArticleTypePlatformConfig: fulfillmentPolicyId, paymentPolicyId,
returnPolicyId, merchantLocationKey (all nullable)
- EbayAccountApiClient: fetches Fulfillment/Payment/Return policies
from eBay Account API (/sell/account/v1)
- EbayInventoryApiClient: adds getLocations()
- EbayPolicyProvider: aggregates choices with 5 min cache; returns
empty array on API failure so the form degrades to TextField
- EbayAdapter: reads real ArticleTypePlatformConfig (category ID no
longer hardcoded), passes listingPolicies + merchantLocationKey
into createOffer() when set
- EbayArticleTypePlatformConfigCrudController: live policy dropdowns
from EbayPolicyProvider; fallback to TextField with help text
- DashboardController: eBay subMenu with Kategorie-Konfigurationen
- 7 new unit tests for EbayAdapter policy scenarios
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces a proper key-value table (article_type_ebay_mappings) that
explicitly maps each eBay aspect name to either an Article field
(manufacturer, modelNumber, …) or an AttributeDefinition, with a
required flag per mapping entry.
- New entity ArticleTypeEbayMapping with SOURCE_ARTICLE_FIELD / SOURCE_ATTRIBUTE
- ArticleType gains OneToMany ebayMappings collection with upsertEbayMapping()
- EbayAdapter.buildAspects() reads from the mapping table instead of implicit name-matching
- Import controller persists mappings via upsertEbayMapping() and syncs required attribute assignments
- Template shows active mappings card and article_field action option
- Migration 20260520100000 creates the new table, drops old JSON column
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds articleTypeName as a mappable article field — Article.getArticleTypeName()
proxies to articleType.getName(). 'Produktart' auto-detects via alias, so the
import UI pre-selects this mapping and the eBay listing gets the type name as
the Produktart aspect value automatically.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds an 'Artikelfeld' action in the aspect import UI alongside skip/match/create.
Aspects like 'Marke' and 'Herstellernummer' auto-detect to manufacturer/modelNumber
via ARTICLE_FIELD_ALIASES. Mappings are persisted as a JSON column on ArticleType.
EbayAdapter.buildAspects() now reads these mappings and populates them from the
article's direct fields when building eBay listing aspects.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Plain redirectToRoute() bypasses EasyAdmin's AdminRouterSubscriber, so
the admin context (ea.i18n etc.) was never set and the EasyAdmin layout
threw "Impossible to access i18n on null". Using AdminUrlGenerator wraps
the redirect URL in the EasyAdmin routing layer, keeping context alive.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds live category search so users don't need to know eBay category IDs.
Typing in the search box hits /admin/ebay/category-search, shows name +
breadcrumb path, and auto-saves the selection to the ArticleType on pick.
Aspect import table now only renders after a category is set.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ArticleType gains ebayCategoryId (migration 20260520080000).
New admin action "Import eBay Aspects" on ArticleType list and detail:
- Fetches aspects via EbayTaxonomyService (cached 7d)
- Sorts: Required → Recommended → Optional
- Auto-matches by case-insensitive name to existing AttributeDefinitions
- Pre-selects "Create new" for required/recommended with no match
- Pre-selects "Skip" for optional with no match
- Already-assigned definitions highlighted green
- Per-row: override to Skip / Match existing / Create new
- Type auto-detected: Select (≤30 eBay values) or String (freetext)
- User can override type in create form
- Required checkbox pre-checked for eBay-required aspects
- "All → Create" / "All → Skip" bulk buttons
- On submit: creates new AttributeDefinitions, links all to ArticleType,
deduplicates, calls applyAttributeAssignments(), flushes
PHPStan level 9 clean throughout.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Frappe ERP:
- findExistingCustomer() on FrappeErpAdapter — two-step name+address lookup
- FrappeHttpClient: add put() method; switch invoice submit to PUT docstatus=1 (Frappe v16)
- buildItemDescription() uses specsText + inventory number + serial number
- Integration tests: find Simon Kühn, create real 1337€ invoice, cancel+delete in tearDown
- FRAPPE_GENERIC_ITEM_CODE=SKU002 added to .env.local and bin/test-integration
Pipeline — model cache:
- PhotoUploadHandler: after vision, check DB for existing article with same modelNumber
- On match: copy ebayTitle/ebayDescription/specsText/attributes, skip specs+JSON+eBay steps
- DraftArticleHandler: apply model_match data and mark job complete directly
- ArticleRepository: findCompletedByModelNumber() query
Pipeline — specs by article type:
- SpecsResearchAgent: accept attributeFields list, format as bullet list in {{fields}} var
- SpecsResearchHandler: derive attribute names from ArticleType, pass to agent
- SpecsResearchMessage: add attributeFields param
- Prompt migration: replace hardcoded laptop spec list with {{fields}} placeholder
Article:
- specsText field (nullable text column + migration)
- stock field visible on index and editable in CRUD form
- addAttributeValue()/removeAttributeValue() adder-remover pair for Symfony form binding
- AttributeValue::getArticle() getter
- AttributeValueFormType: detect required attributes from ArticleType assignments, set required=true
- ManualIngestType: add stock/quantity field (default 1, min 1)
Users / ACL:
- PermissionVoter: define named permission constants + allPermissions()
- User: getGrantedPermissions()/setGrantedPermissions() helpers
- UserCrudController: permissions checkbox group on edit form
UI / assets:
- public/css/admin/custom.css: red asterisk for required fields
- DashboardController: register custom CSS
Infra:
- PipelineJobFailureListener: mark job failed (with real error) when Messenger exhausts retries
- doctrine.yaml: exclude app.inventory_seq from schema diff
- ErpAdapterInterface: add findExistingCustomer()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Photos field moved to first position. ID field removed entirely. eBay
description on detail uses a custom template that renders raw HTML in a
plain div (no span/title wrapper, no overflow constraints). Form view
keeps the textarea editor.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TextField rejects Collection values before formatValue runs. Switching to
the generic Field avoids the type check. ebayDescription now renders its
HTML tags in the detail view instead of showing raw markup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
FK on attribute_values.attribute_definition_id now uses ON DELETE CASCADE
so deleting an AttributeDefinition also clears its values from all
articles. Admin delete action shows a confirmation warning.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove all hardcoded defaults from PromptTemplateService — the DB record
is now mandatory and render() throws if a key is missing. Admin UI
disables new/delete and makes the key field read-only so system prompts
cannot be renamed or removed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds model_name VARCHAR(255) column to app.articles, exposes it in the
admin CRUD form (optional, hidden on index), and adds translations for
both EN and DE.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Ingest page redesigned: camera modal (getUserMedia + capture fallback
for mobile), mandatory search photo with client-side validation,
optional extra photos grid with per-photo remove button
- Camera modal: live video preview, capture → preview → confirm/retake
flow; stops stream on modal close; falls back to native file picker
if getUserMedia is unavailable
- ManualIngestController: uploads extra photos via PhotoService::uploadRaw(),
stores [{storagePathId, filename}] in job inputData as extraPhotos
- PhotoService::attachExtra(): attach already-stored file to an article
by StoragePath ID + filename
- DraftArticleHandler: after creating the article, attaches extra photos
in sort order; errors are best-effort (pipeline not aborted)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add secured GET /admin/photos/{filename} to serve files from var/uploads/
- Add POST /api/articles/{id}/photos/sort for drag-and-drop reordering
- Add PhotoService::reorder() to persist new sort positions
- Add photo gallery field (onlyOnDetail) using custom Twig template:
- Drag-and-drop reorder via SortableJS CDN
- Click or drop to upload new photos
- Set main (★) and delete per photo
- Main photo highlighted with blue border + badge
- Add field.photos translation key (EN/DE)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add hasActiveJobForArticle() to check for queued/processing jobs.
The displayIf closure hides the action while a job is running.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add Translation entity (locale/domain/key/value, unique on all three)
- Add TranslationRepositoryInterface + DoctrineTranslationRepository
- Add DatabaseTranslator decorator (#[AsDecorator]) that checks DB first
and falls back to YAML files; clears per-request cache on setLocale()
- Add TranslationCrudController with locale/domain filters and read-only
key/locale/domain on edit to prevent accidental renames
- Add "Übersetzungen / Translations" menu entry in DashboardController
- Migration 20260520000000: create app.translations table
- Migration 20260520010000: seed from admin.en.yaml + admin.de.yaml (204 rows)
- Flatten admin.de.yaml to dot-notation; add new keys for translation CRUD
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add LocaleSubscriber: detects browser language, honours session override (priority 20)
- Add LocaleSwitchController: stores locale in session, linked from user menu
- Add admin.en.yaml / admin.de.yaml translation files (95 keys each)
- Wire translation fallback to EN in config/packages/translation.yaml
- Replace all hard-coded strings in CRUD controllers with TranslatableMessage
- Inject TranslatorInterface into DashboardController, ArticleCrudController,
AIPipelineJobCrudController and PipelineStreamController; add locale switcher
links (English / Deutsch) to the user menu
- Add confirmation dialog to "Re-run AI" and "Retry" pipeline actions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds real-time toast notifications to all admin pages for AI pipeline
job progress. The browser subscribes to an SSE endpoint
(GET /admin/pipeline/events) which polls the DB every 2 seconds and
emits events whenever a job's step or status changes.
- AIPipelineJob gains updatedAt (migration 20260519020000), bumped on
every state-change method, with an index for efficient polling
- AIPipelineJobRepositoryInterface/Doctrine get findUpdatedSince()
- PipelineStreamController streams SSE with per-connection dedup and
auto-reconnect (retry: 3000); streams for 90 s then signals reconnect
- pipeline-notifications.js handles EventSource, shows colour-coded
toasts (queued/processing/completed/failed/needs_review) and is loaded
globally via DashboardController::configureAssets()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All agent prompts are now stored in app.prompt_templates (migration 20260519000000)
and editable by admins via the new AI Prompts CRUD page. If no DB entry exists
for a key the hardcoded default is used automatically as fallback.
PromptTemplateService renders templates with {{variable}} substitution.
All four agents (SpecsResearch, JsonCoding, EbayText, OllamaVision) use the service.
SpecsResearchAgent now receives the articleType name (e.g. "Laptop") so the
specs prompt is scoped to the correct device category instead of being generic.
SpecsResearchHandler loads the ArticleType from the repository for this purpose.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ManualIngestController: photo upload form that starts a new pipeline job
- AiStatusController: shows active backend config and runs live connectivity tests
- PipelineArchiveCrudController: read-only view of completed/failed jobs
- ManualIngestType / AttributeValueFormType: form types for ingest and attribute editing
- AiConfigService: encapsulates backend info and test methods for the status page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Retry button on index and detail pages for failed/needs-review/processing jobs
- Shows inventory number, current step, attempt count, created-at on index
- Detail page renders full AI step output (vision, specs, attributes)
- Filters active jobs by non-completed status via custom query builder
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Console commands: CreateUser (interactive), BackupCommand, RotateLogsCommand.
Migrations 20260514: initial schema for app/logs schemas.
Config: register new bundles, Doctrine schema filter, Kernel micro-kernel
adjustments, deleted unused api.yaml route file and www.conf override.
Application service and API controller updates for the full article lifecycle.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CRUD controllers for Article, ArticleType, AttributeDefinition,
ArticleTypeAttribute, AIPipelineJob, Order, Customer, Invoice, User
and LogEntry. SecurityController handles login/logout; TotpSetupController
manages 2FA enrollment. API controllers for pipeline and orders.
Admin dashboard template and Twig base layout included.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
eBay adapter covers OAuth, inventory API, fulfillment API, taxonomy
service and webhook signature verification. Frappe ERP adapter wraps
the Frappe HTTP client for order/invoice sync.
Includes CustomerResolver, InvoiceMailer, and the EbayWebhookController
for inbound eBay marketplace notifications.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Promote article_type_attributes join table to ArticleTypeAttribute entity
with a required boolean flag. ArticleType gains virtual form properties
(requiredAttributeDefs / optionalAttributeDefs) reconciled via
applyAttributeAssignments() on persist/update.
Admin form shows two Tom Select multi-selects; a small JS module
(article-type-attr-sync.js) listens for ea.autocomplete.connect events
and keeps the two lists mutually exclusive in real time.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Validates current password, enforces 8-char minimum, links from
EasyAdmin user menu. Also fixes route loader (attribute scan instead
of broken easyadmin.routes type).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>