Merge pull request #4400 from NeverResponse/harden-coupons

[Behat][Promotion] Removed softdeletable from Coupon
This commit is contained in:
Paweł Jędrzejewski 2016-03-15 20:39:03 +01:00
commit 9b61490426
28 changed files with 825 additions and 33 deletions

View file

@ -0,0 +1,47 @@
<?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.
*/
namespace Sylius\Migrations;
use Doctrine\DBAL\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
/**
* Auto-generated Migration: Please modify to your needs!
*/
class Version20160305115221 extends AbstractMigration
{
/**
* @param Schema $schema
*/
public function up(Schema $schema)
{
// this up() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('ALTER TABLE sylius_promotion_coupon DROP deleted_at');
$this->addSql('ALTER TABLE sylius_order DROP FOREIGN KEY FK_6196A1F917B24436');
$this->addSql('ALTER TABLE sylius_order ADD CONSTRAINT FK_6196A1F917B24436 FOREIGN KEY (promotion_coupon_id) REFERENCES sylius_promotion_coupon (id)');
}
/**
* @param Schema $schema
*/
public function down(Schema $schema)
{
// this down() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('ALTER TABLE sylius_order DROP FOREIGN KEY FK_6196A1F917B24436');
$this->addSql('ALTER TABLE sylius_order ADD CONSTRAINT FK_6196A1F917B24436 FOREIGN KEY (promotion_coupon_id) REFERENCES sylius_promotion_coupon (id) ON DELETE SET NULL');
$this->addSql('ALTER TABLE sylius_promotion_coupon ADD deleted_at DATETIME DEFAULT NULL');
}
}

View file

@ -17,6 +17,7 @@
<parameter key="sylius.behat.context.domain.payment.class">Sylius\Behat\Context\Domain\PaymentContext</parameter>
<parameter key="sylius.behat.context.domain.shipment.class">Sylius\Behat\Context\Domain\ShippingContext</parameter>
<parameter key="sylius.behat.context.domain.product.class">Sylius\Behat\Context\Domain\ProductContext</parameter>
<parameter key="sylius.behat.context.domain.promotion.class">Sylius\Behat\Context\Domain\PromotionContext</parameter>
</parameters>
<services>
@ -46,5 +47,12 @@
<argument type="service" id="sylius.repository.product_review" container="symfony" />
<tag name="sylius.behat.context" />
</service>
<service id="sylius.behat.context.domain.promotion" class="%sylius.behat.context.domain.promotion.class%" scope="scenario">
<argument type="service" id="sylius.behat.shared_storage" container="symfony" />
<argument type="service" id="sylius.repository.promotion" container="symfony" />
<argument type="service" id="sylius.repository.promotion_coupon" container="symfony" />
<tag name="sylius.behat.context" />
</service>
</services>
</container>

View file

@ -99,6 +99,7 @@
<service id="sylius.behat.context.setup.promotion" class="%sylius.behat.context.setup.promotion.class%" scope="scenario">
<argument type="service" id="sylius.behat.shared_storage" container="symfony" />
<argument type="service" id="sylius.factory.promotion_action" container="symfony" />
<argument type="service" id="sylius.factory.promotion_coupon" container="symfony" />
<argument type="service" id="sylius.factory.promotion_rule" container="symfony" />
<argument type="service" id="sylius.test.factory.promotion" container="symfony" />
<argument type="service" id="sylius.repository.promotion" container="symfony" />
@ -139,7 +140,7 @@
<argument type="service" id="sylius.factory.payment" container="symfony" />
<argument type="service" id="sylius.factory.order_item" container="symfony" />
<argument type="service" id="sylius.order_item_quantity_modifier" container="symfony" />
<argument type="service" id="sylius.order_processing.shipping_processor" container="symfony" />
<argument type="service" id="sylius.order_processing.order_recalculator" container="symfony" />
<argument type="service" id="doctrine.orm.entity_manager" container="symfony" />
<tag name="sylius.behat.context" />
</service>

View file

@ -25,6 +25,7 @@
<parameter key="sylius.behat.context.transform.taxon.class">Sylius\Behat\Context\Transform\TaxonContext</parameter>
<parameter key="sylius.behat.context.transform.zone.class">Sylius\Behat\Context\Transform\ZoneContext</parameter>
<parameter key="sylius.behat.context.transform.theme.class">Sylius\Behat\Context\Transform\ThemeContext</parameter>
<parameter key="sylius.behat.context.transform.promotion.class">Sylius\Behat\Context\Transform\PromotionContext</parameter>
</parameters>
<services>
<service id="sylius.behat.context.transform.customer" class="%sylius.behat.context.transform.customer.class%" scope="scenario">
@ -89,5 +90,11 @@
<argument type="service" id="sylius.repository.theme" container="symfony" />
<tag name="sylius.behat.context" />
</service>
<service id="sylius.behat.context.transform.promotion" class="%sylius.behat.context.transform.promotion.class%" scope="scenario">
<argument type="service" id="sylius.repository.promotion" container="symfony" />
<argument type="service" id="sylius.repository.promotion_coupon" container="symfony" />
<tag name="sylius.behat.context" />
</service>
</services>
</container>

View file

@ -43,5 +43,6 @@ imports:
- suites/domain/order.yml
- suites/domain/product.yml
- suites/domain/promotion.yml
- suites/cli/installer.yml

View file

