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.
281 lines
9.1 KiB
PHP
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);
|
|
});
|
|
});
|