3 Bounded Contexts (Sales, Invoicing, LegacyFulfillment) avec : - Domaines complets (agrégats, VOs, événements, invariants) - Couche application (commands, queries, ports) - Infrastructure in-memory (repos, gateway fake) - Controllers HTTP Symfony - Couplage naïf synchrone entre BC via NaiveSalesEventPublisher - 20 tests unitaires et d'intégration passants
116 lines
2.8 KiB
PHP
116 lines
2.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace MiniShop\Sales\Domain\Model;
|
|
|
|
use MiniShop\Sales\Domain\Event\OrderCancelled;
|
|
use MiniShop\Sales\Domain\Event\OrderConfirmed;
|
|
use MiniShop\Sales\Domain\Event\OrderPlaced;
|
|
use MiniShop\Sales\Domain\Exception\EmptyOrderException;
|
|
use MiniShop\Sales\Domain\Exception\InvalidOrderStateException;
|
|
|
|
final class Order
|
|
{
|
|
private OrderStatus $status;
|
|
/** @var list<OrderLine> */
|
|
private array $lines;
|
|
/** @var list<object> */
|
|
private array $domainEvents = [];
|
|
|
|
private function __construct(
|
|
public readonly OrderId $id,
|
|
public readonly CustomerId $customerId,
|
|
private readonly \DateTimeImmutable $placedAt,
|
|
) {
|
|
$this->status = OrderStatus::Draft;
|
|
$this->lines = [];
|
|
}
|
|
|
|
/**
|
|
* @param list<OrderLine> $lines
|
|
*/
|
|
public static function place(
|
|
OrderId $id,
|
|
CustomerId $customerId,
|
|
array $lines,
|
|
\DateTimeImmutable $placedAt,
|
|
): self {
|
|
if ($lines === []) {
|
|
throw EmptyOrderException::noLines();
|
|
}
|
|
|
|
$order = new self($id, $customerId, $placedAt);
|
|
$order->lines = $lines;
|
|
$order->status = OrderStatus::Placed;
|
|
|
|
$order->recordEvent(new OrderPlaced($id, $customerId, $order->total(), $placedAt));
|
|
|
|
return $order;
|
|
}
|
|
|
|
public function confirm(): void
|
|
{
|
|
if ($this->status !== OrderStatus::Placed) {
|
|
throw InvalidOrderStateException::cannotConfirm($this->status);
|
|
}
|
|
|
|
$this->status = OrderStatus::Confirmed;
|
|
$this->recordEvent(new OrderConfirmed(
|
|
$this->id,
|
|
$this->customerId,
|
|
$this->total(),
|
|
$this->lines,
|
|
));
|
|
}
|
|
|
|
public function cancel(): void
|
|
{
|
|
if ($this->status !== OrderStatus::Placed) {
|
|
throw InvalidOrderStateException::cannotCancel($this->status);
|
|
}
|
|
|
|
$this->status = OrderStatus::Cancelled;
|
|
$this->recordEvent(new OrderCancelled($this->id));
|
|
}
|
|
|
|
public function total(): Money
|
|
{
|
|
return array_reduce(
|
|
$this->lines,
|
|
static fn (Money $carry, OrderLine $line): Money => $carry->add($line->lineTotal()),
|
|
Money::zero($this->lines[0]->unitPrice->currency),
|
|
);
|
|
}
|
|
|
|
public function status(): OrderStatus
|
|
{
|
|
return $this->status;
|
|
}
|
|
|
|
/** @return list<OrderLine> */
|
|
public function lines(): array
|
|
{
|
|
return $this->lines;
|
|
}
|
|
|
|
public function placedAt(): \DateTimeImmutable
|
|
{
|
|
return $this->placedAt;
|
|
}
|
|
|
|
/** @return list<object> */
|
|
public function releaseEvents(): array
|
|
{
|
|
$events = $this->domainEvents;
|
|
$this->domainEvents = [];
|
|
|
|
return $events;
|
|
}
|
|
|
|
private function recordEvent(object $event): void
|
|
{
|
|
$this->domainEvents[] = $event;
|
|
}
|
|
}
|