@ -0,0 +1,28 @@
# This file is part of the Sylius package.
# (c) Paweł Jędrzejewski
default:
suites:
domain_promotion:
contexts_as_services:
- sylius.behat.context.hook.doctrine_orm
- sylius.behat.context.transform.shared_storage
- sylius.behat.context.transform.addressing
- sylius.behat.context.transform.customer
- sylius.behat.context.transform.payment
- sylius.behat.context.transform.product
- sylius.behat.context.transform.promotion
- sylius.behat.context.transform.shipping
- sylius.behat.context.setup.channel
- sylius.behat.context.setup.payment
- sylius.behat.context.setup.product
- sylius.behat.context.setup.promotion
- sylius.behat.context.setup.shipping
- sylius.behat.context.setup.order
- sylius.behat.context.domain.promotion
filters:
tags: @promotion && @domain

View file

@ -2,14 +2,14 @@
Feature: Deleting a coupon
In order to remove test, obsolete or incorrect coupons
As an Administrator
I want to be able to delete coupons from the registry
I want to be able to delete a coupon from the registry
Background:
Given the store operates on a single channel in "France"
And there is a promotion "Holiday promotion" with coupon "Santa's gift"
And I am logged in as administrator
And the store has promotion "Christmas sale" with coupon "Santa's gift"
@todo
@domain
Scenario: Deleted coupon should disappear from the registry
When I delete "Santa's Gift" coupon
Then this coupon should no longer exist in the registry
Then I should be notified that it has been successfully deleted
And this coupon should no longer exist in the registry

View file

@ -2,19 +2,20 @@
Feature: Not being able to delete a coupon which is in use
In order to maintain proper payment history
As an Administrator
I want to be prevented from deleting used coupons
I want to be prevented from deleting a used coupon
Background:
Given the store operates on a single channel in "France"
And the store ships everywhere for free
And the store has a product "Jacket"
And the store allows paying with "Cash on Delivery"
And the store has promotion "Christmas sale" with coupon "Santa's gift"
And the customer "john.doe@gmail.com" placed an order "#00000022"
And the customer chose "Free" shipping method to "France" with "Cash on Delivery" payment
And the customer bought single "Jacket" using "Santa's gift" coupon
@todo
@domain
Scenario: Being unable to delete a used coupon
When I try to delete "Santa's gift" coupon
Then And I should be notified that it is in use and cannot be deleted
Then I should be notified that it is in use and cannot be deleted
And this coupon should still exist in the registry

View file

@ -0,0 +1,112 @@
<?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.
*/
namespace Sylius\Behat\Context\Domain;
use Behat\Behat\Context\Context;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
use Sylius\Component\Core\Test\Services\SharedStorageInterface;
use Sylius\Component\Promotion\Model\CouponInterface;
use Sylius\Component\Promotion\Repository\PromotionRepositoryInterface;
use Sylius\Component\Resource\Repository\RepositoryInterface;
/**
* @author Jan Góralski <jan.goralski@lakion.com>
*/
final class PromotionContext implements Context
{
/**
* @var SharedStorageInterface
*/
private $sharedStorage;
/**
* @var PromotionRepositoryInterface
*/
private $promotionRepository;
/**
* @var RepositoryInterface
*/
private $couponRepository;
/**
* @param SharedStorageInterface $sharedStorage
* @param PromotionRepositoryInterface $promotionRepository
* @param RepositoryInterface $couponRepository
*/
public function __construct(
SharedStorageInterface $sharedStorage,
PromotionRepositoryInterface $promotionRepository,
RepositoryInterface $couponRepository
) {
$this->sharedStorage = $sharedStorage;
$this->promotionRepository = $promotionRepository;
$this->couponRepository = $couponRepository;
}
/**
* @When /^I try to delete ("([^"]+)" coupon)$/
*/
public function iTryToDeleteCoupon(CouponInterface $coupon)
{
try {
$this->couponRepository->remove($coupon);
throw new \Exception(sprintf('Coupon "%s" has been removed, but it should not.', $coupon->getCode()));
} catch(ForeignKeyConstraintViolationException $exception) {
$this->sharedStorage->set('last_exception', $exception);
}
}
/**
* @When /^I delete ("([^"]+)" coupon)$/
*/
public function iDeleteCoupon(CouponInterface $coupon)
{
$this->sharedStorage->set('coupon_id', $coupon->getId());
$this->couponRepository->remove($coupon);
}
/**
* @Then /^([^"]+ should no longer) exist in the registry$/
*/
public function couponShouldNotExistInTheRegistry($couponId)
{
expect($this->couponRepository->find($couponId))->toBe(null);
}
/**
* @Then I should be notified that it is in use and cannot be deleted
*/
public function iShouldBeNotifiedOfFailure()
{
expect($this->sharedStorage->get('last_exception'))
->toBeAnInstanceOf(ForeignKeyConstraintViolationException::class)
;
}
/**
* @Then /^([^"]+) should still exist in the registry$/
*/
public function couponShouldStillExistInTheRegistry(CouponInterface $coupon)
{
expect($this->couponRepository->find($coupon->getId()))->toNotBe(null);
}
/**
* @Then I should be notified that it has been successfully deleted
*/
public function iShouldBeNotifiedOfSuccess()
{
// Not applicable in the domain scope
}
}

View file

