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 <noreply@anthropic.com>
This commit is contained in:
parent
f55e96b094
commit
9e59123683
12 changed files with 130 additions and 11 deletions
28
migrations/Version20260517240000.php
Normal file
28
migrations/Version20260517240000.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20260517240000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add manufacturer and model_number columns to articles';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -37,6 +37,11 @@ class ArticleType
|
|||
$this->attributeAssignments = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getId(): Uuid
|
||||
{
|
||||
return $this->id;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
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: $message->inventoryNumber,
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ final readonly class JsonCodingMessage
|
|||
public string $articleTypeId,
|
||||
public string $specsText,
|
||||
public array $missingFields = [],
|
||||
public ?string $serialNumber = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ final readonly class SpecsResearchMessage
|
|||
public string $articleTypeId,
|
||||
public string $modelName,
|
||||
public string $serialNumber,
|
||||
public string $manufacturer = '',
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ final readonly class ValidationMessage
|
|||
public string $articleTypeId,
|
||||
public string $specsText,
|
||||
public array $attributes,
|
||||
public ?string $serialNumber = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue