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

299 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`
```php
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: `upsertInventoryItem``createOffer``publishOffer`; 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: `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` |