@ -13,6 +13,8 @@ namespace Sylius\Behat\Context\Setup;
use Behat\Behat\Context\Context;
use Doctrine\Common\Persistence\ObjectManager;
use Sylius\Component\Core\Model\CouponInterface;
use Sylius\Component\Core\OrderProcessing\OrderRecalculatorInterface;
use Sylius\Component\Order\Modifier\OrderItemQuantityModifierInterface;
use Sylius\Component\Core\Model\AddressInterface;
use Sylius\Component\Core\Model\OrderInterface;
@ -70,9 +72,9 @@ final class OrderContext implements Context
private $itemQuantityModifier;
/**
* @var ShippingChargesProcessorInterface
* @var OrderRecalculatorInterface
*/
private $shippingChargesProcessor;
private $orderRecalculator;
/**
* @var ObjectManager
@ -87,7 +89,8 @@ final class OrderContext implements Context
* @param PaymentFactoryInterface $paymentFactory
* @param FactoryInterface $orderItemFactory
* @param OrderItemQuantityModifierInterface $itemQuantityModifier
* @param ShippingChargesProcessorInterface $shippingChargesProcessor
* @param SharedStorageInterface $sharedStorage
* @param OrderRecalculatorInterface $orderRecalculator
* @param ObjectManager $objectManager
*/
public function __construct(
@ -98,7 +101,7 @@ final class OrderContext implements Context
PaymentFactoryInterface $paymentFactory,
FactoryInterface $orderItemFactory,
OrderItemQuantityModifierInterface $itemQuantityModifier,
ShippingChargesProcessorInterface $shippingChargesProcessor,
OrderRecalculatorInterface $orderRecalculator,
ObjectManager $objectManager
) {
$this->sharedStorage = $sharedStorage;
@ -108,7 +111,7 @@ final class OrderContext implements Context
$this->paymentFactory = $paymentFactory;
$this->orderItemFactory = $orderItemFactory;
$this->itemQuantityModifier = $itemQuantityModifier;
$this->shippingChargesProcessor = $shippingChargesProcessor;
$this->orderRecalculator = $orderRecalculator;
$this->objectManager = $objectManager;
}
@ -144,7 +147,7 @@ final class OrderContext implements Context
$this->orderShipmentFactory->processOrderShipment($order);
$order->getShipments()->first()->setMethod($shippingMethod);
$this->shippingChargesProcessor->applyShippingCharges($order);
$this->orderRecalculator->recalculate($order);
$payment = $this->paymentFactory->createWithAmountAndCurrency($order->getTotal(), $order->getCurrency());
$payment->setMethod($paymentMethod);
@ -163,6 +166,8 @@ final class OrderContext implements Context
public function theCustomerBoughtSingleProduct(ProductInterface $product)
{
$this->addSingleProductVariantToOrder($product->getMasterVariant(), $product->getPrice());
$this->objectManager->flush();
}
/**
@ -171,15 +176,31 @@ final class OrderContext implements Context
public function theCustomerBoughtSingleProductVariant(ProductVariantInterface $productVariant)
{
$this->addSingleProductVariantToOrder($productVariant, $productVariant->getPrice());
$this->objectManager->flush();
}
/**
* @Given the customer bought single :product using :coupon coupon
*/
public function theCustomerBoughtSingleUsing(ProductInterface $product, CouponInterface $coupon)
{
$order = $this->addSingleProductVariantToOrder($product->getMasterVariant(), $product->getPrice());
$order->setPromotionCoupon($coupon);
$this->orderRecalculator->recalculate($order);
$this->objectManager->flush();
}
/**
* @param ProductVariantInterface $productVariant
* @param int $price
*
* @return OrderInterface
*/
private function addSingleProductVariantToOrder(ProductVariantInterface $productVariant, $price)
{
/** @var OrderInterface $order */
$order = $this->sharedStorage->get('order');
/** @var OrderItemInterface $item */
@ -191,6 +212,8 @@ final class OrderContext implements Context
$order->addItem($item);
$this->objectManager->flush();
$this->orderRecalculator->recalculate($order);
return $order;
}
}

View file

@ -20,6 +20,8 @@ use Sylius\Component\Core\Model\TaxonInterface;
use Sylius\Component\Core\Test\Factory\TestPromotionFactoryInterface;
use Sylius\Component\Core\Test\Services\SharedStorageInterface;
use Sylius\Component\Promotion\Model\ActionInterface;
use Sylius\Component\Promotion\Factory\CouponFactoryInterface;
use Sylius\Component\Promotion\Model\CouponInterface;
use Sylius\Component\Promotion\Repository\PromotionRepositoryInterface;
/**
@ -37,6 +39,11 @@ final class PromotionContext implements Context
*/
private $actionFactory;
/**
* @var CouponFactoryInterface
*/
private $couponFactory;
/**
* @var RuleFactoryInterface
*/
@ -60,6 +67,7 @@ final class PromotionContext implements Context
/**
* @param SharedStorageInterface $sharedStorage
* @param ActionFactoryInterface $actionFactory
* @param CouponFactoryInterface $couponFactory
* @param RuleFactoryInterface $ruleFactory
* @param TestPromotionFactoryInterface $testPromotionFactory
* @param PromotionRepositoryInterface $promotionRepository
@ -68,6 +76,7 @@ final class PromotionContext implements Context
public function __construct(
SharedStorageInterface $sharedStorage,
ActionFactoryInterface $actionFactory,
CouponFactoryInterface $couponFactory,
RuleFactoryInterface $ruleFactory,
TestPromotionFactoryInterface $testPromotionFactory,
PromotionRepositoryInterface $promotionRepository,
@ -75,6 +84,7 @@ final class PromotionContext implements Context
) {
$this->sharedStorage = $sharedStorage;
$this->actionFactory = $actionFactory;
$this->couponFactory = $couponFactory;
$this->ruleFactory = $ruleFactory;
$this->testPromotionFactory = $testPromotionFactory;
$this->promotionRepository = $promotionRepository;
@ -94,6 +104,26 @@ final class PromotionContext implements Context
$this->sharedStorage->set('promotion', $promotion);
}
/**
* @Given the store has promotion :promotionName with coupon :couponCode
*/
public function thereIsPromotionWithCoupon($promotionName, $couponCode)
{
/** @var CouponInterface $coupon */
$coupon = $this->couponFactory->createNew();
$coupon->setCode($couponCode);
$promotion = $this->testPromotionFactory
->createForChannel($promotionName, $this->sharedStorage->get('channel'))
;
$promotion->addCoupon($coupon);
$promotion->setCouponBased(true);
$this->promotionRepository->add($promotion);
$this->sharedStorage->set('promotion', $promotion);
$this->sharedStorage->set('coupon', $coupon);
}
/**
* @Given /^([^"]+) gives ("[^"]+") fixed discount to every order$/
*/

