diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index cc74237..abaa7d7 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -6,7 +6,7 @@ on: - cron: 5 8 * * 5 env: APP_ENV: test - DATABASE_URL: mysql://root:root@127.0.0.1/sylius?serverVersion=5.7 + DATABASE_URL: mysql://root:root@127.0.0.1/sylius?serverVersion=8.0 jobs: checks: name: PHP ${{ matrix.php-versions }} @@ -15,7 +15,7 @@ jobs: fail-fast: false matrix: operating-system: [ubuntu-latest] - php-versions: ['8.0'] + php-versions: ['8.1'] steps: - name: Start MySQL run: sudo /etc/init.d/mysql start @@ -31,13 +31,14 @@ jobs: - name: Setup node uses: actions/setup-node@v1 with: - node-version: '10.x' + node-version: '20.x' - name: Install Composer dependencies run: composer update --no-progress --no-suggest --prefer-dist --no-interaction - name: Validate composer run: composer validate --strict - - name: Check composer normalized - run: composer normalize --dry-run +# Error: Command "normalize" is not defined. +# - name: Check composer normalized +# run: composer normalize --dry-run - name: Check style run: composer check-style - name: Static analysis diff --git a/composer.json b/composer.json index 792d8cc..b9b5c32 100644 --- a/composer.json +++ b/composer.json @@ -11,38 +11,37 @@ ], "license": "MIT", "require": { - "php": "^8.0 || ^8.1", + "php": "^8.0", "beberlei/doctrineextensions": "^1.2", - "sylius/sylius": "~1.11 || ~1.12", + "sylius/sylius": "~1.11 || ~1.12 || ~1.13", "sylius/resource-bundle": "^1.6", "symfony/config": "^5.4 || ^6.0", "symfony/lock": "^5.4 || ^6.0", - "thecodingmachine/safe": "^1.1" + "thecodingmachine/safe": "^1.1", + "setono/job-status-bundle": "^0.2.3" }, "require-dev": { "roave/security-advisories": "dev-master", "setono/code-quality-pack": "^2.1.2", "setono/sylius-behat-pack": "^0.2.1", - "sylius/sylius": "~1.11 || ~1.12", - "behat/behat": "^3.7", + "behat/behat": "^1.8 || 3.6.1", "behat/mink-selenium2-driver": "^1.4", - "behat/mink": "^1.8", "dmore/behat-chrome-extension": "^1.3", "dmore/chrome-mink-driver": "^2.7", "friends-of-behat/mink": "^1.8", "friends-of-behat/mink-browserkit-driver": "^1.4", + "friends-of-behat/mink-debug-extension": "^2.0.0", "friends-of-behat/mink-extension": "^2.4", "friends-of-behat/page-object-extension": "^0.3", "friends-of-behat/suite-settings-extension": "^1.0", "friends-of-behat/symfony-extension": "^2.1", "friends-of-behat/variadic-extension": "^1.3", - "lakion/mink-debug-extension": "^2.0", "phpspec/phpspec": "^7.0", "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "0.12.88", - "phpstan/phpstan-doctrine": "0.12.37", - "phpstan/phpstan-strict-rules": "^0.12.0", - "phpstan/phpstan-webmozart-assert": "0.12.12", + "phpstan/phpstan": "1.4", + "phpstan/phpstan-doctrine": "^1.0", + "phpstan/phpstan-strict-rules": "^1.0", + "phpstan/phpstan-webmozart-assert": "^1.0", "phpunit/phpunit": "^9.5", "polishsymfonycommunity/symfony-mocker-container": "^1.0", "sylius-labs/coding-standard": "^4.0", @@ -50,7 +49,14 @@ "symfony/debug-bundle": "^5.4 || ^6.0", "symfony/dotenv": "^5.4 || ^6.0", "symfony/intl": "^5.4 || ^6.0", - "symfony/web-profiler-bundle": "^5.4 || ^6.0" + "symfony/web-profiler-bundle": "^5.4 || ^6.0", + "symfony/webpack-encore-bundle": "^1.14", + "symfony/http-foundation": "^4.4 || ^5.0.7", + "doctrine/persistence": "^2.5 || ^3.0" + }, + "conflict": { + "behat/mink-selenium2-driver": ">=1.7.0", + "doctrine/annotations": ">1.15" }, "config": { "sort-packages": true, diff --git a/src/Command/ProcessPromotionsCommand.php b/src/Command/ProcessPromotionsCommand.php index 7e69ce9..de30abf 100644 --- a/src/Command/ProcessPromotionsCommand.php +++ b/src/Command/ProcessPromotionsCommand.php @@ -6,9 +6,11 @@ use DateTimeInterface; use Doctrine\ORM\EntityRepository; -use function Safe\file_get_contents; -use function Safe\file_put_contents; -use function Safe\sprintf; +use Setono\JobStatusBundle\Entity\JobInterface; +use Setono\JobStatusBundle\Entity\Spec\LastJobWithType; +use Setono\JobStatusBundle\Factory\JobFactoryInterface; +use Setono\JobStatusBundle\Manager\JobManagerInterface; +use Setono\JobStatusBundle\Repository\JobRepositoryInterface; use Setono\SyliusCatalogPromotionPlugin\Model\PromotionInterface; use Setono\SyliusCatalogPromotionPlugin\Repository\ChannelPricingRepositoryInterface; use Setono\SyliusCatalogPromotionPlugin\Repository\ProductRepositoryInterface; @@ -22,49 +24,59 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\DependencyInjection\Container; use Webmozart\Assert\Assert; final class ProcessPromotionsCommand extends Command { use LockableTrait; + private const JOB_TYPE = 'sscp_process_promotions'; + protected static $defaultName = 'setono:sylius-catalog-promotion:process'; - /** @var ChannelPricingRepositoryInterface */ - private $channelPricingRepository; + private JobRepositoryInterface $jobRepository; + + private JobFactoryInterface $jobFactory; + + private JobManagerInterface $jobManager; - /** @var ProductRepositoryInterface */ - private $productRepository; + private ChannelPricingRepositoryInterface $channelPricingRepository; - /** @var ProductVariantRepositoryInterface */ - private $productVariantRepository; + private ProductRepositoryInterface $productRepository; - /** @var PromotionRepositoryInterface */ - private $promotionRepository; + private ProductVariantRepositoryInterface $productVariantRepository; - /** @var ServiceRegistryInterface */ - private $ruleRegistry; + private PromotionRepositoryInterface $promotionRepository; - /** @var string */ - private $logsDir; + private ServiceRegistryInterface $ruleRegistry; + private int $jobTtl; + + /** + * @param int $jobTtl the ttl we set for the job (in seconds) + */ public function __construct( + JobRepositoryInterface $jobRepository, + JobFactoryInterface $jobFactory, + JobManagerInterface $jobManager, ChannelPricingRepositoryInterface $channelPricingRepository, ProductRepositoryInterface $productRepository, ProductVariantRepositoryInterface $productVariantRepository, PromotionRepositoryInterface $promotionRepository, ServiceRegistryInterface $ruleRegistry, - string $logsDir + int $jobTtl ) { parent::__construct(); + $this->jobRepository = $jobRepository; + $this->jobFactory = $jobFactory; + $this->jobManager = $jobManager; $this->channelPricingRepository = $channelPricingRepository; $this->productRepository = $productRepository; $this->productVariantRepository = $productVariantRepository; $this->promotionRepository = $promotionRepository; $this->ruleRegistry = $ruleRegistry; - $this->logsDir = $logsDir; + $this->jobTtl = $jobTtl; } protected function configure(): void @@ -79,26 +91,48 @@ protected function execute(InputInterface $input, OutputInterface $output): int { Assert::isInstanceOf($this->productVariantRepository, EntityRepository::class); - if (!$this->lock()) { - $output->writeln('The command is already running in another process.'); + /** @var JobInterface|mixed|null $lastJob */ + $lastJob = $this->jobRepository->matchOneOrNullResult(new LastJobWithType(self::JOB_TYPE)); + Assert::nullOrIsInstanceOf($lastJob, JobInterface::class); + + if (null !== $lastJob && $lastJob->isRunning()) { + $output->writeln('The job is already running'); return 0; } - /** @var bool $force */ - $force = $input->getOption('force'); - $startTime = new \DateTime(); + $force = true === $input->getOption('force'); - /** @var PromotionInterface[] $promotions */ $promotions = $this->promotionRepository->findForProcessing(); - $promotionIds = array_map(static function (PromotionInterface $promotion): int { return (int) $promotion->getId(); }, $promotions); + if (!$force && !$this->isProcessingAllowed($promotionIds, $lastJob)) { + $output->writeln( + 'Nothing to process at the moment. Run command with --force option to force process', + OutputInterface::VERBOSITY_VERBOSE + ); + + return 0; + } + + $job = $this->jobFactory->createNew(); + $job->setExclusive(true); + $job->setType(self::JOB_TYPE); + $job->setName('Sylius Catalog Promotion plugin: Process promotions'); + $job->setTtl($this->jobTtl); + $job->setMetadataEntry('promotions', $promotionIds); + + $this->jobManager->start($job, 3); + + $startTime = $job->getStartedAt(); + Assert::notNull($startTime); + $bulkIdentifier = uniqid('bulk-', true); - $this->channelPricingRepository->resetMultiplier($startTime); + $this->channelPricingRepository->resetMultiplier($startTime, $bulkIdentifier); + $this->jobManager->advance($job); foreach ($promotions as $promotion) { $qb = $this->productVariantRepository->createQueryBuilder('o'); @@ -114,13 +148,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int continue; } - if (!$this->ruleRegistry->has($rule->getType())) { + if (!$this->ruleRegistry->has((string) $rule->getType())) { // todo should this throw an exception or give an error somewhere? continue; } /** @var RuleInterface $ruleQueryBuilder */ - $ruleQueryBuilder = $this->ruleRegistry->get($rule->getType()); + $ruleQueryBuilder = $this->ruleRegistry->get((string) $rule->getType()); $ruleQueryBuilder->filter($qb, $rule->getConfiguration()); } @@ -131,9 +165,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int do { $qb->setFirstResult($i * $bulkSize); + + /** @var array $productVariantIds */ $productVariantIds = $qb->getQuery()->getResult(); $this->channelPricingRepository->updateMultiplier( + (string) $promotion->getCode(), $promotion->getMultiplier(), $productVariantIds, $promotion->getChannelCodes(), @@ -147,33 +184,37 @@ protected function execute(InputInterface $input, OutputInterface $output): int } while (count($productVariantIds) !== 0); } - $this->channelPricingRepository->updatePrices($startTime, $bulkIdentifier); + $this->jobManager->advance($job); - $this->setExecution([ - 'start' => $startTime, - 'end' => new \DateTime(), - 'promotions' => $promotionIds, - ]); + $this->channelPricingRepository->updatePrices($bulkIdentifier); + + $this->jobManager->advance($job); + $this->jobManager->finish($job); return 0; } - private function isProcessingAllowed(array $promotionIds): bool + private function isProcessingAllowed(array $promotionIds, ?JobInterface $lastJob): bool { - // If there was no executions - we can process - $lastExecution = $this->getLastExecution(); - if (null === $lastExecution) { + // if there isn't no last job we can just process + if (null === $lastJob) { return true; } + $lastPromotionIds = $lastJob->getMetadataEntry('promotions'); + Assert::isArray($lastPromotionIds); + // If last execution promotions not exact same as found for processing // or not at the same order - we can process - if ($lastExecution['promotions'] !== $promotionIds) { + if ($lastPromotionIds !== $promotionIds) { return true; } + $lastJobStartedAt = $lastJob->getStartedAt(); + Assert::notNull($lastJobStartedAt); + // If any relevant entities were updated - we can process - if ($this->hasAnyBeenUpdatedSince($lastExecution['start'])) { + if ($this->hasAnyBeenUpdatedSince($lastJobStartedAt)) { return true; } @@ -188,45 +229,6 @@ private function hasAnyBeenUpdatedSince(DateTimeInterface $dateTime): bool $this->productVariantRepository->hasAnyBeenUpdatedSince($dateTime) || $this->channelPricingRepository->hasAnyBeenUpdatedSince($dateTime) || $this->promotionRepository->hasAnyBeenUpdatedSince($dateTime) - ; - } - - // @todo Move storing last execution data to some memory storage / cache? - private function getLastExecution(): ?array - { - $filename = $this->getExecutionLogFilename(); - - if (!file_exists($filename)) { - return null; - } - - $execution = unserialize(file_get_contents($filename), [ - 'allowed_classes' => true, - ]); - - if (false === $execution) { - return null; - } - - // validate contents - if (!isset($execution['start'], $execution['end'], $execution['promotions'])) { - return null; - } - - return $execution; - } - - private function setExecution(array $execution): void - { - $filename = $this->getExecutionLogFilename(); - - file_put_contents($filename, serialize($execution)); - } - - private function getExecutionLogFilename(): string - { - return sprintf('%s/%s.log', - $this->logsDir, Container::underscore(str_replace('\\', '', get_class($this))) - ); + ; } } diff --git a/src/Doctrine/ORM/ChannelPricingRepositoryTrait.php b/src/Doctrine/ORM/ChannelPricingRepositoryTrait.php index 360a7e3..aa3b913 100644 --- a/src/Doctrine/ORM/ChannelPricingRepositoryTrait.php +++ b/src/Doctrine/ORM/ChannelPricingRepositoryTrait.php @@ -5,6 +5,7 @@ namespace Setono\SyliusCatalogPromotionPlugin\Doctrine\ORM; use DateTimeInterface; +use Doctrine\DBAL\TransactionIsolationLevel; use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository; /** @@ -14,25 +15,76 @@ trait ChannelPricingRepositoryTrait { use HasAnyBeenUpdatedSinceTrait; - public function resetMultiplier(DateTimeInterface $dateTime): void + public function resetMultiplier(DateTimeInterface $dateTime, string $bulkIdentifier): void { \assert($this instanceof EntityRepository); - $this - ->createQueryBuilder('o') - ->update() - ->set('o.multiplier', 1) - ->set('o.bulkIdentifier', ':null') - ->set('o.updatedAt', ':updatedAt') - ->andWhere('o.multiplier != 1') - ->setParameter('null', null) - ->setParameter('updatedAt', $dateTime) - ->getQuery() - ->execute() - ; + $connection = $this->_em->getConnection(); + $oldTransactionIsolation = (int) $connection->getTransactionIsolation(); + $connection->setTransactionIsolation(TransactionIsolationLevel::READ_COMMITTED); + + do { + $connection->beginTransaction(); + + try { + $qb = $this->createQueryBuilder('o'); + $ids = $qb + ->select('o.id') + // this ensures that the loop we are in doesn't turn into an infinite loop + ->andWhere( + $qb->expr()->orX( + 'o.bulkIdentifier != :bulkIdentifier', + 'o.bulkIdentifier is null' + ) + ) + ->andWhere( + $qb->expr()->orX( + // if the multiplier is different from 1 we know that it was discounted before, and we reset it + 'o.multiplier != 1', + + // if the previous job timed out, the bulk identifier will be different from the + // bulk identifier for this run. This will ensure that they will also be handled in this run + 'o.bulkIdentifier is not null', + + // if the applied promotions is not null we know that it was discounted before, and we reset it + 'o.usedPromotions is not null' + ) + ) + ->setParameter('bulkIdentifier', $bulkIdentifier) + ->setMaxResults(100) + ->getQuery() + ->getResult() + ; + + $res = (int) $this + ->createQueryBuilder('o') + ->update() + ->set('o.multiplier', 1) + ->set('o.updatedAt', ':updatedAt') + ->set('o.bulkIdentifier', ':bulkIdentifier') + ->set('o.usedPromotions', ':null') + ->andWhere('o.id IN (:ids)') + ->setParameter('updatedAt', $dateTime) + ->setParameter('bulkIdentifier', $bulkIdentifier) + ->setParameter('null', null) + ->setParameter('ids', $ids) + ->getQuery() + ->execute() + ; + + $connection->commit(); + } catch (\Throwable $e) { + $connection->rollBack(); + + throw $e; + } + } while ($res > 0); + + $connection->setTransactionIsolation($oldTransactionIsolation); } public function updateMultiplier( + string $promotionCode, float $multiplier, array $productVariantIds, array $channelCodes, @@ -52,12 +104,36 @@ public function updateMultiplier( $qb->update() ->andWhere('channelPricing.productVariant IN (:productVariantIds)') ->andWhere('channelPricing.channelCode IN (:channelCodes)') + // this 'or' is a safety check. If the previous run timed out, but managed to update some multipliers + // this will end up in discounts being compounded. With this check we ensure we only operate on pricings + // from this run or pricings that haven't been touched + ->andWhere($qb->expr()->orX( + 'channelPricing.bulkIdentifier is null', + 'channelPricing.bulkIdentifier = :bulkIdentifier', + )) + // here is another safety check. If the promotion code is already applied, + // do not select this pricing for a discount + ->andWhere($qb->expr()->orX( + 'channelPricing.usedPromotions IS NULL', + $qb->expr()->andX( + 'channelPricing.usedPromotions NOT LIKE :promotionEnding', + 'channelPricing.usedPromotions NOT LIKE :promotionMiddle', + ) + )) ->set('channelPricing.updatedAt', ':date') ->set('channelPricing.bulkIdentifier', ':bulkIdentifier') + ->set('channelPricing.usedPromotions', "CONCAT(COALESCE(channelPricing.usedPromotions, ''), CONCAT(',', :promotion))") ->setParameter('productVariantIds', $productVariantIds) ->setParameter('channelCodes', $channelCodes) ->setParameter('date', $dateTime) ->setParameter('bulkIdentifier', $bulkIdentifier) + ->setParameter('promotion', $promotionCode) + // if you are checking for the promo code 'all_10_percent' there are two options for the applied_promotions column: + // 1. ,single_tshirt,all_10_percent + // 2. ,all_10_percent,single_tshirt + // and these two wildcards selections will handle those two options + ->setParameter('promotionEnding', '%,' . $promotionCode) + ->setParameter('promotionMiddle', '%,' . $promotionCode . ',%') ; if ($manuallyDiscountedProductsExcluded) { @@ -75,58 +151,80 @@ public function updateMultiplier( $qb->getQuery()->execute(); } - public function updatePrices(DateTimeInterface $dateTime, string $bulkIdentifier): void + public function updatePrices(string $bulkIdentifier): void { \assert($this instanceof EntityRepository); - $this->createQueryBuilder('o') - ->update() - ->set('o.price', 'ROUND(o.originalPrice * o.multiplier)') - ->andWhere('o.originalPrice is not null') - ->andWhere('o.updatedAt >= :date') - ->andWhere('o.bulkIdentifier = :bulkIdentifier') - ->setParameter('date', $dateTime) - ->setParameter('bulkIdentifier', $bulkIdentifier) - ->getQuery() - ->execute() - ; - - $this->createQueryBuilder('o') - ->update() - ->set('o.originalPrice', 'o.price') - ->set('o.price', 'ROUND(o.price * o.multiplier)') - ->andWhere('o.originalPrice is null') - ->andWhere('o.multiplier != 1') - ->andWhere('o.updatedAt >= :date') - ->andWhere('o.bulkIdentifier = :bulkIdentifier') - ->setParameter('date', $dateTime) - ->setParameter('bulkIdentifier', $bulkIdentifier) - ->getQuery() - ->execute() - ; - - $this->createQueryBuilder('o') - ->update() - ->set('o.originalPrice', ':originalPrice') - ->andWhere('o.price = o.originalPrice') - ->andWhere('o.updatedAt >= :date') - ->andWhere('o.bulkIdentifier = :bulkIdentifier') - ->setParameter('originalPrice', null) - ->setParameter('date', $dateTime) - ->setParameter('bulkIdentifier', $bulkIdentifier) - ->getQuery() - ->execute() - ; - - $this->createQueryBuilder('o') - ->update() - ->set('o.price', 'o.originalPrice') - ->andWhere('o.updatedAt <= :date') - ->andWhere('o.bulkIdentifier is null') - ->andWhere('o.originalPrice is not null') - ->setParameter('date', $dateTime) - ->getQuery() - ->execute() - ; + $connection = $this->_em->getConnection(); + $oldTransactionIsolation = (int) $connection->getTransactionIsolation(); + $connection->setTransactionIsolation(TransactionIsolationLevel::READ_COMMITTED); + + do { + $res = 0; + $connection->beginTransaction(); + + try { + // get an array of ids to work on + $ids = $this->createQueryBuilder('o') + ->select('o.id') + ->andWhere('o.bulkIdentifier = :bulkIdentifier') + ->setParameter('bulkIdentifier', $bulkIdentifier) + ->setMaxResults(100) + ->getQuery() + ->getResult(); + + // this query handles the case where an original price is set + // i.e. we have made discounts on this product before + $this->createQueryBuilder('o') + ->update() + ->set('o.price', 'ROUND(o.originalPrice * o.multiplier)') + ->andWhere('o.originalPrice is not null') + ->andWhere('o.id in (:ids)') + ->setParameter('ids', $ids) + ->getQuery() + ->execute(); + + // this query handles the case where a discount hasn't been applied before + // so we want to move the current price to the original price before changing the price + $this->createQueryBuilder('o') + ->update() + ->set('o.originalPrice', 'o.price') + ->set('o.price', 'ROUND(o.price * o.multiplier)') + ->andWhere('o.originalPrice is null') + ->andWhere('o.multiplier != 1') + ->andWhere('o.id in (:ids)') + ->setParameter('ids', $ids) + ->getQuery() + ->execute(); + + // this query sets the original price to null where the original price equals the price + $this->createQueryBuilder('o') + ->update() + ->set('o.originalPrice', ':originalPrice') + ->andWhere('o.price = o.originalPrice') + ->andWhere('o.id in (:ids)') + ->setParameter('ids', $ids) + ->setParameter('originalPrice', null) + ->getQuery() + ->execute(); + + // set the bulk identifier to null to ensure the loop will come to an end ;) + $res = (int) $this + ->createQueryBuilder('o') + ->update() + ->set('o.bulkIdentifier', ':null') + ->andWhere('o.id IN (:ids)') + ->setParameter('null', null) + ->setParameter('ids', $ids) + ->getQuery() + ->execute(); + + $connection->commit(); + } catch (\Throwable $e) { + $connection->rollBack(); + } + } while ($res > 0); + + $connection->setTransactionIsolation($oldTransactionIsolation); } } diff --git a/src/Doctrine/ORM/HasAnyBeenUpdatedSinceTrait.php b/src/Doctrine/ORM/HasAnyBeenUpdatedSinceTrait.php index 91af244..9ed4e4e 100644 --- a/src/Doctrine/ORM/HasAnyBeenUpdatedSinceTrait.php +++ b/src/Doctrine/ORM/HasAnyBeenUpdatedSinceTrait.php @@ -5,15 +5,13 @@ namespace Setono\SyliusCatalogPromotionPlugin\Doctrine\ORM; use DateTimeInterface; -use Doctrine\ORM\QueryBuilder; +use Doctrine\ORM\EntityRepository; +/** + * @mixin EntityRepository + */ trait HasAnyBeenUpdatedSinceTrait { - /** - * @return QueryBuilder - */ - abstract public function createQueryBuilder($alias, $indexBy = null); - public function hasAnyBeenUpdatedSince(DateTimeInterface $dateTime): bool { $qb = $this->createQueryBuilder('o') @@ -29,9 +27,9 @@ public function hasAnyBeenUpdatedSince(DateTimeInterface $dateTime): bool */ $updated = (int) $qb - ->andWhere('o.updatedAt is not null', 'o.updatedAt > :date') - ->getQuery() - ->getSingleScalarResult() > 0 + ->andWhere('o.updatedAt is not null', 'o.updatedAt > :date') + ->getQuery() + ->getSingleScalarResult() > 0 ; if ($updated) { @@ -44,6 +42,6 @@ public function hasAnyBeenUpdatedSince(DateTimeInterface $dateTime): bool ->andWhere('o.createdAt is not null', 'o.createdAt > :date') ->getQuery() ->getSingleScalarResult() > 0 - ; + ; } } diff --git a/src/Doctrine/ORM/PromotionRepository.php b/src/Doctrine/ORM/PromotionRepository.php index 1fb162f..344a468 100644 --- a/src/Doctrine/ORM/PromotionRepository.php +++ b/src/Doctrine/ORM/PromotionRepository.php @@ -4,9 +4,11 @@ namespace Setono\SyliusCatalogPromotionPlugin\Doctrine\ORM; -use Safe\DateTime; +use DateTime; +use Setono\SyliusCatalogPromotionPlugin\Model\PromotionInterface; use Setono\SyliusCatalogPromotionPlugin\Repository\PromotionRepositoryInterface; use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository; +use Webmozart\Assert\Assert; class PromotionRepository extends EntityRepository implements PromotionRepositoryInterface { @@ -16,7 +18,7 @@ public function findForProcessing(): array { $dt = new DateTime(); - return $this->createQueryBuilder('o') + $res = $this->createQueryBuilder('o') ->andWhere('o.enabled = true') ->andWhere('SIZE(o.channels) > 0') ->andWhere('o.startsAt is null OR o.startsAt <= :date') @@ -27,5 +29,10 @@ public function findForProcessing(): array ->getQuery() ->getResult() ; + + Assert::isArray($res); + Assert::allIsInstanceOf($res, PromotionInterface::class); + + return $res; } } diff --git a/src/EventListener/AddChannelPricingIndicesSubscriber.php b/src/EventListener/AddChannelPricingIndicesSubscriber.php index 775039c..388c874 100644 --- a/src/EventListener/AddChannelPricingIndicesSubscriber.php +++ b/src/EventListener/AddChannelPricingIndicesSubscriber.php @@ -8,8 +8,8 @@ use Doctrine\ORM\Event\LoadClassMetadataEventArgs; use Doctrine\ORM\Events; use RuntimeException; -use function Safe\sprintf; use Setono\SyliusCatalogPromotionPlugin\Model\ChannelPricingInterface; +use function sprintf; final class AddChannelPricingIndicesSubscriber implements EventSubscriber { @@ -34,6 +34,7 @@ public function loadClassMetadata(LoadClassMetadataEventArgs $event): void $columnName = $metadata->getColumnName('multiplier'); + /** @psalm-suppress PropertyTypeCoercion */ $metadata->table = array_merge_recursive([ 'indexes' => [ [ diff --git a/src/EventListener/AddTimestampableIndicesSubscriber.php b/src/EventListener/AddTimestampableIndicesSubscriber.php index 2165de5..9f0bf25 100644 --- a/src/EventListener/AddTimestampableIndicesSubscriber.php +++ b/src/EventListener/AddTimestampableIndicesSubscriber.php @@ -9,14 +9,17 @@ use Doctrine\ORM\Events; use Doctrine\ORM\Mapping\ClassMetadata; use RuntimeException; -use function Safe\sprintf; +use function sprintf; use Sylius\Component\Resource\Model\TimestampableInterface; final class AddTimestampableIndicesSubscriber implements EventSubscriber { - /** @var array */ - private $classes; + /** @var array */ + private array $classes; + /** + * @param array $classes + */ public function __construct(array $classes) { $this->classes = $classes; @@ -48,7 +51,9 @@ private static function addIndices(ClassMetadata $metadata): void if (!is_subclass_of($metadata->name, TimestampableInterface::class, true)) { throw new RuntimeException(sprintf( - 'The class %s must implement the interface, %s', $metadata->name, TimestampableInterface::class + 'The class %s must implement the interface, %s', + $metadata->name, + TimestampableInterface::class )); } @@ -65,6 +70,7 @@ private static function addIndices(ClassMetadata $metadata): void ]; } + /** @psalm-suppress PropertyTypeCoercion */ $metadata->table = array_merge_recursive([ 'indexes' => $indices, ], $metadata->table); diff --git a/src/EventListener/UpdateManuallyDiscountedPropertySubscriber.php b/src/EventListener/UpdateManuallyDiscountedPropertySubscriber.php index bda67b7..c3cec35 100644 --- a/src/EventListener/UpdateManuallyDiscountedPropertySubscriber.php +++ b/src/EventListener/UpdateManuallyDiscountedPropertySubscriber.php @@ -9,12 +9,20 @@ use Doctrine\ORM\Event\PreUpdateEventArgs; use Doctrine\ORM\Events; use Setono\SyliusCatalogPromotionPlugin\Model\ChannelPricingInterface; +use Symfony\Component\HttpFoundation\RequestStack; /** * This subscriber has the responsibility to mark a channel pricing as manually discounted if the user did so manually */ final class UpdateManuallyDiscountedPropertySubscriber implements EventSubscriber { + private RequestStack $requestStack; + + public function __construct(RequestStack $requestStack) + { + $this->requestStack = $requestStack; + } + public function getSubscribedEvents(): array { return [ @@ -25,31 +33,45 @@ public function getSubscribedEvents(): array public function prePersist(LifecycleEventArgs $event): void { - self::update($event); - } + $channelPricing = $event->getEntity(); + if (!$channelPricing instanceof ChannelPricingInterface) { + return; + } - public function preUpdate(LifecycleEventArgs $event): void - { - self::update($event); + $channelPricing->setManuallyDiscounted($channelPricing->hasDiscount()); + $this->setOrigin($channelPricing); } - private static function update(LifecycleEventArgs $event): void + public function preUpdate(PreUpdateEventArgs $event): void { $channelPricing = $event->getEntity(); if (!$channelPricing instanceof ChannelPricingInterface) { return; } - if ($event instanceof PreUpdateEventArgs - && !$event->hasChangedField('price') - && !$event->hasChangedField('originalPrice') - ) { + if (!$event->hasChangedField('price') && !$event->hasChangedField('originalPrice')) { return; } - $channelPricing->resetBulkIdentifier(); - $channelPricing->setManuallyDiscounted( - $channelPricing->hasDiscount() - ); + $channelPricing->setManuallyDiscounted($channelPricing->hasDiscount()); + $this->setOrigin($channelPricing); + } + + private function setOrigin(ChannelPricingInterface $channelPricing): void + { + if (!$channelPricing->isManuallyDiscounted()) { + return; + } + + $origin = ''; + + $request = $this->requestStack->getMasterRequest(); + if (null !== $request) { + $origin .= $request->getUri() . "\n\n"; + } + + $origin .= print_r(debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS), true); + + $channelPricing->setManuallyDiscountedOrigin($origin); } } diff --git a/src/Model/ChannelPricingInterface.php b/src/Model/ChannelPricingInterface.php index 47204a0..e0cdbd1 100644 --- a/src/Model/ChannelPricingInterface.php +++ b/src/Model/ChannelPricingInterface.php @@ -25,9 +25,20 @@ public function isManuallyDiscounted(): bool; public function setManuallyDiscounted(bool $manuallyDiscounted): void; + /** + * Returns the origin of the setManuallyDiscounted() method call if isManuallyDiscounted() is true + */ + public function getManuallyDiscountedOrigin(): ?string; + + public function setManuallyDiscountedOrigin(?string $manuallyDiscountedOrigin): void; + public function getMultiplier(): float; public function getBulkIdentifier(): ?string; public function resetBulkIdentifier(): void; + + public function getUsedPromotions(): array; + + public function addUsedPromotion(string $usedPromotion): void; } diff --git a/src/Model/ChannelPricingTrait.php b/src/Model/ChannelPricingTrait.php index 7bd5b56..4d540c7 100644 --- a/src/Model/ChannelPricingTrait.php +++ b/src/Model/ChannelPricingTrait.php @@ -24,6 +24,9 @@ trait ChannelPricingTrait */ protected $manuallyDiscounted = false; + /** @ORM\Column(type="text", nullable=true) */ + protected ?string $manuallyDiscountedOrigin = null; + /** * @ORM\Column(type="decimal", precision=8, scale=4, options={"default": 1}) * @@ -54,6 +57,13 @@ trait ChannelPricingTrait */ protected $updatedAt; + /** + * @ORM\Column(type="simple_array", nullable=true) + * + * @var array + */ + protected array $usedPromotions = []; + public function hasDiscount(): bool { return null !== $this->getOriginalPrice() @@ -92,6 +102,16 @@ public function setManuallyDiscounted(bool $manuallyDiscounted): void $this->manuallyDiscounted = $manuallyDiscounted; } + public function getManuallyDiscountedOrigin(): ?string + { + return $this->manuallyDiscountedOrigin; + } + + public function setManuallyDiscountedOrigin(?string $manuallyDiscountedOrigin): void + { + $this->manuallyDiscountedOrigin = $manuallyDiscountedOrigin; + } + public function getMultiplier(): float { return $this->multiplier; @@ -106,4 +126,15 @@ public function resetBulkIdentifier(): void { $this->bulkIdentifier = null; } + + + public function getUsedPromotions(): array + { + return $this->usedPromotions; + } + + public function addUsedPromotion(string $usedPromotion): void + { + $this->usedPromotions[] = $usedPromotion; + } } diff --git a/src/Repository/ChannelPricingRepositoryInterface.php b/src/Repository/ChannelPricingRepositoryInterface.php index ecebf11..6b30881 100644 --- a/src/Repository/ChannelPricingRepositoryInterface.php +++ b/src/Repository/ChannelPricingRepositoryInterface.php @@ -9,14 +9,15 @@ interface ChannelPricingRepositoryInterface extends HasAnyBeenUpdatedSinceRepositoryInterface { /** - * This could reset the multiplier on ALL channel pricing + * This resets the multiplier on all channel pricings */ - public function resetMultiplier(DateTimeInterface $dateTime): void; + public function resetMultiplier(DateTimeInterface $dateTime, string $bulkIdentifier): void; /** * @param bool $exclusive If true this method will overwrite the multiplier instead of multiplying it */ public function updateMultiplier( + string $promotionCode, float $multiplier, array $productVariantIds, array $channelCodes, @@ -27,7 +28,7 @@ public function updateMultiplier( ): void; /** - * This method will update ALL channel prices based on the multiplier and updated after $dateTime + * This method will update ALL channel prices in the given bulk */ - public function updatePrices(DateTimeInterface $dateTime, string $bulkIdentifier): void; + public function updatePrices(string $bulkIdentifier): void; } diff --git a/src/Resources/config/services/command.xml b/src/Resources/config/services/command.xml index 2598d4d..863ead0 100644 --- a/src/Resources/config/services/command.xml +++ b/src/Resources/config/services/command.xml @@ -2,15 +2,22 @@ + + + 1800 + + + + - %kernel.logs_dir% + %setono_sylius_catalog_promotion.command.process_promotions.job.ttl% diff --git a/src/Resources/config/services/event_listener.xml b/src/Resources/config/services/event_listener.xml index 5fc19ad..0e6c7c4 100644 --- a/src/Resources/config/services/event_listener.xml +++ b/src/Resources/config/services/event_listener.xml @@ -20,7 +20,10 @@ + class="Setono\SyliusCatalogPromotionPlugin\EventListener\UpdateManuallyDiscountedPropertySubscriber" + > + + diff --git a/tests/Application/.env b/tests/Application/.env index 7d31ffd..a2a7c5e 100644 --- a/tests/Application/.env +++ b/tests/Application/.env @@ -12,7 +12,7 @@ APP_SECRET=EDITME # Format described at http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url # For a sqlite database, use: "sqlite:///%kernel.project_dir%/var/data.db" # Set "serverVersion" to your server version to avoid edge-case exceptions and extra database calls -DATABASE_URL=mysql://root@127.0.0.1/setono_catalog_promotion_%kernel.environment%?serverVersion=5.7 +DATABASE_URL=mysql://root@127.0.0.1/setono_catalog_promotion_%kernel.environment%?serverVersion=8.0 ###< doctrine/doctrine-bundle ### ###> lexik/jwt-authentication-bundle ### diff --git a/tests/Application/config/bootstrap.php b/tests/Application/config/bootstrap.php index 8ea2af2..6bb0207 100644 --- a/tests/Application/config/bootstrap.php +++ b/tests/Application/config/bootstrap.php @@ -13,7 +13,7 @@ throw new RuntimeException('Please run "composer require symfony/dotenv" to load the ".env" files configuring the application.'); } else { // load all the .env files - (new Dotenv(true))->loadEnv(dirname(__DIR__).'/.env'); + (new Dotenv())->loadEnv(dirname(__DIR__).'/.env'); } $_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev'; diff --git a/tests/Application/config/bundles.php b/tests/Application/config/bundles.php index 4715744..f76f36b 100644 --- a/tests/Application/config/bundles.php +++ b/tests/Application/config/bundles.php @@ -29,7 +29,6 @@ Sylius\Bundle\ReviewBundle\SyliusReviewBundle::class => ['all' => true], Sylius\Bundle\CoreBundle\SyliusCoreBundle::class => ['all' => true], Sylius\Bundle\ResourceBundle\SyliusResourceBundle::class => ['all' => true], - Setono\SyliusCatalogPromotionPlugin\SetonoSyliusCatalogPromotionPlugin::class => ['all' => true], Sylius\Bundle\GridBundle\SyliusGridBundle::class => ['all' => true], winzou\Bundle\StateMachineBundle\winzouStateMachineBundle::class => ['all' => true], Sonata\BlockBundle\SonataBlockBundle::class => ['all' => true], @@ -47,6 +46,7 @@ Sylius\Bundle\ThemeBundle\SyliusThemeBundle::class => ['all' => true], Sylius\Bundle\AdminBundle\SyliusAdminBundle::class => ['all' => true], Sylius\Bundle\ShopBundle\SyliusShopBundle::class => ['all' => true], + Setono\SyliusCatalogPromotionPlugin\SetonoSyliusCatalogPromotionPlugin::class => ['all' => true], Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true, 'test_cached' => true], Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true, 'test_cached' => true], FriendsOfBehat\SymfonyExtension\Bundle\FriendsOfBehatSymfonyExtensionBundle::class => ['test' => true, 'test_cached' => true], @@ -55,4 +55,9 @@ Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true], Sylius\Bundle\ApiBundle\SyliusApiBundle::class => ['all' => true], SyliusLabs\DoctrineMigrationsExtraBundle\SyliusLabsDoctrineMigrationsExtraBundle::class => ['all' => true], + Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true], + Sylius\Calendar\SyliusCalendarBundle::class => ['all' => true], + League\FlysystemBundle\FlysystemBundle::class => ['all' => true], + BabDev\PagerfantaBundle\BabDevPagerfantaBundle::class => ['all' => true], + SyliusLabs\Polyfill\Symfony\Security\Bundle\SyliusLabsPolyfillSymfonySecurityBundle::class => ['all' => true], ]; diff --git a/tests/Application/config/packages/dev/jms_serializer.yaml b/tests/Application/config/packages/jms_serializer.yaml similarity index 84% rename from tests/Application/config/packages/dev/jms_serializer.yaml rename to tests/Application/config/packages/jms_serializer.yaml index 2f32a9b..a40b1dc 100644 --- a/tests/Application/config/packages/dev/jms_serializer.yaml +++ b/tests/Application/config/packages/jms_serializer.yaml @@ -1,5 +1,7 @@ jms_serializer: visitors: + xml_serialization: + format_output: '%kernel.debug%' json_serialization: options: - JSON_PRETTY_PRINT diff --git a/tests/Application/config/sylius/1.12/packages/security.yaml b/tests/Application/config/packages/security.yaml similarity index 100% rename from tests/Application/config/sylius/1.12/packages/security.yaml rename to tests/Application/config/packages/security.yaml diff --git a/tests/Application/config/sylius/1.11/bundles.php b/tests/Application/config/sylius/1.11/bundles.php deleted file mode 100644 index 732aa21..0000000 --- a/tests/Application/config/sylius/1.11/bundles.php +++ /dev/null @@ -1,10 +0,0 @@ - ['all' => true], - Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle::class => ['all' => true], - SyliusLabs\Polyfill\Symfony\Security\Bundle\SyliusLabsPolyfillSymfonySecurityBundle::class => ['all' => true], - Sylius\Calendar\SyliusCalendarBundle::class => ['all' => true], -]; diff --git a/tests/Application/config/sylius/1.11/packages/dev/jms_serializer.yaml b/tests/Application/config/sylius/1.11/packages/dev/jms_serializer.yaml deleted file mode 100644 index 2f32a9b..0000000 --- a/tests/Application/config/sylius/1.11/packages/dev/jms_serializer.yaml +++ /dev/null @@ -1,12 +0,0 @@ -jms_serializer: - visitors: - json_serialization: - options: - - JSON_PRETTY_PRINT - - JSON_UNESCAPED_SLASHES - - JSON_PRESERVE_ZERO_FRACTION - json_deserialization: - options: - - JSON_PRETTY_PRINT - - JSON_UNESCAPED_SLASHES - - JSON_PRESERVE_ZERO_FRACTION diff --git a/tests/Application/config/sylius/1.11/packages/jms_serializer.yaml b/tests/Application/config/sylius/1.11/packages/jms_serializer.yaml deleted file mode 100644 index ed7bc61..0000000 --- a/tests/Application/config/sylius/1.11/packages/jms_serializer.yaml +++ /dev/null @@ -1,4 +0,0 @@ -jms_serializer: - visitors: - xml_serialization: - format_output: '%kernel.debug%' diff --git a/tests/Application/config/sylius/1.11/packages/prod/jms_serializer.yaml b/tests/Application/config/sylius/1.11/packages/prod/jms_serializer.yaml deleted file mode 100644 index c288182..0000000 --- a/tests/Application/config/sylius/1.11/packages/prod/jms_serializer.yaml +++ /dev/null @@ -1,10 +0,0 @@ -jms_serializer: - visitors: - json_serialization: - options: - - JSON_UNESCAPED_SLASHES - - JSON_PRESERVE_ZERO_FRACTION - json_deserialization: - options: - - JSON_UNESCAPED_SLASHES - - JSON_PRESERVE_ZERO_FRACTION diff --git a/tests/Application/config/sylius/1.11/packages/security.yaml b/tests/Application/config/sylius/1.11/packages/security.yaml deleted file mode 100644 index ef43c09..0000000 --- a/tests/Application/config/sylius/1.11/packages/security.yaml +++ /dev/null @@ -1,138 +0,0 @@ -parameters: - sylius.security.admin_regex: "^/admin" - sylius.security.shop_regex: "^/(?!admin|new-api|api/.*|api$|media/.*)[^/]++" - sylius.security.new_api_route: "/new-api" - sylius.security.new_api_regex: "^%sylius.security.new_api_route%" - -security: - always_authenticate_before_granting: true - providers: - sylius_admin_user_provider: - id: sylius.admin_user_provider.email_or_name_based - sylius_api_admin_user_provider: - id: sylius.admin_user_provider.email_or_name_based - sylius_shop_user_provider: - id: sylius.shop_user_provider.email_or_name_based - sylius_api_shop_user_provider: - id: sylius.shop_user_provider.email_or_name_based - sylius_api_chain_provider: - chain: - providers: [sylius_api_shop_user_provider, sylius_api_admin_user_provider] - - encoders: - Sylius\Component\User\Model\UserInterface: argon2i - firewalls: - admin: - switch_user: true - context: admin - pattern: "%sylius.security.admin_regex%" - provider: sylius_admin_user_provider - form_login: - provider: sylius_admin_user_provider - login_path: sylius_admin_login - check_path: sylius_admin_login_check - failure_path: sylius_admin_login - default_target_path: sylius_admin_dashboard - use_forward: false - use_referer: true - csrf_token_generator: security.csrf.token_manager - csrf_parameter: _csrf_admin_security_token - csrf_token_id: admin_authenticate - remember_me: - secret: "%env(APP_SECRET)%" - path: /admin - name: APP_ADMIN_REMEMBER_ME - lifetime: 31536000 - remember_me_parameter: _remember_me - logout: - path: sylius_admin_logout - target: sylius_admin_login - anonymous: true - - new_api_admin_user: - pattern: "%sylius.security.new_api_route%/admin-user-authentication-token" - provider: sylius_admin_user_provider - stateless: true - anonymous: true - json_login: - check_path: "%sylius.security.new_api_route%/admin-user-authentication-token" - username_path: email - password_path: password - success_handler: lexik_jwt_authentication.handler.authentication_success - failure_handler: lexik_jwt_authentication.handler.authentication_failure - guard: - authenticators: - - lexik_jwt_authentication.jwt_token_authenticator - - new_api_shop_user: - pattern: "%sylius.security.new_api_route%/shop-user-authentication-token" - provider: sylius_shop_user_provider - stateless: true - anonymous: true - json_login: - check_path: "%sylius.security.new_api_route%/shop-user-authentication-token" - username_path: email - password_path: password - success_handler: lexik_jwt_authentication.handler.authentication_success - failure_handler: lexik_jwt_authentication.handler.authentication_failure - guard: - authenticators: - - lexik_jwt_authentication.jwt_token_authenticator - - new_api: - pattern: "%sylius.security.new_api_regex%/*" - provider: sylius_api_chain_provider - stateless: true - anonymous: lazy - guard: - authenticators: - - lexik_jwt_authentication.jwt_token_authenticator - - shop: - switch_user: { role: ROLE_ALLOWED_TO_SWITCH } - context: shop - pattern: "%sylius.security.shop_regex%" - provider: sylius_shop_user_provider - form_login: - success_handler: sylius.authentication.success_handler - failure_handler: sylius.authentication.failure_handler - provider: sylius_shop_user_provider - login_path: sylius_shop_login - check_path: sylius_shop_login_check - failure_path: sylius_shop_login - default_target_path: sylius_shop_homepage - use_forward: false - use_referer: true - csrf_token_generator: security.csrf.token_manager - csrf_parameter: _csrf_shop_security_token - csrf_token_id: shop_authenticate - remember_me: - secret: "%env(APP_SECRET)%" - name: APP_SHOP_REMEMBER_ME - lifetime: 31536000 - remember_me_parameter: _remember_me - logout: - path: sylius_shop_logout - target: sylius_shop_login - invalidate_session: false - success_handler: sylius.handler.shop_user_logout - anonymous: true - - dev: - pattern: ^/(_(profiler|wdt)|css|images|js)/ - security: false - - access_control: - - { path: "%sylius.security.admin_regex%/_partial", role: IS_AUTHENTICATED_ANONYMOUSLY, ips: [127.0.0.1, ::1] } - - { path: "%sylius.security.admin_regex%/_partial", role: ROLE_NO_ACCESS } - - { path: "%sylius.security.shop_regex%/_partial", role: IS_AUTHENTICATED_ANONYMOUSLY, ips: [127.0.0.1, ::1] } - - { path: "%sylius.security.shop_regex%/_partial", role: ROLE_NO_ACCESS } - - - { path: "%sylius.security.admin_regex%/login", role: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: "%sylius.security.shop_regex%/login", role: IS_AUTHENTICATED_ANONYMOUSLY } - - - { path: "%sylius.security.shop_regex%/register", role: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: "%sylius.security.shop_regex%/verify", role: IS_AUTHENTICATED_ANONYMOUSLY } - - - { path: "%sylius.security.admin_regex%", role: ROLE_ADMINISTRATION_ACCESS } - - { path: "%sylius.security.shop_regex%/account", role: ROLE_USER } diff --git a/tests/Application/config/sylius/1.11/packages/swiftmailer.yaml b/tests/Application/config/sylius/1.11/packages/swiftmailer.yaml deleted file mode 100644 index 79957d3..0000000 --- a/tests/Application/config/sylius/1.11/packages/swiftmailer.yaml +++ /dev/null @@ -1,7 +0,0 @@ -swiftmailer: - url: '%env(MAILER_URL)%' - disable_delivery: true - logging: true - spool: - type: file - path: "%kernel.cache_dir%/spool" diff --git a/tests/Application/config/sylius/1.12/bundles.php b/tests/Application/config/sylius/1.12/bundles.php deleted file mode 100644 index 400b877..0000000 --- a/tests/Application/config/sylius/1.12/bundles.php +++ /dev/null @@ -1,9 +0,0 @@ - ['all' => true], - SyliusLabs\Polyfill\Symfony\Security\Bundle\SyliusLabsPolyfillSymfonySecurityBundle::class => ['all' => true], - Sylius\Calendar\SyliusCalendarBundle::class => ['all' => true], -]; diff --git a/tests/Application/config/sylius/1.12/packages/dev/jms_serializer.yaml b/tests/Application/config/sylius/1.12/packages/dev/jms_serializer.yaml deleted file mode 100644 index 2f32a9b..0000000 --- a/tests/Application/config/sylius/1.12/packages/dev/jms_serializer.yaml +++ /dev/null @@ -1,12 +0,0 @@ -jms_serializer: - visitors: - json_serialization: - options: - - JSON_PRETTY_PRINT - - JSON_UNESCAPED_SLASHES - - JSON_PRESERVE_ZERO_FRACTION - json_deserialization: - options: - - JSON_PRETTY_PRINT - - JSON_UNESCAPED_SLASHES - - JSON_PRESERVE_ZERO_FRACTION diff --git a/tests/Application/config/sylius/1.12/packages/jms_serializer.yaml b/tests/Application/config/sylius/1.12/packages/jms_serializer.yaml deleted file mode 100644 index ed7bc61..0000000 --- a/tests/Application/config/sylius/1.12/packages/jms_serializer.yaml +++ /dev/null @@ -1,4 +0,0 @@ -jms_serializer: - visitors: - xml_serialization: - format_output: '%kernel.debug%' diff --git a/tests/Application/config/sylius/1.12/packages/prod/jms_serializer.yaml b/tests/Application/config/sylius/1.12/packages/prod/jms_serializer.yaml deleted file mode 100644 index c288182..0000000 --- a/tests/Application/config/sylius/1.12/packages/prod/jms_serializer.yaml +++ /dev/null @@ -1,10 +0,0 @@ -jms_serializer: - visitors: - json_serialization: - options: - - JSON_UNESCAPED_SLASHES - - JSON_PRESERVE_ZERO_FRACTION - json_deserialization: - options: - - JSON_UNESCAPED_SLASHES - - JSON_PRESERVE_ZERO_FRACTION diff --git a/tests/Application/config/sylius/1.13/bundles.php b/tests/Application/config/sylius/1.13/bundles.php new file mode 100644 index 0000000..5d54121 --- /dev/null +++ b/tests/Application/config/sylius/1.13/bundles.php @@ -0,0 +1,7 @@ + ['all' => true], +]; diff --git a/tests/Application/config/sylius/1.13/packages/_sylius.yaml b/tests/Application/config/sylius/1.13/packages/_sylius.yaml new file mode 100644 index 0000000..259552d --- /dev/null +++ b/tests/Application/config/sylius/1.13/packages/_sylius.yaml @@ -0,0 +1,7 @@ +parameters: + test_default_state_machine_adapter: 'symfony_workflow' + test_sylius_state_machine_adapter: '%env(string:default:test_default_state_machine_adapter:TEST_SYLIUS_STATE_MACHINE_ADAPTER)%' + +sylius_state_machine_abstraction: + graphs_to_adapters_mapping: + sylius_refund_refund_payment: '%test_sylius_state_machine_adapter%' diff --git a/tests/Application/package.json b/tests/Application/package.json index 8512b4d..9bba399 100644 --- a/tests/Application/package.json +++ b/tests/Application/package.json @@ -1,46 +1,52 @@ { "dependencies": { + "@babel/core": "^7.24.9", + "@babel/preset-env": "^7.14.7", "babel-polyfill": "^6.26.0", - "chart.js": "^2.9.3", - "jquery": "^3.4.0", + "chart.js": "3.9.1", + "jquery": "^3.6.0", "jquery.dirtyforms": "^2.0.0", - "lightbox2": "^2.9.0", - "semantic-ui-css": "^2.2.0", + "lightbox2": "^2.11.0", + "semantic-ui-css": "^2.4.0", "slick-carousel": "^1.8.1" }, "devDependencies": { + "@symfony/webpack-encore": "^4.0.0", "babel-core": "^6.26.3", "babel-plugin-external-helpers": "^6.22.0", - "babel-plugin-module-resolver": "^3.1.1", + "babel-plugin-module-resolver": "^3.2.0", "babel-plugin-transform-object-rest-spread": "^6.26.0", "babel-preset-env": "^1.7.0", "babel-register": "^6.26.0", "dedent": "^0.7.0", - "eslint": "^4.19.1", - "eslint-config-airbnb-base": "^12.1.0", - "eslint-import-resolver-babel-module": "^4.0.0", - "eslint-plugin-import": "^2.12.0", - "fast-async": "^6.3.7", + "eslint": "^8.22.0", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-import-resolver-babel-module": "^5.3.0", + "eslint-plugin-import": "^2.26.0", + "fast-async": "^7.0.0", "gulp": "^4.0.0", "gulp-chug": "^0.5", "gulp-concat": "^2.6.0", - "gulp-debug": "^2.1.2", - "gulp-if": "^2.0.0", - "gulp-livereload": "^3.8.1", + "gulp-debug": "^4.0.0", + "gulp-if": "^3.0.0", + "gulp-livereload": "^4.0.1", "gulp-order": "^1.1.1", - "gulp-sass": "^4.0.1", - "gulp-sourcemaps": "^1.6.0", - "gulp-uglifycss": "^1.0.5", - "merge-stream": "^1.0.0", - "rollup": "^0.60.7", - "rollup-plugin-babel": "^3.0.4", - "rollup-plugin-commonjs": "^9.1.3", - "rollup-plugin-inject": "^2.0.0", - "rollup-plugin-node-resolve": "^3.3.0", - "rollup-plugin-uglify": "^4.0.0", - "sass-loader": "^7.0.1", - "upath": "^1.1.0", - "yargs": "^6.4.0" + "gulp-sass": "^5.1.0", + "gulp-sourcemaps": "^3.0.0", + "gulp-uglifycss": "^1.0.9", + "merge-stream": "^2.0.0", + "rollup": "^3.18.0", + "rollup-plugin-babel": "^4.4.0", + "rollup-plugin-commonjs": "^10.1.0", + "rollup-plugin-inject": "^3.0.0", + "rollup-plugin-node-resolve": "^5.2.0", + "rollup-plugin-uglify": "^6.0.0", + "sass": "^1.77.0", + "sass-loader": "^13.0.0", + "upath": "^2.0.0", + "webpack": "^5.93.0", + "webpack-cli": "^5.1.4", + "yargs": "^17.7.1" }, "scripts": { "build": "gulp build",