Skip to content
134 changes: 134 additions & 0 deletions code_samples/discounts/src/Command/OrderPriceCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
<?php declare(strict_types=1);

namespace App\Command;

use Exception;
use Ibexa\Contracts\Core\Repository\PermissionResolver;
use Ibexa\Contracts\Core\Repository\UserService;
use Ibexa\Contracts\OrderManagement\OrderServiceInterface;
use Ibexa\Contracts\ProductCatalog\CurrencyServiceInterface;
use Ibexa\Contracts\ProductCatalog\PriceResolverInterface;
use Ibexa\Contracts\ProductCatalog\ProductPriceServiceInterface;
use Ibexa\Contracts\ProductCatalog\ProductServiceInterface;
use Ibexa\Contracts\ProductCatalog\Values\Price\PriceContext;
use Ibexa\Contracts\ProductCatalog\Values\Price\PriceEnvelopeInterface;
use Ibexa\Discounts\Value\Price\Stamp\DiscountStamp;
use Ibexa\OrderManagement\Discounts\Value\DiscountsData;
use Money\Money;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

final class OrderPriceCommand extends Command
{
protected static $defaultName = 'app:discounts:prices';

private PermissionResolver $permissionResolver;

private UserService $userService;

private ProductServiceInterface $productService;

private OrderServiceInterface $orderService;

private ProductPriceServiceInterface $productPriceService;

private CurrencyServiceInterface $currencyService;

private PriceResolverInterface $priceResolver;

public function __construct(
PermissionResolver $permissionResolver,
UserService $userService,
ProductServiceInterface $productService,
OrderServiceInterface $orderService,
ProductPriceServiceInterface $productPriceService,
CurrencyServiceInterface $currencyService,
PriceResolverInterface $priceResolver
) {
parent::__construct();

$this->permissionResolver = $permissionResolver;
$this->userService = $userService;
$this->productService = $productService;
$this->orderService = $orderService;
$this->productPriceService = $productPriceService;
$this->currencyService = $currencyService;
$this->priceResolver = $priceResolver;
}

public function execute(InputInterface $input, OutputInterface $output): int
{
$this->permissionResolver->setCurrentUserReference($this->userService->loadUserByLogin('admin'));

$productCode = 'product_code_control_unit_0';
$orderIdentifier = '4315bc58-1e96-4f21-82a0-15f736cbc4bc';
$currencyCode = 'EUR';

$output->writeln('Product data:');
$product = $this->productService->getProduct($productCode);
$currency = $this->currencyService->getCurrencyByCode($currencyCode);

$basePrice = $this->productPriceService->getPriceByProductAndCurrency($product, $currency);
$resolvedPrice = $this->priceResolver->resolvePrice($product, new PriceContext($currency));

if ($resolvedPrice === null) {
throw new Exception('Could not resolve price for the product');
}

$output->writeln(sprintf('Base price: %s', $this->formatPrice($basePrice->getMoney())));
$output->writeln(sprintf('Discounted price: %s', $this->formatPrice($resolvedPrice->getMoney())));

if ($resolvedPrice instanceof PriceEnvelopeInterface) {
/** @var \Ibexa\Discounts\Value\Price\Stamp\DiscountStamp $discountStamp */
foreach ($resolvedPrice->all(DiscountStamp::class) as $discountStamp) {
$output->writeln(
sprintf(
'Discount applied: %s , new amount: %s',
$discountStamp->getDiscount()->getName(),
$this->formatPrice(
$discountStamp->getNewPrice()
)
)
);
}
}

$output->writeln('Order details:');

$order = $this->orderService->getOrderByIdentifier($orderIdentifier);
foreach ($order->getItems() as $item) {
/** @var ?DiscountsData $discountData */
$discountData = $item->getContext()['discount_data'] ?? null;
if ($discountData instanceof DiscountsData) {
$output->writeln(
sprintf(
'Product bought with discount: %s, base price: %s, discounted price: %s',
$item->getProduct()->getName(),
$this->formatPrice($discountData->getOriginalPrice()),
$this->formatPrice(
$item->getValue()->getUnitPriceGross()
)
)
);
} else {
$output->writeln(
sprintf(
'Product bought with original price: %s, price: %s',
$item->getProduct()->getName(),
$this->formatPrice(
$item->getValue()->getUnitPriceGross()
)
)
);
}
}

return Command::SUCCESS;
}

private function formatPrice(Money $money): string
{
return $money->getAmount() / 100.0 . ' ' . $money->getCurrency()->getCode();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php declare(strict_types=1);

namespace App\Discounts\Condition;

use Ibexa\Contracts\Discounts\Value\DiscountConditionInterface;
use Ibexa\Discounts\Value\AbstractDiscountExpressionAware;

final class IsAccountAnniversary extends AbstractDiscountExpressionAware implements DiscountConditionInterface
{
public const IDENTIFIER = 'is_account_anniversary';

public function __construct(?int $tolerance = null)
{
parent::__construct([
'tolerance' => $tolerance ?? 0,
]);
}

public function getTolerance(): int
{
return $this->getExpressionValue('tolerance');
}

public function getIdentifier(): string
{
return self::IDENTIFIER;
}

public function getExpression(): string
{
return 'is_anniversary(current_user_registration_date, tolerance)';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php declare(strict_types=1);

namespace App\Discounts\Condition;

use Ibexa\Contracts\Discounts\Value\DiscountConditionInterface;
use Ibexa\Discounts\Repository\DiscountCondition\DiscountConditionFactoryInterface;

final class IsAccountAnniversaryConditionFactory implements DiscountConditionFactoryInterface
{
public function createDiscountCondition(?array $expressionValues): DiscountConditionInterface
{
return new IsAccountAnniversary(
$expressionValues['tolerance'] ?? null
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php declare(strict_types=1);

namespace App\Discounts\ExpressionProvider;

use Ibexa\Contracts\Core\Repository\PermissionResolver;
use Ibexa\Contracts\Core\Repository\UserService;
use Ibexa\Contracts\Discounts\DiscountVariablesResolverInterface;
use Ibexa\Contracts\ProductCatalog\Values\Price\PriceContextInterface;

final class CurrentUserRegistrationDateResolver implements DiscountVariablesResolverInterface
{
private PermissionResolver $permissionResolver;

private UserService $userService;

public function __construct(PermissionResolver $permissionResolver, UserService $userService)
{
$this->permissionResolver = $permissionResolver;
$this->userService = $userService;
}

/**
* @return array{current_user_registration_date: \DateTimeInterface}
*/
public function getVariables(PriceContextInterface $priceContext): array
{
return [
'current_user_registration_date' => $this->userService->loadUser(
$this->permissionResolver->getCurrentUserReference()->getUserId()
)->getContentInfo()->publishedDate,
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php declare(strict_types=1);

namespace App\Discounts\ExpressionProvider;

use DateTimeImmutable;
use DateTimeInterface;

final class IsAnniversaryResolver
{
private const YEAR_MONTH_DAY_FORMAT = 'Y-m-d';

private const MONTH_DAY_FORMAT = 'm-d';

private const REFERENCE_YEAR = 2000;

public function __invoke(DateTimeInterface $date, int $tolerance = 0): bool
{
$d1 = $this->unifyYear(new DateTimeImmutable());
$d2 = $this->unifyYear($date);

$diff = $d1->diff($d2, true)->days;

// Check if the difference between dates is within the tolerance
return $diff <= $tolerance;
}

private function unifyYear(DateTimeInterface $date): DateTimeImmutable
{
// Create a new date using the reference year but with the same month and day
$newDate = DateTimeImmutable::createFromFormat(
self::YEAR_MONTH_DAY_FORMAT,
self::REFERENCE_YEAR . '-' . $date->format(self::MONTH_DAY_FORMAT)
);

if ($newDate === false) {
throw new \RuntimeException('Failed to unify year for date.');
}

return $newDate;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php declare(strict_types=1);

namespace App\Discounts;

use Ibexa\Contracts\Discounts\DiscountPrioritizationStrategyInterface;
use Ibexa\Contracts\Discounts\Value\Query\SortClause\UpdatedAt;

final class RecentDiscountPrioritizationStrategy implements DiscountPrioritizationStrategyInterface
{
private DiscountPrioritizationStrategyInterface $inner;

public function __construct(DiscountPrioritizationStrategyInterface $inner)
{
$this->inner = $inner;
}

public function getOrder(): array
{
return array_merge(
[new UpdatedAt()],
$this->inner->getOrder()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace App\Discounts\Rule;

use Ibexa\Contracts\Discounts\DiscountValueFormatterInterface;
use Ibexa\Contracts\Discounts\Value\DiscountRuleInterface;
use Money\Money;

final class PurchaseParityValueFormatter implements DiscountValueFormatterInterface
{
public function format(DiscountRuleInterface $discountRule, ?Money $money = null): string
{
return 'Regional discount';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php declare(strict_types=1);

namespace App\Discounts\Rule;

use Ibexa\Contracts\Discounts\Value\DiscountRuleInterface;
use Ibexa\Discounts\Value\AbstractDiscountExpressionAware;

final class PurchasingPowerParityRule extends AbstractDiscountExpressionAware implements DiscountRuleInterface
{
public const TYPE = 'purchasing_power_parity';

private const DEFAULT_PARITY_MAP = [
'default' => 100,
'germany' => 81.6,
'france' => 80,
'spain' => 69,
];

/** @param ?array<string, float> $powerParityMap */
public function __construct(?array $powerParityMap = null)
{
parent::__construct(
[
'power_parity_map' => $powerParityMap ?? self::DEFAULT_PARITY_MAP,
]
);
}

/** @return array<string, float> */
public function getMap(): array
{
return $this->getExpressionValue('power_parity_map');
}

public function getExpression(): string
{
return 'amount * (power_parity_map[get_current_region().getIdentifier()] / power_parity_map["default"])';
}

public function getType(): string
{
return self::TYPE;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php declare(strict_types=1);

namespace App\Discounts\Rule;

use Ibexa\Contracts\Discounts\Value\DiscountRuleInterface;
use Ibexa\Discounts\Repository\DiscountRule\DiscountRuleFactoryInterface;

final class PurchasingPowerParityRuleFactory implements DiscountRuleFactoryInterface
{
public function createDiscountRule(?array $expressionValues): DiscountRuleInterface
{
return new PurchasingPowerParityRule($expressionValues['power_parity_map'] ?? null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php declare(strict_types=1);

namespace App\Discounts\Step;

use Ibexa\Contracts\Discounts\Admin\Form\Data\AbstractDiscountStep;

final class AnniversaryConditionStep extends AbstractDiscountStep
{
public const IDENTIFIER = 'anniversary_condition_step';

public bool $enabled;

public int $tolerance;

public function __construct(bool $enabled = false, int $tolerance = 0)
{
$this->enabled = $enabled;
$this->tolerance = $tolerance;
}
}
Loading
Loading