View file

@ -0,0 +1,78 @@
<?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.
*/
namespace Sylius\Behat\Context\Transform;
use Behat\Behat\Context\Context;
use Sylius\Component\Promotion\Repository\PromotionRepositoryInterface;
use Sylius\Component\Resource\Repository\RepositoryInterface;
/**
* @author Jan Góralski <jan.goralski@lakion.com>
*/
final class PromotionContext implements Context
{
/**
* @var PromotionRepositoryInterface
*/
private $promotionRepository;
/**
* @var RepositoryInterface
*/
private $couponRepository;
/**
* @param PromotionRepositoryInterface $promotionRepository
* @param RepositoryInterface $couponRepository
*/
public function __construct(
PromotionRepositoryInterface $promotionRepository,
RepositoryInterface $couponRepository
) {
$this->promotionRepository = $promotionRepository;
$this->couponRepository = $couponRepository;
}
/**
* @Transform /^promotion "([^"]+)"$/
* @Transform /^"([^"]+)" promotion$/
* @Transform :promotion
*/
public function getPromotionByName($promotionName)
{
$promotion = $this->promotionRepository->findOneBy(['name' => $promotionName]);
if (null === $promotion) {
throw new \InvalidArgumentException(
sprintf('Promotion with name "%s" does not exist in the promotion repository.', $promotionName)
);
}
return $promotion;
}
/**
* @Transform /^coupon "([^"]+)"$/
* @Transform /^"([^"]+)" coupon$/
* @Transform :coupon
*/
public function getCouponByCode($couponCode)
{
$coupon = $this->couponRepository->findOneBy(['code' => $couponCode]);
if (null === $coupon) {
throw new \InvalidArgumentException(
sprintf('Coupon with code "%s" does not exist in the coupon repository.', $couponCode)
);
}
return $coupon;
}
}

View file

@ -40,6 +40,14 @@ final class SharedStorageContext implements Context
return $this->sharedStorage->getLatestResource();
}
/**
* @Transform /^(?:this|that|the) ([^"]+) should no longer$/
*/
public function getResourceId($resource)
{
return $this->sharedStorage->get(sprintf('%s_id', $resource));
}
/**
* @Transform /^(?:this|that|the) ([^"]+)$/
*/

View file

@ -12,8 +12,6 @@
namespace spec\Sylius\Behat\Context\Domain;
use Behat\Behat\Context\Context;
use Behat\Mink\Exception\ExpectationException;
use Behat\Mink\Tests\Exception\ExpectationExceptionTest;
use PhpSpec\Exception\Example\NotEqualException;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;

View file

