From 9e59123683cbd581317864309436717a87927c86 Mon Sep 17 00:00:00 2001 From: Simon Kuehn Date: Mon, 18 May 2026 07:18:24 +0000 Subject: [PATCH] feat: add manufacturer/model fields to Article and propagate through pipeline - Article entity gets manufacturer and modelNumber columns (migration 20260517240000) - Vision output (manufacturer/model) is written to the article in DraftArticleHandler - manufacturer is forwarded through SpecsResearchMessage so the specs prompt can use it - serialNumber is now threaded through JsonCodingMessage and ValidationMessage - EbayTextHandler pulls specsText from the job's stored output data - DraftArticleHandler supports re-run mode (existing article reuse) and sets Draft status - ArticleType and AttributeValue get __toString() for form/display use - ArticleService exposes reserveInventoryNumber() publicly Co-Authored-By: Claude Sonnet 4.6 --- migrations/Version20260517240000.php | 28 ++++++++++++ src/Application/Article/ArticleService.php | 5 +++ src/Domain/Article/Article.php | 30 ++++++++++++- src/Domain/Article/ArticleType.php | 5 +++ src/Domain/Article/AttributeValue.php | 5 +++ .../Messenger/Handler/DraftArticleHandler.php | 43 ++++++++++++++++--- .../Messenger/Handler/EbayTextHandler.php | 10 ++++- .../Messenger/Handler/PhotoUploadHandler.php | 9 ++++ .../Messenger/Handler/ValidationHandler.php | 3 +- .../Messenger/Message/JsonCodingMessage.php | 1 + .../Message/SpecsResearchMessage.php | 1 + .../Messenger/Message/ValidationMessage.php | 1 + 12 files changed, 130 insertions(+), 11 deletions(-) create mode 100644 migrations/Version20260517240000.php diff --git a/migrations/Version20260517240000.php b/migrations/Version20260517240000.php new file mode 100644 index 0000000..46df401 --- /dev/null +++ b/migrations/Version20260517240000.php @@ -0,0 +1,28 @@ +addSql('ALTER TABLE app.articles ADD COLUMN manufacturer VARCHAR(255) NULL'); + $this->addSql('ALTER TABLE app.articles ADD COLUMN model_number VARCHAR(255) NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE app.articles DROP COLUMN manufacturer'); + $this->addSql('ALTER TABLE app.articles DROP COLUMN model_number'); + } +} diff --git a/src/Application/Article/ArticleService.php b/src/Application/Article/ArticleService.php index 7eb9366..0c48ae3 100644 --- a/src/Application/Article/ArticleService.php +++ b/src/Application/Article/ArticleService.php @@ -135,6 +135,11 @@ final class ArticleService return ['article' => $article, 'missing' => []]; } + public function reserveInventoryNumber(): string + { + return $this->nextInventoryNumber(); + } + private function nextInventoryNumber(): string { /** @var string $seq */ diff --git a/src/Domain/Article/Article.php b/src/Domain/Article/Article.php index ae1bc64..29ab58f 100644 --- a/src/Domain/Article/Article.php +++ b/src/Domain/Article/Article.php @@ -45,6 +45,12 @@ class Article #[ORM\Column(type: 'string', length: 255, nullable: true)] private ?string $serialNumber = null; + #[ORM\Column(type: 'string', length: 255, nullable: true)] + private ?string $manufacturer = null; + + #[ORM\Column(type: 'string', length: 255, nullable: true)] + private ?string $modelNumber = null; + #[ORM\Column(type: 'string', length: 255, nullable: true)] private ?string $ebayListingId = null; @@ -182,8 +188,8 @@ class Article { foreach ($this->attributeValues as $existing) { if ($existing->getAttributeDefinition()->getId()->equals($value->getAttributeDefinition()->getId())) { - $this->attributeValues->removeElement($existing); - break; + $existing->setValue($value->getValue()); + return; } } $this->attributeValues->add($value); @@ -216,6 +222,26 @@ class Article $this->serialNumber = $sn; } + public function getManufacturer(): ?string + { + return $this->manufacturer; + } + + public function setManufacturer(?string $manufacturer): void + { + $this->manufacturer = $manufacturer; + } + + public function getModelNumber(): ?string + { + return $this->modelNumber; + } + + public function setModelNumber(?string $modelNumber): void + { + $this->modelNumber = $modelNumber; + } + public function setEbayListingId(?string $id): void { $this->ebayListingId = $id; diff --git a/src/Domain/Article/ArticleType.php b/src/Domain/Article/ArticleType.php index 8ae957d..b9dd5b4 100644 --- a/src/Domain/Article/ArticleType.php +++ b/src/Domain/Article/ArticleType.php @@ -37,6 +37,11 @@ class ArticleType $this->attributeAssignments = new ArrayCollection(); } + public function __toString(): string + { + return $this->name; + } + public function getId(): Uuid { return $this->id; diff --git a/src/Domain/Article/AttributeValue.php b/src/Domain/Article/AttributeValue.php index 5386983..4e4ea7a 100644 --- a/src/Domain/Article/AttributeValue.php +++ b/src/Domain/Article/AttributeValue.php @@ -38,6 +38,11 @@ class AttributeValue $this->value = $value; } + public function __toString(): string + { + return $this->attributeDefinition->getName().': '.$this->value; + } + public function getId(): Uuid { return $this->id; diff --git a/src/Infrastructure/Messenger/Handler/DraftArticleHandler.php b/src/Infrastructure/Messenger/Handler/DraftArticleHandler.php index b267162..f6e96b5 100644 --- a/src/Infrastructure/Messenger/Handler/DraftArticleHandler.php +++ b/src/Infrastructure/Messenger/Handler/DraftArticleHandler.php @@ -6,6 +6,8 @@ namespace App\Infrastructure\Messenger\Handler; use App\Application\Article\ArticleService; use App\Domain\Article\ArticleCondition; +use App\Domain\Article\ArticleStatus; +use App\Domain\Article\Repository\ArticleRepositoryInterface; use App\Domain\Pipeline\Repository\AIPipelineJobRepositoryInterface; use App\Infrastructure\Messenger\Message\DraftArticleMessage; use App\Infrastructure\Messenger\Message\EbayTextMessage; @@ -18,6 +20,7 @@ final class DraftArticleHandler { public function __construct( private readonly ArticleService $articleService, + private readonly ArticleRepositoryInterface $articleRepository, private readonly AIPipelineJobRepositoryInterface $jobRepository, private readonly MessageBusInterface $bus, ) { @@ -30,23 +33,49 @@ final class DraftArticleHandler return; } - $condition = ArticleCondition::tryFrom($message->condition) ?? ArticleCondition::Good; + // Re-run mode: job already has an articleId → update existing article + $existingArticleId = $job->getArticleId(); + if (null !== $existingArticleId) { + $article = $this->articleRepository->findById($existingArticleId); + if (null === $article) { + $job->markFailed("Re-run target article {$existingArticleId->toRfc4122()} not found"); + $this->jobRepository->save($job); - $article = $this->articleService->create( - articleTypeId: Uuid::fromString($message->articleTypeId), - condition: $condition, - stock: 1, - inventoryNumber: $message->inventoryNumber, - ); + return; + } + } else { + $condition = ArticleCondition::tryFrom($message->condition) ?? ArticleCondition::Good; + $inventoryNumber = $message->inventoryNumber ?? ($job->getInputData()['inventoryNumber'] ?? null); + + $article = $this->articleService->create( + articleTypeId: Uuid::fromString($message->articleTypeId), + condition: $condition, + stock: 1, + inventoryNumber: $inventoryNumber, + ); + } if (null !== $message->serialNumber) { $article->setSerialNumber($message->serialNumber); } + $vision = $job->getOutputData()['vision'] ?? []; + if (isset($vision['manufacturer']) && '' !== $vision['manufacturer']) { + $article->setManufacturer((string) $vision['manufacturer']); + } + if (isset($vision['model']) && '' !== $vision['model']) { + $article->setModelNumber((string) $vision['model']); + } + if ([] !== $message->attributes) { $this->articleService->updateAttributes($article->getId(), $message->attributes); } + if (ArticleStatus::Ingesting === $article->getStatus() || ArticleStatus::NeedsReview === $article->getStatus()) { + $article->transitionTo(ArticleStatus::Draft); + } + $this->articleRepository->save($article); + $job->setArticleId($article->getId()); $job->markCompleted(['articleId' => $article->getId()->toRfc4122()]); $this->jobRepository->save($job); diff --git a/src/Infrastructure/Messenger/Handler/EbayTextHandler.php b/src/Infrastructure/Messenger/Handler/EbayTextHandler.php index 4ba4d05..4956de4 100644 --- a/src/Infrastructure/Messenger/Handler/EbayTextHandler.php +++ b/src/Infrastructure/Messenger/Handler/EbayTextHandler.php @@ -6,6 +6,7 @@ namespace App\Infrastructure\Messenger\Handler; use App\Application\Article\ArticleService; use App\Domain\Article\Repository\ArticleRepositoryInterface; +use App\Domain\Pipeline\Repository\AIPipelineJobRepositoryInterface; use App\Infrastructure\AI\Agent\EbayTextAgent; use App\Infrastructure\Messenger\Message\EbayTextMessage; use Symfony\Component\Messenger\Attribute\AsMessageHandler; @@ -18,6 +19,7 @@ final class EbayTextHandler private readonly EbayTextAgent $ebayTextAgent, private readonly ArticleRepositoryInterface $articleRepository, private readonly ArticleService $articleService, + private readonly AIPipelineJobRepositoryInterface $jobRepository, ) { } @@ -28,7 +30,13 @@ final class EbayTextHandler return; } - $texts = $this->ebayTextAgent->generate($article); + $specsText = ''; + $job = $this->jobRepository->findById(Uuid::fromString($message->jobId)); + if (null !== $job) { + $specsText = (string) ($job->getOutputData()['specs_research']['specsText'] ?? ''); + } + + $texts = $this->ebayTextAgent->generate($article, $specsText); $this->articleService->setEbayTexts( articleId: $article->getId(), diff --git a/src/Infrastructure/Messenger/Handler/PhotoUploadHandler.php b/src/Infrastructure/Messenger/Handler/PhotoUploadHandler.php index ddd6f02..1565db8 100644 --- a/src/Infrastructure/Messenger/Handler/PhotoUploadHandler.php +++ b/src/Infrastructure/Messenger/Handler/PhotoUploadHandler.php @@ -29,11 +29,19 @@ final class PhotoUploadHandler return; } + $job->incrementAttempt(); $job->markProcessing(); $this->jobRepository->save($job); $result = $this->visionAgent->analyze($message->storedPhotoPath); + $job->recordStep('vision', [ + 'manufacturer' => $result['manufacturer'], + 'model' => $result['model'], + 'serial' => $result['serial'], + ]); + $this->jobRepository->save($job); + if ('' === $result['model']) { $job->markNeedsReview('OllamaVisionAgent: no model name detected on nameplate'); $this->jobRepository->save($job); @@ -46,6 +54,7 @@ final class PhotoUploadHandler articleTypeId: $message->articleTypeId, modelName: $result['model'], serialNumber: $result['serial'], + manufacturer: $result['manufacturer'], )); } } diff --git a/src/Infrastructure/Messenger/Handler/ValidationHandler.php b/src/Infrastructure/Messenger/Handler/ValidationHandler.php index 22836a3..aada242 100644 --- a/src/Infrastructure/Messenger/Handler/ValidationHandler.php +++ b/src/Infrastructure/Messenger/Handler/ValidationHandler.php @@ -41,7 +41,7 @@ final class ValidationHandler attributes: $message->attributes, condition: 'good', inventoryNumber: null, - serialNumber: null, + serialNumber: $message->serialNumber, )); return; @@ -62,6 +62,7 @@ final class ValidationHandler articleTypeId: $message->articleTypeId, specsText: $message->specsText, missingFields: $missing, + serialNumber: $message->serialNumber, )); } diff --git a/src/Infrastructure/Messenger/Message/JsonCodingMessage.php b/src/Infrastructure/Messenger/Message/JsonCodingMessage.php index 798856c..a558f9d 100644 --- a/src/Infrastructure/Messenger/Message/JsonCodingMessage.php +++ b/src/Infrastructure/Messenger/Message/JsonCodingMessage.php @@ -12,6 +12,7 @@ final readonly class JsonCodingMessage public string $articleTypeId, public string $specsText, public array $missingFields = [], + public ?string $serialNumber = null, ) { } } diff --git a/src/Infrastructure/Messenger/Message/SpecsResearchMessage.php b/src/Infrastructure/Messenger/Message/SpecsResearchMessage.php index 2411d8e..40e402b 100644 --- a/src/Infrastructure/Messenger/Message/SpecsResearchMessage.php +++ b/src/Infrastructure/Messenger/Message/SpecsResearchMessage.php @@ -11,6 +11,7 @@ final readonly class SpecsResearchMessage public string $articleTypeId, public string $modelName, public string $serialNumber, + public string $manufacturer = '', ) { } } diff --git a/src/Infrastructure/Messenger/Message/ValidationMessage.php b/src/Infrastructure/Messenger/Message/ValidationMessage.php index 01abe54..1e7362d 100644 --- a/src/Infrastructure/Messenger/Message/ValidationMessage.php +++ b/src/Infrastructure/Messenger/Message/ValidationMessage.php @@ -12,6 +12,7 @@ final readonly class ValidationMessage public string $articleTypeId, public string $specsText, public array $attributes, + public ?string $serialNumber = null, ) { } }