[PriceHistory][API] Add channel setup implementation

This commit is contained in:
Rafikooo 2023-03-24 18:10:06 +01:00
parent b03534491e
commit 3d2ae584f9
No known key found for this signature in database
GPG key ID: 4A26D8327BC2442B
13 changed files with 269 additions and 3 deletions

View file

@ -16,6 +16,7 @@ Feature: Adding a new channel
And I specify its code as "MOBILE"
And I name it "Mobile channel"
And I choose "Euro" as the base currency
And I make it available in "English (United States)"
And I choose "English (United States)" as a default locale
And I select the "Order items based" as tax calculation strategy
And I add it
@ -33,6 +34,7 @@ Feature: Adding a new channel
And I set its contact phone number as "11331122"
And I define its color as "blue"
And I choose "Euro" as the base currency
And I make it available in "English (United States)"
And I choose "English (United States)" as a default locale
And I choose "United States" and "Poland" as operating countries
And I select the "Order items based" as tax calculation strategy

View file

@ -16,6 +16,7 @@ Feature: Adding a new channel with shop billing data
And I specify its code as "MOBILE"
And I name it "Mobile channel"
And I choose "Euro" as the base currency
And I make it available in "English (United States)"
And I choose "English (United States)" as a default locale
And I select the "Order items based" as tax calculation strategy
And I specify company as "Ragnarok"

View file

@ -17,6 +17,7 @@ Feature: Adding a new channel with menu taxon
And I specify its code as "MOBILE"
And I name it "Mobile channel"
And I choose "Euro" as the base currency
And I make it available in "English (United States)"
And I choose "English (United States)" as a default locale
And I select the "Order items based" as tax calculation strategy
And I specify menu taxon as "Clothes"

View file

@ -14,6 +14,7 @@ Feature: Choosing a required address in the checkout for a channel
And I specify its code as "MOBILE"
And I name it "Mobile Store"
And I choose "USD" as the base currency
And I make it available in "English (United States)"
And I choose "English (United States)" as a default locale
And I select the "Order items based" as tax calculation strategy
And I choose shipping address as a required address in the checkout

View file

@ -265,8 +265,8 @@ final class ManagingChannelsContext implements Context
public function iShouldBeNotifiedThatItHasBeenSuccessfullyCreated(): void
{
Assert::true(
$this->responseChecker->isCreationSuccessful($this->client->getLastResponse()),
'Channel could not be created',
$this->responseChecker->isCreationSuccessful($response = $this->client->getLastResponse()),
'Channel could not be created: ' . $response->getContent(),
);
}

View file

@ -42,6 +42,7 @@
<attribute name="normalization_context">
<attribute name="groups">admin:channel:read</attribute>
</attribute>
<attribute name="validation_groups">sylius</attribute>
</collectionOperation>
</collectionOperations>
@ -77,6 +78,7 @@
<attribute name="normalization_context">
<attribute name="groups">admin:channel:read</attribute>
</attribute>
<attribute name="validation_groups">sylius</attribute>
</itemOperation>
</itemOperations>

View file

@ -111,5 +111,20 @@
<group>admin:channel:read</group>
<group>admin:channel:create</group>
</attribute>
<attribute name="lowestPriceForDiscountedProductsVisible">
<group>admin:channel:read</group>
<group>admin:channel:create</group>
<group>admin:channel:update</group>
</attribute>
<attribute name="lowestPriceForDiscountedProductsCheckingPeriod">
<group>admin:channel:read</group>
<group>admin:channel:create</group>
<group>admin:channel:update</group>
</attribute>
<attribute name="taxonsExcludedFromShowingLowestPrice">
<group>admin:channel:read</group>
<group>admin:channel:create</group>
<group>admin:channel:update</group>
</attribute>
</class>
</serializer>

View file

@ -102,5 +102,10 @@
</argument>
<tag name="serializer.normalizer"/>
</service>
<service id="Sylius\Bundle\ApiBundle\Serializer\ChannelExcludedTaxonsDenormalizer">
<argument type="service" id="api_platform.iri_converter" />
<tag name="serializer.normalizer" priority="64" />
</service>
</services>
</container>

View file