@ -0,0 +1,144 @@
<?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.
*/
namespace spec\Sylius\Behat\Context\Domain;
use Behat\Behat\Context\Context;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
use PhpSpec\Exception\Example\FailureException;
use PhpSpec\Exception\Example\NotEqualException;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Sylius\Behat\Context\Domain\PromotionContext;
use Sylius\Component\Core\Repository\PromotionRepositoryInterface;
use Sylius\Component\Core\Test\Services\SharedStorageInterface;
use Sylius\Component\Promotion\Model\CouponInterface;
use Sylius\Component\Resource\Repository\RepositoryInterface;
/**
* @mixin PromotionContext
*
* @author Jan Góralski <jan.goralski@lakion.com>
*/
class PromotionContextSpec extends ObjectBehavior
{
function let(
SharedStorageInterface $sharedStorage,
PromotionRepositoryInterface $promotionRepository,
RepositoryInterface $couponRepository
) {
$this->beConstructedWith($sharedStorage, $promotionRepository, $couponRepository);
}
function it_is_initializable()
{
$this->shouldHaveType('Sylius\Behat\Context\Domain\PromotionContext');
}
function it_implements_context_interface()
{
$this->shouldImplement(Context::class);
}
function it_removes_a_coupon(
SharedStorageInterface $sharedStorage,
RepositoryInterface $couponRepository,
CouponInterface $coupon
) {
$coupon->getId()->willReturn(5);
$sharedStorage->set('coupon_id', 5)->shouldBeCalled();
$couponRepository->remove($coupon)->shouldBeCalled();
$this->iDeleteCoupon($coupon);
}
function it_throws_exception_when_there_was_a_problem_with_removing_a_coupon(
SharedStorageInterface $sharedStorage,
RepositoryInterface $couponRepository,
CouponInterface $coupon
) {
$coupon->getId()->willReturn(5);
$sharedStorage->set('coupon_id', 5)->shouldBeCalled();
$couponRepository->remove($coupon)->willThrow(ForeignKeyConstraintViolationException::class);
$this->shouldThrow(ForeignKeyConstraintViolationException::class)->during('iDeleteCoupon', [$coupon]);
}
function it_tries_to_remove_a_coupon_and_catches_exception_in_case_of_failure(
SharedStorageInterface $sharedStorage,
RepositoryInterface $couponRepository,
CouponInterface $coupon
) {
$coupon->getId()->willReturn(5);
$couponRepository->remove($coupon)->willThrow(ForeignKeyConstraintViolationException::class);
$sharedStorage->set('last_exception', Argument::type(ForeignKeyConstraintViolationException::class))->shouldBeCalled();
$this->iTryToDeleteCoupon($coupon);
}
function it_throws_exception_while_trying_to_remove_a_coupon_and_actually_removing_it(
SharedStorageInterface $sharedStorage,
RepositoryInterface $couponRepository,
CouponInterface $coupon
) {
$coupon->getId()->willReturn(5);
$couponRepository->remove($coupon)->shouldBeCalled();
$sharedStorage->set('last_exception', Argument::any())->shouldNotBeCalled();
$this->shouldThrow(\Exception::class)->during('iTryToDeleteCoupon', [$coupon]);
}
function it_checks_whether_a_coupon_is_not_in_the_registry(
RepositoryInterface $couponRepository
) {
$couponRepository->find(5)->willReturn(null);
$this->couponShouldNotExistInTheRegistry(5);
}
function it_throws_exception_when_a_coupon_is_found_but_it_should_not_exist(
SharedStorageInterface $sharedStorage,
RepositoryInterface $couponRepository,
CouponInterface $coupon
) {
$coupon->getId()->willReturn(5);
$sharedStorage->get('coupon_id')->willReturn(5);
$couponRepository->find(5)->willReturn($coupon);
$this
->shouldThrow(NotEqualException::class)
->during('couponShouldNotExistInTheRegistry', [5])
;
}
function it_checks_whether_a_coupon_is_in_the_registry(
RepositoryInterface $couponRepository,
CouponInterface $coupon
) {
$coupon->getId()->willReturn(5);
$couponRepository->find(5)->willReturn($coupon);
$this->couponShouldStillExistInTheRegistry($coupon);
}
function it_throws_exception_when_a_coupon_is_not_found_but_it_should_exist(
RepositoryInterface $couponRepository,
CouponInterface $coupon
) {
$coupon->getId()->willReturn(5);
$couponRepository->find(5)->willReturn(null);
$this
->shouldThrow(FailureException::class)
->during('couponShouldStillExistInTheRegistry', [$coupon])
;
}
}

View file

@ -15,10 +15,12 @@ use Behat\Behat\Context\Context;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Persistence\ObjectManager;
use PhpSpec\ObjectBehavior;
use Sylius\Component\Core\OrderProcessing\OrderRecalculatorInterface;
use Sylius\Component\Order\Modifier\OrderItemQuantityModifierInterface;
use Sylius\Behat\Context\Setup\OrderContext;
use Sylius\Component\Core\Model\AddressInterface;
use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Core\Model\CouponInterface;
use Sylius\Component\Core\Model\CustomerInterface;
use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Core\Model\OrderItemInterface;
@ -28,7 +30,6 @@ use Sylius\Component\Core\Model\ProductVariantInterface;
use Sylius\Component\Core\Model\ShipmentInterface;
use Sylius\Component\Core\Model\ShippingMethodInterface;
use Sylius\Component\Core\OrderProcessing\OrderShipmentProcessorInterface;
use Sylius\Component\Core\OrderProcessing\ShippingChargesProcessorInterface;
use Sylius\Component\Core\Repository\OrderRepositoryInterface;
use Sylius\Component\Core\Test\Services\SharedStorageInterface;
use Sylius\Component\Currency\Model\CurrencyInterface;
@ -51,7 +52,7 @@ class OrderContextSpec extends ObjectBehavior
PaymentFactoryInterface $paymentFactory,
FactoryInterface $orderItemFactory,
OrderItemQuantityModifierInterface $itemQuantityModifier,
ShippingChargesProcessorInterface $chargesProcessor,
OrderRecalculatorInterface $orderRecalculator,
ObjectManager $objectManager
) {
$this->beConstructedWith(
@ -62,7 +63,7 @@ class OrderContextSpec extends ObjectBehavior
$paymentFactory,
$orderItemFactory,
$itemQuantityModifier,
$chargesProcessor,
$orderRecalculator,
$objectManager
);
}
@ -112,7 +113,7 @@ class OrderContextSpec extends ObjectBehavior
PaymentMethodInterface $paymentMethod,
SharedStorageInterface $sharedStorage,
ShipmentInterface $shipment,
ShippingChargesProcessorInterface $chargesProcessor,
OrderRecalculatorInterface $orderRecalculator,
ShippingMethodInterface $shippingMethod,
ObjectManager $objectManager
) {
@ -135,7 +136,7 @@ class OrderContextSpec extends ObjectBehavior
$objectManager->flush()->shouldBeCalled();
$chargesProcessor->applyShippingCharges($order)->shouldBeCalled();
$orderRecalculator->recalculate($order)->shouldBeCalled();
$this->theCustomerChoseShippingToWithPayment($shippingMethod, $address, $paymentMethod);
}
@ -185,4 +186,37 @@ class OrderContextSpec extends ObjectBehavior
$this->theCustomerBoughtSingleProductVariant($variant);
}
function it_adds_single_item_by_customer_and_applies_a_coupon(
FactoryInterface $orderItemFactory,
OrderInterface $order,
OrderItemInterface $item,
OrderItemQuantityModifierInterface $itemQuantityModifier,
ProductInterface $product,
CouponInterface $coupon,
SharedStorageInterface $sharedStorage,
ProductVariantInterface $variant,
OrderRecalculatorInterface $orderRecalculator,
ObjectManager $objectManager
) {
$sharedStorage->get('order')->willReturn($order);
$orderItemFactory->createNew()->willReturn($item);
$product->getMasterVariant()->willReturn($variant);
$product->getPrice()->willReturn(1234);
$itemQuantityModifier->modify($item, 1)->shouldBeCalled();
$item->setVariant($variant)->shouldBeCalled();
$item->setUnitPrice(1234)->shouldBeCalled();
$order->setPromotionCoupon($coupon)->shouldBeCalled();
$order->addItem($item)->shouldBeCalled();
$orderRecalculator->recalculate($order)->shouldBeCalled();
$objectManager->flush()->shouldBeCalled();
$this->theCustomerBoughtSingleUsing($product, $coupon);
}
}

