Covers layer structure, domain model, all Messenger flows (AI pipeline A+B, order processing, stock sync), channel adapter pattern, eBay integration details, auth, EasyAdmin layout, Doctrine gotchas, and a file-location index. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
17 KiB
SuperSeller3000 — Architecture Reference
Quick-reference for Claude Code. Covers layer structure, domain model, all async flows, eBay integration, auth, and key gotchas. Read this before grepping blindly.
1. Layer Structure
src/
├── Domain/ Pure entities, value objects, repository interfaces — no Symfony, no Doctrine annotations beyond mapping
├── Application/ Use-case services and port interfaces (ChannelAdapterInterface, ErpAdapterInterface, …)
├── Infrastructure/ Symfony/Doctrine/HTTP implementations, Messenger handlers, controllers, console commands
└── Kernel.php
Dependency rule: Domain ← Application ← Infrastructure. Nothing in Domain or Application imports from Infrastructure.
Repository interfaces live in Domain/*/Repository/. Doctrine implementations live in Infrastructure/Persistence/Repository/.
2. Domain Model (key entities and relationships)
ArticleType ──< ArticleTypeAttribute >── AttributeDefinition
│
└──< ArticleTypeEbayMapping (eBay aspect → Article field or AttributeDefinition)
│
ArticleTypePlatformConfig(ArticleType, Platform) (categoryId + 4 policy fields per adapter type)
Article ──> ArticleType
──< AttributeValue >── AttributeDefinition
──< ArticlePhoto
fields: sku, inventoryNumber, status(enum), stock, condition(enum), serialNumber,
manufacturer, modelNumber, modelName, listingPrice, specsText,
ebayListingId, ebayTitle, ebayDescription
Order ──> Article, Customer, Platform
──> Invoice
AIPipelineJob tracks every async AI run; stores input/output per step in JSON columns
Platform type string (e.g. 'ebay'), display name
ApiKey bcrypt-hashed secret, linked to User via ManyToOne
ArticleType virtual form properties (pendingRequired/pendingOptional): EasyAdmin sets these; call applyAttributeAssignments() before flush — see ArticleTypeCrudController.
ArticleTypeEbayMapping.sourceType: 'article_field' → read Article::get{ArticleFieldKey}(); 'attribute' → read AttributeValue for the linked AttributeDefinition.
3. AI Pipeline — Pipeline A (Photo upload)
Entry point: POST /api/pipeline/photo → dispatches PhotoUploadMessage
PhotoUploadMessage
→ PhotoUploadHandler
├─ OllamaVisionAgent (Mistral pixtral-12b-2409) → {manufacturer, modelName, modelNumber, serial}
├─ if modelName+modelNumber empty → markNeedsReview, stop
├─ articleRepo.findCompletedByModelNumber(modelNumber)?
│ YES (cache hit) ──→ records step 'model_match' → DraftArticleMessage
│ DraftArticleHandler copies ebayTitle/Description/specsText
│ from matched article, skips EbayTextMessage, markCompleted
│
└─ NO (cache miss) ──→ SpecsResearchMessage
→ SpecsResearchHandler (Mistral large + Tavily web search)
→ JsonCodingMessage
→ JsonCodingHandler (Mistral large) → {attributes: {defId: value, …}}
→ ValidationMessage
→ ValidationHandler
├─ all required fields present? → DraftArticleMessage
└─ missing + attempts < 3 → JsonCodingMessage (retry with missingFields hint)
└─ attempts ≥ 3 → markNeedsReview
DraftArticleMessage
→ DraftArticleHandler
├─ creates or updates Article (re-run: job.articleId already set)
├─ sets manufacturer/modelNumber/modelName from vision step
├─ if model_match in job output → copies texts, markCompleted (no EbayText step)
└─ otherwise → EbayTextMessage
EbayTextMessage
→ EbayTextHandler (Mistral large) → sets article.ebayTitle + ebayDescription, markCompleted
4. AI Pipeline — Pipeline B (PXE / manual specs dump)
Entry point: POST /api/pipeline/pxe → dispatches PxeInventoryMessage
PxeInventoryMessage
→ PxeInventoryHandler → JsonCodingMessage (specsText = pxeDump)
→ (same JSON→Validation→Draft→EbayText chain as Pipeline A cache-miss path)
5. Order Flow
Entry point: eBay webhook POST /webhook/ebay → EbayWebhookController verifies signature → dispatches OrderReceivedMessage
OrderReceivedMessage (transport: orders, max_retries: 5)
→ OrderReceivedHandler
1. idempotency: findByPlatformOrderId → skip if exists
2. fulfillmentClient.getOrder(platformOrderId) → {ebayListingId, buyer*, shipping*, salePrice, saleDate}
3. articleRepo.findByEbayListingId → UnrecoverableMessageHandlingException if not found
4. platformRepo.findByType('ebay') → UnrecoverableMessageHandlingException if not configured
5. articleRepo.decrementStockAtomic → critical log + Unrecoverable if stock was already 0
6. customerResolver.resolve(platform, buyerUsername, …) → find or create Customer
7. new Order(article, customer, platform, platformOrderId, salePrice, saleDate)
8. erp.createSalesInvoice(order) → Frappe invoice ID
9. erp.fetchInvoicePdf(frappeInvoiceId) → binary PDF
10. storage.store(tmpFile, 'invoice-{id}.pdf') → StoredFile
11. new Invoice(order, frappeInvoiceId, storagePath, filename)
12. mailer.sendInvoice(invoice) → SMTP with PDF attachment
13. dispatch UpdateStockOnChannelsMessage(articleId, newStock)
14. order.setStatus(Completed)
UpdateStockOnChannelsMessage (transport: channel_sync)
→ UpdateStockOnChannelsHandler
if newStock == 0 → dispatch DeactivateListingMessage per platform
else → adapterRegistry.get(platform.type).updateStock(article, newStock)
DeactivateListingMessage
→ DeactivateListingHandler → adapter.deactivateListing(article)
6. Channel Adapter Pattern
Interface: Application/Channel/ChannelAdapterInterface
getType(): string
publishListing(Article): string // returns listingId
updateStock(Article, int): void
deactivateListing(Article): void
pushTracking(Order): void
Registry: Application/Channel/ChannelAdapterRegistry — receives iterable $adapters via tagged_iterator tag app.channel_adapter. Look up with has(type) / get(type).
eBay adapter: Infrastructure/Channel/Ebay/EbayAdapter
- Reads
ArticleTypePlatformConfigviafindByArticleTypeAndPlatformType($articleType, 'ebay')— throws\RuntimeExceptionif missing buildAspects()iteratesArticleType::getEbayMappings()and resolves each to a value via article field getter orAttributeValuepublishListingcalls:upsertInventoryItem→createOffer→publishOffer; conditionally addslistingPolicies(only if non-empty) andmerchantLocationKey(only if not null)
eBay API clients:
EbayOAuthClient— Client-Credentials token with PSR-6 cacheEbayInventoryApiClient— Inventory API (upsert item, create/publish/withdraw offer, bulk stock update, get locations)EbayAccountApiClient— Account API (fulfillment/payment/return policies)EbayFulfillmentApiClient— Fulfillment API (getOrder, addTracking)EbayTaxonomyService— Taxonomy API (category suggestions, aspect metadata)
EbayPolicyProvider: aggregates all 4 choice lists (fulfillment, payment, return, location) with 5-minute PSR-6 cache; returns [] on any exception (triggers fallback TextField in admin form).
7. Messenger Transports
All transports use Redis Streams (DSN from MESSENGER_TRANSPORT_DSN).
| Transport | Stream | Retries | Delay | Used for |
|---|---|---|---|---|
ai_pipeline |
ai_pipeline |
3 | 2 s × 2 | All AI pipeline messages |
orders |
orders |
5 | 1 s × 2 | OrderReceivedMessage |
channel_sync |
channel_sync |
5 | 2 s × 2 ≤ 60 s | Publish/UpdateStock/Deactivate/TrackingPush |
failed |
failed |
— | — | Dead-letter after max retries exhausted |
PipelineJobFailureListener (Infrastructure/Messenger/PipelineJobFailureListener.php): listens for WorkerMessageFailedEvent, extracts jobId from message if available, calls job.markFailed(...).
Workers (run separately per transport in prod):
bin/console messenger:consume ai_pipeline --time-limit=3600
bin/console messenger:consume orders --time-limit=3600
bin/console messenger:consume channel_sync --time-limit=3600
8. Authentication
Browser sessions: Symfony form login + TOTP 2FA (scheb/2fa-bundle). Setup flow: TotpSetupController. Password change: ChangePasswordController.
API requests: Custom ApiKey authenticator. Clients send X-Api-Key: <raw-secret> header. The authenticator loads ApiKey by user email extracted from the JWT-style key prefix, verifies bcrypt hash.
PermissionVoter: Infrastructure/Http/Security/ — checks ApiKey.permissions array against voted attribute strings.
Console commands for key management:
bin/console app:create-user <email> <password>
bin/console app:create-api-key <user-email> <description> [--permissions=read,write]
9. EasyAdmin Structure
Dashboard: Infrastructure/Http/Controller/Admin/DashboardController — uses #[AdminDashboard] attribute, builds menu with MenuItem::subMenu() per adapter section.
Navigation sections:
- Artikel (ArticleType, Article, AttributeDefinition, Photo ingest, Pipeline jobs)
- Bestellungen (Order, Customer, Invoice)
- eBay (EbayArticleTypePlatformConfigCrudController)
- System (User, ApiKey, PromptTemplate, Translation, Logs)
EbayArticleTypePlatformConfigCrudController: filters index to platform.type = 'ebay' via custom createIndexQueryBuilder. Renders ChoiceField (live policy list) or TextField fallback depending on EbayPolicyProvider availability.
ArticleTypeCrudController: overrides persistEntity/updateEntity to call applyAttributeAssignments() before flush.
EbayAspectImportController: action button in ArticleType CRUD that fetches eBay Taxonomy aspects for the type's ebayCategoryId and upserts ArticleTypeEbayMapping rows.
10. Database / Doctrine
- PostgreSQL 17, three schemas:
app(main data),logs(log_entries),logs_archive(rotated, excluded from migrations) schema_filterindoctrine.yamlexcludeslogs_archive.*andapp.inventory_seq- All entity tables are in schema
app(set via#[ORM\Table(schema: 'app')]) - UUIDs generated with
Uuid::v7()in constructors (time-ordered) decrementStockAtomicuses a raw SQL UPDATE withWHERE stock > 0for race-free stock locking
Migrations: bin/console doctrine:migrations:migrate --no-interaction. Never use doctrine:schema:update in prod.
11. AI / LLM Backend
All AI traffic goes through Mistral Cloud API. The interface is named OllamaClientInterface for historical reasons — the DI alias points to MistralClient.
| Role | Model | Agent class |
|---|---|---|
| Vision | pixtral-12b-2409 |
OllamaVisionAgent |
| Specs / text | mistral-large-latest |
SpecsResearchAgent, JsonCodingAgent, EbayTextAgent |
Web search: WebSearchInterface → TavilyWebSearch (Tavily API). Used inside SpecsResearchAgent to augment specs lookup.
Prompt templates are stored in the database (app.prompt_templates) and loaded by PromptTemplateService. Edit them live via the admin UI.
12. Key Gotchas
Routing prefix: API controllers must declare /api prefix in the class-level #[Route('/api/...')], NOT via yaml prefix config — Symfony's yaml prefix mechanism conflicts with attribute routing in this setup.
OllamaClientInterface / OllamaClient: These classes still exist but the DI alias points to MistralClient. Do not instantiate OllamaClient directly.
EbayAdapter requires ArticleTypePlatformConfig: If no config row exists for an ArticleType+eBay combination, publishListing throws \RuntimeException. Create the config via Admin → eBay → Kategorie-Konfigurationen first.
ArticleType::applyAttributeAssignments(): Must be called before Doctrine flush whenever the EasyAdmin form sets pendingRequired/pendingOptional. Forgetting this leaves the DB unchanged silently.
Docker user 1000:1000 / HOME=/tmp: All PHP containers run as UID 1000:1000 with HOME=/tmp. Composer cache, Symfony cache, and file writes must target paths writable by that user.
schema_filter excludes logs_archive.*: Running doctrine:migrations:diff will NOT see the archive schema. Never manually create Doctrine entities targeting logs_archive.
ArticleTypeEbayMapping aspect name uniqueness: Enforced by uq_ebay_mapping DB constraint AND upsertEbayMapping() helper. Always go through the helper to avoid constraint violations.
EbayPolicyProvider cache TTL: 300 seconds. During dev, policy dropdowns may show stale data. Clear the Symfony cache (bin/console cache:clear) or wait 5 minutes.
13. Where to Find Things
| What you need | Where |
|---|---|
| Article domain entity | src/Domain/Article/Article.php |
| eBay aspect→Article field mapping | src/Domain/Article/ArticleTypeEbayMapping.php |
| Platform config (categoryId + policies) | src/Domain/Channel/ArticleTypePlatformConfig.php |
| eBay listing logic | src/Infrastructure/Channel/Ebay/EbayAdapter.php |
| eBay OAuth token | src/Infrastructure/Channel/Ebay/EbayOAuthClient.php |
| eBay policy dropdowns | src/Infrastructure/Channel/Ebay/EbayPolicyProvider.php |
| eBay taxonomy / aspect import | src/Infrastructure/Channel/Ebay/EbayTaxonomyService.php |
| All Messenger messages | src/Infrastructure/Messenger/Message/ |
| All Messenger handlers | src/Infrastructure/Messenger/Handler/ |
| Messenger routing | config/packages/messenger.yaml |
| AI agents | src/Infrastructure/AI/Agent/ |
| Mistral HTTP client | src/Infrastructure/AI/MistralClient.php |
| Prompt templates (runtime) | src/Infrastructure/AI/PromptTemplateService.php + DB table app.prompt_templates |
| Order processing | src/Infrastructure/Messenger/Handler/OrderReceivedHandler.php |
| Stock sync / deactivate | src/Infrastructure/Messenger/Handler/UpdateStockOnChannelsHandler.php |
| Frappe ERP adapter | src/Infrastructure/Channel/Frappe/FrappeErpAdapter.php |
| Admin dashboard / nav | src/Infrastructure/Http/Controller/Admin/DashboardController.php |
| eBay admin config CRUD | src/Infrastructure/Http/Controller/Admin/EbayArticleTypePlatformConfigCrudController.php |
| API controllers | src/Infrastructure/Http/Controller/Api/ |
| Webhook entry point | src/Infrastructure/Http/Controller/Webhook/EbayWebhookController.php |
| Auth / ApiKey | src/Domain/Auth/ApiKey.php, src/Domain/Auth/User.php |
| Doctrine migrations | migrations/ |
| Service wiring | config/services.yaml |
| Transport config | config/packages/messenger.yaml |
| Admin translations (de/en) | translations/admin.de.yaml, translations/admin.en.yaml |
| Integration tests | tests/Integration/ |
| Unit tests | tests/Unit/ |
| Infra / prod setup | docs/superpowers/specs/2026-05-19-infrastructure.md |