- Removed fully qualified names for better readability. - Streamlined code by using direct class references.
304 lines
10 KiB
PHP
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);
|
|
});
|
|
});
|