2026-05-14 04:28:06 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
namespace App\Domain\Article;
|
|
|
|
|
|
|
|
|
|
use Doctrine\Common\Collections\ArrayCollection;
|
|
|
|
|
use Doctrine\Common\Collections\Collection;
|
|
|
|
|
use Doctrine\ORM\Mapping as ORM;
|
|
|
|
|
use Symfony\Component\Uid\Uuid;
|
|
|
|
|
|
|
|
|
|
#[ORM\Entity]
|
|
|
|
|
#[ORM\Table(name: 'articles', schema: 'app')]
|
|
|
|
|
class Article
|
|
|
|
|
{
|
|
|
|
|
#[ORM\Id]
|
|
|
|
|
#[ORM\Column(type: 'uuid')]
|
|
|
|
|
private Uuid $id;
|
|
|
|
|
|
|
|
|
|
#[ORM\ManyToOne(targetEntity: ArticleType::class)]
|
|
|
|
|
#[ORM\JoinColumn(nullable: false)]
|
|
|
|
|
private ArticleType $articleType;
|
|
|
|
|
|
|
|
|
|
#[ORM\Column(type: 'string', length: 255, unique: true)]
|
|
|
|
|
private string $sku;
|
|
|
|
|
|
|
|
|
|
#[ORM\Column(type: 'string', length: 100, unique: true)]
|
|
|
|
|
private string $inventoryNumber;
|
|
|
|
|
|
|
|
|
|
#[ORM\Column(type: 'string', enumType: ArticleStatus::class)]
|
|
|
|
|
private ArticleStatus $status;
|
|
|
|
|
|
|
|
|
|
#[ORM\Column(type: 'integer')]
|
|
|
|
|
private int $stock;
|
|
|
|
|
|
|
|
|
|
#[ORM\Column(type: 'string', enumType: ArticleCondition::class)]
|
|
|
|
|
private ArticleCondition $condition;
|
|
|
|
|
|
|
|
|
|
#[ORM\Column(type: 'text', nullable: true)]
|
|
|
|
|
private ?string $conditionNotes = null;
|
|
|
|
|
|
|
|
|
|
#[ORM\Column(type: 'decimal', precision: 10, scale: 2, nullable: true)]
|
|
|
|
|
private ?string $listingPrice = null;
|
|
|
|
|
|
|
|
|
|
#[ORM\Column(type: 'string', length: 255, nullable: true)]
|
|
|
|
|
private ?string $serialNumber = null;
|
|
|
|
|
|
2026-05-18 07:18:24 +00:00
|
|
|
#[ORM\Column(type: 'string', length: 255, nullable: true)]
|
|
|
|
|
private ?string $manufacturer = null;
|
|
|
|
|
|
|
|
|
|
#[ORM\Column(type: 'string', length: 255, nullable: true)]
|
|
|
|
|
private ?string $modelNumber = null;
|
|
|
|
|
|
2026-05-14 04:28:06 +00:00
|
|
|
#[ORM\Column(type: 'string', length: 255, nullable: true)]
|
|
|
|
|
private ?string $ebayListingId = null;
|
|
|
|
|
|
|
|
|
|
#[ORM\Column(type: 'text', nullable: true)]
|
|
|
|
|
private ?string $ebayTitle = null;
|
|
|
|
|
|
|
|
|
|
#[ORM\Column(type: 'text', nullable: true)]
|
|
|
|
|
private ?string $ebayDescription = null;
|
|
|
|
|
|
|
|
|
|
/** @var Collection<int, AttributeValue> */
|
|
|
|
|
#[ORM\OneToMany(mappedBy: 'article', targetEntity: AttributeValue::class, cascade: ['persist', 'remove'])]
|
|
|
|
|
private Collection $attributeValues;
|
|
|
|
|
|
|
|
|
|
/** @var Collection<int, ArticlePhoto> */
|
2026-05-14 04:34:32 +00:00
|
|
|
#[ORM\OneToMany(mappedBy: 'article', targetEntity: ArticlePhoto::class, cascade: ['persist', 'remove'])]
|
|
|
|
|
#[ORM\OrderBy(['sortOrder' => 'ASC'])]
|
2026-05-14 04:28:06 +00:00
|
|
|
private Collection $photos;
|
|
|
|
|
|
|
|
|
|
public function __construct(
|
|
|
|
|
ArticleType $articleType,
|
|
|
|
|
string $sku,
|
|
|
|
|
string $inventoryNumber,
|
|
|
|
|
int $stock,
|
|
|
|
|
ArticleCondition $condition,
|
|
|
|
|
) {
|
|
|
|
|
$this->id = Uuid::v7();
|
|
|
|
|
$this->articleType = $articleType;
|
|
|
|
|
$this->sku = $sku;
|
|
|
|
|
$this->inventoryNumber = $inventoryNumber;
|
|
|
|
|
$this->status = ArticleStatus::Ingesting;
|
|
|
|
|
$this->stock = $stock;
|
|
|
|
|
$this->condition = $condition;
|
|
|
|
|
$this->attributeValues = new ArrayCollection();
|
|
|
|
|
$this->photos = new ArrayCollection();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function transitionTo(ArticleStatus $newStatus): void
|
|
|
|
|
{
|
|
|
|
|
if (!$this->status->canTransitionTo($newStatus)) {
|
feat: implement Plan 2 — Article Management API
- 6 new domain repository interfaces (StoragePath, ArticlePhoto, AttributeValue, ChannelField, ArticleTypePlatformConfig, AttributeMapping)
- 6 Doctrine repository implementations
- StorageManager with multi-path quota-aware file storage (LocalStorageManager)
- Application services: ArticleTypeService, ArticleService, ArticleValidator, PhotoService, PlatformService, MappingService
- REST controllers: ArticleType, Article, Photo, Platform, Mapping (all under /api prefix)
- inventory_number sequence migration
- 22 unit tests passing, PHPStan level 9 clean, CS Fixer clean
- Fixed phpdoc_to_comment CS Fixer rule (disabled) to preserve @var type annotations
- Fixed PHPStan: User::getUserIdentifier non-empty-string, AIPipelineJob nullable missingFields
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 05:19:20 +00:00
|
|
|
throw new \DomainException(\sprintf('Cannot transition from %s to %s', $this->status->value, $newStatus->value));
|
2026-05-14 04:28:06 +00:00
|
|
|
}
|
|
|
|
|
$this->status = $newStatus;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function decrementStock(): void
|
|
|
|
|
{
|
|
|
|
|
if ($this->stock <= 0) {
|
|
|
|
|
throw new \DomainException('Stock cannot go below zero');
|
|
|
|
|
}
|
|
|
|
|
--$this->stock;
|
|
|
|
|
}
|
|
|
|
|
|
feat: implement Plan 2 — Article Management API
- 6 new domain repository interfaces (StoragePath, ArticlePhoto, AttributeValue, ChannelField, ArticleTypePlatformConfig, AttributeMapping)
- 6 Doctrine repository implementations
- StorageManager with multi-path quota-aware file storage (LocalStorageManager)
- Application services: ArticleTypeService, ArticleService, ArticleValidator, PhotoService, PlatformService, MappingService
- REST controllers: ArticleType, Article, Photo, Platform, Mapping (all under /api prefix)
- inventory_number sequence migration
- 22 unit tests passing, PHPStan level 9 clean, CS Fixer clean
- Fixed phpdoc_to_comment CS Fixer rule (disabled) to preserve @var type annotations
- Fixed PHPStan: User::getUserIdentifier non-empty-string, AIPipelineJob nullable missingFields
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 05:19:20 +00:00
|
|
|
public function isOutOfStock(): bool
|
|
|
|
|
{
|
|
|
|
|
return 0 === $this->stock;
|
|
|
|
|
}
|
2026-05-14 04:28:06 +00:00
|
|
|
|
|
|
|
|
public function getMainPhoto(): ?ArticlePhoto
|
|
|
|
|
{
|
|
|
|
|
foreach ($this->photos as $photo) {
|
|
|
|
|
if ($photo->isMain()) {
|
|
|
|
|
return $photo;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
feat: implement Plan 2 — Article Management API
- 6 new domain repository interfaces (StoragePath, ArticlePhoto, AttributeValue, ChannelField, ArticleTypePlatformConfig, AttributeMapping)
- 6 Doctrine repository implementations
- StorageManager with multi-path quota-aware file storage (LocalStorageManager)
- Application services: ArticleTypeService, ArticleService, ArticleValidator, PhotoService, PlatformService, MappingService
- REST controllers: ArticleType, Article, Photo, Platform, Mapping (all under /api prefix)
- inventory_number sequence migration
- 22 unit tests passing, PHPStan level 9 clean, CS Fixer clean
- Fixed phpdoc_to_comment CS Fixer rule (disabled) to preserve @var type annotations
- Fixed PHPStan: User::getUserIdentifier non-empty-string, AIPipelineJob nullable missingFields
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 05:19:20 +00:00
|
|
|
public function getId(): Uuid
|
|
|
|
|
{
|
|
|
|
|
return $this->id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function getArticleType(): ArticleType
|
|
|
|
|
{
|
|
|
|
|
return $this->articleType;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function getSku(): string
|
|
|
|
|
{
|
|
|
|
|
return $this->sku;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function getInventoryNumber(): string
|
|
|
|
|
{
|
|
|
|
|
return $this->inventoryNumber;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function getStatus(): ArticleStatus
|
|
|
|
|
{
|
|
|
|
|
return $this->status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function getStock(): int
|
|
|
|
|
{
|
|
|
|
|
return $this->stock;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function getCondition(): ArticleCondition
|
|
|
|
|
{
|
|
|
|
|
return $this->condition;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function getConditionNotes(): ?string
|
|
|
|
|
{
|
|
|
|
|
return $this->conditionNotes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function getListingPrice(): ?string
|
|
|
|
|
{
|
|
|
|
|
return $this->listingPrice;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function getSerialNumber(): ?string
|
|
|
|
|
{
|
|
|
|
|
return $this->serialNumber;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function getEbayListingId(): ?string
|
|
|
|
|
{
|
|
|
|
|
return $this->ebayListingId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function getEbayTitle(): ?string
|
|
|
|
|
{
|
|
|
|
|
return $this->ebayTitle;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function getEbayDescription(): ?string
|
|
|
|
|
{
|
|
|
|
|
return $this->ebayDescription;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function setAttributeValue(AttributeValue $value): void
|
|
|
|
|
{
|
|
|
|
|
foreach ($this->attributeValues as $existing) {
|
|
|
|
|
if ($existing->getAttributeDefinition()->getId()->equals($value->getAttributeDefinition()->getId())) {
|
2026-05-18 07:18:24 +00:00
|
|
|
$existing->setValue($value->getValue());
|
|
|
|
|
return;
|
feat: implement Plan 2 — Article Management API
- 6 new domain repository interfaces (StoragePath, ArticlePhoto, AttributeValue, ChannelField, ArticleTypePlatformConfig, AttributeMapping)
- 6 Doctrine repository implementations
- StorageManager with multi-path quota-aware file storage (LocalStorageManager)
- Application services: ArticleTypeService, ArticleService, ArticleValidator, PhotoService, PlatformService, MappingService
- REST controllers: ArticleType, Article, Photo, Platform, Mapping (all under /api prefix)
- inventory_number sequence migration
- 22 unit tests passing, PHPStan level 9 clean, CS Fixer clean
- Fixed phpdoc_to_comment CS Fixer rule (disabled) to preserve @var type annotations
- Fixed PHPStan: User::getUserIdentifier non-empty-string, AIPipelineJob nullable missingFields
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 05:19:20 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
$this->attributeValues->add($value);
|
|
|
|
|
}
|
2026-05-14 04:28:06 +00:00
|
|
|
|
|
|
|
|
/** @return Collection<int, AttributeValue> */
|
feat: implement Plan 2 — Article Management API
- 6 new domain repository interfaces (StoragePath, ArticlePhoto, AttributeValue, ChannelField, ArticleTypePlatformConfig, AttributeMapping)
- 6 Doctrine repository implementations
- StorageManager with multi-path quota-aware file storage (LocalStorageManager)
- Application services: ArticleTypeService, ArticleService, ArticleValidator, PhotoService, PlatformService, MappingService
- REST controllers: ArticleType, Article, Photo, Platform, Mapping (all under /api prefix)
- inventory_number sequence migration
- 22 unit tests passing, PHPStan level 9 clean, CS Fixer clean
- Fixed phpdoc_to_comment CS Fixer rule (disabled) to preserve @var type annotations
- Fixed PHPStan: User::getUserIdentifier non-empty-string, AIPipelineJob nullable missingFields
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 05:19:20 +00:00
|
|
|
public function getAttributeValues(): Collection
|
|
|
|
|
{
|
|
|
|
|
return $this->attributeValues;
|
|
|
|
|
}
|
2026-05-14 04:28:06 +00:00
|
|
|
|
|
|
|
|
/** @return Collection<int, ArticlePhoto> */
|
feat: implement Plan 2 — Article Management API
- 6 new domain repository interfaces (StoragePath, ArticlePhoto, AttributeValue, ChannelField, ArticleTypePlatformConfig, AttributeMapping)
- 6 Doctrine repository implementations
- StorageManager with multi-path quota-aware file storage (LocalStorageManager)
- Application services: ArticleTypeService, ArticleService, ArticleValidator, PhotoService, PlatformService, MappingService
- REST controllers: ArticleType, Article, Photo, Platform, Mapping (all under /api prefix)
- inventory_number sequence migration
- 22 unit tests passing, PHPStan level 9 clean, CS Fixer clean
- Fixed phpdoc_to_comment CS Fixer rule (disabled) to preserve @var type annotations
- Fixed PHPStan: User::getUserIdentifier non-empty-string, AIPipelineJob nullable missingFields
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 05:19:20 +00:00
|
|
|
public function getPhotos(): Collection
|
|
|
|
|
{
|
|
|
|
|
return $this->photos;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function setConditionNotes(?string $notes): void
|
|
|
|
|
{
|
|
|
|
|
$this->conditionNotes = $notes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function setListingPrice(?string $price): void
|
|
|
|
|
{
|
|
|
|
|
$this->listingPrice = $price;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function setSerialNumber(?string $sn): void
|
|
|
|
|
{
|
|
|
|
|
$this->serialNumber = $sn;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-18 07:18:24 +00:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
feat: implement Plan 2 — Article Management API
- 6 new domain repository interfaces (StoragePath, ArticlePhoto, AttributeValue, ChannelField, ArticleTypePlatformConfig, AttributeMapping)
- 6 Doctrine repository implementations
- StorageManager with multi-path quota-aware file storage (LocalStorageManager)
- Application services: ArticleTypeService, ArticleService, ArticleValidator, PhotoService, PlatformService, MappingService
- REST controllers: ArticleType, Article, Photo, Platform, Mapping (all under /api prefix)
- inventory_number sequence migration
- 22 unit tests passing, PHPStan level 9 clean, CS Fixer clean
- Fixed phpdoc_to_comment CS Fixer rule (disabled) to preserve @var type annotations
- Fixed PHPStan: User::getUserIdentifier non-empty-string, AIPipelineJob nullable missingFields
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 05:19:20 +00:00
|
|
|
public function setEbayListingId(?string $id): void
|
|
|
|
|
{
|
|
|
|
|
$this->ebayListingId = $id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function setEbayTitle(?string $title): void
|
|
|
|
|
{
|
|
|
|
|
$this->ebayTitle = $title;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function setEbayDescription(?string $desc): void
|
|
|
|
|
{
|
|
|
|
|
$this->ebayDescription = $desc;
|
|
|
|
|
}
|
2026-05-14 04:28:06 +00:00
|
|
|
}
|