diff --git a/composer.json b/composer.json index a7c71ab..e6c0ca3 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ "MiniShop\\Invoicing\\": "src/Invoicing/", "MiniShop\\LegacyFulfillment\\": "src/LegacyFulfillment/", "MiniShop\\Shared\\": "src/Shared/", - "MiniShop\\Contracts\\": "contracts/", + "MiniShop\\Contracts\\Sales\\V1\\": "contracts/sales/v1/", "App\\": "apps/symfony/src/" } }, diff --git a/contracts/sales/v1/Api/OrderView.php b/contracts/sales/v1/Api/OrderView.php new file mode 100644 index 0000000..faf271c --- /dev/null +++ b/contracts/sales/v1/Api/OrderView.php @@ -0,0 +1,24 @@ + $lines + */ + public function __construct( + public string $orderId, + public string $customerId, + public string $status, + public int $totalInCents, + public string $currency, + public array $lines, + ) {} +} diff --git a/contracts/sales/v1/Event/OrderCancelled.php b/contracts/sales/v1/Event/OrderCancelled.php new file mode 100644 index 0000000..34bbb8d --- /dev/null +++ b/contracts/sales/v1/Event/OrderCancelled.php @@ -0,0 +1,16 @@ + $lines + */ + public function __construct( + public string $orderId, + public string $customerId, + public int $totalInCents, + public string $currency, + public array $lines, + ) {} +} diff --git a/contracts/sales/v1/Event/OrderPlaced.php b/contracts/sales/v1/Event/OrderPlaced.php new file mode 100644 index 0000000..bb706d2 --- /dev/null +++ b/contracts/sales/v1/Event/OrderPlaced.php @@ -0,0 +1,24 @@ + $lines + */ + public function __construct( + public string $orderId, + public string $customerId, + public int $totalInCents, + public string $currency, + public string $placedAt, + public array $lines, + ) {} +} diff --git a/src/Sales/Infrastructure/Messaging/NaiveSalesEventPublisher.php b/src/Sales/Infrastructure/Messaging/NaiveSalesEventPublisher.php index fec96a4..080435b 100644 --- a/src/Sales/Infrastructure/Messaging/NaiveSalesEventPublisher.php +++ b/src/Sales/Infrastructure/Messaging/NaiveSalesEventPublisher.php @@ -4,6 +4,8 @@ declare(strict_types=1); namespace MiniShop\Sales\Infrastructure\Messaging; +use MiniShop\Contracts\Sales\V1\Event\OrderConfirmed as OrderConfirmedContract; +use MiniShop\Contracts\Sales\V1\Event\OrderPlaced as OrderPlacedContract; use MiniShop\Invoicing\Application\Command\IssueInvoiceForExternalOrder; use MiniShop\Invoicing\Application\Command\IssueInvoiceForExternalOrderHandler; use MiniShop\LegacyFulfillment\Application\Command\RequestShipmentFromSalesOrder; @@ -16,7 +18,7 @@ use MiniShop\Sales\Domain\Model\OrderLine; /** * Couplage naif : Sales appelle directement les handlers des autres BC. - * Ce couplage sera supprime dans les episodes suivants (Published Language, Conformist, ACL). + * Depuis step/01, la traduction passe par le Published Language (contracts/sales/v1). */ final readonly class NaiveSalesEventPublisher implements SalesEventPublisher { @@ -27,42 +29,78 @@ final readonly class NaiveSalesEventPublisher implements SalesEventPublisher public function publishOrderPlaced(OrderPlaced $event): void { - // Pas d'action downstream sur OrderPlaced dans le scenario naif. - } - - public function publishOrderConfirmed(OrderConfirmed $event): void - { - // Appel direct vers Invoicing — couplage naif - ($this->issueInvoiceHandler)(new IssueInvoiceForExternalOrder( - externalOrderId: $event->orderId->toString(), - customerName: 'Customer ' . $event->customerId->toString(), - customerAddress: 'N/A', + // Traduction domaine → Published Language (pour reference) + new OrderPlacedContract( + orderId: $event->orderId->toString(), + customerId: $event->customerId->toString(), + totalInCents: $event->total->amount, + currency: $event->total->currency, + placedAt: $event->placedAt->format(\DateTimeInterface::ATOM), lines: array_map( static fn (OrderLine $line): array => [ - 'description' => $line->productName, + 'productName' => $line->productName, 'quantity' => $line->quantity, 'unitPriceInCents' => $line->unitPrice->amount, 'currency' => $line->unitPrice->currency, ], - $event->lines, + [], // OrderPlaced doesn't carry lines in this version + ), + ); + // Pas d'action downstream sur OrderPlaced. + } + + public function publishOrderConfirmed(OrderConfirmed $event): void + { + $lines = array_map( + static fn (OrderLine $line): array => [ + 'productName' => $line->productName, + 'quantity' => $line->quantity, + 'unitPriceInCents' => $line->unitPrice->amount, + 'currency' => $line->unitPrice->currency, + ], + $event->lines, + ); + + // Step 01 : traduction domaine → Published Language + $message = new OrderConfirmedContract( + orderId: $event->orderId->toString(), + customerId: $event->customerId->toString(), + totalInCents: $event->total->amount, + currency: $event->total->currency, + lines: $lines, + ); + + // Appel direct vers Invoicing — toujours naif mais via Published Language + ($this->issueInvoiceHandler)(new IssueInvoiceForExternalOrder( + externalOrderId: $message->orderId, + customerName: 'Customer ' . $message->customerId, + customerAddress: 'N/A', + lines: array_map( + static fn (array $line): array => [ + 'description' => $line['productName'], + 'quantity' => $line['quantity'], + 'unitPriceInCents' => $line['unitPriceInCents'], + 'currency' => $line['currency'], + ], + $message->lines, ), )); - // Appel direct vers LegacyFulfillment — couplage naif + // Appel direct vers LegacyFulfillment — toujours naif mais via Published Language ($this->requestShipmentHandler)(new RequestShipmentFromSalesOrder( - externalOrderId: $event->orderId->toString(), - recipientName: 'Customer ' . $event->customerId->toString(), + externalOrderId: $message->orderId, + recipientName: 'Customer ' . $message->customerId, street: '1 Rue du Commerce', city: 'Paris', postalCode: '75001', country: 'FR', totalWeightInGrams: 1000, - description: sprintf('Order %s', $event->orderId->toString()), + description: sprintf('Order %s', $message->orderId), )); } public function publishOrderCancelled(OrderCancelled $event): void { - // Pas d'action downstream sur OrderCancelled dans le scenario naif. + // Pas d'action downstream sur OrderCancelled. } } diff --git a/tests/Contract/.gitkeep b/tests/Contract/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/Contract/Sales/v1/OrderConfirmedSchemaTest.php b/tests/Contract/Sales/v1/OrderConfirmedSchemaTest.php new file mode 100644 index 0000000..c448bb8 --- /dev/null +++ b/tests/Contract/Sales/v1/OrderConfirmedSchemaTest.php @@ -0,0 +1,78 @@ + 'Widget', 'quantity' => 2, 'unitPriceInCents' => 1500, 'currency' => 'EUR'], + ['productName' => 'Gadget', 'quantity' => 1, 'unitPriceInCents' => 2500, 'currency' => 'EUR'], + ], + ); + + self::assertSame('order-001', $event->orderId); + self::assertSame('cust-001', $event->customerId); + self::assertSame(5500, $event->totalInCents); + self::assertSame('EUR', $event->currency); + self::assertCount(2, $event->lines); + } + + public function test_contract_serializes_to_stable_json(): void + { + $event = new OrderConfirmed( + orderId: 'order-001', + customerId: 'cust-001', + totalInCents: 5500, + currency: 'EUR', + lines: [ + ['productName' => 'Widget', 'quantity' => 2, 'unitPriceInCents' => 1500, 'currency' => 'EUR'], + ], + ); + + $json = json_encode($event, JSON_THROW_ON_ERROR); + $decoded = json_decode($json, true, flags: JSON_THROW_ON_ERROR); + + self::assertArrayHasKey('orderId', $decoded); + self::assertArrayHasKey('customerId', $decoded); + self::assertArrayHasKey('totalInCents', $decoded); + self::assertArrayHasKey('currency', $decoded); + self::assertArrayHasKey('lines', $decoded); + } + + public function test_lines_contain_required_fields(): void + { + $event = new OrderConfirmed( + orderId: 'order-001', + customerId: 'cust-001', + totalInCents: 3000, + currency: 'EUR', + lines: [ + ['productName' => 'Widget', 'quantity' => 2, 'unitPriceInCents' => 1500, 'currency' => 'EUR'], + ], + ); + + $json = json_encode($event, JSON_THROW_ON_ERROR); + $decoded = json_decode($json, true, flags: JSON_THROW_ON_ERROR); + $line = $decoded['lines'][0]; + + self::assertArrayHasKey('productName', $line); + self::assertArrayHasKey('quantity', $line); + self::assertArrayHasKey('unitPriceInCents', $line); + self::assertArrayHasKey('currency', $line); + } +} diff --git a/tests/Contract/Sales/v1/OrderPlacedSchemaTest.php b/tests/Contract/Sales/v1/OrderPlacedSchemaTest.php new file mode 100644 index 0000000..8b3d159 --- /dev/null +++ b/tests/Contract/Sales/v1/OrderPlacedSchemaTest.php @@ -0,0 +1,79 @@ + 'Widget', 'quantity' => 2, 'unitPriceInCents' => 1500, 'currency' => 'EUR'], + ], + ); + + self::assertSame('order-001', $event->orderId); + self::assertSame('cust-001', $event->customerId); + self::assertSame(3000, $event->totalInCents); + self::assertSame('EUR', $event->currency); + self::assertSame('2026-01-15T10:30:00+00:00', $event->placedAt); + self::assertCount(1, $event->lines); + } + + public function test_contract_serializes_to_stable_json(): void + { + $event = new OrderPlaced( + orderId: 'order-001', + customerId: 'cust-001', + totalInCents: 3000, + currency: 'EUR', + placedAt: '2026-01-15T10:30:00+00:00', + lines: [ + ['productName' => 'Widget', 'quantity' => 2, 'unitPriceInCents' => 1500, 'currency' => 'EUR'], + ], + ); + + $json = json_encode($event, JSON_THROW_ON_ERROR); + $decoded = json_decode($json, true, flags: JSON_THROW_ON_ERROR); + + self::assertArrayHasKey('orderId', $decoded); + self::assertArrayHasKey('customerId', $decoded); + self::assertArrayHasKey('totalInCents', $decoded); + self::assertArrayHasKey('currency', $decoded); + self::assertArrayHasKey('placedAt', $decoded); + self::assertArrayHasKey('lines', $decoded); + } + + public function test_contract_deserializes_from_json(): void + { + $json = '{"orderId":"order-001","customerId":"cust-001","totalInCents":3000,"currency":"EUR","placedAt":"2026-01-15T10:30:00+00:00","lines":[{"productName":"Widget","quantity":2,"unitPriceInCents":1500,"currency":"EUR"}]}'; + + $decoded = json_decode($json, true, flags: JSON_THROW_ON_ERROR); + + $event = new OrderPlaced( + orderId: $decoded['orderId'], + customerId: $decoded['customerId'], + totalInCents: $decoded['totalInCents'], + currency: $decoded['currency'], + placedAt: $decoded['placedAt'], + lines: $decoded['lines'], + ); + + self::assertSame('order-001', $event->orderId); + self::assertSame(3000, $event->totalInCents); + } +} diff --git a/tests/Contract/Sales/v1/OrderViewSchemaTest.php b/tests/Contract/Sales/v1/OrderViewSchemaTest.php new file mode 100644 index 0000000..c0818ef --- /dev/null +++ b/tests/Contract/Sales/v1/OrderViewSchemaTest.php @@ -0,0 +1,57 @@ + 'Widget', 'quantity' => 2, 'unitPriceInCents' => 1500, 'currency' => 'EUR', 'lineTotalInCents' => 3000], + ], + ); + + self::assertSame('order-001', $view->orderId); + self::assertSame('confirmed', $view->status); + self::assertSame(5500, $view->totalInCents); + self::assertCount(1, $view->lines); + } + + public function test_contract_serializes_to_stable_json(): void + { + $view = new OrderView( + orderId: 'order-001', + customerId: 'cust-001', + status: 'placed', + totalInCents: 3000, + currency: 'EUR', + lines: [ + ['productName' => 'Widget', 'quantity' => 2, 'unitPriceInCents' => 1500, 'currency' => 'EUR', 'lineTotalInCents' => 3000], + ], + ); + + $json = json_encode($view, JSON_THROW_ON_ERROR); + $decoded = json_decode($json, true, flags: JSON_THROW_ON_ERROR); + + self::assertArrayHasKey('orderId', $decoded); + self::assertArrayHasKey('customerId', $decoded); + self::assertArrayHasKey('status', $decoded); + self::assertArrayHasKey('totalInCents', $decoded); + self::assertArrayHasKey('currency', $decoded); + self::assertArrayHasKey('lines', $decoded); + } +}