diff --git a/.danger.php b/.danger.php index 1a0a2520078..a5e4dd48ba8 100644 --- a/.danger.php +++ b/.danger.php @@ -42,7 +42,7 @@ $files = $context->platform->pullRequest->getFiles(); if ($files->matches('changelog/_unreleased/*.md')->count() > 0) { - $context->failure('The Pull Request makes use of the old changelog format. Please document your changes in the `RELEASE_INFO-6.7.md` and `UPGRADE-6.8.md` file respectively.');; + $context->failure('The Pull Request makes use of the old changelog format. Please document your changes in the `RELEASE_INFO-6.7.md` and `UPGRADE-6.8.md` file respectively. For detailed infos please refer to the [release documentation guide](https://github.com/shopware/shopware/blob/trunk/delivery-process/documenting-a-release.md).');; } }) @@ -50,7 +50,7 @@ $files = $context->platform->pullRequest->getFiles(); if ($files->matches('RELEASE_INFO-6.7.md')->count() === 0) { - $context->warning('The Pull Request doesn\'t contain any release info, if your changes are relevant for external developers please add an entry to the release info file, including the consequences of the change and how it affects external developers.'); + $context->warning('The Pull Request doesn\'t contain any release info, if your changes are relevant for external developers please add an entry to the release info file, including the consequences of the change and how it affects external developers. For detailed infos please refer to the [release documentation guide](https://github.com/shopware/shopware/blob/trunk/delivery-process/documenting-a-release.md).'); } }) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 064e50dec81..593c992ab26 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -31,7 +31,7 @@ If the issue exists only in Jira, include a link to the Jira issue. - [ ] I have updated developer-facing release notes if this change is **relevant** for external developers: - Add a short entry to `RELEASE_INFO-6..md` under “Upcoming” for informational changes, including the consequences of the change and how it affects external developers. - Add an `UPGRADE` section in `UPGRADE-6..md` for breaking changes (what/why/impact/how to adapt). - - See the [Release Notes & Changelog Process](https://github.com/shopware/shopware/blob/trunk/adr/2025-10-28-changelog-release-info-process.md) for details. + - See the [Documenting a Release Process](https://github.com/shopware/shopware/blob/trunk/delivery-process/documenting-a-release.md) for details. - [ ] I have written or adjusted the documentation according to my changes - [ ] This change has comments for package types, values, functions, and non-obvious lines of code - [ ] I have read the contribution requirements and fulfilled them diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4009e3af03f..d6cd532fb0d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ If you want more details about available licensing or the contribution agreement ## Contributing to the Shopware code base If you want to learn how to contribute code to Shopware, please refer to [Contributing Code](https://developer.shopware.com/docs/resources/guidelines/code/contribution.html). -Also, make sure that you add a changelog file that describes your changes in a meaningful way. For more information refer to [this document](https://github.com/shopware/shopware/blob/trunk/adr/2020-08-03-implement-new-changelog.md). +Also, make sure that if you change something in a manner that is relevant to external developers please describe your change in a meaningful way. For more information refer to [this document](https://github.com/shopware/shopware/blob/trunk/delivery-process/documenting-a-release.md). ## Local Docker Setup diff --git a/adr/2025-10-28-changelog-release-info-process.md b/adr/2025-10-28-changelog-release-info-process.md index ca8eb50118c..3c5109face1 100644 --- a/adr/2025-10-28-changelog-release-info-process.md +++ b/adr/2025-10-28-changelog-release-info-process.md @@ -53,6 +53,7 @@ _(Tip: You can use short headings like “What changed” or “How to adjust” 10. CI will later validate that either `RELEASE_INFO` or `UPGRADE` was updated when relevant (or explicitly skipped with justification). --- +For details about how to write entries for these files, please refer to the release [documentation guide](../delivery-process/documenting-a-release.md). ## Consequences diff --git a/delivery-process/documenting-a-release.md b/delivery-process/documenting-a-release.md index 302615ce946..d1e2a11965b 100644 --- a/delivery-process/documenting-a-release.md +++ b/delivery-process/documenting-a-release.md @@ -14,40 +14,56 @@ To have a structured and automated workflow: ## Where to Document Changes -### Changelog (auto-generated) - -The complete raw changelog for each release is generated automatically from GitHub when a tag is created. -It includes every merged PR and commit and is published on the GitHub **Releases** page. -This changelog is **not curated** and complements the human-maintained `RELEASE_INFO` and `UPGRADE` files. -It’s primarily for internal engineers, support, and partners. - Every PR that introduces a significant change must update one or both of these files: - RELEASE_INFO.md: Tracks new features, API updates, and general improvements. - UPGRADE.md: Covers breaking changes, migration steps, and any required developer action. Developers must edit the version-scoped files RELEASE_INFO-6.x.md and UPGRADE-6.x.md directly in the repository. -The legacy bin/console changelog:create command and /changelog/_unreleased folder are deprecated and scheduled for removal. -## How Do I Know Where to Add My Change? -A simple rule of thumb: +### What needs to go into RELEASE_INFO.md? +* New or major reworked user facing features +* improvements / new developer features + * This **does include**: + * Added extension points + * new/changed best practices / guidelines + * Quality of life improvements for other developers + * This **does not include**: + * Refactorings of internal code + * “under the hood” improvements that are backwards compatible +* deprecations we made +* everything else we changed that developers should be aware of +* Critical bugs (not every bug, therefore we have the complete changelog, but critical ones, esp when we do a patch release because of them should be documented) + +Remember: The release notes should describe **why** we made a change and **why** external developers should care; it is **not** (primarily) about **what** you changed. + +### What needs to go into UPGRADE.md? +* Everything that might cause a break in projects, extensions, integrations etc. +* Especially every break defined by our backwards compatibility promise needs to be documented: https://developer.shopware.com/docs/resources/guidelines/code/backward-compatibility.html#backward-compatibility -- Use `RELEASE_INFO.md` for: - - Features, API updates, improvements, and non-breaking changes. - - Example: "Added a new admin UI filter for orders." +### When you do not need to (explicitly) document a change -- Use `UPGRADE.md` for: - - Breaking changes, migration steps, and required developer actions. - - Example: "Deprecated sw-popover, use mt-floating-ui instead." - +* Everything not included above (esp. non-critical bug fixes, internal refactorings) does not need to be documented in RELEASE_INFO.md or UPGRADE.md. + +Those changes do not need to be explicitly documented in the release notes, as we will generate a complete changelog for each release based on the semantic PR titles. +The complete raw changelog for each release is generated automatically from GitHub when a tag is created. +It includes every merged PR and commit and is published on the GitHub **Releases** page. +This changelog is **not curated** and complements the human-maintained `RELEASE_INFO` and `UPGRADE` files. +It’s primarily for internal engineers, support, and partners. + +### Where do deprecations go? When a deprecation is introduced (e.g., in a minor release), document the alternative and the timeline in RELEASE_INFO.md. When the breaking change takes effect (e.g., in a major release), document it in UPGRADE.md with full migration steps. +## What information do we need to provide per topic? +* What we changed +* Why we changed it, the benefit of the change +* Why and when externals need to care +* How they can/need to adjust + ## Content Structure All documented changes should follow this structured format: -1. `RELEASE_INFO.md` (Developer-Facing Changes) - ``` # Features Here we describe all new, changed or improved user facing features. @@ -65,94 +81,6 @@ For changes in the app system. For config and infrastructure related changes. ``` -2. `UPGRADE.md` (Breaking Changes & Migration Guides) - -Each entry should include: - -``` -Changes [A] due to [B], so that [C]. -Required Actions: [D]. -``` - -- Example: -``` -Changes sw-popover due to UI consistency, so that extensions follow a unified component model. -Required Actions: Replace sw-popover with mt-floating-ui. -``` - -## Markdown Formatting Guidelines - -To maintain a consistent structure and reduce merge conflicts, follow these formatting rules when updating RELEASE_INFO.md and UPGRADE.md: - -### General Formatting Rules -1. Newlines Before and After Entries -- Every new entry must have a blank line before and after it. -- Example: -``` -## Features - -- Added support for XYZ functionality. - -## API - -- Introduced new API endpoint for retrieving order statuses. -``` -2. Headings Must Have a Blank Line Above and Below -Example: -``` -## Storefront - -- Improved checkout performance. -``` -3. Use Bullet Points (-) for Entries -- All changes should be written in a bullet list format. -Example: -``` -- Fixed an issue where tax calculations were incorrect in certain cases. -``` -4. Use Consistent Sentence Structure -- Start with a verb and describe what was added, changed, or removed. -- ✅ Correct: -```- Added a new admin UI filter for orders. -- Fixed an issue where the tax rate was miscalculated. -- Deprecated the `sw-popover` component in favor of `mt-floating-ui`. -``` -- ❌ Incorrect: -``` -- New admin UI filter for orders. -- Tax rate miscalculation fix. -- The `sw-popover` component has been deprecated. -``` -5. Code Formatting -- Use backticks () for inline code and commands. -- Example: -``` -- Deprecated `sw-popover`, use `mt-floating-ui` instead. -``` -### Example of a Well-Formatted Entry -``` -## Features - -- Added support for multi-warehouse inventory tracking. - -## API - -- Introduced `GET /api/v1/orders/status` for fetching order statuses. - -## Core - -- Refactored product import logic to improve performance. - -## UPGRADE.md - -### Breaking Changes - -- **What changed**: Deprecated `sw-popover`, use `mt-floating-ui` instead. -- **Why**: Improved UI consistency. -- **Impact**: Affects custom admin extensions using `sw-popover`. -- **Required Actions**: Replace `sw-popover` with `mt-floating-ui`. -``` - ## How This is Made Consistent 1. Every PR must include documentation: If your PR makes a significant change, update RELEASE_INFO.md and/or UPGRADE.md. diff --git a/src/Core/Checkout/Cart/LineItem/Group/LineItemGroupBuilder.php b/src/Core/Checkout/Cart/LineItem/Group/LineItemGroupBuilder.php index 038686d5a1c..cb6a70ce3f2 100644 --- a/src/Core/Checkout/Cart/LineItem/Group/LineItemGroupBuilder.php +++ b/src/Core/Checkout/Cart/LineItem/Group/LineItemGroupBuilder.php @@ -9,9 +9,10 @@ use Shopware\Core\Checkout\Cart\LineItem\LineItemQuantitySplitter; use Shopware\Core\Framework\Log\Package; use Shopware\Core\System\SalesChannel\SalesChannelContext; +use Symfony\Contracts\Service\ResetInterface; #[Package('checkout')] -class LineItemGroupBuilder +class LineItemGroupBuilder implements ResetInterface { /** * @var array @@ -29,6 +30,11 @@ public function __construct( ) { } + public function reset(): void + { + $this->results = []; + } + /** * Searches for all packages that can be built from the provided list of groups. * Every line item will be taken from the cart and only the ones that are left will diff --git a/src/Core/Checkout/Cart/Subscriber/CartOrderEventSubscriber.php b/src/Core/Checkout/Cart/Subscriber/CartOrderEventSubscriber.php index a2d18013685..a70541aa0a6 100644 --- a/src/Core/Checkout/Cart/Subscriber/CartOrderEventSubscriber.php +++ b/src/Core/Checkout/Cart/Subscriber/CartOrderEventSubscriber.php @@ -2,8 +2,11 @@ namespace Shopware\Core\Checkout\Cart\Subscriber; +use Shopware\Core\Checkout\Cart\Event\BeforeLineItemAddedEvent; +use Shopware\Core\Checkout\Cart\Event\BeforeLineItemRemovedEvent; use Shopware\Core\Checkout\Cart\Event\CartDeletedEvent; use Shopware\Core\Checkout\Cart\Event\CheckoutOrderPlacedEvent; +use Shopware\Core\Checkout\Cart\LineItem\Group\LineItemGroupBuilder; use Shopware\Core\Framework\Log\Package; use Shopware\Core\Framework\Validation\DataBag\RequestDataBag; use Shopware\Core\System\SalesChannel\Context\SalesChannelContextService; @@ -17,7 +20,8 @@ readonly class CartOrderEventSubscriber implements EventSubscriberInterface { public function __construct( - private AbstractContextSwitchRoute $contextSwitchRoute + private AbstractContextSwitchRoute $contextSwitchRoute, + private LineItemGroupBuilder $lineItemGroupBuilder ) { } @@ -26,6 +30,8 @@ public static function getSubscribedEvents(): array return [ CartDeletedEvent::class => ['handleContextAddress', 1], CheckoutOrderPlacedEvent::class => ['handleContextAddress', 1], + BeforeLineItemAddedEvent::class => 'resetBuilder', + BeforeLineItemRemovedEvent::class => 'resetBuilder', ]; } @@ -36,4 +42,10 @@ public function handleContextAddress(CartDeletedEvent|CheckoutOrderPlacedEvent $ SalesChannelContextService::BILLING_ADDRESS_ID => null, ]), $event->getSalesChannelContext()); } + + public function resetBuilder(BeforeLineItemAddedEvent|BeforeLineItemRemovedEvent $event): void + { + // We must reset the calculated results when an line item is added or removed. + $this->lineItemGroupBuilder->reset(); + } } diff --git a/src/Core/Checkout/DependencyInjection/cart.xml b/src/Core/Checkout/DependencyInjection/cart.xml index 8f8768fd0c5..9eca5b82b0d 100644 --- a/src/Core/Checkout/DependencyInjection/cart.xml +++ b/src/Core/Checkout/DependencyInjection/cart.xml @@ -451,6 +451,8 @@ + + @@ -548,6 +550,8 @@ + + diff --git a/src/Core/Content/Cms/Extension/CmsSlotsDataCollectExtension.php b/src/Core/Content/Cms/Extension/CmsSlotsDataCollectExtension.php index fab643eb896..5acc14cd67f 100644 --- a/src/Core/Content/Cms/Extension/CmsSlotsDataCollectExtension.php +++ b/src/Core/Content/Cms/Extension/CmsSlotsDataCollectExtension.php @@ -4,6 +4,7 @@ use Shopware\Core\Content\Cms\Aggregate\CmsSlot\CmsSlotCollection; use Shopware\Core\Content\Cms\DataResolver\CriteriaCollection; +use Shopware\Core\Content\Cms\DataResolver\ResolverContext\EntityResolverContext; use Shopware\Core\Content\Cms\DataResolver\ResolverContext\ResolverContext; use Shopware\Core\Framework\Extensions\Extension; use Shopware\Core\Framework\Log\Package; @@ -39,6 +40,8 @@ public function __construct( * @public * * @description Allows you to access to the current resolver-context + * + * @param ResolverContext|EntityResolverContext $resolverContext */ public readonly ResolverContext $resolverContext, ) { diff --git a/src/Core/Content/Cms/Extension/CmsSlotsDataEnrichExtension.php b/src/Core/Content/Cms/Extension/CmsSlotsDataEnrichExtension.php index fdb9a1b1036..502e30b1d4c 100644 --- a/src/Core/Content/Cms/Extension/CmsSlotsDataEnrichExtension.php +++ b/src/Core/Content/Cms/Extension/CmsSlotsDataEnrichExtension.php @@ -4,6 +4,7 @@ use Shopware\Core\Content\Cms\Aggregate\CmsSlot\CmsSlotCollection; use Shopware\Core\Content\Cms\DataResolver\CriteriaCollection; +use Shopware\Core\Content\Cms\DataResolver\ResolverContext\EntityResolverContext; use Shopware\Core\Content\Cms\DataResolver\ResolverContext\ResolverContext; use Shopware\Core\Framework\DataAbstractionLayer\Entity; use Shopware\Core\Framework\DataAbstractionLayer\EntityCollection; @@ -66,6 +67,8 @@ public function __construct( * @public * * @description Allows you to access to the current resolver-context + * + * @param ResolverContext|EntityResolverContext $resolverContext */ public readonly ResolverContext $resolverContext, ) { diff --git a/src/Core/Content/Cms/Extension/CmsSlotsDataResolveExtension.php b/src/Core/Content/Cms/Extension/CmsSlotsDataResolveExtension.php index 0f10d7b2134..44d52cef47e 100644 --- a/src/Core/Content/Cms/Extension/CmsSlotsDataResolveExtension.php +++ b/src/Core/Content/Cms/Extension/CmsSlotsDataResolveExtension.php @@ -3,6 +3,7 @@ namespace Shopware\Core\Content\Cms\Extension; use Shopware\Core\Content\Cms\Aggregate\CmsSlot\CmsSlotCollection; +use Shopware\Core\Content\Cms\DataResolver\ResolverContext\EntityResolverContext; use Shopware\Core\Content\Cms\DataResolver\ResolverContext\ResolverContext; use Shopware\Core\Framework\Extensions\Extension; use Shopware\Core\Framework\Log\Package; @@ -37,6 +38,8 @@ public function __construct( * @public * * @description Allows you to access to the current resolver-context + * + * @param ResolverContext|EntityResolverContext $resolverContext */ public readonly ResolverContext $resolverContext, ) { diff --git a/src/Core/Content/DependencyInjection/media.xml b/src/Core/Content/DependencyInjection/media.xml index 918fe984d2a..2741122fbc2 100644 --- a/src/Core/Content/DependencyInjection/media.xml +++ b/src/Core/Content/DependencyInjection/media.xml @@ -119,6 +119,7 @@ + diff --git a/src/Core/Content/Media/File/DownloadResponseGenerator.php b/src/Core/Content/Media/File/DownloadResponseGenerator.php index ec6764e15a9..da75e8fd8d9 100644 --- a/src/Core/Content/Media/File/DownloadResponseGenerator.php +++ b/src/Core/Content/Media/File/DownloadResponseGenerator.php @@ -6,6 +6,7 @@ use League\Flysystem\FilesystemOperator; use League\Flysystem\UnableToGenerateTemporaryUrl; use Psr\Http\Message\StreamInterface; +use Psr\Log\LoggerInterface; use Shopware\Core\Content\Media\Core\Application\AbstractMediaUrlGenerator; use Shopware\Core\Content\Media\Core\Params\UrlParams; use Shopware\Core\Content\Media\MediaEntity; @@ -31,6 +32,7 @@ class DownloadResponseGenerator * @internal */ public function __construct( + private readonly LoggerInterface $logger, private readonly FilesystemOperator $filesystemPublic, private readonly FilesystemOperator $filesystemPrivate, private readonly MediaService $mediaService, @@ -53,7 +55,10 @@ public function getResponse( $url = $fileSystem->temporaryUrl($path, (new \DateTime())->modify($expiration)); return new RedirectResponse($url); - } catch (UnableToGenerateTemporaryUrl) { + } catch (UnableToGenerateTemporaryUrl $exception) { + $this->logger->warning($exception->getMessage(), ['exception' => $exception]); + } catch (\Exception $exception) { + $this->logger->critical($exception->getMessage(), ['exception' => $exception]); } return $this->getDefaultResponse($media, $context, $fileSystem); diff --git a/src/Elasticsearch/Product/ElasticsearchProductDefinition.php b/src/Elasticsearch/Product/ElasticsearchProductDefinition.php index 47e114f71ed..127106821bf 100644 --- a/src/Elasticsearch/Product/ElasticsearchProductDefinition.php +++ b/src/Elasticsearch/Product/ElasticsearchProductDefinition.php @@ -65,7 +65,7 @@ public function getMapping(Context $context): array ]; } - $debug = $this->environment !== 'prod'; + $debug = $this->environment === 'dev' || $this->environment === 'test'; $properties = [ 'id' => self::KEYWORD_FIELD, diff --git a/tests/integration/Core/Checkout/Promotion/Cart/PromotionCalculatorTest.php b/tests/integration/Core/Checkout/Promotion/Cart/PromotionCalculatorTest.php index e109770cb1f..b05dd6e6da4 100644 --- a/tests/integration/Core/Checkout/Promotion/Cart/PromotionCalculatorTest.php +++ b/tests/integration/Core/Checkout/Promotion/Cart/PromotionCalculatorTest.php @@ -2,14 +2,17 @@ namespace Shopware\Tests\Integration\Core\Checkout\Promotion\Cart; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use Shopware\Core\Checkout\Cart\Cart; use Shopware\Core\Checkout\Cart\CartBehavior; +use Shopware\Core\Checkout\Cart\Error\ErrorCollection; use Shopware\Core\Checkout\Cart\LineItem\LineItem; use Shopware\Core\Checkout\Cart\LineItem\LineItemCollection; use Shopware\Core\Checkout\Cart\Price\Struct\AbsolutePriceDefinition; use Shopware\Core\Checkout\Cart\Price\Struct\CalculatedPrice; use Shopware\Core\Checkout\Cart\Price\Struct\CartPrice; +use Shopware\Core\Checkout\Cart\SalesChannel\CartService; use Shopware\Core\Checkout\Cart\Tax\Struct\CalculatedTaxCollection; use Shopware\Core\Checkout\Cart\Tax\Struct\TaxRuleCollection; use Shopware\Core\Checkout\Promotion\Aggregate\PromotionDiscount\PromotionDiscountEntity; @@ -18,13 +21,19 @@ use Shopware\Core\Checkout\Promotion\Cart\PromotionProcessor; use Shopware\Core\Checkout\Promotion\PromotionCollection; use Shopware\Core\Checkout\Promotion\PromotionDefinition; +use Shopware\Core\Content\Product\Aggregate\ProductVisibility\ProductVisibilityDefinition; +use Shopware\Core\Defaults; +use Shopware\Core\Framework\Context; use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository; use Shopware\Core\Framework\Log\Package; use Shopware\Core\Framework\Test\TestCaseBase\IntegrationTestBehaviour; use Shopware\Core\Framework\Uuid\Uuid; +use Shopware\Core\System\SalesChannel\Context\AbstractSalesChannelContextFactory; +use Shopware\Core\System\SalesChannel\Context\SalesChannelContextFactory; use Shopware\Core\System\SalesChannel\Context\SalesChannelContextService; use Shopware\Core\System\SalesChannel\Context\SalesChannelContextServiceParameters; use Shopware\Core\System\SalesChannel\SalesChannelContext; +use Shopware\Core\Test\Integration\Traits\Promotion\PromotionTestFixtureBehaviour; use Shopware\Core\Test\TestDefaults; use Shopware\Tests\Unit\Core\Checkout\Cart\LineItem\Group\Helpers\Traits\LineItemTestFixtureBehaviour; @@ -32,10 +41,12 @@ * @internal */ #[Package('checkout')] +#[CoversClass(PromotionCalculator::class)] class PromotionCalculatorTest extends TestCase { use IntegrationTestBehaviour; use LineItemTestFixtureBehaviour; + use PromotionTestFixtureBehaviour; private PromotionCalculator $promotionCalculator; @@ -262,6 +273,63 @@ public function testFixedUnitPricePromotions(): void static::assertCount(2, $toCalculate->getLineItems()); } + public function testTest(): void + { + $promotionId = Uuid::randomHex(); + $product1 = $this->createProduct(); + $product2 = $this->createProduct(); + $product3 = $this->createProduct(); + + /** @var AbstractSalesChannelContextFactory $factory */ + $factory = static::getContainer()->get(SalesChannelContextFactory::class); + $context = $factory->create(Uuid::randomHex(), TestDefaults::SALES_CHANNEL); + + $data = [ + 'id' => $promotionId, + 'code' => 'Black Friday', + 'useCodes' => true, + 'useSetGroups' => true, + ]; + + $this->createPromotionWithCustomData($data, $this->promotionRepository, $context); + $this->createSetGroupDiscount($promotionId, 1, static::getContainer(), 50, null, applierKey: '2'); + $this->createSetGroupFixture('COUNT', 2, 'PRICE_ASC', $promotionId, static::getContainer()); + + $promotion = new LineItem($promotionId, PromotionProcessor::LINE_ITEM_TYPE, 'Black Friday'); + + $lineItem = new LineItem($product1, LineItem::PRODUCT_LINE_ITEM_TYPE, $product1); + $lineItem->setRemovable(true); + + $lineItem2 = new LineItem($product2, LineItem::PRODUCT_LINE_ITEM_TYPE, $product2); + $lineItem2->setRemovable(true); + + $lineItem3 = new LineItem($product3, LineItem::PRODUCT_LINE_ITEM_TYPE, $product2); + $lineItem3->setRemovable(true); + + /** @var CartService $cartService */ + $cartService = static::getContainer()->get(CartService::class); + $cart = $cartService->createNew('test-token'); + + $firstCalculatedCard = $cartService->add($cart, [$lineItem, $lineItem2, $lineItem3, $promotion], $context); + static::assertNotNull($firstCalculatedCard->getErrors()->first()); + static::assertSame('Discount Black Friday has been added', $firstCalculatedCard->getErrors()->first()->getMessage()); + static::assertCount(4, $firstCalculatedCard->getLineItems()); + static::assertSame(25.0, $firstCalculatedCard->getPrice()->getTotalPrice()); + + $firstCalculatedCard->setErrors(new ErrorCollection()); + + $secondCalculatedCard = $cartService->remove($firstCalculatedCard, $lineItem->getId(), $context); + static::assertCount(0, $secondCalculatedCard->getErrors()); + static::assertCount(3, $secondCalculatedCard->getLineItems()); + static::assertSame(15.0, $secondCalculatedCard->getPrice()->getTotalPrice()); + + $thirdCalculatedCard = $cartService->remove($secondCalculatedCard, $lineItem2->getId(), $context); + static::assertNotNull($thirdCalculatedCard->getErrors()->first()); + static::assertSame('Promotion Black Friday not eligible for cart!', $thirdCalculatedCard->getErrors()->first()->getMessage()); + static::assertCount(1, $thirdCalculatedCard->getLineItems()); + static::assertSame(10.0, $thirdCalculatedCard->getPrice()->getTotalPrice()); + } + private function getPromotionId(bool $preventCombination = false, int $priority = 1, bool $useCodes = true, string $type = PromotionDiscountEntity::TYPE_ABSOLUTE): string { $promotionId = Uuid::randomHex(); @@ -315,4 +383,42 @@ private function getDiscountItem(string $promotionId, string $typen = PromotionD return $discountItemToBeExcluded; } + + private function createProduct(): string + { + $id = Uuid::randomHex(); + + static::getContainer()->get('product.repository') + ->create([ + [ + 'id' => $id, + 'name' => 'test', + 'productNumber' => Uuid::randomHex(), + 'stock' => 10, + 'price' => [ + ['currencyId' => Defaults::CURRENCY, 'gross' => 10, 'net' => 7, 'linked' => false], + ], + 'purchasePrices' => [ + ['currencyId' => Defaults::CURRENCY, 'gross' => 7.5, 'net' => 5, 'linked' => false], + ['currencyId' => Uuid::randomHex(), 'gross' => 150, 'net' => 100, 'linked' => false], + ], + 'active' => true, + 'taxId' => $this->getValidTaxId(), + 'weight' => 100, + 'height' => 101, + 'width' => 102, + 'length' => 103, + 'visibilities' => [ + ['salesChannelId' => TestDefaults::SALES_CHANNEL, 'visibility' => ProductVisibilityDefinition::VISIBILITY_ALL], + ], + 'translations' => [ + Defaults::LANGUAGE_SYSTEM => [ + 'name' => 'test', + ], + ], + ], + ], Context::createDefaultContext()); + + return $id; + } } diff --git a/tests/unit/Core/Checkout/Cart/Subscriber/CartOrderEventSubscriberTest.php b/tests/unit/Core/Checkout/Cart/Subscriber/CartOrderEventSubscriberTest.php index 93f7aad7016..8c700bf9d18 100644 --- a/tests/unit/Core/Checkout/Cart/Subscriber/CartOrderEventSubscriberTest.php +++ b/tests/unit/Core/Checkout/Cart/Subscriber/CartOrderEventSubscriberTest.php @@ -5,8 +5,11 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Shopware\Core\Checkout\Cart\Event\BeforeLineItemAddedEvent; +use Shopware\Core\Checkout\Cart\Event\BeforeLineItemRemovedEvent; use Shopware\Core\Checkout\Cart\Event\CartDeletedEvent; use Shopware\Core\Checkout\Cart\Event\CheckoutOrderPlacedEvent; +use Shopware\Core\Checkout\Cart\LineItem\Group\LineItemGroupBuilder; use Shopware\Core\Checkout\Cart\Subscriber\CartOrderEventSubscriber; use Shopware\Core\Checkout\Order\OrderEntity; use Shopware\Core\Framework\Log\Package; @@ -29,7 +32,7 @@ class CartOrderEventSubscriberTest extends TestCase protected function setUp(): void { $this->contextSwitchRoute = $this->createMock(AbstractContextSwitchRoute::class); - $this->subscriber = new CartOrderEventSubscriber($this->contextSwitchRoute); + $this->subscriber = new CartOrderEventSubscriber($this->contextSwitchRoute, $this->createMock(LineItemGroupBuilder::class)); } public function testGetSubscribedEvents(): void @@ -40,6 +43,8 @@ public function testGetSubscribedEvents(): void static::assertArrayHasKey(CheckoutOrderPlacedEvent::class, $events); static::assertEquals(['handleContextAddress', 1], $events[CartDeletedEvent::class]); static::assertEquals(['handleContextAddress', 1], $events[CheckoutOrderPlacedEvent::class]); + static::assertEquals('resetBuilder', $events[BeforeLineItemAddedEvent::class]); + static::assertEquals('resetBuilder', $events[BeforeLineItemRemovedEvent::class]); } public function testHandleContextAddressWithCartDeletedEvent(): void @@ -86,4 +91,15 @@ public function testHandleContextAddressWithCheckoutOrderPlacedEvent(): void $this->subscriber->handleContextAddress($event); } + + public function testResetBuilder(): void + { + $builder = $this->createMock(LineItemGroupBuilder::class); + $builder + ->expects($this->once()) + ->method('reset'); + + (new CartOrderEventSubscriber($this->createMock(AbstractContextSwitchRoute::class), $builder)) + ->resetBuilder($this->createMock(BeforeLineItemAddedEvent::class)); + } } diff --git a/tests/unit/Core/Content/Media/File/DownloadResponseGeneratorTest.php b/tests/unit/Core/Content/Media/File/DownloadResponseGeneratorTest.php index 2a49cf56c75..72351501b2c 100644 --- a/tests/unit/Core/Content/Media/File/DownloadResponseGeneratorTest.php +++ b/tests/unit/Core/Content/Media/File/DownloadResponseGeneratorTest.php @@ -10,6 +10,7 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Http\Message\StreamInterface; +use Psr\Log\LoggerInterface; use Shopware\Core\Content\Media\Core\Application\AbstractMediaUrlGenerator; use Shopware\Core\Content\Media\File\DownloadResponseGenerator; use Shopware\Core\Content\Media\MediaEntity; @@ -47,6 +48,7 @@ protected function setUp(): void $publicFilesystem = $this->createMock(Filesystem::class); $this->downloadResponseGenerator = new DownloadResponseGenerator( + $this->createMock(LoggerInterface::class), $publicFilesystem, $this->privateFilesystem, $this->mediaService, @@ -66,6 +68,7 @@ public function testThrowsExceptionWithoutFilesystemAdapter(): void $media->setPath('foobar.txt'); $downloadResponseGenerator = new DownloadResponseGenerator( + $this->createMock(LoggerInterface::class), $this->createMock(FilesystemOperator::class), $this->createMock(FilesystemOperator::class), $this->mediaService, @@ -110,6 +113,7 @@ public function testGetResponse(bool $private, string $type, Response $expectedR $generator->method('generate')->willReturn([$media->getId() => 'foobar.txt']); $this->downloadResponseGenerator = new DownloadResponseGenerator( + $this->createMock(LoggerInterface::class), $privateFilesystem, $publicFilesystem, $this->mediaService, @@ -154,6 +158,48 @@ public static function filesystemProvider(): \Generator yield 'public / local' => [false, 'local', new RedirectResponse('foobar.txt')]; } + public function testGetResponseUsingAzureBlobStorageWithUnsupportedAuth(): void + { + $fileSystem = $this->createMock(Filesystem::class); + $expectedException = new \Exception('UnableToGenerateSasException'); + $fileSystem->method('temporaryUrl')->willThrowException($expectedException); + $logger = $this->createMock(LoggerInterface::class); + $logger->expects($this->once()) + ->method('critical') + ->with( + static::equalTo('UnableToGenerateSasException'), + static::equalTo(['exception' => $expectedException]), + ); + + $media = new MediaEntity(); + $media->setId(Uuid::randomHex()); + $media->setFileName('foobar'); + $media->setFileExtension('txt'); + $media->setPrivate(true); + $media->setPath('foobar.txt'); + + $generator = $this->createMock(AbstractMediaUrlGenerator::class); + $generator->method('generate')->willReturn([$media->getId() => 'foobar.txt']); + + $downloadResponseGenerator = new DownloadResponseGenerator( + $logger, + $fileSystem, + $fileSystem, + $this->mediaService, + 'php', + $generator, + '' + ); + + $streamInterface = $this->createMock(StreamInterface::class); + $streamInterface->method('detach')->willReturn(fopen('php://temp', 'r')); + $this->mediaService->method('loadFileStream')->willReturn($streamInterface); + + $response = $downloadResponseGenerator->getResponse($media, $this->salesChannelContext); + + AssertResponseHelper::assertResponseEquals(self::getExpectedStreamResponse(), $response); + } + /** * @return Filesystem&MockObject */