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>
5.5 KiB
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.
# 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, trackingEbayFulfillmentApiClient— order fetchingEbayOAuthClient— Client-Credentials token withcache.appcachingEbayTaxonomyService— 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.