View file

@ -14,18 +14,23 @@ namespace spec\Sylius\Behat\Context\Setup;
use Behat\Behat\Context\Context;
use Doctrine\Common\Persistence\ObjectManager;
use PhpSpec\ObjectBehavior;
use Sylius\Behat\Context\Setup\PromotionContext;
use Sylius\Component\Core\Factory\ActionFactoryInterface;
use Sylius\Component\Core\Factory\RuleFactoryInterface;
use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Core\Model\CouponInterface;
use Sylius\Component\Core\Model\PromotionInterface;
use Sylius\Component\Core\Model\TaxonInterface;
use Sylius\Component\Core\Test\Factory\TestPromotionFactoryInterface;
use Sylius\Component\Core\Test\Services\SharedStorageInterface;
use Sylius\Component\Promotion\Factory\CouponFactoryInterface;
use Sylius\Component\Promotion\Model\ActionInterface;
use Sylius\Component\Promotion\Model\RuleInterface;
use Sylius\Component\Promotion\Repository\PromotionRepositoryInterface;
/**
* @mixin PromotionContext
*
* @author Mateusz Zalewski <mateusz.zalewski@lakion.com>
*/
class PromotionContextSpec extends ObjectBehavior
@ -33,6 +38,7 @@ class PromotionContextSpec extends ObjectBehavior
function let(
SharedStorageInterface $sharedStorage,
ActionFactoryInterface $actionFactory,
CouponFactoryInterface $couponFactory,
RuleFactoryInterface $ruleFactory,
TestPromotionFactoryInterface $testPromotionFactory,
PromotionRepositoryInterface $promotionRepository,
@ -41,6 +47,7 @@ class PromotionContextSpec extends ObjectBehavior
$this->beConstructedWith(
$sharedStorage,
$actionFactory,
$couponFactory,
$ruleFactory,
$testPromotionFactory,
$promotionRepository,
@ -75,6 +82,30 @@ class PromotionContextSpec extends ObjectBehavior
$this->thereIsPromotion('Super promotion');
}
function it_creates_promotion_with_coupon(
SharedStorageInterface $sharedStorage,
CouponFactoryInterface $couponFactory,
TestPromotionFactoryInterface $testPromotionFactory,
PromotionRepositoryInterface $promotionRepository,
ChannelInterface $channel,
CouponInterface $coupon,
PromotionInterface $promotion
) {
$couponFactory->createNew()->willReturn($coupon);
$coupon->setCode('Coupon galore')->shouldBeCalled();
$sharedStorage->get('channel')->willReturn($channel);
$testPromotionFactory->createForChannel('Promotion galore', $channel)->willReturn($promotion);
$promotion->addCoupon($coupon)->shouldBeCalled();
$promotion->setCouponBased(true)->shouldBeCalled();
$promotionRepository->add($promotion)->shouldBeCalled();
$sharedStorage->set('promotion', $promotion)->shouldBeCalled();
$sharedStorage->set('coupon', $coupon)->shouldBeCalled();
$this->thereIsPromotionWithCoupon('Promotion galore', 'Coupon galore');
}
function it_creates_fixed_discount_action_for_promotion(
ActionFactoryInterface $actionFactory,
ActionInterface $action,

View file

@ -0,0 +1,77 @@
<?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.
*/
namespace spec\Sylius\Behat\Context\Transform;
use Behat\Behat\Context\Context;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Sylius\Behat\Context\Transform\PromotionContext;
use Sylius\Component\Promotion\Model\CouponInterface;
use Sylius\Component\Promotion\Model\PromotionInterface;
use Sylius\Component\Promotion\Repository\PromotionRepositoryInterface;
use Sylius\Component\Resource\Repository\RepositoryInterface;
/**
* @mixin PromotionContext
*
* @author Jan Góralski <jan.goralski@lakion.com>
*/
class PromotionContextSpec extends ObjectBehavior
{
function let(PromotionRepositoryInterface $promotionRepository, RepositoryInterface $couponRepository)
{
$this->beConstructedWith($promotionRepository, $couponRepository);
}
function it_is_initializable()
{
$this->shouldHaveType('Sylius\Behat\Context\Transform\PromotionContext');
}
function it_implements_context_interface()
{
$this->shouldImplement(Context::class);
}
function it_gets_promotion_by_its_name(
PromotionRepositoryInterface $promotionRepository,
PromotionInterface $promotion
) {
$promotion->getName()->willReturn('Best Promotion');
$promotionRepository->findOneBy(['name' => 'Best Promotion'])->willReturn($promotion);
$this->getPromotionByName('Best Promotion');
}
function it_throws_exception_when_promotion_with_given_name_was_not_found(
PromotionRepositoryInterface $promotionRepository
) {
$promotionRepository->findOneBy(['name' => 'Wrong Promotion'])->willReturn(null);
$this->shouldThrow(\InvalidArgumentException::class)->during('getPromotionByName', ['Wrong Promotion']);
}
function it_gets_coupon_by_its_code(RepositoryInterface $couponRepository, CouponInterface $coupon)
{
$coupon->getCode()->willReturn('BEST-CODE');
$couponRepository->findOneBy(['code' => 'BEST-CODE'])->willReturn($coupon);
$this->getCouponByCode('BEST-CODE');
}
function it_throws_exception_when_coupon_with_given_code_was_not_found(RepositoryInterface $couponRepository)
{
$couponRepository->findOneBy(['code' => 'NO-CODE'])->willReturn(null);
$this->shouldThrow(\InvalidArgumentException::class)->during('getCouponByCode', ['NO-CODE']);
}
}

View file

@ -13,10 +13,13 @@ namespace spec\Sylius\Behat\Context\Transform;
use Behat\Behat\Context\Context;
use PhpSpec\ObjectBehavior;
use Sylius\Behat\Context\Transform\SharedStorageContext;
use Sylius\Component\Core\Model\CustomerInterface;
use Sylius\Component\Core\Test\Services\SharedStorageInterface;
/**
* @mixin SharedStorageContext
*
* @author Mateusz Zalewski <mateusz.zalewski@lakion.com>
*/
class SharedStorageContextSpec extends ObjectBehavior
@ -43,7 +46,7 @@ class SharedStorageContextSpec extends ObjectBehavior
$this->getLatestResource()->shouldReturn('string');
}
function it_transform_this_that_and_the_with_resource_name_to_current_resource_of_this_type(
function it_transforms_this_that_and_the_with_resource_name_to_current_resource_of_this_type(
$sharedStorage,
CustomerInterface $customer
) {
@ -51,4 +54,12 @@ class SharedStorageContextSpec extends ObjectBehavior
$this->getResource('customer')->shouldReturn($customer);
}
function it_transforms_this_that_and_the_should_no_longer_with_resource_name_to_current_id_set_for_that_resource(
SharedStorageInterface $sharedStorage
) {
$sharedStorage->get('customer_id')->willReturn(5);
$this->getResourceId('customer')->shouldReturn(5);
}
}

View file

@ -37,7 +37,7 @@
</one-to-one>
<many-to-one field="promotionCoupon" target-entity="Sylius\Component\Promotion\Model\CouponInterface">
<join-column name="promotion_coupon_id" on-delete="SET NULL" />
<join-column name="promotion_coupon_id" />
<cascade>
<cascade-persist />
</cascade>

View file

@ -38,6 +38,7 @@
<parameter key="sylius.order_processing.shipment_processor.class">Sylius\Component\Core\OrderProcessing\OrderShipmentProcessor</parameter>
<parameter key="sylius.order_processing.payment_processor.class">Sylius\Component\Core\OrderProcessing\PaymentProcessor</parameter>
<parameter key="sylius.order_processing.shipping_processor.class">Sylius\Component\Core\OrderProcessing\ShippingChargesProcessor</parameter>
<parameter key="sylius.order_processing.order_recalculator.class">Sylius\Component\Core\OrderProcessing\OrderRecalculator</parameter>
<parameter key="sylius.taxation.order_taxes_applicator.class">Sylius\Component\Core\Taxation\OrderTaxesApplicator</parameter>
<parameter key="sylius.taxation.order_shipment_taxes_by_zone_applicator.class">Sylius\Component\Core\Taxation\OrderShipmentTaxesByZoneApplicator</parameter>
@ -261,6 +262,12 @@
<argument type="service" id="sylius.shipping_calculator" />
</service>
<service id="sylius.order_processing.order_recalculator" class="%sylius.order_processing.order_recalculator.class%">
<argument type="service" id="sylius.promotion_processor" />
<argument type="service" id="sylius.taxation.order_taxes_applicator" />
<argument type="service" id="sylius.order_processing.shipping_processor" />
</service>
<service id="sylius.order.purger" class="%sylius.order.purger.class%">
<argument type="service" id="sylius.manager.order" />
<argument type="service" id="sylius.repository.order" />

View file

@ -34,9 +34,6 @@
<field name="updatedAt" column="updated_at" type="datetime" nullable="true">
<gedmo:timestampable on="update"/>
</field>
<field name="deletedAt" column="deleted_at" type="datetime" nullable="true" />
<gedmo:soft-deleteable field-name="deletedAt" />
</mapped-superclass>
</doctrine-mapping>

View file

@ -0,0 +1,64 @@
<?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.
*/
namespace Sylius\Component\Core\OrderProcessing;
use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Core\Taxation\OrderTaxesApplicatorInterface;
use Sylius\Component\Promotion\Processor\PromotionProcessorInterface;
/**
* @author Jan Góralski <jan.goralski@lakion.com>
*/
class OrderRecalculator implements OrderRecalculatorInterface
{
/**
* @var PromotionProcessorInterface
*/
private $promotionProcessor;
/**
* @var OrderTaxesApplicatorInterface
*/
private $taxesApplicator;
/**
* @var ShippingChargesProcessorInterface
*/
private $shippingChargesProcessor;
/**
* @param PromotionProcessorInterface $promotionProcessor
* @param OrderTaxesApplicatorInterface $taxesApplicator
* @param ShippingChargesProcessorInterface $shippingChargesProcessor
*/
public function __construct(
PromotionProcessorInterface $promotionProcessor,
OrderTaxesApplicatorInterface $taxesApplicator,
ShippingChargesProcessorInterface $shippingChargesProcessor
) {
$this->promotionProcessor = $promotionProcessor;
$this->taxesApplicator = $taxesApplicator;
$this->shippingChargesProcessor = $shippingChargesProcessor;
}
/**
* @param OrderInterface $order
*
* @return OrderInterface
*/
public function recalculate(OrderInterface $order)
{
$this->promotionProcessor->process($order);
$this->taxesApplicator->apply($order);
$this->shippingChargesProcessor->applyShippingCharges($order);
}
}

View file

@ -0,0 +1,27 @@
<?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.
*/
namespace Sylius\Component\Core\OrderProcessing;
use Sylius\Component\Core\Model\OrderInterface;
/**
* @author Jan Góralski <jan.goralski@lakion.com>
*/
interface OrderRecalculatorInterface
{
/**
* @param OrderInterface $order
*
* @return OrderInterface
*/
public function recalculate(OrderInterface $order);
}

View file

@ -0,0 +1,59 @@
<?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.
*/
namespace spec\Sylius\Component\Core\OrderProcessing;
use PhpSpec\ObjectBehavior;
use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Core\OrderProcessing\OrderRecalculator;
use Sylius\Component\Core\OrderProcessing\OrderRecalculatorInterface;
use Sylius\Component\Core\OrderProcessing\ShippingChargesProcessorInterface;
use Sylius\Component\Core\Taxation\OrderTaxesApplicatorInterface;
use Sylius\Component\Promotion\Processor\PromotionProcessorInterface;
/**
* @mixin OrderRecalculator
*
* @author Jan Góralski <jan.goralski@lakion.com>
*/
class OrderRecalculatorSpec extends ObjectBehavior
{
function let(
PromotionProcessorInterface $promotionProcessor,
OrderTaxesApplicatorInterface $taxesApplicator,
ShippingChargesProcessorInterface $shippingChargesProcessor
) {
$this->beConstructedWith($promotionProcessor, $taxesApplicator, $shippingChargesProcessor);
}
function it_is_initializable()
{
$this->shouldHaveType('Sylius\Component\Core\OrderProcessing\OrderRecalculator');
}
function it_implements_order_recalculator_interface()
{
$this->shouldImplement(OrderRecalculatorInterface::class);
}
function it_recalculates_order_promotions_taxes_and_shipping_charges(
PromotionProcessorInterface $promotionProcessor,
OrderTaxesApplicatorInterface $taxesApplicator,
ShippingChargesProcessorInterface $shippingChargesProcessor,
OrderInterface $order
) {
$promotionProcessor->process($order)->shouldBeCalled();
$taxesApplicator->apply($order)->shouldBeCalled();
$shippingChargesProcessor->applyShippingCharges($order)->shouldBeCalled();
$this->recalculate($order);
}
}

View file

@ -11,6 +11,7 @@
namespace Sylius\Component\Promotion\Factory;
use Sylius\Component\Promotion\Model\CouponInterface;
use Sylius\Component\Resource\Factory\FactoryInterface;
/**

View file

@ -11,7 +11,6 @@
namespace Sylius\Component\Promotion\Model;
use Sylius\Component\Resource\Model\SoftDeletableTrait;
use Sylius\Component\Resource\Model\TimestampableTrait;
/**
@ -19,7 +18,7 @@ use Sylius\Component\Resource\Model\TimestampableTrait;
*/
class Coupon implements CouponInterface
{
use SoftDeletableTrait, TimestampableTrait;
use TimestampableTrait;
/**
* @var mixed

View file

@ -13,13 +13,12 @@ namespace Sylius\Component\Promotion\Model;
use Sylius\Component\Resource\Model\CodeAwareInterface;
use Sylius\Component\Resource\Model\ResourceInterface;
use Sylius\Component\Resource\Model\SoftDeletableInterface;
use Sylius\Component\Resource\Model\TimestampableInterface;
/**
* @author Saša Stamenković <umpirsky@gmail.com>
*/
interface CouponInterface extends CodeAwareInterface, SoftDeletableInterface, TimestampableInterface, ResourceInterface
interface CouponInterface extends CodeAwareInterface, TimestampableInterface, ResourceInterface
{
/**
* @return int