@ -0,0 +1,62 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\ApiBundle\Serializer;
use ApiPlatform\Core\Api\IriConverterInterface;
use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Core\Model\TaxonInterface;
use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
use Webmozart\Assert\Assert;
final class ChannelExcludedTaxonsDenormalizer implements ContextAwareDenormalizerInterface, DenormalizerAwareInterface
{
use DenormalizerAwareTrait;
private const ALREADY_CALLED = 'sylius_channel_excluded_taxons_denormalizer_already_called';
public function __construct(private IriConverterInterface $iriConverter)
{
}
public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool
{
return
!isset($context[self::ALREADY_CALLED]) &&
is_array($data) &&
is_a($type, ChannelInterface::class, true)
;
}
public function denormalize(mixed $data, string $type, string $format = null, array $context = [])
{
$context[self::ALREADY_CALLED] = true;
$data = (array) $data;
$channel = $this->denormalizer->denormalize($data, $type, $format, $context);
Assert::isInstanceOf($channel, ChannelInterface::class);
$channel->clearTaxonsExcludedFromShowingLowestPrice();
foreach ($data['taxonsExcludedFromShowingLowestPrice'] ?? [] as $excludedTaxonIri) {
/** @var TaxonInterface $taxon */
$taxon = $this->iriConverter->getItemFromIri($excludedTaxonIri);
$channel->addTaxonExcludedFromShowingLowestPrice($taxon);
}
return $channel;
}
}

View file

@ -0,0 +1,147 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace spec\Sylius\Bundle\ApiBundle\Serializer;
use ApiPlatform\Core\Api\IriConverterInterface;
use Doctrine\Common\Collections\ArrayCollection;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Core\Model\TaxonInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
final class ChannelExcludedTaxonsDenormalizerSpec extends ObjectBehavior
{
private const ALREADY_CALLED = 'sylius_channel_excluded_taxons_denormalizer_already_called';
function let(IriConverterInterface $iriConverter): void
{
$this->beConstructedWith($iriConverter);
}
function it_does_not_support_denormalization_when_the_denormalizer_has_already_been_called(): void
{
$this->supportsDenormalization([], 'string', context: [self::ALREADY_CALLED => true])->shouldReturn(false);
}
function it_does_not_support_denormalization_when_data_is_not_an_array(): void
{
$this->supportsDenormalization('string', 'string')->shouldReturn(false);
}
function it_does_not_support_denormalization_when_type_is_not_a_channel(): void
{
$this->supportsDenormalization([], 'string')->shouldReturn(false);
}
function it_throws_an_exception_when_denormalizing_an_object_that_is_not_a_channel(
DenormalizerInterface $denormalizer,
): void {
$this->setDenormalizer($denormalizer);
$denormalizer->denormalize([], 'string', null, [self::ALREADY_CALLED => true])->willReturn(new \stdClass());
$this->shouldThrow(\InvalidArgumentException::class)->during('denormalize', [[], 'string']);
}
function it_adds_excluded_taxons_from_data(
DenormalizerInterface $denormalizer,
IriConverterInterface $iriConverter,
TaxonInterface $firstTaxon,
TaxonInterface $secondTaxon,
ChannelInterface $channel,
): void {
$this->setDenormalizer($denormalizer);
$data = ['taxonsExcludedFromShowingLowestPrice' => [
'/api/v2/taxons/first-new-taxon',
'/api/v2/taxons/second-new-taxon',
]];
$channel->getTaxonsExcludedFromShowingLowestPrice()->willReturn(new ArrayCollection());
$denormalizer->denormalize($data, 'string', null, [self::ALREADY_CALLED => true])->willReturn($channel);
$channel->clearTaxonsExcludedFromShowingLowestPrice()->shouldBeCalled();
$iriConverter->getItemFromIri('/api/v2/taxons/first-new-taxon')->shouldBeCalledTimes(1)->willReturn($firstTaxon);
$iriConverter->getItemFromIri('/api/v2/taxons/second-new-taxon')->shouldBeCalledTimes(1)->willReturn($secondTaxon);
$channel->addTaxonExcludedFromShowingLowestPrice($firstTaxon)->shouldBeCalledTimes(1);
$channel->addTaxonExcludedFromShowingLowestPrice($secondTaxon)->shouldBeCalledTimes(1);
$this->denormalize($data, 'string')->shouldReturn($channel);
}
function it_removes_excluded_taxons_when_data_has_none(
DenormalizerInterface $denormalizer,
IriConverterInterface $iriConverter,
TaxonInterface $firstTaxon,
TaxonInterface $secondTaxon,
ChannelInterface $channel,
): void {
$this->setDenormalizer($denormalizer);
$data = [];
$denormalizer->denormalize($data, 'string', null, [self::ALREADY_CALLED => true])->willReturn($channel);
$channel->getTaxonsExcludedFromShowingLowestPrice()->willReturn(new ArrayCollection([
$firstTaxon->getWrappedObject(),
$secondTaxon->getWrappedObject(),
]));
$channel->clearTaxonsExcludedFromShowingLowestPrice()->shouldBeCalled();
$iriConverter->getItemFromIri(Argument::cetera())->shouldNotBeCalled();
$channel->addTaxonExcludedFromShowingLowestPrice(Argument::any())->shouldNotBeCalled();
$this->denormalize($data, 'string')->shouldReturn($channel);
}
function it_replaces_current_excluded_taxons_with_ones_from_data(
DenormalizerInterface $denormalizer,
IriConverterInterface $iriConverter,
TaxonInterface $firstCurrentTaxon,
TaxonInterface $secondCurrentTaxon,
TaxonInterface $firstNewTaxon,
TaxonInterface $secondNewTaxon,
ChannelInterface $channel,
): void {
$this->setDenormalizer($denormalizer);
$data = ['taxonsExcludedFromShowingLowestPrice' => [
'/api/v2/taxons/first-new-taxon',
'/api/v2/taxons/second-new-taxon',
]];
$denormalizer->denormalize($data, 'string', null, [self::ALREADY_CALLED => true])->willReturn($channel);
$channel->getTaxonsExcludedFromShowingLowestPrice()->willReturn(new ArrayCollection([
$firstCurrentTaxon->getWrappedObject(),
$secondCurrentTaxon->getWrappedObject(),
]));
$channel->clearTaxonsExcludedFromShowingLowestPrice()->shouldBeCalled();
$iriConverter->getItemFromIri('/api/v2/taxons/first-new-taxon')->shouldBeCalledTimes(1)->willReturn($firstNewTaxon);
$iriConverter->getItemFromIri('/api/v2/taxons/second-new-taxon')->shouldBeCalledTimes(1)->willReturn($secondNewTaxon);
$channel->addTaxonExcludedFromShowingLowestPrice($firstNewTaxon)->shouldBeCalledTimes(1);
$channel->addTaxonExcludedFromShowingLowestPrice($secondNewTaxon)->shouldBeCalledTimes(1);
$this->denormalize($data, 'string')->shouldReturn($channel);
}
}

