300 lines
17 KiB
Markdown
300 lines
17 KiB
Markdown
|
|
# 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` |
|