SuperSeller3000/CLAUDE.md
Simon Kuehn 31c5116a1b docs: add CLAUDE.md and Plan 7 (eBay admin navigation + business policies)
Plan 7 covers: ArticleTypePlatformConfig policy fields, EbayAccountApiClient,
EbayPolicyProvider with live dropdown choices, EbayAdapter reading real config,
and per-adapter admin navigation section.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 06:25:14 +00:00

107 lines
5.5 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.

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Commands
All PHP commands run inside Docker. The app container is named `app`.
```bash
# Unit tests
docker compose exec app php vendor/bin/phpunit tests/Unit/
# Single test file or method
docker compose exec app php vendor/bin/phpunit tests/Unit/Domain/Article/ArticleTest.php
docker compose exec app php vendor/bin/phpunit --filter testSomeMethod
# Integration tests (loads .env.local secrets automatically)
bin/test-integration
bin/test-integration tests/Integration/Channel/EbayAdapterTest.php
# PHPStan (level 9 — must be clean before committing)
docker compose exec app php vendor/bin/phpstan analyse
# CS Fixer (dry-run to check, no --dry-run to fix)
docker compose exec app php vendor/bin/php-cs-fixer fix --dry-run --diff
docker compose exec app php vendor/bin/php-cs-fixer fix
# Migrations
docker compose exec app php bin/console doctrine:migrations:migrate --no-interaction
docker compose exec app php bin/console doctrine:migrations:diff # generate from entity changes
# Cache
docker compose exec app php bin/console cache:clear
# Create first user / API key
docker compose exec app php bin/console app:users:create
docker compose exec app php bin/console app:api-keys:create
```
## Architecture
Hexagonal architecture: **Domain → Application → Infrastructure**. The boundary is enforced by convention and PHPStan.
- `src/Domain/` — pure PHP, zero framework imports. Entities, enums, value objects, repository *interfaces*. Doctrine attributes on entities are the pragmatic exception.
- `src/Application/` — use cases, message handlers, service interfaces (ports). Orchestrates domain via interfaces only.
- `src/Infrastructure/` — all framework/external-system code: Doctrine repositories, Symfony controllers, Messenger handlers, channel adapters, AI clients.
### Dependency Injection
Every Domain repository interface is aliased to its Doctrine implementation in `config/services.yaml`. All Application interfaces (ports) are aliased there too. When adding a new interface+implementation pair, add the alias manually — autowiring alone won't resolve interfaces.
Channel adapters are collected via `tagged_iterator app.channel_adapter` into `ChannelAdapterRegistry`. Tag new adapters in `services.yaml`.
### Routing (Symfony 8 gotcha)
`routing.controllers` auto-discovers all controllers. API controllers **must** declare the `/api` prefix in their class-level `#[Route]` attribute — a yaml `prefix:` on top of auto-discovery is silently ignored.
### AI Pipeline
The AI backend is `MistralClient` (vision: pixtral-12b, text: mistral-large). The interface is named `OllamaClientInterface` for historical reasons — the `services.yaml` alias points it to `MistralClient`. Web search uses `TavilyWebSearch` behind `WebSearchInterface`.
Pipeline A (photo) chains Messages sequentially — each handler dispatches the next:
`PhotoUpload → SpecsResearch → JsonCoding → Validation → DraftArticle → EbayText`
After vision, `findCompletedByModelNumber()` checks the DB for a cache hit and skips the remaining AI steps if found.
`PipelineJobFailureListener` catches `WorkerMessageFailedEvent` after all retries are exhausted and sets `AIPipelineJob.status = failed`.
### Messenger Transports
Three isolated Redis streams — a failing worker never blocks the others:
| Transport | Worker service | Retries | Delay |
|---|---|---|---|
| `ai_pipeline` | `worker-ai` | 3 | 2 s ×2 |
| `orders` | `worker-orders` | 5 | 1 s ×2 |
| `channel_sync` | `worker-channel` | 5 | 2 s ×2, max 60 s |
Exhausted messages land in `failed` transport (persistent). Replay with `messenger:failed:retry`.
### eBay Integration
`EbayAdapter` implements `ChannelAdapterInterface`. It uses:
- `EbayInventoryApiClient` — inventory items, offers, publish/withdraw, stock updates, tracking
- `EbayFulfillmentApiClient` — order fetching
- `EbayOAuthClient` — Client-Credentials token with `cache.app` caching
- `EbayTaxonomyService` — category/aspect lookup, also cached
`ArticleTypePlatformConfig` holds per-ArticleType eBay settings (category ID, business policy IDs). `ArticleTypeEbayMapping` maps each eBay aspect name to either an `Article` field (`SOURCE_ARTICLE_FIELD`) or an `AttributeDefinition` (`SOURCE_ATTRIBUTE`). `EbayAdapter.buildAspects()` reads from this table.
### EasyAdmin (Admin Panel)
`DashboardController` carries `#[AdminDashboard]` and `configureMenuItems()`. All CRUD controllers in `src/Infrastructure/Http/Controller/Admin/` are auto-discovered. Menu items reference controller class names directly via `MenuItem::linkTo()`.
### PostgreSQL Schemas
Three schemas: `app` (all entities), `logs` (live log entries), `logs_archive` (rotated). `doctrine.yaml` sets `schema_filter` to exclude `logs_archive.*` and `app.inventory_seq` from migration diffs — never remove that filter.
### Auth
Browser login: form + optional TOTP (`scheb/two-factor-bundle`). API access: `X-Api-Key` header — stored as bcrypt hash with prefix for lookup. `PermissionVoter` checks `User.permissions` and `ApiKey.permissions` (both jsonb) uniformly. Permission constants live in `PermissionVoter`.
### Docker
All PHP services (`app`, `worker-*`, `cron`) run as user `1000:1000` with `HOME=/tmp`. Never run commands as root inside the container — it creates root-owned files on the host.
`docker-compose.override.yml` exists only for local dev (exposes Postgres/Redis ports). Do not use it on the production server.