mirror of
https://github.com/Sylius/Sylius.git
synced 2026-07-05 20:57:12 +00:00
Merge branch '2.1' into filter-out-disabled-products-from-get-by-code-item-ednpoint
# Conflicts: # UPGRADE-2.1.md
This commit is contained in:
commit
76fa984cc2
113 changed files with 2069 additions and 221 deletions
|
|
@ -1,5 +1,34 @@
|
|||
# CHANGELOG FOR `2.1.X`
|
||||
|
||||
## v2.1.14 (2026-04-10)
|
||||
|
||||
#### Details
|
||||
|
||||
- [#18579](https://github.com/Sylius/Sylius/pull/18579) Fix problem with empty taxon product index ([@tomkalon](https://github.com/tomkalon))
|
||||
- [#18933](https://github.com/Sylius/Sylius/pull/18933) fix: add default filter to breadcrumbs configuration title to prevent ScalarDataBag exception ([@camilleislasse](https://github.com/camilleislasse))
|
||||
- [#18943](https://github.com/Sylius/Sylius/pull/18943) Fix build after APIPlatform 4.3.2 release ([@TheMilek](https://github.com/TheMilek))
|
||||
- [#18941](https://github.com/Sylius/Sylius/pull/18941) Add payment enabled in channel validation ([@marekrzytki](https://github.com/marekrzytki))
|
||||
- [#18926](https://github.com/Sylius/Sylius/pull/18926) [BUGFIX] Allow updating provinceName when provinceCode is null in API ([@rust-le](https://github.com/rust-le))
|
||||
- [#18953](https://github.com/Sylius/Sylius/pull/18953) [BUGFIX] Make mailer services public to allow usage in state machine callbacks ([@rust-le](https://github.com/rust-le))
|
||||
- [#18961](https://github.com/Sylius/Sylius/pull/18961) Resolve flash alert correctly without breaking translation ([@TheMilek](https://github.com/TheMilek))
|
||||
- [#18940](https://github.com/Sylius/Sylius/pull/18940) Restore missing page titles for admin resource show pages ([@marekrzytki](https://github.com/marekrzytki))
|
||||
- [#18922](https://github.com/Sylius/Sylius/pull/18922) Unified name translations handling in admin grid index ([@pbalcerzak](https://github.com/pbalcerzak))
|
||||
- [#18965](https://github.com/Sylius/Sylius/pull/18965) Allow choosing different payment method with skip payment step when it gets disabled ([@TheMilek](https://github.com/TheMilek))
|
||||
- [#18969](https://github.com/Sylius/Sylius/pull/18969) Add missing status code mapping for OrderItemNotFoundException ([@marekrzytki](https://github.com/marekrzytki))
|
||||
- [#18970](https://github.com/Sylius/Sylius/pull/18970) Add email validation constraints to cart update ([@marekrzytki](https://github.com/marekrzytki))
|
||||
- [#18972](https://github.com/Sylius/Sylius/pull/18972) Fix admin templates ([@loic425](https://github.com/loic425))
|
||||
- [#18974](https://github.com/Sylius/Sylius/pull/18974) Unify Tests directory with tests ([@TheMilek](https://github.com/TheMilek))
|
||||
|
||||
## v2.1.13 (2026-03-18)
|
||||
|
||||
#### Details
|
||||
|
||||
- [#18904](https://github.com/Sylius/Sylius/pull/18904) [BUGFIX] remove redundant `object` from PHPDoc union types ([@rust-le](https://github.com/rust-le))
|
||||
- [#18899](https://github.com/Sylius/Sylius/pull/18899) [CS][DX] Refactor
|
||||
- [#18895](https://github.com/Sylius/Sylius/pull/18895) [Admin] Fix product taxon grid `enabled` field always showing `true` ([@serhiilabs](https://github.com/serhiilabs))
|
||||
- [#18911](https://github.com/Sylius/Sylius/pull/18911) [BUGFIX] fix build errors ([@rust-le](https://github.com/rust-le))
|
||||
- [#18920](https://github.com/Sylius/Sylius/pull/18920) Telemetry improvements 2.1 ([@TheMilek](https://github.com/TheMilek))
|
||||
|
||||
## v2.1.12 (2026-03-09)
|
||||
|
||||
#### Details
|
||||
|
|
|
|||
354
UPGRADE-2.1.md
354
UPGRADE-2.1.md
|
|
@ -1,4 +1,4 @@
|
|||
# UPGRADE FROM `2.1.11` TO `2.1.12`
|
||||
# UPGRADE FROM `2.1.13` TO `2.1.14`
|
||||
|
||||
## Shop API
|
||||
|
||||
|
|
@ -9,26 +9,362 @@
|
|||
If you decorate this service, make sure your decorator also implements
|
||||
`ApiPlatform\Doctrine\Orm\Extension\QueryItemExtensionInterface` and proxies the `applyToItem` method.
|
||||
|
||||
## Redirect behavior changes
|
||||
### Admin resource translations
|
||||
|
||||
`CurrencySwitchController`, `ImpersonateUserController` and `StorageBasedLocaleSwitcher` no longer use the
|
||||
HTTP `Referer` header for redirects. Instead, they use the `RouterInterface` to generate a redirect URL based on
|
||||
the `_sylius.redirect` route attribute (defaulting to `sylius_shop_homepage`).
|
||||
Translations for admin-managed resources (such as product options, attributes, associations) are now resolved using the current admin locale instead of the default `%locale%` parameter.
|
||||
|
||||
If your application relied on the Referer-based redirect behavior (e.g., to return the user to the page they came from
|
||||
after switching currency or locale), you can customize the redirect target by overriding the route definition:
|
||||
Previously, translation-aware query builders relied on a static `%locale%` parameter, which could lead to inconsistencies when the admin user was working in a different language than the application default.
|
||||
|
||||
This has been updated to dynamically resolve the locale from the admin context.
|
||||
|
||||
### Before
|
||||
```yaml
|
||||
sylius_grid:
|
||||
grids:
|
||||
sylius_admin_product_association_type:
|
||||
driver:
|
||||
name: doctrine/orm
|
||||
options:
|
||||
class: "%sylius.model.product_association_type.class%"
|
||||
repository:
|
||||
method: createListQueryBuilder
|
||||
arguments: ["%locale%"]
|
||||
```
|
||||
|
||||
### After
|
||||
```yaml
|
||||
sylius_grid:
|
||||
grids:
|
||||
sylius_admin_product_association_type:
|
||||
driver:
|
||||
name: doctrine/orm
|
||||
options:
|
||||
class: "%sylius.model.product_association_type.class%"
|
||||
repository:
|
||||
method: createListQueryBuilder
|
||||
arguments: ["expr:service('sylius.context.locale').getLocaleCode()"]
|
||||
```
|
||||
|
||||
### Restore missing admin resource show title
|
||||
|
||||
A missing page title has been restored to the admin resource show pages (e.g. product, order, customer, shipment, catalog promotion).
|
||||
|
||||
Added separated show routes for each resource with dedicated show page templates.
|
||||
|
||||
### Order payment method eligibility check
|
||||
|
||||
The `Sylius\Bundle\CoreBundle\Validator\Constraints\OrderPaymentMethodEligibilityValidator` and
|
||||
`Sylius\Bundle\ApiBundle\Validator\Constraints\OrderPaymentMethodEligibilityValidator` now checks also if
|
||||
payment method is assigned to the channel of the order.
|
||||
|
||||
# UPGRADE FROM `2.1.12` TO `2.1.13`
|
||||
|
||||
### Telemetry improvements
|
||||
|
||||
Telemetry has been improved with per-query database timeouts to prevent slow queries from blocking admin panel
|
||||
requests. Timeouts are applied automatically using platform-specific mechanisms (MySQL, MariaDB, PostgreSQL).
|
||||
|
||||
The global query timeout (in milliseconds, default: 60000, minimum: 1000) is **configurable** via environment variable:
|
||||
|
||||
```dotenv
|
||||
SYLIUS_TELEMETRY_QUERY_TIMEOUT=30000
|
||||
```
|
||||
|
||||
Additionally, telemetry collection is now rate-limited — it will be skipped if it was already triggered within
|
||||
the last hour, preventing redundant data collection on rapid admin page loads.
|
||||
|
||||
# UPGRADE FROM `2.1.11` TO `2.1.12`
|
||||
|
||||
This is a **security release** addressing multiple vulnerabilities. Updating is strongly recommended.
|
||||
|
||||
## Security fixes
|
||||
|
||||
### [API] DQL Injection via API Order Filters (Critical)
|
||||
|
||||
An unauthenticated DQL injection vulnerability has been fixed in the following API order filters:
|
||||
|
||||
- `Sylius\Bundle\ApiBundle\Filter\Doctrine\ProductPriceOrderFilter`
|
||||
- `Sylius\Bundle\ApiBundle\Filter\Doctrine\TranslationOrderNameAndLocaleFilter`
|
||||
|
||||
Previously, user-supplied sort direction values (e.g. `order[price]`, `order[translation.name]`) were passed directly
|
||||
into DQL `ORDER BY` clauses without validation, allowing an attacker to inject arbitrary DQL expressions.
|
||||
|
||||
Both filters now define an `ALLOWED_DIRECTIONS` whitelist (`['asc', 'desc']`) and validate the input against it
|
||||
before applying it to the query. Invalid values are silently ignored.
|
||||
|
||||
**Changes in `ProductPriceOrderFilter`:**
|
||||
|
||||
```diff
|
||||
final class ProductPriceOrderFilter extends AbstractContextAwareFilter
|
||||
{
|
||||
+ private const ALLOWED_DIRECTIONS = ['asc', 'desc'];
|
||||
+
|
||||
protected function filterProperty(/* ... */)
|
||||
{
|
||||
// ...
|
||||
+ $direction = strtolower($value['price']);
|
||||
+ if (!in_array($direction, self::ALLOWED_DIRECTIONS, true)) {
|
||||
+ return;
|
||||
+ }
|
||||
// ...
|
||||
- ->orderBy('channelPricing.price', $value['price'])
|
||||
+ ->orderBy('channelPricing.price', $direction)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Changes in `TranslationOrderNameAndLocaleFilter`:**
|
||||
|
||||
```diff
|
||||
final class TranslationOrderNameAndLocaleFilter extends AbstractContextAwareFilter
|
||||
{
|
||||
+ private const ALLOWED_DIRECTIONS = ['asc', 'desc'];
|
||||
+
|
||||
protected function filterProperty(/* ... */)
|
||||
{
|
||||
// ...
|
||||
- $direction = $value['translation.name'];
|
||||
+ $direction = strtolower($value['translation.name']);
|
||||
+ if (!in_array($direction, self::ALLOWED_DIRECTIONS, true)) {
|
||||
+ return;
|
||||
+ }
|
||||
// ...
|
||||
- ->orderBy('translation.name', $value['translation.name'])
|
||||
+ ->orderBy('translation.name', $direction)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**No action required** — this fix is applied automatically upon updating. If you have extended or overridden either
|
||||
of these filters, verify that your custom implementation also validates the sort direction.
|
||||
|
||||
### [Promotion] Race condition on promotion usage limit (High)
|
||||
|
||||
A race condition has been fixed where concurrent orders could exceed a promotion's usage limit. When multiple checkouts
|
||||
completed simultaneously, the non-atomic read-then-write of `Promotion::$used` allowed the counter to be incremented
|
||||
beyond `usageLimit`.
|
||||
|
||||
1. A new class has been introduced:
|
||||
|
||||
`Sylius\Bundle\CoreBundle\Doctrine\ORM\Promotion\Modifier\AtomicOrderPromotionsUsageModifier`
|
||||
|
||||
This class implements `Sylius\Component\Core\Promotion\Modifier\OrderPromotionsUsageModifierInterface` and uses
|
||||
atomic SQL statements (`UPDATE ... WHERE used < usage_limit` and `SELECT ... FOR UPDATE`) to enforce promotion
|
||||
and coupon usage limits at the database level, preventing race conditions.
|
||||
|
||||
Its constructor accepts a `Doctrine\DBAL\Connection`:
|
||||
|
||||
```php
|
||||
public function __construct(Connection $connection)
|
||||
```
|
||||
|
||||
2. The new service **decorates** the existing `sylius.promotion_usage_modifier` service:
|
||||
|
||||
```xml
|
||||
<service
|
||||
id="sylius.promotion_usage_modifier.atomic"
|
||||
class="Sylius\Bundle\CoreBundle\Doctrine\ORM\Promotion\Modifier\AtomicOrderPromotionsUsageModifier"
|
||||
decorates="sylius.promotion_usage_modifier"
|
||||
>
|
||||
<argument type="service" id="doctrine.dbal.default_connection" />
|
||||
</service>
|
||||
```
|
||||
|
||||
If you have overridden or decorated the `sylius.promotion_usage_modifier` service, review your customizations
|
||||
to ensure compatibility with the new decorator chain.
|
||||
|
||||
3. A new exception class has been introduced:
|
||||
|
||||
`Sylius\Component\Core\Promotion\Exception\PromotionUsageLimitReachedException`
|
||||
|
||||
This exception extends `Doctrine\ORM\OptimisticLockException` and is thrown when a promotion or coupon usage
|
||||
limit has been reached during checkout. It provides two named constructors:
|
||||
|
||||
```php
|
||||
PromotionUsageLimitReachedException::withPromotionCode(string $code): self
|
||||
PromotionUsageLimitReachedException::withCouponCode(string $code): self
|
||||
```
|
||||
|
||||
If you have custom error handling around the checkout completion workflow (e.g. in state machine callbacks
|
||||
or event listeners), you may want to catch this exception to display an appropriate message to the customer.
|
||||
|
||||
### [Admin] XSS in taxon tree and autocompletes (High)
|
||||
|
||||
A stored XSS vulnerability has been fixed in the admin panel's taxon tree and autocomplete components. Taxon names
|
||||
containing malicious HTML/JavaScript were rendered without escaping.
|
||||
|
||||
**Affected files:**
|
||||
|
||||
- `Sylius\Bundle\AdminBundle\Resources\assets\controllers\ProductTaxonTreeController.js` — taxon name is now escaped
|
||||
via `textContent` before insertion into the DOM template.
|
||||
- `Sylius\Bundle\AdminBundle\Resources\assets\controllers\TaxonTreeController.js` — uses `textContent` assignment
|
||||
instead of string interpolation with `replaceAll('__TAXON_NAME__', name)`.
|
||||
- New file `Sylius\Bundle\AdminBundle\Resources\assets\scripts\autocomplete-xss-protection.js` — intercepts
|
||||
`autocomplete:pre-connect` events and escapes label fields before rendering.
|
||||
|
||||
**No action required** — this fix is applied automatically upon updating. If you have custom JavaScript that renders
|
||||
taxon names or autocomplete labels using `innerHTML` or string templates, review your code for similar XSS vectors.
|
||||
|
||||
### [Shop] XSS in taxon breadcrumbs (High)
|
||||
|
||||
A stored XSS vulnerability has been fixed in the shop breadcrumbs template. Breadcrumb labels were rendered using
|
||||
Twig's `|raw` filter, allowing injected HTML in taxon or order names to execute in the browser.
|
||||
|
||||
**Changes in `shared/breadcrumbs.html.twig`:**
|
||||
|
||||
```diff
|
||||
- <a class="link-reset" href="{{ item.path }}">{{ item.label|raw }}</a>
|
||||
+ <a class="link-reset" href="{{ item.path }}">{{ item.label }}</a>
|
||||
...
|
||||
- <span class="text-body-tertiary text-break">{{ item.label|raw }}</span>
|
||||
+ <span class="text-body-tertiary text-break">{{ item.label }}</span>
|
||||
```
|
||||
|
||||
If you have overridden the `shared/breadcrumbs.html.twig` template, ensure you are not using the `|raw` filter
|
||||
on user-controllable label values.
|
||||
|
||||
### [Shop] XSS in ApiLoginController (Medium)
|
||||
|
||||
A DOM-based XSS vulnerability has been fixed in `ApiLoginController.js`. The server error message was inserted
|
||||
using `innerHTML`, allowing malicious content in the response to execute JavaScript.
|
||||
|
||||
```diff
|
||||
- errorElement.innerHTML = response.message;
|
||||
+ errorElement.textContent = response.message;
|
||||
```
|
||||
|
||||
**No action required** — this fix is applied automatically upon updating.
|
||||
|
||||
### [Shop] IDOR in checkout address LiveComponent (High)
|
||||
|
||||
An Insecure Direct Object Reference (IDOR) vulnerability has been fixed in the checkout address LiveComponent.
|
||||
The `addressFieldUpdated()` method in `Checkout\Address\FormComponent` used `$this->addressRepository->find($addressId)`,
|
||||
allowing any authenticated customer to load another customer's address by manipulating the `#[LiveArg]` value.
|
||||
|
||||
The fix replaces `find()` with `findOneByCustomer()` to scope the lookup to the current customer:
|
||||
|
||||
```diff
|
||||
- $address = $this->addressRepository->find($addressId);
|
||||
+ $customer = $this->customerContext->getCustomer();
|
||||
+ if (!$customer instanceof CustomerInterface) {
|
||||
+ return;
|
||||
+ }
|
||||
+ $address = $this->addressRepository->findOneByCustomer((string) $addressId, $customer);
|
||||
+ if (null === $address) {
|
||||
+ return;
|
||||
+ }
|
||||
```
|
||||
|
||||
Additionally, `SummaryComponent::refreshCart()` and `WidgetComponent::refreshCart()` no longer accept an external
|
||||
`$cartId` argument — they use the internally held cart reference instead, preventing cart ID manipulation.
|
||||
|
||||
**No action required** — this fix is applied automatically upon updating. If you have extended or overridden
|
||||
`Checkout\Address\FormComponent`, `Cart\SummaryComponent`, or `Cart\WidgetComponent`, review your customizations
|
||||
to ensure they do not expose similar IDOR vectors.
|
||||
|
||||
### [API] Cart authorization bypass (High)
|
||||
|
||||
An authorization bypass has been fixed in `AddItemToCartHandler`. Previously, any authenticated user could add items
|
||||
to another user's cart by providing a different cart token in the API request.
|
||||
|
||||
The handler now validates that the current user has access to the cart before modifying it:
|
||||
|
||||
```diff
|
||||
final readonly class AddItemToCartHandler
|
||||
{
|
||||
public function __construct(
|
||||
// ...
|
||||
+ private ?UserContextInterface $userContext = null,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(AddItemToCart $addItemToCart): OrderInterface
|
||||
{
|
||||
// ...
|
||||
+ $this->assertCartAccessible($cart);
|
||||
// ...
|
||||
}
|
||||
+
|
||||
+ private function assertCartAccessible(OrderInterface $cart): void
|
||||
+ {
|
||||
+ // Validates current user matches cart owner
|
||||
+ // Guest carts remain accessible
|
||||
+ // Throws NotFoundHttpException if access denied
|
||||
+ }
|
||||
}
|
||||
```
|
||||
|
||||
The `UserContextInterface` service is now injected into the handler via `command_handlers.xml`.
|
||||
|
||||
**No action required** — this fix is applied automatically upon updating. If you have overridden the
|
||||
`AddItemToCartHandler` or its service definition, ensure the new `UserContextInterface` argument is included.
|
||||
|
||||
### Open Redirect vulnerability (Medium)
|
||||
|
||||
An open redirect vulnerability has been fixed in `CurrencySwitchController`, `ImpersonateUserController` and
|
||||
`StorageBasedLocaleSwitcher`. These controllers no longer use the HTTP `Referer` header for redirects. Instead,
|
||||
they use the `RouterInterface` to generate a redirect URL based on the `_sylius.redirect` route attribute
|
||||
(defaulting to `sylius_shop_homepage`).
|
||||
|
||||
A new trait has been added: `Sylius\Bundle\ShopBundle\Controller\RedirectTrait`.
|
||||
|
||||
**Affected classes:**
|
||||
|
||||
- `Sylius\Bundle\ShopBundle\Controller\CurrencySwitchController`
|
||||
- `Sylius\Bundle\AdminBundle\Controller\ImpersonateUserController`
|
||||
- `Sylius\Bundle\ShopBundle\Locale\StorageBasedLocaleSwitcher`
|
||||
|
||||
If your application relied on the Referer-based redirect behavior, you can customize the redirect target
|
||||
by overriding the route definition:
|
||||
|
||||
```yaml
|
||||
# config/routes/sylius_shop.yaml
|
||||
sylius_shop_switch_currency:
|
||||
path: /{_locale}/switch-currency/{code}
|
||||
methods: [GET]
|
||||
defaults:
|
||||
_controller: sylius.controller.shop.currency_switch:switchAction
|
||||
_sylius:
|
||||
redirect: sylius_shop_homepage # or any route name you prefer
|
||||
redirect: sylius_shop_homepage
|
||||
```
|
||||
|
||||
## Other changes
|
||||
|
||||
### Fix too frequent/long requests to GUS
|
||||
|
||||
The `Sylius\Bundle\AdminBundle\Controller\NotificationController` has been updated to reduce the frequency and
|
||||
duration of outbound requests to GUS.
|
||||
|
||||
1. The constructor of `NotificationController` has been modified:
|
||||
|
||||
```diff
|
||||
public function __construct(
|
||||
private ClientInterface $client,
|
||||
private MessageFactory $messageFactory,
|
||||
string $hubUri,
|
||||
private string $environment,
|
||||
+ private CacheItemPoolInterface $cache,
|
||||
)
|
||||
```
|
||||
|
||||
The `cache.app` service is injected as the new argument.
|
||||
|
||||
2. Responses are now **cached for 24 hours** (`TTL = 86400s`) using PSR-6 `CacheItemPoolInterface`.
|
||||
Subsequent calls to `getVersionAction()` return the cached result without making an HTTP request.
|
||||
|
||||
3. HTTP request timeouts have been added:
|
||||
|
||||
```diff
|
||||
-$hubResponse = $this->client->send($hubRequest, ['verify' => false]);
|
||||
+$hubResponse = $this->client->send($hubRequest, [
|
||||
+ 'verify' => false,
|
||||
+ 'timeout' => 2,
|
||||
+ 'connect_timeout' => 1,
|
||||
+]);
|
||||
```
|
||||
|
||||
If you have overridden the `sylius.controller.admin.notification` service or its arguments, update your
|
||||
configuration to include the new `CacheItemPoolInterface` argument.
|
||||
|
||||
# UPGRADE FROM `2.1.8` TO `2.1.9`
|
||||
|
||||
## Telemetry
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "sylius/sylius",
|
||||
"type": "library",
|
||||
"version": "v2.1.13-dev",
|
||||
"version": "v2.1.15-dev",
|
||||
"description": "E-Commerce platform for PHP, based on Symfony framework.",
|
||||
"homepage": "https://sylius.com",
|
||||
"license": "MIT",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
admin_basic_create:
|
||||
path: /admin/basic/create
|
||||
controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController
|
||||
defaults:
|
||||
template: admin/basic/create.html.twig
|
||||
|
||||
admin_basic_update:
|
||||
path: /admin/basic/update
|
||||
controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController
|
||||
defaults:
|
||||
template: admin/basic/update.html.twig
|
||||
|
||||
admin_basic_index:
|
||||
path: /admin/basic/index
|
||||
controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController
|
||||
defaults:
|
||||
template: admin/basic/index.html.twig
|
||||
|
||||
admin_basic_show:
|
||||
path: /admin/basic/show
|
||||
controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController
|
||||
defaults:
|
||||
template: admin/basic/show.html.twig
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
@paying_for_order
|
||||
Feature: Preventing placing an order with a payment method not available in channel
|
||||
In order to have my order shipped without issues
|
||||
As a Visitor
|
||||
I want to be prevented from placing an order with a payment method not available in my channel
|
||||
|
||||
Background:
|
||||
Given the store operates on a single channel in the "United States" named "US Web Store"
|
||||
And the store has a product "Ubi T-Shirt" priced at "$19.99"
|
||||
And the store ships everywhere for Free
|
||||
And the store allows paying "Bank Transfer"
|
||||
And I am a logged in customer
|
||||
|
||||
@api @ui
|
||||
Scenario: Being prevented from placing an order with a payment method that is not available in my channel
|
||||
Given I added product "Ubi T-Shirt" to the cart
|
||||
And I have proceeded selecting "Bank Transfer" payment method
|
||||
And the payment method "Bank Transfer" has been disabled in "US Web Store" channel
|
||||
When I try to confirm my order
|
||||
Then I should be informed that this payment method has been disabled
|
||||
And I should not see the thank you page
|
||||
|
||||
@api @ui
|
||||
Scenario: Being prevented from placing an order with a payment method that is disabled
|
||||
Given I added product "Ubi T-Shirt" to the cart
|
||||
And I have proceeded selecting "Bank Transfer" payment method
|
||||
But the payment method "Bank Transfer" is disabled
|
||||
When I try to confirm my order
|
||||
Then I should be informed that this payment method has been disabled
|
||||
And I should not see the thank you page
|
||||
|
||||
@api @ui
|
||||
Scenario: Being prevented from placing an order with a payment method that is disabled and not available in my channel
|
||||
Given I added product "Ubi T-Shirt" to the cart
|
||||
And I have proceeded selecting "Bank Transfer" payment method
|
||||
But the payment method "Bank Transfer" is disabled
|
||||
And the payment method "Bank Transfer" has been disabled in "US Web Store" channel
|
||||
When I try to confirm my order
|
||||
Then I should be informed that this payment method has been disabled
|
||||
And I should not see the thank you page
|
||||
|
||||
@api @ui
|
||||
Scenario: Being able to place an order with enabled payment method in my channel
|
||||
Given I added product "Ubi T-Shirt" to the cart
|
||||
And I have proceeded selecting "Bank Transfer" payment method
|
||||
When I try to confirm my order
|
||||
Then I should see the thank you page
|
||||
|
|
@ -45,3 +45,35 @@ Feature: Skipping payment step when only one payment method is available
|
|||
When I complete addressing step with email "guest@example.com" and "United States" based billing address
|
||||
And I complete the shipping step with the first shipping method
|
||||
Then I should be on the checkout payment step
|
||||
|
||||
@api @ui
|
||||
Scenario: Being able to recover after the auto-selected payment method gets disabled
|
||||
Given the payment method "Bank transfer" is disabled
|
||||
And the store allows paying with "Offline"
|
||||
And I added product "Guards! Guards!" to the cart
|
||||
And I complete addressing step with email "guest@example.com" and "United States" based billing address
|
||||
And I complete the shipping step with the first shipping method
|
||||
And the payment method "Offline" is disabled
|
||||
And the payment method "Bank transfer" is enabled
|
||||
And I have tried to confirm my order
|
||||
And I have been informed that this payment method has been disabled
|
||||
When I go back to payment step of the checkout
|
||||
And I choose "Bank transfer" payment method
|
||||
And I confirm my order
|
||||
Then I should see the thank you page
|
||||
|
||||
@api @ui
|
||||
Scenario: Being able to recover after the auto-selected payment method gets removed from channel
|
||||
Given the payment method "Bank transfer" is disabled
|
||||
And the store allows paying with "Offline"
|
||||
And I added product "Guards! Guards!" to the cart
|
||||
And I complete addressing step with email "guest@example.com" and "United States" based billing address
|
||||
And I complete the shipping step with the first shipping method
|
||||
And the payment method "Offline" has been disabled in "United States" channel
|
||||
And the payment method "Bank transfer" is enabled
|
||||
And I have tried to confirm my order
|
||||
And I have been informed that this payment method has been disabled
|
||||
When I go back to payment step of the checkout
|
||||
And I choose "Bank transfer" payment method
|
||||
And I confirm my order
|
||||
Then I should see the thank you page
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ final class CheckoutContext implements Context
|
|||
}
|
||||
|
||||
#[When('the customer is at the checkout payment step')]
|
||||
#[When('I go back to payment step of the checkout')]
|
||||
public function theCustomerIsAtTheCheckoutPaymentStep(): void
|
||||
{
|
||||
// Intentionally left blank, as this is a UI-specific action.
|
||||
|
|
@ -443,9 +444,8 @@ final class CheckoutContext implements Context
|
|||
$this->sharedStorage->set('order', $this->orderRepository->findOneByNumber($this->sharedStorage->get('order_number')));
|
||||
}
|
||||
|
||||
/**
|
||||
* @When I try to confirm my order
|
||||
*/
|
||||
#[Given('I have tried to confirm my order')]
|
||||
#[When('I try to confirm my order')]
|
||||
public function iTryToConfirmMyOrder(): void
|
||||
{
|
||||
$response = $this->completeOrder();
|
||||
|
|
@ -1151,9 +1151,8 @@ final class CheckoutContext implements Context
|
|||
$this->hasProvinceNameInAddress($provinceName, $addressType);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Then /^I should be informed that (this payment method) has been disabled$/
|
||||
*/
|
||||
#[Given('/^I have been informed that (this payment method) has been disabled$/')]
|
||||
#[Then('/^I should be informed that (this payment method) has been disabled$/')]
|
||||
public function iShouldBeInformedThatThisPaymentMethodHasBeenDisabled(PaymentMethodInterface $paymentMethod): void
|
||||
{
|
||||
$response = $this->client->getLastResponse();
|
||||
|
|
|
|||
|
|
@ -123,6 +123,15 @@ final readonly class PaymentContext implements Context
|
|||
$this->paymentMethodManager->flush();
|
||||
}
|
||||
|
||||
#[Given('the payment method :paymentMethod is enabled')]
|
||||
#[Given('the payment method :paymentMethod gets enabled')]
|
||||
public function theStoreHasAPaymentMethodEnabled(PaymentMethodInterface $paymentMethod): void
|
||||
{
|
||||
$paymentMethod->enable();
|
||||
|
||||
$this->paymentMethodManager->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Given /^(it) has instructions "([^"]+)"$/
|
||||
*/
|
||||
|
|
@ -154,6 +163,16 @@ final readonly class PaymentContext implements Context
|
|||
$this->paymentMethodManager->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Given the payment method :paymentMethod has been disabled in :channel channel
|
||||
*/
|
||||
public function theStoreHasDisabledPaymentMethodInChannel(PaymentMethodInterface $paymentMethod, ChannelInterface $channel): void
|
||||
{
|
||||
$paymentMethod->removeChannel($channel);
|
||||
|
||||
$this->paymentMethodManager->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Given the store allows paying with :paymentMethodName in :channel channel
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ namespace Sylius\Behat\Context\Ui\Shop\Checkout;
|
|||
|
||||
use Behat\Behat\Context\Context;
|
||||
use Behat\Mink\Exception\ElementNotFoundException;
|
||||
use Behat\Step\Given;
|
||||
use Behat\Step\Then;
|
||||
use Behat\Step\When;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
|
@ -100,6 +101,7 @@ final readonly class CheckoutCompleteContext implements Context
|
|||
* @Given the customer confirmed the order
|
||||
* @When I try to confirm my order
|
||||
*/
|
||||
#[Given('I have tried to confirm my order')]
|
||||
#[When('I confirm my order')]
|
||||
public function iConfirmMyOrder(): void
|
||||
{
|
||||
|
|
@ -325,9 +327,8 @@ final readonly class CheckoutCompleteContext implements Context
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Then /^I should be informed that (this payment method) has been disabled$/
|
||||
*/
|
||||
#[Given('/^I have been informed that (this payment method) has been disabled$/')]
|
||||
#[Then('/^I should be informed that (this payment method) has been disabled$/')]
|
||||
public function iShouldBeInformedThatThisPaymentMethodHasBeenDisabled(PaymentMethodInterface $paymentMethod): void
|
||||
{
|
||||
Assert::same(
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ sylius_grid:
|
|||
class: "%sylius.model.product_variant.class%"
|
||||
repository:
|
||||
method: createInventoryListQueryBuilder
|
||||
arguments: ["%locale%"]
|
||||
arguments: ["expr:service('sylius.context.locale').getLocaleCode()"]
|
||||
sorting:
|
||||
name: asc
|
||||
fields:
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ sylius_grid:
|
|||
class: "%sylius.model.payment_method.class%"
|
||||
repository:
|
||||
method: createListQueryBuilder
|
||||
arguments: ["%locale%"]
|
||||
arguments: ["expr:service('sylius.context.locale').getLocaleCode()"]
|
||||
sorting:
|
||||
position: asc
|
||||
fields:
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ sylius_grid:
|
|||
class: "%sylius.model.product_association_type.class%"
|
||||
repository:
|
||||
method: createListQueryBuilder
|
||||
arguments: ["%locale%"]
|
||||
arguments: ["expr:service('sylius.context.locale').getLocaleCode()"]
|
||||
sorting:
|
||||
code: asc
|
||||
fields:
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ sylius_grid:
|
|||
class: "%sylius.model.product_option.class%"
|
||||
repository:
|
||||
method: createListQueryBuilder
|
||||
arguments: ["%locale%"]
|
||||
arguments: ["expr:service('sylius.context.locale').getLocaleCode()"]
|
||||
sorting:
|
||||
position: asc
|
||||
fields:
|
||||
|
|
|
|||
|
|
@ -11,6 +11,16 @@ sylius_admin_catalog_promotion:
|
|||
permission: true
|
||||
type: sylius.resource
|
||||
|
||||
sylius_admin_catalog_promotion_show:
|
||||
path: /catalog-promotions/{id}
|
||||
methods: [GET]
|
||||
defaults:
|
||||
_controller: sylius.controller.catalog_promotion::showAction
|
||||
_sylius:
|
||||
section: admin
|
||||
permission: true
|
||||
template: "@SyliusAdmin/catalog_promotion/show.html.twig"
|
||||
|
||||
sylius_admin_catalog_promotion_product_variant_index:
|
||||
path: /catalog-promotions/{id}/variants
|
||||
methods: [GET]
|
||||
|
|
|
|||
|
|
@ -11,6 +11,16 @@ sylius_admin_customer:
|
|||
permission: true
|
||||
type: sylius.resource
|
||||
|
||||
sylius_admin_customer_show:
|
||||
path: /customers/{id}
|
||||
methods: [GET]
|
||||
defaults:
|
||||
_controller: sylius.controller.customer::showAction
|
||||
_sylius:
|
||||
section: admin
|
||||
permission: true
|
||||
template: "@SyliusAdmin/customer/show.html.twig"
|
||||
|
||||
sylius_admin_customer_order_index:
|
||||
path: /customers/{id}/orders
|
||||
methods: [GET]
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ sylius_admin_order_show:
|
|||
arguments:
|
||||
- '$id'
|
||||
permission: true
|
||||
template: "@SyliusAdmin/shared/crud/show.html.twig"
|
||||
template: "@SyliusAdmin/order/show.html.twig"
|
||||
|
||||
sylius_admin_order_history:
|
||||
path: /orders/{id}/history
|
||||
|
|
@ -50,7 +50,7 @@ sylius_admin_order_update:
|
|||
arguments:
|
||||
- '$id'
|
||||
permission: true
|
||||
template: "@SyliusAdmin/shared/crud/update.html.twig"
|
||||
template: "@SyliusAdmin/order/update.html.twig"
|
||||
form:
|
||||
type: Sylius\Bundle\AdminBundle\Form\Type\OrderType
|
||||
options:
|
||||
|
|
|
|||
|
|
@ -9,6 +9,16 @@ sylius_admin_product:
|
|||
type: Sylius\Bundle\AdminBundle\Form\Type\ProductType
|
||||
permission: true
|
||||
type: sylius.resource
|
||||
|
||||
sylius_admin_product_show:
|
||||
path: /products/{id}
|
||||
methods: [GET]
|
||||
defaults:
|
||||
_controller: sylius.controller.product::showAction
|
||||
_sylius:
|
||||
section: admin
|
||||
permission: true
|
||||
template: "@SyliusAdmin/product/show.html.twig"
|
||||
|
||||
sylius_admin_product_create_simple:
|
||||
path: /products/new/simple
|
||||
|
|
|
|||
|
|
@ -7,6 +7,16 @@ sylius_admin_shipment:
|
|||
grid: sylius_admin_shipment
|
||||
permission: true
|
||||
type: sylius.resource
|
||||
|
||||
sylius_admin_shipment_show:
|
||||
path: /shipments/{id}
|
||||
methods: [GET]
|
||||
defaults:
|
||||
_controller: sylius.controller.shipment::showAction
|
||||
_sylius:
|
||||
section: admin
|
||||
permission: true
|
||||
template: "@SyliusAdmin/shipment/show.html.twig"
|
||||
|
||||
sylius_admin_shipment_ship:
|
||||
path: /shipments/{id}/ship
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
{% extends '@SyliusAdmin/shared/layout/base.html.twig' %}
|
||||
|
||||
{% set prefixes = configuration.vars.hook_prefix is defined ? [configuration.vars.hook_prefix] %}
|
||||
{% set prefixes = prefixes|default({})|merge([
|
||||
'sylius_admin.%resource_name%'|replace({'%resource_name%': resource_name|default(metadata.name)}),
|
||||
'sylius_admin.common'
|
||||
]) %}
|
||||
|
||||
{% set header = metadata.applicationName ~ '.ui.' ~ metadata.pluralName %}
|
||||
|
||||
{% block title %}{{ 'sylius.ui.catalog_promotion'|trans ~ ' ' ~ catalog_promotion.name ~ ' | '}} {{ parent() }}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% hook 'show' with { _prefixes: prefixes, resource, metadata, configuration } %}
|
||||
{% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ parent() }}
|
||||
{% hook 'show#stylesheets' with { _prefixes: prefixes } %}
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{{ parent() }}
|
||||
{% hook 'show#javascripts' with { _prefixes: prefixes } %}
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
{% extends '@SyliusAdmin/shared/layout/base.html.twig' %}
|
||||
|
||||
{% set prefixes = configuration.vars.hook_prefix is defined ? [configuration.vars.hook_prefix] %}
|
||||
{% set prefixes = prefixes|default({})|merge([
|
||||
'sylius_admin.%resource_name%'|replace({'%resource_name%': resource_name|default(metadata.name)}),
|
||||
'sylius_admin.common'
|
||||
]) %}
|
||||
|
||||
{% set header = metadata.applicationName ~ '.ui.' ~ metadata.pluralName %}
|
||||
|
||||
{% block title %}{{ 'sylius.ui.customer'|trans ~' '~ customer.email ~ ' | '}} {{ parent() }}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% hook 'show' with { _prefixes: prefixes, resource, metadata, configuration } %}
|
||||
{% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ parent() }}
|
||||
{% hook 'show#stylesheets' with { _prefixes: prefixes } %}
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{{ parent() }}
|
||||
{% hook 'show#javascripts' with { _prefixes: prefixes } %}
|
||||
{% endblock %}
|
||||
|
|
@ -1,6 +1,18 @@
|
|||
<div>
|
||||
<div>
|
||||
<strong>{{ data.product.name }}</strong>
|
||||
{% if data.product.name is not null %}
|
||||
<strong>{{ data.product.name }}</strong>
|
||||
{% else %}
|
||||
<span class="text-danger" {{ sylius_test_html_attribute('missing-translation') }} >
|
||||
{{ ux_icon('tabler:exclamation-circle') }} <i>{{ 'sylius.ui.missing_translation'|trans }}</i>
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-muted small">{{ data.name }}</div>
|
||||
{% if data.name is not null %}
|
||||
<div class="text-muted small">{{ data.name }}</div>
|
||||
{% else %}
|
||||
<span class="text-danger" {{ sylius_test_html_attribute('missing-translation') }} >
|
||||
{{ ux_icon('tabler:exclamation-circle') }} <i>{{ 'sylius.ui.missing_translation'|trans }}</i>
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
] %}
|
||||
|
||||
{% block title %}
|
||||
{{ 'sylius.ui.history'|trans }} | {{ parent() }}
|
||||
{{ 'sylius.ui.order'|trans ~ ' #' ~ order.number ~ ' - ' ~ 'sylius.ui.order_history'|trans ~ ' | ' }}{{ parent() }}
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
|
|
|
|||
25
src/Sylius/Bundle/AdminBundle/templates/order/show.html.twig
Normal file
25
src/Sylius/Bundle/AdminBundle/templates/order/show.html.twig
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{% extends '@SyliusAdmin/shared/layout/base.html.twig' %}
|
||||
|
||||
{% set prefixes = configuration.vars.hook_prefix is defined ? [configuration.vars.hook_prefix] %}
|
||||
{% set prefixes = prefixes|default({})|merge([
|
||||
'sylius_admin.%resource_name%'|replace({'%resource_name%': resource_name|default(metadata.name)}),
|
||||
'sylius_admin.common'
|
||||
]) %}
|
||||
|
||||
{% set header = metadata.applicationName ~ '.ui.' ~ metadata.pluralName %}
|
||||
|
||||
{% block title %}{{ 'sylius.ui.order'|trans ~' #'~ order.number ~ ' | '}} {{ parent() }}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% hook 'show' with { _prefixes: prefixes, resource, metadata, configuration } %}
|
||||
{% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ parent() }}
|
||||
{% hook 'show#stylesheets' with { _prefixes: prefixes } %}
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{{ parent() }}
|
||||
{% hook 'show#javascripts' with { _prefixes: prefixes } %}
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
{% extends '@SyliusAdmin/shared/layout/base.html.twig' %}
|
||||
|
||||
{% set prefixes = configuration.vars.hook_prefix is defined ? [configuration.vars.hook_prefix] %}
|
||||
{% set prefixes = prefixes|default({})|merge([
|
||||
'sylius_admin.%resource_name%'|replace({'%resource_name%': resource_name|default(metadata.name)}),
|
||||
'sylius_admin.common'
|
||||
]) %}
|
||||
|
||||
{% set header = metadata.applicationName ~ '.ui.' ~ metadata.pluralName %}
|
||||
|
||||
{% block title %}{{ 'sylius.ui.edit_order'|trans ~' #'~ order.number ~ ' | '}} {{ parent() }}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% hook 'update' with { _prefixes: prefixes, resource, metadata, configuration, form } %}
|
||||
{% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ parent() }}
|
||||
{% hook 'update#stylesheets' with { _prefixes: prefixes } %}
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{{ parent() }}
|
||||
{% hook 'update#javascripts' with { _prefixes: prefixes } %}
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
{% extends '@SyliusAdmin/shared/layout/base.html.twig' %}
|
||||
|
||||
{% set prefixes = configuration.vars.hook_prefix is defined ? [configuration.vars.hook_prefix] %}
|
||||
{% set prefixes = prefixes|default({})|merge([
|
||||
'sylius_admin.%resource_name%'|replace({'%resource_name%': resource_name|default(metadata.name)}),
|
||||
'sylius_admin.common'
|
||||
]) %}
|
||||
|
||||
{% set header = metadata.applicationName ~ '.ui.' ~ metadata.pluralName %}
|
||||
|
||||
{% block title %}{{ 'sylius.ui.product'|trans ~ ' ' ~ product.name ~ ' | '}} {{ parent() }}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% hook 'show' with { _prefixes: prefixes, resource, metadata, configuration } %}
|
||||
{% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ parent() }}
|
||||
{% hook 'show#stylesheets' with { _prefixes: prefixes } %}
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{{ parent() }}
|
||||
{% hook 'show#javascripts' with { _prefixes: prefixes } %}
|
||||
{% endblock %}
|
||||
|
|
@ -1,12 +1,15 @@
|
|||
{% extends '@SyliusAdmin/shared/components/confirmation_modal.html.twig' %}
|
||||
{% import '@SyliusAdmin/shared/helper/button.html.twig' as button %}
|
||||
|
||||
{% props id = null, path = null, label = 'sylius.ui.delete', icon = 'tabler:trash-x', disabled = false, form_attr = null, type = 'delete' %}
|
||||
{% set modal_id = 'delete-modal-' ~ id %}
|
||||
{% props id = null, path = null, label = 'sylius.ui.delete', icon = 'tabler:trash-x', disabled = false, form_attr = null, type = 'delete', modal_id = null, icon_only = true %}
|
||||
|
||||
{% if modal_id is null %}
|
||||
{% set modal_id = 'delete-modal-' ~ id %}
|
||||
{% endif %}
|
||||
|
||||
{% block trigger %}
|
||||
<div data-bs-toggle="tooltip" data-bs-title="{{ label|trans }}">
|
||||
{{ button.default({icon: icon, icon_class: 'icon text-danger', icon_only: true, disabled: disabled, attr: 'data-bs-toggle=modal data-bs-target=#' ~ modal_id }) }}
|
||||
{{ button.default({icon: icon, icon_class: 'icon text-danger', icon_only: icon_only, text: label|trans, disabled: disabled, attr: 'data-bs-toggle=modal data-bs-target=#' ~ modal_id }) }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +1,27 @@
|
|||
{% if form is not defined %}
|
||||
{% set form = hookable_metadata.context.form %}
|
||||
{% set form = hookable_metadata.context.form|default(null) %}
|
||||
{% endif %}
|
||||
|
||||
{% if resource is not defined %}
|
||||
{% set resource = hookable_metadata.context.resource|default(null) %}
|
||||
{% endif %}
|
||||
|
||||
{% form_theme form '@SyliusAdmin/shared/form_theme.html.twig' %}
|
||||
{% if form is not null %}
|
||||
{% form_theme form '@SyliusAdmin/shared/form_theme.html.twig' %}
|
||||
|
||||
<div class="container-xl" {% if attributes is defined %} {{ attributes }} {% endif %}>
|
||||
{{ form_start(form, {'attr': {'novalidate': 'novalidate', 'id': form.vars.id}}) }}
|
||||
<div class="card-body">
|
||||
{% if hookable_metadata.configuration.method is defined %}
|
||||
<input type="hidden" name="_method" value="{{ hookable_metadata.configuration.method }}"/>
|
||||
{% elseif form.vars.data.id|default(null) is not null %}
|
||||
<input type="hidden" name="_method" value="PUT"/>
|
||||
{% endif %}
|
||||
{{ form_errors(form) }}
|
||||
{{ form_widget(form._token) }}
|
||||
<div class="container-xl" {% if attributes is defined %} {{ attributes }} {% endif %}>
|
||||
{{ form_start(form, {'attr': {'novalidate': 'novalidate', 'id': form.vars.id}}) }}
|
||||
<div class="card-body">
|
||||
{% if hookable_metadata.configuration.method is defined %}
|
||||
<input type="hidden" name="_method" value="{{ hookable_metadata.configuration.method }}"/>
|
||||
{% elseif form.vars.data.id|default(null) is not null %}
|
||||
<input type="hidden" name="_method" value="PUT"/>
|
||||
{% endif %}
|
||||
{{ form_errors(form) }}
|
||||
{{ form_widget(form._token) }}
|
||||
|
||||
{% hook 'form' with { form, resource } %}
|
||||
{% hook 'form' with { form, resource } %}
|
||||
</div>
|
||||
{{ form_end(form, {render_rest: hookable_metadata.configuration.render_rest|default(false)}) }}
|
||||
</div>
|
||||
{{ form_end(form, {render_rest: hookable_metadata.configuration.render_rest|default(false)}) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% set resources = hookable_metadata.context.resources %}
|
||||
{% set resources = hookable_metadata.context.resources|default(null) %}
|
||||
|
||||
<div class="col-12 col-md-auto ms-auto d-print-none">
|
||||
<div class="btn-list">
|
||||
|
|
|
|||
|
|
@ -1,14 +1,20 @@
|
|||
{% import '@SyliusAdmin/shared/helper/button.html.twig' as button %}
|
||||
|
||||
{% set configuration = hookable_metadata.context.configuration %}
|
||||
{% set configuration = hookable_metadata.context.configuration|default(null) %}
|
||||
|
||||
{% set fallback_url = app.request.headers.get('referer')|default(sylius_generate_redirect_path(path(
|
||||
configuration.vars.index.route.name|default(configuration.getRouteName('index')),
|
||||
configuration.vars.index.route.parameters|default(configuration.vars.route.parameters|default({}))
|
||||
))) %}
|
||||
{% if app.request.headers.has('referer') %}
|
||||
{% set fallback_url = app.request.headers.get('referer') %}
|
||||
{% else %}
|
||||
{% set fallback_url = configuration is not null ? sylius_generate_redirect_path(path(
|
||||
configuration.vars.index.route.name|default(configuration.getRouteName('index')),
|
||||
configuration.vars.index.route.parameters|default(configuration.vars.route.parameters|default({}))
|
||||
)): null %}
|
||||
{% endif %}
|
||||
|
||||
{{ button.cancel(sylius_test_form_attribute('cancel-changes-button')|merge({
|
||||
text: 'sylius.ui.back'|trans,
|
||||
class: 'btn',
|
||||
fallback_url: fallback_url
|
||||
})) }}
|
||||
{% if fallback_url is not null %}
|
||||
{{ button.cancel(sylius_test_form_attribute('cancel-changes-button')|merge({
|
||||
text: 'sylius.ui.back'|trans,
|
||||
class: 'btn',
|
||||
fallback_url: fallback_url
|
||||
})) }}
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{% import '@SyliusAdmin/shared/helper/button.html.twig' as button %}
|
||||
|
||||
{% set form = hookable_metadata.context.form %}
|
||||
{% set form = hookable_metadata.context.form|default(null) %}
|
||||
|
||||
{{ button.primary({ text: 'sylius.ui.create'|trans, type: 'submit', form: form.vars.id }) }}
|
||||
{{ button.primary({ text: 'sylius.ui.create'|trans, type: 'submit', form: form.vars.id|default(null) }) }}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,6 @@
|
|||
sylius_test_form_attribute('update-changes-button')|merge({
|
||||
text: 'sylius.ui.update'|trans,
|
||||
type: 'submit',
|
||||
form: hookable_metadata.context.form.vars.id
|
||||
form: hookable_metadata.context.form.vars.id|default(null)
|
||||
})
|
||||
) }}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
{% set metadata = hookable_metadata.context.metadata %}
|
||||
{% set header = hookable_metadata.configuration.title|default(metadata.applicationName~'.ui.'~metadata.pluralName) %}
|
||||
{% else %}
|
||||
{% set header = hookable_metadata.configuration.title %}
|
||||
{% set header = hookable_metadata.configuration.title|default(null) %}
|
||||
{% endif %}
|
||||
|
||||
{% set icon = hookable_metadata.configuration.icon|default(null) %}
|
||||
|
|
|
|||
|
|
@ -4,18 +4,28 @@
|
|||
{% set metadata = hookable_metadata.context.metadata %}
|
||||
{% set resource_name = hookable_metadata.configuration.resource_name|default(metadata.applicationName~'.ui.'~metadata.pluralName) %}
|
||||
{% else %}
|
||||
{% set resource_name = hookable_metadata.configuration.resource_name %}
|
||||
{% set resource_name = hookable_metadata.configuration.resource_name|default(null) %}
|
||||
{% endif %}
|
||||
|
||||
{% set configuration = hookable_metadata.context.configuration %}
|
||||
{% set index_url = path(
|
||||
configuration.vars.index.route.name|default(configuration.getRouteName('index')),
|
||||
configuration.vars.index.route.parameters|default(configuration.vars.route.parameters|default({}))
|
||||
)
|
||||
%}
|
||||
{% set configuration = hookable_metadata.context.configuration|default(null) %}
|
||||
|
||||
{{ breadcrumbs([
|
||||
{ name: 'sylius.ui.dashboard', url: path('sylius_admin_dashboard'), active: false },
|
||||
{ name: resource_name, url: index_url, active: false },
|
||||
{ name: 'sylius.ui.new'|trans, active: true}
|
||||
]) }}
|
||||
{% if configuration is not null %}
|
||||
{% set index_url = path(
|
||||
configuration.vars.index.route.name|default(configuration.getRouteName('index')),
|
||||
configuration.vars.index.route.parameters|default(configuration.vars.route.parameters|default({}))
|
||||
)
|
||||
%}
|
||||
{% endif %}
|
||||
|
||||
{% if index_url is defined and resource_name is not null %}
|
||||
{{ breadcrumbs([
|
||||
{ name: 'sylius.ui.dashboard', url: path('sylius_admin_dashboard'), active: false },
|
||||
{ name: resource_name, url: index_url, active: false },
|
||||
{ name: 'sylius.ui.new'|trans, active: true}
|
||||
]) }}
|
||||
{% else %}
|
||||
{{ breadcrumbs([
|
||||
{ name: 'sylius.ui.dashboard', url: path('sylius_admin_dashboard'), active: false },
|
||||
{ name: 'sylius.ui.new'|trans, active: true}
|
||||
]) }}
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@
|
|||
{% set singular_name = hookable_metadata.configuration.resource_name|default(metadata.applicationName ~ '.ui.' ~ _context.metadata.name) %}
|
||||
{% set header = hookable_metadata.configuration.header|default(null) %}
|
||||
{% else %}
|
||||
{% set singular_name = hookable_metadata.configuration.resource_name %}
|
||||
{% set header = hookable_metadata.configuration.header %}
|
||||
{% set singular_name = hookable_metadata.configuration.resource_name|default(null) %}
|
||||
{% set header = hookable_metadata.configuration.header|default(null) %}
|
||||
{% endif %}
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
{% import '@SyliusAdmin/shared/helper/table.html.twig' as table %}
|
||||
{% import '@SyliusAdmin/shared/helper/pagination.html.twig' as pagination %}
|
||||
|
||||
{% set resources = hookable_metadata.context.resources %}
|
||||
{% set data = resources.data %}
|
||||
{% set definition = resources.definition %}
|
||||
{% set resources = hookable_metadata.context.resources|default(null) %}
|
||||
{% set data = resources.data|default([]) %}
|
||||
{% set definition = resources.definition|default(null) %}
|
||||
|
||||
{% if data|length > 0 %}
|
||||
<div class="card">
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{% from "@SyliusAdmin/shared/helper/accordion.html.twig" import accordion %}
|
||||
{% import "@SyliusAdmin/shared/helper/button.html.twig" as button %}
|
||||
|
||||
{% set resources = hookable_metadata.context.resources %}
|
||||
{% set resources = hookable_metadata.context.resources|default(null) %}
|
||||
|
||||
{% set path = path(app.request.attributes.get('_route'), app.request.attributes.all('_route_params')) %}
|
||||
{% set are_criteria_set = app.request.query.has('criteria') %}
|
||||
|
||||
{% if resources.definition.enabledFilters is not empty %}
|
||||
{% if resources.definition.enabledFilters|default([]) is not empty %}
|
||||
<div class="position-relative z-1 bg-white mb-5">
|
||||
{% set content %}
|
||||
<div class="mb-3">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{% set resources = hookable_metadata.context.resources %}
|
||||
{% set resources = hookable_metadata.context.resources|default(null) %}
|
||||
|
||||
{% if resources.data|length == 0 %}
|
||||
{% if resources.data is defined and resources.data|length == 0 %}
|
||||
<div class="card">
|
||||
<div class="empty">
|
||||
{% hook 'no_results' %}
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@
|
|||
{% if title_from_context is not null or resource_metadata is not null %}
|
||||
{% set title = title_from_context|default(resource_metadata.applicationName~'.ui.'~resource_metadata.pluralName) %}
|
||||
{% else %}
|
||||
{% set title = hookable_metadata.configuration.title %}
|
||||
{% set title = hookable_metadata.configuration.title|default(null) %}
|
||||
{% endif %}
|
||||
|
||||
{{ breadcrumbs([
|
||||
{ name: 'sylius.ui.dashboard', 'url': path('sylius_admin_dashboard'), 'active': false },
|
||||
{ name: title, 'active': true },
|
||||
{ name: title|default('sylius.ui.index'|trans), 'active': true },
|
||||
]) }}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
{% from '@SyliusAdmin/shared/helper/breadcrumbs.html.twig' import breadcrumbs %}
|
||||
|
||||
{% set configuration = hookable_metadata.context.configuration %}
|
||||
{% set resource = hookable_metadata.context.resource %}
|
||||
{% set metadata = hookable_metadata.context.metadata %}
|
||||
{% set configuration = hookable_metadata.context.configuration|default(null) %}
|
||||
{% set resource = hookable_metadata.context.resource|default(null) %}
|
||||
{% set metadata = hookable_metadata.context.metadata|default(null) %}
|
||||
|
||||
{% set resource_index = metadata.applicationName~'.ui.'~metadata.pluralName %}
|
||||
{% set index_url = path(
|
||||
configuration.vars.index.route.name|default(configuration.getRouteName('index')),
|
||||
configuration.vars.index.route.parameters|default(configuration.vars.route.parameters|default({}))
|
||||
) %}
|
||||
{% if configuration and metadata %}
|
||||
{% set resource_index = metadata.applicationName~'.ui.'~metadata.pluralName %}
|
||||
{% set index_url = path(
|
||||
configuration.vars.index.route.name|default(configuration.getRouteName('index')),
|
||||
configuration.vars.index.route.parameters|default(configuration.vars.route.parameters|default({}))
|
||||
) %}
|
||||
{% endif %}
|
||||
|
||||
{% set rendered_field_prefix = hookable_metadata.configuration.rendered_field_prefix|default(null) %}
|
||||
{% set field_name = hookable_metadata.configuration.rendered_field|default('') %}
|
||||
|
|
@ -19,18 +21,26 @@
|
|||
: {}
|
||||
%}
|
||||
|
||||
{% set resource_show_name = rendered_field|default(resource.translations[app.locale].name|default(resource.code|default(resource.id))) %}
|
||||
{% set resource_show_name = rendered_field|default(resource.translations[app.locale].name|default(resource.code|default(resource.id|default(null)))) %}
|
||||
|
||||
{% if action is defined and action.name is defined %}
|
||||
{% set show_url = path(configuration.getRouteName('show'), { id: resource.id }) %}
|
||||
{% set resource_show = { name: resource_show_name, url: show_url} %}
|
||||
{% else %}
|
||||
{% set resource_show = { name: resource_show_name, active: true} %}
|
||||
{% set resource_show = { name: resource_show_name|default('sylius.ui.show'|trans), active: true} %}
|
||||
{% endif %}
|
||||
|
||||
{{ breadcrumbs([
|
||||
{ name: 'sylius.ui.dashboard', url: path('sylius_admin_dashboard'), active: false },
|
||||
{ name: resource_index, url: index_url, active: false },
|
||||
resource_show,
|
||||
action
|
||||
]) }}
|
||||
{% if resource_index is defined and index_url is defined %}
|
||||
{{ breadcrumbs([
|
||||
{ name: 'sylius.ui.dashboard', url: path('sylius_admin_dashboard'), active: false },
|
||||
{ name: resource_index, url: index_url, active: false },
|
||||
resource_show,
|
||||
action
|
||||
]) }}
|
||||
{% else %}
|
||||
{{ breadcrumbs([
|
||||
{ name: 'sylius.ui.dashboard', url: path('sylius_admin_dashboard'), active: false },
|
||||
resource_show,
|
||||
action
|
||||
]) }}
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@
|
|||
{% set singular_name = hookable_metadata.configuration.resource_name|default(metadata.applicationName ~ '.ui.' ~ _context.metadata.name) %}
|
||||
{% set header = hookable_metadata.configuration.header|default(null) %}
|
||||
{% else %}
|
||||
{% set singular_name = hookable_metadata.configuration.resource_name %}
|
||||
{% set header = hookable_metadata.configuration.header %}
|
||||
{% set singular_name = hookable_metadata.configuration.resource_name|default(null) %}
|
||||
{% set header = hookable_metadata.configuration.header|default(null) %}
|
||||
{% endif %}
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
|
|
|
|||
|
|
@ -1,32 +1,44 @@
|
|||
{% from '@SyliusAdmin/shared/helper/breadcrumbs.html.twig' import breadcrumbs %}
|
||||
|
||||
{% set configuration = hookable_metadata.context.configuration %}
|
||||
{% set resource = hookable_metadata.context.resource %}
|
||||
{% set metadata = hookable_metadata.context.metadata %}
|
||||
{% set configuration = hookable_metadata.context.configuration|default(null) %}
|
||||
{% set resource = hookable_metadata.context.resource is defined ? hookable_metadata.context.resource : null %}
|
||||
{% set metadata = hookable_metadata.context.metadata|default(null) %}
|
||||
|
||||
{% set resource_index = metadata.applicationName~'.ui.'~metadata.pluralName %}
|
||||
{% if metadata is not null %}
|
||||
{% set resource_index = metadata.applicationName~'.ui.'~metadata.pluralName %}
|
||||
{% endif %}
|
||||
|
||||
{% if configuration is not null %}
|
||||
{% set index_url = path(
|
||||
configuration.vars.index.route.name|default(configuration.getRouteName('index')),
|
||||
configuration.vars.index.route.parameters|default(configuration.vars.route.parameters|default({}))
|
||||
) %}
|
||||
{% endif %}
|
||||
|
||||
{% set rendered_field_prefix = hookable_metadata.configuration.rendered_field_prefix|default(null) %}
|
||||
{% set field_name = hookable_metadata.configuration.rendered_field|default('') %}
|
||||
{% set rendered_field = rendered_field_prefix ~ (attribute(resource, field_name) is defined ? attribute(resource, field_name) : null) %}
|
||||
|
||||
{% set resource_edit_name = rendered_field|default(resource.translations[app.locale].name|default(resource.code|default(resource.id))) %}
|
||||
{% set show_route_name = configuration.vars.index.route.name|default(configuration.getRouteName('show')) %}
|
||||
{% set resource_edit_name = rendered_field|default(resource.translations[app.locale].name|default(resource.code|default(resource.id|default(null)))) %}
|
||||
{% set show_route_name = configuration.vars.index.route.name|default(configuration.getRouteName('show')|default(null)) %}
|
||||
|
||||
{% if sylius_route_exists(show_route_name) %}
|
||||
{% if show_route_name is not null and sylius_route_exists(show_route_name) %}
|
||||
{% set show_url = path(show_route_name, { id: resource.id }) %}
|
||||
{% set resource_edit = { name: resource_edit_name, url: show_url} %}
|
||||
{% else %}
|
||||
{% set resource_edit = { name: resource_edit_name, active: true} %}
|
||||
{% endif %}
|
||||
|
||||
{{ breadcrumbs([
|
||||
{ name: 'sylius.ui.dashboard', url: path('sylius_admin_dashboard'), active: false },
|
||||
{ name: resource_index, url: index_url, active: false },
|
||||
resource_edit,
|
||||
{ name: 'sylius.ui.edit'|trans, active: false }
|
||||
]) }}
|
||||
{% if resource_index is defined %}
|
||||
{{ breadcrumbs([
|
||||
{ name: 'sylius.ui.dashboard', url: path('sylius_admin_dashboard'), active: false },
|
||||
{ name: resource_index, url: index_url, active: false },
|
||||
resource_edit,
|
||||
{ name: 'sylius.ui.edit'|trans, active: false }
|
||||
]) }}
|
||||
{% else %}
|
||||
{{ breadcrumbs([
|
||||
{ name: 'sylius.ui.dashboard', url: path('sylius_admin_dashboard'), active: false },
|
||||
{ name: 'sylius.ui.edit'|trans, active: false }
|
||||
]) }}
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{% set configuration = hookable_metadata.context.configuration %}
|
||||
{% set show_route_name = configuration.vars.index.route.name|default(configuration.getRouteName('show')) %}
|
||||
{% set configuration = hookable_metadata.context.configuration|default(null) %}
|
||||
{% set show_route_name = configuration.vars.index.route.name|default(configuration.getRouteName('show')|default(null)) %}
|
||||
|
||||
{% if sylius_route_exists(show_route_name) %}
|
||||
{% if show_route_name is not null and sylius_route_exists(show_route_name) %}
|
||||
{% set resource_id = hookable_metadata.context.resource.id %}
|
||||
{% set resource_name = hookable_metadata.context.metadata.name %}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@
|
|||
{% set singular_name = hookable_metadata.configuration.resource_name|default(metadata.applicationName ~ '.ui.' ~ _context.metadata.name) %}
|
||||
{% set header = hookable_metadata.configuration.header|default(null) %}
|
||||
{% else %}
|
||||
{% set singular_name = hookable_metadata.configuration.resource_name %}
|
||||
{% set header = hookable_metadata.configuration.header %}
|
||||
{% set singular_name = hookable_metadata.configuration.resource_name|default(null) %}
|
||||
{% set header = hookable_metadata.configuration.header|default(null) %}
|
||||
{% endif %}
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
{% if data is not null %}
|
||||
<span class="fw-medium">{{ data }}</span>
|
||||
<div class="fw-medium">{{ data }}</div>
|
||||
{% else %}
|
||||
<span class="text-danger" {{ sylius_test_html_attribute('missing-translation-paragraph') }}>
|
||||
{{ ux_icon('tabler:exclamation-circle') }} <i>{{ 'sylius.ui.missing_translation'|trans }}</i>
|
||||
</span>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@
|
|||
{% macro row(grid, definition, row) %}
|
||||
{% import '@SyliusAdmin/shared/helper/dropdown.html.twig' as dropdown %}
|
||||
|
||||
<tr class="item" {{ sylius_test_html_attribute('row') }} {{ sylius_test_html_attribute('resource-id', row.id) }}>
|
||||
{% if definition.actionGroups.bulk is defined and definition.getEnabledActions('bulk')|length > 0 %}
|
||||
<tr class="item" {{ sylius_test_html_attribute('row') }} {{ sylius_test_html_attribute('resource-id', row.id|default(null)) }}>
|
||||
{% if definition.actionGroups.bulk is defined and definition.getEnabledActions('bulk')|length > 0 and row.id is defined %}
|
||||
<td class="text-center"><input data-check-all-group="index" class="form-check-input" type="checkbox" value="{{ row.id }}" /></td>
|
||||
{% endif %}
|
||||
{% for field in definition.enabledFields|sylius_sort_by('position') %}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
{% extends '@SyliusAdmin/shared/layout/base.html.twig' %}
|
||||
|
||||
{% set prefixes = configuration.vars.hook_prefix is defined ? [configuration.vars.hook_prefix] %}
|
||||
{% set prefixes = prefixes|default({})|merge([
|
||||
'sylius_admin.%resource_name%'|replace({'%resource_name%': resource_name|default(metadata.name)}),
|
||||
'sylius_admin.common'
|
||||
]) %}
|
||||
|
||||
{% set header = metadata.applicationName ~ '.ui.' ~ metadata.pluralName %}
|
||||
|
||||
{% block title %}{{ 'sylius.ui.shipment'|trans ~' '~ shipment.id ~ ' | ' }}{{ parent() }}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% hook 'show' with { _prefixes: prefixes, resource, metadata, configuration } %}
|
||||
{% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ parent() }}
|
||||
{% hook 'show#stylesheets' with { _prefixes: prefixes } %}
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{{ parent() }}
|
||||
{% hook 'show#javascripts' with { _prefixes: prefixes } %}
|
||||
{% endblock %}
|
||||
|
|
@ -28,7 +28,7 @@ final readonly class AddressMapper implements AddressMapperInterface
|
|||
$currentAddress->setPostcode($targetAddress->getPostcode());
|
||||
$currentAddress->setPhoneNumber($targetAddress->getPhoneNumber());
|
||||
|
||||
if (null !== $targetAddress->getProvinceCode()) {
|
||||
if (null !== $targetAddress->getProvinceCode() || null !== $targetAddress->getProvinceName()) {
|
||||
$currentAddress->setProvinceCode($targetAddress->getProvinceCode());
|
||||
$currentAddress->setProvinceName($targetAddress->getProvinceName());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ api_platform:
|
|||
Sylius\Component\Payment\Exception\PaymentRequestNotFoundException: 404
|
||||
Sylius\Bundle\ApiBundle\Exception\PaymentNotFoundException: 404
|
||||
Sylius\Bundle\ApiBundle\Exception\PaymentMethodNotFoundException: 404
|
||||
Sylius\Bundle\ApiBundle\Exception\OrderItemNotFoundException: 404
|
||||
Sylius\Bundle\ApiBundle\Exception\StateMachineTransitionFailedException: 422
|
||||
Sylius\Bundle\ApiBundle\Exception\TranslationInDefaultLocaleCannotBeRemoved: 422
|
||||
Sylius\Bundle\ApiBundle\Exception\TranslationLocaleMismatchException: 422
|
||||
|
|
|
|||
|
|
@ -28,6 +28,22 @@
|
|||
<constraint name="Sylius\Bundle\ApiBundle\Validator\Constraints\UpdateCartEmailNotAllowed">
|
||||
<option name="groups">sylius</option>
|
||||
</constraint>
|
||||
<property name="email">
|
||||
<constraint name="Length">
|
||||
<option name="max">254</option>
|
||||
<option name="maxMessage">sylius.customer.email.max</option>
|
||||
<option name="groups">
|
||||
<value>sylius</value>
|
||||
</option>
|
||||
</constraint>
|
||||
<constraint name="Email">
|
||||
<option name="message">sylius.customer.email.invalid</option>
|
||||
<option name="mode">strict</option>
|
||||
<option name="groups">
|
||||
<value>sylius</value>
|
||||
</option>
|
||||
</constraint>
|
||||
</property>
|
||||
<property name="billingAddress">
|
||||
<constraint name="Valid" />
|
||||
</property>
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ final class TranslatableDenormalizer implements DenormalizerInterface, Denormali
|
|||
public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool
|
||||
{
|
||||
return
|
||||
is_array($data) &&
|
||||
Request::METHOD_POST === ($context[ContextKeys::HTTP_REQUEST_METHOD_TYPE] ?? null) &&
|
||||
!isset($context[self::getAlreadyCalledKey($type)]) &&
|
||||
is_a($type, TranslatableInterface::class, true)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ namespace Sylius\Bundle\ApiBundle\Validator\Constraints;
|
|||
use Sylius\Bundle\ApiBundle\Command\Checkout\CompleteOrder;
|
||||
use Sylius\Component\Core\Model\OrderInterface;
|
||||
use Sylius\Component\Core\Model\PaymentInterface;
|
||||
use Sylius\Component\Core\Model\PaymentMethodInterface;
|
||||
use Sylius\Component\Core\Repository\OrderRepositoryInterface;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
|
|
@ -39,12 +40,18 @@ final class OrderPaymentMethodEligibilityValidator extends ConstraintValidator
|
|||
$order = $this->orderRepository->findOneBy(['tokenValue' => $value->orderTokenValue]);
|
||||
Assert::notNull($order);
|
||||
|
||||
$channel = $order->getChannel();
|
||||
Assert::notNull($channel);
|
||||
|
||||
/** @var PaymentInterface $payment */
|
||||
foreach ($order->getPayments() as $payment) {
|
||||
if (!$payment->getMethod()->isEnabled()) {
|
||||
/** @var PaymentMethodInterface $paymentMethod */
|
||||
$paymentMethod = $payment->getMethod();
|
||||
|
||||
if (!$paymentMethod->isEnabled() || !$paymentMethod->hasChannel($channel)) {
|
||||
$this->context->addViolation(
|
||||
$constraint->message,
|
||||
['%paymentMethodName%' => $payment->getMethod()->getName()],
|
||||
['%paymentMethodName%' => $paymentMethod->getName()],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sylius\Bundle\ApiBundle\Tests\Doctrine\ORM\Filter;
|
||||
namespace Tests\Sylius\Bundle\ApiBundle\Doctrine\ORM\Filter;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sylius\Bundle\ApiBundle\Tests\Doctrine\ORM\Filter;
|
||||
namespace Tests\Sylius\Bundle\ApiBundle\Doctrine\ORM\Filter;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
|
@ -75,7 +75,7 @@ final class AddressMapperTest extends TestCase
|
|||
$targetAddressMock->expects(self::once())->method('getPostcode')->willReturn('00000');
|
||||
$targetAddressMock->expects(self::once())->method('getPhoneNumber')->willReturn('123456789');
|
||||
$targetAddressMock->expects(self::once())->method('getProvinceCode')->willReturn(null);
|
||||
$targetAddressMock->expects(self::never())->method('getProvinceName')->willReturn('east');
|
||||
$targetAddressMock->expects(self::once())->method('getProvinceName')->willReturn(null);
|
||||
$currentAddressMock->expects(self::once())->method('setFirstName')->with('John');
|
||||
$currentAddressMock->expects(self::once())->method('setLastName')->with('Doe');
|
||||
$currentAddressMock->expects(self::once())->method('setCompany')->with('CocaCola');
|
||||
|
|
@ -84,8 +84,39 @@ final class AddressMapperTest extends TestCase
|
|||
$currentAddressMock->expects(self::once())->method('setCity')->with('New York');
|
||||
$currentAddressMock->expects(self::once())->method('setPostcode')->with('00000');
|
||||
$currentAddressMock->expects(self::once())->method('setPhoneNumber')->with('123456789');
|
||||
$currentAddressMock->expects(self::never())->method('setProvinceCode')->with('999');
|
||||
$currentAddressMock->expects(self::never())->method('setProvinceName')->with('east');
|
||||
$currentAddressMock->expects(self::never())->method('setProvinceCode');
|
||||
$currentAddressMock->expects(self::never())->method('setProvinceName');
|
||||
|
||||
self::assertSame($currentAddressMock, $this->addressMapper->mapExisting($currentAddressMock, $targetAddressMock));
|
||||
}
|
||||
|
||||
public function testUpdatesAnAddressWithProvinceNameOnly(): void
|
||||
{
|
||||
/** @var AddressInterface|MockObject $currentAddressMock */
|
||||
$currentAddressMock = $this->createMock(AddressInterface::class);
|
||||
/** @var AddressInterface|MockObject $targetAddressMock */
|
||||
$targetAddressMock = $this->createMock(AddressInterface::class);
|
||||
|
||||
$targetAddressMock->expects(self::once())->method('getFirstName')->willReturn('John');
|
||||
$targetAddressMock->expects(self::once())->method('getLastName')->willReturn('Doe');
|
||||
$targetAddressMock->expects(self::once())->method('getCompany')->willReturn('CocaCola');
|
||||
$targetAddressMock->expects(self::once())->method('getStreet')->willReturn('Green Avenue');
|
||||
$targetAddressMock->expects(self::once())->method('getCountryCode')->willReturn('US');
|
||||
$targetAddressMock->expects(self::once())->method('getCity')->willReturn('New York');
|
||||
$targetAddressMock->expects(self::once())->method('getPostcode')->willReturn('00000');
|
||||
$targetAddressMock->expects(self::once())->method('getPhoneNumber')->willReturn('123456789');
|
||||
$targetAddressMock->expects(self::atLeastOnce())->method('getProvinceCode')->willReturn(null);
|
||||
$targetAddressMock->expects(self::atLeastOnce())->method('getProvinceName')->willReturn('East');
|
||||
$currentAddressMock->expects(self::once())->method('setFirstName')->with('John');
|
||||
$currentAddressMock->expects(self::once())->method('setLastName')->with('Doe');
|
||||
$currentAddressMock->expects(self::once())->method('setCompany')->with('CocaCola');
|
||||
$currentAddressMock->expects(self::once())->method('setStreet')->with('Green Avenue');
|
||||
$currentAddressMock->expects(self::once())->method('setCountryCode')->with('US');
|
||||
$currentAddressMock->expects(self::once())->method('setCity')->with('New York');
|
||||
$currentAddressMock->expects(self::once())->method('setPostcode')->with('00000');
|
||||
$currentAddressMock->expects(self::once())->method('setPhoneNumber')->with('123456789');
|
||||
$currentAddressMock->expects(self::once())->method('setProvinceCode')->with(null);
|
||||
$currentAddressMock->expects(self::once())->method('setProvinceName')->with('East');
|
||||
|
||||
self::assertSame($currentAddressMock, $this->addressMapper->mapExisting($currentAddressMock, $targetAddressMock));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,17 @@ final class TranslatableDenormalizerTest extends TestCase
|
|||
|
||||
public function testOnlySupportsTranslatableResource(): void
|
||||
{
|
||||
self::assertFalse(
|
||||
$this->translatableDenormalizer->supportsDenormalization(
|
||||
'iri-string',
|
||||
TranslatableInterface::class,
|
||||
null,
|
||||
[
|
||||
ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST',
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
self::assertFalse(
|
||||
$this->translatableDenormalizer->supportsDenormalization(
|
||||
[],
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ use Sylius\Bundle\ApiBundle\Command\Checkout\CompleteOrder;
|
|||
use Sylius\Bundle\ApiBundle\Command\Checkout\UpdateCart;
|
||||
use Sylius\Bundle\ApiBundle\Validator\Constraints\OrderPaymentMethodEligibility;
|
||||
use Sylius\Bundle\ApiBundle\Validator\Constraints\OrderPaymentMethodEligibilityValidator;
|
||||
use Sylius\Component\Core\Model\ChannelInterface;
|
||||
use Sylius\Component\Core\Model\OrderInterface;
|
||||
use Sylius\Component\Core\Model\PaymentInterface;
|
||||
use Sylius\Component\Core\Model\PaymentMethodInterface;
|
||||
|
|
@ -79,6 +80,8 @@ final class OrderPaymentMethodEligibilityValidatorTest extends TestCase
|
|||
$paymentMethodMock = $this->createMock(PaymentMethodInterface::class);
|
||||
/** @var ExecutionContextInterface|MockObject $executionContextMock */
|
||||
$executionContextMock = $this->createMock(ExecutionContextInterface::class);
|
||||
/** @var ChannelInterface|MockObject $channelMock */
|
||||
$channelMock = $this->createMock(ChannelInterface::class);
|
||||
|
||||
$this->orderPaymentMethodEligibilityValidator->initialize($executionContextMock);
|
||||
|
||||
|
|
@ -91,13 +94,14 @@ final class OrderPaymentMethodEligibilityValidatorTest extends TestCase
|
|||
->with(['tokenValue' => 'token'])
|
||||
->willReturn($orderMock);
|
||||
|
||||
$orderMock->method('getChannel')->willReturn($channelMock);
|
||||
|
||||
$orderMock
|
||||
->expects(self::once())
|
||||
->method('getPayments')
|
||||
->willReturn(new ArrayCollection([$paymentMock]));
|
||||
|
||||
$paymentMock
|
||||
->expects($this->exactly(2))
|
||||
->method('getMethod')
|
||||
->willReturn($paymentMethodMock);
|
||||
|
||||
|
|
@ -122,7 +126,7 @@ final class OrderPaymentMethodEligibilityValidatorTest extends TestCase
|
|||
$this->orderPaymentMethodEligibilityValidator->validate($value, $constraint);
|
||||
}
|
||||
|
||||
public function testDoesNotAddViolationIfPaymentIsAvailable(): void
|
||||
public function testAddsViolationIfPaymentIsNotAvailableInChannel(): void
|
||||
{
|
||||
/** @var OrderInterface|MockObject $orderMock */
|
||||
$orderMock = $this->createMock(OrderInterface::class);
|
||||
|
|
@ -132,6 +136,8 @@ final class OrderPaymentMethodEligibilityValidatorTest extends TestCase
|
|||
$paymentMethodMock = $this->createMock(PaymentMethodInterface::class);
|
||||
/** @var ExecutionContextInterface|MockObject $executionContextMock */
|
||||
$executionContextMock = $this->createMock(ExecutionContextInterface::class);
|
||||
/** @var ChannelInterface|MockObject $channelMock */
|
||||
$channelMock = $this->createMock(ChannelInterface::class);
|
||||
|
||||
$this->orderPaymentMethodEligibilityValidator->initialize($executionContextMock);
|
||||
|
||||
|
|
@ -144,20 +150,69 @@ final class OrderPaymentMethodEligibilityValidatorTest extends TestCase
|
|||
->with(['tokenValue' => 'token'])
|
||||
->willReturn($orderMock);
|
||||
|
||||
$orderMock->method('getChannel')->willReturn($channelMock);
|
||||
|
||||
$orderMock
|
||||
->expects(self::once())
|
||||
->method('getPayments')
|
||||
->willReturn(new ArrayCollection([$paymentMock]));
|
||||
|
||||
$paymentMock
|
||||
->expects(self::once())
|
||||
->method('getMethod')
|
||||
->willReturn($paymentMethodMock);
|
||||
|
||||
$paymentMethodMock
|
||||
$paymentMethodMock->method('isEnabled')->willReturn(true);
|
||||
$paymentMethodMock->method('hasChannel')->with($channelMock)->willReturn(false);
|
||||
$paymentMethodMock->method('getName')->willReturn('bank transfer');
|
||||
|
||||
$executionContextMock
|
||||
->expects(self::once())
|
||||
->method('isEnabled')
|
||||
->willReturn(true);
|
||||
->method('addViolation')
|
||||
->with(
|
||||
'sylius.order.payment_method_eligibility',
|
||||
['%paymentMethodName%' => 'bank transfer'],
|
||||
);
|
||||
|
||||
$this->orderPaymentMethodEligibilityValidator->validate($value, $constraint);
|
||||
}
|
||||
|
||||
public function testDoesNotAddViolationIfPaymentIsAvailable(): void
|
||||
{
|
||||
/** @var OrderInterface|MockObject $orderMock */
|
||||
$orderMock = $this->createMock(OrderInterface::class);
|
||||
/** @var PaymentInterface|MockObject $paymentMock */
|
||||
$paymentMock = $this->createMock(PaymentInterface::class);
|
||||
/** @var PaymentMethodInterface|MockObject $paymentMethodMock */
|
||||
$paymentMethodMock = $this->createMock(PaymentMethodInterface::class);
|
||||
/** @var ExecutionContextInterface|MockObject $executionContextMock */
|
||||
$executionContextMock = $this->createMock(ExecutionContextInterface::class);
|
||||
/** @var ChannelInterface|MockObject $channelMock */
|
||||
$channelMock = $this->createMock(ChannelInterface::class);
|
||||
|
||||
$this->orderPaymentMethodEligibilityValidator->initialize($executionContextMock);
|
||||
|
||||
$constraint = new OrderPaymentMethodEligibility();
|
||||
$value = new CompleteOrder(orderTokenValue: 'token');
|
||||
|
||||
$this->orderRepository
|
||||
->expects(self::once())
|
||||
->method('findOneBy')
|
||||
->with(['tokenValue' => 'token'])
|
||||
->willReturn($orderMock);
|
||||
|
||||
$orderMock->method('getChannel')->willReturn($channelMock);
|
||||
|
||||
$orderMock
|
||||
->expects(self::once())
|
||||
->method('getPayments')
|
||||
->willReturn(new ArrayCollection([$paymentMock]));
|
||||
|
||||
$paymentMock
|
||||
->method('getMethod')
|
||||
->willReturn($paymentMethodMock);
|
||||
|
||||
$paymentMethodMock->method('isEnabled')->willReturn(true);
|
||||
$paymentMethodMock->method('hasChannel')->with($channelMock)->willReturn(true);
|
||||
|
||||
$executionContextMock
|
||||
->expects(self::never())
|
||||
|
|
|
|||
|
|
@ -11,12 +11,12 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sylius\Bundle\ApiBundle\Tests\Validator\Constraints;
|
||||
namespace Tests\Sylius\Bundle\ApiBundle\Validator\Constraints;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Sylius\Bundle\ApiBundle\Command\OrderTokenValueAwareInterface;
|
||||
use Sylius\Bundle\ApiBundle\Command\Checkout\CompleteOrder;
|
||||
use Sylius\Bundle\ApiBundle\Validator\Constraints\OrderProductEligibility;
|
||||
use Sylius\Bundle\ApiBundle\Validator\Constraints\OrderProductEligibilityValidator;
|
||||
use Sylius\Component\Core\Model\ChannelInterface;
|
||||
|
|
@ -48,8 +48,7 @@ final class OrderProductEligibilityValidatorTest extends TestCase
|
|||
$orderToken = 'ORDER_TOKEN';
|
||||
$variantName = 'Variant 1';
|
||||
|
||||
$command = $this->createMock(OrderTokenValueAwareInterface::class);
|
||||
$command->method('getOrderTokenValue')->willReturn($orderToken);
|
||||
$command = new CompleteOrder($orderToken);
|
||||
|
||||
$variant = $this->createMock(ProductVariantInterface::class);
|
||||
$variant->method('isEnabled')->willReturn(false);
|
||||
|
|
@ -86,8 +85,7 @@ final class OrderProductEligibilityValidatorTest extends TestCase
|
|||
$orderToken = 'ORDER_TOKEN';
|
||||
$productName = 'Product 2';
|
||||
|
||||
$command = $this->createMock(OrderTokenValueAwareInterface::class);
|
||||
$command->method('getOrderTokenValue')->willReturn($orderToken);
|
||||
$command = new CompleteOrder($orderToken);
|
||||
|
||||
$variant = $this->createMock(ProductVariantInterface::class);
|
||||
$variant->method('isEnabled')->willReturn(true);
|
||||
|
|
@ -124,8 +122,7 @@ final class OrderProductEligibilityValidatorTest extends TestCase
|
|||
$orderToken = 'ORDER_TOKEN';
|
||||
$productName = 'Product 3';
|
||||
|
||||
$command = $this->createMock(OrderTokenValueAwareInterface::class);
|
||||
$command->method('getOrderTokenValue')->willReturn($orderToken);
|
||||
$command = new CompleteOrder($orderToken);
|
||||
|
||||
$variant = $this->createMock(ProductVariantInterface::class);
|
||||
$variant->method('isEnabled')->willReturn(true);
|
||||
|
|
@ -161,8 +158,7 @@ final class OrderProductEligibilityValidatorTest extends TestCase
|
|||
{
|
||||
$orderToken = 'ORDER_TOKEN';
|
||||
|
||||
$command = $this->createMock(OrderTokenValueAwareInterface::class);
|
||||
$command->method('getOrderTokenValue')->willReturn($orderToken);
|
||||
$command = new CompleteOrder($orderToken);
|
||||
|
||||
$variant = $this->createMock(ProductVariantInterface::class);
|
||||
$variant->method('isEnabled')->willReturn(true);
|
||||
|
|
@ -159,6 +159,7 @@ final class Configuration implements ConfigurationInterface
|
|||
->booleanNode('plugins')->defaultTrue()->end()
|
||||
->scalarNode('salt')->defaultNull()->end()
|
||||
->scalarNode('url')->defaultValue('https://prism.sylius.com/telemetry')->end()
|
||||
->integerNode('query_timeout')->defaultValue(60000)->min(1000)->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@ final class SyliusCoreExtension extends AbstractResourceExtension implements Pre
|
|||
}
|
||||
|
||||
/**
|
||||
* @param array{enabled: bool, business: bool, technical: bool, plugins: bool, salt: string|null, url: string} $telemetryConfig
|
||||
* @param array{enabled: bool, business: bool, technical: bool, plugins: bool, salt: string|null, url: string, query_timeout: int} $telemetryConfig
|
||||
*/
|
||||
private function configureTelemetry(array $telemetryConfig, ContainerBuilder $container, XmlFileLoader $loader): void
|
||||
{
|
||||
|
|
@ -228,6 +228,12 @@ final class SyliusCoreExtension extends AbstractResourceExtension implements Pre
|
|||
$container->setParameter('sylius_core.telemetry.enabled', $telemetryConfig['enabled']);
|
||||
$container->setParameter('sylius_core.telemetry.salt', $telemetrySalt);
|
||||
$container->setParameter('sylius_core.telemetry.url', $telemetryConfig['url']);
|
||||
$queryTimeout = $telemetryConfig['query_timeout'];
|
||||
$envQueryTimeout = $_ENV['SYLIUS_TELEMETRY_QUERY_TIMEOUT'] ?? $_SERVER['SYLIUS_TELEMETRY_QUERY_TIMEOUT'] ?? getenv('SYLIUS_TELEMETRY_QUERY_TIMEOUT');
|
||||
if ($envQueryTimeout !== false) {
|
||||
$queryTimeout = max(1000, (int) $envQueryTimeout);
|
||||
}
|
||||
$container->setParameter('sylius_core.telemetry.query_timeout', $queryTimeout);
|
||||
|
||||
$businessEnabled = $this->isTelemetryCategoryEnabled($telemetryConfig['business'], 'SYLIUS_TELEMETRY_BUSINESS');
|
||||
$technicalEnabled = $this->isTelemetryCategoryEnabled($telemetryConfig['technical'], 'SYLIUS_TELEMETRY_TECHNICAL');
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ class PaymentMethodRepository extends BasePaymentMethodRepository implements Pay
|
|||
public function createListQueryBuilder(string $locale): QueryBuilder
|
||||
{
|
||||
return $this->createQueryBuilder('o')
|
||||
->addSelect('translation')
|
||||
->leftJoin('o.gatewayConfig', 'gatewayConfig')
|
||||
->leftJoin('o.translations', 'translation', 'WITH', 'translation.locale = :locale')
|
||||
->setParameter('locale', $locale)
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ class ProductTaxonRepository extends EntityRepository implements ProductTaxonRep
|
|||
{
|
||||
return $this->createQueryBuilder('o')
|
||||
->innerJoin('o.product', 'product')
|
||||
->innerJoin('product.translations', 'translations', Join::WITH, 'translations.locale = :locale')
|
||||
->leftJoin('product.translations', 'translations', Join::WITH, 'translations.locale = :locale')
|
||||
->andWhere('o.taxon = :taxonId')
|
||||
->setParameter('taxonId', $taxonId)
|
||||
->setParameter('locale', $locale)
|
||||
|
|
|
|||
|
|
@ -33,7 +33,10 @@ class ProductVariantRepository extends BaseProductVariantRepository implements P
|
|||
public function createInventoryListQueryBuilder(string $locale): QueryBuilder
|
||||
{
|
||||
return $this->createQueryBuilder('o')
|
||||
->addSelect('translation', 'product', 'productTranslation')
|
||||
->leftJoin('o.translations', 'translation', 'WITH', 'translation.locale = :locale')
|
||||
->innerJoin('o.product', 'product')
|
||||
->leftJoin('product.translations', 'productTranslation', 'WITH', 'productTranslation.locale = :locale')
|
||||
->andWhere('o.tracked = :tracked')
|
||||
->setParameter('locale', $locale)
|
||||
->setParameter('tracked', true)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Sylius Sp. z o.o.
|
||||
*
|
||||
* 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\CoreBundle\EventListener\Workflow\OrderCheckout;
|
||||
|
||||
use Sylius\Component\Core\Checker\OrderPaymentMethodSelectionAvailabilityCheckerInterface;
|
||||
use Sylius\Component\Core\Model\OrderInterface;
|
||||
use Sylius\Component\Core\OrderCheckoutStates;
|
||||
use Symfony\Component\Workflow\Event\GuardEvent;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
final readonly class BlockSelectPaymentFromPaymentSkippedListener
|
||||
{
|
||||
public function __construct(
|
||||
private OrderPaymentMethodSelectionAvailabilityCheckerInterface $orderPaymentMethodSelectionAvailabilityChecker,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(GuardEvent $event): void
|
||||
{
|
||||
if (!$event->getMarking()->has(OrderCheckoutStates::STATE_PAYMENT_SKIPPED)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$order = $event->getSubject();
|
||||
Assert::isInstanceOf($order, OrderInterface::class);
|
||||
|
||||
if (!$this->orderPaymentMethodSelectionAvailabilityChecker->isPaymentMethodSelectionAvailable($order)) {
|
||||
$event->setBlocked(true, 'Payment method selection is not allowed for an order with zero total.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -26,12 +26,18 @@ winzou_state_machine:
|
|||
from: [shipping_selected, shipping_skipped]
|
||||
to: payment_skipped
|
||||
select_payment:
|
||||
from: [payment_selected, shipping_skipped, shipping_selected]
|
||||
from: [payment_selected, payment_skipped, shipping_skipped, shipping_selected]
|
||||
to: payment_selected
|
||||
complete:
|
||||
from: [payment_selected, payment_skipped]
|
||||
to: completed
|
||||
callbacks:
|
||||
guard:
|
||||
sylius_block_select_payment_from_payment_skipped:
|
||||
on: ["select_payment"]
|
||||
from: ["payment_skipped"]
|
||||
do: ["@sylius.checker.order_payment_method_selection_availability", "isPaymentMethodSelectionAvailable"]
|
||||
args: ["object"]
|
||||
after:
|
||||
sylius_process_cart:
|
||||
on: ["select_shipping", "address", "select_payment", "skip_shipping", "skip_payment"]
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ framework:
|
|||
!php/const Sylius\Component\Core\OrderCheckoutTransitions::TRANSITION_SELECT_PAYMENT:
|
||||
from:
|
||||
- !php/const Sylius\Component\Core\OrderCheckoutStates::STATE_PAYMENT_SELECTED
|
||||
- !php/const Sylius\Component\Core\OrderCheckoutStates::STATE_PAYMENT_SKIPPED
|
||||
- !php/const Sylius\Component\Core\OrderCheckoutStates::STATE_SHIPPING_SKIPPED
|
||||
- !php/const Sylius\Component\Core\OrderCheckoutStates::STATE_SHIPPING_SELECTED
|
||||
to: !php/const Sylius\Component\Core\OrderCheckoutStates::STATE_PAYMENT_SELECTED
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@
|
|||
</service>
|
||||
<service id="Sylius\Component\Core\Checker\OrderPaymentMethodSelectionRequirementCheckerInterface" alias="sylius.checker.order_payment_method_selection_requirement" />
|
||||
|
||||
<service id="sylius.checker.order_payment_method_selection_availability" class="Sylius\Component\Core\Checker\OrderPaymentMethodSelectionAvailabilityChecker" public="true" />
|
||||
<service id="Sylius\Component\Core\Checker\OrderPaymentMethodSelectionAvailabilityCheckerInterface" alias="sylius.checker.order_payment_method_selection_availability" />
|
||||
|
||||
<service id="sylius.checker.cli_context" class="Sylius\Component\Core\Checker\CLIContextChecker">
|
||||
<argument type="service" id="request_stack" />
|
||||
</service>
|
||||
|
|
|
|||
|
|
@ -50,5 +50,10 @@
|
|||
<argument type="service" id="sylius.state_resolver.order_shipping" />
|
||||
<tag name="kernel.event_listener" event="workflow.sylius_order_checkout.completed.complete" priority="100"/>
|
||||
</service>
|
||||
|
||||
<service id="sylius.listener.workflow.order_checkout.block_select_payment_from_payment_skipped" class="Sylius\Bundle\CoreBundle\EventListener\Workflow\OrderCheckout\BlockSelectPaymentFromPaymentSkippedListener">
|
||||
<argument type="service" id="sylius.checker.order_payment_method_selection_availability" />
|
||||
<tag name="kernel.event_listener" event="workflow.sylius_order_checkout.guard.select_payment" />
|
||||
</service>
|
||||
</services>
|
||||
</container>
|
||||
|
|
|
|||
|
|
@ -27,25 +27,25 @@
|
|||
<tag name="kernel.event_listener" event="sylius.customer.post_register" method="sendUserRegistrationEmail" />
|
||||
</service>
|
||||
|
||||
<service id="sylius.mailer.contact_email_manager" class="Sylius\Bundle\CoreBundle\Mailer\ContactEmailManager">
|
||||
<service id="sylius.mailer.contact_email_manager" class="Sylius\Bundle\CoreBundle\Mailer\ContactEmailManager" public="true">
|
||||
<argument type="service" id="sylius.email_sender" />
|
||||
</service>
|
||||
<service id="Sylius\Bundle\CoreBundle\Mailer\ContactEmailManagerInterface" alias="sylius.mailer.contact_email_manager" />
|
||||
<service id="Sylius\Bundle\CoreBundle\Mailer\ContactEmailManagerInterface" alias="sylius.mailer.contact_email_manager" public="true" />
|
||||
|
||||
<service id="sylius.mailer.order_email_manager" class="Sylius\Bundle\CoreBundle\Mailer\OrderEmailManager">
|
||||
<service id="sylius.mailer.order_email_manager" class="Sylius\Bundle\CoreBundle\Mailer\OrderEmailManager" public="true">
|
||||
<argument type="service" id="sylius.email_sender" />
|
||||
</service>
|
||||
<service id="Sylius\Bundle\CoreBundle\Mailer\OrderEmailManagerInterface" alias="sylius.mailer.order_email_manager" />
|
||||
<service id="Sylius\Bundle\CoreBundle\Mailer\OrderEmailManagerInterface" alias="sylius.mailer.order_email_manager" public="true" />
|
||||
|
||||
<service id="sylius.mailer.shipment_email_manager" class="Sylius\Bundle\CoreBundle\Mailer\ShipmentEmailManager">
|
||||
<service id="sylius.mailer.shipment_email_manager" class="Sylius\Bundle\CoreBundle\Mailer\ShipmentEmailManager" public="true">
|
||||
<argument type="service" id="sylius.email_sender" />
|
||||
</service>
|
||||
<service id="Sylius\Bundle\CoreBundle\Mailer\ShipmentEmailManagerInterface" alias="sylius.mailer.shipment_email_manager" />
|
||||
<service id="Sylius\Bundle\CoreBundle\Mailer\ShipmentEmailManagerInterface" alias="sylius.mailer.shipment_email_manager" public="true" />
|
||||
|
||||
<service id="sylius.mailer.reset_password_email_manager" class="Sylius\Bundle\CoreBundle\Mailer\ResetPasswordEmailManager">
|
||||
<service id="sylius.mailer.reset_password_email_manager" class="Sylius\Bundle\CoreBundle\Mailer\ResetPasswordEmailManager" public="true">
|
||||
<argument type="service" id="sylius.email_sender" />
|
||||
</service>
|
||||
<service id="Sylius\Bundle\CoreBundle\Mailer\ResetPasswordEmailManagerInterface" alias="sylius.mailer.reset_password_email_manager" />
|
||||
<service id="Sylius\Bundle\CoreBundle\Mailer\ResetPasswordEmailManagerInterface" alias="sylius.mailer.reset_password_email_manager" public="true" />
|
||||
|
||||
<service
|
||||
id="sylius.mailer.account_registration_email_manager"
|
||||
|
|
|
|||
|
|
@ -13,6 +13,13 @@
|
|||
|
||||
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
|
||||
<services>
|
||||
<service
|
||||
id="sylius.telemetry.query.timeout_runner"
|
||||
class="Sylius\Bundle\CoreBundle\Telemetry\Query\TimeoutRunner"
|
||||
>
|
||||
<argument>%sylius_core.telemetry.query_timeout%</argument>
|
||||
</service>
|
||||
|
||||
<service
|
||||
id="sylius.telemetry.installation_id_generator"
|
||||
class="Sylius\Component\Core\Telemetry\Generator\InstallationIdGenerator"
|
||||
|
|
@ -82,6 +89,7 @@
|
|||
lazy="Sylius\Component\Core\Telemetry\DataProvider\DataProviderInterface"
|
||||
>
|
||||
<argument type="service" id="doctrine.dbal.default_connection" />
|
||||
<argument type="service" id="sylius.telemetry.query.timeout_runner" />
|
||||
<argument type="string">%locale%</argument>
|
||||
<tag name="sylius.telemetry.business_data_provider" />
|
||||
</service>
|
||||
|
|
@ -92,6 +100,7 @@
|
|||
lazy="Sylius\Component\Core\Telemetry\DataProvider\DataProviderInterface"
|
||||
>
|
||||
<argument type="service" id="doctrine.dbal.default_connection" />
|
||||
<argument type="service" id="sylius.telemetry.query.timeout_runner" />
|
||||
<tag name="sylius.telemetry.business_data_provider" />
|
||||
</service>
|
||||
|
||||
|
|
@ -101,6 +110,7 @@
|
|||
lazy="Sylius\Component\Core\Telemetry\DataProvider\DataProviderInterface"
|
||||
>
|
||||
<argument type="service" id="doctrine.dbal.default_connection" />
|
||||
<argument type="service" id="sylius.telemetry.query.timeout_runner" />
|
||||
<tag name="sylius.telemetry.business_data_provider" />
|
||||
</service>
|
||||
|
||||
|
|
@ -110,6 +120,7 @@
|
|||
lazy="Sylius\Component\Core\Telemetry\DataProvider\DataProviderInterface"
|
||||
>
|
||||
<argument type="service" id="doctrine.dbal.default_connection" />
|
||||
<argument type="service" id="sylius.telemetry.query.timeout_runner" />
|
||||
<tag name="sylius.telemetry.business_data_provider" />
|
||||
</service>
|
||||
|
||||
|
|
@ -119,6 +130,7 @@
|
|||
lazy="Sylius\Component\Core\Telemetry\DataProvider\DataProviderInterface"
|
||||
>
|
||||
<argument type="service" id="doctrine.dbal.default_connection" />
|
||||
<argument type="service" id="sylius.telemetry.query.timeout_runner" />
|
||||
<tag name="sylius.telemetry.business_data_provider" />
|
||||
</service>
|
||||
|
||||
|
|
@ -128,6 +140,7 @@
|
|||
lazy="Sylius\Component\Core\Telemetry\DataProvider\DataProviderInterface"
|
||||
>
|
||||
<argument type="service" id="doctrine.dbal.default_connection" />
|
||||
<argument type="service" id="sylius.telemetry.query.timeout_runner" />
|
||||
<tag name="sylius.telemetry.business_data_provider" />
|
||||
</service>
|
||||
|
||||
|
|
@ -137,6 +150,7 @@
|
|||
lazy="Sylius\Component\Core\Telemetry\DataProvider\DataProviderInterface"
|
||||
>
|
||||
<argument type="service" id="doctrine.dbal.default_connection" />
|
||||
<argument type="service" id="sylius.telemetry.query.timeout_runner" />
|
||||
<tag name="sylius.telemetry.business_data_provider" />
|
||||
</service>
|
||||
|
||||
|
|
@ -183,6 +197,7 @@
|
|||
lazy="true"
|
||||
>
|
||||
<argument type="service" id="sylius.telemetry.send_manager" />
|
||||
<argument type="service" id="sylius.telemetry.cache" />
|
||||
<argument>%sylius.security.api_admin_route%</argument>
|
||||
<tag name="kernel.event_listener" event="kernel.terminate" method="onAdminAccess" priority="-1024" />
|
||||
</service>
|
||||
|
|
|
|||
|
|
@ -40,15 +40,15 @@ use Symfony\Component\DependencyInjection\ContainerBuilder;
|
|||
|
||||
final class SyliusCoreBundle extends AbstractResourceBundle
|
||||
{
|
||||
public const VERSION = '2.1.13-DEV';
|
||||
public const VERSION = '2.1.15-DEV';
|
||||
|
||||
public const VERSION_ID = '20113';
|
||||
public const VERSION_ID = '20115';
|
||||
|
||||
public const MAJOR_VERSION = '2';
|
||||
|
||||
public const MINOR_VERSION = '1';
|
||||
|
||||
public const RELEASE_VERSION = '13';
|
||||
public const RELEASE_VERSION = '15';
|
||||
|
||||
public const EXTRA_VERSION = 'DEV';
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Sylius\Bundle\CoreBundle\Telemetry\EventListener;
|
||||
|
||||
use Sylius\Component\Core\Telemetry\Cache\TelemetryCacheInterface;
|
||||
use Sylius\Component\Core\Telemetry\TelemetrySendManagerInterface;
|
||||
use Symfony\Component\HttpKernel\Event\TerminateEvent;
|
||||
|
||||
|
|
@ -21,6 +22,7 @@ class TelemetryListener
|
|||
{
|
||||
public function __construct(
|
||||
private TelemetrySendManagerInterface $telemetrySendManager,
|
||||
private TelemetryCacheInterface $telemetryCache,
|
||||
private string $adminApiPrefix,
|
||||
) {
|
||||
}
|
||||
|
|
@ -33,7 +35,12 @@ class TelemetryListener
|
|||
return;
|
||||
}
|
||||
|
||||
if ($this->telemetryCache->wasRecentlyTriggered()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->telemetryCache->markAsRecentlyTriggered();
|
||||
$this->telemetrySendManager->sendIfNeeded($request);
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,29 +15,34 @@ namespace Sylius\Bundle\CoreBundle\Telemetry\Provider\Business;
|
|||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\DTO\Business\CountriesData;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\Query\TimeoutRunner;
|
||||
use Sylius\Component\Core\Telemetry\DataProvider\DataProviderInterface;
|
||||
use Sylius\Component\Core\Telemetry\DTO\TelemetryDataInterface;
|
||||
|
||||
/** @internal */
|
||||
final class CountriesDataProvider implements DataProviderInterface
|
||||
{
|
||||
public function __construct(private Connection $connection)
|
||||
{
|
||||
public function __construct(
|
||||
private Connection $connection,
|
||||
private TimeoutRunner $queryTimeoutRunner,
|
||||
) {
|
||||
}
|
||||
|
||||
public function provide(): TelemetryDataInterface
|
||||
{
|
||||
try {
|
||||
$channelsWithCountries = $this->connection->fetchAllAssociative(
|
||||
$channelsWithCountries = $this->queryTimeoutRunner->fetchAllAssociative(
|
||||
$this->connection,
|
||||
'SELECT c.id as channel_id, co.code as country_code
|
||||
FROM sylius_channel c
|
||||
LEFT JOIN sylius_channel_countries cc ON cc.channel_id = c.id
|
||||
LEFT JOIN sylius_country co ON co.id = cc.country_id AND co.enabled = 1
|
||||
WHERE c.enabled = 1',
|
||||
LEFT JOIN sylius_country co ON co.id = cc.country_id AND co.enabled = true
|
||||
WHERE c.enabled = true',
|
||||
);
|
||||
|
||||
$allCountries = $this->connection->fetchFirstColumn(
|
||||
'SELECT code FROM sylius_country WHERE enabled = 1',
|
||||
$allCountries = $this->queryTimeoutRunner->fetchFirstColumn(
|
||||
$this->connection,
|
||||
'SELECT code FROM sylius_country WHERE enabled = true',
|
||||
);
|
||||
|
||||
$countriesByChannel = [];
|
||||
|
|
|
|||
|
|
@ -15,20 +15,26 @@ namespace Sylius\Bundle\CoreBundle\Telemetry\Provider\Business;
|
|||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\DTO\Business\CurrenciesData;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\Query\TimeoutRunner;
|
||||
use Sylius\Component\Core\Telemetry\DataProvider\DataProviderInterface;
|
||||
use Sylius\Component\Core\Telemetry\DTO\TelemetryDataInterface;
|
||||
|
||||
/** @internal */
|
||||
final class CurrenciesDataProvider implements DataProviderInterface
|
||||
{
|
||||
public function __construct(private Connection $connection)
|
||||
{
|
||||
public function __construct(
|
||||
private Connection $connection,
|
||||
private TimeoutRunner $queryTimeoutRunner,
|
||||
) {
|
||||
}
|
||||
|
||||
public function provide(): TelemetryDataInterface
|
||||
{
|
||||
try {
|
||||
$currencies = $this->connection->fetchFirstColumn('SELECT code FROM sylius_currency');
|
||||
$currencies = $this->queryTimeoutRunner->fetchFirstColumn(
|
||||
$this->connection,
|
||||
'SELECT code FROM sylius_currency',
|
||||
);
|
||||
|
||||
return new CurrenciesData($currencies);
|
||||
} catch (\Throwable) {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ namespace Sylius\Bundle\CoreBundle\Telemetry\Provider\Business;
|
|||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\DTO\Business\LocalesData;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\Query\TimeoutRunner;
|
||||
use Sylius\Component\Core\Telemetry\DataProvider\DataProviderInterface;
|
||||
use Sylius\Component\Core\Telemetry\DTO\TelemetryDataInterface;
|
||||
|
||||
|
|
@ -23,6 +24,7 @@ final class LocalesDataProvider implements DataProviderInterface
|
|||
{
|
||||
public function __construct(
|
||||
private Connection $connection,
|
||||
private TimeoutRunner $queryTimeoutRunner,
|
||||
private string $defaultLocale,
|
||||
) {
|
||||
}
|
||||
|
|
@ -30,11 +32,15 @@ final class LocalesDataProvider implements DataProviderInterface
|
|||
public function provide(): TelemetryDataInterface
|
||||
{
|
||||
try {
|
||||
$locales = $this->connection->fetchFirstColumn('SELECT code FROM sylius_locale');
|
||||
$channelDefaultLocales = $this->connection->fetchFirstColumn(
|
||||
$locales = $this->queryTimeoutRunner->fetchFirstColumn(
|
||||
$this->connection,
|
||||
'SELECT code FROM sylius_locale',
|
||||
);
|
||||
$channelDefaultLocales = $this->queryTimeoutRunner->fetchFirstColumn(
|
||||
$this->connection,
|
||||
'SELECT DISTINCT l.code FROM sylius_locale l
|
||||
INNER JOIN sylius_channel c ON c.default_locale_id = l.id
|
||||
WHERE c.enabled = 1',
|
||||
WHERE c.enabled = true',
|
||||
);
|
||||
|
||||
return new LocalesData($locales, $channelDefaultLocales, $this->defaultLocale);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ namespace Sylius\Bundle\CoreBundle\Telemetry\Provider\Business;
|
|||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\DTO\Business\MetricsCountsData;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\Query\TimeoutRunner;
|
||||
use Sylius\Component\Core\OrderCheckoutStates;
|
||||
use Sylius\Component\Core\Telemetry\DataProvider\DataProviderInterface;
|
||||
use Sylius\Component\Core\Telemetry\DTO\TelemetryDataInterface;
|
||||
|
|
@ -23,21 +24,24 @@ use Sylius\Component\Core\Telemetry\Mapper\ValueRangeMapper;
|
|||
/** @internal */
|
||||
final class MetricsCountsDataProvider implements DataProviderInterface
|
||||
{
|
||||
public function __construct(private Connection $connection)
|
||||
{
|
||||
public function __construct(
|
||||
private Connection $connection,
|
||||
private TimeoutRunner $queryTimeoutRunner,
|
||||
) {
|
||||
}
|
||||
|
||||
public function provide(): TelemetryDataInterface
|
||||
{
|
||||
try {
|
||||
$counts = $this->connection->fetchAssociative(
|
||||
$counts = $this->queryTimeoutRunner->fetchAssociative(
|
||||
$this->connection,
|
||||
'SELECT
|
||||
(SELECT COUNT(id) FROM sylius_customer) as customers_count,
|
||||
(SELECT COUNT(id) FROM sylius_product) as products_count,
|
||||
(SELECT COUNT(id) FROM sylius_product_variant) as product_variants_count,
|
||||
(SELECT COUNT(id) FROM sylius_product_variant WHERE shipping_required = 0) as virtual_product_variants_count,
|
||||
(SELECT COUNT(id) FROM sylius_product_variant WHERE shipping_required = false) as virtual_product_variants_count,
|
||||
(SELECT COUNT(id) FROM sylius_order WHERE checkout_state = :completedState) as orders_count,
|
||||
(SELECT COUNT(id) FROM sylius_channel WHERE enabled = 1) as channels_count',
|
||||
(SELECT COUNT(id) FROM sylius_channel WHERE enabled = true) as channels_count',
|
||||
[
|
||||
'completedState' => OrderCheckoutStates::STATE_COMPLETED,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ namespace Sylius\Bundle\CoreBundle\Telemetry\Provider\Business;
|
|||
use Doctrine\DBAL\Connection;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\DTO\Business\OrderMetricsData;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\DTO\Business\OrdersBusinessData;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\Query\TimeoutRunner;
|
||||
use Sylius\Component\Core\OrderCheckoutStates;
|
||||
use Sylius\Component\Core\OrderPaymentStates;
|
||||
use Sylius\Component\Core\Telemetry\DataProvider\DataProviderInterface;
|
||||
|
|
@ -25,8 +26,10 @@ use Sylius\Component\Core\Telemetry\Mapper\ValueRangeMapper;
|
|||
/** @internal */
|
||||
final class OrdersBusinessDataProvider implements DataProviderInterface
|
||||
{
|
||||
public function __construct(private Connection $connection)
|
||||
{
|
||||
public function __construct(
|
||||
private Connection $connection,
|
||||
private TimeoutRunner $queryTimeoutRunner,
|
||||
) {
|
||||
}
|
||||
|
||||
public function provide(): TelemetryDataInterface
|
||||
|
|
@ -46,7 +49,8 @@ final class OrdersBusinessDataProvider implements DataProviderInterface
|
|||
{
|
||||
$oneMonthAgo = (new \DateTimeImmutable('-1 month'))->format('Y-m-d H:i:s');
|
||||
|
||||
return $this->connection->fetchAllAssociative(
|
||||
return $this->queryTimeoutRunner->fetchAllAssociative(
|
||||
$this->connection,
|
||||
'SELECT
|
||||
o.currency_code,
|
||||
COUNT(o.id) as order_count,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ namespace Sylius\Bundle\CoreBundle\Telemetry\Provider\Business;
|
|||
use Doctrine\DBAL\Connection;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\DTO\Business\PaymentMethodsData;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\DTO\Business\PaymentProviderData;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\Query\TimeoutRunner;
|
||||
use Sylius\Component\Core\Telemetry\DataProvider\DataProviderInterface;
|
||||
use Sylius\Component\Core\Telemetry\DTO\TelemetryDataInterface;
|
||||
use Sylius\Component\Core\Telemetry\Mapper\ValueRangeMapper;
|
||||
|
|
@ -23,14 +24,17 @@ use Sylius\Component\Core\Telemetry\Mapper\ValueRangeMapper;
|
|||
/** @internal */
|
||||
final class PaymentMethodsDataProvider implements DataProviderInterface
|
||||
{
|
||||
public function __construct(private Connection $connection)
|
||||
{
|
||||
public function __construct(
|
||||
private Connection $connection,
|
||||
private TimeoutRunner $queryTimeoutRunner,
|
||||
) {
|
||||
}
|
||||
|
||||
public function provide(): TelemetryDataInterface
|
||||
{
|
||||
try {
|
||||
$results = $this->connection->fetchAllAssociative(
|
||||
$results = $this->queryTimeoutRunner->fetchAllAssociative(
|
||||
$this->connection,
|
||||
'SELECT pm.code, gc.factory_name, pm.is_enabled, COUNT(p.id) as payments_count
|
||||
FROM sylius_payment_method pm
|
||||
JOIN sylius_gateway_config gc ON pm.gateway_config_id = gc.id
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ namespace Sylius\Bundle\CoreBundle\Telemetry\Provider\Business;
|
|||
use Doctrine\DBAL\Connection;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\DTO\Business\ShippingMethodsData;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\DTO\Business\ShippingProviderData;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\Query\TimeoutRunner;
|
||||
use Sylius\Component\Core\Telemetry\DataProvider\DataProviderInterface;
|
||||
use Sylius\Component\Core\Telemetry\DTO\TelemetryDataInterface;
|
||||
use Sylius\Component\Core\Telemetry\Mapper\ValueRangeMapper;
|
||||
|
|
@ -23,14 +24,17 @@ use Sylius\Component\Core\Telemetry\Mapper\ValueRangeMapper;
|
|||
/** @internal */
|
||||
final class ShippingMethodsDataProvider implements DataProviderInterface
|
||||
{
|
||||
public function __construct(private Connection $connection)
|
||||
{
|
||||
public function __construct(
|
||||
private Connection $connection,
|
||||
private TimeoutRunner $queryTimeoutRunner,
|
||||
) {
|
||||
}
|
||||
|
||||
public function provide(): TelemetryDataInterface
|
||||
{
|
||||
try {
|
||||
$results = $this->connection->fetchAllAssociative(
|
||||
$results = $this->queryTimeoutRunner->fetchAllAssociative(
|
||||
$this->connection,
|
||||
'SELECT sm.code, sm.calculator, sm.is_enabled, COUNT(s.id) as shipments_count
|
||||
FROM sylius_shipping_method sm
|
||||
LEFT JOIN sylius_shipment s ON s.method_id = sm.id
|
||||
|
|
|
|||
122
src/Sylius/Bundle/CoreBundle/Telemetry/Query/TimeoutRunner.php
Normal file
122
src/Sylius/Bundle/CoreBundle/Telemetry/Query/TimeoutRunner.php
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Sylius Sp. z o.o.
|
||||
*
|
||||
* 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\CoreBundle\Telemetry\Query;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
|
||||
use Doctrine\DBAL\Platforms\MariaDBPlatform;
|
||||
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
|
||||
|
||||
/** @internal */
|
||||
final class TimeoutRunner
|
||||
{
|
||||
public function __construct(
|
||||
private int $timeoutMs = 60000,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $params
|
||||
*
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
public function fetchAllAssociative(Connection $connection, string $sql, array $params = []): array
|
||||
{
|
||||
$sql = $this->applyTimeout($connection, $sql);
|
||||
|
||||
return $this->executeInTimeoutContext($connection, fn () => $connection->fetchAllAssociative($sql, $params));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $params
|
||||
*
|
||||
* @return array<string, mixed>|false
|
||||
*/
|
||||
public function fetchAssociative(Connection $connection, string $sql, array $params = [])
|
||||
{
|
||||
$sql = $this->applyTimeout($connection, $sql);
|
||||
|
||||
return $this->executeInTimeoutContext($connection, fn () => $connection->fetchAssociative($sql, $params));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $params
|
||||
*
|
||||
* @return list<mixed>
|
||||
*/
|
||||
public function fetchFirstColumn(Connection $connection, string $sql, array $params = []): array
|
||||
{
|
||||
$sql = $this->applyTimeout($connection, $sql);
|
||||
|
||||
return $this->executeInTimeoutContext($connection, fn () => $connection->fetchFirstColumn($sql, $params));
|
||||
}
|
||||
|
||||
private function applyTimeout(Connection $connection, string $sql): string
|
||||
{
|
||||
$platform = $connection->getDatabasePlatform();
|
||||
|
||||
// MariaDB: SET STATEMENT max_statement_time=X FOR SELECT ... (per-statement, seconds)
|
||||
if ($platform instanceof MariaDBPlatform) {
|
||||
$timeoutSeconds = $this->timeoutMs / 1000;
|
||||
|
||||
return sprintf('SET STATEMENT max_statement_time=%s FOR %s', $timeoutSeconds, $sql);
|
||||
}
|
||||
|
||||
// MySQL: SELECT /*+ MAX_EXECUTION_TIME(X) */ ... (optimizer hint, milliseconds)
|
||||
if ($platform instanceof AbstractMySQLPlatform) {
|
||||
return preg_replace(
|
||||
'/^\s*SELECT\b/i',
|
||||
sprintf('SELECT /*+ MAX_EXECUTION_TIME(%d) */', $this->timeoutMs),
|
||||
$sql,
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
// PostgreSQL: no per-query mechanism, use SET LOCAL in transaction
|
||||
// Handled in executeInTimeoutContext()
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
*
|
||||
* @param callable(): T $callback
|
||||
*
|
||||
* @return T
|
||||
*/
|
||||
private function executeInTimeoutContext(Connection $connection, callable $callback)
|
||||
{
|
||||
$platform = $connection->getDatabasePlatform();
|
||||
|
||||
// PostgreSQL: SET LOCAL only works within a transaction and resets automatically on COMMIT/ROLLBACK
|
||||
if ($platform instanceof PostgreSQLPlatform) {
|
||||
$connection->beginTransaction();
|
||||
|
||||
try {
|
||||
$connection->executeStatement(sprintf("SET LOCAL statement_timeout = '%d'", $this->timeoutMs));
|
||||
$result = $callback();
|
||||
$connection->commit();
|
||||
|
||||
return $result;
|
||||
} catch (\Throwable $e) {
|
||||
$connection->rollBack();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
// MySQL/MariaDB: timeout is embedded in the SQL itself, just execute
|
||||
return $callback();
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,8 @@ declare(strict_types=1);
|
|||
namespace Sylius\Bundle\CoreBundle\Validator\Constraints;
|
||||
|
||||
use Sylius\Component\Core\Model\OrderInterface;
|
||||
use Sylius\Component\Core\Model\PaymentInterface;
|
||||
use Sylius\Component\Core\Model\PaymentMethodInterface;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
|
@ -31,10 +33,21 @@ final class OrderPaymentMethodEligibilityValidator extends ConstraintValidator
|
|||
/** @var OrderPaymentMethodEligibility $constraint */
|
||||
Assert::isInstanceOf($constraint, OrderPaymentMethodEligibility::class);
|
||||
|
||||
$channel = $value->getChannel();
|
||||
Assert::notNull($channel);
|
||||
|
||||
$payments = $value->getPayments();
|
||||
|
||||
/** @var PaymentInterface $payment */
|
||||
foreach ($payments as $payment) {
|
||||
if (!$payment->getMethod()->isEnabled()) {
|
||||
/** @var ?PaymentMethodInterface $paymentMethod */
|
||||
$paymentMethod = $payment->getMethod();
|
||||
|
||||
if (null === $paymentMethod) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$payment->getMethod()->isEnabled() || !$paymentMethod->hasChannel($channel)) {
|
||||
$this->context->addViolation(
|
||||
$constraint->message,
|
||||
['%paymentMethodName%' => $payment->getMethod()->getName()],
|
||||
|
|
|
|||
|
|
@ -248,6 +248,7 @@ final class ConfigurationTest extends TestCase
|
|||
'technical' => true,
|
||||
'plugins' => true,
|
||||
'url' => 'https://prism.sylius.com/telemetry',
|
||||
'query_timeout' => 60000,
|
||||
],
|
||||
],
|
||||
'telemetry',
|
||||
|
|
@ -272,6 +273,7 @@ final class ConfigurationTest extends TestCase
|
|||
'technical' => true,
|
||||
'plugins' => true,
|
||||
'url' => 'https://prism.sylius.com/telemetry',
|
||||
'query_timeout' => 60000,
|
||||
],
|
||||
],
|
||||
'telemetry',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Sylius Sp. z o.o.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Sylius\Bundle\CoreBundle\EventListener\Workflow\OrderCheckout;
|
||||
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Sylius\Bundle\CoreBundle\EventListener\Workflow\OrderCheckout\BlockSelectPaymentFromPaymentSkippedListener;
|
||||
use Sylius\Component\Core\Checker\OrderPaymentMethodSelectionAvailabilityCheckerInterface;
|
||||
use Sylius\Component\Core\Model\OrderInterface;
|
||||
use Sylius\Component\Core\OrderCheckoutStates;
|
||||
use Symfony\Component\Workflow\Event\GuardEvent;
|
||||
use Symfony\Component\Workflow\Marking;
|
||||
use Symfony\Component\Workflow\Transition;
|
||||
|
||||
final class BlockSelectPaymentFromPaymentSkippedListenerTest extends TestCase
|
||||
{
|
||||
private MockObject&OrderPaymentMethodSelectionAvailabilityCheckerInterface $availabilityChecker;
|
||||
|
||||
private BlockSelectPaymentFromPaymentSkippedListener $listener;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->availabilityChecker = $this->createMock(OrderPaymentMethodSelectionAvailabilityCheckerInterface::class);
|
||||
$this->listener = new BlockSelectPaymentFromPaymentSkippedListener($this->availabilityChecker);
|
||||
}
|
||||
|
||||
public function testItDoesNothingWhenMarkingIsNotPaymentSkipped(): void
|
||||
{
|
||||
$order = $this->createMock(OrderInterface::class);
|
||||
$event = $this->createGuardEvent($order, ['shipping_selected' => 1]);
|
||||
|
||||
$this->availabilityChecker->expects($this->never())->method('isPaymentMethodSelectionAvailable');
|
||||
|
||||
($this->listener)($event);
|
||||
|
||||
$this->assertFalse($event->isBlocked());
|
||||
}
|
||||
|
||||
public function testItBlocksTransitionWhenSelectingPaymentIsNotAvailable(): void
|
||||
{
|
||||
$order = $this->createMock(OrderInterface::class);
|
||||
$event = $this->createGuardEvent($order, [OrderCheckoutStates::STATE_PAYMENT_SKIPPED => 1]);
|
||||
|
||||
$this->availabilityChecker
|
||||
->expects($this->once())
|
||||
->method('isPaymentMethodSelectionAvailable')
|
||||
->with($order)
|
||||
->willReturn(false)
|
||||
;
|
||||
|
||||
($this->listener)($event);
|
||||
|
||||
$this->assertTrue($event->isBlocked());
|
||||
}
|
||||
|
||||
public function testItDoesNotBlockTransitionWhenSelectingPaymentIsAvailable(): void
|
||||
{
|
||||
$order = $this->createMock(OrderInterface::class);
|
||||
$event = $this->createGuardEvent($order, [OrderCheckoutStates::STATE_PAYMENT_SKIPPED => 1]);
|
||||
|
||||
$this->availabilityChecker
|
||||
->expects($this->once())
|
||||
->method('isPaymentMethodSelectionAvailable')
|
||||
->with($order)
|
||||
->willReturn(true)
|
||||
;
|
||||
|
||||
($this->listener)($event);
|
||||
|
||||
$this->assertFalse($event->isBlocked());
|
||||
}
|
||||
|
||||
public function testItThrowsAnExceptionOnNonSupportedSubject(): void
|
||||
{
|
||||
$event = $this->createGuardEvent(new \stdClass(), [OrderCheckoutStates::STATE_PAYMENT_SKIPPED => 1]);
|
||||
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
|
||||
($this->listener)($event);
|
||||
}
|
||||
|
||||
private function createGuardEvent(object $subject, array $places): GuardEvent
|
||||
{
|
||||
return new GuardEvent(
|
||||
$subject,
|
||||
new Marking($places),
|
||||
new Transition('select_payment', OrderCheckoutStates::STATE_PAYMENT_SKIPPED, 'payment_selected'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ namespace Tests\Sylius\Bundle\CoreBundle\Telemetry\EventListener;
|
|||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\EventListener\TelemetryListener;
|
||||
use Sylius\Component\Core\Telemetry\Cache\TelemetryCacheInterface;
|
||||
use Sylius\Component\Core\Telemetry\TelemetrySendManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
|
@ -25,16 +26,20 @@ final class TelemetryListenerTest extends TestCase
|
|||
{
|
||||
private TelemetrySendManagerInterface $telemetrySendManager;
|
||||
|
||||
private TelemetryCacheInterface $telemetryCache;
|
||||
|
||||
private TelemetryListener $listener;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->telemetrySendManager = $this->createMock(TelemetrySendManagerInterface::class);
|
||||
$this->listener = new TelemetryListener($this->telemetrySendManager, '/api/v2/admin');
|
||||
$this->telemetryCache = $this->createMock(TelemetryCacheInterface::class);
|
||||
$this->listener = new TelemetryListener($this->telemetrySendManager, $this->telemetryCache, '/api/v2/admin');
|
||||
}
|
||||
|
||||
public function test_it_does_nothing_when_not_admin_route(): void
|
||||
{
|
||||
$this->telemetryCache->expects($this->never())->method('wasRecentlyTriggered');
|
||||
$this->telemetrySendManager->expects($this->never())->method('sendIfNeeded');
|
||||
|
||||
$event = $this->createEvent('sylius_shop_homepage', '/');
|
||||
|
|
@ -44,6 +49,7 @@ final class TelemetryListenerTest extends TestCase
|
|||
|
||||
public function test_it_does_nothing_when_route_is_null_and_path_not_admin(): void
|
||||
{
|
||||
$this->telemetryCache->expects($this->never())->method('wasRecentlyTriggered');
|
||||
$this->telemetrySendManager->expects($this->never())->method('sendIfNeeded');
|
||||
|
||||
$event = $this->createEvent(null, '/shop/products');
|
||||
|
|
@ -53,6 +59,8 @@ final class TelemetryListenerTest extends TestCase
|
|||
|
||||
public function test_it_calls_telemetry_send_manager_on_admin_dashboard_visit(): void
|
||||
{
|
||||
$this->telemetryCache->expects($this->once())->method('wasRecentlyTriggered')->willReturn(false);
|
||||
$this->telemetryCache->expects($this->once())->method('markAsRecentlyTriggered');
|
||||
$this->telemetrySendManager->expects($this->once())->method('sendIfNeeded');
|
||||
|
||||
$event = $this->createEvent('sylius_admin_dashboard', '/admin');
|
||||
|
|
@ -62,6 +70,8 @@ final class TelemetryListenerTest extends TestCase
|
|||
|
||||
public function test_it_calls_telemetry_send_manager_on_admin_order_index(): void
|
||||
{
|
||||
$this->telemetryCache->expects($this->once())->method('wasRecentlyTriggered')->willReturn(false);
|
||||
$this->telemetryCache->expects($this->once())->method('markAsRecentlyTriggered');
|
||||
$this->telemetrySendManager->expects($this->once())->method('sendIfNeeded');
|
||||
|
||||
$event = $this->createEvent('sylius_admin_order_index', '/admin/orders');
|
||||
|
|
@ -71,6 +81,8 @@ final class TelemetryListenerTest extends TestCase
|
|||
|
||||
public function test_it_calls_telemetry_send_manager_on_admin_api_route(): void
|
||||
{
|
||||
$this->telemetryCache->expects($this->once())->method('wasRecentlyTriggered')->willReturn(false);
|
||||
$this->telemetryCache->expects($this->once())->method('markAsRecentlyTriggered');
|
||||
$this->telemetrySendManager->expects($this->once())->method('sendIfNeeded');
|
||||
|
||||
$event = $this->createEvent('api_orders_admin_get_collection', '/api/v2/admin/orders');
|
||||
|
|
@ -80,6 +92,7 @@ final class TelemetryListenerTest extends TestCase
|
|||
|
||||
public function test_it_does_not_trigger_on_shop_api_route(): void
|
||||
{
|
||||
$this->telemetryCache->expects($this->never())->method('wasRecentlyTriggered');
|
||||
$this->telemetrySendManager->expects($this->never())->method('sendIfNeeded');
|
||||
|
||||
$event = $this->createEvent('api_orders_shop_get_collection', '/api/v2/shop/orders');
|
||||
|
|
@ -87,8 +100,21 @@ final class TelemetryListenerTest extends TestCase
|
|||
$this->listener->onAdminAccess($event);
|
||||
}
|
||||
|
||||
public function test_it_skips_when_telemetry_was_recently_triggered(): void
|
||||
{
|
||||
$this->telemetryCache->expects($this->once())->method('wasRecentlyTriggered')->willReturn(true);
|
||||
$this->telemetryCache->expects($this->never())->method('markAsRecentlyTriggered');
|
||||
$this->telemetrySendManager->expects($this->never())->method('sendIfNeeded');
|
||||
|
||||
$event = $this->createEvent('sylius_admin_dashboard', '/admin');
|
||||
|
||||
$this->listener->onAdminAccess($event);
|
||||
}
|
||||
|
||||
public function test_it_handles_exceptions_silently(): void
|
||||
{
|
||||
$this->telemetryCache->expects($this->once())->method('wasRecentlyTriggered')->willReturn(false);
|
||||
$this->telemetryCache->expects($this->once())->method('markAsRecentlyTriggered');
|
||||
$this->telemetrySendManager->method('sendIfNeeded')->willThrowException(new \RuntimeException('Test exception'));
|
||||
|
||||
$event = $this->createEvent('sylius_admin_dashboard', '/admin');
|
||||
|
|
|
|||
|
|
@ -14,9 +14,11 @@ declare(strict_types=1);
|
|||
namespace Tests\Sylius\Bundle\CoreBundle\Telemetry\Provider\Business;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\DTO\Business\CountriesData;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\Provider\Business\CountriesDataProvider;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\Query\TimeoutRunner;
|
||||
|
||||
final class CountriesDataProviderTest extends TestCase
|
||||
{
|
||||
|
|
@ -27,7 +29,8 @@ final class CountriesDataProviderTest extends TestCase
|
|||
protected function setUp(): void
|
||||
{
|
||||
$this->connection = $this->createMock(Connection::class);
|
||||
$this->provider = new CountriesDataProvider($this->connection);
|
||||
$this->connection->method('getDatabasePlatform')->willReturn($this->createMock(AbstractMySQLPlatform::class));
|
||||
$this->provider = new CountriesDataProvider($this->connection, new TimeoutRunner());
|
||||
}
|
||||
|
||||
public function test_it_provides_countries_from_enabled_channels_with_enabled_countries(): void
|
||||
|
|
@ -35,8 +38,8 @@ final class CountriesDataProviderTest extends TestCase
|
|||
$this->connection->expects(self::once())
|
||||
->method('fetchAllAssociative')
|
||||
->with(self::logicalAnd(
|
||||
self::stringContains('co.enabled = 1'),
|
||||
self::stringContains('c.enabled = 1'),
|
||||
self::stringContains('co.enabled = true'),
|
||||
self::stringContains('c.enabled = true'),
|
||||
))
|
||||
->willReturn([
|
||||
['channel_id' => 1, 'country_code' => 'US'],
|
||||
|
|
@ -46,7 +49,7 @@ final class CountriesDataProviderTest extends TestCase
|
|||
]);
|
||||
$this->connection->expects(self::once())
|
||||
->method('fetchFirstColumn')
|
||||
->with(self::stringContains('WHERE enabled = 1'))
|
||||
->with(self::stringContains('WHERE enabled = true'))
|
||||
->willReturn(['US', 'CA', 'DE', 'FR', 'PL', 'GB']);
|
||||
|
||||
$data = $this->provider->provide();
|
||||
|
|
|
|||
|
|
@ -14,9 +14,11 @@ declare(strict_types=1);
|
|||
namespace Tests\Sylius\Bundle\CoreBundle\Telemetry\Provider\Business;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\DTO\Business\CurrenciesData;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\Provider\Business\CurrenciesDataProvider;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\Query\TimeoutRunner;
|
||||
|
||||
final class CurrenciesDataProviderTest extends TestCase
|
||||
{
|
||||
|
|
@ -27,7 +29,8 @@ final class CurrenciesDataProviderTest extends TestCase
|
|||
protected function setUp(): void
|
||||
{
|
||||
$this->connection = $this->createMock(Connection::class);
|
||||
$this->provider = new CurrenciesDataProvider($this->connection);
|
||||
$this->connection->method('getDatabasePlatform')->willReturn($this->createMock(AbstractMySQLPlatform::class));
|
||||
$this->provider = new CurrenciesDataProvider($this->connection, new TimeoutRunner());
|
||||
}
|
||||
|
||||
public function test_it_provides_currency_codes(): void
|
||||
|
|
|
|||
|
|
@ -14,9 +14,11 @@ declare(strict_types=1);
|
|||
namespace Tests\Sylius\Bundle\CoreBundle\Telemetry\Provider\Business;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\DTO\Business\LocalesData;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\Provider\Business\LocalesDataProvider;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\Query\TimeoutRunner;
|
||||
|
||||
final class LocalesDataProviderTest extends TestCase
|
||||
{
|
||||
|
|
@ -29,7 +31,8 @@ final class LocalesDataProviderTest extends TestCase
|
|||
protected function setUp(): void
|
||||
{
|
||||
$this->connection = $this->createMock(Connection::class);
|
||||
$this->provider = new LocalesDataProvider($this->connection, self::DEFAULT_LOCALE);
|
||||
$this->connection->method('getDatabasePlatform')->willReturn($this->createMock(AbstractMySQLPlatform::class));
|
||||
$this->provider = new LocalesDataProvider($this->connection, new TimeoutRunner(), self::DEFAULT_LOCALE);
|
||||
}
|
||||
|
||||
public function test_it_provides_locales_channel_defaults_from_enabled_channels(): void
|
||||
|
|
@ -40,7 +43,7 @@ final class LocalesDataProviderTest extends TestCase
|
|||
if (str_contains($sql, 'sylius_locale') && !str_contains($sql, 'sylius_channel')) {
|
||||
return ['en_US', 'pl_PL', 'de_DE'];
|
||||
}
|
||||
self::assertStringContainsString('c.enabled = 1', $sql);
|
||||
self::assertStringContainsString('c.enabled = true', $sql);
|
||||
|
||||
return ['en_US', 'de_DE'];
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,9 +14,11 @@ declare(strict_types=1);
|
|||
namespace Tests\Sylius\Bundle\CoreBundle\Telemetry\Provider\Business;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\DTO\Business\MetricsCountsData;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\Provider\Business\MetricsCountsDataProvider;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\Query\TimeoutRunner;
|
||||
|
||||
final class MetricsCountsDataProviderTest extends TestCase
|
||||
{
|
||||
|
|
@ -27,7 +29,8 @@ final class MetricsCountsDataProviderTest extends TestCase
|
|||
protected function setUp(): void
|
||||
{
|
||||
$this->connection = $this->createMock(Connection::class);
|
||||
$this->provider = new MetricsCountsDataProvider($this->connection);
|
||||
$this->connection->method('getDatabasePlatform')->willReturn($this->createMock(AbstractMySQLPlatform::class));
|
||||
$this->provider = new MetricsCountsDataProvider($this->connection, new TimeoutRunner());
|
||||
}
|
||||
|
||||
public function test_it_provides_all_counts(): void
|
||||
|
|
|
|||
|
|
@ -14,9 +14,11 @@ declare(strict_types=1);
|
|||
namespace Tests\Sylius\Bundle\CoreBundle\Telemetry\Provider\Business;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\DTO\Business\OrdersBusinessData;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\Provider\Business\OrdersBusinessDataProvider;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\Query\TimeoutRunner;
|
||||
|
||||
final class OrdersBusinessDataProviderTest extends TestCase
|
||||
{
|
||||
|
|
@ -27,7 +29,8 @@ final class OrdersBusinessDataProviderTest extends TestCase
|
|||
protected function setUp(): void
|
||||
{
|
||||
$this->connection = $this->createMock(Connection::class);
|
||||
$this->provider = new OrdersBusinessDataProvider($this->connection);
|
||||
$this->connection->method('getDatabasePlatform')->willReturn($this->createMock(AbstractMySQLPlatform::class));
|
||||
$this->provider = new OrdersBusinessDataProvider($this->connection, new TimeoutRunner());
|
||||
}
|
||||
|
||||
public function test_it_provides_all_order_metrics_for_single_currency(): void
|
||||
|
|
@ -55,19 +58,18 @@ final class OrdersBusinessDataProviderTest extends TestCase
|
|||
|
||||
public function test_it_provides_all_order_metrics_for_multiple_currencies(): void
|
||||
{
|
||||
// Values in minor units
|
||||
$this->connection->method('fetchAllAssociative')->willReturn([
|
||||
[
|
||||
'currency_code' => 'USD',
|
||||
'order_count' => 100,
|
||||
'gmv' => 3000000, // 30,000 in minor units, AOV = 300
|
||||
'gmv' => 3000000,
|
||||
'avg_items' => 3.0,
|
||||
'avg_units' => 4.5,
|
||||
],
|
||||
[
|
||||
'currency_code' => 'EUR',
|
||||
'order_count' => 50,
|
||||
'gmv' => 1500000, // 15,000 in minor units, AOV = 300
|
||||
'gmv' => 1500000,
|
||||
'avg_items' => 2.5,
|
||||
'avg_units' => 3.5,
|
||||
],
|
||||
|
|
@ -114,12 +116,11 @@ final class OrdersBusinessDataProviderTest extends TestCase
|
|||
|
||||
public function test_it_rounds_averages_correctly(): void
|
||||
{
|
||||
// Values in minor units - 1,500 GMV, 10 orders = 150 AOV
|
||||
$this->connection->method('fetchAllAssociative')->willReturn([
|
||||
[
|
||||
'currency_code' => 'USD',
|
||||
'order_count' => 10,
|
||||
'gmv' => 150000, // 1,500 in minor units
|
||||
'gmv' => 150000,
|
||||
'avg_items' => 2.4,
|
||||
'avg_units' => 3.6,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -14,9 +14,11 @@ declare(strict_types=1);
|
|||
namespace Tests\Sylius\Bundle\CoreBundle\Telemetry\Provider\Business;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\DTO\Business\PaymentMethodsData;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\Provider\Business\PaymentMethodsDataProvider;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\Query\TimeoutRunner;
|
||||
|
||||
final class PaymentMethodsDataProviderTest extends TestCase
|
||||
{
|
||||
|
|
@ -27,7 +29,8 @@ final class PaymentMethodsDataProviderTest extends TestCase
|
|||
protected function setUp(): void
|
||||
{
|
||||
$this->connection = $this->createMock(Connection::class);
|
||||
$this->provider = new PaymentMethodsDataProvider($this->connection);
|
||||
$this->connection->method('getDatabasePlatform')->willReturn($this->createMock(AbstractMySQLPlatform::class));
|
||||
$this->provider = new PaymentMethodsDataProvider($this->connection, new TimeoutRunner());
|
||||
}
|
||||
|
||||
public function test_it_provides_payment_providers_assigned_to_channel(): void
|
||||
|
|
|
|||
|
|
@ -14,9 +14,11 @@ declare(strict_types=1);
|
|||
namespace Tests\Sylius\Bundle\CoreBundle\Telemetry\Provider\Business;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\DTO\Business\ShippingMethodsData;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\Provider\Business\ShippingMethodsDataProvider;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\Query\TimeoutRunner;
|
||||
|
||||
final class ShippingMethodsDataProviderTest extends TestCase
|
||||
{
|
||||
|
|
@ -27,7 +29,8 @@ final class ShippingMethodsDataProviderTest extends TestCase
|
|||
protected function setUp(): void
|
||||
{
|
||||
$this->connection = $this->createMock(Connection::class);
|
||||
$this->provider = new ShippingMethodsDataProvider($this->connection);
|
||||
$this->connection->method('getDatabasePlatform')->willReturn($this->createMock(AbstractMySQLPlatform::class));
|
||||
$this->provider = new ShippingMethodsDataProvider($this->connection, new TimeoutRunner());
|
||||
}
|
||||
|
||||
public function test_it_provides_non_archived_shipping_providers_assigned_to_channel(): void
|
||||
|
|
|
|||
|
|
@ -0,0 +1,169 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Sylius Sp. z o.o.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Sylius\Bundle\CoreBundle\Telemetry\Query;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
|
||||
use Doctrine\DBAL\Platforms\MariaDBPlatform;
|
||||
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Sylius\Bundle\CoreBundle\Telemetry\Query\TimeoutRunner;
|
||||
|
||||
final class TimeoutRunnerTest extends TestCase
|
||||
{
|
||||
public function test_mysql_applies_optimizer_hint(): void
|
||||
{
|
||||
$connection = $this->createMock(Connection::class);
|
||||
$connection->method('getDatabasePlatform')->willReturn($this->createMock(AbstractMySQLPlatform::class));
|
||||
$connection->expects(self::once())
|
||||
->method('fetchAllAssociative')
|
||||
->with(
|
||||
self::matchesRegularExpression('/SELECT\s+\/\*\+\s+MAX_EXECUTION_TIME\(5000\)\s+\*\//'),
|
||||
[],
|
||||
)
|
||||
->willReturn([['id' => 1]]);
|
||||
|
||||
$runner = new TimeoutRunner(5000);
|
||||
$result = $runner->fetchAllAssociative($connection, 'SELECT id FROM sylius_order');
|
||||
|
||||
self::assertSame([['id' => 1]], $result);
|
||||
}
|
||||
|
||||
public function test_mariadb_applies_set_statement(): void
|
||||
{
|
||||
$connection = $this->createMock(Connection::class);
|
||||
$connection->method('getDatabasePlatform')->willReturn($this->createMock(MariaDBPlatform::class));
|
||||
$connection->expects(self::once())
|
||||
->method('fetchAllAssociative')
|
||||
->with(
|
||||
'SET STATEMENT max_statement_time=5 FOR SELECT id FROM sylius_order',
|
||||
[],
|
||||
)
|
||||
->willReturn([['id' => 1]]);
|
||||
|
||||
$runner = new TimeoutRunner(5000);
|
||||
$result = $runner->fetchAllAssociative($connection, 'SELECT id FROM sylius_order');
|
||||
|
||||
self::assertSame([['id' => 1]], $result);
|
||||
}
|
||||
|
||||
public function test_postgresql_uses_transaction_with_set_local(): void
|
||||
{
|
||||
$connection = $this->createMock(Connection::class);
|
||||
$connection->method('getDatabasePlatform')->willReturn($this->createMock(PostgreSQLPlatform::class));
|
||||
$connection->expects(self::once())->method('beginTransaction');
|
||||
$connection->expects(self::once())
|
||||
->method('executeStatement')
|
||||
->with("SET LOCAL statement_timeout = '5000'");
|
||||
$connection->expects(self::once())
|
||||
->method('fetchAllAssociative')
|
||||
->with('SELECT id FROM sylius_order', [])
|
||||
->willReturn([['id' => 1]]);
|
||||
$connection->expects(self::once())->method('commit');
|
||||
$connection->expects(self::never())->method('rollBack');
|
||||
|
||||
$runner = new TimeoutRunner(5000);
|
||||
$result = $runner->fetchAllAssociative($connection, 'SELECT id FROM sylius_order');
|
||||
|
||||
self::assertSame([['id' => 1]], $result);
|
||||
}
|
||||
|
||||
public function test_postgresql_rolls_back_on_exception(): void
|
||||
{
|
||||
$connection = $this->createMock(Connection::class);
|
||||
$connection->method('getDatabasePlatform')->willReturn($this->createMock(PostgreSQLPlatform::class));
|
||||
$connection->expects(self::once())->method('beginTransaction');
|
||||
$connection->expects(self::once())->method('executeStatement');
|
||||
$connection->expects(self::once())
|
||||
->method('fetchAllAssociative')
|
||||
->willThrowException(new \RuntimeException('query timeout'));
|
||||
$connection->expects(self::never())->method('commit');
|
||||
$connection->expects(self::once())->method('rollBack');
|
||||
|
||||
$runner = new TimeoutRunner(5000);
|
||||
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$this->expectExceptionMessage('query timeout');
|
||||
|
||||
$runner->fetchAllAssociative($connection, 'SELECT id FROM sylius_order');
|
||||
}
|
||||
|
||||
public function test_default_timeout_is_60_seconds(): void
|
||||
{
|
||||
$connection = $this->createMock(Connection::class);
|
||||
$connection->method('getDatabasePlatform')->willReturn($this->createMock(AbstractMySQLPlatform::class));
|
||||
$connection->expects(self::once())
|
||||
->method('fetchAllAssociative')
|
||||
->with(
|
||||
self::matchesRegularExpression('/MAX_EXECUTION_TIME\(60000\)/'),
|
||||
[],
|
||||
)
|
||||
->willReturn([]);
|
||||
|
||||
$runner = new TimeoutRunner();
|
||||
$runner->fetchAllAssociative($connection, 'SELECT id FROM sylius_order');
|
||||
}
|
||||
|
||||
public function test_fetch_associative_applies_timeout(): void
|
||||
{
|
||||
$connection = $this->createMock(Connection::class);
|
||||
$connection->method('getDatabasePlatform')->willReturn($this->createMock(AbstractMySQLPlatform::class));
|
||||
$connection->expects(self::once())
|
||||
->method('fetchAssociative')
|
||||
->with(
|
||||
self::matchesRegularExpression('/MAX_EXECUTION_TIME\(3000\)/'),
|
||||
[],
|
||||
)
|
||||
->willReturn(['count' => 42]);
|
||||
|
||||
$runner = new TimeoutRunner(3000);
|
||||
$result = $runner->fetchAssociative($connection, 'SELECT COUNT(*) as count FROM sylius_order');
|
||||
|
||||
self::assertSame(['count' => 42], $result);
|
||||
}
|
||||
|
||||
public function test_fetch_first_column_applies_timeout(): void
|
||||
{
|
||||
$connection = $this->createMock(Connection::class);
|
||||
$connection->method('getDatabasePlatform')->willReturn($this->createMock(AbstractMySQLPlatform::class));
|
||||
$connection->expects(self::once())
|
||||
->method('fetchFirstColumn')
|
||||
->with(
|
||||
self::matchesRegularExpression('/MAX_EXECUTION_TIME\(3000\)/'),
|
||||
[],
|
||||
)
|
||||
->willReturn(['USD', 'EUR']);
|
||||
|
||||
$runner = new TimeoutRunner(3000);
|
||||
$result = $runner->fetchFirstColumn($connection, 'SELECT code FROM sylius_currency');
|
||||
|
||||
self::assertSame(['USD', 'EUR'], $result);
|
||||
}
|
||||
|
||||
public function test_mariadb_timeout_converts_to_seconds(): void
|
||||
{
|
||||
$connection = $this->createMock(Connection::class);
|
||||
$connection->method('getDatabasePlatform')->willReturn($this->createMock(MariaDBPlatform::class));
|
||||
$connection->expects(self::once())
|
||||
->method('fetchAllAssociative')
|
||||
->with(
|
||||
'SET STATEMENT max_statement_time=2.5 FOR SELECT 1',
|
||||
[],
|
||||
)
|
||||
->willReturn([]);
|
||||
|
||||
$runner = new TimeoutRunner(2500);
|
||||
$runner->fetchAllAssociative($connection, 'SELECT 1');
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Sylius\Bundle\ProductBundle\Doctrine\ORM;
|
||||
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository;
|
||||
use Sylius\Component\Product\Model\ProductAssociationTypeInterface;
|
||||
|
|
@ -28,8 +29,8 @@ class ProductAssociationTypeRepository extends EntityRepository implements Produ
|
|||
public function createListQueryBuilder(string $locale): QueryBuilder
|
||||
{
|
||||
return $this->createQueryBuilder('o')
|
||||
->innerJoin('o.translations', 'translation')
|
||||
->andWhere('translation.locale = :locale')
|
||||
->addSelect('translation')
|
||||
->leftJoin('o.translations', 'translation', Join::WITH, 'translation.locale = :locale')
|
||||
->setParameter('locale', $locale)
|
||||
;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,8 @@ class ProductAttributeRepository extends EntityRepository implements ProductAttr
|
|||
public function createListQueryBuilder(string $locale): QueryBuilder
|
||||
{
|
||||
return $this->createQueryBuilder('o')
|
||||
->innerJoin('o.translations', 'translation', Join::WITH, 'translation.locale = :locale')
|
||||
->addSelect('translation')
|
||||
->leftJoin('o.translations', 'translation', Join::WITH, 'translation.locale = :locale')
|
||||
->setParameter('locale', $locale)
|
||||
;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Sylius\Bundle\ProductBundle\Doctrine\ORM;
|
||||
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository;
|
||||
use Sylius\Component\Product\Model\ProductOptionInterface;
|
||||
|
|
@ -28,8 +29,8 @@ class ProductOptionRepository extends EntityRepository implements ProductOptionR
|
|||
public function createListQueryBuilder(string $locale): QueryBuilder
|
||||
{
|
||||
return $this->createQueryBuilder('o')
|
||||
->innerJoin('o.translations', 'translation')
|
||||
->andWhere('translation.locale = :locale')
|
||||
->addSelect('translation')
|
||||
->leftJoin('o.translations', 'translation', Join::WITH, 'translation.locale = :locale')
|
||||
->setParameter('locale', $locale)
|
||||
;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,9 @@
|
|||
{% for type in ['success', 'error', 'info', 'warning'] %}
|
||||
{% for flash in app.session.flashbag.get(type) %}
|
||||
{% set header = 'sylius.ui.'~type %}
|
||||
{% if type == 'error' %}
|
||||
{% set type = 'danger' %}
|
||||
{% endif %}
|
||||
{% set alert_class = type == 'error' ? 'danger' : type %}
|
||||
|
||||
<div class="alert alert-{{ type }} my-2" role="alert" data-test-sylius-flash-message>
|
||||
<div class="alert alert-{{ alert_class }} my-2" role="alert" data-test-sylius-flash-message>
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="fw-bold">{{ header|trans }}</div>
|
||||
<span class="close" data-bs-dismiss="alert" aria-label="Close">
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sylius\Bundle\TaxonomyBundle\Tests\Validator\Constraints;
|
||||
namespace Tests\Sylius\Bundle\TaxonomyBundle\Validator\Constraints;
|
||||
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
|
@ -425,6 +425,7 @@ sylius:
|
|||
in: 'In'
|
||||
inactive: 'Inactive'
|
||||
included_in_price: 'Included in price'
|
||||
index: 'Index'
|
||||
index_metadata: 'Metadata'
|
||||
index_of_all_configured_locales: 'Index of all configured locales'
|
||||
info: 'Info'
|
||||
|
|
|
|||
|
|
@ -422,6 +422,7 @@ sylius:
|
|||
in: Dans
|
||||
inactive: Inactif
|
||||
included_in_price: Inclus dans le prix
|
||||
index: Index
|
||||
index_metadata: Méta-données
|
||||
index_of_all_configured_locales: Index de tous les paramètres régionaux configurés
|
||||
info: Info
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Sylius Sp. z o.o.
|
||||
*
|
||||
* 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\Component\Core\Checker;
|
||||
|
||||
use Sylius\Component\Core\Model\OrderInterface;
|
||||
|
||||
final class OrderPaymentMethodSelectionAvailabilityChecker implements OrderPaymentMethodSelectionAvailabilityCheckerInterface
|
||||
{
|
||||
public function isPaymentMethodSelectionAvailable(OrderInterface $order): bool
|
||||
{
|
||||
return $order->getTotal() > 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Sylius package.
|
||||
*
|
||||
* (c) Sylius Sp. z o.o.
|
||||
*
|
||||
* 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\Component\Core\Checker;
|
||||
|
||||
use Sylius\Component\Core\Model\OrderInterface;
|
||||
|
||||
interface OrderPaymentMethodSelectionAvailabilityCheckerInterface
|
||||
{
|
||||
public function isPaymentMethodSelectionAvailable(OrderInterface $order): bool;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue