ldc-boutique-cnc-sable/tests/Unit/Domain/Models/Entities/OrderTest.php
Jérémy Munsch c8f895c759
refactor: simplify use statements and type hints
- Removed fully qualified names for better readability.
- Streamlined code by using direct class references.
2026-10-29 20:35:35 +01:00

304 lines
10 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Unit\Domain\Models\Entities;
use Carbon\CarbonImmutable;
use Domain\Enums\OrderStatus;
use Domain\Enums\ProductBrand;
use Domain\Enums\TaxRate;
use Domain\Enums\Units\OrderUnit;
use Domain\Events\AnOrderHasBeenPlaced;
use Domain\Models\Entities\Basket;
use Domain\Models\Entities\Order;
use Domain\Models\Entities\Shop;
use Domain\Models\Entities\Shopper;
use Domain\Models\ValueObjects\BasketProduct;
use Domain\Models\ValueObjects\DeliveryAddress;
use Domain\Models\ValueObjects\DiscountRate;
use Domain\Models\ValueObjects\Email;
use Domain\Models\ValueObjects\Entities\BasketId;
use Domain\Models\ValueObjects\Entities\OrderId;
use Domain\Models\ValueObjects\Entities\ProductId;
use Domain\Models\ValueObjects\Entities\ShopId;
use Domain\Models\ValueObjects\Entities\ShopperId;
use Domain\Models\ValueObjects\IncoId;
use Domain\Models\ValueObjects\Matricule;
use Domain\Models\ValueObjects\OrderEmployee;
use Domain\Models\ValueObjects\OrderLine;
use Domain\Models\ValueObjects\OrderLineProduct;
use Domain\Models\ValueObjects\OrderReference;
use Domain\Models\ValueObjects\PickupTimeSlot;
use Domain\Models\ValueObjects\ProductWeight;
use Domain\Models\ValueObjects\ShopperEstablishmentCode;
use Domain\Models\ValueObjects\ShopperServiceCode;
use Domain\Models\ValueObjects\ShopperSiteCode;
use Money\Money;
use Ramsey\Uuid\Uuid;
function makeOrderLine(array $overrides = []): OrderLine
{
$product = new OrderLineProduct(
incoId: new IncoId(1),
originalId: new ProductId(Uuid::uuid7()),
name: $overrides['productName'] ?? 'Poulet Rôti',
weight: new ProductWeight(1600, false),
brand: ProductBrand::none,
orderUnit: OrderUnit::PCE,
price: Money::EUR(1000),
discountedPrice: Money::EUR(900),
priceKgTtc: Money::EUR(750),
taxRate: TaxRate::TVA_5_5,
);
return new OrderLine(
product: $product,
quantity: $overrides['quantity'] ?? 2,
unitPriceHt: $overrides['unitPriceHt'] ?? Money::EUR(1000),
unitDiscount: $overrides['unitDiscount'] ?? Money::EUR(100),
unitTax: $overrides['unitTax'] ?? Money::EUR(200),
totalHt: $overrides['totalHt'] ?? Money::EUR(750),
totalDiscount: $overrides['totalDiscount'] ?? Money::EUR(200),
totalTax: $overrides['totalTax'] ?? Money::EUR(99),
totalTtc: $overrides['totalTtc'] ?? Money::EUR(1899),
);
}
function makeOrder(array $overrides = []): Order
{
return new Order(
id: $overrides['id'] ?? new OrderId(Uuid::uuid7()),
shopId: $overrides['shopId'] ?? new ShopId(Uuid::uuid7()),
reference: $overrides['reference'] ?? new OrderReference(
Uuid::uuid7()
),
employee: $overrides['employee'] ?? new OrderEmployee(
matricule: new Matricule('1234567'),
firstname: 'Jean',
lastname: 'Dupont',
facility: new ShopperEstablishmentCode('ETAB01'),
site: new ShopperSiteCode('SITEA'),
service: new ShopperServiceCode('101'),
),
deliveryAddress: $overrides['deliveryAddress'] ?? new DeliveryAddress(
name: 'Cocotte Lyon',
address: '12 rue de la Paix',
postcode: '69001',
city: 'Lyon',
),
pickupTimeSlot: $overrides['pickupTimeSlot'] ?? new PickupTimeSlot(
new \DateTimeImmutable('+1 day 10:00'),
new \DateInterval('PT10M'),
),
status: $overrides['status'] ?? OrderStatus::VALIDATED,
total: $overrides['total'] ?? Money::EUR(3798),
lines: $overrides['lines'] ?? [makeOrderLine()],
);
}
describe('Order - Instantiation', function (): void {
it('can instantiate a valid order', function (): void {
// GIVEN
$line = makeOrderLine();
// WHEN
$order = makeOrder(['lines' => [$line]]);
// THEN
expect($order)->toBeInstanceOf(Order::class)
->and($order->status)->toBe(OrderStatus::VALIDATED)
->and($order->getTotal()->getAmount())->toBe('3798')
->and($order->getLines())->toHaveCount(1);
});
it('throws if total is zero or negative', function (): void {
// GIVEN
$total = Money::EUR(0);
// WHEN
$action = fn () => makeOrder(['total' => $total]);
// THEN
expect($action)->toThrow(\DomainException::class, 'Total amount must be greater than zero');
});
it('throws if lines contain non-OrderLine instances', function (): void {
// GIVEN
$lines = ['not-an-order-line'];
// WHEN
$action = fn () => makeOrder(['lines' => $lines]);
// THEN
expect($action)->toThrow(\DomainException::class, 'All lines must be instances of OrderLine');
});
});
describe('Order - Cancellation', function (): void {
it('cancels a validated order', function (): void {
// GIVEN
$order = makeOrder(['status' => OrderStatus::VALIDATED]);
// WHEN
$order->cancel();
// THEN
expect($order->status)->toBe(OrderStatus::CANCELED);
});
it('cancels a waiting order', function (): void {
// GIVEN
$order = makeOrder(['status' => OrderStatus::WAITING]);
// WHEN
$order->cancel();
// THEN
expect($order->status)->toBe(OrderStatus::CANCELED);
});
it('is idempotent when cancelling an already cancelled order', function (): void {
// GIVEN
$order = makeOrder(['status' => OrderStatus::CANCELED]);
// WHEN
$order->cancel();
// THEN
expect($order->status)->toBe(OrderStatus::CANCELED);
});
});
describe('Order - Getters immutability', function (): void {
it('returns a clone of total', function (): void {
// GIVEN
$order = makeOrder(['total' => Money::EUR(5000)]);
// WHEN
$total1 = $order->getTotal();
$total2 = $order->getTotal();
// THEN
expect($total1)->toEqual($total2);
});
it('returns clones of lines', function (): void {
// GIVEN
$order = makeOrder();
// WHEN
$lines1 = $order->getLines();
$lines2 = $order->getLines();
// THEN
expect($lines1[0])->toEqual($lines2[0]);
});
});
describe('Order - id', function (): void {
it('returns id', function (): void {
// GIVEN
$order = makeOrder();
// WHEN
$id = $order->id();
// THEN
expect($id)->toBeInstanceOf(OrderId::class);
});
});
describe('Order - fromBasket factory', function (): void {
it('creates an order from a basket with all lines mapped', function (): void {
// GIVEN
$basketId = new BasketId(Uuid::uuid7());
$basket = new Basket($basketId);
$basketProduct = new BasketProduct(
id: new ProductId(Uuid::uuid7()),
name: 'Poulet Rôti',
qty: 2,
unitPriceHt: Money::EUR(1000),
unitDiscount: Money::EUR(100),
taxRate: TaxRate::TVA_5_5,
weightGrams: 1200,
brand: ProductBrand::marie,
incoId: new IncoId(45),
orderUnit: OrderUnit::PCE,
slug: 'poulet-roti',
picture: 'images/poulet.jpg',
description: 'Un bon poulet',
weightVariable: false,
discountRate: DiscountRate::fromRatio(0.1),
priceKgTtc: Money::EUR(1500)
);
$basket->addOrUpdate($basketProduct);
$orderId = new OrderId(Uuid::uuid7());
$reference = new OrderReference(Uuid::uuid7());
$pickupTimeSlot = new PickupTimeSlot(
new \DateTimeImmutable('+1 day 10:00'),
new \DateInterval('PT10M')
);
$shopper = new Shopper(
id: new ShopperId(Uuid::uuid7()),
email: new Email('jean.dupont@example.com'),
matricule: new Matricule('1234567'),
nom: 'Dupont',
prenom: 'Jean',
startDate: CarbonImmutable::now(),
endDate: null,
etablissement: new ShopperEstablishmentCode('ETAB01'),
site: new ShopperSiteCode('SITEA'),
service: new ShopperServiceCode('101')
);
$shop = new Shop(
id: new ShopId(Uuid::uuid7()),
name: 'Cocotte Lyon',
address: '12 rue de la Paix',
postcode: '69001',
city: 'Lyon',
phone: '0478123456'
);
// WHEN
$order = Order::fromBasket($orderId, $reference, $basket, $pickupTimeSlot, $shopper, $shop);
// THEN
expect($order->id())->toBe($orderId)
->and($order->reference)->toBe($reference)
->and($order->status)->toBe(OrderStatus::VALIDATED)
->and($order->employee->firstname)->toBe('Jean')
->and($order->employee->lastname)->toBe('Dupont')
->and($order->deliveryAddress->name)->toBe('Cocotte Lyon')
->and($order->getLines())->toHaveCount(1)
->and($order->getLines()[0]->product->name)->toBe('Poulet Rôti')
->and($order->getLines()[0]->quantity)->toBe(2);
});
});
describe('Order - Domain Events', function (): void {
it('produces an AnOrderHasBeenPlaced domain event', function (): void {
// GIVEN
$order = makeOrder();
// WHEN
$event = $order->toPlacedEvent();
// THEN
expect($event)->toBeInstanceOf(AnOrderHasBeenPlaced::class)
->and($event->orderId)->toBe($order->id())
->and($event->reference)->toBe($order->reference)
->and($event->totalTtc->getAmount())->toBe($order->getTotal()->getAmount())
->and($event->byMatricule)->toBe($order->employee->matricule)
->and($event->employee->matricule)->toBe($order->employee->matricule)
->and($event->shop->name)->toBe($order->deliveryAddress->name)
->and($event->lines)->toHaveCount(count($order->getLines()))
->and($event->lines[0]->productName)->toBe($order->getLines()[0]->product->name);
});
});