SuperSeller3000/docs/architecture.md
Simon Kuehn 237b0e6d8e
Some checks are pending
CI / test (push) Waiting to run
docs: add comprehensive architecture reference
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>
2026-05-19 07:39:04 +00:00

17 KiB
Raw Blame History

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/ebayEbayWebhookController 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 ArticleTypePlatformConfig via findByArticleTypeAndPlatformType($articleType, 'ebay') — throws \RuntimeException if missing
  • buildAspects() iterates ArticleType::getEbayMappings() and resolves each to a value via article field getter or AttributeValue
  • publishListing calls: upsertInventoryItemcreateOfferpublishOffer; conditionally adds listingPolicies (only if non-empty) and merchantLocationKey (only if not null)

eBay API clients:

  • EbayOAuthClient — Client-Credentials token with PSR-6 cache
  • EbayInventoryApiClient — 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_filter in doctrine.yaml excludes logs_archive.* and app.inventory_seq
  • All entity tables are in schema app (set via #[ORM\Table(schema: 'app')])
  • UUIDs generated with Uuid::v7() in constructors (time-ordered)
  • decrementStockAtomic uses a raw SQL UPDATE with WHERE stock > 0 for 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: WebSearchInterfaceTavilyWebSearch (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