Step 00 — Squelette + intégration naïve

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
This commit is contained in:
2026-03-04 00:27:15 +01:00
commit a4a14e441b
86 changed files with 7059 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
namespace MiniShop\Tests\Unit\Invoicing\Domain;
use MiniShop\Invoicing\Domain\Event\InvoiceIssued;
use MiniShop\Invoicing\Domain\Model\BillingParty;
use MiniShop\Invoicing\Domain\Model\Invoice;
use MiniShop\Invoicing\Domain\Model\InvoiceId;
use MiniShop\Invoicing\Domain\Model\InvoiceLine;
use MiniShop\Invoicing\Domain\Model\InvoiceStatus;
use MiniShop\Invoicing\Domain\Model\TaxRate;
use PHPUnit\Framework\TestCase;
final class InvoiceTest extends TestCase
{
public function test_issue_invoice_for_external_order(): void
{
$invoice = $this->issueInvoice();
self::assertSame(InvoiceStatus::Issued, $invoice->status());
self::assertSame('ext-order-1', $invoice->externalOrderId);
self::assertCount(1, $invoice->lines());
}
public function test_issue_invoice_records_event(): void
{
$invoice = $this->issueInvoice();
$events = $invoice->releaseEvents();
self::assertCount(1, $events);
self::assertInstanceOf(InvoiceIssued::class, $events[0]);
}
public function test_invoice_without_lines_throws(): void
{
$this->expectException(\DomainException::class);
Invoice::issueForExternalOrder(
InvoiceId::fromString('inv-1'),
'INV-000001',
'ext-order-1',
new BillingParty('Acme', '1 Rue Test'),
[],
new \DateTimeImmutable(),
);
}
public function test_total_coherent_with_lines(): void
{
$invoice = Invoice::issueForExternalOrder(
InvoiceId::fromString('inv-1'),
'INV-000001',
'ext-order-1',
new BillingParty('Acme', '1 Rue Test'),
[
new InvoiceLine('Widget', 2, 1000, 'EUR', TaxRate::standard()),
new InvoiceLine('Gadget', 1, 2500, 'EUR', TaxRate::standard()),
],
new \DateTimeImmutable(),
);
self::assertSame(4500, $invoice->totalExclTax());
self::assertSame(5400, $invoice->totalInclTax());
}
public function test_mark_as_sent(): void
{
$invoice = $this->issueInvoice();
$invoice->markAsSent();
self::assertSame(InvoiceStatus::Sent, $invoice->status());
}
public function test_mark_as_sent_twice_throws(): void
{
$this->expectException(\DomainException::class);
$invoice = $this->issueInvoice();
$invoice->markAsSent();
$invoice->markAsSent();
}
private function issueInvoice(): Invoice
{
return Invoice::issueForExternalOrder(
InvoiceId::fromString('inv-1'),
'INV-000001',
'ext-order-1',
new BillingParty('Acme', '1 Rue Test'),
[new InvoiceLine('Widget', 2, 1500, 'EUR', TaxRate::standard())],
new \DateTimeImmutable(),
);
}
}

