ldc-boutique-cnc-sable/tests/Unit/Domain/Models/Entities/OrderTest.php
Jérémy Munsch 71e0d65f31
refactor(OrderService): simplify order creation from basket
Updated the OrderService to utilize the new fromBasket method for creating orders, enhancing code clarity and maintainability. Removed the ProductAvailabilityService as its functionality is now integrated into the ProductRepository.
2026-10-29 20:21:32 +01:00

281 lines
9.1 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\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);
});
});