new ProductId(Uuid::uuid7()), 'price' => new Money(1000, $currency), // 10.00 € 'tax_rate' => TaxRate::TVA_20, 'product_weight' => null, 'invoicing_unit' => InvoicingUnit::PCE, 'sell_unit' => SellUnit::PCE, 'validDiscount' => null, 'status' => true, 'orderable_start' => null, 'orderable_end' => null, 'hasInco' => false, ]; $data = array_merge($defaultAttributes, $attributes); return new Product( id: $data['id'], price: $data['price'], tax_rate: $data['tax_rate'], product_weight: $data['product_weight'], invoicing_unit: $data['invoicing_unit'], sell_unit: $data['sell_unit'], validDiscount: $data['validDiscount'] ? clone $data['validDiscount'] : null, status: $data['status'], orderable_start: $data['orderable_start'], orderable_end: $data['orderable_end'], hasInco: $data['hasInco'] ); } function createDiscount(Money $amount, bool $isPercentage = false, float $percentageRate = 0.0): DiscountValue { if ($isPercentage) { return DiscountValue::fromPercentage((int) $percentageRate); } return DiscountValue::fromAmount($amount); } function createWeight(int $grams): ProductWeight { return new ProductWeight($grams, false); } dataset('weight_ratios', [ '500g weight on 20€/kg price' => [500, 2000, '1000'], // 20.00€ * (500/1000) = 10.00€ HT '250g weight on 10€/kg price' => [250, 1000, '250'], // 10.00€ * (250/1000) = 2.50€ HT '1500g weight on 10€/kg price' => [1500, 1000, '1500'], // 10.00€ * (1500/1000) = 15.00€ HT ]); describe(PricingService::class, function (): void { it('calculates standard pricing for a product without discount', function (): void { // GIVEN $currency = new Currency('EUR'); $basePrice = new Money(1000, $currency); $product = createBaseProduct([ 'price' => $basePrice, 'tax_rate' => TaxRate::TVA_20, ]); $service = new PricingService(); // WHEN $result = $service->calculateForProduct($product); // THEN expect($result)->toBeInstanceOf(ProductPricing::class) ->and($result->basePriceHt()->getAmount())->toBe('1000') ->and($result->basePriceTtc()->getAmount())->toBe('1200') ->and($result->discountAmountHt()->getAmount())->toBe('0') ->and($result->discountedPriceHt()->getAmount())->toBe('1000') ->and($result->discountedPriceTtc()->getAmount())->toBe('1200') ->and($result->discountRate)->toBe(0.0) ->and($result->hasDiscount)->toBeFalse(); }); it('calculates base price HT depending on weight when invoicing by KGM and selling by PCE', function (int $grams, int $inputPriceCents, string $expectedPriceHtCents): void { // GIVEN $currency = new Currency('EUR'); $product = createBaseProduct([ 'price' => new Money($inputPriceCents, $currency), 'invoicing_unit' => InvoicingUnit::KGM, 'sell_unit' => SellUnit::PCE, 'product_weight' => createWeight($grams), ]); $service = new PricingService(); // WHEN $result = $service->calculateForProduct($product); // THEN expect($result->basePriceHt()->getAmount())->toBe($expectedPriceHtCents); })->with('weight_ratios'); it('applies a fixed discount amount and calculates the discount rate', function (): void { // GIVEN $currency = new Currency('EUR'); $discount = createDiscount( amount: new Money(200, $currency), isPercentage: false ); $product = createBaseProduct([ 'price' => new Money(1000, $currency), 'validDiscount' => $discount, ]); $service = new PricingService(); // WHEN $result = $service->calculateForProduct($product); // THEN expect($result->hasDiscount)->toBeTrue() ->and($result->discountAmountHt()->getAmount())->toBe('200') ->and($result->discountedPriceHt()->getAmount())->toBe('800') ->and($result->discountedPriceTtc()->getAmount())->toBe('960') ->and($result->discountRate)->toBe(20.0); }); it('applies a percentage discount and uses the specific rate from the discount object', function (): void { // GIVEN $currency = new Currency('EUR'); $discount = createDiscount( amount: new Money(150, $currency), isPercentage: true, percentageRate: 15.0 ); $product = createBaseProduct([ 'price' => new Money(1000, $currency), 'validDiscount' => $discount, ]); $service = new PricingService(); // WHEN $result = $service->calculateForProduct($product); // THEN expect($result->hasDiscount)->toBeTrue() ->and($result->discountAmountHt()->getAmount())->toBe('150') ->and($result->discountedPriceHt()->getAmount())->toBe('850') ->and($result->discountRate)->toBe(0.15); }); it('calculates the virtual price per KG TTC for a PCE product with a defined weight', function (): void { // GIVEN $currency = new Currency('EUR'); $product = createBaseProduct([ 'price' => new Money(1000, $currency), 'tax_rate' => TaxRate::TVA_20, 'invoicing_unit' => InvoicingUnit::PCE, 'product_weight' => createWeight(250), 'status' => true, ]); $service = new PricingService(); // WHEN $result = $service->calculateForProduct($product); // THEN expect($result->priceKgTtc()->getAmount())->toBe('4800'); }); it('returns standard price as price per KG TTC if the product status is false', function (): void { // GIVEN $currency = new Currency('EUR'); $product = createBaseProduct([ 'price' => new Money(1000, $currency), 'invoicing_unit' => InvoicingUnit::PCE, 'product_weight' => createWeight(250), 'status' => false, ]); $service = new PricingService(); // WHEN $result = $service->calculateForProduct($product); // THEN expect($result->priceKgTtc()->getAmount())->toBe('1200'); }); it('returns a 0.0 discount rate if base price HT is 0', function (): void { // GIVEN $currency = new Currency('EUR'); $discount = createDiscount( amount: new Money(0, $currency), isPercentage: false ); $product = createBaseProduct([ 'price' => new Money(0, $currency), 'validDiscount' => $discount, ]); $service = new PricingService(); // WHEN $result = $service->calculateForProduct($product); // THEN expect($result->discountRate)->toBe(0.0); }); });