View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace MiniShop\Tests\Unit\LegacyFulfillment\Domain;
use MiniShop\LegacyFulfillment\Domain\Event\ShipmentRequested;
use MiniShop\LegacyFulfillment\Domain\Model\LegacyOrderRef;
use MiniShop\LegacyFulfillment\Domain\Model\ParcelSpec;
use MiniShop\LegacyFulfillment\Domain\Model\ShipmentRequest;
use MiniShop\LegacyFulfillment\Domain\Model\ShipmentStatus;
use MiniShop\LegacyFulfillment\Domain\Model\ShippingAddress;
use PHPUnit\Framework\TestCase;
final class ShipmentRequestTest extends TestCase
{
public function test_create_from_sales_order(): void
{
$request = $this->createRequest();
self::assertSame(ShipmentStatus::Requested, $request->status());
self::assertSame('order-1', $request->orderRef->toString());
}
public function test_create_records_event(): void
{
$request = $this->createRequest();
$events = $request->releaseEvents();
self::assertCount(1, $events);
self::assertInstanceOf(ShipmentRequested::class, $events[0]);
}
public function test_mark_dispatched(): void
{
$request = $this->createRequest();
$request->markDispatched();
self::assertSame(ShipmentStatus::Dispatched, $request->status());
}
public function test_mark_dispatched_twice_throws(): void
{
$this->expectException(\DomainException::class);
$request = $this->createRequest();
$request->markDispatched();
$request->markDispatched();
}
public function test_parcel_with_zero_weight_throws(): void
{
$this->expectException(\InvalidArgumentException::class);
new ParcelSpec(0, 'Widget');
}
private function createRequest(): ShipmentRequest
{
return ShipmentRequest::fromSalesOrder(
LegacyOrderRef::fromExternalId('order-1'),
new ShippingAddress('John Doe', '1 Rue du Commerce', 'Paris', '75001', 'FR'),
new ParcelSpec(1000, 'Order order-1'),
new \DateTimeImmutable(),
);
}
}

View File

@@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
namespace MiniShop\Tests\Unit\Sales\Domain;
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;
use MiniShop\Sales\Domain\Model\CustomerId;
use MiniShop\Sales\Domain\Model\Money;
use MiniShop\Sales\Domain\Model\Order;
use MiniShop\Sales\Domain\Model\OrderId;
use MiniShop\Sales\Domain\Model\OrderLine;
use MiniShop\Sales\Domain\Model\OrderStatus;
use PHPUnit\Framework\TestCase;
final class OrderTest extends TestCase
{
public function test_place_order_with_lines(): void
{
$order = $this->placeOrder();
self::assertSame(OrderStatus::Placed, $order->status());
self::assertCount(1, $order->lines());
self::assertTrue($order->total()->isPositive());
}
public function test_place_order_records_order_placed_event(): void
{
$order = $this->placeOrder();
$events = $order->releaseEvents();
self::assertCount(1, $events);
self::assertInstanceOf(OrderPlaced::class, $events[0]);
}
public function test_place_order_without_lines_throws(): void
{
$this->expectException(EmptyOrderException::class);
Order::place(
OrderId::fromString('order-1'),
CustomerId::fromString('cust-1'),
[],
new \DateTimeImmutable(),
);
}
public function test_confirm_placed_order(): void
{
$order = $this->placeOrder();
$order->releaseEvents();
$order->confirm();
self::assertSame(OrderStatus::Confirmed, $order->status());
$events = $order->releaseEvents();
self::assertCount(1, $events);
self::assertInstanceOf(OrderConfirmed::class, $events[0]);
}
public function test_confirm_draft_order_throws(): void
{
$this->expectException(InvalidOrderStateException::class);
$order = $this->placeOrder();
$order->confirm();
$order->releaseEvents();
$order->confirm(); // already confirmed
}
public function test_cancel_placed_order(): void
{
$order = $this->placeOrder();
$order->releaseEvents();
$order->cancel();
self::assertSame(OrderStatus::Cancelled, $order->status());
$events = $order->releaseEvents();
self::assertCount(1, $events);
self::assertInstanceOf(OrderCancelled::class, $events[0]);
}
public function test_cannot_cancel_confirmed_order(): void
{
$this->expectException(InvalidOrderStateException::class);
$order = $this->placeOrder();
$order->confirm();
$order->cancel();
}
public function test_total_is_sum_of_line_totals(): void
{
$order = Order::place(
OrderId::fromString('order-1'),
CustomerId::fromString('cust-1'),
[
new OrderLine('Widget', 2, new Money(1000, 'EUR')),
new OrderLine('Gadget', 1, new Money(2500, 'EUR')),
],
new \DateTimeImmutable(),
);
self::assertTrue($order->total()->equals(new Money(4500, 'EUR')));
}
private function placeOrder(): Order
{
return Order::place(
OrderId::fromString('order-1'),
CustomerId::fromString('cust-1'),
[new OrderLine('Widget', 2, new Money(1500, 'EUR'))],
new \DateTimeImmutable(),
);
}
}