View file

@ -4,5 +4,8 @@ sylius:
not_blank: Please enter channel code.
regex: Channel code can only be comprised of letters, numbers, dashes and underscores.
unique: Channel code has to be unique.
lowest_price_for_discounted_products_checking_period:
greater_than: 'Value must be greater than {{ compared_value }}'
less_than: 'Value must be less than {{ compared_value }}'
name:
not_blank: Please enter channel name.

View file

@ -24,7 +24,7 @@
<argument type="service" id="Sylius\Bundle\CoreBundle\PriceHistory\Processor\ProductLowestPriceBeforeDiscountProcessorInterface" />
<argument type="service" id="sylius.repository.channel_pricing" />
<argument>%sylius_core.price_history.batch_size%</argument>
<tag name="sylius_price_history.entity_observer" />
<tag name="sylius.entity_observer" />
</service>
<service id="sylius.listener.channel_pricing_change" class="Sylius\Bundle\CoreBundle\PriceHistory\EventListener\OnFlushEntityObserverListener">

View file

@ -54,5 +54,32 @@
</option>
</constraint>
</property>
<getter property="lowestPriceForDiscountedProductsCheckingPeriod">
<constraint name="NotNull">
<option name="groups">
<value>sylius</value>
</option>
</constraint>
<constraint name="Type">
<option name="value">int</option>
<option name="groups">
<value>sylius</value>
</option>
</constraint>
<constraint name="LessThan">
<option name="value">2147483647</option>
<option name="groups">
<value>sylius</value>
</option>
<option name="message">sylius.channel.lowest_price_for_discounted_products_checking_period.less_than</option>
</constraint>
<constraint name="GreaterThan">
<option name="value">0</option>
<option name="groups">
<value>sylius</value>
</option>
<option name="message">sylius.channel.lowest_price_for_discounted_products_checking_period.greater_than</option>
</constraint>
</getter>
</class>
</constraint-mapping>